/** * Vanilla's legacy javascript core. * * @copyright 2009-2018 Vanilla Forums Inc. * @license GPL-2.0-only */ // Global vanilla library function. (function(window, $) { // Prevent auto-execution of scripts when no explicit dataType was provided // See https://github.com/jquery/jquery/issues/2432#issuecomment-403761229 jQuery.ajaxPrefilter(function(s) { if (s.crossDomain) { s.contents.script = false; } }); var Vanilla = function() { }; Vanilla.fn = Vanilla.prototype; if (!window.console) window.console = { log: function() { } }; Vanilla.scrollTo = function(q) { var top = $(q).offset().top; window.scrollTo(0, top); return false; }; // Add a stub for embedding. Vanilla.parent = function() { }; Vanilla.parent.callRemote = function(func, args, success, failure) { console.log("callRemote stub: " + func, args); }; window.gdn = window.gdn || {}; window.Vanilla = Vanilla; gdn.getMeta = function(key, defaultValue) { if (gdn.meta[key] === undefined) { return defaultValue; } else { return gdn.meta[key]; } }; gdn.setMeta = function(key, value) { gdn.meta[key] = value; }; var keyString = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; // See http://ecmanaut.blogspot.de/2006/07/encoding-decoding-utf8-in-javascript.html var uTF8Encode = function(string) { return decodeURI(encodeURIComponent(string)); }; // See http://ecmanaut.blogspot.de/2006/07/encoding-decoding-utf8-in-javascript.html var uTF8Decode = function(string) { return decodeURIComponent(escape(string)); }; $.extend({ // private property keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", base64Encode: function(input) { var output = ""; var chr1, chr2, chr3, enc1, enc2, enc3, enc4; var i = 0; input = uTF8Encode(input); while (i < input.length) { chr1 = input.charCodeAt(i++); chr2 = input.charCodeAt(i++); chr3 = input.charCodeAt(i++); enc1 = chr1 >> 2; enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); enc4 = chr3 & 63; if (isNaN(chr2)) { enc3 = enc4 = 64; } else if (isNaN(chr3)) { enc4 = 64; } output = output + keyString.charAt(enc1) + keyString.charAt(enc2) + keyString.charAt(enc3) + keyString.charAt(enc4); } return output; }, base64Decode: function(input) { var output = ""; var chr1, chr2, chr3; var enc1, enc2, enc3, enc4; var i = 0; input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); while (i < input.length) { enc1 = keyString.indexOf(input.charAt(i++)); enc2 = keyString.indexOf(input.charAt(i++)); enc3 = keyString.indexOf(input.charAt(i++)); enc4 = keyString.indexOf(input.charAt(i++)); chr1 = (enc1 << 2) | (enc2 >> 4); chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); chr3 = ((enc3 & 3) << 6) | enc4; output = output + String.fromCharCode(chr1); if (enc3 != 64) { output = output + String.fromCharCode(chr2); } if (enc4 != 64) { output = output + String.fromCharCode(chr3); } } output = uTF8Decode(output); return output; } }); /** * Takes a jQuery function that updates the DOM and the HTML to add. Converts the html to a jQuery object * and then adds it to the DOM. Triggers 'contentLoad' to allow javascript manipulation of the new DOM elements. * * @param func The jQuery function name. * @param html The html to add. */ var funcTrigger = function(func, html) { this.each(function() { var $elem = $($.parseHTML(html + '')); // Typecast html to a string and create a DOM node $(this)[func]($elem); $elem.trigger('contentLoad'); }); return this; }; $.fn.extend({ appendTrigger: function(html) { return funcTrigger.call(this, 'append', html); }, beforeTrigger: function(html) { return funcTrigger.call(this, 'before', html); }, afterTrigger: function(html) { return funcTrigger.call(this, 'after', html); }, prependTrigger: function(html) { return funcTrigger.call(this, 'prepend', html); }, htmlTrigger: function(html) { funcTrigger.call(this, 'html', html); }, replaceWithTrigger: function(html) { return funcTrigger.call(this, 'replaceWith', html); } }); $(document).ajaxComplete(function(event, jqXHR, ajaxOptions) { var csrfToken = jqXHR.getResponseHeader("X-CSRF-Token"); if (csrfToken) { gdn.setMeta("TransientKey", csrfToken); $("input[name=TransientKey]").val(csrfToken); } }); // Hook into form submissions. Replace element body with server response when we're in a .js-form. $(document).on("contentLoad", function (e) { $("form", e.target).submit(function (e) { var $form = $(this); // Traverse up the DOM, starting from the form that triggered the event, looking for the first .js-form. var $parent = $form.closest(".js-form"); // Bail if we aren't in a .js-form. if ($parent.length === 0) { return; } // Hijack this submission. e.preventDefault(); // An object containing extra data that should be submitted along with the form. var data = { DeliveryType: "VIEW" }; var submitButton = $form.find("input[type=submit]:focus").get(0); if (submitButton) { data[submitButton.name] = submitButton.name; } // Send the request, expect HTML and hope for the best. $form.ajaxSubmit({ data: data, dataType: "html", success: function (data, textStatus, jqXHR) { $parent.html(data).trigger('contentLoad'); } }); }); }); $(document).on("contentLoad", function (e) { // Setup AJAX filtering for flat category module. // Find each flat category module container, if any. $(".BoxFlatCategory", e.target).each(function(index, value){ // Setup the constants we'll need to perform the lookup for this module instance. var container = value; var categoryID = $("input[name=CategoryID]", container).val(); var limit = parseInt($("input[name=Limit]", container).val()); // If we don't even have a category, don't bother setting up filtering. if (typeof categoryID === "undefined") { return; } // limit was parsed as an int when originally defined. If it isn't a valid value now, default to 10. if (isNaN(limit) || limit < 1) { limit = 10; } // Anytime someone types something into the search box in this instance's container... $(container).on("keyup", ".SearchForm .InputBox", function(filterEvent) { var url = gdn.url("module/flatcategorymodule/vanilla"); // ...perform an AJAX request, replacing the current category data with the result's data. jQuery.get( gdn.url("module/flatcategorymodule/vanilla"), { categoryID: categoryID, filter: filterEvent.target.value, limit: limit }, function(data, textStatus, jqXHR) { $(".FlatCategoryResult", container).replaceWith($(".FlatCategoryResult", data)); } ) }); }); // A vanilla JS event wrapper for the contentLoad event so that the new framework can handle it. $(document).on("contentLoad", function(e) { var event = document.createEvent('CustomEvent'); event.initCustomEvent('X-DOMContentReady', true, false, {}); e.target.dispatchEvent(event); }); }); })(window, jQuery); // Stuff to fire on document.ready(). jQuery(document).ready(function($) { /** * @deprecated since Vanilla 2.2 */ $.postParseJson = function(json) { return json; }; gdn.focused = true; gdn.Libraries = {}; $(window).blur(function() { gdn.focused = false; }); $(window).focus(function() { gdn.focused = true; }); // Grab a definition from object in the page gdn.definition = function(definition, defaultVal, set) { if (defaultVal === undefined) defaultVal = definition; if (!(definition in gdn.meta)) { return defaultVal; } if (set) { gdn.meta[definition] = defaultVal; } return gdn.meta[definition]; }; gdn.disable = function(e, progressClass) { var href = $(e).attr('href'); if (href) { $.data(e, 'hrefBak', href); } $(e).addClass(progressClass ? progressClass : 'InProgress').removeAttr('href').attr('disabled', true); }; gdn.enable = function(e) { $(e).attr('disabled', false).removeClass('InProgress'); var href = $.data(e, 'hrefBak'); if (href) { $(e).attr('href', href); $.removeData(e, 'hrefBak'); } }; gdn.elementSupports = function(element, attribute) { var test = document.createElement(element); if (attribute in test) return true; else return false; }; gdn.querySep = function(url) { return url.indexOf('?') == -1 ? '?' : '&'; }; // password strength check gdn.password = function(password, username) { var translations = gdn.definition('PasswordTranslations', 'Too Short,Contains Username,Very Weak,Weak,Ok,Good,Strong').split(','); // calculate entropy var alphabet = 0; if (password.match(/[0-9]/)) alphabet += 10; if (password.match(/[a-z]/)) alphabet += 26; if (password.match(/[A-Z]/)) alphabet += 26; if (password.match(/[^a-zA-Z0-9]/)) alphabet += 31; var natLog = Math.log(Math.pow(alphabet, password.length)); var entropy = natLog / Math.LN2; var response = { pass: false, symbols: alphabet, entropy: entropy, score: 0 }; // reject on length var length = password.length; response.length = length; var requiredLength = gdn.definition('MinPassLength', 6); var requiredScore = gdn.definition('MinPassScore', 2); response.required = requiredLength; if (length < requiredLength) { response.reason = translations[0]; return response; } // password1 == username if (username) { if (password.toLowerCase().indexOf(username.toLowerCase()) >= 0) { response.reason = translations[1]; return response; } } if (entropy < 30) { response.score = 1; response.reason = translations[2]; // very weak } else if (entropy < 40) { response.score = 2; response.reason = translations[3]; // weak } else if (entropy < 55) { response.score = 3; response.reason = translations[4]; // ok } else if (entropy < 70) { response.score = 4; response.reason = translations[5]; // good } else { response.score = 5; response.reason = translations[6]; // strong } return response; }; // Go to notifications if clicking on a user's notification count $('li.UserNotifications a span').click(function() { document.location = gdn.url('/profile/notifications'); return false; }); /** * Add `rel='noopener'` to everything on the page. * * If you really need the linked page to have window.opener, set the `data-allow-opener='true'` on your link. */ $("a[target='_blank']") .filter(":not([rel*='noopener']):not([data-allow-opener='true'])") .each(function() { var $this = $(this); var rel = $this.attr("rel"); if (rel) { $this.attr("rel", rel + " noopener"); } else { $this.attr("rel", "noopener"); } }); // This turns any anchor with the "Popup" class into an in-page pop-up (the // view of the requested in-garden link will be displayed in a popup on the // current screen). if ($.fn.popup) { // Previously, jquery.popup used live() to attach events, even to elements // that do not yet exist. live() has been deprecated. Vanilla upgraded // jQuery to version 1.10.2, which removed a lot of code. Instead, event // delegation will need to be used, which means everywhere that Popup // is called, will need to have a very high up parent delegate to it. //$('a.Popup').popup(); //$('a.PopConfirm').popup({'confirm' : true, 'followConfirm' : true}); $('a.Popup:not(.dashboard a.Popup):not(.Section-Dashboard a.Popup)').popup(); $('a.PopConfirm').popup({'confirm': true, 'followConfirm': true}); } $(document).delegate(".PopupWindow:not(.Message .PopupWindow)", 'click', function() { var $this = $(this); if ($this.hasClass('NoMSIE') && /msie/.test(navigator.userAgent.toLowerCase())) { return; } var width = $this.attr('popupWidth'); width = width ? width : 960; var height = $this.attr('popupHeight'); height = height ? height : 600; var left = (screen.width - width) / 2; var top = (screen.height - height) / 2; var id = $this.attr('id'); var href = $this.attr('href'); if ($this.attr('popupHref')) href = $this.attr('popupHref'); else href += gdn.querySep(href) + 'display=popup'; var win = window.open(href, 'Window_' + id, "left=" + left + ",top=" + top + ",width=" + width + ",height=" + height + ",status=0,scrollbars=0"); if (win) win.focus(); return false; }); // This turns any anchor with the "Popdown" class into an in-page pop-up, but // it does not hijack forms in the popup. if ($.fn.popup) $('a.Popdown').popup({hijackForms: false}); // This turns SignInPopup anchors into in-page popups if ($.fn.popup) $('a.SignInPopup').popup({containerCssClass: 'SignInPopup'}); if ($.fn.popup) $(document).delegate('.PopupClose', 'click', function(event) { var Popup = $(event.target).parents('.Popup'); if (Popup.length) { var PopupID = Popup.prop('id'); $.popup.close({popupId: PopupID}); } }); // Make sure that message dismissalls are ajax'd $(document).delegate('a.Dismiss', 'click', function() { var anchor = this; var container = $(anchor).parent(); var transientKey = gdn.definition('TransientKey'); var data = 'DeliveryType=BOOL&TransientKey=' + transientKey; $.post($(anchor).attr('href'), data, function(response) { if (response == 'TRUE') $(container).fadeOut('fast', function() { $(this).remove(); }); }); return false; }); // This turns any form into a "post-in-place" form so it is ajaxed to save // without a refresh. The form must be within an element with the "AjaxForm" // class. if ($.fn.handleAjaxForm) $('.AjaxForm').handleAjaxForm(); // Handle ToggleMenu toggling and set up default state $('[class^="Toggle-"]').hide(); // hide all toggle containers $('.ToggleMenu a').click(function() { // Make all toggle buttons and toggle containers inactive $(this).parents('.ToggleMenu').find('li').removeClass('Active'); $('[class^="Toggle-"]').hide(); var item = $(this).parents('li'); // Identify the clicked container // The selector of the container that should be revealed. var containerSelector = '.Toggle-' + item.attr('class'); containerSelector = containerSelector.replace(/Handle-/gi, ''); // Reveal the container & make the button active item.addClass('Active'); // Make the clicked form button active $(containerSelector).show(); return false; }); $('.ToggleMenu .Active a').click(); // reveal the currently active item. // Show hoverhelp on hover $('.HoverHelp').hover( function() { $(this).find('.Help').show(); }, function() { $(this).find('.Help').hide(); } ); // If a page loads with a hidden redirect url, go there after a few moments. var redirectTo = gdn.getMeta('RedirectTo', ''); var checkPopup = gdn.getMeta('CheckPopup', false); if (redirectTo !== '') { if (checkPopup && window.opener) { window.opener.location = redirectTo; window.close(); } else { document.location = redirectTo; } } // Make tables sortable if the tableDnD plugin is present. if ($.tableDnD) $("table.Sortable").tableDnD({ onDrop: function(table, row) { var tableId = $($.tableDnD.currentTable).attr('id'); // Add in the transient key for postback authentication var transientKey = gdn.definition('TransientKey'); var data = $.tableDnD.serialize() + '&TableID=' + tableId + '&TransientKey=' + transientKey; var webRoot = gdn.definition('WebRoot', ''); $.post( gdn.url('/utility/sort.json'), data, function(response) { if (response.Result) { $('#' + tableId + ' tbody tr td').effect("highlight", {}, 1000); } } ); } }); // Make sure that the commentbox & aboutbox do not allow more than 1000 characters $.fn.setMaxChars = function(iMaxChars) { $(this).bind('keyup', function() { var txt = $(this).val(); if (txt.length > iMaxChars) $(this).val(txt.substr(0, iMaxChars)); }); }; // Generate a random string of specified length gdn.generateString = function(length) { if (length === undefined) length = 5; var chars = 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%*'; var string = ''; var pos = 0; for (var i = 0; i < length; i++) { pos = Math.floor(Math.random() * chars.length); string += chars.substring(pos, pos + 1); } return string; }; // A simple, fast method of hashing a string. Similar to Java's hash function. // import { hashString } from "@vanilla/utils"; gdn.hashString = function(str) { function hashReduce(prevHash, currVal) { return (prevHash << 5) - prevHash + currVal.charCodeAt(0); } return str.split("").reduce(hashReduce, 0); } // Combine two paths and make sure that there is only a single directory concatenator gdn.combinePaths = function(path1, path2) { if (path1.substr(-1, 1) == '/') path1 = path1.substr(0, path1.length - 1); if (path2.substring(0, 1) == '/') path2 = path2.substring(1); return path1 + '/' + path2; }; gdn.processTargets = function(targets, $elem, $parent) { if (!targets || !targets.length) return; var tar = function(q) { switch (q) { case '!element': return $elem; case '!parent': return $parent; default: return q; } }, item, $target; for (var i = 0; i < targets.length; i++) { item = targets[i]; if (jQuery.isArray(item.Target)) { $target = $(tar(item.Target[0]), tar(item.Target[1])); } else { $target = $(tar(item.Target)); } switch (item.Type) { case 'AddClass': $target.addClass(item.Data); break; case 'Ajax': if (typeof item.Data === 'string') { ajax = { url: item.Data, reprocess: false, data: {} }; } else { ajax = item.Data; } $.ajax({ type: "POST", url: ajax.url, data: ajax.data, success: function (json) { if (!ajax.reprocess) { return; } if (json === null) { json = {}; } var informed = gdn.inform(json); gdn.processTargets(json.Targets, $elem, $parent); } }); break; case 'Append': $target.appendTrigger(item.Data); break; case 'Before': $target.beforeTrigger(item.Data); break; case 'After': $target.afterTrigger(item.Data); break; case 'Highlight': $target.effect("highlight", {}, "slow"); break; case 'Prepend': $target.prependTrigger(item.Data); break; case 'Redirect': window.location.replace(item.Data); break; case 'Refresh': window.location.reload(); break; case 'Remove': $target.remove(); break; case 'RemoveClass': $target.removeClass(item.Data); break; case 'ReplaceWith': $target.replaceWithTrigger(item.Data); break; case 'SlideUp': var removeTarget = false; if ((typeof item.Data === "object" && item.Data !== null) && item.Data.remove !== "undefined") { removeTarget = !!item.Data.remove; } var slideUpComplete = (function (remove) { return function () { if (remove) { $(this).remove(); } } })(removeTarget); $target.slideUp('fast', slideUpComplete); break; case 'SlideDown': $target.slideDown('fast'); break; case 'Text': $target.text(item.Data); break; case 'Trigger': $target.trigger(item.Data); break; case 'Html': $target.htmlTrigger(item.Data); break; case 'Callback': jQuery.proxy(window[item.Data], $target)(); break; case "closePopup": if (typeof $.popup === "function" && typeof $.popup.close === "function") { var popupID = $target.attr("id"); if (popupID) { $.popup.close({ popupId: popupID, sender: $("body").get(0) }) } } break; case "runJob": gdn.runJob(item.Data); } } }; gdn.runJob = function(body) { var date = new Date(); console.log("Processing... (" + date.toLocaleString() + ")"); $.ajax({ contentType: "application/system+jwt", data: body, timeout: 60000, type: "POST", url: gdn.url("/api/v2/calls/run"), success: function (data, textStatus, jqXHR) { if ((typeof data) === "object" && data.statusType) { switch (data.statusType) { case "complete": gdn.informMessage("Job complete."); break; case "incomplete": if ((typeof data.body) === "string") { gdn.runJob(data.body); } else { gdn.informError("Invalid job response."); } } } else { gdn.informError("Invalid job response."); } }, error: function (jqXHR, textStatus, errorThrown) { gdn.informError("An error occurred while performing the job."); } }); } gdn.requires = function(Library) { if (!(Library instanceof Array)) Library = [Library]; var Response = true; $(Library).each(function(i, Lib) { // First check if we already have this library var LibAvailable = gdn.available(Lib); if (!LibAvailable) Response = false; // Skip any libs that are ready or processing if (gdn.Libraries[Lib] === false || gdn.Libraries[Lib] === true) return; // As yet unseen. Try to load gdn.Libraries[Lib] = false; var Src = '/js/' + Lib + '.js'; var head = document.getElementsByTagName('head')[0]; var script = document.createElement('script'); script.type = 'text/javascript'; script.src = Src; head.appendChild(script); }); if (Response) gdn.loaded(null); return Response; }; gdn.loaded = function(Library) { if (Library) gdn.Libraries[Library] = true; $(document).trigger('libraryloaded', [Library]); }; gdn.available = function(Library) { if (!(Library instanceof Array)) Library = [Library]; for (var i = 0; i < Library.length; i++) { var Lib = Library[i]; if (gdn.Libraries[Lib] !== true) return false; } return true; }; gdn.url = function(path) { if (path.indexOf("//") >= 0) return path; // this is an absolute path. var urlFormat = gdn.definition("UrlFormat", "/{Path}"); if (path.substr(0, 1) == "/") path = path.substr(1); if (urlFormat.indexOf("?") >= 0) path = path.replace("?", "&"); return urlFormat.replace("{Path}", path); }; // Fill in placeholders. if (!gdn.elementSupports('input', 'placeholder')) { $('input:text,textarea').not('.NoIE').each(function() { var $this = $(this); var placeholder = $this.attr('placeholder'); if (!$this.val() && placeholder) { $this.val(placeholder); $this.blur(function() { if ($this.val() === '') $this.val(placeholder); }); $this.focus(function() { if ($this.val() == placeholder) $this.val(''); }); $this.closest('form').bind('submit', function() { if ($this.val() == placeholder) $this.val(''); }); } }); } $.fn.popin = function(options) { var settings = $.extend({}, options); this.each(function(i, elem) { var url = $(elem).attr('rel'); var $elem = $(elem); $.ajax({ url: gdn.url(url), data: {DeliveryType: 'VIEW'}, success: function(data) { $elem.html($.parseHTML(data + '')).trigger('contentLoad'); }, complete: function() { $elem.removeClass('Progress TinyProgress InProgress'); if (settings.complete !== undefined) { settings.complete($elem); } } }); }); }; $('.Popin, .js-popin').popin(); // Make poplist items with a rel attribute clickable. $(document).on('click', '.PopList .Item[rel]', function() { window.location.href = $(this).attr('rel'); }); // Add a spinner onclick of buttons with this class $(document).delegate('input.SpinOnClick', 'click', function() { $(this).before(' ').removeClass('SpinOnClick'); }); // Confirmation for item removals $('a.RemoveItem').click(function() { if (!confirm('Are you sure you would like to remove this item?')) { return false; } }); if (window.location.hash === '') { // Jump to the hash if desired. if (gdn.definition('LocationHash', 0) !== 0) { $(window).load(function() { window.location.hash = gdn.definition('LocationHash'); }); } if (gdn.definition('ScrollTo', 0) !== 0) { var scrollTo = $(gdn.definition('ScrollTo')); if (scrollTo.length > 0) { $('html').animate({ scrollTop: scrollTo.offset().top - 10 }); } } } // If a dismissable InformMessage close button is clicked, hide it. $(document).delegate('div.InformWrapper.Dismissable a.Close, div.InformWrapper .js-inform-close', 'click', function() { $(this).parents('div.InformWrapper').fadeOut('fast', function() { $(this).remove(); }); }); gdn.setAutoDismiss = function() { var timerId = $('div.InformMessages').attr('autodismisstimerid'); if (!timerId) { timerId = setTimeout(function() { $('div.InformWrapper.AutoDismiss').fadeOut('fast', function() { $(this).remove(); }); $('div.InformMessages').removeAttr('autodismisstimerid'); }, 7000); $('div.InformMessages').attr('autodismisstimerid', timerId); } }; // Handle autodismissals $(document).on('informMessage', function() { gdn.setAutoDismiss(); }); // Prevent autodismiss if hovering any inform messages $(document).delegate('div.InformWrapper', 'mouseover mouseout', function(e) { if (e.type == 'mouseover') { var timerId = $('div.InformMessages').attr('autodismisstimerid'); if (timerId) { clearTimeout(timerId); $('div.InformMessages').removeAttr('autodismisstimerid'); } } else { gdn.setAutoDismiss(); } }); // Take any "inform" messages out of an ajax response and display them on the screen. gdn.inform = function(response) { if (!response) return false; if (!response.InformMessages || response.InformMessages.length === 0) return false; // If there is no message container in the page, add one var informMessages = $('div.InformMessages'); if (informMessages.length === 0) { $('
').appendTo('body'); informMessages = $('div.InformMessages'); } var wrappers = $('div.InformMessages div.InformWrapper'), css, elementId, sprite, dismissCallback, dismissCallbackUrl; // Loop through the inform messages and add them to the container for (var i = 0; i < response.InformMessages.length; i++) { css = 'InformWrapper'; if (response.InformMessages[i].CssClass) css += ' ' + response.InformMessages[i].CssClass; elementId = ''; if (response.InformMessages[i].id) elementId = response.InformMessages[i].id; sprite = ''; if (response.InformMessages[i].Sprite) { css += ' HasSprite'; sprite = response.InformMessages[i].Sprite; } dismissCallback = response.InformMessages[i].DismissCallback; dismissCallbackUrl = response.InformMessages[i].DismissCallbackUrl; if (dismissCallbackUrl) dismissCallbackUrl = gdn.url(dismissCallbackUrl); try { var message = response.InformMessages[i].Message; var messageID = gdn.hashString(message); var emptyMessage = message === ''; message = '' + message + ''; // Is there a sprite? if (sprite !== '') message = ''; // If the message is dismissable, add a close button if (css.indexOf('Dismissable') > 0) message = '×' + message; message = '