1
0
mirror of https://github.com/e107inc/e107.git synced 2025-10-21 01:36:27 +02:00

Issue #991: Removed all Selectize JS code from PHP. Additional option (settings) for e107::js(). Integrates Selectize.js using e107::js(settings) and JavaScript behaviors with jQuery Once.

This commit is contained in:
Lóna Lore
2015-12-02 14:23:48 +01:00
parent 84c4c8607d
commit 2cd5f1525f
8 changed files with 522 additions and 107 deletions

View File

@@ -1668,6 +1668,10 @@ class e107
switch ($type)
{
case 'settings':
$jshandler->jsSettings($data);
break;
case 'core':
// data is e.g. 'core/tabs.js'
if(null !== $zone) $jshandler->requireCoreLib($data, $zone);

View File

@@ -1954,6 +1954,137 @@ class e_parse extends e_parser
}
/**
* Converts a PHP variable into its JavaScript equivalent.
* We use HTML-safe strings, with several characters escaped.
*
* @param mixed $var
* @return string
*/
public function toJSON($var)
{
// The PHP version cannot change within a request.
static $php530;
if(!isset($php530))
{
$php530 = version_compare(PHP_VERSION, '5.3.0', '>=');
}
if($php530)
{
// Encode <, >, ', &, and " using the json_encode() options parameter.
return json_encode($var, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);
}
return $this->toJSONhelper($var);
}
/**
* Encodes a PHP variable to HTML-safe JSON for PHP versions below 5.3.0.
*
* @param mixed $var
* @return string
*/
public function toJSONhelper($var)
{
switch(gettype($var))
{
case 'boolean':
return $var ? 'true' : 'false'; // Lowercase necessary!
case 'integer':
case 'double':
return $var;
case 'resource':
case 'string':
// Always use Unicode escape sequences (\u0022) over JSON escape
// sequences (\") to prevent browsers interpreting these as
// special characters.
$replace_pairs = array(
// ", \ and U+0000 - U+001F must be escaped according to RFC 4627.
'\\' => '\u005C',
'"' => '\u0022',
"\x00" => '\u0000',
"\x01" => '\u0001',
"\x02" => '\u0002',
"\x03" => '\u0003',
"\x04" => '\u0004',
"\x05" => '\u0005',
"\x06" => '\u0006',
"\x07" => '\u0007',
"\x08" => '\u0008',
"\x09" => '\u0009',
"\x0a" => '\u000A',
"\x0b" => '\u000B',
"\x0c" => '\u000C',
"\x0d" => '\u000D',
"\x0e" => '\u000E',
"\x0f" => '\u000F',
"\x10" => '\u0010',
"\x11" => '\u0011',
"\x12" => '\u0012',
"\x13" => '\u0013',
"\x14" => '\u0014',
"\x15" => '\u0015',
"\x16" => '\u0016',
"\x17" => '\u0017',
"\x18" => '\u0018',
"\x19" => '\u0019',
"\x1a" => '\u001A',
"\x1b" => '\u001B',
"\x1c" => '\u001C',
"\x1d" => '\u001D',
"\x1e" => '\u001E',
"\x1f" => '\u001F',
// Prevent browsers from interpreting these as as special.
"'" => '\u0027',
'<' => '\u003C',
'>' => '\u003E',
'&' => '\u0026',
// Prevent browsers from interpreting the solidus as special and
// non-compliant JSON parsers from interpreting // as a comment.
'/' => '\u002F',
// While these are allowed unescaped according to ECMA-262, section
// 15.12.2, they cause problems in some JSON parsers.
"\xe2\x80\xa8" => '\u2028', // U+2028, Line Separator.
"\xe2\x80\xa9" => '\u2029', // U+2029, Paragraph Separator.
);
return '"' . strtr($var, $replace_pairs) . '"';
case 'array':
// Arrays in JSON can't be associative. If the array is empty or if it
// has sequential whole number keys starting with 0, it's not associative
// so we can go ahead and convert it as an array.
if(empty($var) || array_keys($var) === range(0, sizeof($var) - 1))
{
$output = array();
foreach($var as $v)
{
$output[] = $this->toJSONhelper($v);
}
return '[ ' . implode(', ', $output) . ' ]';
}
break;
// Otherwise, fall through to convert the array as an object.
case 'object':
$output = array();
foreach($var as $k => $v)
{
$output[] = $this->toJSONhelper(strval($k)) . ':' . $this->toJSONhelper($v);
}
return '{' . implode(', ', $output) . '}';
default:
return 'null';
}
}
/**
* Convert Text for RSS/XML use.
*

View File

@@ -422,109 +422,20 @@ class e_form
e107::css('core', 'selectize/css/selectize.bootstrap2.css', 'jquery');
}
$ac = $options['selectize'];
$fieldID = vartrue($options['id'], $this->name2id($name));
// Load selectize behavior.
e107::js('core', 'selectize/js/selectize.init.js', 'jquery');
// TODO: better method to create suffix as unique identifier.
$optionSuffix = $fieldID . vartrue($options['selectize']['e_editable']);
$optionSuffix = md5($optionSuffix);
$jsSettings = array(
'id' => vartrue($options['id'], $this->name2id($name)),
'options' => $options['selectize'],
// Multilingual support.
'strings' => array(
'anonymous' => LAN_ANONYMOUS,
),
);
// We need to declare an options array with unique ID for the selectize.js field, because we have to store
// the latest default values, because if the selectize() would be reinitialized, e.g. in x-editable popover,
// we have to get the latest value(s).
$jsOptions = 'var options' . $optionSuffix . ' = ';
$jsOptions .= (vartrue($ac['options']) ? json_encode($ac['options']) : "[]") . ";\n";
// TODO: more options and callbacks.
$js = "$('#" . $fieldID . "').selectize({
// General options.
items: " . (vartrue($ac['items']) ? json_encode($ac['items']) : '[]') . ",
delimiter: '" . (vartrue($ac['delimiter'], ',')) . "',
diacritics: " . (vartrue($ac['diacritics'], true) ? 'true' : 'false') . ",
create: " . (vartrue($ac['create'], false) ? 'true' : 'false') . ",
createOnBlur: " . (vartrue($ac['createOnBlur'], true) ? 'true' : 'false') . ",
highlight: " . (vartrue($ac['highlight'], false) ? 'true' : 'false') . ",
persist: " . (vartrue($ac['persist'], false) ? 'true' : 'false') . ",
openOnFocus: " . (vartrue($ac['openOnFocus'], false) ? 'true' : 'false') . ",
maxOptions: " . vartrue($ac['maxOptions'], 'null') . ",
maxItems: " . vartrue($ac['maxItems'], 'null') . ",
hideSelected: " . (vartrue($ac['hideSelected'], false) ? 'true' : 'false') . ",
closeAfterSelect: " . (vartrue($ac['closeAfterSelect'], true) ? 'true' : 'false') . ",
allowEmptyOption: " . (vartrue($ac['allowEmptyOption'], false) ? 'true' : 'false') . ",
scrollDuration: " . vartrue($ac['scrollDuration'], 60) . ",
loadThrottle: " . vartrue($ac['loadThrottle'], 300) . ",
loadingClass: '" . vartrue($ac['loadingClass'], 'loading') . "',
preload: " . (vartrue($ac['preload'], false) ? 'true' : 'false') . ",
dropdownParent: " . vartrue($ac['dropdownParent'], 'null') . ",
addPrecedence: " . (vartrue($ac['addPrecedence'], false) ? 'true' : 'false') . ",
selectOnTab: " . (vartrue($ac['selectOnTab'], false) ? 'true' : 'false') . ",
mode: '" . (vartrue($ac['mode'], 'multi')) . "',
plugins: " . (vartrue($ac['plugins']) ? json_encode($ac['plugins']) : '[]') . ",
// Data / Searching.
options: options" . $optionSuffix . ",
valueField: '" . vartrue($ac['valueField'], 'value') . "',
labelField: '" . vartrue($ac['labelField'], 'label') . "',
searchField: '" . vartrue($ac['searchField'], 'label') . "',
// Callbacks.
load: function(query, callback) {
var loadPath = '" . vartrue($ac['loadPath'], '') . "';
if (loadPath == '') return callback([]);
if (!query.length) return callback([]);
$.ajax({
url: loadPath,
type: 'POST',
dataType: 'json',
data: {
q: query,
l: " . vartrue($ac['maxOptions'], 10) . "
},
error: function() {
callback([]);
},
success: function(data) {
// Update items in options array of this field.
options" . $optionSuffix . " = data;
callback(data);
}
});
}
});";
// We have to generate JS for x-editable field, so we have to initialize selectize.js after popover opens.
if(vartrue($options['selectize']['e_editable']))
{
// TODO: instead of setTimeout, we should use an x-editable callback.
$click = "$('#" . $options['selectize']['e_editable'] . "').click(function(){
// Attach success callback for replacing user id with name.
$.fn.editable.defaults.success = function(response, newValue) {
if(response.status == 'error') return;
if ($('#" . $options['selectize']['e_editable'] . "').hasClass('editable-userpicker')) {
$('#" . $options['selectize']['e_editable'] . "').hide();
options" . $optionSuffix . " = options" . $optionSuffix . " || [];
userName = '" . LAN_ANONYMOUS . "';
$.each(options" . $optionSuffix . ", function(key, value) {
if (value." . vartrue($ac['valueField'], 'value') . " == newValue) {
userName = value." . vartrue($ac['labelField'], 'label') . ";
}
});
setTimeout(function(){
$('#" . $options['selectize']['e_editable'] . "').html(userName).show();
$.fn.editable.defaults.success = function(response, newValue) {}
}, 300);
}
};
setTimeout(function(){ " . $js . " }, 300);
})";
e107::js('footer-inline', "$(document).ready(function(){" . $jsOptions . $click . "});");
}
// We have to render JS for a simple form element.
else
{
e107::js('footer-inline', "$(document).ready(function(){" . $jsOptions . $js . "});");
}
// Merge field settings with other selectize field settings.
e107::js('settings', array('selectize' => array($jsSettings)));
}
// TODO: remove typeahead.

View File

@@ -45,7 +45,10 @@ class e_jsmanager
// "http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js",
// "http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"
// "http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"
"https://cdn.jsdelivr.net/jquery/2.1.4/jquery.min.js"
"https://cdn.jsdelivr.net/jquery/2.1.4/jquery.min.js",
// jQuery Once filters out all elements that had the same filter applied on them before. It can be used to
// ensure that a function is only applied once to an element. jQuery Once is used in e107.behaviors.
"https://cdnjs.cloudflare.com/ajax/libs/jquery-once/2.1.1/jquery.once.min.js"
// ,
// "http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.1/jquery-ui.min.js",
// "http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.1/themes/base/jquery-ui.css",
@@ -66,6 +69,15 @@ class e_jsmanager
protected $_core_prefs = array();
/**
* Array to store JavaScript options will be rendered in footer as JSON object.
*
* @var array
*/
protected $_e_js_settings = array(
'basePath' => e_HTTP,
);
/**
* Core JS library files, loaded via e_jslib.php
@@ -243,6 +255,9 @@ class e_jsmanager
{
$this->_libraries['jquery'] = array(
"https://cdn.jsdelivr.net/jquery/2.1.4/jquery.min.js",
// jQuery Once filters out all elements that had the same filter applied on them before. It can be used
// to ensure that a function is only applied once to an element. jQuery Once is used in e107.behaviors.
"https://cdnjs.cloudflare.com/ajax/libs/jquery-once/2.1.1/jquery.once.min.js",
"https://cdn.jsdelivr.net/jquery.ui/1.11.4/jquery-ui.min.js",
"https://cdn.jsdelivr.net/jquery.ui/1.11.4/themes/smoothness/jquery-ui.min.css"
);
@@ -634,6 +649,18 @@ class e_jsmanager
$this->addJs('footer_inline', $js_content, $priority);
return $this;
}
/**
* Add JS settings to site footer
*
* @param array $js_settings
* @return e_jsmanager
*/
public function jsSettings(array $js_settings)
{
$this->addJs('settings', $js_settings);
return $this;
}
function setDependency($dep)
@@ -816,7 +843,7 @@ class e_jsmanager
// FIXME - this could break something after CSS support was added, move it to separate method(s), recursion by type!
// Causes the css error on jquery-ui as a css file is loaded as a js.
if(is_array($file_path) )
if(is_array($file_path) && $type != 'settings')
{
// print_a($file_path);
foreach ($file_path as $fp => $loc)
@@ -960,6 +987,11 @@ class e_jsmanager
return $this;
break;
case 'settings':
$this->_e_js_settings = array_merge_recursive($this->_e_js_settings, $file_path);
return $this;
break;
default:
return $this;
break;
@@ -985,7 +1017,7 @@ class e_jsmanager
* @param boolean $return
* @return string JS content - only if $return is true
*/
public function renderJs($mod, $zone, $external = true, $return = false)
public function renderJs($mod, $zone = null, $external = true, $return = false)
{
if($return)
{
@@ -994,6 +1026,15 @@ class e_jsmanager
switch($mod)
{
case 'settings':
$tp = e107::getParser();
$options = $this->arrayMergeDeepArray(array($this->_e_js_settings));
$json = $tp->toJSON($options);
$js = 'jQuery.extend(e107.settings, ' . $json . ');';
echo '<script>' . $js . '</script>';
echo "\n";
break;
case 'framework': // CDN frameworks - rendered before consolidation script (if enabled)
$fw = array();
foreach ($this->_libraries as $lib)
@@ -1109,6 +1150,36 @@ class e_jsmanager
}
}
/**
* Merges multiple arrays, recursively, and returns the merged array.
*/
public function arrayMergeDeepArray($arrays) {
$result = array();
foreach ($arrays as $array) {
foreach ($array as $key => $value) {
// Renumber integer keys as array_merge_recursive() does. Note that PHP
// automatically converts array keys that are integer strings (e.g., '1')
// to integers.
if (is_integer($key)) {
$result[] = $value;
}
// Recurse when both values are arrays.
elseif (isset($result[$key]) && is_array($result[$key]) && is_array($value)) {
$result[$key] = $this->arrayMergeDeepArray(array($result[$key], $value));
}
// Otherwise, use the latter value, overriding any previous value.
else {
$result[$key] = $value;
}
}
}
return $result;
}
/**
* Render JS/CSS file array
*