From 2cd5f1525f0275442f31b5559d05073dbed0738c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?L=C3=B3na=20Lore?= <lonalore@freemail.hu>
Date: Wed, 2 Dec 2015 14:23:48 +0100
Subject: [PATCH] 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.

---
 e107_admin/footer.php                      |   4 +
 e107_core/templates/footer_default.php     |   4 +
 e107_handlers/e107_class.php               |   4 +
 e107_handlers/e_parse_class.php            | 131 ++++++++++++++++
 e107_handlers/form_handler.php             | 113 ++------------
 e107_handlers/js_manager.php               |  77 ++++++++-
 e107_web/js/core/all.jquery.js             | 123 ++++++++++++++-
 e107_web/js/selectize/js/selectize.init.js | 173 +++++++++++++++++++++
 8 files changed, 522 insertions(+), 107 deletions(-)
 create mode 100644 e107_web/js/selectize/js/selectize.init.js

diff --git a/e107_admin/footer.php b/e107_admin/footer.php
index a388c6773..553339423 100644
--- a/e107_admin/footer.php
+++ b/e107_admin/footer.php
@@ -348,6 +348,10 @@ if (abs($_serverTime - $lastSet) > 120)
      //  </script>\n";
 }
 
+// All JavaScript settings are placed in the footer of the page with the library weight so that inline scripts appear
+// afterwards.
+e107::getJs()->renderJs('settings');
+
 e107::getJs()->renderJs('footer_inline', true);
 
 //
diff --git a/e107_core/templates/footer_default.php b/e107_core/templates/footer_default.php
index b061d1f85..045695121 100644
--- a/e107_core/templates/footer_default.php
+++ b/e107_core/templates/footer_default.php
@@ -326,6 +326,10 @@ e107::getJs()->renderJs('footer', true);
 
 e107::getJs()->renderCached('js');
 
+// All JavaScript settings are placed in the footer of the page with the library weight so that inline scripts appear
+// afterwards.
+e107::getJs()->renderJs('settings');
+
 // [JSManager] Load JS Footer inline code by priority
 e107::getJs()->renderJs('footer_inline', true);
 
diff --git a/e107_handlers/e107_class.php b/e107_handlers/e107_class.php
index a71ca63d7..4c51a853e 100644
--- a/e107_handlers/e107_class.php
+++ b/e107_handlers/e107_class.php
@@ -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);
diff --git a/e107_handlers/e_parse_class.php b/e107_handlers/e_parse_class.php
index 76e3b6629..4ce2f1b4a 100644
--- a/e107_handlers/e_parse_class.php
+++ b/e107_handlers/e_parse_class.php
@@ -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.
 	 *
diff --git a/e107_handlers/form_handler.php b/e107_handlers/form_handler.php
index f5104504d..a7cc055b5 100644
--- a/e107_handlers/form_handler.php
+++ b/e107_handlers/form_handler.php
@@ -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.
diff --git a/e107_handlers/js_manager.php b/e107_handlers/js_manager.php
index 9536606b1..c047f1d4b 100644
--- a/e107_handlers/js_manager.php
+++ b/e107_handlers/js_manager.php
@@ -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
 	 *
diff --git a/e107_web/js/core/all.jquery.js b/e107_web/js/core/all.jquery.js
index 07ab2da55..9f27ee9b4 100644
--- a/e107_web/js/core/all.jquery.js
+++ b/e107_web/js/core/all.jquery.js
@@ -1,9 +1,126 @@
-// handle secured json string - the Prototype implementation
+var e107 = e107 || {'settings': {}, 'behaviors': {}};
 
