UNPKG

jcms

Version:

un cms utilisant node.js et mongoDB

488 lines (436 loc) 16.7 kB
(function($) { // ie hack, http://stackoverflow.com/a/10183573/329700 if(!(window.console && console.log)) { console = { log: function(){}, debug: function(){}, info: function(){}, warn: function(){}, error: function(){} }; } var DEFAULT_SEPARATOR = "=>"; /** * Return a jQuery element for a save button */ var getSaveButton = function() { var button = document.createElement('button'); button.setAttribute('class', 'hulk-save'); button.innerHTML = 'Save'; return button; }; /********************** Handlers ******************************/ var attachSaveHandler = function(button, callback) { $(button).on('click', callback); }; var attachCollapseHandler = function(button) { $(button).on('click', function(e) { e.preventDefault(); var value = $(button).next(); if ($(button).hasClass('collapsed')) { // Expand it $(button).text('Collapse'); $(value).slideDown('slow', function() { $(button).removeClass('collapsed'); }); } else { $(value).slideUp('slow', function() { $(button).addClass('collapsed'); $(button).text('Expand'); }); } }); }; /** * Add a handler to append a new element as HTML to the list */ var attachAddArrayElementHandler = function(button, options) { $(button).on('click', function(e) { e.preventDefault(); var elementHTML = createArrayElementHTML("", options); $(button).before(elementHTML); }); }; /** * Add a handler to append a new dictionary as HTML to the list * * XXX: consider refactoring this with the function above */ var attachAddKeyValuePairElementHandler = function(button, options) { $(button).on('click', function(e) { e.preventDefault(); var elementHTML = createArrayElementHTML({"": ""}, options); $(button).before(elementHTML); }); }; var attachKeyValuePairHandler = function(button, options) { $(button).on('click', function(e) { e.preventDefault(); var elementHTML = createKeyValuePairHTML("", "", options); $(button).before(elementHTML); }); }; /********************** Utilities ******************************/ var getOptionOrDefault = function(options, optionName, defaultValue) { if (typeof options === "undefined") { return defaultValue; } if (typeof options[optionName] === "undefined") { return defaultValue; } return options[optionName]; }; /** * Detect whether a string is a number-ish value * * @param n string * @return boolean Whether the string is a number */ var isNumber = function(n) { return !isNaN(parseFloat(n)) && isFinite(n); }; /** * Detect whether an object is a dictionary (eg, {}) instead of a list or * another data type * * Implementation taken from goog.typeOf here: * docs.closure-library.googlecode.com/git/closure_goog_base.js.source.html */ var isDictionary = function(obj) { return typeof obj === "object" && ! (obj instanceof Array); }; /** * Detect whether a list is a list of dictionaries * * Of course in Javascript a list can combine any types, in this case we are * strict and care only if the list is made up entirely of dictionaries. You * shouldn't mix types in a list. */ var isArrayOfDictionaries = function(list) { try { return list.filter(isDictionary).length === list.length; } catch (TypeError) { return false; } }; /********* These functions take JSON and return HTML **********/ /** * Create HTML for a map key/value pair */ var createKeyValuePairHTML = function(key, value, options) { var separator = getOptionOrDefault( options, "separator", DEFAULT_SEPARATOR); var depth = getOptionOrDefault(options, "depth", -1); var pair = $(document.createElement('div')); pair.addClass('hulk-map-pair'); var keyHTML = $(document.createElement('input')); keyHTML.addClass('hulk-map-key'); keyHTML.attr('value', key); pair.append(keyHTML); var separatorElement = $(document.createElement('p')); separatorElement.addClass('hulk-separator'); separatorElement.text(separator); pair.append(separatorElement); var optionsCopy = jQuery.extend(true, {}, options); if (depth > 0) { optionsCopy.depth = depth - 1; } var valueHTML = convertJSONToHTML(value, optionsCopy, key); valueHTML.addClass('hulk-map-value-container'); if (valueHTML.children('.hulk-map-pair, .hulk-array-element').length > 0) { var button = $(document.createElement('button')); button.addClass('hulk-collapse-item'); if (depth !== 0) { button.text("Collapse"); attachCollapseHandler(button); pair.append(button); } else { button.addClass('collapsed'); button.text("Expand"); valueHTML.hide(); attachCollapseHandler(button); pair.append(button); } } pair.append(valueHTML); return pair; }; var createMapHTML = function(map, options, optionalKey) { var mapHTML = $(document.createElement('div')); mapHTML.addClass('hulk-map'); // ugh, http://stackoverflow.com/q/5467129/329700 var key; var keys = []; for (key in map) { if (map.hasOwnProperty(key)) { keys.push(key); } } keys.sort(); for (var j = 0; j < keys.length; j++) { key = keys[j]; var value = map[key]; var pair = createKeyValuePairHTML(key, value, options); mapHTML.append(pair); } var addPairElement = $(document.createElement('button')); addPairElement.addClass('hulk-map-add-pair'); var text = getAddElementText("key/value pair", optionalKey); addPairElement.text(text); attachKeyValuePairHandler(addPairElement, options); mapHTML.append(addPairElement); return mapHTML; }; /** * Create HTML for a JSON array element * * @return a jQuery object for the element, plus what's inside the element */ var createArrayElementHTML = function(element, options) { var elementHTML = $(document.createElement('div')); elementHTML.addClass('hulk-array-element'); elementHTML.html(convertJSONToHTML(element, options)); return elementHTML; }; var getAddElementText = function(elementName, optionalKey) { var type = typeof optionalKey; var maxKeyLength = 30; if (type === "undefined" || type !== "string") { return ["Add new ", elementName].join(""); } if (optionalKey.length > maxKeyLength) { return [ "Add new ", elementName, " to ", optionalKey.substring(0, maxKeyLength), "..." ].join(""); } else { return [ "Add new ", elementName, " to ", optionalKey ].join(""); } }; var createArrayHTML = function(data, options, optionalKey) { var array = $(document.createElement('div')); array.addClass('hulk-array'); for (var i = 0; i < data.length; i++) { var elementHTML = createArrayElementHTML(data[i], options); array.append(elementHTML); } // If it's a list of dictionaries, add an option to add // a dictionary. var text; if (isArrayOfDictionaries(data)) { var addPairElement = $(document.createElement('button')); addPairElement.addClass('hulk-array-add-pair'); text = getAddElementText("object", optionalKey); addPairElement.text(text); attachAddKeyValuePairElementHandler(addPairElement, options); array.append(addPairElement); } // } else { // // Otherwise, you can only add new values. // var addRowElement = $(document.createElement('button')); // addRowElement.addClass('hulk-array-add-row'); // text = getAddElementText("element", optionalKey); // addRowElement.text(text); // attachAddArrayElementHandler(addRowElement, options); // array.append(addRowElement); // } return array; }; /** * Converts a simple JSON value (string, boolean, number) into HTML * * @return jQuery */ var createInputHTML = function(input, type) { var maxInputTextLength = 80; var valueInput; if (type === "string" && input.length > maxInputTextLength) { valueInput = $(document.createElement('textarea')); // XXX Make this configurable? valueInput.attr('rows', 7); valueInput.attr('cols', 80); valueInput.addClass('hulk-input-textarea'); } else if (type === "boolean") { valueInput = $(document.createElement('select')); var trueOption = $(document.createElement('option')); trueOption.attr('value', 'true').text('true'); var falseOption = $(document.createElement('option')); falseOption.attr('value', 'false').text('false'); valueInput.append(trueOption); valueInput.append(falseOption); valueInput.addClass('hulk-input-select'); } else { valueInput = $(document.createElement('input')); valueInput.addClass('hulk-input-text'); } valueInput.addClass('hulk-map-value'); if (type === "boolean") { valueInput.val(input.toString()); } else { valueInput.val(input); } return valueInput; }; /** * Convert a JSON object into HTML * * This should handle most JSON objects, per the specification here: * http://www.json.org/ * * This function calls itself recursively * * @param object data the JSON object to convert * @param object|undefined options the user specified options * @param string optionalKey for dictionary values, pass in the associated key */ var convertJSONToHTML = function(data, options, optionalKey) { var type = typeof data; // typeof null === "object", so we compare directly against null if (type === "string" || type === "number" || type === "boolean" || data === null) { return createInputHTML(data, type); } // XXX: the JSON specification allows for fractions and exponents. // Handle them here. // javascript you're drunk. http://stackoverflow.com/a/4775737/329700 if (Object.prototype.toString.call(data) === '[object Array]') { return createArrayHTML(data, options, optionalKey); } // Now that we've covered the other cases, only dictionaries should be // left, in theory. return createMapHTML(data, options, optionalKey); }; /************ these functions take HTML and return JSON ***************/ /** * Take a value and try to parse it into JSON data types * * For example, "true" => true * * @param string value * @param object|undefined options * @return The returned data type */ var parseTextInput = function(value, options) { // XXX: the JSON specification allows for fractions and exponents, // handle them here. if (isNumber(value)) { return parseFloat(value); } if (value === "true") { return true; } if (value === "false") { return false; } /** * Note: there's some data loss here as we cannot detect between the * empty string and null. In theory we could attach a data-* attribute * to the input and use that but you'd still break if the user voided a * field while editing the JSON. */ if (value === null || value === "null") { return null; } if (value.length === 0) { var emptyString = getOptionOrDefault(options, "emptyString", false); return emptyString === true ? "" : null; } return value; }; /** * this function calls itself recursively * * input: a JQuery object (the editor in JSON) * output: a JSON object */ var reassembleJSON = function(html, options) { var mapChildren = html.children('.hulk-map'); if (mapChildren.length > 0) { return reassembleJSON(mapChildren, options); } var mapItems = html.children('.hulk-map-pair'); if (mapItems.length > 0) { var d = {}; mapItems.each(function(index, element) { var $element = $(element); var key = $element.children('.hulk-map-key'); // XXX if multiple elements have the same key, last one wins. // what should actually be done here? warn? if (key.val() === "") { return; } d[key.val()] = reassembleJSON( $element.children('.hulk-map-value-container'), options); }); return d; } var arrayChildren = html.children('.hulk-array'); if (arrayChildren.length > 0) { return reassembleJSON(arrayChildren, options); } if (html.hasClass('hulk-array')) { var array = []; html.children('.hulk-array-element').each(function(index, element) { array.push(reassembleJSON($(element), options)); }); return array; } if (html.hasClass('hulk-map-value')) { var smartParsing = getOptionOrDefault(options, "smartParsing", true); if (!smartParsing) { return html.val(); } return parseTextInput(html.val(), options); } // hack, merge this with the above conditional var valueChild = html.children('.hulk-map-value'); if (valueChild.length) { return reassembleJSON(valueChild, options); } if (html.hasClass('hulk-map-value-container')) { return reassembleJSON(html.children('.hulk-map-value'), options); } return {}; }; /********************* Exported Functions **********************/ $.hulk = function(selector, data, callback, options) { if (isDictionary(callback)) { console.warn("Dictionary " + JSON.stringify(callback) + " passed " + "as the callback (3rd) argument, probably meant to pass it " + "as the options (4th) argument"); }; // get option settings var $element = $(selector); $element.addClass('hulk'); if ($element.length === 0) { // XXX console doesn't always exist console.error("Attempting to hulk-ify element with selector " + selector + " failed because the element does not exist. " + "Quitting"); return; } var html = convertJSONToHTML(data, options); $element.html(html); var showSaveButton = getOptionOrDefault(options, "showSaveButton", true); if (showSaveButton) { var button = getSaveButton(); attachSaveHandler(button, function(event) { var newData = reassembleJSON($element.children(), options); callback(newData); event.preventDefault(); }); $element.append(button); } return $element; }; $.hulkSmash = function(selector, options) { return reassembleJSON($(selector), options); }; }(jQuery));