populate-me 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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