Event.observe(window,'load',init); //###TODO // * Disable/enable save/cancel, Add key and onchange listeners keep a note on changed items // * create the path/file browser // * better errormessage for non writable config // * make tabs? // * add warning if leaving page without saving // Config isn't defined until after the Event.observe above // I could have put it below Config = ... but I want all window.load events // at the start of the file var DEBUG = window.location.toString().match(/debug.*/); var g_messageTimeout; if ('debug=validate' == DEBUG) { DEBUG = 'validate'; } function init() { Config.init(); } var ConfigXML = { config: {}, advancedSections: [], getItem: function (id) { return this.config[id]; }, getAllItems: function () { return $H(this.config).pluck('value'); }, addAdvancedSection: function (sectionName) { this.advancedSections.push(sectionName); }, isAdvancedSection: function (sectionName) { return this.advancedSections.find(function (name) { return name == sectionName }); }, getAllAdvancedSections: function () { return this.advancedSections; }, getSectionId: function (sectionName) { return 'firefly_'+sectionName.replace(/\ /g,'').toLowerCase(); }, parseXML: function(xmlDoc) { $A(xmlDoc.getElementsByTagName('section')).each(function (section) { if ('true' == section.getAttribute('advanced')) { // Only used by Config._showAdvancedConfig, Config._showBasicConfig ConfigXML.addAdvancedSection(section.getAttribute('name')); } $A(section.getElementsByTagName('item')).each(function (item) { var returnItem = {}; $A(item.attributes).each(function (attr) { returnItem[attr.name] = attr.value; }); Element.cleanWhitespace(item); $A(item.childNodes).each(function (node) { if (Element.textContent(node) == '') { return; } if ('options' == node.nodeName) { var options = []; $A(item.getElementsByTagName('option')).each(function (option) { options.push({value: option.getAttribute('value'), label: Element.textContent(option)}); }); returnItem['options'] = options; } else { returnItem[node.nodeName] = Element.textContent(node); } $A(node.attributes).each(function (attr) { returnItem[attr.name] = attr.value; }); }); ConfigXML.config[returnItem.id] = returnItem; }); }); } }; var ConfigInitialValues = { values: {}, getValue: function (id) { return ConfigInitialValues.values[id]; }, setValue: function (id,value) { this.values[id] = value; }, parseXML: function (xmldoc) { // IE and w3c treat xmldoc differently make shore firstChild is firstchild of if (xmldoc.childNodes[1] && xmldoc.childNodes[1].nodeName == 'config') { sections = $A(xmldoc.childNodes[1].childNodes); } else { sections = $A(xmldoc.firstChild.childNodes); } var missingItems = []; sections.each(function (section) { var sectionName = section.nodeName; $A(section.childNodes).each(function (node) { var itemId = sectionName + ':' + node.nodeName; if (node.firstChild && node.firstChild.hasChildNodes()) { var values = []; $A(node.childNodes).each(function (n) { values.push(Element.textContent(n)); }); ConfigInitialValues.values[itemId] = values; } else { ConfigInitialValues.values[itemId] = Element.textContent(node); } if (!ConfigXML.getItem(itemId)) { missingItems.push(itemId); } }); }); if (missingItems.length > 0) { //###FIXME A bit ugly, but add a section with values from mt-daapd.conf that aren't // in config.xml ConfigXML.addAdvancedSection('missing_items'); var frag = document.createDocumentFragment(); missingItems.each(function (el){ frag.appendChild(document.createTextNode(el)); frag.appendChild(document.createElement('br')); }); var outerDiv = Builder.node('div',{id: ConfigXML.getSectionId('missing_items')}); outerDiv.appendChild(Builder.node('div',{className: 'naviheader'},'Options missing from config.xml')); var contentDiv = Builder.node('div',{className: 'navibox'}); contentDiv.appendChild(document.createTextNode('The options below are in your mt-daapd.conf and Firefly uses them,')); contentDiv.appendChild(document.createElement('br')); contentDiv.appendChild(document.createTextNode("but this web page can't handle them until they are added to config.xml")); contentDiv.appendChild(document.createElement('br')); contentDiv.appendChild(document.createElement('br')); contentDiv.style.paddingLeft = '1em'; contentDiv.appendChild(frag); outerDiv.appendChild(contentDiv); if (!Cookie.getVar('show_advanced_config')) { outerDiv.style.display = 'none'; } $('theform').appendChild(outerDiv); } } }; var Config ={ configPath: '', init: function () { new Ajax.Request('config.xml',{method: 'get',onComplete: Config.storeConfigLayout}); }, storeConfigLayout: function (request) { // Need to store this until showConfig is run Config.tmpConfigXML = request.responseXML; ConfigXML.parseXML(request.responseXML); ConfigXML.getAllItems().each(function (item) { if (item.multiple) { //###FIXME default values on item.multiple="true" not possible ConfigInitialValues.setValue(item.id,[]); } else { ConfigInitialValues.setValue(item.id,item.default_value || ''); } }); new Ajax.Request('xml-rpc?method=stats',{method: 'get',onComplete: Config.updateStatus}); }, updateStatus: function (request) { Config.configPath = Element.textContent(request.responseXML.getElementsByTagName('config_path')[0]); Config.isWritable = Element.textContent(request.responseXML.getElementsByTagName('writable_config')[0]) == '1'; new Ajax.Request('xml-rpc?method=config',{method: 'get',onComplete: Config.showConfig}); }, showConfig: function (request) { ConfigInitialValues.parseXML(request.responseXML); $A(Config.tmpConfigXML.getElementsByTagName('section')).each(function (section) { var head = document.createElement('div'); head.className= 'naviheader'; var sectionName = section.getAttribute('name'); head.appendChild(document.createTextNode(sectionName)); var body = document.createElement('div'); body.className = 'navibox'; if ('Server' == sectionName) { body.appendChild(Builder.node('span',{id:'config_path_label'},'Config File Location')); var span = Builder.node('span',{id:'config_path'}); span.appendChild(document.createTextNode(Config.configPath)); body.appendChild(span); body.appendChild(Builder.node('br')); body.appendChild(Builder.node('div',{style: 'clear: both;'})); } $A(section.getElementsByTagName('item')).each(function (item) { body.appendChild(Config._buildItem(item.getAttribute('id'))); }); var div = document.createElement('div'); div.id = ConfigXML.getSectionId(sectionName); if (!Cookie.getVar('show_advanced_config') && ConfigXML.isAdvancedSection(sectionName)) { div.style.display = 'none'; } div.appendChild(head); div.appendChild(body); $('theform').appendChild(div); }); // Won't be using the config.xml XML doc anymore get rid of it Config.tmpConfigXML = ''; if (!Config.isWritable) { Effect.Appear('config_not_writable_warning'); } else { // Create save and cancel buttons // var save = Builder.node('button',{id: 'button_save', disabled: 'disabled'},'Save'); var save = Builder.node('input',{id: 'button_save',type: 'button', value:'Save', style: 'font: 1.2em Verdana, Helvetica, Arial, sans-serif'}); Event.observe(save,'click',saveForm); var cancel = Builder.node('input',{id: 'button_cancel',type: 'button',value:'Cancel', style: 'font: 1.2em Verdana, Helvetica, Arial, sans-serif'}); Event.observe(cancel,'click',cancelForm); var spacer = document.createTextNode('\u00a0\u00a0'); var buttons = $('buttons'); if (navigator.platform.toLowerCase().indexOf('mac') != -1) { // We're on mac buttons.appendChild(cancel); buttons.appendChild(spacer); buttons.appendChild(save); } else { //###TODO What about all them unix variants? buttons.appendChild(save); buttons.appendChild(spacer); buttons.appendChild(cancel); } } var advanced = Builder.node('a',{href: 'javascript://',id:'basic_config_button'},'Show basic config'); Event.observe(advanced,'click',Config._showBasicConfig); var basic = Builder.node('a',{href: 'javascript://',id:'advanced_config_button'},'Show advanced config'); Event.observe(basic,'click',Config._showAdvancedConfig); if (Cookie.getVar('show_advanced_config')) { basic.style.display = 'none'; } else { advanced.style.display = 'none'; } var div = $('toggle_basic_advanced'); div.appendChild(advanced); div.appendChild(basic); }, _buildItem: function(itemId) { var frag = document.createElement('div'); var href; var item = ConfigXML.getItem(itemId); switch(item.type) { case 'text': if (item.multiple) { var values = ConfigInitialValues.getValue(itemId); if (!values || values.length === 0) { values = ['']; } // var parentSpan = Builder.node('span'); values.each(function (val,i) { var div = document.createElement('div'); // Crappy IE wants a width on floated elements (or maybe it was w3c requiring it) div.style.width = '60em'; div.appendChild(BuildElement.input(itemId+i,itemId, item.name, val || item.default_value || '', item.size || 20, item.short_description, '')); // if (item.browse) { // href = Builder.node('a',{href: 'javascript://'},'Browse'); // Event.observe(href,'click',Config._browse); // div.appendChild(href); // } div.appendChild(document.createTextNode('\u00a0\u00a0')); href = Builder.node('a',{style: 'width: 6em;',href: 'javascript://'},'Remove'); Event.observe(href,'click',Config._removeItemEvent); div.appendChild(href); div.appendChild(Builder.node('br')); frag.appendChild(div); }); // This is used by cancelForm to find out how // many options a multiple group has // frag.appendChild(parentSpan); href = Builder.node('a',{href:'javascript://',className:'addItemHref'},item.add_item_label); frag.appendChild(href); Event.observe(href,'click',Config._addItemEvent); frag.appendChild(Builder.node('div',{style:'clear: both'})); } else { frag.appendChild(BuildElement.input(itemId,itemId, item.name, ConfigInitialValues.getValue(itemId) || item.default_value || '', item.size || 20, item.short_description, '')); // if (item.browse) { // href = Builder.node('a',{href: 'javascript://'},'Browse'); // Event.observe(href,'click',Config._browse); // frag.appendChild(href); // } // frag.appendChild(Builder.node('br')); } break; case 'select': frag.appendChild(BuildElement.select(itemId, item.name, item.options, ConfigInitialValues.getValue(itemId) || item.default_value, item.short_description)); // frag.appendChild(Builder.node('br')); break; default: alert('This should not happen (1)'); break; } if (!Cookie.getVar('show_advanced_config') && item.advanced) { frag.style.display = 'none'; } frag.style.clear = 'both'; return frag; }, _addItemEvent: function (e) { var div = Event.element(e).previousSibling; Config._addItem(div); }, _addItem: function(div) { var newSpan = div.cloneNode(true); var id = newSpan.getElementsByTagName('input')[0].id; var num = parseInt(id.match(/\d+$/)); num++; id = id.replace(/\d+$/,'') + num; newSpan.getElementsByTagName('label')[0].setAttribute('for',id); newSpan.getElementsByTagName('input')[0].id = id; newSpan.getElementsByTagName('input')[0].value = ''; newSpan.style.display = 'none'; var hrefs = newSpan.getElementsByTagName('a'); if ('Netscape' == navigator.appName) { // Firefox et al doesn't copy registered events on an element deep clone // Don't know if that is w3c or if IE has it right if (hrefs.length == 1) { Event.observe(hrefs[0],'click',Config._removeItemEvent); } else { Event.observe(hrefs[0],'click',Config._browse); Event.observe(hrefs[1],'click',Config._removeItemEvent); } } div.parentNode.insertBefore(newSpan,div.nextSibling); Effect.BlindDown(newSpan,{duration: 0.2}); }, _removeItemEvent: function (e) { var div = Event.element(e).parentNode; Config._removeItem(div); }, _removeItem: function(div,noAnimation) { if (div.parentNode.getElementsByTagName('input').length > 1) { if (noAnimation) { // cancelForm uses a loop to delete elements, the loop can't wait // for Effect.BlindUp to finish Element.remove(div); } else { Effect.BlindUp(div,{duration: 0.2, afterFinish: function (){Element.remove(div);}}); } } else { div.getElementsByTagName('input')[0].value=''; } }, _browse: function(e) { alert('Here goes UI to browse for files and dirs'); }, _showAdvancedConfig: function (e) { Element.toggle('advanced_config_button'); Element.toggle('basic_config_button'); Cookie.setVar('show_advanced_config','true',30); ConfigXML.getAllAdvancedSections().each(function (sectionName) { Effect.BlindDown(ConfigXML.getSectionId(sectionName)); }); ConfigXML.getAllItems().each(function (item) { if (item.advanced) { var element = $(item.id); if (!element) { // Handle options with multiple values $A(document.getElementsByName(item.id)).each(function (el) { Effect.BlindDown(el.parentNode.parentNode); }); } else { Effect.BlindDown(element.parentNode); } } }); }, _showBasicConfig: function (e) { Element.toggle('advanced_config_button'); Element.toggle('basic_config_button'); Cookie.removeVar('show_advanced_config'); ConfigXML.getAllAdvancedSections().each(function (sectionName) { Effect.BlindUp(ConfigXML.getSectionId(sectionName)); }); ConfigXML.getAllItems().each(function (item) { if (item.advanced) { var element = $(item.id); if (!element) { // Handle options with multiple values $A(document.getElementsByName(item.id)).each(function (el) { Effect.BlindUp(el.parentNode.parentNode); }); } else { Effect.BlindUp(element.parentNode); } } }); } }; var BuildElement = { input: function(id,name,displayName,value,size,short_description,long_description) { var frag = document.createDocumentFragment(); var label = document.createElement('label'); label.setAttribute('for',id); label.appendChild(document.createTextNode(displayName)); frag.appendChild(label); var input; if (/KHTML/.test(navigator.userAgent) && (80 == size)) { // Safari & Konqueror input fields gets to long // This is a runner up in the ugly bug fix contest size = 60; } if (Config.isWritable) { input = Builder.node('input',{id: id,name: name,className: 'text', value: value,size: size}); } else { input = Builder.node('input',{id: id,name: name,className: 'text', value: value,size: size, disabled: 'disabled'}); } if (/KHTML/.test(navigator.userAgent)) { // Safari & Konqueror doesn't do font: icon on inputs and selects input.style.fontSize = 'x-small'; } frag.appendChild(input); frag.appendChild(document.createTextNode('\u00a0')); if (short_description) { frag.appendChild(document.createTextNode(short_description)); } return frag; }, select: function(id,displayName,options,value,short_description,long_description) { var frag = document.createDocumentFragment(); var label = document.createElement('label'); label.setAttribute('for',id); label.appendChild(document.createTextNode(displayName)); frag.appendChild(label); var select = Builder.node('select',{id: id,name: id,size: 1}); if (!Config.isWritable) { select.disabled = 'disabled'; } $A(options).each(function (option) { select.appendChild(Builder.node('option',{value: option.value}, option.label)); }); select.value = value; if (/KHTML/.test(navigator.userAgent)) { // Safari & Konqueror doesn't do font: icon on inputs and selects select.style.fontSize = 'x-small'; } frag.appendChild(select); frag.appendChild(document.createTextNode('\u00a0')); if (short_description) { frag.appendChild(document.createTextNode(short_description)); } return frag; } }; function saved(req) { if (g_messageTimeout) { window.clearTimeout(g_messageTimeout); } if ('200' == Element.textContent(req.responseXML.getElementsByTagName('status')[0])) { $('messages').innerHTML = 'Saved'; Effect.Appear('messages',{duration: 0.2}); g_messageTimeout = window.setTimeout(function(){ Effect.Fade('messages',{duration: 0.2}); },3000); } else { $('messages').innerHTML = 'Error: ' + req.responseText; Effect.Appear('messages',{duration: 0.2}); } } function saveForm() { var postVars = []; var multiple = {}; $A($('theform').getElementsByTagName('select')).each(function (select) { if (DEBUG) { debug(select.id,select.value); } else { if (select.value != ConfigInitialValues.getValue(select.id)) { postVars.push(Form.Element.serialize(select.id)); } } }); $A($('theform').getElementsByTagName('input')).each(function (input) { if (ConfigXML.getItem(input.name).multiple) { var value = encodeURIComponent(input.value.replace(/,/g,',,')); if (multiple[input.name]) { multiple[input.name].push(value); } else { multiple[input.name] = [value]; } } else { if (DEBUG) { debug(input.id,input.value); } else { if (input.value != ConfigInitialValues.getValue(input.id)) { postVars.push(Form.Element.serialize(input.id)); ConfigInitialValues.setValue(input.id,input.value); } } } }); $H(multiple).each(function (item) { if (DEBUG) { debug(item.key,item.value.join(',')); } else { var currentValue = item.value.join(','); var initialValue = ConfigInitialValues.getValue(item.key).collect(function (value) { return encodeURIComponent(value.replace(/,/g,',,')); }); if (currentValue != initialValue) { postVars.push(item.key + '=' + currentValue); ConfigInitialValues.setValue(item.key,item.value.collect(function (value) { return decodeURIComponent(value).replace(/,,/g,','); })); } } }); if (DEBUG) { return; } if (postVars.length > 0 ) { new Ajax.Request('xml-rpc?method=updateconfig', {method: 'post', parameters: postVars.join('&'), onComplete: saved}); } function debug(id,value) { var getArr = []; var getString; if ('validate' == DEBUG) { var a = id.split(':'); getArr.push('section='+encodeURIComponent(a[0])); getArr.push('key='+encodeURIComponent(a[1])); getArr.push('value='+encodeURIComponent(value)); getArr.push('verify_only=1'); getString = 'xml-rpc?method=setconfig&' + getArr.join('&'); } else { getString = 'xml-rpc?method=updateconfig&' + Form.Element.serialize(id); } var output = id + '=' + value; new Ajax.Request(getString, {method: 'get', onComplete: function(req){ var errorString = Element.textContent(req.responseXML.getElementsByTagName('statusstring')[0]); if (errorString != 'Success') { console.log(output + ' => ' + errorString); } }}); } } function cancelForm() { ConfigXML.getAllItems().each(function (item) { if (item.multiple) { var values = ConfigInitialValues.getValue(item.id); if (!values || values.length === 0) { values = ['']; } var initialValuesCount = values.length; var currentElements = document.getElementsByName(item.id); var i=0; while (initialValuesCount < currentElements.length) { i++; if (i > 10) { alert('Getting dizzy; too many turns in this loop (silly errormessage 1)'); return; } Config._removeItem(currentElements[0].parentNode,'noAnimation'); } while (initialValuesCount > currentElements.length) { i++; if (i > 10) { alert('An important part came off (silly errormessage 2)'); return; } Config._addItem(currentElements[currentElements.length-1].parentNode); } values.each(function (val,i){ currentElements[i].value = val; }); } else { //###TODO potential error a select without a default value $(item.id).value = ConfigInitialValues.getValue(item.id) || item.default_value || ''; } $('messages').innerHTML = 'Cancelled'; if (g_messageTimeout) { window.clearTimeout(g_messageTimeout); } Effect.Appear('messages',{duration: 0.2}); g_messageTimeout = window.setTimeout(function(){ Effect.Fade('messages',{duration: 0.2}); },3000); }); return; }