Introducing Pillar Framework

Refactor of pillar-server and pillar-web into a single python package. This
simplifies the overall architecture of pillar applications.

Special thanks @sybren and @venomgfx
This commit is contained in:
2016-08-19 09:19:06 +02:00
parent a5e92e1d87
commit 2c5dc34ea2
232 changed files with 79508 additions and 2232 deletions

View File

@@ -0,0 +1,359 @@
$(document).ready(function() {
/********************
* INITIALIZATION
* *******************/
var HITS_PER_PAGE = 25;
var MAX_VALUES_PER_FACET = 30;
// DOM binding
var $inputField = $('#q');
var $hits = $('#hits');
var $stats = $('#stats');
var $facets = $('#facets');
var $pagination = $('#pagination');
// Templates binding
var hitTemplate = Hogan.compile($('#hit-template').text());
var statsTemplate = Hogan.compile($('#stats-template').text());
var facetTemplate = Hogan.compile($('#facet-template').text());
var sliderTemplate = Hogan.compile($('#slider-template').text());
var paginationTemplate = Hogan.compile($('#pagination-template').text());
// Client initialization
var algolia = algoliasearch(APPLICATION_ID, SEARCH_ONLY_API_KEY);
// Helper initialization
var params = {
hitsPerPage: HITS_PER_PAGE,
maxValuesPerFacet: MAX_VALUES_PER_FACET,
facets: $.map(FACET_CONFIG, function(facet) { return !facet.disjunctive ? facet.name : null; }),
disjunctiveFacets: $.map(FACET_CONFIG, function(facet) { return facet.disjunctive ? facet.name : null; })
};
// Setup the search helper
var helper = algoliasearchHelper(algolia, INDEX_NAME, params);
// Check if we passed hidden facets in the FACET_CONFIG
var result = $.grep(FACET_CONFIG, function(e){ return e.hidden && e.hidden == true; });
for (var i = 0; i < result.length; i++) {
var f = result[i];
helper.addFacetRefinement(f.name, f.value);
}
// Input binding
$inputField.on('keyup change', function() {
var query = $inputField.val();
toggleIconEmptyInput(!query.trim());
helper.setQuery(query).search();
}).focus();
// AlgoliaHelper events
helper.on('change', function(state) {
setURLParams(state);
});
helper.on('error', function(error) {
console.log(error);
});
helper.on('result', function(content, state) {
renderStats(content);
renderHits(content);
renderFacets(content, state);
renderPagination(content);
bindSearchObjects();
renderFirstHit($(hits).children('.search-hit:first'));
});
/************
* SEARCH
* ***********/
function renderFirstHit(firstHit) {
firstHit.addClass('active');
firstHit.find('#search-loading').addClass('active');
var getNode = setTimeout(function(){
$.get('/nodes/' + firstHit.attr('data-hit-id') + '/view', function(dataHtml){
$('#search-hit-container').html(dataHtml);
})
.done(function(){
$('.search-loading').removeClass('active');
$('#search-error').hide();
$('#search-hit-container').show();
clearTimeout(getNode);
})
.fail(function(data){
$('.search-loading').removeClass('active');
$('#search-hit-container').hide();
$('#search-error').show().html('Houston!\n\n' + data.status + ' ' + data.statusText);
});
}, 1000);
};
// Initial search
initWithUrlParams();
helper.search();
function convertTimestamp(timestamp) {
var d = new Date(timestamp * 1000), // Convert the passed timestamp to milliseconds
yyyy = d.getFullYear(),
mm = ('0' + (d.getMonth() + 1)).slice(-2), // Months are zero based. Add leading 0.
dd = ('0' + d.getDate()).slice(-2), // Add leading 0.
time;
time = dd + '/' + mm + '/' + yyyy;
return time;
}
function renderStats(content) {
var stats = {
nbHits: numberWithDelimiter(content.nbHits),
processingTimeMS: content.processingTimeMS,
nbHits_plural: content.nbHits !== 1
};
$stats.html(statsTemplate.render(stats));
}
function renderHits(content) {
var hitsHtml = '';
for (var i = 0; i < content.hits.length; ++i) {
// console.log(content.hits[i]);
var created = content.hits[i]['created'];
if (created) {
content.hits[i]['created'] = convertTimestamp(created);
}
var updated = content.hits[i]['updated'];
if (updated) {
content.hits[i]['updated'] = convertTimestamp(updated);
}
hitsHtml += hitTemplate.render(content.hits[i]);
}
if (content.hits.length === 0) hitsHtml = '<p id="no-hits">We didn\'t find any items. Try searching something else.</p>';
$hits.html(hitsHtml);
}
function renderFacets(content, state) {
// If no results
if (content.hits.length === 0) {
$facets.empty();
return;
}
// Process facets
var facets = [];
for (var facetIndex = 0; facetIndex < FACET_CONFIG.length; ++facetIndex) {
var facetParams = FACET_CONFIG[facetIndex];
if (facetParams.hidden) {
continue
}
var facetResult = content.getFacetByName(facetParams.name);
if (facetResult) {
var facetContent = {};
facetContent.facet = facetParams.name;
facetContent.title = facetParams.title;
facetContent.type = facetParams.type;
if (facetParams.type === 'slider') {
// if the facet is a slider
facetContent.min = facetResult.stats.min;
facetContent.max = facetResult.stats.max;
var valueMin = state.getNumericRefinement(facetParams.name, '>=') || facetResult.stats.min;
var valueMax = state.getNumericRefinement(facetParams.name, '<=') || facetResult.stats.max;
valueMin = Math.min(facetContent.max, Math.max(facetContent.min, valueMin));
valueMax = Math.min(facetContent.max, Math.max(facetContent.min, valueMax));
facetContent.values = [valueMin, valueMax];
} else {
// format and sort the facet values
var values = [];
for (var v in facetResult.data) {
var label = '';
if (v === 'true') { label = 'Yes'; }
else if (v === 'false') { label = 'No'; }
// Remove any underscore from the value
else { label = v.replace(/_/g," "); }
values.push({ label: label, value: v, count: facetResult.data[v], refined: helper.isRefined(facetParams.name, v) });
}
var sortFunction = facetParams.sortFunction || sortByCountDesc;
if (facetParams.topListIfRefined) sortFunction = sortByRefined(sortFunction);
values.sort(sortFunction);
facetContent.values = values.slice(0, 10);
facetContent.has_other_values = values.length > 10;
facetContent.other_values = values.slice(10);
facetContent.disjunctive = facetParams.disjunctive;
}
facets.push(facetContent);
}
}
// Display facets
var facetsHtml = '';
for (var indexFacet = 0; indexFacet < facets.length; ++indexFacet) {
var facet = facets[indexFacet];
if (facet.type && facet.type === 'slider') facetsHtml += sliderTemplate.render(facet);
else facetsHtml += facetTemplate.render(facet);
}
$facets.html(facetsHtml);
}
function renderPagination(content) {
// If no results
if (content.hits.length === 0) {
$pagination.empty();
return;
}
var maxPages = 2;
// Process pagination
var pages = [];
if (content.page > maxPages) {
pages.push({ current: false, number: 1 });
// They don't really add much...
// pages.push({ current: false, number: '...', disabled: true });
}
for (var p = content.page - maxPages; p < content.page + maxPages; ++p) {
if (p < 0 || p >= content.nbPages) {
continue;
}
pages.push({ current: content.page === p, number: (p + 1) });
}
if (content.page + maxPages < content.nbPages) {
// They don't really add much...
// pages.push({ current: false, number: '...', disabled: true });
pages.push({ current: false, number: content.nbPages });
}
var pagination = {
pages: pages,
prev_page: (content.page > 0 ? content.page : false),
next_page: (content.page + 1 < content.nbPages ? content.page + 2 : false)
};
// Display pagination
$pagination.html(paginationTemplate.render(pagination));
}
// Event bindings
function bindSearchObjects() {
// Slider binding
// $('#customerReviewCount-slider').slider().on('slideStop', function(ev) {
// helper.addNumericRefinement('customerReviewCount', '>=', ev.value[0]).search();
// helper.addNumericRefinement('customerReviewCount', '<=', ev.value[1]).search();
// });
// Pimp checkboxes
// $('input[type="checkbox"]').checkbox();
}
// Click binding
$(document).on('click','.show-more, .show-less',function(e) {
e.preventDefault();
$(this).closest('ul').find('.show-more').toggle();
$(this).closest('ul').find('.show-less').toggle();
return false;
});
$(document).on('click','.toggleRefine',function() {
helper.toggleRefine($(this).data('facet'), $(this).data('value')).search();
return false;
});
$(document).on('click','.gotoPage',function() {
helper.setCurrentPage(+$(this).data('page') - 1).search();
$("html, body").animate({scrollTop:0}, '500', 'swing');
return false;
});
$(document).on('click','.sortBy',function() {
$(this).closest('.btn-group').find('.sort-by').text($(this).text());
helper.setIndex(INDEX_NAME + $(this).data('index-suffix')).search();
return false;
});
$(document).on('click','#input-loop',function() {
$inputField.val('').change();
});
// Dynamic styles
$('#facets').on("mouseenter mouseleave", ".button-checkbox", function(e){
$(this).parent().find('.facet_link').toggleClass("hover");
});
$('#facets').on("mouseenter mouseleave", ".facet_link", function(e){
$(this).parent().find('.button-checkbox button.btn').toggleClass("hover");
});
/************
* HELPERS
* ***********/
function toggleIconEmptyInput(isEmpty) {
if(isEmpty) {
$('#input-loop').addClass('glyphicon-loop');
$('#input-loop').removeClass('glyphicon-remove');
}
else {
$('#input-loop').removeClass('glyphicon-loop');
$('#input-loop').addClass('glyphicon-remove');
}
}
function numberWithDelimiter(number, delimiter) {
number = number + '';
delimiter = delimiter || ',';
var split = number.split('.');
split[0] = split[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + delimiter);
return split.join('.');
}
var sortByCountDesc = function sortByCountDesc (a, b) { return b.count - a.count; };
var sortByName = function sortByName (a, b) {
return a.value.localeCompare(b.value);
};
var sortByRefined = function sortByRefined (sortFunction) {
return function (a, b) {
if (a.refined !== b.refined) {
if (a.refined) return -1;
if (b.refined) return 1;
}
return sortFunction(a, b);
};
};
function initWithUrlParams() {
var sPageURL = location.hash;
if (!sPageURL || sPageURL.length === 0) { return true; }
var sURLVariables = sPageURL.split('&');
if (!sURLVariables || sURLVariables.length === 0) { return true; }
var query = decodeURIComponent(sURLVariables[0].split('=')[1]);
$inputField.val(query);
helper.setQuery(query);
for (var i = 2; i < sURLVariables.length; i++) {
var sParameterName = sURLVariables[i].split('=');
var facet = decodeURIComponent(sParameterName[0]);
var value = decodeURIComponent(sParameterName[1]);
helper.toggleRefine(facet, value, false);
}
// Page has to be set in the end to avoid being overwritten
var page = decodeURIComponent(sURLVariables[1].split('=')[1])-1;
helper.setCurrentPage(page);
}
function setURLParams(state) {
var urlParams = '#';
var currentQuery = state.query;
urlParams += 'q=' + encodeURIComponent(currentQuery);
var currentPage = state.page+1;
urlParams += '&page=' + currentPage;
for (var facetRefine in state.facetsRefinements) {
urlParams += '&' + encodeURIComponent(facetRefine) + '=' + encodeURIComponent(state.facetsRefinements[facetRefine]);
}
for (var disjunctiveFacetrefine in state.disjunctiveFacetsRefinements) {
for (var value in state.disjunctiveFacetsRefinements[disjunctiveFacetrefine]) {
urlParams += '&' + encodeURIComponent(disjunctiveFacetrefine) + '=' + encodeURIComponent(state.disjunctiveFacetsRefinements[disjunctiveFacetrefine][value]);
}
}
location.replace(urlParams);
}
});

