static/js/zamboni/global.js (462 lines of code) (raw):
import $ from 'jquery';
import _ from 'underscore';
import { format } from '../lib/format';
import { unicode_letters } from './unicode';
// Things global to the site should go here, such as re-usable helper
// functions and common ui components.
// Tooltip display. If you give an element a class of 'tooltip', it will
// display a tooltip on hover. The contents of the tip will be the element's
// title attribute OR the first title attribute in its children. Titles are
// swapped out by the code so the native title doesn't display. If the title of
// the element is changed while the tooltip is displayed, you can update the
// tooltip by with the following:
// $el.trigger("tooltip_change");
//
// You can add a title by using the following format for the title attribute:
// <div title="Title here :: Rest goes after the two colons"></div>
//
// You can set a custom timeout (milliseconds) by using data-delay:
// <div data-delay="100" title="hi"></div>
let uid = 0;
$.fn.tooltip = function (tip_el) {
let $tip = $(tip_el),
$msg = $('span', $tip),
$targets = this,
timeout = false,
$tgt,
$title,
delay;
function setTip() {
if (!$tgt) return;
let pos = $tgt.offset(),
title = $title.attr('title'),
html = $title.attr('data-tooltip-html');
delay = $title.is('[data-delay]') ? $title.attr('data-delay') : 300;
if (!html && title.indexOf('::') > 0) {
let title_split = title.split('::');
$msg.text('');
$msg.append($('<strong>', { text: title_split[0].trim() }));
$msg.append($('<span>', { text: title_split[1].trim() }));
} else {
$msg[html ? 'html' : 'text'](title);
}
$title.attr('data-oldtitle', title).attr('title', '');
let tw = $tip.outerWidth(false) / 2,
th = $tip.outerHeight(false),
toX = pos.left + $tgt.innerWidth() / 2 - tw - 1,
toY = pos.top - $tgt.innerHeight() - th - 2;
timeout = setTimeout(function () {
$tip
.css({
left: toX + 'px',
top: toY + 'px',
})
.show();
}, delay);
}
$(document.body).on('tooltip_change', setTip);
function mouseover() {
$tgt = $(this);
if ($tgt.hasClass('formerror')) $tip.addClass('error');
$title = $tgt.attr('title') ? $tgt : $('[title]', $tgt).first();
if ($title.length) {
setTip();
}
}
function mouseout() {
clearTimeout(timeout);
$tip.hide().removeClass('error');
if ($title && $title.length) {
$tgt = $(this);
$title
.attr('title', $title.attr('data-oldtitle'))
.attr('data-oldtitle', '');
}
}
$targets.on('mouseover', mouseover).on('mouseout', mouseout);
};
// Setting up site tooltips.
$(document).ready(function () {
$('.tooltip').tooltip('#tooltip');
});
// returns an event handler that will hide/unbind an element when a click is
// registered outside itself.
function makeBlurHideCallback(el) {
const hider = function (e) {
const _root = el.get(0);
// Bail if the click was somewhere on the popup.
if (e) {
if (
(e.type == 'click' && _root == e.target) ||
_.indexOf($(e.target).parents(), _root) != -1
) {
return;
}
}
el.hideMe();
if (el.o.emptyme) {
el.empty();
}
if (el.o.deleteme) {
el.remove();
}
};
return hider;
}
// makes an element into a popup.
// click_target defines the element/elements that trigger the popup.
// currently presumes the given element uses the '.popup' style
// o takes the following optional fields:
// callback: a function to run before displaying the popup. Returning
// false from the function cancels the popup.
// container: if set the popup will be appended to the container before
// being displayed.
// pointTo: if set, the popup will be appended to document.body and
// absolutely positioned to point at the given element
// width: the width of the popup.
// delegate: delegates the click handling of the click_target to the
// specified parent element.
// hideme: defaults to true; if set to false, popup will not be hidden
// when the user clicks outside of it.
// emptyme: defaults to false; if set to true, popup will be cleared
// after it is hidden.
// note: all options may be overridden and modified by returning them in an
// object from the callback.
$.fn.popup = function (click_target, o) {
o = o || {};
let $ct = $(click_target),
$popup = this,
spawned = 0;
$popup.o = $.extend(
{
delegate: false,
callback: false,
container: false,
hideme: true,
emptyme: false,
pointTo: false,
offset: {},
width: 300,
},
o,
);
$popup.setWidth = function (w) {
$popup.css({ width: w });
return $popup;
};
$popup.setPos = function (el, offset) {
offset = offset || $popup.o.offset;
el = el || $popup.o.pointTo;
if (!el) return false;
$popup.detach().appendTo('body');
let pt = $(el),
pos = pt.offset(),
tw = pt.outerWidth(false) / 2,
th = pt.outerHeight(false),
pm = pos.left + tw > $('body').outerWidth(false) / 2,
os = pm ? $popup.outerWidth(false) - 84 : 63,
toX = pos.left + (offset.x || tw) - os,
toY = pos.top + (offset.y || th) + 4;
$popup.removeClass('left');
if (pm) $popup.addClass('left');
$popup.css({
left: toX,
top: toY,
right: 'inherit',
bottom: 'inherit',
});
$popup.o.pointTo = el;
return $popup;
};
$popup.hideMe = function () {
$popup.hide();
$popup.off();
$(document.body).off('click.' + uid++, $popup.hider);
return $popup;
};
function handler(e) {
e.preventDefault();
spawned = e.timeStamp;
let resp = o.callback
? o.callback.call($popup, {
click_target: this,
evt: e,
})
: true;
$popup.o = $.extend({ click_target: this }, $popup.o, resp);
if (resp) {
$popup.render();
}
}
$popup.render = function () {
let p = $popup.o,
hideCallback = makeBlurHideCallback($popup);
$popup.hider = function (e) {
if (e.timeStamp != spawned) {
hideCallback.call(this, e);
}
};
if (p.hideme) {
setTimeout(function () {
$(document.body).on('click.' + uid, $popup.hider);
}, 0);
}
$popup.on('click', '.close', function (e) {
e.preventDefault();
$popup.hideMe();
});
$ct.trigger('popup_show', [$popup]);
if (p.container && p.container.length)
$popup.detach().appendTo(p.container);
if (p.pointTo) {
$popup.setPos(p.pointTo);
}
setTimeout(function () {
$popup.show();
}, 0);
return $popup;
};
if ($popup.o.delegate) {
$($popup.o.delegate).on('click', click_target, handler);
} else {
$ct.click(handler);
}
$popup.setWidth($popup.o.width);
return $popup;
};
// makes an element into a modal.
// click_target defines the element/elements that trigger the modal.
// currently presumes the given element uses the '.modal' style
// o takes the following optional fields:
// callback: a function to run before displaying the modal. Returning
// false will cancel the modal.
// container: if set the modal will be appended to the container before
// being displayed.
// width: the width of the modal.
// delegate: delegates the click handling of the click_target to the
// specified parent element.
// hideme: defaults to true; if set to false, modal will not be hidden
// when the user clicks outside of it.
// emptyme: defaults to false; if set to true, modal will be cleared
// after it is hidden.
// deleteme: defaults to false; if set to true, popup will be deleted
// after it is hidden.
// close: defaults to false; if set to true, modal will have a
// close button
// note: all options may be overridden and modified by returning them in an
// object from the callback.
//
// If you want to close all existing modals, use:
// $('.modal').trigger('close');
$.fn.modal = function (click_target, o) {
o = o || {};
let $ct = $(click_target),
$modal = this;
$modal.o = $.extend(
{
delegate: false,
callback: false,
onresize: function () {
$modal.setPos();
},
hideme: true,
emptyme: false,
deleteme: false,
offset: {},
width: 450,
},
o,
);
$modal.setWidth = function (w) {
$modal.css({ width: w });
return $modal;
};
$modal.setPos = function (offset) {
offset = offset || $modal.o.offset;
$modal.detach().appendTo('body');
let toX = offset.x || ($(window).width() - $modal.outerWidth(false)) / 2,
toY = offset.y || 160;
$modal.css({
left: toX + 'px',
top: toY + 'px',
right: 'inherit',
bottom: 'inherit',
position: 'fixed',
});
return $modal;
};
$modal.hideMe = function () {
let p = $modal.o;
$modal.hide();
$modal.off();
$(document.body).off('click newmodal', $modal.hider);
$(window).off('keydown.lightboxDismiss');
$(window).on('resize', p.onresize);
$('.modal-overlay').remove();
return $modal;
};
function handler(e) {
e.preventDefault();
let resp = o.callback
? o.callback.call($modal, {
click_target: this,
evt: e,
}) !== false
: true;
$modal.o = $.extend({ click_target: this }, $modal.o, resp);
if (resp) {
$('.modal').trigger('close'); // We don't want two!
$modal.render();
}
}
$modal.render = function () {
let p = $modal.o;
$modal.hider = makeBlurHideCallback($modal);
if (p.hideme) {
try {
setTimeout(function () {
$('.modal-overlay, .close').on('click modal', $modal.hider);
}, 0);
} catch (err) {
// TODO(Kumar) handle this more gracefully. See bug 701221.
if (typeof console !== 'undefined') {
console.error('Could not close modal:', err);
}
}
}
if (p.close) {
let close = $('<a>', { class: 'close', text: 'X' });
$modal.append(close);
}
$('.popup').hide();
$modal.on('click', '.close', function (e) {
e.preventDefault();
$modal.trigger('close');
});
$modal.on('close', function (e) {
if (p.emptyme) {
$modal.empty();
}
if (p.deleteme) {
$modal.remove();
}
e.preventDefault();
$modal.hideMe();
});
$ct.trigger('modal_show', [$modal]);
if (p.container && p.container.length)
$modal.detach().appendTo(p.container);
$('<div class="modal-overlay"></div>').appendTo('body');
$modal.setPos();
setTimeout(function () {
$modal.show();
}, 0);
$(window)
.on('resize', p.onresize)
.on('keydown.lightboxDismiss', function (e) {
if (e.which == 27) {
$modal.hideMe();
}
});
return $modal;
};
if ($modal.o.delegate) {
$($modal.o.delegate).on('click', click_target, handler);
} else {
$ct.click(handler);
}
$modal.setWidth($modal.o.width);
return $modal;
};
// Slugify
// This allows you to create a line of text with a "Edit" field,
// and swap it out for an editable slug. For example:
//
// http://mozilla.com/slugname <a>edit</a>
// ..to..
// http://mozilla.com/[editable slug name]
function makeslug(s, delimiter) {
if (!s) return '';
let re = new RegExp('[^\\w' + unicode_letters + '\\s-]+', 'g');
s = $.trim(s.replace(re, ' '));
s = s.replace(/[-\s]+/g, delimiter || '-').toLowerCase();
return s;
}
export function show_slug_edit(e) {
$('#slug_readonly').hide();
$('#slug_edit').show();
$('#id_slug').focus();
e.preventDefault();
}
export function slugify() {
let $slug = $('#id_slug');
let url_customized =
$slug.attr('data-customized') === 0 || !$slug.attr('data-customized');
if (url_customized || !$slug.val()) {
let new_slug = makeslug($('#id_name').val());
if (new_slug !== '') {
$slug.val(new_slug);
}
}
const name_val = $slug.val();
$('#slug_value').text($slug.val());
}
// Initializes character counters for textareas.
export function initCharCount() {
let countChars = function (val, cc) {
let max = parseInt(cc.attr('data-maxlength'), 10),
min = parseInt(cc.attr('data-minlength'), 10) || 0,
// Count \r\n as one character, not two.
lineBreaks = val.split('\n').length - 1,
left = max - val.length - lineBreaks,
count = val.length - lineBreaks,
output = [];
if (min || !max) {
// L10n: {0} is the number of characters entered.
output.push(
format(
ngettext('<b>{0}</b> character', '<b>{0}</b> characters', count),
[count],
),
);
}
if (max) {
// L10n: {0} is the number of characters left.
output.push(
format(
ngettext(
'<b>{0}</b> character left',
'<b>{0}</b> characters left',
left,
),
[left],
),
);
}
cc.html(
(cc.attr('data-text-prefix') || '') +
output.join('; ') +
(cc.attr('data-text-postfix') || '.'),
).toggleClass('error', left < 0 || count < min);
};
$('.char-count').each(function () {
let $cc = $(this),
$form = $(this).closest('form'),
$el,
multi = false;
if ($cc.data('for-names') !== undefined) {
multi = true;
let query_string = $cc
.data('for-names')
.split(',')
.map(function (field_name) {
return (
'textarea[name^="' +
field_name +
'"]:visible, input[name^="' +
field_name +
'"]:visible'
);
})
.join(', ');
$el = $(query_string, $form);
} else if ($cc.attr('data-for-startswith') !== undefined) {
$el = $(
'textarea[id^="' +
$cc.attr('data-for-startswith') +
'"]:visible, input[id^="' +
$cc.attr('data-for-startswith') +
'"]:visible',
$form,
);
} else {
$el = $('textarea#' + $cc.attr('data-for'), $form);
}
$el
.on('keyup blur', function () {
let $this = $(this),
val;
if (multi) {
val = $el
.filter('[name$="' + $this.attr('lang') + '"]')
.map(function () {
return $(this).val();
})
.get()
.join('');
} else {
val = $this.val();
}
countChars(val, $cc);
})
.trigger('blur');
});
}
// .exists()
// This returns true if length > 0.
$.fn.exists = function (callback, args) {
let $this = $(this),
len = $this.length;
if (len && callback) {
callback.apply(null, args);
}
return len > 0;
};
export function formatFileSize(size) {
return Intl.NumberFormat(document.documentElement.lang, {
notation: 'compact',
style: 'unit',
unit: 'byte',
unitDisplay: 'narrow',
}).format(size);
}
export function validateFileUploadSize() {
const maxSize = $(this).data('max-upload-size');
const file = this.files[0];
const input = $(this).get(0);
if (file.size > maxSize) {
input.setCustomValidity(
format(gettext('Your file exceeds the maximum size of {0}.'), [
formatFileSize(maxSize),
]),
);
} else {
input.setCustomValidity('');
}
}