populate-me 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/README.md +9 -0
  2. data/lib/populate_me/control/_public/css/main.css +180 -0
  3. data/lib/populate_me/control/_public/css/plugin.asmselect.css +63 -0
  4. data/lib/populate_me/control/_public/css/ui-darkness/images/ui-bg_flat_30_cccccc_40x100.png +0 -0
  5. data/lib/populate_me/control/_public/css/ui-darkness/images/ui-bg_flat_50_5c5c5c_40x100.png +0 -0
  6. data/lib/populate_me/control/_public/css/ui-darkness/images/ui-bg_glass_20_555555_1x400.png +0 -0
  7. data/lib/populate_me/control/_public/css/ui-darkness/images/ui-bg_glass_40_0078a3_1x400.png +0 -0
  8. data/lib/populate_me/control/_public/css/ui-darkness/images/ui-bg_glass_40_ffc73d_1x400.png +0 -0
  9. data/lib/populate_me/control/_public/css/ui-darkness/images/ui-bg_gloss-wave_25_333333_500x100.png +0 -0
  10. data/lib/populate_me/control/_public/css/ui-darkness/images/ui-bg_highlight-soft_80_eeeeee_1x100.png +0 -0
  11. data/lib/populate_me/control/_public/css/ui-darkness/images/ui-bg_inset-soft_25_000000_1x100.png +0 -0
  12. data/lib/populate_me/control/_public/css/ui-darkness/images/ui-bg_inset-soft_30_f58400_1x100.png +0 -0
  13. data/lib/populate_me/control/_public/css/ui-darkness/images/ui-icons_222222_256x240.png +0 -0
  14. data/lib/populate_me/control/_public/css/ui-darkness/images/ui-icons_4b8e0b_256x240.png +0 -0
  15. data/lib/populate_me/control/_public/css/ui-darkness/images/ui-icons_a83300_256x240.png +0 -0
  16. data/lib/populate_me/control/_public/css/ui-darkness/images/ui-icons_cccccc_256x240.png +0 -0
  17. data/lib/populate_me/control/_public/css/ui-darkness/images/ui-icons_ffffff_256x240.png +0 -0
  18. data/lib/populate_me/control/_public/css/ui-darkness/jquery-ui-1.8.17.custom.css +430 -0
  19. data/lib/populate_me/control/_public/img/handle-pattern.png +0 -0
  20. data/lib/populate_me/control/_public/img/icons-cms.png +0 -0
  21. data/lib/populate_me/control/_public/img/placeholder.png +0 -0
  22. data/lib/populate_me/control/_public/img/placeholder.stash_thumb.gif +0 -0
  23. data/lib/populate_me/control/_public/img/placeholder.stash_thumb.png +0 -0
  24. data/lib/populate_me/control/_public/img/small-loader.gif +0 -0
  25. data/lib/populate_me/control/_public/js/addon.timepicker.js +20 -0
  26. data/lib/populate_me/control/_public/js/jquery-ui.js +114 -0
  27. data/lib/populate_me/control/_public/js/jquery.js +167 -0
  28. data/lib/populate_me/control/_public/js/jquery.mustache.js +559 -0
  29. data/lib/populate_me/control/_public/js/main.js +144 -0
  30. data/lib/populate_me/control/_public/js/plugin.asmselect.js +407 -0
  31. data/lib/populate_me/control/_public/js/plugin.form.js +11 -0
  32. data/lib/populate_me/control/_public/js/plugin.quicksearch.js +1 -0
  33. data/lib/populate_me/control/_public/js/plugin.underwood.js +4 -0
  34. data/lib/populate_me/control/views/layout.erb +77 -0
  35. data/lib/populate_me/control.rb +193 -0
  36. data/lib/populate_me/mongo/backend_api_plug.rb +77 -0
  37. data/lib/populate_me/mongo/crushyform.rb +225 -0
  38. data/lib/populate_me/mongo/mutation.rb +266 -0
  39. data/lib/populate_me/mongo/plug.rb +132 -0
  40. data/lib/populate_me/mongo/stash.rb +119 -0
  41. data/lib/populate_me/mongo.rb +1 -0
  42. data/populate-me.gemspec +14 -0
  43. metadata +120 -0