162
src/scripts/file_upload.js Normal file
View File

@@ -0,0 +1,162 @@
function deleteFile(fileField, newFileId) {
if (newFileId) {
fileField.val(newFileId);
} else {
fileField.val('');
}
}
var current_file_uploads = 0;
function on_file_upload_activated() {
if (current_file_uploads == 0) {
// Disable the save buttons.
$('.button-save')
.addClass('disabled')
.find('a').html('<i class="pi-spin spin"></i> Uploading...');
}
current_file_uploads++;
}
function on_file_upload_finished() {
current_file_uploads = Math.max(0, current_file_uploads-1);
if (current_file_uploads == 0) {
// Restore the save buttons.
$('.button-save')
.removeClass('disabled')
.find('a').html('<i class="pi-check"></i> Save Changes');
}
}
function setup_file_uploader(index, upload_element) {
var $upload_element = $(upload_element);
var container = $upload_element.parent().parent();
var progress_bar = container.find('div.form-upload-progress-bar');
function set_progress_bar(progress, html_class) {
progress_bar.css({
'width': progress + '%',
'display': progress == 0 ? 'none' : 'block'});
progress_bar.removeClass('progress-error progress-uploading progress-processing');
if (!!html_class) progress_bar.addClass(html_class);
}
$upload_element.fileupload({
dataType: 'json',
replaceFileInput: false,
dropZone: container,
formData: {},
beforeSend: function (xhr, data) {
var token = this.fileInput.attr('data-token');
xhr.setRequestHeader('Authorization', 'basic ' + btoa(token + ':'));
statusBarSet('info', 'Uploading File...', 'pi-upload-cloud');
// console.log('Uploading from', upload_element, upload_element.value);
// Clear thumbnail & progress bar.
container.find('.preview-thumbnail').hide();
set_progress_bar(0);
$('body').trigger('file-upload:activated');
},
add: function (e, data) {
var uploadErrors = [];
// Load regex if available (like /^image\/(gif|jpe?g|png)$/i;)
var acceptFileTypes = new RegExp($(this).data('file-format'));
if (data.originalFiles[0]['type'].length && !acceptFileTypes.test(data.originalFiles[0]['type'])) {
uploadErrors.push('Not an accepted file type');
}
// Limit upload size to 1GB
if (data.originalFiles[0]['size'] && data.originalFiles[0]['size'] > 1262485504) {
uploadErrors.push('Filesize is too big');
}
if (uploadErrors.length > 0) {
$(this).parent().parent().addClass('error');
$(this).after(uploadErrors.join("\n"));
} else {
$(this).parent().parent().removeClass('error');
data.submit();
}
},
progressall: function (e, data) {
// Update progressbar during upload
var progress = parseInt(data.loaded / data.total * 100, 10);
// console.log('Uploading', upload_element.value, ': ', progress, '%');
set_progress_bar(Math.max(progress, 2),
progress > 99.9 ? 'progress-processing' : 'progress-uploading'
);
},
done: function (e, data) {
if (data.result.status !== 'ok') {
if (console)
console.log('FIXME, do error handling for non-ok status', data.result);
return;
}
// Ensure the form refers to the correct Pillar file ID.
var pillar_file_id = data.result.file_id;
var $file_id_field = $('#' + $(this).attr('data-field-name'));
if ($file_id_field.val()) {
deleteFile($file_id_field, pillar_file_id);
}
$file_id_field.val(pillar_file_id);
// Ugly workaround: If the asset has the default name, name it as the file
if ($('.form-group.name .form-control').val() == 'New asset') {
var filename = data.files[0].name;
$('.form-group.name .form-control').val(filename);
$('.node-edit-title').html(filename);
}
statusBarSet('success', 'File Uploaded Successfully', 'pi-check');
set_progress_bar(100);
$('body').trigger('file-upload:finished');
},
fail: function (jqXHR, textStatus, errorThrown) {
if (console) {
console.log(textStatus, 'Upload error: ' + errorThrown);
}
statusBarSet(textStatus, 'Upload error: ' + errorThrown, 'pi-attention', 8000);
set_progress_bar(100, 'progress-error');
$('body').trigger('file-upload:finished');
}
});
}
$(function () {
// $('.file_delete').click(function(e){
$('body').unbind('click')
.on('click', '.file_delete', function(e) {
e.preventDefault();
var field_name = '#' + $(this).data('field-name');
var file_field = $(field_name);
deleteFile(file_field);
$(this).parent().parent().hide();
$(this).parent().parent().prev().hide();
})
.on('file-upload:activated', on_file_upload_activated)
.on('file-upload:finished', on_file_upload_finished)
;
function inject_project_id_into_url(index, element) {
// console.log('Injecting ', ProjectUtils.projectId(), ' into ', element);
var url = element.getAttribute('data-url');
url = url.replace('{project_id}', ProjectUtils.projectId());
element.setAttribute('data-url', url);
// console.log('The new element is', element);
}
$('.fileupload')
.each(inject_project_id_into_url)
.each(setup_file_uploader)
;
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,114 @@
(function () {
var output, Converter;
if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module
output = exports;
Converter = require("./Markdown.Converter").Converter;
} else {
output = window.Markdown;
Converter = output.Converter;
}
output.getSanitizingConverter = function () {
var converter = new Converter();
converter.hooks.chain("postConversion", sanitizeHtml);
converter.hooks.chain("postConversion", balanceTags);
return converter;
}
function sanitizeHtml(html) {
return html.replace(/<[^>]*>?/gi, sanitizeTag);
}
// (tags that can be opened/closed) | (tags that stand alone)
var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|iframe|kbd|li|ol(?: start="\d+")?|p|pre|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i;
// <a href="url..." optional title>|</a>
var a_white = /^(<a\shref="((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)*[\]$]+"(\stitle="[^"<>]+")?(\sclass="[^"<>]+")?\s?>|<\/a>)$/i;
// Cloud custom: Allow iframe embed from YouTube, Vimeo and SoundCloud
var iframe_youtube = /^(<iframe(\swidth="\d{1,3}")?(\sheight="\d{1,3}")\ssrc="((https?):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)*[\]$]+"(\sframeborder="\d{1,3}")?(\sallowfullscreen)\s?>|<\/iframe>)$/i;
var iframe_vimeo = /^(<iframe(\ssrc="((https?):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)*[\]$]+"?\swidth="\d{1,3}")?(\sheight="\d{1,3}")?(\sframeborder="\d{1,3}")?(\swebkitallowfullscreen)\s?(\smozallowfullscreen)\s?(\sallowfullscreen)\s?>|<\/iframe>)$/i;
var iframe_soundcloud = /^(<iframe(\swidth="\d{1,3}\%")?(\sheight="\d{1,3}")?(\sscrolling="(?:yes|no)")?(\sframeborder="(?:yes|no)")\ssrc="((https?):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)*[\]$]+"\s?>|<\/iframe>)$/i;
// <img src="url..." optional width optional height optional alt optional title
var img_white = /^(<img\ssrc="(https?:\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)*[\]$]+"(\swidth="\d{1,3}")?(\sheight="\d{1,3}")?(\salt="[^"<>]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i;
function sanitizeTag(tag) {
if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white) || tag.match(iframe_youtube) || tag.match(iframe_vimeo) || tag.match(iframe_soundcloud)) {
return tag;
} else {
return "";
}
}
/// <summary>
/// attempt to balance HTML tags in the html string
/// by removing any unmatched opening or closing tags
/// IMPORTANT: we *assume* HTML has *already* been
/// sanitized and is safe/sane before balancing!
///
/// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593
/// </summary>
function balanceTags(html) {
if (html == "")
return "";
var re = /<\/?\w+[^>]*(\s|$|>)/g;
// convert everything to lower case; this makes
// our case insensitive comparisons easier
var tags = html.toLowerCase().match(re);
// no HTML tags present? nothing to do; exit now
var tagcount = (tags || []).length;
if (tagcount == 0)
return html;
var tagname, tag;
var ignoredtags = "<p><img><br><li><hr>";
var match;
var tagpaired = [];
var tagremove = [];
var needsRemoval = false;
// loop through matched tags in forward order
for (var ctag = 0; ctag < tagcount; ctag++) {
tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1");
// skip any already paired tags
// and skip tags in our ignore list; assume they're self-closed
if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1)
continue;
tag = tags[ctag];
match = -1;
if (!/^<\//.test(tag)) {
// this is an opening tag
// search forwards (next tags), look for closing tags
for (var ntag = ctag + 1; ntag < tagcount; ntag++) {
if (!tagpaired[ntag] && tags[ntag] == "</" + tagname + ">") {
match = ntag;
break;
}
}
}
if (match == -1)
needsRemoval = tagremove[ctag] = true; // mark for removal
else
tagpaired[match] = true; // mark paired
}
if (!needsRemoval)
return html;
// delete all orphaned tags from the string
var ctag = 0;
html = html.replace(re, function (match) {
var res = tagremove[ctag] ? "" : match;
ctag++;
return res;
});
return html;
}
})();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,874 @@
(function () {
// A quick way to make sure we're only keeping span-level tags when we need to.
// This isn't supposed to be foolproof. It's just a quick way to make sure we
// keep all span-level tags returned by a pagedown converter. It should allow
// all span-level tags through, with or without attributes.
var inlineTags = new RegExp(['^(<\\/?(a|abbr|acronym|applet|area|b|basefont|',
'bdo|big|button|cite|code|del|dfn|em|figcaption|',
'font|i|iframe|img|input|ins|kbd|label|map|',
'mark|meter|object|param|progress|q|ruby|rp|rt|s|',
'samp|script|select|small|span|strike|strong|',
'sub|sup|textarea|time|tt|u|var|wbr)[^>]*>|',
'<(br)\\s?\\/?>)$'].join(''), 'i');
/******************************************************************
* Utility Functions *
*****************************************************************/
// patch for ie7
if (!Array.indexOf) {
Array.prototype.indexOf = function(obj) {
for (var i = 0; i < this.length; i++) {
if (this[i] == obj) {
return i;
}
}
return -1;
};
}
function trim(str) {
return str.replace(/^\s+|\s+$/g, '');
}
function rtrim(str) {
return str.replace(/\s+$/g, '');
}
// Remove one level of indentation from text. Indent is 4 spaces.
function outdent(text) {
return text.replace(new RegExp('^(\\t|[ ]{1,4})', 'gm'), '');
}
function contains(str, substr) {
return str.indexOf(substr) != -1;
}
// Sanitize html, removing tags that aren't in the whitelist
function sanitizeHtml(html, whitelist) {
return html.replace(/<[^>]*>?/gi, function(tag) {
return tag.match(whitelist) ? tag : '';
});
}
// Merge two arrays, keeping only unique elements.
function union(x, y) {
var obj = {};
for (var i = 0; i < x.length; i++)
obj[x[i]] = x[i];
for (i = 0; i < y.length; i++)
obj[y[i]] = y[i];
var res = [];
for (var k in obj) {
if (obj.hasOwnProperty(k))
res.push(obj[k]);
}
return res;
}
// JS regexes don't support \A or \Z, so we add sentinels, as Pagedown
// does. In this case, we add the ascii codes for start of text (STX) and
// end of text (ETX), an idea borrowed from:
// https://github.com/tanakahisateru/js-markdown-extra
function addAnchors(text) {
if(text.charAt(0) != '\x02')
text = '\x02' + text;
if(text.charAt(text.length - 1) != '\x03')
text = text + '\x03';
return text;
}
// Remove STX and ETX sentinels.
function removeAnchors(text) {
if(text.charAt(0) == '\x02')
text = text.substr(1);
if(text.charAt(text.length - 1) == '\x03')
text = text.substr(0, text.length - 1);
return text;
}
// Convert markdown within an element, retaining only span-level tags
function convertSpans(text, extra) {
return sanitizeHtml(convertAll(text, extra), inlineTags);
}
// Convert internal markdown using the stock pagedown converter
function convertAll(text, extra) {
var result = extra.blockGamutHookCallback(text);
// We need to perform these operations since we skip the steps in the converter
result = unescapeSpecialChars(result);
result = result.replace(/~D/g, "$$").replace(/~T/g, "~");
result = extra.previousPostConversion(result);
return result;
}
// Convert escaped special characters
function processEscapesStep1(text) {
// Markdown extra adds two escapable characters, `:` and `|`
return text.replace(/\\\|/g, '~I').replace(/\\:/g, '~i');
}
function processEscapesStep2(text) {
return text.replace(/~I/g, '|').replace(/~i/g, ':');
}
// Duplicated from PageDown converter
function unescapeSpecialChars(text) {
// Swap back in all the special characters we've hidden.
text = text.replace(/~E(\d+)E/g, function(wholeMatch, m1) {
var charCodeToReplace = parseInt(m1);
return String.fromCharCode(charCodeToReplace);
});
return text;
}
function slugify(text) {
return text.toLowerCase()
.replace(/\s+/g, '-') // Replace spaces with -
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
.replace(/\-\-+/g, '-') // Replace multiple - with single -
.replace(/^-+/, '') // Trim - from start of text
.replace(/-+$/, ''); // Trim - from end of text
}
/*****************************************************************************
* Markdown.Extra *
****************************************************************************/
Markdown.Extra = function() {
// For converting internal markdown (in tables for instance).
// This is necessary since these methods are meant to be called as
// preConversion hooks, and the Markdown converter passed to init()
// won't convert any markdown contained in the html tags we return.
this.converter = null;
// Stores html blocks we generate in hooks so that
// they're not destroyed if the user is using a sanitizing converter
this.hashBlocks = [];
// Stores footnotes
this.footnotes = {};
this.usedFootnotes = [];
// Special attribute blocks for fenced code blocks and headers enabled.
this.attributeBlocks = false;
// Fenced code block options
this.googleCodePrettify = false;
this.highlightJs = false;
// Table options
this.tableClass = '';
this.tabWidth = 4;
};
Markdown.Extra.init = function(converter, options) {
// Each call to init creates a new instance of Markdown.Extra so it's
// safe to have multiple converters, with different options, on a single page
var extra = new Markdown.Extra();
var postNormalizationTransformations = [];
var preBlockGamutTransformations = [];
var postSpanGamutTransformations = [];
var postConversionTransformations = ["unHashExtraBlocks"];
options = options || {};
options.extensions = options.extensions || ["all"];
if (contains(options.extensions, "all")) {
options.extensions = ["tables", "fenced_code_gfm", "def_list", "attr_list", "footnotes", "smartypants", "strikethrough", "newlines"];
}
preBlockGamutTransformations.push("wrapHeaders");
if (contains(options.extensions, "attr_list")) {
postNormalizationTransformations.push("hashFcbAttributeBlocks");
preBlockGamutTransformations.push("hashHeaderAttributeBlocks");
postConversionTransformations.push("applyAttributeBlocks");
extra.attributeBlocks = true;
}
if (contains(options.extensions, "fenced_code_gfm")) {
// This step will convert fcb inside list items and blockquotes
preBlockGamutTransformations.push("fencedCodeBlocks");
// This extra step is to prevent html blocks hashing and link definition/footnotes stripping inside fcb
postNormalizationTransformations.push("fencedCodeBlocks");
}
if (contains(options.extensions, "tables")) {
preBlockGamutTransformations.push("tables");
}
if (contains(options.extensions, "def_list")) {
preBlockGamutTransformations.push("definitionLists");
}
if (contains(options.extensions, "footnotes")) {
postNormalizationTransformations.push("stripFootnoteDefinitions");
preBlockGamutTransformations.push("doFootnotes");
postConversionTransformations.push("printFootnotes");
}
if (contains(options.extensions, "smartypants")) {
postConversionTransformations.push("runSmartyPants");
}
if (contains(options.extensions, "strikethrough")) {
postSpanGamutTransformations.push("strikethrough");
}
if (contains(options.extensions, "newlines")) {
postSpanGamutTransformations.push("newlines");
}
converter.hooks.chain("postNormalization", function(text) {
return extra.doTransform(postNormalizationTransformations, text) + '\n';
});
converter.hooks.chain("preBlockGamut", function(text, blockGamutHookCallback) {
// Keep a reference to the block gamut callback to run recursively
extra.blockGamutHookCallback = blockGamutHookCallback;
text = processEscapesStep1(text);
text = extra.doTransform(preBlockGamutTransformations, text) + '\n';
text = processEscapesStep2(text);
return text;
});
converter.hooks.chain("postSpanGamut", function(text) {
return extra.doTransform(postSpanGamutTransformations, text);
});
// Keep a reference to the hook chain running before doPostConversion to apply on hashed extra blocks
extra.previousPostConversion = converter.hooks.postConversion;
converter.hooks.chain("postConversion", function(text) {
text = extra.doTransform(postConversionTransformations, text);
// Clear state vars that may use unnecessary memory
extra.hashBlocks = [];
extra.footnotes = {};
extra.usedFootnotes = [];
return text;
});
if ("highlighter" in options) {
extra.googleCodePrettify = options.highlighter === 'prettify';
extra.highlightJs = options.highlighter === 'highlight';
}
if ("table_class" in options) {
extra.tableClass = options.table_class;
}
extra.converter = converter;
// Caller usually won't need this, but it's handy for testing.
return extra;
};
// Do transformations
Markdown.Extra.prototype.doTransform = function(transformations, text) {
for(var i = 0; i < transformations.length; i++)
text = this[transformations[i]](text);
return text;
};
// Return a placeholder containing a key, which is the block's index in the
// hashBlocks array. We wrap our output in a <p> tag here so Pagedown won't.
Markdown.Extra.prototype.hashExtraBlock = function(block) {
return '\n<p>~X' + (this.hashBlocks.push(block) - 1) + 'X</p>\n';
};
Markdown.Extra.prototype.hashExtraInline = function(block) {
return '~X' + (this.hashBlocks.push(block) - 1) + 'X';
};
// Replace placeholder blocks in `text` with their corresponding
// html blocks in the hashBlocks array.
Markdown.Extra.prototype.unHashExtraBlocks = function(text) {
var self = this;
function recursiveUnHash() {
var hasHash = false;
text = text.replace(/(?:<p>)?~X(\d+)X(?:<\/p>)?/g, function(wholeMatch, m1) {
hasHash = true;
var key = parseInt(m1, 10);
return self.hashBlocks[key];
});
if(hasHash === true) {
recursiveUnHash();
}
}
recursiveUnHash();
return text;
};
// Wrap headers to make sure they won't be in def lists
Markdown.Extra.prototype.wrapHeaders = function(text) {
function wrap(text) {
return '\n' + text + '\n';
}
text = text.replace(/^.+[ \t]*\n=+[ \t]*\n+/gm, wrap);
text = text.replace(/^.+[ \t]*\n-+[ \t]*\n+/gm, wrap);
text = text.replace(/^\#{1,6}[ \t]*.+?[ \t]*\#*\n+/gm, wrap);
return text;
};
/******************************************************************
* Attribute Blocks *
*****************************************************************/
// TODO: use sentinels. Should we just add/remove them in doConversion?
// TODO: better matches for id / class attributes
var attrBlock = "\\{[ \\t]*((?:[#.][-_:a-zA-Z0-9]+[ \\t]*)+)\\}";
var hdrAttributesA = new RegExp("^(#{1,6}.*#{0,6})[ \\t]+" + attrBlock + "[ \\t]*(?:\\n|0x03)", "gm");
var hdrAttributesB = new RegExp("^(.*)[ \\t]+" + attrBlock + "[ \\t]*\\n" +
"(?=[\\-|=]+\\s*(?:\\n|0x03))", "gm"); // underline lookahead
var fcbAttributes = new RegExp("^(```[^`\\n]*)[ \\t]+" + attrBlock + "[ \\t]*\\n" +
"(?=([\\s\\S]*?)\\n```[ \\t]*(\\n|0x03))", "gm");
// Extract headers attribute blocks, move them above the element they will be
// applied to, and hash them for later.
Markdown.Extra.prototype.hashHeaderAttributeBlocks = function(text) {
var self = this;
function attributeCallback(wholeMatch, pre, attr) {
return '<p>~XX' + (self.hashBlocks.push(attr) - 1) + 'XX</p>\n' + pre + "\n";
}
text = text.replace(hdrAttributesA, attributeCallback); // ## headers
text = text.replace(hdrAttributesB, attributeCallback); // underline headers
return text;
};
// Extract FCB attribute blocks, move them above the element they will be
// applied to, and hash them for later.
Markdown.Extra.prototype.hashFcbAttributeBlocks = function(text) {
// TODO: use sentinels. Should we just add/remove them in doConversion?
// TODO: better matches for id / class attributes
var self = this;
function attributeCallback(wholeMatch, pre, attr) {
return '<p>~XX' + (self.hashBlocks.push(attr) - 1) + 'XX</p>\n' + pre + "\n";
}
return text.replace(fcbAttributes, attributeCallback);
};
Markdown.Extra.prototype.applyAttributeBlocks = function(text) {
var self = this;
var blockRe = new RegExp('<p>~XX(\\d+)XX</p>[\\s]*' +
'(?:<(h[1-6]|pre)(?: +class="(\\S+)")?(>[\\s\\S]*?</\\2>))', "gm");
text = text.replace(blockRe, function(wholeMatch, k, tag, cls, rest) {
if (!tag) // no following header or fenced code block.
return '';
// get attributes list from hash
var key = parseInt(k, 10);
var attributes = self.hashBlocks[key];
// get id
var id = attributes.match(/#[^\s#.]+/g) || [];
var idStr = id[0] ? ' id="' + id[0].substr(1, id[0].length - 1) + '"' : '';
// get classes and merge with existing classes
var classes = attributes.match(/\.[^\s#.]+/g) || [];
for (var i = 0; i < classes.length; i++) // Remove leading dot
classes[i] = classes[i].substr(1, classes[i].length - 1);
var classStr = '';
if (cls)
classes = union(classes, [cls]);
if (classes.length > 0)
classStr = ' class="' + classes.join(' ') + '"';
return "<" + tag + idStr + classStr + rest;
});
return text;
};
/******************************************************************
* Tables *
*****************************************************************/
// Find and convert Markdown Extra tables into html.
Markdown.Extra.prototype.tables = function(text) {
var self = this;
var leadingPipe = new RegExp(
['^' ,
'[ ]{0,3}' , // Allowed whitespace
'[|]' , // Initial pipe
'(.+)\\n' , // $1: Header Row
'[ ]{0,3}' , // Allowed whitespace
'[|]([ ]*[-:]+[-| :]*)\\n' , // $2: Separator
'(' , // $3: Table Body
'(?:[ ]*[|].*\\n?)*' , // Table rows
')',
'(?:\\n|$)' // Stop at final newline
].join(''),
'gm'
);
var noLeadingPipe = new RegExp(
['^' ,
'[ ]{0,3}' , // Allowed whitespace
'(\\S.*[|].*)\\n' , // $1: Header Row
'[ ]{0,3}' , // Allowed whitespace
'([-:]+[ ]*[|][-| :]*)\\n' , // $2: Separator
'(' , // $3: Table Body
'(?:.*[|].*\\n?)*' , // Table rows
')' ,
'(?:\\n|$)' // Stop at final newline
].join(''),
'gm'
);
text = text.replace(leadingPipe, doTable);
text = text.replace(noLeadingPipe, doTable);
// $1 = header, $2 = separator, $3 = body
function doTable(match, header, separator, body, offset, string) {
// remove any leading pipes and whitespace
header = header.replace(/^ *[|]/m, '');
separator = separator.replace(/^ *[|]/m, '');
body = body.replace(/^ *[|]/gm, '');
// remove trailing pipes and whitespace
header = header.replace(/[|] *$/m, '');
separator = separator.replace(/[|] *$/m, '');
body = body.replace(/[|] *$/gm, '');
// determine column alignments
var alignspecs = separator.split(/ *[|] */);
var align = [];
for (var i = 0; i < alignspecs.length; i++) {
var spec = alignspecs[i];
if (spec.match(/^ *-+: *$/m))
align[i] = ' align="right"';
else if (spec.match(/^ *:-+: *$/m))
align[i] = ' align="center"';
else if (spec.match(/^ *:-+ *$/m))
align[i] = ' align="left"';
else align[i] = '';
}
// TODO: parse spans in header and rows before splitting, so that pipes
// inside of tags are not interpreted as separators
var headers = header.split(/ *[|] */);
var colCount = headers.length;
// build html
var cls = self.tableClass ? ' class="' + self.tableClass + '"' : '';
var html = ['<table', cls, '>\n', '<thead>\n', '<tr>\n'].join('');
// build column headers.
for (i = 0; i < colCount; i++) {
var headerHtml = convertSpans(trim(headers[i]), self);
html += [" <th", align[i], ">", headerHtml, "</th>\n"].join('');
}
html += "</tr>\n</thead>\n";
// build rows
var rows = body.split('\n');
for (i = 0; i < rows.length; i++) {
if (rows[i].match(/^\s*$/)) // can apply to final row
continue;
// ensure number of rowCells matches colCount
var rowCells = rows[i].split(/ *[|] */);
var lenDiff = colCount - rowCells.length;
for (var j = 0; j < lenDiff; j++)
rowCells.push('');
html += "<tr>\n";
for (j = 0; j < colCount; j++) {
var colHtml = convertSpans(trim(rowCells[j]), self);
html += [" <td", align[j], ">", colHtml, "</td>\n"].join('');
}
html += "</tr>\n";
}
html += "</table>\n";
// replace html with placeholder until postConversion step
return self.hashExtraBlock(html);
}
return text;
};
/******************************************************************
* Footnotes *
*****************************************************************/
// Strip footnote, store in hashes.
Markdown.Extra.prototype.stripFootnoteDefinitions = function(text) {
var self = this;
text = text.replace(
/\n[ ]{0,3}\[\^(.+?)\]\:[ \t]*\n?([\s\S]*?)\n{1,2}((?=\n[ ]{0,3}\S)|$)/g,
function(wholeMatch, m1, m2) {
m1 = slugify(m1);
m2 += "\n";
m2 = m2.replace(/^[ ]{0,3}/g, "");
self.footnotes[m1] = m2;
return "\n";
});
return text;
};
// Find and convert footnotes references.
Markdown.Extra.prototype.doFootnotes = function(text) {
var self = this;
if(self.isConvertingFootnote === true) {
return text;
}
var footnoteCounter = 0;
text = text.replace(/\[\^(.+?)\]/g, function(wholeMatch, m1) {
var id = slugify(m1);
var footnote = self.footnotes[id];
if (footnote === undefined) {
return wholeMatch;
}
footnoteCounter++;
self.usedFootnotes.push(id);
var html = '<a href="#fn:' + id + '" id="fnref:' + id
+ '" title="See footnote" class="footnote">' + footnoteCounter
+ '</a>';
return self.hashExtraInline(html);
});
return text;
};
// Print footnotes at the end of the document
Markdown.Extra.prototype.printFootnotes = function(text) {
var self = this;
if (self.usedFootnotes.length === 0) {
return text;
}
text += '\n\n<div class="footnotes">\n<hr>\n<ol>\n\n';
for(var i=0; i<self.usedFootnotes.length; i++) {
var id = self.usedFootnotes[i];
var footnote = self.footnotes[id];
self.isConvertingFootnote = true;
var formattedfootnote = convertSpans(footnote, self);
delete self.isConvertingFootnote;
text += '<li id="fn:'
+ id
+ '">'
+ formattedfootnote
+ ' <a href="#fnref:'
+ id
+ '" title="Return to article" class="reversefootnote">&#8617;</a></li>\n\n';
}
text += '</ol>\n</div>';
return text;
};
/******************************************************************
* Fenced Code Blocks (gfm) *
******************************************************************/
// Find and convert gfm-inspired fenced code blocks into html.
Markdown.Extra.prototype.fencedCodeBlocks = function(text) {
function encodeCode(code) {
code = code.replace(/&/g, "&amp;");
code = code.replace(/</g, "&lt;");
code = code.replace(/>/g, "&gt;");
// These were escaped by PageDown before postNormalization
code = code.replace(/~D/g, "$$");
code = code.replace(/~T/g, "~");
return code;
}
var self = this;
text = text.replace(/(?:^|\n)```([^`\n]*)\n([\s\S]*?)\n```[ \t]*(?=\n)/g, function(match, m1, m2) {
var language = trim(m1), codeblock = m2;
// adhere to specified options
var preclass = self.googleCodePrettify ? ' class="prettyprint"' : '';
var codeclass = '';
if (language) {
if (self.googleCodePrettify || self.highlightJs) {
// use html5 language- class names. supported by both prettify and highlight.js
codeclass = ' class="language-' + language + '"';
} else {
codeclass = ' class="' + language + '"';
}
}
var html = ['<pre', preclass, '><code', codeclass, '>',
encodeCode(codeblock), '</code></pre>'].join('');
// replace codeblock with placeholder until postConversion step
return self.hashExtraBlock(html);
});
return text;
};
/******************************************************************
* SmartyPants *
******************************************************************/
Markdown.Extra.prototype.educatePants = function(text) {
var self = this;
var result = '';
var blockOffset = 0;
// Here we parse HTML in a very bad manner
text.replace(/(?:<!--[\s\S]*?-->)|(<)([a-zA-Z1-6]+)([^\n]*?>)([\s\S]*?)(<\/\2>)/g, function(wholeMatch, m1, m2, m3, m4, m5, offset) {
var token = text.substring(blockOffset, offset);
result += self.applyPants(token);
self.smartyPantsLastChar = result.substring(result.length - 1);
blockOffset = offset + wholeMatch.length;
if(!m1) {
// Skip commentary
result += wholeMatch;
return;
}
// Skip special tags
if(!/code|kbd|pre|script|noscript|iframe|math|ins|del|pre/i.test(m2)) {
m4 = self.educatePants(m4);
}
else {
self.smartyPantsLastChar = m4.substring(m4.length - 1);
}
result += m1 + m2 + m3 + m4 + m5;
});
var lastToken = text.substring(blockOffset);
result += self.applyPants(lastToken);
self.smartyPantsLastChar = result.substring(result.length - 1);
return result;
};
function revertPants(wholeMatch, m1) {
var blockText = m1;
blockText = blockText.replace(/&\#8220;/g, "\"");
blockText = blockText.replace(/&\#8221;/g, "\"");
blockText = blockText.replace(/&\#8216;/g, "'");
blockText = blockText.replace(/&\#8217;/g, "'");
blockText = blockText.replace(/&\#8212;/g, "---");
blockText = blockText.replace(/&\#8211;/g, "--");
blockText = blockText.replace(/&\#8230;/g, "...");
return blockText;
}
Markdown.Extra.prototype.applyPants = function(text) {
// Dashes
text = text.replace(/---/g, "&#8212;").replace(/--/g, "&#8211;");
// Ellipses
text = text.replace(/\.\.\./g, "&#8230;").replace(/\.\s\.\s\./g, "&#8230;");
// Backticks
text = text.replace(/``/g, "&#8220;").replace (/''/g, "&#8221;");
if(/^'$/.test(text)) {
// Special case: single-character ' token
if(/\S/.test(this.smartyPantsLastChar)) {
return "&#8217;";
}
return "&#8216;";
}
if(/^"$/.test(text)) {
// Special case: single-character " token
if(/\S/.test(this.smartyPantsLastChar)) {
return "&#8221;";
}
return "&#8220;";
}
// Special case if the very first character is a quote
// followed by punctuation at a non-word-break. Close the quotes by brute force:
text = text.replace (/^'(?=[!"#\$\%'()*+,\-.\/:;<=>?\@\[\\]\^_`{|}~]\B)/, "&#8217;");
text = text.replace (/^"(?=[!"#\$\%'()*+,\-.\/:;<=>?\@\[\\]\^_`{|}~]\B)/, "&#8221;");
// Special case for double sets of quotes, e.g.:
// <p>He said, "'Quoted' words in a larger quote."</p>
text = text.replace(/"'(?=\w)/g, "&#8220;&#8216;");
text = text.replace(/'"(?=\w)/g, "&#8216;&#8220;");
// Special case for decade abbreviations (the '80s):
text = text.replace(/'(?=\d{2}s)/g, "&#8217;");
// Get most opening single quotes:
text = text.replace(/(\s|&nbsp;|--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)'(?=\w)/g, "$1&#8216;");
// Single closing quotes:
text = text.replace(/([^\s\[\{\(\-])'/g, "$1&#8217;");
text = text.replace(/'(?=\s|s\b)/g, "&#8217;");
// Any remaining single quotes should be opening ones:
text = text.replace(/'/g, "&#8216;");
// Get most opening double quotes:
text = text.replace(/(\s|&nbsp;|--|&[mn]dash;|&\#8211;|&\#8212;|&\#x201[34];)"(?=\w)/g, "$1&#8220;");
// Double closing quotes:
text = text.replace(/([^\s\[\{\(\-])"/g, "$1&#8221;");
text = text.replace(/"(?=\s)/g, "&#8221;");
// Any remaining quotes should be opening ones.
text = text.replace(/"/ig, "&#8220;");
return text;
};
// Find and convert markdown extra definition lists into html.
Markdown.Extra.prototype.runSmartyPants = function(text) {
this.smartyPantsLastChar = '';
text = this.educatePants(text);
// Clean everything inside html tags (some of them may have been converted due to our rough html parsing)
text = text.replace(/(<([a-zA-Z1-6]+)\b([^\n>]*?)(\/)?>)/g, revertPants);
return text;
};
/******************************************************************
* Definition Lists *
******************************************************************/
// Find and convert markdown extra definition lists into html.
Markdown.Extra.prototype.definitionLists = function(text) {
var wholeList = new RegExp(
['(\\x02\\n?|\\n\\n)' ,
'(?:' ,
'(' , // $1 = whole list
'(' , // $2
'[ ]{0,3}' ,
'((?:[ \\t]*\\S.*\\n)+)', // $3 = defined term
'\\n?' ,
'[ ]{0,3}:[ ]+' , // colon starting definition
')' ,
'([\\s\\S]+?)' ,
'(' , // $4
'(?=\\0x03)' , // \z
'|' ,
'(?=' ,
'\\n{2,}' ,
'(?=\\S)' ,
'(?!' , // Negative lookahead for another term
'[ ]{0,3}' ,
'(?:\\S.*\\n)+?' , // defined term
'\\n?' ,
'[ ]{0,3}:[ ]+' , // colon starting definition
')' ,
'(?!' , // Negative lookahead for another definition
'[ ]{0,3}:[ ]+' , // colon starting definition
')' ,
')' ,
')' ,
')' ,
')'
].join(''),
'gm'
);
var self = this;
text = addAnchors(text);
text = text.replace(wholeList, function(match, pre, list) {
var result = trim(self.processDefListItems(list));
result = "<dl>\n" + result + "\n</dl>";
return pre + self.hashExtraBlock(result) + "\n\n";
});
return removeAnchors(text);
};
// Process the contents of a single definition list, splitting it
// into individual term and definition list items.
Markdown.Extra.prototype.processDefListItems = function(listStr) {
var self = this;
var dt = new RegExp(
['(\\x02\\n?|\\n\\n+)' , // leading line
'(' , // definition terms = $1
'[ ]{0,3}' , // leading whitespace
'(?![:][ ]|[ ])' , // negative lookahead for a definition
// mark (colon) or more whitespace
'(?:\\S.*\\n)+?' , // actual term (not whitespace)
')' ,
'(?=\\n?[ ]{0,3}:[ ])' // lookahead for following line feed
].join(''), // with a definition mark
'gm'
);
var dd = new RegExp(
['\\n(\\n+)?' , // leading line = $1
'(' , // marker space = $2
'[ ]{0,3}' , // whitespace before colon
'[:][ ]+' , // definition mark (colon)
')' ,
'([\\s\\S]+?)' , // definition text = $3
'(?=\\n*' , // stop at next definition mark,
'(?:' , // next term or end of text
'\\n[ ]{0,3}[:][ ]|' ,
'<dt>|\\x03' , // \z
')' ,
')'
].join(''),
'gm'
);
listStr = addAnchors(listStr);
// trim trailing blank lines:
listStr = listStr.replace(/\n{2,}(?=\\x03)/, "\n");
// Process definition terms.
listStr = listStr.replace(dt, function(match, pre, termsStr) {
var terms = trim(termsStr).split("\n");
var text = '';
for (var i = 0; i < terms.length; i++) {
var term = terms[i];
// process spans inside dt
term = convertSpans(trim(term), self);
text += "\n<dt>" + term + "</dt>";
}
return text + "\n";
});
// Process actual definitions.
listStr = listStr.replace(dd, function(match, leadingLine, markerSpace, def) {
if (leadingLine || def.match(/\n{2,}/)) {
// replace marker with the appropriate whitespace indentation
def = Array(markerSpace.length + 1).join(' ') + def;
// process markdown inside definition
// TODO?: currently doesn't apply extensions
def = outdent(def) + "\n\n";
def = "\n" + convertAll(def, self) + "\n";
} else {
// convert span-level markdown inside definition
def = rtrim(def);
def = convertSpans(outdent(def), self);
}
return "\n<dd>" + def + "</dd>\n";
});
return removeAnchors(listStr);
};
/***********************************************************
* Strikethrough *
************************************************************/
Markdown.Extra.prototype.strikethrough = function(text) {
// Pretty much duplicated from _DoItalicsAndBold
return text.replace(/([\W_]|^)~T~T(?=\S)([^\r]*?\S[\*_]*)~T~T([\W_]|$)/g,
"$1<del>$2</del>$3");
};
/***********************************************************
* New lines *
************************************************************/
Markdown.Extra.prototype.newlines = function(text) {
// We have to ignore already converted newlines and line breaks in sub-list items
return text.replace(/(<(?:br|\/li)>)?\n/g, function(wholeMatch, previousTag) {
return previousTag ? wholeMatch : " <br>\n";
});
};
})();

238
src/scripts/project-edit.js Normal file
View File

@@ -0,0 +1,238 @@
/* Edit Node */
/* Move Node */
var movingMode = Cookies.getJSON('bcloud_moving_node');
function editNode(nodeId) {
// Remove the 'n_' suffix from the id
if (nodeId.substring(0, 2) == 'n_') {
nodeId = nodeId.substr(2);
}
var url = '/nodes/' + nodeId + '/edit?embed=1';
$.get(url, function(dataHtml) {
// Update the DOM injecting the generate HTML into the page
$('#project_context').html(dataHtml);
updateUi(nodeId, 'edit');
})
.fail(function(dataResponse) {
$('#project_context').html($('<iframe id="server_error"/>'));
$('#server_error').attr('src', url);
})
.always(function(){
$('.button-edit-icon').addClass('pi-edit').removeClass('pi-spin spin');
});
}
/* Add Node */
function addNode(nodeTypeName, parentId) {
var url = '/nodes/create';
var node_props = {node_type_name: nodeTypeName, project_id: ProjectUtils.projectId()};
if (typeof(parentId) != 'undefined') {node_props.parent_id = parentId};
$.post(url, node_props)
.done(function(data) {
editNode(data.data.asset_id);
})
.always(function(){
$('.button-add-group-icon').addClass('pi-collection-plus').removeClass('pi-spin spin');
})
.fail(function(data){
statusBarSet('error', 'Error creating node (' + data.status + ' - ' + data.statusText + ')', 'pi-warning', 5000);
});
}
/* Edit Button */
$('#item_edit').click(function(e){
$('.button-edit-icon').addClass('pi-spin spin').removeClass('pi-edit');
// When clicking on the edit icon, embed the edit
e.preventDefault();
if (ProjectUtils.isProject()) {
window.location.replace(urlProjectEdit);
} else {
editNode(ProjectUtils.nodeId());
}
});
function moveModeEnter() {
$('#overlay-mode-move-container').addClass('visible');
$('.button-move').addClass('disabled');
// Scroll to top so we can see the instructions/buttons
$("#project_context-container").scrollTop(0);
}
function moveModeExit() {
/* Remove cookie, display current node, remove UI */
if (ProjectUtils.isProject()) {
displayProject(ProjectUtils.projectId());
} else {
displayNode(ProjectUtils.nodeId());
}
$('#overlay-mode-move-container').removeClass('visible');
$('.button-move').removeClass('disabled');
$('#item_move_accept').html('<i class="pi-check"></i> Move Here');
Cookies.remove('bcloud_moving_node');
}
$( document ).ready(function() {
if (movingMode) {
moveModeEnter();
} else {
$('#overlay-mode-move-container').removeClass('visible');
$('.button-move').removeClass('disabled');
}
/* Add Node Type Button */
$('.item_add_node').click(function(e){
e.preventDefault();
var nodeTypeName = $(this).data('node-type-name');
if (ProjectUtils.isProject()) {
addNode(nodeTypeName);
} else {
addNode(nodeTypeName, ProjectUtils.nodeId());
}
});
$('#item_move').click(function(e){
e.preventDefault();
moveModeEnter();
// Set the nodeId in the cookie
Cookies.set('bcloud_moving_node', { node_id: ProjectUtils.nodeId(), node_type: ProjectUtils.nodeType()});
});
$("#item_move_accept").click(function(e) {
e.preventDefault();
var movingNodeId = Cookies.getJSON('bcloud_moving_node').node_id;
var moveNodeParams = {node_id: movingNodeId};
// If we are not at the root of the project, add the parent node id to the
// request params
if (!ProjectUtils.isProject()) {
moveNodeParams.dest_parent_node_id = ProjectUtils.nodeId();
}
$(this).html('<i class="pi-spin spin"></i> Moving...');
$.post(urlNodeMove, moveNodeParams,
function(data){
}).done(function() {
statusBarSet('success', 'Moved just fine');
Cookies.remove('bcloud_moving_node');
moveModeExit();
$('#project_tree').jstree("refresh");
})
.fail(function(data){
statusBarSet('error', 'Error moving node (' + data.status + ' - ' + data.statusText + ')', 'pi-warning', 6000);
$(this).html('<i class="pi-check"></i> Move Here');
});
});
$("#item_move_cancel").click(function(e) {
e.preventDefault();
$('.button-edit-icon').addClass('pi-spin spin').removeClass('pi-cancel');
moveModeExit();
});
/* Featured Toggle */
$('#item_featured').click(function(e){
e.preventDefault();
$.post(urlNodeFeature, {node_id : ProjectUtils.nodeId()},
function(data){
// Feedback logic
})
.done(function(){
statusBarSet('success', 'Featured status toggled successfully', 'pi-star-filled');
})
.fail(function(data){
statusBarSet('error', 'Error toggling feature (' + data.status + ' - ' + data.statusText + ')', 'pi-warning', 6000);
});
});
/* Project Header toggle */
$('#item_toggle_projheader').click(function (e) {
e.preventDefault();
$.post(urlNodeToggleProjHeader, {node_id: ProjectUtils.nodeId()})
.done(function (data) {
statusBarSet('success', 'Project Header ' + data.action + ' successfully', 'pi-star-filled');
})
.fail(function (jsxhr) {
var content_type = jsxhr.getResponseHeader('Content-Type');
if(content_type.startsWith('application/json')) {
var data = jsxhr.responseJSON;
statusBarSet('error', 'Error toggling (' + data.messsage + ')', 'pi-warning', 6000);
} else {
statusBarSet('error', 'Error toggling (' + jsxhr.responseText + ')', 'pi-warning', 6000);
}
});
});
/* Delete */
$('#item_delete').click(function(e){
e.preventDefault();
if (ProjectUtils.isProject()) {
// url = window.location.href.split('#')[0] + 'delete';
// window.location.replace(url);
$.post(urlProjectDelete, {project_id: ProjectUtils.projectId()},
function (data) {
// Feedback logic
}).done(function () {
window.location.replace('/p/');
});
} else {
$.post(urlNodeDelete, {node_id: ProjectUtils.nodeId()},
function (data) {
// Feedback logic
})
.done(function () {
statusBarSet('success', 'Deleted successfully', 'pi-trash');
if (ProjectUtils.parentNodeId() != '') {
displayNode(ProjectUtils.parentNodeId());
} else {
// Display the project when the group is at the root of the tree
displayProject(ProjectUtils.projectId());
}
setTimeout(function(){
$('#project_tree').jstree('refresh');
}, 1000);
})
.fail(function (data) {
statusBarSet('error', 'Error deleting (' + data.status + ' - ' + data.statusText + ')', 'pi-warning', 6000);
});
}
});
/* Toggle public */
$('#item_toggle_public').click(function(e){
e.preventDefault();
var currentNodeId = ProjectUtils.nodeId();
$.post(urlNodeTogglePublic, {node_id : currentNodeId},
function(data){
// Feedback logic
})
.done(function(data){
statusBarSet('success', data.data.message);
displayNode(currentNodeId);
})
.fail(function(data){
statusBarSet('error', 'Error toggling status (' + data.status + ' - ' + data.statusText + ')', 'pi-warning', 6000);
});
});
$('ul.project-edit-tools').removeClass('disabled');
});

View File

@@ -0,0 +1,70 @@
$(function () {
$('[data-toggle="tooltip"]').tooltip({'delay' : {'show': 1250, 'hide': 250}});
$('[data-toggle="popover"]').popover();
})
function NavbarTransparent() {
var startingpoint = 50;
$(window).on("load scroll", function () {
if ($(this).scrollTop() > startingpoint) {
$('.navbar-overlay, .navbar-transparent').addClass('is-active');
if(document.getElementById("project_context-header") !== null) {
$('#project_context-header').addClass('is-offset');
}
} else {
$('.navbar-overlay, .navbar-transparent').removeClass('is-active');
if(document.getElementById("project_context-header") !== null) {
$('#project_context-header').removeClass('is-offset');
}
};
});
};
NavbarTransparent();
/* Status Bar */
function statusBarSet(classes, html, icon_name, time){
/* Utility to notify the user by temporarily flashing text on the project header
Usage:
'classes' can be: success, error, warning, info, default
'html': the text to display, can contain html tags
(in case of errors, it's better to use data.status + data.statusText instead )
'icon_name': optional, sets a custom icon (otherwise an icon based on the class will be used)
'time': optional, custom time in milliseconds for the text to be displayed
*/
var icon = '';
if (!time) { time = 3000 };
if (!icon_name) {
if (classes == 'error') {
icon_name = 'pi-attention';
} else if (classes == 'success') {
icon_name = 'pi-check';
} else if (classes == 'warning') {
icon_name = 'pi-warning';
} else if (classes == 'info') {
icon_name = 'pi-info';
} else {
icon = '<i class="' + icon_name + '"></i>';
};
} else {
icon = '<i class="' + icon_name + '"></i>';
};
var text = icon + html;
$("#project-statusbar").addClass('active ' + classes);
$("#project-statusbar").html(text);
/* Back to normal */
setTimeout(function(){
$("#project-statusbar").removeAttr('class');
$("#project-statusbar").html();
}, time);
};

View File

@@ -0,0 +1,182 @@
function projectNavCollapse() {
$("#project-side-container").addClass('collapsed');
$("ul.breadcrumb.context").addClass('active');
if (typeof Ps !== 'undefined'){
Ps.destroy(document.getElementById('project_tree'));
};
};
function projectNavExpand() {
$("#project-side-container").removeClass('collapsed');
$("ul.breadcrumb.context").removeAttr('class');
if (typeof Ps !== 'undefined'){
Ps.initialize(document.getElementById('project_tree'), {suppressScrollX: true});
}
};
function projectNavCheck(){
/* Only run if there is a tree */
if(document.getElementById("project_tree") !== null) {
var nav_status = Cookies.getJSON('bcloud_ui');
if (nav_status && nav_status.nav_collapsed) {
if (nav_status.nav_collapsed == 'expanded') {
projectNavExpand();
} else if ( nav_status.nav_collapsed == 'collapsed' ) {
projectNavCollapse();
}
} else {
projectNavExpand();
}
}
}
function projectNavToggle(){
var nav_status = Cookies.getJSON('bcloud_ui');
if (nav_status && nav_status.nav_collapsed) {
if (nav_status.nav_collapsed == 'expanded') {
projectNavCollapse();
setJSONCookie('bcloud_ui', 'nav_collapsed', 'collapsed');
} else if ( nav_status.nav_collapsed == 'collapsed' ) {
projectNavExpand();
setJSONCookie('bcloud_ui', 'nav_collapsed', 'expanded');
}
} else {
projectNavCollapse();
setJSONCookie('bcloud_ui', 'nav_collapsed', 'collapsed');
}
$('#project_context-header').width($('#project_context-container').width());
}
$(function () {
/* Check on first load */
projectNavCheck();
$('.project_split, .project_nav-toggle-btn').on('click', function (e) {
projectNavToggle();
});
/* Only run if there is a tree */
if(document.getElementById("project_tree") !== null) {
$(document).keypress(function(e) {
var tag = e.target.tagName.toLowerCase();
/* Toggle when pressing [T] key */
if(e.which == 116 && tag != 'input' && tag != 'textarea' && !e.ctrlKey && !e.metaKey && !e.altKey) {
projectNavToggle();
}
});
}
});
/* Small utility to enable specific node_types under the Add New dropdown */
/* It takes:
* empty: Enable every item
* false: Disable every item
* array: Disable every item except a list of node_types, e.g: ['asset', 'group']
*/
function addMenuEnable(node_types){
$("#item_add").parent().removeClass('disabled');
$("ul.add_new-menu li[class^='button-']").hide().addClass('disabled');
if (node_types === undefined) {
$("ul.add_new-menu li[class^='button-']").show().removeClass('disabled');
} else if (node_types == false) {
$("#item_add").parent().addClass('disabled');
} else {
$.each(node_types, function(index, value) {
$("ul.add_new-menu li[class*='button-" + value +"']").show().removeClass('disabled');
});
}
}
function addMenuDisable(node_types){
$.each(node_types, function(index, value) {
$("ul.add_new-menu li[class*='button-" + value +"']").addClass('disabled');
});
}
/* Completely hide specific items (like Texture when on project root) */
function addMenuHide(node_types){
$.each(node_types, function(index, value) {
$("ul.add_new-menu li[class*='button-" + value +"']").hide().addClass('disabled');
});
}
/* Jump to the top of the page! */
function hopToTop(limit){
if (limit == null) {
limit = 500;
}
document.getElementById("hop").onclick = function(e){ window.scrollTo(0, 0);}
$(window).scroll(function() {
if ($(window).scrollTop() >= limit) {$("#hop").addClass("active")} else {$("#hop").removeAttr("class")}
});
}
/* Utility to replace a single item on a JSON cookie */
function setJSONCookie(cookieToChange, cookieItem, cookieData){
/* Get cookie to change, and its list if it has any */
var cookieList = Cookies.getJSON(cookieToChange);
/* Create an empty list if there's no cookie */
if (!cookieList){ cookieList = {}; }
cookieList[cookieItem] = cookieData;
/* Set (or create) cookie */
Cookies.set(cookieToChange, cookieList);
}
function containerResizeY(window_height){
var container_offset = $('#project-container').offset();
var container_height = window_height - container_offset.top;
var container_height_wheader = window_height - container_offset.top - $('#project_nav-header').height();
var window_height_minus_nav = $('#project_nav').height() - $('#project_nav-header').height();
$('#project_context-header').width($('#project_context-container').width());
$('#project_nav-container, .project_split').css(
{'max-height': window_height_minus_nav + 'px',
'height': window_height_minus_nav + 'px'}
);
if ($(window).width() > 768) {
if (container_height > parseInt($('#project-container').css("min-height"))) {
if (projectTree){
$(projectTree).css(
{'max-height': container_height_wheader + 'px',
'height': container_height_wheader + 'px'}
);
}
};
};
if (projectTree){ Ps.update(projectTree) }
};

View File

@@ -0,0 +1,109 @@
/* Reply */
$(document).on('click','body .comment-action-reply',function(e){
e.preventDefault();
// container of the comment we are replying to
var parentDiv = $(this).parent().parent();
// container of the first-level comment in the thread
var parentDivFirst = $(this).parent().parent().prevAll('.is-first:first');
// Get the id of the comment
if (parentDiv.hasClass('is-reply')) {
parentNodeId = parentDivFirst.data('node_id');
} else {
parentNodeId = parentDiv.data('node_id');
}
// Get the textarea and set its parent_id data
var commentField = document.getElementById('comment_field');
commentField.setAttribute('data-parent_id', parentNodeId);
// Start the comment field with @authorname:
var replyAuthor = $(this).parent().parent().find('.comment-author:first').html();
$(commentField).val("**@" + replyAuthor + ":** ");
// Add class for styling
$('.comment-container').removeClass('is-replying');
parentDiv.addClass('is-replying');
// Rename Post Comment button to Reply
var commentSubmitButton = document.getElementById('comment_submit');
$(commentSubmitButton).text('Post Reply');
// Move comment-reply container field after the parent container
var commentForm = $('.comment-reply-container').detach();
parentDiv.after(commentForm);
// document.getElementById('comment_field').focus();
$(commentField).focus();
// Convert Markdown
var convert = new Markdown.getSanitizingConverter().makeHtml;
var preview = $('.comment-reply-preview');
preview.html(convert($(commentField).val()));
$('.comment-reply-form').addClass('filled');
});
/* Cancel Reply */
$(document).on('click','body .comment-action-cancel',function(e){
e.preventDefault();
$('.comment-reply-container').detach().prependTo('#comments-list');
var commentField = document.getElementById('comment_field');
$(commentField).val('');
// Convert Markdown
var convert = new Markdown.getSanitizingConverter().makeHtml;
var preview = $('.comment-reply-preview');
preview.html(convert($(commentField).val()));
var commentSubmitButton = document.getElementById('comment_submit');
$(commentSubmitButton).text('Post Comment');
$('.comment-reply-form').removeClass('filled');
$('.comment-container').removeClass('is-replying');
});
/* Rate */
$(document).on('click','body .comment-action-rating',function(e){
e.preventDefault();
var $this = $(this);
var nodeId = $this.parent().parent().parent().data('node_id');
var is_positive = !$this.hasClass('down');
var parentDiv = $this.parent();
var rated_positive = parentDiv.hasClass('positive');
var op;
if (parentDiv.hasClass('rated') && is_positive == rated_positive) {
op = 'revoke';
} else if (is_positive) {
op = 'upvote';
} else {
op = 'downvote';
}
$.post("/nodes/comments/" + nodeId + "/rate/" + op)
.done(function(data){
// Add/remove styles for rated statuses
switch(op) {
case 'revoke':
parentDiv.removeClass('rated');
break;
case 'upvote':
parentDiv.addClass('rated');
parentDiv.addClass('positive');
break;
case 'downvote':
parentDiv.addClass('rated');
parentDiv.removeClass('positive');
break;
}
var rating = data['data']['rating_positive'] - data['data']['rating_negative'];
$this.siblings('.comment-rating-value').text(rating);
});
});

View File

@@ -0,0 +1,15 @@
// Util to handle project, node and parent properties
ProjectUtils = {
nodeId: function() { return document.body.dataset.nodeId; },
parentNodeId: function() { return document.body.dataset.parentNodeId; },
projectId: function() { return document.body.dataset.projectId; },
isProject: function() { return document.body.dataset.isProject === 'true'; },
nodeType: function() { return document.body.dataset.nodeType; },
isModified: function() { return document.body.dataset.isModified === 'true'; },
setProjectAttributes: function(props) {
for (var key in props) {
if (!props.hasOwnProperty(key)) continue;
document.body.dataset[key] = props[key];
}
}
};

View File

@@ -0,0 +1,100 @@
/*
* == Search ==
* index and algolia settings are defined in layout.jade
*/
$(document).ready(function() {
var searchInput = $('#cloud-search');
var tu = searchInput.typeahead({hint: true}, {
source: index.ttAdapter(),
displayKey: 'name',
limit: 10,
minLength: 0,
templates: {
suggestion: function(hit) {
var hitMedia = (hit.media ? ' · <span class="media">'+hit.media+'</span>' : '');
var hitFree = (hit.is_free ? '<div class="search-hit-ribbon"><span>free</span></div>' : '');
var hitPicture;
if (hit.picture){
hitPicture = '<img src="' + hit.picture + '"/>';
} else {
hitPicture = '<div class="search-hit-thumbnail-icon">';
hitPicture += (hit.media ? '<i class="pi-' + hit.media + '"></i>' : '<i class="dark pi-'+ hit.node_type + '"></i>');
hitPicture += '</div>';
};
return '' +
'<a href="/nodes/'+ hit.objectID + '/redir" class="search-site-result" id="'+ hit.objectID + '">' +
'<div class="search-hit">' +
'<div class="search-hit-thumbnail">' +
hitPicture +
hitFree +
'</div>' +
'<div class="search-hit-name" title="' + hit.name + '">' +
hit._highlightResult.name.value + ' ' +
'</div>' +
'<div class="search-hit-meta">' +
'<span class="project">' + hit._highlightResult.project.name.value + '</span> · ' +
'<span class="node_type">' + hit.node_type + '</span>' +
hitMedia +
'</div>' +
'</div>'+
'</a>';
}
}
});
$('.search-site-result.advanced, .search-icon').on('click', function(e){
e.stopPropagation();
e.preventDefault();
window.location.href = '/search#q='+ $("#cloud-search").val() + '&page=1';
});
searchInput.bind('typeahead:select', function(ev, hit) {
$('.search-icon').removeClass('pi-search').addClass('pi-spin spin');
window.location.href = '/nodes/'+ hit.objectID + '/redir';
});
searchInput.bind('typeahead:active', function() {
$('#search-overlay').addClass('active');
$('.page-body').addClass('blur');
});
searchInput.bind('typeahead:close', function() {
$('#search-overlay').removeClass('active');
$('.page-body').removeClass('blur');
});
searchInput.keyup(function(e) {
if ( $('.tt-dataset').is(':empty') ){
if(e.keyCode == 13){
window.location.href = '/search#q='+ $("#cloud-search").val() + '&page=1';
};
};
});
searchInput.bind('typeahead:render', function(event, suggestions, async, dataset) {
if( suggestions != undefined && $('.tt-all-results').length <= 0){
$('.tt-dataset').append(
'<a id="search-advanced" href="/search#q='+ $("#cloud-search").val() + '&page=1" class="search-site-result advanced tt-suggestion">' +
'<div class="search-hit">' +
'<div class="search-hit-thumbnail">' +
'<div class="search-hit-thumbnail-icon">' +
'<i class="pi-search"></i>' +
'</div>' +
'</div>' +
'<div class="search-hit-name">' +
'Use Advanced Search' +
'</div>' +
'</div>'+
'</a>');
};
});
});

View File

@@ -0,0 +1,329 @@
// Store the title, to later append notifications count
var page_title = document.title;
var unread_on_load = 0;
var unread_new = 0;
var first_load = false;
// getNotifications by fetching json every X seconds
function getNotifications(){
$.getJSON( "/notifications/", function( data ) {
if (!first_load) {
unread_on_load = data['items'].length;
first_load = true;
}
var items = [];
unread_new = 0;
// Only if there's actual data
if (data['items'][0]){
// Loop through each item
$.each(data['items'], function(i, no){
// Increase the unread_new counter
if (!no['is_read']){ unread_new++ };
// Check if the current item has been read, to style it
var is_read = no['is_read'] ? 'is_read' : '';
var read_info = 'data-id="'+ no['_id'] + '" data-read="' + no['is_read'] + '"';
// Notification list item
var content = '<li class="nc-item ' + is_read +'" data-id="'+ no['_id'] + '">';
// User's avatar
content += '<div class="nc-avatar">';
content += '<img ' + read_info + ' src="' + no['username_avatar'] + '"/> ';
content += '</div>';
// Text of the notification
content += '<div class="nc-text">';
// Username and action
content += no['username'] + ' ' + no['action'] + ' ';
// Object
content += '<a '+read_info+'" href="'+no['object_url']+'" class="nc-a">';
content += no['context_object_name'] + ' ';
content += '</a> ';
// Date
content += '<span class="nc-date">';
content += '<a '+read_info+'" href="'+no['object_url']+'" class="nc-a">';
content += no['date'];
content += '</a>';
content += '</span>';
// Read Toggle
content += '<a id="'+no['_id']+'" href="/notifications/' + no['_id'] + '/read-toggle" class="nc-button nc-read_toggle">';
if (no['is_read']){
content += '<i title="Mark as Unread" class="pi pi-circle-dot"></i>';
} else {
content += '<i title="Mark as Read" class="pi pi-circle"></i>';
};
content += '</a>';
// Subscription Toggle
content += '<a href="/notifications/' + no['_id'] + '/subscription-toggle" class="nc-button nc-subscription_toggle">';
if (no['is_subscribed']){
content += '<i title="Turn Off Notifications" class="pi-toggle-on"></i>';
} else {
content += '<i title="Turn On Notifications" class="pi-toggle-off"></i>';
};
content += '</a>';
content += '</div>';
content += '</li>';
items.push(content);
}); // each
if (unread_new > 0) {
// Set page title, display notifications and set counter
document.title = '(' + unread_new + ') ' + page_title;
$('#notifications-count').addClass('bloom');
$('#notifications-count').html('<span>' + unread_new + '</span>');
$('#notifications-toggle i').removeClass('pi-notifications-none').addClass('pi-notifications-active');
} else {
document.title = page_title;
$('#notifications-count').removeAttr('class');
$('#notifications-toggle i').removeClass('pi-notifications-active').addClass('pi-notifications-none');
};
checkPopNotification(
data['items'][0]['_id'],
data['items'][0]['username'],
data['items'][0]['username_avatar'],
data['items'][0]['action'],
data['items'][0]['date'],
data['items'][0]['context_object_name'],
data['items'][0]['object_url']);
} else {
var content = '<li class="nc-item nc-item-empty">';
content += 'No notifications... yet.';
content += '</li>';
items.push(content);
}; // if items
// Populate the list
$('ul#notifications-list').html( items.join(''));
})
.done(function(){
// clear the counter
unread_on_load = unread_new;
});
};
// Used when we click somewhere in the page
function hideNotifications(){
$('#notifications').hide();
$('#notifications-toggle').removeClass('active');
};
function popNotification(){
// pop in!
$("#notification-pop").addClass('in');
// After 10s, add a class to make it pop out
setTimeout(function(){
$("#notification-pop").addClass('out');
// And a second later, remove all classes
setTimeout(function(){
$("#notification-pop").removeAttr('class');
}, 1000);
}, 10000);
// Set them the same so it doesn't pop up again
unread_on_load = unread_new;
};
function checkPopNotification(id,username,username_avatar,action,date,context_object_name,object_url)
{
// If there's new content
if (unread_new > unread_on_load){
// Fill in the urls for redirect on click, and mark-read
$("#notification-pop").attr('data-url', object_url);
$("#notification-pop").attr('data-read-toggle', '/notifications/' + id + '/read-toggle');
// The text in the pop
var text = '<span class="nc-author">' + username + '</span> ';
text += action + ' ';
text += context_object_name + ' ';
text += '<span class="nc-date">' + date + '</span>';
// Fill the html
$('#notification-pop .nc-text').html(text);
$('#notification-pop .nc-avatar img').attr('src', username_avatar);
// pop in!
popNotification();
};
};
// Function to set #notifications flyout height and resize if needed
function notificationsResize(){
var height = $(window).height() - 80;
if ($('#notifications').height() > height){
$('#notifications').css({
'max-height' : height / 2,
'overflow-y' : 'scroll'
}
);
} else {
$('#notifications').css({
'max-height' : '1000%',
'overflow-y' : 'initial'
}
);
};
};
$(function() {
// Click anywhere in the page to hide #notifications
$(document).click(function () {
hideNotifications();
});
// ...but clicking inside #notifications shouldn't hide itself
$('#notifications').on('click', function (e) {
e.stopPropagation();
});
// Toggle the #notifications flyout
$('#notifications-toggle').on('click', function (e) {
e.stopPropagation();
$('#notifications').toggle();
$(this).toggleClass("active");
notificationsResize();
// Hide other dropdowns
$('nav .dropdown').removeClass('open');
var navbarCollapse = $('nav.navbar-collapse');
if ($(navbarCollapse).hasClass('in')){
$(navbarCollapse).addClass('show-notifications').removeClass('in');
$('.nav-notifications-icon').removeClass('pi-notifications-none').addClass('pi-cancel');
} else {
$(navbarCollapse).removeClass('show-notifications');
$('.nav-notifications-icon').addClass('pi-notifications-none').removeClass('pi-cancel');
}
});
// Hide flyout when clicking other dropdowns
$('nav').on('click', '.dropdown', function (e) {
$('#notifications').hide();
$('#notifications-toggle').removeClass('active');
});
$('#notification-pop').on('click', function (e) {
e.preventDefault();
e.stopPropagation();
var link_url = $(this).data('url');
var read_url = $(this).data('read-toggle');
$.get(read_url)
.done(function () {
window.location.href = link_url;
});
});
// Read/Subscription Toggles
$('ul#notifications-list').on('click', '.nc-button', function (e) {
e.preventDefault();
var nc = $(this);
// Swap to spin icon while we wait for the response
$('i', nc).addClass('spin');
$.get($(nc).attr('href'))
.done(function (data) {
if ($(nc).hasClass('nc-read_toggle')) {
if (data.data.is_read) {
$('i', nc).removeClass('pi-circle').addClass('pi-circle-dot');
$(nc).closest('.nc-item').addClass('is_read');
} else {
$('i', nc).removeClass('pi-circle-dot').addClass('pi-circle');
$(nc).closest('.nc-item').removeClass('is_read');
}
}
;
if ($(nc).hasClass('nc-subscription_toggle')) {
if (data.data.is_subscribed) {
$('i', nc).removeClass('pi-toggle-on').addClass('pi-toggle-off');
} else {
$('i', nc).removeClass('pi-toggle-off').addClass('pi-toggle-on');
}
}
;
$('i', nc).removeClass('spin');
});
});
// When clicking on links, toggle as read
$('ul#notifications-list').on('click', '.nc-a', function (e) {
e.preventDefault();
var is_read = $(this).data('read');
var link_url = $(this).attr('href');
var read_url = '/notifications/' + $(this).data('id') + '/read-toggle';
if (is_read) {
window.location.href = link_url;
} else {
$.get(read_url)
.done(function () {
window.location.href = link_url;
});
}
});
// Mark All as Read
$('#notifications-markallread').on('click', function (e) {
e.preventDefault();
$.get("/notifications/read-all");
$('ul#notifications-list li.nc-item:not(.is_read)').each(function () {
$(this).addClass('is_read');
});
document.title = page_title;
$('#notifications-count').removeAttr('class');
$('#notifications-toggle i').removeClass('pi-notifications-active').addClass('pi-notifications-none');
unread_on_load = unread_new;
});
});
function getNotificationsLoop() {
getNotifications();
var getLoop = setTimeout(function () {
getNotificationsLoop();
}, 30000);
}

File diff suppressed because one or more lines are too long