From fcfb0bea70dd7dc1a5a5da2f37cf4441932df580 Mon Sep 17 00:00:00 2001 From: Eldad Fux Date: Fri, 1 May 2020 09:49:23 +0300 Subject: [PATCH] Changed timeout value for drop list close event --- public/dist/scripts/app-all.js | 2 +- public/dist/scripts/app.js | 2 +- public/scripts/dependencies/litespeed.js | 1229 +++++++++++++++++++--- public/scripts/views/ui/open.js | 2 +- 4 files changed, 1093 insertions(+), 142 deletions(-) diff --git a/public/dist/scripts/app-all.js b/public/dist/scripts/app-all.js index 7e2f1dc1ec..ea7389b416 100644 --- a/public/dist/scripts/app-all.js +++ b/public/dist/scripts/app-all.js @@ -2679,7 +2679,7 @@ buttonElements.forEach(button=>{button.addEventListener("click",open);});documen document.addEventListener("modal-close",close);element.addEventListener(closeEvent,close);}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-ls-ui-open",controller:function(element,window){let def=element.classList.contains("open")?"open":"close";let buttonClass=element.dataset["buttonClass"]||"ls-ui-open";let buttonText=element.dataset["buttonText"]||"";let buttonIcon=element.dataset["buttonIcon"]||"";let buttonSelector=element.dataset["buttonSelector"]||"";let hover=element.hasAttribute("data-hover");let blur=element.hasAttribute("data-blur");let button=window.document.createElement("button");let isTouch=function(){return("ontouchstart"in window||navigator.maxTouchPoints);};button.innerText=buttonText;button.className=buttonClass;button.tabIndex=1;button.type="button";if(buttonIcon){let icon=window.document.createElement("i");icon.className=buttonIcon;button.insertBefore(icon,button.firstChild);} if(def==="close"){element.classList.add("close");element.classList.remove("open");}else{element.classList.add("open");element.classList.remove("close");} button.addEventListener("click",function(){element.classList.toggle("open");element.classList.toggle("close");});if(hover&&!isTouch()){element.addEventListener("mouseover",function(){element.classList.add("open");element.classList.remove("close");});element.addEventListener("mouseout",function(){element.classList.add("close");element.classList.remove("open");});} -let close=function(){element.classList.add("close");element.classList.remove("open");};let closeDelay=function(){window.setTimeout(function(){close();},350);};let findParent=function(tagName,el){if((el.nodeName||el.tagName).toLowerCase()===tagName.toLowerCase()){return el;} +let close=function(){element.classList.add("close");element.classList.remove("open");};let closeDelay=function(){window.setTimeout(function(){close();},400);};let findParent=function(tagName,el){if((el.nodeName||el.tagName).toLowerCase()===tagName.toLowerCase()){return el;} while((el=el.parentNode)){if((el.nodeName||el.tagName).toLowerCase()===tagName.toLowerCase()){return el;}} return null;};if(blur){button.addEventListener("blur",closeDelay);} if(buttonSelector){let buttonElements=element.querySelectorAll(buttonSelector);buttonElements.forEach(node=>{node.addEventListener("click",function(){element.classList.toggle("open");element.classList.toggle("close");});if(blur){node.addEventListener("blur",closeDelay);}});} diff --git a/public/dist/scripts/app.js b/public/dist/scripts/app.js index 3779dc5b93..565ae06498 100644 --- a/public/dist/scripts/app.js +++ b/public/dist/scripts/app.js @@ -395,7 +395,7 @@ buttonElements.forEach(button=>{button.addEventListener("click",open);});documen document.addEventListener("modal-close",close);element.addEventListener(closeEvent,close);}});})(window);(function(window){window.ls.container.get("view").add({selector:"data-ls-ui-open",controller:function(element,window){let def=element.classList.contains("open")?"open":"close";let buttonClass=element.dataset["buttonClass"]||"ls-ui-open";let buttonText=element.dataset["buttonText"]||"";let buttonIcon=element.dataset["buttonIcon"]||"";let buttonSelector=element.dataset["buttonSelector"]||"";let hover=element.hasAttribute("data-hover");let blur=element.hasAttribute("data-blur");let button=window.document.createElement("button");let isTouch=function(){return("ontouchstart"in window||navigator.maxTouchPoints);};button.innerText=buttonText;button.className=buttonClass;button.tabIndex=1;button.type="button";if(buttonIcon){let icon=window.document.createElement("i");icon.className=buttonIcon;button.insertBefore(icon,button.firstChild);} if(def==="close"){element.classList.add("close");element.classList.remove("open");}else{element.classList.add("open");element.classList.remove("close");} button.addEventListener("click",function(){element.classList.toggle("open");element.classList.toggle("close");});if(hover&&!isTouch()){element.addEventListener("mouseover",function(){element.classList.add("open");element.classList.remove("close");});element.addEventListener("mouseout",function(){element.classList.add("close");element.classList.remove("open");});} -let close=function(){element.classList.add("close");element.classList.remove("open");};let closeDelay=function(){window.setTimeout(function(){close();},350);};let findParent=function(tagName,el){if((el.nodeName||el.tagName).toLowerCase()===tagName.toLowerCase()){return el;} +let close=function(){element.classList.add("close");element.classList.remove("open");};let closeDelay=function(){window.setTimeout(function(){close();},400);};let findParent=function(tagName,el){if((el.nodeName||el.tagName).toLowerCase()===tagName.toLowerCase()){return el;} while((el=el.parentNode)){if((el.nodeName||el.tagName).toLowerCase()===tagName.toLowerCase()){return el;}} return null;};if(blur){button.addEventListener("blur",closeDelay);} if(buttonSelector){let buttonElements=element.querySelectorAll(buttonSelector);buttonElements.forEach(node=>{node.addEventListener("click",function(){element.classList.toggle("open");element.classList.toggle("close");});if(blur){node.addEventListener("blur",closeDelay);}});} diff --git a/public/scripts/dependencies/litespeed.js b/public/scripts/dependencies/litespeed.js index bc1fcb1e4a..c6c6cfa799 100644 --- a/public/scripts/dependencies/litespeed.js +++ b/public/scripts/dependencies/litespeed.js @@ -1,140 +1,1091 @@ +window.ls = window.ls || {}; +window.ls.container = function() { + let stock = {}; + let listeners = {}; + let namespaces = {}; + let set = function(name, object, singleton, watch = true) { + if (typeof name !== 'string') { + throw new Error('var name must be of type string'); + } + if (typeof singleton !== 'boolean') { + throw new Error('var singleton "' + singleton + '" of service "' + name + '" must be of type boolean'); + } + stock[name] = { + name: name, + object: object, + singleton: singleton, + instance: null, + watch: watch, + }; + if (!watch) { + return this; + } + let binds = listeners[name] || {}; + for (let key in binds) { + if (binds.hasOwnProperty(key)) { + document.dispatchEvent(new CustomEvent(key)); + } + } + return this; + }; + let get = function(name) { + let service = (undefined !== stock[name]) ? stock[name] : null; + if (null == service) { + return null; + } + if (service.instance) { + return service.instance; + } + let instance = (typeof service.object === 'function') ? this.resolve(service.object) : service.object; + let skip = false; + if (service.watch && name !== 'window' && name !== 'document' && name !== 'element' && typeof instance === 'object' && instance !== null) { + let handler = { + name: service.name, + watch: function() {}, + get: function(target, key) { + if (key === "__name") { + return this.name; + } + if (key === "__watch") { + return this.watch; + } + if (key === "__proxy") { + return true; + } + if (typeof target[key] === 'object' && target[key] !== null && !target[key].__proxy) { + let handler = Object.assign({}, this); + handler.name = handler.name + '.' + key; + return new Proxy(target[key], handler) + } else { + return target[key]; + } + }, + set: function(target, key, value, receiver) { + if (key === "__name") { + return this.name = value; + } + if (key === "__watch") { + return this.watch = value; + } + target[key] = value; + let path = receiver.__name + '.' + key; + document.dispatchEvent(new CustomEvent(path + '.changed')); + if (skip) { + return true; + } + skip = true; + container.set('$prop', key, true); + container.set('$value', value, true); + container.resolve(this.watch); + container.set('$key', null, true); + container.set('$value', null, true); + skip = false; + return true; + }, + }; + instance = new Proxy(instance, handler); + } + if (service.singleton) { + service.instance = instance; + } + return instance; + }; + let resolve = function(target) { + if (!target) { + return () => {}; + } + let self = this; + const REGEX_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; + const REGEX_FUNCTION_PARAMS = /(?:\s*(?:function\s*[^(]*)?\s*)((?:[^'"]|(?:(?:(['"])(?:(?:.*?[^\\]\2)|\2))))*?)\s*(?=(?:=>)|{)/m; + const REGEX_PARAMETERS_VALUES = /\s*([\w\\$]+)\s*(?:=\s*((?:(?:(['"])(?:\3|(?:.*?[^\\]\3)))((\s*\+\s*)(?:(?:(['"])(?:\6|(?:.*?[^\\]\6)))|(?:[\w$]*)))*)|.*?))?\s*(?:,|$)/gm; -window.ls=window.ls||{};window.ls.container=function(){let stock={};let listeners={};let namespaces={};let set=function(name,object,singleton,watch=true){if(typeof name!=='string'){throw new Error('var name must be of type string');} -if(typeof singleton!=='boolean'){throw new Error('var singleton "'+singleton+'" of service "'+name+'" must be of type boolean');} -stock[name]={name:name,object:object,singleton:singleton,instance:null,watch:watch,};if(!watch){return this;} -let binds=listeners[name]||{};for(let key in binds){if(binds.hasOwnProperty(key)){document.dispatchEvent(new CustomEvent(key));}} -return this;};let get=function(name){let service=(undefined!==stock[name])?stock[name]:null;if(null==service){return null;} -if(service.instance){return service.instance;} -let instance=(typeof service.object==='function')?this.resolve(service.object):service.object;let skip=false;if(service.watch&&name!=='window'&&name!=='document'&&name!=='element'&&typeof instance==='object'&&instance!==null){let handler={name:service.name,watch:function(){},get:function(target,key){if(key==="__name"){return this.name;} -if(key==="__watch"){return this.watch;} -if(key==="__proxy"){return true;} -if(typeof target[key]==='object'&&target[key]!==null&&!target[key].__proxy){let handler=Object.assign({},this);handler.name=handler.name+'.'+key;return new Proxy(target[key],handler)} -else{return target[key];}},set:function(target,key,value,receiver){if(key==="__name"){return this.name=value;} -if(key==="__watch"){return this.watch=value;} -target[key]=value;let path=receiver.__name+'.'+key;document.dispatchEvent(new CustomEvent(path+'.changed'));if(skip){return true;} -skip=true;container.set('$prop',key,true);container.set('$value',value,true);container.resolve(this.watch);container.set('$key',null,true);container.set('$value',null,true);skip=false;return true;},};instance=new Proxy(instance,handler);} -if(service.singleton){service.instance=instance;} -return instance;};let resolve=function(target){if(!target){return()=>{};} -let self=this;const REGEX_COMMENTS=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;const REGEX_FUNCTION_PARAMS=/(?:\s*(?:function\s*[^(]*)?\s*)((?:[^'"]|(?:(?:(['"])(?:(?:.*?[^\\]\2)|\2))))*?)\s*(?=(?:=>)|{)/m;const REGEX_PARAMETERS_VALUES=/\s*([\w\\$]+)\s*(?:=\s*((?:(?:(['"])(?:\3|(?:.*?[^\\]\3)))((\s*\+\s*)(?:(?:(['"])(?:\6|(?:.*?[^\\]\6)))|(?:[\w$]*)))*)|.*?))?\s*(?:,|$)/gm;function getParams(func){let functionAsString=func.toString();let params=[];let match;functionAsString=functionAsString.replace(REGEX_COMMENTS,'');functionAsString=functionAsString.match(REGEX_FUNCTION_PARAMS)[1];if(functionAsString.charAt(0)==='('){functionAsString=functionAsString.slice(1,-1);} -while(match=REGEX_PARAMETERS_VALUES.exec(functionAsString)){params.push(match[1]);} -return params;} -let args=getParams(target);return target.apply(target,args.map(function(value){return self.get(value.trim());}));};let path=function(path,value,type){type=(type)?type:'assign';path=container.scope(path).split('.');let name=path.shift();let object=container.get(name);let result=null;while(path.length>1){if(!object){return null;} -object=object[path.shift()];} -let shift=path.shift();if(value!==null&&value!==undefined&&object&&shift&&(object[shift]!==undefined||object[shift]!==null)){switch(type){case'append':if(!Array.isArray(object[shift])){object[shift]=[];} -object[shift].push(value);break;case'prepend':if(!Array.isArray(object[shift])){object[shift]=[];} -object[shift].unshift(value);break;case'splice':if(!Array.isArray(object[shift])){object[shift]=[];} -object[shift].splice(value,1);break;default:object[shift]=value;} -return true;} -if(!object){return null;} -if(!shift){result=object;} -else{return object[shift];} -return result;};let bind=function(element,path,callback){let event=container.scope(path)+'.changed';let service=event.split('.').slice(0,1).pop();let debug=element.getAttribute('data-debug')||false;listeners[service]=listeners[service]||{};listeners[service][event]=true;let printer=(function(x){return function(){if(!document.body.contains(element)){element=null;document.removeEventListener(event,printer,false);return false;} -let oldNamespaces=namespaces;namespaces=x;callback();namespaces=oldNamespaces;}}(Object.assign({},namespaces)));document.addEventListener(event,printer);};let addNamespace=function(key,scope){namespaces[key]=scope;return this;} -let removeNamespace=function(key){delete namespaces[key];return this;} -let scope=function(path){for(let[key,value]of Object.entries(namespaces)){path=(path.indexOf('.')>-1)?path.replace(key+'.',value+'.'):path.replace(key,value);} -return path;} -let container={set:set,get:get,resolve:resolve,path:path,bind:bind,scope:scope,addNamespace:addNamespace,removeNamespace:removeNamespace,stock:stock,listeners:listeners,namespaces:namespaces,};set('container',container,true,false);return container;}();window.ls.container.set('http',function(document){let globalParams=[],globalHeaders=[];let addParam=function(url,param,value){param=encodeURIComponent(param);let a=document.createElement('a');param+=(value?"="+encodeURIComponent(value):"");a.href=url;a.search+=(a.search?"&":"")+param;return a.href;};let request=function(method,url,headers,payload,progress){let i;if(-1===['GET','POST','PUT','DELETE','TRACE','HEAD','OPTIONS','CONNECT','PATCH'].indexOf(method)){throw new Error('var method must contain a valid HTTP method name');} -if(typeof url!=='string'){throw new Error('var url must be of type string');} -if(typeof headers!=='object'){throw new Error('var headers must be of type object');} -if(typeof url!=='string'){throw new Error('var url must be of type string');} -for(i=0;i-1?part.substr(0,eq):part;let val=eq>-1?decodeURIComponent(part.substr(eq+1)):'';let from=key.indexOf('[');if(from===-1){result[decodeURIComponent(key)]=val;} -else{let to=key.indexOf(']');let index=decodeURIComponent(key.substring(from+1,to));key=decodeURIComponent(key.substring(0,from));if(!result[key]){result[key]=[];} -if(!index){result[key].push(val);} -else{result[key][index]=val;}}});return result;};let states=[];let params=getJsonFromUrl(window.location.search);let hash=window.location.hash;let current=null;let previous=null;let getPrevious=()=>previous;let getCurrent=()=>current;let setPrevious=(value)=>{previous=value;return this;};let setCurrent=(value)=>{current=value;return this;};let setParam=function(key,value){params[key]=value;return this;};let getParam=function(key,def){if(key in params){return params[key];} -return def;};let getParams=function(){return params;};let getURL=function(){return window.location.href;};let add=function(path,view){if(typeof path!=='string'){throw new Error('path must be of type string');} -if(typeof view!=='object'){throw new Error('view must be of type object');} -states[states.length++]={path:path,view:view};return this;};let match=function(location){let url=location.pathname;states.sort(function(a,b){return b.path.length-a.path.length;});states.sort(function(a,b){let n=b.path.split('/').length-a.path.split('/').length;if(n!==0){return n;} -return b.path.length-a.path.length;});for(let i=0;i{let reference=match.substring(2,match.length-2).replace('[\'','.').replace('\']','').trim();reference=reference.split('|');let path=container.scope((reference[0]||''));let result=container.path(path);path=container.scope(path);if(!paths.includes(path)){paths.push(path);} -if(reference.length>=2){for(let i=1;ipaths,}},true,false);window.ls.container.set('filter',function(container){let filters={};let add=function(name,callback){filters[name]=callback;return this;};let apply=function(name,value){container.set('$value',value,true,false);return container.resolve(filters[name]);};add('uppercase',($value)=>{if(typeof $value!=='string'){return $value;} -return $value.toUpperCase();});add('lowercase',($value)=>{if(typeof $value!=='string'){return $value;} -return $value.toLowerCase();});return{add:add,apply:apply}},true,false);window.ls.container.get('filter').add('escape',value=>{if(typeof value!=='string'){return value;} -return value.replace(/&/g,'&').replace(//g,'>').replace(/\"/g,'"').replace(/\'/g,''').replace(/\//g,'/');});window.ls=window.ls||{};window.ls.container.set('window',window,true,false).set('document',window.document,true,false).set('element',window.document,true,false);window.ls.run=function(window){try{this.view.render(window.document);} -catch(error){let handler=window.ls.container.resolve(this.error);handler(error);}};window.ls.error=()=>{return error=>{console.error('ls-error',error.message,error.stack,error.toString());}};window.ls.router=window.ls.container.get('router');window.ls.view=window.ls.container.get('view');window.ls.filter=window.ls.container.get('filter');window.ls.container.get('view').add({selector:'data-ls-router',repeat:false,controller:function(element,window,document,view,router){let firstFromServer=(element.getAttribute('data-first-from-server')==='true');let scope={selector:'data-ls-scope',template:false,repeat:true,controller:function(){},};let init=function(route){let count=parseInt(element.getAttribute('data-ls-scope-count')||0);element.setAttribute('data-ls-scope-count',count+1);window.scrollTo(0,0);if(window.document.body.scrollTo){window.document.body.scrollTo(0,0);} -router.reset();if(null===route){return;} -scope.template=(undefined!==route.view.template)?route.view.template:null;scope.controller=(undefined!==route.view.controller)?route.view.controller:function(){};document.dispatchEvent(new CustomEvent('state-change'));if(firstFromServer&&null===router.getPrevious()){scope.template='';document.dispatchEvent(new CustomEvent('state-changed'));} -else if(count===1){view.render(element,function(){document.dispatchEvent(new CustomEvent('state-changed'));});} -else if(null!==router.getPrevious()){view.render(element,function(){document.dispatchEvent(new CustomEvent('state-changed'));});}};let findParent=function(tagName,el){if((el.nodeName||el.tagName).toLowerCase()===tagName.toLowerCase()){return el;} -while(el=el.parentNode){if((el.nodeName||el.tagName).toLowerCase()===tagName.toLowerCase()){return el;}} -return null;};element.removeAttribute('data-ls-router');element.setAttribute('data-ls-scope','');element.setAttribute('data-ls-scope-count',1);view.add(scope);document.addEventListener('click',function(event){let target=findParent('a',event.target);if(!target){return false;} -if(!target.href){return false;} -if((event.metaKey)){return false;} -if((target.hasAttribute('target'))&&('_blank'===target.getAttribute('target'))){return false;} -if(target.hostname!==window.location.hostname){return false;} -let route=router.match(target);if(null===route){return false;} -event.preventDefault();if(window.location===target.href){return false;} -route.view.state=(undefined===route.view.state)?true:route.view.state;if(true===route.view.state){if(router.getPrevious()&&router.getPrevious().view&&(router.getPrevious().view.scope!==route.view.scope)){window.location.href=target.href;return false;} -window.history.pushState({},'Unknown',target.href);} -init(route);return true;});window.addEventListener('popstate',function(){init(router.match(window.location));});window.addEventListener('hashchange',function(){init(router.match(window.location));});init(router.match(window.location));}});window.ls.container.get('view').add({selector:'data-ls-attrs',controller:function(element,expression,container){let attrs=element.getAttribute('data-ls-attrs').trim().split(',');let paths=[];let debug=element.getAttribute('data-debug')||false;let check=()=>{container.set('element',element,true,false);if(debug){console.info('debug-ls-attrs attributes:',attrs);} -for(let i=0;i{for(let i=0;i-1));value=element.value;} -catch{return null;}} -if(bind){element.addEventListener('change',()=>{for(let i=0;i-1){value.splice(index,1);} -container.path(paths[i],value);}});} -return;} -if(element.value!==value){element.value=value;element.dispatchEvent(new Event('change'));} -if(bind){element.addEventListener('input',sync);element.addEventListener('change',sync);}} -else{if(element.textContent!=value){element.innerHTML=value;}}};let sync=(()=>{return()=>{if(debug){console.info('debug-ls-bind','sync-path',paths);console.info('debug-ls-bind','sync-syntax',syntax);console.info('debug-ls-bind','sync-syntax-parsed',parsedSyntax);console.info('debug-ls-bind','sync-value',element.value);} -for(let i=0;i{echo(expression.parse(parsedSyntax),false);});path.pop();}}}});window.ls.container.get('view').add({selector:'data-ls-if',controller:function(element,expression,container,view){let result='';let syntax=element.getAttribute('data-ls-if')||'';let debug=element.getAttribute('data-debug')||false;let paths=[];let check=()=>{if(debug){console.info('debug-ls-if',expression.parse(syntax.replace(/(\r\n|\n|\r)/gm,' '),'undefined',true));} -try{result=(eval(expression.parse(syntax.replace(/(\r\n|\n|\r)/gm,' '),'undefined',true)));} -catch(error){throw new Error('Failed to evaluate expression "'+syntax+' (resulted with: "'+result+'")": '+error);} -if(debug){console.info('debug-ls-if result:',result);} -paths=expression.getPaths();let prv=element.$lsSkip;element.$lsSkip=!result;if(!result){element.style.visibility='hidden';element.style.display='none';} -else{element.style.removeProperty('display');element.style.removeProperty('visibility');} -if(prv===true&&element.$lsSkip===false){view.render(element)}};check();for(let i=0;i{let context=expr+'.'+index;container.addNamespace(as,context);if(debug){console.info('debug-ls-loop','index',index);console.info('debug-ls-loop','context',context);console.info('debug-ls-loop','context-path',container.path(context).name);console.info('debug-ls-loop','namespaces',container.namespaces);} -container.set(as,container.path(context),true,watch);container.set(key,index,true,false);view.render(children[prop]);container.removeNamespace(as);})(prop);} -element.dispatchEvent(new Event('looped'));};let template=(element.children.length===1)?element.children[0]:window.document.createElement('li');echo();container.bind(element,expr+'.length',echo);let path=(expr+'.length').split('.');while(path.length){container.bind(element,path.join('.'),echo);path.pop();}}});window.ls.container.get('view').add({selector:'data-ls-template',template:false,repeat:false,controller:function(element,view,http,expression,document,container){let template=element.getAttribute('data-ls-template')||'';let type=element.getAttribute('data-type')||'url';let debug=element.getAttribute('data-debug')||false;let paths=[];let check=function(init=false){let source=expression.parse(template);paths=expression.getPaths();element.innerHTML='';if('script'===type){let inlineTemplate=document.getElementById(source);if(inlineTemplate&&inlineTemplate.innerHTML){element.innerHTML=inlineTemplate.innerHTML;element.dispatchEvent(new CustomEvent('template-loaded',{bubbles:true,cancelable:false}));} -else{if(debug){console.error('Missing template "'+source+'"');}} -if(!init){view.render(element);} -return;} -http.get(source).then(function(element){return function(data){element.innerHTML=data;view.render(element);element.dispatchEvent(new CustomEvent('template-loaded',{bubbles:true,cancelable:false}));}}(element),function(){throw new Error('Failed loading template');});};check(true);for(let i=0;i 1) { + if (!object) { + return null; + } + object = object[path.shift()]; + } + let shift = path.shift(); + if (value !== null && value !== undefined && object && shift && (object[shift] !== undefined || object[shift] !== null)) { + switch (type) { + case 'append': + if (!Array.isArray(object[shift])) { + object[shift] = []; + } + object[shift].push(value); + break; + case 'prepend': + if (!Array.isArray(object[shift])) { + object[shift] = []; + } + object[shift].unshift(value); + break; + case 'splice': + if (!Array.isArray(object[shift])) { + object[shift] = []; + } + object[shift].splice(value, 1); + break; + default: + object[shift] = value; + } + return true; + } + if (!object) { + return null; + } + if (!shift) { + result = object; + } else { + return object[shift]; + } + return result; + }; + let bind = function(element, path, callback) { + let event = container.scope(path) + '.changed'; + let service = event.split('.').slice(0, 1).pop(); + let debug = element.getAttribute('data-debug') || false; + listeners[service] = listeners[service] || {}; + listeners[service][event] = true; + let printer = (function(x) { + return function() { + if (!document.body.contains(element)) { + element = null; + document.removeEventListener(event, printer, false); + return false; + } + let oldNamespaces = namespaces; + namespaces = x; + callback(); + namespaces = oldNamespaces; + } + }(Object.assign({}, namespaces))); + document.addEventListener(event, printer); + }; + let addNamespace = function(key, scope) { + namespaces[key] = scope; + return this; + } + let removeNamespace = function(key) { + delete namespaces[key]; + return this; + } + let scope = function(path) { + for (let [key, value] of Object.entries(namespaces)) { + path = (path.indexOf('.') > -1) ? path.replace(key + '.', value + '.') : path.replace(key, value); + } + return path; + } + let container = { + set: set, + get: get, + resolve: resolve, + path: path, + bind: bind, + scope: scope, + addNamespace: addNamespace, + removeNamespace: removeNamespace, + stock: stock, + listeners: listeners, + namespaces: namespaces, + }; + set('container', container, true, false); + return container; +}(); +window.ls.container.set('http', function(document) { + let globalParams = [], + globalHeaders = []; + let addParam = function(url, param, value) { + param = encodeURIComponent(param); + let a = document.createElement('a'); + param += (value ? "=" + encodeURIComponent(value) : ""); + a.href = url; + a.search += (a.search ? "&" : "") + param; + return a.href; + }; + let request = function(method, url, headers, payload, progress) { + let i; + if (-1 === ['GET', 'POST', 'PUT', 'DELETE', 'TRACE', 'HEAD', 'OPTIONS', 'CONNECT', 'PATCH'].indexOf(method)) { + throw new Error('var method must contain a valid HTTP method name'); + } + if (typeof url !== 'string') { + throw new Error('var url must be of type string'); + } + if (typeof headers !== 'object') { + throw new Error('var headers must be of type object'); + } + if (typeof url !== 'string') { + throw new Error('var url must be of type string'); + } + for (i = 0; i < globalParams.length; i++) { + url = addParam(url, globalParams[i].key, globalParams[i].value); + } + return new Promise(function(resolve, reject) { + let xmlhttp = new XMLHttpRequest(); + xmlhttp.open(method, url, true); + xmlhttp.setRequestHeader('Ajax', '1'); + for (i = 0; i < globalHeaders.length; i++) { + xmlhttp.setRequestHeader(globalHeaders[i].key, globalHeaders[i].value); + } + for (let key in headers) { + if (headers.hasOwnProperty(key)) { + xmlhttp.setRequestHeader(key, headers[key]); + } + } + xmlhttp.onload = function() { + if (4 === xmlhttp.readyState && 200 === xmlhttp.status) { + resolve(xmlhttp.response); + } else { + document.dispatchEvent(new CustomEvent('http-' + method.toLowerCase() + '-' + xmlhttp.status)); + reject(new Error(xmlhttp.statusText)); + } + }; + if (progress) { + xmlhttp.addEventListener('progress', progress); + xmlhttp.upload.addEventListener('progress', progress, false); + } + xmlhttp.onerror = function() { + reject(new Error("Network Error")); + }; + xmlhttp.send(payload); + }) + }; + return { + 'get': function(url) { + return request('GET', url, {}, '') + }, + 'post': function(url, headers, payload) { + return request('POST', url, headers, payload) + }, + 'put': function(url, headers, payload) { + return request('PUT', url, headers, payload) + }, + 'patch': function(url, headers, payload) { + return request('PATCH', url, headers, payload) + }, + 'delete': function(url) { + return request('DELETE', url, {}, '') + }, + 'addGlobalParam': function(key, value) { + globalParams.push({ + key: key, + value: value + }); + }, + 'addGlobalHeader': function(key, value) { + globalHeaders.push({ + key: key, + value: value + }); + } + } +}, true, false); +window.ls.container.set('cookie', function(document) { + function get(name) { + let value = "; " + document.cookie, + parts = value.split("; " + name + "="); + if (parts.length === 2) { + return parts.pop().split(";").shift(); + } + return null; + } + + function set(name, value, days) { + let date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + let expires = (0 < days) ? 'expires=' + date.toUTCString() : 'expires=0'; + document.cookie = name + "=" + value + ";" + expires + ";path=/"; + return this; + } + return { + 'get': get, + 'set': set + } +}, true, false); +window.ls.container.set('view', function(http, container) { + let stock = {}; + let execute = function(view, node, container) { + container.set('element', node, true, false); + container.resolve(view.controller); + if (true !== view.repeat) { + node.removeAttribute(view.selector); + } + }; + let parse = function(node, skip, callback) { + if (node.tagName === 'SCRIPT') { + return; + } + if (node.attributes && skip !== true) { + let attrs = []; + let attrsLen = node.attributes.length; + for (let x = 0; x < attrsLen; x++) { + attrs.push(node.attributes[x].nodeName); + } + if (1 !== node.nodeType) { + return; + } + if (attrs && attrsLen) { + for (let x = 0; x < attrsLen; x++) { + if (node.$lsSkip === true) { + break; + } + let pointer = (!/Edge/.test(navigator.userAgent)) ? x : (attrsLen - 1) - x; + let length = attrsLen; + let attr = attrs[pointer]; + if (!stock[attr]) { + continue; + } + let comp = stock[attr]; + if (typeof comp.template === "function") { + comp.template = container.resolve(comp.template); + } + if (!comp.template) { + (function(comp, node, container) { + execute(comp, node, container); + })(comp, node, container); + if (length !== attrsLen) { + x--; + } + if (callback) { + callback(); + } + continue; + } + node.classList.remove('load-end'); + node.classList.add('load-start'); + node.$lsSkip = true; + http.get(comp.template).then(function(node, comp) { + return function(data) { + node.$lsSkip = false; + node.innerHTML = data; + node.classList.remove('load-start'); + node.classList.add('load-end'); + (function(comp, node, container) { + execute(comp, node, container); + })(comp, node, container); + parse(node, true); + if (callback) { + callback(); + } + } + }(node, comp), function(error) { + throw new Error('Failed to load comp template: ' + error.message); + }); + } + } + } + if (true === node.$lsSkip) { + return; + } + let list = (node) ? node.childNodes : []; + if (node.$lsSkip === true) { + list = []; + } + for (let i = 0; i < list.length; i++) { + let child = list[i]; + parse(child); + } + }; + return { + stock: stock, + add: function(object) { + if (typeof object !== 'object') { + throw new Error('object must be of type object'); + } + let defaults = { + 'selector': '', + 'controller': function() {}, + 'template': '', + 'repeat': false, + 'protected': false + }; + for (let prop in defaults) { + if (!defaults.hasOwnProperty(prop)) { + continue; + } + if (prop in object) { + continue; + } + object[prop] = defaults[prop]; + } + if (!object.selector) { + throw new Error('View component is missing a selector attribute'); + } + stock[object.selector] = object; + return this; + }, + render: function(element, callback) { + parse(element, false, callback); + element.dispatchEvent(new window.Event('rendered', { + bubbles: false + })); + } + } +}, true, false); +window.ls.container.set('router', function(window) { + let getJsonFromUrl = function(URL) { + let query; + if (URL) { + let pos = location.search.indexOf('?'); + if (pos === -1) return []; + query = location.search.substr(pos + 1); + } else { + query = location.search.substr(1); + } + let result = {}; + query.split('&').forEach(function(part) { + if (!part) { + return; + } + part = part.split('+').join(' '); + let eq = part.indexOf('='); + let key = eq > -1 ? part.substr(0, eq) : part; + let val = eq > -1 ? decodeURIComponent(part.substr(eq + 1)) : ''; + let from = key.indexOf('['); + if (from === -1) { + result[decodeURIComponent(key)] = val; + } else { + let to = key.indexOf(']'); + let index = decodeURIComponent(key.substring(from + 1, to)); + key = decodeURIComponent(key.substring(0, from)); + if (!result[key]) { + result[key] = []; + } + if (!index) { + result[key].push(val); + } else { + result[key][index] = val; + } + } + }); + return result; + }; + let states = []; + let params = getJsonFromUrl(window.location.search); + let hash = window.location.hash; + let current = null; + let previous = null; + let getPrevious = () => previous; + let getCurrent = () => current; + let setPrevious = (value) => { + previous = value; + return this; + }; + let setCurrent = (value) => { + current = value; + return this; + }; + let setParam = function(key, value) { + params[key] = value; + return this; + }; + let getParam = function(key, def) { + if (key in params) { + return params[key]; + } + return def; + }; + let getParams = function() { + return params; + }; + let getURL = function() { + return window.location.href; + }; + let add = function(path, view) { + if (typeof path !== 'string') { + throw new Error('path must be of type string'); + } + if (typeof view !== 'object') { + throw new Error('view must be of type object'); + } + states[states.length++] = { + path: path, + view: view + }; + return this; + }; + let match = function(location) { + let url = location.pathname; + states.sort(function(a, b) { + return b.path.length - a.path.length; + }); + states.sort(function(a, b) { + let n = b.path.split('/').length - a.path.split('/').length; + if (n !== 0) { + return n; + } + return b.path.length - a.path.length; + }); + for (let i = 0; i < states.length; i++) { + let value = states[i]; + value.path = (value.path.substring(0, 1) !== '/') ? location.pathname + value.path : value.path; + let match = new RegExp("^" + value.path.replace(/:[^\s/]+/g, '([\\w-]+)') + "$"); + let found = url.match(match); + if (found) { + previous = current; + current = value; + return value; + } + } + return null + }; + let change = function(URL, replace) { + if (!replace) { + window.history.pushState({}, '', URL); + } else { + window.history.replaceState({}, '', URL); + } + window.dispatchEvent(new PopStateEvent('popstate', {})); + return this; + }; + let reload = function() { + return change(window.location.href); + }; + return { + setParam: setParam, + getParam: getParam, + getParams: getParams, + getURL: getURL, + add: add, + change: change, + reload: reload, + match: match, + getCurrent: getCurrent, + setCurrent: setCurrent, + getPrevious: getPrevious, + setPrevious: setPrevious, + params: params, + hash: hash, + reset: function() { + this.params = getJsonFromUrl(window.location.search); + this.hash = window.location.hash; + } + }; +}, true, true); +window.ls.container.set('expression', function(container, filter) { + let paths = []; + return { + regex: /(\{{.*?\}})/gi, + parse: function(string, def, cast = false) { + def = def || ''; + paths = []; + return string.replace(this.regex, match => { + let reference = match.substring(2, match.length - 2).replace('[\'', '.').replace('\']', '').trim(); + reference = reference.split('|'); + let path = container.scope((reference[0] || '')); + let result = container.path(path); + path = container.scope(path); + if (!paths.includes(path)) { + paths.push(path); + } + if (reference.length >= 2) { + for (let i = 1; i < reference.length; i++) { + result = filter.apply(reference[i], result); + } + } + if (null === result || undefined === result) { + result = def; + } else if (typeof result === 'object') { + result = JSON.stringify(result, null, 4); + } else if (((typeof result === 'object') || (typeof result === 'string')) && cast) { + result = '\'' + result + '\''; + } + return result; + }).replace(/\\{/g, "{").replace(/\\}/g, "}"); + }, + getPaths: () => paths, + } +}, true, false); +window.ls.container.set('filter', function(container) { + let filters = {}; + let add = function(name, callback) { + filters[name] = callback; + return this; + }; + let apply = function(name, value) { + container.set('$value', value, true, false); + return container.resolve(filters[name]); + }; + add('uppercase', ($value) => { + if (typeof $value !== 'string') { + return $value; + } + return $value.toUpperCase(); + }); + add('lowercase', ($value) => { + if (typeof $value !== 'string') { + return $value; + } + return $value.toLowerCase(); + }); + return { + add: add, + apply: apply + } +}, true, false); +window.ls.container.get('filter').add('escape', value => { + if (typeof value !== 'string') { + return value; + } + return value.replace(/&/g, '&').replace(//g, '>').replace(/\"/g, '"').replace(/\'/g, ''').replace(/\//g, '/'); +}); +window.ls = window.ls || {}; +window.ls.container.set('window', window, true, false).set('document', window.document, true, false).set('element', window.document, true, false); +window.ls.run = function(window) { + try { + this.view.render(window.document); + } catch (error) { + let handler = window.ls.container.resolve(this.error); + handler(error); + } +}; +window.ls.error = () => { + return error => { + console.error('ls-error', error.message, error.stack, error.toString()); + } +}; +window.ls.router = window.ls.container.get('router'); +window.ls.view = window.ls.container.get('view'); +window.ls.filter = window.ls.container.get('filter'); +window.ls.container.get('view').add({ + selector: 'data-ls-router', + repeat: false, + controller: function(element, window, document, view, router) { + let firstFromServer = (element.getAttribute('data-first-from-server') === 'true'); + let scope = { + selector: 'data-ls-scope', + template: false, + repeat: true, + controller: function() {}, + }; + let init = function(route) { + let count = parseInt(element.getAttribute('data-ls-scope-count') || 0); + element.setAttribute('data-ls-scope-count', count + 1); + window.scrollTo(0, 0); + if (window.document.body.scrollTo) { + window.document.body.scrollTo(0, 0); + } + router.reset(); + if (null === route) { + return; + } + scope.template = (undefined !== route.view.template) ? route.view.template : null; + scope.controller = (undefined !== route.view.controller) ? route.view.controller : function() {}; + document.dispatchEvent(new CustomEvent('state-change')); + if (firstFromServer && null === router.getPrevious()) { + scope.template = ''; + document.dispatchEvent(new CustomEvent('state-changed')); + } else if (count === 1) { + view.render(element, function() { + document.dispatchEvent(new CustomEvent('state-changed')); + }); + } else if (null !== router.getPrevious()) { + view.render(element, function() { + document.dispatchEvent(new CustomEvent('state-changed')); + }); + } + }; + let findParent = function(tagName, el) { + if ((el.nodeName || el.tagName).toLowerCase() === tagName.toLowerCase()) { + return el; + } + while (el = el.parentNode) { + if ((el.nodeName || el.tagName).toLowerCase() === tagName.toLowerCase()) { + return el; + } + } + return null; + }; + element.removeAttribute('data-ls-router'); + element.setAttribute('data-ls-scope', ''); + element.setAttribute('data-ls-scope-count', 1); + view.add(scope); + document.addEventListener('click', function(event) { + let target = findParent('a', event.target); + if (!target) { + return false; + } + if (!target.href) { + return false; + } + if ((event.metaKey)) { + return false; + } + if ((target.hasAttribute('target')) && ('_blank' === target.getAttribute('target'))) { + return false; + } + if (target.hostname !== window.location.hostname) { + return false; + } + let route = router.match(target); + if (null === route) { + return false; + } + event.preventDefault(); + if (window.location === target.href) { + return false; + } + route.view.state = (undefined === route.view.state) ? true : route.view.state; + if (true === route.view.state) { + if (router.getPrevious() && router.getPrevious().view && (router.getPrevious().view.scope !== route.view.scope)) { + window.location.href = target.href; + return false; + } + window.history.pushState({}, 'Unknown', target.href); + } + init(route); + return true; + }); + window.addEventListener('popstate', function() { + init(router.match(window.location)); + }); + window.addEventListener('hashchange', function() { + init(router.match(window.location)); + }); + init(router.match(window.location)); + } +}); +window.ls.container.get('view').add({ + selector: 'data-ls-attrs', + controller: function(element, expression, container) { + let attrs = element.getAttribute('data-ls-attrs').trim().split(','); + let paths = []; + let debug = element.getAttribute('data-debug') || false; + let check = () => { + container.set('element', element, true, false); + if (debug) { + console.info('debug-ls-attrs attributes:', attrs); + } + for (let i = 0; i < attrs.length; i++) { + let attr = attrs[i]; + let key = expression.parse((attr.substring(0, attr.indexOf('=')) || attr)); + paths = paths.concat(expression.getPaths()); + let value = ''; + if (attr.indexOf('=') > -1) { + value = expression.parse(attr.substring(attr.indexOf('=') + 1)) || ''; + paths = paths.concat(expression.getPaths()); + } + if (!key) { + return null; + } + element.setAttribute(key, value); + } + }; + check(); + for (let i = 0; i < paths.length; i++) { + let path = paths[i].split('.'); + while (path.length) { + container.bind(element, path.join('.'), check); + path.pop(); + } + } + } +}); +window.ls.container.get('view').add({ + selector: 'data-ls-bind', + controller: function(element, expression, container) { + let debug = element.getAttribute('data-debug') || false; + let echo = function(value, bind = true) { + if (element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'BUTTON' || element.tagName === 'TEXTAREA') { + let type = element.getAttribute('type'); + if ('radio' === type) { + if (value.toString() === element.value) { + element.setAttribute('checked', 'checked'); + } else { + element.removeAttribute('checked'); + } + if (bind) { + element.addEventListener('change', () => { + for (let i = 0; i < paths.length; i++) { + if (element.checked) { + value = element.value; + } + container.path(paths[i], value); + } + }); + } + return; + } + if ('checkbox' === type) { + if (typeof value === 'boolean' || value === 'true' || value === 'false') { + if (value === true || value === 'true') { + element.setAttribute('checked', 'checked'); + element.checked = true; + } else { + element.removeAttribute('checked'); + element.checked = false; + } + } else { + try { + value = JSON.parse(value); + element.checked = (Array.isArray(value) && (value.indexOf(element.value) > -1)); + value = element.value; + } catch { + return null; + } + } + if (bind) { + element.addEventListener('change', () => { + for (let i = 0; i < paths.length; i++) { + let value = container.path(paths[i]); + let index = value.indexOf(element.value); + if (element.checked && index < 0) { + value.push(element.value); + } + if (!element.checked && index > -1) { + value.splice(index, 1); + } + container.path(paths[i], value); + } + }); + } + return; + } + if (element.value !== value) { + element.value = value; + element.dispatchEvent(new Event('change')); + } + if (bind) { + element.addEventListener('input', sync); + element.addEventListener('change', sync); + } + } else { + if (element.textContent != value) { + element.innerHTML = value; + } + } + }; + let sync = (() => { + return () => { + if (debug) { + console.info('debug-ls-bind', 'sync-path', paths); + console.info('debug-ls-bind', 'sync-syntax', syntax); + console.info('debug-ls-bind', 'sync-syntax-parsed', parsedSyntax); + console.info('debug-ls-bind', 'sync-value', element.value); + } + for (let i = 0; i < paths.length; i++) { + if ('{{' + paths[i] + '}}' !== parsedSyntax) { + if (debug) { + console.info('debug-ls-bind', 'sync-skipped-path', paths[i]); + console.info('debug-ls-bind', 'sync-skipped-syntax', syntax); + console.info('debug-ls-bind', 'sync-skipped-syntax-parsed', parsedSyntax); + } + continue; + } + if (debug) { + console.info('debug-ls-bind', 'sync-loop-path', paths[i]); + console.info('debug-ls-bind', 'sync-loop-syntax', parsedSyntax); + } + container.path(paths[i], element.value); + } + } + })(); + let syntax = element.getAttribute('data-ls-bind'); + let parsedSyntax = container.scope(syntax); + let unsync = (!!element.getAttribute('data-unsync')) || false; + let result = expression.parse(syntax); + let paths = expression.getPaths(); + echo(result, !unsync); + element.addEventListener('looped', function() { + echo(expression.parse(parsedSyntax), false); + }); + for (let i = 0; i < paths.length; i++) { + let path = paths[i].split('.'); + if (debug) { + console.info('debug-ls-bind', 'bind-path', path); + console.info('debug-ls-bind', 'bind-syntax', syntax); + } + while (path.length) { + container.bind(element, path.join('.'), () => { + echo(expression.parse(parsedSyntax), false); + }); + path.pop(); + } + } + } +}); +window.ls.container.get('view').add({ + selector: 'data-ls-if', + controller: function(element, expression, container, view) { + let result = ''; + let syntax = element.getAttribute('data-ls-if') || ''; + let debug = element.getAttribute('data-debug') || false; + let paths = []; + let check = () => { + if (debug) { + console.info('debug-ls-if', expression.parse(syntax.replace(/(\r\n|\n|\r)/gm, ' '), 'undefined', true)); + } + try { + result = (eval(expression.parse(syntax.replace(/(\r\n|\n|\r)/gm, ' '), 'undefined', true))); + } catch (error) { + throw new Error('Failed to evaluate expression "' + syntax + ' (resulted with: "' + result + '")": ' + error); + } + if (debug) { + console.info('debug-ls-if result:', result); + } + paths = expression.getPaths(); + let prv = element.$lsSkip; + element.$lsSkip = !result; + if (!result) { + element.style.visibility = 'hidden'; + element.style.display = 'none'; + } else { + element.style.removeProperty('display'); + element.style.removeProperty('visibility'); + } + if (prv === true && element.$lsSkip === false) { + view.render(element) + } + }; + check(); + for (let i = 0; i < paths.length; i++) { + let path = paths[i].split('.'); + while (path.length) { + container.bind(element, path.join('.'), check); + path.pop(); + } + } + } +}); +window.ls.container.get('view').add({ + selector: 'data-ls-loop', + template: false, + repeat: false, + nested: false, + controller: function(element, view, container, window, expression) { + let expr = expression.parse(element.getAttribute('data-ls-loop')); + let as = element.getAttribute('data-ls-as'); + let key = element.getAttribute('data-ls-key') || '$index'; + let limit = parseInt(expression.parse(element.getAttribute('data-limit') || '') || -1); + let debug = element.getAttribute('data-debug') || false; + let echo = function() { + let array = container.path(expr); + let counter = 0; + array = (!array) ? [] : array; + let watch = !!(array && array.__proxy); + while (element.hasChildNodes()) { + element.removeChild(element.lastChild); + element.lastChild = null; + } + if (array instanceof Array && typeof array !== 'object') { + throw new Error('Reference value must be array or object. ' + (typeof array) + ' given'); + } + let children = []; + element.$lsSkip = true; + element.style.visibility = (0 === array.length) ? 'hidden' : 'visible'; + for (let prop in array) { + if (counter == limit) { + break; + } + counter++; + if (!array.hasOwnProperty(prop)) { + continue; + } + children[prop] = template.cloneNode(true); + element.appendChild(children[prop]); + (index => { + let context = expr + '.' + index; + container.addNamespace(as, context); + if (debug) { + console.info('debug-ls-loop', 'index', index); + console.info('debug-ls-loop', 'context', context); + console.info('debug-ls-loop', 'context-path', container.path(context).name); + console.info('debug-ls-loop', 'namespaces', container.namespaces); + } + container.set(as, container.path(context), true, watch); + container.set(key, index, true, false); + view.render(children[prop]); + container.removeNamespace(as); + })(prop); + } + element.dispatchEvent(new Event('looped')); + }; + let template = (element.children.length === 1) ? element.children[0] : window.document.createElement('li'); + echo(); + container.bind(element, expr + '.length', echo); + let path = (expr + '.length').split('.'); + while (path.length) { + container.bind(element, path.join('.'), echo); + path.pop(); + } + } +}); +window.ls.container.get('view').add({ + selector: 'data-ls-template', + template: false, + repeat: false, + controller: function(element, view, http, expression, document, container) { + let template = element.getAttribute('data-ls-template') || ''; + let type = element.getAttribute('data-type') || 'url'; + let debug = element.getAttribute('data-debug') || false; + let paths = []; + let check = function(init = false) { + let source = expression.parse(template); + paths = expression.getPaths(); + element.innerHTML = ''; + if ('script' === type) { + let inlineTemplate = document.getElementById(source); + if (inlineTemplate && inlineTemplate.innerHTML) { + element.innerHTML = inlineTemplate.innerHTML; + element.dispatchEvent(new CustomEvent('template-loaded', { + bubbles: true, + cancelable: false + })); + } else { + if (debug) { + console.error('Missing template "' + source + '"'); + } + } + if (!init) { + view.render(element); + } + return; + } + http.get(source).then(function(element) { + return function(data) { + element.innerHTML = data; + view.render(element); + element.dispatchEvent(new CustomEvent('template-loaded', { + bubbles: true, + cancelable: false + })); + } + }(element), function() { + throw new Error('Failed loading template'); + }); + }; + check(true); + for (let i = 0; i < paths.length; i++) { + let path = paths[i].split('.'); + while (path.length) { + container.bind(element, path.join('.'), check); + path.pop(); + } + } + } +}); \ No newline at end of file diff --git a/public/scripts/views/ui/open.js b/public/scripts/views/ui/open.js index 229497a26f..b3b2f89daf 100644 --- a/public/scripts/views/ui/open.js +++ b/public/scripts/views/ui/open.js @@ -63,7 +63,7 @@ let closeDelay = function() { window.setTimeout(function() { close(); - }, 350); + }, 400); }; let findParent = function(tagName, el) {