Commit a7e6d4af authored by Dries's avatar Dries
Browse files

Merge branch '8.x' of git.drupal.org:project/drupal into 8.x

parents cfab9bc4 00581a73
......@@ -51,12 +51,12 @@ public function remove($path);
* @param $files
* Optionally specify a list of files to be extracted. Files are
* relative to the root of the archive. If not specified, all files
* in the archive will be extracted
* in the archive will be extracted.
*
* @return ArchiverInterface
* The called object.
*/
public function extract($path, Array $files = array());
public function extract($path, array $files = array());
/**
* Lists all files in the archive.
......
......@@ -1850,7 +1850,9 @@ function format_interval($interval, $granularity = 2, $langcode = NULL) {
* A UNIX timestamp to format.
* @param $type
* (optional) The format to use, one of:
* - 'short', 'medium', or 'long' (the corresponding built-in date formats).
* - One of the built-in formats: 'short', 'medium', 'long', 'html_datetime',
* 'html_date', 'html_time', 'html_yearless_date', 'html_week',
* 'html_month', 'html_year'.
* - The name of a date type defined by a module in hook_date_format_types(),
* if it's been assigned a format.
* - The machine name of an administrator-defined date format.
......@@ -1903,6 +1905,34 @@ function format_date($timestamp, $type = 'medium', $format = '', $timezone = NUL
$format = variable_get('date_format_long', 'l, F j, Y - H:i');
break;
case 'html_datetime':
$format = variable_get('date_format_html_datetime', 'Y-m-d\TH:i:sO');
break;
case 'html_date':
$format = variable_get('date_format_html_date', 'Y-m-d');
break;
case 'html_time':
$format = variable_get('date_format_html_time', 'H:i:s');
break;
case 'html_yearless_date':
$format = variable_get('date_format_html_yearless_date', 'm-d');
break;
case 'html_week':
$format = variable_get('date_format_html_week', 'Y-\WW');
break;
case 'html_month':
$format = variable_get('date_format_html_month', 'Y-m');
break;
case 'html_year':
$format = variable_get('date_format_html_year', 'Y');
break;
case 'custom':
// No change to format.
break;
......@@ -6720,6 +6750,9 @@ function drupal_common_theme() {
'render element' => 'elements',
'template' => 'region',
),
'datetime' => array(
'variables' => array('timestamp' => NULL, 'text' => NULL, 'attributes' => array(), 'html' => FALSE),
),
'status_messages' => array(
'variables' => array('display' => NULL),
),
......
......@@ -447,18 +447,10 @@ function menu_get_item($path = NULL, $router_item = NULL) {
}
$original_map = arg(NULL, $path);
// Since there is no limit to the length of $path, use a hash to keep it
// short yet unique.
$cid = 'menu_item:' . hash('sha256', $path);
if ($cached = cache('menu')->get($cid)) {
$router_item = $cached->data;
}
else {
$parts = array_slice($original_map, 0, MENU_MAX_PARTS);
$ancestors = menu_get_ancestors($parts);
$router_item = db_query_range('SELECT * FROM {menu_router} WHERE path IN (:ancestors) ORDER BY fit DESC', 0, 1, array(':ancestors' => $ancestors))->fetchAssoc();
cache('menu')->set($cid, $router_item);
}
$parts = array_slice($original_map, 0, MENU_MAX_PARTS);
$ancestors = menu_get_ancestors($parts);
$router_item = db_query_range('SELECT * FROM {menu_router} WHERE path IN (:ancestors) ORDER BY fit DESC', 0, 1, array(':ancestors' => $ancestors))->fetchAssoc();
if ($router_item) {
// Allow modules to alter the router item before it is translated and
// checked for access.
......
......@@ -1474,6 +1474,66 @@ function theme_disable($theme_list) {
* @{
*/
/**
* Preprocess variables for theme_datetime().
*/
function template_preprocess_datetime(&$variables) {
// Format the 'datetime' attribute based on the timestamp.
// @see http://www.w3.org/TR/html5-author/the-time-element.html#attr-time-datetime
if (!isset($variables['attributes']['datetime']) && isset($variables['timestamp'])) {
$variables['attributes']['datetime'] = format_date($variables['timestamp'], 'html_datetime', '', 'UTC');
}
// If no text was provided, try to auto-generate it.
if (!isset($variables['text'])) {
// Format and use a human-readable version of the timestamp, if any.
if (isset($variables['timestamp'])) {
$variables['text'] = format_date($variables['timestamp']);
$variables['html'] = FALSE;
}
// Otherwise, use the literal datetime attribute.
elseif (isset($variables['attributes']['datetime'])) {
$variables['text'] = $variables['attributes']['datetime'];
$variables['html'] = FALSE;
}
}
}
/**
* Returns HTML for a date / time.
*
* @param $variables
* An associative array containing:
* - timestamp: (optional) A UNIX timestamp for the datetime attribute. If the
* datetime cannot be represented as a UNIX timestamp, use a valid datetime
* attribute value in $variables['attributes']['datetime'].
* - text: (optional) The content to display within the <time> element. Set
* 'html' to TRUE if this value is already sanitized for output in HTML.
* Defaults to a human-readable representation of the timestamp value or the
* datetime attribute value using format_date().
* When invoked as #theme or #theme_wrappers of a render element, the
* rendered #children are autoamtically taken over as 'text', unless #text
* is explicitly set.
* - attributes: (optional) An associative array of HTML attributes to apply
* to the <time> element. A datetime attribute in 'attributes' overrides the
* 'timestamp'. To create a valid datetime attribute value from a UNIX
* timestamp, use format_date() with one of the predefined 'html_*' formats.
* - html: (optional) Whether 'text' is HTML markup (TRUE) or plain-text
* (FALSE). Defaults to FALSE. For example, to use a SPAN tag within the
* TIME element, this must be set to TRUE, or the SPAN tag will be escaped.
* It is the responsibility of the caller to properly sanitize the value
* contained in 'text' (or within the SPAN tag in aforementioned example).
*
* @see template_preprocess_datetime()
* @see http://www.w3.org/TR/html5-author/the-time-element.html#attr-time-datetime
*/
function theme_datetime($variables) {
$output = '<time' . drupal_attributes($variables['attributes']) . '>';
$output .= !empty($variables['html']) ? $variables['text'] : check_plain($variables['text']);
$output .= '</time>';
return $output;
}
/**
* Returns HTML for status and/or error messages, grouped by type.
*
......
......@@ -21,7 +21,7 @@ Drupal.behaviors.states = {
new states.Dependent({
element: $(selector),
state: states.State.sanitize(state),
dependees: settings.states[selector][state]
constraints: settings.states[selector][state]
});
}
}
......@@ -40,12 +40,14 @@ Drupal.behaviors.states = {
* Object with the following keys (all of which are required):
* - element: A jQuery object of the dependent element
* - state: A State object describing the state that is dependent
* - dependees: An object with dependency specifications. Lists all elements
* that this element depends on.
* - constraints: An object with dependency specifications. Lists all elements
* that this element depends on. It can be nested and can contain arbitrary
* AND and OR clauses.
*/
states.Dependent = function (args) {
$.extend(this, { values: {}, oldValue: undefined }, args);
$.extend(this, { values: {}, oldValue: null }, args);
this.dependees = this.getDependees();
for (var selector in this.dependees) {
this.initializeDependee(selector, this.dependees[selector]);
}
......@@ -69,7 +71,7 @@ states.Dependent.comparisons = {
// as a string before applying the strict comparison in compare(). Otherwise
// numeric keys in the form's #states array fail to match string values
// returned from jQuery's val().
return (value.constructor.name === 'String') ? compare(String(reference), value) : compare(reference, value);
return (typeof value === 'string') ? compare(reference.toString(), value) : compare(reference, value);
}
};
......@@ -84,26 +86,33 @@ states.Dependent.prototype = {
* dependee's compliance status.
*/
initializeDependee: function (selector, dependeeStates) {
var self = this;
var state;
// Cache for the states of this dependee.
self.values[selector] = {};
this.values[selector] = {};
$.each(dependeeStates, function (state, value) {
state = states.State.sanitize(state);
for (var i in dependeeStates) {
if (dependeeStates.hasOwnProperty(i)) {
state = dependeeStates[i];
// Make sure we're not initializing this selector/state combination twice.
if ($.inArray(state, dependeeStates) === -1) {
continue;
}
// Initialize the value of this state.
self.values[selector][state.pristine] = undefined;
state = states.State.sanitize(state);
// Monitor state changes of the specified state for this dependee.
$(selector).bind('state:' + state, function (e) {
var complies = self.compare(value, e.value);
self.update(selector, state, complies);
});
// Initialize the value of this state.
this.values[selector][state.name] = null;
// Make sure the event we just bound ourselves to is actually fired.
new states.Trigger({ selector: selector, state: state });
});
// Monitor state changes of the specified state for this dependee.
$(selector).bind('state:' + state, $.proxy(function (e) {
this.update(selector, state, e.value);
}, this));
// Make sure the event we just bound ourselves to is actually fired.
new states.Trigger({ selector: selector, state: state });
}
}
},
/**
......@@ -111,12 +120,16 @@ states.Dependent.prototype = {
*
* @param reference
* The value used for reference.
* @param value
* The value to compare with the reference value.
* @param selector
* CSS selector describing the dependee.
* @param state
* A State object describing the dependee's updated state.
*
* @return
* true, undefined or false.
* true or false.
*/
compare: function (reference, value) {
compare: function (reference, selector, state) {
var value = this.values[selector][state.name];
if (reference.constructor.name in states.Dependent.comparisons) {
// Use a custom compare function for certain reference value types.
return states.Dependent.comparisons[reference.constructor.name](reference, value);
......@@ -139,8 +152,8 @@ states.Dependent.prototype = {
*/
update: function (selector, state, value) {
// Only act when the 'new' value is actually new.
if (value !== this.values[selector][state.pristine]) {
this.values[selector][state.pristine] = value;
if (value !== this.values[selector][state.name]) {
this.values[selector][state.name] = value;
this.reevaluate();
}
},
......@@ -149,16 +162,8 @@ states.Dependent.prototype = {
* Triggers change events in case a state changed.
*/
reevaluate: function () {
var value = undefined;
// Merge all individual values to find out whether this dependee complies.
for (var selector in this.values) {
for (var state in this.values[selector]) {
state = states.State.sanitize(state);
var complies = this.values[selector][state.pristine];
value = ternary(value, invert(complies, state.invert));
}
}
// Check whether any constraint for this dependent state is satisifed.
var value = this.verifyConstraints(this.constraints);
// Only invoke a state change event when the value actually changed.
if (value !== this.oldValue) {
......@@ -173,6 +178,124 @@ states.Dependent.prototype = {
// infinite loops.
this.element.trigger({ type: 'state:' + this.state, value: value, trigger: true });
}
},
/**
* Evaluates child constraints to determine if a constraint is satisfied.
*
* @param constraints
* A constraint object or an array of constraints.
* @param selector
* The selector for these constraints. If undefined, there isn't yet a
* selector that these constraints apply to. In that case, the keys of the
* object are interpreted as the selector if encountered.
*
* @return
* true or false, depending on whether these constraints are satisfied.
*/
verifyConstraints: function(constraints, selector) {
var result;
if ($.isArray(constraints)) {
// This constraint is an array (OR or XOR).
var hasXor = $.inArray('xor', constraints) === -1;
for (var i = 0, len = constraints.length; i < len; i++) {
if (constraints[i] != 'xor') {
var constraint = this.checkConstraints(constraints[i], selector, i);
// Return if this is OR and we have a satisfied constraint or if this
// is XOR and we have a second satisfied constraint.
if (constraint && (hasXor || result)) {
return hasXor;
}
result = result || constraint;
}
}
}
// Make sure we don't try to iterate over things other than objects. This
// shouldn't normally occur, but in case the condition definition is bogus,
// we don't want to end up with an infinite loop.
else if ($.isPlainObject(constraints)) {
// This constraint is an object (AND).
for (var n in constraints) {
if (constraints.hasOwnProperty(n)) {
result = ternary(result, this.checkConstraints(constraints[n], selector, n));
// False and anything else will evaluate to false, so return when any
// false condition is found.
if (result === false) { return false; }
}
}
}
return result;
},
/**
* Checks whether the value matches the requirements for this constraint.
*
* @param value
* Either the value of a state or an array/object of constraints. In the
* latter case, resolving the constraint continues.
* @param selector
* The selector for this constraint. If undefined, there isn't yet a
* selector that this constraint applies to. In that case, the state key is
* propagates to a selector and resolving continues.
* @param state
* The state to check for this constraint. If undefined, resolving
* continues.
* If both selector and state aren't undefined and valid non-numeric
* strings, a lookup for the actual value of that selector's state is
* performed. This parameter is not a State object but a pristine state
* string.
*
* @return
* true or false, depending on whether this constraint is satisfied.
*/
checkConstraints: function(value, selector, state) {
// Normalize the last parameter. If it's non-numeric, we treat it either as
// a selector (in case there isn't one yet) or as a trigger/state.
if (typeof state !== 'string' || (/[0-9]/).test(state[0])) {
state = null;
}
else if (typeof selector === 'undefined') {
// Propagate the state to the selector when there isn't one yet.
selector = state;
state = null;
}
if (state !== null) {
// constraints is the actual constraints of an element to check for.
state = states.State.sanitize(state);
return invert(this.compare(value, selector, state), state.invert);
}
else {
// Resolve this constraint as an AND/OR operator.
return this.verifyConstraints(value, selector);
}
},
/**
* Gathers information about all required triggers.
*/
getDependees: function() {
var cache = {};
// Swivel the lookup function so that we can record all available selector-
// state combinations for initialization.
var _compare = this.compare;
this.compare = function(reference, selector, state) {
(cache[selector] || (cache[selector] = [])).push(state.name);
// Return nothing (=== undefined) so that the constraint loops are not
// broken.
};
// This call doesn't actually verify anything but uses the resolving
// mechanism to go through the constraints array, trying to look up each
// value. Since we swivelled the compare function, this comparison returns
// undefined and lookup continues until the very end. Instead of lookup up
// the value, we record that combination of selector and state so that we
// can initialize all triggers.
this.verifyConstraints(this.constraints);
// Restore the original function.
this.compare = _compare;
return cache;
}
};
......@@ -192,7 +315,6 @@ states.Trigger = function (args) {
states.Trigger.prototype = {
initialize: function () {
var self = this;
var trigger = states.Trigger.states[this.state];
if (typeof trigger == 'function') {
......@@ -200,9 +322,11 @@ states.Trigger.prototype = {
trigger.call(window, this.element);
}
else {
$.each(trigger, function (event, valueFn) {
self.defaultTrigger(event, valueFn);
});
for (var event in trigger) {
if (trigger.hasOwnProperty(event)) {
this.defaultTrigger(event, trigger[event]);
}
}
}
// Mark this trigger as initialized for this element.
......@@ -210,23 +334,22 @@ states.Trigger.prototype = {
},
defaultTrigger: function (event, valueFn) {
var self = this;
var oldValue = valueFn.call(this.element);
// Attach the event callback.
this.element.bind(event, function (e) {
var value = valueFn.call(self.element, e);
this.element.bind(event, $.proxy(function (e) {
var value = valueFn.call(this.element, e);
// Only trigger the event if the value has actually changed.
if (oldValue !== value) {
self.element.trigger({ type: 'state:' + self.state, value: value, oldValue: oldValue });
this.element.trigger({ type: 'state:' + this.state, value: value, oldValue: oldValue });
oldValue = value;
}
});
}, this));
states.postponed.push(function () {
states.postponed.push($.proxy(function () {
// Trigger the event once for initialization purposes.
self.element.trigger({ type: 'state:' + self.state, value: oldValue, oldValue: undefined });
});
this.element.trigger({ type: 'state:' + this.state, value: oldValue, oldValue: null });
}, this));
}
};
......@@ -286,7 +409,7 @@ states.Trigger.states = {
collapsed: {
'collapsed': function(e) {
return (e !== undefined && 'value' in e) ? e.value : this.is('.collapsed');
return (typeof e !== 'undefined' && 'value' in e) ? e.value : this.is('.collapsed');
}
}
};
......@@ -318,7 +441,7 @@ states.State = function(state) {
};
/**
* Create a new State object by sanitizing the passed value.
* Creates a new State object by sanitizing the passed value.
*/
states.State.sanitize = function (state) {
if (state instanceof states.State) {
......@@ -363,72 +486,71 @@ states.State.prototype = {
* bubble up to these handlers. We use this system so that themes and modules
* can override these state change handlers for particular parts of a page.
*/
{
$(document).bind('state:disabled', function(e) {
// Only act when this change was triggered by a dependency and not by the
// element monitoring itself.
if (e.trigger) {
$(e.target)
.attr('disabled', e.value)
.filter('.form-element')
.closest('.form-item, .form-submit, .form-wrapper')[e.value ? 'addClass' : 'removeClass']('form-disabled');
// Note: WebKit nightlies don't reflect that change correctly.
// See https://bugs.webkit.org/show_bug.cgi?id=23789
}
});
$(document).bind('state:required', function(e) {
if (e.trigger) {
if (e.value) {
$(e.target).closest('.form-item, .form-wrapper').find('label').append('<abbr class="form-required" title="' + Drupal.t('This field is required.') + '">*</abbr>');
}
else {
$(e.target).closest('.form-item, .form-wrapper').find('label .form-required').remove();
}
}
});
$(document).bind('state:disabled', function(e) {
// Only act when this change was triggered by a dependency and not by the
// element monitoring itself.
if (e.trigger) {
$(e.target)
.attr('disabled', e.value)
.filter('.form-element')
.closest('.form-item, .form-submit, .form-wrapper')[e.value ? 'addClass' : 'removeClass']('form-disabled');
// Note: WebKit nightlies don't reflect that change correctly.
// See https://bugs.webkit.org/show_bug.cgi?id=23789
}
});
$(document).bind('state:visible', function(e) {
if (e.trigger) {
$(e.target).closest('.form-item, .form-submit, .form-wrapper')[e.value ? 'show' : 'hide']();
$(document).bind('state:required', function(e) {
if (e.trigger) {
if (e.value) {
$(e.target).closest('.form-item, .form-wrapper').find('label').append('<abbr class="form-required" title="' + Drupal.t('This field is required.') + '">*</abbr>');
}
});
$(document).bind('state:checked', function(e) {
if (e.trigger) {
$(e.target).attr('checked', e.value);
else {
$(e.target).closest('.form-item, .form-wrapper').find('label .form-required').remove();
}
});
}
});
$(document).bind('state:collapsed', function(e) {
if (e.trigger) {
if ($(e.target).is('.collapsed') !== e.value) {
$('> legend a', e.target).click();
}
$(document).bind('state:visible', function(e) {
if (e.trigger) {
$(e.target).closest('.form-item, .form-submit, .form-wrapper')[e.value ? 'show' : 'hide']();
}
});
$(document).bind('state:checked', function(e) {
if (e.trigger) {
$(e.target).attr('checked', e.value);
}
});
$(document).bind('state:collapsed', function(e) {
if (e.trigger) {
if ($(e.target).is('.collapsed') !== e.value) {
$('> legend a', e.target).click();
}
});
}
}
});
/**
* These are helper functions implementing addition "operators" and don't
* implement any logic that is particular to states.
*/
{
// Bitwise AND with a third undefined state.
function ternary (a, b) {
return a === undefined ? b : (b === undefined ? a : a && b);
};
// Inverts a (if it's not undefined) when invert is true.
function invert (a, invert) {
return (invert && a !== undefined) ? !a : a;
};
// Compares two values while ignoring undefined values.
function compare (a, b) {
return (a === b) ? (a === undefined ? a : true) : (a === undefined || b === undefined);
}
// Bitwise AND with a third undefined state.
function ternary (a, b) {
return typeof a === 'undefined' ? b : (typeof b === 'undefined' ? a : a && b);
}
// Inverts a (if it's not undefined) when invert is true.
function invert (a, invert) {
return (invert && typeof a !== 'undefined') ? !a : a;
}
// Compares two values while ignoring undefined values.
function compare (a, b) {
return (a === b) ? (typeof a === 'undefined' ? a : true) : (typeof a === 'undefined' || typeof b === 'undefined');
}