Turn audio into a shareable video. forked from nypublicradio/audiogram

themeEditor.js 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. var d3 = require('d3'),
  2. $ = require('jquery'),
  3. preview = require('./preview.js'),
  4. options = require('./themeOptions.js');
  5. var editorIsActive = false;
  6. var themesJson = {};
  7. function updateTheme(options) {
  8. if(options !== undefined){
  9. if(options.backgroundImage !== '')
  10. getImage(options);
  11. preview.theme(options);
  12. }
  13. else{
  14. preview.theme(d3.select(this.options[this.selectedIndex]).datum());
  15. }
  16. }
  17. function getImage(theme) {
  18. if (theme.backgroundImage) {
  19. theme.backgroundImageFile = new Image();
  20. theme.backgroundImageFile.onload = function(){
  21. updateTheme(anonymousTheme());
  22. };
  23. theme.backgroundImageFile.src = '/settings/backgrounds/' + theme.backgroundImage;
  24. }
  25. }
  26. function error(msg) {
  27. if (msg.responseText) {
  28. msg = msg.responseText;
  29. }
  30. if (typeof msg !== 'string') {
  31. msg = JSON.stringify(msg);
  32. }
  33. if (!msg) {
  34. msg = 'Unknown error';
  35. }
  36. d3.select('#loading-message').text('Loading...');
  37. setClass('error', msg);
  38. }
  39. function setClass(cl, msg) {
  40. d3.select('body').attr('class', cl || null);
  41. d3.select('#error').text(msg || '');
  42. }
  43. function anonymousTheme(){
  44. var output = {};
  45. d3.selectAll('.options-attribute-input').each(function(d){
  46. if(d.type == 'number'){
  47. if(Number.isNaN(parseFloat(this.value)))
  48. output[d.name] = null;
  49. else
  50. output[d.name] = parseFloat(this.value);
  51. }
  52. else{
  53. output[d.name] = this.value;
  54. }
  55. });
  56. return output;
  57. }
  58. function postTheme(type, data, cb){
  59. var postData = {
  60. type: type,
  61. data: data
  62. };
  63. $.ajax({
  64. url: '/api/themes',
  65. type: 'POST',
  66. data: JSON.stringify(postData),
  67. contentType: 'application/json',
  68. cache: false,
  69. success: cb,
  70. error: error
  71. });
  72. }
  73. function saveChanges(){
  74. var activeTheme = preview.theme();
  75. var activeThemeName = d3.select('#input-theme').property('value');
  76. function makeTheme(){
  77. var file = {};
  78. file.name = activeTheme.name;
  79. file.currentName = activeThemeName;
  80. d3.selectAll('.options-attribute-input').each(function(d){
  81. if(!this.disabled){
  82. if(this.getAttribute('type') == 'number')
  83. file[d.name] = parseFloat(this.value);
  84. else
  85. file[d.name] = this.value;
  86. }
  87. });
  88. return file;
  89. }
  90. function reloadPage(){
  91. var url = window.location.origin + window.location.pathname + '?t=' + activeTheme.name;
  92. window.location = url;
  93. }
  94. var themeNames = [];
  95. d3.selectAll('#input-theme option').each(function(d){
  96. themeNames.push(d.name);
  97. });
  98. themeNames.splice(themeNames.length-1, 1);
  99. var postData = makeTheme();
  100. if(activeTheme.name == 'default' || activeTheme.name == '0' || activeTheme.name == ''){
  101. window.alert('Please give your theme a name.');
  102. }
  103. else if(themeNames.indexOf(activeTheme.name) != -1 && activeThemeName == 'default'){
  104. window.alert('That theme name already exists. Please choose a different name or select it to edit it.');
  105. }
  106. else if(themeNames.indexOf(activeThemeName) != -1){
  107. var msg = 'Are you sure you want to override the options for "';
  108. msg += activeThemeName + '"? This action is permanent and cannot be undone.';
  109. if(window.confirm(msg)){
  110. postTheme('UPDATE', postData, reloadPage);
  111. }
  112. }
  113. else{
  114. postTheme('ADD', postData, reloadPage);
  115. }
  116. }
  117. function deleteTheme(){
  118. var activeThemeName = d3.select('#input-theme').property('value');
  119. var msg = 'Are you sure you want to delete the theme "';
  120. msg += activeThemeName + '"? This action is permanent and cannot be undone.';
  121. if(window.confirm(msg)){
  122. var postData = {name: activeThemeName};
  123. postTheme('DELETE', postData, function(){
  124. var url = window.location.origin + window.location.pathname;
  125. window.location = url;
  126. });
  127. }
  128. }
  129. function refreshTheme(){
  130. var theme = $.extend({name: preview.theme().name}, themesJson.default, themesJson[preview.theme().name]);
  131. updateTheme(theme);
  132. populateThemeFields();
  133. }
  134. function loadBkgndImages(cb){
  135. $.ajax({
  136. url: '/api/images',
  137. type: 'GET',
  138. cache: false,
  139. success: function(data){
  140. $('#attribute-backgroundImage').html('');
  141. var bkgndImgSelect = d3.select('#attribute-backgroundImage');
  142. for(var img of data){
  143. bkgndImgSelect.append('option')
  144. .attr('value', img)
  145. .text(img);
  146. }
  147. if(cb !== undefined){
  148. cb();
  149. }
  150. },
  151. error: error
  152. });
  153. }
  154. function uploadImage(){
  155. // this = the file input calling the function
  156. var img = this.files[0];
  157. var confirmed = confirm('Are you sure you want to upload ' + img.name + '?');
  158. if(confirmed){
  159. var formData = new FormData();
  160. formData.append('img', img);
  161. setClass('loading');
  162. $.ajax({
  163. url: '/api/images',
  164. type: 'POST',
  165. data: formData,
  166. contentType: false,
  167. cache: false,
  168. processData: false,
  169. success: function(){
  170. var setImg = function(){
  171. var input = d3.select('#container-backgroundImage').select('.options-attribute-input');
  172. input.property('value', img.name).attr('data-value', img.name);
  173. updateTheme(anonymousTheme());
  174. setClass(null);
  175. };
  176. loadBkgndImages(setImg);
  177. },
  178. error: error
  179. });
  180. }
  181. }
  182. function camelToTitle(string){
  183. var conversion = string.replace( /([A-Z])/g, ' $1' );
  184. var title = conversion.charAt(0).toUpperCase() + conversion.slice(1);
  185. return title;
  186. }
  187. function queryParser(query){
  188. query = query.substring(1);
  189. let query_string = {};
  190. const vars = query.split('&');
  191. for (var i=0;i<vars.length;i++) {
  192. var pair = vars[i].split('=');
  193. if (typeof query_string[pair[0]] === 'undefined') {
  194. query_string[pair[0]] = decodeURIComponent(pair[1]);
  195. } else if (typeof query_string[pair[0]] === 'string') {
  196. const arr = [ query_string[pair[0]],decodeURIComponent(pair[1]) ];
  197. query_string[pair[0]] = arr;
  198. } else {
  199. query_string[pair[0]].push(decodeURIComponent(pair[1]));
  200. }
  201. }
  202. if (Object.keys(query_string).includes(''))
  203. return {};
  204. else
  205. return query_string;
  206. };
  207. function isEditor(){
  208. return document.querySelector('#themeEditor') !== null;
  209. }
  210. function isActive(){
  211. return editorIsActive;
  212. }
  213. function initializeThemes(themes){
  214. for (var key in themes) {
  215. themesJson[key] = $.extend({}, themes[key]);
  216. if('foregroundColor' in themesJson[key]){
  217. themesJson[key].captionColor = themesJson[key].foregroundColor;
  218. themesJson[key].waveColor = themesJson[key].foregroundColor;
  219. }
  220. }
  221. }
  222. function initializePreview(){
  223. var container = $('#preview');
  224. var top = $(container).offset().top;
  225. $(window).scroll(function() {
  226. var y = $(this).scrollTop();
  227. if (y >= top){
  228. $(container).addClass('sticky');
  229. }
  230. else{
  231. $(container).removeClass('sticky');
  232. }
  233. });
  234. preview.redraw();
  235. }
  236. function toggleSection(d){
  237. var name;
  238. if(typeof(d) == 'object')
  239. name = d.name;
  240. else
  241. name = d;
  242. $('#section-options-'+ name).slideToggle(500);
  243. $('#section-toggle-' + name).toggleClass('toggled');
  244. }
  245. function initialize(){
  246. var container = d3.select('#options');
  247. // Add Option Sections
  248. var sections = container.selectAll('.section').data(options)
  249. .enter()
  250. .append('div')
  251. .attr('class', 'section');
  252. var headers = sections.append('div')
  253. .attr('class', 'section-header')
  254. .on('click', toggleSection);
  255. // Add Section Toggles
  256. headers.append('svg')
  257. .attr('id', function(d){return 'section-toggle-' + d.name;})
  258. .attr('class', 'section-toggle')
  259. .attr('viewBox', '0 0 24 24')
  260. .append('path')
  261. .attr('d', function(){
  262. var path = 'M12,13.7c-0.5,0-0.9-0.2-1.2-0.5L0.5,2.9c-0.7-0.7-0.7-1.8,0-2.4C0.8,0.2,1.3,0,1.7,0h20.6C23.3,0,' +
  263. '24,0.8,24,1.7c0,0.4-0.2,0.9-0.5,1.2L13.2,13.2C12.9,13.6,12.5,13.7,12,13.7z';
  264. return path;
  265. });
  266. // Add Section Titles
  267. headers.append('h4')
  268. .attr('class', 'section-title')
  269. .text(function(d){return d.name;});
  270. var attributesContainer = sections.append('div')
  271. .attr('class', 'section-options')
  272. .attr('id', function(d){return 'section-options-' + d.name;})
  273. .style('display', 'none');
  274. // Add Option Container
  275. var attributes = attributesContainer.selectAll('.options-attribute')
  276. .data(function(d){return d.options;})
  277. .enter()
  278. .append('div')
  279. .attr('class','options-attribute')
  280. .attr('id', function(d){return 'container-' + d.name;})
  281. .attr('data-type', function(d){return d.type;});
  282. // Add Enable Checkboxes
  283. attributes.append('input')
  284. .attr('id', function(d){return 'enable-' + d.name;})
  285. .attr('class', 'options-checkbox options-attribute-child')
  286. .attr('name', function(d){return 'enable-' + d.name;})
  287. .attr('value', 'true')
  288. .attr('type', 'checkbox')
  289. .property('checked', true)
  290. .on('click', function(d){
  291. var input = d3.select('#attribute-' + d.name);
  292. if(this.checked){
  293. input.property('disabled', false);
  294. input.property('value', input.attr('data-value'));
  295. input.attr('value', input.attr('data-value'));
  296. updateTheme(anonymousTheme());
  297. }
  298. else{
  299. input.property('value', themesJson.default[d.name]);
  300. input.attr('value', themesJson.default[d.name]);
  301. input.property('disabled', true);
  302. updateTheme(anonymousTheme());
  303. }
  304. });
  305. // Add Option Labels
  306. attributes.append('label')
  307. .attr('for', function(d){return 'attribute-' + d.name;})
  308. .attr('class', 'options-attribute-child')
  309. .text(function(d){return camelToTitle(d.name);});
  310. // Add Help Text Icons
  311. attributes.append('i')
  312. .attr('id', function(d){return 'help-' + d.name;})
  313. .attr('class', 'attribute-help options-attribute-child fa fa-question')
  314. .attr('title', function(d){return d.help;});
  315. // Add Inputs For Text Fields
  316. sections.selectAll('.options-attribute:not([data-type="select"])')
  317. .append('input')
  318. .attr('id', function(d){return 'attribute-' + d.name;})
  319. .attr('class', 'options-attribute-input options-attribute-child input-text')
  320. .attr('name', function(d){return d.name;})
  321. .attr('type', function(d){return d.type;})
  322. .on('input', function(){
  323. this.setAttribute('data-value', this.value);
  324. updateTheme(anonymousTheme());
  325. });
  326. // Add Inputs For Select Fields
  327. sections.selectAll('.options-attribute[data-type="select"]')
  328. .append('select')
  329. .attr('id', function(d){return 'attribute-' + d.name;})
  330. .attr('class', 'options-attribute-input options-attribute-child input-select')
  331. .attr('name', function(d){return d.name;})
  332. .attr('type', function(d){return d.type;})
  333. .on('input', function(){updateTheme(anonymousTheme());})
  334. .selectAll('options')
  335. .data(function(d){return d.options;})
  336. .enter()
  337. .append('option')
  338. .attr('value', function(d){return d;})
  339. .text(function(d){return camelToTitle(d);});
  340. // Add Section Notes
  341. attributesContainer.append('p')
  342. .attr('class', 'options-note note')
  343. .text(function(d){return d.note;});
  344. // Add "New..." option to theme select
  345. d3.select('#input-theme')
  346. .append('option')
  347. .data([themesJson.default])
  348. .attr('value', 'default')
  349. .text('New...');
  350. // Add clickHandler for Save Button
  351. d3.select('#saveChanges')
  352. .on('click', saveChanges);
  353. // Add clickHandler for Delete Button
  354. d3.select('#deleteTheme')
  355. .on('click', deleteTheme);
  356. // Add clickHandler for Refresh Button
  357. d3.select('#refreshTheme')
  358. .on('click', refreshTheme);
  359. // Background Images Populate and Add Uploader
  360. loadBkgndImages(populateThemeFields);
  361. var bkgndImgContainer = d3.select('#container-backgroundImage');
  362. bkgndImgContainer.append('label')
  363. .attr('for', 'imgUploader')
  364. .attr('id', 'imgUploader-label')
  365. .attr('class', 'button')
  366. .append('i')
  367. .attr('class', 'fa fa-upload');
  368. bkgndImgContainer.append('input')
  369. .attr('type', 'file')
  370. .attr('id', 'imgUploader')
  371. .attr('name', 'imgUploader')
  372. .on('change', uploadImage);
  373. toggleSection('Metadata');
  374. // Active url theme
  375. var selectedTheme = queryParser(window.location.search);
  376. if('t' in selectedTheme && selectedTheme.t in themesJson){
  377. d3.select('#input-theme')
  378. .property('value', selectedTheme.t)
  379. .dispatch('change');
  380. }
  381. populateThemeFields();
  382. initializePreview();
  383. window.addEventListener('resize', function(){
  384. preview.redraw();
  385. });
  386. d3.select('#toggle-more-instructions').on('click', function(){
  387. $('#more-instructions').slideToggle(500);
  388. })
  389. // Toggle the editor container to loaded
  390. editorIsActive = true;
  391. }
  392. function populateThemeFields(){
  393. var activeTheme = preview.theme();
  394. var themeJson = activeTheme.name !== undefined && activeTheme.name in themesJson ?
  395. themesJson[activeTheme.name] :
  396. themesJson.default;
  397. d3.selectAll('.options-attribute').each(function(d){
  398. var input = d3.select(this).select('.options-attribute-input');
  399. var activeValue = d.name in activeTheme ? activeTheme[d.name] : '';
  400. input.property('value', activeValue).attr('data-value', activeValue);
  401. if(d.name == 'name'){
  402. d3.select('#enable-'+d.name).property('checked', true);
  403. input.property('disabled', false);
  404. }
  405. else if(activeTheme.name == undefined || !(d.name in themeJson)){
  406. d3.select('#enable-'+d.name).property('checked', false);
  407. input.property('disabled', true);
  408. }
  409. else{
  410. d3.select('#enable-'+d.name).property('checked', true);
  411. input.property('disabled', false);
  412. }
  413. });
  414. }
  415. window.themesJson = themesJson;
  416. module.exports = {
  417. isEditor,
  418. isActive,
  419. initialize,
  420. initializeThemes,
  421. populateThemeFields
  422. };