From d17456cd9d106479c9e03578626c9b45d207c01f Mon Sep 17 00:00:00 2001 From: Stuart Olivera Date: Thu, 30 May 2019 16:37:38 -0400 Subject: [PATCH 1/3] feat: Interactively customize CSS variables --- app/assets/javascripts/application.js | 1 + .../theming-editor/theming-editor.js | 112 ++++++++++++++++++ .../{manage/lib => vendor}/debounce.js | 18 +++ app/assets/javascripts/vendor/pickr.min.js | 3 + app/assets/stylesheets/application.sass | 5 +- .../stylesheets/general/_theming-editor.sass | 59 +++++++++ app/assets/stylesheets/vendor/pickr.min.css | 3 + app/controllers/manage/configs_controller.rb | 35 +++++- app/views/layouts/_theming_editor.html.haml | 15 +++ app/views/layouts/application.html.haml | 2 + app/views/manage/configs/index.html.haml | 5 + config/initializers/assets.rb | 2 +- config/routes.rb | 6 +- .../manage/configs_controller_test.rb | 36 ++++++ 14 files changed, 296 insertions(+), 6 deletions(-) create mode 100644 app/assets/javascripts/theming-editor/theming-editor.js rename app/assets/javascripts/{manage/lib => vendor}/debounce.js (55%) create mode 100755 app/assets/javascripts/vendor/pickr.min.js create mode 100644 app/assets/stylesheets/general/_theming-editor.sass create mode 100755 app/assets/stylesheets/vendor/pickr.min.css create mode 100644 app/views/layouts/_theming_editor.html.haml diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 913c2fbd5..1947e1fb1 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -17,6 +17,7 @@ //= require jquery.ui.autocomplete //= require highcharts //= require chartkick +//= require ./vendor/debounce //= require_directory . //= require_directory ./channels //= require ./vendor/simplemde.min.js diff --git a/app/assets/javascripts/theming-editor/theming-editor.js b/app/assets/javascripts/theming-editor/theming-editor.js new file mode 100644 index 000000000..470882911 --- /dev/null +++ b/app/assets/javascripts/theming-editor/theming-editor.js @@ -0,0 +1,112 @@ +//= require ../vendor/pickr.min.js + +(function() { + function _onTextChange() { + var $input = $(this); + var variable = $input.attr('name'); + var value = $input.val(); + + document.documentElement.style.setProperty(variable, value); + window.pickrs[variable].setColor(value, true); + } + + function _onPickrChange(color, instance) { + var value = color ? '#' + color.toHEXA().join('') : ''; + var $variable = $(instance.getRoot().root).parents('.theming-editor__variable'); + $input = $variable.find('.theming-editor__input'); + var variable = $input.attr('name'); + + // Don't update input if the user is typing in it + if (!$input.is(':focus')) { + $input.val(value); + } + // Update the picker button color (doesn't by default) + instance.applyColor(true); + // Update CSS property + document.documentElement.style.setProperty(variable, value); + } + + function onSave() { + var $editor = $(this).parents('.theming-editor__wrapper'); + var css = ':root {\n'; + $editor.find('.theming-editor__input').each(function() { + var variable = $(this).attr('name'); + var value = $(this).val(); + css += ' ' + variable + ': ' + value + ';\n'; + }); + css += '}'; + var data = { + hackathon_config: { + custom_css: css, + }, + }; + fetch('/manage/configs/custom_css', { + method: 'PATCH', + redirect: 'manual', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': Rails.csrfToken(), + }, + body: JSON.stringify(data), + }) + .then(function(response) { + if (!response.ok && response.type != 'opaqueredirect') { + alert('There was an error attempting to save. Please try again or refresh the page.'); + } else { + window.location.replace('/manage/configs/exit_theming_editor'); + } + }) + .catch(function() { + alert('There was an error attempting to save. Please try again or refresh the page.'); + }); + } + + var onTextChange = throttle(_onTextChange, 100); + var onPickrChange = throttle(_onPickrChange, 100); + + function init() { + var variables = Object.values(getComputedStyle(document.documentElement)) + .filter(x => x.startsWith('--primary')) + .sort(); + var $editor = $('#theming-editor'); + var $variables = $editor.find('.theming-editor__variables'); + var $variable_template = $editor.find('.theming-editor__variable').remove(); + variables.forEach(function(variable) { + var $new_variable = $variable_template.clone(); + var $code = $new_variable.find('.theming-editor__variable-name-code'); + var $input = $new_variable.find('.theming-editor__input'); + var value = getComputedStyle(document.documentElement) + .getPropertyValue(variable) + .trim(); + + $code.text(variable); + $input.val(value); + $input.attr('name', variable); + $input.on('input', onTextChange); + $variables.append($new_variable); + }); + + window.pickrs = {}; + $('.theming-editor__color-picker').each(function() { + var $input = $(this) + .parents('.theming-editor__variable') + .find('.theming-editor__input'); + var pickr = Pickr.create({ + el: this, + default: $input.val(), + components: { + opacity: false, + hue: true, + interaction: false, + }, + }); + pickr.on('change', onPickrChange); + var variable = $input.attr('name'); + window.pickrs[variable] = pickr; + }); + + $editor.find('.theming-editor__button-save').on('click', onSave); + } + + document.addEventListener('turbolinks:load', init); +})(); diff --git a/app/assets/javascripts/manage/lib/debounce.js b/app/assets/javascripts/vendor/debounce.js similarity index 55% rename from app/assets/javascripts/manage/lib/debounce.js rename to app/assets/javascripts/vendor/debounce.js index 039a3a687..0dc36d63f 100644 --- a/app/assets/javascripts/manage/lib/debounce.js +++ b/app/assets/javascripts/vendor/debounce.js @@ -18,3 +18,21 @@ function debounce(func, wait, immediate) { if (callNow) func.apply(context, args); }; } + +// Returns a function, that, as long as it continues to be invoked, will only +// trigger every N milliseconds. If `immediate` is passed, trigger the +// function on the leading edge, instead of the trailing. +function throttle(func, wait, immediate) { + var timeout; + return function() { + var context = this, + args = arguments; + var later = function() { + timeout = null; + if (!immediate) func.apply(context, args); + }; + var callNow = immediate && !timeout; + if (!timeout) timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + }; +} diff --git a/app/assets/javascripts/vendor/pickr.min.js b/app/assets/javascripts/vendor/pickr.min.js new file mode 100755 index 000000000..c93b2b877 --- /dev/null +++ b/app/assets/javascripts/vendor/pickr.min.js @@ -0,0 +1,3 @@ +/*! Pickr 0.6.1 MIT | https://github.com/Simonwep/pickr */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.Pickr=e():t.Pickr=e()}(window,function(){return function(t){var e={};function n(o){if(e[o])return e[o].exports;var i=e[o]={i:o,l:!1,exports:{}};return t[o].call(i.exports,i,i.exports,n),i.l=!0,i.exports}return n.m=t,n.c=e,n.d=function(t,e,o){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:o})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var i in t)n.d(o,i,function(e){return t[e]}.bind(null,i));return o},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=1)}([function(t,e,n){},function(t,e,n){"use strict";n.r(e);var o={};n.r(o),n.d(o,"once",function(){return s}),n.d(o,"on",function(){return a}),n.d(o,"off",function(){return c}),n.d(o,"createElementFromString",function(){return p}),n.d(o,"removeAttribute",function(){return u}),n.d(o,"createFromTemplate",function(){return h}),n.d(o,"eventPath",function(){return d}),n.d(o,"adjustableInputNumbers",function(){return f});n(0);function i(t){for(var e=1;ea(t,e,function t(){n.apply(this,arguments),this.removeEventListener(e,t)},o),a=l.bind(null,"addEventListener"),c=l.bind(null,"removeEventListener");function l(t,e,n,o){let r=arguments.length>4&&void 0!==arguments[4]?arguments[4]:{};e instanceof HTMLCollection||e instanceof NodeList?e=Array.from(e):Array.isArray(e)||(e=[e]),Array.isArray(n)||(n=[n]);for(const s of e)for(const e of n)s[t](e,o,i({capture:!1},r));return Array.prototype.slice.call(arguments,1)}function p(t){const e=document.createElement("div");return e.innerHTML=t.trim(),e.firstElementChild}function u(t,e){const n=t.getAttribute(e);return t.removeAttribute(e),n}function h(t){return function t(e){let n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const o=u(e,"data-con"),i=u(e,"data-key");i&&(n[i]=e);const r=o?n[o]={}:n;for(let n of Array.from(e.children)){const e=u(n,"data-arr");e?(r[e]||(r[e]=[])).push(n):t(n,r)}return n}(p(t))}function d(t){let e=t.path||t.composedPath&&t.composedPath();if(e)return e;let n=t.target.parentElement;for(e=[t.target,n];n=n.parentElement;)e.push(n);return e.push(document,window),e}function f(t){let e=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];const n=t=>t>="0"&&t<="9"||"-"===t||"."===t;function o(o){const i=t.value,r=t.selectionStart;let s=r,a="";for(let t=r-1;t>0&&n(i[t]);t--)a=i[t]+a,s--;for(let t=r,e=i.length;t0&&!isNaN(a)&&isFinite(a)){const n=o.deltaY<0?1:-1,r=[1,10,100][Number(o.shiftKey||2*o.ctrlKey)]*n;let c=Number(a)+r;!e&&c<0&&(c=0);const l=i.substr(0,s)+c+i.substring(s+a.length,i.length),p=s+String(c).length;t.value=l,t.focus(),t.setSelectionRange(p,p)}o.preventDefault(),t.dispatchEvent(new Event("input"))}a(t,"focus",()=>a(window,"wheel",o,{passive:!1})),a(t,"blur",()=>c(window,"wheel",o))}function v(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=[],o=!0,i=!1,r=void 0;try{for(var s,a=t[Symbol.iterator]();!(o=(s=a.next()).done)&&(n.push(s.value),!e||n.length!==e);o=!0);}catch(t){i=!0,r=t}finally{try{o||null==a.return||a.return()}finally{if(i)throw r}}return n}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}const m=Math.min,y=Math.max,g=Math.floor,b=Math.round;function _(t,e,n){e/=100,n/=100;let o=g(t=t/360*6),i=t-o,r=n*(1-e),s=n*(1-i*e),a=n*(1-(1-i)*e),c=o%6;return[255*[n,s,r,r,a,n][c],255*[a,n,n,s,r,r][c],255*[r,r,a,n,n,s][c]]}function w(t,e,n){let o,i,r;const s=m(t/=255,e/=255,n/=255),a=y(t,e,n),c=a-s;if(0===c)o=i=0;else{i=c/a;let r=((a-t)/6+c/2)/c,s=((a-e)/6+c/2)/c,l=((a-n)/6+c/2)/c;t===a?o=l-s:e===a?o=1/3+r-l:n===a&&(o=2/3+s-r),o<0?o+=1:o>1&&(o-=1)}return[360*o,100*i,100*(r=a)]}function k(t,e,n,o){return e/=100,n/=100,[...w(255*(1-m(1,(t/=100)*(1-(o/=100))+o)),255*(1-m(1,e*(1-o)+o)),255*(1-m(1,n*(1-o)+o)))]}function A(t,e,n){return e/=100,[t,2*(e*=(n/=100)<.5?n:1-n)/(n+e)*100,100*(n+e)]}function C(t){return w(...t.match(/.{2}/g).map(t=>parseInt(t,16)))}function S(t){t=t.match(/^[a-zA-Z]+$/)?function(t){const e=document.createElement("canvas").getContext("2d");return e.fillStyle=t,e.fillStyle}(t):t;const e={cmyk:/^cmyk[\D]+(\d+)[\D]+(\d+)[\D]+(\d+)[\D]+(\d+)/i,rgba:/^(rgb|rgba)[\D]+(\d+)[\D]+(\d+)[\D]+(\d+)[\D]*?([\d.]+|$)/i,hsla:/^(hsl|hsla)[\D]+(\d+)[\D]+(\d+)[\D]+(\d+)[\D]*?([\d.]+|$)/i,hsva:/^(hsv|hsva)[\D]+(\d+)[\D]+(\d+)[\D]+(\d+)[\D]*?([\d.]+|$)/i,hex:/^#?(([\dA-Fa-f]{3,4})|([\dA-Fa-f]{6})|([\dA-Fa-f]{8}))$/i},n=t=>t.map(t=>/^(|\d+)\.\d+|\d+$/.test(t)?Number(t):void 0);let o;for(const s in e)if(o=e[s].exec(t))switch(s){case"cmyk":{let t=v(n(o),5),e=t[1],i=t[2],r=t[3],a=t[4];if(e>100||i>100||r>100||a>100)break;return{values:[...k(e,i,r,a),1],type:s}}case"rgba":{let t=v(n(o),6),e=t[2],i=t[3],r=t[4],a=t[5],c=void 0===a?1:a;if(e>255||i>255||r>255||c<0||c>1)break;return{values:[...w(e,i,r),c],type:s}}case"hex":{const t=(t,e)=>[t.substring(0,e),t.substring(e,t.length)];let e,n=v(o,2)[1];if(3===n.length?n+="F":6===n.length&&(n+="FF"),4===n.length){var i=v(t(n,3).map(t=>t+t),2);n=i[0],e=i[1]}else if(8===n.length){var r=v(t(n,6),2);n=r[0],e=r[1]}return e=parseInt(e,16)/255,{values:[...C(n),e],type:s}}case"hsla":{let t=v(n(o),6),e=t[2],i=t[3],r=t[4],a=t[5],c=void 0===a?1:a;if(e>360||i>100||r>100||c<0||c>1)break;return{values:[...A(e,i,r),c],type:s}}case"hsva":{let t=v(n(o),6),e=t[2],i=t[3],r=t[4],a=t[5],c=void 0===a?1:a;if(e>360||i>100||r>100||c<0||c>1)break;return{values:[e,i,r,c],type:s}}}return{values:null,type:null}}function O(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:1;const i=Math.ceil,r={h:t,s:e,v:n,a:o,toHSVA(){const t=[r.h,r.s,r.v],e=t.map(i);return t.push(r.a),t.toString=(()=>"hsva(".concat(e[0],", ").concat(e[1],"%, ").concat(e[2],"%, ").concat(r.a.toFixed(1),")")),t},toHSLA(){const t=function(t,e,n){let o=(2-(e/=100))*(n/=100)/2;return 0!==o&&(e=1===o?0:o<.5?e*n/(2*o):e*n/(2-2*o)),[t,100*e,100*o]}(r.h,r.s,r.v),e=t.map(i);return t.push(r.a),t.toString=(()=>"hsla(".concat(e[0],", ").concat(e[1],"%, ").concat(e[2],"%, ").concat(r.a.toFixed(1),")")),t},toRGBA(){const t=_(r.h,r.s,r.v),e=t.map(i);return t.push(r.a),t.toString=(()=>"rgba(".concat(e[0],", ").concat(e[1],", ").concat(e[2],", ").concat(r.a.toFixed(1),")")),t},toCMYK(){const t=function(t,e,n){const o=_(t,e,n),i=o[0]/255,r=o[1]/255,s=o[2]/255;let a,c,l,p;return[100*(c=1===(a=m(1-i,1-r,1-s))?0:(1-i-a)/(1-a)),100*(l=1===a?0:(1-r-a)/(1-a)),100*(p=1===a?0:(1-s-a)/(1-a)),100*a]}(r.h,r.s,r.v),e=t.map(i);return t.toString=(()=>"cmyk(".concat(e[0],"%, ").concat(e[1],"%, ").concat(e[2],"%, ").concat(e[3],"%)")),t},toHEXA(){const t=(e=r.h,n=r.s,o=r.v,_(e,n,o).map(t=>b(t).toString(16).padStart(2,"0")));var e,n,o;return t.toString=(()=>{const e=r.a>=1?"":Number((255*r.a).toFixed(0).toString(16).toUpperCase(),2,"0");return"#".concat(t.join("").toUpperCase()+e)}),t},clone:()=>O(r.h,r.s,r.v,r.a)};return r}function x(t){const e={options:Object.assign({lockX:!1,lockY:!1,onchange:()=>0},t),_tapstart(t){a(document,["mouseup","touchend","touchcancel"],e._tapstop),a(document,["mousemove","touchmove"],e._tapmove),t.preventDefault(),e._tapmove(t)},_tapmove(t){const n=e.options,o=e.cache,i=n.element,r=e.options.wrapper.getBoundingClientRect();let s=0,a=0;if(t){const e=t&&t.touches&&t.touches[0];s=t?(e||t).clientX:0,a=t?(e||t).clientY:0,sr.left+r.width&&(s=r.left+r.width),ar.top+r.height&&(a=r.top+r.height),s-=r.left,a-=r.top}else o&&(s=o.x*r.width,a=o.y*r.height);n.lockX||(i.style.left="calc(".concat(s/r.width*100,"% - ").concat(i.offsetWidth/2,"px)")),n.lockY||(i.style.top="calc(".concat(a/r.height*100,"% - ").concat(i.offsetWidth/2,"px)")),e.cache={x:s/r.width,y:a/r.height},n.onchange(s,a)},_tapstop(){c(document,["mouseup","touchend","touchcancel"],e._tapstop),c(document,["mousemove","touchmove"],e._tapmove)},trigger(){e._tapmove()},update(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;const o=e.options.wrapper.getBoundingClientRect();e._tapmove({clientX:o.left+t,clientY:o.top+n})},destroy(){const t=e.options,n=e._tapstart;c([t.wrapper,t.element],"mousedown",n),c([t.wrapper,t.element],"touchstart",n,{passive:!1})}},n=e.options,o=e._tapstart;return a([n.wrapper,n.element],"mousedown",o),a([n.wrapper,n.element],"touchstart",o,{passive:!1}),e}function E(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};t=Object.assign({onchange:()=>0,className:"",elements:[]},t);const e=a(t.elements,"click",e=>{t.elements.forEach(n=>n.classList[e.target===n?"add":"remove"](t.className)),t.onchange(e)});return{destroy:()=>c(...e)}}function j(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var n=[],o=!0,i=!1,r=void 0;try{for(var s,a=t[Symbol.iterator]();!(o=(s=a.next()).done)&&(n.push(s.value),!e||n.length!==e);o=!0);}catch(t){i=!0,r=t}finally{try{o||null==a.return||a.return()}finally{if(i)throw r}}return n}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var B=t=>{let e=t.components,n=t.strings,o=t.useAsButton,i=t.inline,r=t.appClass;const s=t=>t?"":'style="display:none" hidden',a=h('\n
\n\n '.concat(o?"":'','\n\n
\n
\n
\n \n
\n
\n\n
\n
\n
\n
\n\n
\n
\n
\n
\n\n
\n
\n
\n
\n
\n\n
\n\n
\n \n\n \n \n \n \n \n\n \n \n
\n
\n
\n ")),c=a.interaction;return c.options.find(t=>!t.hidden&&!t.classList.add("active")),c.type=(()=>c.options.find(t=>t.classList.contains("active"))),a};function L(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}class P{constructor(t){L(this,"_initializingActive",!0),L(this,"_recalc",!0),L(this,"_color",O()),L(this,"_lastColor",O()),L(this,"_swatchColors",[]),L(this,"_eventListener",{swatchselect:[],change:[],save:[],init:[]}),this.options=t=Object.assign({appClass:null,useAsButton:!1,disabled:!1,comparison:!0,components:{interaction:{}},strings:{},swatches:null,inline:!1,default:"#42445A",defaultRepresentation:null,position:"bottom-middle",adjustableNumbers:!0,showAlways:!1,closeWithKey:"Escape"},t);const e=t,n=e.swatches,o=e.inline,i=e.components,r=e.position;i.interaction||(i.interaction={});const s=i.preview,a=i.opacity,c=i.hue,l=i.palette;i.palette=l||s||a||c,o&&(t.showAlways=!0),this._preBuild(),this._buildComponents(),this._bindEvents(),this._finalBuild(),n&&n.length&&n.forEach(t=>this.addSwatch(t));const p=this._root.button,u=this;requestAnimationFrame(function e(){if(null===p.offsetParent&&p!==document.body)return requestAnimationFrame(e);u.setColor(t.default),u._nanopop=function(t){let e=t.el,n=t.reference,o=t.pos,i=t.padding,r=void 0===i?8:i;const s={start:"sme",middle:"mse",end:"ems"},a={top:"tb",right:"rl",bottom:"bt",left:"lr"},c=j(o.split("-"),2),l=c[0],p=c[1],u=void 0===p?"middle":p,h="top"===l||"bottom"===l;return{update(){const t=n.getBoundingClientRect(),o=e.getBoundingClientRect(),i=h?{t:t.top-o.height-r,b:t.bottom+r}:{r:t.right+r,l:t.left-o.width-r},c=h?{s:t.left+t.width-o.width,m:-o.width/2+(t.left+t.width/2),e:t.left}:{s:t.bottom-o.height,m:t.bottom-t.height/2-o.height/2,e:t.bottom-t.height};function p(t,n,i){const r="top"===i,s=r?o.height:o.width,a=window[r?"innerHeight":"innerWidth"];for(const o of t){const t=n[o];if(t>0&&t+s>/g).reduce((t,e,n,o)=>(t=t.querySelector(e),nthis._clearColor()),a(t.preview.lastColor,"click",()=>this.setHSVA(...this._lastColor.toHSVA())),a(t.interaction.save,"click",()=>{!this.applyColor()&&!e.showAlways&&this.hide()}),a(t.interaction.result,["keyup","input"],t=>{this._recalc=!1,this.setColor(t.target.value,!0)&&!this._initializingActive&&this._emit("change",this._color),t.stopImmediatePropagation()}),a([t.palette.palette,t.palette.picker,t.hue.slider,t.hue.picker,t.opacity.slider,t.opacity.picker],["mousedown","touchstart"],()=>this._recalc=!0)];if(!e.showAlways){const o=e.closeWithKey;n.push(a(t.button,"click",()=>this.isOpen()?this.hide():this.show()),a(document,"keyup",t=>this.isOpen()&&(t.key===o||t.code===o)&&this.hide()),a(document,["touchstart","mousedown"],e=>{this.isOpen()&&!d(e).some(e=>e===t.app||e===t.button)&&this.hide()},{capture:!0}))}if(e.adjustableNumbers&&f(t.interaction.result,!1),!e.inline){let t=null;const e=this;n.push(a(window,["scroll","resize"],()=>{e.isOpen()&&(null===t?(t=setTimeout(()=>t=null,100),requestAnimationFrame(function n(){e._rePositioningPicker(),null!==t&&requestAnimationFrame(n)})):(clearTimeout(t),t=setTimeout(()=>t=null,100)))}))}this._eventBindings=n}_rePositioningPicker(){this.options.inline||this._nanopop.update()}_updateOutput(){if(this._root.interaction.type()){const t="to".concat(this._root.interaction.type().getAttribute("data-type"));this._root.interaction.result.value="function"==typeof this._color[t]?this._color[t]().toString():""}this._initializingActive||this._emit("change",this._color)}_clearColor(){const t=this._root,e=this.options;e.useAsButton||(t.button.style.color="rgba(0, 0, 0, 0.15)"),t.button.classList.add("clear"),e.showAlways||this.hide(),this._initializingActive||this._emit("save",null)}_emit(t){for(var e=arguments.length,n=new Array(e>1?e-1:0),o=1;ot(...n,this))}on(t,e){return"function"==typeof e&&"string"==typeof t&&t in this._eventListener&&this._eventListener[t].push(e),this}off(t,e){const n=this._eventListener[t];if(n){const t=n.indexOf(e);~t&&n.splice(t,1)}return this}addSwatch(t){const e=S(t).values;if(e){const t=this._swatchColors,n=this._root,o=O(...e),i=p(''));return n.swatches.appendChild(i),t.push({element:i,hsvaColorObject:o}),this._eventBindings.push(a(i,"click",()=>{this.setHSVA(...o.toHSVA(),!0),this._emit("swatchselect",o)})),!0}return!1}removeSwatch(t){if("number"==typeof t){const e=this._swatchColors[t];if(e){const n=e.element;return this._root.swatches.removeChild(n),this._swatchColors.splice(t,1),!0}}return!1}applyColor(){let t=arguments.length>0&&void 0!==arguments[0]&&arguments[0];const e=this._root,n=e.preview,o=e.button,i=this._color.toRGBA().toString();n.lastColor.style.color=i,this.options.useAsButton||(o.style.color=i),o.classList.remove("clear"),this._lastColor=this._color.clone(),this._initializingActive||t||this._emit("save",this._color)}destroy(){this._eventBindings.forEach(t=>c(...t)),Object.keys(this.components).forEach(t=>this.components[t].destroy())}destroyAndRemove(){this.destroy();const t=this._root.root;t.parentElement.removeChild(t);const e=this._root.app;e.parentElement.removeChild(e);const n=this;Object.keys(n).forEach(t=>n[t]=null)}hide(){return this._root.app.classList.remove("visible"),this}show(){if(!this.options.disabled)return this._root.app.classList.add("visible"),this._rePositioningPicker(),this}isOpen(){return this._root.app.classList.contains("visible")}setHSVA(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:360,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:1,i=arguments.length>4&&void 0!==arguments[4]&&arguments[4];const r=this._recalc;if(this._recalc=!1,t<0||t>360||e<0||e>100||n<0||n>100||o<0||o>1)return!1;const s=this.components,a=s.hue,c=s.opacity,l=s.palette,p=a.options.wrapper.offsetHeight*(t/360);a.update(0,p);const u=c.options.wrapper.offsetHeight*o;c.update(0,u);const h=l.options.wrapper,d=h.offsetWidth*(e/100),f=h.offsetHeight*(1-n/100);return l.update(d,f),this._color=O(t,e,n,o),this._recalc=r,this._recalc&&this._updateOutput(),i||this.applyColor(),!0}setColor(t){let e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(null===t)return this._clearColor(),!0;const n=S(t),o=n.values,i=n.type;if(o){const t=i.toUpperCase(),n=this._root.interaction.options,r=n.find(e=>e.getAttribute("data-type").startsWith(t));if(r&&!r.hidden)for(const t of n)t.classList[t===r?"add":"remove"]("active");return this.setHSVA(...o,e)}}setColorRepresentation(t){return t=t.toUpperCase(),!!this._root.interaction.options.find(e=>e.getAttribute("data-type").startsWith(t)&&!e.click())}getColorRepresentation(){return this._representation}getColor(){return this._color}getRoot(){return this._root}disable(){return this.hide(),this.options.disabled=!0,this._root.button.classList.add("disabled"),this}enable(){return this.options.disabled=!1,this._root.button.classList.remove("disabled"),this}}P.utils=o,P.create=(t=>new P(t)),P.version="0.6.1";e.default=P}]).default}); +//# sourceMappingURL=pickr.min.js.map \ No newline at end of file diff --git a/app/assets/stylesheets/application.sass b/app/assets/stylesheets/application.sass index ade6eb6bc..bf1d19b10 100644 --- a/app/assets/stylesheets/application.sass +++ b/app/assets/stylesheets/application.sass @@ -21,13 +21,16 @@ @import general/flashes @import general/main @import general/header -@import general/table @import general/status-colors +@import general/table @import forms/forms @import forms/confirmation +@import general/theming-editor + // media queries needs to be last @import general/media-queries // external dependencies @import font-awesome +@import vendor/pickr.min diff --git a/app/assets/stylesheets/general/_theming-editor.sass b/app/assets/stylesheets/general/_theming-editor.sass new file mode 100644 index 000000000..4670e900b --- /dev/null +++ b/app/assets/stylesheets/general/_theming-editor.sass @@ -0,0 +1,59 @@ +.theming-editor__wrapper + position: fixed + bottom: 10px + right: 10px + display: block + background: #fff + border: 3px solid #ccc + padding: 15px 20px + border-radius: 4px + max-width: 470px + max-height: 700px + overflow: auto + +.theming-editor__title + font-weight: bold + margin-bottom: 1em + font-size: 16px + +.theming-editor__variables + display: flex + flex-flow: row wrap + margin: -10px -10px 10px + +.theming-editor__variable + display: flex + flex-flow: column nowrap + margin: 10px + width: 200px + +.theming-editor__variable-name + color: #e83e8c + margin-bottom: 5px + font-size: 11px + +.theming-editor__input-group + display: flex + flex-flow: row nowrap + align-items: stretch + +.theming-editor__input-group .pickr + margin-left: 5px + display: flex + width: 80px + button + box-shadow: 0 3px 4px rgba(0,0,0,0.1) + display: flex + height: 100% + width: 100% + +input, textarea + &.theming-editor__input, &.theming-editor__input:focus + font-size: 12px + font-family: monospace + padding: 6px 9px + border: 1px solid #ccc + margin: 0 + +.theming-editor__button-wrapper + margin-top: 1em diff --git a/app/assets/stylesheets/vendor/pickr.min.css b/app/assets/stylesheets/vendor/pickr.min.css new file mode 100755 index 000000000..0fcebff2b --- /dev/null +++ b/app/assets/stylesheets/vendor/pickr.min.css @@ -0,0 +1,3 @@ +/*! Pickr 0.6.1 MIT | https://github.com/Simonwep/pickr */ +.pickr{position:relative;overflow:visible;transform:translateY(0)}.pickr *{box-sizing:border-box}.pickr .pcr-button{position:relative;height:2em;width:2em;padding:.5em;cursor:pointer;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;border-radius:.15em;background:url('data:image/svg+xml;utf8, ') no-repeat 50%;background-size:0;transition:all .3s}.pickr .pcr-button:before{background:url('data:image/svg+xml;utf8, ');background-size:.5em;z-index:-1;z-index:auto}.pickr .pcr-button:after,.pickr .pcr-button:before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;border-radius:.15em}.pickr .pcr-button:after{transition:background .3s;background:currentColor}.pickr .pcr-button.clear{background-size:70%}.pickr .pcr-button.clear:before{opacity:0}.pickr .pcr-button.clear:focus{box-shadow:0 0 0 1px #f1f3f4,0 0 0 3px currentColor}.pickr .pcr-button.disabled{cursor:not-allowed}.pcr-app button,.pcr-app input,.pickr button,.pickr input{outline:none;border:none;-webkit-appearance:none}.pcr-app button:focus,.pcr-app input:focus,.pickr button:focus,.pickr input:focus{box-shadow:0 0 0 1px #f1f3f4,0 0 0 3px currentColor}.pcr-app{position:fixed;display:flex;flex-direction:column;z-index:10000;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;box-shadow:0 .15em 1.5em 0 rgba(0,0,0,.1),0 0 1em 0 rgba(0,0,0,.03);width:28.5em;max-width:95vw;padding:.8em;border-radius:.1em;background:#fff;opacity:0;visibility:hidden;transition:opacity .3s;left:0;top:0}.pcr-app.visible{visibility:visible;opacity:1}.pcr-app .pcr-swatches{display:flex;flex-wrap:wrap;margin-top:.75em}.pcr-app .pcr-swatches.pcr-last{margin:0}@supports (display:grid){.pcr-app .pcr-swatches{display:grid;align-items:center;justify-content:space-around;grid-template-columns:repeat(auto-fit,1.75em)}}.pcr-app .pcr-swatches>button{position:relative;width:1.75em;height:1.75em;border-radius:.15em;cursor:pointer;margin:2.5px;flex-shrink:0;justify-self:center;transition:all .15s;overflow:hidden;background:transparent;z-index:1}.pcr-app .pcr-swatches>button:before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url('data:image/svg+xml;utf8, ');background-size:6px;border-radius:.15em;z-index:-1}.pcr-app .pcr-swatches>button:after{content:"";position:absolute;top:0;left:0;width:100%;height:100%;background:currentColor;border:1px solid rgba(0,0,0,.05);border-radius:.15em;box-sizing:border-box}.pcr-app .pcr-swatches>button:hover{filter:brightness(1.05)}.pcr-app .pcr-interaction{display:flex;flex-wrap:wrap;align-items:center;margin:0 -.2em}.pcr-app .pcr-interaction>*{margin:0 .2em}.pcr-app .pcr-interaction input{letter-spacing:.07em;font-size:.75em;text-align:center;cursor:pointer;color:#75797e;background:#f1f3f4;border-radius:.15em;transition:all .15s;padding:.45em .5em;margin-top:.75em}.pcr-app .pcr-interaction input:hover{filter:brightness(.975)}.pcr-app .pcr-interaction input:focus{box-shadow:0 0 0 1px #f1f3f4,0 0 0 3px rgba(66,133,244,.75)}.pcr-app .pcr-interaction .pcr-result{color:#75797e;text-align:left;flex:1 1 8em;min-width:8em;transition:all .2s;border-radius:.15em;background:#f1f3f4;cursor:text}.pcr-app .pcr-interaction .pcr-result::selection{background:#4285f4;color:#fff}.pcr-app .pcr-interaction .pcr-type.active{color:#fff;background:#4285f4}.pcr-app .pcr-interaction .pcr-clear,.pcr-app .pcr-interaction .pcr-save{width:auto;color:#fff}.pcr-app .pcr-interaction .pcr-clear:hover,.pcr-app .pcr-interaction .pcr-save:hover{filter:brightness(.925)}.pcr-app .pcr-interaction .pcr-save{background:#4285f4}.pcr-app .pcr-interaction .pcr-clear{background:#f44250}.pcr-app .pcr-interaction .pcr-clear:focus{box-shadow:0 0 0 1px #f1f3f4,0 0 0 3px rgba(244,66,80,.75)}.pcr-app .pcr-selection{display:flex;justify-content:space-between;flex-grow:1}.pcr-app .pcr-selection .pcr-picker{position:absolute;height:18px;width:18px;border:2px solid #fff;border-radius:100%;user-select:none}.pcr-app .pcr-selection .pcr-color-preview{position:relative;z-index:1;width:2em;display:flex;flex-direction:column;justify-content:space-between;margin-right:.75em}.pcr-app .pcr-selection .pcr-color-preview:before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url('data:image/svg+xml;utf8, ');background-size:.5em;border-radius:.15em;z-index:-1}.pcr-app .pcr-selection .pcr-color-preview .pcr-last-color{cursor:pointer;transition:background-color .3s,box-shadow .3s;border-radius:.15em .15em 0 0;z-index:2}.pcr-app .pcr-selection .pcr-color-preview .pcr-current-color{border-radius:0 0 .15em .15em}.pcr-app .pcr-selection .pcr-color-preview .pcr-current-color,.pcr-app .pcr-selection .pcr-color-preview .pcr-last-color{background:currentColor;width:100%;height:50%}.pcr-app .pcr-selection .pcr-color-chooser,.pcr-app .pcr-selection .pcr-color-opacity,.pcr-app .pcr-selection .pcr-color-palette{position:relative;user-select:none;display:flex;flex-direction:column;cursor:grab;cursor:-moz-grab;cursor:-webkit-grab}.pcr-app .pcr-selection .pcr-color-chooser:active,.pcr-app .pcr-selection .pcr-color-opacity:active,.pcr-app .pcr-selection .pcr-color-palette:active{cursor:grabbing;cursor:-moz-grabbing;cursor:-webkit-grabbing}.pcr-app .pcr-selection .pcr-color-palette{width:100%;height:8em;z-index:1}.pcr-app .pcr-selection .pcr-color-palette .pcr-palette{flex-grow:1;border-radius:.15em}.pcr-app .pcr-selection .pcr-color-palette .pcr-palette:before{position:absolute;content:"";top:0;left:0;width:100%;height:100%;background:url('data:image/svg+xml;utf8, ');background-size:.5em;border-radius:.15em;z-index:-1}.pcr-app .pcr-selection .pcr-color-chooser,.pcr-app .pcr-selection .pcr-color-opacity{margin-left:.75em}.pcr-app .pcr-selection .pcr-color-chooser .pcr-picker,.pcr-app .pcr-selection .pcr-color-opacity .pcr-picker{left:50%;transform:translateX(-50%)}.pcr-app .pcr-selection .pcr-color-chooser .pcr-slider,.pcr-app .pcr-selection .pcr-color-opacity .pcr-slider{width:8px;flex-grow:1;border-radius:50em}.pcr-app .pcr-selection .pcr-color-chooser .pcr-slider{background:linear-gradient(180deg,red,#ff0,#0f0,#0ff,#00f,#f0f,red)}.pcr-app .pcr-selection .pcr-color-opacity .pcr-slider{background:linear-gradient(180deg,transparent,#000),url('data:image/svg+xml;utf8, ');background-size:100%,50%} +/*# sourceMappingURL=pickr.es5.min.js.map*/ \ No newline at end of file diff --git a/app/controllers/manage/configs_controller.rb b/app/controllers/manage/configs_controller.rb index 891883258..030c16d3d 100644 --- a/app/controllers/manage/configs_controller.rb +++ b/app/controllers/manage/configs_controller.rb @@ -1,6 +1,6 @@ class Manage::ConfigsController < Manage::ApplicationController before_action :limit_access_admin - before_action :get_config, only: [:edit, :update] + before_action :get_config, only: [:edit, :update, :update_only_css_variables] respond_to :html, :json @@ -15,8 +15,8 @@ def edit def update key = @config.var.to_sym value = params[:hackathon_config][key] - value = true if value == 'true' - value = false if value == 'false' + value = true if value == "true" + value = false if value == "false" if @config.value != value @config.value = value @config.save @@ -26,6 +26,35 @@ def update end end + def update_only_css_variables + key = @config.var.to_sym + old_value = @config.value.strip + posted_value = params[:hackathon_config][key].strip + if old_value.include? ':root {' + # Replace the old CSS variables and keep the extra css + start_index = old_value.index(':root {') + end_index = old_value.index('}', start_index) + 1 + pre_value = old_value[0...start_index].rstrip + post_value = old_value[end_index..].lstrip + new_value = "#{pre_value}\n\n#{posted_value}\n\n#{post_value}".strip + else + # Prepend the variable definitions to the existing value + new_value = "#{posted_value}\n\n#{old_value}" + end + params[:hackathon_config][key] = new_value + update + end + + def enter_theming_editor + cookies[:theming_editor] = true + redirect_to root_path + end + + def exit_theming_editor + cookies.delete :theming_editor + redirect_to manage_configs_path + end + private def get_config diff --git a/app/views/layouts/_theming_editor.html.haml b/app/views/layouts/_theming_editor.html.haml new file mode 100644 index 000000000..fd99c0796 --- /dev/null +++ b/app/views/layouts/_theming_editor.html.haml @@ -0,0 +1,15 @@ +.theming-editor__wrapper#theming-editor + .theming-editor__title Custom CSS Variables + .theming-editor__variables + .theming-editor__variable + %small.theming-editor__variable-name + %code.theming-editor__variable-name-code my-variable + .theming-editor__input-group + %input.theming-editor__input{type: 'text'} + .theming-editor__color-picker + + .theming-editor__button-wrapper + %button.theming-editor__button-save{type: 'button'} Save + = link_to 'Cancel', exit_theming_editor_manage_configs_path + += javascript_include_tag "theming-editor/theming-editor.js", "data-turbolinks-eval": "false" diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index dce9c31a7..520283471 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -26,3 +26,5 @@ %section.section .container = yield + - if cookies[:theming_editor] + = render "layouts/theming_editor" diff --git a/app/views/manage/configs/index.html.haml b/app/views/manage/configs/index.html.haml index 6bb6f445b..b291941b6 100644 --- a/app/views/manage/configs/index.html.haml +++ b/app/views/manage/configs/index.html.haml @@ -15,6 +15,11 @@ %small %code= key %p.text-muted= t("simple_form.hints.hackathon_config.#{key}").html_safe + - if key == 'custom_css' + %p + = link_to enter_theming_editor_manage_configs_path do + %span.fa.fa-paint-brush.icon-space-r-half + Interactive editor - if value == '' %p.mb-0 %span.badge.badge-secondary Not set diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index f77ff1009..1c79d79c2 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -12,4 +12,4 @@ # application.js, application.css, and all non-JS/CSS in the app/assets # folder are already added. # Rails.application.config.assets.precompile += %w( admin.js admin.css ) -Rails.application.config.assets.precompile += %w[manage.css vendor/*.js vendor/*.css] +Rails.application.config.assets.precompile += %w[manage.css vendor/*.js vendor/*.css theming-editor/*] diff --git a/config/routes.rb b/config/routes.rb index 85c0b9f62..34057a151 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -86,7 +86,11 @@ post :mlh_info_applied, on: :collection post :mlh_info_checked_in, on: :collection end - resources :configs + resources :configs do + patch :update_only_css_variables, on: :member + get :enter_theming_editor, on: :collection + get :exit_theming_editor, on: :collection + end resources :trackable_events resources :trackable_tags end diff --git a/test/controllers/manage/configs_controller_test.rb b/test/controllers/manage/configs_controller_test.rb index d1030c1f1..f3168b402 100644 --- a/test/controllers/manage/configs_controller_test.rb +++ b/test/controllers/manage/configs_controller_test.rb @@ -20,6 +20,12 @@ class Manage::ConfigsControllerTest < ActionController::TestCase assert_equal false, HackathonConfig["registration_is_open"] assert_redirected_to new_user_session_path end + + should "not update css config" do + HackathonConfig["custom_css"] = "" + patch :update_only_css_variables, params: { id: "custom_css", hackathon_config: { custom_css: ":root {\n --foo: #fff;\n}" } } + assert_equal "", HackathonConfig["custom_css"] + end end context "while authenticated as a user" do @@ -47,6 +53,12 @@ class Manage::ConfigsControllerTest < ActionController::TestCase assert_equal false, HackathonConfig["registration_is_open"] assert_redirected_to root_path end + + should "not update css config" do + HackathonConfig["custom_css"] = "" + patch :update_only_css_variables, params: { id: "custom_css", hackathon_config: { custom_css: ":root {\n --foo: #fff;\n}" } } + assert_equal "", HackathonConfig["custom_css"] + end end context "while authenticated as a limited access admin" do @@ -71,6 +83,12 @@ class Manage::ConfigsControllerTest < ActionController::TestCase patch :update, params: { id: "registration_is_open", hackathon_config: { registration_is_open: "true" } } assert_equal false, HackathonConfig["registration_is_open"] end + + should "not update css config" do + HackathonConfig["custom_css"] = "" + patch :update_only_css_variables, params: { id: "custom_css", hackathon_config: { custom_css: ":root {\n --foo: #fff;\n}" } } + assert_equal "", HackathonConfig["custom_css"] + end end context "while authenticated as an admin" do @@ -96,5 +114,23 @@ class Manage::ConfigsControllerTest < ActionController::TestCase assert_equal true, HackathonConfig["registration_is_open"] assert_redirected_to manage_configs_path end + + should "update config CSS variables when custom_css is blank" do + HackathonConfig["custom_css"] = "" + patch :update, params: { id: "custom_css", hackathon_config: { custom_css: ":root {\n --foo: #fff;\n}" } } + assert_equal ":root {\n --foo: #fff;\n}", HackathonConfig["custom_css"] + end + + should "update config CSS variables when custom_css contains custom css" do + HackathonConfig["custom_css"] = ".bar {\n color: red;\n}" + patch :update_only_css_variables, params: { id: "custom_css", hackathon_config: { custom_css: ":root {\n --foo: #fff;\n}" } } + assert_equal ":root {\n --foo: #fff;\n}\n\n.bar {\n color: red;\n}", HackathonConfig["custom_css"] + end + + should "update config CSS variables when custom_css contains custom css and existing variables" do + HackathonConfig["custom_css"] = ".foo {\nabc\n}\n\n:root {\n --foo: #000;\n}\n\n.bar {\n color: red;\n}" + patch :update_only_css_variables, params: { id: "custom_css", hackathon_config: { custom_css: ":root {\n --foo: #fff;\n}" } } + assert_equal ".foo {\nabc\n}\n\n:root {\n --foo: #fff;\n}\n\n.bar {\n color: red;\n}", HackathonConfig["custom_css"] + end end end From ea0d07800045ecff50c8414ffdce8fa44939f270 Mon Sep 17 00:00:00 2001 From: Stuart Olivera Date: Thu, 30 May 2019 19:27:48 -0400 Subject: [PATCH 2/3] Cleanup --- .../theming-editor/theming-editor.js | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/theming-editor/theming-editor.js b/app/assets/javascripts/theming-editor/theming-editor.js index 470882911..992953a0d 100644 --- a/app/assets/javascripts/theming-editor/theming-editor.js +++ b/app/assets/javascripts/theming-editor/theming-editor.js @@ -35,6 +35,10 @@ css += ' ' + variable + ': ' + value + ';\n'; }); css += '}'; + saveConfig(css); + } + + function saveConfig(css) { var data = { hackathon_config: { custom_css: css, @@ -52,9 +56,9 @@ .then(function(response) { if (!response.ok && response.type != 'opaqueredirect') { alert('There was an error attempting to save. Please try again or refresh the page.'); - } else { - window.location.replace('/manage/configs/exit_theming_editor'); + return; } + window.location.replace('/manage/configs/exit_theming_editor'); }) .catch(function() { alert('There was an error attempting to save. Please try again or refresh the page.'); @@ -64,9 +68,11 @@ var onTextChange = throttle(_onTextChange, 100); var onPickrChange = throttle(_onPickrChange, 100); - function init() { + function initHtml() { var variables = Object.values(getComputedStyle(document.documentElement)) - .filter(x => x.startsWith('--primary')) + .filter(function(x) { + return x.startsWith('--primary'); + }) .sort(); var $editor = $('#theming-editor'); var $variables = $editor.find('.theming-editor__variables'); @@ -85,7 +91,10 @@ $input.on('input', onTextChange); $variables.append($new_variable); }); + $editor.find('.theming-editor__button-save').on('click', onSave); + } + function initColorPickers() { window.pickrs = {}; $('.theming-editor__color-picker').each(function() { var $input = $(this) @@ -104,8 +113,11 @@ var variable = $input.attr('name'); window.pickrs[variable] = pickr; }); + } - $editor.find('.theming-editor__button-save').on('click', onSave); + function init() { + initHtml(); + initColorPickers(); } document.addEventListener('turbolinks:load', init); From 6b1a1772670f6a2a1f023b0b3b5425afbac7b811 Mon Sep 17 00:00:00 2001 From: Stuart Olivera Date: Thu, 30 May 2019 19:31:44 -0400 Subject: [PATCH 3/3] Support Ruby 2.5 --- app/controllers/manage/configs_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/manage/configs_controller.rb b/app/controllers/manage/configs_controller.rb index 030c16d3d..a48e5011d 100644 --- a/app/controllers/manage/configs_controller.rb +++ b/app/controllers/manage/configs_controller.rb @@ -35,7 +35,7 @@ def update_only_css_variables start_index = old_value.index(':root {') end_index = old_value.index('}', start_index) + 1 pre_value = old_value[0...start_index].rstrip - post_value = old_value[end_index..].lstrip + post_value = old_value[end_index..-1].lstrip new_value = "#{pre_value}\n\n#{posted_value}\n\n#{post_value}".strip else # Prepend the variable definitions to the existing value