@@ -0,0 +1,11 @@
1
+ /*!
2
+ * jQuery Form Plugin
3
+ * version: 2.83 (11-JUL-2011)
4
+ * @requires jQuery v1.3.2 or later
5
+ *
6
+ * Examples and documentation at: http://malsup.com/jquery/form/
7
+ * Dual licensed under the MIT and GPL licenses:
8
+ * http://www.opensource.org/licenses/mit-license.php
9
+ * http://www.gnu.org/licenses/gpl.html
10
+ */
11
+ ;(function($){$.fn.ajaxSubmit=function(w){if(!this.length){log('ajaxSubmit: skipping submit process - no element selected');return this}var x,action,url,$form=this;if(typeof w=='function'){w={success:w}}x=this.attr('method');action=this.attr('action');url=(typeof action==='string')?$.trim(action):'';url=url||window.location.href||'';if(url){url=(url.match(/^([^#]+)/)||[])[1]}w=$.extend(true,{url:url,success:$.ajaxSettings.success,type:x||'GET',iframeSrc:/^https/i.test(window.location.href||'')?'javascript:false':'about:blank'},w);var y={};this.trigger('form-pre-serialize',[this,w,y]);if(y.veto){log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');return this}if(w.beforeSerialize&&w.beforeSerialize(this,w)===false){log('ajaxSubmit: submit aborted via beforeSerialize callback');return this}var n,v,a=this.formToArray(w.semantic);if(w.data){w.extraData=w.data;for(n in w.data){if(w.data[n]instanceof Array){for(var k in w.data[n]){a.push({name:n,value:w.data[n][k]})}}else{v=w.data[n];v=$.isFunction(v)?v():v;a.push({name:n,value:v})}}}if(w.beforeSubmit&&w.beforeSubmit(a,this,w)===false){log('ajaxSubmit: submit aborted via beforeSubmit callback');return this}this.trigger('form-submit-validate',[a,this,w,y]);if(y.veto){log('ajaxSubmit: submit vetoed via form-submit-validate trigger');return this}var q=$.param(a);if(w.type.toUpperCase()=='GET'){w.url+=(w.url.indexOf('?')>=0?'&':'?')+q;w.data=null}else{w.data=q}var z=[];if(w.resetForm){z.push(function(){$form.resetForm()})}if(w.clearForm){z.push(function(){$form.clearForm()})}if(!w.dataType&&w.target){var A=w.success||function(){};z.push(function(a){var b=w.replaceTarget?'replaceWith':'html';$(w.target)[b](a).each(A,arguments)})}else if(w.success){z.push(w.success)}w.success=function(a,b,c){var d=w.context||w;for(var i=0,max=z.length;i<max;i++){z[i].apply(d,[a,b,c||$form,$form])}};var B=$('input:file',this).length>0;var C='multipart/form-data';var D=($form.attr('enctype')==C||$form.attr('encoding')==C);if(w.iframe!==false&&(B||w.iframe||D)){if(w.closeKeepAlive){$.get(w.closeKeepAlive,function(){fileUpload(a)})}else{fileUpload(a)}}else{if($.browser.msie&&x=='get'){var E=$form[0].getAttribute('method');if(typeof E==='string')w.type=E}$.ajax(w)}this.trigger('form-submit-notify',[this,w]);return this;function fileUpload(a){var l=$form[0],el,i,s,g,id,$io,io,xhr,sub,n,timedOut,timeoutHandle;var m=!!$.fn.prop;if(a){for(i=0;i<a.length;i++){el=$(l[a[i].name]);el[m?'prop':'attr']('disabled',false)}}if($(':input[name=submit],:input[id=submit]',l).length){alert('Error: Form elements must not have name or id of "submit".');return}s=$.extend(true,{},$.ajaxSettings,w);s.context=s.context||s;id='jqFormIO'+(new Date().getTime());if(s.iframeTarget){$io=$(s.iframeTarget);n=$io.attr('name');if(n==null)$io.attr('name',id);else id=n}else{$io=$('<iframe name="'+id+'" src="'+s.iframeSrc+'" />');$io.css({position:'absolute',top:'-1000px',left:'-1000px'})}io=$io[0];xhr={aborted:0,responseText:null,responseXML:null,status:0,statusText:'n/a',getAllResponseHeaders:function(){},getResponseHeader:function(){},setRequestHeader:function(){},abort:function(a){var e=(a==='timeout'?'timeout':'aborted');log('aborting upload... '+e);this.aborted=1;$io.attr('src',s.iframeSrc);xhr.error=e;s.error&&s.error.call(s.context,xhr,e,a);g&&$.event.trigger("ajaxError",[xhr,s,e]);s.complete&&s.complete.call(s.context,xhr,e)}};g=s.global;if(g&&!$.active++){$.event.trigger("ajaxStart")}if(g){$.event.trigger("ajaxSend",[xhr,s])}if(s.beforeSend&&s.beforeSend.call(s.context,xhr,s)===false){if(s.global){$.active--}return}if(xhr.aborted){return}sub=l.clk;if(sub){n=sub.name;if(n&&!sub.disabled){s.extraData=s.extraData||{};s.extraData[n]=sub.value;if(sub.type=="image"){s.extraData[n+'.x']=l.clk_x;s.extraData[n+'.y']=l.clk_y}}}var o=1;var p=2;function getDoc(a){var b=a.contentWindow?a.contentWindow.document:a.contentDocument?a.contentDocument:a.document;return b}function doSubmit(){var t=$form.attr('target'),a=$form.attr('action');l.setAttribute('target',id);if(!x){l.setAttribute('method','POST')}if(a!=s.url){l.setAttribute('action',s.url)}if(!s.skipEncodingOverride&&(!x||/post/i.test(x))){$form.attr({encoding:'multipart/form-data',enctype:'multipart/form-data'})}if(s.timeout){timeoutHandle=setTimeout(function(){timedOut=true;cb(o)},s.timeout)}function checkState(){try{var a=getDoc(io).readyState;log('state = '+a);if(a.toLowerCase()=='uninitialized')setTimeout(checkState,50)}catch(e){log('Server abort: ',e,' (',e.name,')');cb(p);timeoutHandle&&clearTimeout(timeoutHandle);timeoutHandle=undefined}}var b=[];try{if(s.extraData){for(var n in s.extraData){b.push($('<input type="hidden" name="'+n+'" />').attr('value',s.extraData[n]).appendTo(l)[0])}}if(!s.iframeTarget){$io.appendTo('body');io.attachEvent?io.attachEvent('onload',cb):io.addEventListener('load',cb,false)}setTimeout(checkState,15);l.submit()}finally{l.setAttribute('action',a);if(t){l.setAttribute('target',t)}else{$form.removeAttr('target')}$(b).remove()}}if(s.forceSync){doSubmit()}else{setTimeout(doSubmit,10)}var q,doc,domCheckCount=50,callbackProcessed;function cb(e){if(xhr.aborted||callbackProcessed){return}try{doc=getDoc(io)}catch(ex){log('cannot access response document: ',ex);e=p}if(e===o&&xhr){xhr.abort('timeout');return}else if(e==p&&xhr){xhr.abort('server abort');return}if(!doc||doc.location.href==s.iframeSrc){if(!timedOut)return}io.detachEvent?io.detachEvent('onload',cb):io.removeEventListener('load',cb,false);var c='success',errMsg;try{if(timedOut){throw'timeout';}var d=s.dataType=='xml'||doc.XMLDocument||$.isXMLDoc(doc);log('isXml='+d);if(!d&&window.opera&&(doc.body==null||doc.body.innerHTML=='')){if(--domCheckCount){log('requeing onLoad callback, DOM not available');setTimeout(cb,250);return}}var f=doc.body?doc.body:doc.documentElement;xhr.responseText=f?f.innerHTML:null;xhr.responseXML=doc.XMLDocument?doc.XMLDocument:doc;if(d)s.dataType='xml';xhr.getResponseHeader=function(a){var b={'content-type':s.dataType};return b[a]};if(f){xhr.status=Number(f.getAttribute('status'))||xhr.status;xhr.statusText=f.getAttribute('statusText')||xhr.statusText}var h=s.dataType||'';var i=/(json|script|text)/.test(h.toLowerCase());if(i||s.textarea){var j=doc.getElementsByTagName('textarea')[0];if(j){xhr.responseText=j.value;xhr.status=Number(j.getAttribute('status'))||xhr.status;xhr.statusText=j.getAttribute('statusText')||xhr.statusText}else if(i){var k=doc.getElementsByTagName('pre')[0];var b=doc.getElementsByTagName('body')[0];if(k){xhr.responseText=k.textContent?k.textContent:k.innerHTML}else if(b){xhr.responseText=b.innerHTML}}}else if(s.dataType=='xml'&&!xhr.responseXML&&xhr.responseText!=null){xhr.responseXML=r(xhr.responseText)}try{q=v(xhr,s.dataType,s)}catch(e){c='parsererror';xhr.error=errMsg=(e||c)}}catch(e){log('error caught: ',e);c='error';xhr.error=errMsg=(e||c)}if(xhr.aborted){log('upload aborted');c=null}if(xhr.status){c=(xhr.status>=200&&xhr.status<300||xhr.status===304)?'success':'error'}if(c==='success'){s.success&&s.success.call(s.context,q,'success',xhr);g&&$.event.trigger("ajaxSuccess",[xhr,s])}else if(c){if(errMsg==undefined)errMsg=xhr.statusText;s.error&&s.error.call(s.context,xhr,c,errMsg);g&&$.event.trigger("ajaxError",[xhr,s,errMsg])}g&&$.event.trigger("ajaxComplete",[xhr,s]);if(g&&!--$.active){$.event.trigger("ajaxStop")}s.complete&&s.complete.call(s.context,xhr,c);callbackProcessed=true;if(s.timeout)clearTimeout(timeoutHandle);setTimeout(function(){if(!s.iframeTarget)$io.remove();xhr.responseXML=null},100)}var r=$.parseXML||function(s,a){if(window.ActiveXObject){a=new ActiveXObject('Microsoft.XMLDOM');a.async='false';a.loadXML(s)}else{a=(new DOMParser()).parseFromString(s,'text/xml')}return(a&&a.documentElement&&a.documentElement.nodeName!='parsererror')?a:null};var u=$.parseJSON||function(s){return window['eval']('('+s+')')};var v=function(a,b,s){var c=a.getResponseHeader('content-type')||'',xml=b==='xml'||!b&&c.indexOf('xml')>=0,q=xml?a.responseXML:a.responseText;if(xml&&q.documentElement.nodeName==='parsererror'){$.error&&$.error('parsererror')}if(s&&s.dataFilter){q=s.dataFilter(q,b)}if(typeof q==='string'){if(b==='json'||!b&&c.indexOf('json')>=0){q=u(q)}else if(b==="script"||!b&&c.indexOf("javascript")>=0){$.globalEval(q)}}return q}}};$.fn.ajaxForm=function(f){if(this.length===0){var o={s:this.selector,c:this.context};if(!$.isReady&&o.s){log('DOM not ready, queuing ajaxForm');$(function(){$(o.s,o.c).ajaxForm(f)});return this}log('terminating; zero elements found by selector'+($.isReady?'':' (DOM not ready)'));return this}return this.ajaxFormUnbind().bind('submit.form-plugin',function(e){if(!e.isDefaultPrevented()){e.preventDefault();$(this).ajaxSubmit(f)}}).bind('click.form-plugin',function(e){var a=e.target;var b=$(a);if(!(b.is(":submit,input:image"))){var t=b.closest(':submit');if(t.length==0){return}a=t[0]}var c=this;c.clk=a;if(a.type=='image'){if(e.offsetX!=undefined){c.clk_x=e.offsetX;c.clk_y=e.offsetY}else if(typeof $.fn.offset=='function'){var d=b.offset();c.clk_x=e.pageX-d.left;c.clk_y=e.pageY-d.top}else{c.clk_x=e.pageX-a.offsetLeft;c.clk_y=e.pageY-a.offsetTop}}setTimeout(function(){c.clk=c.clk_x=c.clk_y=null},100)})};$.fn.ajaxFormUnbind=function(){return this.unbind('submit.form-plugin click.form-plugin')};$.fn.formToArray=function(b){var a=[];if(this.length===0){return a}var c=this[0];var d=b?c.getElementsByTagName('*'):c.elements;if(!d){return a}var i,j,n,v,el,max,jmax;for(i=0,max=d.length;i<max;i++){el=d[i];n=el.name;if(!n){continue}if(b&&c.clk&&el.type=="image"){if(!el.disabled&&c.clk==el){a.push({name:n,value:$(el).val()});a.push({name:n+'.x',value:c.clk_x},{name:n+'.y',value:c.clk_y})}continue}v=$.fieldValue(el,true);if(v&&v.constructor==Array){for(j=0,jmax=v.length;j<jmax;j++){a.push({name:n,value:v[j]})}}else if(v!==null&&typeof v!='undefined'){a.push({name:n,value:v})}}if(!b&&c.clk){var e=$(c.clk),input=e[0];n=input.name;if(n&&!input.disabled&&input.type=='image'){a.push({name:n,value:e.val()});a.push({name:n+'.x',value:c.clk_x},{name:n+'.y',value:c.clk_y})}}return a};$.fn.formSerialize=function(a){return $.param(this.formToArray(a))};$.fn.fieldSerialize=function(b){var a=[];this.each(function(){var n=this.name;if(!n){return}var v=$.fieldValue(this,b);if(v&&v.constructor==Array){for(var i=0,max=v.length;i<max;i++){a.push({name:n,value:v[i]})}}else if(v!==null&&typeof v!='undefined'){a.push({name:this.name,value:v})}});return $.param(a)};$.fn.fieldValue=function(a){for(var b=[],i=0,max=this.length;i<max;i++){var c=this[i];var v=$.fieldValue(c,a);if(v===null||typeof v=='undefined'||(v.constructor==Array&&!v.length)){continue}v.constructor==Array?$.merge(b,v):b.push(v)}return b};$.fieldValue=function(b,c){var n=b.name,t=b.type,tag=b.tagName.toLowerCase();if(c===undefined){c=true}if(c&&(!n||b.disabled||t=='reset'||t=='button'||(t=='checkbox'||t=='radio')&&!b.checked||(t=='submit'||t=='image')&&b.form&&b.form.clk!=b||tag=='select'&&b.selectedIndex==-1)){return null}if(tag=='select'){var d=b.selectedIndex;if(d<0){return null}var a=[],ops=b.options;var e=(t=='select-one');var f=(e?d+1:ops.length);for(var i=(e?d:0);i<f;i++){var g=ops[i];if(g.selected){var v=g.value;if(!v){v=(g.attributes&&g.attributes['value']&&!(g.attributes['value'].specified))?g.text:g.value}if(e){return v}a.push(v)}}return a}return $(b).val()};$.fn.clearForm=function(){return this.each(function(){$('input,select,textarea',this).clearFields()})};$.fn.clearFields=$.fn.clearInputs=function(){var a=/^(?:color|date|datetime|email|month|number|password|range|search|tel|text|time|url|week)$/i;return this.each(function(){var t=this.type,tag=this.tagName.toLowerCase();if(a.test(t)||tag=='textarea'){this.value=''}else if(t=='checkbox'||t=='radio'){this.checked=false}else if(tag=='select'){this.selectedIndex=-1}})};$.fn.resetForm=function(){return this.each(function(){if(typeof this.reset=='function'||(typeof this.reset=='object'&&!this.reset.nodeType)){this.reset()}})};$.fn.enable=function(b){if(b===undefined){b=true}return this.each(function(){this.disabled=!b})};$.fn.selected=function(b){if(b===undefined){b=true}return this.each(function(){var t=this.type;if(t=='checkbox'||t=='radio'){this.checked=b}else if(this.tagName.toLowerCase()=='option'){var a=$(this).parent('select');if(b&&a[0]&&a[0].type=='select-one'){a.find('option').selected(false)}this.selected=b}})};function log(){var a='[jquery.form] '+Array.prototype.join.call(arguments,'');if(window.console&&window.console.log){window.console.log(a)}else if(window.opera&&window.opera.postError){window.opera.postError(a)}}})(jQuery);
@@ -0,0 +1 @@
1
+ (function($,h,j,k){$.fn.quicksearch=function(d,f){var g,cache,rowcache,jq_results,val='',e=this,options=$.extend({delay:100,selector:null,stripeRows:null,loader:null,noResults:'',bind:'keyup',onBefore:function(){return},onAfter:function(){return},show:function(){this.style.display=""},hide:function(){this.style.display="none"},prepareQuery:function(a){return a.toLowerCase().split(' ')},testQuery:function(a,b,c){for(var i=0;i<a.length;i+=1){if(b.indexOf(a[i])===-1){return false}}return true}},f);this.go=function(){var i=0,noresults=true,query=options.prepareQuery(val),val_empty=(val.replace(' ','').length===0);for(var i=0,len=rowcache.length;i<len;i++){if(val_empty||options.testQuery(query,cache[i],rowcache[i])){options.show.apply(rowcache[i]);noresults=false}else{options.hide.apply(rowcache[i])}}if(noresults){this.results(false)}else{this.results(true);this.stripe()}this.loader(false);options.onAfter();return this};this.stripe=function(){if(typeof options.stripeRows==="object"&&options.stripeRows!==null){var a=options.stripeRows.join(' ');var b=options.stripeRows.length;jq_results.not(':hidden').each(function(i){$(this).removeClass(a).addClass(options.stripeRows[i%b])})}return this};this.strip_html=function(a){var b=a.replace(new RegExp('<[^<]+\>','g'),"");b=$.trim(b.toLowerCase());return b};this.results=function(a){if(typeof options.noResults==="string"&&options.noResults!==""){if(a){$(options.noResults).hide()}else{$(options.noResults).show()}}return this};this.loader=function(a){if(typeof options.loader==="string"&&options.loader!==""){(a)?$(options.loader).show():$(options.loader).hide()}return this};this.cache=function(){jq_results=$(d);if(typeof options.noResults==="string"&&options.noResults!==""){jq_results=jq_results.not(options.noResults)}var t=(typeof options.selector==="string")?jq_results.find(options.selector):$(d).not(options.noResults);cache=t.map(function(){return e.strip_html(this.innerHTML)});rowcache=jq_results.map(function(){return this});return this.go()};this.trigger=function(){this.loader(true);options.onBefore();h.clearTimeout(g);g=h.setTimeout(function(){e.go()},options.delay);return this};this.cache();this.results(true);this.stripe();this.loader(false);return this.each(function(){$(this).bind(options.bind,function(){val=$(this).val();e.trigger()})})}}(jQuery,this,document));
@@ -0,0 +1,4 @@
1
+ // Copyright (c) 2010-11 Mickael Riga - See MIT_LICENCE for details
2
+ // Version 0.2.2
3
+ ;(function($){$.fn.underwood=function(l){var m={toolbar:"title paragraph bold italic link mailto unlink source",title_block:'<h3>',paragraph_block:'<p>',sanitize:true,css_href:null};var n=$.extend({},m,l);if(document.designMode||document.contentEditable){return this.each(function(){$(this).parents('form').unbind('submit.underwood').bind('submit.underwood',function(){$(this).find('iframe.underwood_iframe').each(function(){disable_design_mode(this,true)})});enable_design_mode($(this))})}function enable_design_mode(a){var b=document.createElement("iframe");b.frameBorder=0;b.frameMargin=0;b.framePadding=0;b.height=200;b.className="underwood_iframe";if(a.attr('name'))b.rel=a.attr('name');a.after(b);var c;if(n.css_href){c="<link rel='stylesheet' href='"+n.css_href+"' type='text/css' media='screen' charset='utf-8' />"}else{c=" <style type='text/css'> .underwood_frame_body {font-family:sans-serif;font-size:12px;margin:0;padding:10px;} .underwood_frame_body p {border:1px #DDD solid;padding:2px;} </style>"}var d=a.val();if($.trim(d)=='')d='<br>';var e="<html><head>"+c+"</head><body class='underwood_frame_body'>"+d+"</body></html>";try_enable_design_mode(b,e,function(){$(b).prevAll('.underwood_toolbar').remove().end().before(toolbar(b));a.remove()})}function try_enable_design_mode(a,b,c){try{a.contentWindow.document.open();a.contentWindow.document.write(b);a.contentWindow.document.close()}catch(error){console.log(error)}if(document.contentEditable){a.contentWindow.document.designMode="On";c();return true}else if(document.designMode!=null){try{a.contentWindow.document.designMode="on";c();return true}catch(error){console.log(error)}}setTimeout(function(){try_enable_design_mode(a,b,c)},250);return false}function toolbar(e){var f=$("<div class='underwood_toolbar'></div>");var g=n.toolbar.split(' ');for(var i in g){var h=g[i];var j=h.charAt(0).toUpperCase()+h.substr(1).toLowerCase();var k="<a href='#' class='underwood_btn underwood_btn_"+h+"' title='"+j+"'>"+j+"</a>";f.append($(k))}$('.underwood_btn_title',f).click(function(){exec_command(e,"formatblock",n.title_block);return false});$('.underwood_btn_paragraph',f).click(function(){exec_command(e,"formatblock",n.paragraph_block);return false});$('.underwood_btn_bold',f).click(function(){exec_command(e,'bold');return false});$('.underwood_btn_italic',f).click(function(){exec_command(e,'italic');return false});$('.underwood_btn_link',f).click(function(){var a=prompt("URL:","http://");var b=confirm('Open in a new window? (press cancel to open in the same window)')?'_blank':'_self';if(a){exec_command(e,'CreateLink',a);var c=e.contentWindow;var d=c.getSelection();d.focusNode.parentNode.target=b}return false});$('.underwood_btn_mailto',f).click(function(){var a='mailto:'+prompt("Email address:");if(a)exec_command(e,'CreateLink',a);return false});$('.underwood_btn_unlink',f).click(function(){exec_command(e,'unlink');return false});$('.underwood_btn_image',f).click(function(){var a=prompt("Image URL:");if(a)exec_command(e,'InsertImage',a);return false});$('.underwood_btn_source',f).click(function(){var a=disable_design_mode(e);var b=$("<a href='#' class='underwood_btn underwood_btn_back'>Back to Rich Text Editor</a>");f.empty().append(b);b.click(function(){enable_design_mode(a);return false}).hover(function(){$(this).css('opacity',0.6)},function(){$(this).css('opacity',1)});return false});$('.underwood_btn',f).hover(function(){$(this).css('opacity',0.6)},function(){$(this).css('opacity',1)});return f}function exec_command(a,b,c){a.contentWindow.focus();try{a.contentWindow.document.execCommand(b,false,c)}catch(e){console.log(e)}a.contentWindow.focus()}function sanitize_this(s){s=n.sanitize?s.replace(/(<\/?font[^>]*>|style=.[^'"]*['"])/g,''):s;return(s.match(/(>|^)[^<]+(<|$)/)||s.match(/<(object|iframe|img)/))?s:''};function disable_design_mode(a,b){var c=a.contentWindow.document.getElementsByTagName("body")[0].innerHTML;if(b==true)var d=$('<input type="hidden" />');else var d=$('<textarea cols="40" rows="10"></textarea>');d.val(sanitize_this(c));t=d.get(0);t.className="underwood_textarea_copy";if(a.rel)t.name=a.rel;$(a).before(d);if(b!=true)$(a).remove();return d}function create_targeted_link(a,b,c){selected=a.contentWindow.document.getSelection().getRangeAt(0);var d=a.contentWindow.document.createElement('a');d.href=b;d.target=c;if(selected.toString()==''){d.innerHTML=b;selected.insertNode(d)}else{selected.surroundContents(d)}}}})(jQuery);
4
+
@@ -0,0 +1,77 @@
1
+ <%# coding: UTF-8 %>
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
6
+ <link rel="stylesheet" href="<%= config[:path] %>/_public/css/plugin.asmselect.css" type="text/css" media="screen" charset="utf-8" />
7
+ <link rel="stylesheet" href="<%= config[:path] %>/_public/css/ui-darkness/jquery-ui-1.8.17.custom.css" type="text/css" media="screen" charset="utf-8" />
8
+ <link rel="stylesheet" href="<%= config[:path] %>/_public/css/main.css" type="text/css" media="screen" charset="utf-8" />
9
+ <title><%= config[:client_name] %> - Admin</title>
10
+ <script id='template-menu' type='text/html'>
11
+ <ul class='menu-list'>
12
+ {{#items}}
13
+ <li><a href='{{href}}' class='pushstack'>{{title}}</a></li>
14
+ {{/items}}
15
+ </ul>
16
+ </script>
17
+ <script id='template-nut-tree' type='text/html'>
18
+ {{#command_plus}}
19
+ <a href='<%= config[:path] %>/form/{{class_name}}{{query_string}}' class='btn plus-btn extra-plus pushstack'></a>
20
+ {{/command_plus}}
21
+ <ul class='nut-tree {{#sortable}}sortable{{/sortable}}'>
22
+ {{#items}}{{>nutshell}}{{/items}}
23
+ </ul>
24
+ </script>
25
+ <script id='template-nutshell' type='text/html'>
26
+ <li class='nutshell nutshell-{{class_name}}' id='{{class_name}}-{{id}}'>
27
+ <div class='nutshell-core'>
28
+ <div class='nutshell-bar {{#sortable}}sortable-handle{{/sortable}}'>
29
+
30
+ <a href='<%= config[:path] %>/{{class_name}}/{{id}}' class='btn cross-btn nutshell-delete float-right'></a>
31
+ </div>
32
+ <a href='<%= config[:path] %>/form/{{class_name}}/{{id}}' class='nutshell-edit pushstack'>
33
+ {{#thumb}}<img src='{{thumb}}' onerror="this.style.display='none'" />{{/thumb}}
34
+ {{^thumb}}<img src='/admin/_public/img/placeholder.png' onerror="this.style.display='none'" />{{/thumb}}
35
+ &nbsp;<span class='nutshell-title'>{{title}}</span>
36
+ </a>
37
+ <div class='nutshell-bottom'></div>
38
+ </div>
39
+ <ul class='nutshell-children'>
40
+ {{#children}}
41
+ <li><a href='<%= config[:path] %>/list/{{children_class_name}}?filter[{{foreign_key_name}}]={{id}}' class='btn further-btn further-bar pushstack'>{{title}} {{count}}</a></li>
42
+ {{/children}}
43
+ </ul>
44
+ </li>
45
+ </script>
46
+ </head>
47
+ <body>
48
+ <div id='trunk'>
49
+ <h1 id='page-title' class='shadowed'><%= config[:page_title] %></h1>
50
+ <div id='header' class='shadowed'>
51
+ <a href='#' class='btn back-btn float-left popstack'></a>
52
+ <a href='<%= config[:path] %>' class='btn home-btn float-right'></a>
53
+ <div id='search-wrap'><input type='text' id='search' placeholder='Search' /></div>
54
+ </div>
55
+ <noscript><div>In order to have this content management system working, you need to have javascript enabled.</div></noscript>
56
+ <div id='content'></div>
57
+ <div id='footer'>
58
+ &copy; Mickaël Riga<br>
59
+ <a href='https://github.com/mig-hub/populate-me' target='_blank'>Populate Me</a> is an open source project
60
+ </div>
61
+ </div>
62
+ <script type="text/javascript" charset="utf-8">
63
+ var admin_path = "<%= config[:path] %>";
64
+ </script>
65
+ <!-- // <script src='http://code.jquery.com/jquery-1.6.2.min.js' type='text/javascript'></script> -->
66
+ <script src='<%= config[:path] %>/_public/js/jquery.js' type='text/javascript'></script>
67
+ <script src='<%= config[:path] %>/_public/js/jquery.mustache.js' type='text/javascript'></script>
68
+ <script src='<%= config[:path] %>/_public/js/jquery-ui.js' type='text/javascript'></script>
69
+ <script src='<%= config[:path] %>/_public/js/addon.timepicker.js' type='text/javascript'></script>
70
+ <script src='<%= config[:path] %>/_public/js/plugin.form.js' type='text/javascript'></script>
71
+ <script src='<%= config[:path] %>/_public/js/plugin.underwood.js' type='text/javascript'></script>
72
+ <script src='<%= config[:path] %>/_public/js/plugin.asmselect.js' type='text/javascript'></script>
73
+ <script src='<%= config[:path] %>/_public/js/plugin.quicksearch.js' type='text/javascript'></script>
74
+ <script src='<%= config[:path] %>/_public/js/main.js' type='text/javascript'></script>
75
+ </body>
76
+ </html>
77
+
@@ -0,0 +1,193 @@
1
+ require 'rack/golem'
2
+ require 'json'
3
+
4
+ module PopulateMe
5
+ module Control
6
+ F = ::File
7
+ DIR = F.expand_path(F.dirname(__FILE__)+'/control')
8
+ BEFORE = proc{
9
+ }
10
+
11
+ def self.included(klass)
12
+ klass.class_eval do
13
+ include ::Rack::Golem
14
+ extend ClassMethods
15
+ end
16
+ klass.before(&BEFORE)
17
+ end
18
+
19
+ module ClassMethods
20
+ def new(*)
21
+ ::Rack::Static.new(::Rack::MethodOverride.new(super), :urls => ["/_public"], :root => DIR)
22
+ end
23
+ def config
24
+ @config ||= {
25
+ :client_name => 'Client Name',
26
+ :website_url => 'www.domain.com',
27
+ :page_title => 'Populate Me',
28
+ :path => '/admin',
29
+ :logout_path => '/admin/logout', # sometimes higher in stack
30
+ :menu => [['Home', '/admin']],
31
+ }
32
+ end
33
+ end
34
+
35
+ def index(class_name=nil, id=nil)
36
+ if class_name.nil?
37
+ @r.env['erb.location'] = DIR+'/views/'
38
+ erb :layout
39
+ else
40
+ http_method = @r.request_method.downcase
41
+ if ['post','put','delete'].include?(http_method)
42
+ @model_class_name, @id = class_name, id
43
+ build_model_vars
44
+ return if @res.status==404
45
+ @res['Content-Type'] = 'text/json'
46
+ __send__(http_method)
47
+ else
48
+ send_404
49
+ '' # Trick for the moment but it needs a throw for 404
50
+ end
51
+ end
52
+ end
53
+
54
+ def menu(*levels)
55
+ level_menu = config[:menu]
56
+ levels.each do |l|
57
+ level_menu = level_menu.assoc(l)[1]
58
+ end
59
+ items = level_menu.map do |i|
60
+ {
61
+ 'title'=>i[0].gsub(/-/, ' '),
62
+ 'href'=> i[1].kind_of?(String) ? i[1] : "#{config[:path]}/menu/#{levels.join('/')}/#{i[0]}"
63
+ }
64
+ end
65
+ page_title = levels.empty? ? config[:page_title] : levels.join(' / ').gsub(/-/, ' ')
66
+
67
+ @res['Content-Type'] = 'text/json'
68
+ JSON.generate({
69
+ 'action'=>'menu',
70
+ 'page_title'=>page_title,
71
+ 'items'=>items
72
+ })
73
+ end
74
+
75
+ def list(class_name)
76
+ @model_class_name = class_name
77
+ resolve_model_class
78
+ items = @model_class.find(@r['filter']||{}).map {|m| m.to_nutshell }
79
+
80
+ @res['Content-Type'] = 'text/json'
81
+ JSON.generate({
82
+ 'action'=> 'list',
83
+ 'page_title'=> @model_class.human_plural_name,
84
+ 'class_name'=> class_name,
85
+ 'sortable'=> @model_class.sortable_on_that_page?(@r),
86
+ 'command_plus'=> !@model_class.populate_config[:no_plus],
87
+ 'command_search'=> !@model_class.populate_config[:no_search],
88
+ 'items'=> items,
89
+ })
90
+ end
91
+
92
+ def form(class_name=nil, id=nil)
93
+ @model_class_name, @id = class_name, id
94
+ build_model_vars
95
+ return if @res.status==404
96
+ @model_instance ||= @model_class.backend_post
97
+ @model_instance.backend_put @r['model']
98
+ form = @model_instance.backend_form(@r.path.sub(/\/form/, ''), @r['fields'], :destination => @r['_destination'], :submit_text => @r['_submit_text'])
99
+
100
+ @res['Content-Type'] = 'text/json'
101
+ JSON.generate({
102
+ 'action'=> 'form',
103
+ 'page_title'=> @model_instance.backend_form_title,
104
+ 'form'=>form
105
+ })
106
+ end
107
+
108
+ private
109
+
110
+ def config
111
+ @config ||= self.class.config.update({
112
+ :path => @r.script_name,
113
+ :logout_path => "#{@r.script_name}/logout"
114
+ }).dup
115
+ end
116
+
117
+ def resolve_model_class
118
+ if @model_class_name.nil?
119
+ raise("No model class name provided")
120
+ end
121
+ unless ::Object.const_defined?(@model_class_name)
122
+ raise("#{@model_class_name} is not constant")
123
+ end
124
+ @model_class = Kernel.const_get(@model_class_name)
125
+ unless @model_class.kind_of?(PopulateMe::Mongo::BackendApiPlug::ClassMethods)
126
+ raise("Requested constant #{@model_class_name} is not a model class")
127
+ end
128
+ end
129
+
130
+ def build_model_vars
131
+ resolve_model_class
132
+ @model_instance = @model_class.backend_get(@id) unless @id.nil?
133
+ @clone_instance = @model_class.backend_get(@r['clone_id']) unless @r['clone_id'].nil?
134
+ unless @clone_instance.nil?
135
+ @r['fields'] ||= @clone_instance.cloning_backend_columns
136
+ @r['model'] = @clone_instance.backend_values.reject{|k,v| !@r['fields'].include?(k.to_s)}
137
+ end
138
+ @r['model'] ||= {}
139
+ send_404 if @model_instance.nil?&&!@id.nil?
140
+ end
141
+
142
+ def post
143
+ return put unless @id.nil?
144
+ @model_instance = @model_class.backend_post(@r['model'])
145
+ save_and_respond
146
+ end
147
+
148
+ def put
149
+ if @id.nil? && @r[@model_class_name]
150
+ @model_class.sort(@r[@model_class_name])
151
+ JSON.generate({'ids'=>@r[@model_class_name]})
152
+ else
153
+ @model_instance.backend_put(@r['model'])
154
+ save_and_respond
155
+ end
156
+ end
157
+
158
+ def delete
159
+ @model_instance.backend_delete
160
+ @r['_destination'].nil? ? @res.status=204 : @res.redirect(::Rack::Utils::unescape(@r['_destination'])) # 204 No Content
161
+ end
162
+
163
+ def save_and_respond
164
+ if @model_instance.backend_save?
165
+ if @r['_destination'].nil?
166
+ @res.status=201 # Created
167
+ JSON.generate({
168
+ 'action'=> 'save',
169
+ 'message'=> 'ok'
170
+ })
171
+ else
172
+ @res.redirect(::Rack::Utils::unescape(@r['_destination']))
173
+ end
174
+ else
175
+ form = @model_instance.backend_form(@r.path, @r['fields']||@r['model'].keys, :destination => @r['_destination'], :submit_text => @r['_submit_text'])
176
+ # Bad Request 400 does not give content anymore in safari/lion
177
+ # So I put 200 back until I find a better code
178
+ JSON.generate({
179
+ 'action'=> 'validation',
180
+ 'form'=>form
181
+ })
182
+ end
183
+ end
184
+
185
+ def send_404
186
+ @res.status=404 # Not Found
187
+ @res.headers['X-Cascade']='pass'
188
+ @res.write 'Not Found'
189
+ end
190
+
191
+ end
192
+ end
193
+
@@ -0,0 +1,77 @@
1
+ module PopulateMe
2
+ module Mongo
3
+ module BackendApiPlug
4
+
5
+ # This module adds a layer between the backend API and the models.
6
+ # It is legacy code and could probably be removed,
7
+ # but in a way it makes it easier to plug something else than MongoDB.
8
+ # I'll keep it and see.
9
+
10
+ def self.included(base)
11
+ base.extend(ClassMethods)
12
+ end
13
+
14
+ module ClassMethods
15
+ def backend_get(id); get(id=='unique' ? '000000000000000000000000' : id); end
16
+ def backend_post(doc=nil); inst = new(doc); inst.is_new = true; inst; end
17
+ end
18
+
19
+ # Instance Methods
20
+
21
+ def backend_delete; delete; end
22
+ def backend_put(fields); update_doc(fields); end
23
+ def backend_values; @doc; end
24
+ def backend_save?; !save.nil?; end
25
+ def backend_form(url, cols=nil, opts={})
26
+ cols ||= default_backend_columns
27
+ if block_given?
28
+ fields_list = ''
29
+ yield(fields_list)
30
+ else
31
+ fields_list = respond_to?(:crushyform) ? crushyform(cols) : backend_fields(cols)
32
+ end
33
+ o = "<form action='#{url}' method='POST' #{"enctype='multipart/form-data'" if fields_list.match(/type='file'/)} class='backend-form'>\n"
34
+ #o << backend_form_title unless block_given?
35
+ o << fields_list
36
+ opts[:method] = 'PUT' if (opts[:method].nil? && !self.new?)
37
+ o << "<input type='hidden' name='_method' value='#{opts[:method]}' />\n" unless opts[:method].nil?
38
+ o << "<input type='hidden' name='_destination' value='#{opts[:destination]}' />\n" unless opts[:destination].nil?
39
+ o << "<input type='hidden' name='_submit_text' value='#{opts[:submit_text]}' />\n" unless opts[:submit_text].nil?
40
+ o << "<input type='hidden' name='_no_wrap' value='#{opts[:no_wrap]}' />\n" unless opts[:no_wrap].nil?
41
+ cols.each do |c|
42
+ o << "<input type='hidden' name='fields[]' value='#{c}' />\n"
43
+ end
44
+ o << "<input type='submit' name='save' value='#{opts[:submit_text] || 'SAVE'}' />\n"
45
+ o << "</form>\n"
46
+ o
47
+ end
48
+ def backend_delete_form(url, opts={}); backend_form(url, [], {:submit_text=>'X', :method=>'DELETE'}.update(opts)){}; end
49
+ def backend_clone_form(url, opts={})
50
+ backend_form(url, [], {:submit_text=>'CLONE', :method=>'POST'}.update(opts)) do |out|
51
+ out << "<input type='hidden' name='clone_id' value='#{self.id}' />\n"
52
+ end
53
+ end
54
+ # Silly but usable form prototype
55
+ # Not really meant to be used in a real case
56
+ # It uses a textarea for everything
57
+ # Override it
58
+ # Or even better, use Sequel-Crushyform plugin instead
59
+ def backend_fields(cols)
60
+ o = ''
61
+ cols.each do |c|
62
+ identifier = "#{id}-#{self.class}-#{c}"
63
+ o << "<label for='#{identifier}'>#{c.to_s.capitalize}</label><br />\n"
64
+ o << "<textarea id='#{identifier}' name='model[#{c}]'>#{self[c]}</textarea><br />\n"
65
+ end
66
+ o
67
+ end
68
+ def backend_form_title; self.new? ? "New #{model.human_name}" : "Edit #{self.to_label}"; end
69
+ def backend_show; 'OK'; end
70
+
71
+ def default_backend_columns; model.schema.keys; end
72
+ def cloning_backend_columns; default_backend_columns.reject{|c| model.schema[c][:type]==:attachment}; end
73
+
74
+ end
75
+ end
76
+ end
77
+
@@ -0,0 +1,225 @@
1
+ module PopulateMe
2
+ module Mongo
3
+ module Crushyform
4
+
5
+ # This module adds to mutated models the ability
6
+ # to build forms using the settings of the schema
7
+
8
+ def self.included(base)
9
+ base.extend(ClassMethods)
10
+ end
11
+
12
+ module ClassMethods
13
+
14
+ def crushyform_types
15
+ @crushyform_types ||= {
16
+ :none => proc{''},
17
+ :string => proc do |m,c,o|
18
+ if o[:autocompleted]
19
+ values = o[:autocomplete_options] || m.class.collection.distinct(c)
20
+ js = <<-EOJS
21
+ <script type="text/javascript" charset="utf-8">
22
+ $(function(){
23
+ $( "##{m.field_id_for(c)}" ).autocomplete({source: ["#{values.join('","')}"]});
24
+ });
25
+ </script>
26
+ EOJS
27
+ end
28
+ tag = "<input type='%s' name='%s' value=\"%s\" id='%s' class='%s' %s />%s\n" % [o[:input_type]||'text', o[:input_name], o[:input_value], m.field_id_for(c), o[:input_class], o[:required]&&'required', o[:required]]
29
+ "#{tag}#{js}"
30
+ end,
31
+ :boolean => proc do |m,c,o|
32
+ crushid = m.field_id_for(c)
33
+ s = ['checked', nil]
34
+ s.reverse! unless o[:input_value]
35
+ out = "<span class='%s'>"
36
+ out += "<input type='radio' name='%s' value='true' id='%s' %s /> <label for='%s'>Yes</label> "
37
+ out += "<input type='radio' name='%s' value='false' id='%s-no' %s /> <label for='%s-no'>No</label>"
38
+ out += "</span>\n"
39
+ out % [o[:input_class], o[:input_name], crushid, s[0], crushid, o[:input_name], crushid, s[1], crushid]
40
+ end,
41
+ :text => proc do |m,c,o|
42
+ "<textarea name='%s' id='%s' class='%s' %s>%s</textarea>%s\n" % [o[:input_name], m.field_id_for(c), o[:input_class], o[:required]&&'required', o[:input_value], o[:required]]
43
+ end,
44
+ :date => proc do |m,c,o|
45
+ o[:input_value] = o[:input_value].strftime("%Y-%m-%d") if o[:input_value].respond_to?(:strftime)
46
+ o[:required] = "%s Format: yyyy-mm-dd" % [o[:required]]
47
+ crushyform_types[:string].call(m,c,o)
48
+ end,
49
+ :time => proc do |m,c,o|
50
+ o[:input_value] = o[:input_value].strftime("%T") if o[:input_value].respond_to?(:strftime)
51
+ o[:required] = "%s Format: hh:mm:ss" % [o[:required]]
52
+ crushyform_types[:string].call(m,c,o)
53
+ end,
54
+ :datetime => proc do |m,c,o|
55
+ o[:input_value] = o[:input_value].strftime("%Y-%m-%d %T") if o[:input_value].respond_to?(:strftime)
56
+ o[:required] = "%s Format: yyyy-mm-dd hh:mm:ss" % [o[:required]]
57
+ crushyform_types[:string].call(m,c,o)
58
+ end,
59
+ :parent => proc do |m,c,o|
60
+ parent_class = o[:parent_class].nil? ? Kernel.const_get(c.sub(/^id_/, '')) : m.resolve_class(o[:parent_class])
61
+ option_list = parent_class.to_dropdown(o[:input_value])
62
+ "<select name='%s' id='%s' class='%s'>%s</select>\n" % [o[:input_name], m.field_id_for(c), o[:input_class], option_list]
63
+ end,
64
+ :children => proc do |m,c,o|
65
+ children_class = o[:children_class].nil? ? Kernel.const_get(c.sub(/^ids_/, '')) : m.resolve_class(o[:children_class])
66
+ opts = o.update({
67
+ :multiple=>true,
68
+ :select_options=>children_class.dropdown_cache
69
+ })
70
+ @crushyform_types[:select].call(m,c,opts)
71
+ end,
72
+ :attachment => proc do |m,c,o|
73
+ deleter = "<input type='checkbox' name='#{o[:input_name]}' class='deleter' value='nil' /> Delete this file<br />" unless m.doc[c].nil?
74
+ "%s%s<input type='file' name='%s' id='%s' class='%s' />%s\n" % [m.to_thumb(c), deleter, o[:input_name], m.field_id_for(c), o[:input_class], o[:required]]
75
+ end,
76
+ :select => proc do |m,c,o|
77
+ out = "<select name='%s%s' id='%s' class='%s' %s title='-- Select --'>\n" % [o[:input_name], ('[]' if o[:multiple]), m.field_id_for(c), o[:input_class], ('multiple' if o[:multiple])]
78
+ o[:select_options] = m.__send__(o[:select_options]) unless o[:select_options].kind_of?(Array)
79
+ select_options = o[:select_options].dup
80
+ if (o[:multiple] && !o[:input_value].nil? && o[:input_value].size>1)
81
+ o[:input_value].reverse.each do |v|
82
+ elem = select_options.find{|x| x==v||(x||[])[1]==v }
83
+ select_options.unshift(select_options.delete(elem)) unless elem.nil?
84
+ end
85
+ end
86
+ if select_options.kind_of?(Array)
87
+ select_options.each do |op|
88
+ key,val = op.kind_of?(Array) ? [op[0],op[1]] : [op,op]
89
+ if key==:optgroup
90
+ out << "<optgroup label='%s'>\n" % [val]
91
+ elsif key==:closegroup
92
+ out << "</optgroup>\n"
93
+ else
94
+ selected = 'selected' if (val==o[:input_value] || (o[:input_value]||[]).include?(val))
95
+ out << "<option value='%s' %s>%s</option>\n" % [val,selected,key]
96
+ end
97
+ end
98
+ end
99
+ out << "</select>%s\n" % [o[:required]]
100
+ end,
101
+ :string_list => proc do |m,c,o|
102
+ if o[:autocompleted]
103
+ values = o[:autocomplete_options] || m.class.collection.distinct(c)
104
+ js = <<-EOJS
105
+ <script type="text/javascript" charset="utf-8">
106
+ $(function(){
107
+ $( "##{m.field_id_for(c)}" )
108
+ .bind( "keydown", function( event ) {
109
+ if ( event.keyCode === $.ui.keyCode.TAB &&
110
+ $( this ).data( "autocomplete" ).menu.active ) {
111
+ event.preventDefault();
112
+ }
113
+ })
114
+ .autocomplete({
115
+ minLength: 0,
116
+ source: function( request, response ) {
117
+ response($.ui.autocomplete.filter(["#{values.join('","')}"], request.term.split(/,\s*/).pop()));
118
+ },
119
+ focus: function() { return false; },
120
+ select: function( event, ui ) {
121
+ var terms = this.value.split(/,\s*/);
122
+ terms.pop();
123
+ terms.push(ui.item.value);
124
+ terms.push("");
125
+ this.value = terms.join( ", " );
126
+ return false;
127
+ }
128
+ });
129
+ });
130
+ </script>
131
+ EOJS
132
+ o[:autocompleted] = false # reset so that it does not autocomplete for :string type below
133
+ end
134
+ tag = @crushyform_types[:string].call(m,c,o.update({:input_value=>(o[:input_value]||[]).join(',')}))
135
+ "#{tag}#{js}"
136
+ end,
137
+ :permalink => proc do |instance, column_name, options|
138
+ values = "<option value=''>Or Browse the list</option>\n"
139
+ tag = @crushyform_types[:string].call(instance, column_name, options)
140
+ return tag if options[:permalink_classes].nil?
141
+ options[:permalink_classes].each do |sym|
142
+ c = Kernel.const_get sym
143
+ entries = c.collection.find
144
+ unless entries.count==0
145
+ values << "<optgroup label='#{c.human_name}'>\n"
146
+ entries.each_mutant do |e|
147
+ values << "<option value='#{e.permalink}' #{'selected' if e.permalink==options[:input_value]}>#{e.to_label}</option>\n"
148
+ end
149
+ values << "</optgroup>\n"
150
+ end
151
+ end
152
+ "#{tag}<br />\n<select name='__permalink' class='permalink-dropdown'>\n#{values}</select>\n"
153
+ end
154
+ }
155
+ end
156
+
157
+ # What represents a required field
158
+ # Can be overriden
159
+ def crushyfield_required; "<span class='crushyfield-required'> *</span>"; end
160
+ # Stolen from ERB
161
+ def html_escape(s)
162
+ s.to_s.gsub(/&/, "&amp;").gsub(/\"/, "&quot;").gsub(/>/, "&gt;").gsub(/</, "&lt;")
163
+ end
164
+ # Cache dropdown options for children classes to use
165
+ # Meant to be reseted each time an entry is created, updated or destroyed
166
+ # So it is only rebuild once required after the list has changed
167
+ # Maintaining an array and not rebuilding it all might be faster
168
+ # But it will not happen much so that it is fairly acceptable
169
+ def to_dropdown(selection=nil, nil_name='** UNDEFINED **')
170
+ dropdown_cache.inject("<option value=''>#{nil_name}</option>\n") do |out, row|
171
+ selected = 'selected' if row[1]==selection
172
+ "%s%s%s%s" % [out, row[2], selected, row[3]]
173
+ end
174
+ end
175
+ def dropdown_cache
176
+ @dropdown_cache ||= self.find({},:fields=>['_id',label_column]).inject([]) do |out,row|
177
+ out.push([row.to_label, row.id.to_s, "<option value='#{row.id}' ", ">#{row.to_label}</option>\n"])
178
+ end
179
+ end
180
+ def reset_dropdown_cache; @dropdown_cache = nil; end
181
+
182
+ end
183
+
184
+ # Instance Methods
185
+
186
+ def crushyform(columns=model.schema.keys, action=nil, meth='POST')
187
+ columns.delete('_id')
188
+ fields = columns.inject(""){|out,c|out+crushyfield(c)}
189
+ enctype = fields.match(/type='file'/) ? "enctype='multipart/form-data'" : ''
190
+ action.nil? ? fields : "<form action='%s' method='%s' %s>%s</form>\n" % [action, meth, enctype, fields]
191
+ end
192
+ # crushyfield is crushyinput but with label+error
193
+ def crushyfield(col, o={})
194
+ return '' if (o[:type]==:none || model.schema[col][:type]==:none)
195
+ default_field_name = col[/^id_/] ? Kernel.const_get(col.sub(/^id_/, '')).human_name : col.tr('_', ' ').capitalize
196
+ field_name = o[:name] || model.schema[col][:name] || default_field_name
197
+ error_list = errors_on(col).map{|e|" - #{e}"} if !errors_on(col).nil?
198
+ "<p class='crushyfield %s'><label for='%s'>%s</label><span class='crushyfield-error-list'>%s</span><br />\n%s</p>\n" % [error_list&&'crushyfield-error', field_id_for(col), field_name, error_list, crushyinput(col, o)]
199
+ end
200
+ def crushyinput(col, o={})
201
+ o = model.schema[col].dup.update(o)
202
+ o[:input_name] ||= "model[#{col}]"
203
+ o[:input_value] = o[:input_value].nil? ? self[col] : o[:input_value]
204
+ o[:input_value] = model.html_escape(o[:input_value]) if (o[:input_value].is_a?(String) && o[:html_escape]!=false)
205
+ o[:required] = o[:required]==true ? model.crushyfield_required : o[:required]
206
+ crushyform_type = model.crushyform_types[o[:type]] || model.crushyform_types[:string]
207
+ crushyform_type.call(self,col,o)
208
+ end
209
+ # Provide a thumbnail for the column
210
+ def to_thumb(c)
211
+ current = @doc[c]
212
+ if current.respond_to?(:[])
213
+ "<img src='/gridfs/#{@doc[c]['stash_thumb.gif']}' width='100' onerror=\"this.style.display='none'\" />\n"
214
+ end
215
+ end
216
+ # Reset dropdowns on hooks
217
+ def after_save; model.reset_dropdown_cache; super; end
218
+ def after_delete; model.reset_dropdown_cache; super; end
219
+ # Fix types
220
+ def fix_type_string_list(k,v); @doc[k] = v.to_s.strip.split(/\s*,\s*/).compact if v.is_a?(String); end
221
+
222
+
223
+ end
224
+ end
225
+ end