Commit 7fd9aa5c authored by Steven Wittens's avatar Steven Wittens
Browse files

#69786: jQuery JavaScript Library in Drupal core

parent 291c7b8b
......@@ -8,6 +8,7 @@ Drupal x.x.x, xxxx-xx-xx (development version)
* automatically generate the database configuration file
* install pre-made 'install profiles' or distributions
* import the database structure with automatic table prefixing
- included the jQuery JavaScript library and converted all core JavaScript to use it
- introduced the ability to alter mail sent from system
- moved core modules to their own directories to allow additional flexibility
- added support for different cache backends
......
......@@ -1369,7 +1369,8 @@ function drupal_add_js($data = NULL, $type = 'module', $scope = 'header', $defer
$javascript[$scope] = array('core' => array(), 'module' => array(), 'theme' => array(), 'setting' => array(), 'inline' => array());
if (empty($javascript['header']['core']['misc/drupal.js'])) {
drupal_add_js('misc/drupal.js', 'core');
drupal_add_js('misc/jquery.js', 'core', 'header', FALSE, $cache);
drupal_add_js('misc/drupal.js', 'core', 'header', FALSE, $cache);
}
}
......
// $Id$
// Global Killswitch
if (isJsEnabled()) {
addLoadEvent(autocompleteAutoAttach);
}
/**
* Attaches the autocomplete behaviour to all required fields
*/
function autocompleteAutoAttach() {
Drupal.autocompleteAutoAttach = function () {
var acdb = [];
var inputs = document.getElementsByTagName('input');
for (i = 0; input = inputs[i]; i++) {
if (input && hasClass(input, 'autocomplete')) {
uri = input.value;
if (!acdb[uri]) {
acdb[uri] = new ACDB(uri);
}
input = $(input.id.substr(0, input.id.length - 13));
input.setAttribute('autocomplete', 'OFF');
addSubmitEvent(input.form, autocompleteSubmit);
new jsAC(input, acdb[uri]);
$('input.autocomplete').each(function () {
var uri = this.value;
if (!acdb[uri]) {
acdb[uri] = new Drupal.ACDB(uri);
}
}
var input = $('#' + this.id.substr(0, this.id.length - 13))
.attr('autocomplete', 'OFF')[0];
$(input.form).submit(Drupal.autocompleteSubmit);
new Drupal.jsAC(input, acdb[uri]);
});
}
/**
* Prevents the form from submitting if the suggestions popup is open
* and closes the suggestions popup when doing so.
*/
function autocompleteSubmit() {
var popup = document.getElementById('autocomplete');
if (popup) {
popup.owner.hidePopup();
return false;
}
return true;
Drupal.autocompleteSubmit = function () {
return $('#autocomplete').each(function () {
this.owner.hidePopup();
}).size() == 0;
}
/**
* An AutoComplete object
*/
function jsAC(input, db) {
Drupal.jsAC = function (input, db) {
var ac = this;
this.input = input;
this.db = db;
this.input.onkeydown = function (event) { return ac.onkeydown(this, event); };
this.input.onkeyup = function (event) { ac.onkeyup(this, event) };
this.input.onblur = function () { ac.hidePopup(); ac.db.cancel(); };
this.popup = document.createElement('div');
this.popup.id = 'autocomplete';
this.popup.owner = this;
};
/**
* Hides the autocomplete suggestions
*/
jsAC.prototype.hidePopup = function (keycode) {
if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) {
this.input.value = this.selected.autocompleteValue;
}
if (this.popup.parentNode && this.popup.parentNode.tagName) {
removeNode(this.popup);
}
this.selected = false;
}
$(this.input)
.keydown(function (event) { return ac.onkeydown(this, event); })
.keyup(function (event) { ac.onkeyup(this, event) })
.blur(function () { ac.hidePopup(); ac.db.cancel(); });
};
/**
* Handler for the "keydown" event
*/
jsAC.prototype.onkeydown = function (input, e) {
Drupal.jsAC.prototype.onkeydown = function (input, e) {
if (!e) {
e = window.event;
}
......@@ -89,7 +64,7 @@ jsAC.prototype.onkeydown = function (input, e) {
/**
* Handler for the "keyup" event
*/
jsAC.prototype.onkeyup = function (input, e) {
Drupal.jsAC.prototype.onkeyup = function (input, e) {
if (!e) {
e = window.event;
}
......@@ -126,21 +101,21 @@ jsAC.prototype.onkeyup = function (input, e) {
/**
* Puts the currently highlighted suggestion into the autocomplete field
*/
jsAC.prototype.select = function (node) {
Drupal.jsAC.prototype.select = function (node) {
this.input.value = node.autocompleteValue;
}
/**
* Highlights the next suggestion
*/
jsAC.prototype.selectDown = function () {
Drupal.jsAC.prototype.selectDown = function () {
if (this.selected && this.selected.nextSibling) {
this.highlight(this.selected.nextSibling);
}
else {
var lis = this.popup.getElementsByTagName('li');
if (lis.length > 0) {
this.highlight(lis[0]);
var lis = $('li', this.popup);
if (lis.size() > 0) {
this.highlight(lis.get(0));
}
}
}
......@@ -148,7 +123,7 @@ jsAC.prototype.selectDown = function () {
/**
* Highlights the previous suggestion
*/
jsAC.prototype.selectUp = function () {
Drupal.jsAC.prototype.selectUp = function () {
if (this.selected && this.selected.previousSibling) {
this.highlight(this.selected.previousSibling);
}
......@@ -157,30 +132,61 @@ jsAC.prototype.selectUp = function () {
/**
* Highlights a suggestion
*/
jsAC.prototype.highlight = function (node) {
removeClass(this.selected, 'selected');
addClass(node, 'selected');
Drupal.jsAC.prototype.highlight = function (node) {
if (this.selected) {
$(this.selected).removeClass('selected');
}
$(node).addClass('selected');
this.selected = node;
}
/**
* Unhighlights a suggestion
*/
jsAC.prototype.unhighlight = function (node) {
removeClass(node, 'selected');
Drupal.jsAC.prototype.unhighlight = function (node) {
$(node).removeClass('selected');
this.selected = false;
}
/**
* Hides the autocomplete suggestions
*/
Drupal.jsAC.prototype.hidePopup = function (keycode) {
// Select item if the right key or mousebutton was pressed
if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) {
this.input.value = this.selected.autocompleteValue;
}
// Hide popup
var popup = this.popup;
if (popup) {
this.popup = null;
$(popup).fadeOut('fast', function() { $(popup).remove(); });
}
this.selected = false;
}
/**
* Positions the suggestions popup and starts a search
*/
jsAC.prototype.populatePopup = function () {
var ac = this;
var pos = absolutePosition(this.input);
Drupal.jsAC.prototype.populatePopup = function () {
// Show popup
if (this.popup) {
$(this.popup).remove();
}
var pos = Drupal.absolutePosition(this.input);
this.selected = false;
this.popup.style.top = (pos.y + this.input.offsetHeight) +'px';
this.popup.style.left = pos.x +'px';
this.popup.style.width = (this.input.offsetWidth - 4) +'px';
this.popup = document.createElement('div');
this.popup.id = 'autocomplete';
this.popup.owner = this;
$(this.popup).css({
top: (pos.y + this.input.offsetHeight) +'px',
left: pos.x +'px',
width: (this.input.offsetWidth - 4) +'px',
display: 'none'
});
$('body').append(this.popup);
// Do search
this.db.owner = this;
this.db.search(this.input.value);
}
......@@ -188,45 +194,40 @@ jsAC.prototype.populatePopup = function () {
/**
* Fills the suggestion popup with any matches received
*/
jsAC.prototype.found = function (matches) {
while (this.popup.hasChildNodes()) {
this.popup.removeChild(this.popup.childNodes[0]);
}
if (!this.popup.parentNode || !this.popup.parentNode.tagName) {
document.getElementsByTagName('body')[0].appendChild(this.popup);
}
Drupal.jsAC.prototype.found = function (matches) {
// Prepare matches
var ul = document.createElement('ul');
var ac = this;
for (key in matches) {
var li = document.createElement('li');
var div = document.createElement('div');
div.innerHTML = matches[key];
li.appendChild(div);
$(li)
.html('<div>'+ matches[key] +'</div>')
.mousedown(function () { ac.select(this); })
.mouseover(function () { ac.highlight(this); })
.mouseout(function () { ac.unhighlight(this); });
li.autocompleteValue = key;
li.onmousedown = function() { ac.select(this); };
li.onmouseover = function() { ac.highlight(this); };
li.onmouseout = function() { ac.unhighlight(this); };
ul.appendChild(li);
$(ul).append(li);
}
// Show popup with matches, if any
if (ul.childNodes.length > 0) {
this.popup.appendChild(ul);
$(this.popup).empty().append(ul).show();
}
else {
$(this.popup).css({visibility: 'hidden'});
this.hidePopup();
}
}
jsAC.prototype.setStatus = function (status) {
Drupal.jsAC.prototype.setStatus = function (status) {
switch (status) {
case 'begin':
addClass(this.input, 'throbbing');
$(this.input).addClass('throbbing');
break;
case 'cancel':
case 'error':
case 'found':
removeClass(this.input, 'throbbing');
$(this.input).removeClass('throbbing');
break;
}
}
......@@ -234,7 +235,7 @@ jsAC.prototype.setStatus = function (status) {
/**
* An AutoComplete DataBase object
*/
function ACDB(uri) {
Drupal.ACDB = function (uri) {
this.uri = uri;
this.delay = 300;
this.cache = {};
......@@ -243,47 +244,55 @@ function ACDB(uri) {
/**
* Performs a cached and delayed search
*/
ACDB.prototype.search = function(searchString) {
Drupal.ACDB.prototype.search = function (searchString) {
var db = this;
this.searchString = searchString;
// See if this key has been searched for before
if (this.cache[searchString]) {
return this.owner.found(this.cache[searchString]);
}
// Initiate delayed search
if (this.timer) {
clearTimeout(this.timer);
}
var db = this;
this.timer = setTimeout(function() {
db.owner.setStatus('begin');
db.transport = HTTPGet(db.uri +'/'+ encodeURIComponent(searchString), db.receive, db);
}, this.delay);
}
/**
* HTTP callback function. Passes suggestions to the autocomplete object
*/
ACDB.prototype.receive = function(string, xmlhttp, acdb) {
// Note: Safari returns 'undefined' status if the request returns no data.
if (xmlhttp.status != 200 && typeof xmlhttp.status != 'undefined') {
acdb.owner.setStatus('error');
return alert('An HTTP error '+ xmlhttp.status +' occured.\n'+ acdb.uri);
}
// Parse back result
var matches = parseJson(string);
if (typeof matches['status'] == 'undefined' || matches['status'] != 0) {
acdb.cache[acdb.searchString] = matches;
acdb.owner.found(matches);
acdb.owner.setStatus('found');
}
// Ajax GET request for autocompletion
$.ajax({
type: "GET",
url: db.uri +'/'+ Drupal.encodeURIComponent(searchString),
success: function (xmlhttp) {
// Parse back result
var matches = Drupal.parseJson(xmlhttp.responseText);
if (typeof matches['status'] == 'undefined' || matches['status'] != 0) {
db.cache[searchString] = matches;
// Verify if these are still the matches the user wants to see
if (db.searchString == searchString) {
db.owner.found(matches);
}
db.owner.setStatus('found');
}
},
error: function (xmlhttp) {
alert('An HTTP error '+ xmlhttp.status +' occured.\n'+ db.uri);
}
});
}, this.delay);
}
/**
* Cancels the current autocomplete request
*/
ACDB.prototype.cancel = function() {
Drupal.ACDB.prototype.cancel = function() {
if (this.owner) this.owner.setStatus('cancel');
if (this.timer) clearTimeout(this.timer);
if (this.transport) {
this.transport.onreadystatechange = function() {};
this.transport.abort();
}
this.searchString = '';
}
// Global Killswitch
if (Drupal.jsEnabled) {
$(document).ready(Drupal.autocompleteAutoAttach);
}
// $Id$
if (isJsEnabled()) {
addLoadEvent(collapseAutoAttach);
}
function collapseAutoAttach() {
var fieldsets = document.getElementsByTagName('fieldset');
var legend, fieldset;
for (var i = 0; fieldset = fieldsets[i]; i++) {
if (!hasClass(fieldset, 'collapsible')) {
continue;
}
legend = fieldset.getElementsByTagName('legend');
if (legend.length == 0) {
continue;
}
legend = legend[0];
Drupal.collapseAutoAttach = function () {
$('fieldset.collapsible legend').each(function () {
// Turn the legend into clickable link
var a = document.createElement('a');
a.href = '#';
a.onclick = function() {
toggleClass(this.parentNode.parentNode, 'collapsed');
if (!hasClass(this.parentNode.parentNode, 'collapsed')) {
collapseScrollIntoView(this.parentNode.parentNode);
if (typeof textAreaAutoAttach != 'undefined') {
// Add the grippie to a textarea in a collapsed fieldset.
textAreaAutoAttach(null, this.parentNode.parentNode);
$(a)
.click(function() {
var fieldset = this.parentNode.parentNode;
// Prevent double animations
if (fieldset.animating) {
return false;
}
fieldset.animating = true;
if ($(fieldset).is('.collapsed')) {
// Open fieldset with animation
$(fieldset.contentWrapper).hide();
$(fieldset).removeClass('collapsed');
$(fieldset.contentWrapper).slideDown(300,
{
// Make sure we open to height auto
complete: function() {
$(fieldset.contentWrapper).css('height', 'auto');
Drupal.collapseScrollIntoView(fieldset);
fieldset.animating = false;
},
// Scroll the fieldset into view
step: function() {
Drupal.collapseScrollIntoView(fieldset);
}
}
);
if (typeof Drupal.textareaAttach != 'undefined') {
// Initialize resizable textareas that are now revealed
Drupal.textareaAttach(null, fieldset);
}
}
else {
// Collapse fieldset with animation (reverse of opening)
$(fieldset.contentWrapper)
.slideUp('medium', function () { $(fieldset).addClass('collapsed'); fieldset.animating = false; } )
.show();
}
}
this.blur();
return false;
};
a.innerHTML = legend.innerHTML;
while (legend.hasChildNodes()) {
removeNode(legend.childNodes[0]);
this.blur();
return false;
})
.html(this.innerHTML);
$(this)
.empty()
.append(a);
// Wrap fieldsets contents (except for the legend) into wrapper divs for animating.
// div1 is used to avoid margin problems inside fieldsets,
// div2 is the one that is actually animated.
var div1 = document.createElement('div');
var div2 = document.createElement('div');
this.parentNode.contentWrapper = div2;
$(this).after(div1);
$(div1).append(div2);
var el = div1.nextSibling;
while (el != null) {
var next = el.nextSibling;
$(el).remove();
$(div2).append(el);
el = next;
}
legend.appendChild(a);
collapseEnsureErrorsVisible(fieldset);
}
}
// Avoid jumping around due to margins collapsing into fieldset border
$(div1).css('overflow', 'hidden');
function collapseEnsureErrorsVisible(fieldset) {
if (!hasClass(fieldset, 'collapsed')) {
return;
}
var inputs = [];
inputs = inputs.concat(fieldset.getElementsByTagName('input'));
inputs = inputs.concat(fieldset.getElementsByTagName('textarea'));
inputs = inputs.concat(fieldset.getElementsByTagName('select'));
for (var j = 0; j<3; j++) {
for (var i = 0; i < inputs[j].length; i++) {
if (hasClass(inputs[j][i], 'error')) {
return removeClass(fieldset, 'collapsed');
}
// Expand if there are errors inside
if ($('input.error, textarea.error, select.error', this.parentNode).size() > 0) {
$(this.parentNode).removeClass('collapsed');
}
}
});
}
function collapseScrollIntoView(node) {
/**
* Scroll a given fieldset into view as much as possible.
*/
Drupal.collapseScrollIntoView = function (node) {
var h = self.innerHeight || document.documentElement.clientHeight || document.body.clientHeight || 0;
var offset = self.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
var pos = absolutePosition(node);
if (pos.y + node.scrollHeight > h + offset) {
if (node.scrollHeight > h) {
var pos = Drupal.absolutePosition(node);
var fudge = 55;
if (pos.y + node.offsetHeight + fudge > h + offset) {
if (node.offsetHeight > h) {
window.scrollTo(0, pos.y);
} else {
window.scrollTo(0, pos.y + node.scrollHeight - h);
window.scrollTo(0, pos.y + node.offsetHeight - h + fudge);
}
}
}
// Global Killswitch
if (Drupal.jsEnabled) {
$(document).ready(Drupal.collapseAutoAttach);
}
// $Id$
/**
* Only enable Javascript functionality if all required features are supported.
*/
function isJsEnabled() {
if (typeof document.jsEnabled == 'undefined') {
// Note: ! casts to boolean implicitly.
document.jsEnabled = !(
!document.getElementsByTagName ||
!document.createElement ||
!document.createTextNode ||
!document.documentElement ||
!document.getElementById);
}
return document.jsEnabled;
}
// Global Killswitch on the <html> element
if (isJsEnabled()) {
document.documentElement.className = 'js';
}
var Drupal = Drupal || {};
/**
* The global Drupal variable.
* Set the variable that indicates if JavaScript behaviors should be applied
*/
Drupal = { };
Drupal.jsEnabled = document.getElementsByTagName && document.createElement && document.createTextNode && document.documentElement && document.getElementById;
/**
* Merge an object into the Drupal namespace.
*
* @param obj
* The object that should be merged into the Drupal namespace. Arbitrary objects
* containing functions, variables or other objects can be used. An example object
* would be { settings: { tree: { '/js/menu/tree': { mid: 206 } } } }. This item
* can now be accessed at Drupal.settings.tree['/js/menu/tree'].mid.
* Extends the current object with the parameter. Works recursively.
*/
Drupal.extend = function(obj) {
for (var i in obj) {
......@@ -44,109 +19,26 @@ Drupal.extend = function(obj) {
this[i] = obj[i];
}
}
};
/**
* Make IE's XMLHTTP object accessible through XMLHttpRequest()
*/
if (typeof XMLHttpRequest == 'undefined') {
XMLHttpRequest = function () {
var msxmls = ['MSXML3', 'MSXML2', 'Microsoft']
for (var i=0; i < msxmls.length; i++) {
try {
return new ActiveXObject(msxmls[i]+'.XMLHTTP')
}
catch (e) { }
}
throw new Error("No XML component installed!");
}
}