+// Allow other JavaScript libraries to use $.
+// TODO: Use jQuery.noConflict(), but for this, need to rewrite all e107 javascript to use wrapper: (function ($) { ... })(jQuery);
+// jQuery.noConflict();
+
+(function ($) {
+
+	/**
+	 * Attach all registered behaviors to a page element.
+	 *
+	 * Behaviors are event-triggered actions that attach to page elements, enhancing
+	 * default non-JavaScript UIs. Behaviors are registered in the e107.behaviors
+	 * object using the method 'attach' and optionally also 'detach' as follows:
+	 * @code
+	 *    e107.behaviors.behaviorName = {
+	 *      attach: function (context, settings) {
+	 *        ...
+	 *      },
+	 *      detach: function (context, settings, trigger) {
+	 *        ...
+	 *      }
+	 *    };
+	 * @endcode
+	 *
+	 * e107.attachBehaviors is added below to the jQuery ready event and so
+	 * runs on initial page load. Developers implementing Ajax in their
+	 * solutions should also call this function after new page content has been
+	 * loaded, feeding in an element to be processed, in order to attach all
+	 * behaviors to the new content.
+	 *
+	 * Behaviors should use
+	 * @code
+	 *   $(selector).once('behavior-name', function () {
+	 *     ...
+	 *   });
+	 * @endcode
+	 * to ensure the behavior is attached only once to a given element. (Doing so
+	 * enables the reprocessing of given elements, which may be needed on occasion
+	 * despite the ability to limit behavior attachment to a particular element.)
+	 *
+	 * @param context
+	 *   An element to attach behaviors to. If none is given, the document element
+	 *   is used.
+	 * @param settings
+	 *   An object containing settings for the current context. If none given, the
+	 *   global e107.settings object is used.
+	 */
+	e107.attachBehaviors = function (context, settings) {
+		context = context || document;
+		settings = settings || e107.settings;
+		// Execute all of them.
+		$.each(e107.behaviors, function () {
+			if ($.isFunction(this.attach)) {
+				this.attach(context, settings);
+			}
+		});
+	};
+
+	/**
+	 * Detach registered behaviors from a page element.
+	 *
+	 * Developers implementing AHAH/Ajax in their solutions should call this
+	 * function before page content is about to be removed, feeding in an element
+	 * to be processed, in order to allow special behaviors to detach from the
+	 * content.
+	 *
+	 * Such implementations should look for the class name that was added in their
+	 * corresponding e107.behaviors.behaviorName.attach implementation, i.e.
+	 * behaviorName-processed, to ensure the behavior is detached only from
+	 * previously processed elements.
+	 *
+	 * @param context
+	 *   An element to detach behaviors from. If none is given, the document element
+	 *   is used.
+	 * @param settings
+	 *   An object containing settings for the current context. If none given, the
+	 *   global e107.settings object is used.
+	 * @param trigger
+	 *   A string containing what's causing the behaviors to be detached. The
+	 *   possible triggers are:
+	 *   - unload: (default) The context element is being removed from the DOM.
+	 *   - move: The element is about to be moved within the DOM (for example,
+	 *     during a tabledrag row swap). After the move is completed,
+	 *     e107.attachBehaviors() is called, so that the behavior can undo
+	 *     whatever it did in response to the move. Many behaviors won't need to
+	 *     do anything simply in response to the element being moved, but because
+	 *     IFRAME elements reload their "src" when being moved within the DOM,
+	 *     behaviors bound to IFRAME elements (like WYSIWYG editors) may need to
+	 *     take some action.
+	 *   - serialize: E.g. when an Ajax form is submitted, this is called with the
+	 *     form as the context. This provides every behavior within the form an
+	 *     opportunity to ensure that the field elements have correct content
+	 *     in them before the form is serialized. The canonical use-case is so
+	 *     that WYSIWYG editors can update the hidden textarea to which they are
+	 *     bound.
+	 *
+	 * @see e107.attachBehaviors
+	 */
+	e107.detachBehaviors = function (context, settings, trigger) {
+		context = context || document;
+		settings = settings || e107.settings;
+		trigger = trigger || 'unload';
+		// Execute all of them.
+		$.each(e107.behaviors, function () {
+			if ($.isFunction(this.detach)) {
+				this.detach(context, settings, trigger);
+			}
+		});
+	};
+
+	// Attach all behaviors.
+	$(function () {
+		e107.attachBehaviors(document, e107.settings);
+	});
+
+})(jQuery);
 
 $.ajaxSetup({
-	dataFilter: function(data, type) {
-		if(type != 'json' || !data) return data;
+	dataFilter: function (data, type) {
+		if (type != 'json' || !data) {
+			return data;
+		}
 		return data.replace(/^\/\*-secure-([\s\S]*)\*\/\s*$/, '$1');
 	},
 	cache: false // Was Really NEeded!
diff --git a/e107_web/js/selectize/js/selectize.init.js b/e107_web/js/selectize/js/selectize.init.js
new file mode 100644
index 000000000..1fdb12c0d
--- /dev/null
+++ b/e107_web/js/selectize/js/selectize.init.js
@@ -0,0 +1,173 @@
+(function ($) {
+
+    /**
+     * Behavior to initialize autocomplete fields with selectize.js
+     *
+     * @type {{attach: e107.behaviors.selectizeInit.attach}}
+     */
+    e107.behaviors.selectizeInit = {
+        attach: function (context, settings) {
+            if (e107.settings.selectize) {
+                $.each(e107.settings.selectize, function (index, item) {
+                    var key = item.id;
+
+                    // Inline, popup editor. Initialize selectize after opening popup.
+                    if (item.options.e_editable) {
+                        $(context).find('#' + item.options.e_editable).once('selectize-editable-init').each(function () {
+                            var $eEditable = $('#' + item.options.e_editable);
+
+                            $eEditable.click(function () {
+                                // Attach success callback for replacing user id with name.
+                                $.fn.editable.defaults.success = function (response, newValue) {
+                                    if (response.status == 'error') return;
+                                    if ($eEditable.hasClass('editable-userpicker')) {
+                                        $eEditable.hide();
+                                        var options = item.options.options ? item.options.options : [];
+                                        var userName = item.strings.anonymous ? item.strings.anonymous : '';
+                                        var valueField = item.options.valueField ? item.options.valueField : 'value';
+                                        var labelField = item.options.labelField ? item.options.labelField : 'label';
+                                        $.each(options, function (key, value) {
+                                            if (value[valueField] == newValue) {
+                                                userName = value[labelField];
+                                            }
+                                        });
+                                        setTimeout(function () {
+                                            $eEditable.html(userName).show();
+                                            $.fn.editable.defaults.success = function (response, newValue) {
+                                            }
+                                        }, 300);
+                                    }
+                                };
+
+                                setTimeout(function () {
+                                    $(context).find('#' + key).once('selectize-editable-init').each(function () {
+                                        var $item = $(this);
+
+                                        $item.selectize({
+                                            // General options.
+                                            items: item.options.items ? item.options.items : [],
+                                            delimiter: item.options.delimiter ? item.options.delimiter : ',',
+                                            diacritics: item.options.diacritics ? item.options.diacritics : false,
+                                            create: item.options.create ? item.options.create : false,
+                                            createOnBlur: item.options.createOnBlur ? item.options.createOnBlur : false,
+                                            highlight: item.options.highlight ? item.options.highlight : false,
+                                            persist: item.options.persist ? item.options.persist : false,
+                                            openOnFocus: item.options.openOnFocus ? item.options.openOnFocus : false,
+                                            maxOptions: item.options.maxOptions ? item.options.maxOptions : null,
+                                            maxItems: item.options.maxItems ? item.options.maxItems : null,
+                                            hideSelected: item.options.hideSelected ? item.options.hideSelected : false,
+                                            closeAfterSelect: item.options.closeAfterSelect ? item.options.closeAfterSelect : false,
+                                            allowEmptyOption: item.options.allowEmptyOption ? item.options.allowEmptyOption : false,
+                                            scrollDuration: item.options.scrollDuration ? item.options.scrollDuration : 60,
+                                            loadThrottle: item.options.loadThrottle ? item.options.loadThrottle : 300,
+                                            loadingClass: item.options.loadingClass ? item.options.loadingClass : 'loading',
+                                            preload: item.options.preload ? item.options.preload : false,
+                                            dropdownParent: item.options.dropdownParent ? item.options.dropdownParent : null,
+                                            addPrecedence: item.options.addPrecedence ? item.options.addPrecedence : false,
+                                            selectOnTab: item.options.selectOnTab ? item.options.selectOnTab : false,
+                                            mode: item.options.mode ? item.options.mode : 'multi',
+                                            plugins: item.options.plugins ? item.options.plugins : [],
+
+                                            // Data / Searching.
+                                            options: item.options.options ? item.options.options : [],
+                                            valueField: item.options.valueField ? item.options.valueField : 'value',
+                                            labelField: item.options.labelField ? item.options.labelField : 'label',
+                                            searchField: item.options.searchField ? item.options.searchField : 'label',
+
+                                            // Callbacks.
+                                            load: function (query, callback) {
+                                                var loadPath = item.options.loadPath ? item.options.loadPath : '';
+                                                if (loadPath == '') return callback([]);
+                                                if (!query.length) return callback([]);
+                                                $.ajax({
+                                                    url: loadPath,
+                                                    type: 'POST',
+                                                    dataType: 'json',
+                                                    data: {
+                                                        q: query,
+                                                        l: item.options.maxOptions ? item.options.maxOptions : 10
+                                                    },
+                                                    error: function () {
+                                                        callback([]);
+                                                    },
+                                                    success: function (data) {
+                                                        // Update items in options array of this field.
+                                                        item.options.options = data;
+                                                        callback(data);
+                                                    }
+                                                });
+                                            }
+                                        });
+                                    });
+                                }, 300);
+                            });
+                        });
+                    }
+                    else {
+                        $(context).find('#' + key).once('selectize-init').each(function () {
+                            var $item = $(this);
+
+                            $item.selectize({
+                                // General options.
+                                items: item.options.items ? item.options.items : [],
+                                delimiter: item.options.delimiter ? item.options.delimiter : ',',
+                                diacritics: item.options.diacritics ? item.options.diacritics : false,
+                                create: item.options.create ? item.options.create : false,
+                                createOnBlur: item.options.createOnBlur ? item.options.createOnBlur : false,
+                                highlight: item.options.highlight ? item.options.highlight : false,
+                                persist: item.options.persist ? item.options.persist : false,
+                                openOnFocus: item.options.openOnFocus ? item.options.openOnFocus : false,
+                                maxOptions: item.options.maxOptions ? item.options.maxOptions : null,
+                                maxItems: item.options.maxItems ? item.options.maxItems : null,
+                                hideSelected: item.options.hideSelected ? item.options.hideSelected : false,
+                                closeAfterSelect: item.options.closeAfterSelect ? item.options.closeAfterSelect : false,
+                                allowEmptyOption: item.options.allowEmptyOption ? item.options.allowEmptyOption : false,
+                                scrollDuration: item.options.scrollDuration ? item.options.scrollDuration : 60,
+                                loadThrottle: item.options.loadThrottle ? item.options.loadThrottle : 300,
+                                loadingClass: item.options.loadingClass ? item.options.loadingClass : 'loading',
+                                preload: item.options.preload ? item.options.preload : false,
+                                dropdownParent: item.options.dropdownParent ? item.options.dropdownParent : null,
+                                addPrecedence: item.options.addPrecedence ? item.options.addPrecedence : false,
+                                selectOnTab: item.options.selectOnTab ? item.options.selectOnTab : false,
+                                mode: item.options.mode ? item.options.mode : 'multi',
+                                plugins: item.options.plugins ? item.options.plugins : [],
+
+                                // Data / Searching.
+                                options: item.options.options ? item.options.options : [],
+                                valueField: item.options.valueField ? item.options.valueField : 'value',
+                                labelField: item.options.labelField ? item.options.labelField : 'label',
+                                searchField: item.options.searchField ? item.options.searchField : 'label',
+
+                                // Callbacks.
+                                load: function (query, callback) {
+                                    var loadPath = item.options.loadPath ? item.options.loadPath : '';
+                                    if (loadPath == '') return callback([]);
+                                    if (!query.length) return callback([]);
+                                    $.ajax({
+                                        url: loadPath,
+                                        type: 'POST',
+                                        dataType: 'json',
+                                        data: {
+                                            q: query,
+                                            l: item.options.maxOptions ? item.options.maxOptions : 10
+                                        },
+                                        error: function () {
+                                            callback([]);
+                                        },
+                                        success: function (data) {
+                                            // Update items in options array of this field.
+                                            item.options.options = data;
+                                            callback(data);
+                                        }
+                                    });
+                                }
+                            });
+                        });
+                    }
+
+                });
+            }
+        }
+    };
+
+})(jQuery);