diff --git a/ajax-wrapper/AjaxWrapper.php b/ajax-wrapper/AjaxWrapper.php new file mode 100644 index 0000000..79c730f --- /dev/null +++ b/ajax-wrapper/AjaxWrapper.php @@ -0,0 +1,457 @@ +action = $action; + } + + /** + * @param callable $callback + * @return $this + */ + public function handler($callback) { + $this->callback = $callback; + return $this; + } + + public function requiredParam($name, $type = null, $validateCallback = null) { + return $this->addParameter($name, $type, true, null, $validateCallback); + } + + public function optionalParam($name, $defaultValue = null, $type = null, $validateCallback = null) { + return $this->addParameter($name, $type, false, $defaultValue, $validateCallback); + } + + private function addParameter($name, $type, $required, $defaultValue, $validateCallback) { + if (isset($type) && !isset(Ajaw_v1_Action::$defaultValidators[$type])) { + throw new LogicException(sprintf( + 'Unknown parameter type "%s". Supported types are: %s.', + $type, + implode(', ', array_keys(Ajaw_v1_Action::$defaultValidators[$type])) + )); + } + + $this->params[$name] = array( + 'required' => $required, + 'defaultValue' => $defaultValue, + 'type' => $type, + 'validateCallback' => $validateCallback, + ); + return $this; + } + + public function method($httpMethod) { + $this->httpMethod = strtoupper($httpMethod); + return $this; + } + + public function requiredCap($capability) { + $this->capability = $capability; + return $this; + } + + public function permissionCallback($callback) { + $this->permissionCheckCallback = $callback; + return $this; + } + + public function allowUnprivilegedUsers() { + $this->mustBeLoggedIn = false; + return $this; + } + + public function withoutNonce() { + $this->checkNonce = false; + return $this; + } + + public function build() { + $instance = new Ajaw_v1_Action($this->action, $this->callback, $this->params); + + $instance->mustBeLoggedIn = $this->mustBeLoggedIn; + $instance->requiredCap = $this->capability; + $instance->nonceCheckEnabled = $this->checkNonce; + $instance->method = $this->httpMethod; + $instance->permissionCallback = $this->permissionCheckCallback; + + return $instance; + } + + public function register() { + $instance = $this->build(); + $instance->register(); + return $instance; + } + } + +endif; + +if (!class_exists('Ajaw_v1_Action', false)): + + class Ajaw_v1_Action { + public $action; + public $callback; + public $params = array(); + public $method = null; + + public $requiredCap = null; + public $mustBeLoggedIn = false; + public $nonceCheckEnabled = true; + public $permissionCallback = null; + + private $isScriptRegistered = false; + + public static $defaultValidators = array( + 'int' => array(__CLASS__, 'validateInt'), + 'float' => array(__CLASS__, 'validateFloat'), + 'boolean' => array(__CLASS__, 'validateBoolean'), + 'string' => array(__CLASS__, 'validateString'), + ); + + public function __construct($action, $callback, $params) { + $this->action = $action; + $this->callback = $callback; + $this->params = $params; + + if (empty($this->action)) { + throw new LogicException(sprintf( + 'AJAX action name is missing. You must either pass it to the %1$s constructor ' + . 'or give the %1$s::$action property a valid default value.', + get_class($this) + )); + } + } + + /** + * Set up hooks for AJAX and helper scripts. + */ + public function register() { + //Register the AJAX handler(s). + $hookNames = array('wp_ajax_' . $this->action); + if (!$this->mustBeLoggedIn) { + $hookNames[] = 'wp_ajax_nopriv_' . $this->action; + } + + foreach($hookNames as $hook) { + if (has_action($hook)) { + throw new RuntimeException(sprintf('The action name "%s" is already in use.', $this->action)); + } + add_action($hook, array($this, 'processAjaxRequest')); + } + + //Register the utility JS library after WP is fully loaded. + if (did_action('wp_loaded')) { + $this->registerScript(); + } else { + add_action('wp_loaded', array($this, 'registerScript'), 2); + } + } + + /** + * @access protected + */ + public function processAjaxRequest() { + $result = $this->handleAction(); + + if (is_wp_error($result)) { + $statusCode = $result->get_error_data(); + if (isset($statusCode) && is_int($statusCode) ) { + status_header($statusCode); + } + + $errorResponse = array( + 'error' => array( + 'message' => $result->get_error_message(), + 'code' => $result->get_error_code() + ) + ); + + $result = $errorResponse; + } + + if (isset($result)) { + $this->outputJSON($result); + } + exit; + } + + protected function handleAction() { + $method = $this->getRequestMethod(); + if (isset($this->method) && ($method !== $this->method)) { + return new WP_Error( + 'http_method_not_allowed', + 'The HTTP method is not supported by the request handler.', + 405 + ); + } + + $isAuthorized = $this->checkAuthorization(); + if ($isAuthorized !== true) { + return $isAuthorized; + } + + $params = $this->parseParameters(); + if ($params instanceof WP_Error) { + return $params; + } + + //Call the user-specified action handler. + if (is_callable($this->callback)) { + return call_user_func($this->callback, $params); + } else { + return new WP_Error( + 'missing_ajax_handler', + sprintf( + 'There is no request handler assigned to the "%1$s" action. ' + . 'Either pass a valid callback to $builder->request() or override the %2$s::%3$s method.', + $this->action, + __CLASS__, + __METHOD__ + ), + 500 + ); + } + } + + /** + * Check if the current user is authorized to perform this action. + * + * @return bool|WP_Error + */ + protected function checkAuthorization() { + if ($this->mustBeLoggedIn && !is_user_logged_in()) { + return new WP_Error('login_required', 'You must be logged in to perform this action.', 403); + } + + if (isset($this->requiredCap) && !current_user_can($this->requiredCap)) { + return new WP_Error('capability_missing', 'You don\'t have permission to perform this action.', 403); + } + + if ($this->nonceCheckEnabled && !check_ajax_referer($this->action, false, false)) { + return new WP_Error('nonce_check_failed', 'Invalid or missing nonce.', 403); + } + + if (isset($this->permissionCallback)) { + $result = call_user_func($this->permissionCallback); + if ($result === false) { + return new WP_Error( + 'permission_callback_failed', + 'You don\'t have permission to perform this action.', + 403 + ); + } else if (is_wp_error($result)) { + return $result; + } + } + + return true; + } + + protected function getRequestMethod() { + return strtoupper(filter_input( + INPUT_SERVER, + 'REQUEST_METHOD', + FILTER_VALIDATE_REGEXP, + array('options' => array('regexp' => '/^[a-z]{3,20}$/i')) + )); + } + + protected function parseParameters() { + $method = $this->getRequestMethod(); + + // phpcs:disable WordPress.Security.NonceVerification -- checkAuthorization() is where nonce verification happens. + //Retrieve request parameters. + if ($method === 'GET') { + $rawParams = $_GET; + } else if ($method === 'POST') { + $rawParams = $_POST; + } else { + $rawParams = $_REQUEST; + } + // phpcs:enable + + //Remove magic quotes. WordPress applies them in wp-settings.php. + //There's no hook for wp_magic_quotes, so we use one that's closest in execution order. + if (did_action('sanitize_comment_cookies') && function_exists('wp_magic_quotes')) { + $rawParams = wp_unslash($rawParams); + } + + //Validate all parameters. + $inputParams = $rawParams; + foreach($this->params as $name => $settings) { + //Verify that all the required parameters are present. + //Empty strings are treated as missing parameters. + if (isset($inputParams[$name]) && ($inputParams[$name] !== '')) { + $value = $this->validateParameter($settings, $inputParams[$name], $name); + if (is_wp_error($value)) { + return $value; + } else { + $inputParams[$name] = $value; + } + } else if (empty($settings['required'])) { + //It's an optional parameter. Use the default value. + $inputParams[$name] = $settings['defaultValue']; + } else { + return new WP_Error( + 'missing_required_parameter', + sprintf('Required parameter is missing or empty: "%s".', $name), + 400 + ); + } + } + + return $inputParams; + } + + protected function validateParameter($settings, $value, $name) { + if (isset($settings['type'])) { + $value = call_user_func(self::$defaultValidators[$settings['type']], $value, $name); + if (is_wp_error($value)) { + return $value; + } + } + if (isset($settings['validateCallback'])) { + $success = call_user_func($settings['validateCallback'], $value); + if (is_wp_error($success)) { + return $success; + } else if ($success === false) { + return new WP_Error( + 'invalid_parameter_value', + sprintf('The value of the parameter "%s" is invalid.', $name), + 400 + ); + } + } + return $value; + } + + private static function validateInt($value, $name) { + $result = filter_var($value, FILTER_VALIDATE_INT); + if ($result === false) { + return new WP_Error( + 'invalid_parameter_value', + sprintf('The value of the parameter "%s" is invalid. It must be an integer.', $name), + 400 + ); + } + return $result; + } + + private static function validateFloat($value, $name) { + $result = filter_var($value, FILTER_VALIDATE_FLOAT); + if ($result === false) { + return new WP_Error( + 'invalid_parameter_value', + sprintf('The value of the parameter "%s" is invalid. It must be a float.', $name), + 400 + ); + } + return $result; + } + + private static function validateBoolean($value, $name) { + $result = filter_var($value, FILTER_VALIDATE_BOOLEAN, array('flags' => FILTER_NULL_ON_FAILURE)); + if ($result === null) { + return new WP_Error( + 'invalid_parameter_value', + sprintf('The value of the parameter "%s" is invalid. It must be a boolean.', $name), + 400 + ); + } + return $result; + } + + private static function validateString($value, $name) { + if (!is_string($value)) { + return new WP_Error( + 'invalid_parameter_value', + sprintf('The value of the parameter "%s" is invalid. It must be a string.', $name), + 400 + ); + } + return $value; + } + + protected function outputJSON($response) { + @header('Content-Type: application/json; charset=' . get_option('blog_charset')); + echo wp_json_encode($response); + } + + public function registerScript() { + if ($this->isScriptRegistered) { + return; + } + $this->isScriptRegistered = true; + + //There could be multiple instances of this class, but we only need to register the script once. + $handle = $this->getScriptHandle(); + if (!wp_script_is($handle, 'registered')) { + wp_register_script( + $handle, + plugins_url('ajax-action-wrapper.js', __FILE__), + array('jquery'), + '20161105' + ); + } + + //Pass the action to the script. + if (function_exists('wp_add_inline_script')) { + wp_add_inline_script($handle, $this->generateActionJs(), 'after'); //WP 4.5+ + } else { + add_filter('script_loader_tag', array($this, 'addRegistrationScript'), 10, 2); //WP 4.1+ + } + } + + /** + * Backwards compatibility for older versions of WP that don't have wp_add_inline_script(). + * @internal + * + * @param string $tag + * @param string $handle + * @return string + */ + public function addRegistrationScript($tag, $handle) { + if ($handle === $this->getScriptHandle()) { + $tag .= ''; + } + return $tag; + } + + protected function generateActionJs() { + $properties = array( + 'ajaxUrl' => admin_url('admin-ajax.php'), + 'method' => $this->method, + 'nonce' => $this->nonceCheckEnabled ? wp_create_nonce($this->action) : null, + ); + + return sprintf( + 'AjawV1.actionRegistry.add("%s", %s);' . "\n", + esc_js($this->action), + wp_json_encode($properties) + ); + } + + public function getScriptHandle() { + return 'ajaw-v1-ajax-action-wrapper'; + } + } + +endif; + +if (!function_exists('ajaw_v1_CreateAction')) { + function ajaw_v1_CreateAction($action) { + return new Ajaw_v1_ActionBuilder($action); + } +} diff --git a/ajax-wrapper/README.md b/ajax-wrapper/README.md new file mode 100644 index 0000000..16d6f14 --- /dev/null +++ b/ajax-wrapper/README.md @@ -0,0 +1,74 @@ +# AJAX Action Wrapper + +This helper library makes it easier to handle AJAX requests in WordPress plugins. Mainly for personal use. + +### Example +Define action: +```php +$exampleAction = ajaw_v1_CreateAction('ws_do_something') + ->handler(array($this, 'myAjaxCallback')) + ->requiredCap('manage_options') + ->method('post') + ->requiredParam('foo') + ->optionalParam('bar', 'default value') + ->register(); +``` + +Call from JavaScript: +```javascript +AjawV1.getAction('ws_do_something').post( + { + 'foo': '...' + }, + function(response) { + console.log(response); + } +); +``` + +### Features +- Automate common, boring stuff. + - [x] Automatically pass the `admin-ajax.php` URL and nonce to JS. + - [x] Define required parameters. + ```php + $builder->requiredParam('foo', 'int') + ``` + - [x] Define optional parameters with default values. + ```php + $builder->optionalParam('meaningOfLife', 42, 'int') + ``` + - [x] Automatically remove "magic quotes" that WordPress adds to `$_GET`, `$_POST` and `$_REQUEST`. + - [x] Encode return values as JSON. +- Security should be the default. + - [x] Generate and verify nonces. Nonce verification is on by default, but can be disabled. + ```php + $builder->withoutNonce() + ``` + - [x] Check capabilities. + ```php + $builder->requiredCap('manage_options'); + ``` + - [x] Verify that all required parameters are set. + - [x] Validate parameter values. + ```php + $builder->optionalParam('things', 1, 'int', function($value) { + if ($value > 10) { + return new WP_Error( + 'excessive_things', + 'Too many things!', + 400 //HTTP status code. + ); + } + }) + ``` + - [x] Set the required HTTP method. + ```php + $builder->method('post') + ``` +- Resilience. + - [ ] Lenient response parsing to work around bugs in other plugins. For example, deal with extraneous whitespace and PHP notices in AJAX responses. + - [x] Multiple versions of the library can coexist on the same site. + +### Why not use the REST API instead? + +Backwards compatibility. In theory, this library should be compatible with WP 4.1+. \ No newline at end of file diff --git a/ajax-wrapper/ajax-action-wrapper.d.ts b/ajax-wrapper/ajax-action-wrapper.d.ts new file mode 100644 index 0000000..c5d7b42 --- /dev/null +++ b/ajax-wrapper/ajax-action-wrapper.d.ts @@ -0,0 +1,15 @@ +// Basic type definitions for the Ajaw AJAX wrapper library 1.0 + +declare namespace AjawV1 { + interface RequestParams { [name: string]: any } + interface SuccessCallback { (data, textStatus: string, jqXHR): void } + interface ErrorCallback { (data, textStatus: string, jqXHR, errorThrown): void } + + class AjawAjaxAction { + get(params?: RequestParams, success?: SuccessCallback, error?: ErrorCallback): void; + post(params?: RequestParams, success?: SuccessCallback, error?: ErrorCallback): void; + request(params?: RequestParams, success?: SuccessCallback, error?: ErrorCallback, method?: string): void; + } + + function getAction(action: string): AjawAjaxAction; +} \ No newline at end of file diff --git a/ajax-wrapper/ajax-action-wrapper.js b/ajax-wrapper/ajax-action-wrapper.js new file mode 100644 index 0000000..23a5d1e --- /dev/null +++ b/ajax-wrapper/ajax-action-wrapper.js @@ -0,0 +1,139 @@ +var AjawV1 = window.AjawV1 || {}; + +AjawV1.AjaxAction = (function () { + "use strict"; + + function AjawAjaxAction(action, properties) { + this.action = action; + this.ajaxUrl = properties['ajaxUrl']; + this.nonce = properties['nonce']; + this.requiredMethod = (typeof properties['method'] !== 'undefined') ? properties['method'] : null; + } + + /** + * Send a POST request. + * + * @param {Object} params + * @param {Function} success + * @param {Function} [error] + */ + AjawAjaxAction.prototype.post = function (params, success, error) { + return this.request(params, success, error, 'POST'); + }; + + /** + * Send a GET request. + * + * @param {Object} params + * @param {Function} success + * @param {Function} [error] + */ + AjawAjaxAction.prototype.get = function(params, success, error) { + return this.request(params, success, error, 'GET'); + }; + + /** + * Send an AJAX request using the specified HTTP method. + * + * @param {Object} params + * @param {Function} success + * @param {Function} [error] + * @param {String} [method] + */ + AjawAjaxAction.prototype.request = function(params, success, error, method) { + if (typeof params === 'function') { + //It looks like "params" was omitted and the first argument is actually the success callback. + //Shift all arguments left one step. The reverse order is due to argument binding shenanigans. + method = arguments[2]; + error = arguments[1]; + success = arguments[0]; + params = {}; + } + + if (typeof params === 'undefined') { + params = {}; + } else if (typeof params !== 'object') { + //While jQuery accepts request data in object and string form, this library only supports objects. + throw 'Data that\'s to be sent to the server must be an object, not ' + (typeof params); + } + + if (typeof method === 'undefined') { + method = this.requiredMethod || 'POST'; + } + if (this.requiredMethod && (method !== this.requiredMethod)) { + throw 'Wrong HTTP method. This action requires ' + this.requiredMethod; + } + + //noinspection JSUnusedGlobalSymbols + return jQuery.ajax( + this.ajaxUrl, + { + method: method, + data: this.prepareRequestParams(params), + success: function(data, textStatus, jqXHR) { + if (success) { + success(data, textStatus, jqXHR); + } + }, + error: function(jqXHR, textStatus, errorThrown) { + var data = jqXHR.responseText; + if (typeof jqXHR['responseJSON'] !== 'undefined') { + data = jqXHR['responseJSON']; + } else if (typeof jqXHR['responseXML'] !== 'undefined') { + data = jqXHR['responseXML']; + } + + if (error) { + error(data, textStatus, jqXHR, errorThrown); + } + } + } + ); + }; + + AjawAjaxAction.prototype.prepareRequestParams = function(params) { + if (params === null) { + params = {}; + } + + params['action'] = this.action; + if (this.nonce !== null) { + params['_ajax_nonce'] = this.nonce; + } + return params; + }; + + return AjawAjaxAction; +}()); + +AjawV1.actionRegistry = (function() { + var actions = {}; + + return { + /** + * + * @param {String} actionName + * @return {AjawAjaxAction} + */ + get: function(actionName) { + if (actions.hasOwnProperty(actionName)) { + return actions[actionName]; + } + return null; + }, + + add: function(actionName, properties) { + actions[actionName] = new AjawV1.AjaxAction(actionName, properties); + } + } +})(); + +/** + * Get a registered action wrapper. + * + * @param {string} action + * @return {AjawAjaxAction|null} + */ +AjawV1.getAction = function(action) { + return this.actionRegistry.get(action); +}; \ No newline at end of file diff --git a/css/_boxes.scss b/css/_boxes.scss new file mode 100644 index 0000000..c81fb75 --- /dev/null +++ b/css/_boxes.scss @@ -0,0 +1,88 @@ +$amePostboxBorderColor: #ccd0d4; //Was #e5e5e5 before WP 5.3. +$amePostboxShadow: 0 1px 1px rgba(0, 0, 0, 0.04); + +@mixin ame-emulated-postbox($toggleWidth: 36px, $horizontalPadding: 12px) { + $borderColor: $amePostboxBorderColor; + $headerBackground: #fff; + + position: relative; + box-shadow: $amePostboxShadow; + background: $headerBackground; + + margin-bottom: 20px; + + .ws-ame-postbox-header { + position: relative; + font-size: 14px; + margin: 0; + line-height: 1.4; + + border: 1px solid $borderColor; + + h3 { + padding: 10px $horizontalPadding; + margin: 0; + font-size: 1em; + line-height: 1; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } + + .ws-ame-postbox-toggle { + color: #72777c; + background: $headerBackground; + + display: block; + font: normal 20px/1 dashicons; + text-align: center; + cursor: pointer; + border: none; + + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: $toggleWidth; + height: 100%; + padding: 0; + + &:hover { + color: #23282d; + } + + &:active, &:focus { + outline: none; + padding: 0; + } + + &:before { + content: '\f142'; + display: inline-block; + vertical-align: middle; + } + + &:after { + display: inline-block; + content: ""; + vertical-align: middle; + height: 100%; + } + } + + .ws-ame-postbox-content { + border: 1px solid $borderColor; + border-top: none; + + padding: $horizontalPadding; + } + + &.ws-ame-closed-postbox .ws-ame-postbox-content { + display: none; + } + + &.ws-ame-closed-postbox .ws-ame-postbox-toggle:before { + content: '\f140'; //downward triangle + } +} \ No newline at end of file diff --git a/css/_dashicons.scss b/css/_dashicons.scss new file mode 100644 index 0000000..e49d7ec --- /dev/null +++ b/css/_dashicons.scss @@ -0,0 +1,224 @@ +/* +This file was automatically generated from /wp-includes/css/dashicons.css. +Last update: 2017-06-07T16:55:55+00:00 +*/ +.dashicons-menu:before { content: "\f333" !important; } +.dashicons-admin-site:before { content: "\f319" !important; } +.dashicons-admin-media:before { content: "\f104" !important; } +.dashicons-admin-page:before { content: "\f105" !important; } +.dashicons-admin-comments:before { content: "\f101" !important; } +.dashicons-admin-appearance:before { content: "\f100" !important; } +.dashicons-admin-plugins:before { content: "\f106" !important; } +.dashicons-admin-users:before { content: "\f110" !important; } +.dashicons-admin-tools:before { content: "\f107" !important; } +.dashicons-admin-settings:before { content: "\f108" !important; } +.dashicons-admin-network:before { content: "\f112" !important; } +.dashicons-admin-generic:before { content: "\f111" !important; } +.dashicons-admin-home:before { content: "\f102" !important; } +.dashicons-admin-collapse:before { content: "\f148" !important; } +.dashicons-filter:before { content: "\f536" !important; } +.dashicons-admin-customizer:before { content: "\f540" !important; } +.dashicons-admin-multisite:before { content: "\f541" !important; } +.dashicons-admin-links:before, .dashicons-format-links:before { content: "\f103" !important; } +.dashicons-admin-post:before, .dashicons-format-standard:before { content: "\f109" !important; } +.dashicons-format-image:before { content: "\f128" !important; } +.dashicons-format-gallery:before { content: "\f161" !important; } +.dashicons-format-audio:before { content: "\f127" !important; } +.dashicons-format-video:before { content: "\f126" !important; } +.dashicons-format-chat:before { content: "\f125" !important; } +.dashicons-format-status:before { content: "\f130" !important; } +.dashicons-format-aside:before { content: "\f123" !important; } +.dashicons-format-quote:before { content: "\f122" !important; } +.dashicons-welcome-write-blog:before, .dashicons-welcome-edit-page:before { content: "\f119" !important; } +.dashicons-welcome-add-page:before { content: "\f133" !important; } +.dashicons-welcome-view-site:before { content: "\f115" !important; } +.dashicons-welcome-widgets-menus:before { content: "\f116" !important; } +.dashicons-welcome-comments:before { content: "\f117" !important; } +.dashicons-welcome-learn-more:before { content: "\f118" !important; } +.dashicons-image-crop:before { content: "\f165" !important; } +.dashicons-image-rotate:before { content: "\f531" !important; } +.dashicons-image-rotate-left:before { content: "\f166" !important; } +.dashicons-image-rotate-right:before { content: "\f167" !important; } +.dashicons-image-flip-vertical:before { content: "\f168" !important; } +.dashicons-image-flip-horizontal:before { content: "\f169" !important; } +.dashicons-image-filter:before { content: "\f533" !important; } +.dashicons-undo:before { content: "\f171" !important; } +.dashicons-redo:before { content: "\f172" !important; } +.dashicons-editor-ul:before { content: "\f203" !important; } +.dashicons-editor-ol:before { content: "\f204" !important; } +.dashicons-editor-quote:before { content: "\f205" !important; } +.dashicons-editor-alignleft:before { content: "\f206" !important; } +.dashicons-editor-aligncenter:before { content: "\f207" !important; } +.dashicons-editor-alignright:before { content: "\f208" !important; } +.dashicons-editor-insertmore:before { content: "\f209" !important; } +.dashicons-editor-spellcheck:before { content: "\f210" !important; } +.dashicons-editor-distractionfree:before, .dashicons-editor-expand:before { content: "\f211" !important; } +.dashicons-editor-contract:before { content: "\f506" !important; } +.dashicons-editor-kitchensink:before { content: "\f212" !important; } +.dashicons-editor-underline:before { content: "\f213" !important; } +.dashicons-editor-justify:before { content: "\f214" !important; } +.dashicons-editor-textcolor:before { content: "\f215" !important; } +.dashicons-editor-paste-word:before { content: "\f216" !important; } +.dashicons-editor-paste-text:before { content: "\f217" !important; } +.dashicons-editor-removeformatting:before { content: "\f218" !important; } +.dashicons-editor-video:before { content: "\f219" !important; } +.dashicons-editor-customchar:before { content: "\f220" !important; } +.dashicons-editor-outdent:before { content: "\f221" !important; } +.dashicons-editor-indent:before { content: "\f222" !important; } +.dashicons-editor-help:before { content: "\f223" !important; } +.dashicons-editor-strikethrough:before { content: "\f224" !important; } +.dashicons-editor-unlink:before { content: "\f225" !important; } +.dashicons-editor-rtl:before { content: "\f320" !important; } +.dashicons-editor-break:before { content: "\f474" !important; } +.dashicons-editor-code:before { content: "\f475" !important; } +.dashicons-editor-paragraph:before { content: "\f476" !important; } +.dashicons-editor-table:before { content: "\f535" !important; } +.dashicons-align-left:before { content: "\f135" !important; } +.dashicons-align-right:before { content: "\f136" !important; } +.dashicons-align-center:before { content: "\f134" !important; } +.dashicons-align-none:before { content: "\f138" !important; } +.dashicons-lock:before { content: "\f160" !important; } +.dashicons-unlock:before { content: "\f528" !important; } +.dashicons-calendar:before { content: "\f145" !important; } +.dashicons-calendar-alt:before { content: "\f508" !important; } +.dashicons-visibility:before { content: "\f177" !important; } +.dashicons-hidden:before { content: "\f530" !important; } +.dashicons-post-status:before { content: "\f173" !important; } +.dashicons-edit:before { content: "\f464" !important; } +.dashicons-post-trash:before, .dashicons-trash:before { content: "\f182" !important; } +.dashicons-sticky:before { content: "\f537" !important; } +.dashicons-external:before { content: "\f504" !important; } +.dashicons-leftright:before { content: "\f229" !important; } +.dashicons-sort:before { content: "\f156" !important; } +.dashicons-randomize:before { content: "\f503" !important; } +.dashicons-list-view:before { content: "\f163" !important; } +.dashicons-exerpt-view:before, .dashicons-excerpt-view:before { content: "\f164" !important; } +.dashicons-grid-view:before { content: "\f509" !important; } +.dashicons-move:before { content: "\f545" !important; } +.dashicons-hammer:before { content: "\f308" !important; } +.dashicons-art:before { content: "\f309" !important; } +.dashicons-migrate:before { content: "\f310" !important; } +.dashicons-performance:before { content: "\f311" !important; } +.dashicons-universal-access:before { content: "\f483" !important; } +.dashicons-universal-access-alt:before { content: "\f507" !important; } +.dashicons-tickets:before { content: "\f486" !important; } +.dashicons-nametag:before { content: "\f484" !important; } +.dashicons-clipboard:before { content: "\f481" !important; } +.dashicons-heart:before { content: "\f487" !important; } +.dashicons-megaphone:before { content: "\f488" !important; } +.dashicons-schedule:before { content: "\f489" !important; } +.dashicons-wordpress:before { content: "\f120" !important; } +.dashicons-wordpress-alt:before { content: "\f324" !important; } +.dashicons-pressthis:before { content: "\f157" !important; } +.dashicons-update:before { content: "\f463" !important; } +.dashicons-screenoptions:before { content: "\f180" !important; } +.dashicons-cart:before { content: "\f174" !important; } +.dashicons-feedback:before { content: "\f175" !important; } +.dashicons-cloud:before { content: "\f176" !important; } +.dashicons-translation:before { content: "\f326" !important; } +.dashicons-tag:before { content: "\f323" !important; } +.dashicons-category:before { content: "\f318" !important; } +.dashicons-archive:before { content: "\f480" !important; } +.dashicons-tagcloud:before { content: "\f479" !important; } +.dashicons-text:before { content: "\f478" !important; } +.dashicons-media-archive:before { content: "\f501" !important; } +.dashicons-media-audio:before { content: "\f500" !important; } +.dashicons-media-code:before { content: "\f499" !important; } +.dashicons-media-default:before { content: "\f498" !important; } +.dashicons-media-document:before { content: "\f497" !important; } +.dashicons-media-interactive:before { content: "\f496" !important; } +.dashicons-media-spreadsheet:before { content: "\f495" !important; } +.dashicons-media-text:before { content: "\f491" !important; } +.dashicons-media-video:before { content: "\f490" !important; } +.dashicons-playlist-audio:before { content: "\f492" !important; } +.dashicons-playlist-video:before { content: "\f493" !important; } +.dashicons-controls-play:before { content: "\f522" !important; } +.dashicons-controls-pause:before { content: "\f523" !important; } +.dashicons-controls-forward:before { content: "\f519" !important; } +.dashicons-controls-skipforward:before { content: "\f517" !important; } +.dashicons-controls-back:before { content: "\f518" !important; } +.dashicons-controls-skipback:before { content: "\f516" !important; } +.dashicons-controls-repeat:before { content: "\f515" !important; } +.dashicons-controls-volumeon:before { content: "\f521" !important; } +.dashicons-controls-volumeoff:before { content: "\f520" !important; } +.dashicons-yes:before { content: "\f147" !important; } +.dashicons-no:before { content: "\f158" !important; } +.dashicons-no-alt:before { content: "\f335" !important; } +.dashicons-plus:before { content: "\f132" !important; } +.dashicons-plus-alt:before { content: "\f502" !important; } +.dashicons-plus-alt2:before { content: "\f543" !important; } +.dashicons-minus:before { content: "\f460" !important; } +.dashicons-dismiss:before { content: "\f153" !important; } +.dashicons-marker:before { content: "\f159" !important; } +.dashicons-star-filled:before { content: "\f155" !important; } +.dashicons-star-half:before { content: "\f459" !important; } +.dashicons-star-empty:before { content: "\f154" !important; } +.dashicons-flag:before { content: "\f227" !important; } +.dashicons-info:before { content: "\f348" !important; } +.dashicons-warning:before { content: "\f534" !important; } +.dashicons-share:before { content: "\f237" !important; } +.dashicons-share1:before { content: "\f237" !important; } +.dashicons-share-alt:before { content: "\f240" !important; } +.dashicons-share-alt2:before { content: "\f242" !important; } +.dashicons-twitter:before { content: "\f301" !important; } +.dashicons-rss:before { content: "\f303" !important; } +.dashicons-email:before { content: "\f465" !important; } +.dashicons-email-alt:before { content: "\f466" !important; } +.dashicons-facebook:before { content: "\f304" !important; } +.dashicons-facebook-alt:before { content: "\f305" !important; } +.dashicons-networking:before { content: "\f325" !important; } +.dashicons-googleplus:before { content: "\f462" !important; } +.dashicons-location:before { content: "\f230" !important; } +.dashicons-location-alt:before { content: "\f231" !important; } +.dashicons-camera:before { content: "\f306" !important; } +.dashicons-images-alt:before { content: "\f232" !important; } +.dashicons-images-alt2:before { content: "\f233" !important; } +.dashicons-video-alt:before { content: "\f234" !important; } +.dashicons-video-alt2:before { content: "\f235" !important; } +.dashicons-video-alt3:before { content: "\f236" !important; } +.dashicons-vault:before { content: "\f178" !important; } +.dashicons-shield:before { content: "\f332" !important; } +.dashicons-shield-alt:before { content: "\f334" !important; } +.dashicons-sos:before { content: "\f468" !important; } +.dashicons-search:before { content: "\f179" !important; } +.dashicons-slides:before { content: "\f181" !important; } +.dashicons-analytics:before { content: "\f183" !important; } +.dashicons-chart-pie:before { content: "\f184" !important; } +.dashicons-chart-bar:before { content: "\f185" !important; } +.dashicons-chart-line:before { content: "\f238" !important; } +.dashicons-chart-area:before { content: "\f239" !important; } +.dashicons-groups:before { content: "\f307" !important; } +.dashicons-businessman:before { content: "\f338" !important; } +.dashicons-id:before { content: "\f336" !important; } +.dashicons-id-alt:before { content: "\f337" !important; } +.dashicons-products:before { content: "\f312" !important; } +.dashicons-awards:before { content: "\f313" !important; } +.dashicons-forms:before { content: "\f314" !important; } +.dashicons-testimonial:before { content: "\f473" !important; } +.dashicons-portfolio:before { content: "\f322" !important; } +.dashicons-book:before { content: "\f330" !important; } +.dashicons-book-alt:before { content: "\f331" !important; } +.dashicons-download:before { content: "\f316" !important; } +.dashicons-upload:before { content: "\f317" !important; } +.dashicons-backup:before { content: "\f321" !important; } +.dashicons-clock:before { content: "\f469" !important; } +.dashicons-lightbulb:before { content: "\f339" !important; } +.dashicons-microphone:before { content: "\f482" !important; } +.dashicons-desktop:before { content: "\f472" !important; } +.dashicons-laptop:before { content: "\f547" !important; } +.dashicons-tablet:before { content: "\f471" !important; } +.dashicons-smartphone:before { content: "\f470" !important; } +.dashicons-phone:before { content: "\f525" !important; } +.dashicons-smiley:before { content: "\f328" !important; } +.dashicons-index-card:before { content: "\f510" !important; } +.dashicons-carrot:before { content: "\f511" !important; } +.dashicons-building:before { content: "\f512" !important; } +.dashicons-store:before { content: "\f513" !important; } +.dashicons-album:before { content: "\f514" !important; } +.dashicons-palmtree:before { content: "\f527" !important; } +.dashicons-tickets-alt:before { content: "\f524" !important; } +.dashicons-money:before { content: "\f526" !important; } +.dashicons-thumbs-up:before { content: "\f529" !important; } +.dashicons-thumbs-down:before { content: "\f542" !important; } +.dashicons-layout:before { content: "\f538" !important; } +.dashicons-paperclip:before { content: "\f546" !important; } diff --git a/css/_form-validation.scss b/css/_form-validation.scss new file mode 100644 index 0000000..da71295 --- /dev/null +++ b/css/_form-validation.scss @@ -0,0 +1,15 @@ +$invalidColor: #d63638; //Matches the Theme Customizer. + +@mixin ame-invalid-input-styles { + select, input { + &:invalid { + border-color: $invalidColor; + + //Override the box shadow that WordPress adds on focus. + &:focus { + box-shadow: 0 0 0 1px $invalidColor; + } + } + } +} + diff --git a/css/_indeterminate-checkbox.scss b/css/_indeterminate-checkbox.scss new file mode 100644 index 0000000..098bb0d --- /dev/null +++ b/css/_indeterminate-checkbox.scss @@ -0,0 +1,38 @@ +@mixin ame-indeterminate-checkbox($markColor: #1e8cbe) { + &:indeterminate:before { + content: '\25a0'; //Unicode black square. Another option would be BLACK LARGE SQUARE (U+2B1B). + color: $markColor; + + //Large square. + //margin: -6px 0 0 -1px; + //font: 400 18px/1 dashicons; + + //Smaller square. + margin: -3px 0 0 -1px; + font: 400 14px/1 dashicons; + + //Even smaller square. + //margin: -2px 0 0 -1px; + //font: 400 13px/1 dashicons; + + float: left; + display: inline-block; + vertical-align: middle; + width: 16px; + -webkit-font-smoothing: antialiased; + } + + @media screen and (max-width: 782px) { + &:indeterminate:before { + $boxSize: 1.5625rem; + height: $boxSize; + width: $boxSize; + line-height: $boxSize; + margin: -1px; + + font-size: 18px; + font-family: unset; + font-weight: normal; + } + } +} \ No newline at end of file diff --git a/css/_input-group.scss b/css/_input-group.scss new file mode 100644 index 0000000..273a9e9 --- /dev/null +++ b/css/_input-group.scss @@ -0,0 +1,16 @@ +.ame-input-group { + display: flex; + flex-wrap: wrap; + + > :not(:first-child) { + margin-left: -1px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + > :not(:last-child) { + margin-right: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } +} \ No newline at end of file diff --git a/css/_main-tabs.scss b/css/_main-tabs.scss new file mode 100644 index 0000000..fdec1ad --- /dev/null +++ b/css/_main-tabs.scss @@ -0,0 +1,37 @@ +/*************************************** + Tabs on the settings page + ***************************************/ + +.wrap.ws-ame-too-many-tabs .ws-ame-nav-tab-list { + &.nav-tab-wrapper { + border-bottom-color: transparent; + } + + .nav-tab { + border-bottom: 1px solid #c3c4c7; + margin-bottom: 10px; + margin-top: 0; + } +} + +/* Spacing between the page heading and the tab list. + +Normally, this is handled by .nav-tab styles, but WordPress changes the margins at smaller screen sizes +and the tabs end up without a left margin. Let's put that margin on the heading instead and remove it +from the first tab. */ + +#ws_ame_editor_heading { + margin-right: 0.305em; +} + +.ws-ame-nav-tab-list { + a.nav-tab:first-of-type { + margin-left: 0; + } +} + +/* When in "too many tabs" mode, there's too much space between the bottom of the tab list and the rest +of the page. I haven't found a good way to change the margins of just the last row, so here's a partial fix. */ +.ws-ame-too-many-tabs #ws_actor_selector { + margin-top: 0; +} \ No newline at end of file diff --git a/css/_test-access-screen.scss b/css/_test-access-screen.scss new file mode 100644 index 0000000..6f1cc02 --- /dev/null +++ b/css/_test-access-screen.scss @@ -0,0 +1,131 @@ +/********************************************* + "Test access" dialog +**********************************************/ + +#ws_ame_test_access_screen { + display: none; + background: #fcfcfc; +} + +#ws_ame_test_inputs { + //border-bottom: 1px solid #ddd; + padding-bottom: 16px; +} + +.ws_ame_test_input { + display: block; + float: left; + + width: 100%; + margin: 2px 0; + box-sizing: content-box; +} + +.ws_ame_test_input_name { + display: block; + float: left; + width: 35%; + margin-right: 4%; + + text-align: right; + padding-top: 6px; + line-height: 16px; +} + +.ws_ame_test_input_value { + display: block; + float: right; + width: 60%; + + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +#ws_ame_test_actions { + float: left; + width: 100%; + margin-top: 1em; +} + +#ws_ame_test_button_container { + width: 35%; + margin-right: 4%; + float: left; + text-align: right; +} + +#ws_ame_test_progress { + display: none; + width: 60%; + float: right; + + .spinner { + float: none; + vertical-align: bottom; + margin-left: 0; + margin-right: 4px; + } +} + +#ws_ame_test_access_body { + width: 100%; + position: relative; + + border: 1px solid #ddd; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +#ws_ame_test_frame_container { + margin-right: 250px; + background: white; + + min-height: 500px; + position: relative; +} + +#ws_ame_test_access_frame { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + + width: 100%; + height: 100%; + min-height: 500px; + + border: none; + margin: 0; + padding: 0; +} + +#ws_ame_test_access_sidebar { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + + position: absolute; + top: 0; + right: 0; + bottom: 0; + + width: 250px; + padding: 16px 24px; + + background-color: #f3f3f3; + border-left: 1px solid #ddd; + + h4:first-of-type { + margin-top: 0; + } +} + +#ws_ame_test_frame_placeholder { + display: block; + padding: 16px 24px; +} + +#ws_ame_test_output { + display: none; +} \ No newline at end of file diff --git a/css/admin.css b/css/admin.css new file mode 100644 index 0000000..a99f816 --- /dev/null +++ b/css/admin.css @@ -0,0 +1,129 @@ +/** + * Miscellaneous menu styles that can be used on all admin pages. + */ + +/* + * Submenu separators. + */ +hr.ws-submenu-separator { + display: block; + margin: 2px 0; + padding: 0; + height: 0; + + border-width: 1px 0 0 0; + border-style: solid; + border-color: #ccc; +} + +/* Custom separator style suggested by a customer (Slavo) */ +/* +#adminmenu .ws-submenu-separator { + border-bottom: none; + border-top: 1px dotted rgba(0,0,0,.3); + width: 90%; +} +*/ + +/* S2Member separator style */ +/* +#adminmenu .ws-submenu-separator { + display: block; + border: 0; + margin: 1px 0 1px -5px; + padding: 0; + height: 1px; + line-height: 1px; + background: #CCCCCC; +} +*/ + +/* Override .wp-menu-separator styles as they don't work too well in submenus. */ +#adminmenu .wp-submenu li.ws-submenu-separator-wrap { + margin: 0 0 0 0; + padding: 0; + height: inherit; +} + +/* No pointer/hand on separators. */ +#adminmenu li.ws-submenu-separator-wrap a { + cursor: default; +} + +/* No colored bar/marker when hovering over a separator. */ +#adminmenu li.ws-submenu-separator-wrap a:hover, +#adminmenu li.ws-submenu-separator-wrap a:focus { + box-shadow: none; +} + +/* No extra margin in submenus with icons. The selector uses the URL prefix because we can't control the link class. + * li.ws-submenu-separator-wrap would also work, but it's added via JS so there's an undesirable delay (FOUC). + */ +#adminmenu .ame-has-submenu-icons ul.wp-submenu li a[href^="#submenu-separator-"] { + margin-left: 0; +} + +/* + * Override third-party menu icons with image icons selected by the user. + * + * Some plugins use CSS to put their menu icon in a ::before pseudo-element, like WordPress does with Dashicons. + * When the user assigns a custom icon URL to the menu item (e.g. "https://example.com/icon.png"), the ::before + * element will still be there, and it will push the custom icon out of place. To prevent that, let's forcibly + * hide the ::before element (note: don't do this when the user has selected a custom Dashicon/other icon fonts!). + */ +#adminmenu a.ame-has-custom-image-url > .wp-menu-image::before { + display: none !important; +} + +/* + * Submenu icons. + */ +.ame-submenu-icon { + display: block; + padding-right: 8px; + min-width: 20px; + + /* + Dashicons are 20x20 by default and some of them look pretty bad at smaller sizes. Submenu item titles are 16px high + by default. So lets hack some negative margins to make a 20px icon fit in 16px. With the current admin UI styles + it looks okay - submenu items are ~28px high when including padding/margins, so there's no visual overlap. + */ + height: 20px; + margin-top: -1px; + margin-bottom: -3px; + + vertical-align: top; + + margin-left: -28px; + float: left; + + /* Center image-based icons. Doesn't matter for dashicons. */ + text-align: center; +} + +#adminmenu .ame-has-submenu-icons > ul.wp-submenu > li > a, +.folded #adminmenu .ame-has-submenu-icons > ul.wp-submenu > li > a { + /* Push all submenus to the right to ensure that items with and without icons line up nicely. */ + padding-left: 36px; +} + +#adminmenu .ame-submenu-icon img { + padding-top: 2px; + max-width: 32px; + + opacity: 0.6; + filter: alpha(opacity=60); +} + +#adminmenu .wp-submenu li:hover .ame-submenu-icon img, +#adminmenu .wp-submenu li.current .ame-submenu-icon img { + opacity: 1; + filter: alpha(opacity=100); +} + +/* +When the admin menu is collapsed, don't show a submenu icon in the submenu header. +*/ +.folded #adminmenu li.wp-submenu-head .ame-submenu-icon { + display: none; +} \ No newline at end of file diff --git a/css/force-dashicons.css b/css/force-dashicons.css new file mode 100644 index 0000000..a2903f7 --- /dev/null +++ b/css/force-dashicons.css @@ -0,0 +1,463 @@ +/* + Forcibly set menu icons to the selected custom Dashicons. + + Problem: + Some plugins use CSS to assign icons to their admin menu items. Users want to change the icons. + In many cases, simply changing the icon URL doesn't work because the plugin CSS overrides it. + + Workaround: + Add more CSS that overrides the icon styles that were set by other plugins. +*/ +#adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon { + /* + This file was automatically generated from /wp-includes/css/dashicons.css. + Last update: 2017-06-07T16:55:55+00:00 + */ } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon > .wp-menu-image::before { + font-family: "dashicons", sans-serif !important; + font-size: 20px !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon > .wp-menu-image { + background-image: none !important; + position: static; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-menu:before { + content: "\f333" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-admin-site:before { + content: "\f319" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-admin-media:before { + content: "\f104" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-admin-page:before { + content: "\f105" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-admin-comments:before { + content: "\f101" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-admin-appearance:before { + content: "\f100" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-admin-plugins:before { + content: "\f106" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-admin-users:before { + content: "\f110" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-admin-tools:before { + content: "\f107" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-admin-settings:before { + content: "\f108" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-admin-network:before { + content: "\f112" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-admin-generic:before { + content: "\f111" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-admin-home:before { + content: "\f102" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-admin-collapse:before { + content: "\f148" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-filter:before { + content: "\f536" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-admin-customizer:before { + content: "\f540" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-admin-multisite:before { + content: "\f541" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-admin-links:before, #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-format-links:before { + content: "\f103" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-admin-post:before, #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-format-standard:before { + content: "\f109" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-format-image:before { + content: "\f128" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-format-gallery:before { + content: "\f161" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-format-audio:before { + content: "\f127" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-format-video:before { + content: "\f126" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-format-chat:before { + content: "\f125" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-format-status:before { + content: "\f130" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-format-aside:before { + content: "\f123" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-format-quote:before { + content: "\f122" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-welcome-write-blog:before, #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-welcome-edit-page:before { + content: "\f119" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-welcome-add-page:before { + content: "\f133" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-welcome-view-site:before { + content: "\f115" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-welcome-widgets-menus:before { + content: "\f116" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-welcome-comments:before { + content: "\f117" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-welcome-learn-more:before { + content: "\f118" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-image-crop:before { + content: "\f165" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-image-rotate:before { + content: "\f531" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-image-rotate-left:before { + content: "\f166" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-image-rotate-right:before { + content: "\f167" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-image-flip-vertical:before { + content: "\f168" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-image-flip-horizontal:before { + content: "\f169" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-image-filter:before { + content: "\f533" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-undo:before { + content: "\f171" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-redo:before { + content: "\f172" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-ul:before { + content: "\f203" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-ol:before { + content: "\f204" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-quote:before { + content: "\f205" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-alignleft:before { + content: "\f206" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-aligncenter:before { + content: "\f207" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-alignright:before { + content: "\f208" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-insertmore:before { + content: "\f209" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-spellcheck:before { + content: "\f210" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-distractionfree:before, #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-expand:before { + content: "\f211" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-contract:before { + content: "\f506" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-kitchensink:before { + content: "\f212" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-underline:before { + content: "\f213" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-justify:before { + content: "\f214" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-textcolor:before { + content: "\f215" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-paste-word:before { + content: "\f216" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-paste-text:before { + content: "\f217" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-removeformatting:before { + content: "\f218" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-video:before { + content: "\f219" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-customchar:before { + content: "\f220" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-outdent:before { + content: "\f221" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-indent:before { + content: "\f222" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-help:before { + content: "\f223" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-strikethrough:before { + content: "\f224" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-unlink:before { + content: "\f225" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-rtl:before { + content: "\f320" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-break:before { + content: "\f474" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-code:before { + content: "\f475" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-paragraph:before { + content: "\f476" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-editor-table:before { + content: "\f535" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-align-left:before { + content: "\f135" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-align-right:before { + content: "\f136" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-align-center:before { + content: "\f134" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-align-none:before { + content: "\f138" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-lock:before { + content: "\f160" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-unlock:before { + content: "\f528" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-calendar:before { + content: "\f145" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-calendar-alt:before { + content: "\f508" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-visibility:before { + content: "\f177" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-hidden:before { + content: "\f530" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-post-status:before { + content: "\f173" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-edit:before { + content: "\f464" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-post-trash:before, #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-trash:before { + content: "\f182" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-sticky:before { + content: "\f537" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-external:before { + content: "\f504" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-leftright:before { + content: "\f229" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-sort:before { + content: "\f156" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-randomize:before { + content: "\f503" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-list-view:before { + content: "\f163" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-exerpt-view:before, #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-excerpt-view:before { + content: "\f164" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-grid-view:before { + content: "\f509" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-move:before { + content: "\f545" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-hammer:before { + content: "\f308" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-art:before { + content: "\f309" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-migrate:before { + content: "\f310" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-performance:before { + content: "\f311" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-universal-access:before { + content: "\f483" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-universal-access-alt:before { + content: "\f507" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-tickets:before { + content: "\f486" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-nametag:before { + content: "\f484" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-clipboard:before { + content: "\f481" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-heart:before { + content: "\f487" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-megaphone:before { + content: "\f488" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-schedule:before { + content: "\f489" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-wordpress:before { + content: "\f120" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-wordpress-alt:before { + content: "\f324" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-pressthis:before { + content: "\f157" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-update:before { + content: "\f463" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-screenoptions:before { + content: "\f180" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-cart:before { + content: "\f174" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-feedback:before { + content: "\f175" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-cloud:before { + content: "\f176" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-translation:before { + content: "\f326" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-tag:before { + content: "\f323" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-category:before { + content: "\f318" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-archive:before { + content: "\f480" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-tagcloud:before { + content: "\f479" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-text:before { + content: "\f478" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-media-archive:before { + content: "\f501" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-media-audio:before { + content: "\f500" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-media-code:before { + content: "\f499" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-media-default:before { + content: "\f498" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-media-document:before { + content: "\f497" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-media-interactive:before { + content: "\f496" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-media-spreadsheet:before { + content: "\f495" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-media-text:before { + content: "\f491" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-media-video:before { + content: "\f490" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-playlist-audio:before { + content: "\f492" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-playlist-video:before { + content: "\f493" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-controls-play:before { + content: "\f522" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-controls-pause:before { + content: "\f523" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-controls-forward:before { + content: "\f519" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-controls-skipforward:before { + content: "\f517" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-controls-back:before { + content: "\f518" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-controls-skipback:before { + content: "\f516" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-controls-repeat:before { + content: "\f515" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-controls-volumeon:before { + content: "\f521" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-controls-volumeoff:before { + content: "\f520" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-yes:before { + content: "\f147" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-no:before { + content: "\f158" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-no-alt:before { + content: "\f335" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-plus:before { + content: "\f132" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-plus-alt:before { + content: "\f502" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-plus-alt2:before { + content: "\f543" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-minus:before { + content: "\f460" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-dismiss:before { + content: "\f153" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-marker:before { + content: "\f159" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-star-filled:before { + content: "\f155" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-star-half:before { + content: "\f459" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-star-empty:before { + content: "\f154" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-flag:before { + content: "\f227" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-info:before { + content: "\f348" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-warning:before { + content: "\f534" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-share:before { + content: "\f237" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-share1:before { + content: "\f237" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-share-alt:before { + content: "\f240" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-share-alt2:before { + content: "\f242" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-twitter:before { + content: "\f301" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-rss:before { + content: "\f303" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-email:before { + content: "\f465" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-email-alt:before { + content: "\f466" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-facebook:before { + content: "\f304" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-facebook-alt:before { + content: "\f305" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-networking:before { + content: "\f325" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-googleplus:before { + content: "\f462" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-location:before { + content: "\f230" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-location-alt:before { + content: "\f231" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-camera:before { + content: "\f306" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-images-alt:before { + content: "\f232" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-images-alt2:before { + content: "\f233" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-video-alt:before { + content: "\f234" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-video-alt2:before { + content: "\f235" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-video-alt3:before { + content: "\f236" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-vault:before { + content: "\f178" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-shield:before { + content: "\f332" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-shield-alt:before { + content: "\f334" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-sos:before { + content: "\f468" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-search:before { + content: "\f179" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-slides:before { + content: "\f181" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-analytics:before { + content: "\f183" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-chart-pie:before { + content: "\f184" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-chart-bar:before { + content: "\f185" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-chart-line:before { + content: "\f238" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-chart-area:before { + content: "\f239" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-groups:before { + content: "\f307" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-businessman:before { + content: "\f338" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-id:before { + content: "\f336" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-id-alt:before { + content: "\f337" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-products:before { + content: "\f312" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-awards:before { + content: "\f313" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-forms:before { + content: "\f314" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-testimonial:before { + content: "\f473" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-portfolio:before { + content: "\f322" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-book:before { + content: "\f330" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-book-alt:before { + content: "\f331" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-download:before { + content: "\f316" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-upload:before { + content: "\f317" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-backup:before { + content: "\f321" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-clock:before { + content: "\f469" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-lightbulb:before { + content: "\f339" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-microphone:before { + content: "\f482" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-desktop:before { + content: "\f472" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-laptop:before { + content: "\f547" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-tablet:before { + content: "\f471" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-smartphone:before { + content: "\f470" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-phone:before { + content: "\f525" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-smiley:before { + content: "\f328" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-index-card:before { + content: "\f510" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-carrot:before { + content: "\f511" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-building:before { + content: "\f512" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-store:before { + content: "\f513" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-album:before { + content: "\f514" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-palmtree:before { + content: "\f527" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-tickets-alt:before { + content: "\f524" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-money:before { + content: "\f526" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-thumbs-up:before { + content: "\f529" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-thumbs-down:before { + content: "\f542" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-layout:before { + content: "\f538" !important; } + #adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon .dashicons-paperclip:before { + content: "\f546" !important; } + +/*# sourceMappingURL=force-dashicons.css.map */ diff --git a/css/force-dashicons.css.map b/css/force-dashicons.css.map new file mode 100644 index 0000000..845eb5e --- /dev/null +++ b/css/force-dashicons.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "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", +"sources": ["force-dashicons.scss","_dashicons.scss"], +"names": [], +"file": "force-dashicons.css" +} \ No newline at end of file diff --git a/css/force-dashicons.scss b/css/force-dashicons.scss new file mode 100644 index 0000000..6382f12 --- /dev/null +++ b/css/force-dashicons.scss @@ -0,0 +1,26 @@ +/* + Forcibly set menu icons to the selected custom Dashicons. + + Problem: + Some plugins use CSS to assign icons to their admin menu items. Users want to change the icons. + In many cases, simply changing the icon URL doesn't work because the plugin CSS overrides it. + + Workaround: + Add more CSS that overrides the icon styles that were set by other plugins. +*/ + +//Artificially increase selector specificity by repeating the ID. +#adminmenu#adminmenu#adminmenu a.ame-has-custom-dashicon { + & > .wp-menu-image::before { + font-family: "dashicons", sans-serif !important; + font-size: 20px !important; + } + + //Some plugins set the icon as a background image instead of a pseudo-element. + & > .wp-menu-image { + background-image: none !important; + position: static; + } + + @import '_dashicons'; +} \ No newline at end of file diff --git a/css/jquery.qtip.css b/css/jquery.qtip.css new file mode 100644 index 0000000..9062f8a --- /dev/null +++ b/css/jquery.qtip.css @@ -0,0 +1,617 @@ +/* + * qTip2 - Pretty powerful tooltips - v3.0.3 + * http://qtip2.com + * + * Copyright (c) 2016 + * Released under the MIT licenses + * http://jquery.org/license + * + * Date: Wed May 11 2016 10:31 GMT+0100+0100 + * Plugins: tips modal viewport svg imagemap ie6 + * Styles: core basic css3 + */ +.qtip{ + position: absolute; + left: -28000px; + top: -28000px; + display: none; + + max-width: 280px; + min-width: 50px; + + font-size: 10.5px; + line-height: 12px; + + direction: ltr; + + box-shadow: none; + padding: 0; +} + + .qtip-content{ + position: relative; + padding: 5px 9px; + overflow: hidden; + + text-align: left; + word-wrap: break-word; + } + + .qtip-titlebar{ + position: relative; + padding: 5px 35px 5px 10px; + overflow: hidden; + + border-width: 0 0 1px; + font-weight: bold; + } + + .qtip-titlebar + .qtip-content{ border-top-width: 0 !important; } + + /* Default close button class */ + .qtip-close{ + position: absolute; + right: -9px; top: -9px; + z-index: 11; /* Overlap .qtip-tip */ + + cursor: pointer; + outline: medium none; + + border: 1px solid transparent; + } + + .qtip-titlebar .qtip-close{ + right: 4px; top: 50%; + margin-top: -9px; + } + + * html .qtip-titlebar .qtip-close{ top: 16px; } /* IE fix */ + + .qtip-titlebar .ui-icon, + .qtip-icon .ui-icon{ + display: block; + text-indent: -1000em; + direction: ltr; + } + + .qtip-icon, .qtip-icon .ui-icon{ + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + text-decoration: none; + } + + .qtip-icon .ui-icon{ + width: 18px; + height: 14px; + + line-height: 14px; + text-align: center; + text-indent: 0; + font: normal bold 10px/13px Tahoma,sans-serif; + + color: inherit; + background: transparent none no-repeat -100em -100em; + } + +/* Applied to 'focused' tooltips e.g. most recently displayed/interacted with */ +.qtip-focus{} + +/* Applied on hover of tooltips i.e. added/removed on mouseenter/mouseleave respectively */ +.qtip-hover{} + +/* Default tooltip style */ +.qtip-default{ + border: 1px solid #F1D031; + + background-color: #FFFFA3; + color: #555; +} + + .qtip-default .qtip-titlebar{ + background-color: #FFEF93; + } + + .qtip-default .qtip-icon{ + border-color: #CCC; + background: #F1F1F1; + color: #777; + } + + .qtip-default .qtip-titlebar .qtip-close{ + border-color: #AAA; + color: #111; + } + + +/*! Light tooltip style */ +.qtip-light{ + background-color: white; + border-color: #E2E2E2; + color: #454545; +} + + .qtip-light .qtip-titlebar{ + background-color: #f1f1f1; + } + + +/*! Dark tooltip style */ +.qtip-dark{ + background-color: #505050; + border-color: #303030; + color: #f3f3f3; +} + + .qtip-dark .qtip-titlebar{ + background-color: #404040; + } + + .qtip-dark .qtip-icon{ + border-color: #444; + } + + .qtip-dark .qtip-titlebar .ui-state-hover{ + border-color: #303030; + } + + +/*! Cream tooltip style */ +.qtip-cream{ + background-color: #FBF7AA; + border-color: #F9E98E; + color: #A27D35; +} + + .qtip-cream .qtip-titlebar{ + background-color: #F0DE7D; + } + + .qtip-cream .qtip-close .qtip-icon{ + background-position: -82px 0; + } + + +/*! Red tooltip style */ +.qtip-red{ + background-color: #F78B83; + border-color: #D95252; + color: #912323; +} + + .qtip-red .qtip-titlebar{ + background-color: #F06D65; + } + + .qtip-red .qtip-close .qtip-icon{ + background-position: -102px 0; + } + + .qtip-red .qtip-icon{ + border-color: #D95252; + } + + .qtip-red .qtip-titlebar .ui-state-hover{ + border-color: #D95252; + } + + +/*! Green tooltip style */ +.qtip-green{ + background-color: #CAED9E; + border-color: #90D93F; + color: #3F6219; +} + + .qtip-green .qtip-titlebar{ + background-color: #B0DE78; + } + + .qtip-green .qtip-close .qtip-icon{ + background-position: -42px 0; + } + + +/*! Blue tooltip style */ +.qtip-blue{ + background-color: #E5F6FE; + border-color: #ADD9ED; + color: #5E99BD; +} + + .qtip-blue .qtip-titlebar{ + background-color: #D0E9F5; + } + + .qtip-blue .qtip-close .qtip-icon{ + background-position: -2px 0; + } + + +.qtip-shadow{ + -webkit-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15); + box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15); +} + +/* Add rounded corners to your tooltips in: FF3+, Chrome 2+, Opera 10.6+, IE9+, Safari 2+ */ +.qtip-rounded, +.qtip-tipsy, +.qtip-bootstrap{ + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} + +.qtip-rounded .qtip-titlebar{ + -moz-border-radius: 4px 4px 0 0; + -webkit-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} + +/* Youtube tooltip style */ +.qtip-youtube{ + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + border-radius: 2px; + + -webkit-box-shadow: 0 0 3px #333; + -moz-box-shadow: 0 0 3px #333; + box-shadow: 0 0 3px #333; + + color: white; + border: 0 solid transparent; + + background: #4A4A4A; + background-image: -webkit-gradient(linear,left top,left bottom,color-stop(0,#4A4A4A),color-stop(100%,black)); + background-image: -webkit-linear-gradient(top,#4A4A4A 0,black 100%); + background-image: -moz-linear-gradient(top,#4A4A4A 0,black 100%); + background-image: -ms-linear-gradient(top,#4A4A4A 0,black 100%); + background-image: -o-linear-gradient(top,#4A4A4A 0,black 100%); +} + + .qtip-youtube .qtip-titlebar{ + background-color: #4A4A4A; + background-color: rgba(0,0,0,0); + } + + .qtip-youtube .qtip-content{ + padding: .75em; + font: 12px arial,sans-serif; + + filter: progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr=#4a4a4a,EndColorStr=#000000); + -ms-filter: "progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr=#4a4a4a,EndColorStr=#000000);"; + } + + .qtip-youtube .qtip-icon{ + border-color: #222; + } + + .qtip-youtube .qtip-titlebar .ui-state-hover{ + border-color: #303030; + } + + +/* jQuery TOOLS Tooltip style */ +.qtip-jtools{ + background: #232323; + background: rgba(0, 0, 0, 0.7); + background-image: -webkit-gradient(linear, left top, left bottom, from(#717171), to(#232323)); + background-image: -moz-linear-gradient(top, #717171, #232323); + background-image: -webkit-linear-gradient(top, #717171, #232323); + background-image: -ms-linear-gradient(top, #717171, #232323); + background-image: -o-linear-gradient(top, #717171, #232323); + + border: 2px solid #ddd; + border: 2px solid rgba(241,241,241,1); + + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + border-radius: 2px; + + -webkit-box-shadow: 0 0 12px #333; + -moz-box-shadow: 0 0 12px #333; + box-shadow: 0 0 12px #333; +} + + /* IE Specific */ + .qtip-jtools .qtip-titlebar{ + background-color: transparent; + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171,endColorstr=#4A4A4A); + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171,endColorstr=#4A4A4A)"; + } + .qtip-jtools .qtip-content{ + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A,endColorstr=#232323); + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A,endColorstr=#232323)"; + } + + .qtip-jtools .qtip-titlebar, + .qtip-jtools .qtip-content{ + background: transparent; + color: white; + border: 0 dashed transparent; + } + + .qtip-jtools .qtip-icon{ + border-color: #555; + } + + .qtip-jtools .qtip-titlebar .ui-state-hover{ + border-color: #333; + } + + +/* Cluetip style */ +.qtip-cluetip{ + -webkit-box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4); + -moz-box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4); + box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4); + + background-color: #D9D9C2; + color: #111; + border: 0 dashed transparent; +} + + .qtip-cluetip .qtip-titlebar{ + background-color: #87876A; + color: white; + border: 0 dashed transparent; + } + + .qtip-cluetip .qtip-icon{ + border-color: #808064; + } + + .qtip-cluetip .qtip-titlebar .ui-state-hover{ + border-color: #696952; + color: #696952; + } + + +/* Tipsy style */ +.qtip-tipsy{ + background: black; + background: rgba(0, 0, 0, .87); + + color: white; + border: 0 solid transparent; + + font-size: 11px; + font-family: 'Lucida Grande', sans-serif; + font-weight: bold; + line-height: 16px; + text-shadow: 0 1px black; +} + + .qtip-tipsy .qtip-titlebar{ + padding: 6px 35px 0 10px; + background-color: transparent; + } + + .qtip-tipsy .qtip-content{ + padding: 6px 10px; + } + + .qtip-tipsy .qtip-icon{ + border-color: #222; + text-shadow: none; + } + + .qtip-tipsy .qtip-titlebar .ui-state-hover{ + border-color: #303030; + } + + +/* Tipped style */ +.qtip-tipped{ + border: 3px solid #959FA9; + + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + + background-color: #F9F9F9; + color: #454545; + + font-weight: normal; + font-family: serif; +} + + .qtip-tipped .qtip-titlebar{ + border-bottom-width: 0; + + color: white; + background: #3A79B8; + background-image: -webkit-gradient(linear, left top, left bottom, from(#3A79B8), to(#2E629D)); + background-image: -webkit-linear-gradient(top, #3A79B8, #2E629D); + background-image: -moz-linear-gradient(top, #3A79B8, #2E629D); + background-image: -ms-linear-gradient(top, #3A79B8, #2E629D); + background-image: -o-linear-gradient(top, #3A79B8, #2E629D); + filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D); + -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D)"; + } + + .qtip-tipped .qtip-icon{ + border: 2px solid #285589; + background: #285589; + } + + .qtip-tipped .qtip-icon .ui-icon{ + background-color: #FBFBFB; + color: #555; + } + + +/** + * Twitter Bootstrap style. + * + * Tested with IE 8, IE 9, Chrome 18, Firefox 9, Opera 11. + * Does not work with IE 7. + */ +.qtip-bootstrap{ + /** Taken from Bootstrap body */ + font-size: 14px; + line-height: 20px; + color: #333333; + + /** Taken from Bootstrap .popover */ + padding: 1px; + background-color: #ffffff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + + .qtip-bootstrap .qtip-titlebar{ + /** Taken from Bootstrap .popover-title */ + padding: 8px 14px; + margin: 0; + font-size: 14px; + font-weight: normal; + line-height: 18px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + -webkit-border-radius: 5px 5px 0 0; + -moz-border-radius: 5px 5px 0 0; + border-radius: 5px 5px 0 0; + } + + .qtip-bootstrap .qtip-titlebar .qtip-close{ + /** + * Overrides qTip2: + * .qtip-titlebar .qtip-close{ + * [...] + * right: 4px; + * top: 50%; + * [...] + * border-style: solid; + * } + */ + right: 11px; + top: 45%; + border-style: none; + } + + .qtip-bootstrap .qtip-content{ + /** Taken from Bootstrap .popover-content */ + padding: 9px 14px; + } + + .qtip-bootstrap .qtip-icon{ + /** + * Overrides qTip2: + * .qtip-default .qtip-icon { + * border-color: #CCC; + * background: #F1F1F1; + * color: #777; + * } + */ + background: transparent; + } + + .qtip-bootstrap .qtip-icon .ui-icon{ + /** + * Overrides qTip2: + * .qtip-icon .ui-icon{ + * width: 18px; + * height: 14px; + * } + */ + width: auto; + height: auto; + + /* Taken from Bootstrap .close */ + float: right; + font-size: 20px; + font-weight: bold; + line-height: 18px; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); + } + + .qtip-bootstrap .qtip-icon .ui-icon:hover{ + /* Taken from Bootstrap .close:hover */ + color: #000000; + text-decoration: none; + cursor: pointer; + opacity: 0.4; + filter: alpha(opacity=40); + } + + +/* IE9 fix - removes all filters */ +.qtip:not(.ie9haxors) div.qtip-content, +.qtip:not(.ie9haxors) div.qtip-titlebar{ + filter: none; + -ms-filter: none; +} + + +.qtip .qtip-tip{ + margin: 0 auto; + overflow: hidden; + z-index: 10; + +} + + /* Opera bug #357 - Incorrect tip position + https://github.com/Craga89/qTip2/issues/367 */ + x:-o-prefocus, .qtip .qtip-tip{ + visibility: hidden; + } + + .qtip .qtip-tip, + .qtip .qtip-tip .qtip-vml, + .qtip .qtip-tip canvas{ + position: absolute; + + color: #123456; + background: transparent; + border: 0 dashed transparent; + } + + .qtip .qtip-tip canvas{ top: 0; left: 0; } + + .qtip .qtip-tip .qtip-vml{ + behavior: url(#default#VML); + display: inline-block; + visibility: visible; + } + + +#qtip-overlay{ + position: fixed; + left: 0; top: 0; + width: 100%; height: 100%; +} + + /* Applied to modals with show.modal.blur set to true */ + #qtip-overlay.blurs{ cursor: pointer; } + + /* Change opacity of overlay here */ + #qtip-overlay div{ + position: absolute; + left: 0; top: 0; + width: 100%; height: 100%; + + background-color: black; + + opacity: 0.7; + filter:alpha(opacity=70); + -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)"; + } + + +.qtipmodal-ie6fix{ + position: absolute !important; +} diff --git a/css/jquery.qtip.min.css b/css/jquery.qtip.min.css new file mode 100644 index 0000000..5adc4b7 --- /dev/null +++ b/css/jquery.qtip.min.css @@ -0,0 +1 @@ +#qtip-overlay.blurs,.qtip-close{cursor:pointer}.qtip{position:absolute;left:-28000px;top:-28000px;display:none;max-width:280px;min-width:50px;font-size:10.5px;line-height:12px;direction:ltr;box-shadow:none;padding:0}.qtip-content,.qtip-titlebar{position:relative;overflow:hidden}.qtip-content{padding:5px 9px;text-align:left;word-wrap:break-word}.qtip-titlebar{padding:5px 35px 5px 10px;border-width:0 0 1px;font-weight:700}.qtip-titlebar+.qtip-content{border-top-width:0!important}.qtip-close{position:absolute;right:-9px;top:-9px;z-index:11;outline:0;border:1px solid transparent}.qtip-titlebar .qtip-close{right:4px;top:50%;margin-top:-9px}* html .qtip-titlebar .qtip-close{top:16px}.qtip-icon .ui-icon,.qtip-titlebar .ui-icon{display:block;text-indent:-1000em;direction:ltr}.qtip-icon,.qtip-icon .ui-icon{-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;text-decoration:none}.qtip-icon .ui-icon{width:18px;height:14px;line-height:14px;text-align:center;text-indent:0;font:normal 700 10px/13px Tahoma,sans-serif;color:inherit;background:-100em -100em no-repeat}.qtip-default{border:1px solid #F1D031;background-color:#FFFFA3;color:#555}.qtip-default .qtip-titlebar{background-color:#FFEF93}.qtip-default .qtip-icon{border-color:#CCC;background:#F1F1F1;color:#777}.qtip-default .qtip-titlebar .qtip-close{border-color:#AAA;color:#111}.qtip-light{background-color:#fff;border-color:#E2E2E2;color:#454545}.qtip-light .qtip-titlebar{background-color:#f1f1f1}.qtip-dark{background-color:#505050;border-color:#303030;color:#f3f3f3}.qtip-dark .qtip-titlebar{background-color:#404040}.qtip-dark .qtip-icon{border-color:#444}.qtip-dark .qtip-titlebar .ui-state-hover{border-color:#303030}.qtip-cream{background-color:#FBF7AA;border-color:#F9E98E;color:#A27D35}.qtip-red,.qtip-red .qtip-icon,.qtip-red .qtip-titlebar .ui-state-hover{border-color:#D95252}.qtip-cream .qtip-titlebar{background-color:#F0DE7D}.qtip-cream .qtip-close .qtip-icon{background-position:-82px 0}.qtip-red{background-color:#F78B83;color:#912323}.qtip-red .qtip-titlebar{background-color:#F06D65}.qtip-red .qtip-close .qtip-icon{background-position:-102px 0}.qtip-green{background-color:#CAED9E;border-color:#90D93F;color:#3F6219}.qtip-green .qtip-titlebar{background-color:#B0DE78}.qtip-green .qtip-close .qtip-icon{background-position:-42px 0}.qtip-blue{background-color:#E5F6FE;border-color:#ADD9ED;color:#5E99BD}.qtip-blue .qtip-titlebar{background-color:#D0E9F5}.qtip-blue .qtip-close .qtip-icon{background-position:-2px 0}.qtip-shadow{-webkit-box-shadow:1px 1px 3px 1px rgba(0,0,0,.15);-moz-box-shadow:1px 1px 3px 1px rgba(0,0,0,.15);box-shadow:1px 1px 3px 1px rgba(0,0,0,.15)}.qtip-bootstrap,.qtip-rounded,.qtip-tipsy{-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px}.qtip-rounded .qtip-titlebar{-moz-border-radius:4px 4px 0 0;-webkit-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.qtip-youtube{-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 0 3px #333;-moz-box-shadow:0 0 3px #333;box-shadow:0 0 3px #333;color:#fff;border:0 solid transparent;background:#4A4A4A;background-image:-webkit-gradient(linear,left top,left bottom,color-stop(0,#4A4A4A),color-stop(100%,#000));background-image:-webkit-linear-gradient(top,#4A4A4A 0,#000 100%);background-image:-moz-linear-gradient(top,#4A4A4A 0,#000 100%);background-image:-ms-linear-gradient(top,#4A4A4A 0,#000 100%);background-image:-o-linear-gradient(top,#4A4A4A 0,#000 100%)}.qtip-youtube .qtip-titlebar{background-color:#4A4A4A;background-color:rgba(0,0,0,0)}.qtip-youtube .qtip-content{padding:.75em;font:12px arial,sans-serif;filter:progid:DXImageTransform.Microsoft.Gradient(GradientType=0, StartColorStr=#4a4a4a, EndColorStr=#000000);-ms-filter:"progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr=#4a4a4a,EndColorStr=#000000);"}.qtip-youtube .qtip-icon{border-color:#222}.qtip-youtube .qtip-titlebar .ui-state-hover{border-color:#303030}.qtip-jtools{background:#232323;background:rgba(0,0,0,.7);background-image:-webkit-gradient(linear,left top,left bottom,from(#717171),to(#232323));background-image:-moz-linear-gradient(top,#717171,#232323);background-image:-webkit-linear-gradient(top,#717171,#232323);background-image:-ms-linear-gradient(top,#717171,#232323);background-image:-o-linear-gradient(top,#717171,#232323);border:2px solid #ddd;border:2px solid rgba(241,241,241,1);-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 0 12px #333;-moz-box-shadow:0 0 12px #333;box-shadow:0 0 12px #333}.qtip-jtools .qtip-titlebar{background-color:transparent;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171, endColorstr=#4A4A4A);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171,endColorstr=#4A4A4A)"}.qtip-jtools .qtip-content{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A, endColorstr=#232323);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A,endColorstr=#232323)"}.qtip-jtools .qtip-content,.qtip-jtools .qtip-titlebar{background:0 0;color:#fff;border:0 dashed transparent}.qtip-jtools .qtip-icon{border-color:#555}.qtip-jtools .qtip-titlebar .ui-state-hover{border-color:#333}.qtip-cluetip{-webkit-box-shadow:4px 4px 5px rgba(0,0,0,.4);-moz-box-shadow:4px 4px 5px rgba(0,0,0,.4);box-shadow:4px 4px 5px rgba(0,0,0,.4);background-color:#D9D9C2;color:#111;border:0 dashed transparent}.qtip-cluetip .qtip-titlebar{background-color:#87876A;color:#fff;border:0 dashed transparent}.qtip-cluetip .qtip-icon{border-color:#808064}.qtip-cluetip .qtip-titlebar .ui-state-hover{border-color:#696952;color:#696952}.qtip-tipsy{background:#000;background:rgba(0,0,0,.87);color:#fff;border:0 solid transparent;font-size:11px;font-family:'Lucida Grande',sans-serif;font-weight:700;line-height:16px;text-shadow:0 1px #000}.qtip-tipsy .qtip-titlebar{padding:6px 35px 0 10px;background-color:transparent}.qtip-tipsy .qtip-content{padding:6px 10px}.qtip-tipsy .qtip-icon{border-color:#222;text-shadow:none}.qtip-tipsy .qtip-titlebar .ui-state-hover{border-color:#303030}.qtip-tipped{border:3px solid #959FA9;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;background-color:#F9F9F9;color:#454545;font-weight:400;font-family:serif}.qtip-tipped .qtip-titlebar{border-bottom-width:0;color:#fff;background:#3A79B8;background-image:-webkit-gradient(linear,left top,left bottom,from(#3A79B8),to(#2E629D));background-image:-webkit-linear-gradient(top,#3A79B8,#2E629D);background-image:-moz-linear-gradient(top,#3A79B8,#2E629D);background-image:-ms-linear-gradient(top,#3A79B8,#2E629D);background-image:-o-linear-gradient(top,#3A79B8,#2E629D);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8, endColorstr=#2E629D);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D)"}.qtip-tipped .qtip-icon{border:2px solid #285589;background:#285589}.qtip-tipped .qtip-icon .ui-icon{background-color:#FBFBFB;color:#555}.qtip-bootstrap{font-size:14px;line-height:20px;color:#333;padding:1px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.qtip-bootstrap .qtip-titlebar{padding:8px 14px;margin:0;font-size:14px;font-weight:400;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.qtip-bootstrap .qtip-titlebar .qtip-close{right:11px;top:45%;border-style:none}.qtip-bootstrap .qtip-content{padding:9px 14px}.qtip-bootstrap .qtip-icon{background:0 0}.qtip-bootstrap .qtip-icon .ui-icon{width:auto;height:auto;float:right;font-size:20px;font-weight:700;line-height:18px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}#qtip-overlay,#qtip-overlay div{left:0;top:0;width:100%;height:100%}.qtip-bootstrap .qtip-icon .ui-icon:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}.qtip:not(.ie9haxors) div.qtip-content,.qtip:not(.ie9haxors) div.qtip-titlebar{filter:none;-ms-filter:none}.qtip .qtip-tip{margin:0 auto;overflow:hidden;z-index:10}.qtip .qtip-tip,x:-o-prefocus{visibility:hidden}.qtip .qtip-tip,.qtip .qtip-tip .qtip-vml,.qtip .qtip-tip canvas{position:absolute;color:#123456;background:0 0;border:0 dashed transparent}.qtip .qtip-tip canvas{top:0;left:0}.qtip .qtip-tip .qtip-vml{behavior:url(#default#VML);display:inline-block;visibility:visible}#qtip-overlay{position:fixed}#qtip-overlay div{position:absolute;background-color:#000;opacity:.7;filter:alpha(opacity=70);-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)"}.qtipmodal-ie6fix{position:absolute!important} \ No newline at end of file diff --git a/css/menu-editor.css b/css/menu-editor.css new file mode 100644 index 0000000..b612be6 --- /dev/null +++ b/css/menu-editor.css @@ -0,0 +1,2059 @@ +@charset "UTF-8"; +/* Admin Menu Editor CSS file */ +.ame-input-group { + display: flex; + flex-wrap: wrap; +} +.ame-input-group > :not(:first-child) { + margin-left: -1px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.ame-input-group > :not(:last-child) { + margin-right: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +#ws_menu_editor { + min-width: 780px; +} + +.ame-is-free-version #ws_menu_editor { + margin-top: 9px; +} + +.ws_main_container { + margin: 2px; + width: 316px; + float: left; + display: block; + border: 1px solid #ccd0d4; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); + background-color: #FFFFFF; + border-radius: 0px; + -moz-border-radius: 0px; + -webkit-border-radius: 0px; +} + +.ws_box { + min-height: 30px; + width: 100%; + margin: 0; +} + +.ws_basic_container { + float: left; + display: block; +} + +.ws_dropzone { + display: block; + box-sizing: border-box; + margin: 2px 6px; + border: 3px none #b4b9be; + height: 31px; +} + +.ws_dropzone_active, +.ws_dropzone_hover, +.ws_top_to_submenu_drop_hover .ws_dropzone { + border-style: dashed; +} + +.ws_dropzone_hover, +.ws_top_to_submenu_drop_hover .ws_dropzone { + border-width: 1px; +} + +/************************************************* + Actor UI + *************************************************/ +#ws_actor_selector li:after { + content: "| "; +} + +#ws_actor_selector li:last-child:after { + content: ""; +} + +#ws_actor_selector li a { + display: inline-block; + text-align: center; +} +#ws_actor_selector li a::before { + display: block; + content: attr(data-text); + font-weight: bold; + height: 1px; + overflow: hidden; + visibility: hidden; + margin-bottom: -1px; +} + +#ws_actor_selector { + margin-top: 6px; +} + +/** + * The checkbox that lets the user show/hide a menu for the currently selected actor. + */ +#ws_menu_editor .ws_actor_access_checkbox, +#ws_menu_editor input[type=checkbox].ws_actor_access_checkbox { + margin-right: 2px; + margin-left: 2px; + margin-top: 1px; + vertical-align: text-top; +} +#ws_menu_editor .ws_actor_access_checkbox:indeterminate:before, +#ws_menu_editor input[type=checkbox].ws_actor_access_checkbox:indeterminate:before { + content: "■"; + color: #1e8cbe; + margin: -3px 0 0 -1px; + font: 400 14px/1 dashicons; + float: left; + display: inline-block; + vertical-align: middle; + width: 16px; + -webkit-font-smoothing: antialiased; +} +@media screen and (max-width: 782px) { + #ws_menu_editor .ws_actor_access_checkbox:indeterminate:before, +#ws_menu_editor input[type=checkbox].ws_actor_access_checkbox:indeterminate:before { + height: 1.5625rem; + width: 1.5625rem; + line-height: 1.5625rem; + margin: -1px; + font-size: 18px; + font-family: unset; + font-weight: normal; + } +} + +@media screen and (max-width: 782px) { + #ws_menu_editor input[type=checkbox].ws_actor_access_checkbox:indeterminate:before { + margin: -6px 0 0 1px; + font: 400 26px/1 dashicons; + } +} +/* The checkbox is only visible when viewing the menu configuration for a specific actor. */ +#ws_menu_editor .ws_actor_access_checkbox { + display: none; +} + +#ws_menu_editor.ws_is_actor_view .ws_actor_access_checkbox { + display: inline-block; +} + +/* Gray-out items inaccessible to the currently selected actor */ +.ws_is_actor_view .ws_container.ws_is_hidden_for_actor { + background-color: #F9F9F9; +} + +.ws_is_actor_view .ws_is_hidden_for_actor .ws_item_title { + color: #777; +} + +/* + * The sidebar + */ +#ws_editor_sidebar { + width: auto; + padding: 2px; +} + +#ws_menu_editor .ws_main_button { + clear: both; + display: block; + margin: 4px; + width: 130px; +} + +#ws_menu_editor #ws_save_menu { + margin-bottom: 20px; +} + +#ws_menu_editor #ws_toggle_editor_layout { + display: none; +} + +#ws_menu_editor .ws_sidebar_button_separator { + display: block; + height: 4px; + margin: 0; + padding: 0; +} + +/* + * Page heading and tabs + */ +#ws_ame_editor_heading { + float: left; +} + +/* + * Menu components and widgets + */ +.ws_container { + display: block; + width: 296px; + padding: 3px; + margin: 2px 0 2px 6px; +} +body.rtl .ws_container { + margin-right: 6px; + margin-left: 0; +} + +.ws_submenu { + min-height: 2em; +} + +.ws_item_head { + padding: 0; +} + +.ws_item_title { + display: inline-block; + padding: 2px; + cursor: default; + font-size: 13px; + line-height: 18px; +} + +.ws_edit_link { + float: right; + margin-right: 0; + cursor: pointer; + display: block; + width: 40px; + height: 22px; + border-radius: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + text-decoration: none; +} + +.ws_menu_drop_hover { + background-color: #43b529 !important; +} + +.ws_container.ui-sortable-helper * { + cursor: move !important; +} + +.ws_container.ws_sortable_placeholder { + outline: 1px dashed #b4b9be; + outline-offset: -1px; + background: none; + border-color: transparent; +} + +/* + If you ever want to apply a right-arrow style to the currently selected menu item, + you can do it like this. Commented out for now since it doesn't look all that great, + but might be useful in the future. +*/ +/* +.ws_container { + position: relative; +} + +.ws_menu.ws_active::after { + content: ""; + display: block; + z-index: 1002; + + border-left: 14px solid #8EB0F1; + border-top: 15px solid rgba(255, 255, 255, 0.1); + border-bottom: 15px solid rgba(255, 255, 255, 0.1); + background: transparent; + + position: absolute; + right: -14px; + top: -1px; + + width: 0; + height: 0; +} +*/ +/* + * A left-arrow style alternative. This one is image-based and doesn't suffer from the finicky sizing issues + * of CSS triangles. + */ +.ws_container { + position: relative; +} + +.ws_menu.ws_active::after { + content: ""; + display: block; + position: absolute; + right: -19px; + top: -1px; + width: 19px; + height: 30px; + background: transparent url("../images/submenu-tip.png") no-repeat center; +} + +.ws_container.ws_menu_separator.ws_active::after, +.ws_container.ui-sortable-helper::after { + background-image: none; +} + +/**************************************** + Per-menu settings fields & panels +*****************************************/ +.ws_editbox { + display: block; + padding: 4px; + border-radius: 2px; + border-top-right-radius: 0; + -moz-border-radius: 2px; + -moz-border-radius-topright: 0; + -webkit-border-radius: 2px; + -webkit-border-top-right-radius: 0; +} + +.ws_edit_panel { + margin: 0; + padding: 0; + border: none; +} + +.ws_edit_field { + margin-bottom: 6px; + min-height: 45px; +} +.ws_edit_field:after { + visibility: hidden; + display: block; + height: 0; + font-size: 0; + content: " "; + clear: both; +} + +.ws_edit_field-custom { + margin-top: 10px; +} + +.ws_edit_field.ws_no_field_caption { + margin-top: 10px; + padding-left: 1px; + height: 25px; + min-height: 25px; +} + +/* + * Group headings + */ +.ws_edit_field.ws_field_group_heading { + height: 1px; + min-height: 0; + padding-top: 0; + background: #ccc; + margin: 8px -4px 5px; +} +.ws_edit_field.ws_field_group_heading span { + display: none; + font-weight: bold; +} + +/* The reset-to-default button */ +.ws_reset_button { + display: block; + float: right; + margin-left: 4px; + margin-top: 2px; + margin-right: 6px; + cursor: pointer; + width: 16px; + height: 16px; + vertical-align: top; + background: url("../images/pencil_delete_gray.png") no-repeat center; +} +.ame-is-wp53-plus .ws_reset_button { + margin-top: 5px; +} + +.ws_reset_button:hover { + background-image: url("../images/pencil_delete.png"); +} + +.ws_input_default input, +.ws_input_default select, +.ws_input_default .ws_color_scheme_display { + color: gray; +} + +/* No reset button for fields set to the default value and fields without a default value */ +.ws_input_default .ws_reset_button, +.ws_has_no_default .ws_reset_button { + visibility: hidden; +} + +/* The input box in each field editor */ +#ws_menu_editor .ws_editbox input[type=text], +#ws_menu_editor .ws_editbox select { + display: block; + float: left; + width: 254px; + height: 25px; + font-size: 12px; + line-height: 17px; + padding-top: 3px; + padding-bottom: 3px; +} +.ame-is-wp53-plus #ws_menu_editor .ws_editbox input[type=text], +.ame-is-wp53-plus #ws_menu_editor .ws_editbox select { + height: 28px; + margin-top: 1px; +} + +#ws_menu_editor .ws_edit_field label { + display: block; + float: left; +} + +#ws_menu_editor .ws_edit_field-custom input[type=checkbox] { + margin-top: 0; +} + +#ws_menu_editor input[type=text].ws_field_value { + min-height: 25px; +} +.ame-is-wp53-plus #ws_menu_editor input[type=text].ws_field_value { + min-height: 28px; +} + +/* Dropdown button for combo-box fields */ +#ws_menu_editor .ws_dropdown_button, +#ws_menu_access_editor .ws_dropdown_button { + box-sizing: border-box; + width: 25px; + height: 25px; + min-height: 25px; + margin: 1px 1px 1px 0; + padding: 0 1px 0 0; + text-align: center; + font-family: dashicons; + font-size: 16px !important; + line-height: 25px; + border-color: #dfdfdf; + box-shadow: none; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.ame-is-wp53-plus #ws_menu_editor .ws_dropdown_button, +#ws_menu_access_editor.ame-is-wp53-plus .ws_dropdown_button { + height: 28px; + border-color: #7e8993; + background-color: white; + border-left-style: none; + font-size: 16px !important; + line-height: 24px; + color: #555; +} +.ame-is-wp53-plus #ws_menu_editor .ws_dropdown_button:hover, +#ws_menu_access_editor.ame-is-wp53-plus .ws_dropdown_button:hover { + color: #23282d; +} + +#ws_menu_access_editor .ws_dropdown_button { + display: inline-block; + height: 27px; +} + +#ws_menu_access_editor.ame-is-wp53-plus .ws_dropdown_button { + height: 30px; +} + +#ws_menu_editor .ws_dropdown_button { + display: block; + float: left; +} + +/* +The appearance and size of combo-box fields need to be changed +to accommodate the drop-down button. +*/ +#ws_menu_editor .ws_has_dropdown input.ws_field_value, +#ws_menu_access_editor input.ws_has_dropdown { + margin-right: 0; + border-right: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +#ws_menu_access_editor input.ws_has_dropdown { + width: 90%; + box-sizing: border-box; + height: 27px; + margin-top: 1px; +} + +#ws_menu_access_editor.ame-is-wp53-plus input.ws_has_dropdown { + height: 30px; +} + +#ws_menu_editor .ws_has_dropdown input.ws_field_value { + width: 229px; +} + +/* Unlike others, this field is just a single checkbox, so it has a smaller height */ +#ws_menu_editor .ws_edit_field-custom { + height: 16px; +} + +/* + * "Show/hide advanced fields" + */ +.ws_toggle_container { + text-align: right; + margin-right: 27px; +} + +.ws_toggle_advanced_fields { + color: #6087CB; + text-decoration: none; + font-size: 0.85em; +} + +.ws_toggle_advanced_fields:visited, .ws_toggle_advanced_fields:active { + color: #6087CB; +} + +.ws_toggle_advanced_fields:hover { + color: #d54e21; + text-decoration: underline; +} + +/************************************ + Menu flags +*************************************/ +.ws_flag_container { + float: right; + margin-right: 4px; + padding-top: 2px; +} + +.ws_flag { + display: block; + float: right; + width: 16px; + height: 16px; + margin-left: 4px; + background-repeat: no-repeat; +} + +/* user-created items */ +.ws_custom_flag { + background-image: url("../images/page-add.png"); +} + +/* unused items - those that are in the default menu but not in the custom one */ +.ws_unused_flag { + background-image: url("../images/new-menu-badge.png"); + width: 31px; +} + +/* hidden items */ +.ws_hidden_flag { + background-image: url("../images/page-invisible.png"); +} + +/* items with custom permissions for the selected actor */ +.ws_custom_actor_permissions_flag { + font: 16px/1 "dashicons"; +} + +.ws_custom_actor_permissions_flag::before { + /*content: "\f160";*/ + /* padlock */ + content: "\f110"; + /* human silhouette */ + color: black; + filter: alpha(opacity=25); + /*IE 5-7*/ + opacity: 0.25; +} + +/* Hidden from everyone except the current user and Super Admin. */ +.ws_hidden_from_others_flag { + background-image: url("../images/font-awesome/eye-slash.png"); +} + +/* Item visibility can't be determined because it depends on a meta capability. */ +.ws_uncertain_meta_cap_flag::before { + font: 16px/1 "dashicons"; + content: "\f348"; + color: black; + filter: alpha(opacity=25); + /*IE 5-7*/ + opacity: 0.25; +} + +/* These classes could be used to apply different styles to items depending on their flags */ +/************************************ + Toolbars +*************************************/ +.ws_toolbar { + display: block; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + width: 100%; + padding: 6px 6px 0 6px; +} + +.ws_button { + display: block; + margin-right: 3px; + margin-bottom: 4px; + padding: 4px; + float: left; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + width: 16px; + height: 16px; + border-radius: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; +} +.ws_button img { + vertical-align: top; +} + +a.ws_button:hover { + background-color: #d0e0ff; + border-color: #9090c0; +} + +.ws_button.ws_button_disabled { + border-color: #ccc; +} + +a.ws_button.ws_button_disabled:hover { + background-color: white; + border: 1px solid #ccc; +} + +.ws_button_disabled img { + filter: grayscale(1); + -webkit-filter: grayscale(1); + opacity: 0.65; +} + +.ws_separator { + float: left; + width: 5px; +} + +#ws_toggle_toolbar, .ws_toggle_toolbar_button { + margin-right: 0; +} + +/************************************ + Capability selector +*************************************/ +select.ws_dropdown { + width: 252px; + height: 20em; + z-index: 1002; + position: absolute; + display: none; + font-family: "Lucida Grande", Verdana, Arial, "Bitstream Vera Sans", sans-serif; + font-size: 12px; +} + +select.ws_dropdown option { + font-family: "Lucida Grande", Verdana, Arial, "Bitstream Vera Sans", sans-serif; + font-size: 12px; + padding: 3px; +} + +select.ws_dropdown optgroup option { + padding-left: 10px; +} + +/************************************ + Tabs (small) + ************************************ + Tabbed navigation for dropdowns and small dialogs. + */ +.ws_tool_tab_nav { + list-style: outside none none; + padding: 0; + margin: 0 0 0 6px; +} +.ws_tool_tab_nav li { + display: inline-block; + border: 1px solid transparent; + border-bottom-width: 0; + padding: 3px 5px 5px; + line-height: 1.35em; + margin-bottom: 0; +} +.ws_tool_tab_nav li.ui-tabs-active { + border-color: #dfdfdf; + border-bottom-color: #FDFDFD; + background: #FDFDFD none; +} +.ws_tool_tab_nav a { + text-decoration: none; +} +.ws_tool_tab_nav li.ui-tabs-active a { + color: #32373C; +} + +.ws_tool_tab { + border-top: 1px solid #DFDFDF; + margin-top: -1px; + background-color: #FDFDFD; +} + +/************************************ + Icon selector +*************************************/ +#ws_icon_selector { + border: 1px solid silver; + border-radius: 3px; + background-color: white; + width: 216px; + padding: 4px 0 0 0; + position: absolute; +} + +#ws_icon_selector.ws_with_more_icons { + width: 570px; +} + +#ws_icon_selector .ws_icon_extra { + display: none; +} + +#ws_icon_selector.ws_with_more_icons .ws_icon_extra { + display: inline-block; +} + +#ws_icon_selector .ws_icon_option { + float: left; + height: 30px; + margin: 2px; + cursor: pointer; + border: 1px solid #bbb; + border-radius: 3px; + /* Gradients and colours cribbed from WP 3.5.1 button styles */ + background: #f3f3f3; + background-image: -webkit-gradient(linear, left top, left bottom, from(#fefefe), to(#f4f4f4)); + background-image: -webkit-linear-gradient(top, #fefefe, #f4f4f4); + background-image: -moz-linear-gradient(top, #fefefe, #f4f4f4); + background-image: -o-linear-gradient(top, #fefefe, #f4f4f4); + background-image: linear-gradient(to bottom, #fefefe, #f4f4f4); +} + +#ws_icon_selector .ws_icon_option:hover { + /* Gradients and colours cribbed from WP 3.5.1 button styles */ + border-color: #999; + background: #f3f3f3; + background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f3f3f3)); + background-image: -webkit-linear-gradient(top, #fff, #f3f3f3); + background-image: -moz-linear-gradient(top, #fff, #f3f3f3); + background-image: -ms-linear-gradient(top, #fff, #f3f3f3); + background-image: -o-linear-gradient(top, #fff, #f3f3f3); + background-image: linear-gradient(to bottom, #fff, #f3f3f3); +} + +#ws_icon_selector .ws_icon_option.ws_selected_icon { + border-color: green; + background-color: #deffca; + background-image: none; +} + +#ws_icon_selector .ws_icon_option .ws_icon_image { + float: none; + margin: 0; + padding: 0; +} +#ws_icon_selector .ws_icon_option .ws_icon_image:before { + color: #85888c; + display: inline-block; +} + +#ws_icon_selector .ws_icon_option .ws_icon_image.dashicons { + width: 20px; + height: 20px; + padding: 5px; +} + +#ws_icon_selector .ws_icon_option img { + display: inline-block; + margin: 0; + padding: 7px; + width: 16px; + height: 16px; +} + +#ws_menu_editor .ws_edit_field-icon_url input.ws_field_value { + width: 220px; + margin-right: 5px; +} + +/* The icon button that displays the pop-up icon selector. */ +#ws_menu_editor .ws_select_icon { + margin: 0; + padding: 0; + position: relative; + box-sizing: border-box; + height: 25px; + min-height: 25px; +} +.ame-is-wp53-plus #ws_menu_editor .ws_select_icon { + height: 28px; + min-height: 28px; + margin-top: 1px; +} + +.ws_select_icon .ws_icon_image { + color: #85888c; + padding: 3px; +} +.ws_select_icon .ws_icon_image.dashicons { + padding: 3px 2px; +} +.ws_select_icon .ws_icon_image.dashicons:before { + width: 20px; +} + +/* Current icon node (image version) */ +.ws_select_icon img { + margin: 0; + padding: 4px; + width: 16px; + height: 16px; +} + +#ws_icon_selector .ws_tool_tab_nav { + display: inline-block; + margin-top: 2px; + position: relative; +} +#ws_icon_selector .ws_tool_tab_nav li { + padding: 4px 10px 11px; +} +#ws_icon_selector .ws_tool_tab { + padding: 4px 4px 2px; + max-height: 324px; + overflow-y: auto; +} + +#ws_choose_icon_from_media { + margin: 2px; +} + +/************************************ + Embedded page selector +*************************************/ +#ws_embedded_page_selector { + width: 254px; + padding: 6px 0 0 0; + border: 1px solid silver; + border-radius: 3px; + background-color: white; + box-sizing: border-box; + position: absolute; +} + +.ws_page_selector_tab_nav { + list-style: outside none none; + padding: 0; + margin: 0 0 0 6px; +} + +.ws_page_selector_tab_nav li { + display: inline-block; + border: 1px solid transparent; + border-bottom-width: 0; + padding: 3px 5px 5px; + line-height: 1.35em; + margin-bottom: 0; +} + +.ws_page_selector_tab_nav a { + text-decoration: none; +} + +.ws_page_selector_tab_nav li.ui-tabs-active { + border-color: #dfdfdf; + background-color: #FDFDFD; + border-bottom-color: #FDFDFD; +} + +.ws_page_selector_tab_nav li.ui-tabs-active a { + color: #32373C; +} + +.ws_page_selector_tab { + border-top: 1px solid #DFDFDF; + padding: 12px; + /* The same padding as post editor boxes. */ + margin-top: -1px; + background-color: #FDFDFD; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; +} + +#ws_current_site_pages { + width: 100%; + min-height: 150px; + max-height: 300px; + margin-left: 0; + margin-right: 0; +} + +#ws_embedded_page_selector input { + box-sizing: border-box; + max-width: 100%; +} + +#ws_custom_embedded_page_tab p:first-child { + margin-top: 0; +} + +/* + Make the "Page" field look editable. It is read-only because the user can't change it directly (they have to use + the dropdown), but we don't want it to be greyed-out. +*/ +#ws_menu_editor .ws_edit_field-embedded_page_id input.ws_field_value { + background-color: white; +} + +/************************************ + Menu color picker +*************************************/ +#ws-ame-menu-color-settings { + background: white; + display: none; +} + +#ame-menu-color-list { + height: 500px; + overflow-y: auto; +} + +.ame-menu-color-column { + min-width: 460px; +} + +.ame-menu-color-name { + display: inline-block; + vertical-align: top; + padding-top: 2px; + line-height: 1.3; + font-size: 14px; + font-weight: 600; + min-width: 180px; +} + +.ame-color-option { + padding: 10px 0; +} +.ame-color-option .wp-picker-container { + display: inline-block; +} + +.ame-advanced-menu-color { + display: none; +} + +#ws-ame-apply-colors-to-all { + display: block; + float: left; + margin-left: 5px; +} + +/* Color presets */ +#ame-color-preset-container { + padding: 0 8px 8px 8px; + margin-left: -8px; + margin-right: -8px; + margin-bottom: 4px; + border-bottom: 1px solid #eee; +} + +#ame-menu-color-presets { + width: 290px; + margin-right: 5px; +} + +#ws-ame-save-color-preset { + /*margin-right: 5px;*/ +} + +a#ws-ame-delete-color-preset { + color: #A00; + text-decoration: none; +} + +a#ws-ame-delete-color-preset:hover { + color: #F00; +} + +/* Color scheme display in the editor widget. */ +.ws_color_scheme_display { + display: inline-block; + box-sizing: border-box; + height: 26px; + width: 190px; + margin-right: 5px; + margin-left: 1px; + padding: 2px 4px; + font-size: 12px; + border: 1px solid #ddd; + background: white; + cursor: pointer; + line-height: 20px; +} +.ame-is-wp53-plus .ws_color_scheme_display { + border-color: #7e8993; + border-radius: 4px; + margin-top: 1px; + margin-bottom: 1px; + padding: 3px 8px; + height: 28px; + line-height: 20px; +} + +.ws_open_color_editor { + width: 58px; +} + +.ws_color_display_item { + display: inline-block; + width: 18px; + height: 18px; + margin-right: 4px; + border: 1px solid #ccc; + border-radius: 3px; +} + +.ws_color_display_item:last-child { + margin-right: 0; +} + +/************************************ + Export and import +*************************************/ +#export_dialog, #import_dialog { + display: none; +} + +.ui-widget-overlay { + background-color: black; + position: fixed; + left: 0; + top: 0; + right: 0; + bottom: 0; + opacity: 0.7; + -moz-opacity: 0.7; + filter: alpha(opacity=70); + width: 100%; + height: 100%; +} + +.ui-front { + z-index: 10000; +} + +.settings_page_menu_editor .ui-dialog { + background: white; + border: 1px solid #c0c0c0; + padding: 0; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} +.settings_page_menu_editor .ui-dialog .ui-dialog-content { + padding: 8px 8px 8px 8px; + font-size: 1.1em; +} +.settings_page_menu_editor .ui-dialog .ame-scrollable-dialog-content { + max-height: 500px; + overflow-y: auto; + padding-top: 0.5em; +} +.settings_page_menu_editor .ui-dialog-titlebar { + display: block; + height: 22px; + margin: 0; + padding: 4px 4px 4px 8px; + background-color: #86A7E3; + font-size: 1em; + line-height: 22px; + -webkit-border-top-left-radius: 4px; + -webkit-border-top-right-radius: 4px; + -moz-border-radius-topleft: 4px; + -moz-border-radius-topright: 4px; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom: 1px solid #809fd9; +} +.settings_page_menu_editor .ui-dialog-title { + color: white; + font-weight: bold; +} +.settings_page_menu_editor .ui-button.ui-dialog-titlebar-close { + background: #86A7E3 url(../images/x.png) no-repeat center; + width: 22px; + height: 22px; + display: block; + float: right; + color: white; + border-radius: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; +} +.settings_page_menu_editor .ui-dialog-titlebar-close:hover { + /*background-image: url(../images/x-light.png);*/ + background-color: #a6c2f5; +} +#export_dialog .ws_dialog_panel { + height: 50px; +} + +#import_dialog .ws_dialog_panel { + height: 64px; +} + +.ws_dialog_panel .ame-fixed-label-text { + display: inline-block; + min-width: 6em; +} +.ws_dialog_panel .ame-inline-select-with-input { + vertical-align: baseline; +} +.ws_dialog_panel .ame-box-side-sizes { + display: flex; + flex-wrap: wrap; + max-width: 800px; +} +.ws_dialog_panel .ame-box-side-sizes .ame-fixed-label-text { + min-width: 4em; +} +.ws_dialog_panel .ame-box-side-sizes label { + margin-right: 2.5em; +} +.ws_dialog_panel .ame-box-side-sizes input { + margin-bottom: 0.4em; +} +.ws_dialog_panel .ame-box-side-sizes input[type=number] { + width: 6em; +} + +.ame-flexbox-break { + flex-basis: 100%; + height: 0; +} + +.ws_dialog_buttons { + text-align: right; + margin-top: 20px; + margin-bottom: 1px; + clear: both; +} + +.ws_dialog_buttons .button-primary { + display: block; + float: left; + margin-top: 0; +} + +.ws_dialog_buttons .button { + margin-top: 0; +} + +.ws_dialog_buttons.ame-vertical-button-list { + text-align: left; +} + +.ws_dialog_buttons.ame-vertical-button-list .button-primary { + float: none; +} + +.ws_dialog_buttons.ame-vertical-button-list .button { + width: 100%; + text-align: left; + margin-bottom: 10px; +} + +.ws_dialog_buttons.ame-vertical-button-list .button:last-child { + margin-bottom: 0; +} + +#import_file_selector { + display: block; + width: 286px; + margin: 6px auto 12px; +} + +#ws_start_import { + min-width: 100px; +} + +#import_complete_notice { + text-align: center; + font-size: large; + padding-top: 25px; +} + +#ws_import_error_response { + width: 100%; +} + +.ws_dont_show_again { + display: inline-block; + margin-top: 1em; +} + +/************************************ + Menu access editor +*************************************/ +/* The launch button */ +#ws_menu_editor .ws_edit_field-access_level input.ws_field_value { + width: 190px; + margin-right: 5px; +} + +.ws_launch_access_editor { + min-width: 40px; + width: 58px; +} + +#ws_menu_access_editor { + width: 400px; + display: none; +} + +.ws_dialog_subpanel { + margin-bottom: 1em; +} +.ws_dialog_subpanel fieldset p { + margin-top: 0; + margin-bottom: 4px; +} + +.ws-ame-dialog-subheading { + display: block; + font-weight: 600; + font-size: 1em; + margin: 0 0 0.2em 0; +} + +#ws_menu_access_editor .ws_column_access, +#ws_menu_access_editor .ws_ext_action_check_column { + text-align: center; + width: 1em; + padding-right: 0; +} + +#ws_menu_access_editor .ws_column_access input, +#ws_menu_access_editor .ws_ext_action_check_column input { + margin-right: 0; +} + +#ws_menu_access_editor .ws_column_role { + white-space: nowrap; +} + +#ws_role_table_body_container { + /*max-height: 400px; + overflow: auto;*/ + overflow: hidden; + margin-right: -1px; +} + +.ws_role_table_body { + margin-top: 2px; + max-width: 354px; +} + +.ws_has_separate_header .ws_role_table_header { + border-bottom: none; + -moz-border-radius-bottomleft: 0; + -moz-border-radius-bottomright: 0; + -webkit-border-bottom-left-radius: 0; + -webkit-border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.ws_has_separate_header .ws_role_table_body { + border-top: none; + margin-top: 0; + -moz-border-radius-topleft: 0; + -moz-border-radius-topright: 0; + -webkit-border-top-left-radius: 0; + -webkit-border-top-right-radius: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.ws_role_id { + display: none; +} + +#ws_extra_capability { + width: 100%; +} + +#ws_role_access_container { + position: relative; + max-height: 430px; + overflow: auto; +} + +#ws_role_access_overlay { + width: 100%; + height: 100%; + position: absolute; + line-height: 100%; + background: white; + filter: alpha(opacity=60); + opacity: 0.6; + -moz-opacity: 0.6; +} + +#ws_role_access_overlay_content { + position: absolute; + width: 50%; + left: 22%; + top: 30%; + background: white; + padding: 8px; + border: 2px solid silver; + border-radius: 5px; + color: #555; +} + +#ws_menu_access_editor div.error { + margin-left: 0; + margin-right: 0; + margin-bottom: 5px; +} + +#ws_hardcoded_role_error { + display: none; +} + +/*--------------------------------------------* + The CPT/taxonomy permissions panel + *--------------------------------------------*/ +/* + * When there are CPT/taxonomy permissions available, the appearance of the role list changes a bit. + */ +.ws_has_extended_permissions { + /* The role or actor whose CPT/taxonomy permissions are currently expanded. */ +} +.ws_has_extended_permissions .ws_role_table_body .ws_column_role { + cursor: pointer; +} +.ws_has_extended_permissions .ws_role_table_body .ws_column_selected_role_tip { + display: table-cell; +} +.ws_has_extended_permissions .ws_role_table_body tr:hover { + background: #EAF2FA; +} +.ws_has_extended_permissions .ws_role_table_body td { + border-top: 1px solid #f1f1f1; +} +.ws_has_extended_permissions .ws_role_table_body tr:first-child td { + border-top-width: 0; +} +.ws_has_extended_permissions .ws_role_table_body tr.ws_cpt_selected_role { + background-color: #dddddd; +} +.ws_has_extended_permissions .ws_role_table_body tr.ws_cpt_selected_role .ws_column_role { + font-weight: bold; +} +.ws_has_extended_permissions .ws_role_table_body tr.ws_cpt_selected_role .ws_cpt_selected_role_tip { + visibility: visible; +} +.ws_has_extended_permissions .ws_role_table_body tr.ws_cpt_selected_role td { + color: #222; +} + +#ws_ext_permissions_container { + float: left; + width: 352px; + padding: 0 9px 0 0; +} + +#ws_ext_permissions_container_caption { + padding-left: 15px; + max-width: 352px; + position: relative; + white-space: nowrap; +} + +#ws_ext_permissions_container .ws_ext_permissions_table { + margin-top: 2px; +} +#ws_ext_permissions_container .ws_ext_permissions_table tr td:first-child { + padding-left: 15px; +} +#ws_ext_permissions_container .ws_ext_permissions_table .ws_ext_group_title { + padding-bottom: 0; + font-weight: bold; +} +#ws_ext_permissions_container .ws_ext_permissions_table .ws_ext_action_check_column, +#ws_ext_permissions_container .ws_ext_permissions_table .ws_ext_action_name_column { + padding-top: 3px; + padding-bottom: 3px; +} +#ws_ext_permissions_container .ws_ext_permissions_table tr.ws_ext_padding_row td { + padding: 0 0 0 0; + height: 1px; +} +#ws_ext_permissions_container .ws_ext_permissions_table .ws_same_as_required_cap { + text-decoration: underline; +} +#ws_ext_permissions_container .ws_ext_permissions_table .ws_ext_has_custom_setting label.ws_ext_action_name::after { + content: " *"; +} + +#ws_ext_permissions_container #ws_ext_toggle_capability_names { + cursor: pointer; + position: absolute; + right: 0; + color: #0073aa; +} +#ws_ext_permissions_container.ws_ext_readable_names_enabled #ws_ext_toggle_capability_names { + color: #b4b9be; +} +#ws_ext_permissions_container .ws_ext_readable_name { + display: none; +} +#ws_ext_permissions_container .ws_ext_capability { + display: inline; +} +#ws_ext_permissions_container.ws_ext_readable_names_enabled .ws_ext_readable_name { + display: inline; +} +#ws_ext_permissions_container.ws_ext_readable_names_enabled .ws_ext_capability { + display: none; +} + +#ws_ext_permissions_container #ws_taxonomy_permissions_table tr:first-child td { + padding-top: 8px; +} + +/* The "selected role" indicator. */ +.ws_cpt_selected_role_tip { + display: block; + visibility: hidden; + box-sizing: border-box; + width: 26px; + height: 26px; + position: absolute; + right: 0; + background: white; + transform: translate(1px, 0) rotate(-45deg); + transform-origin: top right; +} + +.ws_role_table_body .ws_column_selected_role_tip { + display: none; + padding: 0; + width: 40px; + height: 100%; + text-align: right; + overflow: visible; + position: relative; + cursor: pointer; +} + +.ws_ame_breadcrumb_separator { + color: #999; +} + +#ws_menu_editor .ws_ext_permissions_indicator { + font-size: 16px; + height: 16px; + width: 16px; + visibility: hidden; + vertical-align: bottom; + cursor: pointer; + color: #4aa100; +} + +#ws_menu_editor.ws_is_actor_view .ws_ext_permissions_indicator { + visibility: visible; +} + +/************************************ + Visible users dialog +*************************************/ +#ws_visible_users_dialog { + background: white; + padding: 8px; +} + +#ws_user_selection_panels { + min-width: 710px; +} +#ws_user_selection_panels .ws_user_selection_panel { + display: block; + float: left; + position: relative; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + width: 350px; + height: 400px; + border: 1px solid #e5e5e5; + margin-right: 10px; + padding: 10px; +} +#ws_user_selection_panels #ws_user_selection_target_panel { + margin-right: 0; +} +#ws_user_selection_panels #ws_available_user_query { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + width: 100%; + max-height: 28px; +} +#ws_user_selection_panels .ws_user_list_wrapper { + position: absolute; + top: 50px; + left: 10px; + right: 10px; + height: 338px; + overflow-x: auto; + overflow-y: auto; +} +#ws_user_selection_panels .ws_user_selection_list { + min-height: 20px; + border-width: 0; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} +#ws_user_selection_panels .ws_user_selection_list .ws_user_action_column { + width: 20px; + text-align: center; + padding-top: 9px; + padding-bottom: 0; +} +#ws_user_selection_panels .ws_user_selection_list .ws_user_action_button { + cursor: pointer; + color: #b4b9be; +} +#ws_user_selection_panels .ws_user_selection_list .ws_user_username_column { + padding-left: 0; +} +#ws_user_selection_panels .ws_user_selection_list .ws_user_display_name_column { + white-space: nowrap; +} +#ws_user_selection_panels #ws_available_users tr { + cursor: pointer; +} +#ws_user_selection_panels #ws_available_users tr:hover, #ws_user_selection_panels #ws_available_users tr.ws_user_best_match { + background-color: #eaf2fa; +} +#ws_user_selection_panels #ws_available_users tr:hover .ws_user_action_button { + color: #7ad03a; +} +#ws_user_selection_panels #ws_selected_users .ws_user_action_button::before { + content: "\f158"; +} +#ws_user_selection_panels #ws_selected_users .ws_user_action_button:hover { + color: #dd3d36; +} +#ws_user_selection_panels #ws_selected_users .ws_user_action_column { + padding-left: 6px; +} +#ws_user_selection_panels #ws_selected_users .ws_user_display_name_column { + display: none; +} +#ws_user_selection_panels #ws_selected_users tr.ws_user_must_be_selected .ws_user_action_button { + display: none; +} +#ws_user_selection_panels #ws_selected_users_caption { + font-size: 14px; + line-height: 1.4em; + padding: 7px 10px; + color: #555; + font-weight: 600; +} +#ws_user_selection_panels::after { + display: block; + height: 1px; + visibility: hidden; + content: " "; + clear: both; +} + +#ws_loading_users_indicator { + position: absolute; + right: 10px; + bottom: 10px; + margin-right: 0; + margin-bottom: 0; +} + +/************************************ + Menu deletion error +*************************************/ +#ws-ame-menu-deletion-error { + max-width: 400px; +} + +/************************************ + Tooltips and hints +*************************************/ +.ws_tooltip_trigger, .ws_field_tooltip_trigger { + cursor: pointer; +} + +.ws_tooltip_content_list { + list-style: disc; + margin-left: 1em; + margin-bottom: 0; +} + +.ws_tooltip_node { + font-size: 13px; + line-height: 1.3; + border-radius: 3px; + max-width: 300px; +} + +.ws_field_tooltip_trigger .dashicons { + font-size: 16px; + height: 16px; + vertical-align: bottom; +} + +.ws_field_tooltip_trigger { + color: #a1a1a1; +} + +#ws_plugin_settings_form .ws_tooltip_trigger .dashicons { + font-size: 18px; +} + +.ws_ame_custom_postbox .ws_tooltip_trigger .dashicons { + font-size: 18px; + height: 18px; + vertical-align: bottom; +} + +.ws_tooltip_trigger.ame-warning-tooltip { + color: orange; +} + +.ws_wide_tooltip { + max-width: 450px; +} + +.ws_hint { + background: #FFFFE0; + border: 1px solid #E6DB55; + margin-bottom: 0.5em; + border-radius: 3px; + position: relative; + padding-right: 20px; +} + +.ws_hint_close { + border: 1px solid #E6DB55; + border-right: none; + border-top: none; + color: #dcc500; + font-weight: bold; + cursor: pointer; + width: 18px; + text-align: center; + border-radius: 3px; + position: absolute; + right: 0; + top: 0; +} + +.ws_hint_close:hover { + background-color: #ffef4c; + border-color: #e0b900; + color: black; +} + +.ws_hint_content { + padding: 0.4em 0 0.4em 0.4em; +} + +.ws_hint_content ul { + list-style: disc; + list-style-position: inside; + margin-left: 0.5em; +} + +.ws_ame_doc_box .hndle, .ws_ame_custom_postbox .hndle { + cursor: default !important; + border-bottom: 1px solid #ccd0d4; +} +.ws_ame_doc_box .handlediv, .ws_ame_custom_postbox .handlediv { + display: block; + float: right; +} +.ws_ame_doc_box .inside, .ws_ame_custom_postbox .inside { + margin-bottom: 0; +} +.ws_ame_doc_box ul, .ws_ame_custom_postbox ul { + list-style: disc outside; + margin-left: 1em; +} +.ws_ame_doc_box li > ul, .ws_ame_custom_postbox li > ul { + margin-top: 6px; +} +.ws_ame_doc_box .button-link .toggle-indicator::before, .ws_ame_custom_postbox .button-link .toggle-indicator::before { + margin-top: 4px; + width: 20px; + -webkit-border-radius: 50%; + border-radius: 50%; + text-indent: -1px; + content: "\f142"; + display: inline-block; + font: normal 20px/1 dashicons; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-decoration: none !important; +} +.ws_ame_doc_box.closed .button-link .toggle-indicator::before, .ws_ame_custom_postbox.closed .button-link .toggle-indicator::before { + content: "\f140"; +} + +.ws_basic_container .ws_ame_custom_postbox { + margin-left: 2px; + margin-right: 2px; +} + +.ws_ame_custom_postbox .ame-tutorial-list { + margin: 0; +} +.ws_ame_custom_postbox .ame-tutorial-list a { + text-decoration: none; + display: block; + padding: 4px; +} +.ws_ame_custom_postbox .ame-tutorial-list ul { + margin-left: 1em; +} +.ws_ame_custom_postbox .ame-tutorial-list li { + display: block; + margin: 0; + list-style: none; +} + +/************************************ + Copy Permissions dialog +*************************************/ +#ws-ame-copy-permissions-dialog select { + min-width: 280px; +} + +/********************************************* + Capability suggestions and preview +**********************************************/ +#ws_capability_suggestions { + padding: 4px; + width: 350px; + border: 1px solid #cdd5d5; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); + background: #fff; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} +#ws_capability_suggestions #ws_previewed_caps { + margin-top: 0; + margin-bottom: 6px; +} +#ws_capability_suggestions td, #ws_capability_suggestions th { + padding-top: 3px; + padding-bottom: 3px; +} +#ws_capability_suggestions tr.ws_preview_has_access .ws_ame_role_name { + background-color: lightgreen; +} +#ws_capability_suggestions .ws_ame_suggested_capability { + cursor: pointer; +} +#ws_capability_suggestions .ws_ame_suggested_capability:hover { + background-color: #d0f2d0; +} + +/********************************************* + Settings page stuff +**********************************************/ +#ws_plugin_settings_form figure { + margin-left: 0; + margin-top: 0; + margin-bottom: 1em; +} + +.ame-available-add-ons tr:first-of-type td { + margin-top: 0; + padding-top: 0; +} +.ame-available-add-ons td { + padding-top: 10px; + padding-bottom: 10px; +} +.ame-available-add-ons .ame-add-on-heading { + padding-left: 0; +} + +.ame-add-on-name { + font-weight: 600; +} + +.ame-add-on-details-link::after { + /*content: " \f504"; + font-family: dashicons, sans-serif;*/ +} + +/********************************************* + WordPress 5.3+ consistent styles +**********************************************/ +.ame-is-wp53-plus .ws_edit_field input[type=button] { + margin-top: 1px; +} + +/********************************************* + CSS border style selector +**********************************************/ +.ame-css-border-styles .ame-fixed-label-text { + min-width: 5em; +} +.ame-css-border-styles .ame-border-sample-container { + display: inline-block; + vertical-align: top; + min-height: 28px; +} +.ame-css-border-styles .ame-border-sample { + display: inline-block; + width: 14em; + border-top: 0.3em solid #444; +} + +/********************************************* + Miscellaneous +**********************************************/ +#ws_sidebar_pro_ad { + min-width: 225px; + margin-top: 5px; + margin-left: 3px; + position: fixed; + right: 20px; + bottom: 40px; + z-index: 100; +} + +.ws-ame-icon-radio-button-group > label { + display: inline-block; + padding: 8px; + border: 1px solid #ccd0d4; + border-radius: 2px; + margin-right: 0.5em; +} + +span.description { + color: #666; + font-style: italic; +} + +.wrap :target { + background-color: rgba(255, 230, 81, 0.7); +} + +.test-wrap { + background-color: #444444; + padding: 30px; +} + +.test-container { + width: 400px; + height: 200px; + background-color: white; + border: 1px solid black; + border-radius: 10px; + overflow: hidden; +} + +.test-header { + background-color: #67d6ff; + padding: 6px; + border-top-left-radius: 8px; + border-top-right-radius: 8px; +} + +.test-content { + padding: 8px; +} + +/********************************************* + "Test access" dialog +**********************************************/ +#ws_ame_test_access_screen { + display: none; + background: #fcfcfc; +} + +#ws_ame_test_inputs { + padding-bottom: 16px; +} + +.ws_ame_test_input { + display: block; + float: left; + width: 100%; + margin: 2px 0; + box-sizing: content-box; +} + +.ws_ame_test_input_name { + display: block; + float: left; + width: 35%; + margin-right: 4%; + text-align: right; + padding-top: 6px; + line-height: 16px; +} + +.ws_ame_test_input_value { + display: block; + float: right; + width: 60%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +#ws_ame_test_actions { + float: left; + width: 100%; + margin-top: 1em; +} + +#ws_ame_test_button_container { + width: 35%; + margin-right: 4%; + float: left; + text-align: right; +} + +#ws_ame_test_progress { + display: none; + width: 60%; + float: right; +} +#ws_ame_test_progress .spinner { + float: none; + vertical-align: bottom; + margin-left: 0; + margin-right: 4px; +} + +#ws_ame_test_access_body { + width: 100%; + position: relative; + border: 1px solid #ddd; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +#ws_ame_test_frame_container { + margin-right: 250px; + background: white; + min-height: 500px; + position: relative; +} + +#ws_ame_test_access_frame { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + width: 100%; + height: 100%; + min-height: 500px; + border: none; + margin: 0; + padding: 0; +} + +#ws_ame_test_access_sidebar { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 250px; + padding: 16px 24px; + background-color: #f3f3f3; + border-left: 1px solid #ddd; +} +#ws_ame_test_access_sidebar h4:first-of-type { + margin-top: 0; +} + +#ws_ame_test_frame_placeholder { + display: block; + padding: 16px 24px; +} + +#ws_ame_test_output { + display: none; +} + +/*************************************** + Tabs on the settings page + ***************************************/ +.wrap.ws-ame-too-many-tabs .ws-ame-nav-tab-list.nav-tab-wrapper { + border-bottom-color: transparent; +} +.wrap.ws-ame-too-many-tabs .ws-ame-nav-tab-list .nav-tab { + border-bottom: 1px solid #c3c4c7; + margin-bottom: 10px; + margin-top: 0; +} + +/* Spacing between the page heading and the tab list. + +Normally, this is handled by .nav-tab styles, but WordPress changes the margins at smaller screen sizes +and the tabs end up without a left margin. Let's put that margin on the heading instead and remove it +from the first tab. */ +#ws_ame_editor_heading { + margin-right: 0.305em; +} + +.ws-ame-nav-tab-list a.nav-tab:first-of-type { + margin-left: 0; +} + +/* When in "too many tabs" mode, there's too much space between the bottom of the tab list and the rest +of the page. I haven't found a good way to change the margins of just the last row, so here's a partial fix. */ +.ws-ame-too-many-tabs #ws_actor_selector { + margin-top: 0; +} + +/*# sourceMappingURL=menu-editor.css.map */ diff --git a/css/menu-editor.css.map b/css/menu-editor.css.map new file mode 100644 index 0000000..1b905b9 --- /dev/null +++ b/css/menu-editor.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["menu-editor.scss","_input-group.scss","_indeterminate-checkbox.scss","_test-access-screen.scss","_main-tabs.scss"],"names":[],"mappings":"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AAA2B;EAC3B;;;AAGD;AACA;EACC;;;AAGD;AACA;EACC;EACA;EACA;EAEA;AAA2B;EAC3B;;;AAGD;AAMA;AAAA;AAAA;AAIA;EACC;EAEA;EACA;EACA;EAEA;EACA;;;AASD;EACC;EACA;EACA;EAEA;EACA;EAEA;EACA;EACA;EAEA;EACA;EAEA;EACA;EACA;;AAEA;EACC;;;AAIF;EACC;EACA;;;AAID;EACC;;;AAED;EACC;EACA;;;AAED;EACC;EACA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAGD;AAAA;AAAA;AAIA;EACC;EACA;EAEA;EACA;EACA;EAEA;EACA;;;AAID;EACC;EACA;EACA,SAJ0B;;;AAO3B;EACC;;;AAGD;AAAA;AAAA;AAAA;AAAA;AAQA;EACC;EACA;EACA;;AAEA;EACC;EAEA;EACA;EACA;EACA;EAEA;;AAGD;EACC;EACA,qBApBwB;EAqBxB;;AAGD;EACC;;AAGD;EACC;;;AAIF;EACC;EACA;EACA,kBApCyB;;;AAyC1B;AAAA;AAAA;AAMA;EACC;EACA;EACA;EAEA;EACA;EACG;;;AAGJ;EACI;;;AAGJ;EACI;;;AAGJ;EACI;;;AAIJ;EACC;EACA;EAEA;EACA;EACA;EACA;AAEA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;AACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;;AAEA;EACC,OAnEc;EAoEd;;;AAIF;EACI;EACA;EACA;;;AAGJ;EACC;EACA;EACA;EAEA;EACA;;;AAGD;EACC;EACA;;;AAGD;AACA;EACC;EACA;EACA;EAEG;EACA;EACH;;AAEA;EACC,QA/dqB;EAgerB,YAheqB;EAierB;;;AAIF;EACC,OA9Ge;EA+Gf;;AAEA;EACC;;AAEA;EACC;;;AAKH;AACA;EACC;EACA;EACA;EACA;;;AAOA;EACC;EACA;EAMA;;AAJA;EACC;;AAMF;EACC;EACA;EACA;;;AAIF;EACC;;;AAGD;AAAA;AAAA;AAIA;EACC;EACA;EAEA;EACA;EACA;EAEA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EAEA;EACA;EACA;EACA;EAEA;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;AAAe;EACf;EACA;EAEA;EACA;;;AAGD;EACC;EACA;EACA;EAEA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AAGD;AAAA;AAAA;AAAA;AAIA;EACC;;;AAID;AAAA;AAAA;AAIA;EACI;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;EAEA;EACA;EACA;EAEA;;;AAGJ;EACI;;AAEH;EACC;;;AAIF;EACI;;;AAGJ;EACC;EACA;EACA;;;AAGD;AACA;EACC;EAEA;EACA;EACA;EAEA;;;AAGD;EACC;EACA;;;AAGD;AACC;;;AAGD;EACC;EACA;;;AAED;EACC;;;AAGD;AAKA;EAGI;EACH;EACG,QAJgB;EAKhB,OATc;EAWd,cAVoB;EAWvB;EACG;EAEH;EACG;EACA;EACA;EAEH;;AAEA;EACC;EACA;EAEA;EACA;EAEA;EACA;EACA;;;AAIF;EACC;;;AAGD;EACI;EACA;EACA;EAEA;EACA;EACA;;;AAGJ;EACI;;;AAGJ;AAAA;AAAA;AAIA;EACC;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEG;EACA;;;AAGJ;EACI;;;AAIH;EACC;EACA;EAEA;EAEA;EACA;EACA;;AAEA;EACC;EACA;;AAGD;EACC;EACA;EACA;;AAIF;EACC;EACA;EACA;EACA;EAEA;EACA;EACA;EAEA;EACA;EAEA;EACA;EAEA;EACA;EAEA;;AAGD;EACC;EACA;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;;AAGD;AACC;EACA;;AAQF;EACC;;;AAGD;EACC;;;AAIA;EACC;EACA;;AAGD;EACC;;AAGD;EACC;EACA;EACA;;AAEA;EACC;;AAGD;EACC;;AAGD;EACC;;AAGD;EACC;;;AAKH;EACC;EACA;;;AAGD;EACC;EACA;EACG;EACA;;;AAGJ;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACI;;;AAGJ;EACI;;;AAGJ;EACI;EACA;EACA;;;AAGJ;EACI;;;AAGJ;EACC;EACA;EAEA;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACI;EACA;;;AAGJ;AAAA;AAAA;AAIA;AAGA;EAEC,OAJuB;EAKvB,cAJwB;;;AAOzB;EACC;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;;AAEA;EACC;EACA;;;AAIF;EACC;EACA;EACA;EACA;;;AAGD;AAAA;EAEC;EACA;EACA;;;AAGD;AAAA;EAEC;;;AAGD;EACC;;;AAGD;AACC;AAAA;EAEA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EAEA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EAEA;EAEA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EAEA;EACA;EAEA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;AAAA;AAAA;AAIA;AAAA;AAAA;AAGA;AAuBC;;AArBA;EACC;;AAGD;EACC;;AAGD;EACC;;AAIA;EACC;;AAED;EACC;;AAKF;EACC;;AAEA;EACC;;AAGD;EACC;;AAGD;EACC;;;AAKH;EACC;EACA;EACA;;;AAKD;EACC,cAH0B;EAI1B;EACA;EACA;;;AAGD;EACC;;AAEA;EACC,cAbyB;;AAgB1B;EACC;EACA;;AAGD;AAAA;EAEC;EACA;;AAGD;EACC;EACA;;AAGD;EACC;;AAIA;EACC;;;AAQF;EACC;EACA;EACA;EACA;;AAGD;EACC;;AAID;EACC;;AAED;EACC;;AAKA;EACC;;AAED;EACC;;;AAQF;EACC;;;AAIF;AACA;EACC;EACA;EAEA;EACA;EACA;EAEA;EACA;EAGA;EAEA;EACA;;;AAGD;EACC;EAEA;EACA;EACA;EACA;EACA;EAEA;EAEA;;;AAGD;EACC;;;AAID;EAGC,WAFgB;EAGhB,QAHgB;EAIhB,OAJgB;EAOhB;EAEA;EACA;EACA;;;AAGD;EACC;;;AAGD;AAAA;AAAA;AAQA;EAEC;EACA,SAFoB;;;AAKrB;EACC;;AAEA;EACC;EACA;EACA;EAEA;EACA;EACA;EAEA,OAtBwB;EAuBxB,QAtByB;EAwBzB;EACA;EACA;;AAGD;EACC;;AAGD;EACC;EACA;EACA;EACA;EACA;;AAGD;EACC;EAGA,KAFc;EAGd,MA7C0B;EA8C1B,OA9C0B;EAiD1B;EAEA;EACA;;AAGD;EACC;EAGA;EACA;EACA;EACA;;AAEA;EACC;EACA;EAEA;EACA;;AAGD;EACC;EACA;;AAGD;EACC;;AAGD;EACC;;AAKD;EACC;;AAGD;EACC;;AAID;EACC;;AAMD;EACC;;AAED;EACC;;AAED;EACC;;AAGD;EACC;;AAKA;EACC;;AAaH;EACC;EACA;EACA;EAEA;EACA;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAIF;EACC;EACA,OAzJ2B;EA0J3B,QA1J2B;EA4J3B;EACA;;;AAKD;AAAA;AAAA;AAIA;EACI;;;AAMJ;AAAA;AAAA;AAIA;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAID;EACC;;;AAID;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EAEA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EAEA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAIA;EACC;EACA;;AAGD;EACC;EACA;;AAGD;EACC;;AAGD;EACC;EACA;;AAGD;EACC;;AAGD;EACC;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EAEA;EACA;EACA;;AAGD;EACC;;;AAIF;EAEC;EACA;;;AAGD;EACC;;AAEA;EACC;EACA;EACA;;AAGD;EACC;;AAGD;EACC;EACA;EACA;;;AAIF;AAAA;AAAA;AAGA;EACC;;;AAGD;AAAA;AAAA;AAIA;EACC;EACA;EAEA;EACA;EACA;EAEA;EACA;;AAEA;EACC;EACA;;AAGD;EAEC,aAr0CyB;EAs0CzB,gBAt0CyB;;AAy0C1B;EACC;;AAGD;EACC;;AAEA;EACC;;;AAKH;AAAA;AAAA;AAIA;EACC;EACA;EACA;;;AAIA;EACC;EACA;;AAGD;EACC;EACA;;AAGD;EACC;;;AAIF;EACC;;;AAGD;AACC;AAAA;;;AAID;AAAA;AAAA;AAIA;EACC;;;AAGD;AAAA;AAAA;AAKC;EACC;;AAGD;EACC;EACA;EACA;;AAGD;EACC;EACA;EACA;;;AAIF;AAAA;AAAA;AAIA;EACI;EAEH;EACA;EAEA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EAEA;;;AAGD;EACC;EAEA;;;AAGD;EACC;;;AAGD;EACI;EACA;;;AAGJ;EACI;EACA;EACA;EAEA;EACA;EAEA;;;AAGJ;EACI;EACA;EAEA;EACA;;;AAGJ;EACI;;;AGlsEJ;AAAA;AAAA;AAIA;EACC;EACA;;;AAGD;EAEC;;;AAGD;EACC;EACA;EAEA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EAEA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EAEA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;AAEA;EACC;EACA;EACA;EACA;;;AAIF;EACC;EACA;EAEA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EAEA;EACA;;;AAGD;EACC;EACA;EACA;EAEA;EACA;EACA;EAEA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EAEA;EACA;EACA;EACA;EAEA;EACA;EAEA;EACA;;AAEA;EACC;;;AAIF;EACC;EACA;;;AAGD;EACC;;;ACjID;AAAA;AAAA;AAKC;EACC;;AAGD;EACC;EACA;EACA;;;AAIF;;AAAA;AAAA;AAAA;AAMA;EACC;;;AAIA;EACC;;;AAIF;AAAA;AAEA;EACC","file":"menu-editor.css"} \ No newline at end of file diff --git a/css/menu-editor.scss b/css/menu-editor.scss new file mode 100644 index 0000000..916da07 --- /dev/null +++ b/css/menu-editor.scss @@ -0,0 +1,2248 @@ +/* Admin Menu Editor CSS file */ + +@import "boxes"; +@import "input-group"; + +#ws_menu_editor { + min-width: 780px; +} + +.ame-is-free-version #ws_menu_editor { + margin-top: 9px; +} + +$mainContainerWidth: 316px; +$mainContainerBorderWidth: 1px; +$mainContainerBorderRadius: 0px; +$mainContainerBorderColor: $amePostboxBorderColor; //Was #cdd5d5 before WP 5.3. + +.ws_main_container { + margin: 2px; + width: $mainContainerWidth; + float: left; + display:block; + + border: $mainContainerBorderWidth solid $mainContainerBorderColor; + box-shadow: 0 1px 1px rgba(0,0,0,0.04); + background-color: #FFFFFF; + + border-radius: $mainContainerBorderRadius; + -moz-border-radius: $mainContainerBorderRadius; + -webkit-border-radius: $mainContainerBorderRadius; +} + +.ws_box { + min-height: 30px; + width: 100%; + margin: 0; +} + +.ws_basic_container { + float: left; + display:block; +} + +#ws_menu_box { +} + +#ws_submenu_box { +} + +.ws_dropzone { + display: block; + box-sizing: border-box; + + margin: 2px 6px; + border: 3px none #b4b9be; + + height: 31px; +} + +.ws_dropzone_active, +.ws_dropzone_hover, +.ws_top_to_submenu_drop_hover .ws_dropzone { + border-style: dashed; +} + +.ws_dropzone_hover, +.ws_top_to_submenu_drop_hover .ws_dropzone { + border-width: 1px; +} + +/************************************************* + Actor UI + *************************************************/ +#ws_actor_selector li:after { + content: '| '; +} + +#ws_actor_selector li:last-child:after { + content: ''; +} + +#ws_actor_selector li { + a { + display: inline-block; + text-align: center; + } + + a::before { + display: block; + content: attr(data-text); + font-weight: bold; + + height: 1px; + overflow: hidden; + visibility: hidden; + margin-bottom: -1px; + } +} + +#ws_actor_selector { + margin-top: 6px; +} + +/** + * The checkbox that lets the user show/hide a menu for the currently selected actor. + */ +@import "_indeterminate-checkbox.scss"; + +#ws_menu_editor .ws_actor_access_checkbox, +#ws_menu_editor input[type="checkbox"].ws_actor_access_checkbox /* Ensure we override WP defaults. */ +{ + margin-right: 2px; + margin-left: 2px; + margin-top: 1px; + vertical-align: text-top; + + @include ame-indeterminate-checkbox; +} + +@media screen and (max-width: 782px) { + #ws_menu_editor input[type="checkbox"].ws_actor_access_checkbox { + &:indeterminate:before { + margin: -6px 0 0 1px; + font: 400 26px/1 dashicons; + } + } +} + +/* The checkbox is only visible when viewing the menu configuration for a specific actor. */ +#ws_menu_editor .ws_actor_access_checkbox { + display: none; +} + +#ws_menu_editor.ws_is_actor_view .ws_actor_access_checkbox { + display: inline-block; +} + +/* Gray-out items inaccessible to the currently selected actor */ + +.ws_is_actor_view .ws_container.ws_is_hidden_for_actor { + background-color: #F9F9F9; +} + +.ws_is_actor_view .ws_is_hidden_for_actor .ws_item_title { + color: #777; +} + +/* + * The sidebar + */ + +#ws_editor_sidebar { + width: auto; + padding: 2px; +} + +#ws_menu_editor .ws_main_button { + clear: both; + display: block; + margin: 4px; + width: 130px; +} + +#ws_menu_editor #ws_save_menu { + margin-bottom: 20px; +} + +#ws_menu_editor #ws_toggle_editor_layout { + display: none; +} + +#ws_menu_editor .ws_sidebar_button_separator { + display: block; + height: 4px; + margin: 0; + padding: 0; +} + +/* + * Page heading and tabs + */ + +#ws_ame_editor_heading { + float: left; +} + +/* + * Menu components and widgets + */ + +.ws_container { + $itemWidth: $mainContainerWidth - 20px; + $itemPadding: 3px; + $itemBorderWidth: 1px; + $itemHorizontalMargin: ($mainContainerWidth - $itemWidth - $itemPadding * 2 - $itemBorderWidth * 2) / 2; + + display: block; + width: $itemWidth; + + padding : $itemPadding; + margin: 2px 0 2px $itemHorizontalMargin; + + body.rtl & { + margin-right: $itemHorizontalMargin; + margin-left: 0; + } +} + +.ws_active { } + +.ws_menu { } +.ws_item { } + +.ws_menu_separator { } + +.ws_submenu { + min-height: 2em; +} + + +.ws_item_head { + padding: 0; +} + +.ws_item_title { + display: inline-block; + padding: 2px; + cursor: default; + + font-size: 13px; + line-height: 18px; +} + +.ws_edit_link { + float: right; + margin-right: 0; + cursor: pointer; + display:block; + width: 40px; + height: 22px; + + border-radius: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + + text-decoration: none; +} + +.ws_edit_link_expanded { } + + +.ws_menu_drop_hover { + background-color: #43b529 !important; +} + +.ws_container.ui-sortable-helper * { + cursor: move !important; +} + +.ws_container.ws_sortable_placeholder { + outline: 1px dashed #b4b9be; + outline-offset: -1px; + background: none; + border-color: transparent; +} + +/* + If you ever want to apply a right-arrow style to the currently selected menu item, + you can do it like this. Commented out for now since it doesn't look all that great, + but might be useful in the future. +*/ +/* +.ws_container { + position: relative; +} + +.ws_menu.ws_active::after { + content: ""; + display: block; + z-index: 1002; + + border-left: 14px solid #8EB0F1; + border-top: 15px solid rgba(255, 255, 255, 0.1); + border-bottom: 15px solid rgba(255, 255, 255, 0.1); + background: transparent; + + position: absolute; + right: -14px; + top: -1px; + + width: 0; + height: 0; +} +*/ + +/* + * A left-arrow style alternative. This one is image-based and doesn't suffer from the finicky sizing issues + * of CSS triangles. + */ + +.ws_container { + position: relative; +} + +.ws_menu.ws_active::after { + //These should match the background image size. + $submenuTipHeight: 30px; + $submenuTipWidth: 19px; + + content: ""; + display: block; + //z-index: 1002; + + position: absolute; + right: -$submenuTipWidth; + top: -1px; + + width: $submenuTipWidth; + height: $submenuTipHeight; + background: transparent url("../images/submenu-tip.png") no-repeat center; +} + +//No arrows for separators and items that are being dragged. +.ws_container.ws_menu_separator.ws_active::after, +.ws_container.ui-sortable-helper::after { + background-image: none; +} + +/**************************************** + Per-menu settings fields & panels +*****************************************/ + +.ws_editbox { + display: block; + padding: 4px; + + border-radius: 2px; + border-top-right-radius: 0; + + -moz-border-radius: 2px; + -moz-border-radius-topright: 0; + + -webkit-border-radius: 2px; + -webkit-border-top-right-radius: 0; +} + +.ws_edit_panel { + margin: 0; + padding: 0; + border: none; +} + +.ws_edit_field { + margin-bottom: 6px; + min-height: 45px; + + //Clear-fix. + &:after { + visibility: hidden; + display: block; + height: 0; + font-size: 0; + + content: " "; + clear: both; + } +} + +.ws_edit_field-custom { + margin-top: 10px; +} + +.ws_edit_field.ws_no_field_caption { + margin-top: 10px; + padding-left: 1px; + height: 25px; + min-height: 25px; +} + +/* + * Group headings + */ +.ws_edit_field.ws_field_group_heading { + //display: none; + height: 1px; + min-height: 0; + padding-top: 0; + + background: #ccc; + margin: 8px -4px 5px; + + & span { + display: none; + font-weight: bold; + } +} + +/* The reset-to-default button */ +.ws_reset_button { + display: block; + float: right; + + margin-left: 4px; + margin-top: 2px; + margin-right: 6px; + cursor: pointer; + + width: 16px; + height: 16px; + vertical-align: top; + + background: url("../images/pencil_delete_gray.png") no-repeat center; + + .ame-is-wp53-plus & { + margin-top: 5px; + } +} + +.ws_reset_button:hover { + background-image: url("../images/pencil_delete.png"); +} + +.ws_input_default input, +.ws_input_default select, +.ws_input_default .ws_color_scheme_display { + color: gray; +} + +/* No reset button for fields set to the default value and fields without a default value */ +.ws_input_default .ws_reset_button, +.ws_has_no_default .ws_reset_button { + visibility: hidden; +} + +/* The input box in each field editor */ +$basicInputWidth: 254px; +$basicInputWp53Height: 28px; + +#ws_menu_editor .ws_editbox input[type="text"], +#ws_menu_editor .ws_editbox select { + display: block; + float: left; + width: $basicInputWidth; + height: 25px; + + font-size: 12px; + line-height: 17px; + + padding-top: 3px; + padding-bottom: 3px; + + .ame-is-wp53-plus & { + height: $basicInputWp53Height; + margin-top: 1px; + } +} + +#ws_menu_editor .ws_edit_field label { + display: block; + float: left; +} + +#ws_menu_editor .ws_edit_field-custom input[type="checkbox"] { + margin-top: 0; +} + +#ws_menu_editor input[type="text"].ws_field_value { + min-height: 25px; + + .ame-is-wp53-plus & { + min-height: $basicInputWp53Height; + } +} + +/* Dropdown button for combo-box fields */ +$dropdownButtonWidth: 25px; + +#ws_menu_editor .ws_dropdown_button, +#ws_menu_access_editor .ws_dropdown_button +{ + box-sizing: border-box; + width: $dropdownButtonWidth; + height: 25px; + min-height: 25px; + + margin: 1px 1px 1px 0; + padding: 0 1px 0 0; + + text-align: center; + + font-family: dashicons; + font-size: 16px !important; + line-height: 25px; + + border-color: #dfdfdf; + box-shadow: none; + + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.ame-is-wp53-plus #ws_menu_editor .ws_dropdown_button, +#ws_menu_access_editor.ame-is-wp53-plus .ws_dropdown_button +{ + height: $basicInputWp53Height; + + border-color: #7e8993; + background-color: white; + border-left-style: none; + + font-size: 16px !important; + line-height: 24px; + color: #555; + + &:hover { + color: #23282d; + } +} + +#ws_menu_access_editor .ws_dropdown_button { + display: inline-block; + height: 27px; +} + +#ws_menu_access_editor.ame-is-wp53-plus .ws_dropdown_button { + height: 30px; +} + +#ws_menu_editor .ws_dropdown_button { + display: block; + float: left; +} + +/* +The appearance and size of combo-box fields need to be changed +to accommodate the drop-down button. +*/ +#ws_menu_editor .ws_has_dropdown input.ws_field_value, +#ws_menu_access_editor input.ws_has_dropdown +{ + margin-right: 0; + border-right: 0; + + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +#ws_menu_access_editor input.ws_has_dropdown { + width: 90%; + box-sizing: border-box; + height: 27px; + margin-top: 1px; +} + +#ws_menu_access_editor.ame-is-wp53-plus input.ws_has_dropdown { + height: 30px; +} + +#ws_menu_editor .ws_has_dropdown input.ws_field_value { + width: $basicInputWidth - $dropdownButtonWidth; +} + +/* Unlike others, this field is just a single checkbox, so it has a smaller height */ +#ws_menu_editor .ws_edit_field-custom { + height: 16px; +} + +/* + * "Show/hide advanced fields" + */ +.ws_toggle_container { + text-align: right; + margin-right: 27px; +} + +.ws_toggle_advanced_fields { + color: #6087CB; + text-decoration: none; + font-size: 0.85em; +} + +.ws_toggle_advanced_fields:visited, .ws_toggle_advanced_fields:active { + color: #6087CB; +} + +.ws_toggle_advanced_fields:hover { + color: #d54e21; + text-decoration: underline; +} + +/************************************ + Menu flags +*************************************/ + +.ws_flag_container { + float: right; + margin-right: 4px; + padding-top: 2px; +} + +.ws_flag { + display: block; + float: right; + width: 16px; + height: 16px; + margin-left: 4px; + background-repeat: no-repeat; +} + +/* user-created items */ +.ws_custom_flag { + background-image: url('../images/page-add.png'); +} + +/* unused items - those that are in the default menu but not in the custom one */ +.ws_unused_flag { + background-image: url('../images/new-menu-badge.png'); + width: 31px; +} + +/* hidden items */ +.ws_hidden_flag { + background-image: url('../images/page-invisible.png'); +} + +/* items with custom permissions for the selected actor */ +.ws_custom_actor_permissions_flag { + font: 16px/1 'dashicons'; +} +.ws_custom_actor_permissions_flag::before { + /*content: "\f160";*/ /* padlock */ + content: "\f110"; /* human silhouette */ + color: black; + + filter: alpha(opacity=25); /*IE 5-7*/ + opacity: 0.25; +} + +/* Hidden from everyone except the current user and Super Admin. */ +.ws_hidden_from_others_flag { + background-image: url('../images/font-awesome/eye-slash.png'); +} + +/* Item visibility can't be determined because it depends on a meta capability. */ +.ws_uncertain_meta_cap_flag::before { + font: 16px/1 'dashicons'; + content: "\f348"; + color: black; + + filter: alpha(opacity=25); /*IE 5-7*/ + opacity: 0.25; +} + +/* These classes could be used to apply different styles to items depending on their flags */ +.ws_custom { } +.ws_hidden { } +.ws_unused { } + + +/************************************ + Toolbars +*************************************/ + +.ws_toolbar { + display: block; + + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + + width: 100%; + padding: 6px 6px 0 6px; + //height: 34px; +} + +.ws_button_container { + //padding-left: 6px; + //padding-top: 6px; +} + +.ws_button { + display: block; + margin-right: 3px; + margin-bottom: 4px; + + padding: 4px; + float: left; + + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + + width: 16px; + height: 16px; + + border-radius: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + + img { + vertical-align: top; + } +} + +a.ws_button:hover { + background-color: #d0e0ff; + border-color: #9090c0; +} + +//Disabled button state. +.ws_button.ws_button_disabled { + border-color: #ccc; +} +a.ws_button.ws_button_disabled:hover { + background-color: white; + border: 1px solid #ccc; +} +.ws_button_disabled img { + filter: grayscale(1); + -webkit-filter: grayscale(1); + opacity: 0.65; +} + +.ws_separator { + float: left; + width: 5px; +} + +#ws_toggle_toolbar, .ws_toggle_toolbar_button { + margin-right: 0; +} + +/************************************ + Capability selector +*************************************/ + +select.ws_dropdown { + width: 252px; + height: 20em; + + z-index: 1002; + position: absolute; + display: none; + + font-family : "Lucida Grande",Verdana,Arial,"Bitstream Vera Sans",sans-serif; + font-size: 12px; +} + +$dropdownOptionPaddingTop: 3px; +select.ws_dropdown option { + font-family : "Lucida Grande",Verdana,Arial,"Bitstream Vera Sans",sans-serif; + font-size: 12px; + padding: $dropdownOptionPaddingTop; +} + +select.ws_dropdown optgroup option { + padding-left: 10px; +} + +/************************************ + Tabs (small) + ************************************ + Tabbed navigation for dropdowns and small dialogs. + */ + +$activeToolTabBackground: #FDFDFD; + +.ws_tool_tab_nav { + list-style: outside none none; + padding: 0; + margin: 0 0 0 6px; + + li { + display: inline-block; + + border: 1px solid transparent; + border-bottom-width: 0; + padding: 3px 5px 5px; + line-height: 1.35em; + + margin-bottom: 0; + } + + li.ui-tabs-active { + border-color: #dfdfdf; + border-bottom-color: $activeToolTabBackground; + background: #FDFDFD none; + } + + a { + text-decoration: none; + } + + li.ui-tabs-active a { + color: #32373C; + } +} + +.ws_tool_tab { + border-top: 1px solid #DFDFDF; + margin-top: -1px; + background-color: $activeToolTabBackground; + + //Suggestion: Use 12px inner padding like in post editor boxes. +} + +/************************************ + Icon selector +*************************************/ + +$iconFontColor: #85888c; + +#ws_icon_selector { + border: 1px solid silver; + border-radius: 3px; + background-color: white; + + width: 216px; + padding: 4px 0 0 0; + position: absolute; +} + +#ws_icon_selector.ws_with_more_icons { + width: 570px; +} + +#ws_icon_selector .ws_icon_extra { + display: none; +} + +#ws_icon_selector.ws_with_more_icons .ws_icon_extra { + display: inline-block; +} + + +#ws_icon_selector .ws_icon_option { + float: left; + height: 30px; + + margin: 2px; + cursor: pointer; + border: 1px solid #bbb; + border-radius: 3px; + + /* Gradients and colours cribbed from WP 3.5.1 button styles */ + background: #f3f3f3; + background-image: -webkit-gradient(linear, left top, left bottom, from(#fefefe), to(#f4f4f4)); + background-image: -webkit-linear-gradient(top, #fefefe, #f4f4f4); + background-image: -moz-linear-gradient(top, #fefefe, #f4f4f4); + background-image: -o-linear-gradient(top, #fefefe, #f4f4f4); + background-image: linear-gradient(to bottom, #fefefe, #f4f4f4); +} + +#ws_icon_selector .ws_icon_option:hover { + /* Gradients and colours cribbed from WP 3.5.1 button styles */ + border-color: #999; + background: #f3f3f3; + background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f3f3f3)); + background-image: -webkit-linear-gradient(top, #fff, #f3f3f3); + background-image: -moz-linear-gradient(top, #fff, #f3f3f3); + background-image: -ms-linear-gradient(top, #fff, #f3f3f3); + background-image: -o-linear-gradient(top, #fff, #f3f3f3); + background-image: linear-gradient(to bottom, #fff, #f3f3f3); +} + +#ws_icon_selector .ws_icon_option.ws_selected_icon { + border-color: green; + background-color: #deffca; + background-image: none; +} + +#ws_icon_selector .ws_icon_option .ws_icon_image { + float: none; + margin: 0; + padding: 0; + + &:before { + color: $iconFontColor; + display: inline-block; + } +} + +#ws_icon_selector .ws_icon_option .ws_icon_image.dashicons { + width: 20px; + height: 20px; + padding: 5px; +} + +#ws_icon_selector .ws_icon_option img { + display: inline-block; + margin: 0; + padding: 7px; + + width: 16px; + height: 16px; +} + +#ws_menu_editor .ws_edit_field-icon_url input.ws_field_value { + width: 220px; + margin-right: 5px; +} + +/* The icon button that displays the pop-up icon selector. */ +#ws_menu_editor .ws_select_icon { + margin: 0; + padding: 0; + position: relative; + + box-sizing: border-box; + height: 25px; + min-height: 25px; + + .ame-is-wp53-plus & { + height: $basicInputWp53Height; + min-height: $basicInputWp53Height; + margin-top: 1px; + } +} + +.ws_select_icon .ws_icon_image { + color: $iconFontColor; + padding: 3px; + + &.dashicons { + padding: 3px 2px; + + &:before { + width: 20px; + } + } +} + +/* Current icon node (image version) */ +.ws_select_icon img { + margin: 0; + padding: 4px; + width: 16px; + height: 16px; +} + +#ws_icon_selector { + $tabTopPadding: 4px; + $tabHorizontalPadding: 4px; + + .ws_tool_tab_nav { + display: inline-block; + margin-top: 2px; + + li { + padding: 4px 10px 11px; + } + + position: relative; + } + + .ws_tool_tab { + padding: $tabTopPadding $tabHorizontalPadding 2px; + max-height: 324px; + overflow-y: auto; + } +} + +#ws_choose_icon_from_media { + margin: 2px; +} + +/************************************ + Embedded page selector +*************************************/ + +#ws_embedded_page_selector { + width: 254px; + padding: 6px 0 0 0; + + border: 1px solid silver; + border-radius: 3px; + background-color: white; + + box-sizing: border-box; + position: absolute; +} + +.ws_page_selector_tab_nav { + list-style: outside none none; + padding: 0; + margin: 0 0 0 6px; +} + +.ws_page_selector_tab_nav li { + display: inline-block; + + border: 1px solid transparent; + border-bottom-width: 0; + padding: 3px 5px 5px; + line-height: 1.35em; + + margin-bottom: 0; +} + +.ws_page_selector_tab_nav a { + text-decoration: none; +} + +.ws_page_selector_tab_nav li.ui-tabs-active { + border-color: #dfdfdf; + background-color: #FDFDFD; + border-bottom-color: #FDFDFD; +} + +.ws_page_selector_tab_nav li.ui-tabs-active a { + color: #32373C; +} + +.ws_page_selector_tab { + border-top: 1px solid #DFDFDF; + padding: 12px; /* The same padding as post editor boxes. */ + margin-top: -1px; + background-color: #FDFDFD; + + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; +} + +#ws_current_site_pages { + width: 100%; + min-height: 150px; + max-height: 300px; + + margin-left: 0; + margin-right: 0; +} + +#ws_embedded_page_selector input { + box-sizing: border-box; + max-width: 100%; +} + +#ws_custom_embedded_page_tab p:first-child { + margin-top: 0; +} + +/* + Make the "Page" field look editable. It is read-only because the user can't change it directly (they have to use + the dropdown), but we don't want it to be greyed-out. +*/ +#ws_menu_editor .ws_edit_field-embedded_page_id input.ws_field_value { + background-color: white; +} + + +/************************************ + Menu color picker +*************************************/ + +#ws-ame-menu-color-settings { + background: white; + display: none; +} + +#ame-menu-color-list { + height: 500px; + overflow-y: auto; +} + +.ame-menu-color-column { + min-width: 460px; +} + +.ame-menu-color-name { + display: inline-block; + vertical-align: top; + padding-top: 2px; + + line-height: 1.3; + font-size: 14px; + font-weight: 600; + + min-width: 180px; +} + +.ame-color-option { + padding: 10px 0; + + .wp-picker-container { + display: inline-block; + } +} + +.ame-advanced-menu-color { + display: none; +} + +#ws-ame-apply-colors-to-all { + display: block; + float: left; + margin-left: 5px; +} + +/* Color presets */ +#ame-color-preset-container { + padding: 0 8px 8px 8px; + + margin-left: -8px; + margin-right: -8px; + margin-bottom: 4px; + + border-bottom: 1px solid #eee; +} + +#ame-menu-color-presets { + width: 290px; + margin-right: 5px; +} + +#ws-ame-save-color-preset { + /*margin-right: 5px;*/ +} + +a#ws-ame-delete-color-preset { + color: #A00; + text-decoration: none; +} +a#ws-ame-delete-color-preset:hover { + color: #F00; +} + +/* Color scheme display in the editor widget. */ + +$colorFieldWidth: 190px; +$colorFieldRightMargin: 5px; + +.ws_color_scheme_display { + $colorFieldHeight: 26px; + + display: inline-block; + box-sizing: border-box; + height: $colorFieldHeight; + width: $colorFieldWidth; + + margin-right: $colorFieldRightMargin; + margin-left: 1px; + padding: 2px 4px; + + font-size: 12px; + border: 1px solid #ddd; + background: white; + cursor: pointer; + + line-height: $colorFieldHeight - 6px; + + .ame-is-wp53-plus & { + border-color: #7e8993; + border-radius: 4px; + + margin-top: 1px; + margin-bottom: 1px; + + padding: 3px 8px; + height: 28px; + line-height: 20px; + } +} + +.ws_open_color_editor { + width: $basicInputWidth - $colorFieldWidth - $colorFieldRightMargin - 1px; +} + +.ws_color_display_item { + display: inline-block; + width: 18px; + height: 18px; + + margin-right: 4px; + border: 1px solid #ccc; + border-radius: 3px; +} + +.ws_color_display_item:last-child { + margin-right: 0; +} + +/************************************ + Export and import +*************************************/ + +#export_dialog, #import_dialog { + display: none; +} + +.ui-widget-overlay { + background-color: black; + position: fixed; + left: 0; + top: 0; + right: 0; + bottom: 0; + opacity: 0.70; + -moz-opacity: 0.70; + filter: alpha(opacity=70); + + width: 100%; + height: 100%; +} + +.ui-front { + z-index: 10000; +} + +.settings_page_menu_editor { + .ui-dialog { + background: white; + border: 1px solid #c0c0c0; + + padding: 0; + + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; + + .ui-dialog-content { + padding: 8px 8px 8px 8px; + font-size: 1.1em; + } + + .ame-scrollable-dialog-content { + max-height: 500px; + overflow-y: auto; + padding-top: 0.5em; + } + } + + .ui-dialog-titlebar { + display: block; + height: 22px; + margin: 0; + padding: 4px 4px 4px 8px; + + background-color: #86A7E3; + font-size: 1.0em; + line-height: 22px; + + -webkit-border-top-left-radius: 4px; + -webkit-border-top-right-radius: 4px; + + -moz-border-radius-topleft: 4px; + -moz-border-radius-topright: 4px; + + border-top-left-radius: 4px; + border-top-right-radius: 4px; + + border-bottom: 1px solid #809fd9; + } + + .ui-dialog-title { + color: white; + font-weight: bold; + } + + .ui-button.ui-dialog-titlebar-close { + background: #86A7E3 url(../images/x.png) no-repeat center; + width: 22px; + height: 22px; + display: block; + float: right; + color: white; + + border-radius: 3px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + } + + .ui-dialog-titlebar-close:hover { + /*background-image: url(../images/x-light.png);*/ + background-color: #a6c2f5; + } + + .ui-icon-closethick { + + } +} + +#export_dialog .ws_dialog_panel { + height: 50px; +} + +#import_dialog .ws_dialog_panel { + height: 64px; +} + +.ws_dialog_panel { + .ame-fixed-label-text { + display: inline-block; + min-width: 6em; + } + + .ame-inline-select-with-input { + vertical-align: baseline; + } + + .ame-box-side-sizes { + display: flex; + flex-wrap: wrap; + max-width: 800px; + + .ame-fixed-label-text { + min-width: 4em; + } + + label { + margin-right: 2.5em; + } + + input { + margin-bottom: 0.4em; + } + + input[type=number] { + width: 6em; + } + } +} + +.ame-flexbox-break { + flex-basis: 100%; + height: 0; +} + +.ws_dialog_buttons { + text-align: right; + margin-top: 20px; + margin-bottom: 1px; + clear: both; +} + +.ws_dialog_buttons .button-primary { + display: block; + float: left; + margin-top: 0; +} + +.ws_dialog_buttons .button { + margin-top: 0; +} + +.ws_dialog_buttons.ame-vertical-button-list { + text-align: left; +} + +.ws_dialog_buttons.ame-vertical-button-list .button-primary { + float: none; +} + +.ws_dialog_buttons.ame-vertical-button-list .button { + width: 100%; + text-align: left; + margin-bottom: 10px; +} + +.ws_dialog_buttons.ame-vertical-button-list .button:last-child { + margin-bottom: 0; +} + +#import_file_selector { + display: block; + width: 286px; + + margin: 6px auto 12px; +} + +#ws_start_import { + min-width: 100px; +} + +#import_complete_notice { + text-align: center; + font-size: large; + padding-top: 25px; +} + +#ws_import_error_response { + width: 100%; +} + +.ws_dont_show_again { + display: inline-block; + margin-top: 1em; +} + +/************************************ + Menu access editor +*************************************/ + +/* The launch button */ +$accessLevelInputWidth: 190px; +$accessLevelRightMargin: 5px; +#ws_menu_editor .ws_edit_field-access_level input.ws_field_value +{ + width: $accessLevelInputWidth; + margin-right: $accessLevelRightMargin; +} + +.ws_launch_access_editor { + min-width: 40px; + width: $basicInputWidth - $accessLevelRightMargin - $accessLevelInputWidth - 1px; +} + +#ws_menu_access_editor { + width: 400px; + display: none; +} + +.ws_dialog_subpanel { + margin-bottom: 1em; + + fieldset p { + margin-top: 0; + margin-bottom: 4px; + } +} + +.ws-ame-dialog-subheading { + display: block; + font-weight: 600; + font-size: 1em; + margin: 0 0 0.2em 0; +} + +#ws_menu_access_editor .ws_column_access, +#ws_menu_access_editor .ws_ext_action_check_column { + text-align: center; + width: 1em; + padding-right: 0; +} + +#ws_menu_access_editor .ws_column_access input, +#ws_menu_access_editor .ws_ext_action_check_column input { + margin-right: 0; +} + +#ws_menu_access_editor .ws_column_role { + white-space: nowrap; +} + +#ws_role_table_body_container { + /*max-height: 400px; + overflow: auto;*/ + overflow: hidden; + margin-right: -1px; +} + +.ws_role_table_body { + margin-top: 2px; + max-width: 354px; +} + +.ws_has_separate_header .ws_role_table_header { + border-bottom: none; + + -moz-border-radius-bottomleft: 0; + -moz-border-radius-bottomright: 0; + -webkit-border-bottom-left-radius: 0; + -webkit-border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.ws_has_separate_header .ws_role_table_body { + border-top: none; + margin-top: 0; + + -moz-border-radius-topleft: 0; + -moz-border-radius-topright: 0; + -webkit-border-top-left-radius: 0; + -webkit-border-top-right-radius: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.ws_role_id { + display: none; +} + +#ws_extra_capability { + width: 100%; +} + +#ws_role_access_container { + position: relative; + max-height: 430px; + overflow: auto; +} + +#ws_role_access_overlay { + width: 100%; + height: 100%; + position: absolute; + + line-height: 100%; + + background: white; + filter: alpha(opacity=60); + opacity: 0.6; + -moz-opacity:0.6; +} + +#ws_role_access_overlay_content { + position: absolute; + width: 50%; + left: 22%; + top: 30%; + + background: white; + padding: 8px; + + border: 2px solid silver; + border-radius: 5px; + color: #555; +} + +#ws_menu_access_editor div.error { + margin-left: 0; + margin-right: 0; + margin-bottom: 5px; +} + +#ws_hardcoded_role_error { + display: none; +} + +/*--------------------------------------------* + The CPT/taxonomy permissions panel + *--------------------------------------------*/ + +/* + * When there are CPT/taxonomy permissions available, the appearance of the role list changes a bit. + */ +.ws_has_extended_permissions { + //The role name column also functions as a select button. + .ws_role_table_body .ws_column_role { + cursor: pointer; + } + + .ws_role_table_body .ws_column_selected_role_tip { + display: table-cell; + } + + .ws_role_table_body tr:hover { + background: #EAF2FA; + } + + .ws_role_table_body { + td { + border-top: 1px solid #f1f1f1; + } + tr:first-child td { + border-top-width: 0; + } + } + + /* The role or actor whose CPT/taxonomy permissions are currently expanded. */ + .ws_role_table_body tr.ws_cpt_selected_role { + background-color: #dddddd; + + .ws_column_role { + font-weight: bold; + } + + .ws_cpt_selected_role_tip { + visibility: visible; + } + + td { + color: #222; + } + } +} + +#ws_ext_permissions_container { + float: left; + width: 352px; + padding: 0 9px 0 0; +} + +$extendedPanelLeftPadding: 15px; + +#ws_ext_permissions_container_caption { + padding-left: $extendedPanelLeftPadding; + max-width: 352px; + position: relative; + white-space: nowrap; +} + +#ws_ext_permissions_container .ws_ext_permissions_table { + margin-top: 2px; + + tr td:first-child { + padding-left: $extendedPanelLeftPadding; + } + + .ws_ext_group_title { + padding-bottom: 0; + font-weight: bold; + } + + .ws_ext_action_check_column, + .ws_ext_action_name_column { + padding-top: 3px; + padding-bottom: 3px; + } + + tr.ws_ext_padding_row td { + padding: 0 0 0 0; + height: 1px; + } + + .ws_same_as_required_cap { + text-decoration: underline; + } + + .ws_ext_has_custom_setting { + label.ws_ext_action_name::after { + content: " *"; + } + } +} + +#ws_ext_permissions_container { + //Toggle between readable names and capabilities. + //The default is to show readable names (toggle = off), and the alternative is to show capabilities (toggle = on). + #ws_ext_toggle_capability_names { + cursor: pointer; + position: absolute; + right: 0; + color: #0073aa; + } + + &.ws_ext_readable_names_enabled #ws_ext_toggle_capability_names { + color: #b4b9be; + } + + //State: Show capabilities. + .ws_ext_readable_name { + display: none; + } + .ws_ext_capability { + display: inline; + } + + //State: Show readable names. This is the plugin default. + &.ws_ext_readable_names_enabled { + .ws_ext_readable_name { + display: inline; + } + .ws_ext_capability { + display: none; + } + } +} + +//The taxonomy table doesn't have capability groups (they're not needed - there are only 4 taxonomy permissions), +//so its first row needs a bit of extra padding to align vertically with the first role. +#ws_ext_permissions_container #ws_taxonomy_permissions_table { + tr:first-child td { + padding-top: 8px; + } +} + +/* The "selected role" indicator. */ +.ws_cpt_selected_role_tip { + display: block; + visibility: hidden; + + box-sizing: border-box; + width: 26px; + height: 26px; + + position: absolute; + right: 0; + + //border: 1px solid #E5E5E5; + background: white; + + transform: translate(1px, 0) rotate(-45deg); + transform-origin: top right; +} + +.ws_role_table_body .ws_column_selected_role_tip { + display: none; + + padding: 0; + width: 40px; + height: 100%; + text-align: right; + overflow: visible; + + position: relative; + + cursor: pointer; +} + +.ws_ame_breadcrumb_separator { + color: #999; +} + +//The "additional permissions available" notification for individual menu items. +#ws_menu_editor .ws_ext_permissions_indicator { + $indicatorSize: 16px; + + font-size: $indicatorSize; + height: $indicatorSize; + width: $indicatorSize; + + //Only show when an actor is selected. + visibility: hidden; + + vertical-align: bottom; + cursor: pointer; + color: #4aa100; //Derived from the border-color of an .updated notice. Lab color space, reduced lightness. +} + +#ws_menu_editor.ws_is_actor_view .ws_ext_permissions_indicator { + visibility: visible; +} + +/************************************ + Visible users dialog +*************************************/ + +$userSelectionPanelWidth: 350px; +$userSelectionPanelHeight: 400px; +$userSelectionPanelPadding: 10px; + +#ws_visible_users_dialog { + $userDialogPadding: 8px; + background: white; + padding: $userDialogPadding; +} + +#ws_user_selection_panels { + min-width: $userSelectionPanelWidth * 2 + $userSelectionPanelPadding; + + .ws_user_selection_panel { + display: block; + float: left; + position: relative; + + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + + width: $userSelectionPanelWidth; + height: $userSelectionPanelHeight; + + border: 1px solid #e5e5e5; + margin-right: 10px; + padding: 10px; + } + + #ws_user_selection_target_panel { + margin-right: 0; + } + + #ws_available_user_query { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + width: 100%; + max-height: 28px; + } + + .ws_user_list_wrapper { + position: absolute; + $userListTop: 50px; + + top: $userListTop; + left: $userSelectionPanelPadding; + right: $userSelectionPanelPadding; + + //width: $userSelectionPanelWidth - 2 * $userSelectionPanelPadding - 2px; + height: $userSelectionPanelHeight - $userListTop - $userSelectionPanelPadding - 2px; + + overflow-x: auto; + overflow-y: auto; //Allow scrolling. + } + + .ws_user_selection_list { + min-height: 20px; + + //No borders. The panels themselves already have borders. + border-width: 0; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + + .ws_user_action_column { + width: 20px; + text-align: center; + + padding-top: 9px; + padding-bottom: 0; + } + + .ws_user_action_button { + cursor: pointer; + color: #b4b9be; + } + + .ws_user_username_column { + padding-left: 0; + } + + .ws_user_display_name_column { + white-space: nowrap; + } + } + + #ws_available_users { + tr { + cursor: pointer; + } + + tr:hover, tr.ws_user_best_match { + background-color: #eaf2fa; + } + + //The "add user" button. + tr:hover .ws_user_action_button { + color: #7ad03a; + } + } + + #ws_selected_users { + //The "remove from list" button. + .ws_user_action_button::before { + content: "\f158"; + } + .ws_user_action_button:hover { + color: #dd3d36; + } + .ws_user_action_column { + padding-left: 6px; + } + + .ws_user_display_name_column { + display: none; + } + + //You can't deselect the current user. It always stays in the visible actor list. + tr.ws_user_must_be_selected { + .ws_user_action_button { + display: none; + } + } + + td { + //padding-bottom: 8px; + } + tr:not(:first-child) td { + //padding-top: 0; + } + + } + + #ws_selected_users_caption { + font-size: 14px; + line-height: 1.4em; + padding: 7px 10px; + + color: #555; + font-weight: 600; + } + + &::after { + display: block; + height: 1px; + visibility: hidden; + content: ' '; + clear: both;; + } +} + +#ws_loading_users_indicator { + position: absolute; + right: $userSelectionPanelPadding; + bottom: $userSelectionPanelPadding; + + margin-right: 0; + margin-bottom: 0; +} + + + +/************************************ + Menu deletion error +*************************************/ + +#ws-ame-menu-deletion-error { + max-width: 400px; +} + + + + +/************************************ + Tooltips and hints +*************************************/ + +.ws_tooltip_trigger, .ws_field_tooltip_trigger { + cursor: pointer; +} + +.ws_tooltip_content_list { + list-style: disc; + margin-left: 1em; + margin-bottom: 0; +} + +.ws_tooltip_node { + font-size: 13px; + line-height: 1.3; + border-radius: 3px; + max-width: 300px; +} + +.ws_field_tooltip_trigger .dashicons { + font-size: 16px; + height: 16px; + vertical-align: bottom; +} + +.ws_field_tooltip_trigger { + color: #a1a1a1; +} + +//Tooltips on the settings page. +#ws_plugin_settings_form .ws_tooltip_trigger .dashicons { + font-size: 18px; +} + +//And in other boxes. +.ws_ame_custom_postbox .ws_tooltip_trigger .dashicons { + font-size: 18px; + height: 18px; + vertical-align: bottom; +} + +.ws_tooltip_trigger.ame-warning-tooltip { + color: orange; +} + +.ws_wide_tooltip { + max-width: 450px; +} + +.ws_hint { + background: #FFFFE0; + border: 1px solid #E6DB55; + + margin-bottom: 0.5em; + border-radius: 3px; + position: relative; + padding-right: 20px; +} + +.ws_hint_close { + border: 1px solid #E6DB55; + border-right: none; + border-top: none; + color: #dcc500; + font-weight: bold; + cursor: pointer; + + width: 18px; + text-align: center; + border-radius: 3px; + + position: absolute; + right: 0; + top: 0; +} + +.ws_hint_close:hover { + background-color: #ffef4c; + border-color: #e0b900; + color: black; +} + +.ws_hint_content { + padding: 0.4em 0 0.4em 0.4em; +} + +.ws_hint_content ul { + list-style: disc; + list-style-position: inside; + margin-left: 0.5em; +} + +.ws_ame_doc_box, .ws_ame_custom_postbox { + .hndle { + cursor: default !important; + border-bottom: 1px solid $amePostboxBorderColor; + } + + .handlediv { + display: block; + float: right; + } + + .inside { + margin-bottom: 0; + } + + ul { + list-style: disc outside; + margin-left: 1em; + } + + li > ul { + margin-top: 6px; + } + + .button-link .toggle-indicator::before { + margin-top: 4px; + width: 20px; + -webkit-border-radius: 50%; + border-radius: 50%; + text-indent: -1px; + + content: "\f142"; + display: inline-block; + font: normal 20px/1 dashicons; + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-decoration: none !important; + } + + &.closed .button-link .toggle-indicator::before { + content: "\f140"; + } +} + +.ws_basic_container .ws_ame_custom_postbox { + //Match .ws_main_container's horizontal margins for proper alignment. + margin-left: 2px; + margin-right: 2px; +} + +.ws_ame_custom_postbox .ame-tutorial-list { + margin: 0; + + a { + text-decoration: none; + display: block; + padding: 4px; + } + + ul { + margin-left: 1em; + } + + li { + display: block; + margin: 0; + list-style: none; + } +} + +/************************************ + Copy Permissions dialog +*************************************/ +#ws-ame-copy-permissions-dialog select { + min-width: 280px; +} + +/********************************************* + Capability suggestions and preview +**********************************************/ + +#ws_capability_suggestions { + padding: 4px; + width: 350px; + + border: 1px solid #cdd5d5; + box-shadow: 0 1px 1px rgba(0,0,0,0.04); + background: #fff; + + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + + #ws_previewed_caps { + margin-top: 0; + margin-bottom: 6px; + } + + td, th { + //For consistency, padding should match the capability dropdown. + padding-top: $dropdownOptionPaddingTop; + padding-bottom: $dropdownOptionPaddingTop; + } + + tr.ws_preview_has_access .ws_ame_role_name{ + background-color: lightgreen; + } + + .ws_ame_suggested_capability { + cursor: pointer; + + &:hover { + background-color: #d0f2d0; + } + } +} + +/********************************************* + Settings page stuff +**********************************************/ + +#ws_plugin_settings_form figure { + margin-left: 0; + margin-top: 0; + margin-bottom: 1em; +} + +.ame-available-add-ons { + tr:first-of-type td { + margin-top: 0; + padding-top: 0; + } + + td { + padding-top: 10px; + padding-bottom: 10px; + } + + .ame-add-on-heading { + padding-left: 0; + } +} + +.ame-add-on-name { + font-weight: 600; +} + +.ame-add-on-details-link::after { + /*content: " \f504"; + font-family: dashicons, sans-serif;*/ +} + +/********************************************* + WordPress 5.3+ consistent styles +**********************************************/ + +.ame-is-wp53-plus .ws_edit_field input[type="button"] { + margin-top: 1px; +} + +/********************************************* + CSS border style selector +**********************************************/ + +.ame-css-border-styles { + .ame-fixed-label-text { + min-width: 5em; + } + + .ame-border-sample-container { + display: inline-block; + vertical-align: top; + min-height: 28px; + } + + .ame-border-sample { + display: inline-block; + width: 14em; + border-top: 0.3em solid #444; + } +} + +/********************************************* + Miscellaneous +**********************************************/ + +#ws_sidebar_pro_ad { + min-width: 225px; + + margin-top: 5px; + margin-left: 3px; + + position: fixed; + right: 20px; + bottom: 40px; + z-index: 100; +} + +.ws-ame-icon-radio-button-group > label { + display: inline-block; + padding: 8px; + border: 1px solid #ccd0d4; + border-radius: 2px; + + margin-right: 0.5em; +} + +span.description { + color: #666; + //Before WP 5.5 description text was displayed in italics. We'll preserve that style for now. + font-style: italic; +} + +.wrap :target { + background-color: rgba(255, 230, 81, 0.7); +} + +.test-wrap { + background-color: #444444; + padding: 30px; +} + +.test-container { + width: 400px; + height: 200px; + background-color: white; + + border: 1px solid black; + border-radius: 10px; + + overflow: hidden; +} + +.test-header { + background-color: #67d6ff; + padding: 6px; + + border-top-left-radius: 8px; + border-top-right-radius: 8px; +} + +.test-content { + padding: 8px; +} + +@import "test-access-screen"; + +@import "main-tabs"; \ No newline at end of file diff --git a/css/screen-meta-old-wp.css b/css/screen-meta-old-wp.css new file mode 100644 index 0000000..819c7d1 --- /dev/null +++ b/css/screen-meta-old-wp.css @@ -0,0 +1,57 @@ +/************************************ + Screen meta buttons + for WP 3.7 and below +*************************************/ + +/* All buttons */ +.custom-screen-meta-link-wrap { + float: right; + height: 22px; + padding: 0; + margin: 0 0 0 6px; + font-family: sans-serif; + -moz-border-radius-bottomleft: 3px; + -moz-border-radius-bottomright: 3px; + -webkit-border-bottom-left-radius: 3px; + -webkit-border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + + background: #e3e3e3; + + border-right: 1px solid transparent; + border-left: 1px solid transparent; + border-bottom: 1px solid transparent; + background-image: -ms-linear-gradient(bottom, #dfdfdf, #f1f1f1); /* IE10 */ + background-image: -moz-linear-gradient(bottom, #dfdfdf, #f1f1f1); /* Firefox */ + background-image: -o-linear-gradient(bottom, #dfdfdf, #f1f1f1); /* Opera */ + background-image: -webkit-gradient(linear, left bottom, left top, from(#dfdfdf), to(#f1f1f1)); /* old Webkit */ + background-image: -webkit-linear-gradient(bottom, #dfdfdf, #f1f1f1); /* new Webkit */ + background-image: linear-gradient(bottom, #dfdfdf, #f1f1f1); /* proposed W3C Markup */ +} + +#screen-meta .custom-screen-meta-link-wrap a.custom-screen-meta-link, +#screen-meta-links .custom-screen-meta-link-wrap a.custom-screen-meta-link +{ + background-image: none; + padding: 0 6px 0 6px; +} + +#screen-meta-links a.custom-screen-meta-link::after { + display: none; +} + +/* "Upgrade to Pro" */ +#ws-pro-version-notice { + background: #00C31F none; +} + +#ws-pro-version-notice a.show-settings { + font-weight: bold; + color: #DEFFD8; + text-shadow: none; +} + +#ws-pro-version-notice a.show-settings:hover { + color: white; +} \ No newline at end of file diff --git a/css/screen-meta.css b/css/screen-meta.css new file mode 100644 index 0000000..286147e --- /dev/null +++ b/css/screen-meta.css @@ -0,0 +1,51 @@ +/************************************ + Screen meta buttons + for WP 3.8+ +*************************************/ + +/* All buttons */ +.custom-screen-meta-link-wrap { + float: right; + height: 28px; + margin: 0 0 0 6px; + + border: 1px solid #ccd0d4; + border-top: none; + border-radius: 0 0 4px 4px; + + background: #fff; + box-shadow: none; +} + +#screen-meta .custom-screen-meta-link-wrap a.custom-screen-meta-link, +#screen-meta-links .custom-screen-meta-link-wrap a.custom-screen-meta-link +{ + padding: 3px 16px 3px 16px; + text-decoration: none; + display: block; + min-height: 22px; + box-shadow: none; +} + +#screen-meta-links a.custom-screen-meta-link::after { + display: none; +} + +/* "Upgrade to Pro" */ +#ws-pro-version-notice { + background-color: #00C31F; + border-color: #0a0; +} + +#screen-meta-links #ws-pro-version-notice a.show-settings { + font-weight: bold; + color: #DEFFD8; + text-shadow: none; + box-shadow: none; + border: none; + background-color: #00C31F; +} + +#screen-meta-links #ws-pro-version-notice a.show-settings:hover { + color: white; +} \ No newline at end of file diff --git a/css/style-classic.css b/css/style-classic.css new file mode 100644 index 0000000..70f5bea --- /dev/null +++ b/css/style-classic.css @@ -0,0 +1,142 @@ +.ws_main_container { + padding-bottom: 4px; +} + +.ws_container { + border: 1px solid #a9badb; + background-color: #bdd3ff; +} + +#ws_menu_editor .ws_active { + background-color : #8eb0f1; /* make sure this overrides ws_menu_separator */ +} + +#ws_menu_editor.ws_is_actor_view .ws_is_hidden_for_actor.ws_active { + background-color : #dadee6; +} + +.ws_menu_separator { + background: #F9F9F9 url("../images/menu-arrows.png") no-repeat 4px 8px; + border-color: #d9d9d9; +} + +.ws_edit_link { + background-image: url('../images/bullet_arrow_down2.png'); + background-repeat: no-repeat; + background-position: center 3px; +} + +a.ws_edit_link:hover { + background-color: #ffffd0; + background-image: url('../images/bullet_arrow_down2.png'); +} + +.ws_edit_link:active { + background-repeat: no-repeat; + background-position: center 3px; +} + +.ws_edit_link_expanded { + background-color: #ffffd0; + border-bottom: none; + border-color: #ffffd0; + + padding-bottom: 1px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + + -moz-border-radius-bottomright: 0; + -moz-border-radius-bottomleft: 0; + + -webkit-border-bottom-right-radius: 0; + -webkit-border-bottom-left-radius: 0; +} + + +.ws_editbox { + background-color: #ffffd0; +} + +.ws_input_default input, .ws_input_default select { + color: gray; +} + +/* + * Show/Hide advanced fields + */ + +.ws_toggle_advanced_fields { + color: #6087CB; + font-size: 0.85em; +} + +.ws_toggle_advanced_fields:visited, .ws_toggle_advanced_fields:active { + color: #6087CB; +} + +.ws_toggle_advanced_fields:hover { + color: #d54e21; + text-decoration: underline; +} + + +/* + * Toolbars + */ + +.ws_button { + border: 1px solid #c0c0e0; +} + +a.ws_button:hover { + background-color: #d0e0ff; + border-color: #9090c0; +} + +/************************************ + Export and import +*************************************/ + +.settings_page_menu_editor .ui-dialog { + background: white; + border: 1px solid #c0c0c0; +} + +.settings_page_menu_editor .ui-dialog-titlebar { + background-color: #86A7E3; + height: 22px; +} + +.settings_page_menu_editor .ui-dialog-title { + color: white; +} + +.settings_page_menu_editor .ui-button.ui-dialog-titlebar-close { + background-color: transparent; + background-image: none; + border-style: none; + + color: white; /* WP default: #666; */ + cursor: pointer; + padding: 0; + position: absolute; + top: 0; + right: 0; + width: 36px; + height: 22px; + text-align: center; +} + +.settings_page_menu_editor .ui-dialog-titlebar-close::before { + font: normal 20px/30px 'dashicons'; + content: '\f158'; + + vertical-align: top; + width: 36px; + height: 22px; +} + +.settings_page_menu_editor .ui-dialog-titlebar-close:hover { + background: transparent none; + color: #004665; +} \ No newline at end of file diff --git a/css/style-modern-one.css b/css/style-modern-one.css new file mode 100644 index 0000000..dc230ee --- /dev/null +++ b/css/style-modern-one.css @@ -0,0 +1,224 @@ +/* +//Alternative: like the "invalid" state in the menu customizer. +$hiddenItemBackground: #f6c9cc; +$hiddenItemBorder: #f1acb1; +//*/ +.ws_container { + border: 0 solid transparent; + background: #fafafa; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + width: 304px; + padding: 0; + margin-top: 0; + margin-bottom: 9px; + margin-left: 10px; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); +} +.ws_container .ws_item_title { + color: #23282D; + padding-left: 0; + font-weight: 600; + font-size: 13px; +} +.ws_container .ws_item_head { + border: 1px solid #dfdfdf; + padding: 7px 0 7px 7px; +} +.ws_container .ws_flag_container .ws_custom_actor_permissions_flag, +.ws_container .ws_flag_container .ws_custom_flag { + display: none; +} + +#ws_menu_editor.ws_is_actor_view input[type=checkbox].ws_actor_access_checkbox { + margin-right: 5px; +} + +.ws_editbox { + background: white; + padding: 7px; + border: 1px solid #dfdfdf; + border-top-width: 0; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +a.ws_edit_link { + background: transparent; + color: #A0A5AA; + text-align: center; + border-radius: 0; +} +a.ws_edit_link::before { + content: "\f140"; + font: normal 20px/1 dashicons; + display: block; +} +a.ws_edit_link:hover { + color: #777; +} + +.ws_edit_link.ws_edit_link_expanded::before { + content: "\f142"; +} + +.ws_toolbar .ws_button { + border: 1px solid #C0C0C0; +} + +.ws_box { + margin-top: 6px; +} + +.ws_menu_separator .ws_item_head::after { + content: ""; + display: inline-block; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + vertical-align: middle; + width: 249.28px; + height: 0; + border: 2px inset rgba(0, 0, 0, 0.2); +} +.ws_menu_separator .ws_item_title { + width: 0; + padding-left: 0; + padding-right: 0; +} + +.ws_menu.ws_active::after { + right: -22px; +} + +.ws_container.ws_active, .ws_container.ws_is_hidden_for_actor.ws_active { + z-index: 2; +} +.ws_container.ws_active .ws_item_head, .ws_container.ws_is_hidden_for_actor.ws_active .ws_item_head { + border-color: #999; + background-color: #c7c7c7; +} +.ws_container.ws_active .ws_item_title, .ws_container.ws_is_hidden_for_actor.ws_active .ws_item_title { + color: #23282D; +} +.ws_container.ws_active .ws_editbox, .ws_container.ws_is_hidden_for_actor.ws_active .ws_editbox { + border-color: #999; +} + +.ws_container.ws_is_hidden_for_actor .ws_item_head { + border-color: #dfdfdf; + background-color: #e3e3e3; +} +.ws_container.ws_is_hidden_for_actor .ws_editbox { + border-color: #dfdfdf; +} +.ws_container.ws_is_hidden_for_actor .ws_item_title { + color: #888; +} + +.ws_compact_layout .ws_container { + margin-top: -1px; + margin-bottom: 0; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} +.ws_compact_layout .ws_container .ws_item_head { + padding-top: 4px; + padding-bottom: 4px; +} +.ws_compact_layout .ws_container.ws_active { + z-index: 2; +} +.ws_compact_layout .ws_container:last-child { + margin-bottom: 9px; +} +.ws_compact_layout.ws_is_hidden_for_actor .ws_item_head, .ws_compact_layout.ws_is_hidden_for_actor .ws_editbox { + border-color: #dfdfdf; +} +.ws_compact_layout.ws_active .ws_item_head, .ws_compact_layout.ws_active .ws_editbox { + border-color: #999; +} + +#ws_menu_editor #ws_toggle_editor_layout { + display: block; +} + +#ws_icon_selector, #ws_embedded_page_selector { + z-index: 3; +} + +.ws_container.ui-sortable-helper { + box-shadow: 1px 3px 6px 0 rgba(1, 1, 1, 0.4); +} + +.ws_main_container { + width: 324px; +} +.ws_main_container .ws_toolbar { + padding: 10px 10px 0; +} +.ws_main_container .ws_dropzone { + margin-left: 10px; + margin-right: 10px; +} + +#ws_editor_sidebar { + padding: 6px 10px; +} +#ws_editor_sidebar .ws_main_button { + margin-left: 0; + margin-right: 0; +} + +.settings_page_menu_editor .ui-dialog { + background: white; + border: 1px solid #c0c0c0; + border-radius: 0; +} +.settings_page_menu_editor .ui-dialog-titlebar { + background-color: #fcfcfc; + border-bottom: 1px solid #dfdfdf; + height: auto; + padding: 0; +} +.settings_page_menu_editor .ui-dialog-titlebar .ui-button.ui-dialog-titlebar-close { + background: none; + border-style: none; + color: #666; + cursor: pointer; + padding: 0; + margin: 0; + position: absolute; + top: 0; + right: 0; + width: 36px; + height: 36px; + text-align: center; +} +.settings_page_menu_editor .ui-dialog-titlebar .ui-button.ui-dialog-titlebar-close .ui-icon, .settings_page_menu_editor .ui-dialog-titlebar .ui-button.ui-dialog-titlebar-close .ui-button-text { + display: none; +} +.settings_page_menu_editor .ui-dialog-titlebar .ui-dialog-titlebar-close::before { + font: normal 20px/36px "dashicons"; + content: "\f158"; + vertical-align: middle; + width: 36px; + height: 36px; +} +.settings_page_menu_editor .ui-dialog-titlebar .ui-dialog-titlebar-close:hover { + background: transparent none; + color: #2ea2cc; +} +.settings_page_menu_editor .ui-dialog-title { + color: #444444; + font-size: 18px; + font-weight: 600; + line-height: 36px; + padding: 0 36px 0 8px; + display: block; +} + +/*# sourceMappingURL=style-modern-one.css.map */ diff --git a/css/style-modern-one.css.map b/css/style-modern-one.css.map new file mode 100644 index 0000000..b79d916 --- /dev/null +++ b/css/style-modern-one.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["style-modern-one.scss"],"names":[],"mappings":"AAmBA;AAAA;AAAA;AAAA;AAAA;AAaA;EAGC;EACA,YApCgB;EAsChB;EACA;EACA;EAEA,OA9BW;EA+BX;EAEA;EACA,eAjCkB;EAkClB,aAduB;EAgBvB;;AAEA;EACC,OA9CS;EA+CT;EACA;EACA;;AAGD;EACC;EACA;;AAMA;AAAA;EAEC;;;AAKH;EACC;;;AAGD;EACC;EACA,SArEmB;EAuEnB;EACA;EAEA;EACA;EACA;;;AAGD;EACC;EACA;EAEA;EACA;;AAEA;EACC;EACA;EACA;;AAGD;EACC;;;AAIF;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAQA;EACC;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EAGA;;AAGD;EACC;EACA;EACA;;;AAKF;EACC;;;AAMD;EACC;;AAEA;EACC,cA9ImB;EA+InB,kBAhJuB;;AAmJxB;EACC,OAlJiB;;AAqJlB;EACC,cAvJmB;;;AA+JpB;EACC,cA/KW;EAgLX,kBAxJqB;;AA2JtB;EACC,cApLW;;AAuLZ;EACC,OA9Je;;;AA0KhB;EACC;EACA;EAEA;EACA;EACA;;AAEA;EACC,aAXuB;EAYvB,gBAZuB;;AAqBxB;EACC;;AAIF;EACC,eAjNiB;;AAqNjB;EACC,cAlOU;;AAsOX;EACC,cAxNkB;;;AA6NrB;EACC;;;AAOD;EACC;;;AAOD;EACC;;;AAOD;EACC,OAxOoB;;AA0OpB;EACC;;AAGD;EACC,aAhPc;EAiPd,cAjPc;;;AAqPhB;EACC;;AAEA;EACC;EACA;;;AAUD;EACC;EACA;EACA;;AAGD;EACC;EACA;EACA;EACA;;AAEA;EACC;EACA;EAEA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA;EACA;EAEA;;AAEA;EACC;;AAIF;EACC;EACA;EAEA;EACA;EACA;;AAGD;EACC;EACA;;AAIF;EACC;EACA;EACA;EACA;EAEA;EACA","file":"style-modern-one.css"} \ No newline at end of file diff --git a/css/style-modern-one.scss b/css/style-modern-one.scss new file mode 100644 index 0000000..08c8fd2 --- /dev/null +++ b/css/style-modern-one.scss @@ -0,0 +1,348 @@ +$itemBackground: #fafafa; +$itemBorder: #dfdfdf; + +//$itemBackground: #f7f7f7; +//$itemBorder: #cacaca; + +$itemText: #23282D; //WordPress default. +//$itemText: #5a5a5a; //CodePress default. +//$itemText: #555; //Theme customizer widget headings. + +$horizontalPadding: 7px; +$headVerticalPadding: 7px; +$itemWidth: 304px; +$itemMarginBottom: 9px; + +$selectedItemBackground: #c7c7c7; +$selectedItemBorder: #999; +$selectedItemText: #23282D; + +/* +//Alternative: like the "invalid" state in the menu customizer. +$hiddenItemBackground: #f6c9cc; +$hiddenItemBorder: #f1acb1; +//*/ + +$hiddenItemBackground: darken($itemBackground, 9); +$hiddenItemBorder: $itemBorder; +$hiddenItemText: #888; //#9a9ea5; //#82878C; //#999 + +$columnPadding: 10px; +$mainContainerWidth: $itemWidth + $columnPadding * 2; + +.ws_container { + $itemHorizontalMargin: ($mainContainerWidth - $itemWidth) / 2; + + border: 0 solid transparent; + background: $itemBackground; + + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + + width: $itemWidth; + padding: 0; + + margin-top: 0; + margin-bottom: $itemMarginBottom; + margin-left: $itemHorizontalMargin; + + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); + + .ws_item_title { + color: $itemText; + padding-left: 0; + font-weight: 600; + font-size: 13px; + } + + .ws_item_head { + border: 1px solid $itemBorder; + padding: $headVerticalPadding 0 $headVerticalPadding $horizontalPadding; + } + + .ws_flag_container { + //Hide low-importance flags. It's debatable (knowing which roles have custom permissions is useful + //when debugging configuration issues), but lets leave them hidden for now. + .ws_custom_actor_permissions_flag, + .ws_custom_flag { + display: none; + } + } +} + +#ws_menu_editor.ws_is_actor_view input[type="checkbox"].ws_actor_access_checkbox { + margin-right: 5px; +} + +.ws_editbox { + background: white; + padding: $horizontalPadding; + + border: 1px solid $itemBorder; + border-top-width: 0; + + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} + +a.ws_edit_link { + background: transparent; + color: #A0A5AA; + + text-align: center; + border-radius: 0; + + &::before { + content: "\f140"; + font: normal 20px/1 dashicons; + display: block; + } + + &:hover { + color: #777; + } +} + +.ws_edit_link.ws_edit_link_expanded::before { + content: "\f142"; +} + +.ws_toolbar .ws_button { + border: 1px solid #C0C0C0; +} + +.ws_box { + margin-top: 6px; +} + +.ws_menu_separator { + .ws_item_head { + //background: url("../images/menu-arrows.png") no-repeat ($horizontalPadding + 22px) center; + } + + .ws_item_head::after { + content: ''; + display: inline-block; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + + vertical-align: middle; + width: $itemWidth * 0.82; + height: 0; + $separatorColor: rgba(0, 0, 0, 0.2); + + border: 2px inset $separatorColor; + } + + .ws_item_title { + width: 0; + padding-left: 0; + padding-right: 0; + } +} + + +.ws_menu.ws_active::after { + right: -22px; +} + +//============================================== +// Selected state +//============================================== +.ws_container.ws_active, .ws_container.ws_is_hidden_for_actor.ws_active { + z-index: 2; + + .ws_item_head { + border-color: $selectedItemBorder; + background-color: $selectedItemBackground; + } + + .ws_item_title { + color: $selectedItemText; + } + + .ws_editbox { + border-color: $selectedItemBorder; + } +} + +//============================================== +// Hidden state +//============================================== +.ws_container.ws_is_hidden_for_actor { + .ws_item_head { + border-color: $hiddenItemBorder; + background-color: $hiddenItemBackground; + } + + .ws_editbox { + border-color: $hiddenItemBorder; + } + + .ws_item_title { + color: $hiddenItemText; + } +} + +//============================================== +// Compact layout option +//============================================== + +.ws_compact_layout { + $compactBorderColor: $itemBorder; //Alternative: #cacaca + $compactVerticalPadding: 4px; + + .ws_container { + margin-top: -1px; + margin-bottom: 0; + + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + + .ws_item_head { + padding-top: $compactVerticalPadding; + padding-bottom: $compactVerticalPadding; + } + + .ws_item_title { + //Alternative: thinner, less in-your-face title text. + //font-weight: normal; + //font-size: 14px; + } + + &.ws_active { + z-index: 2; + } + } + + .ws_container:last-child { + margin-bottom: $itemMarginBottom; + } + + &.ws_is_hidden_for_actor { + .ws_item_head, .ws_editbox { + border-color: $hiddenItemBorder; + } + } + &.ws_active { + .ws_item_head, .ws_editbox { + border-color: $selectedItemBorder; + } + } +} + +#ws_menu_editor #ws_toggle_editor_layout { + display: block; +} + +//==================================================== +// Dropdowns should appear above the selected item. +//==================================================== + +#ws_icon_selector, #ws_embedded_page_selector { + z-index: 3; +} + +//============================================== +// Item dragging +//============================================== + +.ws_container.ui-sortable-helper { + box-shadow: 1px 3px 6px 0 rgba(1, 1, 1, 0.4); +} + +//============================================== +// Columns / containers +//============================================== + +.ws_main_container { + width: $mainContainerWidth; + + .ws_toolbar { + padding: $columnPadding $columnPadding 0; + } + + .ws_dropzone { + margin-left: $columnPadding; + margin-right: $columnPadding; + } +} + +#ws_editor_sidebar { + padding: ($columnPadding - 4px) $columnPadding; + + .ws_main_button { + margin-left: 0; + margin-right: 0; + } +} + + +//============================================== +// Dialogs +//============================================== + +.settings_page_menu_editor { + .ui-dialog { + background: white; + border: 1px solid #c0c0c0; + border-radius: 0; + } + + .ui-dialog-titlebar { + background-color: #fcfcfc; + border-bottom: 1px solid #dfdfdf; + height: auto; + padding: 0; + + .ui-button.ui-dialog-titlebar-close { + background: none; + border-style: none; + + color: #666; + cursor: pointer; + padding: 0; + margin: 0; + position: absolute; + top: 0; + right: 0; + + width: 36px; + height: 36px; + + text-align: center; + + .ui-icon, .ui-button-text { + display: none; + } + } + + .ui-dialog-titlebar-close::before { + font: normal 20px/36px 'dashicons'; + content: '\f158'; + + vertical-align: middle; + width: 36px; + height: 36px; + } + + .ui-dialog-titlebar-close:hover { + background: transparent none; + color: #2ea2cc; + } + } + + .ui-dialog-title { + color: #444444; + font-size: 18px; + font-weight: 600; + line-height: 36px; + + padding: 0 36px 0 8px; + display: block; + } +} diff --git a/css/style-wp-grey.css b/css/style-wp-grey.css new file mode 100644 index 0000000..223d360 --- /dev/null +++ b/css/style-wp-grey.css @@ -0,0 +1,245 @@ +.ws_container { + padding: 0; + width: 296px; + margin-bottom: 5px; + + background: white; + + border: 1px solid #aeaeae; + + -webkit-box-shadow: inset 0 1px 0 #fff; + box-shadow: inset 0 1px 0 #fff; + + /*-webkit-border-radius: 2px; + border-radius: 2px;*/ +} + +/** + * Item head elements + */ + +.ws_item_head { + padding: 3px; + + background-color: #d9d9d9; + background-image: -ms-linear-gradient(top, #e9e9e9, #d9d9d9); + background-image: -moz-linear-gradient(top, #e9e9e9, #d9d9d9); + background-image: -webkit-gradient(linear, left top, left bottom, from(#e9e9e9), to(#d9d9d9)); + background-image: -webkit-linear-gradient(top, #e9e9e9, #d9d9d9); + background-image: linear-gradient(to bottom, #e9e9e9, #d9d9d9); +} + +.ws_item_title { + color: #222; + text-shadow: #FFFFFF 0 1px 0; +} + +/** + * The down-arrow that expands menu settings + */ + +.ws_edit_link { + background: transparent url(../images/arrows.png) no-repeat center 3px; + overflow: hidden; + text-indent:-999em; +} + +a.ws_edit_link:hover { + background-image: url(../images/arrows-dark.png); +} + +.ws_edit_link:active { + background-image: url(../images/arrows-dark.png); +} + +.ws_edit_link_expanded { + border-bottom: none; + border-color: #ffffd0; + + padding-bottom: 1px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + + -moz-border-radius-bottomright: 0; + -moz-border-radius-bottomleft: 0; + + -webkit-border-bottom-right-radius: 0; + -webkit-border-bottom-left-radius: 0; +} + + +/** + * Separators + */ + +.ws_menu_separator { + border-color: #d9d9d9; +} + +.ws_menu_separator .ws_item_head { + min-height: 22px; + background: #F9F9F9 url("../images/menu-arrows.png") no-repeat 4px 8px; +} + +.ws_menu_separator.ws_active .ws_item_head { + background: #999 url("../images/menu-arrows.png") no-repeat 4px 8px; +} + +/* Offset the separator image in actor view to prevent it from overlapping the checkbox. */ +.ws_is_actor_view .ws_menu_separator .ws_item_head { + background-position: 25px 8px; +} + +/** + * Active item + */ + +.ws_active .ws_item_head { + background: #777; + background-image: -webkit-gradient(linear, left bottom, left top, from(#6d6d6d), to(#808080)); + background-image: -webkit-linear-gradient(bottom, #6d6d6d, #808080); + background-image: -moz-linear-gradient(bottom, #6d6d6d, #808080); + background-image: -o-linear-gradient(bottom, #6d6d6d, #808080); + background-image: linear-gradient(to top, #6d6d6d, #808080); +} + +.ws_active .ws_item_title { + text-shadow: 0 -1px 0 #333; + color: #fff; + border-top-color: #808080; + border-bottom-color: #6d6d6d; +} + +/** + * Hidden items. + */ +.ws_is_actor_view .ws_container.ws_is_hidden_for_actor .ws_item_head { + background: #F9F9F9 linear-gradient(to top, #F3F3F3, #FFFFFF); +} + +/* selected hidden items */ +.ws_is_actor_view .ws_is_hidden_for_actor.ws_active .ws_item_head { + background: #dedede linear-gradient(to top, #8f8f8f, #a2a2a2); +} +.ws_is_actor_view .ws_is_hidden_for_actor.ws_active .ws_item_title { + color: #fff; + text-shadow: none; +} + +/* hidden separators */ +.ws_is_actor_view .ws_menu_separator.ws_is_hidden_for_actor .ws_item_head { + /* Override gradient with the separator image. */ + background: url("../images/menu-arrows.png") no-repeat 25px 8px; +} +/* selected hidden separators */ +.ws_menu_separator.ws_is_hidden_for_actor.ws_active .ws_item_head { + background: #aaa url("../images/menu-arrows.png") no-repeat 25px 8px; +} + + +/** + * Dropping menus on other menus. + */ + +.ws_menu_drop_hover, .ws_menu_drop_hover .ws_item_head { + background: #43b529; +} + +.ws_menu_drop_hover .ws_item_title { + text-shadow: none; +} + +/** + * Misc + */ + +.ws_editbox { + /*background-color: #ffffd0;*/ + background-color: #FBFBFB; + padding: 4px 6px; +} + +.ws_input_default input, .ws_input_default select { + color: gray; +} + +/* + * Show/Hide advanced fields + */ + +.ws_toggle_advanced_fields { + color: #6087CB; + font-size: 0.85em; +} + +.ws_toggle_advanced_fields:visited, .ws_toggle_advanced_fields:active { + color: #6087CB; +} + +.ws_toggle_advanced_fields:hover { + color: #d54e21; + text-decoration: underline; +} + + +/* + * Toolbars + */ + +.ws_button { + border: 1px solid #c0c0e0; +} + +a.ws_button:hover { + background-color: #d0e0ff; + border-color: #9090c0; +} + +/************************************ + Export and import +*************************************/ + +.ui-dialog { + background: white; + border: 1px solid #c0c0c0; + + border-radius: 0; +} + +.ui-dialog-titlebar { + background-color: #fcfcfc;; + border-bottom-color: #dfdfdf; +} + +.ui-dialog-title { + color: #444444; +} + +.ui-dialog-titlebar-close { + background-color: transparent; + border-style: none; + + color: #666; + cursor: pointer; + padding: 0; + position: absolute; + top: 0; + right: 0; + width: 36px; + height: 30px; + text-align: center; +} + +.ui-dialog-titlebar-close::before { + font: normal 20px/30px 'dashicons'; + content: '\f158'; + + vertical-align: top; + width: 36px; + height: 30px; +} + +.ui-dialog-titlebar-close:hover { + background: transparent none; + color: #2ea2cc; +} \ No newline at end of file diff --git a/extras.php b/extras.php new file mode 100644 index 0000000..368fb95 --- /dev/null +++ b/extras.php @@ -0,0 +1,2595 @@ +wp_menu_editor = $wp_menu_editor; + + add_filter('admin_menu_editor-available_modules', array($this, 'filter_available_modules'), 10, 1); + + //Multisite: Clear caches when switching to another site. + add_action('switch_blog', array($this, 'clear_site_specific_caches'), 10, 0); + + //Apply most Pro version menu customizations all in one go. This reduces apply_filters() overhead + //and is slightly faster than adding a separate filter for each feature. + add_filter('custom_admin_menu', array($this, 'apply_admin_menu_filters')); + add_filter('custom_admin_submenu', array($this, 'apply_admin_menu_filters'), 10, 2); + + add_action('admin_menu_editor-menu_merged', array($this, 'on_menu_merged'), 10, 1); + add_action('admin_menu_editor-menu_built', array($this, 'on_menu_built'), 10, 2); + + //Add some extra shortcodes of our own + $shortcode_callback = array($this, 'handle_shortcode'); + $info_shortcodes = array( + 'wp-wpurl', //WordPress address (URI), as returned by get_bloginfo() + 'wp-siteurl', //Blog address (URI) + 'wp-admin', //Admin area URL (with a trailing slash) + 'wp-name', //Weblog title + 'wp-version', //Current WP version + 'wp-user-display-name', //Current user's display name, + 'wp-user-first-name', // first name, + 'wp-user-last-name', // last name, + 'wp-user-login', // and username/login. + 'wp-logout-url', //A URL that lets the current user log out. + ); + foreach($info_shortcodes as $tag){ + add_shortcode($tag, $shortcode_callback); + } + add_shortcode('ame-count-bubble', array($this, 'handle_count_shortcode')); + + //Output the menu-modification JS after the menu has been generated. + //'in_admin_header' is, AFAIK, the action that fires the soonest after menu + //output has been completed, so we use that. + add_action('in_admin_header', array($this, 'fix_flagged_menus')); + + //Import/export settings + $this->export_settings = array( + 'max_file_size' => 5*1024*1024, + 'file_extension' => 'dat', + 'old_format_string' => 'wsMenuEditor_ExportFile', + ); + + //Insert the import and export dialog HTML into the editor's page + add_action('admin_menu_editor-footer', array($this, 'menu_editor_footer')); + //Handle menu downloads and uploads + add_action('admin_menu_editor-header', array($this, 'menu_editor_header')); + //Handle export requests + add_action( 'wp_ajax_export_custom_menu', array($this,'ajax_export_custom_menu') ); + //Add the "Import" and "Export" buttons + add_action('admin_menu_editor-sidebar', array($this, 'add_extra_buttons')); + + //Initialise the universal import/export handler. + wsAmeImportExportFeature::get_instance($this->wp_menu_editor); + + add_filter('admin_menu_editor-self_page_title', array($this, 'pro_page_title'), 10, 0); + add_filter('admin_menu_editor-self_menu_title', array($this, 'pro_menu_title'), 10, 0); + + //Let other components know we're Pro. + add_filter('admin_menu_editor_is_pro', array($this, 'is_pro_version'), 10, 0); + + //Add menu item drop zones to the top-level and sub-menu containers. + add_action('admin_menu_editor-container', array($this, 'output_menu_dropzone'), 10, 1); + + //Add submenu icons. + add_filter('admin_menu_editor-submenu_with_icon', array($this, 'add_submenu_icon_html'), 10, 2); + + //Multisite: Let people edit the network admin menu. + add_action( + 'network_admin_menu', + array($this->wp_menu_editor, 'hook_admin_menu'), + $this->wp_menu_editor->get_magic_hook_priority() + ); + + //Add extra scripts to the menu editor. + add_action('admin_menu_editor-register_scripts', array($this, 'register_extra_scripts')); + add_filter('admin_menu_editor-editor_script_dependencies', array($this, 'add_extra_editor_dependencies')); + + /** + * Access management extensions. + */ + + //Allow usernames to be used in capability checks. Syntax : "user:user_login" + add_filter('admin_menu_editor-virtual_caps', array($this, 'add_user_actor_cap'), 10, 2); + + //Enable advanced capability operations (OR, AND, NOT) for internal use. + add_filter('admin_menu_editor-current_user_can', array($this, 'grant_computed_caps_to_current_user'), 10, 2); + + //Custom per-role and per-user access settings (distinct from the "extra capability" field. + add_filter('custom_admin_menu_capability', array($this, 'apply_custom_access')); + + //Role access: Grant virtual capabilities to roles/users that need them to access certain menus. + add_filter('admin_menu_editor-virtual_caps', array($this, 'prepare_virtual_caps_for_user'), 10, 2); + add_filter('role_has_cap', array($this, 'grant_virtual_caps_to_role'), 200, 3); + + add_action('load-options.php', array($this, 'disable_virtual_caps_on_all_options')); + + //Make it possible to automatically hide new admin menus. + add_filter('admin_menu_editor-new_menu_grant_access', array($this, 'get_new_menu_grant_access')); + + //Remove the plugin from the "Plugins" page for users who're not allowed to see it. + if ( $this->wp_menu_editor->get_plugin_option('plugins_page_allowed_user_id') !== null ) { + add_filter('all_plugins', array($this, 'filter_plugin_list')); + } + + /** + * Menu color scheme generation. + */ + add_filter('ame_pre_set_custom_menu', array($this, 'add_menu_color_css')); + add_action('wp_ajax_ame_output_menu_color_css', array($this,'ajax_output_menu_color_css') ); + + //FontAwesome icons. + add_filter('custom_admin_menu', array($this, 'add_menu_fa_icon'), 10, 1); + add_filter('admin_menu_editor-icon_selector_tabs', array($this, 'add_fa_selector_tab'), 10, 1); + add_action('admin_menu_editor-icon_selector', array($this, 'output_fa_selector_tab')); + + //Menu headings. + if ( (is_admin() || (defined('DOING_AJAX') && DOING_AJAX)) && version_compare(PHP_VERSION, '5.6', '>=') ) { + require_once AME_ROOT_DIR . '/extras/menu-headings/ameMenuHeadingStyler.php'; + new ameMenuHeadingStyler($this->wp_menu_editor); + } + + //License management + add_filter('wslm_license_ui_title-admin-menu-editor-pro', array($this, 'license_ui_title'), 10, 0); + add_action('wslm_license_ui_logo-admin-menu-editor-pro', array($this, 'license_ui_logo')); + add_action('wslm_license_ui_details-admin-menu-editor-pro', array($this, 'license_ui_upgrade_link'), 10, 3); + add_filter('wslm_product_name-admin-menu-editor-pro', array($this, 'license_ui_product_name'), 10, 0); + + /** + * Add scripts and styles that apply to all admin pages. + */ + add_action('admin_enqueue_scripts', array($this, 'enqueue_dashboard_deps')); + + /** + * Add-on display and installation. + */ + //Disabled for now. This feature should be finished later. + /* + add_action('admin-menu-editor-display_addons', array($this, 'display_addons')); + add_action('admin_menu_editor-enqueue_scripts-settings', array($this, 'enqueue_addon_scripts')); + + add_action('wp_ajax_ws_ame_activate_add_on', array($this, 'ajax_activate_addon')); + add_action('wp_ajax_ws_ame_activate_add_on', array($this, 'ajax_install_addon')); + //*/ + } + + /** + * Process shortcodes in menu fields + * + * @param array $item + * @return array + */ + function do_shortcodes($item){ + foreach($this->fields_supporting_shortcodes as $field){ + if ( isset($item[$field]) ) { + $value = $item[$field]; + if ( strpos($value, '[') !== false ){ + $this->current_shortcode_item = $item; + $item[$field] = do_shortcode($value); + $this->current_shortcode_item = null; + } + } + } + return $item; + } + + /** + * Get the value of one of our extra shortcodes + * + * @param array $atts Shortcode attributes (ignored) + * @param string $content Content enclosed by the shortcode (ignored) + * @param string $code + * @return string Shortcode will be replaced with this value + */ + function handle_shortcode($atts, /** @noinspection PhpUnusedParameterInspection */ $content = null, $code = ''){ + //The shortcode tag can be either $code or the zeroth member of the $atts array. + if ( empty($code) ){ + $code = isset($atts[0]) ? $atts[0] : ''; + } + + $info = '['.$code.']'; //Default value + switch($code){ + case 'wp-wpurl': + $info = get_bloginfo('wpurl'); + break; + + case 'wp-siteurl': + $info = get_bloginfo('url'); + break; + + case 'wp-admin': + $info = admin_url(); + break; + + case 'wp-name': + $info = get_bloginfo('name'); + break; + + case 'wp-version': + $info = get_bloginfo('version'); + break; + + case 'wp-user-display-name': + $info = $this->get_current_user_property('display_name'); + break; + + case 'wp-user-first-name': + $info = $this->get_current_user_property('first_name'); + break; + + case 'wp-user-last-name': + $info = $this->get_current_user_property('last_name'); + break; + + case 'wp-user-login': + $info = $this->get_current_user_property('user_login'); + break; + + case 'wp-logout-url': + $info = wp_logout_url(); + break; + } + + return $info; + } + + private function get_current_user_property($property) { + $user = wp_get_current_user(); + if (is_object($user)) { + return strval($user->get($property)); + } + return ''; + } + + /** + * Get the HTML code for the small "(123)" bubble in the title of the current menu item. + * + * The count bubble shortcode is intended for situations where the user wants to rename + * a menu item like "WooCommerce -> Orders" that includes a small bubble showing the number + * of pending orders (or plugin updates, comments awaiting moderation, etc). The shortcode + * extracts the count from the default menu title and shows it in the custom title. + * + * @return string + */ + public function handle_count_shortcode() { + if (isset( + $this->current_shortcode_item, + $this->current_shortcode_item['defaults'], + $this->current_shortcode_item['defaults']['menu_title'] + )) { + //Oh boy, this is excessive! Tests say it takes < 1 ms per shortcode, + //but it still seems wrong to go this far just to extract a tag. + $title = $this->current_shortcode_item['defaults']['menu_title']; + if ( stripos($title, 'loadHTML($title) ) { + /** @noinspection PhpComposerExtensionStubsInspection */ + $xpath = new DOMXpath($dom); + $result = $xpath->query('//span[contains(@class,"update-plugins") or contains(@class,"awaiting-mod")]'); + if ( $result->length > 0 ) { + $span = $result->item(0); + return $span->ownerDocument->saveHTML($span); + } + } + } + } + return ''; + } + + /** + * Flag menus (and menu items) that are set to open in a new window + * so that they can be identified later. + * + * Adds a element to the title + * of each detected menu. + * + * @param array $item + * @return array + */ + function flag_new_window_menus($item){ + $open_in = ameMenuItem::get($item, 'open_in', 'same_window'); + if ( $open_in == 'new_window' ){ + $old_title = ameMenuItem::get($item, 'menu_title', ''); + $item['menu_title'] = $old_title . ''; + + //For compatibility with Ozh's Admin Drop Down menu, record the link ID that will be + //assigned to this item. This lets us modify it later. + if ( function_exists('wp_ozh_adminmenu_sanitize_id') ){ + $subid = 'oamsub_'.wp_ozh_adminmenu_sanitize_id( + ameMenuItem::get($item, 'file', '') + ); + $this->ozhs_new_window_menus[] = '#' . str_replace( + array(':', '&'), + array('\\\\:', '\\\\&'), + $subid + ); + } + } + + return $item; + } + + /** + * Output a piece of JS that will find flagged menu links and make them + * open in a new window. + * + * @return void + */ + function fix_flagged_menus(){ + ?> + + framed_pages[$slug] = $item; //Used by the callback function + + //Default to using menu title for page title, if no custom title specified + if ( empty($item['page_title']) ) { + $item['page_title'] = $item['menu_title']; + } + + //Add a virtual menu. The menu record created by add_menu_page will be + //thrown away; what matters is that this populates other structures + //like $_registered_pages. + add_menu_page( + $item['page_title'], + $item['menu_title'], + $item['access_level'], + $slug, + array($this, 'display_framed_page') + ); + + //Change the slug to our newly created page. + $item['file'] = $slug; + } + + return $item; + } + + /** + * Intercept menu items that need to be displayed in an IFrame. + * + * @see wsMenuEditorExtras::create_framed_menu() + * + * @param array $item + * @param string $parent_file + * @return array + */ + function create_framed_item($item, $parent_file = null){ + if ( ($item['open_in'] == 'iframe') && !empty($parent_file) ){ + + $slug = 'framed-menu-item-' . md5($item['file'] . '|' . $parent_file); + $this->framed_pages[$slug] = $item; + + if ( empty($item['page_title']) ) { + $item['page_title'] = $item['menu_title']; + } + add_submenu_page( + $parent_file, + $item['page_title'], + $item['menu_title'], + $item['access_level'], + $slug, + array($this, 'display_framed_page') + ); + + $item['file'] = $slug; + } + + return $item; + } + + /** + * Display a page in an IFrame. + * This callback is used by all menu items that are set to open in a frame. + * + * @return void + */ + function display_framed_page(){ + global $plugin_page; + + if ( isset($this->framed_pages[$plugin_page]) ){ + $item = $this->framed_pages[$plugin_page]; + } else { + return; + } + + if ( !current_user_can($item['access_level'], -1) ){ + echo "You do not have sufficient permissions to view this page."; + return; + } + + $styles = array( + 'border' => '0 none', + 'width' => '100%', + 'min-height' => '300px', + ); + + //The user can set the frame height manually or let the plugin calculate it automatically (the default). + $height = !empty($item['iframe_height']) ? intval($item['iframe_height']) : 0; + $height = min(max($height, 0), 10000); + if ( !empty($height) ) { + $styles['height'] = $height . 'px'; + unset($styles['min-height']); + } + + $is_scrolling_disabled = !empty($item['is_iframe_scroll_disabled']); + + $style_attr = ''; + foreach($styles as $property => $value) { + $style_attr .= $property . ': ' . $value . ';'; + } + + $heading = !empty($item['page_title'])?$item['page_title']:$item['menu_title']; + $heading = sprintf('<%1$s>%2$s', WPMenuEditor::$admin_heading_tag, $heading); + ?> +
+ + + +
+ + + embedded_wp_pages)); + $this->embedded_wp_pages[$slug] = $item; //Used by the callback function. + + //Add a virtual menu. + if ( empty($parent_file) ) { + add_menu_page( + $item['page_title'], + $item['menu_title'], + $item['access_level'], + $slug, + array($this, 'display_embedded_wp_page') + ); + } else { + add_submenu_page( + $parent_file, + $item['menu_title'], + $item['menu_title'], + $item['access_level'], + $slug, + array($this, 'display_embedded_wp_page') + ); + } + + //Change the slug to our newly created page. + $item['file'] = $slug; + //Force automatic URL generation. + $item['url'] = ''; + //Make sure admin-helpers.js won't replace the real heading with the placeholder. + if ($item['page_heading'] === ameMenuItem::embeddedPagePlaceholderHeading) { + $item['page_heading'] = null; + } + + return $item; + } + + /** + * A callback for menu items that embed a page or CPT item in the admin panel. Displays the content of the page. + */ + public function display_embedded_wp_page() { + $slug = isset($_GET['page']) ? strval($_GET['page']) : null; + if ( empty($slug) || !isset($this->embedded_wp_pages[$slug]) ) { + echo '

Error: Invalid page. How did you get here?

'; + return; + } + + $item = $this->embedded_wp_pages[$slug]; + $page_id = ameMenuItem::get($item, 'embedded_page_id', 0); + $page_blog_id = ameMenuItem::get($item, 'embedded_page_blog_id', get_current_blog_id()); + + $should_switch = ($page_blog_id !== get_current_blog_id()); + if ( $should_switch ) { + switch_to_blog($page_blog_id); + } + + $page = get_post($page_id); + $expected_post_statuses = array('publish', 'private'); + if ( empty($page) ) { + printf( + 'Error: Page not found. Post ID %1$d does not exist on blog ID %2$d.', + $page_id, + $page_blog_id + ); + } else if ( !in_array($page->post_status, $expected_post_statuses) ) { + printf( + 'Error: This page is not published. Post ID: %1$d, expected status: %2$s, actual status: "%3$s".', + $page_id, + esc_html('"' . implode('" or "', $expected_post_statuses) . '"'), + esc_html($page->post_status) + ); + } else { + $heading = $item['page_heading']; + if ( $heading === ameMenuItem::embeddedPagePlaceholderHeading ) { + //Note that this means the user can't set the heading to the same text as the placeholder. + //That's poor design, but it probably won't matter in practice. + $heading = strip_tags($item['menu_title']); + } + + echo '
'; + if ( !empty($heading) ) { + printf('<%2$s>%1$s', $heading, WPMenuEditor::$admin_heading_tag); + } + echo apply_filters('the_content', $page->post_content); + echo '
'; + } + + if ( $should_switch ) { + restore_current_blog(); + } + } + + private function set_final_hidden_flag($item) { + //Globally hidden items stay hidden regardless of who is currently logged in. + if ( !empty($item['hidden']) ) { + return $item; + } + + static $user = null, $user_login = ''; + if ( $user === null ) { + $user = wp_get_current_user(); + $user_login = $user->get('user_login'); + } + + //User-specific settings take precedence. + $user_actor = 'user:' . $user_login; + if ( isset($item['hidden_from_actor'][$user_actor]) ) { + $item['hidden'] = $item['hidden_from_actor'][$user_actor]; + return $item; + } + + //The item will be hidden only if *all* of the user's roles have it hidden. + //Unlike with capabilities and permissions, there are no defaults to worry about. + $actors = array(); + if ( is_multisite() && is_super_admin($user->ID) ) { + $actors[] = 'special:super_admin'; + } + foreach($this->wp_menu_editor->get_user_roles($user) as $role) { + $actors[] = 'role:' . $role; + } + + $is_hidden = null; + foreach($actors as $actor) { + if ( !isset($is_hidden) ) { + $is_hidden = !empty($item['hidden_from_actor'][$actor]); + } else { + $is_hidden = $is_hidden && !empty($item['hidden_from_actor'][$actor]); + } + } + + if ( $is_hidden ) { + $item['hidden'] = true; + } + return $item; + } + + /** + * Output the HTML for import and export dialogs. + * Callback for the 'menu_editor_footer' action. + * + * @return void + */ + function menu_editor_footer(){ + if ( !$this->wp_menu_editor->is_editor_page() ) { + return; + } + + include AME_ROOT_DIR . '/extras/menu-headings/menu-headings-template.php'; + ?> +
+
+
+ wait + Creating export file... +
+
+ Click the "Download" button below to download the exported admin menu to your computer. +
+
+ +
+ +
+
+ + +
+
+ wait + Uploading file... +
+
+ wait + Importing menu... +
+
+ Import Complete! +
+ + +
+ Choose an exported menu file (.export_settings['file_extension']; ?>) + to import: + + + +
+
+ + + +
+ + +
+
+
+ + + + + wp_menu_editor; + if (!$wp_menu_editor->current_user_can_edit_menu() || !check_ajax_referer('export_custom_menu', false, false)){ + die( $wp_menu_editor->json_encode( array( + 'error' => __("You're not allowed to do that!", 'admin-menu-editor') + ))); + } + + //Prepare the export record. + $export = $this->get_exported_menu(); + $export['total']++; //Export counter. Could be used to make download URLs unique. + + //Compress menu data to make export files smaller. + $post = $this->wp_menu_editor->get_post_params(); + $menu_data = $post['data']; + $menu = ameMenu::load_json($menu_data); + $menu_data = ameMenu::to_json(ameMenu::compress($menu)); + + //Save the menu structure. + $export['menu'] = $menu_data; + + //Include the blog's domain name in the export filename to make it easier to + //distinguish between multiple export files. + $siteurl = get_bloginfo('url'); + $domain = @parse_url($siteurl); + $domain = isset($domain['host']) ? ($domain['host'] . ' ') : ''; + + $export['filename'] = sprintf( + '%sadmin menu (%s).dat', + $domain, + date('Y-m-d') + ); + + //Store the modified export record. The plugin will need it when the user + //actually tries to download the menu. + $this->set_exported_menu($export); + + $download_url = $this->wp_menu_editor->get_plugin_page_url(array( + 'noheader' => '1', + 'action' => 'download_menu', + 'export_num' => $export['total'], + )); + + $result = array( + 'download_url' => $download_url, + 'filename' => $export['filename'], + 'filesize' => strlen($export['menu']), + ); + + die($wp_menu_editor->json_encode($result)); + } + + /** + * Get the current exported record + * + * @return array + */ + function get_exported_menu(){ + $user = wp_get_current_user(); + $exports = get_metadata('user', $user->ID, 'custom_menu_export', true); + + $defaults = array( + 'total' => 0, + 'menu' => '', + 'filename' => '', + ); + + if ( !is_array($exports) ){ + $exports = array(); + } + + return array_merge($defaults, $exports); + } + + /** + * Store the export record. + * + * @param array $export + * @return bool + */ + function set_exported_menu($export){ + //Caution: update_metadata expects slashed data. + $export = wp_slash($export); + $user = wp_get_current_user(); + return update_metadata('user', $user->ID, 'custom_menu_export', $export); + } + + /** + * Handle menu uploads and downloads. + * This is a callback for the 'admin_menu_editor_header' action. + * + * @param string $action + * @return void + */ + function menu_editor_header($action = ''){ + $wp_menu_editor = $this->wp_menu_editor; + + //Add Pro version buttons to the menu editor toolbar. + add_filter('admin_menu_editor-toolbar_icons', array($this, 'add_toolbar_icons'), 10, 2); + add_action('admin_menu_editor-register_toolbar_buttons', array($this, 'register_toolbar_buttons'), 10, 2); + + //Handle menu download requests + if ( $action == 'download_menu' ){ + $export = $this->get_exported_menu(); + if ( empty($export['menu']) || empty($export['filename']) ){ + die("Exported data not found"); + } + + //Force file download + header("Content-Description: File Transfer"); + header('Content-Disposition: attachment; filename="' . $export['filename'] . '"'); + header("Content-Type: application/force-download"); + header("Content-Transfer-Encoding: binary"); + header("Content-Length: " . strlen($export['menu'])); + + /* The three lines below basically make the download non-cacheable */ + header("Cache-control: private"); + header("Pragma: private"); + header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); + + echo $export['menu']; + + die(); + + //Handle menu uploads + } elseif ( $action == 'upload_menu' ) { + + header('Content-Type: text/html'); + + if ( empty($_FILES['menu']) ){ + echo $wp_menu_editor->json_encode(array('error' => "No file specified")); + die(); + } + + $file_data = $_FILES['menu']; + if ( filesize($file_data['tmp_name']) > $this->export_settings['max_file_size'] ){ + $this->output_for_jquery_form( $wp_menu_editor->json_encode(array('error' => "File too big")) ); + die(); + } + + //Check for general upload errors. + if ($file_data['error'] != UPLOAD_ERR_OK) { + $message = self::get_upload_error_message($file_data['error']); + $this->output_for_jquery_form( $wp_menu_editor->json_encode(array('error' => $message)) ); + die(); + } + + $file_contents = file_get_contents($file_data['tmp_name']); + + //Check if this file could plausibly contain an exported menu + if ( strpos($file_contents, $this->export_settings['old_format_string']) !== false ){ + + //This is an exported menu in the old format. + $data = $wp_menu_editor->json_decode($file_contents, true); + if ( !(isset($data['menu']) && is_array($data['menu'])) ) { + $this->output_for_jquery_form( $wp_menu_editor->json_encode(array('error' => "Unknown or corrupted file format")) ); + die(); + } + + try { + $menu = ameMenu::load_array($data['menu'], false, true); + } catch (InvalidMenuException $ex) { + $this->output_for_jquery_form( $wp_menu_editor->json_encode(array('error' => $ex->getMessage())) ); + die(); + } + + } else { + if (strpos($file_contents, ameMenu::format_name) !== false) { + + //This is an export file in the new format. + try { + $menu = ameMenu::load_json($file_contents, false, true); + } catch (InvalidMenuException $ex) { + $this->output_for_jquery_form( $wp_menu_editor->json_encode(array('error' => $ex->getMessage())) ); + die(); + } + + } else { + + //This is an unknown file. + $this->output_for_jquery_form($wp_menu_editor->json_encode(array('error' => "Unknown file format"))); + die(); + + } + } + + //Merge the imported menu with the current one. + $menu['tree'] = $wp_menu_editor->menu_merge($menu['tree']); + + //Everything looks okay, send back the menu data + $this->output_for_jquery_form( ameMenu::to_json($menu) ); + die (); + } + } + + public static function get_upload_error_message($errorCode) { + switch($errorCode) { + case UPLOAD_ERR_INI_SIZE: + $message = sprintf( + 'The uploaded file exceeds the upload_max_filesize directive in php.ini. Limit: %s', + strval(ini_get('upload_max_filesize')) + ); + break; + case UPLOAD_ERR_FORM_SIZE: + $message = "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form."; + break; + case UPLOAD_ERR_PARTIAL: + $message = "The file was only partially uploaded."; + break; + case UPLOAD_ERR_NO_FILE: + $message = "No file was uploaded."; + break; + case UPLOAD_ERR_NO_TMP_DIR: + $message = "Missing a temporary folder."; + break; + case UPLOAD_ERR_CANT_WRITE: + $message = "Failed to write file to disk."; + break; + case UPLOAD_ERR_EXTENSION: + $message = "File upload stopped by a PHP extension."; + break; + + default: + $message = 'Unknown upload error #' . $errorCode; + break; + } + return $message; + } + + /** + * Utility method that outputs data in a format suitable to the jQuery Form plugin. + * + * Specifically, the docs recommend enclosing JSON data in a '; + } + } + + /** + * Output the "Import" and "Export" buttons. + * Callback for the 'admin_menu_editor_sidebar' action. + * + * @return void + */ + function add_extra_buttons(){ + ?> +
+ + + + + +
+ + + + insertAllAfter( + 'new-separator', + array( + 'new-heading' => array( + 'title' => 'New heading', + 'topLevelOnly' => true, + ), + 'pro-separator-1' => null, + 'deny' => array( + 'title' => "Hide and prevent access. \n" . $hideButtonExtraTooltip, + 'alt' => 'Hide', + 'iconName' => 'hide-and-deny', + ), + ) + ); + + $secondRow->addAll(array( + 'pro-separator-2' => null, + 'toggle-all' => array( + 'title' => 'Toggle all menus for the selected role', + 'alt' => 'Toggle all', + 'topLevelOnly' => true, + ), + 'reset-permissions' => array( + 'title' => 'Reset all permissions for the selected role or user', + 'alt' => 'Reset permissions', + 'topLevelOnly' => true, + ), + 'copy-permissions' => array( + 'title' => 'Copy all menu permissions from one role to another', + 'alt' => 'Copy permissions', + 'topLevelOnly' => true, + ), + 'pro-separator-3' => null, + )); + } + + public function register_extra_scripts() { + wp_register_auto_versioned_script( + 'ame-menu-editor-extras', + plugins_url('extras/menu-editor-extras.js', $this->wp_menu_editor->plugin_file), + array('jquery') + ); + wp_register_auto_versioned_script( + 'ame-ko-extensions', + plugins_url('extras/ko-extensions.js', $this->wp_menu_editor->plugin_file), + array('knockout', 'jquery', 'jquery-ui-dialog', 'ame-lodash', 'wp-color-picker') + ); + wp_register_auto_versioned_script( + 'ame-menu-heading-settings', + plugins_url('extras/menu-headings/menu-headings.js', $this->wp_menu_editor->plugin_file), + array('jquery', 'knockout', 'jquery-ui-dialog', 'wp-color-picker', 'ame-lodash', 'ame-ko-extensions',), + true + ); + } + + /** + * @param array $dependencies + * @return array + */ + public function add_extra_editor_dependencies($dependencies) { + $dependencies[] = 'ame-menu-editor-extras'; + $dependencies[] = 'ame-menu-heading-settings'; + return $dependencies; + } + + /** + * Add "user:user_login" to the user's capabilities. This makes it possible to restrict + * menu access on a per-user basis. + * + * @param array $virtual_caps + * @param WP_User $user + * @return array + */ + function add_user_actor_cap($virtual_caps, $user){ + if ( empty($user) || empty($user->user_login) ){ + return $virtual_caps; + } + $virtual_caps[WPMenuEditor::ALL_VIRTUAL_CAPS]['user:' . $user->user_login] = true; + return $virtual_caps; + } + + /** + * Apply custom per-role and per-user access settings to a menu item. + * + * If the user can't access this menu, this method will change the required + * capability to "do_not_allow". Otherwise, it will be left unmodified. + * + * Callback for the 'custom_admin_menu_capability' filter. + * + * @param array $item + * @return array Modified item. + */ + public function apply_custom_access($item) { + if ( !isset($item['access_check_log']) ) { + $item['access_check_log'] = array(); + } + + if ( ! $this->current_user_is_granted_access($item) ) { + $item['access_check_log'][] = '! Changing the required capability to "do_not_allow".'; + $item['access_level'] = 'do_not_allow'; + $item['user_has_access_level'] = false; + } else { + $item['user_has_access_level'] = true; + } + + return $item; + } + + /** + * Check if the current user should be granted access to the specified menu item. + * + * Applies explicit per-role and per-user settings from $item['grant_access']. + * DOES NOT apply the extra capability. That should be done elsewhere. + * + * If there are no custom permissions set that match the current user, this method + * will check if the user would normally be able to access the menu (i.e. without + * virtual caps). + * + * @param array $item Menu item. + * @return bool + */ + private function current_user_is_granted_access(&$item) { + static $is_multisite = null; + static $user = null, $user_login = ''; + + if ( $is_multisite === null ) { + $is_multisite = is_multisite(); + } + if ( $user === null ) { + $user = wp_get_current_user(); + $user_login = $user->get('user_login'); + } + + $has_access = null; + $log = array(); + + $reason = null; + $debug_title = ameMenuItem::get($item, 'full_title', ameMenuItem::get($item, 'menu_title', '[untitled menu]')); + + $log[] = sprintf('Checking "%s" permissions:', ameMenuItem::get($item, 'menu_title', '[untitled menu item]')); + + if ( isset($item['grant_access']) ) { + $grants = $item['grant_access']; + + //If this user is specifically allowed/forbidden, use that setting. + if ( isset($grants['user:' . $user_login]) ) { + $has_access = $grants['user:' . $user_login]; + $log[] = sprintf( + '+ Custom permissions for user "%s": %s.', + $user_login, + $has_access ? 'ALLOW' : 'DENY' + ); + + $reason = sprintf( + 'The "%1$s" menu item is explicitly %2$s for the user "%3$s".', + $debug_title, + $has_access ? 'enabled' : 'disabled', + $user_login + ); + } else { + $log[] = sprintf( + '- No custom permissions for the "%s" username.', + $user_login + ); + } + + //Or if they're a super admin, allow *everything* unless explicitly denied. + $this->wp_menu_editor->disable_virtual_caps = true; + if ( is_null($has_access) && $is_multisite && is_super_admin($user->ID) ) { + $log[] = '+ The current user is a Super Admin.'; + if ( isset($grants['special:super_admin']) ) { + $has_access = $grants['special:super_admin']; + $log[] = sprintf("+ Custom permissions for Super Admin: %s.", $has_access ? 'ALLOW' : 'DENY'); + + $reason = sprintf( + 'The user "%3$s" is a Super Admin. The "%1$s" menu item is explicitly %2$s for Super Admin.', + $debug_title, + $has_access ? 'enabled' : 'disabled', + $user_login + ); + } else { + $has_access = true; + $log[] = '+ ALLOW access to everything by default.'; + + $reason = sprintf( + 'As a Super Admin, the user "%2$s" has access to the "%1$s" menu item by default.', + $debug_title, + $user_login + ); + } + } else if ( is_null($has_access) ) { + $log[] = '- The current user is not a Super Admin, or this is not a Multisite install.'; + } + $this->wp_menu_editor->disable_virtual_caps = false; + + + if ( is_null($has_access) ) { + //Allow the user if at least one of their roles is allowed, + //or disallow if all their roles are forbidden. + //TODO: We could get the roles when starting menu generation and then just reuse them. + $roles = $this->wp_menu_editor->get_user_roles($user); + $log[] = sprintf( + '- Current user\'s role: %s', + !empty($roles) ? implode(', ', $roles) : 'N/A (not logged in?)' + ); + if ( empty($roles) ) { + $log[] = sprintf( + '- Current user\'s capabilities: %s', + implode(', ', array_keys(array_filter($user->allcaps))) + ); + } + + foreach ($roles as $role_id) { + if ( isset($grants['role:' . $role_id]) ) { + $role_has_access = $grants['role:' . $role_id]; + $log[] = sprintf( + '+ Permissions for the "%1$s" role: %2$s', + $role_id, + $role_has_access ? 'ALLOW' : 'DENY' + ); + if ( is_null($has_access) ){ + $has_access = $role_has_access; + $reason = sprintf( + 'The "%1$s" menu item is %2$s for the "%3$s" role.', + $debug_title, + $has_access ? 'enabled' : 'disabled', + $role_id + ); + } else { + $has_access = $has_access || $role_has_access; //Allow access if at least one role has access. + if ( $role_has_access ) { + $reason = sprintf( + 'The "%1$s" menu item is enabled for the "%2$s" role.', + $debug_title, + $role_id + ); + } + } + } else { + $log[] = sprintf('- No custom permissions for the "%s" role.', $role_id); + } + } + } + } + + if ( is_null($has_access) ) { + //There are no custom settings for this user. Check if + //they would be able to access the menu by default. + $required_capability = $item['access_level']; + + $log[] = '- There are no custom permissions for the current user or any of their roles.'; + $log[] = '- Checking the default required capability: ' . $required_capability; + + $this->wp_menu_editor->virtual_cap_mode = WPMenuEditor::DIRECTLY_GRANTED_VIRTUAL_CAPS; + //Cache capability checks because they're relatively slow (determined by profiling). + //Right now we can rely on capabilities not changing when this method is called, but that's not a safe + //assumption in general so we only use the cache in this specific case. + if ( isset($this->cached_user_caps[$required_capability]) ) { + $has_access = $this->cached_user_caps[$required_capability]; + } else { + $has_access = $user && $user->has_cap($required_capability); + $this->cached_user_caps[$required_capability] = $has_access; + } + + $log[] = sprintf( + '+ The current user %1$s the "%2$s" capability.', + $has_access ? 'HAS' : 'does not have', + htmlentities($required_capability) + ); + $this->wp_menu_editor->virtual_cap_mode = WPMenuEditor::ALL_VIRTUAL_CAPS; + + $reason = sprintf( + 'The user "%1$s" %2$s the "%3$s" capability that is required to access the "%4$s" menu item.', + $user_login, + $has_access ? 'has' : 'doesn\'t have', + $required_capability, + $debug_title + ); + } + + $log[] = '= Result: ' . ($has_access ? 'ALLOW' : 'DENY'); + + //Store the log in the item for debugging and configuration analysis. + if ( !isset($item['access_check_log']) ) { + $item['access_check_log'] = $log; + } else { + $item['access_check_log'] = array_merge($item['access_check_log'], $log); + } + + //Store the decision summary as well. + $item['access_decision_reason'] = $reason; + + return $has_access; + } + + /** + * Check if the current user has access to something. + * + * This is a general method for checking authorization based on a combination of + * actor-specific and capability-based permissions. + * + * @param array $grants List of grants as an [actorId => boolean] map. + * @param string|null $default_cap + * @param string|null $extra_cap + * @param bool $default_access + * @param int $flags + * @return bool + */ + public function check_current_user_access( + $grants = array(), + $default_cap = null, + $extra_cap = null, + $default_access = false, + $flags = AME_RC_ONLY_CUSTOM + ) { + static $is_multisite = null, $user = null, $user_login = ''; + if ( $is_multisite === null ) { + $is_multisite = is_multisite(); + } + if ( $user === null ) { + $user = wp_get_current_user(); + $user_login = $user->get('user_login'); + } + + //User-specific settings have the highest priority. + if ( isset($grants['user:' . $user_login]) ) { + return $grants['user:' . $user_login]; + } + + //Super Admins have access to *everything* unless explicitly denied. + $this->wp_menu_editor->disable_virtual_caps = true; + $is_super_admin = $is_multisite && is_super_admin($user->ID); + $this->wp_menu_editor->disable_virtual_caps = false; + + if ( $is_super_admin ) { + if ( isset($grants['special:super_admin']) ) { + return $grants['special:super_admin']; + } else { + return true; + } + } + + //Allow the user if at least one of their roles is allowed, + //or disallow if all their roles are forbidden. + $has_access = null; + $roles = $this->wp_menu_editor->get_user_roles($user); + foreach ($roles as $role_id) { + if ( !isset($grants['role:' . $role_id]) && ($flags & AME_RC_ONLY_CUSTOM) ) { + continue; + } + + if ( isset($grants['role:' . $role_id]) ) { + $role_has_access = $grants['role:' . $role_id]; + } else if ($flags & AME_RC_USE_DEFAULT_ACCESS) { + $role_has_access = $default_access; + } else { + throw new RuntimeException(sprintf( + "Can't determine default permissions for role \"%s\". Check the flags passed to %s().", + $role_id, + __FUNCTION__ + )); + } + + if ( is_null($has_access) ) { + $has_access = $role_has_access; + } else { + $has_access = $has_access || $role_has_access; + } + } + + if ( $has_access !== null ) { + return $has_access; + } + + //There are no custom settings for this user. Check if they have the capabilities. + if ( isset($default_cap) ) { + $this->wp_menu_editor->virtual_cap_mode = WPMenuEditor::DIRECTLY_GRANTED_VIRTUAL_CAPS; + //Cache capability checks because they're relatively slow. + if ( isset($this->cached_user_caps[$default_cap]) ) { + $has_access = $this->cached_user_caps[$default_cap]; + } else { + $has_access = $user && $user->has_cap($default_cap); + $this->cached_user_caps[$default_cap] = $has_access; + } + $this->wp_menu_editor->virtual_cap_mode = WPMenuEditor::ALL_VIRTUAL_CAPS; + } + + //The extra capability is an optional filter that's applied on top of other settings. + //TODO: Check extra cap even if there are user-specific permissions or they are a Super Admin. + if ( isset($extra_cap) && $has_access ) { + $this->wp_menu_editor->disable_virtual_caps = true; + if ( isset($this->cached_user_caps[$extra_cap]) ) { + $has_extra_cap = $this->cached_user_caps[$extra_cap]; + } else { + $has_extra_cap = $user && $user->has_cap($extra_cap); + $this->cached_user_caps[$extra_cap] = $has_extra_cap; + } + $this->wp_menu_editor->disable_virtual_caps = false; + + $has_access = $has_access && $has_extra_cap; + } + + if ( $has_access !== null ) { + return $has_access; + } + return $default_access; + } + + /** + * Give the user virtual caps that they'll need to access certain menu items and directly granted caps. + * + * @param array $virtual_caps + * @param WP_User $user + * @return array + */ + public function prepare_virtual_caps_for_user($virtual_caps, $user) { + $grant_keys = array(); + if ( $user ) { + if ( isset($user->user_login) ) { + $grant_keys[] = 'user:' . $user->user_login; + } + $roles = $this->wp_menu_editor->get_user_roles($user); + if ( !empty($roles) ) { + foreach ($roles as $role_id) { + $grant_keys[] = 'role:' . $role_id; + } + } + } + + //is_super_admin() will call has_cap on single-site installs. + $this->wp_menu_editor->disable_virtual_caps = true; + if ( is_multisite() && is_super_admin($user->ID) ) { + $grant_keys[] = 'special:super_admin'; + } + $this->wp_menu_editor->disable_virtual_caps = false; + + $modes = array(WPMenuEditor::ALL_VIRTUAL_CAPS, WPMenuEditor::DIRECTLY_GRANTED_VIRTUAL_CAPS); + foreach ($modes as $virtual_cap_mode) { + $stored_caps = $this->wp_menu_editor->get_virtual_caps($virtual_cap_mode); + $caps_to_grant = array(); + foreach ($grant_keys as $grant) { + if ( isset($stored_caps[$grant]) ) { + $caps_to_grant = array_merge($caps_to_grant, $stored_caps[$grant]); + } + } + if ( !empty($caps_to_grant) ) { + $virtual_caps[$virtual_cap_mode] = array_merge( + $virtual_caps[$virtual_cap_mode], + $caps_to_grant + ); + } + } + + return $virtual_caps; + } + + public function clear_site_specific_caches() { + $this->cached_user_caps = array(); + } + + /** + * Grant a role virtual caps it'll need to access certain menu items. + * + * @param array $capabilities Current role capabilities. + * @param string $required_cap The required capability. + * @param string $role_id Role name/slug. + * @return array Filtered capability list. + */ + function grant_virtual_caps_to_role($capabilities, /** @noinspection PhpUnusedParameterInspection */ $required_cap, $role_id){ + $wp_menu_editor = $this->wp_menu_editor; + + if ( $this->wp_menu_editor->disable_virtual_caps ) { + return $capabilities; + } + + $virtual_caps = $wp_menu_editor->get_virtual_caps($this->wp_menu_editor->virtual_cap_mode); + $grant_key = 'role:' . $role_id; + if ( isset($virtual_caps[$grant_key]) ) { + $capabilities = array_merge($capabilities, $virtual_caps[$grant_key]); + } + + return $capabilities; + } + + /** + * Hook for the internal current_user_can() function used by Admin Menu Editor. + * Enables us to use computed capabilities. + * + * @uses wsMenuEditorExtras::current_user_can_computed() + * + * @param bool $allow The return value of current_user_can($capablity). + * @param string $capability The capability to check for. + * @return bool Whether the user has the specified capability. + */ + function grant_computed_caps_to_current_user($allow, $capability) { + return $this->current_user_can_computed($capability, $allow); + } + + /** + * Check if the current user has the specified computed capability. Basically, this method + * implements a very limited subset of Boolean logic for use in capability checks. + * + * Supported operations: + * "capX" - Normal capability check. Returns true if the user has the capability "capX". + * "not:capX" - Logical NOT. Returns true if the user *doesn't* have "capX". + * "capX,capY" - Logical OR. Returns true if the user has at least one of "capX" or "capY". + * "capX+capY" - Logical AND. Returns true if the user has all the listed capabilities. + * + * Operator precedence: NOT, AND, OR. + * + * @uses current_user_can() Uses the capability checking function from WordPress core. + * + * @param string $capability + * @param bool $default + * @return bool + */ + private function current_user_can_computed($capability, $default = null) { + $or_operator = ','; + if ( strpos($capability, $or_operator) !== false ) { + $allow = false; + foreach(explode($or_operator, $capability) as $term) { + $allow = $allow || $this->current_user_can_computed($term); + } + return $allow; + } + + $and_operator = '+'; + if ( strpos($capability, $and_operator) !== false ) { + $allow = true; + foreach(explode($and_operator, $capability) as $term) { + $allow = $allow && $this->current_user_can_computed($term); + } + return $allow; + } + + $not_operator = 'not:'; + $length = strlen($not_operator); + if ( substr($capability, 0, $length) == $not_operator ) { + return ! $this->current_user_can_computed(substr($capability, $length)); + } + + $capability = trim($capability); + + //Special case to handle weird input like "capability+" and " ,capability". + if ($capability == '') { + return true; + } + + return isset($default) ? $default : current_user_can($capability, -1); + } + + /** + * @see WPMenuEditor::get_new_menu_grant_access() + * + * @param array $defaultGrantAccess Ignored. The default is completely replaced. + * @return array + */ + public function get_new_menu_grant_access(/** @noinspection PhpUnusedParameterInspection */$defaultGrantAccess = array()) { + $capsWereDisabled = $this->wp_menu_editor->disable_virtual_caps; + $this->wp_menu_editor->disable_virtual_caps = true; + + $grantAccess = array(); + + $roles = array_keys(ameRoleUtils::get_role_names()); + $currentUser = wp_get_current_user(); + $access = $this->wp_menu_editor->get_plugin_option('plugin_access'); + + if ( ($access === 'super_admin') && !is_multisite() ) { + //On a regular WordPress site, is_super_admin() just checks for the "delete_users" capability. + $access = 'delete_users'; + } + + if ( $access === 'super_admin' ) { + //Hide from everyone except Super Admins. + foreach($roles as $roleId) { + $grantAccess['role:' . $roleId] = false; + } + $grantAccess['special:super_admin'] = true; + } else if ( $access === 'specific_user' ) { + //Hide from everyone except a specific user. + $allowedUser = get_user_by('id', $this->wp_menu_editor->get_plugin_option('allowed_user_id')); + if ( $allowedUser && $allowedUser->exists() ) { + foreach($roles as $roleId) { + $grantAccess['role:' . $roleId] = false; + } + $grantAccess['user:' . $allowedUser->user_login] = true; + if ( is_multisite() ) { + $grantAccess['special:super_admin'] = false; + } + } + } else { + //Only show to roles that have a certain capability (usually "manage_options"). + $capability = apply_filters('admin_menu_editor-capability', $access); + $grantAccess['user:' . $currentUser->user_login] = current_user_can($capability); + foreach($roles as $roleId) { + $role = get_role($roleId); + if ( $role ) { + $grantAccess['role:' . $roleId] = $role->has_cap($capability); + } + } + } + + $this->wp_menu_editor->disable_virtual_caps = $capsWereDisabled; + return $grantAccess; + } + + function output_menu_dropzone($type = 'menu') { + printf( + '
', + ($type == 'menu') ? 'top_menu' : 'sub_menu' + ); + } + + function pro_page_title(){ + return 'Menu Editor Pro'; + } + + function pro_menu_title(){ + return 'Menu Editor Pro'; + } + + /** + * Callback for the 'admin_menu_editor_is_pro' hook. Always returns True to indicate that + * the Pro version extras are installed. + * + * @return bool True + */ + function is_pro_version(){ + return true; + } + + function license_ui_title() { + $title = 'Admin Menu Editor Pro License'; + return $title; + } + + function license_ui_logo() { + printf( + '

Logo

', + esc_attr(plugins_url('images/logo-medium.png', __FILE__)) + ); + } + + /** + * @param string|null $currentKey + * @param string|null $currentToken + * @param Wslm_ProductLicense $currentLicense + */ + public function license_ui_upgrade_link($currentKey = null, $currentToken = null, $currentLicense = null) { + if ( empty($currentKey) && empty($currentToken) ) { + return; + } + + $upgradeLink = 'http://adminmenueditor.com/upgrade-license/'; + $upgradeText = 'Upgrade or renew license'; + + if ( $currentLicense && ($currentLicense->getStatus() === 'expired') ) { + $upgradeLink = 'http://adminmenueditor.com/renew-license/'; + $upgradeText = 'Renew license'; + } + + if ( !empty($currentKey) ) { + $upgradeLink = add_query_arg('license_key', $currentKey, $upgradeLink); + } + $externalIcon = plugins_url('/images/external.png', $this->wp_menu_editor->plugin_file); + ?>

+ + + + External link icon + +

'; + $item['file'] = '#submenu-separator-' . ($separator_num++); + } + return $item; + } + + /** + * Generate the HTML for submenu icons. + * + * @param array $item A submenu item in the internal format. + * @param boolean $hasCustomIconUrl Whether the item has a custom icon URL. + * @return array Modified $item. + */ + public function add_submenu_icon_html($item, $hasCustomIconUrl) { + $enabled = $this->wp_menu_editor->get_plugin_option('submenu_icons_enabled'); + if ( empty($enabled) || ($enabled === 'never') ) { + //Icons are disabled. + return $item; + } + + if ( ($enabled == 'if_custom') && !$hasCustomIconUrl ) { + //Only enabled for icons with custom icons. + return $item; + } + + if ( !empty($item['separator']) ) { + //Separators can't have icons. + return $item; + } + + if (strpos($item['icon_url'], 'dashicons-') === 0) { + + $item['menu_title'] = sprintf( + '
%2$s', + esc_attr($item['icon_url']), + $item['menu_title'] + ); + $item['has_submenu_icon'] = true; + + } elseif (strpos($item['icon_url'], 'ame-fa-') === 0) { + + $item['menu_title'] = sprintf( + '
%2$s', + esc_attr($item['icon_url']), + $item['menu_title'] + ); + $item['has_submenu_icon'] = true; + + } elseif ( !empty($item['icon_url']) ) { + + $item['menu_title'] = sprintf( + '
Menu icon
%2$s', + esc_attr($item['icon_url']), + $item['menu_title'] + ); + $item['has_submenu_icon'] = true; + + } + + return $item; + } + + /** + * Remove Admin Menu Editor Pro from the list of plugins unless the current user + * is explicitly allowed to see it. + * + * @param array $plugins List of installed plugins. + * @return array Filtered list of plugins. + */ + public function filter_plugin_list($plugins) { + if ( !is_array($plugins) && !($plugins instanceof ArrayAccess) ) { + return $plugins; + } + + $allowed_user_id = $this->wp_menu_editor->get_plugin_option('plugins_page_allowed_user_id'); + if ( get_current_user_id() != $allowed_user_id ) { + unset($plugins[$this->wp_menu_editor->plugin_basename]); + + //Also hide the Branding and Toolbar Editor add-ons. + if ( defined('AME_BRANDING_ADD_ON_FILE') ) { + $brandingAddon = plugin_basename(constant('AME_BRANDING_ADD_ON_FILE')); + if ( $brandingAddon ) { + unset($plugins[$brandingAddon]); + } + } + if ( defined('WS_ADMIN_BAR_EDITOR_FILE') ) { + $toolbarAddon = plugin_basename(constant('WS_ADMIN_BAR_EDITOR_FILE')); + if ( $toolbarAddon ) { + unset($plugins[$toolbarAddon]); + } + } + } + return $plugins; + } + + + /** + * Apply Pro version menu customizations like shortcode support, "open in new window" support, + * submenu separators and so on. + * + * @param array $item Admin menu item in the internal format. + * @param string|null $parent_file + * @return array Modified admin menu item. + */ + public function apply_admin_menu_filters($item, $parent_file = null) { + //Allow the usage of shortcodes in the admin menu + $item = $this->do_shortcodes($item); + + //Flag menus that are set to open in a new window so that we can later find + //and modify them with JS. This is necessary because there is no practical + //way to intercept and modify the menu HTML with PHP alone. + $item = $this->flag_new_window_menus($item); + + //Handle pages that need to be displayed in a frame. + if ( isset($item['open_in']) ) { + if ( !empty($parent_file) ) { + $item = $this->create_framed_item($item, $parent_file); + } else { + $item = $this->create_framed_menu($item); + } + } + + //Handle submenu separators. + if ( current_filter() == 'custom_admin_submenu' ) { + $item = $this->create_submenu_separator($item); + } + + //Handle menus that display a WP page in the admin. + if ( $item['template_id'] === ameMenuItem::embeddedPageTemplateId ) { + $item = $this->create_embedded_wp_page($item, $parent_file); + } + + //Apply per-role visibility (cosmetic, not permissions). + if ( !empty($item['hidden_from_actor']) ) { + $item = $this->set_final_hidden_flag($item); + } + + if ( ameMenuItem::get($item, 'sub_type') === 'heading' ) { + $item['is_unvisitable'] = true; + //Mark headings as collapsible if that feature is enabled. + if ( $this->are_headings_collapsible ) { + $item['css_class'] .= ' ame-collapsible-heading'; + } + } + + return $item; + } + + /** + * @param array $customMenu + */ + public function on_menu_merged($customMenu = array()) { + //Note: If we never add per-heading collapsible settings, it would be more efficient + //to get rid of this hook and the "ame-collapsible-heading" class, and instead tell + //the helper script to enable the expand/collapse feature for all headings. + $this->are_headings_collapsible = ameUtils::get( + $customMenu, + array('menu_headings', 'collapsible'), + $this->are_headings_collapsible + ); + } + + /** + * @param array $customMenu + * @param WPMenuEditor $menuEditor + * @noinspection PhpUnusedParameterInspection The unused $customMenu param is part of the hook signature. + */ + public function on_menu_built($customMenu = array(), $menuEditor = null) { + if ( !$menuEditor ) { + return; + } + + if ( $menuEditor->is_custom_menu_deep() ) { + if ( did_action('admin_enqueue_scripts') ) { + $this->enqueue_third_level_script(); + } else { + add_action('admin_enqueue_scripts', array($this, 'enqueue_third_level_script')); + } + + add_action('in_admin_header', array($this, 'output_menu_ready_trigger')); + } + } + + public function enqueue_third_level_script() { + //Third level menu support. + wp_enqueue_auto_versioned_script( + 'ame-third-level-menus', + plugins_url('/extras/third-level-menus.js', __FILE__), + array('jquery', 'hoverIntent') + ); + } + + public function output_menu_ready_trigger() { + ?> + + getCss( + '', + $custom_menu['color_presets']['[global]'], + array(), + dirname(__FILE__) . '/extras/global-menu-color-template.txt' + ); + $css[] = $base_css; + $global_icon_colors = $generator->getIconColorScheme(); + } + + foreach($custom_menu['tree'] as &$item) { + if ( !isset($item['colors']) || empty($item['colors']) ) { + continue; + } + $colorized_menu_count++; + + //Each item needs to have a unique ID so we can target it in CSS. Using a class would be cleaner, + //but the selectors wouldn't have enough specificity to override WP defaults. + $id = ameMenuItem::get($item, 'hookname'); + if ( empty($id) || isset($used_ids[$id]) ) { + $id = (empty($id) ? 'ame-colorized-item' : $id) . '-'; + $id .= $colorized_menu_count . '-t' . time(); + $item['hookname'] = $id; + } + $used_ids[$id] = true; + + $sub_type = ameMenuItem::get($item, 'sub_type'); + if ( $sub_type === 'heading' ) { + $extra_selectors = array('.ame-menu-heading-item'); + } else { + $extra_selectors = array(); + } + + $item_css = $generator->getCss($id, $item['colors'], $extra_selectors); + if ( !empty($item_css) ) { + $css[] = sprintf( + '/* %1$s (%2$s) */', + str_replace('*/', ' ', ameMenuItem::get($item, 'menu_title', 'Untitled menu')), + str_replace('*/', ' ', ameMenuItem::get($item, 'file', '(no URL)')) + ); + $css[] = $item_css; + } + } + + if ( !empty($css) ) { + $css = implode("\n", $css); + $custom_menu['color_css'] = $css; + $custom_menu['color_css_modified'] = time(); + $custom_menu['icon_color_overrides'] = $global_icon_colors; + } else { + $custom_menu['color_css'] = ''; + $custom_menu['color_css_modified'] = 0; + $custom_menu['icon_color_overrides'] = null; + } + + return $custom_menu; + } + + /** + * Enqueue the user-defined menu color scheme, if any. + */ + public function enqueue_menu_color_style() { + try { + $custom_menu = $this->wp_menu_editor->load_custom_menu(); + } catch (InvalidMenuException $e) { + //This exception is best handled elsewhere. + return; + } + if ( empty($custom_menu) || empty($custom_menu['color_css']) ) { + return; + } + + wp_enqueue_style( + 'ame-custom-menu-colors', + add_query_arg( + 'ame_config_id', + $this->wp_menu_editor->get_loaded_menu_config_id(), + admin_url('admin-ajax.php?action=ame_output_menu_color_css') + ), + array(), + $custom_menu['color_css_modified'] + ); + + if ( isset($custom_menu['icon_color_overrides']) ) { + add_action('admin_head', array($this, 'override_menu_icon_color_scheme'), 9); + } + } + + /** + * Output menu color CSS for the current custom menu. + */ + public function ajax_output_menu_color_css() { + $config_id = null; + if ( isset($_GET['ame_config_id']) && !empty($_GET['ame_config_id']) ) { + $config_id = (string) ($_GET['ame_config_id']); + } + + try { + $custom_menu = $this->wp_menu_editor->load_custom_menu($config_id); + } catch (InvalidMenuException $e) { + return; + } + if ( empty($custom_menu) || empty($custom_menu['color_css']) ) { + return; + } + + header('Content-Type: text/css'); + header('X-Content-Type-Options: nosniff'); //No really IE, it's CSS. Honest. + + //Enable browser caching. + header('Cache-Control: public'); + header('Expires: Thu, 31 Dec ' . date('Y', strtotime('+1 year')) . ' 23:59:59 GMT'); + header('Pragma: cache'); + + echo $custom_menu['color_css']; + exit(); + } + + /** + * Replace the icon colors in the current admin color scheme with the custom colors + * set by the user. This is necessary to make SVG icons display in the right color. + */ + public function override_menu_icon_color_scheme() { + global $_wp_admin_css_colors; + + $custom_menu = $this->wp_menu_editor->load_custom_menu(); + if (!isset($custom_menu['icon_color_overrides'])) { + return; + } + + $color_scheme = get_user_option('admin_color'); + if ( empty( $_wp_admin_css_colors[ $color_scheme ] ) ) { + $color_scheme = 'fresh'; + } + + $custom_colors = array_merge( + array( + 'base' => '#a0a5aa', + 'focus' => '#00a0d2', + 'current' => '#fff', + ), + $custom_menu['icon_color_overrides'] + ); + + if ( isset($_wp_admin_css_colors[$color_scheme]) ) { + $_wp_admin_css_colors[$color_scheme]->icon_colors = $custom_colors; + } + } + + /** + * Enqueue various dependencies on all admin pages. + * + * It seems inelegant for one plugin to have a dozen different "admin_print_scripts" hooks + * (and it might hurt performance), so I've combined some of them into this method. + */ + public function enqueue_dashboard_deps() { + $this->enqueue_menu_color_style(); + $this->enqueue_fontawesome(); + + wp_enqueue_auto_versioned_style( + 'ame-pro-admin-styles', + plugins_url('extras/pro-admin-styles.css', __FILE__), + array() + ); + + $is_helper_needed = true; + $helper_data = array(); + + $config_id = $this->wp_menu_editor->get_loaded_menu_config_id(); + if ( !empty($config_id) ) { + $custom_menu = $this->wp_menu_editor->load_custom_menu($config_id); + + if ( ameUtils::get($custom_menu, array('menu_headings', 'textColorType')) === 'default' ) { + $is_helper_needed = true; + $helper_data['setHeadingHoverColor'] = true; + } + } + + if ( $is_helper_needed ) { + $this->wp_menu_editor->register_jquery_plugins(array('ame-jquery-cookie')); + + wp_enqueue_auto_versioned_script( + 'ame-pro-admin-helpers', + plugins_url('extras/pro-admin-helpers.js', __FILE__), + array('jquery', 'ame-jquery-cookie') + ); + + wp_add_inline_script( + 'ame-pro-admin-helpers', + sprintf('wsAmeProAdminHelperData = (%s);', json_encode($helper_data)), + 'before' + ); + } + } + + /** + * Enqueue the Font Awesome icon font & CSS. + */ + public function enqueue_fontawesome() { + wp_enqueue_auto_versioned_style( + 'ame-font-awesome', + plugins_url('extras/font-awesome/scss/font-awesome.css', __FILE__) + ); + } + + /** + * Add FA icons to top level menus. + * + * @param array $menu + * @return array + */ + public function add_menu_fa_icon($menu) { + $fa_prefix = 'ame-fa-'; + if ( strpos($menu['icon_url'], $fa_prefix) !== 0 ) { + return $menu; + } + + $icon = substr($menu['icon_url'], strlen($fa_prefix)); + + //Add a placeholder icon to force WP to generate a .wp-menu-image node. + $menu['wp_icon_url'] = 'dashicons-warning'; + + //Override the icon using CSS. + $menu['css_class'] .= ' ame-menu-fa ame-menu-fa-' . $icon; + + return $menu; + } + + public function add_fa_selector_tab($tabs) { + $tabs['ws_fontawesome_icons_tab'] = 'Font Awesome'; + return $tabs; + } + + public function output_fa_selector_tab() { + echo ''; + } + + private function get_available_fa_icons() { + $icon_list = dirname(__FILE__) . '/extras/font-awesome/scss/_ame-icons.scss'; + if ( !is_readable($icon_list) ) { + return array(); + } + + $scss = file_get_contents($icon_list); + if ( preg_match_all('@^#\{[^}]+?\}-(?P[\w\d\-]+)\s[^{]*?[^#]\{@m', $scss, $matches) ) { + return $matches['name']; + } + + return array(); + } + + public function display_addons() { + $addOns = $this->get_addons(); + if ( empty($addOns) ) { + return; + } + + echo 'Add-ons'; + foreach ($addOns as $slug => $addOn) { + printf('', esc_attr($slug)); + printf( + '', + esc_attr($addOn['detailsUrl']), + $addOn['name'] + ); + + if ( $addOn['isActive'] ) { + echo ''; + } else if ( $this->is_add_on_installed($slug) ) { + printf( + ' + ', + esc_attr(wp_create_nonce('ws_ame_activate_add_on-' . $slug)) + ); + } else if ( $this->can_download_add_on($slug) ) { + printf( + ' + ', + esc_attr(wp_create_nonce('ws_ame_install_add_on-' . $slug)) + ); + } else { + $license = $this->get_license(); + if ( $license->hasAddOn($slug) ) { + echo ''; + } else { + echo ''; + } + } + + echo ''; + } + echo '
+ + %s + + ActiveInstalledAvailableDownload not available - no access to updates.Not purchased
'; + } + + /** + * Get all available add-ons including those that the user hasn't purchased. + * + * @return array + */ + private function get_addons() { + return array( + 'wp-toolbar-editor' => array( + 'slug' => 'wp-toolbar-editor', + 'name' => 'WordPress Toolbar Editor', + 'detailsUrl' => 'https://adminmenueditor.com/toolbar-editor/', + 'isActive' => defined('WS_ADMIN_BAR_EDITOR_FILE'), + 'fileName' => 'wp-toolbar-editor/load.php', + ), + 'ame-branding-add-on' => array( + 'slug' => 'ame-branding-add-on', + 'name' => 'Branding', + 'detailsUrl' => 'https://adminmenueditor.com/', + 'isActive' => defined('AME_BRANDING_ADD_ON_FILE'), + 'fileName' => 'ame-branding-add-on/ame-branding-add-on.php', + ), + ); + } + + private function is_add_on_installed($slug) { + $addOns = $this->get_addons(); + if ( !isset($addOns[$slug]) ) { + return false; + } + return file_exists(WP_PLUGIN_DIR . '/' . $addOns[$slug]['fileName']); + } + + /** + * @param string $slug + * @return bool + */ + private function can_download_add_on($slug) { + $license = $this->get_license(); + if (!$license) { + return false; + } + return $license->canDownloadCurrentVersion() && $license->hasAddOn($slug); + } + + private function get_license() { + global $ameProLicenseManager; /** @var Wslm_LicenseManagerClient $ameProLicenseManager */ + if ( !$ameProLicenseManager ) { + return null; + } + return $ameProLicenseManager->getLicense(); + } + + public function enqueue_addon_scripts() { + wp_enqueue_auto_versioned_script( + 'ws-ame-add-on-management', + plugins_url('extras/add-on-management.js', __FILE__), + array('jquery') + ); + + wp_localize_script( + 'ws-ame-add-on-management', + 'wsAmeAddOnData', + array( + 'ajaxUrl' => self_admin_url('admin-ajax.php'), + ) + ); + } + + public function ajax_activate_addon() { + if ( !isset($_POST['slug']) || !is_string($_POST['slug']) ) { + exit('Error: No add-on slug specified'); + } + + $slug = $_POST['slug']; + check_ajax_referer('ws_ame_activate_add_on-' . $slug); + + $addOns = $this->get_addons(); + if ( !isset($addOns[$slug]) ) { + exit('Error: Add-on not found'); + } + + if ( !current_user_can('activate_plugins') ) { + exit('You cannot activate plugins'); + } + + $result = activate_plugin($addOns[$slug]['fileName']); + if ( is_wp_error($result) ) { + exit($result->get_error_code() . ': ' . $result->get_error_message()); + } + exit('OK'); + } + + public function ajax_install_addon() { + + } + + /** + * Prevent non-privileged users from accessing the special "All Settings" page even if + * they've been granted access to other pages that require the "manage_options" capability. + */ + public function disable_virtual_caps_on_all_options() { + //options.php also handles the saving of settings created with the Settings API, so we + //need to check if this is a direct request for options.php and not just a form submission. + $action = ameUtils::get($_POST, 'action', ameUtils::get($_GET, 'action', '')); + $option_page = ameUtils::get($_POST, 'option_page', ameUtils::get($_GET, 'option_page', 'options')); + + if ( ($action !== 'update') && (($option_page === 'options') || empty($option_page)) ) { + $this->wp_menu_editor->disable_virtual_caps = true; + add_action('admin_enqueue_scripts', array($this, 'enable_virtual_caps')); + } + } + + public function enable_virtual_caps() { + $this->wp_menu_editor->disable_virtual_caps = false; + } + + public function filter_available_modules($modules) { + $modules['plugin-visibility'] = array_merge( + $modules['plugin-visibility'], + array( + 'path' => AME_ROOT_DIR . '/extras/modules/plugin-visibility/plugin-visibility.php', + 'className' => 'amePluginVisibilityPro', + 'title' => 'Plugins', + ) + ); + $modules['role-editor'] = array( + 'path' => AME_ROOT_DIR . '/extras/modules/role-editor/load.php', + 'className' => 'ameRoleEditor', + 'requiredPhpVersion' => '5.3.6', + 'title' => 'Role Editor', + ); + + $modules['tweaks'] = array( + 'path' => AME_ROOT_DIR . '/extras/modules/tweaks/tweaks.php', + 'className' => 'ameTweakManager', + 'title' => 'Tweaks', + 'requiredPhpVersion' => '5.4', + ); + + $modules['separator-styles'] = array( + 'path' => AME_ROOT_DIR . '/extras/modules/separator-styles/ameMenuSeparatorStyler.php', + 'className' => 'ameMenuSeparatorStyler', + 'title' => 'Custom menu separator styles', + 'requiredPhpVersion' => '5.6', + ); + + return $modules; + } +} + +if ( isset($wp_menu_editor) && !defined('WP_UNINSTALL_PLUGIN') ) { + //Initialize extras + global $wsMenuEditorExtras; + $wsMenuEditorExtras = new wsMenuEditorExtras($wp_menu_editor); +} + +if ( !defined('IS_DEMO_MODE') && !defined('IS_MASTER_MODE') ) { + +//Load the custom update checker (requires PHP 5) +if ( (version_compare(PHP_VERSION, '5.0.0', '>=')) && isset($wp_menu_editor) ){ + require dirname(__FILE__) . '/plugin-updates/plugin-update-checker.php'; + $ameProUpdateChecker = PucFactory::buildUpdateChecker( + 'https://adminmenueditor.com/?get_metadata_for=admin-menu-editor-pro', + $wp_menu_editor->plugin_file, //Note: This variable is set in the framework constructor + 'admin-menu-editor-pro', + 12, //check every 12 hours + 'ame_pro_external_updates', //store bookkeeping info in this WP option + 'admin-menu-editor-mu.php' + ); + + //Hack. See PluginUpdateChecker::installHooks(). + function wsDisableAmeCron(){ + wp_clear_scheduled_hook('check_plugin_updates-admin-menu-editor-pro'); + } + register_deactivation_hook($wp_menu_editor->plugin_file, 'wsDisableAmeCron'); +} + +//Load the license manager. +require dirname(__FILE__) . '/license-manager/LicenseManager.php'; +global $ameProLicenseManager; +$ameProLicenseManager = new Wslm_LicenseManagerClient(array( + 'api_url' => 'https://adminmenueditor.com/licensing_api/', + 'product_slug' => 'admin-menu-editor-pro', + 'license_scope' => Wslm_LicenseManagerClient::LICENSE_SCOPE_NETWORK, + 'update_checker' => isset($ameProUpdateChecker) ? $ameProUpdateChecker : null, + 'token_history_size' => 5, +)); +if ( isset($wp_menu_editor) ) { + $ameLicensingUi = new Wslm_BasicPluginLicensingUI( + $ameProLicenseManager, + $wp_menu_editor->plugin_file, + isset($ameProUpdateChecker) ? $ameProUpdateChecker : null, + 'AME_LICENSE_KEY' + ); +} + +//Load WP-CLI commands. +if ( defined('WP_CLI') && WP_CLI && isset($wp_menu_editor) ) { + include dirname(__FILE__) . '/extras/wp-cli-integration.php'; +} + +} diff --git a/extras/.htaccess b/extras/.htaccess new file mode 100644 index 0000000..cb825d2 --- /dev/null +++ b/extras/.htaccess @@ -0,0 +1,23 @@ + + # Apache 2.4 + + Require all granted + + + # Apache 2.2 + + Order Allow,Deny + Allow from all + + + +# Apache 2.4 + + Require all denied + + +# Apache 2.2 + + Order Allow,Deny + Deny from all + \ No newline at end of file diff --git a/extras/_cat-nav.scss b/extras/_cat-nav.scss new file mode 100644 index 0000000..87b392f --- /dev/null +++ b/extras/_cat-nav.scss @@ -0,0 +1,62 @@ +$catNavHoverColor: #E5F3FF; +$catNavSelectedColor: #CCE8FF; + +.ame-cat-nav-item { + cursor: pointer; + margin: 0; +} + +.ame-cat-nav-item:hover { + background-color: $catNavHoverColor; +} + +.ame-selected-cat-nav-item { + background-color: $catNavSelectedColor; + box-shadow: 0px -1px 0px 0px #99D1FF, 0px 1px 0px 0px #99D1FF; + + //Don't change the color when hovering over a selected item. + &:hover { + background-color: $catNavSelectedColor; + } +} + +$navItemLevelPadding: 13px; +@for $level from 2 through 5 { + $padding: ($level - 2) * $navItemLevelPadding; + .ame-cat-nav-item.ame-cat-nav-level-#{$level} { + padding-left: $padding; + } +} + +.ame-cat-nav-toggle { + visibility: hidden; + display: inline-block; + + box-sizing: border-box; + + max-height: 100%; + width: 20px; + text-align: right; + vertical-align: middle; + + margin-right: 0.3em; + + &:after { + font-family: dashicons, sans-serif; + content: "\f345"; + } + + &:hover { + color: #3ECEF9; + } +} + +.ame-cat-nav-is-expanded > .ame-cat-nav-toggle { + &:after { + content: "\f347"; + } +} + +.ame-cat-nav-has-children > .ame-cat-nav-toggle { + visibility: visible; +} \ No newline at end of file diff --git a/extras/add-on-management.js b/extras/add-on-management.js new file mode 100644 index 0000000..41096c1 --- /dev/null +++ b/extras/add-on-management.js @@ -0,0 +1,37 @@ +/** + * @property {string} wsAmeAddOnData.ajaxUrl + */ + +jQuery(function ($) { + $('.ame-activate-add-on').on('click', function () { + var $button = $(this), + $addOn = $button.closest('.ame-add-on-item'), + $statusField = $addOn.find('.ame-add-on-status'); + + $button.prop('disabled', true).text('Activating...'); + $.post( + wsAmeAddOnData.ajaxUrl, + { + 'action': 'ws_ame_activate_add_on', + '_ajax_nonce': $button.data('nonce'), + 'slug': $addOn.data('slug') + }) + .success(function (result) { + $button.remove(); + if (result === 'OK') { + $statusField.text('Active'); + } else { + $statusField.text(result); + } + }) + .error(function (response) { + $button.remove(); + $statusField.text('Error. ' + response.statusText + ': ' + response.responseText); + }); + return false; + }); + + $('.ame-install-add-on').on('click', function () { + return false; + }); +}); diff --git a/extras/copy-permissions-dialog.php b/extras/copy-permissions-dialog.php new file mode 100644 index 0000000..1822f49 --- /dev/null +++ b/extras/copy-permissions-dialog.php @@ -0,0 +1,26 @@ + \ No newline at end of file diff --git a/extras/exportable-module.php b/extras/exportable-module.php new file mode 100644 index 0000000..f993e34 --- /dev/null +++ b/extras/exportable-module.php @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/extras/font-awesome/fonts/fontawesome-webfont.ttf b/extras/font-awesome/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..f221e50 Binary files /dev/null and b/extras/font-awesome/fonts/fontawesome-webfont.ttf differ diff --git a/extras/font-awesome/fonts/fontawesome-webfont.woff b/extras/font-awesome/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..6e7483c Binary files /dev/null and b/extras/font-awesome/fonts/fontawesome-webfont.woff differ diff --git a/extras/font-awesome/fonts/fontawesome-webfont.woff2 b/extras/font-awesome/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000..7eb74fd Binary files /dev/null and b/extras/font-awesome/fonts/fontawesome-webfont.woff2 differ diff --git a/extras/font-awesome/scss/_ame-icons.scss b/extras/font-awesome/scss/_ame-icons.scss new file mode 100644 index 0000000..10b6342 --- /dev/null +++ b/extras/font-awesome/scss/_ame-icons.scss @@ -0,0 +1,732 @@ +//This is like _icons.scss, but with different selectors. + +#{$ame-icon-image-prefix}-glass .wp-menu-image:before { content: $fa-var-glass; } +#{$ame-icon-image-prefix}-music .wp-menu-image:before { content: $fa-var-music; } +#{$ame-icon-image-prefix}-search .wp-menu-image:before { content: $fa-var-search; } +#{$ame-icon-image-prefix}-envelope-o .wp-menu-image:before { content: $fa-var-envelope-o; } +#{$ame-icon-image-prefix}-heart .wp-menu-image:before { content: $fa-var-heart; } +#{$ame-icon-image-prefix}-star .wp-menu-image:before { content: $fa-var-star; } +#{$ame-icon-image-prefix}-star-o .wp-menu-image:before { content: $fa-var-star-o; } +#{$ame-icon-image-prefix}-user .wp-menu-image:before { content: $fa-var-user; } +#{$ame-icon-image-prefix}-film .wp-menu-image:before { content: $fa-var-film; } +#{$ame-icon-image-prefix}-th-large .wp-menu-image:before { content: $fa-var-th-large; } +#{$ame-icon-image-prefix}-th .wp-menu-image:before { content: $fa-var-th; } +#{$ame-icon-image-prefix}-th-list .wp-menu-image:before { content: $fa-var-th-list; } +#{$ame-icon-image-prefix}-check .wp-menu-image:before { content: $fa-var-check; } +#{$ame-icon-image-prefix}-remove .wp-menu-image:before, +#{$ame-icon-image-prefix}-close .wp-menu-image:before, +#{$ame-icon-image-prefix}-times .wp-menu-image:before { content: $fa-var-times; } +#{$ame-icon-image-prefix}-search-plus .wp-menu-image:before { content: $fa-var-search-plus; } +#{$ame-icon-image-prefix}-search-minus .wp-menu-image:before { content: $fa-var-search-minus; } +#{$ame-icon-image-prefix}-power-off .wp-menu-image:before { content: $fa-var-power-off; } +#{$ame-icon-image-prefix}-signal .wp-menu-image:before { content: $fa-var-signal; } +#{$ame-icon-image-prefix}-gear .wp-menu-image:before, +#{$ame-icon-image-prefix}-cog .wp-menu-image:before { content: $fa-var-cog; } +#{$ame-icon-image-prefix}-trash-o .wp-menu-image:before { content: $fa-var-trash-o; } +#{$ame-icon-image-prefix}-home .wp-menu-image:before { content: $fa-var-home; } +#{$ame-icon-image-prefix}-file-o .wp-menu-image:before { content: $fa-var-file-o; } +#{$ame-icon-image-prefix}-clock-o .wp-menu-image:before { content: $fa-var-clock-o; } +#{$ame-icon-image-prefix}-road .wp-menu-image:before { content: $fa-var-road; } +#{$ame-icon-image-prefix}-download .wp-menu-image:before { content: $fa-var-download; } +#{$ame-icon-image-prefix}-arrow-circle-o-down .wp-menu-image:before { content: $fa-var-arrow-circle-o-down; } +#{$ame-icon-image-prefix}-arrow-circle-o-up .wp-menu-image:before { content: $fa-var-arrow-circle-o-up; } +#{$ame-icon-image-prefix}-inbox .wp-menu-image:before { content: $fa-var-inbox; } +#{$ame-icon-image-prefix}-play-circle-o .wp-menu-image:before { content: $fa-var-play-circle-o; } +#{$ame-icon-image-prefix}-rotate-right .wp-menu-image:before, +#{$ame-icon-image-prefix}-repeat .wp-menu-image:before { content: $fa-var-repeat; } +#{$ame-icon-image-prefix}-refresh .wp-menu-image:before { content: $fa-var-refresh; } +#{$ame-icon-image-prefix}-list-alt .wp-menu-image:before { content: $fa-var-list-alt; } +#{$ame-icon-image-prefix}-lock .wp-menu-image:before { content: $fa-var-lock; } +#{$ame-icon-image-prefix}-flag .wp-menu-image:before { content: $fa-var-flag; } +#{$ame-icon-image-prefix}-headphones .wp-menu-image:before { content: $fa-var-headphones; } +#{$ame-icon-image-prefix}-volume-off .wp-menu-image:before { content: $fa-var-volume-off; } +#{$ame-icon-image-prefix}-volume-down .wp-menu-image:before { content: $fa-var-volume-down; } +#{$ame-icon-image-prefix}-volume-up .wp-menu-image:before { content: $fa-var-volume-up; } +#{$ame-icon-image-prefix}-qrcode .wp-menu-image:before { content: $fa-var-qrcode; } +#{$ame-icon-image-prefix}-barcode .wp-menu-image:before { content: $fa-var-barcode; } +#{$ame-icon-image-prefix}-tag .wp-menu-image:before { content: $fa-var-tag; } +#{$ame-icon-image-prefix}-tags .wp-menu-image:before { content: $fa-var-tags; } +#{$ame-icon-image-prefix}-book .wp-menu-image:before { content: $fa-var-book; } +#{$ame-icon-image-prefix}-bookmark .wp-menu-image:before { content: $fa-var-bookmark; } +#{$ame-icon-image-prefix}-print .wp-menu-image:before { content: $fa-var-print; } +#{$ame-icon-image-prefix}-camera .wp-menu-image:before { content: $fa-var-camera; } +#{$ame-icon-image-prefix}-font .wp-menu-image:before { content: $fa-var-font; } +#{$ame-icon-image-prefix}-bold .wp-menu-image:before { content: $fa-var-bold; } +#{$ame-icon-image-prefix}-italic .wp-menu-image:before { content: $fa-var-italic; } +#{$ame-icon-image-prefix}-text-height .wp-menu-image:before { content: $fa-var-text-height; } +#{$ame-icon-image-prefix}-text-width .wp-menu-image:before { content: $fa-var-text-width; } +#{$ame-icon-image-prefix}-align-left .wp-menu-image:before { content: $fa-var-align-left; } +#{$ame-icon-image-prefix}-align-center .wp-menu-image:before { content: $fa-var-align-center; } +#{$ame-icon-image-prefix}-align-right .wp-menu-image:before { content: $fa-var-align-right; } +#{$ame-icon-image-prefix}-align-justify .wp-menu-image:before { content: $fa-var-align-justify; } +#{$ame-icon-image-prefix}-list .wp-menu-image:before { content: $fa-var-list; } +#{$ame-icon-image-prefix}-dedent .wp-menu-image:before, +#{$ame-icon-image-prefix}-outdent .wp-menu-image:before { content: $fa-var-outdent; } +#{$ame-icon-image-prefix}-indent .wp-menu-image:before { content: $fa-var-indent; } +#{$ame-icon-image-prefix}-video-camera .wp-menu-image:before { content: $fa-var-video-camera; } +#{$ame-icon-image-prefix}-photo .wp-menu-image:before, +#{$ame-icon-image-prefix}-image .wp-menu-image:before, +#{$ame-icon-image-prefix}-picture-o .wp-menu-image:before { content: $fa-var-picture-o; } +#{$ame-icon-image-prefix}-pencil .wp-menu-image:before { content: $fa-var-pencil; } +#{$ame-icon-image-prefix}-map-marker .wp-menu-image:before { content: $fa-var-map-marker; } +#{$ame-icon-image-prefix}-adjust .wp-menu-image:before { content: $fa-var-adjust; } +#{$ame-icon-image-prefix}-tint .wp-menu-image:before { content: $fa-var-tint; } +#{$ame-icon-image-prefix}-edit .wp-menu-image:before, +#{$ame-icon-image-prefix}-pencil-square-o .wp-menu-image:before { content: $fa-var-pencil-square-o; } +#{$ame-icon-image-prefix}-share-square-o .wp-menu-image:before { content: $fa-var-share-square-o; } +#{$ame-icon-image-prefix}-check-square-o .wp-menu-image:before { content: $fa-var-check-square-o; } +#{$ame-icon-image-prefix}-arrows .wp-menu-image:before { content: $fa-var-arrows; } +#{$ame-icon-image-prefix}-step-backward .wp-menu-image:before { content: $fa-var-step-backward; } +#{$ame-icon-image-prefix}-fast-backward .wp-menu-image:before { content: $fa-var-fast-backward; } +#{$ame-icon-image-prefix}-backward .wp-menu-image:before { content: $fa-var-backward; } +#{$ame-icon-image-prefix}-play .wp-menu-image:before { content: $fa-var-play; } +#{$ame-icon-image-prefix}-pause .wp-menu-image:before { content: $fa-var-pause; } +#{$ame-icon-image-prefix}-stop .wp-menu-image:before { content: $fa-var-stop; } +#{$ame-icon-image-prefix}-forward .wp-menu-image:before { content: $fa-var-forward; } +#{$ame-icon-image-prefix}-fast-forward .wp-menu-image:before { content: $fa-var-fast-forward; } +#{$ame-icon-image-prefix}-step-forward .wp-menu-image:before { content: $fa-var-step-forward; } +#{$ame-icon-image-prefix}-eject .wp-menu-image:before { content: $fa-var-eject; } +#{$ame-icon-image-prefix}-chevron-left .wp-menu-image:before { content: $fa-var-chevron-left; } +#{$ame-icon-image-prefix}-chevron-right .wp-menu-image:before { content: $fa-var-chevron-right; } +#{$ame-icon-image-prefix}-plus-circle .wp-menu-image:before { content: $fa-var-plus-circle; } +#{$ame-icon-image-prefix}-minus-circle .wp-menu-image:before { content: $fa-var-minus-circle; } +#{$ame-icon-image-prefix}-times-circle .wp-menu-image:before { content: $fa-var-times-circle; } +#{$ame-icon-image-prefix}-check-circle .wp-menu-image:before { content: $fa-var-check-circle; } +#{$ame-icon-image-prefix}-question-circle .wp-menu-image:before { content: $fa-var-question-circle; } +#{$ame-icon-image-prefix}-info-circle .wp-menu-image:before { content: $fa-var-info-circle; } +#{$ame-icon-image-prefix}-crosshairs .wp-menu-image:before { content: $fa-var-crosshairs; } +#{$ame-icon-image-prefix}-times-circle-o .wp-menu-image:before { content: $fa-var-times-circle-o; } +#{$ame-icon-image-prefix}-check-circle-o .wp-menu-image:before { content: $fa-var-check-circle-o; } +#{$ame-icon-image-prefix}-ban .wp-menu-image:before { content: $fa-var-ban; } +#{$ame-icon-image-prefix}-arrow-left .wp-menu-image:before { content: $fa-var-arrow-left; } +#{$ame-icon-image-prefix}-arrow-right .wp-menu-image:before { content: $fa-var-arrow-right; } +#{$ame-icon-image-prefix}-arrow-up .wp-menu-image:before { content: $fa-var-arrow-up; } +#{$ame-icon-image-prefix}-arrow-down .wp-menu-image:before { content: $fa-var-arrow-down; } +#{$ame-icon-image-prefix}-mail-forward .wp-menu-image:before, +#{$ame-icon-image-prefix}-share .wp-menu-image:before { content: $fa-var-share; } +#{$ame-icon-image-prefix}-expand .wp-menu-image:before { content: $fa-var-expand; } +#{$ame-icon-image-prefix}-compress .wp-menu-image:before { content: $fa-var-compress; } +#{$ame-icon-image-prefix}-plus .wp-menu-image:before { content: $fa-var-plus; } +#{$ame-icon-image-prefix}-minus .wp-menu-image:before { content: $fa-var-minus; } +#{$ame-icon-image-prefix}-asterisk .wp-menu-image:before { content: $fa-var-asterisk; } +#{$ame-icon-image-prefix}-exclamation-circle .wp-menu-image:before { content: $fa-var-exclamation-circle; } +#{$ame-icon-image-prefix}-gift .wp-menu-image:before { content: $fa-var-gift; } +#{$ame-icon-image-prefix}-leaf .wp-menu-image:before { content: $fa-var-leaf; } +#{$ame-icon-image-prefix}-fire .wp-menu-image:before { content: $fa-var-fire; } +#{$ame-icon-image-prefix}-eye .wp-menu-image:before { content: $fa-var-eye; } +#{$ame-icon-image-prefix}-eye-slash .wp-menu-image:before { content: $fa-var-eye-slash; } +#{$ame-icon-image-prefix}-warning .wp-menu-image:before, +#{$ame-icon-image-prefix}-exclamation-triangle .wp-menu-image:before { content: $fa-var-exclamation-triangle; } +#{$ame-icon-image-prefix}-plane .wp-menu-image:before { content: $fa-var-plane; } +#{$ame-icon-image-prefix}-calendar .wp-menu-image:before { content: $fa-var-calendar; } +#{$ame-icon-image-prefix}-random .wp-menu-image:before { content: $fa-var-random; } +#{$ame-icon-image-prefix}-comment .wp-menu-image:before { content: $fa-var-comment; } +#{$ame-icon-image-prefix}-magnet .wp-menu-image:before { content: $fa-var-magnet; } +#{$ame-icon-image-prefix}-chevron-up .wp-menu-image:before { content: $fa-var-chevron-up; } +#{$ame-icon-image-prefix}-chevron-down .wp-menu-image:before { content: $fa-var-chevron-down; } +#{$ame-icon-image-prefix}-retweet .wp-menu-image:before { content: $fa-var-retweet; } +#{$ame-icon-image-prefix}-shopping-cart .wp-menu-image:before { content: $fa-var-shopping-cart; } +#{$ame-icon-image-prefix}-folder .wp-menu-image:before { content: $fa-var-folder; } +#{$ame-icon-image-prefix}-folder-open .wp-menu-image:before { content: $fa-var-folder-open; } +#{$ame-icon-image-prefix}-arrows-v .wp-menu-image:before { content: $fa-var-arrows-v; } +#{$ame-icon-image-prefix}-arrows-h .wp-menu-image:before { content: $fa-var-arrows-h; } +#{$ame-icon-image-prefix}-bar-chart-o .wp-menu-image:before, +#{$ame-icon-image-prefix}-bar-chart .wp-menu-image:before { content: $fa-var-bar-chart; } +#{$ame-icon-image-prefix}-twitter-square .wp-menu-image:before { content: $fa-var-twitter-square; } +#{$ame-icon-image-prefix}-facebook-square .wp-menu-image:before { content: $fa-var-facebook-square; } +#{$ame-icon-image-prefix}-camera-retro .wp-menu-image:before { content: $fa-var-camera-retro; } +#{$ame-icon-image-prefix}-key .wp-menu-image:before { content: $fa-var-key; } +#{$ame-icon-image-prefix}-gears .wp-menu-image:before, +#{$ame-icon-image-prefix}-cogs .wp-menu-image:before { content: $fa-var-cogs; } +#{$ame-icon-image-prefix}-comments .wp-menu-image:before { content: $fa-var-comments; } +#{$ame-icon-image-prefix}-thumbs-o-up .wp-menu-image:before { content: $fa-var-thumbs-o-up; } +#{$ame-icon-image-prefix}-thumbs-o-down .wp-menu-image:before { content: $fa-var-thumbs-o-down; } +#{$ame-icon-image-prefix}-star-half .wp-menu-image:before { content: $fa-var-star-half; } +#{$ame-icon-image-prefix}-heart-o .wp-menu-image:before { content: $fa-var-heart-o; } +#{$ame-icon-image-prefix}-sign-out .wp-menu-image:before { content: $fa-var-sign-out; } +#{$ame-icon-image-prefix}-linkedin-square .wp-menu-image:before { content: $fa-var-linkedin-square; } +#{$ame-icon-image-prefix}-thumb-tack .wp-menu-image:before { content: $fa-var-thumb-tack; } +#{$ame-icon-image-prefix}-external-link .wp-menu-image:before { content: $fa-var-external-link; } +#{$ame-icon-image-prefix}-sign-in .wp-menu-image:before { content: $fa-var-sign-in; } +#{$ame-icon-image-prefix}-trophy .wp-menu-image:before { content: $fa-var-trophy; } +#{$ame-icon-image-prefix}-github-square .wp-menu-image:before { content: $fa-var-github-square; } +#{$ame-icon-image-prefix}-upload .wp-menu-image:before { content: $fa-var-upload; } +#{$ame-icon-image-prefix}-lemon-o .wp-menu-image:before { content: $fa-var-lemon-o; } +#{$ame-icon-image-prefix}-phone .wp-menu-image:before { content: $fa-var-phone; } +#{$ame-icon-image-prefix}-square-o .wp-menu-image:before { content: $fa-var-square-o; } +#{$ame-icon-image-prefix}-bookmark-o .wp-menu-image:before { content: $fa-var-bookmark-o; } +#{$ame-icon-image-prefix}-phone-square .wp-menu-image:before { content: $fa-var-phone-square; } +#{$ame-icon-image-prefix}-twitter .wp-menu-image:before { content: $fa-var-twitter; } +#{$ame-icon-image-prefix}-facebook-f .wp-menu-image:before, +#{$ame-icon-image-prefix}-facebook .wp-menu-image:before { content: $fa-var-facebook; } +#{$ame-icon-image-prefix}-github .wp-menu-image:before { content: $fa-var-github; } +#{$ame-icon-image-prefix}-unlock .wp-menu-image:before { content: $fa-var-unlock; } +#{$ame-icon-image-prefix}-credit-card .wp-menu-image:before { content: $fa-var-credit-card; } +#{$ame-icon-image-prefix}-feed .wp-menu-image:before, +#{$ame-icon-image-prefix}-rss .wp-menu-image:before { content: $fa-var-rss; } +#{$ame-icon-image-prefix}-hdd-o .wp-menu-image:before { content: $fa-var-hdd-o; } +#{$ame-icon-image-prefix}-bullhorn .wp-menu-image:before { content: $fa-var-bullhorn; } +#{$ame-icon-image-prefix}-bell .wp-menu-image:before { content: $fa-var-bell; } +#{$ame-icon-image-prefix}-certificate .wp-menu-image:before { content: $fa-var-certificate; } +#{$ame-icon-image-prefix}-hand-o-right .wp-menu-image:before { content: $fa-var-hand-o-right; } +#{$ame-icon-image-prefix}-hand-o-left .wp-menu-image:before { content: $fa-var-hand-o-left; } +#{$ame-icon-image-prefix}-hand-o-up .wp-menu-image:before { content: $fa-var-hand-o-up; } +#{$ame-icon-image-prefix}-hand-o-down .wp-menu-image:before { content: $fa-var-hand-o-down; } +#{$ame-icon-image-prefix}-arrow-circle-left .wp-menu-image:before { content: $fa-var-arrow-circle-left; } +#{$ame-icon-image-prefix}-arrow-circle-right .wp-menu-image:before { content: $fa-var-arrow-circle-right; } +#{$ame-icon-image-prefix}-arrow-circle-up .wp-menu-image:before { content: $fa-var-arrow-circle-up; } +#{$ame-icon-image-prefix}-arrow-circle-down .wp-menu-image:before { content: $fa-var-arrow-circle-down; } +#{$ame-icon-image-prefix}-globe .wp-menu-image:before { content: $fa-var-globe; } +#{$ame-icon-image-prefix}-wrench .wp-menu-image:before { content: $fa-var-wrench; } +#{$ame-icon-image-prefix}-tasks .wp-menu-image:before { content: $fa-var-tasks; } +#{$ame-icon-image-prefix}-filter .wp-menu-image:before { content: $fa-var-filter; } +#{$ame-icon-image-prefix}-briefcase .wp-menu-image:before { content: $fa-var-briefcase; } +#{$ame-icon-image-prefix}-arrows-alt .wp-menu-image:before { content: $fa-var-arrows-alt; } +#{$ame-icon-image-prefix}-group .wp-menu-image:before, +#{$ame-icon-image-prefix}-users .wp-menu-image:before { content: $fa-var-users; } +#{$ame-icon-image-prefix}-chain .wp-menu-image:before, +#{$ame-icon-image-prefix}-link .wp-menu-image:before { content: $fa-var-link; } +#{$ame-icon-image-prefix}-cloud .wp-menu-image:before { content: $fa-var-cloud; } +#{$ame-icon-image-prefix}-flask .wp-menu-image:before { content: $fa-var-flask; } +#{$ame-icon-image-prefix}-cut .wp-menu-image:before, +#{$ame-icon-image-prefix}-scissors .wp-menu-image:before { content: $fa-var-scissors; } +#{$ame-icon-image-prefix}-copy .wp-menu-image:before, +#{$ame-icon-image-prefix}-files-o .wp-menu-image:before { content: $fa-var-files-o; } +#{$ame-icon-image-prefix}-paperclip .wp-menu-image:before { content: $fa-var-paperclip; } +#{$ame-icon-image-prefix}-save .wp-menu-image:before, +#{$ame-icon-image-prefix}-floppy-o .wp-menu-image:before { content: $fa-var-floppy-o; } +#{$ame-icon-image-prefix}-square .wp-menu-image:before { content: $fa-var-square; } +#{$ame-icon-image-prefix}-navicon .wp-menu-image:before, +#{$ame-icon-image-prefix}-reorder .wp-menu-image:before, +#{$ame-icon-image-prefix}-bars .wp-menu-image:before { content: $fa-var-bars; } +#{$ame-icon-image-prefix}-list-ul .wp-menu-image:before { content: $fa-var-list-ul; } +#{$ame-icon-image-prefix}-list-ol .wp-menu-image:before { content: $fa-var-list-ol; } +#{$ame-icon-image-prefix}-strikethrough .wp-menu-image:before { content: $fa-var-strikethrough; } +#{$ame-icon-image-prefix}-underline .wp-menu-image:before { content: $fa-var-underline; } +#{$ame-icon-image-prefix}-table .wp-menu-image:before { content: $fa-var-table; } +#{$ame-icon-image-prefix}-magic .wp-menu-image:before { content: $fa-var-magic; } +#{$ame-icon-image-prefix}-truck .wp-menu-image:before { content: $fa-var-truck; } +#{$ame-icon-image-prefix}-pinterest .wp-menu-image:before { content: $fa-var-pinterest; } +#{$ame-icon-image-prefix}-pinterest-square .wp-menu-image:before { content: $fa-var-pinterest-square; } +#{$ame-icon-image-prefix}-google-plus-square .wp-menu-image:before { content: $fa-var-google-plus-square; } +#{$ame-icon-image-prefix}-google-plus .wp-menu-image:before { content: $fa-var-google-plus; } +#{$ame-icon-image-prefix}-money .wp-menu-image:before { content: $fa-var-money; } +#{$ame-icon-image-prefix}-caret-down .wp-menu-image:before { content: $fa-var-caret-down; } +#{$ame-icon-image-prefix}-caret-up .wp-menu-image:before { content: $fa-var-caret-up; } +#{$ame-icon-image-prefix}-caret-left .wp-menu-image:before { content: $fa-var-caret-left; } +#{$ame-icon-image-prefix}-caret-right .wp-menu-image:before { content: $fa-var-caret-right; } +#{$ame-icon-image-prefix}-columns .wp-menu-image:before { content: $fa-var-columns; } +#{$ame-icon-image-prefix}-unsorted .wp-menu-image:before, +#{$ame-icon-image-prefix}-sort .wp-menu-image:before { content: $fa-var-sort; } +#{$ame-icon-image-prefix}-sort-down .wp-menu-image:before, +#{$ame-icon-image-prefix}-sort-desc .wp-menu-image:before { content: $fa-var-sort-desc; } +#{$ame-icon-image-prefix}-sort-up .wp-menu-image:before, +#{$ame-icon-image-prefix}-sort-asc .wp-menu-image:before { content: $fa-var-sort-asc; } +#{$ame-icon-image-prefix}-envelope .wp-menu-image:before { content: $fa-var-envelope; } +#{$ame-icon-image-prefix}-linkedin .wp-menu-image:before { content: $fa-var-linkedin; } +#{$ame-icon-image-prefix}-rotate-left .wp-menu-image:before, +#{$ame-icon-image-prefix}-undo .wp-menu-image:before { content: $fa-var-undo; } +#{$ame-icon-image-prefix}-legal .wp-menu-image:before, +#{$ame-icon-image-prefix}-gavel .wp-menu-image:before { content: $fa-var-gavel; } +#{$ame-icon-image-prefix}-dashboard .wp-menu-image:before, +#{$ame-icon-image-prefix}-tachometer .wp-menu-image:before { content: $fa-var-tachometer; } +#{$ame-icon-image-prefix}-comment-o .wp-menu-image:before { content: $fa-var-comment-o; } +#{$ame-icon-image-prefix}-comments-o .wp-menu-image:before { content: $fa-var-comments-o; } +#{$ame-icon-image-prefix}-flash .wp-menu-image:before, +#{$ame-icon-image-prefix}-bolt .wp-menu-image:before { content: $fa-var-bolt; } +#{$ame-icon-image-prefix}-sitemap .wp-menu-image:before { content: $fa-var-sitemap; } +#{$ame-icon-image-prefix}-umbrella .wp-menu-image:before { content: $fa-var-umbrella; } +#{$ame-icon-image-prefix}-paste .wp-menu-image:before, +#{$ame-icon-image-prefix}-clipboard .wp-menu-image:before { content: $fa-var-clipboard; } +#{$ame-icon-image-prefix}-lightbulb-o .wp-menu-image:before { content: $fa-var-lightbulb-o; } +#{$ame-icon-image-prefix}-exchange .wp-menu-image:before { content: $fa-var-exchange; } +#{$ame-icon-image-prefix}-cloud-download .wp-menu-image:before { content: $fa-var-cloud-download; } +#{$ame-icon-image-prefix}-cloud-upload .wp-menu-image:before { content: $fa-var-cloud-upload; } +#{$ame-icon-image-prefix}-user-md .wp-menu-image:before { content: $fa-var-user-md; } +#{$ame-icon-image-prefix}-stethoscope .wp-menu-image:before { content: $fa-var-stethoscope; } +#{$ame-icon-image-prefix}-suitcase .wp-menu-image:before { content: $fa-var-suitcase; } +#{$ame-icon-image-prefix}-bell-o .wp-menu-image:before { content: $fa-var-bell-o; } +#{$ame-icon-image-prefix}-coffee .wp-menu-image:before { content: $fa-var-coffee; } +#{$ame-icon-image-prefix}-cutlery .wp-menu-image:before { content: $fa-var-cutlery; } +#{$ame-icon-image-prefix}-file-text-o .wp-menu-image:before { content: $fa-var-file-text-o; } +#{$ame-icon-image-prefix}-building-o .wp-menu-image:before { content: $fa-var-building-o; } +#{$ame-icon-image-prefix}-hospital-o .wp-menu-image:before { content: $fa-var-hospital-o; } +#{$ame-icon-image-prefix}-ambulance .wp-menu-image:before { content: $fa-var-ambulance; } +#{$ame-icon-image-prefix}-medkit .wp-menu-image:before { content: $fa-var-medkit; } +#{$ame-icon-image-prefix}-fighter-jet .wp-menu-image:before { content: $fa-var-fighter-jet; } +#{$ame-icon-image-prefix}-beer .wp-menu-image:before { content: $fa-var-beer; } +#{$ame-icon-image-prefix}-h-square .wp-menu-image:before { content: $fa-var-h-square; } +#{$ame-icon-image-prefix}-plus-square .wp-menu-image:before { content: $fa-var-plus-square; } +#{$ame-icon-image-prefix}-angle-double-left .wp-menu-image:before { content: $fa-var-angle-double-left; } +#{$ame-icon-image-prefix}-angle-double-right .wp-menu-image:before { content: $fa-var-angle-double-right; } +#{$ame-icon-image-prefix}-angle-double-up .wp-menu-image:before { content: $fa-var-angle-double-up; } +#{$ame-icon-image-prefix}-angle-double-down .wp-menu-image:before { content: $fa-var-angle-double-down; } +#{$ame-icon-image-prefix}-angle-left .wp-menu-image:before { content: $fa-var-angle-left; } +#{$ame-icon-image-prefix}-angle-right .wp-menu-image:before { content: $fa-var-angle-right; } +#{$ame-icon-image-prefix}-angle-up .wp-menu-image:before { content: $fa-var-angle-up; } +#{$ame-icon-image-prefix}-angle-down .wp-menu-image:before { content: $fa-var-angle-down; } +#{$ame-icon-image-prefix}-desktop .wp-menu-image:before { content: $fa-var-desktop; } +#{$ame-icon-image-prefix}-laptop .wp-menu-image:before { content: $fa-var-laptop; } +#{$ame-icon-image-prefix}-tablet .wp-menu-image:before { content: $fa-var-tablet; } +#{$ame-icon-image-prefix}-mobile-phone .wp-menu-image:before, +#{$ame-icon-image-prefix}-mobile .wp-menu-image:before { content: $fa-var-mobile; } +#{$ame-icon-image-prefix}-circle-o .wp-menu-image:before { content: $fa-var-circle-o; } +#{$ame-icon-image-prefix}-quote-left .wp-menu-image:before { content: $fa-var-quote-left; } +#{$ame-icon-image-prefix}-quote-right .wp-menu-image:before { content: $fa-var-quote-right; } +#{$ame-icon-image-prefix}-spinner .wp-menu-image:before { content: $fa-var-spinner; } +#{$ame-icon-image-prefix}-circle .wp-menu-image:before { content: $fa-var-circle; } +#{$ame-icon-image-prefix}-mail-reply .wp-menu-image:before, +#{$ame-icon-image-prefix}-reply .wp-menu-image:before { content: $fa-var-reply; } +#{$ame-icon-image-prefix}-github-alt .wp-menu-image:before { content: $fa-var-github-alt; } +#{$ame-icon-image-prefix}-folder-o .wp-menu-image:before { content: $fa-var-folder-o; } +#{$ame-icon-image-prefix}-folder-open-o .wp-menu-image:before { content: $fa-var-folder-open-o; } +#{$ame-icon-image-prefix}-smile-o .wp-menu-image:before { content: $fa-var-smile-o; } +#{$ame-icon-image-prefix}-frown-o .wp-menu-image:before { content: $fa-var-frown-o; } +#{$ame-icon-image-prefix}-meh-o .wp-menu-image:before { content: $fa-var-meh-o; } +#{$ame-icon-image-prefix}-gamepad .wp-menu-image:before { content: $fa-var-gamepad; } +#{$ame-icon-image-prefix}-keyboard-o .wp-menu-image:before { content: $fa-var-keyboard-o; } +#{$ame-icon-image-prefix}-flag-o .wp-menu-image:before { content: $fa-var-flag-o; } +#{$ame-icon-image-prefix}-flag-checkered .wp-menu-image:before { content: $fa-var-flag-checkered; } +#{$ame-icon-image-prefix}-terminal .wp-menu-image:before { content: $fa-var-terminal; } +#{$ame-icon-image-prefix}-code .wp-menu-image:before { content: $fa-var-code; } +#{$ame-icon-image-prefix}-mail-reply-all .wp-menu-image:before, +#{$ame-icon-image-prefix}-reply-all .wp-menu-image:before { content: $fa-var-reply-all; } +#{$ame-icon-image-prefix}-star-half-empty .wp-menu-image:before, +#{$ame-icon-image-prefix}-star-half-full .wp-menu-image:before, +#{$ame-icon-image-prefix}-star-half-o .wp-menu-image:before { content: $fa-var-star-half-o; } +#{$ame-icon-image-prefix}-location-arrow .wp-menu-image:before { content: $fa-var-location-arrow; } +#{$ame-icon-image-prefix}-crop .wp-menu-image:before { content: $fa-var-crop; } +#{$ame-icon-image-prefix}-code-fork .wp-menu-image:before { content: $fa-var-code-fork; } +#{$ame-icon-image-prefix}-unlink .wp-menu-image:before, +#{$ame-icon-image-prefix}-chain-broken .wp-menu-image:before { content: $fa-var-chain-broken; } +#{$ame-icon-image-prefix}-question .wp-menu-image:before { content: $fa-var-question; } +#{$ame-icon-image-prefix}-info .wp-menu-image:before { content: $fa-var-info; } +#{$ame-icon-image-prefix}-exclamation .wp-menu-image:before { content: $fa-var-exclamation; } +#{$ame-icon-image-prefix}-superscript .wp-menu-image:before { content: $fa-var-superscript; } +#{$ame-icon-image-prefix}-subscript .wp-menu-image:before { content: $fa-var-subscript; } +#{$ame-icon-image-prefix}-eraser .wp-menu-image:before { content: $fa-var-eraser; } +#{$ame-icon-image-prefix}-puzzle-piece .wp-menu-image:before { content: $fa-var-puzzle-piece; } +#{$ame-icon-image-prefix}-microphone .wp-menu-image:before { content: $fa-var-microphone; } +#{$ame-icon-image-prefix}-microphone-slash .wp-menu-image:before { content: $fa-var-microphone-slash; } +#{$ame-icon-image-prefix}-shield .wp-menu-image:before { content: $fa-var-shield; } +#{$ame-icon-image-prefix}-calendar-o .wp-menu-image:before { content: $fa-var-calendar-o; } +#{$ame-icon-image-prefix}-fire-extinguisher .wp-menu-image:before { content: $fa-var-fire-extinguisher; } +#{$ame-icon-image-prefix}-rocket .wp-menu-image:before { content: $fa-var-rocket; } +#{$ame-icon-image-prefix}-maxcdn .wp-menu-image:before { content: $fa-var-maxcdn; } +#{$ame-icon-image-prefix}-chevron-circle-left .wp-menu-image:before { content: $fa-var-chevron-circle-left; } +#{$ame-icon-image-prefix}-chevron-circle-right .wp-menu-image:before { content: $fa-var-chevron-circle-right; } +#{$ame-icon-image-prefix}-chevron-circle-up .wp-menu-image:before { content: $fa-var-chevron-circle-up; } +#{$ame-icon-image-prefix}-chevron-circle-down .wp-menu-image:before { content: $fa-var-chevron-circle-down; } +#{$ame-icon-image-prefix}-html5 .wp-menu-image:before { content: $fa-var-html5; } +#{$ame-icon-image-prefix}-css3 .wp-menu-image:before { content: $fa-var-css3; } +#{$ame-icon-image-prefix}-anchor .wp-menu-image:before { content: $fa-var-anchor; } +#{$ame-icon-image-prefix}-unlock-alt .wp-menu-image:before { content: $fa-var-unlock-alt; } +#{$ame-icon-image-prefix}-bullseye .wp-menu-image:before { content: $fa-var-bullseye; } +#{$ame-icon-image-prefix}-ellipsis-h .wp-menu-image:before { content: $fa-var-ellipsis-h; } +#{$ame-icon-image-prefix}-ellipsis-v .wp-menu-image:before { content: $fa-var-ellipsis-v; } +#{$ame-icon-image-prefix}-rss-square .wp-menu-image:before { content: $fa-var-rss-square; } +#{$ame-icon-image-prefix}-play-circle .wp-menu-image:before { content: $fa-var-play-circle; } +#{$ame-icon-image-prefix}-ticket .wp-menu-image:before { content: $fa-var-ticket; } +#{$ame-icon-image-prefix}-minus-square .wp-menu-image:before { content: $fa-var-minus-square; } +#{$ame-icon-image-prefix}-minus-square-o .wp-menu-image:before { content: $fa-var-minus-square-o; } +#{$ame-icon-image-prefix}-level-up .wp-menu-image:before { content: $fa-var-level-up; } +#{$ame-icon-image-prefix}-level-down .wp-menu-image:before { content: $fa-var-level-down; } +#{$ame-icon-image-prefix}-check-square .wp-menu-image:before { content: $fa-var-check-square; } +#{$ame-icon-image-prefix}-pencil-square .wp-menu-image:before { content: $fa-var-pencil-square; } +#{$ame-icon-image-prefix}-external-link-square .wp-menu-image:before { content: $fa-var-external-link-square; } +#{$ame-icon-image-prefix}-share-square .wp-menu-image:before { content: $fa-var-share-square; } +#{$ame-icon-image-prefix}-compass .wp-menu-image:before { content: $fa-var-compass; } +#{$ame-icon-image-prefix}-toggle-down .wp-menu-image:before, +#{$ame-icon-image-prefix}-caret-square-o-down .wp-menu-image:before { content: $fa-var-caret-square-o-down; } +#{$ame-icon-image-prefix}-toggle-up .wp-menu-image:before, +#{$ame-icon-image-prefix}-caret-square-o-up .wp-menu-image:before { content: $fa-var-caret-square-o-up; } +#{$ame-icon-image-prefix}-toggle-right .wp-menu-image:before, +#{$ame-icon-image-prefix}-caret-square-o-right .wp-menu-image:before { content: $fa-var-caret-square-o-right; } +#{$ame-icon-image-prefix}-euro .wp-menu-image:before, +#{$ame-icon-image-prefix}-eur .wp-menu-image:before { content: $fa-var-eur; } +#{$ame-icon-image-prefix}-gbp .wp-menu-image:before { content: $fa-var-gbp; } +#{$ame-icon-image-prefix}-dollar .wp-menu-image:before, +#{$ame-icon-image-prefix}-usd .wp-menu-image:before { content: $fa-var-usd; } +#{$ame-icon-image-prefix}-rupee .wp-menu-image:before, +#{$ame-icon-image-prefix}-inr .wp-menu-image:before { content: $fa-var-inr; } +#{$ame-icon-image-prefix}-cny .wp-menu-image:before, +#{$ame-icon-image-prefix}-rmb .wp-menu-image:before, +#{$ame-icon-image-prefix}-yen .wp-menu-image:before, +#{$ame-icon-image-prefix}-jpy .wp-menu-image:before { content: $fa-var-jpy; } +#{$ame-icon-image-prefix}-ruble .wp-menu-image:before, +#{$ame-icon-image-prefix}-rouble .wp-menu-image:before, +#{$ame-icon-image-prefix}-rub .wp-menu-image:before { content: $fa-var-rub; } +#{$ame-icon-image-prefix}-won .wp-menu-image:before, +#{$ame-icon-image-prefix}-krw .wp-menu-image:before { content: $fa-var-krw; } +#{$ame-icon-image-prefix}-bitcoin .wp-menu-image:before, +#{$ame-icon-image-prefix}-btc .wp-menu-image:before { content: $fa-var-btc; } +#{$ame-icon-image-prefix}-file .wp-menu-image:before { content: $fa-var-file; } +#{$ame-icon-image-prefix}-file-text .wp-menu-image:before { content: $fa-var-file-text; } +#{$ame-icon-image-prefix}-sort-alpha-asc .wp-menu-image:before { content: $fa-var-sort-alpha-asc; } +#{$ame-icon-image-prefix}-sort-alpha-desc .wp-menu-image:before { content: $fa-var-sort-alpha-desc; } +#{$ame-icon-image-prefix}-sort-amount-asc .wp-menu-image:before { content: $fa-var-sort-amount-asc; } +#{$ame-icon-image-prefix}-sort-amount-desc .wp-menu-image:before { content: $fa-var-sort-amount-desc; } +#{$ame-icon-image-prefix}-sort-numeric-asc .wp-menu-image:before { content: $fa-var-sort-numeric-asc; } +#{$ame-icon-image-prefix}-sort-numeric-desc .wp-menu-image:before { content: $fa-var-sort-numeric-desc; } +#{$ame-icon-image-prefix}-thumbs-up .wp-menu-image:before { content: $fa-var-thumbs-up; } +#{$ame-icon-image-prefix}-thumbs-down .wp-menu-image:before { content: $fa-var-thumbs-down; } +#{$ame-icon-image-prefix}-youtube-square .wp-menu-image:before { content: $fa-var-youtube-square; } +#{$ame-icon-image-prefix}-youtube .wp-menu-image:before { content: $fa-var-youtube; } +#{$ame-icon-image-prefix}-xing .wp-menu-image:before { content: $fa-var-xing; } +#{$ame-icon-image-prefix}-xing-square .wp-menu-image:before { content: $fa-var-xing-square; } +#{$ame-icon-image-prefix}-youtube-play .wp-menu-image:before { content: $fa-var-youtube-play; } +#{$ame-icon-image-prefix}-dropbox .wp-menu-image:before { content: $fa-var-dropbox; } +#{$ame-icon-image-prefix}-stack-overflow .wp-menu-image:before { content: $fa-var-stack-overflow; } +#{$ame-icon-image-prefix}-instagram .wp-menu-image:before { content: $fa-var-instagram; } +#{$ame-icon-image-prefix}-flickr .wp-menu-image:before { content: $fa-var-flickr; } +#{$ame-icon-image-prefix}-adn .wp-menu-image:before { content: $fa-var-adn; } +#{$ame-icon-image-prefix}-bitbucket .wp-menu-image:before { content: $fa-var-bitbucket; } +#{$ame-icon-image-prefix}-bitbucket-square .wp-menu-image:before { content: $fa-var-bitbucket-square; } +#{$ame-icon-image-prefix}-tumblr .wp-menu-image:before { content: $fa-var-tumblr; } +#{$ame-icon-image-prefix}-tumblr-square .wp-menu-image:before { content: $fa-var-tumblr-square; } +#{$ame-icon-image-prefix}-long-arrow-down .wp-menu-image:before { content: $fa-var-long-arrow-down; } +#{$ame-icon-image-prefix}-long-arrow-up .wp-menu-image:before { content: $fa-var-long-arrow-up; } +#{$ame-icon-image-prefix}-long-arrow-left .wp-menu-image:before { content: $fa-var-long-arrow-left; } +#{$ame-icon-image-prefix}-long-arrow-right .wp-menu-image:before { content: $fa-var-long-arrow-right; } +#{$ame-icon-image-prefix}-apple .wp-menu-image:before { content: $fa-var-apple; } +#{$ame-icon-image-prefix}-windows .wp-menu-image:before { content: $fa-var-windows; } +#{$ame-icon-image-prefix}-android .wp-menu-image:before { content: $fa-var-android; } +#{$ame-icon-image-prefix}-linux .wp-menu-image:before { content: $fa-var-linux; } +#{$ame-icon-image-prefix}-dribbble .wp-menu-image:before { content: $fa-var-dribbble; } +#{$ame-icon-image-prefix}-skype .wp-menu-image:before { content: $fa-var-skype; } +#{$ame-icon-image-prefix}-foursquare .wp-menu-image:before { content: $fa-var-foursquare; } +#{$ame-icon-image-prefix}-trello .wp-menu-image:before { content: $fa-var-trello; } +#{$ame-icon-image-prefix}-female .wp-menu-image:before { content: $fa-var-female; } +#{$ame-icon-image-prefix}-male .wp-menu-image:before { content: $fa-var-male; } +#{$ame-icon-image-prefix}-gittip .wp-menu-image:before, +#{$ame-icon-image-prefix}-gratipay .wp-menu-image:before { content: $fa-var-gratipay; } +#{$ame-icon-image-prefix}-sun-o .wp-menu-image:before { content: $fa-var-sun-o; } +#{$ame-icon-image-prefix}-moon-o .wp-menu-image:before { content: $fa-var-moon-o; } +#{$ame-icon-image-prefix}-archive .wp-menu-image:before { content: $fa-var-archive; } +#{$ame-icon-image-prefix}-bug .wp-menu-image:before { content: $fa-var-bug; } +#{$ame-icon-image-prefix}-vk .wp-menu-image:before { content: $fa-var-vk; } +#{$ame-icon-image-prefix}-weibo .wp-menu-image:before { content: $fa-var-weibo; } +#{$ame-icon-image-prefix}-renren .wp-menu-image:before { content: $fa-var-renren; } +#{$ame-icon-image-prefix}-pagelines .wp-menu-image:before { content: $fa-var-pagelines; } +#{$ame-icon-image-prefix}-stack-exchange .wp-menu-image:before { content: $fa-var-stack-exchange; } +#{$ame-icon-image-prefix}-arrow-circle-o-right .wp-menu-image:before { content: $fa-var-arrow-circle-o-right; } +#{$ame-icon-image-prefix}-arrow-circle-o-left .wp-menu-image:before { content: $fa-var-arrow-circle-o-left; } +#{$ame-icon-image-prefix}-toggle-left .wp-menu-image:before, +#{$ame-icon-image-prefix}-caret-square-o-left .wp-menu-image:before { content: $fa-var-caret-square-o-left; } +#{$ame-icon-image-prefix}-dot-circle-o .wp-menu-image:before { content: $fa-var-dot-circle-o; } +#{$ame-icon-image-prefix}-wheelchair .wp-menu-image:before { content: $fa-var-wheelchair; } +#{$ame-icon-image-prefix}-vimeo-square .wp-menu-image:before { content: $fa-var-vimeo-square; } +#{$ame-icon-image-prefix}-turkish-lira .wp-menu-image:before, +#{$ame-icon-image-prefix}-try .wp-menu-image:before { content: $fa-var-try; } +#{$ame-icon-image-prefix}-plus-square-o .wp-menu-image:before { content: $fa-var-plus-square-o; } +#{$ame-icon-image-prefix}-space-shuttle .wp-menu-image:before { content: $fa-var-space-shuttle; } +#{$ame-icon-image-prefix}-slack .wp-menu-image:before { content: $fa-var-slack; } +#{$ame-icon-image-prefix}-envelope-square .wp-menu-image:before { content: $fa-var-envelope-square; } +#{$ame-icon-image-prefix}-wordpress .wp-menu-image:before { content: $fa-var-wordpress; } +#{$ame-icon-image-prefix}-openid .wp-menu-image:before { content: $fa-var-openid; } +#{$ame-icon-image-prefix}-institution .wp-menu-image:before, +#{$ame-icon-image-prefix}-bank .wp-menu-image:before, +#{$ame-icon-image-prefix}-university .wp-menu-image:before { content: $fa-var-university; } +#{$ame-icon-image-prefix}-mortar-board .wp-menu-image:before, +#{$ame-icon-image-prefix}-graduation-cap .wp-menu-image:before { content: $fa-var-graduation-cap; } +#{$ame-icon-image-prefix}-yahoo .wp-menu-image:before { content: $fa-var-yahoo; } +#{$ame-icon-image-prefix}-google .wp-menu-image:before { content: $fa-var-google; } +#{$ame-icon-image-prefix}-reddit .wp-menu-image:before { content: $fa-var-reddit; } +#{$ame-icon-image-prefix}-reddit-square .wp-menu-image:before { content: $fa-var-reddit-square; } +#{$ame-icon-image-prefix}-stumbleupon-circle .wp-menu-image:before { content: $fa-var-stumbleupon-circle; } +#{$ame-icon-image-prefix}-stumbleupon .wp-menu-image:before { content: $fa-var-stumbleupon; } +#{$ame-icon-image-prefix}-delicious .wp-menu-image:before { content: $fa-var-delicious; } +#{$ame-icon-image-prefix}-digg .wp-menu-image:before { content: $fa-var-digg; } +#{$ame-icon-image-prefix}-pied-piper-pp .wp-menu-image:before { content: $fa-var-pied-piper-pp; } +#{$ame-icon-image-prefix}-pied-piper-alt .wp-menu-image:before { content: $fa-var-pied-piper-alt; } +#{$ame-icon-image-prefix}-drupal .wp-menu-image:before { content: $fa-var-drupal; } +#{$ame-icon-image-prefix}-joomla .wp-menu-image:before { content: $fa-var-joomla; } +#{$ame-icon-image-prefix}-language .wp-menu-image:before { content: $fa-var-language; } +#{$ame-icon-image-prefix}-fax .wp-menu-image:before { content: $fa-var-fax; } +#{$ame-icon-image-prefix}-building .wp-menu-image:before { content: $fa-var-building; } +#{$ame-icon-image-prefix}-child .wp-menu-image:before { content: $fa-var-child; } +#{$ame-icon-image-prefix}-paw .wp-menu-image:before { content: $fa-var-paw; } +#{$ame-icon-image-prefix}-spoon .wp-menu-image:before { content: $fa-var-spoon; } +#{$ame-icon-image-prefix}-cube .wp-menu-image:before { content: $fa-var-cube; } +#{$ame-icon-image-prefix}-cubes .wp-menu-image:before { content: $fa-var-cubes; } +#{$ame-icon-image-prefix}-behance .wp-menu-image:before { content: $fa-var-behance; } +#{$ame-icon-image-prefix}-behance-square .wp-menu-image:before { content: $fa-var-behance-square; } +#{$ame-icon-image-prefix}-steam .wp-menu-image:before { content: $fa-var-steam; } +#{$ame-icon-image-prefix}-steam-square .wp-menu-image:before { content: $fa-var-steam-square; } +#{$ame-icon-image-prefix}-recycle .wp-menu-image:before { content: $fa-var-recycle; } +#{$ame-icon-image-prefix}-automobile .wp-menu-image:before, +#{$ame-icon-image-prefix}-car .wp-menu-image:before { content: $fa-var-car; } +#{$ame-icon-image-prefix}-cab .wp-menu-image:before, +#{$ame-icon-image-prefix}-taxi .wp-menu-image:before { content: $fa-var-taxi; } +#{$ame-icon-image-prefix}-tree .wp-menu-image:before { content: $fa-var-tree; } +#{$ame-icon-image-prefix}-spotify .wp-menu-image:before { content: $fa-var-spotify; } +#{$ame-icon-image-prefix}-deviantart .wp-menu-image:before { content: $fa-var-deviantart; } +#{$ame-icon-image-prefix}-soundcloud .wp-menu-image:before { content: $fa-var-soundcloud; } +#{$ame-icon-image-prefix}-database .wp-menu-image:before { content: $fa-var-database; } +#{$ame-icon-image-prefix}-file-pdf-o .wp-menu-image:before { content: $fa-var-file-pdf-o; } +#{$ame-icon-image-prefix}-file-word-o .wp-menu-image:before { content: $fa-var-file-word-o; } +#{$ame-icon-image-prefix}-file-excel-o .wp-menu-image:before { content: $fa-var-file-excel-o; } +#{$ame-icon-image-prefix}-file-powerpoint-o .wp-menu-image:before { content: $fa-var-file-powerpoint-o; } +#{$ame-icon-image-prefix}-file-photo-o .wp-menu-image:before, +#{$ame-icon-image-prefix}-file-picture-o .wp-menu-image:before, +#{$ame-icon-image-prefix}-file-image-o .wp-menu-image:before { content: $fa-var-file-image-o; } +#{$ame-icon-image-prefix}-file-zip-o .wp-menu-image:before, +#{$ame-icon-image-prefix}-file-archive-o .wp-menu-image:before { content: $fa-var-file-archive-o; } +#{$ame-icon-image-prefix}-file-sound-o .wp-menu-image:before, +#{$ame-icon-image-prefix}-file-audio-o .wp-menu-image:before { content: $fa-var-file-audio-o; } +#{$ame-icon-image-prefix}-file-movie-o .wp-menu-image:before, +#{$ame-icon-image-prefix}-file-video-o .wp-menu-image:before { content: $fa-var-file-video-o; } +#{$ame-icon-image-prefix}-file-code-o .wp-menu-image:before { content: $fa-var-file-code-o; } +#{$ame-icon-image-prefix}-vine .wp-menu-image:before { content: $fa-var-vine; } +#{$ame-icon-image-prefix}-codepen .wp-menu-image:before { content: $fa-var-codepen; } +#{$ame-icon-image-prefix}-jsfiddle .wp-menu-image:before { content: $fa-var-jsfiddle; } +#{$ame-icon-image-prefix}-life-bouy .wp-menu-image:before, +#{$ame-icon-image-prefix}-life-buoy .wp-menu-image:before, +#{$ame-icon-image-prefix}-life-saver .wp-menu-image:before, +#{$ame-icon-image-prefix}-support .wp-menu-image:before, +#{$ame-icon-image-prefix}-life-ring .wp-menu-image:before { content: $fa-var-life-ring; } +#{$ame-icon-image-prefix}-circle-o-notch .wp-menu-image:before { content: $fa-var-circle-o-notch; } +#{$ame-icon-image-prefix}-ra .wp-menu-image:before, +#{$ame-icon-image-prefix}-resistance .wp-menu-image:before, +#{$ame-icon-image-prefix}-rebel .wp-menu-image:before { content: $fa-var-rebel; } +#{$ame-icon-image-prefix}-ge .wp-menu-image:before, +#{$ame-icon-image-prefix}-empire .wp-menu-image:before { content: $fa-var-empire; } +#{$ame-icon-image-prefix}-git-square .wp-menu-image:before { content: $fa-var-git-square; } +#{$ame-icon-image-prefix}-git .wp-menu-image:before { content: $fa-var-git; } +#{$ame-icon-image-prefix}-y-combinator-square .wp-menu-image:before, +#{$ame-icon-image-prefix}-yc-square .wp-menu-image:before, +#{$ame-icon-image-prefix}-hacker-news .wp-menu-image:before { content: $fa-var-hacker-news; } +#{$ame-icon-image-prefix}-tencent-weibo .wp-menu-image:before { content: $fa-var-tencent-weibo; } +#{$ame-icon-image-prefix}-qq .wp-menu-image:before { content: $fa-var-qq; } +#{$ame-icon-image-prefix}-wechat .wp-menu-image:before, +#{$ame-icon-image-prefix}-weixin .wp-menu-image:before { content: $fa-var-weixin; } +#{$ame-icon-image-prefix}-send .wp-menu-image:before, +#{$ame-icon-image-prefix}-paper-plane .wp-menu-image:before { content: $fa-var-paper-plane; } +#{$ame-icon-image-prefix}-send-o .wp-menu-image:before, +#{$ame-icon-image-prefix}-paper-plane-o .wp-menu-image:before { content: $fa-var-paper-plane-o; } +#{$ame-icon-image-prefix}-history .wp-menu-image:before { content: $fa-var-history; } +#{$ame-icon-image-prefix}-circle-thin .wp-menu-image:before { content: $fa-var-circle-thin; } +#{$ame-icon-image-prefix}-header .wp-menu-image:before { content: $fa-var-header; } +#{$ame-icon-image-prefix}-paragraph .wp-menu-image:before { content: $fa-var-paragraph; } +#{$ame-icon-image-prefix}-sliders .wp-menu-image:before { content: $fa-var-sliders; } +#{$ame-icon-image-prefix}-share-alt .wp-menu-image:before { content: $fa-var-share-alt; } +#{$ame-icon-image-prefix}-share-alt-square .wp-menu-image:before { content: $fa-var-share-alt-square; } +#{$ame-icon-image-prefix}-bomb .wp-menu-image:before { content: $fa-var-bomb; } +#{$ame-icon-image-prefix}-soccer-ball-o .wp-menu-image:before, +#{$ame-icon-image-prefix}-futbol-o .wp-menu-image:before { content: $fa-var-futbol-o; } +#{$ame-icon-image-prefix}-tty .wp-menu-image:before { content: $fa-var-tty; } +#{$ame-icon-image-prefix}-binoculars .wp-menu-image:before { content: $fa-var-binoculars; } +#{$ame-icon-image-prefix}-plug .wp-menu-image:before { content: $fa-var-plug; } +#{$ame-icon-image-prefix}-slideshare .wp-menu-image:before { content: $fa-var-slideshare; } +#{$ame-icon-image-prefix}-twitch .wp-menu-image:before { content: $fa-var-twitch; } +#{$ame-icon-image-prefix}-yelp .wp-menu-image:before { content: $fa-var-yelp; } +#{$ame-icon-image-prefix}-newspaper-o .wp-menu-image:before { content: $fa-var-newspaper-o; } +#{$ame-icon-image-prefix}-wifi .wp-menu-image:before { content: $fa-var-wifi; } +#{$ame-icon-image-prefix}-calculator .wp-menu-image:before { content: $fa-var-calculator; } +#{$ame-icon-image-prefix}-paypal .wp-menu-image:before { content: $fa-var-paypal; } +#{$ame-icon-image-prefix}-google-wallet .wp-menu-image:before { content: $fa-var-google-wallet; } +#{$ame-icon-image-prefix}-cc-visa .wp-menu-image:before { content: $fa-var-cc-visa; } +#{$ame-icon-image-prefix}-cc-mastercard .wp-menu-image:before { content: $fa-var-cc-mastercard; } +#{$ame-icon-image-prefix}-cc-discover .wp-menu-image:before { content: $fa-var-cc-discover; } +#{$ame-icon-image-prefix}-cc-amex .wp-menu-image:before { content: $fa-var-cc-amex; } +#{$ame-icon-image-prefix}-cc-paypal .wp-menu-image:before { content: $fa-var-cc-paypal; } +#{$ame-icon-image-prefix}-cc-stripe .wp-menu-image:before { content: $fa-var-cc-stripe; } +#{$ame-icon-image-prefix}-bell-slash .wp-menu-image:before { content: $fa-var-bell-slash; } +#{$ame-icon-image-prefix}-bell-slash-o .wp-menu-image:before { content: $fa-var-bell-slash-o; } +#{$ame-icon-image-prefix}-trash .wp-menu-image:before { content: $fa-var-trash; } +#{$ame-icon-image-prefix}-copyright .wp-menu-image:before { content: $fa-var-copyright; } +#{$ame-icon-image-prefix}-at .wp-menu-image:before { content: $fa-var-at; } +#{$ame-icon-image-prefix}-eyedropper .wp-menu-image:before { content: $fa-var-eyedropper; } +#{$ame-icon-image-prefix}-paint-brush .wp-menu-image:before { content: $fa-var-paint-brush; } +#{$ame-icon-image-prefix}-birthday-cake .wp-menu-image:before { content: $fa-var-birthday-cake; } +#{$ame-icon-image-prefix}-area-chart .wp-menu-image:before { content: $fa-var-area-chart; } +#{$ame-icon-image-prefix}-pie-chart .wp-menu-image:before { content: $fa-var-pie-chart; } +#{$ame-icon-image-prefix}-line-chart .wp-menu-image:before { content: $fa-var-line-chart; } +#{$ame-icon-image-prefix}-lastfm .wp-menu-image:before { content: $fa-var-lastfm; } +#{$ame-icon-image-prefix}-lastfm-square .wp-menu-image:before { content: $fa-var-lastfm-square; } +#{$ame-icon-image-prefix}-toggle-off .wp-menu-image:before { content: $fa-var-toggle-off; } +#{$ame-icon-image-prefix}-toggle-on .wp-menu-image:before { content: $fa-var-toggle-on; } +#{$ame-icon-image-prefix}-bicycle .wp-menu-image:before { content: $fa-var-bicycle; } +#{$ame-icon-image-prefix}-bus .wp-menu-image:before { content: $fa-var-bus; } +#{$ame-icon-image-prefix}-ioxhost .wp-menu-image:before { content: $fa-var-ioxhost; } +#{$ame-icon-image-prefix}-angellist .wp-menu-image:before { content: $fa-var-angellist; } +#{$ame-icon-image-prefix}-cc .wp-menu-image:before { content: $fa-var-cc; } +#{$ame-icon-image-prefix}-shekel .wp-menu-image:before, +#{$ame-icon-image-prefix}-sheqel .wp-menu-image:before, +#{$ame-icon-image-prefix}-ils .wp-menu-image:before { content: $fa-var-ils; } +#{$ame-icon-image-prefix}-meanpath .wp-menu-image:before { content: $fa-var-meanpath; } +#{$ame-icon-image-prefix}-buysellads .wp-menu-image:before { content: $fa-var-buysellads; } +#{$ame-icon-image-prefix}-connectdevelop .wp-menu-image:before { content: $fa-var-connectdevelop; } +#{$ame-icon-image-prefix}-dashcube .wp-menu-image:before { content: $fa-var-dashcube; } +#{$ame-icon-image-prefix}-forumbee .wp-menu-image:before { content: $fa-var-forumbee; } +#{$ame-icon-image-prefix}-leanpub .wp-menu-image:before { content: $fa-var-leanpub; } +#{$ame-icon-image-prefix}-sellsy .wp-menu-image:before { content: $fa-var-sellsy; } +#{$ame-icon-image-prefix}-shirtsinbulk .wp-menu-image:before { content: $fa-var-shirtsinbulk; } +#{$ame-icon-image-prefix}-simplybuilt .wp-menu-image:before { content: $fa-var-simplybuilt; } +#{$ame-icon-image-prefix}-skyatlas .wp-menu-image:before { content: $fa-var-skyatlas; } +#{$ame-icon-image-prefix}-cart-plus .wp-menu-image:before { content: $fa-var-cart-plus; } +#{$ame-icon-image-prefix}-cart-arrow-down .wp-menu-image:before { content: $fa-var-cart-arrow-down; } +#{$ame-icon-image-prefix}-diamond .wp-menu-image:before { content: $fa-var-diamond; } +#{$ame-icon-image-prefix}-ship .wp-menu-image:before { content: $fa-var-ship; } +#{$ame-icon-image-prefix}-user-secret .wp-menu-image:before { content: $fa-var-user-secret; } +#{$ame-icon-image-prefix}-motorcycle .wp-menu-image:before { content: $fa-var-motorcycle; } +#{$ame-icon-image-prefix}-street-view .wp-menu-image:before { content: $fa-var-street-view; } +#{$ame-icon-image-prefix}-heartbeat .wp-menu-image:before { content: $fa-var-heartbeat; } +#{$ame-icon-image-prefix}-venus .wp-menu-image:before { content: $fa-var-venus; } +#{$ame-icon-image-prefix}-mars .wp-menu-image:before { content: $fa-var-mars; } +#{$ame-icon-image-prefix}-mercury .wp-menu-image:before { content: $fa-var-mercury; } +#{$ame-icon-image-prefix}-intersex .wp-menu-image:before, +#{$ame-icon-image-prefix}-transgender .wp-menu-image:before { content: $fa-var-transgender; } +#{$ame-icon-image-prefix}-transgender-alt .wp-menu-image:before { content: $fa-var-transgender-alt; } +#{$ame-icon-image-prefix}-venus-double .wp-menu-image:before { content: $fa-var-venus-double; } +#{$ame-icon-image-prefix}-mars-double .wp-menu-image:before { content: $fa-var-mars-double; } +#{$ame-icon-image-prefix}-venus-mars .wp-menu-image:before { content: $fa-var-venus-mars; } +#{$ame-icon-image-prefix}-mars-stroke .wp-menu-image:before { content: $fa-var-mars-stroke; } +#{$ame-icon-image-prefix}-mars-stroke-v .wp-menu-image:before { content: $fa-var-mars-stroke-v; } +#{$ame-icon-image-prefix}-mars-stroke-h .wp-menu-image:before { content: $fa-var-mars-stroke-h; } +#{$ame-icon-image-prefix}-neuter .wp-menu-image:before { content: $fa-var-neuter; } +#{$ame-icon-image-prefix}-genderless .wp-menu-image:before { content: $fa-var-genderless; } +#{$ame-icon-image-prefix}-facebook-official .wp-menu-image:before { content: $fa-var-facebook-official; } +#{$ame-icon-image-prefix}-pinterest-p .wp-menu-image:before { content: $fa-var-pinterest-p; } +#{$ame-icon-image-prefix}-whatsapp .wp-menu-image:before { content: $fa-var-whatsapp; } +#{$ame-icon-image-prefix}-server .wp-menu-image:before { content: $fa-var-server; } +#{$ame-icon-image-prefix}-user-plus .wp-menu-image:before { content: $fa-var-user-plus; } +#{$ame-icon-image-prefix}-user-times .wp-menu-image:before { content: $fa-var-user-times; } +#{$ame-icon-image-prefix}-hotel .wp-menu-image:before, +#{$ame-icon-image-prefix}-bed .wp-menu-image:before { content: $fa-var-bed; } +#{$ame-icon-image-prefix}-viacoin .wp-menu-image:before { content: $fa-var-viacoin; } +#{$ame-icon-image-prefix}-train .wp-menu-image:before { content: $fa-var-train; } +#{$ame-icon-image-prefix}-subway .wp-menu-image:before { content: $fa-var-subway; } +#{$ame-icon-image-prefix}-medium .wp-menu-image:before { content: $fa-var-medium; } +#{$ame-icon-image-prefix}-yc .wp-menu-image:before, +#{$ame-icon-image-prefix}-y-combinator .wp-menu-image:before { content: $fa-var-y-combinator; } +#{$ame-icon-image-prefix}-optin-monster .wp-menu-image:before { content: $fa-var-optin-monster; } +#{$ame-icon-image-prefix}-opencart .wp-menu-image:before { content: $fa-var-opencart; } +#{$ame-icon-image-prefix}-expeditedssl .wp-menu-image:before { content: $fa-var-expeditedssl; } +#{$ame-icon-image-prefix}-battery-4 .wp-menu-image:before, +#{$ame-icon-image-prefix}-battery-full .wp-menu-image:before { content: $fa-var-battery-full; } +#{$ame-icon-image-prefix}-battery-3 .wp-menu-image:before, +#{$ame-icon-image-prefix}-battery-three-quarters .wp-menu-image:before { content: $fa-var-battery-three-quarters; } +#{$ame-icon-image-prefix}-battery-2 .wp-menu-image:before, +#{$ame-icon-image-prefix}-battery-half .wp-menu-image:before { content: $fa-var-battery-half; } +#{$ame-icon-image-prefix}-battery-1 .wp-menu-image:before, +#{$ame-icon-image-prefix}-battery-quarter .wp-menu-image:before { content: $fa-var-battery-quarter; } +#{$ame-icon-image-prefix}-battery-0 .wp-menu-image:before, +#{$ame-icon-image-prefix}-battery-empty .wp-menu-image:before { content: $fa-var-battery-empty; } +#{$ame-icon-image-prefix}-mouse-pointer .wp-menu-image:before { content: $fa-var-mouse-pointer; } +#{$ame-icon-image-prefix}-i-cursor .wp-menu-image:before { content: $fa-var-i-cursor; } +#{$ame-icon-image-prefix}-object-group .wp-menu-image:before { content: $fa-var-object-group; } +#{$ame-icon-image-prefix}-object-ungroup .wp-menu-image:before { content: $fa-var-object-ungroup; } +#{$ame-icon-image-prefix}-sticky-note .wp-menu-image:before { content: $fa-var-sticky-note; } +#{$ame-icon-image-prefix}-sticky-note-o .wp-menu-image:before { content: $fa-var-sticky-note-o; } +#{$ame-icon-image-prefix}-cc-jcb .wp-menu-image:before { content: $fa-var-cc-jcb; } +#{$ame-icon-image-prefix}-cc-diners-club .wp-menu-image:before { content: $fa-var-cc-diners-club; } +#{$ame-icon-image-prefix}-clone .wp-menu-image:before { content: $fa-var-clone; } +#{$ame-icon-image-prefix}-balance-scale .wp-menu-image:before { content: $fa-var-balance-scale; } +#{$ame-icon-image-prefix}-hourglass-o .wp-menu-image:before { content: $fa-var-hourglass-o; } +#{$ame-icon-image-prefix}-hourglass-1 .wp-menu-image:before, +#{$ame-icon-image-prefix}-hourglass-start .wp-menu-image:before { content: $fa-var-hourglass-start; } +#{$ame-icon-image-prefix}-hourglass-2 .wp-menu-image:before, +#{$ame-icon-image-prefix}-hourglass-half .wp-menu-image:before { content: $fa-var-hourglass-half; } +#{$ame-icon-image-prefix}-hourglass-3 .wp-menu-image:before, +#{$ame-icon-image-prefix}-hourglass-end .wp-menu-image:before { content: $fa-var-hourglass-end; } +#{$ame-icon-image-prefix}-hourglass .wp-menu-image:before { content: $fa-var-hourglass; } +#{$ame-icon-image-prefix}-hand-grab-o .wp-menu-image:before, +#{$ame-icon-image-prefix}-hand-rock-o .wp-menu-image:before { content: $fa-var-hand-rock-o; } +#{$ame-icon-image-prefix}-hand-stop-o .wp-menu-image:before, +#{$ame-icon-image-prefix}-hand-paper-o .wp-menu-image:before { content: $fa-var-hand-paper-o; } +#{$ame-icon-image-prefix}-hand-scissors-o .wp-menu-image:before { content: $fa-var-hand-scissors-o; } +#{$ame-icon-image-prefix}-hand-lizard-o .wp-menu-image:before { content: $fa-var-hand-lizard-o; } +#{$ame-icon-image-prefix}-hand-spock-o .wp-menu-image:before { content: $fa-var-hand-spock-o; } +#{$ame-icon-image-prefix}-hand-pointer-o .wp-menu-image:before { content: $fa-var-hand-pointer-o; } +#{$ame-icon-image-prefix}-hand-peace-o .wp-menu-image:before { content: $fa-var-hand-peace-o; } +#{$ame-icon-image-prefix}-trademark .wp-menu-image:before { content: $fa-var-trademark; } +#{$ame-icon-image-prefix}-registered .wp-menu-image:before { content: $fa-var-registered; } +#{$ame-icon-image-prefix}-creative-commons .wp-menu-image:before { content: $fa-var-creative-commons; } +#{$ame-icon-image-prefix}-gg .wp-menu-image:before { content: $fa-var-gg; } +#{$ame-icon-image-prefix}-gg-circle .wp-menu-image:before { content: $fa-var-gg-circle; } +#{$ame-icon-image-prefix}-tripadvisor .wp-menu-image:before { content: $fa-var-tripadvisor; } +#{$ame-icon-image-prefix}-odnoklassniki .wp-menu-image:before { content: $fa-var-odnoklassniki; } +#{$ame-icon-image-prefix}-odnoklassniki-square .wp-menu-image:before { content: $fa-var-odnoklassniki-square; } +#{$ame-icon-image-prefix}-get-pocket .wp-menu-image:before { content: $fa-var-get-pocket; } +#{$ame-icon-image-prefix}-wikipedia-w .wp-menu-image:before { content: $fa-var-wikipedia-w; } +#{$ame-icon-image-prefix}-safari .wp-menu-image:before { content: $fa-var-safari; } +#{$ame-icon-image-prefix}-chrome .wp-menu-image:before { content: $fa-var-chrome; } +#{$ame-icon-image-prefix}-firefox .wp-menu-image:before { content: $fa-var-firefox; } +#{$ame-icon-image-prefix}-opera .wp-menu-image:before { content: $fa-var-opera; } +#{$ame-icon-image-prefix}-internet-explorer .wp-menu-image:before { content: $fa-var-internet-explorer; } +#{$ame-icon-image-prefix}-tv .wp-menu-image:before, +#{$ame-icon-image-prefix}-television .wp-menu-image:before { content: $fa-var-television; } +#{$ame-icon-image-prefix}-contao .wp-menu-image:before { content: $fa-var-contao; } +#{$ame-icon-image-prefix}-500px .wp-menu-image:before { content: $fa-var-500px; } +#{$ame-icon-image-prefix}-amazon .wp-menu-image:before { content: $fa-var-amazon; } +#{$ame-icon-image-prefix}-calendar-plus-o .wp-menu-image:before { content: $fa-var-calendar-plus-o; } +#{$ame-icon-image-prefix}-calendar-minus-o .wp-menu-image:before { content: $fa-var-calendar-minus-o; } +#{$ame-icon-image-prefix}-calendar-times-o .wp-menu-image:before { content: $fa-var-calendar-times-o; } +#{$ame-icon-image-prefix}-calendar-check-o .wp-menu-image:before { content: $fa-var-calendar-check-o; } +#{$ame-icon-image-prefix}-industry .wp-menu-image:before { content: $fa-var-industry; } +#{$ame-icon-image-prefix}-map-pin .wp-menu-image:before { content: $fa-var-map-pin; } +#{$ame-icon-image-prefix}-map-signs .wp-menu-image:before { content: $fa-var-map-signs; } +#{$ame-icon-image-prefix}-map-o .wp-menu-image:before { content: $fa-var-map-o; } +#{$ame-icon-image-prefix}-map .wp-menu-image:before { content: $fa-var-map; } +#{$ame-icon-image-prefix}-commenting .wp-menu-image:before { content: $fa-var-commenting; } +#{$ame-icon-image-prefix}-commenting-o .wp-menu-image:before { content: $fa-var-commenting-o; } +#{$ame-icon-image-prefix}-houzz .wp-menu-image:before { content: $fa-var-houzz; } +#{$ame-icon-image-prefix}-vimeo .wp-menu-image:before { content: $fa-var-vimeo; } +#{$ame-icon-image-prefix}-black-tie .wp-menu-image:before { content: $fa-var-black-tie; } +#{$ame-icon-image-prefix}-fonticons .wp-menu-image:before { content: $fa-var-fonticons; } +#{$ame-icon-image-prefix}-reddit-alien .wp-menu-image:before { content: $fa-var-reddit-alien; } +#{$ame-icon-image-prefix}-edge .wp-menu-image:before { content: $fa-var-edge; } +#{$ame-icon-image-prefix}-credit-card-alt .wp-menu-image:before { content: $fa-var-credit-card-alt; } +#{$ame-icon-image-prefix}-codiepie .wp-menu-image:before { content: $fa-var-codiepie; } +#{$ame-icon-image-prefix}-modx .wp-menu-image:before { content: $fa-var-modx; } +#{$ame-icon-image-prefix}-fort-awesome .wp-menu-image:before { content: $fa-var-fort-awesome; } +#{$ame-icon-image-prefix}-usb .wp-menu-image:before { content: $fa-var-usb; } +#{$ame-icon-image-prefix}-product-hunt .wp-menu-image:before { content: $fa-var-product-hunt; } +#{$ame-icon-image-prefix}-mixcloud .wp-menu-image:before { content: $fa-var-mixcloud; } +#{$ame-icon-image-prefix}-scribd .wp-menu-image:before { content: $fa-var-scribd; } +#{$ame-icon-image-prefix}-pause-circle .wp-menu-image:before { content: $fa-var-pause-circle; } +#{$ame-icon-image-prefix}-pause-circle-o .wp-menu-image:before { content: $fa-var-pause-circle-o; } +#{$ame-icon-image-prefix}-stop-circle .wp-menu-image:before { content: $fa-var-stop-circle; } +#{$ame-icon-image-prefix}-stop-circle-o .wp-menu-image:before { content: $fa-var-stop-circle-o; } +#{$ame-icon-image-prefix}-shopping-bag .wp-menu-image:before { content: $fa-var-shopping-bag; } +#{$ame-icon-image-prefix}-shopping-basket .wp-menu-image:before { content: $fa-var-shopping-basket; } +#{$ame-icon-image-prefix}-hashtag .wp-menu-image:before { content: $fa-var-hashtag; } +#{$ame-icon-image-prefix}-bluetooth .wp-menu-image:before { content: $fa-var-bluetooth; } +#{$ame-icon-image-prefix}-bluetooth-b .wp-menu-image:before { content: $fa-var-bluetooth-b; } +#{$ame-icon-image-prefix}-percent .wp-menu-image:before { content: $fa-var-percent; } +#{$ame-icon-image-prefix}-gitlab .wp-menu-image:before { content: $fa-var-gitlab; } +#{$ame-icon-image-prefix}-wpbeginner .wp-menu-image:before { content: $fa-var-wpbeginner; } +#{$ame-icon-image-prefix}-wpforms .wp-menu-image:before { content: $fa-var-wpforms; } +#{$ame-icon-image-prefix}-envira .wp-menu-image:before { content: $fa-var-envira; } +#{$ame-icon-image-prefix}-universal-access .wp-menu-image:before { content: $fa-var-universal-access; } +#{$ame-icon-image-prefix}-wheelchair-alt .wp-menu-image:before { content: $fa-var-wheelchair-alt; } +#{$ame-icon-image-prefix}-question-circle-o .wp-menu-image:before { content: $fa-var-question-circle-o; } +#{$ame-icon-image-prefix}-blind .wp-menu-image:before { content: $fa-var-blind; } +#{$ame-icon-image-prefix}-audio-description .wp-menu-image:before { content: $fa-var-audio-description; } +#{$ame-icon-image-prefix}-volume-control-phone .wp-menu-image:before { content: $fa-var-volume-control-phone; } +#{$ame-icon-image-prefix}-braille .wp-menu-image:before { content: $fa-var-braille; } +#{$ame-icon-image-prefix}-assistive-listening-systems .wp-menu-image:before { content: $fa-var-assistive-listening-systems; } +#{$ame-icon-image-prefix}-asl-interpreting .wp-menu-image:before, +#{$ame-icon-image-prefix}-american-sign-language-interpreting .wp-menu-image:before { content: $fa-var-american-sign-language-interpreting; } +#{$ame-icon-image-prefix}-deafness .wp-menu-image:before, +#{$ame-icon-image-prefix}-hard-of-hearing .wp-menu-image:before, +#{$ame-icon-image-prefix}-deaf .wp-menu-image:before { content: $fa-var-deaf; } +#{$ame-icon-image-prefix}-glide .wp-menu-image:before { content: $fa-var-glide; } +#{$ame-icon-image-prefix}-glide-g .wp-menu-image:before { content: $fa-var-glide-g; } +#{$ame-icon-image-prefix}-signing .wp-menu-image:before, +#{$ame-icon-image-prefix}-sign-language .wp-menu-image:before { content: $fa-var-sign-language; } +#{$ame-icon-image-prefix}-low-vision .wp-menu-image:before { content: $fa-var-low-vision; } +#{$ame-icon-image-prefix}-viadeo .wp-menu-image:before { content: $fa-var-viadeo; } +#{$ame-icon-image-prefix}-viadeo-square .wp-menu-image:before { content: $fa-var-viadeo-square; } +#{$ame-icon-image-prefix}-snapchat .wp-menu-image:before { content: $fa-var-snapchat; } +#{$ame-icon-image-prefix}-snapchat-ghost .wp-menu-image:before { content: $fa-var-snapchat-ghost; } +#{$ame-icon-image-prefix}-snapchat-square .wp-menu-image:before { content: $fa-var-snapchat-square; } +#{$ame-icon-image-prefix}-pied-piper .wp-menu-image:before { content: $fa-var-pied-piper; } +#{$ame-icon-image-prefix}-first-order .wp-menu-image:before { content: $fa-var-first-order; } +#{$ame-icon-image-prefix}-yoast .wp-menu-image:before { content: $fa-var-yoast; } +#{$ame-icon-image-prefix}-themeisle .wp-menu-image:before { content: $fa-var-themeisle; } +#{$ame-icon-image-prefix}-google-plus-circle .wp-menu-image:before, +#{$ame-icon-image-prefix}-google-plus-official .wp-menu-image:before { content: $fa-var-google-plus-official; } +#{$ame-icon-image-prefix}-fa .wp-menu-image:before, +#{$ame-icon-image-prefix}-font-awesome .wp-menu-image:before { content: $fa-var-font-awesome; } diff --git a/extras/font-awesome/scss/_ame-variables.scss b/extras/font-awesome/scss/_ame-variables.scss new file mode 100644 index 0000000..32d4df2 --- /dev/null +++ b/extras/font-awesome/scss/_ame-variables.scss @@ -0,0 +1,2 @@ +$ame-menu-prefix: ".ame-menu-fa"; +$ame-icon-image-prefix: "#adminmenu#adminmenu #{$ame-menu-prefix}"; \ No newline at end of file diff --git a/extras/font-awesome/scss/_animated.scss b/extras/font-awesome/scss/_animated.scss new file mode 100644 index 0000000..8a020db --- /dev/null +++ b/extras/font-awesome/scss/_animated.scss @@ -0,0 +1,34 @@ +// Spinning Icons +// -------------------------- + +.#{$fa-css-prefix}-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} + +.#{$fa-css-prefix}-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); +} + +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} + +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} diff --git a/extras/font-awesome/scss/_bordered-pulled.scss b/extras/font-awesome/scss/_bordered-pulled.scss new file mode 100644 index 0000000..d4b85a0 --- /dev/null +++ b/extras/font-awesome/scss/_bordered-pulled.scss @@ -0,0 +1,25 @@ +// Bordered & Pulled +// ------------------------- + +.#{$fa-css-prefix}-border { + padding: .2em .25em .15em; + border: solid .08em $fa-border-color; + border-radius: .1em; +} + +.#{$fa-css-prefix}-pull-left { float: left; } +.#{$fa-css-prefix}-pull-right { float: right; } + +.#{$fa-css-prefix} { + &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } + &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } +} + +/* Deprecated as of 4.4.0 */ +.pull-right { float: right; } +.pull-left { float: left; } + +.#{$fa-css-prefix} { + &.pull-left { margin-right: .3em; } + &.pull-right { margin-left: .3em; } +} diff --git a/extras/font-awesome/scss/_core.scss b/extras/font-awesome/scss/_core.scss new file mode 100644 index 0000000..7425ef8 --- /dev/null +++ b/extras/font-awesome/scss/_core.scss @@ -0,0 +1,12 @@ +// Base Class Definition +// ------------------------- + +.#{$fa-css-prefix} { + display: inline-block; + font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration + font-size: inherit; // can't have font-size inherit on line above, so need to override + text-rendering: auto; // optimizelegibility throws things off #1094 + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + +} diff --git a/extras/font-awesome/scss/_fixed-width.scss b/extras/font-awesome/scss/_fixed-width.scss new file mode 100644 index 0000000..b221c98 --- /dev/null +++ b/extras/font-awesome/scss/_fixed-width.scss @@ -0,0 +1,6 @@ +// Fixed Width Icons +// ------------------------- +.#{$fa-css-prefix}-fw { + width: (18em / 14); + text-align: center; +} diff --git a/extras/font-awesome/scss/_icons.scss b/extras/font-awesome/scss/_icons.scss new file mode 100644 index 0000000..2944344 --- /dev/null +++ b/extras/font-awesome/scss/_icons.scss @@ -0,0 +1,733 @@ +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ + +.#{$fa-css-prefix}-glass:before { content: $fa-var-glass; } +.#{$fa-css-prefix}-music:before { content: $fa-var-music; } +.#{$fa-css-prefix}-search:before { content: $fa-var-search; } +.#{$fa-css-prefix}-envelope-o:before { content: $fa-var-envelope-o; } +.#{$fa-css-prefix}-heart:before { content: $fa-var-heart; } +.#{$fa-css-prefix}-star:before { content: $fa-var-star; } +.#{$fa-css-prefix}-star-o:before { content: $fa-var-star-o; } +.#{$fa-css-prefix}-user:before { content: $fa-var-user; } +.#{$fa-css-prefix}-film:before { content: $fa-var-film; } +.#{$fa-css-prefix}-th-large:before { content: $fa-var-th-large; } +.#{$fa-css-prefix}-th:before { content: $fa-var-th; } +.#{$fa-css-prefix}-th-list:before { content: $fa-var-th-list; } +.#{$fa-css-prefix}-check:before { content: $fa-var-check; } +.#{$fa-css-prefix}-remove:before, +.#{$fa-css-prefix}-close:before, +.#{$fa-css-prefix}-times:before { content: $fa-var-times; } +.#{$fa-css-prefix}-search-plus:before { content: $fa-var-search-plus; } +.#{$fa-css-prefix}-search-minus:before { content: $fa-var-search-minus; } +.#{$fa-css-prefix}-power-off:before { content: $fa-var-power-off; } +.#{$fa-css-prefix}-signal:before { content: $fa-var-signal; } +.#{$fa-css-prefix}-gear:before, +.#{$fa-css-prefix}-cog:before { content: $fa-var-cog; } +.#{$fa-css-prefix}-trash-o:before { content: $fa-var-trash-o; } +.#{$fa-css-prefix}-home:before { content: $fa-var-home; } +.#{$fa-css-prefix}-file-o:before { content: $fa-var-file-o; } +.#{$fa-css-prefix}-clock-o:before { content: $fa-var-clock-o; } +.#{$fa-css-prefix}-road:before { content: $fa-var-road; } +.#{$fa-css-prefix}-download:before { content: $fa-var-download; } +.#{$fa-css-prefix}-arrow-circle-o-down:before { content: $fa-var-arrow-circle-o-down; } +.#{$fa-css-prefix}-arrow-circle-o-up:before { content: $fa-var-arrow-circle-o-up; } +.#{$fa-css-prefix}-inbox:before { content: $fa-var-inbox; } +.#{$fa-css-prefix}-play-circle-o:before { content: $fa-var-play-circle-o; } +.#{$fa-css-prefix}-rotate-right:before, +.#{$fa-css-prefix}-repeat:before { content: $fa-var-repeat; } +.#{$fa-css-prefix}-refresh:before { content: $fa-var-refresh; } +.#{$fa-css-prefix}-list-alt:before { content: $fa-var-list-alt; } +.#{$fa-css-prefix}-lock:before { content: $fa-var-lock; } +.#{$fa-css-prefix}-flag:before { content: $fa-var-flag; } +.#{$fa-css-prefix}-headphones:before { content: $fa-var-headphones; } +.#{$fa-css-prefix}-volume-off:before { content: $fa-var-volume-off; } +.#{$fa-css-prefix}-volume-down:before { content: $fa-var-volume-down; } +.#{$fa-css-prefix}-volume-up:before { content: $fa-var-volume-up; } +.#{$fa-css-prefix}-qrcode:before { content: $fa-var-qrcode; } +.#{$fa-css-prefix}-barcode:before { content: $fa-var-barcode; } +.#{$fa-css-prefix}-tag:before { content: $fa-var-tag; } +.#{$fa-css-prefix}-tags:before { content: $fa-var-tags; } +.#{$fa-css-prefix}-book:before { content: $fa-var-book; } +.#{$fa-css-prefix}-bookmark:before { content: $fa-var-bookmark; } +.#{$fa-css-prefix}-print:before { content: $fa-var-print; } +.#{$fa-css-prefix}-camera:before { content: $fa-var-camera; } +.#{$fa-css-prefix}-font:before { content: $fa-var-font; } +.#{$fa-css-prefix}-bold:before { content: $fa-var-bold; } +.#{$fa-css-prefix}-italic:before { content: $fa-var-italic; } +.#{$fa-css-prefix}-text-height:before { content: $fa-var-text-height; } +.#{$fa-css-prefix}-text-width:before { content: $fa-var-text-width; } +.#{$fa-css-prefix}-align-left:before { content: $fa-var-align-left; } +.#{$fa-css-prefix}-align-center:before { content: $fa-var-align-center; } +.#{$fa-css-prefix}-align-right:before { content: $fa-var-align-right; } +.#{$fa-css-prefix}-align-justify:before { content: $fa-var-align-justify; } +.#{$fa-css-prefix}-list:before { content: $fa-var-list; } +.#{$fa-css-prefix}-dedent:before, +.#{$fa-css-prefix}-outdent:before { content: $fa-var-outdent; } +.#{$fa-css-prefix}-indent:before { content: $fa-var-indent; } +.#{$fa-css-prefix}-video-camera:before { content: $fa-var-video-camera; } +.#{$fa-css-prefix}-photo:before, +.#{$fa-css-prefix}-image:before, +.#{$fa-css-prefix}-picture-o:before { content: $fa-var-picture-o; } +.#{$fa-css-prefix}-pencil:before { content: $fa-var-pencil; } +.#{$fa-css-prefix}-map-marker:before { content: $fa-var-map-marker; } +.#{$fa-css-prefix}-adjust:before { content: $fa-var-adjust; } +.#{$fa-css-prefix}-tint:before { content: $fa-var-tint; } +.#{$fa-css-prefix}-edit:before, +.#{$fa-css-prefix}-pencil-square-o:before { content: $fa-var-pencil-square-o; } +.#{$fa-css-prefix}-share-square-o:before { content: $fa-var-share-square-o; } +.#{$fa-css-prefix}-check-square-o:before { content: $fa-var-check-square-o; } +.#{$fa-css-prefix}-arrows:before { content: $fa-var-arrows; } +.#{$fa-css-prefix}-step-backward:before { content: $fa-var-step-backward; } +.#{$fa-css-prefix}-fast-backward:before { content: $fa-var-fast-backward; } +.#{$fa-css-prefix}-backward:before { content: $fa-var-backward; } +.#{$fa-css-prefix}-play:before { content: $fa-var-play; } +.#{$fa-css-prefix}-pause:before { content: $fa-var-pause; } +.#{$fa-css-prefix}-stop:before { content: $fa-var-stop; } +.#{$fa-css-prefix}-forward:before { content: $fa-var-forward; } +.#{$fa-css-prefix}-fast-forward:before { content: $fa-var-fast-forward; } +.#{$fa-css-prefix}-step-forward:before { content: $fa-var-step-forward; } +.#{$fa-css-prefix}-eject:before { content: $fa-var-eject; } +.#{$fa-css-prefix}-chevron-left:before { content: $fa-var-chevron-left; } +.#{$fa-css-prefix}-chevron-right:before { content: $fa-var-chevron-right; } +.#{$fa-css-prefix}-plus-circle:before { content: $fa-var-plus-circle; } +.#{$fa-css-prefix}-minus-circle:before { content: $fa-var-minus-circle; } +.#{$fa-css-prefix}-times-circle:before { content: $fa-var-times-circle; } +.#{$fa-css-prefix}-check-circle:before { content: $fa-var-check-circle; } +.#{$fa-css-prefix}-question-circle:before { content: $fa-var-question-circle; } +.#{$fa-css-prefix}-info-circle:before { content: $fa-var-info-circle; } +.#{$fa-css-prefix}-crosshairs:before { content: $fa-var-crosshairs; } +.#{$fa-css-prefix}-times-circle-o:before { content: $fa-var-times-circle-o; } +.#{$fa-css-prefix}-check-circle-o:before { content: $fa-var-check-circle-o; } +.#{$fa-css-prefix}-ban:before { content: $fa-var-ban; } +.#{$fa-css-prefix}-arrow-left:before { content: $fa-var-arrow-left; } +.#{$fa-css-prefix}-arrow-right:before { content: $fa-var-arrow-right; } +.#{$fa-css-prefix}-arrow-up:before { content: $fa-var-arrow-up; } +.#{$fa-css-prefix}-arrow-down:before { content: $fa-var-arrow-down; } +.#{$fa-css-prefix}-mail-forward:before, +.#{$fa-css-prefix}-share:before { content: $fa-var-share; } +.#{$fa-css-prefix}-expand:before { content: $fa-var-expand; } +.#{$fa-css-prefix}-compress:before { content: $fa-var-compress; } +.#{$fa-css-prefix}-plus:before { content: $fa-var-plus; } +.#{$fa-css-prefix}-minus:before { content: $fa-var-minus; } +.#{$fa-css-prefix}-asterisk:before { content: $fa-var-asterisk; } +.#{$fa-css-prefix}-exclamation-circle:before { content: $fa-var-exclamation-circle; } +.#{$fa-css-prefix}-gift:before { content: $fa-var-gift; } +.#{$fa-css-prefix}-leaf:before { content: $fa-var-leaf; } +.#{$fa-css-prefix}-fire:before { content: $fa-var-fire; } +.#{$fa-css-prefix}-eye:before { content: $fa-var-eye; } +.#{$fa-css-prefix}-eye-slash:before { content: $fa-var-eye-slash; } +.#{$fa-css-prefix}-warning:before, +.#{$fa-css-prefix}-exclamation-triangle:before { content: $fa-var-exclamation-triangle; } +.#{$fa-css-prefix}-plane:before { content: $fa-var-plane; } +.#{$fa-css-prefix}-calendar:before { content: $fa-var-calendar; } +.#{$fa-css-prefix}-random:before { content: $fa-var-random; } +.#{$fa-css-prefix}-comment:before { content: $fa-var-comment; } +.#{$fa-css-prefix}-magnet:before { content: $fa-var-magnet; } +.#{$fa-css-prefix}-chevron-up:before { content: $fa-var-chevron-up; } +.#{$fa-css-prefix}-chevron-down:before { content: $fa-var-chevron-down; } +.#{$fa-css-prefix}-retweet:before { content: $fa-var-retweet; } +.#{$fa-css-prefix}-shopping-cart:before { content: $fa-var-shopping-cart; } +.#{$fa-css-prefix}-folder:before { content: $fa-var-folder; } +.#{$fa-css-prefix}-folder-open:before { content: $fa-var-folder-open; } +.#{$fa-css-prefix}-arrows-v:before { content: $fa-var-arrows-v; } +.#{$fa-css-prefix}-arrows-h:before { content: $fa-var-arrows-h; } +.#{$fa-css-prefix}-bar-chart-o:before, +.#{$fa-css-prefix}-bar-chart:before { content: $fa-var-bar-chart; } +.#{$fa-css-prefix}-twitter-square:before { content: $fa-var-twitter-square; } +.#{$fa-css-prefix}-facebook-square:before { content: $fa-var-facebook-square; } +.#{$fa-css-prefix}-camera-retro:before { content: $fa-var-camera-retro; } +.#{$fa-css-prefix}-key:before { content: $fa-var-key; } +.#{$fa-css-prefix}-gears:before, +.#{$fa-css-prefix}-cogs:before { content: $fa-var-cogs; } +.#{$fa-css-prefix}-comments:before { content: $fa-var-comments; } +.#{$fa-css-prefix}-thumbs-o-up:before { content: $fa-var-thumbs-o-up; } +.#{$fa-css-prefix}-thumbs-o-down:before { content: $fa-var-thumbs-o-down; } +.#{$fa-css-prefix}-star-half:before { content: $fa-var-star-half; } +.#{$fa-css-prefix}-heart-o:before { content: $fa-var-heart-o; } +.#{$fa-css-prefix}-sign-out:before { content: $fa-var-sign-out; } +.#{$fa-css-prefix}-linkedin-square:before { content: $fa-var-linkedin-square; } +.#{$fa-css-prefix}-thumb-tack:before { content: $fa-var-thumb-tack; } +.#{$fa-css-prefix}-external-link:before { content: $fa-var-external-link; } +.#{$fa-css-prefix}-sign-in:before { content: $fa-var-sign-in; } +.#{$fa-css-prefix}-trophy:before { content: $fa-var-trophy; } +.#{$fa-css-prefix}-github-square:before { content: $fa-var-github-square; } +.#{$fa-css-prefix}-upload:before { content: $fa-var-upload; } +.#{$fa-css-prefix}-lemon-o:before { content: $fa-var-lemon-o; } +.#{$fa-css-prefix}-phone:before { content: $fa-var-phone; } +.#{$fa-css-prefix}-square-o:before { content: $fa-var-square-o; } +.#{$fa-css-prefix}-bookmark-o:before { content: $fa-var-bookmark-o; } +.#{$fa-css-prefix}-phone-square:before { content: $fa-var-phone-square; } +.#{$fa-css-prefix}-twitter:before { content: $fa-var-twitter; } +.#{$fa-css-prefix}-facebook-f:before, +.#{$fa-css-prefix}-facebook:before { content: $fa-var-facebook; } +.#{$fa-css-prefix}-github:before { content: $fa-var-github; } +.#{$fa-css-prefix}-unlock:before { content: $fa-var-unlock; } +.#{$fa-css-prefix}-credit-card:before { content: $fa-var-credit-card; } +.#{$fa-css-prefix}-feed:before, +.#{$fa-css-prefix}-rss:before { content: $fa-var-rss; } +.#{$fa-css-prefix}-hdd-o:before { content: $fa-var-hdd-o; } +.#{$fa-css-prefix}-bullhorn:before { content: $fa-var-bullhorn; } +.#{$fa-css-prefix}-bell:before { content: $fa-var-bell; } +.#{$fa-css-prefix}-certificate:before { content: $fa-var-certificate; } +.#{$fa-css-prefix}-hand-o-right:before { content: $fa-var-hand-o-right; } +.#{$fa-css-prefix}-hand-o-left:before { content: $fa-var-hand-o-left; } +.#{$fa-css-prefix}-hand-o-up:before { content: $fa-var-hand-o-up; } +.#{$fa-css-prefix}-hand-o-down:before { content: $fa-var-hand-o-down; } +.#{$fa-css-prefix}-arrow-circle-left:before { content: $fa-var-arrow-circle-left; } +.#{$fa-css-prefix}-arrow-circle-right:before { content: $fa-var-arrow-circle-right; } +.#{$fa-css-prefix}-arrow-circle-up:before { content: $fa-var-arrow-circle-up; } +.#{$fa-css-prefix}-arrow-circle-down:before { content: $fa-var-arrow-circle-down; } +.#{$fa-css-prefix}-globe:before { content: $fa-var-globe; } +.#{$fa-css-prefix}-wrench:before { content: $fa-var-wrench; } +.#{$fa-css-prefix}-tasks:before { content: $fa-var-tasks; } +.#{$fa-css-prefix}-filter:before { content: $fa-var-filter; } +.#{$fa-css-prefix}-briefcase:before { content: $fa-var-briefcase; } +.#{$fa-css-prefix}-arrows-alt:before { content: $fa-var-arrows-alt; } +.#{$fa-css-prefix}-group:before, +.#{$fa-css-prefix}-users:before { content: $fa-var-users; } +.#{$fa-css-prefix}-chain:before, +.#{$fa-css-prefix}-link:before { content: $fa-var-link; } +.#{$fa-css-prefix}-cloud:before { content: $fa-var-cloud; } +.#{$fa-css-prefix}-flask:before { content: $fa-var-flask; } +.#{$fa-css-prefix}-cut:before, +.#{$fa-css-prefix}-scissors:before { content: $fa-var-scissors; } +.#{$fa-css-prefix}-copy:before, +.#{$fa-css-prefix}-files-o:before { content: $fa-var-files-o; } +.#{$fa-css-prefix}-paperclip:before { content: $fa-var-paperclip; } +.#{$fa-css-prefix}-save:before, +.#{$fa-css-prefix}-floppy-o:before { content: $fa-var-floppy-o; } +.#{$fa-css-prefix}-square:before { content: $fa-var-square; } +.#{$fa-css-prefix}-navicon:before, +.#{$fa-css-prefix}-reorder:before, +.#{$fa-css-prefix}-bars:before { content: $fa-var-bars; } +.#{$fa-css-prefix}-list-ul:before { content: $fa-var-list-ul; } +.#{$fa-css-prefix}-list-ol:before { content: $fa-var-list-ol; } +.#{$fa-css-prefix}-strikethrough:before { content: $fa-var-strikethrough; } +.#{$fa-css-prefix}-underline:before { content: $fa-var-underline; } +.#{$fa-css-prefix}-table:before { content: $fa-var-table; } +.#{$fa-css-prefix}-magic:before { content: $fa-var-magic; } +.#{$fa-css-prefix}-truck:before { content: $fa-var-truck; } +.#{$fa-css-prefix}-pinterest:before { content: $fa-var-pinterest; } +.#{$fa-css-prefix}-pinterest-square:before { content: $fa-var-pinterest-square; } +.#{$fa-css-prefix}-google-plus-square:before { content: $fa-var-google-plus-square; } +.#{$fa-css-prefix}-google-plus:before { content: $fa-var-google-plus; } +.#{$fa-css-prefix}-money:before { content: $fa-var-money; } +.#{$fa-css-prefix}-caret-down:before { content: $fa-var-caret-down; } +.#{$fa-css-prefix}-caret-up:before { content: $fa-var-caret-up; } +.#{$fa-css-prefix}-caret-left:before { content: $fa-var-caret-left; } +.#{$fa-css-prefix}-caret-right:before { content: $fa-var-caret-right; } +.#{$fa-css-prefix}-columns:before { content: $fa-var-columns; } +.#{$fa-css-prefix}-unsorted:before, +.#{$fa-css-prefix}-sort:before { content: $fa-var-sort; } +.#{$fa-css-prefix}-sort-down:before, +.#{$fa-css-prefix}-sort-desc:before { content: $fa-var-sort-desc; } +.#{$fa-css-prefix}-sort-up:before, +.#{$fa-css-prefix}-sort-asc:before { content: $fa-var-sort-asc; } +.#{$fa-css-prefix}-envelope:before { content: $fa-var-envelope; } +.#{$fa-css-prefix}-linkedin:before { content: $fa-var-linkedin; } +.#{$fa-css-prefix}-rotate-left:before, +.#{$fa-css-prefix}-undo:before { content: $fa-var-undo; } +.#{$fa-css-prefix}-legal:before, +.#{$fa-css-prefix}-gavel:before { content: $fa-var-gavel; } +.#{$fa-css-prefix}-dashboard:before, +.#{$fa-css-prefix}-tachometer:before { content: $fa-var-tachometer; } +.#{$fa-css-prefix}-comment-o:before { content: $fa-var-comment-o; } +.#{$fa-css-prefix}-comments-o:before { content: $fa-var-comments-o; } +.#{$fa-css-prefix}-flash:before, +.#{$fa-css-prefix}-bolt:before { content: $fa-var-bolt; } +.#{$fa-css-prefix}-sitemap:before { content: $fa-var-sitemap; } +.#{$fa-css-prefix}-umbrella:before { content: $fa-var-umbrella; } +.#{$fa-css-prefix}-paste:before, +.#{$fa-css-prefix}-clipboard:before { content: $fa-var-clipboard; } +.#{$fa-css-prefix}-lightbulb-o:before { content: $fa-var-lightbulb-o; } +.#{$fa-css-prefix}-exchange:before { content: $fa-var-exchange; } +.#{$fa-css-prefix}-cloud-download:before { content: $fa-var-cloud-download; } +.#{$fa-css-prefix}-cloud-upload:before { content: $fa-var-cloud-upload; } +.#{$fa-css-prefix}-user-md:before { content: $fa-var-user-md; } +.#{$fa-css-prefix}-stethoscope:before { content: $fa-var-stethoscope; } +.#{$fa-css-prefix}-suitcase:before { content: $fa-var-suitcase; } +.#{$fa-css-prefix}-bell-o:before { content: $fa-var-bell-o; } +.#{$fa-css-prefix}-coffee:before { content: $fa-var-coffee; } +.#{$fa-css-prefix}-cutlery:before { content: $fa-var-cutlery; } +.#{$fa-css-prefix}-file-text-o:before { content: $fa-var-file-text-o; } +.#{$fa-css-prefix}-building-o:before { content: $fa-var-building-o; } +.#{$fa-css-prefix}-hospital-o:before { content: $fa-var-hospital-o; } +.#{$fa-css-prefix}-ambulance:before { content: $fa-var-ambulance; } +.#{$fa-css-prefix}-medkit:before { content: $fa-var-medkit; } +.#{$fa-css-prefix}-fighter-jet:before { content: $fa-var-fighter-jet; } +.#{$fa-css-prefix}-beer:before { content: $fa-var-beer; } +.#{$fa-css-prefix}-h-square:before { content: $fa-var-h-square; } +.#{$fa-css-prefix}-plus-square:before { content: $fa-var-plus-square; } +.#{$fa-css-prefix}-angle-double-left:before { content: $fa-var-angle-double-left; } +.#{$fa-css-prefix}-angle-double-right:before { content: $fa-var-angle-double-right; } +.#{$fa-css-prefix}-angle-double-up:before { content: $fa-var-angle-double-up; } +.#{$fa-css-prefix}-angle-double-down:before { content: $fa-var-angle-double-down; } +.#{$fa-css-prefix}-angle-left:before { content: $fa-var-angle-left; } +.#{$fa-css-prefix}-angle-right:before { content: $fa-var-angle-right; } +.#{$fa-css-prefix}-angle-up:before { content: $fa-var-angle-up; } +.#{$fa-css-prefix}-angle-down:before { content: $fa-var-angle-down; } +.#{$fa-css-prefix}-desktop:before { content: $fa-var-desktop; } +.#{$fa-css-prefix}-laptop:before { content: $fa-var-laptop; } +.#{$fa-css-prefix}-tablet:before { content: $fa-var-tablet; } +.#{$fa-css-prefix}-mobile-phone:before, +.#{$fa-css-prefix}-mobile:before { content: $fa-var-mobile; } +.#{$fa-css-prefix}-circle-o:before { content: $fa-var-circle-o; } +.#{$fa-css-prefix}-quote-left:before { content: $fa-var-quote-left; } +.#{$fa-css-prefix}-quote-right:before { content: $fa-var-quote-right; } +.#{$fa-css-prefix}-spinner:before { content: $fa-var-spinner; } +.#{$fa-css-prefix}-circle:before { content: $fa-var-circle; } +.#{$fa-css-prefix}-mail-reply:before, +.#{$fa-css-prefix}-reply:before { content: $fa-var-reply; } +.#{$fa-css-prefix}-github-alt:before { content: $fa-var-github-alt; } +.#{$fa-css-prefix}-folder-o:before { content: $fa-var-folder-o; } +.#{$fa-css-prefix}-folder-open-o:before { content: $fa-var-folder-open-o; } +.#{$fa-css-prefix}-smile-o:before { content: $fa-var-smile-o; } +.#{$fa-css-prefix}-frown-o:before { content: $fa-var-frown-o; } +.#{$fa-css-prefix}-meh-o:before { content: $fa-var-meh-o; } +.#{$fa-css-prefix}-gamepad:before { content: $fa-var-gamepad; } +.#{$fa-css-prefix}-keyboard-o:before { content: $fa-var-keyboard-o; } +.#{$fa-css-prefix}-flag-o:before { content: $fa-var-flag-o; } +.#{$fa-css-prefix}-flag-checkered:before { content: $fa-var-flag-checkered; } +.#{$fa-css-prefix}-terminal:before { content: $fa-var-terminal; } +.#{$fa-css-prefix}-code:before { content: $fa-var-code; } +.#{$fa-css-prefix}-mail-reply-all:before, +.#{$fa-css-prefix}-reply-all:before { content: $fa-var-reply-all; } +.#{$fa-css-prefix}-star-half-empty:before, +.#{$fa-css-prefix}-star-half-full:before, +.#{$fa-css-prefix}-star-half-o:before { content: $fa-var-star-half-o; } +.#{$fa-css-prefix}-location-arrow:before { content: $fa-var-location-arrow; } +.#{$fa-css-prefix}-crop:before { content: $fa-var-crop; } +.#{$fa-css-prefix}-code-fork:before { content: $fa-var-code-fork; } +.#{$fa-css-prefix}-unlink:before, +.#{$fa-css-prefix}-chain-broken:before { content: $fa-var-chain-broken; } +.#{$fa-css-prefix}-question:before { content: $fa-var-question; } +.#{$fa-css-prefix}-info:before { content: $fa-var-info; } +.#{$fa-css-prefix}-exclamation:before { content: $fa-var-exclamation; } +.#{$fa-css-prefix}-superscript:before { content: $fa-var-superscript; } +.#{$fa-css-prefix}-subscript:before { content: $fa-var-subscript; } +.#{$fa-css-prefix}-eraser:before { content: $fa-var-eraser; } +.#{$fa-css-prefix}-puzzle-piece:before { content: $fa-var-puzzle-piece; } +.#{$fa-css-prefix}-microphone:before { content: $fa-var-microphone; } +.#{$fa-css-prefix}-microphone-slash:before { content: $fa-var-microphone-slash; } +.#{$fa-css-prefix}-shield:before { content: $fa-var-shield; } +.#{$fa-css-prefix}-calendar-o:before { content: $fa-var-calendar-o; } +.#{$fa-css-prefix}-fire-extinguisher:before { content: $fa-var-fire-extinguisher; } +.#{$fa-css-prefix}-rocket:before { content: $fa-var-rocket; } +.#{$fa-css-prefix}-maxcdn:before { content: $fa-var-maxcdn; } +.#{$fa-css-prefix}-chevron-circle-left:before { content: $fa-var-chevron-circle-left; } +.#{$fa-css-prefix}-chevron-circle-right:before { content: $fa-var-chevron-circle-right; } +.#{$fa-css-prefix}-chevron-circle-up:before { content: $fa-var-chevron-circle-up; } +.#{$fa-css-prefix}-chevron-circle-down:before { content: $fa-var-chevron-circle-down; } +.#{$fa-css-prefix}-html5:before { content: $fa-var-html5; } +.#{$fa-css-prefix}-css3:before { content: $fa-var-css3; } +.#{$fa-css-prefix}-anchor:before { content: $fa-var-anchor; } +.#{$fa-css-prefix}-unlock-alt:before { content: $fa-var-unlock-alt; } +.#{$fa-css-prefix}-bullseye:before { content: $fa-var-bullseye; } +.#{$fa-css-prefix}-ellipsis-h:before { content: $fa-var-ellipsis-h; } +.#{$fa-css-prefix}-ellipsis-v:before { content: $fa-var-ellipsis-v; } +.#{$fa-css-prefix}-rss-square:before { content: $fa-var-rss-square; } +.#{$fa-css-prefix}-play-circle:before { content: $fa-var-play-circle; } +.#{$fa-css-prefix}-ticket:before { content: $fa-var-ticket; } +.#{$fa-css-prefix}-minus-square:before { content: $fa-var-minus-square; } +.#{$fa-css-prefix}-minus-square-o:before { content: $fa-var-minus-square-o; } +.#{$fa-css-prefix}-level-up:before { content: $fa-var-level-up; } +.#{$fa-css-prefix}-level-down:before { content: $fa-var-level-down; } +.#{$fa-css-prefix}-check-square:before { content: $fa-var-check-square; } +.#{$fa-css-prefix}-pencil-square:before { content: $fa-var-pencil-square; } +.#{$fa-css-prefix}-external-link-square:before { content: $fa-var-external-link-square; } +.#{$fa-css-prefix}-share-square:before { content: $fa-var-share-square; } +.#{$fa-css-prefix}-compass:before { content: $fa-var-compass; } +.#{$fa-css-prefix}-toggle-down:before, +.#{$fa-css-prefix}-caret-square-o-down:before { content: $fa-var-caret-square-o-down; } +.#{$fa-css-prefix}-toggle-up:before, +.#{$fa-css-prefix}-caret-square-o-up:before { content: $fa-var-caret-square-o-up; } +.#{$fa-css-prefix}-toggle-right:before, +.#{$fa-css-prefix}-caret-square-o-right:before { content: $fa-var-caret-square-o-right; } +.#{$fa-css-prefix}-euro:before, +.#{$fa-css-prefix}-eur:before { content: $fa-var-eur; } +.#{$fa-css-prefix}-gbp:before { content: $fa-var-gbp; } +.#{$fa-css-prefix}-dollar:before, +.#{$fa-css-prefix}-usd:before { content: $fa-var-usd; } +.#{$fa-css-prefix}-rupee:before, +.#{$fa-css-prefix}-inr:before { content: $fa-var-inr; } +.#{$fa-css-prefix}-cny:before, +.#{$fa-css-prefix}-rmb:before, +.#{$fa-css-prefix}-yen:before, +.#{$fa-css-prefix}-jpy:before { content: $fa-var-jpy; } +.#{$fa-css-prefix}-ruble:before, +.#{$fa-css-prefix}-rouble:before, +.#{$fa-css-prefix}-rub:before { content: $fa-var-rub; } +.#{$fa-css-prefix}-won:before, +.#{$fa-css-prefix}-krw:before { content: $fa-var-krw; } +.#{$fa-css-prefix}-bitcoin:before, +.#{$fa-css-prefix}-btc:before { content: $fa-var-btc; } +.#{$fa-css-prefix}-file:before { content: $fa-var-file; } +.#{$fa-css-prefix}-file-text:before { content: $fa-var-file-text; } +.#{$fa-css-prefix}-sort-alpha-asc:before { content: $fa-var-sort-alpha-asc; } +.#{$fa-css-prefix}-sort-alpha-desc:before { content: $fa-var-sort-alpha-desc; } +.#{$fa-css-prefix}-sort-amount-asc:before { content: $fa-var-sort-amount-asc; } +.#{$fa-css-prefix}-sort-amount-desc:before { content: $fa-var-sort-amount-desc; } +.#{$fa-css-prefix}-sort-numeric-asc:before { content: $fa-var-sort-numeric-asc; } +.#{$fa-css-prefix}-sort-numeric-desc:before { content: $fa-var-sort-numeric-desc; } +.#{$fa-css-prefix}-thumbs-up:before { content: $fa-var-thumbs-up; } +.#{$fa-css-prefix}-thumbs-down:before { content: $fa-var-thumbs-down; } +.#{$fa-css-prefix}-youtube-square:before { content: $fa-var-youtube-square; } +.#{$fa-css-prefix}-youtube:before { content: $fa-var-youtube; } +.#{$fa-css-prefix}-xing:before { content: $fa-var-xing; } +.#{$fa-css-prefix}-xing-square:before { content: $fa-var-xing-square; } +.#{$fa-css-prefix}-youtube-play:before { content: $fa-var-youtube-play; } +.#{$fa-css-prefix}-dropbox:before { content: $fa-var-dropbox; } +.#{$fa-css-prefix}-stack-overflow:before { content: $fa-var-stack-overflow; } +.#{$fa-css-prefix}-instagram:before { content: $fa-var-instagram; } +.#{$fa-css-prefix}-flickr:before { content: $fa-var-flickr; } +.#{$fa-css-prefix}-adn:before { content: $fa-var-adn; } +.#{$fa-css-prefix}-bitbucket:before { content: $fa-var-bitbucket; } +.#{$fa-css-prefix}-bitbucket-square:before { content: $fa-var-bitbucket-square; } +.#{$fa-css-prefix}-tumblr:before { content: $fa-var-tumblr; } +.#{$fa-css-prefix}-tumblr-square:before { content: $fa-var-tumblr-square; } +.#{$fa-css-prefix}-long-arrow-down:before { content: $fa-var-long-arrow-down; } +.#{$fa-css-prefix}-long-arrow-up:before { content: $fa-var-long-arrow-up; } +.#{$fa-css-prefix}-long-arrow-left:before { content: $fa-var-long-arrow-left; } +.#{$fa-css-prefix}-long-arrow-right:before { content: $fa-var-long-arrow-right; } +.#{$fa-css-prefix}-apple:before { content: $fa-var-apple; } +.#{$fa-css-prefix}-windows:before { content: $fa-var-windows; } +.#{$fa-css-prefix}-android:before { content: $fa-var-android; } +.#{$fa-css-prefix}-linux:before { content: $fa-var-linux; } +.#{$fa-css-prefix}-dribbble:before { content: $fa-var-dribbble; } +.#{$fa-css-prefix}-skype:before { content: $fa-var-skype; } +.#{$fa-css-prefix}-foursquare:before { content: $fa-var-foursquare; } +.#{$fa-css-prefix}-trello:before { content: $fa-var-trello; } +.#{$fa-css-prefix}-female:before { content: $fa-var-female; } +.#{$fa-css-prefix}-male:before { content: $fa-var-male; } +.#{$fa-css-prefix}-gittip:before, +.#{$fa-css-prefix}-gratipay:before { content: $fa-var-gratipay; } +.#{$fa-css-prefix}-sun-o:before { content: $fa-var-sun-o; } +.#{$fa-css-prefix}-moon-o:before { content: $fa-var-moon-o; } +.#{$fa-css-prefix}-archive:before { content: $fa-var-archive; } +.#{$fa-css-prefix}-bug:before { content: $fa-var-bug; } +.#{$fa-css-prefix}-vk:before { content: $fa-var-vk; } +.#{$fa-css-prefix}-weibo:before { content: $fa-var-weibo; } +.#{$fa-css-prefix}-renren:before { content: $fa-var-renren; } +.#{$fa-css-prefix}-pagelines:before { content: $fa-var-pagelines; } +.#{$fa-css-prefix}-stack-exchange:before { content: $fa-var-stack-exchange; } +.#{$fa-css-prefix}-arrow-circle-o-right:before { content: $fa-var-arrow-circle-o-right; } +.#{$fa-css-prefix}-arrow-circle-o-left:before { content: $fa-var-arrow-circle-o-left; } +.#{$fa-css-prefix}-toggle-left:before, +.#{$fa-css-prefix}-caret-square-o-left:before { content: $fa-var-caret-square-o-left; } +.#{$fa-css-prefix}-dot-circle-o:before { content: $fa-var-dot-circle-o; } +.#{$fa-css-prefix}-wheelchair:before { content: $fa-var-wheelchair; } +.#{$fa-css-prefix}-vimeo-square:before { content: $fa-var-vimeo-square; } +.#{$fa-css-prefix}-turkish-lira:before, +.#{$fa-css-prefix}-try:before { content: $fa-var-try; } +.#{$fa-css-prefix}-plus-square-o:before { content: $fa-var-plus-square-o; } +.#{$fa-css-prefix}-space-shuttle:before { content: $fa-var-space-shuttle; } +.#{$fa-css-prefix}-slack:before { content: $fa-var-slack; } +.#{$fa-css-prefix}-envelope-square:before { content: $fa-var-envelope-square; } +.#{$fa-css-prefix}-wordpress:before { content: $fa-var-wordpress; } +.#{$fa-css-prefix}-openid:before { content: $fa-var-openid; } +.#{$fa-css-prefix}-institution:before, +.#{$fa-css-prefix}-bank:before, +.#{$fa-css-prefix}-university:before { content: $fa-var-university; } +.#{$fa-css-prefix}-mortar-board:before, +.#{$fa-css-prefix}-graduation-cap:before { content: $fa-var-graduation-cap; } +.#{$fa-css-prefix}-yahoo:before { content: $fa-var-yahoo; } +.#{$fa-css-prefix}-google:before { content: $fa-var-google; } +.#{$fa-css-prefix}-reddit:before { content: $fa-var-reddit; } +.#{$fa-css-prefix}-reddit-square:before { content: $fa-var-reddit-square; } +.#{$fa-css-prefix}-stumbleupon-circle:before { content: $fa-var-stumbleupon-circle; } +.#{$fa-css-prefix}-stumbleupon:before { content: $fa-var-stumbleupon; } +.#{$fa-css-prefix}-delicious:before { content: $fa-var-delicious; } +.#{$fa-css-prefix}-digg:before { content: $fa-var-digg; } +.#{$fa-css-prefix}-pied-piper-pp:before { content: $fa-var-pied-piper-pp; } +.#{$fa-css-prefix}-pied-piper-alt:before { content: $fa-var-pied-piper-alt; } +.#{$fa-css-prefix}-drupal:before { content: $fa-var-drupal; } +.#{$fa-css-prefix}-joomla:before { content: $fa-var-joomla; } +.#{$fa-css-prefix}-language:before { content: $fa-var-language; } +.#{$fa-css-prefix}-fax:before { content: $fa-var-fax; } +.#{$fa-css-prefix}-building:before { content: $fa-var-building; } +.#{$fa-css-prefix}-child:before { content: $fa-var-child; } +.#{$fa-css-prefix}-paw:before { content: $fa-var-paw; } +.#{$fa-css-prefix}-spoon:before { content: $fa-var-spoon; } +.#{$fa-css-prefix}-cube:before { content: $fa-var-cube; } +.#{$fa-css-prefix}-cubes:before { content: $fa-var-cubes; } +.#{$fa-css-prefix}-behance:before { content: $fa-var-behance; } +.#{$fa-css-prefix}-behance-square:before { content: $fa-var-behance-square; } +.#{$fa-css-prefix}-steam:before { content: $fa-var-steam; } +.#{$fa-css-prefix}-steam-square:before { content: $fa-var-steam-square; } +.#{$fa-css-prefix}-recycle:before { content: $fa-var-recycle; } +.#{$fa-css-prefix}-automobile:before, +.#{$fa-css-prefix}-car:before { content: $fa-var-car; } +.#{$fa-css-prefix}-cab:before, +.#{$fa-css-prefix}-taxi:before { content: $fa-var-taxi; } +.#{$fa-css-prefix}-tree:before { content: $fa-var-tree; } +.#{$fa-css-prefix}-spotify:before { content: $fa-var-spotify; } +.#{$fa-css-prefix}-deviantart:before { content: $fa-var-deviantart; } +.#{$fa-css-prefix}-soundcloud:before { content: $fa-var-soundcloud; } +.#{$fa-css-prefix}-database:before { content: $fa-var-database; } +.#{$fa-css-prefix}-file-pdf-o:before { content: $fa-var-file-pdf-o; } +.#{$fa-css-prefix}-file-word-o:before { content: $fa-var-file-word-o; } +.#{$fa-css-prefix}-file-excel-o:before { content: $fa-var-file-excel-o; } +.#{$fa-css-prefix}-file-powerpoint-o:before { content: $fa-var-file-powerpoint-o; } +.#{$fa-css-prefix}-file-photo-o:before, +.#{$fa-css-prefix}-file-picture-o:before, +.#{$fa-css-prefix}-file-image-o:before { content: $fa-var-file-image-o; } +.#{$fa-css-prefix}-file-zip-o:before, +.#{$fa-css-prefix}-file-archive-o:before { content: $fa-var-file-archive-o; } +.#{$fa-css-prefix}-file-sound-o:before, +.#{$fa-css-prefix}-file-audio-o:before { content: $fa-var-file-audio-o; } +.#{$fa-css-prefix}-file-movie-o:before, +.#{$fa-css-prefix}-file-video-o:before { content: $fa-var-file-video-o; } +.#{$fa-css-prefix}-file-code-o:before { content: $fa-var-file-code-o; } +.#{$fa-css-prefix}-vine:before { content: $fa-var-vine; } +.#{$fa-css-prefix}-codepen:before { content: $fa-var-codepen; } +.#{$fa-css-prefix}-jsfiddle:before { content: $fa-var-jsfiddle; } +.#{$fa-css-prefix}-life-bouy:before, +.#{$fa-css-prefix}-life-buoy:before, +.#{$fa-css-prefix}-life-saver:before, +.#{$fa-css-prefix}-support:before, +.#{$fa-css-prefix}-life-ring:before { content: $fa-var-life-ring; } +.#{$fa-css-prefix}-circle-o-notch:before { content: $fa-var-circle-o-notch; } +.#{$fa-css-prefix}-ra:before, +.#{$fa-css-prefix}-resistance:before, +.#{$fa-css-prefix}-rebel:before { content: $fa-var-rebel; } +.#{$fa-css-prefix}-ge:before, +.#{$fa-css-prefix}-empire:before { content: $fa-var-empire; } +.#{$fa-css-prefix}-git-square:before { content: $fa-var-git-square; } +.#{$fa-css-prefix}-git:before { content: $fa-var-git; } +.#{$fa-css-prefix}-y-combinator-square:before, +.#{$fa-css-prefix}-yc-square:before, +.#{$fa-css-prefix}-hacker-news:before { content: $fa-var-hacker-news; } +.#{$fa-css-prefix}-tencent-weibo:before { content: $fa-var-tencent-weibo; } +.#{$fa-css-prefix}-qq:before { content: $fa-var-qq; } +.#{$fa-css-prefix}-wechat:before, +.#{$fa-css-prefix}-weixin:before { content: $fa-var-weixin; } +.#{$fa-css-prefix}-send:before, +.#{$fa-css-prefix}-paper-plane:before { content: $fa-var-paper-plane; } +.#{$fa-css-prefix}-send-o:before, +.#{$fa-css-prefix}-paper-plane-o:before { content: $fa-var-paper-plane-o; } +.#{$fa-css-prefix}-history:before { content: $fa-var-history; } +.#{$fa-css-prefix}-circle-thin:before { content: $fa-var-circle-thin; } +.#{$fa-css-prefix}-header:before { content: $fa-var-header; } +.#{$fa-css-prefix}-paragraph:before { content: $fa-var-paragraph; } +.#{$fa-css-prefix}-sliders:before { content: $fa-var-sliders; } +.#{$fa-css-prefix}-share-alt:before { content: $fa-var-share-alt; } +.#{$fa-css-prefix}-share-alt-square:before { content: $fa-var-share-alt-square; } +.#{$fa-css-prefix}-bomb:before { content: $fa-var-bomb; } +.#{$fa-css-prefix}-soccer-ball-o:before, +.#{$fa-css-prefix}-futbol-o:before { content: $fa-var-futbol-o; } +.#{$fa-css-prefix}-tty:before { content: $fa-var-tty; } +.#{$fa-css-prefix}-binoculars:before { content: $fa-var-binoculars; } +.#{$fa-css-prefix}-plug:before { content: $fa-var-plug; } +.#{$fa-css-prefix}-slideshare:before { content: $fa-var-slideshare; } +.#{$fa-css-prefix}-twitch:before { content: $fa-var-twitch; } +.#{$fa-css-prefix}-yelp:before { content: $fa-var-yelp; } +.#{$fa-css-prefix}-newspaper-o:before { content: $fa-var-newspaper-o; } +.#{$fa-css-prefix}-wifi:before { content: $fa-var-wifi; } +.#{$fa-css-prefix}-calculator:before { content: $fa-var-calculator; } +.#{$fa-css-prefix}-paypal:before { content: $fa-var-paypal; } +.#{$fa-css-prefix}-google-wallet:before { content: $fa-var-google-wallet; } +.#{$fa-css-prefix}-cc-visa:before { content: $fa-var-cc-visa; } +.#{$fa-css-prefix}-cc-mastercard:before { content: $fa-var-cc-mastercard; } +.#{$fa-css-prefix}-cc-discover:before { content: $fa-var-cc-discover; } +.#{$fa-css-prefix}-cc-amex:before { content: $fa-var-cc-amex; } +.#{$fa-css-prefix}-cc-paypal:before { content: $fa-var-cc-paypal; } +.#{$fa-css-prefix}-cc-stripe:before { content: $fa-var-cc-stripe; } +.#{$fa-css-prefix}-bell-slash:before { content: $fa-var-bell-slash; } +.#{$fa-css-prefix}-bell-slash-o:before { content: $fa-var-bell-slash-o; } +.#{$fa-css-prefix}-trash:before { content: $fa-var-trash; } +.#{$fa-css-prefix}-copyright:before { content: $fa-var-copyright; } +.#{$fa-css-prefix}-at:before { content: $fa-var-at; } +.#{$fa-css-prefix}-eyedropper:before { content: $fa-var-eyedropper; } +.#{$fa-css-prefix}-paint-brush:before { content: $fa-var-paint-brush; } +.#{$fa-css-prefix}-birthday-cake:before { content: $fa-var-birthday-cake; } +.#{$fa-css-prefix}-area-chart:before { content: $fa-var-area-chart; } +.#{$fa-css-prefix}-pie-chart:before { content: $fa-var-pie-chart; } +.#{$fa-css-prefix}-line-chart:before { content: $fa-var-line-chart; } +.#{$fa-css-prefix}-lastfm:before { content: $fa-var-lastfm; } +.#{$fa-css-prefix}-lastfm-square:before { content: $fa-var-lastfm-square; } +.#{$fa-css-prefix}-toggle-off:before { content: $fa-var-toggle-off; } +.#{$fa-css-prefix}-toggle-on:before { content: $fa-var-toggle-on; } +.#{$fa-css-prefix}-bicycle:before { content: $fa-var-bicycle; } +.#{$fa-css-prefix}-bus:before { content: $fa-var-bus; } +.#{$fa-css-prefix}-ioxhost:before { content: $fa-var-ioxhost; } +.#{$fa-css-prefix}-angellist:before { content: $fa-var-angellist; } +.#{$fa-css-prefix}-cc:before { content: $fa-var-cc; } +.#{$fa-css-prefix}-shekel:before, +.#{$fa-css-prefix}-sheqel:before, +.#{$fa-css-prefix}-ils:before { content: $fa-var-ils; } +.#{$fa-css-prefix}-meanpath:before { content: $fa-var-meanpath; } +.#{$fa-css-prefix}-buysellads:before { content: $fa-var-buysellads; } +.#{$fa-css-prefix}-connectdevelop:before { content: $fa-var-connectdevelop; } +.#{$fa-css-prefix}-dashcube:before { content: $fa-var-dashcube; } +.#{$fa-css-prefix}-forumbee:before { content: $fa-var-forumbee; } +.#{$fa-css-prefix}-leanpub:before { content: $fa-var-leanpub; } +.#{$fa-css-prefix}-sellsy:before { content: $fa-var-sellsy; } +.#{$fa-css-prefix}-shirtsinbulk:before { content: $fa-var-shirtsinbulk; } +.#{$fa-css-prefix}-simplybuilt:before { content: $fa-var-simplybuilt; } +.#{$fa-css-prefix}-skyatlas:before { content: $fa-var-skyatlas; } +.#{$fa-css-prefix}-cart-plus:before { content: $fa-var-cart-plus; } +.#{$fa-css-prefix}-cart-arrow-down:before { content: $fa-var-cart-arrow-down; } +.#{$fa-css-prefix}-diamond:before { content: $fa-var-diamond; } +.#{$fa-css-prefix}-ship:before { content: $fa-var-ship; } +.#{$fa-css-prefix}-user-secret:before { content: $fa-var-user-secret; } +.#{$fa-css-prefix}-motorcycle:before { content: $fa-var-motorcycle; } +.#{$fa-css-prefix}-street-view:before { content: $fa-var-street-view; } +.#{$fa-css-prefix}-heartbeat:before { content: $fa-var-heartbeat; } +.#{$fa-css-prefix}-venus:before { content: $fa-var-venus; } +.#{$fa-css-prefix}-mars:before { content: $fa-var-mars; } +.#{$fa-css-prefix}-mercury:before { content: $fa-var-mercury; } +.#{$fa-css-prefix}-intersex:before, +.#{$fa-css-prefix}-transgender:before { content: $fa-var-transgender; } +.#{$fa-css-prefix}-transgender-alt:before { content: $fa-var-transgender-alt; } +.#{$fa-css-prefix}-venus-double:before { content: $fa-var-venus-double; } +.#{$fa-css-prefix}-mars-double:before { content: $fa-var-mars-double; } +.#{$fa-css-prefix}-venus-mars:before { content: $fa-var-venus-mars; } +.#{$fa-css-prefix}-mars-stroke:before { content: $fa-var-mars-stroke; } +.#{$fa-css-prefix}-mars-stroke-v:before { content: $fa-var-mars-stroke-v; } +.#{$fa-css-prefix}-mars-stroke-h:before { content: $fa-var-mars-stroke-h; } +.#{$fa-css-prefix}-neuter:before { content: $fa-var-neuter; } +.#{$fa-css-prefix}-genderless:before { content: $fa-var-genderless; } +.#{$fa-css-prefix}-facebook-official:before { content: $fa-var-facebook-official; } +.#{$fa-css-prefix}-pinterest-p:before { content: $fa-var-pinterest-p; } +.#{$fa-css-prefix}-whatsapp:before { content: $fa-var-whatsapp; } +.#{$fa-css-prefix}-server:before { content: $fa-var-server; } +.#{$fa-css-prefix}-user-plus:before { content: $fa-var-user-plus; } +.#{$fa-css-prefix}-user-times:before { content: $fa-var-user-times; } +.#{$fa-css-prefix}-hotel:before, +.#{$fa-css-prefix}-bed:before { content: $fa-var-bed; } +.#{$fa-css-prefix}-viacoin:before { content: $fa-var-viacoin; } +.#{$fa-css-prefix}-train:before { content: $fa-var-train; } +.#{$fa-css-prefix}-subway:before { content: $fa-var-subway; } +.#{$fa-css-prefix}-medium:before { content: $fa-var-medium; } +.#{$fa-css-prefix}-yc:before, +.#{$fa-css-prefix}-y-combinator:before { content: $fa-var-y-combinator; } +.#{$fa-css-prefix}-optin-monster:before { content: $fa-var-optin-monster; } +.#{$fa-css-prefix}-opencart:before { content: $fa-var-opencart; } +.#{$fa-css-prefix}-expeditedssl:before { content: $fa-var-expeditedssl; } +.#{$fa-css-prefix}-battery-4:before, +.#{$fa-css-prefix}-battery-full:before { content: $fa-var-battery-full; } +.#{$fa-css-prefix}-battery-3:before, +.#{$fa-css-prefix}-battery-three-quarters:before { content: $fa-var-battery-three-quarters; } +.#{$fa-css-prefix}-battery-2:before, +.#{$fa-css-prefix}-battery-half:before { content: $fa-var-battery-half; } +.#{$fa-css-prefix}-battery-1:before, +.#{$fa-css-prefix}-battery-quarter:before { content: $fa-var-battery-quarter; } +.#{$fa-css-prefix}-battery-0:before, +.#{$fa-css-prefix}-battery-empty:before { content: $fa-var-battery-empty; } +.#{$fa-css-prefix}-mouse-pointer:before { content: $fa-var-mouse-pointer; } +.#{$fa-css-prefix}-i-cursor:before { content: $fa-var-i-cursor; } +.#{$fa-css-prefix}-object-group:before { content: $fa-var-object-group; } +.#{$fa-css-prefix}-object-ungroup:before { content: $fa-var-object-ungroup; } +.#{$fa-css-prefix}-sticky-note:before { content: $fa-var-sticky-note; } +.#{$fa-css-prefix}-sticky-note-o:before { content: $fa-var-sticky-note-o; } +.#{$fa-css-prefix}-cc-jcb:before { content: $fa-var-cc-jcb; } +.#{$fa-css-prefix}-cc-diners-club:before { content: $fa-var-cc-diners-club; } +.#{$fa-css-prefix}-clone:before { content: $fa-var-clone; } +.#{$fa-css-prefix}-balance-scale:before { content: $fa-var-balance-scale; } +.#{$fa-css-prefix}-hourglass-o:before { content: $fa-var-hourglass-o; } +.#{$fa-css-prefix}-hourglass-1:before, +.#{$fa-css-prefix}-hourglass-start:before { content: $fa-var-hourglass-start; } +.#{$fa-css-prefix}-hourglass-2:before, +.#{$fa-css-prefix}-hourglass-half:before { content: $fa-var-hourglass-half; } +.#{$fa-css-prefix}-hourglass-3:before, +.#{$fa-css-prefix}-hourglass-end:before { content: $fa-var-hourglass-end; } +.#{$fa-css-prefix}-hourglass:before { content: $fa-var-hourglass; } +.#{$fa-css-prefix}-hand-grab-o:before, +.#{$fa-css-prefix}-hand-rock-o:before { content: $fa-var-hand-rock-o; } +.#{$fa-css-prefix}-hand-stop-o:before, +.#{$fa-css-prefix}-hand-paper-o:before { content: $fa-var-hand-paper-o; } +.#{$fa-css-prefix}-hand-scissors-o:before { content: $fa-var-hand-scissors-o; } +.#{$fa-css-prefix}-hand-lizard-o:before { content: $fa-var-hand-lizard-o; } +.#{$fa-css-prefix}-hand-spock-o:before { content: $fa-var-hand-spock-o; } +.#{$fa-css-prefix}-hand-pointer-o:before { content: $fa-var-hand-pointer-o; } +.#{$fa-css-prefix}-hand-peace-o:before { content: $fa-var-hand-peace-o; } +.#{$fa-css-prefix}-trademark:before { content: $fa-var-trademark; } +.#{$fa-css-prefix}-registered:before { content: $fa-var-registered; } +.#{$fa-css-prefix}-creative-commons:before { content: $fa-var-creative-commons; } +.#{$fa-css-prefix}-gg:before { content: $fa-var-gg; } +.#{$fa-css-prefix}-gg-circle:before { content: $fa-var-gg-circle; } +.#{$fa-css-prefix}-tripadvisor:before { content: $fa-var-tripadvisor; } +.#{$fa-css-prefix}-odnoklassniki:before { content: $fa-var-odnoklassniki; } +.#{$fa-css-prefix}-odnoklassniki-square:before { content: $fa-var-odnoklassniki-square; } +.#{$fa-css-prefix}-get-pocket:before { content: $fa-var-get-pocket; } +.#{$fa-css-prefix}-wikipedia-w:before { content: $fa-var-wikipedia-w; } +.#{$fa-css-prefix}-safari:before { content: $fa-var-safari; } +.#{$fa-css-prefix}-chrome:before { content: $fa-var-chrome; } +.#{$fa-css-prefix}-firefox:before { content: $fa-var-firefox; } +.#{$fa-css-prefix}-opera:before { content: $fa-var-opera; } +.#{$fa-css-prefix}-internet-explorer:before { content: $fa-var-internet-explorer; } +.#{$fa-css-prefix}-tv:before, +.#{$fa-css-prefix}-television:before { content: $fa-var-television; } +.#{$fa-css-prefix}-contao:before { content: $fa-var-contao; } +.#{$fa-css-prefix}-500px:before { content: $fa-var-500px; } +.#{$fa-css-prefix}-amazon:before { content: $fa-var-amazon; } +.#{$fa-css-prefix}-calendar-plus-o:before { content: $fa-var-calendar-plus-o; } +.#{$fa-css-prefix}-calendar-minus-o:before { content: $fa-var-calendar-minus-o; } +.#{$fa-css-prefix}-calendar-times-o:before { content: $fa-var-calendar-times-o; } +.#{$fa-css-prefix}-calendar-check-o:before { content: $fa-var-calendar-check-o; } +.#{$fa-css-prefix}-industry:before { content: $fa-var-industry; } +.#{$fa-css-prefix}-map-pin:before { content: $fa-var-map-pin; } +.#{$fa-css-prefix}-map-signs:before { content: $fa-var-map-signs; } +.#{$fa-css-prefix}-map-o:before { content: $fa-var-map-o; } +.#{$fa-css-prefix}-map:before { content: $fa-var-map; } +.#{$fa-css-prefix}-commenting:before { content: $fa-var-commenting; } +.#{$fa-css-prefix}-commenting-o:before { content: $fa-var-commenting-o; } +.#{$fa-css-prefix}-houzz:before { content: $fa-var-houzz; } +.#{$fa-css-prefix}-vimeo:before { content: $fa-var-vimeo; } +.#{$fa-css-prefix}-black-tie:before { content: $fa-var-black-tie; } +.#{$fa-css-prefix}-fonticons:before { content: $fa-var-fonticons; } +.#{$fa-css-prefix}-reddit-alien:before { content: $fa-var-reddit-alien; } +.#{$fa-css-prefix}-edge:before { content: $fa-var-edge; } +.#{$fa-css-prefix}-credit-card-alt:before { content: $fa-var-credit-card-alt; } +.#{$fa-css-prefix}-codiepie:before { content: $fa-var-codiepie; } +.#{$fa-css-prefix}-modx:before { content: $fa-var-modx; } +.#{$fa-css-prefix}-fort-awesome:before { content: $fa-var-fort-awesome; } +.#{$fa-css-prefix}-usb:before { content: $fa-var-usb; } +.#{$fa-css-prefix}-product-hunt:before { content: $fa-var-product-hunt; } +.#{$fa-css-prefix}-mixcloud:before { content: $fa-var-mixcloud; } +.#{$fa-css-prefix}-scribd:before { content: $fa-var-scribd; } +.#{$fa-css-prefix}-pause-circle:before { content: $fa-var-pause-circle; } +.#{$fa-css-prefix}-pause-circle-o:before { content: $fa-var-pause-circle-o; } +.#{$fa-css-prefix}-stop-circle:before { content: $fa-var-stop-circle; } +.#{$fa-css-prefix}-stop-circle-o:before { content: $fa-var-stop-circle-o; } +.#{$fa-css-prefix}-shopping-bag:before { content: $fa-var-shopping-bag; } +.#{$fa-css-prefix}-shopping-basket:before { content: $fa-var-shopping-basket; } +.#{$fa-css-prefix}-hashtag:before { content: $fa-var-hashtag; } +.#{$fa-css-prefix}-bluetooth:before { content: $fa-var-bluetooth; } +.#{$fa-css-prefix}-bluetooth-b:before { content: $fa-var-bluetooth-b; } +.#{$fa-css-prefix}-percent:before { content: $fa-var-percent; } +.#{$fa-css-prefix}-gitlab:before { content: $fa-var-gitlab; } +.#{$fa-css-prefix}-wpbeginner:before { content: $fa-var-wpbeginner; } +.#{$fa-css-prefix}-wpforms:before { content: $fa-var-wpforms; } +.#{$fa-css-prefix}-envira:before { content: $fa-var-envira; } +.#{$fa-css-prefix}-universal-access:before { content: $fa-var-universal-access; } +.#{$fa-css-prefix}-wheelchair-alt:before { content: $fa-var-wheelchair-alt; } +.#{$fa-css-prefix}-question-circle-o:before { content: $fa-var-question-circle-o; } +.#{$fa-css-prefix}-blind:before { content: $fa-var-blind; } +.#{$fa-css-prefix}-audio-description:before { content: $fa-var-audio-description; } +.#{$fa-css-prefix}-volume-control-phone:before { content: $fa-var-volume-control-phone; } +.#{$fa-css-prefix}-braille:before { content: $fa-var-braille; } +.#{$fa-css-prefix}-assistive-listening-systems:before { content: $fa-var-assistive-listening-systems; } +.#{$fa-css-prefix}-asl-interpreting:before, +.#{$fa-css-prefix}-american-sign-language-interpreting:before { content: $fa-var-american-sign-language-interpreting; } +.#{$fa-css-prefix}-deafness:before, +.#{$fa-css-prefix}-hard-of-hearing:before, +.#{$fa-css-prefix}-deaf:before { content: $fa-var-deaf; } +.#{$fa-css-prefix}-glide:before { content: $fa-var-glide; } +.#{$fa-css-prefix}-glide-g:before { content: $fa-var-glide-g; } +.#{$fa-css-prefix}-signing:before, +.#{$fa-css-prefix}-sign-language:before { content: $fa-var-sign-language; } +.#{$fa-css-prefix}-low-vision:before { content: $fa-var-low-vision; } +.#{$fa-css-prefix}-viadeo:before { content: $fa-var-viadeo; } +.#{$fa-css-prefix}-viadeo-square:before { content: $fa-var-viadeo-square; } +.#{$fa-css-prefix}-snapchat:before { content: $fa-var-snapchat; } +.#{$fa-css-prefix}-snapchat-ghost:before { content: $fa-var-snapchat-ghost; } +.#{$fa-css-prefix}-snapchat-square:before { content: $fa-var-snapchat-square; } +.#{$fa-css-prefix}-pied-piper:before { content: $fa-var-pied-piper; } +.#{$fa-css-prefix}-first-order:before { content: $fa-var-first-order; } +.#{$fa-css-prefix}-yoast:before { content: $fa-var-yoast; } +.#{$fa-css-prefix}-themeisle:before { content: $fa-var-themeisle; } +.#{$fa-css-prefix}-google-plus-circle:before, +.#{$fa-css-prefix}-google-plus-official:before { content: $fa-var-google-plus-official; } +.#{$fa-css-prefix}-fa:before, +.#{$fa-css-prefix}-font-awesome:before { content: $fa-var-font-awesome; } diff --git a/extras/font-awesome/scss/_larger.scss b/extras/font-awesome/scss/_larger.scss new file mode 100644 index 0000000..41e9a81 --- /dev/null +++ b/extras/font-awesome/scss/_larger.scss @@ -0,0 +1,13 @@ +// Icon Sizes +// ------------------------- + +/* makes the font 33% larger relative to the icon container */ +.#{$fa-css-prefix}-lg { + font-size: (4em / 3); + line-height: (3em / 4); + vertical-align: -15%; +} +.#{$fa-css-prefix}-2x { font-size: 2em; } +.#{$fa-css-prefix}-3x { font-size: 3em; } +.#{$fa-css-prefix}-4x { font-size: 4em; } +.#{$fa-css-prefix}-5x { font-size: 5em; } diff --git a/extras/font-awesome/scss/_list.scss b/extras/font-awesome/scss/_list.scss new file mode 100644 index 0000000..7d1e4d5 --- /dev/null +++ b/extras/font-awesome/scss/_list.scss @@ -0,0 +1,19 @@ +// List Icons +// ------------------------- + +.#{$fa-css-prefix}-ul { + padding-left: 0; + margin-left: $fa-li-width; + list-style-type: none; + > li { position: relative; } +} +.#{$fa-css-prefix}-li { + position: absolute; + left: -$fa-li-width; + width: $fa-li-width; + top: (2em / 14); + text-align: center; + &.#{$fa-css-prefix}-lg { + left: -$fa-li-width + (4em / 14); + } +} diff --git a/extras/font-awesome/scss/_mixins.scss b/extras/font-awesome/scss/_mixins.scss new file mode 100644 index 0000000..c3bbd57 --- /dev/null +++ b/extras/font-awesome/scss/_mixins.scss @@ -0,0 +1,60 @@ +// Mixins +// -------------------------- + +@mixin fa-icon() { + display: inline-block; + font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration + font-size: inherit; // can't have font-size inherit on line above, so need to override + text-rendering: auto; // optimizelegibility throws things off #1094 + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + +} + +@mixin fa-icon-rotate($degrees, $rotation) { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; + -webkit-transform: rotate($degrees); + -ms-transform: rotate($degrees); + transform: rotate($degrees); +} + +@mixin fa-icon-flip($horiz, $vert, $rotation) { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; + -webkit-transform: scale($horiz, $vert); + -ms-transform: scale($horiz, $vert); + transform: scale($horiz, $vert); +} + + +// Only display content to screen readers. A la Bootstrap 4. +// +// See: http://a11yproject.com/posts/how-to-hide-content/ + +@mixin sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0,0,0,0); + border: 0; +} + +// Use in conjunction with .sr-only to only display content when it's focused. +// +// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 +// +// Credit: HTML5 Boilerplate + +@mixin sr-only-focusable { + &:active, + &:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; + } +} diff --git a/extras/font-awesome/scss/_path.scss b/extras/font-awesome/scss/_path.scss new file mode 100644 index 0000000..bb457c2 --- /dev/null +++ b/extras/font-awesome/scss/_path.scss @@ -0,0 +1,15 @@ +/* FONT PATH + * -------------------------- */ + +@font-face { + font-family: 'FontAwesome'; + src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); + src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), + url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), + url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), + url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), + url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); +// src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts + font-weight: normal; + font-style: normal; +} diff --git a/extras/font-awesome/scss/_rotated-flipped.scss b/extras/font-awesome/scss/_rotated-flipped.scss new file mode 100644 index 0000000..a3558fd --- /dev/null +++ b/extras/font-awesome/scss/_rotated-flipped.scss @@ -0,0 +1,20 @@ +// Rotated & Flipped Icons +// ------------------------- + +.#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } +.#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } +.#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } + +.#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } +.#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } + +// Hook for IE8-9 +// ------------------------- + +:root .#{$fa-css-prefix}-rotate-90, +:root .#{$fa-css-prefix}-rotate-180, +:root .#{$fa-css-prefix}-rotate-270, +:root .#{$fa-css-prefix}-flip-horizontal, +:root .#{$fa-css-prefix}-flip-vertical { + filter: none; +} diff --git a/extras/font-awesome/scss/_screen-reader.scss b/extras/font-awesome/scss/_screen-reader.scss new file mode 100644 index 0000000..637426f --- /dev/null +++ b/extras/font-awesome/scss/_screen-reader.scss @@ -0,0 +1,5 @@ +// Screen Readers +// ------------------------- + +.sr-only { @include sr-only(); } +.sr-only-focusable { @include sr-only-focusable(); } diff --git a/extras/font-awesome/scss/_stacked.scss b/extras/font-awesome/scss/_stacked.scss new file mode 100644 index 0000000..aef7403 --- /dev/null +++ b/extras/font-awesome/scss/_stacked.scss @@ -0,0 +1,20 @@ +// Stacked Icons +// ------------------------- + +.#{$fa-css-prefix}-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.#{$fa-css-prefix}-stack-1x { line-height: inherit; } +.#{$fa-css-prefix}-stack-2x { font-size: 2em; } +.#{$fa-css-prefix}-inverse { color: $fa-inverse; } diff --git a/extras/font-awesome/scss/_variables.scss b/extras/font-awesome/scss/_variables.scss new file mode 100644 index 0000000..32cd650 --- /dev/null +++ b/extras/font-awesome/scss/_variables.scss @@ -0,0 +1,744 @@ +// Variables +// -------------------------- + +$fa-font-path: "../fonts" !default; +$fa-font-size-base: 18px !default; +$fa-line-height-base: 1 !default; +//$fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.6.3/fonts" !default; // for referencing Bootstrap CDN font files directly +$fa-css-prefix: ame-fa !default; +$fa-version: "4.6.3" !default; +$fa-border-color: #eee !default; +$fa-inverse: #fff !default; +$fa-li-width: (30em / 14) !default; + +$fa-var-500px: "\f26e"; +$fa-var-adjust: "\f042"; +$fa-var-adn: "\f170"; +$fa-var-align-center: "\f037"; +$fa-var-align-justify: "\f039"; +$fa-var-align-left: "\f036"; +$fa-var-align-right: "\f038"; +$fa-var-amazon: "\f270"; +$fa-var-ambulance: "\f0f9"; +$fa-var-american-sign-language-interpreting: "\f2a3"; +$fa-var-anchor: "\f13d"; +$fa-var-android: "\f17b"; +$fa-var-angellist: "\f209"; +$fa-var-angle-double-down: "\f103"; +$fa-var-angle-double-left: "\f100"; +$fa-var-angle-double-right: "\f101"; +$fa-var-angle-double-up: "\f102"; +$fa-var-angle-down: "\f107"; +$fa-var-angle-left: "\f104"; +$fa-var-angle-right: "\f105"; +$fa-var-angle-up: "\f106"; +$fa-var-apple: "\f179"; +$fa-var-archive: "\f187"; +$fa-var-area-chart: "\f1fe"; +$fa-var-arrow-circle-down: "\f0ab"; +$fa-var-arrow-circle-left: "\f0a8"; +$fa-var-arrow-circle-o-down: "\f01a"; +$fa-var-arrow-circle-o-left: "\f190"; +$fa-var-arrow-circle-o-right: "\f18e"; +$fa-var-arrow-circle-o-up: "\f01b"; +$fa-var-arrow-circle-right: "\f0a9"; +$fa-var-arrow-circle-up: "\f0aa"; +$fa-var-arrow-down: "\f063"; +$fa-var-arrow-left: "\f060"; +$fa-var-arrow-right: "\f061"; +$fa-var-arrow-up: "\f062"; +$fa-var-arrows: "\f047"; +$fa-var-arrows-alt: "\f0b2"; +$fa-var-arrows-h: "\f07e"; +$fa-var-arrows-v: "\f07d"; +$fa-var-asl-interpreting: "\f2a3"; +$fa-var-assistive-listening-systems: "\f2a2"; +$fa-var-asterisk: "\f069"; +$fa-var-at: "\f1fa"; +$fa-var-audio-description: "\f29e"; +$fa-var-automobile: "\f1b9"; +$fa-var-backward: "\f04a"; +$fa-var-balance-scale: "\f24e"; +$fa-var-ban: "\f05e"; +$fa-var-bank: "\f19c"; +$fa-var-bar-chart: "\f080"; +$fa-var-bar-chart-o: "\f080"; +$fa-var-barcode: "\f02a"; +$fa-var-bars: "\f0c9"; +$fa-var-battery-0: "\f244"; +$fa-var-battery-1: "\f243"; +$fa-var-battery-2: "\f242"; +$fa-var-battery-3: "\f241"; +$fa-var-battery-4: "\f240"; +$fa-var-battery-empty: "\f244"; +$fa-var-battery-full: "\f240"; +$fa-var-battery-half: "\f242"; +$fa-var-battery-quarter: "\f243"; +$fa-var-battery-three-quarters: "\f241"; +$fa-var-bed: "\f236"; +$fa-var-beer: "\f0fc"; +$fa-var-behance: "\f1b4"; +$fa-var-behance-square: "\f1b5"; +$fa-var-bell: "\f0f3"; +$fa-var-bell-o: "\f0a2"; +$fa-var-bell-slash: "\f1f6"; +$fa-var-bell-slash-o: "\f1f7"; +$fa-var-bicycle: "\f206"; +$fa-var-binoculars: "\f1e5"; +$fa-var-birthday-cake: "\f1fd"; +$fa-var-bitbucket: "\f171"; +$fa-var-bitbucket-square: "\f172"; +$fa-var-bitcoin: "\f15a"; +$fa-var-black-tie: "\f27e"; +$fa-var-blind: "\f29d"; +$fa-var-bluetooth: "\f293"; +$fa-var-bluetooth-b: "\f294"; +$fa-var-bold: "\f032"; +$fa-var-bolt: "\f0e7"; +$fa-var-bomb: "\f1e2"; +$fa-var-book: "\f02d"; +$fa-var-bookmark: "\f02e"; +$fa-var-bookmark-o: "\f097"; +$fa-var-braille: "\f2a1"; +$fa-var-briefcase: "\f0b1"; +$fa-var-btc: "\f15a"; +$fa-var-bug: "\f188"; +$fa-var-building: "\f1ad"; +$fa-var-building-o: "\f0f7"; +$fa-var-bullhorn: "\f0a1"; +$fa-var-bullseye: "\f140"; +$fa-var-bus: "\f207"; +$fa-var-buysellads: "\f20d"; +$fa-var-cab: "\f1ba"; +$fa-var-calculator: "\f1ec"; +$fa-var-calendar: "\f073"; +$fa-var-calendar-check-o: "\f274"; +$fa-var-calendar-minus-o: "\f272"; +$fa-var-calendar-o: "\f133"; +$fa-var-calendar-plus-o: "\f271"; +$fa-var-calendar-times-o: "\f273"; +$fa-var-camera: "\f030"; +$fa-var-camera-retro: "\f083"; +$fa-var-car: "\f1b9"; +$fa-var-caret-down: "\f0d7"; +$fa-var-caret-left: "\f0d9"; +$fa-var-caret-right: "\f0da"; +$fa-var-caret-square-o-down: "\f150"; +$fa-var-caret-square-o-left: "\f191"; +$fa-var-caret-square-o-right: "\f152"; +$fa-var-caret-square-o-up: "\f151"; +$fa-var-caret-up: "\f0d8"; +$fa-var-cart-arrow-down: "\f218"; +$fa-var-cart-plus: "\f217"; +$fa-var-cc: "\f20a"; +$fa-var-cc-amex: "\f1f3"; +$fa-var-cc-diners-club: "\f24c"; +$fa-var-cc-discover: "\f1f2"; +$fa-var-cc-jcb: "\f24b"; +$fa-var-cc-mastercard: "\f1f1"; +$fa-var-cc-paypal: "\f1f4"; +$fa-var-cc-stripe: "\f1f5"; +$fa-var-cc-visa: "\f1f0"; +$fa-var-certificate: "\f0a3"; +$fa-var-chain: "\f0c1"; +$fa-var-chain-broken: "\f127"; +$fa-var-check: "\f00c"; +$fa-var-check-circle: "\f058"; +$fa-var-check-circle-o: "\f05d"; +$fa-var-check-square: "\f14a"; +$fa-var-check-square-o: "\f046"; +$fa-var-chevron-circle-down: "\f13a"; +$fa-var-chevron-circle-left: "\f137"; +$fa-var-chevron-circle-right: "\f138"; +$fa-var-chevron-circle-up: "\f139"; +$fa-var-chevron-down: "\f078"; +$fa-var-chevron-left: "\f053"; +$fa-var-chevron-right: "\f054"; +$fa-var-chevron-up: "\f077"; +$fa-var-child: "\f1ae"; +$fa-var-chrome: "\f268"; +$fa-var-circle: "\f111"; +$fa-var-circle-o: "\f10c"; +$fa-var-circle-o-notch: "\f1ce"; +$fa-var-circle-thin: "\f1db"; +$fa-var-clipboard: "\f0ea"; +$fa-var-clock-o: "\f017"; +$fa-var-clone: "\f24d"; +$fa-var-close: "\f00d"; +$fa-var-cloud: "\f0c2"; +$fa-var-cloud-download: "\f0ed"; +$fa-var-cloud-upload: "\f0ee"; +$fa-var-cny: "\f157"; +$fa-var-code: "\f121"; +$fa-var-code-fork: "\f126"; +$fa-var-codepen: "\f1cb"; +$fa-var-codiepie: "\f284"; +$fa-var-coffee: "\f0f4"; +$fa-var-cog: "\f013"; +$fa-var-cogs: "\f085"; +$fa-var-columns: "\f0db"; +$fa-var-comment: "\f075"; +$fa-var-comment-o: "\f0e5"; +$fa-var-commenting: "\f27a"; +$fa-var-commenting-o: "\f27b"; +$fa-var-comments: "\f086"; +$fa-var-comments-o: "\f0e6"; +$fa-var-compass: "\f14e"; +$fa-var-compress: "\f066"; +$fa-var-connectdevelop: "\f20e"; +$fa-var-contao: "\f26d"; +$fa-var-copy: "\f0c5"; +$fa-var-copyright: "\f1f9"; +$fa-var-creative-commons: "\f25e"; +$fa-var-credit-card: "\f09d"; +$fa-var-credit-card-alt: "\f283"; +$fa-var-crop: "\f125"; +$fa-var-crosshairs: "\f05b"; +$fa-var-css3: "\f13c"; +$fa-var-cube: "\f1b2"; +$fa-var-cubes: "\f1b3"; +$fa-var-cut: "\f0c4"; +$fa-var-cutlery: "\f0f5"; +$fa-var-dashboard: "\f0e4"; +$fa-var-dashcube: "\f210"; +$fa-var-database: "\f1c0"; +$fa-var-deaf: "\f2a4"; +$fa-var-deafness: "\f2a4"; +$fa-var-dedent: "\f03b"; +$fa-var-delicious: "\f1a5"; +$fa-var-desktop: "\f108"; +$fa-var-deviantart: "\f1bd"; +$fa-var-diamond: "\f219"; +$fa-var-digg: "\f1a6"; +$fa-var-dollar: "\f155"; +$fa-var-dot-circle-o: "\f192"; +$fa-var-download: "\f019"; +$fa-var-dribbble: "\f17d"; +$fa-var-dropbox: "\f16b"; +$fa-var-drupal: "\f1a9"; +$fa-var-edge: "\f282"; +$fa-var-edit: "\f044"; +$fa-var-eject: "\f052"; +$fa-var-ellipsis-h: "\f141"; +$fa-var-ellipsis-v: "\f142"; +$fa-var-empire: "\f1d1"; +$fa-var-envelope: "\f0e0"; +$fa-var-envelope-o: "\f003"; +$fa-var-envelope-square: "\f199"; +$fa-var-envira: "\f299"; +$fa-var-eraser: "\f12d"; +$fa-var-eur: "\f153"; +$fa-var-euro: "\f153"; +$fa-var-exchange: "\f0ec"; +$fa-var-exclamation: "\f12a"; +$fa-var-exclamation-circle: "\f06a"; +$fa-var-exclamation-triangle: "\f071"; +$fa-var-expand: "\f065"; +$fa-var-expeditedssl: "\f23e"; +$fa-var-external-link: "\f08e"; +$fa-var-external-link-square: "\f14c"; +$fa-var-eye: "\f06e"; +$fa-var-eye-slash: "\f070"; +$fa-var-eyedropper: "\f1fb"; +$fa-var-fa: "\f2b4"; +$fa-var-facebook: "\f09a"; +$fa-var-facebook-f: "\f09a"; +$fa-var-facebook-official: "\f230"; +$fa-var-facebook-square: "\f082"; +$fa-var-fast-backward: "\f049"; +$fa-var-fast-forward: "\f050"; +$fa-var-fax: "\f1ac"; +$fa-var-feed: "\f09e"; +$fa-var-female: "\f182"; +$fa-var-fighter-jet: "\f0fb"; +$fa-var-file: "\f15b"; +$fa-var-file-archive-o: "\f1c6"; +$fa-var-file-audio-o: "\f1c7"; +$fa-var-file-code-o: "\f1c9"; +$fa-var-file-excel-o: "\f1c3"; +$fa-var-file-image-o: "\f1c5"; +$fa-var-file-movie-o: "\f1c8"; +$fa-var-file-o: "\f016"; +$fa-var-file-pdf-o: "\f1c1"; +$fa-var-file-photo-o: "\f1c5"; +$fa-var-file-picture-o: "\f1c5"; +$fa-var-file-powerpoint-o: "\f1c4"; +$fa-var-file-sound-o: "\f1c7"; +$fa-var-file-text: "\f15c"; +$fa-var-file-text-o: "\f0f6"; +$fa-var-file-video-o: "\f1c8"; +$fa-var-file-word-o: "\f1c2"; +$fa-var-file-zip-o: "\f1c6"; +$fa-var-files-o: "\f0c5"; +$fa-var-film: "\f008"; +$fa-var-filter: "\f0b0"; +$fa-var-fire: "\f06d"; +$fa-var-fire-extinguisher: "\f134"; +$fa-var-firefox: "\f269"; +$fa-var-first-order: "\f2b0"; +$fa-var-flag: "\f024"; +$fa-var-flag-checkered: "\f11e"; +$fa-var-flag-o: "\f11d"; +$fa-var-flash: "\f0e7"; +$fa-var-flask: "\f0c3"; +$fa-var-flickr: "\f16e"; +$fa-var-floppy-o: "\f0c7"; +$fa-var-folder: "\f07b"; +$fa-var-folder-o: "\f114"; +$fa-var-folder-open: "\f07c"; +$fa-var-folder-open-o: "\f115"; +$fa-var-font: "\f031"; +$fa-var-font-awesome: "\f2b4"; +$fa-var-fonticons: "\f280"; +$fa-var-fort-awesome: "\f286"; +$fa-var-forumbee: "\f211"; +$fa-var-forward: "\f04e"; +$fa-var-foursquare: "\f180"; +$fa-var-frown-o: "\f119"; +$fa-var-futbol-o: "\f1e3"; +$fa-var-gamepad: "\f11b"; +$fa-var-gavel: "\f0e3"; +$fa-var-gbp: "\f154"; +$fa-var-ge: "\f1d1"; +$fa-var-gear: "\f013"; +$fa-var-gears: "\f085"; +$fa-var-genderless: "\f22d"; +$fa-var-get-pocket: "\f265"; +$fa-var-gg: "\f260"; +$fa-var-gg-circle: "\f261"; +$fa-var-gift: "\f06b"; +$fa-var-git: "\f1d3"; +$fa-var-git-square: "\f1d2"; +$fa-var-github: "\f09b"; +$fa-var-github-alt: "\f113"; +$fa-var-github-square: "\f092"; +$fa-var-gitlab: "\f296"; +$fa-var-gittip: "\f184"; +$fa-var-glass: "\f000"; +$fa-var-glide: "\f2a5"; +$fa-var-glide-g: "\f2a6"; +$fa-var-globe: "\f0ac"; +$fa-var-google: "\f1a0"; +$fa-var-google-plus: "\f0d5"; +$fa-var-google-plus-circle: "\f2b3"; +$fa-var-google-plus-official: "\f2b3"; +$fa-var-google-plus-square: "\f0d4"; +$fa-var-google-wallet: "\f1ee"; +$fa-var-graduation-cap: "\f19d"; +$fa-var-gratipay: "\f184"; +$fa-var-group: "\f0c0"; +$fa-var-h-square: "\f0fd"; +$fa-var-hacker-news: "\f1d4"; +$fa-var-hand-grab-o: "\f255"; +$fa-var-hand-lizard-o: "\f258"; +$fa-var-hand-o-down: "\f0a7"; +$fa-var-hand-o-left: "\f0a5"; +$fa-var-hand-o-right: "\f0a4"; +$fa-var-hand-o-up: "\f0a6"; +$fa-var-hand-paper-o: "\f256"; +$fa-var-hand-peace-o: "\f25b"; +$fa-var-hand-pointer-o: "\f25a"; +$fa-var-hand-rock-o: "\f255"; +$fa-var-hand-scissors-o: "\f257"; +$fa-var-hand-spock-o: "\f259"; +$fa-var-hand-stop-o: "\f256"; +$fa-var-hard-of-hearing: "\f2a4"; +$fa-var-hashtag: "\f292"; +$fa-var-hdd-o: "\f0a0"; +$fa-var-header: "\f1dc"; +$fa-var-headphones: "\f025"; +$fa-var-heart: "\f004"; +$fa-var-heart-o: "\f08a"; +$fa-var-heartbeat: "\f21e"; +$fa-var-history: "\f1da"; +$fa-var-home: "\f015"; +$fa-var-hospital-o: "\f0f8"; +$fa-var-hotel: "\f236"; +$fa-var-hourglass: "\f254"; +$fa-var-hourglass-1: "\f251"; +$fa-var-hourglass-2: "\f252"; +$fa-var-hourglass-3: "\f253"; +$fa-var-hourglass-end: "\f253"; +$fa-var-hourglass-half: "\f252"; +$fa-var-hourglass-o: "\f250"; +$fa-var-hourglass-start: "\f251"; +$fa-var-houzz: "\f27c"; +$fa-var-html5: "\f13b"; +$fa-var-i-cursor: "\f246"; +$fa-var-ils: "\f20b"; +$fa-var-image: "\f03e"; +$fa-var-inbox: "\f01c"; +$fa-var-indent: "\f03c"; +$fa-var-industry: "\f275"; +$fa-var-info: "\f129"; +$fa-var-info-circle: "\f05a"; +$fa-var-inr: "\f156"; +$fa-var-instagram: "\f16d"; +$fa-var-institution: "\f19c"; +$fa-var-internet-explorer: "\f26b"; +$fa-var-intersex: "\f224"; +$fa-var-ioxhost: "\f208"; +$fa-var-italic: "\f033"; +$fa-var-joomla: "\f1aa"; +$fa-var-jpy: "\f157"; +$fa-var-jsfiddle: "\f1cc"; +$fa-var-key: "\f084"; +$fa-var-keyboard-o: "\f11c"; +$fa-var-krw: "\f159"; +$fa-var-language: "\f1ab"; +$fa-var-laptop: "\f109"; +$fa-var-lastfm: "\f202"; +$fa-var-lastfm-square: "\f203"; +$fa-var-leaf: "\f06c"; +$fa-var-leanpub: "\f212"; +$fa-var-legal: "\f0e3"; +$fa-var-lemon-o: "\f094"; +$fa-var-level-down: "\f149"; +$fa-var-level-up: "\f148"; +$fa-var-life-bouy: "\f1cd"; +$fa-var-life-buoy: "\f1cd"; +$fa-var-life-ring: "\f1cd"; +$fa-var-life-saver: "\f1cd"; +$fa-var-lightbulb-o: "\f0eb"; +$fa-var-line-chart: "\f201"; +$fa-var-link: "\f0c1"; +$fa-var-linkedin: "\f0e1"; +$fa-var-linkedin-square: "\f08c"; +$fa-var-linux: "\f17c"; +$fa-var-list: "\f03a"; +$fa-var-list-alt: "\f022"; +$fa-var-list-ol: "\f0cb"; +$fa-var-list-ul: "\f0ca"; +$fa-var-location-arrow: "\f124"; +$fa-var-lock: "\f023"; +$fa-var-long-arrow-down: "\f175"; +$fa-var-long-arrow-left: "\f177"; +$fa-var-long-arrow-right: "\f178"; +$fa-var-long-arrow-up: "\f176"; +$fa-var-low-vision: "\f2a8"; +$fa-var-magic: "\f0d0"; +$fa-var-magnet: "\f076"; +$fa-var-mail-forward: "\f064"; +$fa-var-mail-reply: "\f112"; +$fa-var-mail-reply-all: "\f122"; +$fa-var-male: "\f183"; +$fa-var-map: "\f279"; +$fa-var-map-marker: "\f041"; +$fa-var-map-o: "\f278"; +$fa-var-map-pin: "\f276"; +$fa-var-map-signs: "\f277"; +$fa-var-mars: "\f222"; +$fa-var-mars-double: "\f227"; +$fa-var-mars-stroke: "\f229"; +$fa-var-mars-stroke-h: "\f22b"; +$fa-var-mars-stroke-v: "\f22a"; +$fa-var-maxcdn: "\f136"; +$fa-var-meanpath: "\f20c"; +$fa-var-medium: "\f23a"; +$fa-var-medkit: "\f0fa"; +$fa-var-meh-o: "\f11a"; +$fa-var-mercury: "\f223"; +$fa-var-microphone: "\f130"; +$fa-var-microphone-slash: "\f131"; +$fa-var-minus: "\f068"; +$fa-var-minus-circle: "\f056"; +$fa-var-minus-square: "\f146"; +$fa-var-minus-square-o: "\f147"; +$fa-var-mixcloud: "\f289"; +$fa-var-mobile: "\f10b"; +$fa-var-mobile-phone: "\f10b"; +$fa-var-modx: "\f285"; +$fa-var-money: "\f0d6"; +$fa-var-moon-o: "\f186"; +$fa-var-mortar-board: "\f19d"; +$fa-var-motorcycle: "\f21c"; +$fa-var-mouse-pointer: "\f245"; +$fa-var-music: "\f001"; +$fa-var-navicon: "\f0c9"; +$fa-var-neuter: "\f22c"; +$fa-var-newspaper-o: "\f1ea"; +$fa-var-object-group: "\f247"; +$fa-var-object-ungroup: "\f248"; +$fa-var-odnoklassniki: "\f263"; +$fa-var-odnoklassniki-square: "\f264"; +$fa-var-opencart: "\f23d"; +$fa-var-openid: "\f19b"; +$fa-var-opera: "\f26a"; +$fa-var-optin-monster: "\f23c"; +$fa-var-outdent: "\f03b"; +$fa-var-pagelines: "\f18c"; +$fa-var-paint-brush: "\f1fc"; +$fa-var-paper-plane: "\f1d8"; +$fa-var-paper-plane-o: "\f1d9"; +$fa-var-paperclip: "\f0c6"; +$fa-var-paragraph: "\f1dd"; +$fa-var-paste: "\f0ea"; +$fa-var-pause: "\f04c"; +$fa-var-pause-circle: "\f28b"; +$fa-var-pause-circle-o: "\f28c"; +$fa-var-paw: "\f1b0"; +$fa-var-paypal: "\f1ed"; +$fa-var-pencil: "\f040"; +$fa-var-pencil-square: "\f14b"; +$fa-var-pencil-square-o: "\f044"; +$fa-var-percent: "\f295"; +$fa-var-phone: "\f095"; +$fa-var-phone-square: "\f098"; +$fa-var-photo: "\f03e"; +$fa-var-picture-o: "\f03e"; +$fa-var-pie-chart: "\f200"; +$fa-var-pied-piper: "\f2ae"; +$fa-var-pied-piper-alt: "\f1a8"; +$fa-var-pied-piper-pp: "\f1a7"; +$fa-var-pinterest: "\f0d2"; +$fa-var-pinterest-p: "\f231"; +$fa-var-pinterest-square: "\f0d3"; +$fa-var-plane: "\f072"; +$fa-var-play: "\f04b"; +$fa-var-play-circle: "\f144"; +$fa-var-play-circle-o: "\f01d"; +$fa-var-plug: "\f1e6"; +$fa-var-plus: "\f067"; +$fa-var-plus-circle: "\f055"; +$fa-var-plus-square: "\f0fe"; +$fa-var-plus-square-o: "\f196"; +$fa-var-power-off: "\f011"; +$fa-var-print: "\f02f"; +$fa-var-product-hunt: "\f288"; +$fa-var-puzzle-piece: "\f12e"; +$fa-var-qq: "\f1d6"; +$fa-var-qrcode: "\f029"; +$fa-var-question: "\f128"; +$fa-var-question-circle: "\f059"; +$fa-var-question-circle-o: "\f29c"; +$fa-var-quote-left: "\f10d"; +$fa-var-quote-right: "\f10e"; +$fa-var-ra: "\f1d0"; +$fa-var-random: "\f074"; +$fa-var-rebel: "\f1d0"; +$fa-var-recycle: "\f1b8"; +$fa-var-reddit: "\f1a1"; +$fa-var-reddit-alien: "\f281"; +$fa-var-reddit-square: "\f1a2"; +$fa-var-refresh: "\f021"; +$fa-var-registered: "\f25d"; +$fa-var-remove: "\f00d"; +$fa-var-renren: "\f18b"; +$fa-var-reorder: "\f0c9"; +$fa-var-repeat: "\f01e"; +$fa-var-reply: "\f112"; +$fa-var-reply-all: "\f122"; +$fa-var-resistance: "\f1d0"; +$fa-var-retweet: "\f079"; +$fa-var-rmb: "\f157"; +$fa-var-road: "\f018"; +$fa-var-rocket: "\f135"; +$fa-var-rotate-left: "\f0e2"; +$fa-var-rotate-right: "\f01e"; +$fa-var-rouble: "\f158"; +$fa-var-rss: "\f09e"; +$fa-var-rss-square: "\f143"; +$fa-var-rub: "\f158"; +$fa-var-ruble: "\f158"; +$fa-var-rupee: "\f156"; +$fa-var-safari: "\f267"; +$fa-var-save: "\f0c7"; +$fa-var-scissors: "\f0c4"; +$fa-var-scribd: "\f28a"; +$fa-var-search: "\f002"; +$fa-var-search-minus: "\f010"; +$fa-var-search-plus: "\f00e"; +$fa-var-sellsy: "\f213"; +$fa-var-send: "\f1d8"; +$fa-var-send-o: "\f1d9"; +$fa-var-server: "\f233"; +$fa-var-share: "\f064"; +$fa-var-share-alt: "\f1e0"; +$fa-var-share-alt-square: "\f1e1"; +$fa-var-share-square: "\f14d"; +$fa-var-share-square-o: "\f045"; +$fa-var-shekel: "\f20b"; +$fa-var-sheqel: "\f20b"; +$fa-var-shield: "\f132"; +$fa-var-ship: "\f21a"; +$fa-var-shirtsinbulk: "\f214"; +$fa-var-shopping-bag: "\f290"; +$fa-var-shopping-basket: "\f291"; +$fa-var-shopping-cart: "\f07a"; +$fa-var-sign-in: "\f090"; +$fa-var-sign-language: "\f2a7"; +$fa-var-sign-out: "\f08b"; +$fa-var-signal: "\f012"; +$fa-var-signing: "\f2a7"; +$fa-var-simplybuilt: "\f215"; +$fa-var-sitemap: "\f0e8"; +$fa-var-skyatlas: "\f216"; +$fa-var-skype: "\f17e"; +$fa-var-slack: "\f198"; +$fa-var-sliders: "\f1de"; +$fa-var-slideshare: "\f1e7"; +$fa-var-smile-o: "\f118"; +$fa-var-snapchat: "\f2ab"; +$fa-var-snapchat-ghost: "\f2ac"; +$fa-var-snapchat-square: "\f2ad"; +$fa-var-soccer-ball-o: "\f1e3"; +$fa-var-sort: "\f0dc"; +$fa-var-sort-alpha-asc: "\f15d"; +$fa-var-sort-alpha-desc: "\f15e"; +$fa-var-sort-amount-asc: "\f160"; +$fa-var-sort-amount-desc: "\f161"; +$fa-var-sort-asc: "\f0de"; +$fa-var-sort-desc: "\f0dd"; +$fa-var-sort-down: "\f0dd"; +$fa-var-sort-numeric-asc: "\f162"; +$fa-var-sort-numeric-desc: "\f163"; +$fa-var-sort-up: "\f0de"; +$fa-var-soundcloud: "\f1be"; +$fa-var-space-shuttle: "\f197"; +$fa-var-spinner: "\f110"; +$fa-var-spoon: "\f1b1"; +$fa-var-spotify: "\f1bc"; +$fa-var-square: "\f0c8"; +$fa-var-square-o: "\f096"; +$fa-var-stack-exchange: "\f18d"; +$fa-var-stack-overflow: "\f16c"; +$fa-var-star: "\f005"; +$fa-var-star-half: "\f089"; +$fa-var-star-half-empty: "\f123"; +$fa-var-star-half-full: "\f123"; +$fa-var-star-half-o: "\f123"; +$fa-var-star-o: "\f006"; +$fa-var-steam: "\f1b6"; +$fa-var-steam-square: "\f1b7"; +$fa-var-step-backward: "\f048"; +$fa-var-step-forward: "\f051"; +$fa-var-stethoscope: "\f0f1"; +$fa-var-sticky-note: "\f249"; +$fa-var-sticky-note-o: "\f24a"; +$fa-var-stop: "\f04d"; +$fa-var-stop-circle: "\f28d"; +$fa-var-stop-circle-o: "\f28e"; +$fa-var-street-view: "\f21d"; +$fa-var-strikethrough: "\f0cc"; +$fa-var-stumbleupon: "\f1a4"; +$fa-var-stumbleupon-circle: "\f1a3"; +$fa-var-subscript: "\f12c"; +$fa-var-subway: "\f239"; +$fa-var-suitcase: "\f0f2"; +$fa-var-sun-o: "\f185"; +$fa-var-superscript: "\f12b"; +$fa-var-support: "\f1cd"; +$fa-var-table: "\f0ce"; +$fa-var-tablet: "\f10a"; +$fa-var-tachometer: "\f0e4"; +$fa-var-tag: "\f02b"; +$fa-var-tags: "\f02c"; +$fa-var-tasks: "\f0ae"; +$fa-var-taxi: "\f1ba"; +$fa-var-television: "\f26c"; +$fa-var-tencent-weibo: "\f1d5"; +$fa-var-terminal: "\f120"; +$fa-var-text-height: "\f034"; +$fa-var-text-width: "\f035"; +$fa-var-th: "\f00a"; +$fa-var-th-large: "\f009"; +$fa-var-th-list: "\f00b"; +$fa-var-themeisle: "\f2b2"; +$fa-var-thumb-tack: "\f08d"; +$fa-var-thumbs-down: "\f165"; +$fa-var-thumbs-o-down: "\f088"; +$fa-var-thumbs-o-up: "\f087"; +$fa-var-thumbs-up: "\f164"; +$fa-var-ticket: "\f145"; +$fa-var-times: "\f00d"; +$fa-var-times-circle: "\f057"; +$fa-var-times-circle-o: "\f05c"; +$fa-var-tint: "\f043"; +$fa-var-toggle-down: "\f150"; +$fa-var-toggle-left: "\f191"; +$fa-var-toggle-off: "\f204"; +$fa-var-toggle-on: "\f205"; +$fa-var-toggle-right: "\f152"; +$fa-var-toggle-up: "\f151"; +$fa-var-trademark: "\f25c"; +$fa-var-train: "\f238"; +$fa-var-transgender: "\f224"; +$fa-var-transgender-alt: "\f225"; +$fa-var-trash: "\f1f8"; +$fa-var-trash-o: "\f014"; +$fa-var-tree: "\f1bb"; +$fa-var-trello: "\f181"; +$fa-var-tripadvisor: "\f262"; +$fa-var-trophy: "\f091"; +$fa-var-truck: "\f0d1"; +$fa-var-try: "\f195"; +$fa-var-tty: "\f1e4"; +$fa-var-tumblr: "\f173"; +$fa-var-tumblr-square: "\f174"; +$fa-var-turkish-lira: "\f195"; +$fa-var-tv: "\f26c"; +$fa-var-twitch: "\f1e8"; +$fa-var-twitter: "\f099"; +$fa-var-twitter-square: "\f081"; +$fa-var-umbrella: "\f0e9"; +$fa-var-underline: "\f0cd"; +$fa-var-undo: "\f0e2"; +$fa-var-universal-access: "\f29a"; +$fa-var-university: "\f19c"; +$fa-var-unlink: "\f127"; +$fa-var-unlock: "\f09c"; +$fa-var-unlock-alt: "\f13e"; +$fa-var-unsorted: "\f0dc"; +$fa-var-upload: "\f093"; +$fa-var-usb: "\f287"; +$fa-var-usd: "\f155"; +$fa-var-user: "\f007"; +$fa-var-user-md: "\f0f0"; +$fa-var-user-plus: "\f234"; +$fa-var-user-secret: "\f21b"; +$fa-var-user-times: "\f235"; +$fa-var-users: "\f0c0"; +$fa-var-venus: "\f221"; +$fa-var-venus-double: "\f226"; +$fa-var-venus-mars: "\f228"; +$fa-var-viacoin: "\f237"; +$fa-var-viadeo: "\f2a9"; +$fa-var-viadeo-square: "\f2aa"; +$fa-var-video-camera: "\f03d"; +$fa-var-vimeo: "\f27d"; +$fa-var-vimeo-square: "\f194"; +$fa-var-vine: "\f1ca"; +$fa-var-vk: "\f189"; +$fa-var-volume-control-phone: "\f2a0"; +$fa-var-volume-down: "\f027"; +$fa-var-volume-off: "\f026"; +$fa-var-volume-up: "\f028"; +$fa-var-warning: "\f071"; +$fa-var-wechat: "\f1d7"; +$fa-var-weibo: "\f18a"; +$fa-var-weixin: "\f1d7"; +$fa-var-whatsapp: "\f232"; +$fa-var-wheelchair: "\f193"; +$fa-var-wheelchair-alt: "\f29b"; +$fa-var-wifi: "\f1eb"; +$fa-var-wikipedia-w: "\f266"; +$fa-var-windows: "\f17a"; +$fa-var-won: "\f159"; +$fa-var-wordpress: "\f19a"; +$fa-var-wpbeginner: "\f297"; +$fa-var-wpforms: "\f298"; +$fa-var-wrench: "\f0ad"; +$fa-var-xing: "\f168"; +$fa-var-xing-square: "\f169"; +$fa-var-y-combinator: "\f23b"; +$fa-var-y-combinator-square: "\f1d4"; +$fa-var-yahoo: "\f19e"; +$fa-var-yc: "\f23b"; +$fa-var-yc-square: "\f1d4"; +$fa-var-yelp: "\f1e9"; +$fa-var-yen: "\f157"; +$fa-var-yoast: "\f2b1"; +$fa-var-youtube: "\f167"; +$fa-var-youtube-play: "\f16a"; +$fa-var-youtube-square: "\f166"; + diff --git a/extras/font-awesome/scss/font-awesome.css b/extras/font-awesome/scss/font-awesome.css new file mode 100644 index 0000000..75d09dc --- /dev/null +++ b/extras/font-awesome/scss/font-awesome.css @@ -0,0 +1,5530 @@ +/*! + * Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ +/* FONT PATH + * -------------------------- */ +@font-face { + font-family: "FontAwesome"; + src: url("../fonts/fontawesome-webfont.eot?v=4.6.3"); + src: url("../fonts/fontawesome-webfont.eot?#iefix&v=4.6.3") format("embedded-opentype"), url("../fonts/fontawesome-webfont.woff2?v=4.6.3") format("woff2"), url("../fonts/fontawesome-webfont.woff?v=4.6.3") format("woff"), url("../fonts/fontawesome-webfont.ttf?v=4.6.3") format("truetype"), url("../fonts/fontawesome-webfont.svg?v=4.6.3#fontawesomeregular") format("svg"); + font-weight: normal; + font-style: normal; +} +.ame-fa { + display: inline-block; + font: normal normal normal 18px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* makes the font 33% larger relative to the icon container */ +.ame-fa-lg { + font-size: 1.3333333333em; + line-height: 0.75em; + vertical-align: -15%; +} + +.ame-fa-2x { + font-size: 2em; +} + +.ame-fa-3x { + font-size: 3em; +} + +.ame-fa-4x { + font-size: 4em; +} + +.ame-fa-5x { + font-size: 5em; +} + +.ame-fa-fw { + width: 1.2857142857em; + text-align: center; +} + +.ame-fa-ul { + padding-left: 0; + margin-left: 2.1428571429em; + list-style-type: none; +} +.ame-fa-ul > li { + position: relative; +} + +.ame-fa-li { + position: absolute; + left: -2.1428571429em; + width: 2.1428571429em; + top: 0.1428571429em; + text-align: center; +} +.ame-fa-li.ame-fa-lg { + left: -1.8571428571em; +} + +.ame-fa-border { + padding: 0.2em 0.25em 0.15em; + border: solid 0.08em #eee; + border-radius: 0.1em; +} + +.ame-fa-pull-left { + float: left; +} + +.ame-fa-pull-right { + float: right; +} + +.ame-fa.ame-fa-pull-left { + margin-right: 0.3em; +} +.ame-fa.ame-fa-pull-right { + margin-left: 0.3em; +} + +/* Deprecated as of 4.4.0 */ +.pull-right { + float: right; +} + +.pull-left { + float: left; +} + +.ame-fa.pull-left { + margin-right: 0.3em; +} +.ame-fa.pull-right { + margin-left: 0.3em; +} + +.ame-fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} + +.ame-fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); +} + +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.ame-fa-rotate-90 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} + +.ame-fa-rotate-180 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} + +.ame-fa-rotate-270 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} + +.ame-fa-flip-horizontal { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} + +.ame-fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} + +:root .ame-fa-rotate-90, +:root .ame-fa-rotate-180, +:root .ame-fa-rotate-270, +:root .ame-fa-flip-horizontal, +:root .ame-fa-flip-vertical { + filter: none; +} + +.ame-fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} + +.ame-fa-stack-1x, .ame-fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} + +.ame-fa-stack-1x { + line-height: inherit; +} + +.ame-fa-stack-2x { + font-size: 2em; +} + +.ame-fa-inverse { + color: #fff; +} + +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.ame-fa-glass:before { + content: "\f000"; +} + +.ame-fa-music:before { + content: "\f001"; +} + +.ame-fa-search:before { + content: "\f002"; +} + +.ame-fa-envelope-o:before { + content: "\f003"; +} + +.ame-fa-heart:before { + content: "\f004"; +} + +.ame-fa-star:before { + content: "\f005"; +} + +.ame-fa-star-o:before { + content: "\f006"; +} + +.ame-fa-user:before { + content: "\f007"; +} + +.ame-fa-film:before { + content: "\f008"; +} + +.ame-fa-th-large:before { + content: "\f009"; +} + +.ame-fa-th:before { + content: "\f00a"; +} + +.ame-fa-th-list:before { + content: "\f00b"; +} + +.ame-fa-check:before { + content: "\f00c"; +} + +.ame-fa-remove:before, +.ame-fa-close:before, +.ame-fa-times:before { + content: "\f00d"; +} + +.ame-fa-search-plus:before { + content: "\f00e"; +} + +.ame-fa-search-minus:before { + content: "\f010"; +} + +.ame-fa-power-off:before { + content: "\f011"; +} + +.ame-fa-signal:before { + content: "\f012"; +} + +.ame-fa-gear:before, +.ame-fa-cog:before { + content: "\f013"; +} + +.ame-fa-trash-o:before { + content: "\f014"; +} + +.ame-fa-home:before { + content: "\f015"; +} + +.ame-fa-file-o:before { + content: "\f016"; +} + +.ame-fa-clock-o:before { + content: "\f017"; +} + +.ame-fa-road:before { + content: "\f018"; +} + +.ame-fa-download:before { + content: "\f019"; +} + +.ame-fa-arrow-circle-o-down:before { + content: "\f01a"; +} + +.ame-fa-arrow-circle-o-up:before { + content: "\f01b"; +} + +.ame-fa-inbox:before { + content: "\f01c"; +} + +.ame-fa-play-circle-o:before { + content: "\f01d"; +} + +.ame-fa-rotate-right:before, +.ame-fa-repeat:before { + content: "\f01e"; +} + +.ame-fa-refresh:before { + content: "\f021"; +} + +.ame-fa-list-alt:before { + content: "\f022"; +} + +.ame-fa-lock:before { + content: "\f023"; +} + +.ame-fa-flag:before { + content: "\f024"; +} + +.ame-fa-headphones:before { + content: "\f025"; +} + +.ame-fa-volume-off:before { + content: "\f026"; +} + +.ame-fa-volume-down:before { + content: "\f027"; +} + +.ame-fa-volume-up:before { + content: "\f028"; +} + +.ame-fa-qrcode:before { + content: "\f029"; +} + +.ame-fa-barcode:before { + content: "\f02a"; +} + +.ame-fa-tag:before { + content: "\f02b"; +} + +.ame-fa-tags:before { + content: "\f02c"; +} + +.ame-fa-book:before { + content: "\f02d"; +} + +.ame-fa-bookmark:before { + content: "\f02e"; +} + +.ame-fa-print:before { + content: "\f02f"; +} + +.ame-fa-camera:before { + content: "\f030"; +} + +.ame-fa-font:before { + content: "\f031"; +} + +.ame-fa-bold:before { + content: "\f032"; +} + +.ame-fa-italic:before { + content: "\f033"; +} + +.ame-fa-text-height:before { + content: "\f034"; +} + +.ame-fa-text-width:before { + content: "\f035"; +} + +.ame-fa-align-left:before { + content: "\f036"; +} + +.ame-fa-align-center:before { + content: "\f037"; +} + +.ame-fa-align-right:before { + content: "\f038"; +} + +.ame-fa-align-justify:before { + content: "\f039"; +} + +.ame-fa-list:before { + content: "\f03a"; +} + +.ame-fa-dedent:before, +.ame-fa-outdent:before { + content: "\f03b"; +} + +.ame-fa-indent:before { + content: "\f03c"; +} + +.ame-fa-video-camera:before { + content: "\f03d"; +} + +.ame-fa-photo:before, +.ame-fa-image:before, +.ame-fa-picture-o:before { + content: "\f03e"; +} + +.ame-fa-pencil:before { + content: "\f040"; +} + +.ame-fa-map-marker:before { + content: "\f041"; +} + +.ame-fa-adjust:before { + content: "\f042"; +} + +.ame-fa-tint:before { + content: "\f043"; +} + +.ame-fa-edit:before, +.ame-fa-pencil-square-o:before { + content: "\f044"; +} + +.ame-fa-share-square-o:before { + content: "\f045"; +} + +.ame-fa-check-square-o:before { + content: "\f046"; +} + +.ame-fa-arrows:before { + content: "\f047"; +} + +.ame-fa-step-backward:before { + content: "\f048"; +} + +.ame-fa-fast-backward:before { + content: "\f049"; +} + +.ame-fa-backward:before { + content: "\f04a"; +} + +.ame-fa-play:before { + content: "\f04b"; +} + +.ame-fa-pause:before { + content: "\f04c"; +} + +.ame-fa-stop:before { + content: "\f04d"; +} + +.ame-fa-forward:before { + content: "\f04e"; +} + +.ame-fa-fast-forward:before { + content: "\f050"; +} + +.ame-fa-step-forward:before { + content: "\f051"; +} + +.ame-fa-eject:before { + content: "\f052"; +} + +.ame-fa-chevron-left:before { + content: "\f053"; +} + +.ame-fa-chevron-right:before { + content: "\f054"; +} + +.ame-fa-plus-circle:before { + content: "\f055"; +} + +.ame-fa-minus-circle:before { + content: "\f056"; +} + +.ame-fa-times-circle:before { + content: "\f057"; +} + +.ame-fa-check-circle:before { + content: "\f058"; +} + +.ame-fa-question-circle:before { + content: "\f059"; +} + +.ame-fa-info-circle:before { + content: "\f05a"; +} + +.ame-fa-crosshairs:before { + content: "\f05b"; +} + +.ame-fa-times-circle-o:before { + content: "\f05c"; +} + +.ame-fa-check-circle-o:before { + content: "\f05d"; +} + +.ame-fa-ban:before { + content: "\f05e"; +} + +.ame-fa-arrow-left:before { + content: "\f060"; +} + +.ame-fa-arrow-right:before { + content: "\f061"; +} + +.ame-fa-arrow-up:before { + content: "\f062"; +} + +.ame-fa-arrow-down:before { + content: "\f063"; +} + +.ame-fa-mail-forward:before, +.ame-fa-share:before { + content: "\f064"; +} + +.ame-fa-expand:before { + content: "\f065"; +} + +.ame-fa-compress:before { + content: "\f066"; +} + +.ame-fa-plus:before { + content: "\f067"; +} + +.ame-fa-minus:before { + content: "\f068"; +} + +.ame-fa-asterisk:before { + content: "\f069"; +} + +.ame-fa-exclamation-circle:before { + content: "\f06a"; +} + +.ame-fa-gift:before { + content: "\f06b"; +} + +.ame-fa-leaf:before { + content: "\f06c"; +} + +.ame-fa-fire:before { + content: "\f06d"; +} + +.ame-fa-eye:before { + content: "\f06e"; +} + +.ame-fa-eye-slash:before { + content: "\f070"; +} + +.ame-fa-warning:before, +.ame-fa-exclamation-triangle:before { + content: "\f071"; +} + +.ame-fa-plane:before { + content: "\f072"; +} + +.ame-fa-calendar:before { + content: "\f073"; +} + +.ame-fa-random:before { + content: "\f074"; +} + +.ame-fa-comment:before { + content: "\f075"; +} + +.ame-fa-magnet:before { + content: "\f076"; +} + +.ame-fa-chevron-up:before { + content: "\f077"; +} + +.ame-fa-chevron-down:before { + content: "\f078"; +} + +.ame-fa-retweet:before { + content: "\f079"; +} + +.ame-fa-shopping-cart:before { + content: "\f07a"; +} + +.ame-fa-folder:before { + content: "\f07b"; +} + +.ame-fa-folder-open:before { + content: "\f07c"; +} + +.ame-fa-arrows-v:before { + content: "\f07d"; +} + +.ame-fa-arrows-h:before { + content: "\f07e"; +} + +.ame-fa-bar-chart-o:before, +.ame-fa-bar-chart:before { + content: "\f080"; +} + +.ame-fa-twitter-square:before { + content: "\f081"; +} + +.ame-fa-facebook-square:before { + content: "\f082"; +} + +.ame-fa-camera-retro:before { + content: "\f083"; +} + +.ame-fa-key:before { + content: "\f084"; +} + +.ame-fa-gears:before, +.ame-fa-cogs:before { + content: "\f085"; +} + +.ame-fa-comments:before { + content: "\f086"; +} + +.ame-fa-thumbs-o-up:before { + content: "\f087"; +} + +.ame-fa-thumbs-o-down:before { + content: "\f088"; +} + +.ame-fa-star-half:before { + content: "\f089"; +} + +.ame-fa-heart-o:before { + content: "\f08a"; +} + +.ame-fa-sign-out:before { + content: "\f08b"; +} + +.ame-fa-linkedin-square:before { + content: "\f08c"; +} + +.ame-fa-thumb-tack:before { + content: "\f08d"; +} + +.ame-fa-external-link:before { + content: "\f08e"; +} + +.ame-fa-sign-in:before { + content: "\f090"; +} + +.ame-fa-trophy:before { + content: "\f091"; +} + +.ame-fa-github-square:before { + content: "\f092"; +} + +.ame-fa-upload:before { + content: "\f093"; +} + +.ame-fa-lemon-o:before { + content: "\f094"; +} + +.ame-fa-phone:before { + content: "\f095"; +} + +.ame-fa-square-o:before { + content: "\f096"; +} + +.ame-fa-bookmark-o:before { + content: "\f097"; +} + +.ame-fa-phone-square:before { + content: "\f098"; +} + +.ame-fa-twitter:before { + content: "\f099"; +} + +.ame-fa-facebook-f:before, +.ame-fa-facebook:before { + content: "\f09a"; +} + +.ame-fa-github:before { + content: "\f09b"; +} + +.ame-fa-unlock:before { + content: "\f09c"; +} + +.ame-fa-credit-card:before { + content: "\f09d"; +} + +.ame-fa-feed:before, +.ame-fa-rss:before { + content: "\f09e"; +} + +.ame-fa-hdd-o:before { + content: "\f0a0"; +} + +.ame-fa-bullhorn:before { + content: "\f0a1"; +} + +.ame-fa-bell:before { + content: "\f0f3"; +} + +.ame-fa-certificate:before { + content: "\f0a3"; +} + +.ame-fa-hand-o-right:before { + content: "\f0a4"; +} + +.ame-fa-hand-o-left:before { + content: "\f0a5"; +} + +.ame-fa-hand-o-up:before { + content: "\f0a6"; +} + +.ame-fa-hand-o-down:before { + content: "\f0a7"; +} + +.ame-fa-arrow-circle-left:before { + content: "\f0a8"; +} + +.ame-fa-arrow-circle-right:before { + content: "\f0a9"; +} + +.ame-fa-arrow-circle-up:before { + content: "\f0aa"; +} + +.ame-fa-arrow-circle-down:before { + content: "\f0ab"; +} + +.ame-fa-globe:before { + content: "\f0ac"; +} + +.ame-fa-wrench:before { + content: "\f0ad"; +} + +.ame-fa-tasks:before { + content: "\f0ae"; +} + +.ame-fa-filter:before { + content: "\f0b0"; +} + +.ame-fa-briefcase:before { + content: "\f0b1"; +} + +.ame-fa-arrows-alt:before { + content: "\f0b2"; +} + +.ame-fa-group:before, +.ame-fa-users:before { + content: "\f0c0"; +} + +.ame-fa-chain:before, +.ame-fa-link:before { + content: "\f0c1"; +} + +.ame-fa-cloud:before { + content: "\f0c2"; +} + +.ame-fa-flask:before { + content: "\f0c3"; +} + +.ame-fa-cut:before, +.ame-fa-scissors:before { + content: "\f0c4"; +} + +.ame-fa-copy:before, +.ame-fa-files-o:before { + content: "\f0c5"; +} + +.ame-fa-paperclip:before { + content: "\f0c6"; +} + +.ame-fa-save:before, +.ame-fa-floppy-o:before { + content: "\f0c7"; +} + +.ame-fa-square:before { + content: "\f0c8"; +} + +.ame-fa-navicon:before, +.ame-fa-reorder:before, +.ame-fa-bars:before { + content: "\f0c9"; +} + +.ame-fa-list-ul:before { + content: "\f0ca"; +} + +.ame-fa-list-ol:before { + content: "\f0cb"; +} + +.ame-fa-strikethrough:before { + content: "\f0cc"; +} + +.ame-fa-underline:before { + content: "\f0cd"; +} + +.ame-fa-table:before { + content: "\f0ce"; +} + +.ame-fa-magic:before { + content: "\f0d0"; +} + +.ame-fa-truck:before { + content: "\f0d1"; +} + +.ame-fa-pinterest:before { + content: "\f0d2"; +} + +.ame-fa-pinterest-square:before { + content: "\f0d3"; +} + +.ame-fa-google-plus-square:before { + content: "\f0d4"; +} + +.ame-fa-google-plus:before { + content: "\f0d5"; +} + +.ame-fa-money:before { + content: "\f0d6"; +} + +.ame-fa-caret-down:before { + content: "\f0d7"; +} + +.ame-fa-caret-up:before { + content: "\f0d8"; +} + +.ame-fa-caret-left:before { + content: "\f0d9"; +} + +.ame-fa-caret-right:before { + content: "\f0da"; +} + +.ame-fa-columns:before { + content: "\f0db"; +} + +.ame-fa-unsorted:before, +.ame-fa-sort:before { + content: "\f0dc"; +} + +.ame-fa-sort-down:before, +.ame-fa-sort-desc:before { + content: "\f0dd"; +} + +.ame-fa-sort-up:before, +.ame-fa-sort-asc:before { + content: "\f0de"; +} + +.ame-fa-envelope:before { + content: "\f0e0"; +} + +.ame-fa-linkedin:before { + content: "\f0e1"; +} + +.ame-fa-rotate-left:before, +.ame-fa-undo:before { + content: "\f0e2"; +} + +.ame-fa-legal:before, +.ame-fa-gavel:before { + content: "\f0e3"; +} + +.ame-fa-dashboard:before, +.ame-fa-tachometer:before { + content: "\f0e4"; +} + +.ame-fa-comment-o:before { + content: "\f0e5"; +} + +.ame-fa-comments-o:before { + content: "\f0e6"; +} + +.ame-fa-flash:before, +.ame-fa-bolt:before { + content: "\f0e7"; +} + +.ame-fa-sitemap:before { + content: "\f0e8"; +} + +.ame-fa-umbrella:before { + content: "\f0e9"; +} + +.ame-fa-paste:before, +.ame-fa-clipboard:before { + content: "\f0ea"; +} + +.ame-fa-lightbulb-o:before { + content: "\f0eb"; +} + +.ame-fa-exchange:before { + content: "\f0ec"; +} + +.ame-fa-cloud-download:before { + content: "\f0ed"; +} + +.ame-fa-cloud-upload:before { + content: "\f0ee"; +} + +.ame-fa-user-md:before { + content: "\f0f0"; +} + +.ame-fa-stethoscope:before { + content: "\f0f1"; +} + +.ame-fa-suitcase:before { + content: "\f0f2"; +} + +.ame-fa-bell-o:before { + content: "\f0a2"; +} + +.ame-fa-coffee:before { + content: "\f0f4"; +} + +.ame-fa-cutlery:before { + content: "\f0f5"; +} + +.ame-fa-file-text-o:before { + content: "\f0f6"; +} + +.ame-fa-building-o:before { + content: "\f0f7"; +} + +.ame-fa-hospital-o:before { + content: "\f0f8"; +} + +.ame-fa-ambulance:before { + content: "\f0f9"; +} + +.ame-fa-medkit:before { + content: "\f0fa"; +} + +.ame-fa-fighter-jet:before { + content: "\f0fb"; +} + +.ame-fa-beer:before { + content: "\f0fc"; +} + +.ame-fa-h-square:before { + content: "\f0fd"; +} + +.ame-fa-plus-square:before { + content: "\f0fe"; +} + +.ame-fa-angle-double-left:before { + content: "\f100"; +} + +.ame-fa-angle-double-right:before { + content: "\f101"; +} + +.ame-fa-angle-double-up:before { + content: "\f102"; +} + +.ame-fa-angle-double-down:before { + content: "\f103"; +} + +.ame-fa-angle-left:before { + content: "\f104"; +} + +.ame-fa-angle-right:before { + content: "\f105"; +} + +.ame-fa-angle-up:before { + content: "\f106"; +} + +.ame-fa-angle-down:before { + content: "\f107"; +} + +.ame-fa-desktop:before { + content: "\f108"; +} + +.ame-fa-laptop:before { + content: "\f109"; +} + +.ame-fa-tablet:before { + content: "\f10a"; +} + +.ame-fa-mobile-phone:before, +.ame-fa-mobile:before { + content: "\f10b"; +} + +.ame-fa-circle-o:before { + content: "\f10c"; +} + +.ame-fa-quote-left:before { + content: "\f10d"; +} + +.ame-fa-quote-right:before { + content: "\f10e"; +} + +.ame-fa-spinner:before { + content: "\f110"; +} + +.ame-fa-circle:before { + content: "\f111"; +} + +.ame-fa-mail-reply:before, +.ame-fa-reply:before { + content: "\f112"; +} + +.ame-fa-github-alt:before { + content: "\f113"; +} + +.ame-fa-folder-o:before { + content: "\f114"; +} + +.ame-fa-folder-open-o:before { + content: "\f115"; +} + +.ame-fa-smile-o:before { + content: "\f118"; +} + +.ame-fa-frown-o:before { + content: "\f119"; +} + +.ame-fa-meh-o:before { + content: "\f11a"; +} + +.ame-fa-gamepad:before { + content: "\f11b"; +} + +.ame-fa-keyboard-o:before { + content: "\f11c"; +} + +.ame-fa-flag-o:before { + content: "\f11d"; +} + +.ame-fa-flag-checkered:before { + content: "\f11e"; +} + +.ame-fa-terminal:before { + content: "\f120"; +} + +.ame-fa-code:before { + content: "\f121"; +} + +.ame-fa-mail-reply-all:before, +.ame-fa-reply-all:before { + content: "\f122"; +} + +.ame-fa-star-half-empty:before, +.ame-fa-star-half-full:before, +.ame-fa-star-half-o:before { + content: "\f123"; +} + +.ame-fa-location-arrow:before { + content: "\f124"; +} + +.ame-fa-crop:before { + content: "\f125"; +} + +.ame-fa-code-fork:before { + content: "\f126"; +} + +.ame-fa-unlink:before, +.ame-fa-chain-broken:before { + content: "\f127"; +} + +.ame-fa-question:before { + content: "\f128"; +} + +.ame-fa-info:before { + content: "\f129"; +} + +.ame-fa-exclamation:before { + content: "\f12a"; +} + +.ame-fa-superscript:before { + content: "\f12b"; +} + +.ame-fa-subscript:before { + content: "\f12c"; +} + +.ame-fa-eraser:before { + content: "\f12d"; +} + +.ame-fa-puzzle-piece:before { + content: "\f12e"; +} + +.ame-fa-microphone:before { + content: "\f130"; +} + +.ame-fa-microphone-slash:before { + content: "\f131"; +} + +.ame-fa-shield:before { + content: "\f132"; +} + +.ame-fa-calendar-o:before { + content: "\f133"; +} + +.ame-fa-fire-extinguisher:before { + content: "\f134"; +} + +.ame-fa-rocket:before { + content: "\f135"; +} + +.ame-fa-maxcdn:before { + content: "\f136"; +} + +.ame-fa-chevron-circle-left:before { + content: "\f137"; +} + +.ame-fa-chevron-circle-right:before { + content: "\f138"; +} + +.ame-fa-chevron-circle-up:before { + content: "\f139"; +} + +.ame-fa-chevron-circle-down:before { + content: "\f13a"; +} + +.ame-fa-html5:before { + content: "\f13b"; +} + +.ame-fa-css3:before { + content: "\f13c"; +} + +.ame-fa-anchor:before { + content: "\f13d"; +} + +.ame-fa-unlock-alt:before { + content: "\f13e"; +} + +.ame-fa-bullseye:before { + content: "\f140"; +} + +.ame-fa-ellipsis-h:before { + content: "\f141"; +} + +.ame-fa-ellipsis-v:before { + content: "\f142"; +} + +.ame-fa-rss-square:before { + content: "\f143"; +} + +.ame-fa-play-circle:before { + content: "\f144"; +} + +.ame-fa-ticket:before { + content: "\f145"; +} + +.ame-fa-minus-square:before { + content: "\f146"; +} + +.ame-fa-minus-square-o:before { + content: "\f147"; +} + +.ame-fa-level-up:before { + content: "\f148"; +} + +.ame-fa-level-down:before { + content: "\f149"; +} + +.ame-fa-check-square:before { + content: "\f14a"; +} + +.ame-fa-pencil-square:before { + content: "\f14b"; +} + +.ame-fa-external-link-square:before { + content: "\f14c"; +} + +.ame-fa-share-square:before { + content: "\f14d"; +} + +.ame-fa-compass:before { + content: "\f14e"; +} + +.ame-fa-toggle-down:before, +.ame-fa-caret-square-o-down:before { + content: "\f150"; +} + +.ame-fa-toggle-up:before, +.ame-fa-caret-square-o-up:before { + content: "\f151"; +} + +.ame-fa-toggle-right:before, +.ame-fa-caret-square-o-right:before { + content: "\f152"; +} + +.ame-fa-euro:before, +.ame-fa-eur:before { + content: "\f153"; +} + +.ame-fa-gbp:before { + content: "\f154"; +} + +.ame-fa-dollar:before, +.ame-fa-usd:before { + content: "\f155"; +} + +.ame-fa-rupee:before, +.ame-fa-inr:before { + content: "\f156"; +} + +.ame-fa-cny:before, +.ame-fa-rmb:before, +.ame-fa-yen:before, +.ame-fa-jpy:before { + content: "\f157"; +} + +.ame-fa-ruble:before, +.ame-fa-rouble:before, +.ame-fa-rub:before { + content: "\f158"; +} + +.ame-fa-won:before, +.ame-fa-krw:before { + content: "\f159"; +} + +.ame-fa-bitcoin:before, +.ame-fa-btc:before { + content: "\f15a"; +} + +.ame-fa-file:before { + content: "\f15b"; +} + +.ame-fa-file-text:before { + content: "\f15c"; +} + +.ame-fa-sort-alpha-asc:before { + content: "\f15d"; +} + +.ame-fa-sort-alpha-desc:before { + content: "\f15e"; +} + +.ame-fa-sort-amount-asc:before { + content: "\f160"; +} + +.ame-fa-sort-amount-desc:before { + content: "\f161"; +} + +.ame-fa-sort-numeric-asc:before { + content: "\f162"; +} + +.ame-fa-sort-numeric-desc:before { + content: "\f163"; +} + +.ame-fa-thumbs-up:before { + content: "\f164"; +} + +.ame-fa-thumbs-down:before { + content: "\f165"; +} + +.ame-fa-youtube-square:before { + content: "\f166"; +} + +.ame-fa-youtube:before { + content: "\f167"; +} + +.ame-fa-xing:before { + content: "\f168"; +} + +.ame-fa-xing-square:before { + content: "\f169"; +} + +.ame-fa-youtube-play:before { + content: "\f16a"; +} + +.ame-fa-dropbox:before { + content: "\f16b"; +} + +.ame-fa-stack-overflow:before { + content: "\f16c"; +} + +.ame-fa-instagram:before { + content: "\f16d"; +} + +.ame-fa-flickr:before { + content: "\f16e"; +} + +.ame-fa-adn:before { + content: "\f170"; +} + +.ame-fa-bitbucket:before { + content: "\f171"; +} + +.ame-fa-bitbucket-square:before { + content: "\f172"; +} + +.ame-fa-tumblr:before { + content: "\f173"; +} + +.ame-fa-tumblr-square:before { + content: "\f174"; +} + +.ame-fa-long-arrow-down:before { + content: "\f175"; +} + +.ame-fa-long-arrow-up:before { + content: "\f176"; +} + +.ame-fa-long-arrow-left:before { + content: "\f177"; +} + +.ame-fa-long-arrow-right:before { + content: "\f178"; +} + +.ame-fa-apple:before { + content: "\f179"; +} + +.ame-fa-windows:before { + content: "\f17a"; +} + +.ame-fa-android:before { + content: "\f17b"; +} + +.ame-fa-linux:before { + content: "\f17c"; +} + +.ame-fa-dribbble:before { + content: "\f17d"; +} + +.ame-fa-skype:before { + content: "\f17e"; +} + +.ame-fa-foursquare:before { + content: "\f180"; +} + +.ame-fa-trello:before { + content: "\f181"; +} + +.ame-fa-female:before { + content: "\f182"; +} + +.ame-fa-male:before { + content: "\f183"; +} + +.ame-fa-gittip:before, +.ame-fa-gratipay:before { + content: "\f184"; +} + +.ame-fa-sun-o:before { + content: "\f185"; +} + +.ame-fa-moon-o:before { + content: "\f186"; +} + +.ame-fa-archive:before { + content: "\f187"; +} + +.ame-fa-bug:before { + content: "\f188"; +} + +.ame-fa-vk:before { + content: "\f189"; +} + +.ame-fa-weibo:before { + content: "\f18a"; +} + +.ame-fa-renren:before { + content: "\f18b"; +} + +.ame-fa-pagelines:before { + content: "\f18c"; +} + +.ame-fa-stack-exchange:before { + content: "\f18d"; +} + +.ame-fa-arrow-circle-o-right:before { + content: "\f18e"; +} + +.ame-fa-arrow-circle-o-left:before { + content: "\f190"; +} + +.ame-fa-toggle-left:before, +.ame-fa-caret-square-o-left:before { + content: "\f191"; +} + +.ame-fa-dot-circle-o:before { + content: "\f192"; +} + +.ame-fa-wheelchair:before { + content: "\f193"; +} + +.ame-fa-vimeo-square:before { + content: "\f194"; +} + +.ame-fa-turkish-lira:before, +.ame-fa-try:before { + content: "\f195"; +} + +.ame-fa-plus-square-o:before { + content: "\f196"; +} + +.ame-fa-space-shuttle:before { + content: "\f197"; +} + +.ame-fa-slack:before { + content: "\f198"; +} + +.ame-fa-envelope-square:before { + content: "\f199"; +} + +.ame-fa-wordpress:before { + content: "\f19a"; +} + +.ame-fa-openid:before { + content: "\f19b"; +} + +.ame-fa-institution:before, +.ame-fa-bank:before, +.ame-fa-university:before { + content: "\f19c"; +} + +.ame-fa-mortar-board:before, +.ame-fa-graduation-cap:before { + content: "\f19d"; +} + +.ame-fa-yahoo:before { + content: "\f19e"; +} + +.ame-fa-google:before { + content: "\f1a0"; +} + +.ame-fa-reddit:before { + content: "\f1a1"; +} + +.ame-fa-reddit-square:before { + content: "\f1a2"; +} + +.ame-fa-stumbleupon-circle:before { + content: "\f1a3"; +} + +.ame-fa-stumbleupon:before { + content: "\f1a4"; +} + +.ame-fa-delicious:before { + content: "\f1a5"; +} + +.ame-fa-digg:before { + content: "\f1a6"; +} + +.ame-fa-pied-piper-pp:before { + content: "\f1a7"; +} + +.ame-fa-pied-piper-alt:before { + content: "\f1a8"; +} + +.ame-fa-drupal:before { + content: "\f1a9"; +} + +.ame-fa-joomla:before { + content: "\f1aa"; +} + +.ame-fa-language:before { + content: "\f1ab"; +} + +.ame-fa-fax:before { + content: "\f1ac"; +} + +.ame-fa-building:before { + content: "\f1ad"; +} + +.ame-fa-child:before { + content: "\f1ae"; +} + +.ame-fa-paw:before { + content: "\f1b0"; +} + +.ame-fa-spoon:before { + content: "\f1b1"; +} + +.ame-fa-cube:before { + content: "\f1b2"; +} + +.ame-fa-cubes:before { + content: "\f1b3"; +} + +.ame-fa-behance:before { + content: "\f1b4"; +} + +.ame-fa-behance-square:before { + content: "\f1b5"; +} + +.ame-fa-steam:before { + content: "\f1b6"; +} + +.ame-fa-steam-square:before { + content: "\f1b7"; +} + +.ame-fa-recycle:before { + content: "\f1b8"; +} + +.ame-fa-automobile:before, +.ame-fa-car:before { + content: "\f1b9"; +} + +.ame-fa-cab:before, +.ame-fa-taxi:before { + content: "\f1ba"; +} + +.ame-fa-tree:before { + content: "\f1bb"; +} + +.ame-fa-spotify:before { + content: "\f1bc"; +} + +.ame-fa-deviantart:before { + content: "\f1bd"; +} + +.ame-fa-soundcloud:before { + content: "\f1be"; +} + +.ame-fa-database:before { + content: "\f1c0"; +} + +.ame-fa-file-pdf-o:before { + content: "\f1c1"; +} + +.ame-fa-file-word-o:before { + content: "\f1c2"; +} + +.ame-fa-file-excel-o:before { + content: "\f1c3"; +} + +.ame-fa-file-powerpoint-o:before { + content: "\f1c4"; +} + +.ame-fa-file-photo-o:before, +.ame-fa-file-picture-o:before, +.ame-fa-file-image-o:before { + content: "\f1c5"; +} + +.ame-fa-file-zip-o:before, +.ame-fa-file-archive-o:before { + content: "\f1c6"; +} + +.ame-fa-file-sound-o:before, +.ame-fa-file-audio-o:before { + content: "\f1c7"; +} + +.ame-fa-file-movie-o:before, +.ame-fa-file-video-o:before { + content: "\f1c8"; +} + +.ame-fa-file-code-o:before { + content: "\f1c9"; +} + +.ame-fa-vine:before { + content: "\f1ca"; +} + +.ame-fa-codepen:before { + content: "\f1cb"; +} + +.ame-fa-jsfiddle:before { + content: "\f1cc"; +} + +.ame-fa-life-bouy:before, +.ame-fa-life-buoy:before, +.ame-fa-life-saver:before, +.ame-fa-support:before, +.ame-fa-life-ring:before { + content: "\f1cd"; +} + +.ame-fa-circle-o-notch:before { + content: "\f1ce"; +} + +.ame-fa-ra:before, +.ame-fa-resistance:before, +.ame-fa-rebel:before { + content: "\f1d0"; +} + +.ame-fa-ge:before, +.ame-fa-empire:before { + content: "\f1d1"; +} + +.ame-fa-git-square:before { + content: "\f1d2"; +} + +.ame-fa-git:before { + content: "\f1d3"; +} + +.ame-fa-y-combinator-square:before, +.ame-fa-yc-square:before, +.ame-fa-hacker-news:before { + content: "\f1d4"; +} + +.ame-fa-tencent-weibo:before { + content: "\f1d5"; +} + +.ame-fa-qq:before { + content: "\f1d6"; +} + +.ame-fa-wechat:before, +.ame-fa-weixin:before { + content: "\f1d7"; +} + +.ame-fa-send:before, +.ame-fa-paper-plane:before { + content: "\f1d8"; +} + +.ame-fa-send-o:before, +.ame-fa-paper-plane-o:before { + content: "\f1d9"; +} + +.ame-fa-history:before { + content: "\f1da"; +} + +.ame-fa-circle-thin:before { + content: "\f1db"; +} + +.ame-fa-header:before { + content: "\f1dc"; +} + +.ame-fa-paragraph:before { + content: "\f1dd"; +} + +.ame-fa-sliders:before { + content: "\f1de"; +} + +.ame-fa-share-alt:before { + content: "\f1e0"; +} + +.ame-fa-share-alt-square:before { + content: "\f1e1"; +} + +.ame-fa-bomb:before { + content: "\f1e2"; +} + +.ame-fa-soccer-ball-o:before, +.ame-fa-futbol-o:before { + content: "\f1e3"; +} + +.ame-fa-tty:before { + content: "\f1e4"; +} + +.ame-fa-binoculars:before { + content: "\f1e5"; +} + +.ame-fa-plug:before { + content: "\f1e6"; +} + +.ame-fa-slideshare:before { + content: "\f1e7"; +} + +.ame-fa-twitch:before { + content: "\f1e8"; +} + +.ame-fa-yelp:before { + content: "\f1e9"; +} + +.ame-fa-newspaper-o:before { + content: "\f1ea"; +} + +.ame-fa-wifi:before { + content: "\f1eb"; +} + +.ame-fa-calculator:before { + content: "\f1ec"; +} + +.ame-fa-paypal:before { + content: "\f1ed"; +} + +.ame-fa-google-wallet:before { + content: "\f1ee"; +} + +.ame-fa-cc-visa:before { + content: "\f1f0"; +} + +.ame-fa-cc-mastercard:before { + content: "\f1f1"; +} + +.ame-fa-cc-discover:before { + content: "\f1f2"; +} + +.ame-fa-cc-amex:before { + content: "\f1f3"; +} + +.ame-fa-cc-paypal:before { + content: "\f1f4"; +} + +.ame-fa-cc-stripe:before { + content: "\f1f5"; +} + +.ame-fa-bell-slash:before { + content: "\f1f6"; +} + +.ame-fa-bell-slash-o:before { + content: "\f1f7"; +} + +.ame-fa-trash:before { + content: "\f1f8"; +} + +.ame-fa-copyright:before { + content: "\f1f9"; +} + +.ame-fa-at:before { + content: "\f1fa"; +} + +.ame-fa-eyedropper:before { + content: "\f1fb"; +} + +.ame-fa-paint-brush:before { + content: "\f1fc"; +} + +.ame-fa-birthday-cake:before { + content: "\f1fd"; +} + +.ame-fa-area-chart:before { + content: "\f1fe"; +} + +.ame-fa-pie-chart:before { + content: "\f200"; +} + +.ame-fa-line-chart:before { + content: "\f201"; +} + +.ame-fa-lastfm:before { + content: "\f202"; +} + +.ame-fa-lastfm-square:before { + content: "\f203"; +} + +.ame-fa-toggle-off:before { + content: "\f204"; +} + +.ame-fa-toggle-on:before { + content: "\f205"; +} + +.ame-fa-bicycle:before { + content: "\f206"; +} + +.ame-fa-bus:before { + content: "\f207"; +} + +.ame-fa-ioxhost:before { + content: "\f208"; +} + +.ame-fa-angellist:before { + content: "\f209"; +} + +.ame-fa-cc:before { + content: "\f20a"; +} + +.ame-fa-shekel:before, +.ame-fa-sheqel:before, +.ame-fa-ils:before { + content: "\f20b"; +} + +.ame-fa-meanpath:before { + content: "\f20c"; +} + +.ame-fa-buysellads:before { + content: "\f20d"; +} + +.ame-fa-connectdevelop:before { + content: "\f20e"; +} + +.ame-fa-dashcube:before { + content: "\f210"; +} + +.ame-fa-forumbee:before { + content: "\f211"; +} + +.ame-fa-leanpub:before { + content: "\f212"; +} + +.ame-fa-sellsy:before { + content: "\f213"; +} + +.ame-fa-shirtsinbulk:before { + content: "\f214"; +} + +.ame-fa-simplybuilt:before { + content: "\f215"; +} + +.ame-fa-skyatlas:before { + content: "\f216"; +} + +.ame-fa-cart-plus:before { + content: "\f217"; +} + +.ame-fa-cart-arrow-down:before { + content: "\f218"; +} + +.ame-fa-diamond:before { + content: "\f219"; +} + +.ame-fa-ship:before { + content: "\f21a"; +} + +.ame-fa-user-secret:before { + content: "\f21b"; +} + +.ame-fa-motorcycle:before { + content: "\f21c"; +} + +.ame-fa-street-view:before { + content: "\f21d"; +} + +.ame-fa-heartbeat:before { + content: "\f21e"; +} + +.ame-fa-venus:before { + content: "\f221"; +} + +.ame-fa-mars:before { + content: "\f222"; +} + +.ame-fa-mercury:before { + content: "\f223"; +} + +.ame-fa-intersex:before, +.ame-fa-transgender:before { + content: "\f224"; +} + +.ame-fa-transgender-alt:before { + content: "\f225"; +} + +.ame-fa-venus-double:before { + content: "\f226"; +} + +.ame-fa-mars-double:before { + content: "\f227"; +} + +.ame-fa-venus-mars:before { + content: "\f228"; +} + +.ame-fa-mars-stroke:before { + content: "\f229"; +} + +.ame-fa-mars-stroke-v:before { + content: "\f22a"; +} + +.ame-fa-mars-stroke-h:before { + content: "\f22b"; +} + +.ame-fa-neuter:before { + content: "\f22c"; +} + +.ame-fa-genderless:before { + content: "\f22d"; +} + +.ame-fa-facebook-official:before { + content: "\f230"; +} + +.ame-fa-pinterest-p:before { + content: "\f231"; +} + +.ame-fa-whatsapp:before { + content: "\f232"; +} + +.ame-fa-server:before { + content: "\f233"; +} + +.ame-fa-user-plus:before { + content: "\f234"; +} + +.ame-fa-user-times:before { + content: "\f235"; +} + +.ame-fa-hotel:before, +.ame-fa-bed:before { + content: "\f236"; +} + +.ame-fa-viacoin:before { + content: "\f237"; +} + +.ame-fa-train:before { + content: "\f238"; +} + +.ame-fa-subway:before { + content: "\f239"; +} + +.ame-fa-medium:before { + content: "\f23a"; +} + +.ame-fa-yc:before, +.ame-fa-y-combinator:before { + content: "\f23b"; +} + +.ame-fa-optin-monster:before { + content: "\f23c"; +} + +.ame-fa-opencart:before { + content: "\f23d"; +} + +.ame-fa-expeditedssl:before { + content: "\f23e"; +} + +.ame-fa-battery-4:before, +.ame-fa-battery-full:before { + content: "\f240"; +} + +.ame-fa-battery-3:before, +.ame-fa-battery-three-quarters:before { + content: "\f241"; +} + +.ame-fa-battery-2:before, +.ame-fa-battery-half:before { + content: "\f242"; +} + +.ame-fa-battery-1:before, +.ame-fa-battery-quarter:before { + content: "\f243"; +} + +.ame-fa-battery-0:before, +.ame-fa-battery-empty:before { + content: "\f244"; +} + +.ame-fa-mouse-pointer:before { + content: "\f245"; +} + +.ame-fa-i-cursor:before { + content: "\f246"; +} + +.ame-fa-object-group:before { + content: "\f247"; +} + +.ame-fa-object-ungroup:before { + content: "\f248"; +} + +.ame-fa-sticky-note:before { + content: "\f249"; +} + +.ame-fa-sticky-note-o:before { + content: "\f24a"; +} + +.ame-fa-cc-jcb:before { + content: "\f24b"; +} + +.ame-fa-cc-diners-club:before { + content: "\f24c"; +} + +.ame-fa-clone:before { + content: "\f24d"; +} + +.ame-fa-balance-scale:before { + content: "\f24e"; +} + +.ame-fa-hourglass-o:before { + content: "\f250"; +} + +.ame-fa-hourglass-1:before, +.ame-fa-hourglass-start:before { + content: "\f251"; +} + +.ame-fa-hourglass-2:before, +.ame-fa-hourglass-half:before { + content: "\f252"; +} + +.ame-fa-hourglass-3:before, +.ame-fa-hourglass-end:before { + content: "\f253"; +} + +.ame-fa-hourglass:before { + content: "\f254"; +} + +.ame-fa-hand-grab-o:before, +.ame-fa-hand-rock-o:before { + content: "\f255"; +} + +.ame-fa-hand-stop-o:before, +.ame-fa-hand-paper-o:before { + content: "\f256"; +} + +.ame-fa-hand-scissors-o:before { + content: "\f257"; +} + +.ame-fa-hand-lizard-o:before { + content: "\f258"; +} + +.ame-fa-hand-spock-o:before { + content: "\f259"; +} + +.ame-fa-hand-pointer-o:before { + content: "\f25a"; +} + +.ame-fa-hand-peace-o:before { + content: "\f25b"; +} + +.ame-fa-trademark:before { + content: "\f25c"; +} + +.ame-fa-registered:before { + content: "\f25d"; +} + +.ame-fa-creative-commons:before { + content: "\f25e"; +} + +.ame-fa-gg:before { + content: "\f260"; +} + +.ame-fa-gg-circle:before { + content: "\f261"; +} + +.ame-fa-tripadvisor:before { + content: "\f262"; +} + +.ame-fa-odnoklassniki:before { + content: "\f263"; +} + +.ame-fa-odnoklassniki-square:before { + content: "\f264"; +} + +.ame-fa-get-pocket:before { + content: "\f265"; +} + +.ame-fa-wikipedia-w:before { + content: "\f266"; +} + +.ame-fa-safari:before { + content: "\f267"; +} + +.ame-fa-chrome:before { + content: "\f268"; +} + +.ame-fa-firefox:before { + content: "\f269"; +} + +.ame-fa-opera:before { + content: "\f26a"; +} + +.ame-fa-internet-explorer:before { + content: "\f26b"; +} + +.ame-fa-tv:before, +.ame-fa-television:before { + content: "\f26c"; +} + +.ame-fa-contao:before { + content: "\f26d"; +} + +.ame-fa-500px:before { + content: "\f26e"; +} + +.ame-fa-amazon:before { + content: "\f270"; +} + +.ame-fa-calendar-plus-o:before { + content: "\f271"; +} + +.ame-fa-calendar-minus-o:before { + content: "\f272"; +} + +.ame-fa-calendar-times-o:before { + content: "\f273"; +} + +.ame-fa-calendar-check-o:before { + content: "\f274"; +} + +.ame-fa-industry:before { + content: "\f275"; +} + +.ame-fa-map-pin:before { + content: "\f276"; +} + +.ame-fa-map-signs:before { + content: "\f277"; +} + +.ame-fa-map-o:before { + content: "\f278"; +} + +.ame-fa-map:before { + content: "\f279"; +} + +.ame-fa-commenting:before { + content: "\f27a"; +} + +.ame-fa-commenting-o:before { + content: "\f27b"; +} + +.ame-fa-houzz:before { + content: "\f27c"; +} + +.ame-fa-vimeo:before { + content: "\f27d"; +} + +.ame-fa-black-tie:before { + content: "\f27e"; +} + +.ame-fa-fonticons:before { + content: "\f280"; +} + +.ame-fa-reddit-alien:before { + content: "\f281"; +} + +.ame-fa-edge:before { + content: "\f282"; +} + +.ame-fa-credit-card-alt:before { + content: "\f283"; +} + +.ame-fa-codiepie:before { + content: "\f284"; +} + +.ame-fa-modx:before { + content: "\f285"; +} + +.ame-fa-fort-awesome:before { + content: "\f286"; +} + +.ame-fa-usb:before { + content: "\f287"; +} + +.ame-fa-product-hunt:before { + content: "\f288"; +} + +.ame-fa-mixcloud:before { + content: "\f289"; +} + +.ame-fa-scribd:before { + content: "\f28a"; +} + +.ame-fa-pause-circle:before { + content: "\f28b"; +} + +.ame-fa-pause-circle-o:before { + content: "\f28c"; +} + +.ame-fa-stop-circle:before { + content: "\f28d"; +} + +.ame-fa-stop-circle-o:before { + content: "\f28e"; +} + +.ame-fa-shopping-bag:before { + content: "\f290"; +} + +.ame-fa-shopping-basket:before { + content: "\f291"; +} + +.ame-fa-hashtag:before { + content: "\f292"; +} + +.ame-fa-bluetooth:before { + content: "\f293"; +} + +.ame-fa-bluetooth-b:before { + content: "\f294"; +} + +.ame-fa-percent:before { + content: "\f295"; +} + +.ame-fa-gitlab:before { + content: "\f296"; +} + +.ame-fa-wpbeginner:before { + content: "\f297"; +} + +.ame-fa-wpforms:before { + content: "\f298"; +} + +.ame-fa-envira:before { + content: "\f299"; +} + +.ame-fa-universal-access:before { + content: "\f29a"; +} + +.ame-fa-wheelchair-alt:before { + content: "\f29b"; +} + +.ame-fa-question-circle-o:before { + content: "\f29c"; +} + +.ame-fa-blind:before { + content: "\f29d"; +} + +.ame-fa-audio-description:before { + content: "\f29e"; +} + +.ame-fa-volume-control-phone:before { + content: "\f2a0"; +} + +.ame-fa-braille:before { + content: "\f2a1"; +} + +.ame-fa-assistive-listening-systems:before { + content: "\f2a2"; +} + +.ame-fa-asl-interpreting:before, +.ame-fa-american-sign-language-interpreting:before { + content: "\f2a3"; +} + +.ame-fa-deafness:before, +.ame-fa-hard-of-hearing:before, +.ame-fa-deaf:before { + content: "\f2a4"; +} + +.ame-fa-glide:before { + content: "\f2a5"; +} + +.ame-fa-glide-g:before { + content: "\f2a6"; +} + +.ame-fa-signing:before, +.ame-fa-sign-language:before { + content: "\f2a7"; +} + +.ame-fa-low-vision:before { + content: "\f2a8"; +} + +.ame-fa-viadeo:before { + content: "\f2a9"; +} + +.ame-fa-viadeo-square:before { + content: "\f2aa"; +} + +.ame-fa-snapchat:before { + content: "\f2ab"; +} + +.ame-fa-snapchat-ghost:before { + content: "\f2ac"; +} + +.ame-fa-snapchat-square:before { + content: "\f2ad"; +} + +.ame-fa-pied-piper:before { + content: "\f2ae"; +} + +.ame-fa-first-order:before { + content: "\f2b0"; +} + +.ame-fa-yoast:before { + content: "\f2b1"; +} + +.ame-fa-themeisle:before { + content: "\f2b2"; +} + +.ame-fa-google-plus-circle:before, +.ame-fa-google-plus-official:before { + content: "\f2b3"; +} + +.ame-fa-fa:before, +.ame-fa-font-awesome:before { + content: "\f2b4"; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +.sr-only-focusable:active, .sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} + +#adminmenu#adminmenu .ame-menu-fa-glass .wp-menu-image:before { + content: "\f000"; +} + +#adminmenu#adminmenu .ame-menu-fa-music .wp-menu-image:before { + content: "\f001"; +} + +#adminmenu#adminmenu .ame-menu-fa-search .wp-menu-image:before { + content: "\f002"; +} + +#adminmenu#adminmenu .ame-menu-fa-envelope-o .wp-menu-image:before { + content: "\f003"; +} + +#adminmenu#adminmenu .ame-menu-fa-heart .wp-menu-image:before { + content: "\f004"; +} + +#adminmenu#adminmenu .ame-menu-fa-star .wp-menu-image:before { + content: "\f005"; +} + +#adminmenu#adminmenu .ame-menu-fa-star-o .wp-menu-image:before { + content: "\f006"; +} + +#adminmenu#adminmenu .ame-menu-fa-user .wp-menu-image:before { + content: "\f007"; +} + +#adminmenu#adminmenu .ame-menu-fa-film .wp-menu-image:before { + content: "\f008"; +} + +#adminmenu#adminmenu .ame-menu-fa-th-large .wp-menu-image:before { + content: "\f009"; +} + +#adminmenu#adminmenu .ame-menu-fa-th .wp-menu-image:before { + content: "\f00a"; +} + +#adminmenu#adminmenu .ame-menu-fa-th-list .wp-menu-image:before { + content: "\f00b"; +} + +#adminmenu#adminmenu .ame-menu-fa-check .wp-menu-image:before { + content: "\f00c"; +} + +#adminmenu#adminmenu .ame-menu-fa-remove .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-close .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-times .wp-menu-image:before { + content: "\f00d"; +} + +#adminmenu#adminmenu .ame-menu-fa-search-plus .wp-menu-image:before { + content: "\f00e"; +} + +#adminmenu#adminmenu .ame-menu-fa-search-minus .wp-menu-image:before { + content: "\f010"; +} + +#adminmenu#adminmenu .ame-menu-fa-power-off .wp-menu-image:before { + content: "\f011"; +} + +#adminmenu#adminmenu .ame-menu-fa-signal .wp-menu-image:before { + content: "\f012"; +} + +#adminmenu#adminmenu .ame-menu-fa-gear .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-cog .wp-menu-image:before { + content: "\f013"; +} + +#adminmenu#adminmenu .ame-menu-fa-trash-o .wp-menu-image:before { + content: "\f014"; +} + +#adminmenu#adminmenu .ame-menu-fa-home .wp-menu-image:before { + content: "\f015"; +} + +#adminmenu#adminmenu .ame-menu-fa-file-o .wp-menu-image:before { + content: "\f016"; +} + +#adminmenu#adminmenu .ame-menu-fa-clock-o .wp-menu-image:before { + content: "\f017"; +} + +#adminmenu#adminmenu .ame-menu-fa-road .wp-menu-image:before { + content: "\f018"; +} + +#adminmenu#adminmenu .ame-menu-fa-download .wp-menu-image:before { + content: "\f019"; +} + +#adminmenu#adminmenu .ame-menu-fa-arrow-circle-o-down .wp-menu-image:before { + content: "\f01a"; +} + +#adminmenu#adminmenu .ame-menu-fa-arrow-circle-o-up .wp-menu-image:before { + content: "\f01b"; +} + +#adminmenu#adminmenu .ame-menu-fa-inbox .wp-menu-image:before { + content: "\f01c"; +} + +#adminmenu#adminmenu .ame-menu-fa-play-circle-o .wp-menu-image:before { + content: "\f01d"; +} + +#adminmenu#adminmenu .ame-menu-fa-rotate-right .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-repeat .wp-menu-image:before { + content: "\f01e"; +} + +#adminmenu#adminmenu .ame-menu-fa-refresh .wp-menu-image:before { + content: "\f021"; +} + +#adminmenu#adminmenu .ame-menu-fa-list-alt .wp-menu-image:before { + content: "\f022"; +} + +#adminmenu#adminmenu .ame-menu-fa-lock .wp-menu-image:before { + content: "\f023"; +} + +#adminmenu#adminmenu .ame-menu-fa-flag .wp-menu-image:before { + content: "\f024"; +} + +#adminmenu#adminmenu .ame-menu-fa-headphones .wp-menu-image:before { + content: "\f025"; +} + +#adminmenu#adminmenu .ame-menu-fa-volume-off .wp-menu-image:before { + content: "\f026"; +} + +#adminmenu#adminmenu .ame-menu-fa-volume-down .wp-menu-image:before { + content: "\f027"; +} + +#adminmenu#adminmenu .ame-menu-fa-volume-up .wp-menu-image:before { + content: "\f028"; +} + +#adminmenu#adminmenu .ame-menu-fa-qrcode .wp-menu-image:before { + content: "\f029"; +} + +#adminmenu#adminmenu .ame-menu-fa-barcode .wp-menu-image:before { + content: "\f02a"; +} + +#adminmenu#adminmenu .ame-menu-fa-tag .wp-menu-image:before { + content: "\f02b"; +} + +#adminmenu#adminmenu .ame-menu-fa-tags .wp-menu-image:before { + content: "\f02c"; +} + +#adminmenu#adminmenu .ame-menu-fa-book .wp-menu-image:before { + content: "\f02d"; +} + +#adminmenu#adminmenu .ame-menu-fa-bookmark .wp-menu-image:before { + content: "\f02e"; +} + +#adminmenu#adminmenu .ame-menu-fa-print .wp-menu-image:before { + content: "\f02f"; +} + +#adminmenu#adminmenu .ame-menu-fa-camera .wp-menu-image:before { + content: "\f030"; +} + +#adminmenu#adminmenu .ame-menu-fa-font .wp-menu-image:before { + content: "\f031"; +} + +#adminmenu#adminmenu .ame-menu-fa-bold .wp-menu-image:before { + content: "\f032"; +} + +#adminmenu#adminmenu .ame-menu-fa-italic .wp-menu-image:before { + content: "\f033"; +} + +#adminmenu#adminmenu .ame-menu-fa-text-height .wp-menu-image:before { + content: "\f034"; +} + +#adminmenu#adminmenu .ame-menu-fa-text-width .wp-menu-image:before { + content: "\f035"; +} + +#adminmenu#adminmenu .ame-menu-fa-align-left .wp-menu-image:before { + content: "\f036"; +} + +#adminmenu#adminmenu .ame-menu-fa-align-center .wp-menu-image:before { + content: "\f037"; +} + +#adminmenu#adminmenu .ame-menu-fa-align-right .wp-menu-image:before { + content: "\f038"; +} + +#adminmenu#adminmenu .ame-menu-fa-align-justify .wp-menu-image:before { + content: "\f039"; +} + +#adminmenu#adminmenu .ame-menu-fa-list .wp-menu-image:before { + content: "\f03a"; +} + +#adminmenu#adminmenu .ame-menu-fa-dedent .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-outdent .wp-menu-image:before { + content: "\f03b"; +} + +#adminmenu#adminmenu .ame-menu-fa-indent .wp-menu-image:before { + content: "\f03c"; +} + +#adminmenu#adminmenu .ame-menu-fa-video-camera .wp-menu-image:before { + content: "\f03d"; +} + +#adminmenu#adminmenu .ame-menu-fa-photo .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-image .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-picture-o .wp-menu-image:before { + content: "\f03e"; +} + +#adminmenu#adminmenu .ame-menu-fa-pencil .wp-menu-image:before { + content: "\f040"; +} + +#adminmenu#adminmenu .ame-menu-fa-map-marker .wp-menu-image:before { + content: "\f041"; +} + +#adminmenu#adminmenu .ame-menu-fa-adjust .wp-menu-image:before { + content: "\f042"; +} + +#adminmenu#adminmenu .ame-menu-fa-tint .wp-menu-image:before { + content: "\f043"; +} + +#adminmenu#adminmenu .ame-menu-fa-edit .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-pencil-square-o .wp-menu-image:before { + content: "\f044"; +} + +#adminmenu#adminmenu .ame-menu-fa-share-square-o .wp-menu-image:before { + content: "\f045"; +} + +#adminmenu#adminmenu .ame-menu-fa-check-square-o .wp-menu-image:before { + content: "\f046"; +} + +#adminmenu#adminmenu .ame-menu-fa-arrows .wp-menu-image:before { + content: "\f047"; +} + +#adminmenu#adminmenu .ame-menu-fa-step-backward .wp-menu-image:before { + content: "\f048"; +} + +#adminmenu#adminmenu .ame-menu-fa-fast-backward .wp-menu-image:before { + content: "\f049"; +} + +#adminmenu#adminmenu .ame-menu-fa-backward .wp-menu-image:before { + content: "\f04a"; +} + +#adminmenu#adminmenu .ame-menu-fa-play .wp-menu-image:before { + content: "\f04b"; +} + +#adminmenu#adminmenu .ame-menu-fa-pause .wp-menu-image:before { + content: "\f04c"; +} + +#adminmenu#adminmenu .ame-menu-fa-stop .wp-menu-image:before { + content: "\f04d"; +} + +#adminmenu#adminmenu .ame-menu-fa-forward .wp-menu-image:before { + content: "\f04e"; +} + +#adminmenu#adminmenu .ame-menu-fa-fast-forward .wp-menu-image:before { + content: "\f050"; +} + +#adminmenu#adminmenu .ame-menu-fa-step-forward .wp-menu-image:before { + content: "\f051"; +} + +#adminmenu#adminmenu .ame-menu-fa-eject .wp-menu-image:before { + content: "\f052"; +} + +#adminmenu#adminmenu .ame-menu-fa-chevron-left .wp-menu-image:before { + content: "\f053"; +} + +#adminmenu#adminmenu .ame-menu-fa-chevron-right .wp-menu-image:before { + content: "\f054"; +} + +#adminmenu#adminmenu .ame-menu-fa-plus-circle .wp-menu-image:before { + content: "\f055"; +} + +#adminmenu#adminmenu .ame-menu-fa-minus-circle .wp-menu-image:before { + content: "\f056"; +} + +#adminmenu#adminmenu .ame-menu-fa-times-circle .wp-menu-image:before { + content: "\f057"; +} + +#adminmenu#adminmenu .ame-menu-fa-check-circle .wp-menu-image:before { + content: "\f058"; +} + +#adminmenu#adminmenu .ame-menu-fa-question-circle .wp-menu-image:before { + content: "\f059"; +} + +#adminmenu#adminmenu .ame-menu-fa-info-circle .wp-menu-image:before { + content: "\f05a"; +} + +#adminmenu#adminmenu .ame-menu-fa-crosshairs .wp-menu-image:before { + content: "\f05b"; +} + +#adminmenu#adminmenu .ame-menu-fa-times-circle-o .wp-menu-image:before { + content: "\f05c"; +} + +#adminmenu#adminmenu .ame-menu-fa-check-circle-o .wp-menu-image:before { + content: "\f05d"; +} + +#adminmenu#adminmenu .ame-menu-fa-ban .wp-menu-image:before { + content: "\f05e"; +} + +#adminmenu#adminmenu .ame-menu-fa-arrow-left .wp-menu-image:before { + content: "\f060"; +} + +#adminmenu#adminmenu .ame-menu-fa-arrow-right .wp-menu-image:before { + content: "\f061"; +} + +#adminmenu#adminmenu .ame-menu-fa-arrow-up .wp-menu-image:before { + content: "\f062"; +} + +#adminmenu#adminmenu .ame-menu-fa-arrow-down .wp-menu-image:before { + content: "\f063"; +} + +#adminmenu#adminmenu .ame-menu-fa-mail-forward .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-share .wp-menu-image:before { + content: "\f064"; +} + +#adminmenu#adminmenu .ame-menu-fa-expand .wp-menu-image:before { + content: "\f065"; +} + +#adminmenu#adminmenu .ame-menu-fa-compress .wp-menu-image:before { + content: "\f066"; +} + +#adminmenu#adminmenu .ame-menu-fa-plus .wp-menu-image:before { + content: "\f067"; +} + +#adminmenu#adminmenu .ame-menu-fa-minus .wp-menu-image:before { + content: "\f068"; +} + +#adminmenu#adminmenu .ame-menu-fa-asterisk .wp-menu-image:before { + content: "\f069"; +} + +#adminmenu#adminmenu .ame-menu-fa-exclamation-circle .wp-menu-image:before { + content: "\f06a"; +} + +#adminmenu#adminmenu .ame-menu-fa-gift .wp-menu-image:before { + content: "\f06b"; +} + +#adminmenu#adminmenu .ame-menu-fa-leaf .wp-menu-image:before { + content: "\f06c"; +} + +#adminmenu#adminmenu .ame-menu-fa-fire .wp-menu-image:before { + content: "\f06d"; +} + +#adminmenu#adminmenu .ame-menu-fa-eye .wp-menu-image:before { + content: "\f06e"; +} + +#adminmenu#adminmenu .ame-menu-fa-eye-slash .wp-menu-image:before { + content: "\f070"; +} + +#adminmenu#adminmenu .ame-menu-fa-warning .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-exclamation-triangle .wp-menu-image:before { + content: "\f071"; +} + +#adminmenu#adminmenu .ame-menu-fa-plane .wp-menu-image:before { + content: "\f072"; +} + +#adminmenu#adminmenu .ame-menu-fa-calendar .wp-menu-image:before { + content: "\f073"; +} + +#adminmenu#adminmenu .ame-menu-fa-random .wp-menu-image:before { + content: "\f074"; +} + +#adminmenu#adminmenu .ame-menu-fa-comment .wp-menu-image:before { + content: "\f075"; +} + +#adminmenu#adminmenu .ame-menu-fa-magnet .wp-menu-image:before { + content: "\f076"; +} + +#adminmenu#adminmenu .ame-menu-fa-chevron-up .wp-menu-image:before { + content: "\f077"; +} + +#adminmenu#adminmenu .ame-menu-fa-chevron-down .wp-menu-image:before { + content: "\f078"; +} + +#adminmenu#adminmenu .ame-menu-fa-retweet .wp-menu-image:before { + content: "\f079"; +} + +#adminmenu#adminmenu .ame-menu-fa-shopping-cart .wp-menu-image:before { + content: "\f07a"; +} + +#adminmenu#adminmenu .ame-menu-fa-folder .wp-menu-image:before { + content: "\f07b"; +} + +#adminmenu#adminmenu .ame-menu-fa-folder-open .wp-menu-image:before { + content: "\f07c"; +} + +#adminmenu#adminmenu .ame-menu-fa-arrows-v .wp-menu-image:before { + content: "\f07d"; +} + +#adminmenu#adminmenu .ame-menu-fa-arrows-h .wp-menu-image:before { + content: "\f07e"; +} + +#adminmenu#adminmenu .ame-menu-fa-bar-chart-o .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-bar-chart .wp-menu-image:before { + content: "\f080"; +} + +#adminmenu#adminmenu .ame-menu-fa-twitter-square .wp-menu-image:before { + content: "\f081"; +} + +#adminmenu#adminmenu .ame-menu-fa-facebook-square .wp-menu-image:before { + content: "\f082"; +} + +#adminmenu#adminmenu .ame-menu-fa-camera-retro .wp-menu-image:before { + content: "\f083"; +} + +#adminmenu#adminmenu .ame-menu-fa-key .wp-menu-image:before { + content: "\f084"; +} + +#adminmenu#adminmenu .ame-menu-fa-gears .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-cogs .wp-menu-image:before { + content: "\f085"; +} + +#adminmenu#adminmenu .ame-menu-fa-comments .wp-menu-image:before { + content: "\f086"; +} + +#adminmenu#adminmenu .ame-menu-fa-thumbs-o-up .wp-menu-image:before { + content: "\f087"; +} + +#adminmenu#adminmenu .ame-menu-fa-thumbs-o-down .wp-menu-image:before { + content: "\f088"; +} + +#adminmenu#adminmenu .ame-menu-fa-star-half .wp-menu-image:before { + content: "\f089"; +} + +#adminmenu#adminmenu .ame-menu-fa-heart-o .wp-menu-image:before { + content: "\f08a"; +} + +#adminmenu#adminmenu .ame-menu-fa-sign-out .wp-menu-image:before { + content: "\f08b"; +} + +#adminmenu#adminmenu .ame-menu-fa-linkedin-square .wp-menu-image:before { + content: "\f08c"; +} + +#adminmenu#adminmenu .ame-menu-fa-thumb-tack .wp-menu-image:before { + content: "\f08d"; +} + +#adminmenu#adminmenu .ame-menu-fa-external-link .wp-menu-image:before { + content: "\f08e"; +} + +#adminmenu#adminmenu .ame-menu-fa-sign-in .wp-menu-image:before { + content: "\f090"; +} + +#adminmenu#adminmenu .ame-menu-fa-trophy .wp-menu-image:before { + content: "\f091"; +} + +#adminmenu#adminmenu .ame-menu-fa-github-square .wp-menu-image:before { + content: "\f092"; +} + +#adminmenu#adminmenu .ame-menu-fa-upload .wp-menu-image:before { + content: "\f093"; +} + +#adminmenu#adminmenu .ame-menu-fa-lemon-o .wp-menu-image:before { + content: "\f094"; +} + +#adminmenu#adminmenu .ame-menu-fa-phone .wp-menu-image:before { + content: "\f095"; +} + +#adminmenu#adminmenu .ame-menu-fa-square-o .wp-menu-image:before { + content: "\f096"; +} + +#adminmenu#adminmenu .ame-menu-fa-bookmark-o .wp-menu-image:before { + content: "\f097"; +} + +#adminmenu#adminmenu .ame-menu-fa-phone-square .wp-menu-image:before { + content: "\f098"; +} + +#adminmenu#adminmenu .ame-menu-fa-twitter .wp-menu-image:before { + content: "\f099"; +} + +#adminmenu#adminmenu .ame-menu-fa-facebook-f .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-facebook .wp-menu-image:before { + content: "\f09a"; +} + +#adminmenu#adminmenu .ame-menu-fa-github .wp-menu-image:before { + content: "\f09b"; +} + +#adminmenu#adminmenu .ame-menu-fa-unlock .wp-menu-image:before { + content: "\f09c"; +} + +#adminmenu#adminmenu .ame-menu-fa-credit-card .wp-menu-image:before { + content: "\f09d"; +} + +#adminmenu#adminmenu .ame-menu-fa-feed .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-rss .wp-menu-image:before { + content: "\f09e"; +} + +#adminmenu#adminmenu .ame-menu-fa-hdd-o .wp-menu-image:before { + content: "\f0a0"; +} + +#adminmenu#adminmenu .ame-menu-fa-bullhorn .wp-menu-image:before { + content: "\f0a1"; +} + +#adminmenu#adminmenu .ame-menu-fa-bell .wp-menu-image:before { + content: "\f0f3"; +} + +#adminmenu#adminmenu .ame-menu-fa-certificate .wp-menu-image:before { + content: "\f0a3"; +} + +#adminmenu#adminmenu .ame-menu-fa-hand-o-right .wp-menu-image:before { + content: "\f0a4"; +} + +#adminmenu#adminmenu .ame-menu-fa-hand-o-left .wp-menu-image:before { + content: "\f0a5"; +} + +#adminmenu#adminmenu .ame-menu-fa-hand-o-up .wp-menu-image:before { + content: "\f0a6"; +} + +#adminmenu#adminmenu .ame-menu-fa-hand-o-down .wp-menu-image:before { + content: "\f0a7"; +} + +#adminmenu#adminmenu .ame-menu-fa-arrow-circle-left .wp-menu-image:before { + content: "\f0a8"; +} + +#adminmenu#adminmenu .ame-menu-fa-arrow-circle-right .wp-menu-image:before { + content: "\f0a9"; +} + +#adminmenu#adminmenu .ame-menu-fa-arrow-circle-up .wp-menu-image:before { + content: "\f0aa"; +} + +#adminmenu#adminmenu .ame-menu-fa-arrow-circle-down .wp-menu-image:before { + content: "\f0ab"; +} + +#adminmenu#adminmenu .ame-menu-fa-globe .wp-menu-image:before { + content: "\f0ac"; +} + +#adminmenu#adminmenu .ame-menu-fa-wrench .wp-menu-image:before { + content: "\f0ad"; +} + +#adminmenu#adminmenu .ame-menu-fa-tasks .wp-menu-image:before { + content: "\f0ae"; +} + +#adminmenu#adminmenu .ame-menu-fa-filter .wp-menu-image:before { + content: "\f0b0"; +} + +#adminmenu#adminmenu .ame-menu-fa-briefcase .wp-menu-image:before { + content: "\f0b1"; +} + +#adminmenu#adminmenu .ame-menu-fa-arrows-alt .wp-menu-image:before { + content: "\f0b2"; +} + +#adminmenu#adminmenu .ame-menu-fa-group .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-users .wp-menu-image:before { + content: "\f0c0"; +} + +#adminmenu#adminmenu .ame-menu-fa-chain .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-link .wp-menu-image:before { + content: "\f0c1"; +} + +#adminmenu#adminmenu .ame-menu-fa-cloud .wp-menu-image:before { + content: "\f0c2"; +} + +#adminmenu#adminmenu .ame-menu-fa-flask .wp-menu-image:before { + content: "\f0c3"; +} + +#adminmenu#adminmenu .ame-menu-fa-cut .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-scissors .wp-menu-image:before { + content: "\f0c4"; +} + +#adminmenu#adminmenu .ame-menu-fa-copy .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-files-o .wp-menu-image:before { + content: "\f0c5"; +} + +#adminmenu#adminmenu .ame-menu-fa-paperclip .wp-menu-image:before { + content: "\f0c6"; +} + +#adminmenu#adminmenu .ame-menu-fa-save .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-floppy-o .wp-menu-image:before { + content: "\f0c7"; +} + +#adminmenu#adminmenu .ame-menu-fa-square .wp-menu-image:before { + content: "\f0c8"; +} + +#adminmenu#adminmenu .ame-menu-fa-navicon .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-reorder .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-bars .wp-menu-image:before { + content: "\f0c9"; +} + +#adminmenu#adminmenu .ame-menu-fa-list-ul .wp-menu-image:before { + content: "\f0ca"; +} + +#adminmenu#adminmenu .ame-menu-fa-list-ol .wp-menu-image:before { + content: "\f0cb"; +} + +#adminmenu#adminmenu .ame-menu-fa-strikethrough .wp-menu-image:before { + content: "\f0cc"; +} + +#adminmenu#adminmenu .ame-menu-fa-underline .wp-menu-image:before { + content: "\f0cd"; +} + +#adminmenu#adminmenu .ame-menu-fa-table .wp-menu-image:before { + content: "\f0ce"; +} + +#adminmenu#adminmenu .ame-menu-fa-magic .wp-menu-image:before { + content: "\f0d0"; +} + +#adminmenu#adminmenu .ame-menu-fa-truck .wp-menu-image:before { + content: "\f0d1"; +} + +#adminmenu#adminmenu .ame-menu-fa-pinterest .wp-menu-image:before { + content: "\f0d2"; +} + +#adminmenu#adminmenu .ame-menu-fa-pinterest-square .wp-menu-image:before { + content: "\f0d3"; +} + +#adminmenu#adminmenu .ame-menu-fa-google-plus-square .wp-menu-image:before { + content: "\f0d4"; +} + +#adminmenu#adminmenu .ame-menu-fa-google-plus .wp-menu-image:before { + content: "\f0d5"; +} + +#adminmenu#adminmenu .ame-menu-fa-money .wp-menu-image:before { + content: "\f0d6"; +} + +#adminmenu#adminmenu .ame-menu-fa-caret-down .wp-menu-image:before { + content: "\f0d7"; +} + +#adminmenu#adminmenu .ame-menu-fa-caret-up .wp-menu-image:before { + content: "\f0d8"; +} + +#adminmenu#adminmenu .ame-menu-fa-caret-left .wp-menu-image:before { + content: "\f0d9"; +} + +#adminmenu#adminmenu .ame-menu-fa-caret-right .wp-menu-image:before { + content: "\f0da"; +} + +#adminmenu#adminmenu .ame-menu-fa-columns .wp-menu-image:before { + content: "\f0db"; +} + +#adminmenu#adminmenu .ame-menu-fa-unsorted .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-sort .wp-menu-image:before { + content: "\f0dc"; +} + +#adminmenu#adminmenu .ame-menu-fa-sort-down .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-sort-desc .wp-menu-image:before { + content: "\f0dd"; +} + +#adminmenu#adminmenu .ame-menu-fa-sort-up .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-sort-asc .wp-menu-image:before { + content: "\f0de"; +} + +#adminmenu#adminmenu .ame-menu-fa-envelope .wp-menu-image:before { + content: "\f0e0"; +} + +#adminmenu#adminmenu .ame-menu-fa-linkedin .wp-menu-image:before { + content: "\f0e1"; +} + +#adminmenu#adminmenu .ame-menu-fa-rotate-left .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-undo .wp-menu-image:before { + content: "\f0e2"; +} + +#adminmenu#adminmenu .ame-menu-fa-legal .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-gavel .wp-menu-image:before { + content: "\f0e3"; +} + +#adminmenu#adminmenu .ame-menu-fa-dashboard .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-tachometer .wp-menu-image:before { + content: "\f0e4"; +} + +#adminmenu#adminmenu .ame-menu-fa-comment-o .wp-menu-image:before { + content: "\f0e5"; +} + +#adminmenu#adminmenu .ame-menu-fa-comments-o .wp-menu-image:before { + content: "\f0e6"; +} + +#adminmenu#adminmenu .ame-menu-fa-flash .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-bolt .wp-menu-image:before { + content: "\f0e7"; +} + +#adminmenu#adminmenu .ame-menu-fa-sitemap .wp-menu-image:before { + content: "\f0e8"; +} + +#adminmenu#adminmenu .ame-menu-fa-umbrella .wp-menu-image:before { + content: "\f0e9"; +} + +#adminmenu#adminmenu .ame-menu-fa-paste .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-clipboard .wp-menu-image:before { + content: "\f0ea"; +} + +#adminmenu#adminmenu .ame-menu-fa-lightbulb-o .wp-menu-image:before { + content: "\f0eb"; +} + +#adminmenu#adminmenu .ame-menu-fa-exchange .wp-menu-image:before { + content: "\f0ec"; +} + +#adminmenu#adminmenu .ame-menu-fa-cloud-download .wp-menu-image:before { + content: "\f0ed"; +} + +#adminmenu#adminmenu .ame-menu-fa-cloud-upload .wp-menu-image:before { + content: "\f0ee"; +} + +#adminmenu#adminmenu .ame-menu-fa-user-md .wp-menu-image:before { + content: "\f0f0"; +} + +#adminmenu#adminmenu .ame-menu-fa-stethoscope .wp-menu-image:before { + content: "\f0f1"; +} + +#adminmenu#adminmenu .ame-menu-fa-suitcase .wp-menu-image:before { + content: "\f0f2"; +} + +#adminmenu#adminmenu .ame-menu-fa-bell-o .wp-menu-image:before { + content: "\f0a2"; +} + +#adminmenu#adminmenu .ame-menu-fa-coffee .wp-menu-image:before { + content: "\f0f4"; +} + +#adminmenu#adminmenu .ame-menu-fa-cutlery .wp-menu-image:before { + content: "\f0f5"; +} + +#adminmenu#adminmenu .ame-menu-fa-file-text-o .wp-menu-image:before { + content: "\f0f6"; +} + +#adminmenu#adminmenu .ame-menu-fa-building-o .wp-menu-image:before { + content: "\f0f7"; +} + +#adminmenu#adminmenu .ame-menu-fa-hospital-o .wp-menu-image:before { + content: "\f0f8"; +} + +#adminmenu#adminmenu .ame-menu-fa-ambulance .wp-menu-image:before { + content: "\f0f9"; +} + +#adminmenu#adminmenu .ame-menu-fa-medkit .wp-menu-image:before { + content: "\f0fa"; +} + +#adminmenu#adminmenu .ame-menu-fa-fighter-jet .wp-menu-image:before { + content: "\f0fb"; +} + +#adminmenu#adminmenu .ame-menu-fa-beer .wp-menu-image:before { + content: "\f0fc"; +} + +#adminmenu#adminmenu .ame-menu-fa-h-square .wp-menu-image:before { + content: "\f0fd"; +} + +#adminmenu#adminmenu .ame-menu-fa-plus-square .wp-menu-image:before { + content: "\f0fe"; +} + +#adminmenu#adminmenu .ame-menu-fa-angle-double-left .wp-menu-image:before { + content: "\f100"; +} + +#adminmenu#adminmenu .ame-menu-fa-angle-double-right .wp-menu-image:before { + content: "\f101"; +} + +#adminmenu#adminmenu .ame-menu-fa-angle-double-up .wp-menu-image:before { + content: "\f102"; +} + +#adminmenu#adminmenu .ame-menu-fa-angle-double-down .wp-menu-image:before { + content: "\f103"; +} + +#adminmenu#adminmenu .ame-menu-fa-angle-left .wp-menu-image:before { + content: "\f104"; +} + +#adminmenu#adminmenu .ame-menu-fa-angle-right .wp-menu-image:before { + content: "\f105"; +} + +#adminmenu#adminmenu .ame-menu-fa-angle-up .wp-menu-image:before { + content: "\f106"; +} + +#adminmenu#adminmenu .ame-menu-fa-angle-down .wp-menu-image:before { + content: "\f107"; +} + +#adminmenu#adminmenu .ame-menu-fa-desktop .wp-menu-image:before { + content: "\f108"; +} + +#adminmenu#adminmenu .ame-menu-fa-laptop .wp-menu-image:before { + content: "\f109"; +} + +#adminmenu#adminmenu .ame-menu-fa-tablet .wp-menu-image:before { + content: "\f10a"; +} + +#adminmenu#adminmenu .ame-menu-fa-mobile-phone .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-mobile .wp-menu-image:before { + content: "\f10b"; +} + +#adminmenu#adminmenu .ame-menu-fa-circle-o .wp-menu-image:before { + content: "\f10c"; +} + +#adminmenu#adminmenu .ame-menu-fa-quote-left .wp-menu-image:before { + content: "\f10d"; +} + +#adminmenu#adminmenu .ame-menu-fa-quote-right .wp-menu-image:before { + content: "\f10e"; +} + +#adminmenu#adminmenu .ame-menu-fa-spinner .wp-menu-image:before { + content: "\f110"; +} + +#adminmenu#adminmenu .ame-menu-fa-circle .wp-menu-image:before { + content: "\f111"; +} + +#adminmenu#adminmenu .ame-menu-fa-mail-reply .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-reply .wp-menu-image:before { + content: "\f112"; +} + +#adminmenu#adminmenu .ame-menu-fa-github-alt .wp-menu-image:before { + content: "\f113"; +} + +#adminmenu#adminmenu .ame-menu-fa-folder-o .wp-menu-image:before { + content: "\f114"; +} + +#adminmenu#adminmenu .ame-menu-fa-folder-open-o .wp-menu-image:before { + content: "\f115"; +} + +#adminmenu#adminmenu .ame-menu-fa-smile-o .wp-menu-image:before { + content: "\f118"; +} + +#adminmenu#adminmenu .ame-menu-fa-frown-o .wp-menu-image:before { + content: "\f119"; +} + +#adminmenu#adminmenu .ame-menu-fa-meh-o .wp-menu-image:before { + content: "\f11a"; +} + +#adminmenu#adminmenu .ame-menu-fa-gamepad .wp-menu-image:before { + content: "\f11b"; +} + +#adminmenu#adminmenu .ame-menu-fa-keyboard-o .wp-menu-image:before { + content: "\f11c"; +} + +#adminmenu#adminmenu .ame-menu-fa-flag-o .wp-menu-image:before { + content: "\f11d"; +} + +#adminmenu#adminmenu .ame-menu-fa-flag-checkered .wp-menu-image:before { + content: "\f11e"; +} + +#adminmenu#adminmenu .ame-menu-fa-terminal .wp-menu-image:before { + content: "\f120"; +} + +#adminmenu#adminmenu .ame-menu-fa-code .wp-menu-image:before { + content: "\f121"; +} + +#adminmenu#adminmenu .ame-menu-fa-mail-reply-all .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-reply-all .wp-menu-image:before { + content: "\f122"; +} + +#adminmenu#adminmenu .ame-menu-fa-star-half-empty .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-star-half-full .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-star-half-o .wp-menu-image:before { + content: "\f123"; +} + +#adminmenu#adminmenu .ame-menu-fa-location-arrow .wp-menu-image:before { + content: "\f124"; +} + +#adminmenu#adminmenu .ame-menu-fa-crop .wp-menu-image:before { + content: "\f125"; +} + +#adminmenu#adminmenu .ame-menu-fa-code-fork .wp-menu-image:before { + content: "\f126"; +} + +#adminmenu#adminmenu .ame-menu-fa-unlink .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-chain-broken .wp-menu-image:before { + content: "\f127"; +} + +#adminmenu#adminmenu .ame-menu-fa-question .wp-menu-image:before { + content: "\f128"; +} + +#adminmenu#adminmenu .ame-menu-fa-info .wp-menu-image:before { + content: "\f129"; +} + +#adminmenu#adminmenu .ame-menu-fa-exclamation .wp-menu-image:before { + content: "\f12a"; +} + +#adminmenu#adminmenu .ame-menu-fa-superscript .wp-menu-image:before { + content: "\f12b"; +} + +#adminmenu#adminmenu .ame-menu-fa-subscript .wp-menu-image:before { + content: "\f12c"; +} + +#adminmenu#adminmenu .ame-menu-fa-eraser .wp-menu-image:before { + content: "\f12d"; +} + +#adminmenu#adminmenu .ame-menu-fa-puzzle-piece .wp-menu-image:before { + content: "\f12e"; +} + +#adminmenu#adminmenu .ame-menu-fa-microphone .wp-menu-image:before { + content: "\f130"; +} + +#adminmenu#adminmenu .ame-menu-fa-microphone-slash .wp-menu-image:before { + content: "\f131"; +} + +#adminmenu#adminmenu .ame-menu-fa-shield .wp-menu-image:before { + content: "\f132"; +} + +#adminmenu#adminmenu .ame-menu-fa-calendar-o .wp-menu-image:before { + content: "\f133"; +} + +#adminmenu#adminmenu .ame-menu-fa-fire-extinguisher .wp-menu-image:before { + content: "\f134"; +} + +#adminmenu#adminmenu .ame-menu-fa-rocket .wp-menu-image:before { + content: "\f135"; +} + +#adminmenu#adminmenu .ame-menu-fa-maxcdn .wp-menu-image:before { + content: "\f136"; +} + +#adminmenu#adminmenu .ame-menu-fa-chevron-circle-left .wp-menu-image:before { + content: "\f137"; +} + +#adminmenu#adminmenu .ame-menu-fa-chevron-circle-right .wp-menu-image:before { + content: "\f138"; +} + +#adminmenu#adminmenu .ame-menu-fa-chevron-circle-up .wp-menu-image:before { + content: "\f139"; +} + +#adminmenu#adminmenu .ame-menu-fa-chevron-circle-down .wp-menu-image:before { + content: "\f13a"; +} + +#adminmenu#adminmenu .ame-menu-fa-html5 .wp-menu-image:before { + content: "\f13b"; +} + +#adminmenu#adminmenu .ame-menu-fa-css3 .wp-menu-image:before { + content: "\f13c"; +} + +#adminmenu#adminmenu .ame-menu-fa-anchor .wp-menu-image:before { + content: "\f13d"; +} + +#adminmenu#adminmenu .ame-menu-fa-unlock-alt .wp-menu-image:before { + content: "\f13e"; +} + +#adminmenu#adminmenu .ame-menu-fa-bullseye .wp-menu-image:before { + content: "\f140"; +} + +#adminmenu#adminmenu .ame-menu-fa-ellipsis-h .wp-menu-image:before { + content: "\f141"; +} + +#adminmenu#adminmenu .ame-menu-fa-ellipsis-v .wp-menu-image:before { + content: "\f142"; +} + +#adminmenu#adminmenu .ame-menu-fa-rss-square .wp-menu-image:before { + content: "\f143"; +} + +#adminmenu#adminmenu .ame-menu-fa-play-circle .wp-menu-image:before { + content: "\f144"; +} + +#adminmenu#adminmenu .ame-menu-fa-ticket .wp-menu-image:before { + content: "\f145"; +} + +#adminmenu#adminmenu .ame-menu-fa-minus-square .wp-menu-image:before { + content: "\f146"; +} + +#adminmenu#adminmenu .ame-menu-fa-minus-square-o .wp-menu-image:before { + content: "\f147"; +} + +#adminmenu#adminmenu .ame-menu-fa-level-up .wp-menu-image:before { + content: "\f148"; +} + +#adminmenu#adminmenu .ame-menu-fa-level-down .wp-menu-image:before { + content: "\f149"; +} + +#adminmenu#adminmenu .ame-menu-fa-check-square .wp-menu-image:before { + content: "\f14a"; +} + +#adminmenu#adminmenu .ame-menu-fa-pencil-square .wp-menu-image:before { + content: "\f14b"; +} + +#adminmenu#adminmenu .ame-menu-fa-external-link-square .wp-menu-image:before { + content: "\f14c"; +} + +#adminmenu#adminmenu .ame-menu-fa-share-square .wp-menu-image:before { + content: "\f14d"; +} + +#adminmenu#adminmenu .ame-menu-fa-compass .wp-menu-image:before { + content: "\f14e"; +} + +#adminmenu#adminmenu .ame-menu-fa-toggle-down .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-caret-square-o-down .wp-menu-image:before { + content: "\f150"; +} + +#adminmenu#adminmenu .ame-menu-fa-toggle-up .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-caret-square-o-up .wp-menu-image:before { + content: "\f151"; +} + +#adminmenu#adminmenu .ame-menu-fa-toggle-right .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-caret-square-o-right .wp-menu-image:before { + content: "\f152"; +} + +#adminmenu#adminmenu .ame-menu-fa-euro .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-eur .wp-menu-image:before { + content: "\f153"; +} + +#adminmenu#adminmenu .ame-menu-fa-gbp .wp-menu-image:before { + content: "\f154"; +} + +#adminmenu#adminmenu .ame-menu-fa-dollar .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-usd .wp-menu-image:before { + content: "\f155"; +} + +#adminmenu#adminmenu .ame-menu-fa-rupee .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-inr .wp-menu-image:before { + content: "\f156"; +} + +#adminmenu#adminmenu .ame-menu-fa-cny .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-rmb .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-yen .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-jpy .wp-menu-image:before { + content: "\f157"; +} + +#adminmenu#adminmenu .ame-menu-fa-ruble .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-rouble .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-rub .wp-menu-image:before { + content: "\f158"; +} + +#adminmenu#adminmenu .ame-menu-fa-won .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-krw .wp-menu-image:before { + content: "\f159"; +} + +#adminmenu#adminmenu .ame-menu-fa-bitcoin .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-btc .wp-menu-image:before { + content: "\f15a"; +} + +#adminmenu#adminmenu .ame-menu-fa-file .wp-menu-image:before { + content: "\f15b"; +} + +#adminmenu#adminmenu .ame-menu-fa-file-text .wp-menu-image:before { + content: "\f15c"; +} + +#adminmenu#adminmenu .ame-menu-fa-sort-alpha-asc .wp-menu-image:before { + content: "\f15d"; +} + +#adminmenu#adminmenu .ame-menu-fa-sort-alpha-desc .wp-menu-image:before { + content: "\f15e"; +} + +#adminmenu#adminmenu .ame-menu-fa-sort-amount-asc .wp-menu-image:before { + content: "\f160"; +} + +#adminmenu#adminmenu .ame-menu-fa-sort-amount-desc .wp-menu-image:before { + content: "\f161"; +} + +#adminmenu#adminmenu .ame-menu-fa-sort-numeric-asc .wp-menu-image:before { + content: "\f162"; +} + +#adminmenu#adminmenu .ame-menu-fa-sort-numeric-desc .wp-menu-image:before { + content: "\f163"; +} + +#adminmenu#adminmenu .ame-menu-fa-thumbs-up .wp-menu-image:before { + content: "\f164"; +} + +#adminmenu#adminmenu .ame-menu-fa-thumbs-down .wp-menu-image:before { + content: "\f165"; +} + +#adminmenu#adminmenu .ame-menu-fa-youtube-square .wp-menu-image:before { + content: "\f166"; +} + +#adminmenu#adminmenu .ame-menu-fa-youtube .wp-menu-image:before { + content: "\f167"; +} + +#adminmenu#adminmenu .ame-menu-fa-xing .wp-menu-image:before { + content: "\f168"; +} + +#adminmenu#adminmenu .ame-menu-fa-xing-square .wp-menu-image:before { + content: "\f169"; +} + +#adminmenu#adminmenu .ame-menu-fa-youtube-play .wp-menu-image:before { + content: "\f16a"; +} + +#adminmenu#adminmenu .ame-menu-fa-dropbox .wp-menu-image:before { + content: "\f16b"; +} + +#adminmenu#adminmenu .ame-menu-fa-stack-overflow .wp-menu-image:before { + content: "\f16c"; +} + +#adminmenu#adminmenu .ame-menu-fa-instagram .wp-menu-image:before { + content: "\f16d"; +} + +#adminmenu#adminmenu .ame-menu-fa-flickr .wp-menu-image:before { + content: "\f16e"; +} + +#adminmenu#adminmenu .ame-menu-fa-adn .wp-menu-image:before { + content: "\f170"; +} + +#adminmenu#adminmenu .ame-menu-fa-bitbucket .wp-menu-image:before { + content: "\f171"; +} + +#adminmenu#adminmenu .ame-menu-fa-bitbucket-square .wp-menu-image:before { + content: "\f172"; +} + +#adminmenu#adminmenu .ame-menu-fa-tumblr .wp-menu-image:before { + content: "\f173"; +} + +#adminmenu#adminmenu .ame-menu-fa-tumblr-square .wp-menu-image:before { + content: "\f174"; +} + +#adminmenu#adminmenu .ame-menu-fa-long-arrow-down .wp-menu-image:before { + content: "\f175"; +} + +#adminmenu#adminmenu .ame-menu-fa-long-arrow-up .wp-menu-image:before { + content: "\f176"; +} + +#adminmenu#adminmenu .ame-menu-fa-long-arrow-left .wp-menu-image:before { + content: "\f177"; +} + +#adminmenu#adminmenu .ame-menu-fa-long-arrow-right .wp-menu-image:before { + content: "\f178"; +} + +#adminmenu#adminmenu .ame-menu-fa-apple .wp-menu-image:before { + content: "\f179"; +} + +#adminmenu#adminmenu .ame-menu-fa-windows .wp-menu-image:before { + content: "\f17a"; +} + +#adminmenu#adminmenu .ame-menu-fa-android .wp-menu-image:before { + content: "\f17b"; +} + +#adminmenu#adminmenu .ame-menu-fa-linux .wp-menu-image:before { + content: "\f17c"; +} + +#adminmenu#adminmenu .ame-menu-fa-dribbble .wp-menu-image:before { + content: "\f17d"; +} + +#adminmenu#adminmenu .ame-menu-fa-skype .wp-menu-image:before { + content: "\f17e"; +} + +#adminmenu#adminmenu .ame-menu-fa-foursquare .wp-menu-image:before { + content: "\f180"; +} + +#adminmenu#adminmenu .ame-menu-fa-trello .wp-menu-image:before { + content: "\f181"; +} + +#adminmenu#adminmenu .ame-menu-fa-female .wp-menu-image:before { + content: "\f182"; +} + +#adminmenu#adminmenu .ame-menu-fa-male .wp-menu-image:before { + content: "\f183"; +} + +#adminmenu#adminmenu .ame-menu-fa-gittip .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-gratipay .wp-menu-image:before { + content: "\f184"; +} + +#adminmenu#adminmenu .ame-menu-fa-sun-o .wp-menu-image:before { + content: "\f185"; +} + +#adminmenu#adminmenu .ame-menu-fa-moon-o .wp-menu-image:before { + content: "\f186"; +} + +#adminmenu#adminmenu .ame-menu-fa-archive .wp-menu-image:before { + content: "\f187"; +} + +#adminmenu#adminmenu .ame-menu-fa-bug .wp-menu-image:before { + content: "\f188"; +} + +#adminmenu#adminmenu .ame-menu-fa-vk .wp-menu-image:before { + content: "\f189"; +} + +#adminmenu#adminmenu .ame-menu-fa-weibo .wp-menu-image:before { + content: "\f18a"; +} + +#adminmenu#adminmenu .ame-menu-fa-renren .wp-menu-image:before { + content: "\f18b"; +} + +#adminmenu#adminmenu .ame-menu-fa-pagelines .wp-menu-image:before { + content: "\f18c"; +} + +#adminmenu#adminmenu .ame-menu-fa-stack-exchange .wp-menu-image:before { + content: "\f18d"; +} + +#adminmenu#adminmenu .ame-menu-fa-arrow-circle-o-right .wp-menu-image:before { + content: "\f18e"; +} + +#adminmenu#adminmenu .ame-menu-fa-arrow-circle-o-left .wp-menu-image:before { + content: "\f190"; +} + +#adminmenu#adminmenu .ame-menu-fa-toggle-left .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-caret-square-o-left .wp-menu-image:before { + content: "\f191"; +} + +#adminmenu#adminmenu .ame-menu-fa-dot-circle-o .wp-menu-image:before { + content: "\f192"; +} + +#adminmenu#adminmenu .ame-menu-fa-wheelchair .wp-menu-image:before { + content: "\f193"; +} + +#adminmenu#adminmenu .ame-menu-fa-vimeo-square .wp-menu-image:before { + content: "\f194"; +} + +#adminmenu#adminmenu .ame-menu-fa-turkish-lira .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-try .wp-menu-image:before { + content: "\f195"; +} + +#adminmenu#adminmenu .ame-menu-fa-plus-square-o .wp-menu-image:before { + content: "\f196"; +} + +#adminmenu#adminmenu .ame-menu-fa-space-shuttle .wp-menu-image:before { + content: "\f197"; +} + +#adminmenu#adminmenu .ame-menu-fa-slack .wp-menu-image:before { + content: "\f198"; +} + +#adminmenu#adminmenu .ame-menu-fa-envelope-square .wp-menu-image:before { + content: "\f199"; +} + +#adminmenu#adminmenu .ame-menu-fa-wordpress .wp-menu-image:before { + content: "\f19a"; +} + +#adminmenu#adminmenu .ame-menu-fa-openid .wp-menu-image:before { + content: "\f19b"; +} + +#adminmenu#adminmenu .ame-menu-fa-institution .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-bank .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-university .wp-menu-image:before { + content: "\f19c"; +} + +#adminmenu#adminmenu .ame-menu-fa-mortar-board .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-graduation-cap .wp-menu-image:before { + content: "\f19d"; +} + +#adminmenu#adminmenu .ame-menu-fa-yahoo .wp-menu-image:before { + content: "\f19e"; +} + +#adminmenu#adminmenu .ame-menu-fa-google .wp-menu-image:before { + content: "\f1a0"; +} + +#adminmenu#adminmenu .ame-menu-fa-reddit .wp-menu-image:before { + content: "\f1a1"; +} + +#adminmenu#adminmenu .ame-menu-fa-reddit-square .wp-menu-image:before { + content: "\f1a2"; +} + +#adminmenu#adminmenu .ame-menu-fa-stumbleupon-circle .wp-menu-image:before { + content: "\f1a3"; +} + +#adminmenu#adminmenu .ame-menu-fa-stumbleupon .wp-menu-image:before { + content: "\f1a4"; +} + +#adminmenu#adminmenu .ame-menu-fa-delicious .wp-menu-image:before { + content: "\f1a5"; +} + +#adminmenu#adminmenu .ame-menu-fa-digg .wp-menu-image:before { + content: "\f1a6"; +} + +#adminmenu#adminmenu .ame-menu-fa-pied-piper-pp .wp-menu-image:before { + content: "\f1a7"; +} + +#adminmenu#adminmenu .ame-menu-fa-pied-piper-alt .wp-menu-image:before { + content: "\f1a8"; +} + +#adminmenu#adminmenu .ame-menu-fa-drupal .wp-menu-image:before { + content: "\f1a9"; +} + +#adminmenu#adminmenu .ame-menu-fa-joomla .wp-menu-image:before { + content: "\f1aa"; +} + +#adminmenu#adminmenu .ame-menu-fa-language .wp-menu-image:before { + content: "\f1ab"; +} + +#adminmenu#adminmenu .ame-menu-fa-fax .wp-menu-image:before { + content: "\f1ac"; +} + +#adminmenu#adminmenu .ame-menu-fa-building .wp-menu-image:before { + content: "\f1ad"; +} + +#adminmenu#adminmenu .ame-menu-fa-child .wp-menu-image:before { + content: "\f1ae"; +} + +#adminmenu#adminmenu .ame-menu-fa-paw .wp-menu-image:before { + content: "\f1b0"; +} + +#adminmenu#adminmenu .ame-menu-fa-spoon .wp-menu-image:before { + content: "\f1b1"; +} + +#adminmenu#adminmenu .ame-menu-fa-cube .wp-menu-image:before { + content: "\f1b2"; +} + +#adminmenu#adminmenu .ame-menu-fa-cubes .wp-menu-image:before { + content: "\f1b3"; +} + +#adminmenu#adminmenu .ame-menu-fa-behance .wp-menu-image:before { + content: "\f1b4"; +} + +#adminmenu#adminmenu .ame-menu-fa-behance-square .wp-menu-image:before { + content: "\f1b5"; +} + +#adminmenu#adminmenu .ame-menu-fa-steam .wp-menu-image:before { + content: "\f1b6"; +} + +#adminmenu#adminmenu .ame-menu-fa-steam-square .wp-menu-image:before { + content: "\f1b7"; +} + +#adminmenu#adminmenu .ame-menu-fa-recycle .wp-menu-image:before { + content: "\f1b8"; +} + +#adminmenu#adminmenu .ame-menu-fa-automobile .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-car .wp-menu-image:before { + content: "\f1b9"; +} + +#adminmenu#adminmenu .ame-menu-fa-cab .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-taxi .wp-menu-image:before { + content: "\f1ba"; +} + +#adminmenu#adminmenu .ame-menu-fa-tree .wp-menu-image:before { + content: "\f1bb"; +} + +#adminmenu#adminmenu .ame-menu-fa-spotify .wp-menu-image:before { + content: "\f1bc"; +} + +#adminmenu#adminmenu .ame-menu-fa-deviantart .wp-menu-image:before { + content: "\f1bd"; +} + +#adminmenu#adminmenu .ame-menu-fa-soundcloud .wp-menu-image:before { + content: "\f1be"; +} + +#adminmenu#adminmenu .ame-menu-fa-database .wp-menu-image:before { + content: "\f1c0"; +} + +#adminmenu#adminmenu .ame-menu-fa-file-pdf-o .wp-menu-image:before { + content: "\f1c1"; +} + +#adminmenu#adminmenu .ame-menu-fa-file-word-o .wp-menu-image:before { + content: "\f1c2"; +} + +#adminmenu#adminmenu .ame-menu-fa-file-excel-o .wp-menu-image:before { + content: "\f1c3"; +} + +#adminmenu#adminmenu .ame-menu-fa-file-powerpoint-o .wp-menu-image:before { + content: "\f1c4"; +} + +#adminmenu#adminmenu .ame-menu-fa-file-photo-o .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-file-picture-o .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-file-image-o .wp-menu-image:before { + content: "\f1c5"; +} + +#adminmenu#adminmenu .ame-menu-fa-file-zip-o .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-file-archive-o .wp-menu-image:before { + content: "\f1c6"; +} + +#adminmenu#adminmenu .ame-menu-fa-file-sound-o .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-file-audio-o .wp-menu-image:before { + content: "\f1c7"; +} + +#adminmenu#adminmenu .ame-menu-fa-file-movie-o .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-file-video-o .wp-menu-image:before { + content: "\f1c8"; +} + +#adminmenu#adminmenu .ame-menu-fa-file-code-o .wp-menu-image:before { + content: "\f1c9"; +} + +#adminmenu#adminmenu .ame-menu-fa-vine .wp-menu-image:before { + content: "\f1ca"; +} + +#adminmenu#adminmenu .ame-menu-fa-codepen .wp-menu-image:before { + content: "\f1cb"; +} + +#adminmenu#adminmenu .ame-menu-fa-jsfiddle .wp-menu-image:before { + content: "\f1cc"; +} + +#adminmenu#adminmenu .ame-menu-fa-life-bouy .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-life-buoy .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-life-saver .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-support .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-life-ring .wp-menu-image:before { + content: "\f1cd"; +} + +#adminmenu#adminmenu .ame-menu-fa-circle-o-notch .wp-menu-image:before { + content: "\f1ce"; +} + +#adminmenu#adminmenu .ame-menu-fa-ra .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-resistance .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-rebel .wp-menu-image:before { + content: "\f1d0"; +} + +#adminmenu#adminmenu .ame-menu-fa-ge .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-empire .wp-menu-image:before { + content: "\f1d1"; +} + +#adminmenu#adminmenu .ame-menu-fa-git-square .wp-menu-image:before { + content: "\f1d2"; +} + +#adminmenu#adminmenu .ame-menu-fa-git .wp-menu-image:before { + content: "\f1d3"; +} + +#adminmenu#adminmenu .ame-menu-fa-y-combinator-square .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-yc-square .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-hacker-news .wp-menu-image:before { + content: "\f1d4"; +} + +#adminmenu#adminmenu .ame-menu-fa-tencent-weibo .wp-menu-image:before { + content: "\f1d5"; +} + +#adminmenu#adminmenu .ame-menu-fa-qq .wp-menu-image:before { + content: "\f1d6"; +} + +#adminmenu#adminmenu .ame-menu-fa-wechat .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-weixin .wp-menu-image:before { + content: "\f1d7"; +} + +#adminmenu#adminmenu .ame-menu-fa-send .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-paper-plane .wp-menu-image:before { + content: "\f1d8"; +} + +#adminmenu#adminmenu .ame-menu-fa-send-o .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-paper-plane-o .wp-menu-image:before { + content: "\f1d9"; +} + +#adminmenu#adminmenu .ame-menu-fa-history .wp-menu-image:before { + content: "\f1da"; +} + +#adminmenu#adminmenu .ame-menu-fa-circle-thin .wp-menu-image:before { + content: "\f1db"; +} + +#adminmenu#adminmenu .ame-menu-fa-header .wp-menu-image:before { + content: "\f1dc"; +} + +#adminmenu#adminmenu .ame-menu-fa-paragraph .wp-menu-image:before { + content: "\f1dd"; +} + +#adminmenu#adminmenu .ame-menu-fa-sliders .wp-menu-image:before { + content: "\f1de"; +} + +#adminmenu#adminmenu .ame-menu-fa-share-alt .wp-menu-image:before { + content: "\f1e0"; +} + +#adminmenu#adminmenu .ame-menu-fa-share-alt-square .wp-menu-image:before { + content: "\f1e1"; +} + +#adminmenu#adminmenu .ame-menu-fa-bomb .wp-menu-image:before { + content: "\f1e2"; +} + +#adminmenu#adminmenu .ame-menu-fa-soccer-ball-o .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-futbol-o .wp-menu-image:before { + content: "\f1e3"; +} + +#adminmenu#adminmenu .ame-menu-fa-tty .wp-menu-image:before { + content: "\f1e4"; +} + +#adminmenu#adminmenu .ame-menu-fa-binoculars .wp-menu-image:before { + content: "\f1e5"; +} + +#adminmenu#adminmenu .ame-menu-fa-plug .wp-menu-image:before { + content: "\f1e6"; +} + +#adminmenu#adminmenu .ame-menu-fa-slideshare .wp-menu-image:before { + content: "\f1e7"; +} + +#adminmenu#adminmenu .ame-menu-fa-twitch .wp-menu-image:before { + content: "\f1e8"; +} + +#adminmenu#adminmenu .ame-menu-fa-yelp .wp-menu-image:before { + content: "\f1e9"; +} + +#adminmenu#adminmenu .ame-menu-fa-newspaper-o .wp-menu-image:before { + content: "\f1ea"; +} + +#adminmenu#adminmenu .ame-menu-fa-wifi .wp-menu-image:before { + content: "\f1eb"; +} + +#adminmenu#adminmenu .ame-menu-fa-calculator .wp-menu-image:before { + content: "\f1ec"; +} + +#adminmenu#adminmenu .ame-menu-fa-paypal .wp-menu-image:before { + content: "\f1ed"; +} + +#adminmenu#adminmenu .ame-menu-fa-google-wallet .wp-menu-image:before { + content: "\f1ee"; +} + +#adminmenu#adminmenu .ame-menu-fa-cc-visa .wp-menu-image:before { + content: "\f1f0"; +} + +#adminmenu#adminmenu .ame-menu-fa-cc-mastercard .wp-menu-image:before { + content: "\f1f1"; +} + +#adminmenu#adminmenu .ame-menu-fa-cc-discover .wp-menu-image:before { + content: "\f1f2"; +} + +#adminmenu#adminmenu .ame-menu-fa-cc-amex .wp-menu-image:before { + content: "\f1f3"; +} + +#adminmenu#adminmenu .ame-menu-fa-cc-paypal .wp-menu-image:before { + content: "\f1f4"; +} + +#adminmenu#adminmenu .ame-menu-fa-cc-stripe .wp-menu-image:before { + content: "\f1f5"; +} + +#adminmenu#adminmenu .ame-menu-fa-bell-slash .wp-menu-image:before { + content: "\f1f6"; +} + +#adminmenu#adminmenu .ame-menu-fa-bell-slash-o .wp-menu-image:before { + content: "\f1f7"; +} + +#adminmenu#adminmenu .ame-menu-fa-trash .wp-menu-image:before { + content: "\f1f8"; +} + +#adminmenu#adminmenu .ame-menu-fa-copyright .wp-menu-image:before { + content: "\f1f9"; +} + +#adminmenu#adminmenu .ame-menu-fa-at .wp-menu-image:before { + content: "\f1fa"; +} + +#adminmenu#adminmenu .ame-menu-fa-eyedropper .wp-menu-image:before { + content: "\f1fb"; +} + +#adminmenu#adminmenu .ame-menu-fa-paint-brush .wp-menu-image:before { + content: "\f1fc"; +} + +#adminmenu#adminmenu .ame-menu-fa-birthday-cake .wp-menu-image:before { + content: "\f1fd"; +} + +#adminmenu#adminmenu .ame-menu-fa-area-chart .wp-menu-image:before { + content: "\f1fe"; +} + +#adminmenu#adminmenu .ame-menu-fa-pie-chart .wp-menu-image:before { + content: "\f200"; +} + +#adminmenu#adminmenu .ame-menu-fa-line-chart .wp-menu-image:before { + content: "\f201"; +} + +#adminmenu#adminmenu .ame-menu-fa-lastfm .wp-menu-image:before { + content: "\f202"; +} + +#adminmenu#adminmenu .ame-menu-fa-lastfm-square .wp-menu-image:before { + content: "\f203"; +} + +#adminmenu#adminmenu .ame-menu-fa-toggle-off .wp-menu-image:before { + content: "\f204"; +} + +#adminmenu#adminmenu .ame-menu-fa-toggle-on .wp-menu-image:before { + content: "\f205"; +} + +#adminmenu#adminmenu .ame-menu-fa-bicycle .wp-menu-image:before { + content: "\f206"; +} + +#adminmenu#adminmenu .ame-menu-fa-bus .wp-menu-image:before { + content: "\f207"; +} + +#adminmenu#adminmenu .ame-menu-fa-ioxhost .wp-menu-image:before { + content: "\f208"; +} + +#adminmenu#adminmenu .ame-menu-fa-angellist .wp-menu-image:before { + content: "\f209"; +} + +#adminmenu#adminmenu .ame-menu-fa-cc .wp-menu-image:before { + content: "\f20a"; +} + +#adminmenu#adminmenu .ame-menu-fa-shekel .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-sheqel .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-ils .wp-menu-image:before { + content: "\f20b"; +} + +#adminmenu#adminmenu .ame-menu-fa-meanpath .wp-menu-image:before { + content: "\f20c"; +} + +#adminmenu#adminmenu .ame-menu-fa-buysellads .wp-menu-image:before { + content: "\f20d"; +} + +#adminmenu#adminmenu .ame-menu-fa-connectdevelop .wp-menu-image:before { + content: "\f20e"; +} + +#adminmenu#adminmenu .ame-menu-fa-dashcube .wp-menu-image:before { + content: "\f210"; +} + +#adminmenu#adminmenu .ame-menu-fa-forumbee .wp-menu-image:before { + content: "\f211"; +} + +#adminmenu#adminmenu .ame-menu-fa-leanpub .wp-menu-image:before { + content: "\f212"; +} + +#adminmenu#adminmenu .ame-menu-fa-sellsy .wp-menu-image:before { + content: "\f213"; +} + +#adminmenu#adminmenu .ame-menu-fa-shirtsinbulk .wp-menu-image:before { + content: "\f214"; +} + +#adminmenu#adminmenu .ame-menu-fa-simplybuilt .wp-menu-image:before { + content: "\f215"; +} + +#adminmenu#adminmenu .ame-menu-fa-skyatlas .wp-menu-image:before { + content: "\f216"; +} + +#adminmenu#adminmenu .ame-menu-fa-cart-plus .wp-menu-image:before { + content: "\f217"; +} + +#adminmenu#adminmenu .ame-menu-fa-cart-arrow-down .wp-menu-image:before { + content: "\f218"; +} + +#adminmenu#adminmenu .ame-menu-fa-diamond .wp-menu-image:before { + content: "\f219"; +} + +#adminmenu#adminmenu .ame-menu-fa-ship .wp-menu-image:before { + content: "\f21a"; +} + +#adminmenu#adminmenu .ame-menu-fa-user-secret .wp-menu-image:before { + content: "\f21b"; +} + +#adminmenu#adminmenu .ame-menu-fa-motorcycle .wp-menu-image:before { + content: "\f21c"; +} + +#adminmenu#adminmenu .ame-menu-fa-street-view .wp-menu-image:before { + content: "\f21d"; +} + +#adminmenu#adminmenu .ame-menu-fa-heartbeat .wp-menu-image:before { + content: "\f21e"; +} + +#adminmenu#adminmenu .ame-menu-fa-venus .wp-menu-image:before { + content: "\f221"; +} + +#adminmenu#adminmenu .ame-menu-fa-mars .wp-menu-image:before { + content: "\f222"; +} + +#adminmenu#adminmenu .ame-menu-fa-mercury .wp-menu-image:before { + content: "\f223"; +} + +#adminmenu#adminmenu .ame-menu-fa-intersex .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-transgender .wp-menu-image:before { + content: "\f224"; +} + +#adminmenu#adminmenu .ame-menu-fa-transgender-alt .wp-menu-image:before { + content: "\f225"; +} + +#adminmenu#adminmenu .ame-menu-fa-venus-double .wp-menu-image:before { + content: "\f226"; +} + +#adminmenu#adminmenu .ame-menu-fa-mars-double .wp-menu-image:before { + content: "\f227"; +} + +#adminmenu#adminmenu .ame-menu-fa-venus-mars .wp-menu-image:before { + content: "\f228"; +} + +#adminmenu#adminmenu .ame-menu-fa-mars-stroke .wp-menu-image:before { + content: "\f229"; +} + +#adminmenu#adminmenu .ame-menu-fa-mars-stroke-v .wp-menu-image:before { + content: "\f22a"; +} + +#adminmenu#adminmenu .ame-menu-fa-mars-stroke-h .wp-menu-image:before { + content: "\f22b"; +} + +#adminmenu#adminmenu .ame-menu-fa-neuter .wp-menu-image:before { + content: "\f22c"; +} + +#adminmenu#adminmenu .ame-menu-fa-genderless .wp-menu-image:before { + content: "\f22d"; +} + +#adminmenu#adminmenu .ame-menu-fa-facebook-official .wp-menu-image:before { + content: "\f230"; +} + +#adminmenu#adminmenu .ame-menu-fa-pinterest-p .wp-menu-image:before { + content: "\f231"; +} + +#adminmenu#adminmenu .ame-menu-fa-whatsapp .wp-menu-image:before { + content: "\f232"; +} + +#adminmenu#adminmenu .ame-menu-fa-server .wp-menu-image:before { + content: "\f233"; +} + +#adminmenu#adminmenu .ame-menu-fa-user-plus .wp-menu-image:before { + content: "\f234"; +} + +#adminmenu#adminmenu .ame-menu-fa-user-times .wp-menu-image:before { + content: "\f235"; +} + +#adminmenu#adminmenu .ame-menu-fa-hotel .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-bed .wp-menu-image:before { + content: "\f236"; +} + +#adminmenu#adminmenu .ame-menu-fa-viacoin .wp-menu-image:before { + content: "\f237"; +} + +#adminmenu#adminmenu .ame-menu-fa-train .wp-menu-image:before { + content: "\f238"; +} + +#adminmenu#adminmenu .ame-menu-fa-subway .wp-menu-image:before { + content: "\f239"; +} + +#adminmenu#adminmenu .ame-menu-fa-medium .wp-menu-image:before { + content: "\f23a"; +} + +#adminmenu#adminmenu .ame-menu-fa-yc .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-y-combinator .wp-menu-image:before { + content: "\f23b"; +} + +#adminmenu#adminmenu .ame-menu-fa-optin-monster .wp-menu-image:before { + content: "\f23c"; +} + +#adminmenu#adminmenu .ame-menu-fa-opencart .wp-menu-image:before { + content: "\f23d"; +} + +#adminmenu#adminmenu .ame-menu-fa-expeditedssl .wp-menu-image:before { + content: "\f23e"; +} + +#adminmenu#adminmenu .ame-menu-fa-battery-4 .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-battery-full .wp-menu-image:before { + content: "\f240"; +} + +#adminmenu#adminmenu .ame-menu-fa-battery-3 .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-battery-three-quarters .wp-menu-image:before { + content: "\f241"; +} + +#adminmenu#adminmenu .ame-menu-fa-battery-2 .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-battery-half .wp-menu-image:before { + content: "\f242"; +} + +#adminmenu#adminmenu .ame-menu-fa-battery-1 .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-battery-quarter .wp-menu-image:before { + content: "\f243"; +} + +#adminmenu#adminmenu .ame-menu-fa-battery-0 .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-battery-empty .wp-menu-image:before { + content: "\f244"; +} + +#adminmenu#adminmenu .ame-menu-fa-mouse-pointer .wp-menu-image:before { + content: "\f245"; +} + +#adminmenu#adminmenu .ame-menu-fa-i-cursor .wp-menu-image:before { + content: "\f246"; +} + +#adminmenu#adminmenu .ame-menu-fa-object-group .wp-menu-image:before { + content: "\f247"; +} + +#adminmenu#adminmenu .ame-menu-fa-object-ungroup .wp-menu-image:before { + content: "\f248"; +} + +#adminmenu#adminmenu .ame-menu-fa-sticky-note .wp-menu-image:before { + content: "\f249"; +} + +#adminmenu#adminmenu .ame-menu-fa-sticky-note-o .wp-menu-image:before { + content: "\f24a"; +} + +#adminmenu#adminmenu .ame-menu-fa-cc-jcb .wp-menu-image:before { + content: "\f24b"; +} + +#adminmenu#adminmenu .ame-menu-fa-cc-diners-club .wp-menu-image:before { + content: "\f24c"; +} + +#adminmenu#adminmenu .ame-menu-fa-clone .wp-menu-image:before { + content: "\f24d"; +} + +#adminmenu#adminmenu .ame-menu-fa-balance-scale .wp-menu-image:before { + content: "\f24e"; +} + +#adminmenu#adminmenu .ame-menu-fa-hourglass-o .wp-menu-image:before { + content: "\f250"; +} + +#adminmenu#adminmenu .ame-menu-fa-hourglass-1 .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-hourglass-start .wp-menu-image:before { + content: "\f251"; +} + +#adminmenu#adminmenu .ame-menu-fa-hourglass-2 .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-hourglass-half .wp-menu-image:before { + content: "\f252"; +} + +#adminmenu#adminmenu .ame-menu-fa-hourglass-3 .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-hourglass-end .wp-menu-image:before { + content: "\f253"; +} + +#adminmenu#adminmenu .ame-menu-fa-hourglass .wp-menu-image:before { + content: "\f254"; +} + +#adminmenu#adminmenu .ame-menu-fa-hand-grab-o .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-hand-rock-o .wp-menu-image:before { + content: "\f255"; +} + +#adminmenu#adminmenu .ame-menu-fa-hand-stop-o .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-hand-paper-o .wp-menu-image:before { + content: "\f256"; +} + +#adminmenu#adminmenu .ame-menu-fa-hand-scissors-o .wp-menu-image:before { + content: "\f257"; +} + +#adminmenu#adminmenu .ame-menu-fa-hand-lizard-o .wp-menu-image:before { + content: "\f258"; +} + +#adminmenu#adminmenu .ame-menu-fa-hand-spock-o .wp-menu-image:before { + content: "\f259"; +} + +#adminmenu#adminmenu .ame-menu-fa-hand-pointer-o .wp-menu-image:before { + content: "\f25a"; +} + +#adminmenu#adminmenu .ame-menu-fa-hand-peace-o .wp-menu-image:before { + content: "\f25b"; +} + +#adminmenu#adminmenu .ame-menu-fa-trademark .wp-menu-image:before { + content: "\f25c"; +} + +#adminmenu#adminmenu .ame-menu-fa-registered .wp-menu-image:before { + content: "\f25d"; +} + +#adminmenu#adminmenu .ame-menu-fa-creative-commons .wp-menu-image:before { + content: "\f25e"; +} + +#adminmenu#adminmenu .ame-menu-fa-gg .wp-menu-image:before { + content: "\f260"; +} + +#adminmenu#adminmenu .ame-menu-fa-gg-circle .wp-menu-image:before { + content: "\f261"; +} + +#adminmenu#adminmenu .ame-menu-fa-tripadvisor .wp-menu-image:before { + content: "\f262"; +} + +#adminmenu#adminmenu .ame-menu-fa-odnoklassniki .wp-menu-image:before { + content: "\f263"; +} + +#adminmenu#adminmenu .ame-menu-fa-odnoklassniki-square .wp-menu-image:before { + content: "\f264"; +} + +#adminmenu#adminmenu .ame-menu-fa-get-pocket .wp-menu-image:before { + content: "\f265"; +} + +#adminmenu#adminmenu .ame-menu-fa-wikipedia-w .wp-menu-image:before { + content: "\f266"; +} + +#adminmenu#adminmenu .ame-menu-fa-safari .wp-menu-image:before { + content: "\f267"; +} + +#adminmenu#adminmenu .ame-menu-fa-chrome .wp-menu-image:before { + content: "\f268"; +} + +#adminmenu#adminmenu .ame-menu-fa-firefox .wp-menu-image:before { + content: "\f269"; +} + +#adminmenu#adminmenu .ame-menu-fa-opera .wp-menu-image:before { + content: "\f26a"; +} + +#adminmenu#adminmenu .ame-menu-fa-internet-explorer .wp-menu-image:before { + content: "\f26b"; +} + +#adminmenu#adminmenu .ame-menu-fa-tv .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-television .wp-menu-image:before { + content: "\f26c"; +} + +#adminmenu#adminmenu .ame-menu-fa-contao .wp-menu-image:before { + content: "\f26d"; +} + +#adminmenu#adminmenu .ame-menu-fa-500px .wp-menu-image:before { + content: "\f26e"; +} + +#adminmenu#adminmenu .ame-menu-fa-amazon .wp-menu-image:before { + content: "\f270"; +} + +#adminmenu#adminmenu .ame-menu-fa-calendar-plus-o .wp-menu-image:before { + content: "\f271"; +} + +#adminmenu#adminmenu .ame-menu-fa-calendar-minus-o .wp-menu-image:before { + content: "\f272"; +} + +#adminmenu#adminmenu .ame-menu-fa-calendar-times-o .wp-menu-image:before { + content: "\f273"; +} + +#adminmenu#adminmenu .ame-menu-fa-calendar-check-o .wp-menu-image:before { + content: "\f274"; +} + +#adminmenu#adminmenu .ame-menu-fa-industry .wp-menu-image:before { + content: "\f275"; +} + +#adminmenu#adminmenu .ame-menu-fa-map-pin .wp-menu-image:before { + content: "\f276"; +} + +#adminmenu#adminmenu .ame-menu-fa-map-signs .wp-menu-image:before { + content: "\f277"; +} + +#adminmenu#adminmenu .ame-menu-fa-map-o .wp-menu-image:before { + content: "\f278"; +} + +#adminmenu#adminmenu .ame-menu-fa-map .wp-menu-image:before { + content: "\f279"; +} + +#adminmenu#adminmenu .ame-menu-fa-commenting .wp-menu-image:before { + content: "\f27a"; +} + +#adminmenu#adminmenu .ame-menu-fa-commenting-o .wp-menu-image:before { + content: "\f27b"; +} + +#adminmenu#adminmenu .ame-menu-fa-houzz .wp-menu-image:before { + content: "\f27c"; +} + +#adminmenu#adminmenu .ame-menu-fa-vimeo .wp-menu-image:before { + content: "\f27d"; +} + +#adminmenu#adminmenu .ame-menu-fa-black-tie .wp-menu-image:before { + content: "\f27e"; +} + +#adminmenu#adminmenu .ame-menu-fa-fonticons .wp-menu-image:before { + content: "\f280"; +} + +#adminmenu#adminmenu .ame-menu-fa-reddit-alien .wp-menu-image:before { + content: "\f281"; +} + +#adminmenu#adminmenu .ame-menu-fa-edge .wp-menu-image:before { + content: "\f282"; +} + +#adminmenu#adminmenu .ame-menu-fa-credit-card-alt .wp-menu-image:before { + content: "\f283"; +} + +#adminmenu#adminmenu .ame-menu-fa-codiepie .wp-menu-image:before { + content: "\f284"; +} + +#adminmenu#adminmenu .ame-menu-fa-modx .wp-menu-image:before { + content: "\f285"; +} + +#adminmenu#adminmenu .ame-menu-fa-fort-awesome .wp-menu-image:before { + content: "\f286"; +} + +#adminmenu#adminmenu .ame-menu-fa-usb .wp-menu-image:before { + content: "\f287"; +} + +#adminmenu#adminmenu .ame-menu-fa-product-hunt .wp-menu-image:before { + content: "\f288"; +} + +#adminmenu#adminmenu .ame-menu-fa-mixcloud .wp-menu-image:before { + content: "\f289"; +} + +#adminmenu#adminmenu .ame-menu-fa-scribd .wp-menu-image:before { + content: "\f28a"; +} + +#adminmenu#adminmenu .ame-menu-fa-pause-circle .wp-menu-image:before { + content: "\f28b"; +} + +#adminmenu#adminmenu .ame-menu-fa-pause-circle-o .wp-menu-image:before { + content: "\f28c"; +} + +#adminmenu#adminmenu .ame-menu-fa-stop-circle .wp-menu-image:before { + content: "\f28d"; +} + +#adminmenu#adminmenu .ame-menu-fa-stop-circle-o .wp-menu-image:before { + content: "\f28e"; +} + +#adminmenu#adminmenu .ame-menu-fa-shopping-bag .wp-menu-image:before { + content: "\f290"; +} + +#adminmenu#adminmenu .ame-menu-fa-shopping-basket .wp-menu-image:before { + content: "\f291"; +} + +#adminmenu#adminmenu .ame-menu-fa-hashtag .wp-menu-image:before { + content: "\f292"; +} + +#adminmenu#adminmenu .ame-menu-fa-bluetooth .wp-menu-image:before { + content: "\f293"; +} + +#adminmenu#adminmenu .ame-menu-fa-bluetooth-b .wp-menu-image:before { + content: "\f294"; +} + +#adminmenu#adminmenu .ame-menu-fa-percent .wp-menu-image:before { + content: "\f295"; +} + +#adminmenu#adminmenu .ame-menu-fa-gitlab .wp-menu-image:before { + content: "\f296"; +} + +#adminmenu#adminmenu .ame-menu-fa-wpbeginner .wp-menu-image:before { + content: "\f297"; +} + +#adminmenu#adminmenu .ame-menu-fa-wpforms .wp-menu-image:before { + content: "\f298"; +} + +#adminmenu#adminmenu .ame-menu-fa-envira .wp-menu-image:before { + content: "\f299"; +} + +#adminmenu#adminmenu .ame-menu-fa-universal-access .wp-menu-image:before { + content: "\f29a"; +} + +#adminmenu#adminmenu .ame-menu-fa-wheelchair-alt .wp-menu-image:before { + content: "\f29b"; +} + +#adminmenu#adminmenu .ame-menu-fa-question-circle-o .wp-menu-image:before { + content: "\f29c"; +} + +#adminmenu#adminmenu .ame-menu-fa-blind .wp-menu-image:before { + content: "\f29d"; +} + +#adminmenu#adminmenu .ame-menu-fa-audio-description .wp-menu-image:before { + content: "\f29e"; +} + +#adminmenu#adminmenu .ame-menu-fa-volume-control-phone .wp-menu-image:before { + content: "\f2a0"; +} + +#adminmenu#adminmenu .ame-menu-fa-braille .wp-menu-image:before { + content: "\f2a1"; +} + +#adminmenu#adminmenu .ame-menu-fa-assistive-listening-systems .wp-menu-image:before { + content: "\f2a2"; +} + +#adminmenu#adminmenu .ame-menu-fa-asl-interpreting .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-american-sign-language-interpreting .wp-menu-image:before { + content: "\f2a3"; +} + +#adminmenu#adminmenu .ame-menu-fa-deafness .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-hard-of-hearing .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-deaf .wp-menu-image:before { + content: "\f2a4"; +} + +#adminmenu#adminmenu .ame-menu-fa-glide .wp-menu-image:before { + content: "\f2a5"; +} + +#adminmenu#adminmenu .ame-menu-fa-glide-g .wp-menu-image:before { + content: "\f2a6"; +} + +#adminmenu#adminmenu .ame-menu-fa-signing .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-sign-language .wp-menu-image:before { + content: "\f2a7"; +} + +#adminmenu#adminmenu .ame-menu-fa-low-vision .wp-menu-image:before { + content: "\f2a8"; +} + +#adminmenu#adminmenu .ame-menu-fa-viadeo .wp-menu-image:before { + content: "\f2a9"; +} + +#adminmenu#adminmenu .ame-menu-fa-viadeo-square .wp-menu-image:before { + content: "\f2aa"; +} + +#adminmenu#adminmenu .ame-menu-fa-snapchat .wp-menu-image:before { + content: "\f2ab"; +} + +#adminmenu#adminmenu .ame-menu-fa-snapchat-ghost .wp-menu-image:before { + content: "\f2ac"; +} + +#adminmenu#adminmenu .ame-menu-fa-snapchat-square .wp-menu-image:before { + content: "\f2ad"; +} + +#adminmenu#adminmenu .ame-menu-fa-pied-piper .wp-menu-image:before { + content: "\f2ae"; +} + +#adminmenu#adminmenu .ame-menu-fa-first-order .wp-menu-image:before { + content: "\f2b0"; +} + +#adminmenu#adminmenu .ame-menu-fa-yoast .wp-menu-image:before { + content: "\f2b1"; +} + +#adminmenu#adminmenu .ame-menu-fa-themeisle .wp-menu-image:before { + content: "\f2b2"; +} + +#adminmenu#adminmenu .ame-menu-fa-google-plus-circle .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-google-plus-official .wp-menu-image:before { + content: "\f2b3"; +} + +#adminmenu#adminmenu .ame-menu-fa-fa .wp-menu-image:before, +#adminmenu#adminmenu .ame-menu-fa-font-awesome .wp-menu-image:before { + content: "\f2b4"; +} + +#adminmenu .ame-menu-fa .wp-menu-image:before { + font-family: "FontAwesome", sans-serif !important; + font-size: 18px !important; +} + +#adminmenu#adminmenu#adminmenu .ame-menu-fa .wp-menu-image { + background-image: none; +} + +#adminmenu .ame-submenu-icon .ame-fa { + font-size: 18px; +} + +#ws_icon_selector .ws_icon_option .ws_icon_image.ame-fa { + width: 26px; + height: 20px; + padding: 6px 2px 4px 2px; + text-align: center; + font-size: 18px; +} +#ws_icon_selector .ws_icon_option .ws_icon_image.ame-fa:before { + color: #85888c; +} + +.ws_select_icon .ws_icon_image.ame-fa { + padding: 2px 2px 3px 2px; +} +.ws_select_icon .ws_icon_image.ame-fa:before { + font-family: "FontAwesome", sans-serif; + font-size: 18px; + margin-top: 2px; + display: inline-block; + min-width: 20px; +} + +/*# sourceMappingURL=font-awesome.css.map */ diff --git a/extras/font-awesome/scss/font-awesome.css.map b/extras/font-awesome/scss/font-awesome.css.map new file mode 100644 index 0000000..c769085 --- /dev/null +++ b/extras/font-awesome/scss/font-awesome.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["font-awesome.scss","_path.scss","_core.scss","_larger.scss","_fixed-width.scss","_list.scss","_variables.scss","_bordered-pulled.scss","_animated.scss","_rotated-flipped.scss","_mixins.scss","_stacked.scss","_icons.scss","_screen-reader.scss","_ame-icons.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA;ACAA;AAAA;AAGA;EACE;EACA;EACA;EAMA;EACA;;ACVF;EACE;EACA;EACA;EACA;EACA;EACA;;;ACNF;AACA;EACE;EACA;EACA;;;AAEF;EAAwB;;;AACxB;EAAwB;;;AACxB;EAAwB;;;AACxB;EAAwB;;;ACVxB;EACE;EACA;;;ACDF;EACE;EACA,aCMoB;EDLpB;;AACA;EAAO;;;AAET;EACE;EACA;EACA,OCDoB;EDEpB;EACA;;AACA;EACE;;;AEbJ;EACE;EACA;EACA;;;AAGF;EAA+B;;;AAC/B;EAAgC;;;AAG9B;EAAgC;;AAChC;EAAiC;;;AAGnC;AACA;EAAc;;;AACd;EAAa;;;AAGX;EAAc;;AACd;EAAe;;;ACpBjB;EACE;EACQ;;;AAGV;EACE;EACQ;;;AAGV;EACE;IACE;IACQ;;EAEV;IACE;IACQ;;;AAIZ;EACE;IACE;IACQ;;EAEV;IACE;IACQ;;;AC5BZ;ECWE;EACA;EACI;EACI;;;ADbV;ECUE;EACA;EACI;EACI;;;ADZV;ECSE;EACA;EACI;EACI;;;ADVV;ECcE;EACA;EACI;EACI;;;ADhBV;ECaE;EACA;EACI;EACI;;;ADXV;AAAA;AAAA;AAAA;AAAA;EAKE;;;AEfF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EAA8B;;;AAC9B;EAA8B;;;AAC9B;EAA6B,OLTP;;;AMVtB;AAAA;AAGA;EAAkC,SNyTnB;;;AMxTf;EAAkC,SNmcnB;;;AMlcf;EAAmC,SN8hBnB;;;AM7hBhB;EAAuC,SN2NnB;;;AM1NpB;EAAkC,SNsVnB;;;AMrVf;EAAiC,SNolBnB;;;AMnlBd;EAAmC,SNwlBnB;;;AMvlBhB;EAAiC,SN4qBnB;;;AM3qBd;EAAiC,SNqQnB;;;AMpQd;EAAqC,SNunBnB;;;AMtnBlB;EAA+B,SNqnBnB;;;AMpnBZ;EAAoC,SNsnBnB;;;AMrnBjB;EAAkC,SNiInB;;;AMhIf;AAAA;AAAA;EAEkC,SN0nBnB;;;AMznBf;EAAwC,SNkhBnB;;;AMjhBrB;EAAyC,SNghBnB;;;AM/gBtB;EAAsC,SNmenB;;;AMlenB;EAAmC,SNoiBnB;;;AMniBhB;AAAA;EACgC,SNwJnB;;;AMvJb;EAAoC,SNkoBnB;;;AMjoBjB;EAAiC,SNuUnB;;;AMtUd;EAAmC,SNyOnB;;;AMxOhB;EAAoC,SNwInB;;;AMvIjB;EAAiC,SNwfnB;;;AMvfd;EAAqC,SNwLnB;;;AMvLlB;EAAgD,SNQnB;;;AMP7B;EAA8C,SNUnB;;;AMT3B;EAAkC,SNgVnB;;;AM/Uf;EAA0C,SNgdnB;;;AM/cvB;AAAA;EACmC,SN2enB;;;AM1ehB;EAAoC,SNqenB;;;AMpejB;EAAqC,SNkXnB;;;AMjXlB;EAAiC,SNqXnB;;;AMpXd;EAAiC,SN8OnB;;;AM7Od;EAAuC,SNmTnB;;;AMlTpB;EAAuC,SN+pBnB;;;AM9pBpB;EAAwC,SN6pBnB;;;AM5pBrB;EAAsC,SN8pBnB;;;AM7pBnB;EAAmC,SNgdnB;;;AM/chB;EAAoC,SNmBnB;;;AMlBjB;EAAgC,SN0kBnB;;;AMzkBb;EAAiC,SN0kBnB;;;AMzkBd;EAAiC,SNiDnB;;;AMhDd;EAAqC,SNiDnB;;;AMhDlB;EAAkC,SNscnB;;;AMrcf;EAAmC,SNmEnB;;;AMlEhB;EAAiC,SN4OnB;;;AM3Od;EAAiC,SNyCnB;;;AMxCd;EAAmC,SNqUnB;;;AMpUhB;EAAwC,SNwkBnB;;;AMvkBrB;EAAuC,SNwkBnB;;;AMvkBpB;EAAuC,SNxCnB;;;AMyCpB;EAAyC,SN3CnB;;;AM4CtB;EAAwC,SNzCnB;;;AM0CrB;EAA0C,SN5CnB;;;AM6CvB;EAAiC,SNyVnB;;;AMxVd;AAAA;EACoC,SNmZnB;;;AMlZjB;EAAmC,SNiTnB;;;AMhThB;EAAyC,SNgoBnB;;;AM/nBtB;AAAA;AAAA;EAEsC,SNkanB;;;AMjanB;EAAmC,SN0ZnB;;;AMzZhB;EAAuC,SNkWnB;;;AMjWpB;EAAmC,SN1DnB;;;AM2DhB;EAAiC,SNskBnB;;;AMrkBd;AAAA;EAC4C,SNuZnB;;;AMtZzB;EAA2C,SNkenB;;;AMjexB;EAA2C,SNuEnB;;;AMtExB;EAAmC,SN7BnB;;;AM8BhB;EAA0C,SNqhBnB;;;AMphBvB;EAA0C,SNuKnB;;;AMtKvB;EAAqC,SNtBnB;;;AMuBlB;EAAiC,SN8ZnB;;;AM7Zd;EAAkC,SNwYnB;;;AMvYf;EAAiC,SNqhBnB;;;AMphBd;EAAoC,SNiNnB;;;AMhNjB;EAAyC,SNkKnB;;;AMjKtB;EAAyC,SN8gBnB;;;AM7gBtB;EAAkC,SNoInB;;;AMnIf;EAAyC,SNiEnB;;;AMhEtB;EAA0C,SNiEnB;;;AMhEvB;EAAwC,SN0ZnB;;;AMzZrB;EAAyC,SN+VnB;;;AM9VtB;EAAyC,SNgjBnB;;;AM/iBtB;EAAyC,SNmDnB;;;AMlDtB;EAA4C,SNganB;;;AM/ZzB;EAAwC,SNqRnB;;;AMpRrB;EAAuC,SNkGnB;;;AMjGpB;EAA2C,SN4iBnB;;;AM3iBxB;EAA2C,SN+CnB;;;AM9CxB;EAAgC,SNvCnB;;;AMwCb;EAAuC,SNvDnB;;;AMwDpB;EAAwC,SNvDnB;;;AMwDrB;EAAqC,SNvDnB;;;AMwDlB;EAAuC,SN3DnB;;;AM4DpB;AAAA;EACkC,SNgcnB;;;AM/bf;EAAmC,SNgInB;;;AM/HhB;EAAqC,SN8EnB;;;AM7ElB;EAAiC,SNuYnB;;;AMtYd;EAAkC,SN4UnB;;;AM3Uf;EAAqC,SNxDnB;;;AMyDlB;EAA+C,SNyHnB;;;AMxH5B;EAAiC,SNmMnB;;;AMlMd;EAAiC,SNqRnB;;;AMpRd;EAAiC,SN+JnB;;;AM9Jd;EAAgC,SN2HnB;;;AM1Hb;EAAsC,SN2HnB;;;AM1HnB;AAAA;EACiD,SNmHnB;;;AMlH9B;EAAkC,SNuXnB;;;AMtXf;EAAqC,SNRnB;;;AMSlB;EAAmC,SN0YnB;;;AMzYhB;EAAoC,SNwDnB;;;AMvDjB;EAAmC,SNuSnB;;;AMtShB;EAAuC,SN+BnB;;;AM9BpB;EAAyC,SN2BnB;;;AM1BtB;EAAoC,SNoZnB;;;AMnZjB;EAA0C,SNsbnB;;;AMrbvB;EAAmC,SN4JnB;;;AM3JhB;EAAwC,SN6JnB;;;AM5JrB;EAAqC,SN/EnB;;;AMgFlB;EAAqC,SNjFnB;;;AMkFlB;AAAA;EACsC,SNvEnB;;;AMwEnB;EAA2C,SNkiBnB;;;AMjiBxB;EAA4C,SN8GnB;;;AM7GzB;EAAyC,SNjBnB;;;AMkBtB;EAAgC,SNsPnB;;;AMrPb;AAAA;EACiC,SNqCnB;;;AMpCd;EAAqC,SN0CnB;;;AMzClB;EAAwC,SN2fnB;;;AM1frB;EAA0C,SNyfnB;;;AMxfvB;EAAsC,SN6cnB;;;AM5cnB;EAAoC,SN6MnB;;;AM5MjB;EAAqC,SNuanB;;;AMtalB;EAA4C,SNkQnB;;;AMjQzB;EAAuC,SNkfnB;;;AMjfpB;EAA0C,SNwFnB;;;AMvFvB;EAAoC,SNianB;;;AMhajB;EAAmC,SNwgBnB;;;AMvgBhB;EAA0C,SNiKnB;;;AMhKvB;EAAmC,SN0hBnB;;;AMzhBhB;EAAoC,SNgPnB;;;AM/OjB;EAAkC,SNyUnB;;;AMxUf;EAAqC,SN6bnB;;;AM5blB;EAAuC,SNzDnB;;;AM0DpB;EAAyC,SNuUnB;;;AMtUtB;EAAoC,SNygBnB;;;AMxgBjB;AAAA;EACqC,SNkFnB;;;AMjFlB;EAAmC,SNqJnB;;;AMpJhB;EAAmC,SN6gBnB;;;AM5gBhB;EAAwC,SN4BnB;;;AM3BrB;AAAA;EACgC,SNoXnB;;;AMnXb;EAAkC,SNmLnB;;;AMlLf;EAAqC,SN7DnB;;;AM8DlB;EAAiC,SNxFnB;;;AMyFd;EAAwC,SN7BnB;;;AM8BrB;EAAyC,SNoKnB;;;AMnKtB;EAAwC,SNkKnB;;;AMjKrB;EAAsC,SNmKnB;;;AMlKnB;EAAwC,SN+JnB;;;AM9JrB;EAA8C,SNzInB;;;AM0I3B;EAA+C,SNrInB;;;AMsI5B;EAA4C,SNrInB;;;AMsIzB;EAA8C,SN7InB;;;AM8I3B;EAAkC,SN4InB;;;AM3If;EAAmC,SNqiBnB;;;AMpiBhB;EAAkC,SNscnB;;;AMrcf;EAAmC,SN2FnB;;;AM1FhB;EAAsC,SNjFnB;;;AMkFnB;EAAuC,SNtInB;;;AMuIpB;AAAA;EACkC,SNigBnB;;;AMhgBf;AAAA;EACiC,SNuNnB;;;AMtNd;EAAkC,SNtBnB;;;AMuBf;EAAkC,SN4FnB;;;AM3Ff;AAAA;EACqC,SNiWnB;;;AMhWlB;AAAA;EACoC,SN6EnB;;;AM5EjB;EAAsC,SNqRnB;;;AMpRnB;AAAA;EACqC,SNuFnB;;;AMtFlB;EAAmC,SNkZnB;;;AMjZhB;AAAA;AAAA;EAEiC,SNvInB;;;AMwId;EAAoC,SNgNnB;;;AM/MjB;EAAoC,SN8MnB;;;AM7MjB;EAA0C,SNianB;;;AMhavB;EAAsC,SN8dnB;;;AM7dnB;EAAkC,SNwanB;;;AMvaf;EAAkC,SNmNnB;;;AMlNf;EAAkC,SNgdnB;;;AM/cf;EAAsC,SN2RnB;;;AM1RnB;EAA6C,SN4RnB;;;AM3R1B;EAA+C,SNiHnB;;;AMhH5B;EAAwC,SN6GnB;;;AM5GrB;EAAkC,SN6OnB;;;AM5Of;EAAuC,SN5FnB;;;AM6FpB;EAAqC,SNtFnB;;;AMuFlB;EAAuC,SN7FnB;;;AM8FpB;EAAwC,SN7FnB;;;AM8FrB;EAAoC,SNxCnB;;;AMyCjB;AAAA;EACiC,SN4WnB;;;AM3Wd;AAAA;EACsC,SNgXnB;;;AM/WnB;AAAA;EACqC,SN6WnB;;;AM5WlB;EAAqC,SNDnB;;;AMElB;EAAqC,SNkLnB;;;AMjLlB;AAAA;EACiC,SNwcnB;;;AMvcd;AAAA;EACkC,SNqEnB;;;AMpEf;AAAA;EACuC,SNgZnB;;;AM/YpB;EAAsC,SNrDnB;;;AMsDnB;EAAuC,SNlDnB;;;AMmDpB;AAAA;EACiC,SN5InB;;;AM6Id;EAAoC,SNgVnB;;;AM/UjB;EAAqC,SN4bnB;;;AM3blB;AAAA;EACsC,SN7EnB;;;AM8EnB;EAAwC,SNgKnB;;;AM/JrB;EAAqC,SNXnB;;;AMYlB;EAA2C,SN3EnB;;;AM4ExB;EAAyC,SN3EnB;;;AM4EtB;EAAoC,SNkcnB;;;AMjcjB;EAAwC,SNgXnB;;;AM/WrB;EAAqC,SN2XnB;;;AM1XlB;EAAmC,SNtKnB;;;AMuKhB;EAAmC,SN1EnB;;;AM2EhB;EAAoC,SNlDnB;;;AMmDjB;EAAwC,SNgBnB;;;AMfrB;EAAuC,SNlJnB;;;AMmJpB;EAAuC,SNqGnB;;;AMpGpB;EAAsC,SNzOnB;;;AM0OnB;EAAmC,SNsLnB;;;AMrLhB;EAAwC,SNJnB;;;AMKrB;EAAiC,SNnLnB;;;AMoLd;EAAqC,SNuEnB;;;AMtElB;EAAwC,SNmPnB;;;AMlPrB;EAA8C,SNzOnB;;;AM0O3B;EAA+C,SNzOnB;;;AM0O5B;EAA4C,SNzOnB;;;AM0OzB;EAA8C,SN7OnB;;;AM8O3B;EAAuC,SNzOnB;;;AM0OpB;EAAwC,SNzOnB;;;AM0OrB;EAAqC,SNzOnB;;;AM0OlB;EAAuC,SN7OnB;;;AM8OpB;EAAoC,SN5DnB;;;AM6DjB;EAAmC,SNuHnB;;;AMtHhB;EAAmC,SNyWnB;;;AMxWhB;AAAA;EACmC,SN+KnB;;;AM9KhB;EAAqC,SNjHnB;;;AMkHlB;EAAuC,SN+OnB;;;AM9OpB;EAAwC,SN+OnB;;;AM9OrB;EAAoC,SNiUnB;;;AMhUjB;EAAmC,SNtHnB;;;AMuHhB;AAAA;EACkC,SNyPnB;;;AMxPf;EAAuC,SNgCnB;;;AM/BpB;EAAqC,SNKnB;;;AMJlB;EAA0C,SNMnB;;;AMLvB;EAAoC,SNwSnB;;;AMvSjB;EAAoC,SNYnB;;;AMXjB;EAAkC,SNyJnB;;;AMxJf;EAAoC,SNYnB;;;AMXjB;EAAuC,SNkGnB;;;AMjGpB;EAAmC,SNRnB;;;AMShB;EAA2C,SNVnB;;;AMWxB;EAAqC,SN6VnB;;;AM5VlB;EAAiC,SNxHnB;;;AMyHd;AAAA;EACsC,SN4OnB;;;AM3OnB;AAAA;AAAA;EAEwC,SNwTnB;;;AMvTrB;EAA2C,SNkHnB;;;AMjHxB;EAAiC,SNxGnB;;;AMyGd;EAAsC,SN/HnB;;;AMgInB;AAAA;EACyC,SN9JnB;;;AM+JtB;EAAqC,SNgNnB;;;AM/MlB;EAAiC,SNqEnB;;;AMpEd;EAAwC,SNxEnB;;;AMyErB;EAAwC,SNmUnB;;;AMlUrB;EAAsC,SN8TnB;;;AM7TnB;EAAmC,SN/EnB;;;AMgFhB;EAAyC,SNuMnB;;;AMtMtB;EAAuC,SNmInB;;;AMlIpB;EAA6C,SNmInB;;;AMlI1B;EAAmC,SN0PnB;;;AMzPhB;EAAuC,SNpMnB;;;AMqMpB;EAA8C,SNtCnB;;;AMuC3B;EAAmC,SN4NnB;;;AM3NhB;EAAmC,SNuHnB;;;AMtHhB;EAAgD,SNtKnB;;;AMuK7B;EAAiD,SNtKnB;;;AMuK9B;EAA8C,SNtKnB;;;AMuK3B;EAAgD,SN1KnB;;;AM2K7B;EAAkC,SN6CnB;;;AM5Cf;EAAiC,SN7HnB;;;AM8Hd;EAAmC,SN3SnB;;;AM4ShB;EAAuC,SN8WnB;;;AM7WpB;EAAqC,SNxNnB;;;AMyNlB;EAAuC,SNxGnB;;;AMyGpB;EAAuC,SNxGnB;;;AMyGpB;EAAuC,SNoNnB;;;AMnNpB;EAAwC,SNyKnB;;;AMxKrB;EAAmC,SNkUnB;;;AMjUhB;EAAyC,SNkHnB;;;AMjHtB;EAA2C,SNkHnB;;;AMjHxB;EAAqC,SNgEnB;;;AM/DlB;EAAuC,SN8DnB;;;AM7DpB;EAAyC,SN3LnB;;;AM4LtB;EAA0C,SNkJnB;;;AMjJvB;EAAiD,SNlGnB;;;AMmG9B;EAAyC,SN4NnB;;;AM3NtB;EAAoC,SNzJnB;;;AM0JjB;AAAA;EACgD,SNvNnB;;;AMwN7B;AAAA;EAC8C,SNtNnB;;;AMuN3B;AAAA;EACiD,SNzNnB;;;AM0N9B;AAAA;EACgC,SNrHnB;;;AMsHb;EAAgC,SN/CnB;;;AMgDb;AAAA;EACgC,SNwVnB;;;AMvVb;AAAA;EACgC,SNuBnB;;;AMtBb;AAAA;AAAA;AAAA;EAGgC,SN2BnB;;;AM1Bb;AAAA;AAAA;EAEgC,SNsLnB;;;AMrLb;AAAA;EACgC,SN0BnB;;;AMzBb;AAAA;EACgC,SNnQnB;;;AMoQb;EAAiC,SN9GnB;;;AM+Gd;EAAsC,SNlGnB;;;AMmGnB;EAA2C,SN4NnB;;;AM3NxB;EAA4C,SN4NnB;;;AM3NzB;EAA4C,SN4NnB;;;AM3NzB;EAA6C,SN4NnB;;;AM3N1B;EAA6C,SN+NnB;;;AM9N1B;EAA8C,SN+NnB;;;AM9N3B;EAAsC,SNuRnB;;;AMtRnB;EAAwC,SNmRnB;;;AMlRrB;EAA2C,SNiXnB;;;AMhXxB;EAAoC,SN8WnB;;;AM7WjB;EAAiC,SNmWnB;;;AMlWd;EAAwC,SNmWnB;;;AMlWrB;EAAyC,SN4WnB;;;AM3WtB;EAAoC,SNlKnB;;;AMmKjB;EAA2C,SNgOnB;;;AM/NxB;EAAsC,SNLnB;;;AMMnB;EAAmC,SNlGnB;;;AMmGhB;EAAgC,SN/WnB;;;AMgXb;EAAsC,SNvSnB;;;AMwSnB;EAA6C,SNvSnB;;;AMwS1B;EAAmC,SNkSnB;;;AMjShB;EAA0C,SNkSnB;;;AMjSvB;EAA4C,SN0BnB;;;AMzBzB;EAA0C,SN4BnB;;;AM3BvB;EAA4C,SNyBnB;;;AMxBzB;EAA6C,SNyBnB;;;AMxB1B;EAAkC,SNrWnB;;;AMsWf;EAAoC,SN4UnB;;;AM3UjB;EAAoC,SNjXnB;;;AMkXjB;EAAkC,SNYnB;;;AMXf;EAAqC,SNpLnB;;;AMqLlB;EAAkC,SNmLnB;;;AMlLf;EAAuC,SNtGnB;;;AMuGpB;EAAmC,SN+QnB;;;AM9QhB;EAAmC,SNpJnB;;;AMqJhB;EAAiC,SNuBnB;;;AMtBd;AAAA;EACqC,SN3EnB;;;AM4ElB;EAAkC,SNgOnB;;;AM/Nf;EAAmC,SN+CnB;;;AM9ChB;EAAoC,SNlXnB;;;AMmXjB;EAAgC,SN9SnB;;;AM+Sb;EAA+B,SN+SnB;;;AM9SZ;EAAkC,SNqTnB;;;AMpTf;EAAmC,SNoHnB;;;AMnHhB;EAAsC,SN0DnB;;;AMzDnB;EAA2C,SN+LnB;;;AM9LxB;EAAiD,SNnXnB;;;AMoX9B;EAAgD,SNrXnB;;;AMsX7B;AAAA;EACgD,SNjSnB;;;AMkS7B;EAAyC,SN3MnB;;;AM4MtB;EAAuC,SN+SnB;;;AM9SpB;EAAyC,SNkSnB;;;AMjStB;AAAA;EACgC,SN6PnB;;;AM5Pb;EAA0C,SNkFnB;;;AMjFvB;EAA0C,SN8KnB;;;AM7KvB;EAAkC,SNyJnB;;;AMxJf;EAA4C,SNtMnB;;;AMuMzB;EAAsC,SN6SnB;;;AM5SnB;EAAmC,SNsCnB;;;AMrChB;AAAA;AAAA;EAEuC,SNiQnB;;;AMhQpB;AAAA;EAC2C,SNzGnB;;;AM0GxB;EAAkC,SN8SnB;;;AM7Sf;EAAmC,SNjHnB;;;AMkHhB;EAAmC,SNqFnB;;;AMpFhB;EAA0C,SNsFnB;;;AMrFvB;EAA+C,SN2LnB;;;AM1L5B;EAAwC,SNyLnB;;;AMxLrB;EAAsC,SNvOnB;;;AMwOnB;EAAiC,SNpOnB;;;AMqOd;EAA0C,SNmDnB;;;AMlDvB;EAA2C,SNiDnB;;;AMhDxB;EAAmC,SNjOnB;;;AMkOhB;EAAmC,SN9DnB;;;AM+DhB;EAAqC,SNzDnB;;;AM0DlB;EAAgC,SNpMnB;;;AMqMb;EAAqC,SNrVnB;;;AMsVlB;EAAkC,SNlSnB;;;AMmSf;EAAgC,SN8BnB;;;AM7Bb;EAAkC,SNqJnB;;;AMpJf;EAAiC,SN7PnB;;;AM8Pd;EAAkC,SN7PnB;;;AM8Pf;EAAoC,SNrXnB;;;AMsXjB;EAA2C,SNrXnB;;;AMsXxB;EAAkC,SN4JnB;;;AM3Jf;EAAyC,SN4JnB;;;AM3JtB;EAAoC,SN8DnB;;;AM7DjB;AAAA;EACgC,SNjVnB;;;AMkVb;AAAA;EACiC,SNgLnB;;;AM/Kd;EAAiC,SN+MnB;;;AM9Md;EAAoC,SNyInB;;;AMxIjB;EAAuC,SN9PnB;;;AM+PpB;EAAuC,SNmInB;;;AMlIpB;EAAqC,SNtQnB;;;AMuQlB;EAAuC,SN7MnB;;;AM8MpB;EAAwC,SNtMnB;;;AMuMrB;EAAyC,SNnNnB;;;AMoNtB;EAA8C,SN7MnB;;;AM8M3B;AAAA;AAAA;EAEyC,SNtNnB;;;AMuNtB;AAAA;EAC2C,SN5NnB;;;AM6NxB;AAAA;EACyC,SN7NnB;;;AM8NtB;AAAA;EACyC,SNlNnB;;;AMmNtB;EAAwC,SN/NnB;;;AMgOrB;EAAiC,SNqOnB;;;AMpOd;EAAoC,SNpTnB;;;AMqTjB;EAAqC,SNnGnB;;;AMoGlB;AAAA;AAAA;AAAA;AAAA;EAIsC,SNxFnB;;;AMyFnB;EAA2C,SNvUnB;;;AMwUxB;AAAA;AAAA;EAEkC,SN0BnB;;;AMzBf;AAAA;EACmC,SN9QnB;;;AM+QhB;EAAuC,SNxLnB;;;AMyLpB;EAAgC,SN1LnB;;;AM2Lb;AAAA;AAAA;EAEwC,SNxKnB;;;AMyKrB;EAA0C,SN2InB;;;AM1IvB;EAA+B,SNQnB;;;AMPZ;AAAA;EACmC,SNwNnB;;;AMvNhB;AAAA;EACwC,SNlCnB;;;AMmCrB;AAAA;EAC0C,SNnCnB;;;AMoCvB;EAAoC,SN3JnB;;;AM4JjB;EAAwC,SN1VnB;;;AM2VrB;EAAmC,SNlKnB;;;AMmKhB;EAAsC,SNrCnB;;;AMsCnB;EAAoC,SNkEnB;;;AMjEjB;EAAsC,SN2CnB;;;AM1CnB;EAA6C,SN2CnB;;;AM1C1B;EAAiC,SNjanB;;;AMkad;AAAA;EACqC,SN3NnB;;;AM4NlB;EAAgC,SN6JnB;;;AM5Jb;EAAuC,SNhbnB;;;AMibpB;EAAiC,SNpBnB;;;AMqBd;EAAuC,SN0DnB;;;AMzDpB;EAAmC,SN8JnB;;;AM7JhB;EAAiC,SNuNnB;;;AMtNd;EAAwC,SNjEnB;;;AMkErB;EAAiC,SNsMnB;;;AMrMd;EAAuC,SN7ZnB;;;AM8ZpB;EAAmC,SN/CnB;;;AMgDhB;EAA0C,SN1MnB;;;AM2MvB;EAAoC,SNpYnB;;;AMqYjB;EAA0C,SNxYnB;;;AMyYvB;EAAwC,SN3YnB;;;AM4YrB;EAAoC,SN9YnB;;;AM+YjB;EAAsC,SN1YnB;;;AM2YnB;EAAsC,SN1YnB;;;AM2YnB;EAAuC,SNncnB;;;AMocpB;EAAyC,SNncnB;;;AMoctB;EAAkC,SNkInB;;;AMjIf;EAAsC,SN3VnB;;;AM4VnB;EAA+B,SNlenB;;;AMmeZ;EAAuC,SN1SnB;;;AM2SpB;EAAwC,SNvEnB;;;AMwErB;EAA0C,SNtcnB;;;AMucvB;EAAuC,SN1fnB;;;AM2fpB;EAAsC,SNvDnB;;;AMwDnB;EAAuC,SN9InB;;;AM+IpB;EAAmC,SN5JnB;;;AM6JhB;EAA0C,SN5JnB;;;AM6JvB;EAAuC,SN+GnB;;;AM9GpB;EAAsC,SN+GnB;;;AM9GnB;EAAoC,SNhdnB;;;AMidjB;EAAgC,SNzbnB;;;AM0bb;EAAoC,SN5KnB;;;AM6KjB;EAAsC,SN/gBnB;;;AMghBnB;EAA+B,SNranB;;;AMsaZ;AAAA;AAAA;EAEgC,SN7LnB;;;AM8Lb;EAAqC,SN1HnB;;;AM2HlB;EAAuC,SNhcnB;;;AMicpB;EAA2C,SNpXnB;;;AMqXxB;EAAqC,SNtWnB;;;AMuWlB;EAAqC,SN5QnB;;;AM6QlB;EAAoC,SN1KnB;;;AM2KjB;EAAmC,SNbnB;;;AMchB;EAAyC,SNDnB;;;AMEtB;EAAwC,SNOnB;;;AMNrB;EAAqC,SNQnB;;;AMPlB;EAAsC,SNpbnB;;;AMqbnB;EAA4C,SNtbnB;;;AMubzB;EAAoC,SNvWnB;;;AMwWjB;EAAiC,SNRnB;;;AMSd;EAAwC,SN8HnB;;;AM7HrB;EAAuC,SNvHnB;;;AMwHpB;EAAwC,SN+CnB;;;AM9CrB;EAAsC,SN/NnB;;;AMgOnB;EAAkC,SN6HnB;;;AM5Hf;EAAiC,SNnJnB;;;AMoJd;EAAoC,SN1InB;;;AM2IjB;AAAA;EACwC,SNqFnB;;;AMpFrB;EAA4C,SNqFnB;;;AMpFzB;EAAyC,SNwHnB;;;AMvHtB;EAAwC,SNxJnB;;;AMyJrB;EAAuC,SNuHnB;;;AMtHpB;EAAwC,SNzJnB;;;AM0JrB;EAA0C,SNxJnB;;;AMyJvB;EAA0C,SN1JnB;;;AM2JvB;EAAmC,SNlInB;;;AMmIhB;EAAuC,SN5RnB;;;AM6RpB;EAA8C,SNxVnB;;;AMyV3B;EAAwC,SNjGnB;;;AMkGrB;EAAqC,SNgInB;;;AM/HlB;EAAmC,SNvCnB;;;AMwChB;EAAsC,SNuGnB;;;AMtGnB;EAAuC,SNwGnB;;;AMvGpB;AAAA;EACgC,SNvgBnB;;;AMwgBb;EAAoC,SN0GnB;;;AMzGjB;EAAkC,SNiEnB;;;AMhEf;EAAmC,SN0BnB;;;AMzBhB;EAAmC,SNpKnB;;;AMqKhB;AAAA;EACyC,SNkInB;;;AMjItB;EAA0C,SNzInB;;;AM0IvB;EAAqC,SN7InB;;;AM8IlB;EAAyC,SNjXnB;;;AMkXtB;AAAA;EACyC,SNthBnB;;;AMuhBtB;AAAA;EACmD,SNrhBnB;;;AMshBhC;AAAA;EACyC,SNzhBnB;;;AM0hBtB;AAAA;EAC4C,SN1hBnB;;;AM2hBzB;AAAA;EAC0C,SN/hBnB;;;AMgiBvB;EAA0C,SNlKnB;;;AMmKvB;EAAqC,SN3PnB;;;AM4PlB;EAAyC,SN/JnB;;;AMgKtB;EAA2C,SN/JnB;;;AMgKxB;EAAwC,SNLnB;;;AMMrB;EAA0C,SNLnB;;;AMMvB;EAAmC,SNtenB;;;AMuehB;EAA2C,SNzenB;;;AM0exB;EAAkC,SN3cnB;;;AM4cf;EAA0C,SNrjBnB;;;AMsjBvB;EAAwC,SNxQnB;;;AMyQrB;AAAA;EAC4C,SNzQnB;;;AM0QzB;AAAA;EAC2C,SN7QnB;;;AM8QxB;AAAA;EAC0C,SNhRnB;;;AMiRvB;EAAsC,SNrRnB;;;AMsRnB;AAAA;EACwC,SNvSnB;;;AMwSrB;AAAA;EACyC,SN5SnB;;;AM6StB;EAA4C,SNzSnB;;;AM0SzB;EAA0C,SNnTnB;;;AMoTvB;EAAyC,SN1SnB;;;AM2StB;EAA2C,SN9SnB;;;AM+SxB;EAAyC,SNhTnB;;;AMiTtB;EAAsC,SNmBnB;;;AMlBnB;EAAuC,SNzHnB;;;AM0HpB;EAA6C,SNtcnB;;;AMuc1B;EAA+B,SNpVnB;;;AMqVZ;EAAsC,SNpVnB;;;AMqVnB;EAAwC,SNsBnB;;;AMrBrB;EAA0C,SN5LnB;;;AM6LvB;EAAiD,SN5LnB;;;AM6L9B;EAAuC,SN1VnB;;;AM2VpB;EAAwC,SNuEnB;;;AMtErB;EAAmC,SN9GnB;;;AM+GhB;EAAmC,SNhfnB;;;AMifhB;EAAoC,SN3XnB;;;AM4XjB;EAAkC,SN/LnB;;;AMgMf;EAA8C,SNxRnB;;;AMyR3B;AAAA;EACuC,SNtBnB;;;AMuBpB;EAAmC,SNxdnB;;;AMydhB;EAAkC,SNxoBnB;;;AMyoBf;EAAmC,SNloBnB;;;AMmoBhB;EAA4C,SNliBnB;;;AMmiBzB;EAA6C,SNriBnB;;;AMsiB1B;EAA6C,SNniBnB;;;AMoiB1B;EAA6C,SNxiBnB;;;AMyiB1B;EAAqC,SNxSnB;;;AMySlB;EAAoC,SNjPnB;;;AMkPjB;EAAsC,SNjPnB;;;AMkPnB;EAAkC,SNpPnB;;;AMqPf;EAAgC,SNvPnB;;;AMwPb;EAAuC,SN3enB;;;AM4epB;EAAyC,SN3enB;;;AM4etB;EAAkC,SNtTnB;;;AMuTf;EAAkC,SNgCnB;;;AM/Bf;EAAsC,SNzkBnB;;;AM0kBnB;EAAsC,SNlYnB;;;AMmYnB;EAAyC,SN9JnB;;;AM+JtB;EAAiC,SN7cnB;;;AM8cd;EAA4C,SNvenB;;;AMwezB;EAAqC,SN3fnB;;;AM4flB;EAAiC,SNzOnB;;;AM0Od;EAAyC,SNvYnB;;;AMwYtB;EAAgC,SNQnB;;;AMPb;EAAyC,SNnLnB;;;AMoLtB;EAAqC,SNhPnB;;;AMiPlB;EAAmC,SN7InB;;;AM8IhB;EAAyC,SNpNnB;;;AMqNtB;EAA2C,SNpNnB;;;AMqNxB;EAAwC,SNxEnB;;;AMyErB;EAA0C,SNxEnB;;;AMyEvB;EAAyC,SNhInB;;;AMiItB;EAA4C,SNhInB;;;AMiIzB;EAAoC,SN7VnB;;;AM8VjB;EAAsC,SN1lBnB;;;AM2lBnB;EAAwC,SN1lBnB;;;AM2lBrB;EAAoC,SNtNnB;;;AMuNjB;EAAmC,SNhYnB;;;AMiYhB;EAAuC,SN4BnB;;;AM3BpB;EAAoC,SN4BnB;;;AM3BjB;EAAmC,SN1dnB;;;AM2dhB;EAA6C,SNjBnB;;;AMkB1B;EAA2C,SNkBnB;;;AMjBxB;EAA8C,SNhMnB;;;AMiM3B;EAAkC,SNrmBnB;;;AMsmBf;EAA8C,SNzoBnB;;;AM0oB3B;EAAiD,SNInB;;;AMH9B;EAAoC,SN/lBnB;;;AMgmBjB;EAAwD,SN/oBnB;;;AMgpBrC;AAAA;EACgE,SNjrBnB;;;AMkrB7C;AAAA;AAAA;EAEiC,SN9fnB;;;AM+fd;EAAkC,SN9YnB;;;AM+Yf;EAAoC,SN9YnB;;;AM+YjB;AAAA;EAC0C,SNtJnB;;;AMuJvB;EAAuC,SN9SnB;;;AM+SpB;EAAmC,SNhBnB;;;AMiBhB;EAA0C,SNhBnB;;;AMiBvB;EAAqC,SN9InB;;;AM+IlB;EAA2C,SN9InB;;;AM+IxB;EAA4C,SN9InB;;;AM+IzB;EAAuC,SN5OnB;;;AM6OpB;EAAwC,SNjcnB;;;AMkcrB;EAAkC,SNYnB;;;AMXf;EAAsC,SNnFnB;;;AMoFnB;AAAA;EACiD,SNvZnB;;;AMwZ9B;AAAA;EACyC,SN1bnB;;;AO/RtB;EH8BE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAUA;EAEE;EACA;EACA;EACA;EACA;EACA;;;AIvDJ;EAAwD,SR0TzC;;;AQzTf;EAAwD,SRoczC;;;AQncf;EAAyD,SR+hBzC;;;AQ9hBhB;EAA6D,SR4NzC;;;AQ3NpB;EAAwD,SRuVzC;;;AQtVf;EAAuD,SRqlBzC;;;AQplBd;EAAyD,SRylBzC;;;AQxlBhB;EAAuD,SR6qBzC;;;AQ5qBd;EAAuD,SRsQzC;;;AQrQd;EAA2D,SRwnBzC;;;AQvnBlB;EAAqD,SRsnBzC;;;AQrnBZ;EAA0D,SRunBzC;;;AQtnBjB;EAAwD,SRkIzC;;;AQjIf;AAAA;AAAA;EAEwD,SR2nBzC;;;AQ1nBf;EAA8D,SRmhBzC;;;AQlhBrB;EAA+D,SRihBzC;;;AQhhBtB;EAA4D,SRoezC;;;AQnenB;EAAyD,SRqiBzC;;;AQpiBhB;AAAA;EACsD,SRyJzC;;;AQxJb;EAA0D,SRmoBzC;;;AQloBjB;EAAuD,SRwUzC;;;AQvUd;EAAyD,SR0OzC;;;AQzOhB;EAA0D,SRyIzC;;;AQxIjB;EAAuD,SRyfzC;;;AQxfd;EAA2D,SRyLzC;;;AQxLlB;EAAsE,SRSzC;;;AQR7B;EAAoE,SRWzC;;;AQV3B;EAAwD,SRiVzC;;;AQhVf;EAAgE,SRidzC;;;AQhdvB;AAAA;EACyD,SR4ezC;;;AQ3ehB;EAA0D,SRsezC;;;AQrejB;EAA2D,SRmXzC;;;AQlXlB;EAAuD,SRsXzC;;;AQrXd;EAAuD,SR+OzC;;;AQ9Od;EAA6D,SRoTzC;;;AQnTpB;EAA6D,SRgqBzC;;;AQ/pBpB;EAA8D,SR8pBzC;;;AQ7pBrB;EAA4D,SR+pBzC;;;AQ9pBnB;EAAyD,SRidzC;;;AQhdhB;EAA0D,SRoBzC;;;AQnBjB;EAAsD,SR2kBzC;;;AQ1kBb;EAAuD,SR2kBzC;;;AQ1kBd;EAAuD,SRkDzC;;;AQjDd;EAA2D,SRkDzC;;;AQjDlB;EAAwD,SRuczC;;;AQtcf;EAAyD,SRoEzC;;;AQnEhB;EAAuD,SR6OzC;;;AQ5Od;EAAuD,SR0CzC;;;AQzCd;EAAyD,SRsUzC;;;AQrUhB;EAA8D,SRykBzC;;;AQxkBrB;EAA6D,SRykBzC;;;AQxkBpB;EAA6D,SRvCzC;;;AQwCpB;EAA+D,SR1CzC;;;AQ2CtB;EAA8D,SRxCzC;;;AQyCrB;EAAgE,SR3CzC;;;AQ4CvB;EAAuD,SR0VzC;;;AQzVd;AAAA;EAC0D,SRoZzC;;;AQnZjB;EAAyD,SRkTzC;;;AQjThB;EAA+D,SRioBzC;;;AQhoBtB;AAAA;AAAA;EAE4D,SRmazC;;;AQlanB;EAAyD,SR2ZzC;;;AQ1ZhB;EAA6D,SRmWzC;;;AQlWpB;EAAyD,SRzDzC;;;AQ0DhB;EAAuD,SRukBzC;;;AQtkBd;AAAA;EACkE,SRwZzC;;;AQvZzB;EAAiE,SRmezC;;;AQlexB;EAAiE,SRwEzC;;;AQvExB;EAAyD,SR5BzC;;;AQ6BhB;EAAgE,SRshBzC;;;AQrhBvB;EAAgE,SRwKzC;;;AQvKvB;EAA2D,SRrBzC;;;AQsBlB;EAAuD,SR+ZzC;;;AQ9Zd;EAAwD,SRyYzC;;;AQxYf;EAAuD,SRshBzC;;;AQrhBd;EAA0D,SRkNzC;;;AQjNjB;EAA+D,SRmKzC;;;AQlKtB;EAA+D,SR+gBzC;;;AQ9gBtB;EAAwD,SRqIzC;;;AQpIf;EAA+D,SRkEzC;;;AQjEtB;EAAgE,SRkEzC;;;AQjEvB;EAA8D,SR2ZzC;;;AQ1ZrB;EAA+D,SRgWzC;;;AQ/VtB;EAA+D,SRijBzC;;;AQhjBtB;EAA+D,SRoDzC;;;AQnDtB;EAAkE,SRiazC;;;AQhazB;EAA8D,SRsRzC;;;AQrRrB;EAA6D,SRmGzC;;;AQlGpB;EAAiE,SR6iBzC;;;AQ5iBxB;EAAiE,SRgDzC;;;AQ/CxB;EAAsD,SRtCzC;;;AQuCb;EAA6D,SRtDzC;;;AQuDpB;EAA8D,SRtDzC;;;AQuDrB;EAA2D,SRtDzC;;;AQuDlB;EAA6D,SR1DzC;;;AQ2DpB;AAAA;EACwD,SRiczC;;;AQhcf;EAAyD,SRiIzC;;;AQhIhB;EAA2D,SR+EzC;;;AQ9ElB;EAAuD,SRwYzC;;;AQvYd;EAAwD,SR6UzC;;;AQ5Uf;EAA2D,SRvDzC;;;AQwDlB;EAAqE,SR0HzC;;;AQzH5B;EAAuD,SRoMzC;;;AQnMd;EAAuD,SRsRzC;;;AQrRd;EAAuD,SRgKzC;;;AQ/Jd;EAAsD,SR4HzC;;;AQ3Hb;EAA4D,SR4HzC;;;AQ3HnB;AAAA;EACuE,SRoHzC;;;AQnH9B;EAAwD,SRwXzC;;;AQvXf;EAA2D,SRPzC;;;AQQlB;EAAyD,SR2YzC;;;AQ1YhB;EAA0D,SRyDzC;;;AQxDjB;EAAyD,SRwSzC;;;AQvShB;EAA6D,SRgCzC;;;AQ/BpB;EAA+D,SR4BzC;;;AQ3BtB;EAA0D,SRqZzC;;;AQpZjB;EAAgE,SRubzC;;;AQtbvB;EAAyD,SR6JzC;;;AQ5JhB;EAA8D,SR8JzC;;;AQ7JrB;EAA2D,SR9EzC;;;AQ+ElB;EAA2D,SRhFzC;;;AQiFlB;AAAA;EAC4D,SRtEzC;;;AQuEnB;EAAiE,SRmiBzC;;;AQliBxB;EAAkE,SR+GzC;;;AQ9GzB;EAA+D,SRhBzC;;;AQiBtB;EAAsD,SRuPzC;;;AQtPb;AAAA;EACuD,SRsCzC;;;AQrCd;EAA2D,SR2CzC;;;AQ1ClB;EAA8D,SR4fzC;;;AQ3frB;EAAgE,SR0fzC;;;AQzfvB;EAA4D,SR8czC;;;AQ7cnB;EAA0D,SR8MzC;;;AQ7MjB;EAA2D,SRwazC;;;AQvalB;EAAkE,SRmQzC;;;AQlQzB;EAA6D,SRmfzC;;;AQlfpB;EAAgE,SRyFzC;;;AQxFvB;EAA0D,SRkazC;;;AQjajB;EAAyD,SRygBzC;;;AQxgBhB;EAAgE,SRkKzC;;;AQjKvB;EAAyD,SR2hBzC;;;AQ1hBhB;EAA0D,SRiPzC;;;AQhPjB;EAAwD,SR0UzC;;;AQzUf;EAA2D,SR8bzC;;;AQ7blB;EAA6D,SRxDzC;;;AQyDpB;EAA+D,SRwUzC;;;AQvUtB;EAA0D,SR0gBzC;;;AQzgBjB;AAAA;EAC2D,SRmFzC;;;AQlFlB;EAAyD,SRsJzC;;;AQrJhB;EAAyD,SR8gBzC;;;AQ7gBhB;EAA8D,SR6BzC;;;AQ5BrB;AAAA;EACsD,SRqXzC;;;AQpXb;EAAwD,SRoLzC;;;AQnLf;EAA2D,SR5DzC;;;AQ6DlB;EAAuD,SRvFzC;;;AQwFd;EAA8D,SR5BzC;;;AQ6BrB;EAA+D,SRqKzC;;;AQpKtB;EAA8D,SRmKzC;;;AQlKrB;EAA4D,SRoKzC;;;AQnKnB;EAA8D,SRgKzC;;;AQ/JrB;EAAoE,SRxIzC;;;AQyI3B;EAAqE,SRpIzC;;;AQqI5B;EAAkE,SRpIzC;;;AQqIzB;EAAoE,SR5IzC;;;AQ6I3B;EAAwD,SR6IzC;;;AQ5If;EAAyD,SRsiBzC;;;AQriBhB;EAAwD,SRuczC;;;AQtcf;EAAyD,SR4FzC;;;AQ3FhB;EAA4D,SRhFzC;;;AQiFnB;EAA6D,SRrIzC;;;AQsIpB;AAAA;EACwD,SRkgBzC;;;AQjgBf;AAAA;EACuD,SRwNzC;;;AQvNd;EAAwD,SRrBzC;;;AQsBf;EAAwD,SR6FzC;;;AQ5Ff;AAAA;EAC2D,SRkWzC;;;AQjWlB;AAAA;EAC0D,SR8EzC;;;AQ7EjB;EAA4D,SRsRzC;;;AQrRnB;AAAA;EAC2D,SRwFzC;;;AQvFlB;EAAyD,SRmZzC;;;AQlZhB;AAAA;AAAA;EAEuD,SRtIzC;;;AQuId;EAA0D,SRiNzC;;;AQhNjB;EAA0D,SR+MzC;;;AQ9MjB;EAAgE,SRkazC;;;AQjavB;EAA4D,SR+dzC;;;AQ9dnB;EAAwD,SRyazC;;;AQxaf;EAAwD,SRoNzC;;;AQnNf;EAAwD,SRidzC;;;AQhdf;EAA4D,SR4RzC;;;AQ3RnB;EAAmE,SR6RzC;;;AQ5R1B;EAAqE,SRkHzC;;;AQjH5B;EAA8D,SR8GzC;;;AQ7GrB;EAAwD,SR8OzC;;;AQ7Of;EAA6D,SR3FzC;;;AQ4FpB;EAA2D,SRrFzC;;;AQsFlB;EAA6D,SR5FzC;;;AQ6FpB;EAA8D,SR5FzC;;;AQ6FrB;EAA0D,SRvCzC;;;AQwCjB;AAAA;EACuD,SR6WzC;;;AQ5Wd;AAAA;EAC4D,SRiXzC;;;AQhXnB;AAAA;EAC2D,SR8WzC;;;AQ7WlB;EAA2D;;;AAC3D;EAA2D,SRmLzC;;;AQlLlB;AAAA;EACuD,SRyczC;;;AQxcd;AAAA;EACwD,SRsEzC;;;AQrEf;AAAA;EAC6D,SRiZzC;;;AQhZpB;EAA4D,SRpDzC;;;AQqDnB;EAA6D,SRjDzC;;;AQkDpB;AAAA;EACuD,SR3IzC;;;AQ4Id;EAA0D,SRiVzC;;;AQhVjB;EAA2D,SR6bzC;;;AQ5blB;AAAA;EAC4D,SR5EzC;;;AQ6EnB;EAA8D,SRiKzC;;;AQhKrB;EAA2D,SRVzC;;;AQWlB;EAAiE,SR1EzC;;;AQ2ExB;EAA+D,SR1EzC;;;AQ2EtB;EAA0D,SRmczC;;;AQlcjB;EAA8D,SRiXzC;;;AQhXrB;EAA2D,SR4XzC;;;AQ3XlB;EAAyD,SRrKzC;;;AQsKhB;EAAyD,SRzEzC;;;AQ0EhB;EAA0D,SRjDzC;;;AQkDjB;EAA8D,SRiBzC;;;AQhBrB;EAA6D,SRjJzC;;;AQkJpB;EAA6D,SRsGzC;;;AQrGpB;EAA4D,SRxOzC;;;AQyOnB;EAAyD,SRuLzC;;;AQtLhB;EAA8D,SRHzC;;;AQIrB;EAAuD,SRlLzC;;;AQmLd;EAA2D,SRwEzC;;;AQvElB;EAA8D,SRoPzC;;;AQnPrB;EAAoE,SRxOzC;;;AQyO3B;EAAqE,SRxOzC;;;AQyO5B;EAAkE,SRxOzC;;;AQyOzB;EAAoE,SR5OzC;;;AQ6O3B;EAA6D,SRxOzC;;;AQyOpB;EAA8D,SRxOzC;;;AQyOrB;EAA2D,SRxOzC;;;AQyOlB;EAA6D,SR5OzC;;;AQ6OpB;EAA0D,SR3DzC;;;AQ4DjB;EAAyD,SRwHzC;;;AQvHhB;EAAyD,SR0WzC;;;AQzWhB;AAAA;EACyD,SRgLzC;;;AQ/KhB;EAA2D,SRhHzC;;;AQiHlB;EAA6D,SRgPzC;;;AQ/OpB;EAA8D,SRgPzC;;;AQ/OrB;EAA0D,SRkUzC;;;AQjUjB;EAAyD,SRrHzC;;;AQsHhB;AAAA;EACwD,SR0PzC;;;AQzPf;EAA6D,SRiCzC;;;AQhCpB;EAA2D,SRMzC;;;AQLlB;EAAgE,SROzC;;;AQNvB;EAA0D,SRySzC;;;AQxSjB;EAA0D,SRazC;;;AQZjB;EAAwD,SR0JzC;;;AQzJf;EAA0D,SRazC;;;AQZjB;EAA6D,SRmGzC;;;AQlGpB;EAAyD,SRPzC;;;AQQhB;EAAiE,SRTzC;;;AQUxB;EAA2D,SR8VzC;;;AQ7VlB;EAAuD,SRvHzC;;;AQwHd;AAAA;EAC4D,SR6OzC;;;AQ5OnB;AAAA;AAAA;EAE8D,SRyTzC;;;AQxTrB;EAAiE,SRmHzC;;;AQlHxB;EAAuD,SRvGzC;;;AQwGd;EAA4D,SR9HzC;;;AQ+HnB;AAAA;EAC+D,SR7JzC;;;AQ8JtB;EAA2D,SRiNzC;;;AQhNlB;EAAuD,SRsEzC;;;AQrEd;EAA8D,SRvEzC;;;AQwErB;EAA8D,SRoUzC;;;AQnUrB;EAA4D,SR+TzC;;;AQ9TnB;EAAyD,SR9EzC;;;AQ+EhB;EAA+D,SRwMzC;;;AQvMtB;EAA6D,SRoIzC;;;AQnIpB;EAAmE,SRoIzC;;;AQnI1B;EAAyD,SR2PzC;;;AQ1PhB;EAA6D,SRnMzC;;;AQoMpB;EAAoE,SRrCzC;;;AQsC3B;EAAyD,SR6NzC;;;AQ5NhB;EAAyD,SRwHzC;;;AQvHhB;EAAsE,SRrKzC;;;AQsK7B;EAAuE,SRrKzC;;;AQsK9B;EAAoE,SRrKzC;;;AQsK3B;EAAsE,SRzKzC;;;AQ0K7B;EAAwD,SR8CzC;;;AQ7Cf;EAAuD,SR5HzC;;;AQ6Hd;EAAyD,SR1SzC;;;AQ2ShB;EAA6D,SR+WzC;;;AQ9WpB;EAA2D,SRvNzC;;;AQwNlB;EAA6D,SRvGzC;;;AQwGpB;EAA6D,SRvGzC;;;AQwGpB;EAA6D,SRqNzC;;;AQpNpB;EAA8D,SR0KzC;;;AQzKrB;EAAyD,SRmUzC;;;AQlUhB;EAA+D,SRmHzC;;;AQlHtB;EAAiE,SRmHzC;;;AQlHxB;EAA2D,SRiEzC;;;AQhElB;EAA6D,SR+DzC;;;AQ9DpB;EAA+D,SR1LzC;;;AQ2LtB;EAAgE,SRmJzC;;;AQlJvB;EAAuE,SRjGzC;;;AQkG9B;EAA+D,SR6NzC;;;AQ5NtB;EAA0D,SRxJzC;;;AQyJjB;AAAA;EACsE,SRtNzC;;;AQuN7B;AAAA;EACoE,SRrNzC;;;AQsN3B;AAAA;EACuE,SRxNzC;;;AQyN9B;AAAA;EACsD,SRpHzC;;;AQqHb;EAAsD,SR9CzC;;;AQ+Cb;AAAA;EACsD,SRyVzC;;;AQxVb;AAAA;EACsD,SRwBzC;;;AQvBb;AAAA;AAAA;AAAA;EAGsD,SR4BzC;;;AQ3Bb;AAAA;AAAA;EAEsD,SRuLzC;;;AQtLb;AAAA;EACsD,SR2BzC;;;AQ1Bb;AAAA;EACsD,SRlQzC;;;AQmQb;EAAuD,SR7GzC;;;AQ8Gd;EAA4D,SRjGzC;;;AQkGnB;EAAiE,SR6NzC;;;AQ5NxB;EAAkE,SR6NzC;;;AQ5NzB;EAAkE,SR6NzC;;;AQ5NzB;EAAmE,SR6NzC;;;AQ5N1B;EAAmE,SRgOzC;;;AQ/N1B;EAAoE,SRgOzC;;;AQ/N3B;EAA4D,SRwRzC;;;AQvRnB;EAA8D,SRoRzC;;;AQnRrB;EAAiE,SRkXzC;;;AQjXxB;EAA0D,SR+WzC;;;AQ9WjB;EAAuD,SRoWzC;;;AQnWd;EAA8D,SRoWzC;;;AQnWrB;EAA+D,SR6WzC;;;AQ5WtB;EAA0D,SRjKzC;;;AQkKjB;EAAiE,SRiOzC;;;AQhOxB;EAA4D,SRJzC;;;AQKnB;EAAyD,SRjGzC;;;AQkGhB;EAAsD,SR9WzC;;;AQ+Wb;EAA4D,SRtSzC;;;AQuSnB;EAAmE,SRtSzC;;;AQuS1B;EAAyD,SRmSzC;;;AQlShB;EAAgE,SRmSzC;;;AQlSvB;EAAkE,SR2BzC;;;AQ1BzB;EAAgE,SR6BzC;;;AQ5BvB;EAAkE,SR0BzC;;;AQzBzB;EAAmE,SR0BzC;;;AQzB1B;EAAwD,SRpWzC;;;AQqWf;EAA0D,SR6UzC;;;AQ5UjB;EAA0D,SRhXzC;;;AQiXjB;EAAwD,SRazC;;;AQZf;EAA2D,SRnLzC;;;AQoLlB;EAAwD,SRoLzC;;;AQnLf;EAA6D,SRrGzC;;;AQsGpB;EAAyD,SRgRzC;;;AQ/QhB;EAAyD,SRnJzC;;;AQoJhB;EAAuD,SRwBzC;;;AQvBd;AAAA;EAC2D,SR1EzC;;;AQ2ElB;EAAwD,SRiOzC;;;AQhOf;EAAyD,SRgDzC;;;AQ/ChB;EAA0D,SRjXzC;;;AQkXjB;EAAsD,SR7SzC;;;AQ8Sb;EAAqD,SRgTzC;;;AQ/SZ;EAAwD,SRsTzC;;;AQrTf;EAAyD,SRqHzC;;;AQpHhB;EAA4D,SR2DzC;;;AQ1DnB;EAAiE,SRgMzC;;;AQ/LxB;EAAuE,SRlXzC;;;AQmX9B;EAAsE,SRpXzC;;;AQqX7B;AAAA;EACsE,SRhSzC;;;AQiS7B;EAA+D,SR1MzC;;;AQ2MtB;EAA6D,SRgTzC;;;AQ/SpB;EAA+D,SRmSzC;;;AQlStB;AAAA;EACsD,SR8PzC;;;AQ7Pb;EAAgE,SRmFzC;;;AQlFvB;EAAgE,SR+KzC;;;AQ9KvB;EAAwD,SR0JzC;;;AQzJf;EAAkE,SRrMzC;;;AQsMzB;EAA4D,SR8SzC;;;AQ7SnB;EAAyD,SRuCzC;;;AQtChB;AAAA;AAAA;EAE6D,SRkQzC;;;AQjQpB;AAAA;EACiE,SRxGzC;;;AQyGxB;EAAwD,SR+SzC;;;AQ9Sf;EAAyD,SRhHzC;;;AQiHhB;EAAyD,SRsFzC;;;AQrFhB;EAAgE,SRuFzC;;;AQtFvB;EAAqE,SR4LzC;;;AQ3L5B;EAA8D,SR0LzC;;;AQzLrB;EAA4D,SRtOzC;;;AQuOnB;EAAuD,SRnOzC;;;AQoOd;EAAgE,SRoDzC;;;AQnDvB;EAAiE,SRkDzC;;;AQjDxB;EAAyD,SRhOzC;;;AQiOhB;EAAyD,SR7DzC;;;AQ8DhB;EAA2D,SRxDzC;;;AQyDlB;EAAsD,SRnMzC;;;AQoMb;EAA2D,SRpVzC;;;AQqVlB;EAAwD,SRjSzC;;;AQkSf;EAAsD,SR+BzC;;;AQ9Bb;EAAwD,SRsJzC;;;AQrJf;EAAuD,SR5PzC;;;AQ6Pd;EAAwD,SR5PzC;;;AQ6Pf;EAA0D,SRpXzC;;;AQqXjB;EAAiE,SRpXzC;;;AQqXxB;EAAwD,SR6JzC;;;AQ5Jf;EAA+D,SR6JzC;;;AQ5JtB;EAA0D,SR+DzC;;;AQ9DjB;AAAA;EACsD,SRhVzC;;;AQiVb;AAAA;EACuD,SRiLzC;;;AQhLd;EAAuD,SRgNzC;;;AQ/Md;EAA0D,SR0IzC;;;AQzIjB;EAA6D,SR7PzC;;;AQ8PpB;EAA6D,SRoIzC;;;AQnIpB;EAA2D,SRrQzC;;;AQsQlB;EAA6D,SR5MzC;;;AQ6MpB;EAA8D,SRrMzC;;;AQsMrB;EAA+D,SRlNzC;;;AQmNtB;EAAoE,SR5MzC;;;AQ6M3B;AAAA;AAAA;EAE+D,SRrNzC;;;AQsNtB;AAAA;EACiE,SR3NzC;;;AQ4NxB;AAAA;EAC+D,SR5NzC;;;AQ6NtB;AAAA;EAC+D,SRjNzC;;;AQkNtB;EAA8D,SR9NzC;;;AQ+NrB;EAAuD,SRsOzC;;;AQrOd;EAA0D,SRnTzC;;;AQoTjB;EAA2D,SRlGzC;;;AQmGlB;AAAA;AAAA;AAAA;AAAA;EAI4D,SRvFzC;;;AQwFnB;EAAiE,SRtUzC;;;AQuUxB;AAAA;AAAA;EAEwD,SR2BzC;;;AQ1Bf;AAAA;EACyD,SR7QzC;;;AQ8QhB;EAA6D,SRvLzC;;;AQwLpB;EAAsD,SRzLzC;;;AQ0Lb;AAAA;AAAA;EAE8D,SRvKzC;;;AQwKrB;EAAgE,SR4IzC;;;AQ3IvB;EAAqD,SRSzC;;;AQRZ;AAAA;EACyD,SRyNzC;;;AQxNhB;AAAA;EAC8D,SRjCzC;;;AQkCrB;AAAA;EACgE,SRlCzC;;;AQmCvB;EAA0D,SR1JzC;;;AQ2JjB;EAA8D,SRzVzC;;;AQ0VrB;EAAyD,SRjKzC;;;AQkKhB;EAA4D,SRpCzC;;;AQqCnB;EAA0D,SRmEzC;;;AQlEjB;EAA4D,SR4CzC;;;AQ3CnB;EAAmE,SR4CzC;;;AQ3C1B;EAAuD,SRhazC;;;AQiad;AAAA;EAC2D,SR1NzC;;;AQ2NlB;EAAsD,SR8JzC;;;AQ7Jb;EAA6D,SR/azC;;;AQgbpB;EAAuD,SRnBzC;;;AQoBd;EAA6D,SR2DzC;;;AQ1DpB;EAAyD,SR+JzC;;;AQ9JhB;EAAuD,SRwNzC;;;AQvNd;EAA8D,SRhEzC;;;AQiErB;EAAuD,SRuMzC;;;AQtMd;EAA6D,SR5ZzC;;;AQ6ZpB;EAAyD,SR9CzC;;;AQ+ChB;EAAgE,SRzMzC;;;AQ0MvB;EAA0D,SRnYzC;;;AQoYjB;EAAgE,SRvYzC;;;AQwYvB;EAA8D,SR1YzC;;;AQ2YrB;EAA0D,SR7YzC;;;AQ8YjB;EAA4D,SRzYzC;;;AQ0YnB;EAA4D,SRzYzC;;;AQ0YnB;EAA6D,SRlczC;;;AQmcpB;EAA+D,SRlczC;;;AQmctB;EAAwD,SRmIzC;;;AQlIf;EAA4D,SR1VzC;;;AQ2VnB;EAAqD,SRjezC;;;AQkeZ;EAA6D,SRzSzC;;;AQ0SpB;EAA8D,SRtEzC;;;AQuErB;EAAgE,SRrczC;;;AQscvB;EAA6D,SRzfzC;;;AQ0fpB;EAA4D,SRtDzC;;;AQuDnB;EAA6D,SR7IzC;;;AQ8IpB;EAAyD,SR3JzC;;;AQ4JhB;EAAgE,SR3JzC;;;AQ4JvB;EAA6D,SRgHzC;;;AQ/GpB;EAA4D,SRgHzC;;;AQ/GnB;EAA0D,SR/czC;;;AQgdjB;EAAsD,SRxbzC;;;AQybb;EAA0D,SR3KzC;;;AQ4KjB;EAA4D,SR9gBzC;;;AQ+gBnB;EAAqD,SRpazC;;;AQqaZ;AAAA;AAAA;EAEsD,SR5LzC;;;AQ6Lb;EAA2D,SRzHzC;;;AQ0HlB;EAA6D,SR/bzC;;;AQgcpB;EAAiE,SRnXzC;;;AQoXxB;EAA2D,SRrWzC;;;AQsWlB;EAA2D,SR3QzC;;;AQ4QlB;EAA0D,SRzKzC;;;AQ0KjB;EAAyD,SRZzC;;;AQahB;EAA+D;;;AAC/D;EAA8D,SRQzC;;;AQPrB;EAA2D,SRSzC;;;AQRlB;EAA4D,SRnbzC;;;AQobnB;EAAkE,SRrbzC;;;AQsbzB;EAA0D,SRtWzC;;;AQuWjB;EAAuD,SRPzC;;;AQQd;EAA8D,SR+HzC;;;AQ9HrB;EAA6D,SRtHzC;;;AQuHpB;EAA8D,SRgDzC;;;AQ/CrB;EAA4D,SR9NzC;;;AQ+NnB;EAAwD,SR8HzC;;;AQ7Hf;EAAuD,SRlJzC;;;AQmJd;EAA0D,SRzIzC;;;AQ0IjB;AAAA;EAC8D,SRsFzC;;;AQrFrB;EAAkE,SRsFzC;;;AQrFzB;EAA+D,SRyHzC;;;AQxHtB;EAA8D,SRvJzC;;;AQwJrB;EAA6D,SRwHzC;;;AQvHpB;EAA8D,SRxJzC;;;AQyJrB;EAAgE,SRvJzC;;;AQwJvB;EAAgE,SRzJzC;;;AQ0JvB;EAAyD,SRjIzC;;;AQkIhB;EAA6D,SR3RzC;;;AQ4RpB;EAAoE,SRvVzC;;;AQwV3B;EAA8D,SRhGzC;;;AQiGrB;EAA2D,SRiIzC;;;AQhIlB;EAAyD,SRtCzC;;;AQuChB;EAA4D,SRwGzC;;;AQvGnB;EAA6D,SRyGzC;;;AQxGpB;AAAA;EACsD,SRtgBzC;;;AQugBb;EAA0D,SR2GzC;;;AQ1GjB;EAAwD,SRkEzC;;;AQjEf;EAAyD,SR2BzC;;;AQ1BhB;EAAyD,SRnKzC;;;AQoKhB;AAAA;EAC+D,SRmIzC;;;AQlItB;EAAgE,SRxIzC;;;AQyIvB;EAA2D,SR5IzC;;;AQ6IlB;EAA+D,SRhXzC;;;AQiXtB;AAAA;EAC+D,SRrhBzC;;;AQshBtB;AAAA;EACyE,SRphBzC;;;AQqhBhC;AAAA;EAC+D,SRxhBzC;;;AQyhBtB;AAAA;EACkE,SRzhBzC;;;AQ0hBzB;AAAA;EACgE,SR9hBzC;;;AQ+hBvB;EAAgE,SRjKzC;;;AQkKvB;EAA2D,SR1PzC;;;AQ2PlB;EAA+D,SR9JzC;;;AQ+JtB;EAAiE,SR9JzC;;;AQ+JxB;EAA8D,SRJzC;;;AQKrB;EAAgE,SRJzC;;;AQKvB;EAAyD,SRrezC;;;AQsehB;EAAiE,SRxezC;;;AQyexB;EAAwD,SR1czC;;;AQ2cf;EAAgE,SRpjBzC;;;AQqjBvB;EAA8D,SRvQzC;;;AQwQrB;AAAA;EACkE,SRxQzC;;;AQyQzB;AAAA;EACiE,SR5QzC;;;AQ6QxB;AAAA;EACgE,SR/QzC;;;AQgRvB;EAA4D,SRpRzC;;;AQqRnB;AAAA;EAC8D,SRtSzC;;;AQuSrB;AAAA;EAC+D,SR3SzC;;;AQ4StB;EAAkE,SRxSzC;;;AQySzB;EAAgE,SRlTzC;;;AQmTvB;EAA+D,SRzSzC;;;AQ0StB;EAAiE,SR7SzC;;;AQ8SxB;EAA+D,SR/SzC;;;AQgTtB;EAA4D,SRoBzC;;;AQnBnB;EAA6D,SRxHzC;;;AQyHpB;EAAmE,SRrczC;;;AQsc1B;EAAqD,SRnVzC;;;AQoVZ;EAA4D,SRnVzC;;;AQoVnB;EAA8D,SRuBzC;;;AQtBrB;EAAgE,SR3LzC;;;AQ4LvB;EAAuE,SR3LzC;;;AQ4L9B;EAA6D,SRzVzC;;;AQ0VpB;EAA8D,SRwEzC;;;AQvErB;EAAyD,SR7GzC;;;AQ8GhB;EAAyD,SR/ezC;;;AQgfhB;EAA0D,SR1XzC;;;AQ2XjB;EAAwD,SR9LzC;;;AQ+Lf;EAAoE,SRvRzC;;;AQwR3B;AAAA;EAC6D,SRrBzC;;;AQsBpB;EAAyD,SRvdzC;;;AQwdhB;EAAwD,SRvoBzC;;;AQwoBf;EAAyD,SRjoBzC;;;AQkoBhB;EAAkE,SRjiBzC;;;AQkiBzB;EAAmE,SRpiBzC;;;AQqiB1B;EAAmE,SRliBzC;;;AQmiB1B;EAAmE,SRviBzC;;;AQwiB1B;EAA2D,SRvSzC;;;AQwSlB;EAA0D,SRhPzC;;;AQiPjB;EAA4D,SRhPzC;;;AQiPnB;EAAwD,SRnPzC;;;AQoPf;EAAsD,SRtPzC;;;AQuPb;EAA6D,SR1ezC;;;AQ2epB;EAA+D,SR1ezC;;;AQ2etB;EAAwD,SRrTzC;;;AQsTf;EAAwD,SRiCzC;;;AQhCf;EAA4D,SRxkBzC;;;AQykBnB;EAA4D,SRjYzC;;;AQkYnB;EAA+D,SR7JzC;;;AQ8JtB;EAAuD,SR5czC;;;AQ6cd;EAAkE,SRtezC;;;AQuezB;EAA2D,SR1fzC;;;AQ2flB;EAAuD,SRxOzC;;;AQyOd;EAA+D,SRtYzC;;;AQuYtB;EAAsD,SRSzC;;;AQRb;EAA+D,SRlLzC;;;AQmLtB;EAA2D,SR/OzC;;;AQgPlB;EAAyD,SR5IzC;;;AQ6IhB;EAA+D,SRnNzC;;;AQoNtB;EAAiE,SRnNzC;;;AQoNxB;EAA8D,SRvEzC;;;AQwErB;EAAgE,SRvEzC;;;AQwEvB;EAA+D,SR/HzC;;;AQgItB;EAAkE,SR/HzC;;;AQgIzB;EAA0D,SR5VzC;;;AQ6VjB;EAA4D,SRzlBzC;;;AQ0lBnB;EAA8D,SRzlBzC;;;AQ0lBrB;EAA0D,SRrNzC;;;AQsNjB;EAAyD,SR/XzC;;;AQgYhB;EAA6D,SR6BzC;;;AQ5BpB;EAA0D,SR6BzC;;;AQ5BjB;EAAyD,SRzdzC;;;AQ0dhB;EAAmE,SRhBzC;;;AQiB1B;EAAiE,SRmBzC;;;AQlBxB;EAAoE,SR/LzC;;;AQgM3B;EAAwD,SRpmBzC;;;AQqmBf;EAAoE,SRxoBzC;;;AQyoB3B;EAAuE,SRKzC;;;AQJ9B;EAA0D,SR9lBzC;;;AQ+lBjB;EAA8E,SR9oBzC;;;AQ+oBrC;AAAA;EACsF,SRhrBzC;;;AQirB7C;AAAA;AAAA;EAEuD,SR7fzC;;;AQ8fd;EAAwD,SR7YzC;;;AQ8Yf;EAA0D,SR7YzC;;;AQ8YjB;AAAA;EACgE,SRrJzC;;;AQsJvB;EAA6D,SR7SzC;;;AQ8SpB;EAAyD,SRfzC;;;AQgBhB;EAAgE,SRfzC;;;AQgBvB;EAA2D,SR7IzC;;;AQ8IlB;EAAiE,SR7IzC;;;AQ8IxB;EAAkE,SR7IzC;;;AQ8IzB;EAA6D,SR3OzC;;;AQ4OpB;EAA8D,SRhczC;;;AQicrB;EAAwD,SRazC;;;AQZf;EAA4D,SRlFzC;;;AQmFnB;AAAA;EACuE,SRtZzC;;;AQuZ9B;AAAA;EAC+D,SRzbzC;;;AN1QtB;EACC;EACA;;;AAKD;EACC;;;AAGD;EACC;;;AAKD;EACC;EACA;EACA;EACA;EAEA;;AAEA;EACC;;;AAIF;EACC;;AAEA;EACC;EACA;EACA;EAEA;EACA","file":"font-awesome.css"} \ No newline at end of file diff --git a/extras/font-awesome/scss/font-awesome.scss b/extras/font-awesome/scss/font-awesome.scss new file mode 100644 index 0000000..bb6f004 --- /dev/null +++ b/extras/font-awesome/scss/font-awesome.scss @@ -0,0 +1,66 @@ +/*! + * Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ + +@import "variables"; +@import "mixins"; +@import "path"; +@import "core"; +@import "larger"; +@import "fixed-width"; +@import "list"; +@import "bordered-pulled"; +@import "animated"; +@import "rotated-flipped"; +@import "stacked"; +@import "icons"; +@import "screen-reader"; + +//Adapt Font Awesome to the admin menu. + +@import "ame-variables"; +@import "ame-icons"; + +#adminmenu #{$ame-menu-prefix} .wp-menu-image:before { + font-family: "FontAwesome", sans-serif !important; + font-size: 18px !important; + //line-height: 20px !important; //Maybe, if user approves. +} + +//Override icons set as background images. +#adminmenu#adminmenu#adminmenu #{$ame-menu-prefix} .wp-menu-image { + background-image: none; +} + +#adminmenu .ame-submenu-icon .#{$fa-css-prefix} { + font-size: 18px; +} + +//Icon selector support. + +#ws_icon_selector .ws_icon_option .ws_icon_image.#{$fa-css-prefix} { + width: 26px; + height: 20px; + padding: 6px 2px 4px 2px; + text-align: center; + + font-size: 18px; + + &:before { + color: #85888c; + } +} + +.ws_select_icon .ws_icon_image.#{$fa-css-prefix} { + padding: 2px 2px 3px 2px; + + &:before { + font-family: "FontAwesome", sans-serif; + font-size: 18px; + margin-top: 2px; + + display: inline-block; + min-width: 20px; + } +} \ No newline at end of file diff --git a/extras/global-menu-color-template.txt b/extras/global-menu-color-template.txt new file mode 100644 index 0000000..c790881 --- /dev/null +++ b/extras/global-menu-color-template.txt @@ -0,0 +1,71 @@ +/* Admin Menu - global colors */ +#adminmenu > li { + background: $base-color; + /* Admin Menu: submenu */ + /* Admin Menu: current */ + /* Admin Menu: bubble */ } + #adminmenu > li a { + color: $text-color; } + #adminmenu > li div.wp-menu-image:before { + color: $icon-color; } + + #adminmenu > li a:hover, #adminmenu > li.menu-top:hover, #adminmenu > li.opensub > a.menu-top, #adminmenu > li > a.menu-top:focus { + color: $menu-highlight-text; } + #adminmenu > li.menu-top:hover, #adminmenu > li.opensub > a.menu-top, #adminmenu > li > a.menu-top:focus { + background-color: $menu-highlight-background; } + #adminmenu > li.menu-top:hover div.wp-menu-image:before, #adminmenu > li.menu-top > a:focus div.wp-menu-image:before, #adminmenu > li.opensub > a.menu-top div.wp-menu-image:before { + color: $menu-highlight-icon; } + + #adminmenu > li .wp-submenu, #adminmenu > li.wp-has-current-submenu .wp-submenu, #adminmenu > li.wp-has-current-submenu.opensub .wp-submenu, .folded #adminmenu > li.wp-has-current-submenu .wp-submenu + a.wp-has-current-submenu:focus + .wp-submenu { + background: $menu-submenu-background; } + #adminmenu > li.wp-has-submenu.wp-not-current-submenu.opensub:hover:after { + border-right-color: $menu-submenu-background; } + + #adminmenu > li .wp-submenu .wp-submenu-head { + color: $menu-submenu-text; } + #adminmenu > li .wp-submenu a, #adminmenu > li.wp-has-current-submenu .wp-submenu a, + #adminmenu > li a.wp-has-current-submenu:focus + .wp-submenu a, .folded #adminmenu > li.wp-has-current-submenu .wp-submenu a + #adminmenu > li.wp-has-current-submenu.opensub .wp-submenu a { + color: $menu-submenu-text; } + #adminmenu > li .wp-submenu a:focus, #adminmenu > li .wp-submenu a:hover, #adminmenu > li.wp-has-current-submenu .wp-submenu a:focus, #adminmenu > li.wp-has-current-submenu .wp-submenu a:hover, + #adminmenu > li a.wp-has-current-submenu:focus + .wp-submenu a:focus, + #adminmenu > li a.wp-has-current-submenu:focus + .wp-submenu a:hover, .folded #adminmenu > li.wp-has-current-submenu .wp-submenu a + #adminmenu > li.wp-has-current-submenu.opensub .wp-submenu a:focus, .folded #adminmenu > li.wp-has-current-submenu .wp-submenu a + #adminmenu > li.wp-has-current-submenu.opensub .wp-submenu a:hover { + color: $menu-submenu-focus-text; } + + #adminmenu > li .wp-submenu li.current a, + #adminmenu > li a.wp-has-current-submenu:focus + .wp-submenu li.current a, #adminmenu > li.wp-has-current-submenu.opensub .wp-submenu li.current a { + color: $menu-submenu-current-text; } + #adminmenu > li .wp-submenu li.current a:hover, #adminmenu > li .wp-submenu li.current a:focus, + #adminmenu > li a.wp-has-current-submenu:focus + .wp-submenu li.current a:hover, + #adminmenu > li a.wp-has-current-submenu:focus + .wp-submenu li.current a:focus, #adminmenu > li.wp-has-current-submenu.opensub .wp-submenu li.current a:hover, #adminmenu > li.wp-has-current-submenu.opensub .wp-submenu li.current a:focus { + color: $menu-submenu-focus-text; } + + #adminmenu > li.current a.menu-top, #adminmenu > li.wp-has-current-submenu a.wp-has-current-submenu, #adminmenu > li.wp-has-current-submenu .wp-submenu .wp-submenu-head, .folded #adminmenu > li.current.menu-top { + color: $menu-current-text; + background: $menu-current-background; } + + #adminmenu > li.wp-has-current-submenu div.wp-menu-image:before, + #adminmenu a.current:hover div.wp-menu-image:before, + #adminmenu > li.current div.wp-menu-image::before, + #adminmenu > li.wp-has-current-submenu a:focus div.wp-menu-image:before, + #adminmenu > li.wp-has-current-submenu.opensub div.wp-menu-image:before, + #adminmenu > li:hover div.wp-menu-image:before, + #adminmenu > li a:focus div.wp-menu-image:before, + #adminmenu > li.opensub div.wp-menu-image:before, + .ie8 #adminmenu > li.opensub div.wp-menu-image:before { + color: $menu-current-icon; } + + #adminmenu > li .awaiting-mod, + #adminmenu > li .update-plugins { + color: $menu-bubble-text; + background: $menu-bubble-background; } + #adminmenu > li .current a .awaiting-mod, + #adminmenu > li a.wp-has-current-submenu .update-plugins, #adminmenu > li:hover a .awaiting-mod, #adminmenu > li.menu-top:hover > a .update-plugins { + color: $menu-bubble-current-text; + background: $menu-bubble-current-background; } + +#adminmenuback, #adminmenuwrap, #adminmenu { + background-color: $base-color; } \ No newline at end of file diff --git a/extras/import-export/import-export.js b/extras/import-export/import-export.js new file mode 100644 index 0000000..633b6e7 --- /dev/null +++ b/extras/import-export/import-export.js @@ -0,0 +1,18 @@ +jQuery(function($) { + var $importForm = $('form.ame-unified-import-form').first(), + $importFile = $importForm.find('#ame-import-file-selector'), + $submitButton = $importForm.find(':submit'); + + //Enable the "next" button when the user selects a file. + $importFile.on('change', function () { + $submitButton.prop('disabled', !$importFile.val()); + }); + + if ( $importForm.is('#ame-import-step-2') ) { + var $importableModules = $importForm.find('.ame-importable-module'); + //Only enable the submit button when at least one module is selected. + $importableModules.change(function () { + $submitButton.prop('disabled', $importableModules.filter(':checked').length === 0); + }); + } +}); \ No newline at end of file diff --git a/extras/import-export/import-export.php b/extras/import-export/import-export.php new file mode 100644 index 0000000..96fa1e1 --- /dev/null +++ b/extras/import-export/import-export.php @@ -0,0 +1,580 @@ +wp_menu_editor = $menuEditor; + + add_action('admin_menu_editor-header', array($this, 'menu_editor_header'), 10, 2); + + add_filter('admin_menu_editor-tabs', array($this, 'add_import_export_tabs'), 30); + add_action('admin_menu_editor-section-import', array($this, 'display_import_tab')); + add_action('admin_menu_editor-section-export', array($this, 'display_export_tab')); + add_action('admin_menu_editor-clean_up_import', array($this, 'clean_up_import_data'), 10, 3); + + add_action('admin_menu_editor-register_scripts', array($this, 'register_scripts')); + foreach (array('import', 'export') as $tabSlug) { + add_action('admin_menu_editor-enqueue_scripts-' . $tabSlug, array($this, 'enqueue_tab_scripts')); + } + } + + public function menu_editor_header($action = '', $post = array()) { + //Handle universal export. + if ( $action === 'ame_export_settings' ) { + $this->handle_export_request($action, $post); + } + } + + public function export_data() { + $settings = array(); + + $globalOptions = array(); + foreach($this->exportable_global_options as $key) { + $globalOptions[$key] = $this->wp_menu_editor->get_plugin_option($key); + } + if ( !empty($globalOptions) ) { + $settings['global'] = $globalOptions; + } + + try { + $customMenu = $this->wp_menu_editor->load_custom_menu(); + if ( !empty($customMenu) ) { + $settings['admin-menu'] = ameMenu::add_format_header(ameMenu::compress($customMenu)); + } + } catch (InvalidMenuException $e) { + //Ignore it. We can still try to export other settings if this part fails. + } + + foreach ($this->wp_menu_editor->get_loaded_modules() as $module) { + if ( !($module instanceof ameModule) ) { + continue; + } + $id = $module->getModuleId(); + if ( empty($id) || isset($settings[$id]) ) { + continue; + } + + $exportedData = null; + if ( $module instanceof ameExportableModule ) { + $exportedData = $module->exportSettings(); + } else if ( $module instanceof amePersistentModule ) { + $exportedData = $module->loadSettings(); + } + + if ( $exportedData !== null ) { + $settings[$id] = $exportedData; + } + } + + $settings = apply_filters('admin_menu_editor-exported_data', $settings); + + $container = array( + 'format' => array( + 'name' => self::$export_container_format_name, + 'version' => self::$export_container_format_version, + ), + 'settings' => $settings, + ); + + return $container; + } + + /** + * @param array $container + * @param array|null $enabledModules + * @return array + */ + public function import_data($container, $enabledModules = null) { + $status = array_fill_keys(array_keys($container['settings']), array('success' => false, 'skipped' => true)); + + $settings = $container['settings']; + + //Import global plugin settings. + if ( + !empty($settings['global']) + && (!isset($enabledModules) || !empty($enabledModules['global'])) + ) { + $importableOptions = array_intersect_key( + $settings['global'], + array_fill_keys($this->exportable_global_options, true) + ); + if ( !empty($importableOptions) ) { + $this->wp_menu_editor->set_many_plugin_options($importableOptions); + $status['global'] = array( + 'success' => true, + 'message' => sprintf('OK, %d options imported', count($importableOptions)), + ); + } + } + + //Import the admin menu. + if ( + !empty($settings['admin-menu']) + && (!isset($enabledModules) || !empty($enabledModules['admin-menu'])) + ) { + try { + $loadedMenu = ameMenu::load_array($settings['admin-menu']); + $menuEditor = $this->wp_menu_editor; + $menuEditor->set_custom_menu($loadedMenu); + $status['admin-menu'] = array('success' => true); + } catch (Exception $ex) { + $status['admin-menu'] = array('success' => false, 'message' => $ex->getMessage()); + } + } + + //Import module settings. + foreach ($this->wp_menu_editor->get_loaded_modules() as $module) { + if ( !($module instanceof ameModule) ) { + continue; + } + $id = $module->getModuleId(); + if ( empty($id) || !isset($settings[$id]) ) { + continue; + } + + if ( isset($enabledModules) && empty($enabledModules[$id]) ) { + continue; + } + + $newSettings = $settings[$id]; + if ( $module instanceof ameExportableModule ) { + $module->importSettings($newSettings); + $status[$id] = array('success' => true); + } else if ( ($module instanceof amePersistentModule) && is_array($newSettings) && !empty($newSettings) ) { + $module->mergeSettingsWith($newSettings); + $module->saveSettings(); + $status[$id] = array('success' => true); + } + } + + return $status; + } + + private function handle_export_request($action, $post) { + check_admin_referer($action); + + $enabledOptions = array(); + foreach (ameUtils::get($post, 'ame-selected-modules', array()) as $option => $value) { + if ( !empty($value) && ($value !== 'off') ) { + $enabledOptions[$option] = true; + } + } + + //todo: Consider adding some buffer space at the end to avoid truncation when other plugins add superfluous whitespace. + + $data = $this->export_data(); + $data['settings'] = array_intersect_key($data['settings'], $enabledOptions); + $json = json_encode($data); + + $domain = @parse_url(get_bloginfo('url'), PHP_URL_HOST); + $fileName = sprintf('%s-AME-configuration(%s).json', $domain, date('Y-m-d')); + $fileName = apply_filters('admin_menu_editor-export_file_name', $fileName); + + header('Content-Description: File Transfer'); + header('Content-Disposition: attachment; filename=' . $fileName); + header('Content-Type: application/json; charset=' . get_option('blog_charset'), true); + header('Connection: close'); + + $size = strlen($json); + if ( ob_get_level() > 0 ) { + $size += ob_get_length(); + } + header('Content-Length: ' . $size); + + echo $json; + + wp_ob_end_flush_all(); + flush(); + exit; + } + + public function get_exportable_components() { + $options = array( + 'global' => array('label' => 'General plugin settings'), + 'admin-menu' => array('label' => 'Admin menu'), + ); + + foreach ($this->wp_menu_editor->get_loaded_modules() as $module) { + if ( !($module instanceof ameModule) ) { + continue; + } + $id = $module->getModuleId(); + if ( !isset($id) ) { + continue; + } + + if ( $module instanceof ameExportableModule ) { + $options[$id] = array( + 'label' => $module->getExportOptionLabel(), + 'description' => $module->getExportOptionDescription(), + 'module' => $module, + ); + } else if ( $module instanceof amePersistentModule ) { + $options[$id] = array('label' => $module->getTabTitle()); + } + } + + return $options; + } + + public function add_import_export_tabs($tabs) { + $tabs['import'] = 'Import'; + $tabs['export'] = 'Export'; + return $tabs; + } + + public function display_import_tab() { + if ( !empty($_REQUEST['action']) ) { + check_admin_referer($_REQUEST['action']); + } + + $step = isset($_REQUEST['step']) ? intval($_REQUEST['step']) : 1; + $step = min(max($step, 1), 3); + + $this->wp_menu_editor->display_settings_page_header(); + + if ( $step === 1 ) { + $formSubmitUrl = $this->get_import_tab_url(2); + $action = 'ame_upload_settings'; + $maxSize = wp_max_upload_size(); + $formattedSize = size_format($maxSize); + + printf('
', esc_attr($formSubmitUrl)); + ?> +

Import plugin settings

+

+ +
+ + + +

+ 'disabled')); + echo '
'; + } else if ( $step === 2 ) { + $this->do_import_step_2(); + } else if ( $step === 3 ) { + $this->do_import_step_3(); + } + + $this->wp_menu_editor->display_settings_page_footer(); + } + + private function get_import_tab_url($step = 1) { + return $this->wp_menu_editor->get_plugin_page_url(array( + 'sub_section' => 'import', + 'step' => $step, + )); + } + + private function do_import_step_2() { + $errorTemplate = '

%s

'; + $backButton = sprintf('

Go back

', esc_attr($this->get_import_tab_url())); + + if ( empty($_FILES['imported-data']) ) { + printf( + $errorTemplate, + 'No file uploaded. Please try again.' + . ' (If you get this error when trying to upload a large file, make sure that post_max_size is at least as high as upload_max_filesize in php.ini.)' + ); + echo $backButton; + return; + } + + $upload = $_FILES['imported-data']; + if ( $upload['error'] !== UPLOAD_ERR_OK ) { + $message = wsMenuEditorExtras::get_upload_error_message($upload['error']); + printf($errorTemplate, $message); + echo $backButton; + return; + } + + $size = filesize($upload['tmp_name']); + if ( $size <= 0 ) { + printf($errorTemplate, 'File is empty. Please upload a different file.'); + echo $backButton; + return; + } + + if ( !@is_uploaded_file($upload['tmp_name']) || !@is_file($upload['tmp_name']) ) { + printf($errorTemplate, 'That doesn\'t seem to be a valid uploaded file.'); + echo $backButton; + return; + } + + $content = file_get_contents($upload['tmp_name']); + if ( empty($content) || !preg_match('/^\s{0,30}+[\[\{]/', $content) ) { + printf($errorTemplate, 'File format is unknown or the data is corrupted.'); + echo $backButton; + return; + } + + $importedData = json_decode($content, true); + /** @noinspection PhpComposerExtensionStubsInspection */ + if ( function_exists('json_last_error') && (json_last_error() !== JSON_ERROR_NONE) ) { + printf($errorTemplate, 'File is not valid JSON.'); + echo $backButton; + return; + } + + if ( + !is_array($importedData) + || !isset($importedData['format'], $importedData['format']['name'], $importedData['format']['version']) + ) { + printf($errorTemplate, 'That is not an Admin Menu Editor Pro export file.'); + echo $backButton; + return; + } + + if ( ($importedData['format']['name'] !== self::$export_container_format_name) || empty($importedData['settings']) ) { + printf( + $errorTemplate, + 'Unsupported file format. Please upload a file that was downloaded from the "Export" tab in Admin Menu Editor Pro.' + ); + echo $backButton; + return; + } else if ( version_compare($importedData['format']['version'], self::$export_container_format_version, '>') ) { + printf( + $errorTemplate, + sprintf( + "Cannot import a file created by a newer version of the plugin. File format: '%s', newest supported format: '%s'.", + esc_html(strval($importedData['format']['version'])), + self::$export_container_format_version + ) + ); + echo $backButton; + return; + } + + $knownComponents = $this->get_exportable_components(); + $importableComponents = array_intersect_key($importedData['settings'], $knownComponents); + + //Move the file somewhere else to ensure it survives until the next step. + $tempFile = get_temp_dir() . sprintf('AME-import-file-%d-%.3f.json', get_current_user_id(), microtime(true)); + $metaKey = 'ame_import_' . time() . '_' . substr(sha1($tempFile), 0, 10); + move_uploaded_file($upload['tmp_name'], $tempFile); + add_user_meta(get_current_user_id(), $metaKey, wp_slash($tempFile), true); + + //Schedule a cleanup in case the user doesn't go through with the import. + wp_schedule_single_event( + time() + 12 * 3600, + 'admin_menu_editor-clean_up_import', + array(get_current_user_id(), $metaKey, $tempFile) + ); + + //Finally, we can get on with choosing which settings to import! + $action = 'ame_import_uploaded_settings'; + + printf('
', esc_attr($this->get_import_tab_url(3))); + + echo '

Choose what to import

'; + echo '
    '; + foreach (array_keys($importedData['settings']) as $key) { + $label = $key; + if ( isset($knownComponents[$key], $knownComponents[$key]['label']) ) { + $label = $knownComponents[$key]['label']; + } + + echo '
  • '; + printf( + '', + esc_attr($key), + isset($importableComponents[$key]) ? ' checked ' : '', + !isset($importableComponents[$key]) ? ' disabled ' : '', + $label + ); + + if ( !isset($importableComponents[$key]) ) { + echo ' (You may need to install an add-on to import this.)'; + } + + echo '
  • '; + } + echo '
'; + + echo ''; + wp_nonce_field($action); + submit_button('Import Settings'); + echo '
'; + } + + private function do_import_step_3() { + echo '
'; + + $errorTemplate = '

%s

'; + + if ( !$this->wp_menu_editor->current_user_can_edit_menu() ) { + printf($errorTemplate, 'Access denied.'); + return; + } + + if ( empty($_POST['meta-key']) ) { + printf($errorTemplate, 'One of the required fields is missing. Please try re-uploading the file.'); + return; + } + + $metaKey = strval($_POST['meta-key']); + if ( strpos($metaKey, 'ame_import_') !== 0 ) { + printf($errorTemplate, 'Invalid meta key. (This should never happen.)'); + return; + } + + $tempFile = get_user_meta(get_current_user_id(), $metaKey, true); + if ( empty($tempFile) ) { + printf($errorTemplate, 'Import data is missing. This may be a bug or a plugin conflict.'); + return; + } + + if ( !is_file($tempFile) || !is_readable($tempFile) ) { + printf($errorTemplate, 'File not found. This may be a bug.'); + return; + } + + $enabledOptions = array(); + foreach (ameUtils::get($_POST, 'ame-selected-modules', array()) as $option => $value) { + if ( !empty($value) && ($value !== 'off') ) { + $enabledOptions[$option] = true; + } + } + + if ( empty($enabledOptions) ) { + printf($errorTemplate, 'No options selected.'); + return; + } + + echo '

Importing settings...

'; + + $container = json_decode(file_get_contents($tempFile), true); + $moduleStatus = $this->import_data($container, $enabledOptions); + + foreach ($moduleStatus as $id => $status) { + if ( isset($status['message']) ) { + $message = esc_html($status['message']); + } else if ( !empty($status['success']) ) { + $message = 'OK'; + } else if ( !empty($status['skipped']) ) { + $message = 'Skipped'; + } else { + $message = 'Error'; + } + + printf('

%s: %s

', esc_html($id), $message); + } + + if ( @unlink($tempFile) ) { + echo '

Temporary file deleted.

'; + } + if ( delete_user_meta(get_current_user_id(), $metaKey) ) { + echo '

Database cleanup complete.

'; + } + + echo '

Done.

'; + } + + public function clean_up_import_data($userId, $metaKey, $tempFileName) { + $storedFileName = get_user_meta($userId, $metaKey, true); + delete_user_meta($userId, $metaKey); + + if ( empty($storedFileName) || !is_string($storedFileName) ) { + return; + } + + $extension = pathinfo($storedFileName, PATHINFO_EXTENSION); + if ( $storedFileName === $tempFileName ) { + if ( strtolower($extension) === 'json' ) { + @unlink($storedFileName); + } else { + trigger_error( + sprintf( + 'Admin Menu Editor Pro: Failed to clean up an import file because' + . ' it does not have the correct extension. Expected: "json", actual: "%s".', + $extension + ), + E_USER_WARNING + ); + } + } else { + trigger_error( + sprintf( + 'Admin Menu Editor Pro: Cannot delete an old import file because the stored file names do not match.' + . ' Database value: "%s", Cron job value: "%s"', + $storedFileName, + $tempFileName + ), + E_USER_WARNING + ); + } + } + + public function display_export_tab() { + $exportAction = 'ame_export_settings'; + $exportTabUrl = $this->wp_menu_editor->get_plugin_page_url(array( + 'sub_section' => 'export', + 'noheader' => '1', + )); + + $this->wp_menu_editor->display_settings_page_header(); + echo '

Choose what to export

'; + + printf('
', esc_attr($exportTabUrl)); + echo '
    '; + foreach ($this->get_exportable_components() as $key => $details) { + printf( + '
  • ', + esc_attr($key), + $details['label'] + ); + } + echo '
'; + + echo ''; + wp_nonce_field($exportAction); + submit_button('Download Export File', 'primary', 'submit', true); + + echo '
'; + + $this->wp_menu_editor->display_settings_page_footer(); + } + + public function register_scripts() { + wp_register_auto_versioned_script( + 'ws-ame-import-export', + plugins_url('extras/import-export/import-export.js', $this->wp_menu_editor->plugin_file), + array('jquery'), + true + ); + } + + public function enqueue_tab_scripts() { + wp_enqueue_script('ws-ame-import-export'); + } + + public static function get_instance($menuEditor = null) { + if ( self::$last_instance === null ) { + self::$last_instance = new self($menuEditor); + } + return self::$last_instance; + } +} \ No newline at end of file diff --git a/extras/ko-extensions.js b/extras/ko-extensions.js new file mode 100644 index 0000000..016d44c --- /dev/null +++ b/extras/ko-extensions.js @@ -0,0 +1,210 @@ +/// +/// +/// +/// +/* + * jQuery Dialog binding for Knockout. + * + * The main parameter of the binding is an instance of AmeKnockoutDialog. In addition to the standard + * options provided by jQuery UI, the binding supports two additional properties: + * + * isOpen - Required. A boolean observable that controls whether the dialog is open or closed. + * autoCancelButton - Set to true to add a WordPress-style "Cancel" button that automatically closes the dialog. + * + * Usage example: + *
...
+ */ +ko.bindingHandlers.ameDialog = { + init: function (element, valueAccessor) { + var dialog = ko.utils.unwrapObservable(valueAccessor()); + var _ = wsAmeLodash; + var options = dialog.options ? dialog.options : {}; + if (!dialog.hasOwnProperty('isOpen')) { + dialog.isOpen = ko.observable(false); + } + options = _.defaults(options, { + autoCancelButton: _.get(dialog, 'autoCancelButton', true), + autoOpen: dialog.isOpen(), + modal: true, + closeText: ' ' + }); + //Update isOpen when the dialog is opened or closed. + options.open = function (event, ui) { + dialog.isOpen(true); + if (dialog.onOpen) { + dialog.onOpen(event, ui); + } + }; + options.close = function (event, ui) { + dialog.isOpen(false); + if (dialog.onClose) { + dialog.onClose(event, ui); + } + }; + var buttons = (typeof options['buttons'] !== 'undefined') ? ko.utils.unwrapObservable(options.buttons) : []; + if (options.autoCancelButton) { + //In WordPress, the "Cancel" option is usually on the left side of the form/dialog/pop-up. + buttons.unshift({ + text: 'Cancel', + 'class': 'button button-secondary ame-dialog-cancel-button', + click: function () { + jQuery(this).closest('.ui-dialog-content').dialog('close'); + } + }); + } + options.buttons = buttons; + if (!dialog.hasOwnProperty('title') || (dialog.title === null)) { + dialog.title = ko.observable(_.get(options, 'title', null)); + } + else if (dialog.title()) { + options.title = dialog.title(); + } + //Do in a setTimeout so that applyBindings doesn't bind twice from element being copied and moved to bottom. + window.setTimeout(function () { + jQuery(element).dialog(options); + dialog.jQueryWidget = jQuery(element).dialog('widget'); + dialog.title(jQuery(element).dialog('option', 'title')); + dialog.title.subscribe(function (newTitle) { + jQuery(element).dialog('option', 'title', newTitle); + }); + if (ko.utils.unwrapObservable(dialog.isOpen)) { + jQuery(element).dialog('open'); + } + }, 0); + ko.utils.domNodeDisposal.addDisposeCallback(element, function () { + jQuery(element).dialog('destroy'); + }); + }, + update: function (element, valueAccessor) { + var dialog = ko.utils.unwrapObservable(valueAccessor()); + var $element = jQuery(element); + var shouldBeOpen = ko.utils.unwrapObservable(dialog.isOpen); + //Do nothing if the dialog hasn't been initialized yet. + var $widget = $element.dialog('instance'); + if (!$widget) { + return; + } + if (shouldBeOpen !== $element.dialog('isOpen')) { + $element.dialog(shouldBeOpen ? 'open' : 'close'); + } + } +}; +ko.bindingHandlers.ameOpenDialog = { + init: function (element, valueAccessor) { + var clickHandler = function (event) { + var dialogSelector = ko.utils.unwrapObservable(valueAccessor()); + //Do nothing if the dialog hasn't been initialized yet. + var $widget = jQuery(dialogSelector); + if (!$widget.dialog('instance')) { + return; + } + $widget.dialog('open'); + event.stopPropagation(); + }; + jQuery(element).on('click', clickHandler); + ko.utils.domNodeDisposal.addDisposeCallback(element, function () { + jQuery(element).off('click', clickHandler); + }); + } +}; +/* + * The ameEnableDialogButton binding enables the specified jQuery UI button only when the "enabled" parameter is true. + * + * It's tricky to bind directly to dialog buttons because they're created dynamically and jQuery UI places them + * outside dialog content. This utility binding takes a jQuery selector, letting you bind to a button indirectly. + * You can apply it to any element inside a dialog, or the dialog itself. + * + * Usage: + *
...
+ *
...
+ * + * If you omit the selector, the binding will enable/disable the first button that has the "button-primary" CSS class. + */ +ko.bindingHandlers.ameEnableDialogButton = { + init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { + //This binding could be applied before the dialog is initialised. In this case, the button won't exist yet. + //Wait for the dialog to be created and then update the button. + var dialogNode = jQuery(element).closest('.ui-dialog'); + if (dialogNode.length < 1) { + var body_1 = jQuery(element).closest('body'); + function setInitialButtonState() { + //Is this our dialog? + var dialogNode = jQuery(element).closest('.ui-dialog'); + if (dialogNode.length < 1) { + return; //Nope. + } + //Yes. Remove the event handler and update the binding. + body_1.off('dialogcreate', setInitialButtonState); + ko.bindingHandlers.ameEnableDialogButton.update(element, valueAccessor, allBindings, viewModel, bindingContext); + } + body_1.on('dialogcreate', setInitialButtonState); + //If our dialog never gets created, we still want to clean up the event handler eventually. + ko.utils.domNodeDisposal.addDisposeCallback(element, function () { + body_1.off('dialogcreate', setInitialButtonState); + }); + } + }, + update: function (element, valueAccessor) { + var _ = wsAmeLodash; + var options = ko.unwrap(valueAccessor()); + if (!_.isPlainObject(options)) { + options = { enabled: options }; + } + options = _.defaults(options, { + selector: '.button-primary:first', + enabled: false + }); + jQuery(element) + .closest('.ui-dialog') + .find('.ui-dialog-buttonset') + .find(options.selector) + .button('option', 'disabled', !ko.utils.unwrapObservable(options.enabled)); + } +}; +ko.bindingHandlers.ameColorPicker = { + init: function (element, valueAccessor) { + var valueUnwrapped = ko.unwrap(valueAccessor()); + var input = jQuery(element); + input.val(valueUnwrapped); + input.wpColorPicker({ + change: function (event, ui) { + var value = valueAccessor(); + value(ui.color.toString()); + }, + clear: function () { + var value = valueAccessor(); + value(''); + } + }); + }, + update: function (element, valueAccessor) { + var newValue = ko.unwrap(valueAccessor()); + if (typeof newValue !== 'string') { + newValue = ''; + } + if (newValue === '') { + //Programmatically click the "Clear" button. It's not elegant, but I haven't found + //a way to do this using the Iris API. + jQuery(element).closest('.wp-picker-input-wrap').find('.wp-picker-clear').trigger('click'); + } + else { + jQuery(element).iris('color', newValue); + } + } +}; +//A one-way binding for indeterminate checkbox states. +ko.bindingHandlers.indeterminate = { + update: function (element, valueAccessor) { + element.indeterminate = !!(ko.unwrap(valueAccessor())); + } +}; +//A "readonly" property binding for input and textarea elements. +ko.bindingHandlers.readonly = { + update: function (element, valueAccessor) { + var value = !!(ko.unwrap(valueAccessor())); + if (value !== element.readOnly) { + element.readOnly = value; + } + } +}; +//# sourceMappingURL=ko-extensions.js.map \ No newline at end of file diff --git a/extras/ko-extensions.js.map b/extras/ko-extensions.js.map new file mode 100644 index 0000000..90bfa76 --- /dev/null +++ b/extras/ko-extensions.js.map @@ -0,0 +1 @@ +{"version":3,"file":"ko-extensions.js","sourceRoot":"","sources":["ko-extensions.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/extras/ko-extensions.ts b/extras/ko-extensions.ts new file mode 100644 index 0000000..4bc3a5c --- /dev/null +++ b/extras/ko-extensions.ts @@ -0,0 +1,256 @@ +/// +/// +/// +/// + +declare var wsAmeLodash: _.LoDashStatic; + +interface AmeKnockoutDialog { + isOpen: KnockoutObservable; + + options?: Record; + autoCancelButton?: boolean; + jQueryWidget: JQuery; + title: KnockoutObservable; + + onOpen?(event, ui); + + onClose?(event, ui); +} + +/* + * jQuery Dialog binding for Knockout. + * + * The main parameter of the binding is an instance of AmeKnockoutDialog. In addition to the standard + * options provided by jQuery UI, the binding supports two additional properties: + * + * isOpen - Required. A boolean observable that controls whether the dialog is open or closed. + * autoCancelButton - Set to true to add a WordPress-style "Cancel" button that automatically closes the dialog. + * + * Usage example: + *
...
+ */ +ko.bindingHandlers.ameDialog = { + init: function (element, valueAccessor) { + const dialog = ko.utils.unwrapObservable(valueAccessor()) as AmeKnockoutDialog; + const _ = wsAmeLodash; + + let options = dialog.options ? dialog.options : {}; + if (!dialog.hasOwnProperty('isOpen')) { + dialog.isOpen = ko.observable(false); + } + + options = _.defaults(options, { + autoCancelButton: _.get(dialog, 'autoCancelButton', true), + autoOpen: dialog.isOpen(), + modal: true, + closeText: ' ' + }); + + //Update isOpen when the dialog is opened or closed. + options.open = function (event, ui) { + dialog.isOpen(true); + if (dialog.onOpen) { + dialog.onOpen(event, ui); + } + }; + options.close = function (event, ui) { + dialog.isOpen(false); + if (dialog.onClose) { + dialog.onClose(event, ui); + } + }; + + let buttons = (typeof options['buttons'] !== 'undefined') ? ko.utils.unwrapObservable(options.buttons) : []; + if (options.autoCancelButton) { + //In WordPress, the "Cancel" option is usually on the left side of the form/dialog/pop-up. + buttons.unshift({ + text: 'Cancel', + 'class': 'button button-secondary ame-dialog-cancel-button', + click: function () { + jQuery(this).closest('.ui-dialog-content').dialog('close'); + } + }); + } + options.buttons = buttons; + + if (!dialog.hasOwnProperty('title') || (dialog.title === null)) { + dialog.title = ko.observable(_.get(options, 'title', null)); + } else if (dialog.title()) { + options.title = dialog.title(); + } + + //Do in a setTimeout so that applyBindings doesn't bind twice from element being copied and moved to bottom. + window.setTimeout(function () { + jQuery(element).dialog(options); + + dialog.jQueryWidget = jQuery(element).dialog('widget'); + dialog.title(jQuery(element).dialog('option', 'title')); + + dialog.title.subscribe(function (newTitle) { + jQuery(element).dialog('option', 'title', newTitle); + }); + + if (ko.utils.unwrapObservable(dialog.isOpen)) { + jQuery(element).dialog('open'); + } + }, 0); + + + ko.utils.domNodeDisposal.addDisposeCallback(element, function () { + jQuery(element).dialog('destroy'); + }); + }, + + update: function (element, valueAccessor) { + const dialog = ko.utils.unwrapObservable(valueAccessor()) as AmeKnockoutDialog; + const $element = jQuery(element); + const shouldBeOpen = ko.utils.unwrapObservable(dialog.isOpen); + + //Do nothing if the dialog hasn't been initialized yet. + const $widget = $element.dialog('instance'); + if (!$widget) { + return; + } + + if (shouldBeOpen !== $element.dialog('isOpen')) { + $element.dialog(shouldBeOpen ? 'open' : 'close'); + } + } +}; + +ko.bindingHandlers.ameOpenDialog = { + init: function (element, valueAccessor) { + const clickHandler = function (event) { + const dialogSelector = ko.utils.unwrapObservable(valueAccessor()); + + //Do nothing if the dialog hasn't been initialized yet. + const $widget = jQuery(dialogSelector); + if (!$widget.dialog('instance')) { + return; + } + + $widget.dialog('open'); + event.stopPropagation(); + }; + jQuery(element).on('click', clickHandler); + + ko.utils.domNodeDisposal.addDisposeCallback(element, function () { + jQuery(element).off('click', clickHandler); + }); + } +}; + +/* + * The ameEnableDialogButton binding enables the specified jQuery UI button only when the "enabled" parameter is true. + * + * It's tricky to bind directly to dialog buttons because they're created dynamically and jQuery UI places them + * outside dialog content. This utility binding takes a jQuery selector, letting you bind to a button indirectly. + * You can apply it to any element inside a dialog, or the dialog itself. + * + * Usage: + *
...
+ *
...
+ * + * If you omit the selector, the binding will enable/disable the first button that has the "button-primary" CSS class. + */ +ko.bindingHandlers.ameEnableDialogButton = { + init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { + //This binding could be applied before the dialog is initialised. In this case, the button won't exist yet. + //Wait for the dialog to be created and then update the button. + const dialogNode = jQuery(element).closest('.ui-dialog'); + if (dialogNode.length < 1) { + const body = jQuery(element).closest('body'); + + function setInitialButtonState() { + //Is this our dialog? + let dialogNode = jQuery(element).closest('.ui-dialog'); + if (dialogNode.length < 1) { + return; //Nope. + } + + //Yes. Remove the event handler and update the binding. + body.off('dialogcreate', setInitialButtonState); + ko.bindingHandlers.ameEnableDialogButton.update(element, valueAccessor, allBindings, viewModel, bindingContext); + } + + body.on('dialogcreate', setInitialButtonState); + //If our dialog never gets created, we still want to clean up the event handler eventually. + ko.utils.domNodeDisposal.addDisposeCallback(element, function () { + body.off('dialogcreate', setInitialButtonState); + }); + } + }, + + update: function (element, valueAccessor) { + const _ = wsAmeLodash; + let options = ko.unwrap(valueAccessor()); + if (!_.isPlainObject(options)) { + options = {enabled: options}; + } + + options = _.defaults( + options, + { + selector: '.button-primary:first', + enabled: false + } + ); + + jQuery(element) + .closest('.ui-dialog') + .find('.ui-dialog-buttonset') + .find(options.selector) + .button('option', 'disabled', !ko.utils.unwrapObservable(options.enabled)); + } +}; + +ko.bindingHandlers.ameColorPicker = { + init: function (element, valueAccessor) { + let valueUnwrapped = ko.unwrap(valueAccessor()); + + const input = jQuery(element); + input.val(valueUnwrapped); + + input.wpColorPicker({ + change: function (event, ui) { + let value = valueAccessor(); + value(ui.color.toString()); + }, + clear: function () { + let value = valueAccessor(); + value(''); + } + }); + }, + update: function (element, valueAccessor) { + let newValue = ko.unwrap(valueAccessor()); + if (typeof newValue !== 'string') { + newValue = ''; + } + if (newValue === '') { + //Programmatically click the "Clear" button. It's not elegant, but I haven't found + //a way to do this using the Iris API. + jQuery(element).closest('.wp-picker-input-wrap').find('.wp-picker-clear').trigger('click'); + } else { + jQuery(element).iris('color', newValue); + } + } +}; + +//A one-way binding for indeterminate checkbox states. +ko.bindingHandlers.indeterminate = { + update: function (element, valueAccessor) { + element.indeterminate = !!(ko.unwrap(valueAccessor())); + } +}; + +//A "readonly" property binding for input and textarea elements. +ko.bindingHandlers.readonly = { + update: function (element, valueAccessor) { + const value = !!(ko.unwrap(valueAccessor())); + if (value !== element.readOnly) { + element.readOnly = value; + } + } +}; \ No newline at end of file diff --git a/extras/menu-color-dialog.php b/extras/menu-color-dialog.php new file mode 100644 index 0000000..ec428d9 --- /dev/null +++ b/extras/menu-color-dialog.php @@ -0,0 +1,100 @@ + \ No newline at end of file diff --git a/extras/menu-color-generator.php b/extras/menu-color-generator.php new file mode 100644 index 0000000..ccb1b90 --- /dev/null +++ b/extras/menu-color-generator.php @@ -0,0 +1,145 @@ + #aabbcc, ..]. + * @param array $extraSelectors + * @param string $templateFilename + * @return string + */ + public function getCss($menuId, $colors, $extraSelectors = array(), $templateFilename = null) { + if ( empty($templateFilename) ) { + $templateFilename = dirname(__FILE__) . '/menu-color-template.txt'; + } + $template = file_get_contents($templateFilename); + + $this->colors = $this->addComputedColors($colors); + + //Replace $variables with colors. + $css = preg_replace_callback( + '@(?P\w[\w\-]*)\s*:\s*\$(?P[\w\-]+)\s*;@i', + array($this, 'replaceVariable'), + $template + ); + + if ( !empty($menuId) ) { + //WordPress replaces special characters in the ID with dashes before output. + //See /wp-admin/menu-header.php, line #110 in WP 5.5-alpha. + $sanitizedId = preg_replace('|[^a-zA-Z0-9_:.]|', '-', $menuId); + $replacement = implode('', $extraSelectors) . '#' . $sanitizedId; + $css = str_replace('#menu-id-placeholder', $replacement, $css); + } + + return $css; + } + + /** + * Fill out missing color fields based on available colors. + * + * Many admin color schemes just define a small set of base colors and generate the rest with Sass. + * This method does the same thing in PHP. Based on /wp-admin/css/colors/_variables.scss from WP 3.9-beta2. + * + * @param array $colors + * @return array + */ + protected function addComputedColors($colors) { + if ( !empty($colors['base-color']) ) { + $baseColor = new phpColor($colors['base-color']); + + if ( empty($colors['icon-color']) ) { + $hsl = $baseColor->getHsl(); + $hsl['S'] = 0.07; + $hsl['L'] = 0.95; + $colors['icon-color'] = '#' . phpColor::hslToHex($hsl); + } + + if ( empty($colors['menu-submenu-text']) && !empty($colors['text-color']) ) { + $baseColor = new phpColor($colors['base-color']); + //WP sets the submenu text color to mix($base-color, $text-color, 30%), but phpColors expects + //the mixing amount to be -100% to +100%. So we need to convert from [0, 100] to [-100, 100]. + $colors['menu-submenu-text'] = '#' . $baseColor->mix($colors['text-color'], 2 * 30 - 100); + } + + if ( empty($colors['menu-submenu-background']) ) { + $baseColor = new phpColor($colors['base-color']); + $colors['menu-submenu-background'] = '#' . $baseColor->darken(7); + } + } + + $defaults = array( + 'menu-text' => 'text-color', + 'menu-icon' => 'icon-color', + 'menu-background' => 'base-color', + + 'menu-highlight-text' => 'text-color', + 'menu-highlight-icon' => 'text-color', + 'menu-highlight-background' => 'highlight-color', + + 'menu-current-text' => 'menu-highlight-text', + 'menu-current-icon' => 'menu-highlight-icon', + 'menu-current-background' => 'menu-highlight-background', + + 'menu-submenu-focus-text' => 'highlight-color', + 'menu-submenu-current-text' => 'text-color', + + 'menu-bubble-text' => 'text-color', + 'menu-bubble-background' => 'notification-color', + 'menu-bubble-current-text' => 'text-color', + 'menu-bubble-current-background' => 'menu-submenu-background', + ); + + foreach($defaults as $target => $source) { + if ( empty($colors[$target]) && !empty($colors[$source]) ) { + $colors[$target] = $colors[$source]; + } + } + + return $colors; + } + + /** + * Get the calculated icon colors from the last getCss() call. + * + * @return array Returns an array of icon colors in the format used by admin color schemes. + */ + public function getIconColorScheme() { + if ( empty($this->colors) ) { + throw new RuntimeException('You must call getCss() before calling ' . __METHOD__ . '()'); + } + + return array( + 'base' => !empty($this->colors['menu-icon']) ? $this->colors['menu-icon'] : '#a0a5aa', + 'focus' => !empty($this->colors['menu-highlight-icon']) ? $this->colors['menu-highlight-icon'] : '#00a0d2', + 'current' => !empty($this->colors['menu-current-icon']) ? $this->colors['menu-current-icon'] : '#fff', + ); + } + + /** + * Replace the $variable in "css-property: $variable" with the corresponding value from $this->colors. + * + * @param array $matches + * @return string + */ + protected function replaceVariable($matches) { + if ( !empty($this->colors[$matches['name']]) ) { + return str_replace( + '$' . $matches['name'], + $this->colors[$matches['name']], + $matches[0] + ); + } else { + return sprintf('/* $%s is not set. */', $matches['name']); + } + } + +} \ No newline at end of file diff --git a/extras/menu-color-template.txt b/extras/menu-color-template.txt new file mode 100644 index 0000000..3606bbf --- /dev/null +++ b/extras/menu-color-template.txt @@ -0,0 +1,58 @@ +li#menu-id-placeholder { + background: $base-color; +} + li#menu-id-placeholder a { + color: $text-color; } + li#menu-id-placeholder div.wp-menu-image:before { + color: $icon-color; } + + li#menu-id-placeholder a:hover, li#menu-id-placeholder.menu-top:hover, li#menu-id-placeholder.opensub > a.menu-top, li#menu-id-placeholder > a.menu-top:focus { + color: $menu-highlight-text; + background-color: $menu-highlight-background; + } + + li#menu-id-placeholder.menu-top:hover div.wp-menu-image:before, #adminmenu > li#menu-id-placeholder > a:focus div.wp-menu-image:before, li#menu-id-placeholder.opensub > a.menu-top div.wp-menu-image:before { + color: $menu-highlight-icon; } + + li#menu-id-placeholder .wp-submenu, li#menu-id-placeholder.wp-has-current-submenu .wp-submenu, li#menu-id-placeholder.wp-has-current-submenu.opensub .wp-submenu, .folded li#menu-id-placeholder.wp-has-current-submenu .wp-submenu + a.wp-has-current-submenu:focus + .wp-submenu { + background: $menu-submenu-background; } + + #adminmenu li#menu-id-placeholder.wp-has-submenu.wp-not-current-submenu.opensub:hover:after { + border-right-color: $menu-submenu-background; } + + li#menu-id-placeholder .wp-submenu .wp-submenu-head { + color: $menu-submenu-text; } + li#menu-id-placeholder .wp-submenu a, li#menu-id-placeholder.wp-has-current-submenu .wp-submenu a, + li#menu-id-placeholder a.wp-has-current-submenu:focus + .wp-submenu a, .folded li#menu-id-placeholder.wp-has-current-submenu .wp-submenu a + li#menu-id-placeholder.wp-has-current-submenu.opensub .wp-submenu a { + color: $menu-submenu-text; } + li#menu-id-placeholder .wp-submenu a:focus, li#menu-id-placeholder .wp-submenu a:hover, li#menu-id-placeholder.wp-has-current-submenu .wp-submenu a:focus, li#menu-id-placeholder.wp-has-current-submenu .wp-submenu a:hover, + li#menu-id-placeholder a.wp-has-current-submenu:focus + .wp-submenu a:focus, + li#menu-id-placeholder a.wp-has-current-submenu:focus + .wp-submenu a:hover, .folded li#menu-id-placeholder.wp-has-current-submenu .wp-submenu a + li#menu-id-placeholder.wp-has-current-submenu.opensub .wp-submenu a:focus, .folded li#menu-id-placeholder.wp-has-current-submenu .wp-submenu a + li#menu-id-placeholder.wp-has-current-submenu.opensub .wp-submenu a:hover { + color: $menu-submenu-focus-text; } + + li#menu-id-placeholder .wp-submenu li.current a, + li#menu-id-placeholder a.wp-has-current-submenu:focus + .wp-submenu li.current a, li#menu-id-placeholder.wp-has-current-submenu.opensub .wp-submenu li.current a { + color: $menu-submenu-current-text; } + li#menu-id-placeholder .wp-submenu li.current a:hover, li#menu-id-placeholder .wp-submenu li.current a:focus, + li#menu-id-placeholder a.wp-has-current-submenu:focus + .wp-submenu li.current a:hover, + li#menu-id-placeholder a.wp-has-current-submenu:focus + .wp-submenu li.current a:focus, li#menu-id-placeholder.wp-has-current-submenu.opensub .wp-submenu li.current a:hover, li#menu-id-placeholder.wp-has-current-submenu.opensub .wp-submenu li.current a:focus { + color: $menu-submenu-focus-text; } + + li#menu-id-placeholder.current a.menu-top, li#menu-id-placeholder.wp-has-current-submenu a.wp-has-current-submenu, li#menu-id-placeholder.wp-has-current-submenu .wp-submenu .wp-submenu-head, .folded li#menu-id-placeholder.current.menu-top { + color: $menu-current-text; + background: $menu-current-background; } + li#menu-id-placeholder.wp-has-current-submenu div.wp-menu-image:before, + li#menu-id-placeholder.current div.wp-menu-image:before { + color: $menu-current-icon; } + li#menu-id-placeholder .awaiting-mod, + li#menu-id-placeholder .update-plugins { + color: $menu-bubble-text; + background: $menu-bubble-background; } + li#menu-id-placeholder .current a .awaiting-mod, + li#menu-id-placeholder a.wp-has-current-submenu .update-plugins, li#menu-id-placeholder:hover a .awaiting-mod, li#menu-id-placeholder.menu-top:hover > a .update-plugins { + color: $menu-bubble-current-text; + background: $menu-bubble-current-background;} diff --git a/extras/menu-colors.css b/extras/menu-colors.css new file mode 100644 index 0000000..152018a --- /dev/null +++ b/extras/menu-colors.css @@ -0,0 +1,56 @@ +/* This is just a template for testing. */ +/* Admin Menu */ +#adminmenu > li { + background: #1e73be; + /* Admin Menu: submenu */ + /* Admin Menu: current */ + /* Admin Menu: bubble */ } + #adminmenu > li a { + color: #dd9933; } + #adminmenu > li div.wp-menu-image:before { + color: #dd9933; } + #adminmenu > li a:hover, #adminmenu > li.menu-top:hover, #adminmenu > li.opensub > a.menu-top, #adminmenu > li > a.menu-top:focus { + color: #dd9933; } + #adminmenu > li.menu-top:hover, #adminmenu > li.opensub > a.menu-top, #adminmenu > li > a.menu-top:focus { + background-color: #81d742; } + #adminmenu > li.menu-top:hover div.wp-menu-image:before, #adminmenu > li.menu-top > a:focus div.wp-menu-image:before, #adminmenu > li.opensub > a.menu-top div.wp-menu-image:before { + color: #dd9933; } + #adminmenu > li .wp-submenu, #adminmenu > li.wp-has-current-submenu .wp-submenu, #adminmenu > li.wp-has-current-submenu.opensub .wp-submenu, .folded #adminmenu > li.wp-has-current-submenu .wp-submenu, + #adminmenu > li a.wp-has-current-submenu:focus + .wp-submenu { + background: #19609f; } + #adminmenu > li.wp-has-submenu.wp-not-current-submenu.opensub:hover:after { + border-right-color: #19609f; } + #adminmenu > li .wp-submenu .wp-submenu-head { + color: #a48e5d; } + #adminmenu > li .wp-submenu a, #adminmenu > li.wp-has-current-submenu .wp-submenu a, + #adminmenu > li a.wp-has-current-submenu:focus + .wp-submenu a, .folded #adminmenu > li.wp-has-current-submenu .wp-submenu a, #adminmenu > li.wp-has-current-submenu.opensub .wp-submenu a { + color: #a48e5d; } + #adminmenu > li .wp-submenu a:focus, #adminmenu > li .wp-submenu a:hover, #adminmenu > li.wp-has-current-submenu .wp-submenu a:focus, #adminmenu > li.wp-has-current-submenu .wp-submenu a:hover, + #adminmenu > li a.wp-has-current-submenu:focus + .wp-submenu a:focus, + #adminmenu > li a.wp-has-current-submenu:focus + .wp-submenu a:hover, .folded #adminmenu > li.wp-has-current-submenu .wp-submenu a:focus, .folded #adminmenu > li.wp-has-current-submenu .wp-submenu a:hover, #adminmenu > li.wp-has-current-submenu.opensub .wp-submenu a:focus, #adminmenu > li.wp-has-current-submenu.opensub .wp-submenu a:hover { + color: #81d742; } + #adminmenu > li .wp-submenu li.current a, + #adminmenu > li a.wp-has-current-submenu:focus + .wp-submenu li.current a, #adminmenu > li.wp-has-current-submenu.opensub .wp-submenu li.current a { + color: #dd9933; } + #adminmenu > li .wp-submenu li.current a:hover, #adminmenu > li .wp-submenu li.current a:focus, + #adminmenu > li a.wp-has-current-submenu:focus + .wp-submenu li.current a:hover, + #adminmenu > li a.wp-has-current-submenu:focus + .wp-submenu li.current a:focus, #adminmenu > li.wp-has-current-submenu.opensub .wp-submenu li.current a:hover, #adminmenu > li.wp-has-current-submenu.opensub .wp-submenu li.current a:focus { + color: #81d742; } + #adminmenu > li.current a.menu-top, #adminmenu > li.wp-has-current-submenu a.wp-has-current-submenu, #adminmenu > li.wp-has-current-submenu .wp-submenu .wp-submenu-head, .folded #adminmenu > li.current.menu-top { + color: #dd9933; + background: #81d742; } + #adminmenu > li.wp-has-current-submenu div.wp-menu-image:before { + color: #dd9933; } + #adminmenu > li .awaiting-mod, + #adminmenu > li .update-plugins { + color: #dd9933; + background: #d54e21; } + #adminmenu > li .current a .awaiting-mod, + #adminmenu > li a.wp-has-current-submenu .update-plugins, #adminmenu > li:hover a .awaiting-mod, #adminmenu > li.menu-top:hover > a .update-plugins { + color: #dd9933; + background: #19609f; } + +#adminmenuback, #adminmenuwrap, #adminmenu { + background-color: #1e73be; } + +/*# sourceMappingURL=menu-colors.css.map */ diff --git a/extras/menu-colors.scss b/extras/menu-colors.scss new file mode 100644 index 0000000..0ab9333 --- /dev/null +++ b/extras/menu-colors.scss @@ -0,0 +1,154 @@ +/* This is just a template for testing. */ + +$base-color: #1e73be; +$highlight-color: #81d742; +$text-color: #dd9933; +$icon-color: #dd9933; + +// assign default value to all undefined variables +// core variables + +$base-color: #222 !default; +$text-color: #fff !default; +$icon-color: hsl( hue( $base-color ), 7%, 95% ) !default; +$highlight-color: #0074a2 !default; +$notification-color: #d54e21 !default; + +// admin menu & admin-bar + +$menu-text: $text-color !default; +$menu-icon: $icon-color !default; +$menu-background: $base-color !default; + +$menu-highlight-text: $text-color !default; +$menu-highlight-icon: $text-color !default; +$menu-highlight-background: $highlight-color !default; + +$menu-current-text: $menu-highlight-text !default; +$menu-current-icon: $menu-highlight-icon !default; +$menu-current-background: $menu-highlight-background !default; + +$menu-submenu-text: mix( $base-color, $text-color, 30% ) !default; +$menu-submenu-background: darken( $base-color, 7% ) !default; +$menu-submenu-background-alt: desaturate( lighten( $menu-background, 7% ), 7% ) !default; + +$menu-submenu-focus-text: $highlight-color !default; +$menu-submenu-current-text: $text-color !default; + +$menu-bubble-text: $text-color !default; +$menu-bubble-background: $notification-color !default; +$menu-bubble-current-text: $text-color !default; +$menu-bubble-current-background: $menu-submenu-background !default; + +$menu-id: "";// #test-hook-name; + +/* Admin Menu */ +#adminmenu > li#{$menu-id} { + background: $menu-background; + + a { + color: $menu-text; + } + + div.wp-menu-image:before { + color: $menu-icon; + } + + a:hover, + &.menu-top:hover, + &.opensub > a.menu-top, + & > a.menu-top:focus { + color: $menu-highlight-text; + } + + &.menu-top:hover, + &.opensub > a.menu-top, + & > a.menu-top:focus { + background-color: $menu-highlight-background; + } + + &.menu-top:hover div.wp-menu-image:before, + &.menu-top > a:focus div.wp-menu-image:before, + &.opensub > a.menu-top div.wp-menu-image:before { + color: $menu-highlight-icon; + } + + /* Admin Menu: submenu */ + + .wp-submenu, + &.wp-has-current-submenu .wp-submenu, + &.wp-has-current-submenu.opensub .wp-submenu, + .folded &.wp-has-current-submenu .wp-submenu, + a.wp-has-current-submenu:focus + .wp-submenu { + background: $menu-submenu-background; + } + + &.wp-has-submenu.wp-not-current-submenu.opensub:hover:after { + border-right-color: $menu-submenu-background; + } + + .wp-submenu .wp-submenu-head { + color: $menu-submenu-text; + } + + .wp-submenu a, + &.wp-has-current-submenu .wp-submenu a, + a.wp-has-current-submenu:focus + .wp-submenu a, + .folded &.wp-has-current-submenu .wp-submenu a, + &.wp-has-current-submenu.opensub .wp-submenu a { + color: $menu-submenu-text; + + &:focus, &:hover { + color: $menu-submenu-focus-text; + } + } + + + /* Admin Menu: current */ + + .wp-submenu li.current a, + a.wp-has-current-submenu:focus + .wp-submenu li.current a, + &.wp-has-current-submenu.opensub .wp-submenu li.current a { + color: $menu-submenu-current-text; + + &:hover, &:focus { + color: $menu-submenu-focus-text; + } + } + + &.current a.menu-top, + &.wp-has-current-submenu a.wp-has-current-submenu, + &.wp-has-current-submenu .wp-submenu .wp-submenu-head, + .folded &.current.menu-top { + color: $menu-current-text; + background: $menu-current-background; + } + + &.wp-has-current-submenu div.wp-menu-image:before { + color: $menu-current-icon; + } + + + /* Admin Menu: bubble */ + + .awaiting-mod, + .update-plugins { + color: $menu-bubble-text; + background: $menu-bubble-background; + } + + .current a .awaiting-mod, + a.wp-has-current-submenu .update-plugins, + &:hover a .awaiting-mod, + &.menu-top:hover > a .update-plugins { + color: $menu-bubble-current-text; + background: $menu-bubble-current-background; + } + +} + +@if $menu-id == "" { + #adminmenuback, #adminmenuwrap, #adminmenu { + background-color: $menu-background; + } +} diff --git a/extras/menu-editor-extras.js b/extras/menu-editor-extras.js new file mode 100644 index 0000000..b536dc4 --- /dev/null +++ b/extras/menu-editor-extras.js @@ -0,0 +1,183 @@ +'use strict'; +jQuery(function ($) { + const menuEditorNode = $('#ws_menu_editor'); + + $(document).on('filterMenuFields.adminMenuEditor', function (event, knownMenuFields, baseField) { + var scrollCheckboxField = $.extend({}, baseField, { + caption: 'Hide the frame scrollbar', + advanced: true, + type: 'checkbox', + standardCaption: false, + + visible: function (menuItem) { + return wsEditorData.wsMenuEditorPro && (AmeEditorApi.getFieldValue(menuItem, 'open_in') === 'iframe'); + }, + + display: function (menuItem, displayValue) { + if (displayValue === 0 || displayValue === '0') { + displayValue = false; + } + return displayValue; + } + }); + + //Insert this field after the "iframe_height" field. + //To do that, we back up and delete all properties. + var backup = $.extend({}, knownMenuFields); + $.each(backup, function (key) { + delete knownMenuFields[key]; + }); + //Then re-insert all of the properties in the desired order. + $.each(backup, function (key, value) { + knownMenuFields[key] = value; + if (key === 'iframe_height') { + knownMenuFields['is_iframe_scroll_disabled'] = scrollCheckboxField; + } + }); + }); + + $(document).on('filterVisibleMenuFields.adminMenuEditor', function (event, visibleMenuFieldsByType) { + visibleMenuFieldsByType['heading'] = { + file: false, + open_in: false, + hookname: false, + page_heading: false, + page_title: false, + is_always_open: false, + iframe_height: false, + template_id: false + }; + }); + + //The "Reset permissions" toolbar button. + menuEditorNode.on( + 'adminMenuEditor:action-reset-permissions', + function (event) { + event.preventDefault(); + + var selectedActor = AmeEditorApi.actorSelectorWidget.selectedActor; + if (selectedActor === null) { + alert( + 'This button resets all permissions for the selected role. ' + + 'To use it, click a role and then click this button again.' + ); + return; + } + + var displayName = AmeEditorApi.actorSelectorWidget.selectedDisplayName; + if (!confirm('Reset all permissions for "' + displayName + '"?')) { + return; + } + + //Reset CPT/taxonomy permissions and other directly granted capabilities. + var hadGrantedCaps = AmeCapabilityManager.resetActorCaps(selectedActor); + + //Reset permissions and visibility for all menu items. + AmeEditorApi.forEachMenuItem(function (menuItem, containerNode) { + var wasModified = hadGrantedCaps; + + //Reset the "hide without changing permissions" settings (aka "cosmetically hidden"). + if ( + menuItem.hidden_from_actor + && $.isPlainObject(menuItem.hidden_from_actor) + && menuItem.hidden_from_actor.hasOwnProperty(selectedActor) + ) { + delete menuItem.hidden_from_actor[selectedActor]; + wasModified = true; + } + + //Reset permissions. + if ( + menuItem.grant_access + && $.isPlainObject(menuItem.grant_access) + && menuItem.grant_access.hasOwnProperty(selectedActor) + ) { + delete menuItem.grant_access[selectedActor]; + wasModified = true; + } + + if (wasModified) { + AmeEditorApi.updateItemEditor(containerNode); + AmeEditorApi.updateParentAccessUi(containerNode); + } + }); + } + ); + + //"New heading" toolbar button. + let headingCount = 0; + menuEditorNode.on( + 'adminMenuEditor:action-new-heading', + /** + * @param event + * @param {JQuery|null} selectedItem + * @param {AmeEditorColumn} column + */ + function (event, selectedItem, column) { + headingCount++; + + //The new menu starts out rather bare + const randomId = AmeEditorApi.randomMenuId('heading-'); + let menu = $.extend(true, {}, wsEditorData.blankMenuItem, { + sub_type: 'heading', + menu_title: 'Heading ' + headingCount, + custom: true, + template_id: '', + css_class: 'menu-top ame-menu-heading-item', + file: randomId, + hookname: randomId, + access_level: 'read', + items: [] + }); + + column.outputItem(menu, selectedItem); + + $(document).trigger('adminMenuEditor:newHeadingCreated'); + } + ); + + //Three level menu confirmation dialog. + let $deepNestingDialog; + function initNestingDialog() { + if ($deepNestingDialog) { + return; + } + + $deepNestingDialog = $('#ws_ame_deep_nesting_dialog'); + $deepNestingDialog.dialog({ + autoOpen: false, + closeText: ' ', + draggable: false, + modal: true, + minHeight: 300, + minWidth: 400 + }); + } + + menuEditorNode.on( + 'adminMenuEditor:queryDeepNesting', + /** + * @param event + * @param {Array} queue + */ + function(event, queue) { + let isEnabled = $.Deferred(); + queue.push(isEnabled); + + initNestingDialog(); + $deepNestingDialog.dialog('open'); + + $deepNestingDialog.find('#ame_allow_deep_nesting').one('click', function() { + isEnabled.resolve(); + $deepNestingDialog.dialog('close'); + return false; + }); + + $deepNestingDialog.find('#ame_reject_deep_nesting').one('click', function() { + isEnabled.reject(); + $deepNestingDialog.dialog('close'); + return false; + }); + } + ); +}); \ No newline at end of file diff --git a/extras/menu-headings/ameMenuHeadingStyler.php b/extras/menu-headings/ameMenuHeadingStyler.php new file mode 100644 index 0000000..d6f5654 --- /dev/null +++ b/extras/menu-headings/ameMenuHeadingStyler.php @@ -0,0 +1,277 @@ +menuEditor = $menuEditor; + + //Put the heading stylesheet before the admin colors stylesheet to make it easier + //to override the heading colors for individual items (using the "Color scheme" field). + add_action('admin_enqueue_scripts', array($this, 'enqueueHeadingCustomizations'), 5); + add_action('wp_ajax_' . self::CSS_AJAX_ACTION, array($this, 'ajaxOutputCss')); + + add_action('admin_menu_editor-enqueue_styles-editor', array($this, 'enqueueEditorStyles')); + + if ( !empty($_COOKIE['ame-collapsed-menu-headings']) ) { + add_action('in_admin_header', array($this, 'outputRestorationTrigger')); + } + } + + public function enqueueHeadingCustomizations() { + $configId = $this->menuEditor->get_loaded_menu_config_id(); + $customMenu = $this->menuEditor->load_custom_menu($configId); + if ( empty($customMenu) || empty($customMenu['menu_headings']) ) { + return; + } + + //Use the modification timestamp for versioning and cache busting. + $currentTime = time(); + $modificationTime = (int)ameUtils::get($customMenu, 'menu_headings.modificationTimestamp', $currentTime); + if ( ($modificationTime <= 0) || ($modificationTime > ($currentTime + 10)) ) { + $modificationTime = $currentTime; + } + + wp_enqueue_style( + 'ame-menu-heading-style', + add_query_arg( + 'ame_config_id', + $this->menuEditor->get_loaded_menu_config_id(), + admin_url('admin-ajax.php?action=' . urlencode(self::CSS_AJAX_ACTION)) + ), + [], + $modificationTime + ); + } + + public function ajaxOutputCss() { + header('Content-Type: text/css'); + header('X-Content-Type-Options: nosniff'); + + $configId = null; + if ( isset($_GET['ame_config_id']) && !empty($_GET['ame_config_id']) ) { + $configId = (string)($_GET['ame_config_id']); + } + $customMenu = $this->menuEditor->load_custom_menu($configId); + + if ( empty($customMenu) || empty($customMenu['menu_headings']) ) { + echo '/* No heading settings found. */'; + return; + } + + $timestamp = (int)ameUtils::get($customMenu, 'menu_headings.modificationTimestamp', time()); + //Support the If-Modified-Since header. + $omitResponseBody = false; + if ( isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && !empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) ) { + $threshold = strtotime((string)$_SERVER['HTTP_IF_MODIFIED_SINCE']); + if ( $timestamp <= $threshold ) { + header('HTTP/1.1 304 Not Modified'); + $omitResponseBody = true; + } + } + + //Enable browser caching. + //Note that admin-ajax.php always adds HTTP headers that prevent caching, so we will + //override all of them even though we don't actually need some of them, like "Expires". + $cacheLifeTime = 30 * 24 * 3600; + header('Cache-Control: public, max-age=' . $cacheLifeTime); + header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $timestamp) . 'GMT'); + header('Expires: ' . gmdate('D, d M Y H:i:s ', $timestamp + $cacheLifeTime) . 'GMT'); + if ( $omitResponseBody ) { + exit(); + } + + $output = $this->generateCss($customMenu['menu_headings']); + echo $output; + exit; + } + + protected function generateCss($settings) { + $textColor = null; + $backgroundColor = null; + + //General heading appearance. + $linkStyles = ['cursor' => 'default']; + $hoverStyles = ['background-color' => 'transparent']; + + if ( ameUtils::get($settings, 'textColorType') === 'custom' ) { + $textColor = ameUtils::get($settings, 'textColor'); + if ( !empty($textColor) ) { + $linkStyles['color'] = $textColor; + $hoverStyles['color'] = $textColor; + } + } + if ( ameUtils::get($settings, 'backgroundColorType') === 'custom' ) { + $backgroundColor = ameUtils::get($settings, 'backgroundColor'); + if ( !empty($backgroundColor) ) { + $linkStyles['background-color'] = $backgroundColor; + $hoverStyles['background-color'] = $backgroundColor; + } + } + + if ( !empty($settings['fontWeight']) ) { + $linkStyles['font-weight'] = $settings['fontWeight']; + } + if ( !empty($settings['fontSizeValue']) && !empty($settings['fontSizeUnit']) ) { + $unit = ($settings['fontSizeUnit'] === 'percentage') ? '%' : $settings['fontSizeUnit']; + $linkStyles['font-size'] = $settings['fontSizeValue'] . $unit; + } + + $textTransform = ameUtils::get($settings, 'textTransform', 'none'); + if ( $textTransform !== 'none' ) { + $linkStyles['text-transform'] = $textTransform; + } + + //Bottom border. + $borderStyles = []; + $borderType = ameUtils::get($settings, 'bottomBorder.style', 'none'); + if ( $borderType !== 'none' ) { + $borderStyles = [ + 'display' => 'block', + 'height' => '1px', + 'width' => '100%', + 'content' => "''", + ]; + + $borderColor = ameUtils::get($settings, 'bottomBorder.color'); + if ( empty($borderColor) ) { + $borderColor = $textColor; + } + if ( empty($borderColor) ) { + $borderColor = '#eee'; //Menu text color in the default admin color scheme. + } + + $borderStyles['border-bottom'] = sprintf( + '%dpx %s %s', + ameUtils::get($settings, 'bottomBorder.width', 1), + $borderType, + $borderColor + ); + } + + //Padding can be automatic or custom. + $paddingType = ameUtils::get($settings, 'paddingType', 'auto'); + $paddingStyles = []; + if ( $paddingType === 'custom' ) { + foreach (['top', 'bottom', 'left', 'right'] as $side) { + $value = ameUtils::get($settings, 'padding' . ucfirst($side)); + if ( isset($value) && ($value > 0) ) { + $paddingStyles['padding-' . $side] = $value . 'px'; + } + } + } + + //The icon. + $iconStyles = []; + $collapsedIconStyles = []; + $iconVisibility = ameUtils::get($settings, 'iconVisibility', 'always'); + if ( $iconVisibility !== 'always' ) { + $iconStyles['display'] = 'none'; + + if ( $iconVisibility === 'if-collapsed' ) { + $collapsedIconStyles['display'] = 'unset'; + } + + if ( $paddingType === 'auto' ) { + $paddingStyles['padding-left'] = '9px'; + } + } + + $output = [ + $this->makeCssRule( + ['& a'], + $linkStyles + ), + $this->makeCssRule( + ['& .wp-menu-name'], + $paddingStyles + ), + $this->makeCssRule( + ['& .wp-menu-name::after'], + $borderStyles + ), + $this->makeCssRule( + ['&:hover', '&:active', '&:focus', '& a:hover', '&.menu-top a:active', '&.menu-top a:focus',], + $hoverStyles + ), + $this->makeCssRule( + ['& .wp-menu-image'], + $iconStyles + ), + $this->makeCssRule( + ['body.folded & .wp-menu-image'], + $collapsedIconStyles + ), + $this->makeCssRule( + ['&.ame-collapsible-heading a'], + ['cursor' => 'pointer'] + ), + //Remove the colored bar from item unless the heading is clickable. + $this->makeCssRule( + ['&:not(.ame-collapsible-heading) a'], + ['box-shadow' => 'none'] + ), + ]; + + //Some rules might be empty if we have no custom settings for their properties, + //let's filter them out. + $output = array_filter($output, function ($input) { + return ($input !== ''); + }); + + return implode("\n", $output); + } + + /** + * @param string[] $selectors + * @param string[] $properties + * @param string $parentSelector + * @return string + */ + protected function makeCssRule($selectors, $properties, $parentSelector = '#adminmenu li.ame-menu-heading-item') { + if ( empty($properties) || empty($selectors) ) { + return ''; + } + + if ( !empty($parentSelector) ) { + $selectors = array_map( + function ($selector) use ($parentSelector) { + return str_replace('&', $parentSelector, $selector); + }, + $selectors + ); + } + + $output = implode(",\n", $selectors) . " {\n"; + foreach ($properties as $name => $value) { + $output .= "\t" . $name . ': ' . $value . ";\n"; + } + $output .= "}\n"; + return $output; + } + + public function enqueueEditorStyles() { + wp_enqueue_auto_versioned_style( + 'ame-menu-heading-editor-css', + plugins_url('menu-headings-editor.css', __FILE__) + ); + } + + public function outputRestorationTrigger() { + ?> + + +
+
+ +
+

Text color

+
+

+ +

+

+ + + +

+ +
+
+ +
+

Background color

+
+

+ +

+

+ + + +

+ +
+
+ +
+

Font

+
+

+ + + + + +

+ +

+ +

+ +

+ +

+
+
+ +
+

Bottom border

+
+ 'No border', + 'solid' => 'Solid', + 'dashed' => 'Dashed', + 'double' => 'Double', + 'dotted' => 'Dotted', + ); + foreach ($styleOptions as $style => $label): + ?> +

+ +

+ +
+
+ +
+

+ px +
+ +
+

Show the icon

+ + +
+ +
+

Padding

+
+

+ +

+

+ +

+
+
+ + +
+ + +
+
+ +
+

Collapsible headings

+
+

+ +

+
+
+
+
+ +
+ + +
+ \ No newline at end of file diff --git a/extras/menu-headings/menu-headings.js b/extras/menu-headings/menu-headings.js new file mode 100644 index 0000000..fdbb1fb --- /dev/null +++ b/extras/menu-headings/menu-headings.js @@ -0,0 +1,263 @@ +/// +/// +/// +//Idea: Maybe code generator that generates both TS/KO stuff and PHP classes with validation? +var AmePlainMenuHeadingSettings = /** @class */ (function () { + function AmePlainMenuHeadingSettings() { + this.fontWeight = 'normal'; + this.fontSizeValue = 14; + this.fontSizeUnit = 'px'; + this.fontFamily = null; + this.textTransform = 'none'; + this.textColorType = 'default'; + this.textColor = ''; + this.backgroundColorType = 'default'; + this.backgroundColor = ''; + this.paddingTop = 8; + this.paddingBottom = 8; + this.paddingLeft = 36; + this.paddingRight = 8; + this.paddingType = 'auto'; + this.iconVisibility = 'if-collapsed'; + this.bottomBorder = { + style: 'none', + width: 1, + color: '' + }; + this.collapsible = false; + this.modificationTimestamp = 0; + } + return AmePlainMenuHeadingSettings; +}()); +var AmeMenuHeadingSettings = /** @class */ (function () { + function AmeMenuHeadingSettings() { + this.defaults = new AmePlainMenuHeadingSettings(); + this.bottomBorder = { + style: ko.observable(this.defaults.bottomBorder.style), + color: ko.observable(this.defaults.bottomBorder.color), + width: ko.observable(this.defaults.bottomBorder.width), + }; + this.backgroundColor = ko.observable(this.defaults.backgroundColor); + this.backgroundColorType = ko.observable(this.defaults.backgroundColorType); + this.fontFamily = ko.observable(this.defaults.fontFamily); + this.fontSizeUnit = ko.observable(this.defaults.fontSizeUnit); + this.fontSizeValue = ko.observable(this.defaults.fontSizeValue); + this.fontWeight = ko.observable(this.defaults.fontWeight); + this.iconVisibility = ko.observable(this.defaults.iconVisibility); + this.paddingBottom = ko.observable(this.defaults.paddingBottom); + this.paddingTop = ko.observable(this.defaults.paddingTop); + this.paddingLeft = ko.observable(this.defaults.paddingLeft); + this.paddingRight = ko.observable(this.defaults.paddingRight); + this.paddingType = ko.observable(this.defaults.paddingType); + this.textColor = ko.observable(this.defaults.textColor); + this.textTransform = ko.observable(this.defaults.textTransform); + this.textColorType = ko.observable(this.defaults.textColorType); + this.collapsible = ko.observable(this.defaults.collapsible); + this.modificationTimestamp = ko.observable(this.defaults.modificationTimestamp); + } + AmeMenuHeadingSettings.prototype.setAll = function (settings) { + var newSettings = wsAmeLodash.defaults({}, settings, this.defaults); + //The default object has all of the valid properties. We can use that to ensure that + //we only copy or create relevant properties. + var properties = Object.keys(this.defaults); + for (var i = 0; i < properties.length; i++) { + var key = properties[i]; + if (typeof this[key] === 'undefined') { + this[key] = ko.observable(null); + } + if (ko.isWriteableObservable(this[key])) { + this[key](newSettings[key]); + } + } + if (typeof settings['bottomBorder'] !== 'undefined') { + this.bottomBorder.style(settings.bottomBorder.style || this.defaults.bottomBorder.style); + this.bottomBorder.color((typeof settings.bottomBorder.color === 'string') + ? settings.bottomBorder.color + : this.defaults.bottomBorder.color); + var width = this.defaults.bottomBorder.width; + if (typeof settings.bottomBorder.width === 'string') { + width = parseInt(settings.bottomBorder.width, 10); + } + else if (typeof settings.bottomBorder.width === 'number') { + width = settings.bottomBorder.width; + } + this.bottomBorder.width(width); + } + }; + AmeMenuHeadingSettings.prototype.getAll = function () { + var result = {}; + var properties = Object.keys(this.defaults); + for (var i = 0; i < properties.length; i++) { + var key = properties[i]; + if (ko.isObservable(this[key])) { + result[key] = this[key](); + } + } + result.bottomBorder = { + style: this.bottomBorder.style(), + color: this.bottomBorder.color(), + width: this.bottomBorder.width() + }; + return result; + }; + AmeMenuHeadingSettings.prototype.resetToDefault = function () { + for (var key in this.defaults) { + if (!this.defaults.hasOwnProperty(key) || !ko.isObservable(this[key])) { + continue; + } + this[key](this.defaults[key]); + } + this.bottomBorder.color(this.defaults.bottomBorder.color); + this.bottomBorder.style(this.defaults.bottomBorder.style); + this.bottomBorder.width(this.defaults.bottomBorder.width); + }; + AmeMenuHeadingSettings.prototype.setDefaultFontSize = function (size, units) { + this.defaults.fontSizeValue = size; + this.defaults.fontSizeUnit = units; + }; + return AmeMenuHeadingSettings; +}()); +var AmeMenuHeadingSettingsScreen = /** @class */ (function () { + function AmeMenuHeadingSettingsScreen() { + this.currentSavedSettings = null; + this.dialog = null; + this.settings = new AmeMenuHeadingSettings(); + this.isOpen = ko.observable(false); + } + AmeMenuHeadingSettingsScreen.prototype.onConfirm = function () { + //Change color settings back to default if the user hasn't specified a color. + if (AmeMenuHeadingSettingsScreen.isEmptyColor(this.settings.textColor())) { + this.settings.textColorType('default'); + } + if (AmeMenuHeadingSettingsScreen.isEmptyColor(this.settings.backgroundColor())) { + this.settings.backgroundColorType('default'); + } + this.settings.modificationTimestamp(Math.round(Date.now() / 1000)); + this.currentSavedSettings = this.settings.getAll(); + this.closeDialog(); + }; + AmeMenuHeadingSettingsScreen.prototype.onCancel = function () { + this.discardChanges(); + this.closeDialog(); + }; + AmeMenuHeadingSettingsScreen.prototype.closeDialog = function () { + if (this.dialog) { + this.dialog.dialog('close'); + } + }; + AmeMenuHeadingSettingsScreen.isEmptyColor = function (color) { + if (typeof color !== 'string') { + return true; + } + return (color === ''); + }; + AmeMenuHeadingSettingsScreen.prototype.setSettings = function (settings) { + this.currentSavedSettings = settings; + if (settings === null) { + this.settings.resetToDefault(); + return; + } + this.settings.setAll(settings); + }; + AmeMenuHeadingSettingsScreen.prototype.getSettings = function () { + return this.currentSavedSettings; + }; + AmeMenuHeadingSettingsScreen.prototype.discardChanges = function () { + if (this.currentSavedSettings !== null) { + this.settings.setAll(this.currentSavedSettings); + } + else { + this.settings.resetToDefault(); + } + }; + AmeMenuHeadingSettingsScreen.prototype.setDialog = function ($dialog) { + this.dialog = $dialog; + }; + AmeMenuHeadingSettingsScreen.prototype.setDefaultFontSize = function (pixels) { + this.settings.setDefaultFontSize(pixels, 'px'); + }; + return AmeMenuHeadingSettingsScreen; +}()); +(function ($) { + var screen = null; + var currentSettings = null; + $(document) + .on('menuConfigurationLoaded.adminMenuEditor', function (event, menuConfiguration) { + currentSettings = menuConfiguration['menu_headings'] || null; + if (screen) { + screen.setSettings(currentSettings); + } + }) + .on('getMenuConfiguration.adminMenuEditor', function (event, menuConfiguration) { + var settings = (screen !== null) ? screen.getSettings() : currentSettings; + if (settings !== null) { + menuConfiguration['menu_headings'] = settings; + } + else { + delete menuConfiguration['menu_headings']; + } + }) + .on('adminMenuEditor:newHeadingCreated', function () { + //Populate heading settings with default values the first time the user creates a heading. + //This is necessary to make the PHP module output heading CSS. + if (!currentSettings && !screen) { + var defaultSettings = new AmeMenuHeadingSettings(); + currentSettings = defaultSettings.getAll(); + } + }); + $(function () { + function getDefaultMenuFontSize() { + var $menus = $('#adminmenumain #adminmenu li.menu-top') + .not('.wp-menu-separator') + .not('.ame-menu-heading-item') + .slice(0, 5) + .find('> a'); + var mostCommonSize = wsAmeLodash.chain($menus) + .countBy(function (menu) { + return $(menu).css('fontSize'); + }) + .pairs() + .sortBy(1) + .last() + .value(); + if (mostCommonSize && (mostCommonSize.length >= 1) && wsAmeLodash.isString(mostCommonSize[0])) { + var matches = mostCommonSize[0].match(/^(\d+)px$/i); + if (matches.length > 0) { + var result = parseInt(matches[1], 10); + if (result > 0) { + return result; + } + } + } + return 14; //Default menu font size in WP 5.6. + } + var headingDialog = $('#ws-ame-menu-heading-settings'); + var isDialogInitialized = false; + function initializeHeadingDialog() { + screen = new AmeMenuHeadingSettingsScreen(); + screen.setDefaultFontSize(getDefaultMenuFontSize()); + if (currentSettings !== null) { + screen.setSettings(currentSettings); + } + headingDialog.dialog({ + autoOpen: false, + closeText: ' ', + draggable: false, + modal: true, + minHeight: 400, + minWidth: 520 + }); + isDialogInitialized = true; + screen.setDialog(headingDialog); + ko.applyBindings(screen, headingDialog.get(0)); + } + $('#ws_edit_heading_styles').on('click', function () { + if (!isDialogInitialized) { + initializeHeadingDialog(); + } + screen.discardChanges(); + headingDialog.dialog('open'); + }); + }); +})(jQuery); +//# sourceMappingURL=menu-headings.js.map \ No newline at end of file diff --git a/extras/menu-headings/menu-headings.js.map b/extras/menu-headings/menu-headings.js.map new file mode 100644 index 0000000..6d56463 --- /dev/null +++ b/extras/menu-headings/menu-headings.js.map @@ -0,0 +1 @@ +{"version":3,"file":"menu-headings.js","sourceRoot":"","sources":["menu-headings.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/extras/menu-headings/menu-headings.ts b/extras/menu-headings/menu-headings.ts new file mode 100644 index 0000000..0fb83a7 --- /dev/null +++ b/extras/menu-headings/menu-headings.ts @@ -0,0 +1,346 @@ +/// +/// +/// + +declare var wsAmeLodash: _.LoDashStatic; + +type AmeCssFontWeight = number | 'bold' | 'lighter' | 'bolder' | 'normal' | 'inherit'; +type AmeFontSizeUnit = 'px' | 'percentage' | 'em'; +type AmeTextTransformOption = 'none' | 'capitalize' | 'uppercase' | 'lowercase' | 'full-width'; +type AmeHeadingIconVisibility = 'always' | 'never' | 'if-collapsed'; + +type AmeHeadingColorType = 'default' | 'custom'; +type AmeHeadingPaddingType = 'auto' | 'custom'; + +//Idea: Maybe code generator that generates both TS/KO stuff and PHP classes with validation? +class AmePlainMenuHeadingSettings { + fontWeight: AmeCssFontWeight = 'normal'; + fontSizeValue: number = 14; + fontSizeUnit: AmeFontSizeUnit = 'px'; + fontFamily: string = null; + + textTransform: AmeTextTransformOption = 'none'; + + textColorType: AmeHeadingColorType = 'default'; + textColor: string = ''; + backgroundColorType: AmeHeadingColorType = 'default'; + backgroundColor: string = ''; + + paddingTop: number = 8; + paddingBottom: number = 8; + paddingLeft: number = 36; + paddingRight: number = 8; + paddingType: AmeHeadingPaddingType = 'auto'; + + iconVisibility: AmeHeadingIconVisibility = 'if-collapsed'; + + bottomBorder: AmeCssBorderSettings = { + style: 'none', + width: 1, + color: '' + }; + + collapsible: boolean = false; + + modificationTimestamp: number = 0; +} + +interface IAmePlainMenuHeadingSettings extends AmePlainMenuHeadingSettings { +} + +class AmeMenuHeadingSettings implements AmeRecursiveObservablePropertiesOf { + protected defaults: IAmePlainMenuHeadingSettings; + + backgroundColor: KnockoutObservable; + bottomBorder: { + style: KnockoutObservable; + color: KnockoutObservable; + width: KnockoutObservable; + }; + fontFamily: KnockoutObservable; + fontSizeUnit: KnockoutObservable; + fontSizeValue: KnockoutObservable; + fontWeight: KnockoutObservable; + iconVisibility: KnockoutObservable; + paddingBottom: KnockoutObservable; + paddingLeft: KnockoutObservable; + paddingRight: KnockoutObservable; + paddingTop: KnockoutObservable; + paddingType: KnockoutObservable; + textColor: KnockoutObservable; + textTransform: KnockoutObservable; + backgroundColorType: KnockoutObservable; + textColorType: KnockoutObservable; + collapsible: KnockoutObservable; + modificationTimestamp: KnockoutObservable; + + constructor() { + this.defaults = new AmePlainMenuHeadingSettings(); + + this.bottomBorder = { + style: ko.observable(this.defaults.bottomBorder.style), + color: ko.observable(this.defaults.bottomBorder.color), + width: ko.observable(this.defaults.bottomBorder.width), + }; + + this.backgroundColor = ko.observable(this.defaults.backgroundColor); + this.backgroundColorType = ko.observable(this.defaults.backgroundColorType); + this.fontFamily = ko.observable(this.defaults.fontFamily); + this.fontSizeUnit = ko.observable(this.defaults.fontSizeUnit); + this.fontSizeValue = ko.observable(this.defaults.fontSizeValue); + this.fontWeight = ko.observable(this.defaults.fontWeight); + + this.iconVisibility = ko.observable(this.defaults.iconVisibility); + this.paddingBottom = ko.observable(this.defaults.paddingBottom); + this.paddingTop = ko.observable(this.defaults.paddingTop); + this.paddingLeft = ko.observable(this.defaults.paddingLeft); + this.paddingRight = ko.observable(this.defaults.paddingRight); + this.paddingType = ko.observable(this.defaults.paddingType); + + this.textColor = ko.observable(this.defaults.textColor); + this.textTransform = ko.observable(this.defaults.textTransform); + this.textColorType = ko.observable(this.defaults.textColorType); + + this.collapsible = ko.observable(this.defaults.collapsible); + this.modificationTimestamp = ko.observable(this.defaults.modificationTimestamp); + } + + setAll(settings: AmePlainMenuHeadingSettings) { + const newSettings = wsAmeLodash.defaults({}, settings, this.defaults); + + //The default object has all of the valid properties. We can use that to ensure that + //we only copy or create relevant properties. + const properties = Object.keys(this.defaults); + + for (let i = 0; i < properties.length; i++) { + const key = properties[i]; + if (typeof this[key] === 'undefined') { + this[key] = ko.observable(null); + } + if (ko.isWriteableObservable(this[key])) { + this[key](newSettings[key]); + } + } + + if (typeof settings['bottomBorder'] !== 'undefined') { + this.bottomBorder.style(settings.bottomBorder.style || this.defaults.bottomBorder.style); + this.bottomBorder.color( + (typeof settings.bottomBorder.color === 'string') + ? settings.bottomBorder.color + : this.defaults.bottomBorder.color + ); + + let width = this.defaults.bottomBorder.width; + if (typeof settings.bottomBorder.width === 'string') { + width = parseInt(settings.bottomBorder.width, 10); + } else if (typeof settings.bottomBorder.width === 'number') { + width = settings.bottomBorder.width; + } + this.bottomBorder.width(width); + } + } + + getAll(): AmePlainMenuHeadingSettings { + let result: Partial = {}; + const properties = Object.keys(this.defaults); + for (let i = 0; i < properties.length; i++) { + const key = properties[i]; + if (ko.isObservable(this[key])) { + result[key] = this[key](); + } + } + + result.bottomBorder = { + style: this.bottomBorder.style(), + color: this.bottomBorder.color(), + width: this.bottomBorder.width() + }; + + return result as AmePlainMenuHeadingSettings; + } + + resetToDefault() { + for (let key in this.defaults) { + if (!this.defaults.hasOwnProperty(key) || !ko.isObservable(this[key])) { + continue; + } + this[key](this.defaults[key]); + } + + this.bottomBorder.color(this.defaults.bottomBorder.color); + this.bottomBorder.style(this.defaults.bottomBorder.style); + this.bottomBorder.width(this.defaults.bottomBorder.width); + } + + setDefaultFontSize(size: number, units: AmeFontSizeUnit) { + this.defaults.fontSizeValue = size; + this.defaults.fontSizeUnit = units; + } +} + +class AmeMenuHeadingSettingsScreen { + private currentSavedSettings: AmePlainMenuHeadingSettings = null; + settings: AmeMenuHeadingSettings; + + dialog: JQuery = null; + isOpen: KnockoutObservable; + + constructor() { + this.settings = new AmeMenuHeadingSettings(); + this.isOpen = ko.observable(false); + } + + onConfirm() { + //Change color settings back to default if the user hasn't specified a color. + if (AmeMenuHeadingSettingsScreen.isEmptyColor(this.settings.textColor())) { + this.settings.textColorType('default'); + } + if (AmeMenuHeadingSettingsScreen.isEmptyColor(this.settings.backgroundColor())) { + this.settings.backgroundColorType('default'); + } + + this.settings.modificationTimestamp(Math.round(Date.now() / 1000)); + this.currentSavedSettings = this.settings.getAll(); + this.closeDialog(); + } + + onCancel() { + this.discardChanges(); + this.closeDialog(); + } + + protected closeDialog() { + if (this.dialog) { + this.dialog.dialog('close'); + } + } + + private static isEmptyColor(color: any): boolean { + if (typeof color !== 'string') { + return true; + } + return (color === ''); + } + + setSettings(settings: IAmePlainMenuHeadingSettings) { + this.currentSavedSettings = settings; + if (settings === null) { + this.settings.resetToDefault(); + return; + } + + this.settings.setAll(settings); + } + + getSettings(): IAmePlainMenuHeadingSettings { + return this.currentSavedSettings; + } + + discardChanges() { + if (this.currentSavedSettings !== null) { + this.settings.setAll(this.currentSavedSettings); + } else { + this.settings.resetToDefault(); + } + } + + setDialog($dialog: JQuery) { + this.dialog = $dialog; + } + + setDefaultFontSize(pixels: number) { + this.settings.setDefaultFontSize(pixels, 'px'); + } +} + +(function ($) { + let screen: AmeMenuHeadingSettingsScreen = null; + let currentSettings: IAmePlainMenuHeadingSettings = null; + + $(document) + .on('menuConfigurationLoaded.adminMenuEditor', function (event, menuConfiguration) { + currentSettings = menuConfiguration['menu_headings'] || null; + if (screen) { + screen.setSettings(currentSettings); + } + }) + .on('getMenuConfiguration.adminMenuEditor', function (event, menuConfiguration) { + const settings = (screen !== null) ? screen.getSettings() : currentSettings; + if (settings !== null) { + menuConfiguration['menu_headings'] = settings; + } else { + delete menuConfiguration['menu_headings']; + } + }) + .on('adminMenuEditor:newHeadingCreated', function () { + //Populate heading settings with default values the first time the user creates a heading. + //This is necessary to make the PHP module output heading CSS. + if (!currentSettings && !screen) { + const defaultSettings = new AmeMenuHeadingSettings(); + currentSettings = defaultSettings.getAll(); + } + }); + + $(function () { + function getDefaultMenuFontSize(): number { + const $menus = $('#adminmenumain #adminmenu li.menu-top') + .not('.wp-menu-separator') + .not('.ame-menu-heading-item') + .slice(0, 5) + .find('> a'); + + const mostCommonSize = wsAmeLodash.chain($menus) + .countBy(function (menu) { + return $(menu).css('fontSize'); + }) + .pairs() + .sortBy(1) + .last() + .value(); + + if (mostCommonSize && (mostCommonSize.length >= 1) && wsAmeLodash.isString(mostCommonSize[0])) { + let matches = mostCommonSize[0].match(/^(\d+)px$/i); + if (matches.length > 0) { + let result = parseInt(matches[1], 10); + if (result > 0) { + return result; + } + } + } + return 14; //Default menu font size in WP 5.6. + } + + const headingDialog = $('#ws-ame-menu-heading-settings'); + let isDialogInitialized = false; + + function initializeHeadingDialog() { + screen = new AmeMenuHeadingSettingsScreen(); + screen.setDefaultFontSize(getDefaultMenuFontSize()); + if (currentSettings !== null) { + screen.setSettings(currentSettings); + } + + headingDialog.dialog({ + autoOpen: false, + closeText: ' ', + draggable: false, + modal: true, + minHeight: 400, + minWidth: 520 + }); + isDialogInitialized = true; + screen.setDialog(headingDialog); + + ko.applyBindings(screen, headingDialog.get(0)); + } + + $('#ws_edit_heading_styles').on('click', function () { + if (!isDialogInitialized) { + initializeHeadingDialog(); + } + + screen.discardChanges(); + headingDialog.dialog('open'); + }); + }); +})(jQuery); \ No newline at end of file diff --git a/extras/modules/dashboard-widget-editor/ameCustomHtmlWidget.php b/extras/modules/dashboard-widget-editor/ameCustomHtmlWidget.php new file mode 100644 index 0000000..890ef7f --- /dev/null +++ b/extras/modules/dashboard-widget-editor/ameCustomHtmlWidget.php @@ -0,0 +1,61 @@ +setProperties($widgetProperties); + return $widget; + } + + protected function setProperties(array $properties) { + parent::setProperties($properties); + $this->content = isset($properties['content']) ? strval($properties['content']) : ''; + $this->filtersEnabled = isset($properties['filtersEnabled']) ? (bool)($properties['filtersEnabled']) : false; + } + + public function toArray() { + $properties = parent::toArray(); + $properties['content'] = $this->content; + $properties['filtersEnabled'] = $this->filtersEnabled; + return $properties; + } + + public function getCallback() { + return array($this, 'displayContent'); + } + + public function displayContent() { + $content = $this->content; + + if ( $this->filtersEnabled ) { + //The same filters as on the_content. + $content = wptexturize($content); + $content = convert_smilies($content); + $content = wpautop($content); + $content = shortcode_unautop($content); + //This filter is usually applied on content_save_pre. + $content = convert_invalid_entities($content); + } + + $content = do_shortcode($content); + + if ( $this->filtersEnabled ) { + //This filter is also applied on content_save_pre but at a late priority. + $content = balanceTags($content, true); + } + + echo $content; + } +} \ No newline at end of file diff --git a/extras/modules/dashboard-widget-editor/ameCustomRssWidget.php b/extras/modules/dashboard-widget-editor/ameCustomRssWidget.php new file mode 100644 index 0000000..27f1ea7 --- /dev/null +++ b/extras/modules/dashboard-widget-editor/ameCustomRssWidget.php @@ -0,0 +1,68 @@ +setProperties($widgetProperties); + return $widget; + } + + protected function setProperties(array $properties) { + parent::setProperties($properties); + + $this->feedUrl = isset($properties['feedUrl']) ? strval($properties['feedUrl']) : null; + $this->maxItems = isset($properties['maxItems']) ? max(1, min(intval($properties['maxItems']), 20)) : 5; + + $booleanProperties = array('showAuthor', 'showDate', 'showSummary'); + foreach ($booleanProperties as $name) { + if ( isset($properties[$name]) ) { + $this->$name = (bool)($properties[$name]); + } else { + $this->$name = true; + } + } + } + + public function toArray() { + $properties = parent::toArray(); + + $storedProperties = array('feedUrl', 'maxItems', 'showAuthor', 'showDate', 'showSummary'); + foreach ($storedProperties as $name) { + $properties[$name] = $this->$name; + } + + return $properties; + } + + public function getCallback() { + return array($this, 'displayContent'); + } + + public function displayContent() { + if ( empty($this->feedUrl) ) { + echo 'Error: No feed URL specified'; + return; + } + + wp_widget_rss_output(array( + 'url' => $this->feedUrl, + 'items' => $this->maxItems, + 'show_author' => $this->showAuthor ? 1 : 0, //Yes, this function actually expects int's and not booleans. + 'show_date' => $this->showDate ? 1 : 0, + 'show_summary' => $this->showSummary ? 1 : 0, + )); + } +} \ No newline at end of file diff --git a/extras/modules/dashboard-widget-editor/ameDashboardWidget.php b/extras/modules/dashboard-widget-editor/ameDashboardWidget.php new file mode 100644 index 0000000..52647c0 --- /dev/null +++ b/extras/modules/dashboard-widget-editor/ameDashboardWidget.php @@ -0,0 +1,228 @@ + boolean]. + */ + protected $grantAccess; + + /** + * @var string|null + */ + protected $widgetType = null; + + protected function __construct(array $properties) { + $this->id = $properties['id']; + + $properties = array_merge( + array( + 'title' => '', + 'location' => 'normal', + 'priority' => 'core', + 'callback' => null, + 'callbackArgs' => null, + ), + $properties + ); + + $this->setProperties($properties); + } + + public function toArray() { + return array( + 'id' => $this->id, + 'title' => $this->title, + 'location' => $this->location, + 'priority' => $this->priority, + 'isPresent' => $this->isPresent(), + 'grantAccess' => $this->grantAccess, + + 'widgetType' => $this->widgetType, + ); + } + + public static function fromArray($widgetProperties) { + $widgetType = isset($widgetProperties['widgetType']) ? strval($widgetProperties['widgetType']) : null; + if ( isset($widgetProperties['wrappedWidget']) ) { + return ameStandardWidgetWrapper::fromArray($widgetProperties); + } else if ( $widgetType === 'custom-html' ) { + return ameCustomHtmlWidget::fromArray($widgetProperties); + } else if ( $widgetType === 'custom-rss' ) { + return ameCustomRssWidget::fromArray($widgetProperties); + } else { + throw new RuntimeException('Unsupported dashboard widget type "' . $widgetType . '"'); + } + } + + protected function setProperties(array $properties) { + $this->title = $properties['title']; + $this->location = $properties['location']; + $this->priority = $properties['priority']; + $this->callback = isset($properties['callback']) ? $properties['callback'] : null; + $this->callbackArgs = isset($properties['callbackArgs']) ? $properties['callbackArgs'] : null; + $this->grantAccess = isset($properties['grantAccess']) ? $properties['grantAccess'] : array(); + } + + /* + * Basic getters + */ + + public function getId() { + return $this->id; + } + + public function getTitle() { + return $this->title; + } + + public function getLocation() { + return $this->location; + } + + public function getPriority() { + return $this->priority; + } + + public function getCallback() { + return $this->callback; + } + + public function getGrantAccess() { + return $this->grantAccess; + } + + /** + * @param array $grantAccess + * @return bool True if access was modified, false if the new settings are the same as the old settings. + */ + public function setGrantAccess($grantAccess) { + $oldAccess = isset($this->grantAccess) ? $this->grantAccess : array(); + + $isDifferent = !ameUtils::areAssocArraysEqual($oldAccess, $grantAccess); + if ( $isDifferent ) { + $this->grantAccess = $grantAccess; + } + return $isDifferent; + } + + public function isPresent() { + return true; + } + + public function canBeRegistered() { + return true; + } + + /** + * Is the specified user allowed to see/access this widget? + * + * @param WP_User $user + * @param WPMenuEditor $menuEditor + * @return bool + */ + public function isVisibleTo($user, $menuEditor = null) { + return self::userCanAccess($user, $this->grantAccess, $menuEditor); + } + + /** + * @param WP_User $user + * @param array $grantAccess + * @param WPMenuEditor|null $menuEditor + * @return bool + */ + public static function userCanAccess($user, $grantAccess, $menuEditor = null) { + //By default, any user can see any widget. + if ( empty($user) ) { + return true; + } + + $userActor = 'user:' . $user->user_login; + if ( isset($grantAccess[$userActor]) ) { + return $grantAccess[$userActor]; + } + + if ( is_multisite() && is_super_admin($user->ID) ) { + //Super Admin can access everything unless explicitly denied. + if ( isset($grantAccess['special:super_admin']) ) { + return $grantAccess['special:super_admin']; + } + return true; + } + + if ( !$menuEditor ) { + $menuEditor = $GLOBALS['wp_menu_editor']; + } + + //Allow access if at least one role has access. + $roles = $menuEditor->get_user_roles($user); + $hasAccess = null; + foreach ($roles as $roleId) { + if ( isset($grantAccess['role:' . $roleId]) ) { + $roleHasAccess = $grantAccess['role:' . $roleId]; + if ( is_null($hasAccess) ){ + $hasAccess = $roleHasAccess; + } else { + $hasAccess = $hasAccess || $roleHasAccess; + } + } else { + //By default, all roles have access. + $hasAccess = true; + } + } + + if ( $hasAccess !== null ) { + return $hasAccess; + } + return true; + } + + /** + * Register this widget with WordPress. + */ + public function addToDashboard() { + add_meta_box( + $this->getId(), + $this->getTitle(), + $this->getCallback(), + 'dashboard', + $this->getLocation(), + $this->getPriority(), + $this->callbackArgs + ); + } +} diff --git a/extras/modules/dashboard-widget-editor/ameStandardWidgetWrapper.php b/extras/modules/dashboard-widget-editor/ameStandardWidgetWrapper.php new file mode 100644 index 0000000..47bffd2 --- /dev/null +++ b/extras/modules/dashboard-widget-editor/ameStandardWidgetWrapper.php @@ -0,0 +1,179 @@ + '', + 'location' => '', + 'priority' => '', + ) + ); + parent::__construct($properties); + + $this->wrappedWidget = $widgetToWrap; + $this->wasPresent = $this->hasValidCallback(); + + if ( $this->hasValidCallback() ) { + $this->updateCallbackFileName(); + } + } + + /** + * @param array $properties + * @return boolean True if any properties were changed, false otherwise. + */ + public function updateWrappedWidget(array $properties) { + if ( $properties['id'] !== $this->id ) { + throw new LogicException(sprintf( + 'Widget ID mismatch. Expected: "%s", got: "%s".', + $this->id, + $properties['id'] + )); + } + + $oldProperties = $this->wrappedWidget; + $this->wrappedWidget = $properties; + if ( isset($properties['callback']) ) { + $this->callback = $properties['callback']; + } + if ( isset($properties['callbackArgs']) ) { + $this->callbackArgs = $properties['callbackArgs']; + } + + $changesDetected = false; + + //Update callback file name. + if ( $this->hasValidCallback() ) { + $changesDetected = $this->updateCallbackFileName() || $changesDetected; + } + + foreach(array('title', 'location', 'priority') as $key) { + if ( $oldProperties[$key] !== $properties[$key] ) { + $changesDetected = true; + break; + } + } + + $changesDetected = $this->setPresence($this->hasValidCallback()) || $changesDetected; + + return $changesDetected; + } + + /** + * Copy the wrapped widget and related properties from another wrapper to this wrapper. + * + * Only copies defaults. Doesn't change custom titles and so on. + * + * @param ameStandardWidgetWrapper $otherWidget + */ + public function copyWrappedWidgetFrom($otherWidget) { + $this->wrappedWidget = $otherWidget->wrappedWidget; + $this->wasPresent = $otherWidget->wasPresent; + $this->callbackFileName = $otherWidget->callbackFileName; + } + + private function updateCallbackFileName() { + $reflection = new AmeReflectionCallable($this->callback); + + $fileName = $reflection->getFileName(); + if ($fileName === false) { + $fileName = null; + } + + if ( $fileName !== $this->callbackFileName ) { + $this->callbackFileName = $fileName; + return true; //File name has changed. + } + return false; //No changes. + } + + public function getCallbackFileName() { + return $this->callbackFileName; + } + + public function getTitle() { + return $this->getProperty('title'); + } + + public function getLocation() { + return $this->getProperty('location'); + } + + public function getPriority() { + return $this->getProperty('priority'); + } + + private function getProperty($name) { + if ( $this->$name !== '' ) { + return $this->$name; + } + return $this->wrappedWidget[$name]; + } + + public function isPresent() { + return $this->wasPresent || $this->hasValidCallback(); + } + + public function canBeRegistered() { + return $this->hasValidCallback(); + } + + protected function hasValidCallback() { + return isset($this->callback) && is_callable($this->callback); + } + + public function setPresence($isPresent) { + $changed = ($this->wasPresent !== $isPresent); + $this->wasPresent = $isPresent; + return $changed; + } + + public static function fromArray($widgetProperties) { + $widget = new self(array_merge( + (array)($widgetProperties['wrappedWidget']), + array('id' => $widgetProperties['id']) + )); + $widget->setProperties($widgetProperties); + return $widget; + } + + protected function setProperties(array $properties) { + parent::setProperties($properties); + + $keysToCopy = array('wasPresent', 'callbackFileName'); + foreach($keysToCopy as $name) { + if (isset($properties[$name])) { + $this->$name = $properties[$name]; + } + } + } + + public function toArray() { + $result = parent::toArray(); + $result['wrappedWidget'] = array( + 'title' => $this->wrappedWidget['title'], + 'location' => $this->wrappedWidget['location'], + 'priority' => $this->wrappedWidget['priority'], + ); + + $result['wasPresent'] = $this->wasPresent; + $result['callbackFileName'] = $this->callbackFileName; + return $result; + } +} \ No newline at end of file diff --git a/extras/modules/dashboard-widget-editor/ameWidgetCollection.php b/extras/modules/dashboard-widget-editor/ameWidgetCollection.php new file mode 100644 index 0000000..0e84bce --- /dev/null +++ b/extras/modules/dashboard-widget-editor/ameWidgetCollection.php @@ -0,0 +1,404 @@ +convertMetaBoxesToProperties($dashboardMetaBoxes); + + //Update existing wrapped widgets, add new ones. + $previousWidget = null; + foreach($presentWidgets as $properties) { + $wrapper = $this->getWrapper($properties['id']); + if ($wrapper === null) { + $wrapper = new ameStandardWidgetWrapper($properties); + $this->insertAfter($wrapper, $previousWidget); + $changesDetected = true; + } else { + $changesDetected = $wrapper->updateWrappedWidget($properties) || $changesDetected; + } + + $previousWidget = $wrapper; + } + + //Flag wrappers that are on the list as present and the rest as not present. + foreach($this->getWrappedWidgets() as $widget) { + $changed = $widget->setPresence(array_key_exists($widget->getId(), $presentWidgets)); + $changesDetected = $changesDetected || $changed; + } + + return $changesDetected; + } + + /** + * Convert the input from the deeply nested array structure that's used by WP core + * to a flat [id => widget-properties] dictionary. + * + * @param array $metaBoxes + * @return array + */ + private function convertMetaBoxesToProperties($metaBoxes) { + $widgetProperties = array(); + + foreach($metaBoxes as $location => $priorities) { + foreach($priorities as $priority => $items) { + foreach($items as $standardWidget) { + //Skip removed widgets. remove_meta_box() replaces widgets that it removes with false. + //Also, The Events Calendar somehow creates a widget that's just "true"(?!), so we'll + //also skip all entries that are not arrays. + if (empty($standardWidget) || !is_array($standardWidget)) { + continue; + } + + $properties = array_merge( + array( + 'priority' => $priority, + 'location' => $location, + 'callbackArgs' => isset($standardWidget['args']) ? $standardWidget['args'] : null, + ), + $standardWidget + ); + $widgetProperties[$properties['id']] = $properties; + } + } + } + + return $widgetProperties; + } + + /** + * Get a wrapped widget by ID. + * + * @param string $id + * @return ameStandardWidgetWrapper|null + */ + protected function getWrapper($id) { + if (!array_key_exists($id, $this->widgets)) { + return null; + } + $widget = $this->widgets[$id]; + if ($widget instanceof ameStandardWidgetWrapper) { + return $widget; + } + return null; + } + + + /** + * Insert a widget after the $target widget. + * + * If $target is omitted or not in the collection, this method adds the widget to the end of the collection. + * + * @param ameDashboardWidget $widget + * @param ameDashboardWidget|null $target + */ + protected function insertAfter(ameDashboardWidget $widget, ameDashboardWidget $target = null) { + if (($target === null) || !array_key_exists($target->getId(), $this->widgets)) { + //Just put it at the bottom. + $this->widgets[$widget->getId()] = $widget; + } else { + $offset = array_search($target->getId(), array_keys($this->widgets)) + 1; + + $this->widgets = array_merge( + array_slice($this->widgets, 0, $offset, true), + array($widget->getId() => $widget), + array_slice($this->widgets, $offset, null, true) + ); + } + } + + /** + * Merge wrapped widgets from another collection into this one. + * + * @param ameWidgetCollection $otherCollection + */ + public function mergeWithWrappersFrom($otherCollection) { + $previousWidget = null; + + foreach($otherCollection->getWrappedWidgets() as $otherWidget) { + if (!$otherWidget->isPresent()) { + continue; + } + + $myWidget = $this->getWrapper($otherWidget->getId()); + if ($myWidget === null) { + $myWidget = $otherWidget; + $this->insertAfter($myWidget, $previousWidget); + } else { + $myWidget->copyWrappedWidgetFrom($otherWidget); + } + + $previousWidget = $myWidget; + } + } + + /** + * Get a list of all wrapped widgets. + * + * @return ameStandardWidgetWrapper[] + */ + protected function getWrappedWidgets() { + $results = array(); + foreach($this->widgets as $widget) { + if ($widget instanceof ameStandardWidgetWrapper) { + $results[] = $widget; + } + } + return $results; + } + + /** + * Get a list of wrapped widgets that are NOT present on the current site. + * + * @return ameStandardWidgetWrapper[] + */ + public function getMissingWrappedWidgets() { + $results = array(); + foreach($this->getWrappedWidgets() as $widget) { + if (!$widget->isPresent()) { + $results[] = $widget; + } + } + return $results; + } + + /** + * Get widgets that are present on the current site. + * + * @return ameDashboardWidget[] + */ + public function getPresentWidgets() { + $results = array(); + foreach($this->widgets as $widget) { + if ($widget->isPresent()) { + $results[] = $widget; + } + } + return $results; + } + + /** + * Get a widget by ID. + * + * @param string $id + * @return \ameDashboardWidget|null + */ + public function getWidgetById($id) { + if (array_key_exists($id, $this->widgets)) { + return $this->widgets[$id]; + } + return null; + } + + /** + * Remove a widget from the collection. + * + * @param string $widgetId + */ + public function remove($widgetId) { + unset($this->widgets[$widgetId]); + } + + /** + * Is the collection empty (zero widgets)? + * + * @return bool + */ + public function isEmpty() { + return count($this->widgets) === 0; + } + + public function toArray() { + $widgets = array(); + foreach($this->widgets as $widget) { + $widgets[] = $widget->toArray(); + } + + $output = array( + 'format' => array( + 'name' => self::FORMAT_NAME, + 'version' => self::FORMAT_VERSION, + ), + 'widgets' => $widgets, + 'welcomePanel' => $this->welcomePanel, + 'siteComponentHash' => $this->siteComponentHash, + ); + + return $output; + } + + /** + * @return string + */ + public function toJSON() { + return json_encode($this->toArray(), JSON_PRETTY_PRINT); + } + + /** + * Get the visibility settings for the "Welcome" panel. + * + * @return array [actorId => boolean] + */ + public function getWelcomePanelVisibility() { + if (isset($this->welcomePanel['grantAccess']) && is_array($this->welcomePanel['grantAccess'])) { + return $this->welcomePanel['grantAccess']; + } + return array(); + } + + /** + * @param array $grantAccess + */ + public function setWelcomePanelVisibility($grantAccess) { + $this->welcomePanel['grantAccess'] = $grantAccess; + } + + /** + * @param array $input + * @return self + */ + public static function fromArray($input) { + if ( !is_array($input) ) { + throw new ameInvalidWidgetDataException(sprintf( + 'Failed to decode widget data. Expected type: array, actual type: %s', + gettype($input) + )); + } + if ( + !isset($input['format']['name'], $input['format']['version']) + || ($input['format']['name'] !== self::FORMAT_NAME) + ) { + throw new ameInvalidWidgetDataException( + "Unknown widget format. The format.name or format.version key is missing or invalid." + ); + } + + if ( version_compare($input['format']['version'], self::FORMAT_VERSION) > 0 ) { + throw new ameInvalidWidgetDataException(sprintf( + "Can't import widget settings that were created by a newer version of the plugin. '. + 'Update the plugin and try again. (Newest supported format: '%s', input format: '%s'.)", + self::FORMAT_VERSION, + $input['format']['version'] + )); + } + + $collection = new self(); + foreach($input['widgets'] as $widgetProperties) { + $widget = ameDashboardWidget::fromArray($widgetProperties); + $collection->widgets[$widget->getId()] = $widget; + } + + if ( isset($input['welcomePanel'], $input['welcomePanel']['grantAccess']) ) { + $collection->welcomePanel = array( + 'grantAccess' => (array)($input['welcomePanel']['grantAccess']), + ); + } + + $collection->siteComponentHash = isset($input['siteComponentHash']) ? strval($input['siteComponentHash']) : ''; + return $collection; + } + + /** + * @param string $json + * @return self|null + */ + public static function fromJSON($json) { + $input = json_decode($json, true); + + if ($input === null) { + throw new ameInvalidJsonException('Cannot parse widget data. The input is not valid JSON.'); + } + + return self::fromArray($input); + } + + /** + * @return string + */ + public function toDbString() { + $serializedData = $this->toJSON(); + $tags = array(); + + if ( function_exists('gzcompress') ) { + /** @noinspection PhpComposerExtensionStubsInspection */ + $compressed = gzcompress($serializedData); + if ( is_string($compressed) ) { + $serializedData = $compressed; + $tags[] = 'gz'; + } + } + + if ( function_exists('base64_encode') ) { + $serializedData = base64_encode($serializedData); + $tags[] = '64'; + } + + $header = self::HEADER_PREFIX . sprintf('%4s', implode('', $tags)); + return $header . 'D' . $serializedData; + } + + /** + * @param string $serializedData + * @return self|null + */ + public static function fromDbString($serializedData) { + $fragment = substr($serializedData, 0, 32); + $prefixLength = strlen(self::HEADER_PREFIX); + + if ( substr($fragment, 0, $prefixLength) !== self::HEADER_PREFIX ) { + return self::fromJSON($serializedData); + } + $dataMarkerPos = strpos($fragment, 'D', $prefixLength); + if ( $dataMarkerPos === false ) { + return self::fromJSON($serializedData); + } + + $tags = str_split(substr($fragment, $prefixLength, $dataMarkerPos - $prefixLength), 2); + + $data = substr($serializedData, $dataMarkerPos + 1); + if ( in_array('64', $tags) ) { + $data = base64_decode($data); + } + if ( in_array('gz', $tags) ) { + if ( !function_exists('gzuncompress') ) { + throw new RuntimeException( + 'Cannot decompress dashboard widget data. This site may be missing the Zlib extension.' + ); + } + $data = gzuncompress($data); + } + + return self::fromJSON($data); + } +} + +class ameInvalidWidgetDataException extends RuntimeException {} \ No newline at end of file diff --git a/extras/modules/dashboard-widget-editor/ameWidgetEditor.php b/extras/modules/dashboard-widget-editor/ameWidgetEditor.php new file mode 100644 index 0000000..debd78a --- /dev/null +++ b/extras/modules/dashboard-widget-editor/ameWidgetEditor.php @@ -0,0 +1,504 @@ +requiredParam('widgetData') + ->permissionCallback(array($this, 'userCanEditWidgets')) + ->handler(array($this, 'ajaxExportWidgets')) + ->register(); + + ajaw_v1_CreateAction('ws-ame-import-widgets') + ->permissionCallback(array($this, 'userCanEditWidgets')) + ->handler(array($this, 'ajaxImportWidgets')) + ->register(); + + add_action( + 'admin_menu_editor-register_hideable_items', + array($this, 'registerHideableItems'), + 10, + 1 + ); + add_filter( + 'admin_menu_editor-save_hideable_items-d-widgets', + array($this, 'saveHideableItems'), + 10, + 2 + ); + } + + public function setupDashboard() { + global $wp_meta_boxes; + + $this->loadSettings(); + $changesDetected = $this->dashboardWidgets->merge($wp_meta_boxes['dashboard']); + + //Store new widgets and changed defaults. + //We want a complete list of widgets, so we only do this when an administrator is logged in. + //Admins usually can see everything. Other roles might be missing specific widgets. + if ( ($changesDetected || !empty($_GET['ame-cache-buster'])) && $this->userCanEditWidgets() ) { + //Remove wrapped widgets where the file no longer exists. + foreach($this->dashboardWidgets->getMissingWrappedWidgets() as $widget) { + $callbackFileName = $widget->getCallbackFileName(); + if ( !empty($callbackFileName) && !is_file($callbackFileName) ) { + $this->dashboardWidgets->remove($widget->getId()); + } + } + + $this->dashboardWidgets->siteComponentHash = $this->generateCompontentHash(); + $this->saveSettings(); + } + + //Remove all Dashboard widgets. + //Important: Using remove_meta_box() would prevent widgets being re-added. Clearing the array does not. + $wp_meta_boxes['dashboard'] = array(); + + //Re-add all widgets, this time with custom settings. + $currentUser = wp_get_current_user(); + foreach($this->dashboardWidgets->getPresentWidgets() as $widget) { + if ( $widget->isVisibleTo($currentUser, $this->menuEditor) ) { + $widget->addToDashboard(); + } else { + //Technically, this line is not required. It just ensures that other plugins can't recreate the widget. + remove_meta_box($widget->getId(), 'dashboard', $widget->getLocation()); + } + } + + //Optionally, hide the "Welcome to WordPress!" panel. It's technically not a widget, but users + //assume that it is, it looks similar, and it shows up in the same place. + $isWelcomePanelHidden = !ameDashboardWidget::userCanAccess( + $currentUser, + $this->dashboardWidgets->getWelcomePanelVisibility(), + $this->menuEditor + ); + if ( $isWelcomePanelHidden ) { + remove_action('welcome_panel', 'wp_welcome_panel'); + } + } + + public function enqueueTabScripts() { + //TODO: Remove this later, it's already registered in register_base_dependencies. + wp_register_auto_versioned_script( + 'knockout', + plugins_url('js/knockout.js', $this->menuEditor->plugin_file) + ); + + wp_register_auto_versioned_script( + 'ame-dashboard-widget', + plugins_url('dashboard-widget.js', __FILE__), + array('knockout', 'ame-lodash', 'ame-actor-manager',) + ); + + wp_register_auto_versioned_script( + 'ame-dashboard-widget-editor', + plugins_url('dashboard-widget-editor.js', __FILE__), + array( + 'ame-lodash', 'ame-dashboard-widget', 'knockout', 'ame-actor-selector', + 'ame-jquery-form', 'jquery-ui-dialog', 'ame-ko-extensions', + ) + ); + + //Automatically refresh the list of available dashboard widgets. + $this->loadSettings(); + $query = $this->menuEditor->get_query_params(); + $this->shouldRefreshWidgets = empty($query['ame-widget-refresh-done']) + && ( + //Refresh when the list hasn't been populated yet (usually on the first run). + $this->dashboardWidgets->isEmpty() + //Refresh when plugins/themes are activated or deactivated. + || ($this->dashboardWidgets->siteComponentHash !== $this->generateCompontentHash()) + ); + + if ( $this->shouldRefreshWidgets ) { + wp_enqueue_auto_versioned_script( + 'ame-refresh-widgets', + plugins_url('refresh-widgets.js', __FILE__), + array('jquery') + ); + + wp_localize_script( + 'ame-refresh-widgets', + 'wsWidgetRefresherData', + array( + 'editorUrl' => $this->getEditorUrl(array('ame-widget-refresh-done' => 1)), + 'dashboardUrl' => add_query_arg('ame-cache-buster', time() . '_' . rand(), admin_url('index.php')), + ) + ); + return; + } + + wp_enqueue_script('ame-dashboard-widget-editor'); + + $selectedActor = null; + if ( isset($query['selected_actor']) ) { + $selectedActor = strval($query['selected_actor']); + } + + wp_localize_script( + 'ame-dashboard-widget-editor', + 'wsWidgetEditorData', + array( + 'widgetSettings' => $this->dashboardWidgets->toArray(), + 'selectedActor' => $selectedActor, + 'isMultisite' => is_multisite(), + ) + ); + } + + public function enqueueTabStyles() { + wp_enqueue_auto_versioned_style( + 'ame-dashboard-widget-editor-css', + plugins_url('dashboard-widget-editor.css', __FILE__) + ); + } + + public function displaySettingsPage() { + if ( $this->shouldRefreshWidgets ) { + require dirname(__FILE__) . '/widget-refresh-template.php'; + } else { + parent::displaySettingsPage(); + } + } + + public function handleFormSubmission($action, $post = array()) { + //Note: We don't need to check user permissions here because plugin core already did. + if ( $action === 'save_widgets' ) { + check_admin_referer($action); + + $this->dashboardWidgets = ameWidgetCollection::fromJSON($post['data']); + $this->saveSettings(); + + $params = array('updated' => 1); + + //Re-select the same actor. + if ( !empty($post['selected_actor']) ) { + $params['selected_actor'] = strval($post['selected_actor']); + } + + wp_redirect($this->getEditorUrl($params)); + exit; + } + } + + private function getEditorUrl($queryParameters = array()) { + $queryParameters = array_merge( + array( + 'page' => 'menu_editor', + 'sub_section' => 'dashboard-widgets' + ), + $queryParameters + ); + return add_query_arg($queryParameters, admin_url('options-general.php')); + } + + public function ajaxExportWidgets($params) { + $exportData = $params['widgetData']; + + //The widget data must be valid JSON. + $json = json_decode($exportData); + if ( $json === null ) { + return new WP_Error('The widget data is not valid JSON.', 'invalid_json'); + } + + $fileName = sprintf( + '%1$s dashboard widgets (%2$s).json', + parse_url(get_site_url(), PHP_URL_HOST), + date('Y-m-d') + ); + + //Force file download. + header("Content-Description: File Transfer"); + header('Content-Disposition: attachment; filename="' . $fileName . '"'); + header("Content-Type: application/force-download"); + header("Content-Transfer-Encoding: binary"); + header("Content-Length: " . strlen($exportData)); + + //The three lines below basically disable caching. + header("Cache-control: private"); + header("Pragma: private"); + header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); + + echo $exportData; + exit(); + } + + public function ajaxImportWidgets() { + if ( empty($_FILES['widgetFile']) ) { + return new WP_Error('no_file', 'No file specified'); + } + + $importFile = $_FILES['widgetFile']; + if ( filesize($importFile['tmp_name']) > self::MAX_IMPORT_FILE_SIZE ){ + return new WP_Error( + 'file_too_large', + sprintf( + 'Import file too large. Maximum allowed size: %s bytes', + number_format_i18n(self::MAX_IMPORT_FILE_SIZE) + ) + ); + } + + //Check for general upload errors. + if ( $importFile['error'] !== UPLOAD_ERR_OK ) { + + $knownErrorCodes = array( + UPLOAD_ERR_INI_SIZE => sprintf( + 'The uploaded file exceeds the upload_max_filesize directive in php.ini. Limit: %s', + strval(ini_get('upload_max_filesize')) + ), + UPLOAD_ERR_FORM_SIZE => "The uploaded file exceeds the internal file size limit. Please contact the developer.", + UPLOAD_ERR_PARTIAL => "The file was only partially uploaded", + UPLOAD_ERR_NO_FILE => "No file was uploaded", + UPLOAD_ERR_NO_TMP_DIR => "Missing a temporary folder", + UPLOAD_ERR_CANT_WRITE => "Failed to write file to disk", + UPLOAD_ERR_EXTENSION => "File upload stopped by a PHP extension", + ); + + if ( array_key_exists($importFile['error'], $knownErrorCodes) ) { + $message = $knownErrorCodes[$importFile['error']]; + } else { + $message = 'Unknown upload error #' . $importFile['error']; + } + + return new WP_Error('internal_upload_error', $message); + } + + $fileContents = file_get_contents($importFile['tmp_name']); + + //Check if this file could plausibly contain an exported widget collection. + if ( strpos($fileContents, ameWidgetCollection::FORMAT_NAME) === false ) { + return new WP_Error('unknown_file_format', 'Unknown file format'); + } + + try { + $collection = ameWidgetCollection::fromJSON($fileContents); + } catch (ameInvalidJsonException $ex) { + return new WP_Error($ex->getCode(), $ex->getMessage()); + } catch (ameInvalidWidgetDataException $ex) { + return new WP_Error($ex->getCode(), $ex->getMessage()); + } + + //Merge standard widgets from the existing config with the imported config. + //Otherwise, we could end up with imported defaults that are incorrect for this site. + $collection->mergeWithWrappersFrom($this->loadSettings()); + + $collection->siteComponentHash = $this->generateCompontentHash(); + + return $collection->toArray(); + } + + private function loadSettings() { + if ( isset($this->dashboardWidgets) ) { + return $this->dashboardWidgets; + } + + $settings = $this->getScopedOption(self::OPTION_NAME, null); + if ( empty($settings) ) { + $this->dashboardWidgets = new ameWidgetCollection(); + } else { + $this->dashboardWidgets = ameWidgetCollection::fromDbString($settings); + } + return $this->dashboardWidgets; + } + + private function saveSettings() { + //Save per site or site-wide based on plugin configuration. + $settings = $this->dashboardWidgets->toDbString(); + $this->setScopedOption(self::OPTION_NAME, $settings); + } + + public function exportSettings() { + $dashboardWidgets = $this->loadSettings(); + if ( !$dashboardWidgets || $dashboardWidgets->isEmpty() ) { + return null; + } + return $dashboardWidgets->toArray(); + } + + public function importSettings($newSettings) { + if ( empty($newSettings) ) { + return; + } + + $this->loadSettings(); + $collection = ameWidgetCollection::fromArray($newSettings); + + //Merge standard widgets from the existing config with the imported config. + //Otherwise, we could end up with imported defaults that are incorrect for this site. + $collection->mergeWithWrappersFrom($this->dashboardWidgets); + + $collection->siteComponentHash = $this->generateCompontentHash(); + + $this->dashboardWidgets = $collection; + $this->saveSettings(); + } + + public function getExportOptionLabel() { + return 'Dashboard widgets'; + } + + public function getExportOptionDescription() { + return ''; + } + + public function userCanEditWidgets() { + return $this->menuEditor->current_user_can_edit_menu(); + } + + /** + * Calculate a hash of site components: WordPress version, active theme, and active plugins. + * + * Any of these components can register dashboard widgets, so the hash is useful for detecting + * when widgets might have changed. + * + * @return string + */ + private function generateCompontentHash() { + $components = array(); + + //WordPress. + $components[] = 'WordPress ' . (isset($GLOBALS['wp_version']) ? $GLOBALS['wp_version'] : 'unknown'); + + //Active theme. + $theme = wp_get_theme(); + if ( $theme && $theme->exists() ) { + $components[] = $theme->get_stylesheet() . ' : ' . $theme->get('Version'); + } + + //Active plugins. + $activePlugins = wp_get_active_and_valid_plugins(); + if ( is_multisite() ) { + $activePlugins = array_merge($activePlugins, wp_get_active_network_plugins()); + } + //The hash shouldn't depend on the order of plugins. + sort($activePlugins); + $components = array_merge($components, $activePlugins); + + return md5(implode('|' , $components)); + } + + /** + * @param \YahnisElsts\AdminMenuEditor\EasyHide\HideableItemStore $store + */ + public function registerHideableItems($store) { + $collection = $this->loadSettings(); + $widgets = $collection->getPresentWidgets(); + if ( empty($widgets) ) { + return; + } + + $cat = $store->getOrCreateCategory( + 'dashboard-widgets', + 'Dashboard Widgets', + null, + true, + 1, + 0 + ); + + foreach ($widgets as $widget) { + $store->addItem( + self::HIDEABLE_ITEM_PREFIX . $widget->getId(), + $this->sanitizeTitleForHiding($widget->getTitle()), + array($cat), + null, + $widget->getGrantAccess(), + 'd-widgets', + $widget->getId() + ); + } + + //Register the special "Welcome" pseudo-widget. + $store->addItem( + self::HIDEABLE_WELCOME_ITEM_ID, + 'Welcome', + array($cat), + null, + $collection->getWelcomePanelVisibility(), + 'd-widgets' + ); + } + + private function sanitizeTitleForHiding($title) { + if ( !is_string($title) ) { + return strval($title); + } + + /*$title = preg_replace( + '@]+class=[\'"](hide-if-js|postbox).++>@i', + '', + $title + );*/ + + return trim(strip_tags($title)); + } + + public function saveHideableItems($errors, $items) { + $collection = $this->loadSettings(); + $wasAnyWidgetModified = false; + + //Handle the special "Welcome" panel. + if ( isset($items[self::HIDEABLE_WELCOME_ITEM_ID]) ) { + $welcomePanelEnabled = ameUtils::get( + $items, + array(self::HIDEABLE_WELCOME_ITEM_ID, 'enabled'), + array() + ); + unset($items[self::HIDEABLE_WELCOME_ITEM_ID]); + + if ( !ameUtils::areAssocArraysEqual( + $collection->getWelcomePanelVisibility(), + $welcomePanelEnabled + ) ) { + $collection->setWelcomePanelVisibility($welcomePanelEnabled); + $wasAnyWidgetModified = true; + } + } + + foreach ($items as $id => $item) { + $widgetId = substr($id, strlen(self::HIDEABLE_ITEM_PREFIX)); + $enabled = !empty($item['enabled']) ? $item['enabled'] : array(); + + $widget = $collection->getWidgetById($widgetId); + if ( $widget !== null ) { + $modified = $widget->setGrantAccess($enabled); + $wasAnyWidgetModified = $wasAnyWidgetModified || $modified; + } + } + + if ( $wasAnyWidgetModified ) { + $this->saveSettings(); + } + + return $errors; + } +} \ No newline at end of file diff --git a/extras/modules/dashboard-widget-editor/dashboard-widget-editor-template.php b/extras/modules/dashboard-widget-editor/dashboard-widget-editor-template.php new file mode 100644 index 0000000..4193d86 --- /dev/null +++ b/extras/modules/dashboard-widget-editor/dashboard-widget-editor-template.php @@ -0,0 +1,227 @@ +
+ + + +
+
+ +
+ +
+
+
+

+ +   +

+
+
+ +
+ + + + + + +
+ + + + + + + + + + + +
+ +
+ Close + + | + Delete + +
+
+
+
+ +
+
+ + + + + + +
+ + 'click: addHtmlWidget' + ) + ); + + submit_button( + 'Add RSS Widget', + 'secondary', + 'ame-add-rss-widget', + false, + array( + 'data-bind' => 'click: addRssWidget' + ) + ); + ?> + + + + +
+ + + + + 'enable: isExportButtonEnabled') + ); ?> +
+ + + + + 'click: openImportDialog') + ); + ?> +
+ +
+ + +
+ +
+ + + + + + + +
\ No newline at end of file diff --git a/extras/modules/dashboard-widget-editor/dashboard-widget-editor.css b/extras/modules/dashboard-widget-editor/dashboard-widget-editor.css new file mode 100644 index 0000000..e65b8d5 --- /dev/null +++ b/extras/modules/dashboard-widget-editor/dashboard-widget-editor.css @@ -0,0 +1,214 @@ +@charset "UTF-8"; +#ame-dashboard-widget-editor #ws_actor_selector { + margin-top: 6px; +} + +#ame-dashboard-widgets { + width: 600px; + padding: 10px 8px 0 8px; + margin: 2px 0 0 0; + float: left; +} +#ame-dashboard-widgets:after { + display: block; + clear: both; + content: "."; + visibility: hidden; + height: 0; + font-size: 0; +} + +#ame-major-widget-actions { + padding: 10px 8px; + margin: 2px 0 0 10px; + width: 150px; + float: left; +} +#ame-major-widget-actions input.button.button-primary { + margin-top: 0; + margin-bottom: 21px; +} +#ame-major-widget-actions input.button { + width: 100%; + margin-top: 4px; +} +#ame-major-widget-actions #ame-export-widgets { + margin-top: 12px; +} + +#ame-dashboard-widgets, +#ame-major-widget-actions { + box-sizing: border-box; + background: white; + border: 1px solid #ccd0d4; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); +} + +.ame-dashboard-widget { + margin: 0 auto 10px; + position: relative; + box-sizing: border-box; +} + +.ame-widget-top { + position: relative; + background: #fafafa; + color: #23282D; + font-size: 13px; + font-weight: 600; + line-height: 1.4em; + border: 1px solid #ccd0d4; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); +} +.ame-widget-top h3 { + padding: 15px; + margin: 0; + font-size: 1em; + line-height: 1; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} +.ame-widget-top .ame-widget-title-action { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 40px; + height: 100%; + cursor: pointer; + text-align: center; + text-decoration: none; + outline: none; + color: #72777c; +} +.ame-widget-top .ame-widget-title-action:before { + display: inline-block; + content: "\f140"; + font: normal 20px/1 dashicons; + vertical-align: middle; +} +.ame-widget-top .ame-widget-title-action:after { + display: inline-block; + content: ""; + vertical-align: middle; + height: 100%; +} +.ame-widget-top .ame-widget-title-action:hover { + color: #23282d; +} +body.rtl .ame-widget-top .ame-widget-title-action { + left: 0; + right: auto; +} +.ame-widget-top .ame-widget-access-checkbox { + margin-right: 5px; +} +.ame-widget-top .ame-widget-flags { + position: absolute; + right: 40px; + top: 0; + bottom: 0; + height: 100%; + text-align: right; +} +.ame-widget-top .ame-widget-flags::after { + display: inline-block; + content: ""; + vertical-align: middle; + height: 100%; +} +.ame-widget-top .ame-widget-flag { + height: 20px; + width: 20px; + display: inline-block; + vertical-align: middle; +} +.ame-widget-top .ame-widget-flag::after { + display: inline-block; + width: 20px; + height: 20px; + font: normal 20px/1 dashicons; + vertical-align: baseline; + color: #666; +} + +.ame-missing-widget-flag::after { + content: "\f225"; +} + +.ame-widget-properties { + display: none; + background: white; + padding: 15px; + border: 1px solid #ccd0d4; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); + border-top: none; +} +.ame-widget-properties ame-widget-property { + display: block; + padding: 0; + margin-bottom: 1em; +} +.ame-widget-properties .ame-widget-property-name { + display: inline-block; +} +.ame-widget-properties input[type=text].ame-widget-property-value, +.ame-widget-properties input[type=url].ame-widget-property-value, +.ame-widget-properties select.ame-widget-property-value, +.ame-widget-properties textarea.ame-widget-property-value { + width: 100%; +} +.ame-widget-properties a.ame-delete-widget:hover { + color: #f00; + text-decoration: none; + border: none; +} + +.ame-open-dashboard-widget .ame-widget-properties { + display: block; +} +.ame-open-dashboard-widget .ame-widget-title-action:before { + content: "\f142"; +} +.ame-open-dashboard-widget .ame-widget-top { + box-shadow: none; +} + +.ame-widget-access-checkbox:indeterminate:before, +input[type=checkbox].ame-widget-access-checkbox:indeterminate:before { + content: "■"; + color: #1e8cbe; + margin: -3px 0 0 -1px; + font: 400 14px/1 dashicons; + float: left; + display: inline-block; + vertical-align: middle; + width: 16px; + -webkit-font-smoothing: antialiased; +} +@media screen and (max-width: 782px) { + .ame-widget-access-checkbox:indeterminate:before, +input[type=checkbox].ame-widget-access-checkbox:indeterminate:before { + height: 1.5625rem; + width: 1.5625rem; + line-height: 1.5625rem; + margin: -1px; + font-size: 18px; + font-family: unset; + font-weight: normal; + } +} + +/* + * Import dialog + */ +#ame-import-panel { + min-height: 70px; +} + +#ame-import-file-selector { + max-width: 100%; +} + +/*# sourceMappingURL=dashboard-widget-editor.css.map */ diff --git a/extras/modules/dashboard-widget-editor/dashboard-widget-editor.js b/extras/modules/dashboard-widget-editor/dashboard-widget-editor.js new file mode 100644 index 0000000..eecbc6f --- /dev/null +++ b/extras/modules/dashboard-widget-editor/dashboard-widget-editor.js @@ -0,0 +1,266 @@ +/// +/// +/// +/// +/// +/// +/// +var AmeDashboardWidgetEditor = /** @class */ (function () { + function AmeDashboardWidgetEditor(widgetSettings, selectedActor, isMultisite) { + if (selectedActor === void 0) { selectedActor = null; } + if (isMultisite === void 0) { isMultisite = false; } + var _this = this; + this.isMultisite = false; + this.newWidgetCounter = 0; + this.isMultisite = isMultisite; + this.actorSelector = new AmeActorSelector(AmeActors, true); + //Wrap the selected actor in a computed observable so that it can be used with Knockout. + var _selectedActor = ko.observable(this.actorSelector.selectedActor); + this.selectedActor = ko.computed({ + read: function () { + return _selectedActor(); + }, + write: function (newActor) { + _this.actorSelector.setSelectedActor(newActor); + } + }); + this.actorSelector.onChange(function (newSelectedActor) { + _selectedActor(newSelectedActor); + }); + //Re-select the previously selected actor, or select "All" (null) by default. + this.selectedActor(selectedActor); + this.widgets = ko.observableArray([]); + this.loadSettings(widgetSettings); + //These are only updated when saving or exporting widget settings. + this.widgetData = ko.observable(''); + this.widgetDataLength = ko.observable(0); + this.isExportButtonEnabled = ko.observable(true); + //Similarly, these are used when importing settings. + this.importState = ko.observable('start'); + this.uploadButtonEnabled = ko.observable(false); + this.importErrorHttpCode = ko.observable(0); + this.importErrorMessage = ko.observable(''); + this.importErrorResponse = ko.observable(''); + this.setupImportDialog(); + } + AmeDashboardWidgetEditor.prototype.loadSettings = function (widgetSettings) { + var _ = AmeDashboardWidgetEditor._; + this.widgets.removeAll(); + this.welcomePanel = new AmeWelcomeWidget(_.get(widgetSettings, 'welcomePanel', {}), this); + this.widgets.push(this.welcomePanel); + for (var i = 0; i < widgetSettings.widgets.length; i++) { + var properties = widgetSettings.widgets[i], widget = null; + if (properties.hasOwnProperty('wrappedWidget')) { + widget = new AmeStandardWidgetWrapper(properties, this); + } + else if (_.get(properties, 'widgetType') === 'custom-html') { + widget = new AmeCustomHtmlWidget(properties, this); + } + else if (_.get(properties, 'widgetType') === 'custom-rss') { + widget = new AmeCustomRssWidget(properties, this); + } + else { + throw { message: 'Unknown widget type', widgetProperties: properties }; + } + //On a normal site we don't have to worry about plugins that are active on some sites but not others, + //so we can just remove/filter out widgets that are not present. Just to be safe, however, these changes + //won't be saved unless the user saves the filtered widget list. + if (!this.isMultisite && !widget.isPresent && AmeDashboardWidgetEditor.autoCleanupEnabled) { + continue; + } + this.widgets.push(widget); + //The custom ID counter should be high enough not to clash with existing widgets. + if (widget.id.indexOf(AmeDashboardWidgetEditor.customIdPrefix) === 0) { + var idNum = parseInt(widget.id.substr(AmeDashboardWidgetEditor.customIdPrefix.length), 10); + if (!isNaN(idNum)) { + this.newWidgetCounter = Math.max(idNum, this.newWidgetCounter); + } + } + } + this.initialWidgetSettings = widgetSettings; + }; + // noinspection JSUnusedGlobalSymbols Used in Knockout templates. + AmeDashboardWidgetEditor.prototype.removeWidget = function (widget, event) { + var _this = this; + jQuery(event.target).closest('.ame-dashboard-widget').slideUp(300, function () { + _this.widgets.remove(widget); + }); + }; + // noinspection JSUnusedGlobalSymbols Used in Knockout templates. + AmeDashboardWidgetEditor.prototype.addHtmlWidget = function () { + this.newWidgetCounter++; + var widget = new AmeCustomHtmlWidget({ + id: AmeDashboardWidgetEditor.customIdPrefix + this.newWidgetCounter, + title: 'New Widget ' + this.newWidgetCounter + }, this); + //Expand the new widget. + widget.isOpen(true); + this.insertAfterWelcomePanel(widget); + }; + // noinspection JSUnusedGlobalSymbols Used in Knockout templates. + AmeDashboardWidgetEditor.prototype.addRssWidget = function () { + this.newWidgetCounter++; + var widget = new AmeCustomRssWidget({ + id: AmeDashboardWidgetEditor.customIdPrefix + this.newWidgetCounter, + title: 'New RSS Widget ' + this.newWidgetCounter + }, this); + //Expand the new widget. + widget.isOpen(true); + this.insertAfterWelcomePanel(widget); + }; + AmeDashboardWidgetEditor.prototype.insertAfterWelcomePanel = function (widget) { + //The "Welcome" panel is always first, so we can cheat for performance. + if (this.widgets.indexOf(this.welcomePanel) === 0) { + var welcomePanel = this.widgets.shift(); + this.widgets.unshift(widget); + this.widgets.unshift(welcomePanel); + } + else { + //But just in case it's not first for some odd reason, + //let's fall back to inserting the widget at the beginning. + this.widgets.unshift(widget); + } + }; + // noinspection JSUnusedGlobalSymbols Used in Knockout templates. + AmeDashboardWidgetEditor.prototype.saveChanges = function () { + var settings = this.getCurrentSettings(); + //Set the hidden form fields. + this.widgetData(JSON.stringify(settings)); + this.widgetDataLength(this.widgetData().length); + //Submit the form. + return true; + }; + AmeDashboardWidgetEditor.prototype.getCurrentSettings = function () { + var collectionFormatName = 'Admin Menu Editor dashboard widgets'; + var collectionFormatVersion = '1.1'; + var _ = AmeDashboardWidgetEditor._; + var settings = { + format: { + name: collectionFormatName, + version: collectionFormatVersion + }, + widgets: [], + welcomePanel: { + grantAccess: _.pick(this.welcomePanel.grantAccess.getAll(), function (hasAccess, actorId) { + //Remove "allow" settings for actors that can't actually see the panel. + return AmeActors.hasCapByDefault(actorId, 'edit_theme_options') || !hasAccess; + }), + }, + siteComponentHash: this.initialWidgetSettings.siteComponentHash + }; + _.forEach(_.without(this.widgets(), this.welcomePanel), function (widget) { + settings.widgets.push(widget.toPropertyMap()); + }); + return settings; + }; + // noinspection JSUnusedGlobalSymbols Used in Knockout templates. + AmeDashboardWidgetEditor.prototype.exportWidgets = function () { + var _this = this; + //Temporarily disable the export button to prevent accidental repeated clicks. + this.isExportButtonEnabled(false); + this.widgetData(JSON.stringify(this.getCurrentSettings())); + //Re-enable the export button after a few seconds. + window.setTimeout(function () { + _this.isExportButtonEnabled(true); + }, 3000); + //Explicitly allow form submission. + return true; + }; + AmeDashboardWidgetEditor.prototype.setupImportDialog = function () { + //Note to self: Refactor this as a separate view-model, perhaps. + var _this = this; + this.importDialog = jQuery('#ame-import-widgets-dialog'); + var importForm = this.importDialog.find('#ame-import-widgets-form'); + this.importDialog.dialog({ + autoOpen: false, + modal: true, + closeText: ' ', + open: function () { + importForm.resetForm(); + _this.importState('start'); + _this.uploadButtonEnabled(false); + } + }); + //jQuery moves the dialog to the end of the DOM tree, which puts it outside our KO root node. + //This means we must apply bindings directly to the dialog node. + ko.applyBindings(this, this.importDialog.get(0)); + //Enable the upload button only when the user selects a file. + importForm.find('#ame-import-file-selector').on('change', function (event) { + _this.uploadButtonEnabled(!!jQuery(event.target).val()); + }); + //This function displays unhandled server side errors. In theory, our upload handler always returns a well-formed + //response even if there's an error. In practice, stuff can go wrong in unexpected ways (e.g. plugin conflicts). + var handleUnexpectedImportError = function (xhr, errorMessage) { + //The server-side code didn't catch this error, so it's probably something serious + //and retrying won't work. + importForm.resetForm(); + _this.importState('unexpected-error'); + //Display error information. + _this.importErrorMessage(errorMessage); + _this.importErrorHttpCode(xhr.status); + _this.importErrorResponse((xhr.responseText !== '') ? xhr.responseText : '[Empty response]'); + }; + importForm.ajaxForm({ + dataType: 'json', + beforeSubmit: function (formData) { + //Check if the user has selected a file + for (var i = 0; i < formData.length; i++) { + if (formData[i].name === 'widget_file') { + if ((typeof formData[i].value === 'undefined') || !formData[i].value) { + alert('Select a file first!'); + return false; + } + } + } + _this.importState('uploading'); + _this.uploadButtonEnabled(false); + return true; + }, + success: function (data, status, xhr) { + if (!_this.importDialog.dialog('isOpen')) { + //Whoops, the user closed the dialog while the upload was in progress. + //Discard the response silently. + return; + } + if ((data === null) || (typeof data !== 'object')) { + handleUnexpectedImportError(xhr, 'Invalid response from server. Please check your PHP error log.'); + return; + } + if (typeof data.error !== 'undefined') { + alert(data.error.message || data.error.code); + //Let the user try again. + importForm.resetForm(); + _this.importState('start'); + } + if ((typeof data.widgets !== 'undefined') && data.widgets) { + //Lets load these widgets into the editor. + _this.loadSettings(data); + //Display a success message, then automatically close the window after a few moments. + _this.importState('complete'); + setTimeout(function () { + _this.importDialog.dialog('close'); + }, 700); + } + }, + error: function (xhr, status, errorMessage) { + handleUnexpectedImportError(xhr, errorMessage); + } + }); + this.importDialog.find('#ame-cancel-widget-import').on('click', function () { + _this.importDialog.dialog('close'); + }); + }; + // noinspection JSUnusedGlobalSymbols Used in Knockout templates. + AmeDashboardWidgetEditor.prototype.openImportDialog = function () { + this.importDialog.dialog('open'); + }; + AmeDashboardWidgetEditor._ = wsAmeLodash; + AmeDashboardWidgetEditor.autoCleanupEnabled = true; + AmeDashboardWidgetEditor.customIdPrefix = 'ame-custom-widget-'; + return AmeDashboardWidgetEditor; +}()); +jQuery(function () { + ameWidgetEditor = new AmeDashboardWidgetEditor(wsWidgetEditorData.widgetSettings, wsWidgetEditorData.selectedActor, wsWidgetEditorData.isMultisite); + ko.applyBindings(ameWidgetEditor, document.getElementById('ame-dashboard-widget-editor')); +}); +//# sourceMappingURL=dashboard-widget-editor.js.map \ No newline at end of file diff --git a/extras/modules/dashboard-widget-editor/dashboard-widget-editor.js.map b/extras/modules/dashboard-widget-editor/dashboard-widget-editor.js.map new file mode 100644 index 0000000..6a58baa --- /dev/null +++ b/extras/modules/dashboard-widget-editor/dashboard-widget-editor.js.map @@ -0,0 +1 @@ +{"version":3,"file":"dashboard-widget-editor.js","sourceRoot":"","sources":["dashboard-widget-editor.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/extras/modules/dashboard-widget-editor/dashboard-widget-editor.scss b/extras/modules/dashboard-widget-editor/dashboard-widget-editor.scss new file mode 100644 index 0000000..9310e22 --- /dev/null +++ b/extras/modules/dashboard-widget-editor/dashboard-widget-editor.scss @@ -0,0 +1,236 @@ +@import "../../../css/boxes"; + +$widgetBorderColor: $amePostboxBorderColor; + +#ame-dashboard-widget-editor #ws_actor_selector { + margin-top: 6px; +} + +#ame-dashboard-widgets { + width: 600px; + padding: 10px 8px 0 8px; + margin: 2px 0 0 0; + float: left; + + &:after { + display: block; + clear: both; + content: "."; + visibility: hidden; + height: 0; + font-size: 0; + } +} + +#ame-major-widget-actions { + padding: 10px 8px; + margin: 2px 0 0 10px; + width: 150px; + float: left; + + input.button.button-primary { + margin-top: 0; + margin-bottom: 21px; + } + + input.button { + width: 100%; + margin-top: 4px; + } + + #ame-export-widgets { + margin-top: 12px; + } +} + +#ame-dashboard-widgets, +#ame-major-widget-actions { + box-sizing: border-box; + + background: white; + border: 1px solid $widgetBorderColor; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); +} + +.ame-dashboard-widget { + margin: 0 auto 10px; + position: relative; + box-sizing: border-box; +} + +$titleActionWidth: 40px; + +.ame-widget-top { + position: relative; + background: #fafafa; + color: #23282D; + + font-size: 13px; + font-weight: 600; + line-height: 1.4em; + + border: 1px solid $widgetBorderColor; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); + + h3 { + padding: 15px; + margin: 0; + + font-size: 1em; + line-height: 1; + + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden;; + } + + .ame-widget-title-action { + position: absolute; + top: 0; + right: 0; + bottom: 0; + + width: $titleActionWidth; + height: 100%; + + cursor: pointer; + text-align: center; + + text-decoration: none; + outline: none; + color: #72777c; + + &:before { + display: inline-block; + content: '\f140'; //downward triangle + font: normal 20px/1 dashicons; + vertical-align: middle; + } + + &:after { + display: inline-block; + content: ""; + vertical-align: middle; + height: 100%; + } + + &:hover { + color: #23282d; + } + } + + body.rtl & .ame-widget-title-action { + left: 0; + right: auto; + } + + .ame-widget-access-checkbox { + margin-right: 5px; + } + + .ame-widget-flags { + position: absolute; + right: $titleActionWidth; + top: 0; + bottom: 0; + height: 100%; + text-align: right; + + &::after { + display: inline-block; + content: ""; + vertical-align: middle; + height: 100%; + } + } + + $flagSize: 20px; + + .ame-widget-flag { + height: $flagSize; + width: $flagSize; + display: inline-block; + vertical-align: middle; + + &::after { + display: inline-block; + width: $flagSize; + height: $flagSize; + font: normal 20px/1 dashicons; + vertical-align: baseline; + color: #666; + } + } +} + +.ame-missing-widget-flag::after { + content: '\f225'; +} + +.ame-widget-properties { + display: none; + background: white; + + padding: 15px; + + border: 1px solid $widgetBorderColor; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); + + border-top: none; + + ame-widget-property { + display: block; + padding: 0; + margin-bottom: 1em; + } + + .ame-widget-property-name { + display: inline-block; + } + + input[type="text"].ame-widget-property-value, + input[type="url"].ame-widget-property-value, + select.ame-widget-property-value, + textarea.ame-widget-property-value { + width: 100%; + } + + a.ame-delete-widget:hover { + color: #f00; + text-decoration: none; + border: none; + } +} + +.ame-open-dashboard-widget { + .ame-widget-properties { + display: block; + } + + .ame-widget-title-action:before { + content: '\f142'; //upward triangle + } + + .ame-widget-top { + box-shadow: none; + } +} + +@import "../../../css/indeterminate-checkbox"; + +.ame-widget-access-checkbox, +input[type="checkbox"].ame-widget-access-checkbox { + @include ame-indeterminate-checkbox; +} + +/* + * Import dialog + */ + +#ame-import-panel { + min-height: 70px; +} + +#ame-import-file-selector { + max-width: 100%; +} \ No newline at end of file diff --git a/extras/modules/dashboard-widget-editor/dashboard-widget-editor.ts b/extras/modules/dashboard-widget-editor/dashboard-widget-editor.ts new file mode 100644 index 0000000..2b20101 --- /dev/null +++ b/extras/modules/dashboard-widget-editor/dashboard-widget-editor.ts @@ -0,0 +1,355 @@ +/// +/// +/// +/// +/// +/// +/// + +declare var wsWidgetEditorData: any; +declare var ameWidgetEditor: AmeDashboardWidgetEditor; + +interface WidgetEditorSettings { + format: { + name: string, + version: string + }, + widgets: Array; + welcomePanel: { + grantAccess: AmeDictionary + }, + siteComponentHash: string; +} + +class AmeDashboardWidgetEditor { + private static _ = wsAmeLodash; + private static autoCleanupEnabled: boolean = true; + + widgets: KnockoutObservableArray; + + private welcomePanel: AmeWelcomeWidget; + + actorSelector: AmeActorSelector; + selectedActor: KnockoutComputed; + + public widgetData: KnockoutObservable; + public widgetDataLength: KnockoutObservable; + + public isExportButtonEnabled: KnockoutObservable; + + private initialWidgetSettings: WidgetEditorSettings; + private readonly isMultisite: boolean = false; + + private static customIdPrefix = 'ame-custom-widget-'; + private newWidgetCounter = 0; + + private importDialog: JQuery; + public importState: KnockoutObservable; + public uploadButtonEnabled: KnockoutObservable; + + public importErrorMessage: KnockoutObservable; + public importErrorHttpCode: KnockoutObservable; + public importErrorResponse: KnockoutObservable; + + + constructor(widgetSettings: WidgetEditorSettings, selectedActor: string = null, isMultisite: boolean = false) { + this.isMultisite = isMultisite; + + this.actorSelector = new AmeActorSelector(AmeActors, true); + + //Wrap the selected actor in a computed observable so that it can be used with Knockout. + let _selectedActor = ko.observable(this.actorSelector.selectedActor); + this.selectedActor = ko.computed({ + read: function () { + return _selectedActor(); + }, + write: (newActor: string) => { + this.actorSelector.setSelectedActor(newActor); + } + }); + this.actorSelector.onChange((newSelectedActor: string) => { + _selectedActor(newSelectedActor); + }); + + //Re-select the previously selected actor, or select "All" (null) by default. + this.selectedActor(selectedActor); + + this.widgets = ko.observableArray([]); + this.loadSettings(widgetSettings); + + //These are only updated when saving or exporting widget settings. + this.widgetData = ko.observable(''); + this.widgetDataLength = ko.observable(0); + this.isExportButtonEnabled = ko.observable(true); + + //Similarly, these are used when importing settings. + this.importState = ko.observable('start'); + this.uploadButtonEnabled = ko.observable(false); + + this.importErrorHttpCode = ko.observable(0); + this.importErrorMessage = ko.observable(''); + this.importErrorResponse = ko.observable(''); + + this.setupImportDialog(); + } + + loadSettings(widgetSettings: WidgetEditorSettings) { + const _ = AmeDashboardWidgetEditor._; + this.widgets.removeAll(); + + this.welcomePanel = new AmeWelcomeWidget(_.get(widgetSettings, 'welcomePanel', {}), this); + this.widgets.push(this.welcomePanel); + + for (let i = 0; i < widgetSettings.widgets.length; i++) { + let properties = widgetSettings.widgets[i], + widget = null; + + if (properties.hasOwnProperty('wrappedWidget')) { + widget = new AmeStandardWidgetWrapper(properties, this); + } else if (_.get(properties, 'widgetType') === 'custom-html') { + widget = new AmeCustomHtmlWidget(properties, this); + } else if (_.get(properties, 'widgetType') === 'custom-rss') { + widget = new AmeCustomRssWidget(properties, this); + } else { + throw {message: 'Unknown widget type', widgetProperties: properties}; + } + + //On a normal site we don't have to worry about plugins that are active on some sites but not others, + //so we can just remove/filter out widgets that are not present. Just to be safe, however, these changes + //won't be saved unless the user saves the filtered widget list. + if (!this.isMultisite && !widget.isPresent && AmeDashboardWidgetEditor.autoCleanupEnabled) { + continue; + } + + this.widgets.push(widget); + + //The custom ID counter should be high enough not to clash with existing widgets. + if (widget.id.indexOf(AmeDashboardWidgetEditor.customIdPrefix) === 0) { + let idNum = parseInt(widget.id.substr(AmeDashboardWidgetEditor.customIdPrefix.length), 10); + if (!isNaN(idNum)) { + this.newWidgetCounter = Math.max(idNum, this.newWidgetCounter); + } + } + } + + this.initialWidgetSettings = widgetSettings; + } + + // noinspection JSUnusedGlobalSymbols Used in Knockout templates. + removeWidget(widget: AmeDashboardWidget, event) { + jQuery(event.target).closest('.ame-dashboard-widget').slideUp(300, () => { + this.widgets.remove(widget); + }); + } + + // noinspection JSUnusedGlobalSymbols Used in Knockout templates. + addHtmlWidget() { + this.newWidgetCounter++; + + let widget = new AmeCustomHtmlWidget({ + id: AmeDashboardWidgetEditor.customIdPrefix + this.newWidgetCounter, + title: 'New Widget ' + this.newWidgetCounter + }, this); + + //Expand the new widget. + widget.isOpen(true); + + this.insertAfterWelcomePanel(widget); + } + + // noinspection JSUnusedGlobalSymbols Used in Knockout templates. + addRssWidget() { + this.newWidgetCounter++; + + let widget = new AmeCustomRssWidget({ + id: AmeDashboardWidgetEditor.customIdPrefix + this.newWidgetCounter, + title: 'New RSS Widget ' + this.newWidgetCounter + }, this); + + //Expand the new widget. + widget.isOpen(true); + + this.insertAfterWelcomePanel(widget); + } + + private insertAfterWelcomePanel(widget: AmeDashboardWidget) { + //The "Welcome" panel is always first, so we can cheat for performance. + if (this.widgets.indexOf(this.welcomePanel) === 0) { + let welcomePanel = this.widgets.shift(); + this.widgets.unshift(widget); + this.widgets.unshift(welcomePanel); + } else { + //But just in case it's not first for some odd reason, + //let's fall back to inserting the widget at the beginning. + this.widgets.unshift(widget); + } + } + + // noinspection JSUnusedGlobalSymbols Used in Knockout templates. + saveChanges() { + let settings = this.getCurrentSettings(); + + //Set the hidden form fields. + this.widgetData(JSON.stringify(settings)); + this.widgetDataLength(this.widgetData().length); + + //Submit the form. + return true; + } + + protected getCurrentSettings(): WidgetEditorSettings { + const collectionFormatName = 'Admin Menu Editor dashboard widgets'; + const collectionFormatVersion = '1.1'; + const _ = AmeDashboardWidgetEditor._; + + let settings: WidgetEditorSettings = { + format: { + name: collectionFormatName, + version: collectionFormatVersion + }, + widgets: [], + welcomePanel: { + grantAccess: _.pick(this.welcomePanel.grantAccess.getAll(), function(hasAccess, actorId) { + //Remove "allow" settings for actors that can't actually see the panel. + return AmeActors.hasCapByDefault(actorId, 'edit_theme_options') || !hasAccess; + + }), + }, + siteComponentHash: this.initialWidgetSettings.siteComponentHash + }; + _.forEach(_.without(this.widgets(), this.welcomePanel), function (widget) { + settings.widgets.push(widget.toPropertyMap()); + }); + + return settings; + } + + // noinspection JSUnusedGlobalSymbols Used in Knockout templates. + exportWidgets() { + //Temporarily disable the export button to prevent accidental repeated clicks. + this.isExportButtonEnabled(false); + + this.widgetData(JSON.stringify(this.getCurrentSettings())); + + //Re-enable the export button after a few seconds. + window.setTimeout(() => { + this.isExportButtonEnabled(true); + }, 3000); + + //Explicitly allow form submission. + return true; + } + + setupImportDialog() { + //Note to self: Refactor this as a separate view-model, perhaps. + + this.importDialog = jQuery('#ame-import-widgets-dialog'); + let importForm = this.importDialog.find('#ame-import-widgets-form'); + + this.importDialog.dialog({ + autoOpen: false, + modal: true, + closeText: ' ', + open: () => { + importForm.resetForm(); + this.importState('start'); + this.uploadButtonEnabled(false); + } + }); + + //jQuery moves the dialog to the end of the DOM tree, which puts it outside our KO root node. + //This means we must apply bindings directly to the dialog node. + ko.applyBindings(this, this.importDialog.get(0)); + + //Enable the upload button only when the user selects a file. + importForm.find('#ame-import-file-selector').on('change', (event) => { + this.uploadButtonEnabled( !!jQuery(event.target).val() ); + }); + + //This function displays unhandled server side errors. In theory, our upload handler always returns a well-formed + //response even if there's an error. In practice, stuff can go wrong in unexpected ways (e.g. plugin conflicts). + let handleUnexpectedImportError = (xhr, errorMessage) => { + //The server-side code didn't catch this error, so it's probably something serious + //and retrying won't work. + importForm.resetForm(); + this.importState('unexpected-error'); + + //Display error information. + this.importErrorMessage(errorMessage); + this.importErrorHttpCode(xhr.status); + this.importErrorResponse((xhr.responseText !== '') ? xhr.responseText : '[Empty response]'); + }; + + importForm.ajaxForm({ + dataType : 'json', + beforeSubmit: (formData) => { + + //Check if the user has selected a file + for (let i = 0; i < formData.length; i++) { + if ( formData[i].name === 'widget_file' ){ + if ( (typeof formData[i].value === 'undefined') || !formData[i].value){ + alert('Select a file first!'); + return false; + } + } + } + + this.importState('uploading'); + this.uploadButtonEnabled(false); + return true; + }, + success: (data, status, xhr) => { + if (!this.importDialog.dialog('isOpen')){ + //Whoops, the user closed the dialog while the upload was in progress. + //Discard the response silently. + return; + } + + if ((data === null) || (typeof data !== 'object')) { + handleUnexpectedImportError(xhr, 'Invalid response from server. Please check your PHP error log.'); + return; + } + + if (typeof data.error !== 'undefined'){ + alert(data.error.message || data.error.code); + //Let the user try again. + importForm.resetForm(); + this.importState('start'); + } + + if ((typeof data.widgets !== 'undefined') && data.widgets) { + //Lets load these widgets into the editor. + this.loadSettings(data); + + //Display a success message, then automatically close the window after a few moments. + this.importState('complete'); + setTimeout(() => { + this.importDialog.dialog('close'); + }, 700); + } + + }, + error: function(xhr, status, errorMessage) { + handleUnexpectedImportError(xhr, errorMessage); + } + }); + + this.importDialog.find('#ame-cancel-widget-import').on('click', () => { + this.importDialog.dialog('close'); + }); + } + + // noinspection JSUnusedGlobalSymbols Used in Knockout templates. + openImportDialog() { + this.importDialog.dialog('open'); + } +} + +jQuery(function () { + ameWidgetEditor = new AmeDashboardWidgetEditor( + wsWidgetEditorData.widgetSettings, + wsWidgetEditorData.selectedActor, + wsWidgetEditorData.isMultisite + ); + ko.applyBindings(ameWidgetEditor, document.getElementById('ame-dashboard-widget-editor')); +}); \ No newline at end of file diff --git a/extras/modules/dashboard-widget-editor/dashboard-widget-template.php b/extras/modules/dashboard-widget-editor/dashboard-widget-template.php new file mode 100644 index 0000000..266096c --- /dev/null +++ b/extras/modules/dashboard-widget-editor/dashboard-widget-template.php @@ -0,0 +1,3 @@ + diff --git a/extras/modules/dashboard-widget-editor/dashboard-widget.js b/extras/modules/dashboard-widget-editor/dashboard-widget.js new file mode 100644 index 0000000..31a76b5 --- /dev/null +++ b/extras/modules/dashboard-widget-editor/dashboard-widget.js @@ -0,0 +1,386 @@ +/// +/// +/// +/// +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var AmeDashboardWidget = /** @class */ (function () { + function AmeDashboardWidget(settings, widgetEditor) { + var _this = this; + this.isPresent = true; + this.missingWidgetTooltip = "N/A"; + this.canBeDeleted = false; + this.canChangePriority = false; + this.canChangeTitle = true; + this.propertyTemplate = ''; + this.widgetType = null; + this.rawProperties = settings; + this.widgetEditor = widgetEditor; + this.id = settings['id']; + this.isPresent = !!(settings['isPresent']); + this.canBeDeleted = !this.isPresent; + var self = this; + this.safeTitle = ko.computed({ + read: function () { + return AmeDashboardWidget.stripAllTags(self.title()); + }, + deferEvaluation: true //this.title might not be initialised at this point, so skip it until later. + }); + this.isOpen = ko.observable(false); + this.areAdvancedPropertiesVisible = ko.observable(true); + this.grantAccess = new AmeActorAccessDictionary(settings.hasOwnProperty('grantAccess') ? settings['grantAccess'] : {}); + //Indeterminate checkbox state: when the widget is enabled for some roles and disabled for others. + var _isIndeterminate = ko.observable(false); + this.isIndeterminate = ko.computed(function () { + if (widgetEditor.selectedActor() !== null) { + return false; + } + return _isIndeterminate(); + }); + //Is the widget enabled for the selected actor? + this.isEnabled = ko.computed({ + read: function () { + var actor = widgetEditor.selectedActor(); + if (actor !== null) { + return _this.actorHasAccess(actor); + } + else { + //Check if any actors have this widget enabled. + //We only care about visible actors. There might be some users that are loaded but not visible. + var actors = widgetEditor.actorSelector.getVisibleActors(); + var areAnyActorsEnabled = false, areAnyActorsDisabled = false; + for (var index = 0; index < actors.length; index++) { + var hasAccess = _this.actorHasAccess(actors[index].getId(), actors[index]); + if (hasAccess) { + areAnyActorsEnabled = true; + } + else if (hasAccess === false) { + areAnyActorsDisabled = true; + } + } + _isIndeterminate(areAnyActorsEnabled && areAnyActorsDisabled); + return areAnyActorsEnabled; + } + }, + write: function (enabled) { + var actor = widgetEditor.selectedActor(); + if (actor !== null) { + _this.grantAccess.set(actor, enabled); + } + else { + //Enable/disable all. + var actors = widgetEditor.actorSelector.getVisibleActors(); + for (var index = 0; index < actors.length; index++) { + _this.grantAccess.set(actors[index].getId(), enabled); + } + } + } + }); + } + AmeDashboardWidget.stripAllTags = function (input) { + //Based on: http://phpjs.org/functions/strip_tags/ + var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, commentsAndPhpTags = /|<\?(?:php)?[\s\S]*?\?>/gi; + return input.replace(commentsAndPhpTags, '').replace(tags, ''); + }; + AmeDashboardWidget.prototype.createObservableWithDefault = function (customValue, defaultValue, writeCallback) { + //Sentinel value: '' (the empty string). Null is also accepted and automatically converted to ''. + var sentinel = ''; + customValue = writeCallback(customValue, sentinel); + if ((customValue === defaultValue) || (customValue === null)) { + customValue = sentinel; + } + var _customValue = ko.observable(customValue); + var observable = ko.computed({ + read: function () { + var customValue = _customValue(); + if (customValue === sentinel) { + return defaultValue; + } + else { + return customValue; + } + }, + write: function (newValue) { + var oldValue = _customValue(); + var valueToWrite = writeCallback(newValue, oldValue); + if ((valueToWrite === defaultValue) || (valueToWrite === null)) { + valueToWrite = sentinel; + } + if (valueToWrite !== oldValue) { + _customValue(valueToWrite); + } + else if ((valueToWrite !== newValue) || (valueToWrite === sentinel)) { + observable.notifySubscribers(); + } + } + }); + observable.resetToDefault = function () { + _customValue(sentinel); + }; + observable.getCustomValue = function () { + return _customValue(); + }; + return observable; + }; + AmeDashboardWidget.prototype.toggle = function () { + this.isOpen(!this.isOpen()); + }; + AmeDashboardWidget.prototype.toPropertyMap = function () { + var properties = { + 'id': this.id, + 'title': this.title(), + 'location': this.location(), + 'priority': this.priority(), + 'grantAccess': this.grantAccess.getAll() + }; + properties = AmeDashboardWidget._.merge({}, this.rawProperties, properties); + if (this.widgetType !== null) { + properties['widgetType'] = this.widgetType; + } + return properties; + }; + AmeDashboardWidget.prototype.actorHasAccess = function (actorId, actor, defaultAccess) { + if (defaultAccess === void 0) { defaultAccess = true; } + //Is there a setting for this actor specifically? + var hasAccess = this.grantAccess.get(actorId, null); + if (hasAccess !== null) { + return hasAccess; + } + if (!actor) { + actor = AmeActors.getActor(actorId); + } + if (actor instanceof AmeUser) { + //The Super Admin has access to everything by default, and it takes priority over roles. + if (actor.isSuperAdmin) { + return this.grantAccess.get('special:super_admin', true); + } + //Allow access if at least one role has access. + var result = false; + for (var index = 0; index < actor.roles.length; index++) { + var roleActor = 'role:' + actor.roles[index], roleHasAccess = this.grantAccess.get(roleActor, true); + result = result || roleHasAccess; + } + return result; + } + //By default, all widgets are visible to everyone. + return defaultAccess; + }; + AmeDashboardWidget._ = wsAmeLodash; + return AmeDashboardWidget; +}()); +var AmeActorAccessDictionary = /** @class */ (function () { + function AmeActorAccessDictionary(initialData) { + this.items = {}; + this.numberOfObservables = ko.observable(0); + if (initialData) { + this.setAll(initialData); + } + } + AmeActorAccessDictionary.prototype.get = function (actor, defaultValue) { + if (defaultValue === void 0) { defaultValue = null; } + if (this.items.hasOwnProperty(actor)) { + return this.items[actor](); + } + this.numberOfObservables(); //Establish a dependency. + return defaultValue; + }; + AmeActorAccessDictionary.prototype.set = function (actor, value) { + if (!this.items.hasOwnProperty(actor)) { + this.items[actor] = ko.observable(value); + this.numberOfObservables(this.numberOfObservables() + 1); + } + else { + this.items[actor](value); + } + }; + // noinspection JSUnusedGlobalSymbols + AmeActorAccessDictionary.prototype.getAll = function () { + var result = {}; + for (var actorId in this.items) { + if (this.items.hasOwnProperty(actorId)) { + result[actorId] = this.items[actorId](); + } + } + return result; + }; + AmeActorAccessDictionary.prototype.setAll = function (values) { + for (var actorId in values) { + if (values.hasOwnProperty(actorId)) { + this.set(actorId, values[actorId]); + } + } + }; + return AmeActorAccessDictionary; +}()); +var AmeStandardWidgetWrapper = /** @class */ (function (_super) { + __extends(AmeStandardWidgetWrapper, _super); + function AmeStandardWidgetWrapper(settings, widgetEditor) { + var _this = _super.call(this, settings, widgetEditor) || this; + _this.wrappedWidget = settings['wrappedWidget']; + _this.title = _this.createObservableWithDefault(settings['title'], _this.wrappedWidget.title, function (value) { + //Trim leading and trailing whitespace. + value = value.replace(/^\s+|\s+$/g, ""); + if (value === '') { + return null; + } + return value; + }); + _this.location = _this.createObservableWithDefault(settings['location'], _this.wrappedWidget.location, function () { + return null; + }); + _this.priority = _this.createObservableWithDefault(settings['priority'], _this.wrappedWidget.priority, function () { + return null; + }); + if (!_this.isPresent) { + //Note: This is not intended to be perfectly accurate. + var wasCreatedByTheme = _this.rawProperties.hasOwnProperty('callbackFileName') + && _this.rawProperties['callbackFileName'].match(/[/\\]wp-content[/\\]themes[/\\]/); + _this.missingWidgetTooltip = (wasCreatedByTheme ? 'The theme' : 'The plugin') + + ' that created this widget is not active.' + + '\nTo remove the widget, open it and click "Delete".'; + } + return _this; + } + AmeStandardWidgetWrapper.prototype.toPropertyMap = function () { + var properties = _super.prototype.toPropertyMap.call(this); + properties['wrappedWidget'] = this.wrappedWidget; + properties['title'] = this.title.getCustomValue(); + properties['location'] = this.location.getCustomValue(); + properties['priority'] = this.priority.getCustomValue(); + return properties; + }; + return AmeStandardWidgetWrapper; +}(AmeDashboardWidget)); +var AmeCustomHtmlWidget = /** @class */ (function (_super) { + __extends(AmeCustomHtmlWidget, _super); + function AmeCustomHtmlWidget(settings, widgetEditor) { + var _this = this; + var _ = AmeDashboardWidget._; + settings = _.merge({ + id: 'new-untitled-widget', + isPresent: true, + grantAccess: {} + }, settings); + _this = _super.call(this, settings, widgetEditor) || this; + _this.widgetType = 'custom-html'; + _this.canChangePriority = true; + _this.title = ko.observable(_.get(settings, 'title', 'New Widget')); + _this.location = ko.observable(_.get(settings, 'location', 'normal')); + _this.priority = ko.observable(_.get(settings, 'priority', 'high')); + _this.content = ko.observable(_.get(settings, 'content', '')); + _this.filtersEnabled = ko.observable(_.get(settings, 'filtersEnabled', true)); + //Custom widgets are always present and can always be deleted. + _this.isPresent = true; + _this.canBeDeleted = true; + _this.propertyTemplate = 'ame-custom-html-widget-template'; + return _this; + } + AmeCustomHtmlWidget.prototype.toPropertyMap = function () { + var properties = _super.prototype.toPropertyMap.call(this); + properties['content'] = this.content(); + properties['filtersEnabled'] = this.filtersEnabled(); + return properties; + }; + return AmeCustomHtmlWidget; +}(AmeDashboardWidget)); +var AmeCustomRssWidget = /** @class */ (function (_super) { + __extends(AmeCustomRssWidget, _super); + function AmeCustomRssWidget(settings, widgetEditor) { + var _this = this; + var _ = AmeDashboardWidget._; + settings = _.merge({ + id: 'new-untitled-rss-widget', + isPresent: true, + grantAccess: {} + }, settings); + _this = _super.call(this, settings, widgetEditor) || this; + _this.widgetType = 'custom-rss'; + _this.canChangePriority = true; + _this.title = ko.observable(_.get(settings, 'title', 'New RSS Widget')); + _this.location = ko.observable(_.get(settings, 'location', 'normal')); + _this.priority = ko.observable(_.get(settings, 'priority', 'high')); + _this.feedUrl = ko.observable(_.get(settings, 'feedUrl', '')); + _this.maxItems = ko.observable(_.get(settings, 'maxItems', 5)); + _this.showAuthor = ko.observable(_.get(settings, 'showAuthor', true)); + _this.showDate = ko.observable(_.get(settings, 'showDate', true)); + _this.showSummary = ko.observable(_.get(settings, 'showSummary', true)); + _this.isPresent = true; + _this.canBeDeleted = true; + _this.propertyTemplate = 'ame-custom-rss-widget-template'; + return _this; + } + AmeCustomRssWidget.prototype.toPropertyMap = function () { + var properties = _super.prototype.toPropertyMap.call(this); + var storedProps = ['feedUrl', 'showAuthor', 'showDate', 'showSummary', 'maxItems']; + for (var i = 0; i < storedProps.length; i++) { + var name_1 = storedProps[i]; + properties[name_1] = this[name_1](); + } + return properties; + }; + return AmeCustomRssWidget; +}(AmeDashboardWidget)); +var AmeWelcomeWidget = /** @class */ (function (_super) { + __extends(AmeWelcomeWidget, _super); + function AmeWelcomeWidget(settings, widgetEditor) { + var _this = this; + var _ = AmeDashboardWidget._; + if (_.isArray(settings)) { + settings = {}; + } + settings = _.merge({ + id: AmeWelcomeWidget.permanentId, + isPresent: true, + grantAccess: {} + }, settings); + _this = _super.call(this, settings, widgetEditor) || this; + _this.title = ko.observable('Welcome'); + _this.location = ko.observable('normal'); + _this.priority = ko.observable('high'); + _this.canChangeTitle = false; + _this.canChangePriority = false; + _this.areAdvancedPropertiesVisible(false); + //The "Welcome" widget is part of WordPress core. It's always present and can't be deleted. + _this.isPresent = true; + _this.canBeDeleted = false; + _this.propertyTemplate = 'ame-welcome-widget-template'; + return _this; + } + AmeWelcomeWidget.prototype.actorHasAccess = function (actorId, actor, defaultAccess) { + if (defaultAccess === void 0) { defaultAccess = true; } + //Only people who have the "edit_theme_options" capability can see the "Welcome" panel. + //See /wp-admin/index.php, line #108 or thereabouts. + defaultAccess = AmeActors.hasCapByDefault(actorId, 'edit_theme_options'); + return _super.prototype.actorHasAccess.call(this, actorId, actor, defaultAccess); + }; + AmeWelcomeWidget.permanentId = 'special:welcome-panel'; + return AmeWelcomeWidget; +}(AmeDashboardWidget)); +var AmeWidgetPropertyComponent = /** @class */ (function () { + function AmeWidgetPropertyComponent(params) { + this.widget = params['widget']; + this.label = params['label'] || ''; + } + return AmeWidgetPropertyComponent; +}()); +//Custom element: +ko.components.register('ame-widget-property', { + viewModel: AmeWidgetPropertyComponent, + template: { + element: 'ame-widget-property-template' + } +}); +//# sourceMappingURL=dashboard-widget.js.map \ No newline at end of file diff --git a/extras/modules/dashboard-widget-editor/dashboard-widget.js.map b/extras/modules/dashboard-widget-editor/dashboard-widget.js.map new file mode 100644 index 0000000..dc3570a --- /dev/null +++ b/extras/modules/dashboard-widget-editor/dashboard-widget.js.map @@ -0,0 +1 @@ +{"version":3,"file":"dashboard-widget.js","sourceRoot":"","sources":["dashboard-widget.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/extras/modules/dashboard-widget-editor/dashboard-widget.ts b/extras/modules/dashboard-widget-editor/dashboard-widget.ts new file mode 100644 index 0000000..c5d97fd --- /dev/null +++ b/extras/modules/dashboard-widget-editor/dashboard-widget.ts @@ -0,0 +1,499 @@ +/// +/// +/// +/// + +declare var wsAmeLodash: _.LoDashStatic; + +interface WidgetPropertyMap { + [name: string] : any; +} + +abstract class AmeDashboardWidget { + protected static _ = wsAmeLodash; + + id: string; + + title: KnockoutObservable; + location: KnockoutObservable; + priority: KnockoutObservable; + isPresent: boolean = true; + grantAccess: AmeActorAccessDictionary; + + safeTitle: KnockoutComputed; + + missingWidgetTooltip: string = "N/A"; + + rawProperties: WidgetPropertyMap; + + isOpen: KnockoutObservable; + isEnabled: KnockoutComputed; + isIndeterminate: KnockoutObservable; + + canBeDeleted: boolean = false; + canChangePriority: boolean = false; + canChangeTitle: boolean = true; + areAdvancedPropertiesVisible: KnockoutObservable; + + widgetEditor: AmeDashboardWidgetEditor; + + propertyTemplate: string = ''; + protected widgetType: string = null; + + protected constructor(settings: WidgetPropertyMap, widgetEditor: AmeDashboardWidgetEditor) { + this.rawProperties = settings; + this.widgetEditor = widgetEditor; + + this.id = settings['id']; + this.isPresent = !!(settings['isPresent']); + this.canBeDeleted = !this.isPresent; + + const self = this; + this.safeTitle = ko.computed({ + read: function () { + return AmeDashboardWidget.stripAllTags(self.title()); + }, + deferEvaluation: true //this.title might not be initialised at this point, so skip it until later. + }); + + this.isOpen = ko.observable(false); + this.areAdvancedPropertiesVisible = ko.observable(true); + + this.grantAccess = new AmeActorAccessDictionary( + settings.hasOwnProperty('grantAccess') ? settings['grantAccess'] : {} + ); + + //Indeterminate checkbox state: when the widget is enabled for some roles and disabled for others. + let _isIndeterminate = ko.observable(false); + this.isIndeterminate = ko.computed(() => { + if (widgetEditor.selectedActor() !== null) { + return false; + } + return _isIndeterminate(); + }); + + //Is the widget enabled for the selected actor? + this.isEnabled = ko.computed({ + read: (): boolean => { + let actor = widgetEditor.selectedActor(); + if (actor !== null) { + return this.actorHasAccess(actor); + } else { + //Check if any actors have this widget enabled. + //We only care about visible actors. There might be some users that are loaded but not visible. + const actors = widgetEditor.actorSelector.getVisibleActors(); + let areAnyActorsEnabled = false, areAnyActorsDisabled = false; + + for (let index = 0; index < actors.length; index++) { + let hasAccess = this.actorHasAccess(actors[index].getId(), actors[index]); + if (hasAccess) { + areAnyActorsEnabled = true; + } else if (hasAccess === false) { + areAnyActorsDisabled = true; + } + } + _isIndeterminate(areAnyActorsEnabled && areAnyActorsDisabled); + + return areAnyActorsEnabled; + } + }, + write: (enabled: boolean) => { + let actor = widgetEditor.selectedActor(); + if (actor !== null) { + this.grantAccess.set(actor, enabled); + } else { + //Enable/disable all. + const actors = widgetEditor.actorSelector.getVisibleActors(); + for (let index = 0; index < actors.length; index++) { + this.grantAccess.set(actors[index].getId(), enabled); + } + } + } + }); + } + + private static stripAllTags(input: string) { + //Based on: http://phpjs.org/functions/strip_tags/ + const tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, + commentsAndPhpTags = /|<\?(?:php)?[\s\S]*?\?>/gi; + return input.replace(commentsAndPhpTags, '').replace(tags, ''); + } + + protected createObservableWithDefault(customValue: string, defaultValue: string, writeCallback) { + //Sentinel value: '' (the empty string). Null is also accepted and automatically converted to ''. + const sentinel = ''; + + customValue = writeCallback(customValue, sentinel); + if ((customValue === defaultValue) || (customValue === null)) { + customValue = sentinel; + } + + let _customValue = ko.observable(customValue); + + let observable = ko.computed({ + read: function (): string { + let customValue = _customValue(); + if (customValue === sentinel) { + return defaultValue; + } else { + return customValue; + } + }, + write: function (newValue: string) { + const oldValue = _customValue(); + let valueToWrite = writeCallback(newValue, oldValue); + + if ((valueToWrite === defaultValue) || (valueToWrite === null)) { + valueToWrite = sentinel; + } + + if (valueToWrite !== oldValue) { + _customValue(valueToWrite); + } else if ((valueToWrite !== newValue) || (valueToWrite === sentinel)) { + observable.notifySubscribers(); + } + } + }); + + observable.resetToDefault = function () { + _customValue(sentinel); + }; + + observable.getCustomValue = function () { + return _customValue(); + }; + + return observable; + } + + toggle() { + this.isOpen(!this.isOpen()); + } + + toPropertyMap(): WidgetPropertyMap { + let properties = { + 'id': this.id, + 'title': this.title(), + 'location': this.location(), + 'priority': this.priority(), + + 'grantAccess': this.grantAccess.getAll() + }; + + properties = AmeDashboardWidget._.merge({}, this.rawProperties, properties); + + if (this.widgetType !== null) { + properties['widgetType'] = this.widgetType; + } + return properties; + } + + protected actorHasAccess(actorId: string, actor?: IAmeActor, defaultAccess: boolean = true) { + //Is there a setting for this actor specifically? + let hasAccess = this.grantAccess.get(actorId, null); + if (hasAccess !== null) { + return hasAccess; + } + + if (!actor) { + actor = AmeActors.getActor(actorId); + } + if (actor instanceof AmeUser) { + //The Super Admin has access to everything by default, and it takes priority over roles. + if (actor.isSuperAdmin) { + return this.grantAccess.get('special:super_admin', true); + } + + //Allow access if at least one role has access. + let result = false; + for (let index = 0; index < actor.roles.length; index++) { + let roleActor = 'role:' + actor.roles[index], + roleHasAccess = this.grantAccess.get(roleActor, true); + result = result || roleHasAccess; + } + return result; + } + + //By default, all widgets are visible to everyone. + return defaultAccess; + } +} + +interface KnockoutComputed { + resetToDefault: () => void; + getCustomValue: () => T; +} + +class AmeActorAccessDictionary { + items: { [actorId: string] : KnockoutObservable; } = {}; + private readonly numberOfObservables: KnockoutObservable; + + constructor(initialData?: AmeDictionary) { + this.numberOfObservables = ko.observable(0); + if (initialData) { + this.setAll(initialData); + } + } + + get(actor: string, defaultValue = null): boolean { + if (this.items.hasOwnProperty(actor)) { + return this.items[actor](); + } + this.numberOfObservables(); //Establish a dependency. + return defaultValue; + } + + set(actor: string, value: boolean) { + if (!this.items.hasOwnProperty(actor)) { + this.items[actor] = ko.observable(value); + this.numberOfObservables(this.numberOfObservables() + 1); + } else { + this.items[actor](value); + } + } + + // noinspection JSUnusedGlobalSymbols + getAll(): AmeDictionary { + let result: AmeDictionary = {}; + for (let actorId in this.items) { + if (this.items.hasOwnProperty(actorId)) { + result[actorId] = this.items[actorId](); + } + } + return result; + } + + setAll(values: AmeDictionary) { + for (let actorId in values) { + if (values.hasOwnProperty(actorId)) { + this.set(actorId, values[actorId]); + } + } + } +} + + +interface WrappedWidgetProperties { + title: string; + location: string; + priority: string; +} + +class AmeStandardWidgetWrapper extends AmeDashboardWidget { + wrappedWidget: WrappedWidgetProperties; + + title: KnockoutComputed; + location: KnockoutComputed; + priority: KnockoutComputed; + + constructor(settings: WidgetPropertyMap, widgetEditor: AmeDashboardWidgetEditor) { + super(settings, widgetEditor); + this.wrappedWidget = settings['wrappedWidget']; + + this.title = this.createObservableWithDefault( + settings['title'], + this.wrappedWidget.title, + function (value: string) { + //Trim leading and trailing whitespace. + value = value.replace(/^\s+|\s+$/g, ""); + if (value === '') { + return null; + } + return value; + } + ); + + this.location = this.createObservableWithDefault( + settings['location'], + this.wrappedWidget.location, + function () { + return null; + } + ); + + this.priority = this.createObservableWithDefault( + settings['priority'], + this.wrappedWidget.priority, + function () { + return null; + } + ); + + if (!this.isPresent) { + //Note: This is not intended to be perfectly accurate. + const wasCreatedByTheme = this.rawProperties.hasOwnProperty('callbackFileName') + && this.rawProperties['callbackFileName'].match(/[/\\]wp-content[/\\]themes[/\\]/); + + this.missingWidgetTooltip = (wasCreatedByTheme ? 'The theme' : 'The plugin') + + ' that created this widget is not active.' + + '\nTo remove the widget, open it and click "Delete".'; + } + } + + toPropertyMap(): WidgetPropertyMap { + let properties = super.toPropertyMap(); + properties['wrappedWidget'] = this.wrappedWidget; + properties['title'] = this.title.getCustomValue(); + properties['location'] = this.location.getCustomValue(); + properties['priority'] = this.priority.getCustomValue(); + return properties; + } +} + + +class AmeCustomHtmlWidget extends AmeDashboardWidget { + content: KnockoutObservable; + filtersEnabled: KnockoutObservable; + + constructor(settings: WidgetPropertyMap, widgetEditor: AmeDashboardWidgetEditor) { + const _ = AmeDashboardWidget._; + settings = _.merge( + { + id: 'new-untitled-widget', + isPresent: true, + grantAccess: {} + }, + settings + ); + super(settings, widgetEditor); + + this.widgetType = 'custom-html'; + this.canChangePriority = true; + + this.title = ko.observable(_.get(settings, 'title', 'New Widget')); + this.location = ko.observable(_.get(settings, 'location', 'normal')); + this.priority = ko.observable(_.get(settings, 'priority', 'high')); + + this.content = ko.observable(_.get(settings, 'content', '')); + this.filtersEnabled = ko.observable(_.get(settings, 'filtersEnabled', true)); + + //Custom widgets are always present and can always be deleted. + this.isPresent = true; + this.canBeDeleted = true; + + this.propertyTemplate = 'ame-custom-html-widget-template'; + } + + toPropertyMap(): WidgetPropertyMap { + let properties = super.toPropertyMap(); + properties['content'] = this.content(); + properties['filtersEnabled'] = this.filtersEnabled(); + return properties; + } +} + +class AmeCustomRssWidget extends AmeDashboardWidget { + feedUrl: KnockoutObservable; + + showAuthor: KnockoutObservable; + showDate: KnockoutObservable; + showSummary: KnockoutObservable; + + maxItems: KnockoutObservable; + + constructor(settings: WidgetPropertyMap, widgetEditor: AmeDashboardWidgetEditor) { + const _ = AmeDashboardWidget._; + settings = _.merge( + { + id: 'new-untitled-rss-widget', + isPresent: true, + grantAccess: {} + }, + settings + ); + super(settings, widgetEditor); + + this.widgetType = 'custom-rss'; + this.canChangePriority = true; + + this.title = ko.observable(_.get(settings, 'title', 'New RSS Widget')); + this.location = ko.observable(_.get(settings, 'location', 'normal')); + this.priority = ko.observable(_.get(settings, 'priority', 'high')); + + this.feedUrl = ko.observable(_.get(settings, 'feedUrl', '')); + this.maxItems = ko.observable(_.get(settings, 'maxItems', 5)); + this.showAuthor = ko.observable(_.get(settings, 'showAuthor', true)); + this.showDate = ko.observable(_.get(settings, 'showDate', true)); + this.showSummary = ko.observable(_.get(settings, 'showSummary', true)); + + this.isPresent = true; + this.canBeDeleted = true; + + this.propertyTemplate = 'ame-custom-rss-widget-template'; + } + + toPropertyMap(): WidgetPropertyMap { + let properties = super.toPropertyMap(); + let storedProps = ['feedUrl', 'showAuthor', 'showDate', 'showSummary', 'maxItems']; + for (let i = 0; i < storedProps.length; i++) { + let name = storedProps[i]; + properties[name] = this[name](); + } + return properties; + } +} + +class AmeWelcomeWidget extends AmeDashboardWidget { + static permanentId: string = 'special:welcome-panel'; + + constructor(settings: WidgetPropertyMap, widgetEditor: AmeDashboardWidgetEditor) { + const _ = AmeDashboardWidget._; + + if (_.isArray(settings)) { + settings = {}; + } + settings = _.merge( + { + id: AmeWelcomeWidget.permanentId, + isPresent: true, + grantAccess: {} + }, + settings + ); + super(settings, widgetEditor); + + this.title = ko.observable('Welcome'); + this.location = ko.observable('normal'); + this.priority = ko.observable('high'); + + this.canChangeTitle = false; + this.canChangePriority = false; + this.areAdvancedPropertiesVisible(false); + + //The "Welcome" widget is part of WordPress core. It's always present and can't be deleted. + this.isPresent = true; + this.canBeDeleted = false; + + this.propertyTemplate = 'ame-welcome-widget-template'; + } + + protected actorHasAccess( + actorId: string, + actor?: AmeBaseActor, + defaultAccess: boolean = true + ): boolean | boolean | boolean | boolean { + //Only people who have the "edit_theme_options" capability can see the "Welcome" panel. + //See /wp-admin/index.php, line #108 or thereabouts. + defaultAccess = AmeActors.hasCapByDefault(actorId, 'edit_theme_options'); + return super.actorHasAccess(actorId, actor, defaultAccess); + } +} + + +class AmeWidgetPropertyComponent { + widget: AmeDashboardWidget; + label: string; + + constructor(params) { + this.widget = params['widget']; + this.label = params['label'] || ''; + } +} + +//Custom element: +ko.components.register('ame-widget-property', { + viewModel: AmeWidgetPropertyComponent, + template: { + element: 'ame-widget-property-template' + } +}); \ No newline at end of file diff --git a/extras/modules/dashboard-widget-editor/import-dialog-template.php b/extras/modules/dashboard-widget-editor/import-dialog-template.php new file mode 100644 index 0000000..ff1baae --- /dev/null +++ b/extras/modules/dashboard-widget-editor/import-dialog-template.php @@ -0,0 +1,55 @@ + + \ No newline at end of file diff --git a/extras/modules/dashboard-widget-editor/load.php b/extras/modules/dashboard-widget-editor/load.php new file mode 100644 index 0000000..a36298f --- /dev/null +++ b/extras/modules/dashboard-widget-editor/load.php @@ -0,0 +1,9 @@ +'); + + frame.on('load', function() { + //When done, redirect back to the widget editor. + window.location.href = wsWidgetRefresherData['editorUrl']; + }); + + frame.attr({ + 'src': wsWidgetRefresherData['dashboardUrl'], + 'width': 1, + 'height': 1 + }); + frame.css('visibility', 'hidden'); + + frame.appendTo('#wpwrap'); +}); diff --git a/extras/modules/dashboard-widget-editor/widget-refresh-template.php b/extras/modules/dashboard-widget-editor/widget-refresh-template.php new file mode 100644 index 0000000..fe43dd6 --- /dev/null +++ b/extras/modules/dashboard-widget-editor/widget-refresh-template.php @@ -0,0 +1,13 @@ +

+ Refreshing available widgets... + +

+ +

+ + You'll be redirected to widget settings when it's done. If that doesn't happen within a couple of minutes, + please go to Dashboard -> Home and then return + to this page. +

\ No newline at end of file diff --git a/extras/modules/easy-hide/easy-hide-style.css b/extras/modules/easy-hide/easy-hide-style.css new file mode 100644 index 0000000..8d05d98 --- /dev/null +++ b/extras/modules/easy-hide/easy-hide-style.css @@ -0,0 +1,376 @@ +@charset "UTF-8"; +.ame-cat-nav-item { + cursor: pointer; + margin: 0; +} + +.ame-cat-nav-item:hover { + background-color: #E5F3FF; +} + +.ame-selected-cat-nav-item { + background-color: #CCE8FF; + box-shadow: 0px -1px 0px 0px #99D1FF, 0px 1px 0px 0px #99D1FF; +} +.ame-selected-cat-nav-item:hover { + background-color: #CCE8FF; +} + +.ame-cat-nav-item.ame-cat-nav-level-2 { + padding-left: 0px; +} + +.ame-cat-nav-item.ame-cat-nav-level-3 { + padding-left: 13px; +} + +.ame-cat-nav-item.ame-cat-nav-level-4 { + padding-left: 26px; +} + +.ame-cat-nav-item.ame-cat-nav-level-5 { + padding-left: 39px; +} + +.ame-cat-nav-toggle { + visibility: hidden; + display: inline-block; + box-sizing: border-box; + max-height: 100%; + width: 20px; + text-align: right; + vertical-align: middle; + margin-right: 0.3em; +} +.ame-cat-nav-toggle:after { + font-family: dashicons, sans-serif; + content: "\f345"; +} +.ame-cat-nav-toggle:hover { + color: #3ECEF9; +} + +.ame-cat-nav-is-expanded > .ame-cat-nav-toggle:after { + content: "\f347"; +} + +.ame-cat-nav-has-children > .ame-cat-nav-toggle { + visibility: visible; +} + +#ws_ame_editor_heading { + float: none; +} + +#ws_actor_selector_container { + margin-bottom: 8px; +} + +#ame-easy-hide-ui { + display: flex; + border: 1px solid #ccd0d4; +} + +#ame-eh-category-container { + flex-basis: 220px; + flex-grow: 0; + max-width: 220px; + background: #f8f8f8; + border-right: 1px solid #ccd0d4; + padding-top: 6px; +} + +#ame-eh-content-area { + flex-grow: 1; + display: flex; + flex-direction: column; +} + +#ame-eh-view-toolbar { + flex-grow: 0; + display: flex; + align-items: center; + padding: 10px 12px; + background: #fcfcfc; + border-bottom: 1px solid #ddd; +} + +#ame-eh-search-container { + position: relative; + min-width: 250px; + max-width: 100%; +} + +#ame-eh-search-query { + position: relative; + width: 100%; + appearance: none; + padding-right: calc(0.3em * 2 + 20px); +} + +.ame-eh-clear-search-box { + position: absolute; + top: 0; + right: 0; + height: 100%; + border: none; + padding: 0 0.3em; + background: none; + cursor: pointer; + color: #888; +} +.ame-eh-clear-search-box:hover { + color: #444; +} + +#ame-eh-item-container { + flex-grow: 1; + padding: 0 12px 12px 12px; + background: #fff; +} +#ame-eh-item-container input[type=checkbox]:indeterminate:before { + content: "■"; + color: #D81536; + margin: -3px 0 0 -1px; + font: 400 14px/1 dashicons; + float: left; + display: inline-block; + vertical-align: middle; + width: 16px; + -webkit-font-smoothing: antialiased; +} +@media screen and (max-width: 782px) { + #ame-eh-item-container input[type=checkbox]:indeterminate:before { + height: 1.5625rem; + width: 1.5625rem; + line-height: 1.5625rem; + margin: -1px; + font-size: 18px; + font-family: unset; + font-weight: normal; + } +} + +input[type=checkbox].ame-eh-negative-box:checked:before { + content: "🞬"; + font-weight: bold; + font-size: 15px; + line-height: 1rem; + color: #D81536; + float: left; + display: inline-block; + vertical-align: middle; + width: 1rem; + height: 1rem; + margin: -1px; + margin-left: -1.5px; +} + +@media screen and (max-width: 782px) { + input[type=checkbox].ame-eh-negative-box:checked:before { + height: 1.5625rem; + width: 1.5625rem; + line-height: 1.5625rem; + font-size: 18px; + } +} +.ame-eh-item { + padding: 0; + margin: 0; + font-size: 14px; + line-height: 1.65; +} + +.ame-eh-item-list { + margin: 0; + padding: 0; + /* + Ideally, I'd like to avoid inserting a column break after an item that has + any children, but nothing I've tried has worked. The "orphans" and "widows" + properties seem to have no effect in nested lists. The following CSS also doesn't + seem to work, at least not with the current HTML structure. I've left it here + in case future browsers start treating lists better. + */ +} +.ame-eh-item-list li:first-child { + break-after: avoid; +} +.ame-eh-item-list li:last-child { + break-before: avoid; +} + +.ame-eh-item > .ame-eh-item-list { + margin-left: 1.7em; +} + +.ame-eh-search-highlight { + background-color: #ffff00; +} + +.ame-eh-category-heading { + background: #fcfcfc; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + padding: 0.4em 12px 0.6em 12px; + margin: 1.5em -12px 0.7em -12px; + font-size: 1.3em; +} + +.ame-eh-category-list { + margin: 0; + padding: 0; + list-style: none; +} +.ame-eh-category-list li { + margin: 0; + padding: 0; +} + +.ame-eh-cat-nav-item { + padding: 4px 8px 4px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +table.ame-eh-category-table-view th, table.ame-eh-category-table-view td { + box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1); +} +table.ame-eh-category-table-view th:last-child, table.ame-eh-category-table-view td:last-child { + box-shadow: none; +} +table.ame-eh-category-table-view tbody th { + text-align: left; +} +table.ame-eh-category-table-view input[type=checkbox] { + vertical-align: middle; + margin: -0.25rem 0.25rem 0 -1px; +} +table.ame-eh-category-table-view td { + text-align: center; +} +table.ame-eh-category-table-view td input[type=checkbox] { + margin-right: 0; +} +table.ame-eh-category-table-view td label { + box-sizing: border-box; + display: block; + width: 100%; +} +table.ame-eh-category-table-view .ame-eh-table-corner-cell { + border-bottom: none; +} +table.ame-eh-category-table-view tbody tr:hover th, table.ame-eh-category-table-view tbody tr:hover td { + background-color: #E5F3FF; +} +table.ame-eh-category-table-view .ame-eh-hovered-column { + background-color: #E5F3FF; +} +table.ame-eh-category-table-view thead th { + position: sticky; + top: 32px; + background: white; +} +table.ame-eh-category-table-view thead th:first-child { + position: unset; +} + +.ame-eh-category-subtitle { + color: #888; + font-size: 0.95em; + font-family: Consolas, Monaco, monospace; + line-height: 1; +} +.ame-eh-category-subtitle::before { + content: "("; +} +.ame-eh-category-subtitle::after { + content: ")"; +} + +.ame-eh-lazy-category { + min-height: 100px; + outline: 1px dashed #ccd0d4; + padding-bottom: 0.5rem; + margin-bottom: 1rem; +} + +#ame-eh-side-save-button { + margin-top: 50px; + padding: 8px 8px; + border-top: 1px solid #dcdcde; + position: sticky; + top: 32px; +} + +#ame-easy-hide #ws_actor_selector_container { + margin-right: 130px; +} + +#ame-eh-top-save-button { + float: right; + box-sizing: border-box; + width: 129px; + text-align: right; + padding-left: 10px; +} +#ame-eh-top-save-button input[type=submit] { + margin-top: 7px; + max-width: 129px; + margin-bottom: 5px; +} + +.ame-eh-item-columns-2 .ame-eh-columns-allowed .ame-eh-category-items > .ame-eh-item-list { + column-count: 2; +} + +.ame-eh-item-columns-3 .ame-eh-columns-allowed .ame-eh-category-items > .ame-eh-item-list { + column-count: 3; +} + +#ame-eh-column-selector { + margin-left: auto; + display: flex; + align-items: center; +} + +.ame-eh-column-option-list { + margin-left: 0.4em; + display: flex; +} +.ame-eh-column-option-list .ame-eh-column-option:not(:first-child) { + border-left-style: none; +} +.ame-eh-column-option-list .ame-eh-column-option:not(:first-child, :last-child) { + border-radius: 0; +} +.ame-eh-column-option-list .ame-eh-column-option:first-child { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.ame-eh-column-option-list .ame-eh-column-option:last-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.ame-eh-column-option-list .ame-eh-column-option.ame-eh-selected-column-option { + background: #CCE8FF; + color: #222; +} + +.ame-eh-selected-cat > .ame-eh-category-item-wrap .ame-eh-category-heading { + margin-top: 0; + border-top-color: #fff; + background: #f8f8f8; +} + +.ame-eh-is-root-category > .ame-eh-category-item-wrap .ame-eh-category-heading { + background: transparent; + border-top-style: none; + border-bottom: none; + font-size: 1.1em; + font-weight: normal; + margin-top: 0; + margin-bottom: -1.7627272727em; +} + +/*# sourceMappingURL=easy-hide-style.css.map */ diff --git a/extras/modules/easy-hide/easy-hide-style.css.map b/extras/modules/easy-hide/easy-hide-style.css.map new file mode 100644 index 0000000..1da18c8 --- /dev/null +++ b/extras/modules/easy-hide/easy-hide-style.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["../../_cat-nav.scss","easy-hide-style.scss","../../../css/_indeterminate-checkbox.scss"],"names":[],"mappings":";AAGA;EACC;EACA;;;AAGD;EACC,kBATkB;;;AAYnB;EACC,kBAZqB;EAarB;;AAGA;EACC,kBAjBoB;;;AAwBrB;EACC,cAFS;;;AACV;EACC,cAFS;;;AACV;EACC,cAFS;;;AACV;EACC,cAFS;;;AAMX;EACC;EACA;EAEA;EAEA;EACA;EACA;EACA;EAEA;;AAEA;EACC;EACA;;AAGD;EACC;;;AAKD;EACC;;;AAIF;EACC;;;ACjDD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EAEA;EACA;EAEA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EAEA;EAEA;EAEA;EACA;;;AAKD;EACC;EACA;EACA;;;AAKD;EACC;EACA;EACA;EAIA;;;AAGD;EACC;EACA;EACA;EACA;EAEA;EACA;EAEA;EACA;EAEA;;AAEA;EACC;;;AAMF;EACC;EACA;EAEA;;ACjGA;EACC;EACA,ODCe;ECMf;EACA;EAMA;EACA;EACA;EACA;EACA;;AAGD;EACC;IAEC,QADU;IAEV,OAFU;IAGV,aAHU;IAIV;IAEA;IACA;IACA;;;;ADwEF;EACC;EAIA;EACA;EACA;EAEA,OA/Ge;EAkHf;EACA;EACA;EAEA;EACA;EAEA;EACA;;;AAIF;EAEE;IAEC,QADU;IAEV,OAFU;IAGV,aAHU;IAIV;;;AAKH;EACC;EACA;EAEA;EACA;;;AAOD;EACC;EACA;AAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAOA;EACC;;AAGD;EACC;;;AAIF;EACC;;;AAGD;EACC;;;AAGD;EACC;EAEA;EACA;EAEA;EACA;EAEA,WAtLiB;;;AA2LlB;EACC;EACA;EACA;;AAEA;EACC;EACA;;;AAIF;EACC;EAEA;EACA;EACA;;;AAUA;EAEC;;AAEA;EACC;;AAIF;EACC;;AAGD;EACC;EACA;;AAGD;EACC;;AAEA;EACC;;AAGD;EACC;EACA;EACA;;AAIF;EACC;;AAIA;EACC,kBDpQgB;;ACwQlB;EACC,kBDzQiB;;AC6QlB;EACC;EACA,KA1QuB;EA2QvB;;AAKD;EACC;;;AAIF;EACC;EACA;EACA;EACA;;AAEA;EACC;;AAGD;EACC;;;AAIF;EACC;EACA;EAEA;EACA;;;AAID;EACC;EACA;EACA;EAEA;EACA,KAnTwB;;;AAwTzB;EACC,cAH6B;;;AAM9B;EACC;EACA;EACA;EACA;EACA;;AAEA;EAIC;EAEA;EACA;;;AAUA;EACC,cAHoB;;;AAErB;EACC,cAHoB;;;AAQvB;EACC;EAEA;EACA;;;AAGD;EACC;EACA;;AAGC;EACC;;AAGD;EACC;;AAGD;EACC;EACA;;AAGD;EACC;EACA;;AAGD;EACC,YD7XmB;EC8XnB;;;AAOH;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EAGA,WADe;EAEf;EAEA;EAIA","file":"easy-hide-style.css"} \ No newline at end of file diff --git a/extras/modules/easy-hide/easy-hide-style.scss b/extras/modules/easy-hide/easy-hide-style.scss new file mode 100644 index 0000000..8999ae7 --- /dev/null +++ b/extras/modules/easy-hide/easy-hide-style.scss @@ -0,0 +1,411 @@ +@use "../../cat-nav"; +@import "../../../css/indeterminate-checkbox"; +@import "../../../css/boxes"; + +$crossMarkColor: #D81536; +$expectedAdminBarHeight: 32px; +$itemContainerPadding: 12px; + +$headingTopMargin: 1.5em; +$headingFontSize: 1.3em; + +#ws_ame_editor_heading { + float: none; +} + +#ws_actor_selector_container { + margin-bottom: 8px; +} + +#ame-easy-hide-ui { + display: flex; + border: 1px solid $amePostboxBorderColor; +} + +#ame-eh-category-container { + flex-basis: 220px; + flex-grow: 0; + max-width: 220px; + + background: #f8f8f8; + border-right: 1px solid $amePostboxBorderColor; + + padding-top: 6px; +} + +#ame-eh-content-area { + flex-grow: 1; + display: flex; + flex-direction: column; +} + +#ame-eh-view-toolbar { + flex-grow: 0; + display: flex; + + align-items: center; + + padding: 10px 12px; + + background: #fcfcfc; + border-bottom: 1px solid #ddd; +} + +//region Search box + +#ame-eh-search-container { + position: relative; + min-width: 250px; + max-width: 100%; +} + +$clearButtonHPadding: 0.3em; + +#ame-eh-search-query { + position: relative; + width: 100%; + appearance: none; + + //Make space for the clear button. The button width is the width + //of the Dashicon (20px) plus some padding. + padding-right: calc(#{$clearButtonHPadding} * 2 + 20px); +} + +.ame-eh-clear-search-box { + position: absolute; + top: 0; + right: 0; + height: 100%; + + border: none; + padding: 0 $clearButtonHPadding; + + background: none; + cursor: pointer; + + color: #888; + + &:hover { + color: #444; + } +} + +//endregion + +#ame-eh-item-container { + flex-grow: 1; + padding: 0 $itemContainerPadding $itemContainerPadding $itemContainerPadding; + + background: #fff; + + input[type=checkbox] { + @include ame-indeterminate-checkbox($crossMarkColor); + } +} + +input[type=checkbox].ame-eh-negative-box { + &:checked:before { + content: '\1F7AC'; + //See Wikipedia for other "X" marks: + //https://en.wikipedia.org/wiki/X_mark + + font-weight: bold; + font-size: 15px; + line-height: 1rem; + + color: $crossMarkColor; + //background: rgba(100, 100, 200, 0.3); + + float: left; + display: inline-block; + vertical-align: middle; + + width: 1rem; + height: 1rem; + + margin: -1px; + margin-left: -1.5px; + } +} + +@media screen and (max-width: 782px) { + input[type=checkbox].ame-eh-negative-box { + &:checked:before { + $boxSize: 1.5625rem; + height: $boxSize; + width: $boxSize; + line-height: $boxSize; + font-size: 18px; + } + } +} + +.ame-eh-item { + padding: 0; + margin: 0; + + font-size: 14px; + line-height: 1.65; +} + +.ame-eh-item-self { + //padding: 3px 0 3px 0; +} + +.ame-eh-item-list { + margin: 0; + padding: 0; + + /* + Ideally, I'd like to avoid inserting a column break after an item that has + any children, but nothing I've tried has worked. The "orphans" and "widows" + properties seem to have no effect in nested lists. The following CSS also doesn't + seem to work, at least not with the current HTML structure. I've left it here + in case future browsers start treating lists better. + */ + li:first-child { + break-after: avoid; + } + + li:last-child { + break-before: avoid; + } +} + +.ame-eh-item > .ame-eh-item-list { + margin-left: 1.7em; +} + +.ame-eh-search-highlight { + background-color: #ffff00; +} + +.ame-eh-category-heading { + background: #fcfcfc; + + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + + padding: 0.4em $itemContainerPadding 0.6em $itemContainerPadding; + margin: $headingTopMargin (-$itemContainerPadding) 0.70em (-$itemContainerPadding); + + font-size: $headingFontSize; +} + +//region Category navigation + +.ame-eh-category-list { + margin: 0; + padding: 0; + list-style: none; + + li { + margin: 0; + padding: 0; + } +} + +.ame-eh-cat-nav-item { + padding: 4px 8px 4px; + + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.ame-eh-cat-label { + +} + +//endregion + +table.ame-eh-category-table-view { + th, td { + //Right border. + box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1); + + &:last-child { + box-shadow: none; + } + } + + tbody th { + text-align: left; + } + + input[type=checkbox] { + vertical-align: middle; + margin: -0.25rem 0.25rem 0 -1px; + } + + td { + text-align: center; + + input[type=checkbox] { + margin-right: 0; + } + + label { + box-sizing: border-box; + display: block; + width: 100%; + } + } + + .ame-eh-table-corner-cell { + border-bottom: none; + } + + tbody tr:hover { + th, td { + background-color: cat-nav.$catNavHoverColor; + } + } + + .ame-eh-hovered-column { + background-color: cat-nav.$catNavHoverColor; + } + + //Fixed table header. + thead th { + position: sticky; + top: $expectedAdminBarHeight; //Leave room for the WordPress Toolbar. + background: white; //Avoid text overlap; better for readability. + } + + //The first column doesn't have any header text, so that cell doesn't + //need to be sticky. + thead th:first-child { + position: unset; + } +} + +.ame-eh-category-subtitle { + color: #888; + font-size: 0.95em; + font-family: Consolas, Monaco, monospace; + line-height: 1; + + &::before { + content: "("; + } + + &::after { + content: ")"; + } +} + +.ame-eh-lazy-category { + min-height: 100px; + outline: 1px dashed $amePostboxBorderColor; + + padding-bottom: 0.5rem; + margin-bottom: 1rem; +} + +//region Save buttons +#ame-eh-side-save-button { + margin-top: 50px; + padding: 8px 8px; + border-top: 1px solid #dcdcde; + + position: sticky; + top: $expectedAdminBarHeight; +} + +$topSaveButtonContainerWidth: 130px; + +#ame-easy-hide #ws_actor_selector_container { + margin-right: $topSaveButtonContainerWidth; +} + +#ame-eh-top-save-button { + float: right; + box-sizing: border-box; + width: $topSaveButtonContainerWidth - 1px; + text-align: right; + padding-left: 10px; + + input[type="submit"] { + //Align the button with the text in the actor selector. This is not exactly + //correct because the line heights are different and the selector has a nested + //structure that uses different units for margins and padding. + margin-top: 7px; + + max-width: $topSaveButtonContainerWidth - 1px; + margin-bottom: 5px; + } +} + +//endregion + +//region Variable number of columns + +@for $columnCount from 2 through 3 { + .ame-eh-item-columns-#{$columnCount} .ame-eh-columns-allowed { + .ame-eh-category-items > .ame-eh-item-list { + column-count: $columnCount; + } + } +} + +#ame-eh-column-selector { + margin-left: auto; + + display: flex; + align-items: center; +} + +.ame-eh-column-option-list { + margin-left: 0.4em; + display: flex; + + .ame-eh-column-option { + &:not(:first-child) { + border-left-style: none; + } + + &:not(:first-child,:last-child) { + border-radius: 0; + } + + &:first-child { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + &:last-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + &.ame-eh-selected-column-option { + background: cat-nav.$catNavSelectedColor; + color: #222; + } + } +} + +//endregion + +.ame-eh-selected-cat > .ame-eh-category-item-wrap .ame-eh-category-heading { + margin-top: 0; + border-top-color: #fff; + background: #f8f8f8; +} + +.ame-eh-is-root-category > .ame-eh-category-item-wrap .ame-eh-category-heading { + background: transparent; + border-top-style: none; + border-bottom: none; + + $rootFontSize: 1.1em; + font-size: $rootFontSize; + font-weight: normal; + + margin-top: 0; + + //Pull the next heading up. Because the font sizes are different, the em-based + //measurements need to be scaled up proportionally. Let's add a safety factor, too. + margin-bottom: (-($headingTopMargin * ($headingFontSize / $rootFontSize)) + 0.01em); +} \ No newline at end of file diff --git a/extras/modules/easy-hide/easy-hide-template.php b/extras/modules/easy-hide/easy-hide-template.php new file mode 100644 index 0000000..5f93018 --- /dev/null +++ b/extras/modules/easy-hide/easy-hide-template.php @@ -0,0 +1,284 @@ +'; + +if ( isset($_GET['message']) && (intval($_GET['message']) === 1) ) { + add_settings_error('ame-easy-hide-page', 'settings_updated', __('Settings saved.'), 'updated'); +} +settings_errors('ame-easy-hide-page'); + +//WP 4.3+ uses H1 headings for admin pages. +$headingTag = 'h1'; + +printf( + '<%1$s id="ws_ame_editor_heading">%2$s - Easy Hide', + $headingTag, + apply_filters('admin_menu_editor-self_page_title', 'Menu Editor') +); +?> + +
+ +
+

+ Tip: This page puts all the + settings that are related to hiding things in one place. It's an alternative way + to quickly find and edit those settings. +

+

+ If you don't need this page, you can disable the "" + module in Settings. +

+
+ + +
+ Loading... +
+ +
+ +
+ + + + + + + + + +
+ +'; //Close the "wrap" container. \ No newline at end of file diff --git a/extras/modules/easy-hide/easy-hide.js b/extras/modules/easy-hide/easy-hide.js new file mode 100644 index 0000000..751048f --- /dev/null +++ b/extras/modules/easy-hide/easy-hide.js @@ -0,0 +1,867 @@ +/// +/// +/// +/// +/// +/// +/// +'use strict'; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var ameEasyHideModel = null; +var AmeEasyHide; +(function (AmeEasyHide) { + var _ = wsAmeLodash; + var SortOrder; + (function (SortOrder) { + SortOrder[SortOrder["SORT_ALPHA"] = 0] = "SORT_ALPHA"; + SortOrder[SortOrder["SORT_INSERTION"] = 1] = "SORT_INSERTION"; + })(SortOrder || (SortOrder = {})); + var Category = /** @class */ (function () { + function Category(id, label, parent, invertItemState, filterState, initialSortOrder, itemSortOrder, priority, subtitle) { + if (parent === void 0) { parent = null; } + if (invertItemState === void 0) { invertItemState = false; } + if (filterState === void 0) { filterState = null; } + if (initialSortOrder === void 0) { initialSortOrder = SortOrder.SORT_ALPHA; } + if (itemSortOrder === void 0) { itemSortOrder = SortOrder.SORT_INSERTION; } + if (priority === void 0) { priority = Category.DEFAULT_PRIORITY; } + if (subtitle === void 0) { subtitle = null; } + var _this = this; + this.id = id; + this.label = label; + this.parent = parent; + this.invertItemState = invertItemState; + this.initialSortOrder = initialSortOrder; + this.itemSortOrder = itemSortOrder; + this.priority = priority; + this.subtitle = subtitle; + this.containsSelectedCategory = ko.observable(false); + this.isStandardRenderingEnabled = ko.observable(true); + this.tableViewEnabled = false; + this.tableView = null; + this.cachedParentList = null; + Category.counter++; + this.safeElementId = 'ame-eh-category-n-' + Category.counter; + this.isSelected = ko.observable(false); + /** + * Is this category selected or inside a selected category? + */ + this.isInSelectedCategory = ko.pureComputed(function () { + var current = _this; + while (current !== null) { + if (current.isSelected()) { + return true; + } + current = current.parent; + } + return false; + }); + this.isShowingItems = ko.pureComputed(function () { + //The items are visible if this category or one of its parents is selected. + return _this.isInSelectedCategory(); + }); + this.isExpanded = ko.observable(this.parent === null); + this.isVisible = ko.pureComputed(function () { + var inSelectedTree = _this.isInSelectedCategory() || _this.containsSelectedCategory(); + if (!inSelectedTree) { + return false; + } + //Show the category if it has any visible children: items or subcategories. + var items = _this.directItems(); + if (_.some(items, function (item) { return item.isVisible(); })) { + return true; + } + var subcategories = _this.subcategories(); + return _.some(subcategories, function (category) { return category.isVisible(); }); + }); + this.subcategories = ko.observableArray([]); + this.sortedSubcategories = ko.pureComputed(function () { + var cats = _this.subcategories(); + //Remove categories that won't be rendered. + cats = cats.filter(function (c) { return c.isStandardRenderingEnabled(); }); + if (_this.initialSortOrder === SortOrder.SORT_ALPHA) { + cats.sort(function (a, b) { + if (a.priority !== b.priority) { + return a.priority - b.priority; + } + return a.label.localeCompare(b.label); + }); + } + else if (_this.initialSortOrder === SortOrder.SORT_INSERTION) { + //We want a stable sort that preserves insertion order for + //categories that have the same priority. As of this writing, + //Array.sort() is not stable in IE, so let's use Lodash. + cats = _.sortBy(cats, 'priority'); + } + return cats; + }); + this.items = ko.observableArray([]); + this.directItems = ko.pureComputed(function () { + var results = _(_this.items()) + .filter(function (item) { + //An item is a direct/root level item in this category + //only if it has no parent or if its parent is not + //in the same category. + if (item.parent === null) { + return true; + } + else { + return !_.contains(item.parent.categories, _this); + } + }) + .value(); + //Sort items alphabetically if requested. + if (_this.itemSortOrder === SortOrder.SORT_ALPHA) { + results.sort(function (a, b) { + return a.label.localeCompare(b.label); + }); + } + return results; + }); + this.isIndeterminate = ko.observable(false); + //The whole category is checked if at least one of its + //items or subcategories is checked. + this.isChecked = ko.computed({ + read: function () { + var hasIndeterminateChildren = false; + var hasCheckedItems = false, hasUncheckedItems = false; + _.forEach(_this.items(), function (item) { + if (item.isChecked()) { + hasCheckedItems = true; + } + else { + hasUncheckedItems = true; + } + if (item.isIndeterminate()) { + hasIndeterminateChildren = true; + } + if (hasCheckedItems && hasUncheckedItems) { + //We know the category has a mix of checked and + //unchecked items, so there's no need to continue. + return false; + } + }); + var hasCheckedCats = false, hasUncheckedCats = false; + _.forEach(_this.subcategories(), function (category) { + if (category.isChecked()) { + hasCheckedCats = true; + } + else { + hasUncheckedCats = true; + } + if (category.isIndeterminate()) { + hasIndeterminateChildren = true; + } + if (hasCheckedCats && hasUncheckedCats) { + return false; + } + }); + var areAnyChecked = hasCheckedItems || hasCheckedCats; + var areAnyUnchecked = hasUncheckedItems || hasUncheckedCats; + _this.isIndeterminate(hasIndeterminateChildren || (areAnyChecked && areAnyUnchecked)); + return areAnyChecked; + }, + write: function (checked) { + //Update items. + _.forEach(_this.items(), function (item) { + if (item.isEditableForSelectedActor) { + item.isChecked(checked); + } + }); + //Update subcategories. + _.forEach(_this.subcategories(), function (category) { + category.isChecked(checked); + }); + }, + deferEvaluation: true + }).extend({ rateLimit: { timeout: 20, method: 'notifyWhenChangesStop' } }); + this.nestingDepth = ko.pureComputed({ + read: function () { + if (_this.parent !== null) { + return _this.parent.nestingDepth() + 1; + } + return 1; + }, + deferEvaluation: true + }); + this.navCssClasses = ko.pureComputed({ + read: function () { + var classes = []; + if (_this.isSelected()) { + //classes.push('ame-selected-cat-nav-item'); + } + if (_this.sortedSubcategories().length > 0) { + classes.push('ame-cat-nav-has-children'); + } + if (_this.isExpanded()) { + classes.push('ame-cat-nav-is-expanded'); + } + if (_this.isSelected()) { + classes.push('ame-selected-cat-nav-item'); + } + classes.push('ame-cat-nav-level-' + _this.nestingDepth()); + return classes.join(' '); + }, + deferEvaluation: true + }); + this.isNavVisible = ko.pureComputed({ + read: function () { + if (_this.parent === null) { + return true; + } + if (!_this.isStandardRenderingEnabled()) { + return false; + } + return _this.parent.isNavVisible() && _this.parent.isExpanded(); + }, + deferEvaluation: true + }); + //Category labels are not searched, but the table view has categories that + //represent the same item being used on multiple screens. In that case, + //category labels typically match item labels, so let's highlight them. + this.highlightedLabel = ko.pureComputed(function () { + var text = _.escape(_this.label); + if (filterState !== null) { + text = filterState.highlightSearchKeywords(text); + } + return text; + }); + var wasRendered = ko.observable(false); + this.shouldRenderContent = ko.computed({ + read: function () { + return wasRendered(); + }, + write: function (state) { + //This is a write-once flag. Once it's turned on, + //it can't be turned off again. + if (wasRendered()) { + return; + } + wasRendered(state); + //Notify that a category is being rendered. The DOM might not be + //updated yet when this event happens, so subscribers should wait + //at least until the next frame. + if (wasRendered()) { + jQuery(document).trigger('adminmenueditor:ehCategoryRendering', [_this]); + } + } + }); + } + Category.prototype.toggle = function () { + this.isExpanded(!this.isExpanded()); + }; + Category.prototype.enableTableView = function (rowCat, columnCat) { + this.tableViewEnabled = true; + this.tableView = new CategoryTableView(rowCat, columnCat); + //Disable normal rendering for the two row/column categories. + //They will only appear in the table. + rowCat.isStandardRenderingEnabled(false); + columnCat.isStandardRenderingEnabled(false); + }; + Object.defineProperty(Category.prototype, "allParents", { + get: function () { + //The parent property is readonly, so the result should not change after + //the category is initialized. We can cache it indefinitely. + if (this.cachedParentList === null) { + var parents = []; + var current = this.parent; + while (current !== null) { + parents.push(current); + current = current.parent; + } + //Reverse the list so that it starts at the root. + parents.reverse(); + this.cachedParentList = parents; + } + return this.cachedParentList; + }, + enumerable: false, + configurable: true + }); + Category.fromProps = function (props, parent, filterState) { + var _a, _b, _c, _d, _e; + if (parent === void 0) { parent = null; } + if (filterState === void 0) { filterState = null; } + return new Category(props.id, props.label, parent, (_a = props.invertItemState) !== null && _a !== void 0 ? _a : false, filterState, (_b = props.sort) !== null && _b !== void 0 ? _b : SortOrder.SORT_ALPHA, (_c = props.itemSort) !== null && _c !== void 0 ? _c : SortOrder.SORT_INSERTION, (_d = props.priority) !== null && _d !== void 0 ? _d : Category.DEFAULT_PRIORITY, (_e = props.subtitle) !== null && _e !== void 0 ? _e : null); + }; + Category.DEFAULT_PRIORITY = 10; + Category.counter = 0; + return Category; + }()); + function isBinaryProps(props) { + return ('binary' in props) ? props.binary : false; + } + var HideableItem = /** @class */ (function () { + function HideableItem(id, label, categories, parent, initialEnabled, isInverted, component, tooltip, subtitle, selectedActorRef, allActorsRef, filterState) { + if (categories === void 0) { categories = []; } + if (parent === void 0) { parent = null; } + if (initialEnabled === void 0) { initialEnabled = {}; } + if (isInverted === void 0) { isInverted = false; } + if (component === void 0) { component = null; } + if (tooltip === void 0) { tooltip = null; } + if (subtitle === void 0) { subtitle = null; } + var _this = this; + this.id = id; + this.label = label; + this.categories = categories; + this.parent = parent; + this.initialEnabled = initialEnabled; + this.isInverted = isInverted; + this.component = component; + this.tooltip = tooltip; + this.subtitle = subtitle; + this.selectedActorRef = selectedActorRef; + this.children = []; + this.actorSettings = new AmeObservableActorSettings(initialEnabled); + var _isIndeterminate = ko.observable(false); + this.isIndeterminate = ko.pureComputed(function () { + if (selectedActorRef() === null) { + return _isIndeterminate(); + } + return false; + }); + this.isChecked = this.createCheckedObservable(selectedActorRef, allActorsRef, _isIndeterminate); + var wasRendered = false; + this.shouldRender = ko.observable(false); + this.isVisible = ko.computed(function () { + var visible = filterState.itemMatchesFilter(_this); + if (visible && !wasRendered) { + wasRendered = true; + _this.shouldRender(true); + } + return visible; + }); + this.htmlLabel = ko.pureComputed(function () { + var html = _.escape(_this.label); + if (_this.isVisible()) { + html = filterState.highlightSearchKeywords(html); + } + return html; + }); + } + HideableItem.prototype.createCheckedObservable = function (selectedActorRef, allActorsRef, outIndeterminate) { + var _this = this; + return ko.computed({ + read: function () { + var enabled = _this.actorSettings.isEnabledFor(selectedActorRef(), allActorsRef(), _this.isInverted, _this.isInverted, _this.isInverted, outIndeterminate); + return _this.isInverted ? (!enabled) : enabled; + }, + write: function (checked) { + _this.actorSettings.setEnabledFor(selectedActorRef(), _this.isInverted ? !checked : checked, allActorsRef(), _this.isInverted); + for (var i = 0; i < _this.children.length; i++) { + _this.children[i].isChecked(checked); + } + }, + deferEvaluation: true + }); + }; + HideableItem.fromJs = function (props, selectedActor, allActors, filterState, categories, parent) { + var _a, _b, _c, _d, _e; + if (categories === void 0) { categories = []; } + if (parent === void 0) { parent = null; } + if (isBinaryProps(props)) { + return BinaryHideableItem.fromJs(props, selectedActor, allActors, filterState, categories, parent); + } + return new HideableItem(props.id, props.label, categories, parent, (_a = props.enabled) !== null && _a !== void 0 ? _a : {}, (_b = props.inverted) !== null && _b !== void 0 ? _b : false, (_c = props.component) !== null && _c !== void 0 ? _c : null, (_d = props.tooltip) !== null && _d !== void 0 ? _d : null, (_e = props.subtitle) !== null && _e !== void 0 ? _e : null, selectedActor, allActors, filterState); + }; + HideableItem.prototype.toJs = function () { + var result = {}; + if (this.isInverted) { + result.inverted = true; + } + if ((this.component !== null) && (this.component !== '')) { + result.component = this.component; + } + var enabled = this.actorSettings.getAll(); + if (!_.isEmpty(enabled)) { + result.enabled = enabled; + } + return result; + }; + Object.defineProperty(HideableItem.prototype, "isEditableForSelectedActor", { + get: function () { + return true; + }, + enumerable: false, + configurable: true + }); + return HideableItem; + }()); + var BinaryHideableItem = /** @class */ (function (_super) { + __extends(BinaryHideableItem, _super); + function BinaryHideableItem() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.isEnabledForAll = ko.observable(false); + return _this; + } + BinaryHideableItem.prototype.createCheckedObservable = function (selectedActorRef, allActorsRef, outIndeterminate) { + var _this = this; + var observable = ko.computed({ + read: function () { + if (_this.isInverted) { + return !_this.isEnabledForAll(); + } + else { + return _this.isEnabledForAll(); + } + }, + write: function (value) { + if (_this.isEditableForSelectedActor) { + if (_this.isInverted) { + value = !value; + } + _this.isEnabledForAll(value); + } + else { + //Reset the checkbox to the original value. + observable.notifySubscribers(); + } + }, + deferEvaluation: true, + }); + return observable; + }; + BinaryHideableItem.fromJs = function (props, selectedActor, allActors, filterState, categories, parent) { + var _a, _b, _c, _d; + if (categories === void 0) { categories = []; } + if (parent === void 0) { parent = null; } + var instance = new BinaryHideableItem(props.id, props.label, categories, parent, {}, (_a = props.inverted) !== null && _a !== void 0 ? _a : false, (_b = props.component) !== null && _b !== void 0 ? _b : null, (_c = props.tooltip) !== null && _c !== void 0 ? _c : null, (_d = props.subtitle) !== null && _d !== void 0 ? _d : null, selectedActor, allActors, filterState); + if (props.hasOwnProperty('enabledForAll')) { + instance.isEnabledForAll(props.enabledForAll); + } + return instance; + }; + BinaryHideableItem.prototype.toJs = function () { + var result = _super.prototype.toJs.call(this); + delete result.enabled; + result.enabledForAll = this.isEnabledForAll(); + return result; + }; + Object.defineProperty(BinaryHideableItem.prototype, "isEditableForSelectedActor", { + get: function () { + return (this.selectedActorRef() === null); + }, + enumerable: false, + configurable: true + }); + return BinaryHideableItem; + }(HideableItem)); + var CategoryTableView = /** @class */ (function () { + function CategoryTableView(rowCategory, columnCategory) { + this.rowCategory = rowCategory; + this.columnCategory = columnCategory; + this.itemLookup = {}; + this.columnHeaders = null; + } + Object.defineProperty(CategoryTableView.prototype, "rows", { + get: function () { + return CategoryTableView.sortCategoriesByLabel(this.rowCategory.subcategories); + }, + enumerable: false, + configurable: true + }); + Object.defineProperty(CategoryTableView.prototype, "columns", { + get: function () { + return CategoryTableView.sortCategoriesByLabel(this.columnCategory.subcategories); + }, + enumerable: false, + configurable: true + }); + CategoryTableView.sortCategoriesByLabel = function (categories) { + var list = categories(); + list.sort(function (a, b) { + return a.label.localeCompare(b.label); + }); + return list; + }; + CategoryTableView.prototype.getCellItems = function (row, column) { + var path = [row.id, column.id]; + var items = _.get(this.itemLookup, path, null); + if (items !== null) { + return items; + } + //Find items that are present in both categories. + items = _.intersection(row.directItems(), column.directItems()); + _.set(this.itemLookup, path, items); + return items; + }; + /** + * Highlight the column heading when the user hovers over a table cell. + * + * @param unused Knockout provides the current model value to the callback, but we don't need it. + * @param event JavaScript event object + */ + CategoryTableView.prototype.onTableHover = function (unused, event) { + if (!event || !event.target) { + return; + } + var $cell = jQuery(event.target).closest('td, th'); + if ($cell.length < 1) { + return; + } + if ( + //Has the header list been initialized? + (this.columnHeaders === null) + //The table might have been re-rendered or removed from the DOM. + //In that case, we'll need to find the new header elements. + || (this.columnHeaders.closest('body').length < 1)) { + this.columnHeaders = $cell.closest('table').find('thead tr').first().find('th'); + } + var index = $cell.index(); + //The first column doesn't have a header, so it doesn't need to be highlighted. + if (index === 0) { + return; + } + var $heading = this.columnHeaders.eq(index); + if (!$heading || ($heading.length === 0)) { + return; + } + var highlightClass = 'ame-eh-hovered-column'; + if ($heading.hasClass(highlightClass)) { + return; //This column is already highlighted. + } + this.columnHeaders.removeClass(highlightClass); + $heading.addClass(highlightClass); + }; + return CategoryTableView; + }()); + var FilterOptions = /** @class */ (function () { + function FilterOptions() { + var _this = this; + /** + * Bind this observable to the search box, then use searchQuery to actually + * read the query. This is only public because it's used in a KO template. + */ + this.internalSearchQuery = ko.observable(''); + this.searchQuery = ko.pureComputed(function () { return _this.internalSearchQuery(); }); + this.searchQuery.extend({ rateLimit: { timeout: 100, method: "notifyWhenChangesStop" } }); + this.searchKeywords = ko.pureComputed(function () { + var query = _this.searchQuery().trim(); + if (query === '') { + return []; + } + return _(query.split(' ')) + .map(function (keyword) { return keyword.trim(); }) + .filter(function (keyword) { return (keyword !== ''); }) + .value(); + }); + this.highlightRegex = ko.pureComputed(function () { + var keywordList = _this.searchKeywords(); + if (keywordList.length < 1) { + return null; + } + var keywordGroup = _.map(keywordList, _.escapeRegExp).join('|'); + return new RegExp('(?:' + keywordGroup + ')', 'gi'); + }); + } + FilterOptions.prototype.itemMatchesFilter = function (item) { + var keywords = this.searchKeywords(); + if (keywords.length > 0) { + var haystack_1 = item.label.toLowerCase(); + var matchesKeywords = _.all(keywords, function (keyword) { return (haystack_1.indexOf(keyword) >= 0); }); + if (!matchesKeywords) { + return false; + } + } + return true; + }; + FilterOptions.prototype.highlightSearchKeywords = function (input) { + var regex = this.highlightRegex(); + if (regex === null) { + return input; + } + return input.replace(regex, function (foundKeyword) { + return '' + foundKeyword + ''; + }); + }; + FilterOptions.prototype.clearSearchBox = function () { + this.internalSearchQuery(''); + }; + FilterOptions.prototype.processEscKey = function (unusedKoModel, event) { + //Ignore events triggered during IME composition. + //See https://developer.mozilla.org/en-US/docs/Web/API/Document/keydown_event#ignoring_keydown_during_ime_composition + if (event.isComposing) { + return true; + } + //noinspection JSDeprecatedSymbols + var isEscape = (((typeof event['code'] !== 'undefined') && (event.code === 'Escape')) + //IE doesn't support KeyboardEvent.code, so use keyCode instead. + || ((typeof event['keyCode'] !== 'undefined') && (event.keyCode === 27))); + if (isEscape) { + this.clearSearchBox(); + } + return true; + }; + return FilterOptions; + }()); + var Model = /** @class */ (function () { + function Model(settings, prefs) { + var _this = this; + var _a, _b; + this.categoryLookup = {}; + this.settingsData = ko.observable(''); + this.isSaveButtonEnabled = ko.observable(true); + this.preferences = prefs.observableObject(ko); + prefs.enableAutoSave(3000); + this.actorSelector = new AmeActorSelector(AmeActors, true); + this.selectedActor = this.actorSelector.createActorObservable(ko); + this.selectedActorId = ko.pureComputed(function () { + var actor = _this.selectedActor(); + if (actor === null) { + return ''; + } + return actor.getId(); + }); + var allActors = ko.pureComputed(function () { + return _this.actorSelector.getVisibleActors(); + }); + //Reselect the previously selected actor. + if (settings.selectedActor && AmeActors.actorExists(settings.selectedActor)) { + this.selectedActor(AmeActors.getActor(settings.selectedActor)); + } + this.filterState = new FilterOptions(); + var _ = wsAmeLodash; + //Initialize categories. + this.rootCategory = new Category('_root', 'All', null, false, this.filterState, SortOrder.SORT_ALPHA); + this.rootCategory.shouldRenderContent(true); + var catsWithTableView = []; + _.forEach(settings.categories, function (props) { + var parent = _this.rootCategory; + if (props.parent) { + parent = _this.categoryLookup[props.parent]; + } + var cat = Category.fromProps(props, parent, _this.filterState); + _this.categoryLookup[cat.id] = cat; + parent.subcategories.push(cat); + if (props.tableView) { + catsWithTableView.push(props); + } + }); + //Initialize table views. This is a separate step because tables need + //their row and column categories to be already created. + _.forEach(catsWithTableView, function (props) { + var cat = _this.categoryLookup[props.id]; + cat.enableTableView(_this.categoryLookup[props.tableView.rowCategory], _this.categoryLookup[props.tableView.columnCategory]); + }); + //Initialize items. + var itemsById = {}; + _.forEach(settings.items, function (props) { + var parent = null; + if (props.parent && itemsById.hasOwnProperty(props.parent)) { + parent = itemsById[props.parent]; + } + var categories = []; + if (props.categories) { + _.forEach(props.categories, function (id) { + if (_this.categoryLookup.hasOwnProperty(id)) { + categories.push(_this.categoryLookup[id]); + } + }); + } + if (categories.length < 1) { + categories.push(_this.rootCategory); + } + if (_.some(categories, 'invertItemState') && (typeof props['inverted'] === 'undefined')) { + props.inverted = true; + } + var item = HideableItem.fromJs(props, _this.selectedActor, allActors, _this.filterState, categories, parent); + itemsById[item.id] = item; + if (parent) { + parent.children.push(item); + } + _.forEach(categories, function (category) { + category.items.push(item); + }); + }); + this.itemContainerClasses = ko.pureComputed(function () { + var classes = []; + var columns = _this.preferences.numberOfColumns(); + if (columns > 1) { + classes.push('ame-eh-item-columns-' + columns); + } + return classes.join(' '); + }); + this.selectedCategory = ko.observable(this.rootCategory); + //Update the "isSelected" and "containsSelectedCategory" flags + //on the category object when the user selects a category. + var previousSelectedCategory = this.selectedCategory.peek(); + if (previousSelectedCategory) { + previousSelectedCategory.isSelected(true); + } + this.selectedCategory.subscribe(function (newSelection) { + if (newSelection !== previousSelectedCategory) { + //Save the old selection in case changing isSelected also triggers this callback somehow. + var oldSelection = previousSelectedCategory; + previousSelectedCategory = newSelection; + //The previous category is no longer selected. + oldSelection.isSelected(false); + var previousTree = oldSelection.allParents; + var newTree = newSelection.allParents; + //Find the point of divergence. + var minLength = Math.min(previousTree.length, newTree.length); + var divergenceIndex = -1; + for (var i = 0; i < minLength; i++) { + if (newTree[i] !== previousTree[i]) { + divergenceIndex = i; + break; + } + } + //Update categories that are no longer in the selected tree. + if (divergenceIndex >= 0) { + for (var i = divergenceIndex; i < previousTree.length; i++) { + previousTree[i].containsSelectedCategory(false); + } + } + //Update categories that contain the new selection. + for (var i = Math.max(divergenceIndex, 0); i < newTree.length; i++) { + newTree[i].containsSelectedCategory(true); + } + } + newSelection.isSelected(true); + newSelection.shouldRenderContent(true); + }); + //Restore previously expanded categories. + _.forEach((_a = this.preferences.csExpandedCategories()) === null || _a === void 0 ? void 0 : _a.split("\n"), function (id) { + if ((typeof id === 'string') && _this.categoryLookup.hasOwnProperty(id)) { + _this.categoryLookup[id].isExpanded(true); + } + }); + //Save expanded categories in user preferences. + var expandedCategories = ko.computed(function () { + //Make a list of category IDs. + return _(_this.categoryLookup).filter(function (category) { + //Skip the root category. It's always expanded. + if (category === _this.rootCategory) { + return false; + } + //Skip categories that don't have any children. + if (category.subcategories().length === 0) { + return false; + } + return category.isExpanded(); + }).pluck('id').value(); + }).extend({ rateLimit: { timeout: 100, method: 'notifyWhenChangesStop' } }); + expandedCategories.subscribe(function (newValue) { + _this.preferences.csExpandedCategories(newValue.join("\n")); + }); + //Reselect the previously selected category. + if (settings.selectedCategory + && this.categoryLookup.hasOwnProperty(settings.selectedCategory)) { + this.selectedCategory(this.categoryLookup[settings.selectedCategory]); + } + //Render the first couple of categories to push the other category + //placeholders below the bottom of the viewport. + _(this.rootCategory.sortedSubcategories()).take(2).forEach(function (c) { + c.shouldRenderContent(true); + }).commit(); + //Always render the selected category. + (_b = this.selectedCategory()) === null || _b === void 0 ? void 0 : _b.shouldRenderContent(true); + this.selectedCategoryId = ko.pureComputed(function () { + var category = _this.selectedCategory(); + if (category === null) { + return ''; + } + return category.id; + }); + } + Model.prototype.onCategoryEntersViewport = function (element) { + var category = ko.dataFor(element); + if (category instanceof Category) { + if (console && console.log) { + console.log('Rendering category', category.id); + } + category.shouldRenderContent(true); + } + }; + Model.prototype.renderAllCategories = function () { + function renderChildren(category) { + _.forEach(category.sortedSubcategories(), function (c) { + c.shouldRenderContent(true); + renderChildren(c); + }); + } + renderChildren(this.rootCategory); + }; + Model.prototype.saveChanges = function () { + this.isSaveButtonEnabled(false); + this.settingsData(JSON.stringify(this.getCurrentSettings())); + return true; + }; + Model.prototype.getCurrentSettings = function () { + function collectItemsRecursively(category, output) { + if (output === void 0) { output = {}; } + _.forEach(category.items(), function (item) { + if (!output.hasOwnProperty(item.id)) { + output[item.id] = item.toJs(); + } + }); + _.forEach(category.subcategories(), function (subcategory) { return collectItemsRecursively(subcategory, output); }); + return output; + } + return { + items: collectItemsRecursively(this.rootCategory) + }; + }; + return Model; + }()); + AmeEasyHide.Model = Model; +})(AmeEasyHide || (AmeEasyHide = {})); +document.addEventListener('DOMContentLoaded', function () { + ameEasyHideModel = new AmeEasyHide.Model(wsEasyHideData, ameEhUserPreferences); + ko.applyBindings(ameEasyHideModel, document.getElementById('ame-easy-hide-container')); + //Render categories lazily. + try { + var lazyUpdateTimer_1 = null; + var ameEhLazyLoad_1 = new LazyLoad({ + elements_selector: '.ame-eh-lazy-category', + unobserve_entered: true, + callback_enter: function (element) { + ameEasyHideModel.onCategoryEntersViewport(element); + } + }); + jQuery(document).on('adminmenueditor:ehCategoryRendering', function () { + //New placeholders might be created after rendering a category, + //so let's update LazyLoad. + //Debounce updates by ensuring that there's only one pending timer. + if (lazyUpdateTimer_1 !== null) { + clearTimeout(lazyUpdateTimer_1); + } + lazyUpdateTimer_1 = window.setTimeout(function () { + lazyUpdateTimer_1 = null; + ameEhLazyLoad_1.update(); + }, 40); + }); + } + catch (ex) { + //I'm not sure if LazyLoad will actually throw an exception if the user has + //an old browser that doesn't support IntersectionObserver, but let's fall back + //to showing all categories if anything goes wrong. + ameEasyHideModel.renderAllCategories(); + } + //Handle clicks on the "dismiss" button in the explanatory notice. It wouldn't be safe + //to use Knockout for this because WordPress automatically moves notices below the first + //h1/h2 element, and any external DOM manipulation can mess up KO bindings. + jQuery('#ame-easy-hide-explanation').on('click', '.notice-dismiss', function () { + if (typeof ameEhIsExplanationHidden !== 'undefined') { + ameEhIsExplanationHidden(true); + ameEhIsExplanationHidden.save(); + } + }); + //We no longer need the input data, so we can potentially free up + //some memory by clearing it. + wsEasyHideData = null; +}); +//# sourceMappingURL=easy-hide.js.map \ No newline at end of file diff --git a/extras/modules/easy-hide/easy-hide.js.map b/extras/modules/easy-hide/easy-hide.js.map new file mode 100644 index 0000000..ec81d23 --- /dev/null +++ b/extras/modules/easy-hide/easy-hide.js.map @@ -0,0 +1 @@ +{"version":3,"file":"easy-hide.js","sourceRoot":"","sources":["easy-hide.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/extras/modules/easy-hide/easy-hide.php b/extras/modules/easy-hide/easy-hide.php new file mode 100644 index 0000000..5d9ea31 --- /dev/null +++ b/extras/modules/easy-hide/easy-hide.php @@ -0,0 +1,676 @@ +menuEditor = $menuEditor; + + //This module uses the WP_Error::merge_from() method, which is only available in WP 5.6.0+. + if ( !method_exists(WP_Error::class, 'merge_from') ) { + add_action('all_admin_notices', function () { + if ( !current_user_can('activate_plugins') ) { + return; + } + printf( + '

%s

', + 'The "Easy Hide" module requires WordPress 5.6.0 or newer.' + . ' Please upgrade your WordPress installation or disable this module in Admin Menu Editor settings.' + ); + }); + return; + } + + //Register the EasyHide menu item after the "Menu Editor (Pro)" item. + add_action('admin_menu_editor-editor_menu_registered', [$this, 'addAdminMenu']); + + if ( is_admin() ) { + $this->isExplanationHidden = new AmeEhIsExplanationHidden(); + $this->userPreferences = new AmeEhUserPreferences(); + + $this->isExplanationHidden + ->installAjaxCallback(array($this->menuEditor, 'current_user_can_edit_menu')); + $this->userPreferences + ->installAjaxCallback(array($this->menuEditor, 'current_user_can_edit_menu')); + } + } + + public function addAdminMenu() { + if ( !$this->menuEditor->current_user_can_edit_menu() ) { + return; + } + + $this->parentMenuSlug = is_network_admin() ? 'settings.php' : 'options-general.php'; + + $suffix = add_submenu_page( + $this->parentMenuSlug, + 'Easy Hide', + 'Easy Hide', + apply_filters('admin_menu_editor-capability', 'manage_options'), + self::MENU_SLUG, + [$this, 'displaySettingsPage'] + ); + + //Technically, admin_enqueue_scripts is the officially approved way + //to enqueue scripts on admin pages, but that hook runs on every admin + //page. We would have to add more logic to only enqueue scripts on + //our own page, and that logic would then run on every page. + //admin_print_scripts-$suffix provides a simpler solution. + add_action( + 'admin_print_scripts-' . $suffix, + [$this, 'enqueueDependencies'], + 2000 + ); + } + + public function enqueueDependencies() { + $this->menuEditor->register_base_dependencies(); + + wp_register_auto_versioned_script( + 'ame-lazyload', + plugins_url('js/lazyload.min.js', $this->menuEditor->plugin_file) + ); + + wp_register_auto_versioned_script( + self::SCRIPT_HANDLE, + plugins_url('easy-hide.js', __FILE__), + [ + 'knockout', + 'jquery', + 'ame-ko-extensions', + 'ame-actor-selector', + 'ame-actor-manager', + 'ame-lodash', + 'ame-lazyload', + $this->isExplanationHidden->getScriptHandle(), + $this->userPreferences->getScriptHandle(), + ] + ); + + wp_enqueue_script(self::SCRIPT_HANDLE); + + $scriptData = $this->getPopulatedStore()->jsonSerialize(); + $scriptData['selectedActor'] = !empty($_GET['selectedActor']) ? ((string)$_GET['selectedActor']) : null; + $scriptData['selectedCategory'] = !empty($_GET['selectedCategory']) ? ((string)$_GET['selectedCategory']) : null; + + wp_add_inline_script( + self::SCRIPT_HANDLE, + sprintf('wsEasyHideData = (%s);', wp_json_encode($scriptData)), + 'before' + ); + + wp_enqueue_auto_versioned_style( + self::STYLE_HANDLE, + plugins_url('easy-hide-style.css', __FILE__), + ['menu-editor-base-style'] + ); + } + + public function displaySettingsPage() { + if ( !$this->menuEditor->current_user_can_edit_menu() ) { + //This should never happen since we already check permissions when adding + //the admin menu item, but let's be extra safe. + wp_die('You do not have permission to access this AME page.'); + } + + $post = $this->menuEditor->get_post_params(); + if ( isset($post['action']) && ($post['action'] == self::SAVE_ACTION) ) { + $this->handleSettingsForm($post); + } + + $this->outputMainTemplate(); + + //Output the "select visible users" dialog template. This doesn't happen + //automatically outside the "Settings -> Menu Editor (Pro)" page. + do_action('admin_menu_editor-visible_users_template'); + } + + private function outputMainTemplate() { + $settingsPageUrl = $this->getSettingsPageUrl(); + $moduleTitle = 'Easy Hide'; + $moduleSettingsUrl = $this->menuEditor->get_settings_page_url() . '#ame-available-modules'; + + $isExplanationVisible = !$this->isExplanationHidden->__invoke(); + + require __DIR__ . '/' . 'easy-hide-template.php'; + } + + private function getSettingsPageUrl() { + return add_query_arg('page', self::MENU_SLUG, self_admin_url($this->parentMenuSlug)); + } + + /** + * @param array $post + */ + private function handleSettingsForm($post) { + check_admin_referer(self::SAVE_ACTION); + + if ( !isset($post['settings']) || !is_string($post['settings']) ) { + wp_die('The required "settings" parameter is missing or invalid.'); + } + $settings = json_decode($post['settings'], true); + + if ( !isset($settings['items']) || !is_array($settings['items']) ) { + wp_die('The required "items" key is missing or has an incorrect data type.'); + } + + //This script doesn't actually change any settings, it just notifies + //other components and plugins that new settings should be saved. + //First, a global notification about all items. + $errors = apply_filters('admin_menu_editor-save_hideable_items', [], $settings['items']); + + //Trigger individual actions for each component. This way a component/module + //won't have to scan the whole item list to find the settings it cares about. + $itemsByComponent = []; + foreach ($settings['items'] as $id => $itemData) { + if ( isset($itemData['component']) ) { + if ( !isset($itemsByComponent[$itemData['component']]) ) { + $itemsByComponent[$itemData['component']] = []; + } + $itemsByComponent[$itemData['component']][$id] = $itemData; + } + } + + foreach ($itemsByComponent as $component => $items) { + $componentErrors = apply_filters( + 'admin_menu_editor-save_hideable_items-' . $component, + [], + $items + ); + $errors = array_merge($errors, $componentErrors); + } + + if ( empty($errors) ) { + //Pass through the previously selected actor and so on. + $redirectParams = ['message' => 1]; + $passThrough = ['selectedActor', 'selectedCategory']; + foreach ($passThrough as $parameter) { + if ( !empty($post[$parameter]) ) { + $redirectParams[$parameter] = (string)$post[$parameter]; + } + } + + wp_redirect(add_query_arg($redirectParams, $this->getSettingsPageUrl())); + exit(); + } else { + $container = new WP_Error( + 'eh_save_failed', + 'One or more errors occurred while saving settings.' + ); + foreach ($errors as $error) { + if ( is_wp_error($error) ) { + $container->merge_from($error); + } else if ( is_string($error) ) { + $container->add('string_error', $error); + } else { + $container->add( + 'invalid_error', + 'Unrecognized error type. Expected a string or a WP_Error instance.' + ); + } + } + wp_die($container); + } + } + + /** + * @return HideableItemStore + */ + private function getPopulatedStore() { + if ( $this->store !== null ) { + return $this->store; + } + + $this->store = new HideableItemStore(); + + //Let modules register their items. + do_action( + 'admin_menu_editor-register_hideable_items', + $this->store + ); + + return $this->store; + } +} + +class HideableItemStore implements JsonSerializable { + /** + * @var array + */ + private $categories = []; + /** + * @var array + */ + private $items = []; + + /** + * @param string $id + * @param string $label + * @param Category[] $categories + * @param string|HideableItem|null $parent + * @param Array $enabled + * @param string|null $component + * @param string|null $tooltip + * @param bool|null $inverted + * @return HideableItem + */ + public function addItem( + $id, + $label, + $categories = [], + $parent = null, + $enabled = [], + $component = null, + $tooltip = null, + $inverted = null + ) { + if ( is_string($parent) ) { + $parent = $this->items[$parent]; + } + + $item = new HideableItem( + $id, + $label, + array_filter($categories), + $parent, + $enabled, + $component, + $tooltip, + $inverted + ); + $this->items[$id] = $item; + + return $item; + } + + /** + * @param string $id + * @param string $label + * @param Category[] $categories + * @param string|HideableItem|null $parent + * @param boolean $enabledForAll + * @param string|null $component + * @param string|null $tooltip + * @param bool|null $inverted + * @return BinaryHideableItem + */ + public function addBinaryItem( + $id, + $label, + $categories = [], + $parent = null, + $enabledForAll = false, + $component = null, + $tooltip = null, + $inverted = null + ) { + if ( is_string($parent) ) { + $parent = $this->items[$parent]; + } + + $item = new BinaryHideableItem( + $id, + $label, + array_filter($categories), + $parent, + $enabledForAll, + $component, + $tooltip, + $inverted + ); + $this->items[$id] = $item; + + return $item; + } + + /** + * @param string $id + * @return \YahnisElsts\AdminMenuEditor\EasyHide\HideableItem|null + */ + public function getItemById($id) { + if ( isset($this->items[$id]) ) { + return $this->items[$id]; + } + return null; + } + + /** + * @param string $id + * @param string $label + * @param Category|null $parent + * @param bool $invertItemState + * @param int $defaultSortOrder + * @param int $itemSortOrder + * @return Category + */ + public function getOrCreateCategory( + $id, + $label, + $parent = null, + $invertItemState = false, + $defaultSortOrder = Category::SORT_ALPHA, + $itemSortOrder = Category::SORT_INSERTION + ) { + if ( isset($this->categories[$id]) ) { + return $this->categories[$id]; + } + + $cat = new Category( + $id, + $label, + $parent, + $invertItemState, + $defaultSortOrder, + $itemSortOrder + ); + $this->categories[$id] = $cat; + return $cat; + } + + /** + * @param Array $hierarchy + * @return Category + */ + public function getOrCreateCategoryTree($hierarchy) { + $previous = null; + $cat = null; + foreach ($hierarchy as $id => $label) { + $cat = $this->getOrCreateCategory($id, $label, $previous); + $previous = $cat; + } + return $cat; + } + + /** + * @param string $id + * @return Category|null + */ + public function getCategory($id) { + if ( isset($this->categories[$id]) ) { + return $this->categories[$id]; + } + return null; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() { + $cats = []; + foreach ($this->categories as $category) { + $cats[] = $category->jsonSerialize(); + } + + $items = []; + foreach ($this->items as $item) { + $items[] = $item->jsonSerialize(); + } + + return [ + 'categories' => $cats, + 'items' => $items, + ]; + } +} + +class Category implements JsonSerializable { + const SORT_ALPHA = 0; + const SORT_INSERTION = 1; + + protected $id; + protected $label; + protected $parent; + protected $invertItemState; + protected $defaultSortOrder; + protected $itemSortOrder; + protected $priority = null; + protected $subtitle = null; + + protected $tableView = []; + + public function __construct( + $id, + $label, + Category $parent = null, + $invertItemState = false, + $defaultSortOrder = self::SORT_ALPHA, + $itemSortOrder = self::SORT_INSERTION + ) { + $this->id = $id; + $this->label = $label; + $this->parent = $parent; + $this->invertItemState = $invertItemState; + $this->defaultSortOrder = $defaultSortOrder; + $this->itemSortOrder = $itemSortOrder; + } + + public function getId() { + return $this->id; + } + + /** + * @param Category $rowCategory + * @param Category $columnCategory + * @return $this + */ + public function enableTableView($rowCategory, $columnCategory) { + $this->tableView = [ + 'rowCategory' => $rowCategory, + 'columnCategory' => $columnCategory, + ]; + return $this; + } + + /** + * @param string $subtitle + * @return $this + */ + public function addSubtitle($subtitle) { + $this->subtitle = $subtitle; + return $this; + } + + /** + * @param int|null $priority + * @return \YahnisElsts\AdminMenuEditor\EasyHide\Category + */ + public function setSortPriority($priority) { + $this->priority = $priority; + return $this; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() { + $result = [ + 'id' => $this->id, + 'label' => $this->label, + ]; + if ( $this->parent !== null ) { + $result['parent'] = $this->parent->getId(); + } + if ( $this->defaultSortOrder !== self::SORT_ALPHA ) { + $result['sort'] = $this->defaultSortOrder; + } + if ( $this->itemSortOrder !== self::SORT_INSERTION ) { + $result['itemSort'] = $this->itemSortOrder; + } + if ( $this->invertItemState ) { + $result['invertItemState'] = true; + } + if ( !empty($this->tableView) ) { + $result['tableView'] = [ + 'rowCategory' => $this->tableView['rowCategory']->getId(), + 'columnCategory' => $this->tableView['columnCategory']->getId(), + ]; + } + if ( !empty($this->subtitle) ) { + $result['subtitle'] = $this->subtitle; + } + if ( $this->priority !== null ) { + $result['priority'] = $this->priority; + } + return $result; + } + + public function isInvertingItemState() { + return $this->invertItemState; + } +} + +class HideableItem implements JsonSerializable { + private $id; + private $label; + + /** + * @var Category[] + */ + private $categories; + + /** + * @var HideableItem|null + */ + private $parent; + private $enabled; + private $component; + private $tooltip; + private $subtitle; + private $inverted; + + /** + * @param $id + * @param $label + * @param Category[] $categories + * @param HideableItem|null $parent + * @param array $enabled + * @param $component + * @param string|null $tooltip + * @param bool|null $inverted + */ + public function __construct( + $id, + $label, + $categories = [], + HideableItem $parent = null, + $enabled = [], + $component = null, + $tooltip = null, + $inverted = null + ) { + $this->id = $id; + $this->label = $label; + $this->categories = $categories; + $this->parent = $parent; + $this->enabled = $enabled; + $this->component = $component; + $this->tooltip = $tooltip; + $this->inverted = $inverted; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() { + $result = [ + 'id' => $this->id, + 'label' => $this->label, + 'categories' => $this->getCategoryIds(), + ]; + if ( $this->parent !== null ) { + $result['parent'] = $this->parent->getId(); + } + if ( !empty($this->enabled) ) { + $result['enabled'] = $this->enabled; + } + if ( $this->component !== null ) { + $result['component'] = $this->component; + } + if ( !empty($this->tooltip) ) { + $result['tooltip'] = $this->tooltip; + } + if ( !empty($this->subtitle) ) { + $result['subtitle'] = $this->subtitle; + } + if ( $this->inverted !== null ) { + $result['inverted'] = $this->inverted; + } + return $result; + } + + private function getCategoryIds() { + $ids = []; + foreach ($this->categories as $category) { + $ids[] = $category->getId(); + } + return $ids; + } + + public function getId() { + return $this->id; + } + + public function addSubtitle($text) { + $this->subtitle = $text; + } +} + +class BinaryHideableItem extends HideableItem { + /** + * @var bool + */ + private $enabledForAll = false; + + public function __construct( + $id, + $label, + $categories = [], + HideableItem $parent = null, + $enabledForAll = false, + $component = null, + $tooltip = null, + $inverted = null + ) { + parent::__construct($id, $label, $categories, $parent, array(), $component, $tooltip, $inverted); + $this->enabledForAll = $enabledForAll; + } + + public function jsonSerialize() { + $result = parent::jsonSerialize(); + $result['binary'] = true; + unset($result['enabled']); + $result['enabledForAll'] = $this->enabledForAll; + return $result; + } +} \ No newline at end of file diff --git a/extras/modules/easy-hide/easy-hide.ts b/extras/modules/easy-hide/easy-hide.ts new file mode 100644 index 0000000..81f4d9d --- /dev/null +++ b/extras/modules/easy-hide/easy-hide.ts @@ -0,0 +1,1168 @@ +/// +/// +/// +/// +/// +/// +/// + +'use strict'; + +let ameEasyHideModel: AmeEasyHide.Model = null; +declare let wsEasyHideData: AmeEasyHide.ScriptData; + +namespace AmeEasyHide { + const _ = wsAmeLodash; + + enum SortOrder { + SORT_ALPHA = 0, + SORT_INSERTION = 1 + } + + interface CategoryProperties { + id: string, + label: string, + parent?: string, + invertItemState?: boolean, + sort?: SortOrder, + itemSort?: SortOrder, + priority?: number, + tableView?: { + rowCategory: string, + columnCategory: string + }, + subtitle?: string + } + + class Category { + public static readonly DEFAULT_PRIORITY = 10; + + readonly subcategories: KnockoutObservableArray; + readonly items: KnockoutObservableArray; + + readonly directItems: KnockoutComputed; + readonly sortedSubcategories: KnockoutComputed; + + readonly isSelected: KnockoutObservable; + readonly isShowingItems: KnockoutComputed; + readonly isExpanded: KnockoutObservable; + readonly isNavVisible: KnockoutComputed; + readonly isVisible: KnockoutComputed; + + private readonly isInSelectedCategory: KnockoutComputed; + readonly containsSelectedCategory: KnockoutObservable = ko.observable(false); + + readonly isStandardRenderingEnabled: KnockoutObservable = ko.observable(true); + readonly shouldRenderContent: KnockoutComputed; + + readonly isChecked: KnockoutComputed; + readonly isIndeterminate: KnockoutObservable; + + readonly navCssClasses: KnockoutComputed; + readonly nestingDepth: KnockoutComputed; + + tableViewEnabled: boolean = false; + tableView: CategoryTableView | null = null; + readonly highlightedLabel: KnockoutComputed; + + protected static counter: number = 0; + readonly safeElementId: string; + + private cachedParentList: Category[] = null; + + constructor( + public readonly id: string, + public readonly label: string, + public readonly parent: Category = null, + public readonly invertItemState: boolean = false, + filterState: FilterOptions | null = null, + public readonly initialSortOrder: SortOrder = SortOrder.SORT_ALPHA, + public readonly itemSortOrder: SortOrder = SortOrder.SORT_INSERTION, + private readonly priority: number = Category.DEFAULT_PRIORITY, + public readonly subtitle: string | null = null + ) { + Category.counter++; + this.safeElementId = 'ame-eh-category-n-' + Category.counter; + + this.isSelected = ko.observable(false); + + /** + * Is this category selected or inside a selected category? + */ + this.isInSelectedCategory = ko.pureComputed(() => { + let current: Category = this; + while (current !== null) { + if (current.isSelected()) { + return true; + } + current = current.parent; + } + return false; + }); + + this.isShowingItems = ko.pureComputed(() => { + //The items are visible if this category or one of its parents is selected. + return this.isInSelectedCategory(); + }); + this.isExpanded = ko.observable(this.parent === null); + + this.isVisible = ko.pureComputed(() => { + const inSelectedTree = this.isInSelectedCategory() || this.containsSelectedCategory(); + if (!inSelectedTree) { + return false; + } + + //Show the category if it has any visible children: items or subcategories. + const items = this.directItems(); + if (_.some(items, item => item.isVisible())) { + return true; + } + + const subcategories = this.subcategories(); + return _.some(subcategories, category => category.isVisible()); + }); + + this.subcategories = ko.observableArray([]); + this.sortedSubcategories = ko.pureComputed(() => { + let cats = this.subcategories(); + + //Remove categories that won't be rendered. + cats = cats.filter(c => c.isStandardRenderingEnabled()); + + if (this.initialSortOrder === SortOrder.SORT_ALPHA) { + cats.sort(function (a, b) { + if (a.priority !== b.priority) { + return a.priority - b.priority; + } + return a.label.localeCompare(b.label); + }); + } else if (this.initialSortOrder === SortOrder.SORT_INSERTION) { + //We want a stable sort that preserves insertion order for + //categories that have the same priority. As of this writing, + //Array.sort() is not stable in IE, so let's use Lodash. + cats = _.sortBy(cats, 'priority'); + } + + return cats; + }); + + this.items = ko.observableArray([]); + + this.directItems = ko.pureComputed(() => { + let results = _(this.items()) + .filter((item) => { + //An item is a direct/root level item in this category + //only if it has no parent or if its parent is not + //in the same category. + if (item.parent === null) { + return true; + } else { + return !_.contains(item.parent.categories, this); + } + }) + .value(); + + //Sort items alphabetically if requested. + if (this.itemSortOrder === SortOrder.SORT_ALPHA) { + results.sort(function (a, b) { + return a.label.localeCompare(b.label); + }); + } + + return results; + }); + + this.isIndeterminate = ko.observable(false); + + //The whole category is checked if at least one of its + //items or subcategories is checked. + this.isChecked = ko.computed({ + read: () => { + let hasIndeterminateChildren = false; + + let hasCheckedItems = false, hasUncheckedItems = false; + _.forEach(this.items(), function (item) { + if (item.isChecked()) { + hasCheckedItems = true; + } else { + hasUncheckedItems = true; + } + + if (item.isIndeterminate()) { + hasIndeterminateChildren = true; + } + + if (hasCheckedItems && hasUncheckedItems) { + //We know the category has a mix of checked and + //unchecked items, so there's no need to continue. + return false; + } + }); + + let hasCheckedCats = false, hasUncheckedCats = false; + _.forEach(this.subcategories(), function (category) { + if (category.isChecked()) { + hasCheckedCats = true; + } else { + hasUncheckedCats = true; + } + + if (category.isIndeterminate()) { + hasIndeterminateChildren = true; + } + + if (hasCheckedCats && hasUncheckedCats) { + return false; + } + }); + + const areAnyChecked = hasCheckedItems || hasCheckedCats; + const areAnyUnchecked = hasUncheckedItems || hasUncheckedCats; + + this.isIndeterminate(hasIndeterminateChildren || (areAnyChecked && areAnyUnchecked)); + return areAnyChecked; + }, + write: (checked: boolean) => { + //Update items. + _.forEach(this.items(), function (item) { + if (item.isEditableForSelectedActor) { + item.isChecked(checked); + } + }); + + //Update subcategories. + _.forEach(this.subcategories(), function (category) { + category.isChecked(checked); + }); + }, + deferEvaluation: true + }).extend({rateLimit: {timeout: 20, method: 'notifyWhenChangesStop'}}); + + this.nestingDepth = ko.pureComputed({ + read: () => { + if (this.parent !== null) { + return this.parent.nestingDepth() + 1; + } + return 1; + }, + deferEvaluation: true + }); + + this.navCssClasses = ko.pureComputed({ + read: () => { + let classes = []; + if (this.isSelected()) { + //classes.push('ame-selected-cat-nav-item'); + } + if (this.sortedSubcategories().length > 0) { + classes.push('ame-cat-nav-has-children'); + } + if (this.isExpanded()) { + classes.push('ame-cat-nav-is-expanded'); + } + if (this.isSelected()) { + classes.push('ame-selected-cat-nav-item'); + } + classes.push('ame-cat-nav-level-' + this.nestingDepth()); + return classes.join(' '); + }, + deferEvaluation: true + }); + + this.isNavVisible = ko.pureComputed({ + read: () => { + if (this.parent === null) { + return true; + } + if (!this.isStandardRenderingEnabled()) { + return false; + } + return this.parent.isNavVisible() && this.parent.isExpanded(); + }, + deferEvaluation: true + }); + + //Category labels are not searched, but the table view has categories that + //represent the same item being used on multiple screens. In that case, + //category labels typically match item labels, so let's highlight them. + this.highlightedLabel = ko.pureComputed(() => { + let text = _.escape(this.label); + if (filterState !== null) { + text = filterState.highlightSearchKeywords(text); + } + return text; + }); + + const wasRendered = ko.observable(false); + this.shouldRenderContent = ko.computed({ + read: () => { + return wasRendered(); + }, + write: (state: boolean) => { + //This is a write-once flag. Once it's turned on, + //it can't be turned off again. + if (wasRendered()) { + return; + } + wasRendered(state); + + //Notify that a category is being rendered. The DOM might not be + //updated yet when this event happens, so subscribers should wait + //at least until the next frame. + if (wasRendered()) { + jQuery(document).trigger('adminmenueditor:ehCategoryRendering', [this]); + } + } + }); + } + + toggle() { + this.isExpanded(!this.isExpanded()); + } + + enableTableView(rowCat: Category, columnCat: Category) { + this.tableViewEnabled = true; + this.tableView = new CategoryTableView(rowCat, columnCat); + + //Disable normal rendering for the two row/column categories. + //They will only appear in the table. + rowCat.isStandardRenderingEnabled(false); + columnCat.isStandardRenderingEnabled(false); + } + + get allParents(): Category[] { + //The parent property is readonly, so the result should not change after + //the category is initialized. We can cache it indefinitely. + if (this.cachedParentList === null) { + const parents = []; + let current = this.parent; + while (current !== null) { + parents.push(current); + current = current.parent; + } + //Reverse the list so that it starts at the root. + parents.reverse(); + this.cachedParentList = parents; + } + return this.cachedParentList; + } + + static fromProps( + props: CategoryProperties, + parent: Category = null, + filterState: FilterOptions | null = null + ): Category { + return new Category( + props.id, props.label, parent, + props.invertItemState ?? false, + filterState, + props.sort ?? SortOrder.SORT_ALPHA, + props.itemSort ?? SortOrder.SORT_INSERTION, + props.priority ?? Category.DEFAULT_PRIORITY, + props.subtitle ?? null + ); + } + } + + interface CommonItemProperties { + id: string, + label: string, + categories?: string[], + parent?: string, + inverted?: boolean, + component?: string, + tooltip?: string | null; + subtitle?: string | null; + } + + interface PerActorItemProperties extends CommonItemProperties { + enabled?: AmeDictionary, + } + + interface BinaryItemProperties extends CommonItemProperties { + binary: true; + enabledForAll?: boolean; + } + + type ItemProperties = PerActorItemProperties | BinaryItemProperties; + + function isBinaryProps(props: ItemProperties): props is BinaryItemProperties { + return ('binary' in props) ? props.binary : false; + } + + interface StorableItemProperties { + enabled?: PerActorItemProperties['enabled']; + inverted?: boolean; + component?: ItemProperties['component']; + enabledForAll?: BinaryItemProperties['enabledForAll']; + } + + class HideableItem { + private readonly actorSettings: AmeObservableActorSettings; + public readonly isChecked: KnockoutComputed; + public readonly isIndeterminate: KnockoutComputed; + public readonly children: HideableItem[] = []; + + public readonly shouldRender: KnockoutObservable; + public readonly isVisible: KnockoutComputed; + + public readonly htmlLabel: KnockoutComputed; + + constructor( + public readonly id: string, + public readonly label: string, + public readonly categories: Array = [], + public readonly parent: HideableItem = null, + private readonly initialEnabled: Record = {}, + protected readonly isInverted: boolean = false, + public readonly component: string = null, + public readonly tooltip: string | null = null, + public readonly subtitle: string | null = null, + protected readonly selectedActorRef: KnockoutComputed, + allActorsRef: KnockoutComputed, + filterState: FilterOptions + ) { + this.actorSettings = new AmeObservableActorSettings(initialEnabled); + + let _isIndeterminate = ko.observable(false); + + this.isIndeterminate = ko.pureComputed(() => { + if (selectedActorRef() === null) { + return _isIndeterminate(); + } + return false; + }); + + this.isChecked = this.createCheckedObservable( + selectedActorRef, + allActorsRef, + _isIndeterminate + ); + + let wasRendered = false; + this.shouldRender = ko.observable(false); + + this.isVisible = ko.computed(() => { + let visible = filterState.itemMatchesFilter(this); + + if (visible && !wasRendered) { + wasRendered = true; + this.shouldRender(true); + } + return visible; + }); + + this.htmlLabel = ko.pureComputed(() => { + let html = _.escape(this.label); + + if (this.isVisible()) { + html = filterState.highlightSearchKeywords(html); + } + + return html; + }); + } + + protected createCheckedObservable( + selectedActorRef: KnockoutComputed, + allActorsRef: KnockoutComputed, + outIndeterminate: KnockoutObservable | null + ): KnockoutComputed { + return ko.computed({ + read: (): boolean => { + let enabled = this.actorSettings.isEnabledFor( + selectedActorRef(), + allActorsRef(), + this.isInverted, + this.isInverted, + this.isInverted, + outIndeterminate + ); + return this.isInverted ? (!enabled) : enabled; + }, + write: (checked: boolean) => { + this.actorSettings.setEnabledFor( + selectedActorRef(), + this.isInverted ? !checked : checked, + allActorsRef(), + this.isInverted + ); + + for (let i = 0; i < this.children.length; i++) { + this.children[i].isChecked(checked); + } + }, + deferEvaluation: true + }); + } + + static fromJs( + props: ItemProperties, + selectedActor: KnockoutComputed, + allActors: KnockoutComputed, + filterState: FilterOptions, + categories: Category[] = [], + parent: HideableItem = null + ): HideableItem { + if (isBinaryProps(props)) { + return BinaryHideableItem.fromJs( + props, + selectedActor, + allActors, + filterState, + categories, + parent + ); + } + + return new HideableItem( + props.id, + props.label, + categories, + parent, + props.enabled ?? {}, + props.inverted ?? false, + props.component ?? null, + props.tooltip ?? null, + props.subtitle ?? null, + selectedActor, + allActors, + filterState + ); + } + + toJs(): StorableItemProperties { + let result: StorableItemProperties = {}; + + if (this.isInverted) { + result.inverted = true; + } + + if ((this.component !== null) && (this.component !== '')) { + result.component = this.component; + } + + let enabled = this.actorSettings.getAll(); + if (!_.isEmpty(enabled)) { + result.enabled = enabled; + } + + return result; + } + + get isEditableForSelectedActor(): boolean { + return true; + } + } + + class BinaryHideableItem extends HideableItem { + private isEnabledForAll: KnockoutObservable = ko.observable(false); + + protected createCheckedObservable( + selectedActorRef: KnockoutComputed, + allActorsRef: KnockoutComputed, + outIndeterminate: KnockoutObservable | null + ): KnockoutComputed { + const observable = ko.computed({ + read: () => { + if (this.isInverted) { + return !this.isEnabledForAll(); + } else { + return this.isEnabledForAll(); + } + }, + write: (value: boolean) => { + if (this.isEditableForSelectedActor) { + if (this.isInverted) { + value = !value; + } + this.isEnabledForAll(value); + } else { + //Reset the checkbox to the original value. + observable.notifySubscribers(); + } + }, + deferEvaluation: true, + }); + return observable; + } + + static fromJs( + props: BinaryItemProperties, + selectedActor: KnockoutComputed, + allActors: KnockoutComputed, + filterState: FilterOptions, + categories: Category[] = [], + parent: HideableItem = null + ): HideableItem { + const instance = new BinaryHideableItem( + props.id, + props.label, + categories, + parent, + {}, + props.inverted ?? false, + props.component ?? null, + props.tooltip ?? null, + props.subtitle ?? null, + selectedActor, + allActors, + filterState + ); + + if (props.hasOwnProperty('enabledForAll')) { + instance.isEnabledForAll(props.enabledForAll); + } + + return instance; + } + + toJs(): StorableItemProperties { + let result = super.toJs(); + + delete result.enabled; + result.enabledForAll = this.isEnabledForAll(); + + return result; + } + + get isEditableForSelectedActor(): boolean { + return (this.selectedActorRef() === null); + } + } + + class CategoryTableView { + private itemLookup: Record> = {}; + private columnHeaders: JQuery = null; + + constructor( + public readonly rowCategory: Category, + public readonly columnCategory: Category + ) { + + } + + get rows(): Category[] { + return CategoryTableView.sortCategoriesByLabel(this.rowCategory.subcategories); + } + + get columns(): Category[] { + return CategoryTableView.sortCategoriesByLabel(this.columnCategory.subcategories); + } + + private static sortCategoriesByLabel(categories: KnockoutObservableArray) { + let list = categories(); + list.sort(function (a, b) { + return a.label.localeCompare(b.label); + }); + return list; + } + + getCellItems(row: Category, column: Category): HideableItem[] { + const path = [row.id, column.id]; + + let items = _.get(this.itemLookup, path, null); + if (items !== null) { + return items; + } + + //Find items that are present in both categories. + items = _.intersection(row.directItems(), column.directItems()); + _.set(this.itemLookup, path, items); + + return items; + } + + /** + * Highlight the column heading when the user hovers over a table cell. + * + * @param unused Knockout provides the current model value to the callback, but we don't need it. + * @param event JavaScript event object + */ + onTableHover(unused: any, event: Event) { + if (!event || !event.target) { + return; + } + + const $cell = jQuery(event.target).closest('td, th'); + if ($cell.length < 1) { + return; + } + + if ( + //Has the header list been initialized? + (this.columnHeaders === null) + //The table might have been re-rendered or removed from the DOM. + //In that case, we'll need to find the new header elements. + || (this.columnHeaders.closest('body').length < 1) + ) { + this.columnHeaders = $cell.closest('table').find('thead tr').first().find('th'); + } + + const index = $cell.index(); + //The first column doesn't have a header, so it doesn't need to be highlighted. + if (index === 0) { + return; + } + + const $heading = this.columnHeaders.eq(index); + if (!$heading || ($heading.length === 0)) { + return; + } + + const highlightClass = 'ame-eh-hovered-column'; + if ($heading.hasClass(highlightClass)) { + return; //This column is already highlighted. + } + + this.columnHeaders.removeClass(highlightClass); + $heading.addClass(highlightClass); + } + } + + class FilterOptions { + /** + * Bind this observable to the search box, then use searchQuery to actually + * read the query. This is only public because it's used in a KO template. + */ + readonly internalSearchQuery: KnockoutObservable = ko.observable(''); + + /** + * A rate-limited interface for accessing the search query. + */ + readonly searchQuery: KnockoutComputed; + readonly searchKeywords: KnockoutComputed; + + private readonly highlightRegex: KnockoutComputed; + + constructor() { + this.searchQuery = ko.pureComputed(() => this.internalSearchQuery()); + this.searchQuery.extend({rateLimit: {timeout: 100, method: "notifyWhenChangesStop"}}); + + this.searchKeywords = ko.pureComputed(() => { + let query = this.searchQuery().trim(); + if (query === '') { + return []; + } + + return _(query.split(' ')) + .map(keyword => keyword.trim()) + .filter(keyword => (keyword !== '')) + .value(); + }); + + this.highlightRegex = ko.pureComputed(() => { + const keywordList = this.searchKeywords(); + if (keywordList.length < 1) { + return null; + } + + let keywordGroup = _.map(keywordList, _.escapeRegExp).join('|'); + return new RegExp('(?:' + keywordGroup + ')', 'gi'); + }); + } + + itemMatchesFilter(item: HideableItem): boolean { + const keywords = this.searchKeywords(); + + if (keywords.length > 0) { + const haystack = item.label.toLowerCase(); + const matchesKeywords = _.all( + keywords, + keyword => (haystack.indexOf(keyword) >= 0) + ); + + if (!matchesKeywords) { + return false; + } + } + return true; + } + + highlightSearchKeywords(input: string): string { + const regex = this.highlightRegex(); + if (regex === null) { + return input; + } + + return input.replace( + regex, + function (foundKeyword) { + return '' + foundKeyword + ''; + } + ); + } + + clearSearchBox() { + this.internalSearchQuery(''); + } + + processEscKey(unusedKoModel: any, event: KeyboardEvent) { + //Ignore events triggered during IME composition. + //See https://developer.mozilla.org/en-US/docs/Web/API/Document/keydown_event#ignoring_keydown_during_ime_composition + if (event.isComposing) { + return true; + } + + //noinspection JSDeprecatedSymbols + const isEscape = ( + ((typeof event['code'] !== 'undefined') && (event.code === 'Escape')) + //IE doesn't support KeyboardEvent.code, so use keyCode instead. + || ((typeof event['keyCode'] !== 'undefined') && (event.keyCode === 27)) + ); + + if (isEscape) { + this.clearSearchBox(); + } + return true; + } + } + + export interface ScriptData { + categories: CategoryProperties[], + items: ItemProperties[], + selectedActor?: string, + selectedCategory?: string, + } + + export class Model { + readonly actorSelector: AmeActorSelector; + readonly selectedActor: KnockoutComputed; + readonly selectedActorId: KnockoutComputed; + readonly selectedCategoryId: KnockoutComputed; + + protected readonly categoryLookup: Record = {}; + public readonly rootCategory: Category; + public readonly selectedCategory: KnockoutObservable; + + readonly settingsData: KnockoutObservable = ko.observable(''); + readonly isSaveButtonEnabled: KnockoutObservable = ko.observable(true); + + readonly filterState: FilterOptions; + + readonly preferences: AmeEhUserPreferencesKoProxy; + + readonly itemContainerClasses: KnockoutComputed; + + constructor(settings: ScriptData, prefs: AmeEhUserPreferences) { + this.preferences = prefs.observableObject(ko); + prefs.enableAutoSave(3000); + + this.actorSelector = new AmeActorSelector(AmeActors, true); + this.selectedActor = this.actorSelector.createActorObservable(ko); + + this.selectedActorId = ko.pureComputed(() => { + const actor = this.selectedActor(); + if (actor === null) { + return ''; + } + return actor.getId(); + }); + + const allActors = ko.pureComputed(() => { + return this.actorSelector.getVisibleActors(); + }); + + //Reselect the previously selected actor. + if (settings.selectedActor && AmeActors.actorExists(settings.selectedActor)) { + this.selectedActor(AmeActors.getActor(settings.selectedActor)); + } + + this.filterState = new FilterOptions(); + + const _ = wsAmeLodash; + + //Initialize categories. + this.rootCategory = new Category( + '_root', 'All', null, false, this.filterState, SortOrder.SORT_ALPHA + ); + this.rootCategory.shouldRenderContent(true); + + let catsWithTableView: CategoryProperties[] = []; + + _.forEach(settings.categories, (props) => { + let parent: Category = this.rootCategory; + if (props.parent) { + parent = this.categoryLookup[props.parent]; + } + + const cat = Category.fromProps(props, parent, this.filterState); + this.categoryLookup[cat.id] = cat; + + parent.subcategories.push(cat); + + if (props.tableView) { + catsWithTableView.push(props); + } + }); + + //Initialize table views. This is a separate step because tables need + //their row and column categories to be already created. + _.forEach(catsWithTableView, (props) => { + const cat = this.categoryLookup[props.id]; + cat.enableTableView( + this.categoryLookup[props.tableView.rowCategory], + this.categoryLookup[props.tableView.columnCategory] + ); + }); + + //Initialize items. + const itemsById: Record = {}; + _.forEach(settings.items, (props) => { + let parent: HideableItem = null; + if (props.parent && itemsById.hasOwnProperty(props.parent)) { + parent = itemsById[props.parent]; + } + + let categories: Category[] = []; + if (props.categories) { + _.forEach(props.categories, (id) => { + if (this.categoryLookup.hasOwnProperty(id)) { + categories.push(this.categoryLookup[id]); + } + }); + } + + if (categories.length < 1) { + categories.push(this.rootCategory); + } + + if (_.some(categories, 'invertItemState') && (typeof props['inverted'] === 'undefined')) { + props.inverted = true; + } + + const item = HideableItem.fromJs( + props, + this.selectedActor, + allActors, + this.filterState, + categories, + parent + ); + + itemsById[item.id] = item; + if (parent) { + parent.children.push(item); + } + + _.forEach(categories, function (category) { + category.items.push(item); + }); + }); + + this.itemContainerClasses = ko.pureComputed(() => { + let classes = []; + + const columns = this.preferences.numberOfColumns(); + if (columns > 1) { + classes.push('ame-eh-item-columns-' + columns); + } + + return classes.join(' '); + }); + + this.selectedCategory = ko.observable(this.rootCategory); + + //Update the "isSelected" and "containsSelectedCategory" flags + //on the category object when the user selects a category. + let previousSelectedCategory = this.selectedCategory.peek(); + if (previousSelectedCategory) { + previousSelectedCategory.isSelected(true); + } + + this.selectedCategory.subscribe((newSelection: Category) => { + if (newSelection !== previousSelectedCategory) { + //Save the old selection in case changing isSelected also triggers this callback somehow. + const oldSelection = previousSelectedCategory; + previousSelectedCategory = newSelection; + //The previous category is no longer selected. + oldSelection.isSelected(false); + + const previousTree = oldSelection.allParents; + const newTree = newSelection.allParents; + + //Find the point of divergence. + const minLength = Math.min(previousTree.length, newTree.length); + let divergenceIndex = -1; + for (let i = 0; i < minLength; i++) { + if (newTree[i] !== previousTree[i]) { + divergenceIndex = i; + break; + } + } + + //Update categories that are no longer in the selected tree. + if (divergenceIndex >= 0) { + for (let i = divergenceIndex; i < previousTree.length; i++) { + previousTree[i].containsSelectedCategory(false); + } + } + //Update categories that contain the new selection. + for (let i = Math.max(divergenceIndex, 0); i < newTree.length; i++) { + newTree[i].containsSelectedCategory(true); + } + } + + newSelection.isSelected(true); + newSelection.shouldRenderContent(true); + }); + + //Restore previously expanded categories. + _.forEach( + this.preferences.csExpandedCategories()?.split("\n"), + (id) => { + if ((typeof id === 'string') && this.categoryLookup.hasOwnProperty(id)) { + this.categoryLookup[id].isExpanded(true); + } + } + ); + + //Save expanded categories in user preferences. + const expandedCategories = ko.computed(() => { + //Make a list of category IDs. + return _(this.categoryLookup).filter((category: Category) => { + //Skip the root category. It's always expanded. + if (category === this.rootCategory) { + return false; + } + + //Skip categories that don't have any children. + if (category.subcategories().length === 0) { + return false; + } + + return category.isExpanded(); + }).pluck('id').value(); + }).extend({rateLimit: {timeout: 100, method: 'notifyWhenChangesStop'}}); + + expandedCategories.subscribe((newValue: string[]) => { + this.preferences.csExpandedCategories(newValue.join("\n")); + }); + + //Reselect the previously selected category. + if ( + settings.selectedCategory + && this.categoryLookup.hasOwnProperty(settings.selectedCategory) + ) { + this.selectedCategory(this.categoryLookup[settings.selectedCategory]); + } + + //Render the first couple of categories to push the other category + //placeholders below the bottom of the viewport. + _(this.rootCategory.sortedSubcategories()).take(2).forEach(function (c) { + c.shouldRenderContent(true); + }).commit(); + + //Always render the selected category. + this.selectedCategory()?.shouldRenderContent(true); + + this.selectedCategoryId = ko.pureComputed(() => { + const category = this.selectedCategory(); + if (category === null) { + return ''; + } + return category.id; + }); + } + + onCategoryEntersViewport(element: HTMLElement) { + const category = ko.dataFor(element); + if (category instanceof Category) { + if (console && console.log) { + console.log('Rendering category', category.id); + } + category.shouldRenderContent(true); + } + } + + renderAllCategories() { + function renderChildren(category: Category) { + _.forEach(category.sortedSubcategories(), function (c) { + c.shouldRenderContent(true); + renderChildren(c); + }); + } + + renderChildren(this.rootCategory); + } + + saveChanges() { + this.isSaveButtonEnabled(false); + this.settingsData(JSON.stringify(this.getCurrentSettings())); + return true; + } + + private getCurrentSettings(): object { + function collectItemsRecursively( + category: Category, + output: Record = {} + ) { + _.forEach(category.items(), function (item) { + if (!output.hasOwnProperty(item.id)) { + output[item.id] = item.toJs(); + } + }); + + _.forEach( + category.subcategories(), + subcategory => collectItemsRecursively(subcategory, output) + ); + + return output; + } + + return { + items: collectItemsRecursively(this.rootCategory) + }; + } + } +} + +document.addEventListener('DOMContentLoaded', function () { + ameEasyHideModel = new AmeEasyHide.Model(wsEasyHideData, ameEhUserPreferences); + ko.applyBindings(ameEasyHideModel, document.getElementById('ame-easy-hide-container')); + + //Render categories lazily. + try { + let lazyUpdateTimer = null; + const ameEhLazyLoad = new LazyLoad({ + elements_selector: '.ame-eh-lazy-category', + unobserve_entered: true, + callback_enter: function (element) { + ameEasyHideModel.onCategoryEntersViewport(element); + } + }); + + jQuery(document).on('adminmenueditor:ehCategoryRendering', function () { + //New placeholders might be created after rendering a category, + //so let's update LazyLoad. + //Debounce updates by ensuring that there's only one pending timer. + if (lazyUpdateTimer !== null) { + clearTimeout(lazyUpdateTimer); + } + lazyUpdateTimer = window.setTimeout(function () { + lazyUpdateTimer = null; + ameEhLazyLoad.update(); + }, 40); + }); + } catch (ex) { + //I'm not sure if LazyLoad will actually throw an exception if the user has + //an old browser that doesn't support IntersectionObserver, but let's fall back + //to showing all categories if anything goes wrong. + ameEasyHideModel.renderAllCategories(); + } + + //Handle clicks on the "dismiss" button in the explanatory notice. It wouldn't be safe + //to use Knockout for this because WordPress automatically moves notices below the first + //h1/h2 element, and any external DOM manipulation can mess up KO bindings. + jQuery('#ame-easy-hide-explanation').on('click', '.notice-dismiss', function () { + if (typeof ameEhIsExplanationHidden !== 'undefined') { + ameEhIsExplanationHidden(true); + ameEhIsExplanationHidden.save(); + } + }); + + //We no longer need the input data, so we can potentially free up + //some memory by clearing it. + wsEasyHideData = null; +}); \ No newline at end of file diff --git a/extras/modules/easy-hide/eh-preferences-uninstall.php b/extras/modules/easy-hide/eh-preferences-uninstall.php new file mode 100644 index 0000000..ceaba89 --- /dev/null +++ b/extras/modules/easy-hide/eh-preferences-uninstall.php @@ -0,0 +1,9 @@ +; +} +declare const ameEhIsExplanationHidden: AmeEhIsExplanationHidden; + +interface AmeEhUserPreferencesKoProxy{ + numberOfColumns: KnockoutComputed; + csExpandedCategories: KnockoutComputed; +} +interface AmeEhUserPreferences extends AmeEhBasePrefInterface{ + numberOfColumns: number; + csExpandedCategories: string; + observableObject(ko: KnockoutStatic): AmeEhUserPreferencesKoProxy; +} +declare const ameEhUserPreferences: AmeEhUserPreferences; diff --git a/extras/modules/easy-hide/eh-preferences.js b/extras/modules/easy-hide/eh-preferences.js new file mode 100644 index 0000000..af92bdd --- /dev/null +++ b/extras/modules/easy-hide/eh-preferences.js @@ -0,0 +1,453 @@ +//This file was automatically generated +//Do not edit this file directly. +///*{WS_PCGN_GENERATED_FILE}*/ + +const ameEhPreferenceCodegen = (function() { + 'use strict'; + + function parseConfiguration(jsData) { + //Copy the configuration data to a new object so that it doesn't unexpectedly + //change if someone modifies the properties of the input object. This also + //allows us to set defaults easily. + return jQuery.extend({ + ajaxUrl: null, + ajaxNonce: null, + ajaxAction: null, + name: '[UnnamedPreference]', + data: {}, + defaults: {}, + autoSaveEnabled: false, + }, + jsData + ); + } + + function initialisePreference(instance, config, entityType) { + 'use strict'; + + entityType = entityType || 'preference'; + + let isAutoSaveEnabled = config.autoSaveEnabled || false; + + let rateLimitTimeout = 2000; + let saveTimerId = null; + + Object.defineProperty(instance, 'isAutoSaveEnabled', { + get: function() { + return isAutoSaveEnabled; + } + }); + + instance.scheduleSave = function() { + //Debounce: Save the data only after no changes have happened + //for X milliseconds. + if (saveTimerId) { + clearTimeout(saveTimerId); + } + saveTimerId = setTimeout(function() { + saveTimerId = null; + instance.save(); + }, rateLimitTimeout); + } + + instance.save = function() { + //When someone directly triggers a save, cancel any pending save timer. + if (saveTimerId) { + clearTimeout(saveTimerId); + saveTimerId = null; + } + + //This only works if AJAX is enabled. + if (!config.ajaxAction) { + if (console && console.warn) { + console.warn( + 'Cannot save ' + entityType + ' "' + config.name + '" because ' + + 'AJAX configuration is not available.' + ); + } + return false; + } + + //Save the value via AJAX. + // noinspection JSUnusedGlobalSymbols + jQuery.ajax( + config.ajaxUrl, { + method: 'POST', + data: { + "action": config.ajaxAction, + "_ajax_nonce": config.ajaxNonce, + "data": JSON.stringify(instance.toJs()) + }, + dataType: 'json', + success: function(response) { + if (response && response.error) { + if (console && console.error) { + console.error( + 'Error saving ' + entityType + ' "' + config.name + '": ' + + response.error.message, + response + ); + } + } + }, + error: function(jqXHR, textStatus) { + let data = jqXHR.responseText; + if (typeof jqXHR['responseJSON'] !== 'undefined') { + data = jqXHR['responseJSON']; + } else if (typeof jqXHR['responseXML'] !== 'undefined') { + data = jqXHR['responseXML']; + } + + if (console && console.error) { + console.error( + 'A network error happened while saving ' + entityType + + ' "' + config.name + '". ', + data, + 'Status: ' + textStatus, + ); + } + } + } + ); + }; + + instance.enableAutoSave = function(debounceTimeout) { + if (debounceTimeout === undefined) { + rateLimitTimeout = 2000; + } else { + rateLimitTimeout = debounceTimeout; + } + + isAutoSaveEnabled = true; + }; + + let subscriptions = { + 'beforeChange': [], + 'afterChange': [], + }; + + /** + * @param {string} event + * @param callback + * @param {string} [preferenceName] + */ + instance.subscribe = function(event, callback, preferenceName) { + if (typeof subscriptions[event] !== 'undefined') { + subscriptions[event].push({ + callback: callback, + preferenceName: preferenceName + }); + } else { + throw new Error('Cannot subscribe to unknown event type "' + event + '"'); + } + }; + + instance.triggerEvent = function(event, preferenceName) { + if (typeof subscriptions[event] !== 'undefined') { + for (let i = 0; i < subscriptions[event].length; i++) { + let subscription = subscriptions[event][i]; + if ( + (typeof subscription.preferenceName === 'undefined') || + (subscription.preferenceName === preferenceName) + ) { + subscription.callback(preferenceName); + } + } + } + }; + + instance.beforeChange = function(preferenceName) { + instance.triggerEvent('beforeChange', preferenceName); + }; + instance.afterChange = function(preferenceName) { + instance.triggerEvent('afterChange', preferenceName); + }; + + return instance; + } + + function setupSinglePreference(jsData, validator) { + 'use strict'; + + const config = parseConfiguration(jsData); + let currentValue = config.value; + + const instance = function(newValue) { + if (typeof newValue === 'undefined') { + return currentValue; + } + + const validationResult = instance.validate(newValue); + if (validationResult.error) { + throw new Error(config.name + ': ' + validationResult.error); + } else { + if (currentValue !== validationResult.value) { + instance.beforeChange(); + currentValue = validationResult.value; + instance.afterChange(); + //Auto-save the new value. + if (instance.isAutoSaveEnabled) { + instance.scheduleSave(); + } + } + } + + return instance; + }; + + instance.validate = function(inputValue) { + return validator(inputValue); + }; + + instance.toJs = function() { + return currentValue; + }; + + /** + * @param {KnockoutStatic} ko + * @returns {KnockoutComputed} + */ + instance.observable = function(ko) { + const dependencyTrigger = ko.observable(currentValue); + + const obs = ko.computed({ + read: function() { + dependencyTrigger(); + return instance() + }, + write: function(newValue) { + instance(newValue); + }, + deferEvaluation: true + }); + + instance.subscribe('afterChange', function() { + dependencyTrigger(currentValue); + }); + + return obs; + }; + + initialisePreference(instance, config, 'preference'); + + return instance; + } + + function setupPreferenceGroup(instance, jsData, validators, availableProperties) { + const config = parseConfiguration(jsData); + + let currentValues = jQuery.extend({}, config.data); + const defaults = jQuery.extend({}, config.defaults); + + instance.toJs = function() { + return jQuery.extend({}, currentValues); + }; + + /** + * @param {KnockoutStatic} ko + */ + instance.observableObject = function(ko) { + /* + Hack: For each property, we have a computed observable that reads/writes + the property and a second observable that exists purely to make Knockout + update stuff that depends on the computed observable (e.g. templates). + + Simply calling notifySubscribers() would not work because Knockout treats + a computed observable without dependencies as inactive. + + So why not use one regular (not computed) observable instead? Because we + want the property to remain the single source of truth for the value. + Also, you would need extra logic to avoid infinite recursion. + */ + const observableProperties = {}; + const dependencyTriggers = {}; + + availableProperties.forEach(function(preferenceName) { + dependencyTriggers[preferenceName] = ko.observable(instance[preferenceName]); + + observableProperties[preferenceName] = ko.computed({ + read: function() { + dependencyTriggers[preferenceName](); + return instance[preferenceName]; + }, + write: function(newValue) { + instance[preferenceName] = newValue; + }, + deferEvaluation: true + }); + }); + + instance.subscribe('afterChange', function(preferenceName) { + if (typeof preferenceName === 'undefined') { + return; + } + if (typeof dependencyTriggers[preferenceName] !== 'undefined') { + dependencyTriggers[preferenceName](instance[preferenceName]); + } + }); + + return observableProperties; + }; + + function makeGetter(preferenceName) { + return function() { + if (currentValues.hasOwnProperty(preferenceName)) { + return currentValues[preferenceName]; + } else if (defaults.hasOwnProperty(preferenceName)) { + return defaults[preferenceName]; + } + return null; + }; + } + + function makeSetter(preferenceName) { + return function(newValue) { + const validationResult = validators[preferenceName](newValue); + if (validationResult.error) { + throw new Error(config.name + ': ' + validationResult.error); + } else { + if (currentValues[preferenceName] !== validationResult.value) { + instance.beforeChange(preferenceName); + currentValues[preferenceName] = validationResult.value; + instance.afterChange(preferenceName); + + //Auto-save the new value. + if (instance.isAutoSaveEnabled) { + instance.scheduleSave(); + } + } + } + }; + } + + availableProperties.forEach(function(preferenceName) { + Object.defineProperty(instance, preferenceName, { + get: makeGetter(preferenceName), + set: makeSetter(preferenceName) + }); + }); + + initialisePreference(instance, config, 'preference group'); + } + + return { + setupPreference: setupSinglePreference, + setupGroup: setupPreferenceGroup + } +})(); + +function AmeEhIsExplanationHidden(jsData) { + "use strict"; + + return ameEhPreferenceCodegen.setupPreference( + jsData, + function(inputValue) { + let convertedValue; + convertedValue = Number(inputValue); + if (isNaN(convertedValue)) { + return { + "error": "Value must be a number", + "value": "convertedValue" + }; + } + if (convertedValue !== Math.trunc(convertedValue)) { + return { + "error": "Value must be an integer", + "value": "convertedValue" + }; + } + if (convertedValue < 0) { + return { + "error": "Value must be at least 0.", + "value": convertedValue + }; + } + if (convertedValue > 1) { + return { + "error": "Value must be less than or equal to 1", + "value": convertedValue + }; + } + return { + "error": null, + "value": convertedValue + }; + } + ); +} + +AmeEhIsExplanationHidden.pcgnIsConstructor = false; + +function AmeEhUserPreferences(jsData) { + 'use strict'; + + ameEhPreferenceCodegen.setupGroup( + this, + jsData, { + 'numberOfColumns': function(inputValue) { + if (inputValue === null) { + return { + "error": null, + "value": inputValue + }; + } + let convertedValue; + convertedValue = Number(inputValue); + if (isNaN(convertedValue)) { + return { + "error": "Value must be a number", + "value": "convertedValue" + }; + } + if (convertedValue !== Math.trunc(convertedValue)) { + return { + "error": "Value must be an integer", + "value": "convertedValue" + }; + } + if (convertedValue < 1) { + return { + "error": "Value must be at least 1.", + "value": convertedValue + }; + } + if (convertedValue > 3) { + return { + "error": "Value must be less than or equal to 3", + "value": convertedValue + }; + } + return { + "error": null, + "value": convertedValue + }; + }, + 'csExpandedCategories': function(inputValue) { + let convertedValue; + if ( + (typeof inputValue === 'object') || + (typeof inputValue === 'function') + ) { + return { + "error": "Value must be a string or a non-null scalar", + "value": "inputValue" + }; + } + convertedValue = String(inputValue); + if (convertedValue.length > 1000) { + return { + "error": "Value must be no more than 1000 characters long", + "value": convertedValue + }; + } + return { + "error": null, + "value": convertedValue + }; + }, + }, + ["numberOfColumns", "csExpandedCategories"] + ); +} + +AmeEhUserPreferences.pcgnIsConstructor = true; \ No newline at end of file diff --git a/extras/modules/easy-hide/eh-preferences.php b/extras/modules/easy-hide/eh-preferences.php new file mode 100644 index 0000000..540957e --- /dev/null +++ b/extras/modules/easy-hide/eh-preferences.php @@ -0,0 +1,640 @@ +delayUpdates) { + $this->saveChanges(); + } + } + public function saveChanges() + { + //AKA flush + $this->delayUpdates = false; + if (!$this->hasUnsavedChanges || !$this->isPersistable()) { + return false; + } + //Since update_user_meta() does not provide a way to determine if an update failed + //because of an error or because the new value matches the old value, we just assume + //that changes are always saved successfully. + $this->hasUnsavedChanges = false; + return $this->writeToDb(); + } + protected abstract function writeToDb(); + protected abstract function loadFromDb(); + /** + * @return bool + */ + protected function isPersistable() + { + return is_string($this->storageKey) && $this->storageKey !== ''; + } + public function delete() + { + if ($this->isPersistable()) { + delete_user_meta(get_current_user_id(), $this->storageKey); + } + } + public function bufferChanges() + { + $this->delayUpdates = true; + } + //endregion + //region AJAX updates + public function installAjaxCallback($permissionCallback) + { + if (!$this->ajaxEnabled || empty($this->ajaxAction)) { + throw new LogicException('AJAX is not enabled for this item'); + } + $this->permissionCallback = $permissionCallback; + $hook = 'wp_ajax_' . $this->ajaxAction; + add_action($hook, array($this, 'handleAjaxUpdate')); + } + public function handleAjaxUpdate() + { + if (!$this->ajaxEnabled) { + $this->outputAjaxError(new WP_Error('AJAX updates are not enabled for this item', 'ajax_disabled', 500)); + exit; + } + $authStatus = $this->checkAjaxAuthorization(); + if ($authStatus !== true) { + if (is_wp_error($authStatus)) { + $this->outputAjaxError($authStatus); + } else { + $this->outputAjaxError(new WP_Error('unknown_authorization_error', 'Access denied', 403)); + } + exit; + } + $params = $_POST; + //Remove magic quotes. + if (did_action('sanitize_comment_cookies') && function_exists('wp_magic_quotes')) { + $params = wp_unslash($params); + } + if (!array_key_exists('data', $params)) { + $this->outputAjaxError(new WP_Error('The required "data" field is missing', 'no_value')); + return; + } + $newData = json_decode($params['data'], true); + if ($newData === null && function_exists('json_last_error') && defined('JSON_ERROR_NONE') && json_last_error() !== JSON_ERROR_NONE) { + $this->outputAjaxError(new WP_Error('The "data" field does not contain valid JSON', 'json_decode_error')); + return; + } + list($validationErrors, $isSuccess, $changeCount) = $this->updateFromAjax($newData); + $errorMessages = array(); + foreach ($validationErrors as $error) { + $errorMessages = array_merge($errorMessages, $error->get_error_messages()); + } + if ($isSuccess) { + $this->outputJson(array( + 'success' => true, + //There can still be some errors if this was a partial update. + 'validationErrors' => $errorMessages, + 'changes' => $changeCount, + )); + } else { + status_header(400); + $this->outputJson(array('error' => array('message' => 'Validation failed', 'code' => 'validation_error', 'validationErrors' => $errorMessages), 'changes' => $changeCount)); + } + exit; + } + /** + * @return bool|\WP_Error + */ + protected function checkAjaxAuthorization() + { + if (!check_ajax_referer($this->ajaxAction, false, false)) { + return new WP_Error('nonce_check_failed', 'Invalid or missing nonce.', 403); + } + if (isset($this->permissionCallback)) { + $result = call_user_func($this->permissionCallback); + if ($result === false) { + return new WP_Error('permission_callback_failed', 'You don\'t have permission to perform this action.', 403); + } else { + if (is_wp_error($result)) { + return $result; + } + } + } + return true; + } + /** + * @param WP_Error $error + * @return void + */ + protected function outputAjaxError($error) + { + $statusCode = 400; + $customStatusCode = $error->get_error_data(); + if (isset($customStatusCode) && is_int($customStatusCode)) { + $statusCode = $customStatusCode; + } + $errorResponse = array('error' => array('message' => $error->get_error_message(), 'code' => $error->get_error_code())); + status_header($statusCode); + $this->outputJson($errorResponse); + exit; + } + protected function outputJson($response) + { + @header('Content-Type: application/json; charset=' . get_option('blog_charset')); + echo json_encode($response); + } + /** + * Update the preference(s) with the value(s) received in an AJAX request. + * + * The implementation should validate the input data and save it. + * + * @param scalar|array $data + * @return array Multiple values as returned by makeUpdateResult(). + */ + protected abstract function updateFromAjax($data); + /** + * @param WP_Error[] $validationErrors + * @param boolean $isSuccess + * @param int $changeCount + * @return array + */ + protected static final function makeUpdateResult($validationErrors, $isSuccess, $changeCount) + { + return array($validationErrors, $isSuccess, $changeCount); + } + //endregion + //region JavaScript integration + protected $jsGlobalVariable = ''; + protected $jsClassName = ''; + protected $jsScriptHandle = ''; + //Could be the same for multiple preferences. + protected $jsRelativeScriptFileName = ''; + //??? Could be bundled and/or moved. + protected $jsScriptVersion = '20220325'; + protected $isRegistrationDone = false; + protected $isRegistrationHookSet = false; + /** + * @return string|null + */ + public function getScriptHandle() + { + //Register the script lazily. There could be multiple preferences that use + //the same JS script, but we only need to register it once. + if (!$this->isRegistrationDone && !$this->isRegistrationHookSet) { + if (did_action('wp_loaded')) { + $this->registerScript(); + } else { + add_action('wp_loaded', array($this, 'registerScript')); + $this->isRegistrationHookSet = true; + } + } + return $this->jsScriptHandle; + } + public function registerScript() + { + if ($this->isRegistrationDone) { + return; + } + //Multiple preferences could use the same bundled JS script. + //We only need to register it once. + if (!wp_script_is($this->jsScriptHandle, 'registered')) { + wp_register_script($this->jsScriptHandle, plugins_url($this->jsRelativeScriptFileName, __FILE__), array('jquery'), $this->jsScriptVersion); + } + //Add the initialization code that passes data to the script + //and sets up the global variable. Each preference has its own + //initializer even if they share the same JS file. + if (function_exists('wp_add_inline_script') && !empty($this->jsGlobalVariable)) { + //WP 4.5+ + wp_add_inline_script($this->jsScriptHandle, $this->generateJsInitializer(), 'after'); + } + $this->isRegistrationDone = true; + } + protected function generateJsInitializer() + { + if (!$this->triedLoading && $this->isPersistable()) { + $this->loadFromDb(); + } + $constructorData = array_merge(array('ajaxUrl' => admin_url('admin-ajax.php'), 'ajaxAction' => $this->ajaxAction, 'ajaxNonce' => wp_create_nonce($this->ajaxAction), 'name' => $this->name), $this->getJsConstructorData()); + $code = array(); + $code[] = sprintf('window.%1$s = (function(jsData) { + return (%2$s.pcgnIsConstructor) ? (new %2$s(jsData)) : (%2$s(jsData)); + })(%3$s);', $this->jsGlobalVariable, $this->jsClassName, json_encode($constructorData)); + return implode("\n", $code); + } + /** + * @return array + */ + protected abstract function getJsConstructorData(); + //endregion + //region Validation and conversion + /** + * Check if the type of value is numeric (i.e. integer or float). + * + * @param mixed $value + * @return bool + */ + protected static function isNumber($value) + { + return is_float($value) || is_int($value); + } + /** + * Convert a value to a number if it can be done without data loss. + * + * For example, "12.3" is converted to 12.3, but "12.3 things" is not. + * The idea is to convert numbers stored as strings, but to reject strings + * that just happen to have a number in them. + * + * @param mixed $value + * @return float|int|null Either a number, or NULL if the value cannot be converted. + */ + protected static function convertToNumber($value) + { + if (self::isNumber($value)) { + return $value; + } else { + if (is_string($value)) { + $value = trim($value); + //is_numeric() is too permissive, so we'll use a regex to further restrict + //the accepted number formats. + if (is_numeric($value) && preg_match('/^[+-]?\\d{1,40}+(?:[.,]\\d{1,40}+)?$/', $value)) { + $value = str_replace(',', '.', $value); + return floatval($value); + } else { + return null; + } + } else { + if (is_bool($value)) { + return $value ? 1 : 0; + } + } + } + return null; + } + /** + * Convert a scalar value to a string. + * + * @param mixed $value + * @return string|null + */ + protected static function convertToString($value) + { + if (is_string($value)) { + return $value; + } else { + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } else { + if (is_scalar($value)) { + return strval($value); + } + } + } + return null; + } + //endregion +} +abstract class AmeEhBasePreferenceGroup extends AmeEhAbstractPreference +{ + protected $data = array(); + protected $defaults = array(); + protected $ajaxEnabled = true; + protected $ajaxAction = 'ws_pgn_update_group'; + protected $partialUpdatesAllowed = false; + protected $validatorMethods = array(); + /** + * @param string $propertyName + * @return mixed + */ + protected function getSimpleProperty($propertyName) + { + //Lazy-load preferences. + if (!$this->triedLoading && $this->isPersistable()) { + $this->loadFromDb(); + } + if (array_key_exists($propertyName, $this->data)) { + return $this->data[$propertyName]; + } else { + if (array_key_exists($propertyName, $this->defaults)) { + return $this->defaults[$propertyName]; + } + } + return null; + } + /** + * @param string $propertyName + * @param mixed $newValue + * @return $this + */ + protected function setSimpleProperty($propertyName, $newValue) + { + list($errors, $convertedValue) = call_user_func(array($this, $this->validatorMethods[$propertyName]), $newValue); + /** @var \WP_Error[] $errors */ + if (empty($errors)) { + //Make sure preferences are loaded before making changes. + //If we don't do this, we could end up unintentionally deleting an existing + //property ["A" => 123] while saving a new property ["B" => 456]. + if (!$this->triedLoading && $this->isPersistable()) { + $this->loadFromDb(); + } + $this->data[$propertyName] = $convertedValue; + $this->hasUnsavedChanges = true; + $this->maybeSaveChanges(); + } else { + $firstError = reset($errors); + throw new InvalidArgumentException($firstError->get_error_message()); + } + return $this; + } + protected function writeToDb() + { + $updated = update_user_meta(get_current_user_id(), $this->storageKey, $this->data); + return $updated !== false; + } + protected function loadFromDb() + { + $this->triedLoading = true; + $metaValues = get_user_meta(get_current_user_id(), $this->storageKey, false); + if (empty($metaValues) || !is_array($metaValues)) { + return false; + } + $data = reset($metaValues); + if (!is_array($data)) { + return false; + } + $this->data = $data; + return true; + } + protected function updateFromAjax($data) + { + if (!is_array($data)) { + return array(new WP_Error('Input data must be an associative array', 'not_an_array', 400)); + } + $validationErrors = array(); + $validData = array(); + $knownKeys = array_keys(array_merge($this->data, $this->defaults, $this->validatorMethods)); + foreach ($knownKeys as $key) { + if (!array_key_exists($key, $data)) { + continue; + } + $value = $data[$key]; + if (isset($this->validatorMethods[$key])) { + list($errors, $convertedValue) = call_user_func(array($this, $this->validatorMethods[$key]), $value); + if (empty($errors) || $errors === true) { + $validData[$key] = $convertedValue; + } else { + $validationErrors = array_merge($validationErrors, $errors); + } + } + } + if (empty($validationErrors) && empty($validData)) { + //The request did not change any supported properties. This is weird, + //but technically not an error. + return self::makeUpdateResult($validationErrors, true, 0); + } + //Save new values if: + // a) all the input data is valid, or + // b) there are some valid values, and partial updates are allowed. + $isAllValid = empty($validationErrors); + $canDoPartialUpdate = $this->partialUpdatesAllowed && !empty($validData); + if ($isAllValid || $canDoPartialUpdate) { + if (!$this->triedLoading && $this->isPersistable()) { + $this->loadFromDb(); + } + $this->data = array_merge($this->data, $validData); + $this->hasUnsavedChanges = true; + $this->saveChanges(); + return self::makeUpdateResult($validationErrors, true, count($validData)); + } else { + return self::makeUpdateResult($validationErrors, false, 0); + } + } + protected function getJsConstructorData() + { + return array('data' => $this->data, 'defaults' => $this->defaults); + } +} +abstract class AmeEhBasePreference extends AmeEhAbstractPreference +{ + protected $value = null; + protected $hasValue = false; + protected $defaultValue = null; + public function __invoke($newValue = null) + { + if ($newValue === null) { + return $this->getEffectiveValue(); + } else { + return $this->update($newValue); + } + } + protected function getEffectiveValue() + { + if (!$this->triedLoading) { + $this->loadFromDb(); + } + if ($this->hasValue) { + return $this->value; + } + return $this->defaultValue; + } + public function setToNull() + { + return $this->update(null); + } + public function update($newValue) + { + list($errors, $convertedValue) = $this->validate($newValue); + if (!empty($errors)) { + return $errors; + } + $newValue = $convertedValue; + $this->hasValue = true; + if ($newValue !== $this->value) { + $this->value = $newValue; + $this->hasUnsavedChanges = true; + $this->maybeSaveChanges(); + } + return $errors; + } + /** + * @param $newValue + * @psalm-return array{array, mixed} The first element is an array of errors, + * the second is the coerced/converted value or NULL. + */ + public abstract function validate($newValue); + protected function writeToDb() + { + $updated = update_user_meta(get_current_user_id(), $this->storageKey, $this->value); + return $updated !== false; + } + protected function loadFromDb() + { + $this->triedLoading = true; + //There should only be a single value, but setting $single to false helps + //distinguish between "no results" and "value is NULL". + $metaValues = get_user_meta(get_current_user_id(), $this->storageKey, false); + if (empty($metaValues) || !is_array($metaValues)) { + return false; + } + $this->value = reset($metaValues); + $this->hasValue = true; + return true; + } + protected function updateFromAjax($data) + { + $errors = $this->update($data); + $isSuccess = empty($errors); + return self::makeUpdateResult($errors, $isSuccess, $isSuccess ? 1 : 0); + } + protected function getJsConstructorData() + { + return array('value' => $this->value, 'defaultValue' => $this->defaultValue, 'hasValue' => $this->hasValue); + } +} +/** + * Automatically generated class for preference isExplanationHidden + */ +class AmeEhIsExplanationHidden extends AmeEhBasePreference +{ + protected $defaultValue = 0; + protected $ajaxEnabled = true; + protected $ajaxAction = 'ame_eh_pcgn_isExplanationHidden'; + protected $jsGlobalVariable = 'ameEhIsExplanationHidden'; + protected $jsClassName = 'AmeEhIsExplanationHidden'; + //The handle could be the same for multiple preferences. + protected $jsScriptHandle = 'ame-ehjs-eh-preferences'; + //The script could be bundled and/or moved. + protected $jsRelativeScriptFileName = 'eh-preferences.js'; + protected $jsScriptVersion = '20220421-9946'; + public function validate($newValue) + { + $convertedValue = self::convertToNumber($newValue); + if ($convertedValue === null) { + return array(array(new WP_Error('Value must be a number')), $convertedValue); + } + if ($convertedValue != intval($convertedValue)) { + return array(array(new WP_Error('Value must be an integer')), $convertedValue); + } + $convertedValue = intval($convertedValue); + if ($convertedValue < 0) { + return array(array(new WP_Error('Value must be at least 0')), $convertedValue); + } + if ($convertedValue > 1) { + return array(array(new WP_Error('Value must be at most 1')), $convertedValue); + } + return array(array(), $convertedValue); + } + protected $name = 'isExplanationHidden'; + protected $storageKey = 'ws_ame_eh_hide_info'; +} +/** + * Automatically generated class for group UserPreferences + */ +class AmeEhUserPreferences extends AmeEhBasePreferenceGroup +{ + protected $defaults = array('numberOfColumns' => 1, 'csExpandedCategories' => ''); + protected $validatorMethods = array('numberOfColumns' => 'validateNumberOfColumns', 'csExpandedCategories' => 'validateCsExpandedCategories'); + protected $ajaxEnabled = true; + protected $ajaxAction = 'ame_eh_pcgn_UserPreferences'; + /** + * Gets the value of the numberOfColumns preference. + * + * @return int|null + */ + public function getNumberOfColumns() + { + return $this->getSimpleProperty('numberOfColumns'); + } + /** + * @param int $newValue + * @return $this + */ + public function setNumberOfColumns($newValue) + { + return $this->setSimpleProperty('numberOfColumns', $newValue); + } + /** + * Validate numberOfColumns + * + * @param int $newValue + * @return array{array<\WP_Error>, int|mixed} + */ + public function validateNumberOfColumns($newValue) + { + if ($newValue === null) { + return array(array(), $newValue); + } + $convertedValue = self::convertToNumber($newValue); + if ($convertedValue === null) { + return array(array(new WP_Error('Value must be a number')), $convertedValue); + } + if ($convertedValue != intval($convertedValue)) { + return array(array(new WP_Error('Value must be an integer')), $convertedValue); + } + $convertedValue = intval($convertedValue); + if ($convertedValue < 1) { + return array(array(new WP_Error('Value must be at least 1')), $convertedValue); + } + if ($convertedValue > 3) { + return array(array(new WP_Error('Value must be at most 3')), $convertedValue); + } + return array(array(), $convertedValue); + } + /** + * Gets the value of the csExpandedCategories preference. + * + * @return string + */ + public function getCsExpandedCategories() + { + return $this->getSimpleProperty('csExpandedCategories'); + } + /** + * @param string $newValue + * @return $this + */ + public function setCsExpandedCategories($newValue) + { + return $this->setSimpleProperty('csExpandedCategories', $newValue); + } + /** + * Validate csExpandedCategories + * + * @param string $newValue + * @return array{array<\WP_Error>, string|mixed} + */ + public function validateCsExpandedCategories($newValue) + { + $convertedValue = self::convertToString($newValue); + if ($convertedValue === null) { + return array(array(new WP_Error('Value must be a string')), $convertedValue); + } + if (strlen($convertedValue) > 1000) { + return array(array(new WP_Error('Value must be at most 1000 characters long')), $convertedValue); + } + return array(array(), $convertedValue); + } + protected $name = 'UserPreferences'; + protected $storageKey = 'ws_ame_eh_prefs'; + protected $jsGlobalVariable = 'ameEhUserPreferences'; + protected $jsClassName = 'AmeEhUserPreferences'; + protected $jsScriptHandle = 'ame-ehjs-eh-preferences'; + protected $jsRelativeScriptFileName = 'eh-preferences.js'; + protected $jsScriptVersion = '20220421-9946'; +} \ No newline at end of file diff --git a/extras/modules/easy-hide/prefgen.json b/extras/modules/easy-hide/prefgen.json new file mode 100644 index 0000000..22002ff --- /dev/null +++ b/extras/modules/easy-hide/prefgen.json @@ -0,0 +1,35 @@ +{ + "commonPrefix": "AmeEh", + "allowAjaxUpdate": true, + "phpOutputFileName": "eh-preferences.php", + "jsOutputFileName": "eh-preferences.js", + "items": [ + { + "name": "isExplanationHidden", + "storageKey": "ws_ame_eh_hide_info", + "type": "integer", + "defaultValue": 0, + "minimum": 0, + "maximum": 1 + }, + { + "name": "UserPreferences", + "storageKey": "ws_ame_eh_prefs", + "type": "object", + "properties": { + "numberOfColumns": { + "type": "integer", + "nullable": true, + "defaultValue": 1, + "minimum": 1, + "maximum": 3 + }, + "csExpandedCategories": { + "type": "string", + "defaultValue": "", + "maxLength": 1000 + } + } + } + ] +} \ No newline at end of file diff --git a/extras/modules/hide-admin-bar/ameDummyAdminBar.php b/extras/modules/hide-admin-bar/ameDummyAdminBar.php new file mode 100644 index 0000000..8c18684 --- /dev/null +++ b/extras/modules/hide-admin-bar/ameDummyAdminBar.php @@ -0,0 +1,19 @@ +_bind(); + + //WordPress uses the height of the admin bar in the calculation that determines if the admin menu + //should be scrollable or not. See the setPinMenu() function in /wp-admin/js/common.js. This means + //we can't completely get rid of the admin bar because the calculation will fail if the height is + //undefined. To avoid that, let's output an empty, hidden element. + ?> +
+ menuEditor = $menuEditor; + + add_action('init', array($this, 'maybe_hide_admin_bar')); + add_filter('admin_menu_editor-show_general_box', '__return_true'); + add_action('admin_menu_editor-general_box', array($this, 'output_option'), 20); + + add_filter('admin_menu_editor-hideable_vis_components', array($this, 'add_hideable_component')); + } + + public function maybe_hide_admin_bar() { + $this->extras = $GLOBALS['wsMenuEditorExtras']; + + if ( $this->should_hide_admin_bar() ) { + $this->hide_admin_bar(); + } + } + + /** + * Should we hide the admin bar from the current user? + * + * @return bool + */ + private function should_hide_admin_bar() { + $config = $this->menuEditor->load_custom_menu(); + if ( !isset($config, $config['component_visibility'], $config['component_visibility']['toolbar']) ) { + return false; + } + + $grant_access = $config['component_visibility']['toolbar']; + return !$this->extras->check_current_user_access($grant_access, null, null, true, AME_RC_USE_DEFAULT_ACCESS); + } + + /** + * Hide the Toolbar/Admin Bar both on the front-end and the dashboard. + */ + private function hide_admin_bar() { + add_filter('show_admin_bar', '__return_false'); + add_action('in_admin_header', array($this, 'remove_admin_bar_css_classes')); + add_filter('wp_admin_bar_class', array($this, 'filter_admin_bar_class')); + add_action('admin_print_scripts-profile.php', array($this, 'hide_toolbar_settings')); + add_action('admin_bar_init', array($this, 'remove_bump_css')); + } + + /** + * Remove Admin Bar related classes from the and tags. Usually + * these classes are not filterable, so we have to remove them with JS. + */ + public function remove_admin_bar_css_classes() { + ?> + + + + + and . + * + * Normally this isn't necessary. It's a compatibility workaround. + */ + public function remove_bump_css() { + remove_action('wp_head', '_admin_bar_bump_cb'); + } + + /** + * Add a checkbox to the menu editor page. + */ + public function output_option() { + ?> + + 'Toolbar (Admin Bar)', + 'component' => 'toolbar' + ); + return $definitions; + } +} \ No newline at end of file diff --git a/extras/modules/hide-admin-menu/hide-admin-menu.php b/extras/modules/hide-admin-menu/hide-admin-menu.php new file mode 100644 index 0000000..7e91a8d --- /dev/null +++ b/extras/modules/hide-admin-menu/hide-admin-menu.php @@ -0,0 +1,102 @@ +menuEditor = $menuEditor; + + add_action('init', array($this, 'maybe_hide_admin_menu')); + add_filter('admin_menu_editor-show_general_box', '__return_true'); + add_action('admin_menu_editor-general_box', array($this, 'output_option'), 10); + + add_filter('admin_menu_editor-hideable_vis_components', array($this, 'add_hideable_component')); + } + + public function maybe_hide_admin_menu() { + $this->extras = $GLOBALS['wsMenuEditorExtras']; + + if ( $this->should_hide_admin_menu() ) { + $this->hide_admin_menu(); + } + } + + /** + * Should we hide the entire admin menu from the current user? + * + * @return bool + */ + private function should_hide_admin_menu() { + $grant_access = $this->get_grant_access(); + if ( empty($grant_access) ) { + return false; + } + + return !$this->extras->check_current_user_access($grant_access, null, null, true, AME_RC_USE_DEFAULT_ACCESS); + } + + private function get_grant_access() { + $config = $this->menuEditor->load_custom_menu(); + if ( isset($config, $config['component_visibility'], $config['component_visibility']['adminMenu']) ) { + return $config['component_visibility']['adminMenu']; + } + return array(); + } + + private function hide_admin_menu() { + add_action('in_admin_header', array($this, 'output_hiding_css')); + } + + /** + * Output CSS that hides the admin menu container(s). + */ + public function output_hiding_css() { + ?> + + + +
+ 'Admin menu', + 'component' => 'adminMenu' + ); + return $definitions; + } +} \ No newline at end of file diff --git a/extras/modules/metaboxes/ameMbeScreenSettings.php b/extras/modules/metaboxes/ameMbeScreenSettings.php new file mode 100644 index 0000000..cec471e --- /dev/null +++ b/extras/modules/metaboxes/ameMbeScreenSettings.php @@ -0,0 +1,130 @@ +screenId = $screenId; + + if ( $screen !== null ) { + if ( !empty($screen->post_type) ) { + $this->postType = $screen->post_type; + } + if ( !empty($screen->taxonomy) ) { + $this->taxonomy = $screen->taxonomy; + } + } + } + + /** + * @return ameMetaBoxCollection + */ + public function getMetaBoxes() { + if ( !isset($this->metaBoxes) ) { + $this->metaBoxes = new ameMetaBoxCollection($this->screenId); + } + return $this->metaBoxes; + } + + /** + * @return amePostTypeFeatureCollection + */ + public function getPostTypeFeatures() { + if ( !isset($this->postTypeFeatures) ) { + $this->postTypeFeatures = new amePostTypeFeatureCollection(); + } + return $this->postTypeFeatures; + } + + /** + * @param \WP_Screen $screen + * @return boolean + */ + public function mergeScreenInfo($screen) { + if ( !class_exists('WP_Screen', false) || !($screen instanceof WP_Screen) ) { + return false; + } + $modified = false; + + $currentPostType = !empty($screen->post_type) ? $screen->post_type : null; + if ( $currentPostType !== $this->postType ) { + $this->postType = $currentPostType; + $modified = true; + } + $currentTaxonomy = !empty($screen->taxonomy) ? $screen->taxonomy : null; + if ( $currentTaxonomy !== $this->taxonomy ) { + $this->taxonomy = $currentTaxonomy; + $modified = true; + } + + return $modified; + } + + /** + * Check if the post type or taxonomy associated with this screen is missing. + * + * Note that not every screen has an associated post type or taxonomy, so getting + * "false" from this method does not mean that the screen has a valid post type. + * + * @return bool + */ + public function isContentTypeMissing() { + if ( !empty($this->postType) && function_exists('post_type_exists') ) { + if ( !post_type_exists($this->postType) ) { + return true; + } + } + if ( !empty($this->taxonomy) && function_exists('taxonomy_exists') ) { + if ( !taxonomy_exists($this->taxonomy) ) { + return true; + } + } + return false; + } + + public function toArray() { + return array( + self::META_BOX_KEY => $this->getMetaBoxes()->toArray(), + self::CPT_FEATURE_KEY => $this->getPostTypeFeatures()->toArray(), + self::CPT_NAME_KEY => $this->postType, + self::TAXONOMY_NAME_KEY => $this->taxonomy, + 'isContentTypeMissing:' => $this->isContentTypeMissing(), + ); + } + + public static function fromArray($data, $screenId) { + $instance = new self($screenId); + if ( isset($data[self::META_BOX_KEY]) ) { + $instance->metaBoxes = ameMetaBoxCollection::fromArray($data[self::META_BOX_KEY], $screenId); + } else { + $instance->metaBoxes = ameMetaBoxCollection::fromArray($data, $screenId); + } + if ( isset($data[self::CPT_FEATURE_KEY]) ) { + $instance->postTypeFeatures = amePostTypeFeatureCollection::fromArray($data[self::CPT_FEATURE_KEY]); + } + + if ( isset($data[self::CPT_NAME_KEY]) ) { + $instance->postType = $data[self::CPT_NAME_KEY]; + } + if ( isset($data[self::TAXONOMY_NAME_KEY]) ) { + $instance->taxonomy = $data[self::TAXONOMY_NAME_KEY]; + } + + return $instance; + } +} \ No newline at end of file diff --git a/extras/modules/metaboxes/ameMetaBox.php b/extras/modules/metaboxes/ameMetaBox.php new file mode 100644 index 0000000..c217167 --- /dev/null +++ b/extras/modules/metaboxes/ameMetaBox.php @@ -0,0 +1,259 @@ + boolean]. + */ + protected $grantAccess = array(); + + /** + * @var bool[] Default visibility for roles and users. Format: [actorID => boolean]. + * + * Can use the filter default_hidden_meta_boxes in screen.php. + */ + protected $visibleByDefault = array(); + + /** + * @var bool Whether this box is on the default list of hidden meta boxes. + */ + protected $isHiddenByDefault = false; + + public function __construct($properties) { + $this->id = strval($properties['id']); + + $properties = array_merge( + array( + 'title' => '', + 'context' => 'normal', + 'callback' => null, + 'callbackArgs' => null, + ), + $properties + ); + + $this->setProperties($properties); + } + + /** + * Is the specified user allowed to access this meta box? + * + * @param WP_User $user + * @param WPMenuEditor $menuEditor + * @return bool + */ + public function isAvailableTo($user, $menuEditor = null) { + //By default, any user can see any meta box. + if ( empty($user) ) { + return true; + } + + $userActor = 'user:' . $user->user_login; + if ( isset($this->grantAccess[$userActor]) ) { + return $this->grantAccess[$userActor]; + } + + if ( is_multisite() && is_super_admin($user->ID) ) { + //Super Admin can access everything unless explicitly denied. + if ( isset($this->grantAccess['special:super_admin']) ) { + return $this->grantAccess['special:super_admin']; + } + return true; + } + + if (!$menuEditor) { + $menuEditor = $GLOBALS['wp_menu_editor']; + } + + //Allow access if at least one role has access. + $roles = $menuEditor->get_user_roles($user); + $hasAccess = null; + foreach ($roles as $roleId) { + if ( isset($this->grantAccess['role:' . $roleId]) ) { + $roleHasAccess = $this->grantAccess['role:' . $roleId]; + if ( is_null($hasAccess) ){ + $hasAccess = $roleHasAccess; + } else { + $hasAccess = $hasAccess || $roleHasAccess; + } + } else { + //By default, all roles have access. + $hasAccess = true; + } + } + + if ( $hasAccess !== null ) { + return $hasAccess; + } + return true; + } + + /** + * Is the meta box enabled by default for this user? + * + * @param WP_User $user + * @param WPMenuEditor $menuEditor + * @return bool|null + */ + public function isVisibleByDefaultFor($user, $menuEditor) { + $initialSetting = !$this->isHiddenByDefault; + if ( !$user || !$user->exists() ) { + return $initialSetting; + } + + //User-specific settings take precedence over everything else. + $userActor = 'user:' . $user->user_login; + if ( isset($this->visibleByDefault[$userActor]) ) { + return $this->visibleByDefault[$userActor]; + } + + if ( is_multisite() && is_super_admin($user->ID) ) { + //Super Admin can priority over normal roles. + if ( isset($this->grantAccess['special:super_admin']) ) { + return $this->grantAccess['special:super_admin']; + } + } + + if (!$menuEditor) { + $menuEditor = $GLOBALS['wp_menu_editor']; + } + + //Allow access if at least one role has access. + $roles = $menuEditor->get_user_roles($user); + $hasAccess = null; + foreach ($roles as $roleId) { + if ( isset($this->visibleByDefault['role:' . $roleId]) ) { + $roleHasAccess = $this->visibleByDefault['role:' . $roleId]; + if ( is_null($hasAccess) ){ + $hasAccess = $roleHasAccess; + } else { + $hasAccess = $hasAccess || $roleHasAccess; + } + } else { + //Use the default setting for this role. + $hasAccess = $initialSetting; + } + } + + if ( $hasAccess !== null ) { + return $hasAccess; + } + return $initialSetting; + } + + public function toArray() { + return array( + 'id' => $this->id, + 'title' => $this->title, + 'context' => $this->context, + 'isPresent' => $this->isPresent(), + 'grantAccess' => $this->grantAccess, + 'defaultVisibility' => $this->visibleByDefault, + 'isHiddenByDefault' => $this->isHiddenByDefault, + 'className' => get_class($this), + ); + } + + public static function fromArray($widgetProperties) { + if ( !empty($widgetProperties['className']) ) { + $className = $widgetProperties['className']; + return new $className($widgetProperties); + } + return new static($widgetProperties); + } + + protected function setProperties(array $properties) { + //Always overwritten. + $this->title = strval($properties['title']); + $this->callback = ameUtils::get($properties, 'callback'); + $this->callbackArgs = ameUtils::get($properties, 'callbackArgs'); + $this->context = ameUtils::get($properties, 'context', 'normal'); + + //Usually only written upon deserialization. + $this->grantAccess = ameUtils::get($properties, 'grantAccess', $this->grantAccess); + $this->visibleByDefault = ameUtils::get($properties, 'defaultVisibility', $this->visibleByDefault); + $this->isHiddenByDefault = ameUtils::get($properties, 'isHiddenByDefault', $this->isHiddenByDefault); + } + + /* + * Basic getters & setters + */ + + public function getId() { + return $this->id; + } + + public function getTitle() { + return $this->title; + } + + public function getContext() { + return $this->context; + } + + public function getCallback() { + return $this->callback; + } + + public function getGrantAccess() { + return $this->grantAccess; + } + + /** + * @param array $newAccess + * @return bool True if access was modified. + */ + public function setGrantAccess($newAccess) { + if ( !ameUtils::areAssocArraysEqual($this->grantAccess, $newAccess) ) { + $this->grantAccess = $newAccess; + return true; + } + return false; + } + + public function getHiddenByDefault() { + return $this->isHiddenByDefault; + } + + public function setHiddenByDefault($isHidden) { + $isChanged = ($this->isHiddenByDefault xor $isHidden); + $this->isHiddenByDefault = $isHidden; + return $isChanged; + } + + /** + * Is the meta box present on the current site? + * + * @return bool + */ + public function isPresent() { + return true; + } +} \ No newline at end of file diff --git a/extras/modules/metaboxes/ameMetaBoxCollection.php b/extras/modules/metaboxes/ameMetaBoxCollection.php new file mode 100644 index 0000000..8f47c01 --- /dev/null +++ b/extras/modules/metaboxes/ameMetaBoxCollection.php @@ -0,0 +1,221 @@ +screenId = $screenId; + } + + public function merge($coreMetaBoxes) { + $changesDetected = false; + + $activeBoxes = $this->convertMetaBoxesToProperties($coreMetaBoxes); + + //Update existing boxes, add new ones. + $previousBox = null; + foreach($activeBoxes as $properties) { + $wrapper = $this->getWrapper($properties['id']); + if ($wrapper === null) { + $wrapper = new ameMetaBoxWrapper($properties); + $this->insertAfter($wrapper, $previousBox); + $changesDetected = true; + } else { + $changesDetected = $wrapper->syncProperties($properties) || $changesDetected; + } + + $previousBox = $wrapper; + } + + //Flag wrappers that are on the list as present and the rest as not present. + foreach($this->getWrappedBoxes() as $metaBox) { + $changed = $metaBox->setPresence(array_key_exists($metaBox->getId(), $activeBoxes)); + $changesDetected = $changesDetected || $changed; + } + + return $changesDetected; + } + + /** + * Convert the input from the deeply nested array structure that's used by WP core + * to a flat [id => widget-properties] dictionary. + * + * @param array $coreMetaBoxes + * @return array + */ + private function convertMetaBoxesToProperties($coreMetaBoxes) { + $metaBoxProperties = array(); + + foreach($coreMetaBoxes as $context => $priorities) { + foreach($priorities as $priority => $items) { + foreach($items as $metaBox) { + //Skip removed boxes. remove_meta_box() replaces widgets that it removes with false. + if (empty($metaBox) || !is_array($metaBox)) { + continue; + } + + //Skip boxes that have an invalid ID. The ID must be a string. + //This is a workaround for plugins that set the ID to null or another unsupported value. + //Example: "Amazon Simple Affiliate (ASA2)", version 1.15.3. + if (!is_string($metaBox['id'])) { + continue; + } + + $properties = array_merge( + array( + 'context' => $context, + 'priority' => $priority, + 'callbackArgs' => isset($metaBox['args']) ? $metaBox['args'] : null, + ), + $metaBox + ); + $metaBoxProperties[$properties['id']] = $properties; + } + } + } + + return $metaBoxProperties; + } + + /** + * Get a wrapped meta box by ID. + * + * @param string $id + * @return ameMetaBoxWrapper|null + */ + protected function getWrapper($id) { + if (!array_key_exists($id, $this->boxes)) { + return null; + } + $metaBox = $this->boxes[$id]; + if ($metaBox instanceof ameMetaBoxWrapper) { + return $metaBox; + } + return null; + } + + /** + * Insert a meta box after the $target meta box. + * + * If $target is omitted or not in the collection, this method adds the box to the end of the collection. + * + * @param ameMetaBox $metaBox + * @param ameMetaBox|null $target + */ + protected function insertAfter(ameMetaBox $metaBox, ameMetaBox $target = null) { + if (($target === null) || !array_key_exists($target->getId(), $this->boxes)) { + //Just put it at the bottom. + $this->boxes[$metaBox->getId()] = $metaBox; + } else { + $offset = array_search($target->getId(), array_keys($this->boxes)) + 1; + + $this->boxes = array_merge( + array_slice($this->boxes, 0, $offset, true), + array($metaBox->getId() => $metaBox), + array_slice($this->boxes, $offset, null, true) + ); + } + } + + /** + * Remove a meta box from the collection. + * + * @param string $metBoxId + */ + public function remove($metBoxId) { + unset($this->boxes[$metBoxId]); + } + + /** + * Set the default list of hidden meta boxes. + * + * @param string[] $metaBoxIds + * @return bool + */ + public function setHiddenByDefault($metaBoxIds) { + if ( !is_array($metaBoxIds) ) { + return false; + } + + $changesDetected = false; + foreach($this->getWrappedBoxes() as $box) { + $changesDetected = $box->setHiddenByDefault(in_array($box->getId(), $metaBoxIds)) || $changesDetected; + } + return $changesDetected; + } + + /* + * Item filters + */ + + /** + * @return ameMetaBox[] + */ + public function getPresentBoxes() { + return array_filter($this->boxes, function(ameMetaBox $box) { + return $box->isPresent(); + }); + } + + /** + * Get a list of all wrapped meta boxes. + * + * @return ameMetaBoxWrapper[] + * @noinspection PhpReturnDocTypeMismatchInspection PhpStorm misidentifies the actual return type. + */ + protected function getWrappedBoxes() { + /** @noinspection PhpIncompatibleReturnTypeInspection */ + return array_filter($this->boxes, function($metaBox) { + return ($metaBox instanceof ameMetaBoxWrapper); + }); + } + + /** + * Get a list of wrapped meta boxes that are NOT present on the current site. + * + * @return ameMetaBoxWrapper[] + */ + public function getMissingWrappedBoxes() { + return array_filter($this->getWrappedBoxes(), function(ameMetaBox $metaBox) { + return !$metaBox->isPresent(); + }); + } + + /* + * Serialize / deserialize + */ + + public function toArray() { + return array_map(function(ameMetaBox $metaBox) { + return $metaBox->toArray(); + }, $this->boxes); + } + + public static function fromArray($data, $screenId) { + $instance = new self($screenId); + foreach($data as $id => $properties) { + $instance->boxes[$id] = ameMetaBox::fromArray($properties); + } + return $instance; + } + + #[\ReturnTypeWillChange] + public function getIterator() { + return new ArrayIterator($this->boxes); + } + + /** + * @return bool + */ + public function isEmpty() { + return empty($this->boxes); + } +} \ No newline at end of file diff --git a/extras/modules/metaboxes/ameMetaBoxEditor.php b/extras/modules/metaboxes/ameMetaBoxEditor.php new file mode 100644 index 0000000..66dfd24 --- /dev/null +++ b/extras/modules/metaboxes/ameMetaBoxEditor.php @@ -0,0 +1,678 @@ + true, 'editor' => true); + + protected $tabSlug = 'metaboxes'; + protected $tabTitle = 'Meta Boxes'; + + /** + * @var ameMetaBoxSettings + */ + private $settings = null; + + private $shouldRefreshMetaBoxes = false; + private $hiddenBoxCache = array(); + + private $areSettingsCorrupted = false; + + public function __construct($menuEditor) { + parent::__construct($menuEditor); + + if ( !$this->isEnabledForRequest() ) { + return; + } + + add_action('add_meta_boxes', array($this, 'addDelayedMetaBoxHook'), 10, 1); + add_filter('default_hidden_meta_boxes', array($this, 'filterDefaultHiddenBoxes'), 10, 2); + //Gutenberg support. + add_action('enqueue_block_editor_assets', array($this, 'enqueueGutenbergScripts'), 200); + + add_action('admin_menu_editor-header', array($this, 'handleFormSubmission'), 10, 2); + + //Clear caches when switching to another site or user. + add_action('switch_blog', array($this, 'clearCache'), 10, 0); + add_action('set_current_user', array($this, 'clearCache'), 10, 0); + add_action('updated_user_meta', array($this, 'clearCache'), 10, 0); + add_action('deleted_user_meta', array($this, 'clearCache'), 10, 0); + + add_action('current_screen', array($this, 'processPostTypeFeatures'), 200, 1); + + add_action('admin_menu_editor-register_hideable_items', array($this, 'registerHideableBoxes'), 10, 1); + add_filter('admin_menu_editor-save_hideable_items-mb', array($this, 'saveHideableItems'), 10, 2); + } + + protected function isEnabledForRequest() { + return !is_network_admin(); + } + + public function addDelayedMetaBoxHook($postType) { + /* + * Some plugins add their meta boxes using the "admin_head" action (example: WPML) or other non-standard hooks. + * Unfortunately, this means we can't reliably catch all boxes by using "add_meta_boxes" or "do_meta_boxes". + * We use the "in_admin_header" action instead because it runs after the meta box related actions and after most + * other header hooks. + * + * However, this workaround is not fully reliable because there are parts of WP admin that output meta boxes + * immediately after registering them. Examples: + * /wp-admin/edit-form-comment.php + * /wp-admin/edit-link-form.php + * + * Partial solution: Let's use the "in_admin_header" hook only on "Edit $CPT" pages. + */ + $latePriority = 2000; + + //Is the current page a post editing screen? + $currentScreen = get_current_screen(); + if ( !empty($currentScreen) ) { + if ( + isset($currentScreen->base) + && ($currentScreen->base === 'post') + && !empty($postType) + && !did_action('in_admin_header') + ) { + add_action('in_admin_header', array($this, 'processMetaBoxes'), $latePriority, 0); + return; + } + } + + add_action('add_meta_boxes_' . $postType, array($this, 'processMetaBoxes'), $latePriority, 0); + } + + public function processMetaBoxes() { + global $wp_meta_boxes; + + $currentScreen = get_current_screen(); + if ( empty($currentScreen) || $this->areSettingsCorrupted ) { + return; + } + + $currentUser = wp_get_current_user(); + + $screenSettings = $this->getScreenSettings($currentScreen); + $changesDetected = $screenSettings->mergeScreenInfo($currentScreen); + + //Get the box settings for the current screen. + $metaBoxes = $screenSettings->getMetaBoxes(); + //Update existing boxes, add new boxes, flag stored boxes that no longer exist. + $changesDetected = ($metaBoxes->merge(ameUtils::get($wp_meta_boxes, $currentScreen->id, array())) + || $changesDetected); + + //If anything has changed, save the updated box collection. + if ( $changesDetected && $this->userCanEditMetaBoxes() ) { + //Remove wrapped meta boxes where the file no longer exists. + foreach ($metaBoxes->getMissingWrappedBoxes() as $missingBox) { + $callbackFileName = $missingBox->getCallbackFileName(); + if ( !empty($callbackFileName) && !is_file($callbackFileName) ) { + $metaBoxes->remove($missingBox->getId()); + } + } + + //Also update the default list of hidden boxes. + $metaBoxes->setHiddenByDefault($this->getUnmodifiedDefaultHiddenBoxes($currentScreen)); + + $this->saveSettings(); + } + + //Remove hidden boxes. + foreach ($metaBoxes->getPresentBoxes() as $box) { + if ( !$box->isAvailableTo($currentUser, $this->menuEditor) ) { + remove_meta_box($box->getId(), $currentScreen, $box->getContext()); + } + } + } + + /** + * @param \WP_Screen $screen + * @return ameMbeScreenSettings + */ + protected function getScreenSettings($screen) { + $this->loadSettings(); + + $screenId = $screen->id; + if ( isset($this->settings[$screenId]) ) { + return $this->settings[$screenId]; + } + + $collection = new ameMbeScreenSettings($screenId, $screen); + $this->settings[$screenId] = $collection; + return $collection; + } + + /** + * Change the default list of hidden meta boxes. + * + * @param array $hidden + * @param WP_Screen $screen + * @return array + */ + public function filterDefaultHiddenBoxes($hidden, $screen) { + if ( empty($screen) || !($screen instanceof WP_Screen) || $this->areSettingsCorrupted ) { + return $hidden; + } + if ( isset($this->hiddenBoxCache[$screen->id]) ) { + return $this->hiddenBoxCache[$screen->id]; + } + + $metaBoxes = $this->getScreenSettings($screen)->getMetaBoxes(); + + static $isUpdateDone = false; + if ( !$isUpdateDone ) { + $changesDetected = $metaBoxes->setHiddenByDefault($hidden); + if ( $changesDetected ) { + $this->saveSettings(); + } + $isUpdateDone = true; + } + + $user = wp_get_current_user(); + $visible = array(); + + foreach ($metaBoxes->getPresentBoxes() as $box) { + if ( $box->isVisibleByDefaultFor($user, $this->menuEditor) ) { + $visible[] = $box->getId(); + } else { + $hidden[] = $box->getId(); + } + } + + $hidden = array_unique(array_diff($hidden, $visible)); + + //Note: It might be a good idea to cache intermediate results (i.e. only custom hidden & visible settings) + //instead of the final result. Consider that if there are plugin compatibility issues. + $this->hiddenBoxCache[$screen->id] = $hidden; + + return $hidden; + } + + private function getUnmodifiedDefaultHiddenBoxes(WP_Screen $screen) { + //This is a slightly modified excerpt from the get_hidden_meta_boxes() core function in screen.php. + $hidden = array(); + if ( 'post' == $screen->base ) { + if ( in_array($screen->post_type, array('post', 'page', 'attachment')) ) { + $hidden = array( + 'slugdiv', + 'trackbacksdiv', + 'postcustom', + 'postexcerpt', + 'commentstatusdiv', + 'commentsdiv', + 'authordiv', + 'revisionsdiv', + ); + } else { + $hidden = array('slugdiv'); + } + } + return apply_filters('default_hidden_meta_boxes', $hidden, $screen); + } + + /** + * @param WP_Screen $screen + */ + public function processPostTypeFeatures($screen = null) { + if ( + !isset($screen, $screen->post_type, $screen->id) + || empty($screen->post_type) + || $this->areSettingsCorrupted + ) { + return; + } + //Scan only the "Add Item" and "Edit Item" screens. + //The "All Items" screen also has a post type, but it contains no editor boxes. + if ( isset($screen->base) && ($screen->base !== 'post') ) { + return; + } + + $currentFeatures = get_all_post_type_supports($screen->post_type); + if ( empty($currentFeatures) || !is_array($currentFeatures) ) { + return; + } + + $screenSettings = $this->getScreenSettings($screen); + $changesDetected = $screenSettings->mergeScreenInfo($screen); + + $currentFeatures = array_intersect_key($currentFeatures, self::$supportedCptFeatures); + + $featureSettings = $screenSettings->getPostTypeFeatures(); + $changesDetected = $featureSettings->merge($currentFeatures) || $changesDetected; + + if ( $changesDetected && $this->userCanEditMetaBoxes() ) { + $this->saveSettings(); + } + + //Remove disabled features. + $currentUser = wp_get_current_user(); + foreach ($featureSettings->getFeatures() as $feature) { + if ( !$feature->isAvailableTo($currentUser, $this->menuEditor) ) { + remove_post_type_support($screen->post_type, $feature->getId()); + } + } + } + + public function userCanEditMetaBoxes() { + return $this->menuEditor->current_user_can_edit_menu(); + } + + private function saveSettings() { + $json = $this->settings->toJSON(); + $lock = ameFileLock::create(__FILE__); + $lock->acquire(1); + $this->setScopedOption(self::OPTION_NAME, $json); + $lock->release(); + } + + private function loadSettings() { + if ( isset($this->settings) ) { + return $this->settings; + } + + /** @noinspection PhpRedundantOptionalArgumentInspection The default value could change, so make it explicit. */ + $json = $this->getScopedOption(self::OPTION_NAME, null); + + if ( empty($json) ) { + $this->settings = new ameMetaBoxSettings(); + } else { + try { + $this->settings = ameMetaBoxSettings::fromJSON($json); + } catch (ameInvalidJsonException $ex) { + $this->areSettingsCorrupted = true; + $this->settings = new ameMetaBoxSettings(); + if ( is_admin() && is_user_logged_in() && !did_action('all_admin_notices') ) { + add_action('all_admin_notices', array($this, 'showSettingsCorruptionError')); + } + } + } + + return $this->settings; + } + + public function showSettingsCorruptionError() { + if ( !$this->userCanEditMetaBoxes() ) { + return; + } + printf( + '

%s

', + 'Admin Menu Editor Pro error: Cannot load meta box settings. The data might be corrupted.
' + . sprintf( + 'If you have recently migrated this site to a new server, try restoring the %s option from backup.', + esc_html(self::OPTION_NAME) + ) + ); + } + + public function exportSettings() { + $this->loadSettings(); + if ( $this->settings->isEmpty() ) { + return null; + } + return $this->settings->toArray(); + } + + public function importSettings($newSettings) { + if ( empty($newSettings) || !is_array($newSettings) ) { + return; + } + + $settings = ameMetaBoxSettings::fromArray($newSettings); + $settings->setFirstRefreshState(true); + $this->settings = $settings; + $this->saveSettings(); + } + + public function getExportOptionLabel() { + return 'Meta boxes'; + } + + public function getExportOptionDescription() { + return ''; + } + + public function enqueueTabScripts() { + parent::enqueueTabScripts(); + $this->loadSettings(); + + //Automatically refresh the list of available meta boxes. + $query = $this->menuEditor->get_query_params(); + $this->shouldRefreshMetaBoxes = empty($query['ame-meta-box-refresh-done']) + && ( + $this->settings->isEmpty() + || (!empty($query[self::FORCE_REFRESH_PARAM]) && check_admin_referer(self::FORCE_REFRESH_PARAM)) + || (!$this->settings->isFirstRefreshDone()) + ); + + if ( $this->shouldRefreshMetaBoxes ) { + $pagesWithMetaBoxes = array(); + if ( get_option('link_manager_enabled') ) { + $pagesWithMetaBoxes[] = 'link-add.php'; + } + $postTypes = get_post_types(array('public' => true, 'show_ui' => true), 'objects', 'or'); + foreach ($postTypes as $postType) { + $pagesWithMetaBoxes[] = add_query_arg( + array( + 'post_type' => $postType->name, + 'ame_mb_rand' => rand(1, 10000), + ), + self_admin_url('post-new.php') + ); + } + + //Include Media/attachments. This post type doesn't have a standard "new post" screen, + //so lets use the edit URL of the most recently uploaded image instead. + $attachments = get_posts(array( + 'post_type' => 'attachment', + 'post_mime_type' => 'image', + 'numberposts' => 1, + 'post_status' => null, + 'post_parent' => 'any', + )); + if ( !empty($attachments) ) { + $firstAttachment = reset($attachments); + $pagesWithMetaBoxes[] = get_edit_post_link($firstAttachment->ID, 'raw'); + } + + wp_enqueue_auto_versioned_script( + 'ame-refresh-meta-boxes', + plugins_url('refresh-meta-boxes.js', __FILE__), + array('jquery') + ); + + wp_localize_script( + 'ame-refresh-meta-boxes', + 'wsMetaBoxRefresherData', + array( + 'editorUrl' => $this->getTabUrl(array('ame-meta-box-refresh-done' => 1)), + 'pagesWithMetaBoxes' => $pagesWithMetaBoxes, + ) + ); + return; + } + + wp_register_auto_versioned_script( + 'ame-meta-box-editor', + plugins_url('metabox-editor.js', __FILE__), + array( + 'ame-lodash', + 'knockout', + 'ame-actor-selector', + 'jquery', + 'ame-actor-manager', + ) + ); + + $settings = $this->loadSettings(); + wp_localize_script( + 'ame-meta-box-editor', + 'wsAmeMetaBoxEditorData', + array( + 'settings' => $settings->toArray(), + 'refreshUrl' => wp_nonce_url( + $this->getTabUrl(array( + self::FORCE_REFRESH_PARAM => 1, + 'ame-mb-random' => rand(1, 10000), + )), + self::FORCE_REFRESH_PARAM + ), + ) + ); + + wp_enqueue_script('jquery-qtip'); + wp_enqueue_script('ame-meta-box-editor'); + + wp_enqueue_auto_versioned_style( + 'ame-meta-box-editor-style', + plugins_url('metabox-editor.css', __FILE__) + ); + } + + public function handleFormSubmission($action, $post = array()) { + //For debugging. + if ( $action === 'ame_reset_meta_box_settings' && defined('WP_DEBUG') ) { + $this->settings = new ameMetaBoxSettings(); + $this->saveSettings(); + return; + } + + //Note: We don't need to check user permissions here because plugin core already did. + if ( $action === 'ame_save_meta_boxes' ) { + check_admin_referer($action); + + //Save settings. + $settings = ameMetaBoxSettings::fromJSON($post['settings']); + $settings->setFirstRefreshState(true); + $this->settings = $settings; + $this->saveSettings(); + + wp_redirect($this->getTabUrl(array('updated' => 1))); + exit; + } + } + + public function displaySettingsPage() { + $this->loadSettings(); + + if ( $this->areSettingsCorrupted ) { + echo '

Meta box settings are not available.

'; + return; + } + + if ( $this->shouldRefreshMetaBoxes ) { + if ( !$this->settings->isFirstRefreshDone() ) { + //Let's update the initial refresh flag before the refresh actually happens. + //This helps prevent an infinite loop when the initial refresh fails. + $this->settings->setFirstRefreshState(true); + $this->saveSettings(); + } + $this->outputTemplate('box-refresh'); + } else { + parent::displaySettingsPage(); + } + } + + public function clearCache() { + $this->hiddenBoxCache = array(); + $this->settings = null; + } + + /** + * Add a script that will remove Gutenberg document panels that correspond to hidden meta boxes. + */ + public function enqueueGutenbergScripts() { + //Some plugins load the Gutenberg editor outside the admin dashboard in which case + //the get_current_screen() function might not be available. + if ( !function_exists('get_current_screen') ) { + return; + } + $currentScreen = get_current_screen(); + if ( empty($currentScreen) ) { + return; + } + $currentUser = wp_get_current_user(); + + $boxesToPanels = array( + 'slugdiv' => 'post-link', + 'postexcerpt' => 'post-excerpt', + 'postimagediv' => 'featured-image', + 'commentstatusdiv' => 'discussion-panel', + 'categorydiv' => 'taxonomy-panel-category', + 'pageparentdiv' => 'page-attributes', + ); + + $boxesToSelectors = array( + 'submitdiv' => '#editor .edit-post-post-schedule', + 'formatdiv' => '#editor .editor-post-format, #editor .edit-post-post-schedule + .components-panel__row', + ); + + $panelsToRemove = array(); + $selectorsToHide = array(); + $metaBoxes = $this->getScreenSettings($currentScreen)->getMetaBoxes(); + $presentBoxes = $metaBoxes->getPresentBoxes(); + foreach ($presentBoxes as $box) { + if ( $box->isAvailableTo($currentUser, $this->menuEditor) ) { + continue; + } + + //What's the panel name for this box? + $boxId = $box->getId(); + if ( isset($boxesToPanels[$boxId]) ) { + $panelsToRemove[] = $boxesToPanels[$boxId]; + } else if ( preg_match('/^tagsdiv-(?P.++)$/', $boxId, $matches) ) { + $panelsToRemove[] = 'taxonomy-panel-' . $matches['taxonomy']; + } else if ( isset($boxesToSelectors[$boxId]) ) { + $selectorsToHide[] = $boxesToSelectors[$boxId]; + } + //We deliberately skip non-core boxes. For now, the remove_meta_box() call + //in processMetaBoxes() seems to remove them effectively. + } + + if ( empty($panelsToRemove) && empty($selectorsToHide) ) { + return; + } + + wp_enqueue_auto_versioned_script( + 'ame-hide-gutenberg-panels', + plugins_url('hide-gutenberg-panels.js', __FILE__), + array('wp-data', 'wp-blocks', 'wp-edit-post', 'jquery') + ); + + wp_localize_script( + 'ame-hide-gutenberg-panels', + 'wsAmeGutenbergPanelData', + array( + 'panelsToRemove' => $panelsToRemove, + 'selectorsToHide' => $selectorsToHide, + ) + ); + } + + /** + * @param \YahnisElsts\AdminMenuEditor\EasyHide\HideableItemStore $store + */ + public function registerHideableBoxes($store) { + $settings = $this->loadSettings(); + if ( $this->areSettingsCorrupted ) { + return; + } + + /* + * Each meta box will be put into *two* categories: + * + * 1) "Meta Boxes" / "Post Types (or Screens)" / "Post Type Name" + * 2) "Meta Boxes" / "Boxes" / "Box Name" + * + * The second category exists because often the same box is used + * on multiple screens / post types. It could be convenient to hide + * a box on all screens at once. + */ + $postEditorCategory = $store->getOrCreateCategory('post-editor', 'Editor', null, false, 0, 0); + $baseCat = $store->getOrCreateCategory('meta-boxes', 'Meta Boxes', $postEditorCategory); + $byScreenCat = $store->getOrCreateCategory('mb-post-types', 'Post Types', $baseCat, true, 0, 0); + $byBoxCat = $store->getOrCreateCategory('mb-boxes', 'Boxes', $baseCat, true, 0, 0); + + $baseCat->enableTableView($byBoxCat, $byScreenCat); + + /** @var array> $usedBoxCatLabels */ + $usedBoxCatLabels = array(); + + /** @var ameMbeScreenSettings $screenSettings */ + foreach ($settings as $screenId => $screenSettings) { + $boxes = $screenSettings->getMetaBoxes(); + if ( $boxes->isEmpty() ) { + continue; //Skip screens that don't have any boxes. This can apparently happen. + } + + $screenCat = $store->getOrCreateCategory( + 'meta-boxes/s/' . $screenId, + ucfirst($screenId), + $byScreenCat, + true, + 0, + 0 + ); + + /** @var \ameMetaBox $box */ + foreach ($boxes as $box) { + $label = trim(strip_tags($box->getTitle())); + if ( $label === '' ) { + $label = '(No title)'; + } + + $boxCat = $store->getOrCreateCategory( + 'mb-boxes/' . $box->getId(), + $label, + $byBoxCat, + true, + 0, + 0 + ); + + $store->addItem( + $this->makeHideableItemId($screenId, $box), + $label, + array($screenCat, $boxCat), + null, + $box->getGrantAccess(), + 'mb', + $box->getId() + ); + + //Detect boxes that have the same name and give them subtitle. + //For example, multiple plugins have a "Tags" meta box. + if ( isset($usedBoxCatLabels[$label]) && ($usedBoxCatLabels[$label][0] !== $boxCat) ) { + $boxCat->addSubtitle($box->getId()); + list($existingCategory, $firstBox) = $usedBoxCatLabels[$label]; + $existingCategory->addSubtitle($firstBox->getId()); + } else { + $usedBoxCatLabels[$label] = array($boxCat, $box); + } + } + } + } + + /** + * @param string $screenId + * @param ameMetaBox $box + * @return string + */ + private function makeHideableItemId($screenId, $box) { + return 'meta-boxes/' . $screenId . '/' . $box->getId(); + } + + public function saveHideableItems($errors, $items) { + $settings = $this->loadSettings(); + if ( $this->areSettingsCorrupted ) { + $errors[] = new WP_Error( + 'ame_corrupted_boxes', + 'Existing meta box settings are corrupted and cannot be changed.' + ); + return $errors; + } + + $anySettingsModified = false; + + foreach ($settings as $screenId => $screenSettings) { + /** @var \ameMetaBox $box */ + foreach ($screenSettings->getMetaBoxes() as $box) { + $id = $this->makeHideableItemId($screenId, $box); + if ( isset($items[$id]) ) { + $enabled = isset($items[$id]['enabled']) ? $items[$id]['enabled'] : array(); + $boxModified = $box->setGrantAccess($enabled); + $anySettingsModified = $anySettingsModified || $boxModified; + } + } + } + + if ( $anySettingsModified ) { + $this->settings = $settings; + $this->saveSettings(); + } + + return $errors; + } +} \ No newline at end of file diff --git a/extras/modules/metaboxes/ameMetaBoxSettings.php b/extras/modules/metaboxes/ameMetaBoxSettings.php new file mode 100644 index 0000000..b0cad16 --- /dev/null +++ b/extras/modules/metaboxes/ameMetaBoxSettings.php @@ -0,0 +1,170 @@ +screens); + } + + /** + * @return bool + */ + public function isFirstRefreshDone() { + return $this->isInitialRefreshDone; + } + + /** + * @param bool $isRefreshDone + */ + public function setFirstRefreshState($isRefreshDone) { + $this->isInitialRefreshDone = $isRefreshDone; + } + + public function toArray() { + $screenSettings = array_map(function(ameMbeScreenSettings $collection) { + return $collection->toArray(); + }, $this->screens); + + return array( + 'format' => array( + 'name' => self::FORMAT_NAME, + 'version' => self::FORMAT_VERSION, + ), + 'screens' => $screenSettings, + 'isInitialRefreshDone' => $this->isInitialRefreshDone, + ); + } + + public static function fromArray($input) { + if ( + !isset($input['format']['name'], $input['format']['version']) + || ($input['format']['name'] !== self::FORMAT_NAME) + ) { + throw new ameInvalidMetaBoxDataException( + "Unknown meta box format. The format.name or format.version key is missing or invalid." + ); + } + + if ( version_compare($input['format']['version'], self::FORMAT_VERSION) > 0 ) { + throw new ameInvalidMetaBoxDataException(sprintf( + "Can't import meta box settings that were created by a newer version of the plugin. '. + 'Update the plugin and try again. (Newest supported format: '%s', input format: '%s'.)", + $input['format']['version'], + self::FORMAT_VERSION + )); + } + + $settings = new self(); + foreach($input['screens'] as $screenId => $collectionData) { + $settings->screens[$screenId] = ameMbeScreenSettings::fromArray($collectionData, $screenId); + } + + if ( isset($input['isInitialRefreshDone']) && is_scalar($input['isInitialRefreshDone']) ) { + $settings->isInitialRefreshDone = !empty($input['isInitialRefreshDone']); + } + + return $settings; + } + + public function toJSON() { + return json_encode($this->toArray()); + } + + public static function fromJSON($json) { + $input = json_decode($json, true); + + if ($input === null) { + throw new ameInvalidJsonException('Cannot parse meta box data. The input is not valid JSON.'); + } + + if (!is_array($input)) { + throw new ameInvalidMetaBoxDataException(sprintf( + 'Failed to decode meta box data. Expected type: array, actual type: %s', + gettype($input) + )); + } + + return self::fromArray($input); + } + + /** + * Whether an offset exists + * + * @link http://php.net/manual/en/arrayaccess.offsetexists.php + * @param mixed $offset

+ * An offset to check for. + *

+ * @return boolean true on success or false on failure. + *

+ *

+ * The return value will be cast to boolean if non-boolean was returned. + * @since 5.0.0 + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) { + return array_key_exists($offset, $this->screens); + } + + /** + * Offset to retrieve + * + * @link http://php.net/manual/en/arrayaccess.offsetget.php + * @param mixed $offset

+ * The offset to retrieve. + *

+ * @return mixed Can return all value types. + * @since 5.0.0 + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) { + return $this->screens[$offset]; + } + + /** + * Offset to set + * + * @link http://php.net/manual/en/arrayaccess.offsetset.php + * @param mixed $offset

+ * The offset to assign the value to. + *

+ * @param mixed $value

+ * The value to set. + *

+ * @return void + * @since 5.0.0 + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) { + $this->screens[$offset] = $value; + } + + /** + * Offset to unset + * + * @link http://php.net/manual/en/arrayaccess.offsetunset.php + * @param mixed $offset

+ * The offset to unset. + *

+ * @return void + * @since 5.0.0 + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) { + unset($this->screens[$offset]); + } + + #[\ReturnTypeWillChange] + public function getIterator() { + return new ArrayIterator($this->screens); + } +} + +class ameInvalidMetaBoxDataException extends RuntimeException {} \ No newline at end of file diff --git a/extras/modules/metaboxes/ameMetaBoxWrapper.php b/extras/modules/metaboxes/ameMetaBoxWrapper.php new file mode 100644 index 0000000..0949073 --- /dev/null +++ b/extras/modules/metaboxes/ameMetaBoxWrapper.php @@ -0,0 +1,106 @@ +id ) { + throw new LogicException(sprintf( + 'Meta box ID mismatch. Expected: "%s", got: "%s".', + $this->id, + $newProperties['id'] + )); + } + + $oldProperties = $this->toArray(); + $this->setProperties($newProperties); + + $changesDetected = $this->setPresence($this->hasValidCallback()); + + //Update callback file name. + if ( $this->hasValidCallback() ) { + $changesDetected = $this->updateCallbackFileName() || $changesDetected; + } + + foreach (array('title', 'context') as $key) { + if ( $oldProperties[$key] !== $newProperties[$key] ) { + $changesDetected = true; + break; + } + } + + return $changesDetected; + } + + /* + * Presence detection + */ + + public function isPresent() { + return $this->wasPresent || $this->hasValidCallback(); + } + + protected function hasValidCallback() { + return isset($this->callback) && is_callable($this->callback); + } + + public function setPresence($isPresent) { + $changed = ($this->wasPresent !== $isPresent); + $this->wasPresent = $isPresent; + return $changed; + } + + /* + * Callback file name + */ + + public function getCallbackFileName() { + //TODO: Maybe normalize this to use forward slashes always? Could help with JSON corruption caused by some DB migration plugins. + return $this->callbackFileName; + } + + private function updateCallbackFileName() { + $reflection = new AmeReflectionCallable($this->callback); + + $fileName = $reflection->getFileName(); + if ( $fileName === false ) { + $fileName = null; + } + + if ( $fileName !== $this->callbackFileName ) { + $this->callbackFileName = $fileName; + return true; //File name has changed. + } + return false; //No changes. + } + + /* + * Include additional properties on load/save. + */ + + public function toArray() { + $properties = parent::toArray(); + $properties['wasPresent'] = $this->wasPresent; + $properties['callbackFileName'] = $this->callbackFileName; + return $properties; + } + + protected function setProperties(array $properties) { + parent::setProperties($properties); + + $keysToCopy = array('wasPresent', 'callbackFileName'); + foreach ($keysToCopy as $name) { + if ( isset($properties[$name]) ) { + $this->$name = $properties[$name]; + } + } + } +} \ No newline at end of file diff --git a/extras/modules/metaboxes/amePostTypeFeature.php b/extras/modules/metaboxes/amePostTypeFeature.php new file mode 100644 index 0000000..4087d20 --- /dev/null +++ b/extras/modules/metaboxes/amePostTypeFeature.php @@ -0,0 +1,9 @@ + $unused) { + if ( !isset($this->features[$featureName]) ) { + $this->features[$featureName] = amePostTypeFeature::fromArray(array( + 'id' => $featureName, + 'title' => $this->getFeatureTitle($featureName), + )); + $changesDetected = true; + } + } + + //Remove features that no longer exist. + $existingFeatures = array_intersect_key($this->features, $currentFeatures); + if ( count($existingFeatures) !== count($this->features) ) { + $this->features = $existingFeatures; + $changesDetected = true; + } + + return $changesDetected; + } + + private function getFeatureTitle($featureName) { + if ( $featureName === 'editor' ) { + return 'Content Editor'; + } + return ucfirst($featureName); + } + + /** + * @return amePostTypeFeature[] + */ + public function getFeatures() { + return $this->features; + } + + /* + * Serialize / deserialize + */ + + public function toArray() { + return array_map(function (amePostTypeFeature $item) { + return $item->toArray(); + }, $this->features); + } + + public static function fromArray($data) { + $instance = new self(); + foreach ($data as $id => $properties) { + $instance->features[$id] = amePostTypeFeature::fromArray($properties); + } + return $instance; + } +} \ No newline at end of file diff --git a/extras/modules/metaboxes/box-refresh-template.php b/extras/modules/metaboxes/box-refresh-template.php new file mode 100644 index 0000000..ae90de9 --- /dev/null +++ b/extras/modules/metaboxes/box-refresh-template.php @@ -0,0 +1,13 @@ +

+ Refreshing available meta boxes... + + +
+ +

+ +

+ This may take a few minutes. You'll be redirected to the settings screen when it's done. + If the refresh takes too long or doesn't work, please go to the screen that contains + the meta boxes that you want to hide, then go back to this page. +

\ No newline at end of file diff --git a/extras/modules/metaboxes/hide-gutenberg-panels.js b/extras/modules/metaboxes/hide-gutenberg-panels.js new file mode 100644 index 0000000..06631b7 --- /dev/null +++ b/extras/modules/metaboxes/hide-gutenberg-panels.js @@ -0,0 +1,28 @@ +'use strict'; + +( + /** + * @param {Object} data + * @param {Array} data.panelsToRemove List of Gutenberg panels to remove. + * @param {Array} data.selectorsToHide List of jQuery selectors to hide. + */ + function (data) { + if (typeof data['panelsToRemove'] !== 'undefined') { + for (var i = 0; i < data.panelsToRemove.length; i++) { + // noinspection JSUnresolvedFunction + wp.data.dispatch('core/edit-post').removeEditorPanel(data.panelsToRemove[i]); + } + } + + if (typeof data['selectorsToHide'] !== 'undefined') { + jQuery(function () { + var styleTag = jQuery(''); + var css = ''; + for (var j = 0; j < data.selectorsToHide.length; j++) { + css = css + data.selectorsToHide[j] + ' { display: none !important; }' + "\n"; + } + styleTag.text(css).appendTo('head'); + }); + } + } +)(window['wsAmeGutenbergPanelData'] || {}); \ No newline at end of file diff --git a/extras/modules/metaboxes/load.php b/extras/modules/metaboxes/load.php new file mode 100644 index 0000000..0417829 --- /dev/null +++ b/extras/modules/metaboxes/load.php @@ -0,0 +1,12 @@ + +/// +/// +/// +var AmeMetaBoxEditor = /** @class */ (function () { + function AmeMetaBoxEditor(settings, forceRefreshUrl) { + var _this = this; + this.canAnyBoxesBeDeleted = false; + this.actorSelector = new AmeActorSelector(AmeActors, true); + //Wrap the selected actor in a computed observable so that it can be used with Knockout. + var _selectedActor = ko.observable(this.actorSelector.selectedActor + ? AmeActors.getActor(this.actorSelector.selectedActor) + : null); + this.selectedActor = ko.computed({ + read: function () { + return _selectedActor(); + }, + write: function (newActor) { + _this.actorSelector.setSelectedActor(newActor ? newActor.id : null); + } + }); + this.actorSelector.onChange(function (newSelectedActorId) { + if (newSelectedActorId === null) { + _selectedActor(null); + } + else { + _selectedActor(AmeActors.getActor(newSelectedActorId)); + } + }); + this.screens = ko.observableArray(AmeMetaBoxEditor._.map(settings.screens, function (screenData, id) { + var metaBoxes = screenData['metaBoxes:']; + if (AmeMetaBoxEditor._.isEmpty(metaBoxes)) { + metaBoxes = {}; + } + if (screenData['postTypeFeatures:'] && !AmeMetaBoxEditor._.isEmpty(screenData['postTypeFeatures:'])) { + var features = screenData['postTypeFeatures:']; + for (var featureName in features) { + if (features.hasOwnProperty(featureName)) { + metaBoxes['cpt-feature:' + featureName] = features[featureName]; + } + } + } + return new AmeMetaBoxCollection(id, metaBoxes, screenData['isContentTypeMissing:'], _this); + })); + this.screens.sort(function (a, b) { + return a.formattedTitle.localeCompare(b.formattedTitle); + }); + this.canAnyBoxesBeDeleted = AmeMetaBoxEditor._.some(this.screens(), 'canAnyBeDeleted'); + this.settingsData = ko.observable(''); + this.forceRefreshUrl = forceRefreshUrl; + this.isSlugWarningEnabled = ko.observable(true); + } + //noinspection JSUnusedGlobalSymbols It's actually used in the KO template, but PhpStorm doesn't realise that. + AmeMetaBoxEditor.prototype.saveChanges = function () { + var settings = this.getCurrentSettings(); + //Set the hidden form fields. + this.settingsData(JSON.stringify(settings)); + //Submit the form. + return true; + }; + AmeMetaBoxEditor.prototype.getCurrentSettings = function () { + var collectionFormatName = 'Admin Menu Editor meta boxes', collectionFormatVersion = '1.0'; + var settings = { + format: { + name: collectionFormatName, + version: collectionFormatVersion + }, + screens: {}, + isInitialRefreshDone: true + }; + var _ = AmeMetaBoxEditor._; + _.forEach(this.screens(), function (collection) { + var thisScreenData = { + 'metaBoxes:': {}, + 'postTypeFeatures:': {}, + 'isContentTypeMissing:': collection.isContentTypeMissing + }; + _.forEach(collection.boxes(), function (metaBox) { + var key = metaBox.parentCollectionKey ? metaBox.parentCollectionKey : 'metaBoxes:'; + thisScreenData[key][metaBox.id] = metaBox.toPropertyMap(); + }); + settings.screens[collection.screenId] = thisScreenData; + }); + return settings; + }; + //noinspection JSUnusedGlobalSymbols It's used in the KO template. + AmeMetaBoxEditor.prototype.promptForRefresh = function () { + if (confirm('Refresh the list of available meta boxes?\n\nWarning: Unsaved changes will be lost.')) { + window.location.href = this.forceRefreshUrl; + } + }; + AmeMetaBoxEditor.prototype.deleteScreen = function (screen) { + if (!screen.isContentTypeMissing) { + alert('That screen may still exist; it cannot be deleted.'); + return; + } + this.screens.remove(screen); + }; + AmeMetaBoxEditor._ = wsAmeLodash; + return AmeMetaBoxEditor; +}()); +var AmeMetaBox = /** @class */ (function () { + function AmeMetaBox(settings, metaBoxEditor) { + var _this = this; + this.isHiddenByDefault = false; + this.canBeDeleted = false; + this.isVirtual = false; + this.tooltipText = null; + AmeMetaBox.counter++; + this.uniqueHtmlId = 'ame-mb-item-' + AmeMetaBox.counter; + var _ = AmeMetaBox._; + this.metaBoxEditor = metaBoxEditor; + this.initialProperties = settings; + if (settings['parentCollectionKey']) { + this.parentCollectionKey = settings['parentCollectionKey']; + } + this.id = settings['id']; + this.title = _.get(settings, 'title', '[Untitled widget]'); + this.context = _.get(settings, 'context', 'normal'); + this.isHiddenByDefault = _.get(settings, 'isHiddenByDefault', false); + this.grantAccess = new AmeActorAccessDictionary(_.get(settings, 'grantAccess', {})); + this.defaultVisibility = new AmeActorAccessDictionary(_.get(settings, 'defaultVisibility', {})); + this.canBeDeleted = !_.get(settings, 'isPresent', true); + this.isVirtual = _.get(settings, 'isVirtual', false); + if (this.isVirtual) { + this.tooltipText = 'Technically, this is not a meta box, but it\'s included here for convenience.'; + } + this.isAvailable = ko.computed({ + read: function () { + var actor = metaBoxEditor.selectedActor(); + if (actor !== null) { + return AmeMetaBox.actorHasAccess(actor, _this.grantAccess, true, true); + } + else { + //Check if any actors have this widget enabled. + //We only care about visible actors. There might be some users that are loaded but not visible. + var actors = metaBoxEditor.actorSelector.getVisibleActors(); + return _.some(actors, function (anActor) { + return AmeMetaBox.actorHasAccess(anActor, _this.grantAccess, true, true); + }); + } + }, + write: function (checked) { + if ((_this.id === 'slugdiv') && !checked && _this.metaBoxEditor.isSlugWarningEnabled()) { + var warningMessage = 'Hiding the "Slug" metabox can prevent the user from changing the post slug.\n' + + 'This is caused by a known bug in WordPress core.\n' + + 'Do you want to hide this metabox anyway?'; + if (confirm(warningMessage)) { + //Suppress the warning. + _this.metaBoxEditor.isSlugWarningEnabled(false); + } + else { + _this.isAvailable.notifySubscribers(); + return; + } + } + var actor = metaBoxEditor.selectedActor(); + if (actor !== null) { + _this.grantAccess.set(actor.getId(), checked); + } + else { + //Enable/disable all. + _.forEach(metaBoxEditor.actorSelector.getVisibleActors(), function (anActor) { _this.grantAccess.set(anActor.getId(), checked); }); + } + } + }); + this.isVisibleByDefault = ko.computed({ + read: function () { + var actor = metaBoxEditor.selectedActor(); + if (actor !== null) { + return AmeMetaBox.actorHasAccess(actor, _this.defaultVisibility, !_this.isHiddenByDefault, null); + } + else { + var actors = metaBoxEditor.actorSelector.getVisibleActors(); + return _.some(actors, function (anActor) { + return AmeMetaBox.actorHasAccess(anActor, _this.defaultVisibility, !_this.isHiddenByDefault, null); + }); + } + }, + write: function (checked) { + var actor = metaBoxEditor.selectedActor(); + if (actor !== null) { + _this.defaultVisibility.set(actor.getId(), checked); + } + else { + //Enable/disable all. + _.forEach(metaBoxEditor.actorSelector.getVisibleActors(), function (anActor) { _this.defaultVisibility.set(anActor.getId(), checked); }); + } + } + }); + this.canChangeDefaultVisibility = ko.computed(function () { + return _this.isAvailable() && !_this.isVirtual; + }); + this.safeTitle = ko.computed(function () { + return AmeMetaBox.stripAllTags(_this.title); + }); + } + AmeMetaBox.actorHasAccess = function (actor, grants, roleDefault, superAdminDefault) { + if (roleDefault === void 0) { roleDefault = true; } + if (superAdminDefault === void 0) { superAdminDefault = true; } + //Is there a setting for this actor specifically? + var hasAccess = grants.get(actor.getId(), null); + if (hasAccess !== null) { + return hasAccess; + } + if (actor instanceof AmeUser) { + //The Super Admin has access to everything by default, and it takes priority over roles. + if (actor.isSuperAdmin) { + var adminHasAccess = grants.get('special:super_admin', null); + if (adminHasAccess !== null) { + return adminHasAccess; + } + else if (superAdminDefault !== null) { + return superAdminDefault; + } + } + //Allow access if at least one role has access. + var result = false; + for (var index = 0; index < actor.roles.length; index++) { + var roleActor = 'role:' + actor.roles[index], roleHasAccess = grants.get(roleActor, roleDefault); + result = result || roleHasAccess; + } + return result; + } + return roleDefault; + }; + AmeMetaBox.prototype.toPropertyMap = function () { + var properties = { + 'id': this.id, + 'title': this.title, + 'context': this.context, + 'grantAccess': this.grantAccess.getAll(), + 'defaultVisibility': this.defaultVisibility.getAll(), + 'isHiddenByDefault': this.isHiddenByDefault + }; + //Preserve unused properties on round-trip. + properties = AmeMetaBox._.merge({}, this.initialProperties, properties); + return properties; + }; + AmeMetaBox.stripAllTags = function (input) { + //Based on: http://phpjs.org/functions/strip_tags/ + var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, commentsAndPhpTags = /|<\?(?:php)?[\s\S]*?\?>/gi; + return input.replace(commentsAndPhpTags, '').replace(tags, ''); + }; + AmeMetaBox._ = wsAmeLodash; + AmeMetaBox.counter = 0; + return AmeMetaBox; +}()); +var AmeActorAccessDictionary = /** @class */ (function () { + function AmeActorAccessDictionary(initialData) { + this.items = {}; + this.numberOfObservables = ko.observable(0); + if (initialData) { + this.setAll(initialData); + } + } + AmeActorAccessDictionary.prototype.get = function (actor, defaultValue) { + if (defaultValue === void 0) { defaultValue = null; } + if (this.items.hasOwnProperty(actor)) { + return this.items[actor](); + } + this.numberOfObservables(); //Establish a dependency. + return defaultValue; + }; + AmeActorAccessDictionary.prototype.set = function (actor, value) { + if (!this.items.hasOwnProperty(actor)) { + this.items[actor] = ko.observable(value); + this.numberOfObservables(this.numberOfObservables() + 1); + } + else { + this.items[actor](value); + } + }; + AmeActorAccessDictionary.prototype.getAll = function () { + var result = {}; + for (var actorId in this.items) { + if (this.items.hasOwnProperty(actorId)) { + result[actorId] = this.items[actorId](); + } + } + return result; + }; + AmeActorAccessDictionary.prototype.setAll = function (values) { + for (var actorId in values) { + if (values.hasOwnProperty(actorId)) { + this.set(actorId, values[actorId]); + } + } + }; + return AmeActorAccessDictionary; +}()); +var AmeMetaBoxCollection = /** @class */ (function () { + function AmeMetaBoxCollection(screenId, metaBoxes, isContentTypeMissing, metaBoxEditor) { + this.canAnyBeDeleted = false; + this.isContentTypeMissing = false; + this.screenId = screenId; + this.formattedTitle = screenId.charAt(0).toUpperCase() + screenId.slice(1); + this.isContentTypeMissing = isContentTypeMissing; + this.boxes = ko.observableArray(AmeMetaBoxCollection._.map(metaBoxes, function (properties) { + return new AmeMetaBox(properties, metaBoxEditor); + })); + this.boxes.sort(function (a, b) { + return a.id.localeCompare(b.id); + }); + this.canAnyBeDeleted = AmeMetaBoxCollection._.some(this.boxes(), 'canBeDeleted'); + } + //noinspection JSUnusedGlobalSymbols Use by KO. + AmeMetaBoxCollection.prototype.deleteBox = function (item) { + this.boxes.remove(item); + }; + AmeMetaBoxCollection._ = wsAmeLodash; + return AmeMetaBoxCollection; +}()); +jQuery(function () { + var metaBoxEditor = new AmeMetaBoxEditor(wsAmeMetaBoxEditorData.settings, wsAmeMetaBoxEditorData.refreshUrl); + ko.applyBindings(metaBoxEditor, document.getElementById('ame-meta-box-editor')); + //Make the column widths the same in all tables. + var $ = jQuery; + var tables = $('.ame-meta-box-list'), columnCount = tables.find('thead').first().find('th').length, maxWidths = wsAmeLodash.fill(Array(columnCount), 0); + tables.find('tr').each(function () { + $(this).find('td,th').each(function (index) { + var width = $(this).width(); + if (maxWidths[index]) { + maxWidths[index] = Math.max(width, maxWidths[index]); + } + else { + maxWidths[index] = width; + } + }); + }); + tables.each(function () { + $(this).find('thead th').each(function (index) { + $(this).width(maxWidths[index]); + }); + }); + //Set up tooltips. + if ($['qtip']) { + $('#ame-meta-box-editor .ws_tooltip_trigger').qtip({ + style: { + classes: 'qtip qtip-rounded ws_tooltip_node' + } + }); + } +}); +//# sourceMappingURL=metabox-editor.js.map \ No newline at end of file diff --git a/extras/modules/metaboxes/metabox-editor.js.map b/extras/modules/metaboxes/metabox-editor.js.map new file mode 100644 index 0000000..e4f77a2 --- /dev/null +++ b/extras/modules/metaboxes/metabox-editor.js.map @@ -0,0 +1 @@ +{"version":3,"file":"metabox-editor.js","sourceRoot":"","sources":["metabox-editor.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/extras/modules/metaboxes/metabox-editor.scss b/extras/modules/metaboxes/metabox-editor.scss new file mode 100644 index 0000000..a17c5ff --- /dev/null +++ b/extras/modules/metaboxes/metabox-editor.scss @@ -0,0 +1,91 @@ +@import "../../../css/boxes"; + +#ame-meta-box-editor { + .ame-mb-check-column { + padding: 11px 0 0 3px; + + width: 1.8em; + vertical-align: top; + } + + .ame-meta-box-list { + width: auto; + + td label { + vertical-align: top; + } + } + + .ame-mb-default-visibility-column { + text-align: center; + } + +} + +#ws_actor_selector_container { + margin-right: 130px; +} + +@mixin wp-generic-box { + background: white; + + border: 1px solid $amePostboxBorderColor; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); + + padding: 10px 8px; +} + +#ame-mb-screen-list { + display: inline-block; + @include wp-generic-box; + + //background: #f5f5f5; //Light grey alternative. + padding-top: 0; + + .ame-mb-delete-section { + text-decoration: none; + + &:not(:hover) { + color: #888; + } + + &:hover, &:active { + color: #b32d2e; + } + } +} + +#ame-mb-action-container { + display: inline-block; + vertical-align: top; +} + +#ame-mb-main-actions { + @include wp-generic-box; + + box-sizing: border-box; + + width: 150px; + margin-left: 10px; + margin-top: 0; + + input.button-primary { + margin-bottom: 20px; + } + + input.button { + width: 100%; + } +} + +/* Tooltips */ +#ame-meta-box-editor .ame-meta-box-list { + .ws_tooltip_trigger { + visibility: hidden; + color: silver; + } + + tr:hover .ws_tooltip_trigger { + visibility: visible; + } +} \ No newline at end of file diff --git a/extras/modules/metaboxes/metabox-editor.ts b/extras/modules/metaboxes/metabox-editor.ts new file mode 100644 index 0000000..c5c515a --- /dev/null +++ b/extras/modules/metaboxes/metabox-editor.ts @@ -0,0 +1,471 @@ +/// +/// +/// +/// + +declare const wsAmeMetaBoxEditorData; + +interface MetaBoxEditorSettings { + format: { + name: string, + version: string + }, + screens: {[id: string] : ScreenSettingsData}; + isInitialRefreshDone: boolean; +} + +interface ScreenSettingsData { + 'metaBoxes:': MetaBoxPropertyMap; + 'postTypeFeatures:': any; + 'isContentTypeMissing:': boolean; +} + +class AmeMetaBoxEditor { + private static _ = wsAmeLodash; + + screens: KnockoutObservableArray; + + actorSelector: AmeActorSelector; + selectedActor: KnockoutComputed; + + settingsData: KnockoutObservable; + canAnyBoxesBeDeleted: boolean = false; + + isSlugWarningEnabled: KnockoutObservable; + + private readonly forceRefreshUrl: string; + + constructor(settings: MetaBoxEditorSettings, forceRefreshUrl: string) { + this.actorSelector = new AmeActorSelector(AmeActors, true); + + //Wrap the selected actor in a computed observable so that it can be used with Knockout. + let _selectedActor = ko.observable( + this.actorSelector.selectedActor + ? AmeActors.getActor(this.actorSelector.selectedActor) + : null + ); + this.selectedActor = ko.computed({ + read: function () { + return _selectedActor(); + }, + write: (newActor: AmeBaseActor) => { + this.actorSelector.setSelectedActor(newActor ? newActor.id : null); + } + }); + this.actorSelector.onChange((newSelectedActorId: string|null) => { + if (newSelectedActorId === null) { + _selectedActor(null); + } else { + _selectedActor(AmeActors.getActor(newSelectedActorId)); + } + }); + + this.screens = ko.observableArray(AmeMetaBoxEditor._.map( + settings.screens, + (screenData, id) => { + let metaBoxes = screenData['metaBoxes:']; + if (AmeMetaBoxEditor._.isEmpty(metaBoxes)) { + metaBoxes = {}; + } + + if (screenData['postTypeFeatures:'] && !AmeMetaBoxEditor._.isEmpty(screenData['postTypeFeatures:'])) { + const features = screenData['postTypeFeatures:']; + for (let featureName in features) { + if (features.hasOwnProperty(featureName)) { + metaBoxes['cpt-feature:' + featureName] = features[featureName]; + } + } + } + + return new AmeMetaBoxCollection( + id, + metaBoxes, + screenData['isContentTypeMissing:'], + this + ); + }) + ); + this.screens.sort(function(a, b) { + return a.formattedTitle.localeCompare(b.formattedTitle); + }); + + this.canAnyBoxesBeDeleted = AmeMetaBoxEditor._.some(this.screens(), 'canAnyBeDeleted'); + + this.settingsData = ko.observable(''); + this.forceRefreshUrl = forceRefreshUrl; + this.isSlugWarningEnabled = ko.observable(true); + } + + //noinspection JSUnusedGlobalSymbols It's actually used in the KO template, but PhpStorm doesn't realise that. + saveChanges() { + let settings = this.getCurrentSettings(); + + //Set the hidden form fields. + this.settingsData(JSON.stringify(settings)); + + //Submit the form. + return true; + } + + protected getCurrentSettings(): MetaBoxEditorSettings { + const collectionFormatName = 'Admin Menu Editor meta boxes', + collectionFormatVersion = '1.0'; + + let settings: MetaBoxEditorSettings = { + format: { + name: collectionFormatName, + version: collectionFormatVersion + }, + screens: {}, + isInitialRefreshDone: true + }; + + const _ = AmeMetaBoxEditor._; + _.forEach(this.screens(), function (collection) { + let thisScreenData = { + 'metaBoxes:' : {}, + 'postTypeFeatures:' : {}, + 'isContentTypeMissing:' : collection.isContentTypeMissing + }; + _.forEach(collection.boxes(), function(metaBox) { + let key = metaBox.parentCollectionKey ? metaBox.parentCollectionKey : 'metaBoxes:'; + thisScreenData[key][metaBox.id] = metaBox.toPropertyMap(); + }); + settings.screens[collection.screenId] = thisScreenData; + }); + + return settings; + } + + //noinspection JSUnusedGlobalSymbols It's used in the KO template. + promptForRefresh() { + if (confirm('Refresh the list of available meta boxes?\n\nWarning: Unsaved changes will be lost.')) { + window.location.href = this.forceRefreshUrl; + } + } + + deleteScreen(screen: AmeMetaBoxCollection) { + if (!screen.isContentTypeMissing) { + alert('That screen may still exist; it cannot be deleted.'); + return; + } + this.screens.remove(screen); + } +} + +class AmeMetaBox { + private static _ = wsAmeLodash; + protected static counter = 0; + uniqueHtmlId: string; + + id: string; + title: string; + context: string; + safeTitle: KnockoutComputed; + parentCollectionKey?: string; + + isAvailable: KnockoutComputed; + grantAccess: AmeActorAccessDictionary; + + isVisibleByDefault: KnockoutComputed; + defaultVisibility: AmeActorAccessDictionary; + isHiddenByDefault: boolean = false; + canChangeDefaultVisibility: KnockoutComputed; + + canBeDeleted: boolean = false; + isVirtual: boolean = false; + tooltipText: string = null; + + private readonly initialProperties: MetaBoxPropertyMap; + protected metaBoxEditor: AmeMetaBoxEditor; + + constructor(settings: MetaBoxPropertyMap, metaBoxEditor: AmeMetaBoxEditor) { + AmeMetaBox.counter++; + this.uniqueHtmlId = 'ame-mb-item-' + AmeMetaBox.counter; + + const _ = AmeMetaBox._; + this.metaBoxEditor = metaBoxEditor; + this.initialProperties = settings; + + if (settings['parentCollectionKey']) { + this.parentCollectionKey = settings['parentCollectionKey']; + } + + this.id = settings['id']; + this.title = _.get(settings, 'title', '[Untitled widget]'); + this.context = _.get(settings, 'context', 'normal'); + this.isHiddenByDefault = _.get(settings, 'isHiddenByDefault', false); + + this.grantAccess = new AmeActorAccessDictionary(_.get(settings, 'grantAccess', {})); + this.defaultVisibility = new AmeActorAccessDictionary(_.get(settings, 'defaultVisibility', {})); + + this.canBeDeleted = !_.get(settings, 'isPresent', true); + + this.isVirtual = _.get(settings, 'isVirtual', false); + if (this.isVirtual) { + this.tooltipText = 'Technically, this is not a meta box, but it\'s included here for convenience.'; + } + + this.isAvailable = ko.computed({ + read: () => { + const actor = metaBoxEditor.selectedActor(); + if (actor !== null) { + return AmeMetaBox.actorHasAccess(actor, this.grantAccess, true, true); + } else { + //Check if any actors have this widget enabled. + //We only care about visible actors. There might be some users that are loaded but not visible. + const actors = metaBoxEditor.actorSelector.getVisibleActors(); + return _.some(actors, (anActor) => { + return AmeMetaBox.actorHasAccess(anActor, this.grantAccess, true, true); + }); + } + }, + write: (checked: boolean) => { + if ((this.id === 'slugdiv') && !checked && this.metaBoxEditor.isSlugWarningEnabled()) { + const warningMessage = + 'Hiding the "Slug" metabox can prevent the user from changing the post slug.\n' + + 'This is caused by a known bug in WordPress core.\n' + + 'Do you want to hide this metabox anyway?'; + if (confirm(warningMessage)) { + //Suppress the warning. + this.metaBoxEditor.isSlugWarningEnabled(false); + } else { + this.isAvailable.notifySubscribers(); + return; + } + } + + const actor = metaBoxEditor.selectedActor(); + if (actor !== null) { + this.grantAccess.set(actor.getId(), checked); + } else { + //Enable/disable all. + _.forEach( + metaBoxEditor.actorSelector.getVisibleActors(), + (anActor) => { this.grantAccess.set(anActor.getId(), checked); } + ); + } + } + }); + + this.isVisibleByDefault = ko.computed({ + read: () => { + const actor = metaBoxEditor.selectedActor(); + if (actor !== null) { + return AmeMetaBox.actorHasAccess(actor, this.defaultVisibility, !this.isHiddenByDefault, null); + } else { + const actors = metaBoxEditor.actorSelector.getVisibleActors(); + return _.some(actors, (anActor) => { + return AmeMetaBox.actorHasAccess(anActor, this.defaultVisibility, !this.isHiddenByDefault, null); + }); + } + }, + write: (checked) => { + const actor = metaBoxEditor.selectedActor(); + if (actor !== null) { + this.defaultVisibility.set(actor.getId(), checked); + } else { + //Enable/disable all. + _.forEach( + metaBoxEditor.actorSelector.getVisibleActors(), + (anActor) => { this.defaultVisibility.set(anActor.getId(), checked); } + ); + } + } + }); + + this.canChangeDefaultVisibility = ko.computed(() => { + return this.isAvailable() && !this.isVirtual; + }); + + this.safeTitle = ko.computed(() => { + return AmeMetaBox.stripAllTags(this.title); + }); + } + + private static actorHasAccess( + actor: IAmeActor, + grants: AmeActorAccessDictionary, + roleDefault: boolean = true, + superAdminDefault: boolean | null = true + ) { + //Is there a setting for this actor specifically? + let hasAccess = grants.get(actor.getId(), null); + if (hasAccess !== null) { + return hasAccess; + } + + if (actor instanceof AmeUser) { + //The Super Admin has access to everything by default, and it takes priority over roles. + if (actor.isSuperAdmin) { + const adminHasAccess = grants.get('special:super_admin', null); + if (adminHasAccess !== null) { + return adminHasAccess; + } else if (superAdminDefault !== null) { + return superAdminDefault; + } + } + + //Allow access if at least one role has access. + let result = false; + for (let index = 0; index < actor.roles.length; index++) { + let roleActor = 'role:' + actor.roles[index], + roleHasAccess = grants.get(roleActor, roleDefault); + result = result || roleHasAccess; + } + return result; + } + + return roleDefault; + } + + toPropertyMap() { + let properties = { + 'id': this.id, + 'title': this.title, + 'context': this.context, + 'grantAccess': this.grantAccess.getAll(), + + 'defaultVisibility': this.defaultVisibility.getAll(), + 'isHiddenByDefault': this.isHiddenByDefault + }; + + //Preserve unused properties on round-trip. + properties = AmeMetaBox._.merge({}, this.initialProperties, properties); + + return properties; + } + + private static stripAllTags(input: string) { + //Based on: http://phpjs.org/functions/strip_tags/ + const tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, + commentsAndPhpTags = /|<\?(?:php)?[\s\S]*?\?>/gi; + return input.replace(commentsAndPhpTags, '').replace(tags, ''); + } +} + +interface MetaBoxPropertyMap { + [name: string] : any; +} + +class AmeActorAccessDictionary { + items: { [actorId: string] : KnockoutObservable; } = {}; + private readonly numberOfObservables: KnockoutObservable; + + constructor(initialData?: AmeDictionary) { + this.numberOfObservables = ko.observable(0); + if (initialData) { + this.setAll(initialData); + } + } + + get(actor: string, defaultValue = null): boolean { + if (this.items.hasOwnProperty(actor)) { + return this.items[actor](); + } + this.numberOfObservables(); //Establish a dependency. + return defaultValue; + } + + set(actor: string, value: boolean) { + if (!this.items.hasOwnProperty(actor)) { + this.items[actor] = ko.observable(value); + this.numberOfObservables(this.numberOfObservables() + 1); + } else { + this.items[actor](value); + } + } + + getAll(): AmeDictionary { + let result: AmeDictionary = {}; + for (let actorId in this.items) { + if (this.items.hasOwnProperty(actorId)) { + result[actorId] = this.items[actorId](); + } + } + return result; + } + + setAll(values: AmeDictionary) { + for (let actorId in values) { + if (values.hasOwnProperty(actorId)) { + this.set(actorId, values[actorId]); + } + } + } +} + +class AmeMetaBoxCollection { + private static _ = wsAmeLodash; + + screenId: string; + formattedTitle: string; + boxes: KnockoutObservableArray; + + canAnyBeDeleted: boolean = false; + isContentTypeMissing: boolean = false; + + constructor( + screenId: string, + metaBoxes: {[id: string]: MetaBoxPropertyMap}, + isContentTypeMissing: boolean, + metaBoxEditor: AmeMetaBoxEditor + ) { + this.screenId = screenId; + this.formattedTitle = screenId.charAt(0).toUpperCase() + screenId.slice(1); + this.isContentTypeMissing = isContentTypeMissing; + + this.boxes = ko.observableArray(AmeMetaBoxCollection._.map(metaBoxes, function(properties) { + return new AmeMetaBox(properties, metaBoxEditor); + })); + + this.boxes.sort(function(a, b) { + return a.id.localeCompare(b.id); + }); + + this.canAnyBeDeleted = AmeMetaBoxCollection._.some(this.boxes(), 'canBeDeleted'); + } + + //noinspection JSUnusedGlobalSymbols Use by KO. + deleteBox(item) { + this.boxes.remove(item); + } +} + + +jQuery(function() { + let metaBoxEditor = new AmeMetaBoxEditor(wsAmeMetaBoxEditorData.settings, wsAmeMetaBoxEditorData.refreshUrl); + ko.applyBindings(metaBoxEditor, document.getElementById('ame-meta-box-editor')); + + //Make the column widths the same in all tables. + const $ = jQuery; + let tables = $('.ame-meta-box-list'), + columnCount = tables.find('thead').first().find('th').length, + maxWidths = wsAmeLodash.fill(Array(columnCount), 0); + + tables.find('tr').each(function() { + $(this).find('td,th').each(function(index) { + const width = $(this).width(); + if (maxWidths[index]) { + maxWidths[index] = Math.max(width, maxWidths[index]); + } else { + maxWidths[index] = width; + } + }) + }); + + tables.each(function() { + $(this).find('thead th').each(function(index) { + $(this).width(maxWidths[index]); + }); + }); + + //Set up tooltips. + if ($['qtip']) { + $('#ame-meta-box-editor .ws_tooltip_trigger').qtip({ + style: { + classes: 'qtip qtip-rounded ws_tooltip_node' + } + }); + } +}); \ No newline at end of file diff --git a/extras/modules/metaboxes/metaboxes-template.php b/extras/modules/metaboxes/metaboxes-template.php new file mode 100644 index 0000000..0ba75b5 --- /dev/null +++ b/extras/modules/metaboxes/metaboxes-template.php @@ -0,0 +1,93 @@ + '1'), $moduleTabUrl); +?> + + \ No newline at end of file diff --git a/extras/modules/metaboxes/refresh-meta-boxes.js b/extras/modules/metaboxes/refresh-meta-boxes.js new file mode 100644 index 0000000..9921ac6 --- /dev/null +++ b/extras/modules/metaboxes/refresh-meta-boxes.js @@ -0,0 +1,38 @@ +/*global wsMetaBoxRefresherData */ + +jQuery(function($) { + //Load all of the pages that could have meta boxes in hidden frames. + var pages = wsMetaBoxRefresherData['pagesWithMetaBoxes'], + frame = null, + loadedPages = 0, + totalPages = pages.length, + $progressBar = $('#ame-mb-refresh-progress'); + + $progressBar.prop('max', totalPages); + + for (var i = 0; i < totalPages; i++) { + frame = $(''); + + frame.on('load', function() { + loadedPages++; + $progressBar.prop('value', loadedPages); + //console.log(loadedPages + ' of ' + totalPages); + + //When done, redirect back to the widget editor. + if (loadedPages >= totalPages) { + //console.log('Done'); + window.location.href = wsMetaBoxRefresherData['editorUrl']; + } + }); + + frame.attr({ + 'src': pages[i], + 'width': 1, + 'height': 1 + }); + frame.css('visibility', 'hidden'); + + frame.appendTo('#wpwrap'); + } + +}); diff --git a/extras/modules/plugin-visibility/plugin-visibility.php b/extras/modules/plugin-visibility/plugin-visibility.php new file mode 100644 index 0000000..d9e2f53 --- /dev/null +++ b/extras/modules/plugin-visibility/plugin-visibility.php @@ -0,0 +1,45 @@ +moduleDir = dirname($reflector->getFileName()); + $this->moduleId = basename($this->moduleDir); + } + } + + public function exportSettings() { + $settings = $this->loadSettings(); + if ( empty($settings['plugins']) && empty($settings['grantAccessByDefault']) ) { + return null; + } + return $settings; + } + + public function importSettings($newSettings) { + if ( !is_array($newSettings) || empty($newSettings) ) { + return; + } + + $this->loadSettings(); + $this->settings = array_merge($this->settings, $newSettings); + $this->saveSettings(); + } + + /** + * @return string + */ + public function getExportOptionLabel() { + return 'Plugin visibility'; + } + + public function getExportOptionDescription() { + return ''; + } +} \ No newline at end of file diff --git a/extras/modules/role-editor/ameRexCapability.php b/extras/modules/role-editor/ameRexCapability.php new file mode 100644 index 0000000..6d34034 --- /dev/null +++ b/extras/modules/role-editor/ameRexCapability.php @@ -0,0 +1,103 @@ +usedByComponents[$componentId] = true; + if ( $componentContext instanceof ameRexComponentCapabilityInfo ) { + $this->componentContext[$componentId] = $componentContext; + } + } + + /** + * @param ameRexComponentCapabilityInfo[] $components + */ + public function addManyUsages($components) { + foreach ($components as $componentId => $info) { + $this->usedByComponents[$componentId] = true; + if ( $info instanceof ameRexComponentCapabilityInfo ) { + $this->componentContext[$componentId] = $info; + } + } + } + + /** + * Whether a offset exists + * + * @link https://php.net/manual/en/arrayaccess.offsetexists.php + * @param mixed $offset

+ * An offset to check for. + *

+ * @return boolean true on success or false on failure. + *

+ *

+ * The return value will be casted to boolean if non-boolean was returned. + * @since 5.0.0 + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) { + return property_exists($this, $offset); + } + + /** + * Offset to retrieve + * + * @link https://php.net/manual/en/arrayaccess.offsetget.php + * @param mixed $offset

+ * The offset to retrieve. + *

+ * @return mixed Can return all value types. + * @since 5.0.0 + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) { + return $this->$offset; + } + + /** + * Offset to set + * + * @link https://php.net/manual/en/arrayaccess.offsetset.php + * @param mixed $offset

+ * The offset to assign the value to. + *

+ * @param mixed $value

+ * The value to set. + *

+ * @return void + * @since 5.0.0 + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) { + $this->$offset = $value; + } + + /** + * Offset to unset + * + * @link https://php.net/manual/en/arrayaccess.offsetunset.php + * @param mixed $offset

+ * The offset to unset. + *

+ * @return void + * @since 5.0.0 + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) { + $this->$offset = null; + } +} \ No newline at end of file diff --git a/extras/modules/role-editor/ameRexCapabilityDataSource.php b/extras/modules/role-editor/ameRexCapabilityDataSource.php new file mode 100644 index 0000000..714e3b5 --- /dev/null +++ b/extras/modules/role-editor/ameRexCapabilityDataSource.php @@ -0,0 +1,153 @@ +fileName = $fileName; + } + + /** + * @inheritDoc + */ + public function findCapabilities($capabilities, ameRexComponentRegistry $componentRegistry) { + $results = array(); + + /** @noinspection PhpComposerExtensionStubsInspection */ + $meta = json_decode(file_get_contents($this->fileName), true); + + foreach ($capabilities as $capability) { + if ( !isset($meta['capabilities'][$capability]) ) { + continue; + } + + $capDetails = $meta['capabilities'][$capability]; + if ( isset($capDetails['origins']) ) { + $relatedComponents = $capDetails['origins']; + } else { + $relatedComponents = $capDetails; + } + + $componentContext = array(); + foreach ($relatedComponents as $origin) { + $info = null; + if ( is_string($origin) ) { + $componentId = $origin; + } else { + $componentId = $origin['id']; + if ( count($origin) > 1 ) { + $info = ameRexComponentCapabilityInfo::fromArray($origin); + } + } + $componentContext[$componentId] = $info; + + $componentMeta = ameUtils::get($meta, 'components.' . $componentId, array()); + $componentRegistry->getOrCreate($componentId, $componentMeta); + } + + $results[$capability] = $componentContext; + } + + return $results; + } +} + +class ameRexSqliteDataSource extends ameRexCapabilityDataSource { + protected $fileName; + /** + * @var PDO|null + */ + protected $pdo = null; + + public function __construct($fileName) { + $this->fileName = $fileName; + } + + /** + * @inheritDoc + */ + public function findCapabilities($capabilities, ameRexComponentRegistry $componentRegistry) { + if ( !$this->connectToDb() ) { + return array(); + } + + $selectCap = $this->pdo->prepare(' + SELECT componentId, componentName, activeInstalls, notes, permissions, + documentationUrl, capabilityDocumentationUrl + FROM capabilityInfoView + WHERE capabilityName = :capability + '); + $selectCap->setFetchMode(PDO::FETCH_ASSOC); + + $results = array(); + foreach ($capabilities as $capability) { + $selectCap->execute(array(':capability' => $capability)); + $rows = $selectCap->fetchAll(); + $selectCap->closeCursor(); + + if ( empty($rows) ) { + continue; + } + + $componentContext = array(); + foreach ($rows as $capDetails) { + $info = null; + if ( !empty($capDetails['notes']) || !empty($capDetails['permissions']) || !empty($capDetails['documentationUrl']) ) { + if ( isset($capDetails['permissions']) ) { + //Assume one permission per line. + $capDetails['permissions'] = preg_split( + '@[\n\r]++@', + $capDetails['permissions'], + -1, + PREG_SPLIT_NO_EMPTY + ); + } + $info = ameRexComponentCapabilityInfo::fromArray($capDetails); + } + $componentContext[$capDetails['componentId']] = $info; + + $componentRegistry->updateComponent( + $capDetails['componentId'], + array( + 'name' => $capDetails['componentName'], + 'activeInstalls' => $capDetails['activeInstalls'], + 'capabilityDocumentationUrl' => $capDetails['capabilityDocumentationUrl'], + ) + ); + } + + $results[$capability] = $componentContext; + } + + return $results; + } + + protected function connectToDb() { + if ( $this->pdo ) { + return true; + } + if ( !in_array('sqlite', PDO::getAvailableDrivers()) ) { + return false; + } + + try { + $this->pdo = new PDO('sqlite:' . $this->fileName); + $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } catch (PDOException $ex) { + //If the user doesn't have a SQLite driver, we can't really do anything about it. + $this->pdo = null; + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/extras/modules/role-editor/ameRexCapabilityInfoSearch.php b/extras/modules/role-editor/ameRexCapabilityInfoSearch.php new file mode 100644 index 0000000..ffed19c --- /dev/null +++ b/extras/modules/role-editor/ameRexCapabilityInfoSearch.php @@ -0,0 +1,31 @@ +sources as $source) { + $matches = $source->findCapabilities($capabilities, $componentRegistry); + foreach($matches as $capabilityName => $components) { + foreach($components as $componentId => $capInfo) { + $dataset->addResult($capabilityName, $componentId, $capInfo); + } + } + } + return $dataset; + } + + public function addDataSource(ameRexCapabilityDataSource $source) { + $this->sources[] = $source; + } +} \ No newline at end of file diff --git a/extras/modules/role-editor/ameRexCapabilitySearchResultSet.php b/extras/modules/role-editor/ameRexCapabilitySearchResultSet.php new file mode 100644 index 0000000..4439326 --- /dev/null +++ b/extras/modules/role-editor/ameRexCapabilitySearchResultSet.php @@ -0,0 +1,129 @@ +results[$capabilityName]) ) { + $this->results[$capabilityName] = array(); + } + //Only the first result per component is used. + if ( isset($this->results[$capabilityName][$componentId]) ) { + return; + } + $this->results[$capabilityName][$componentId] = $info; + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) { + return array_key_exists($offset, $this->results); + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) { + return $this->results[$offset]; + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) { + $this->results[$offset] = $value; + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) { + unset($this->results[$offset]); + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function count() { + return count($this->results); + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function getIterator() { + return new ArrayIterator($this->results); + } +} + +class ameRexComponentCapabilityInfo implements ArrayAccess { + /** + * @var string[]|null + */ + public $permissions = null; + + /** + * @var string|null + */ + public $notes = null; + + /** + * @var string|null + */ + public $documentationUrl = null; + + public static function fromArray($properties) { + $instance = new self(); + if ( isset($properties['permissions']) ) { + $instance->permissions = $properties['permissions']; + } + if ( isset($properties['notes']) ) { + $instance->notes = $properties['notes']; + } + if ( isset($properties['documentationUrl']) ) { + $instance->documentationUrl = $properties['documentationUrl']; + } + return $instance; + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) { + return isset($this->$offset); + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) { + return $this->$offset; + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) { + $this->$offset = $value; + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) { + $this->$offset = null; + } +} + diff --git a/extras/modules/role-editor/ameRexCategory.php b/extras/modules/role-editor/ameRexCategory.php new file mode 100644 index 0000000..d0f4fe0 --- /dev/null +++ b/extras/modules/role-editor/ameRexCategory.php @@ -0,0 +1,142 @@ +name = $name; + $this->componentId = $componentId; + } + + /** + * Does this category contain a specific capability either directly or in a subcategory? + * + * @param string $capability + * @return boolean + */ + public function hasCapability($capability) { + if (isset($this->capabilities[$capability])) { + return true; + } + foreach ($this->subcategories as $subcategory) { + if ($subcategory->hasCapability($capability)) { + return true; + } + } + return false; + } + + /** + * @param ameRexCategory $category + */ + public function addSubcategory($category) { + if (!empty($category->slug)) { + $this->subcategories[$category->slug] = $category; + } else { + $this->subcategories[] = $category; + } + } + + /** + * @param string $capability + */ + public function addCapabilityToDefaultLocation($capability) { + //TODO: Handle the situation where the default is a "General" subcategory or something like that. + $this->capabilities[$capability] = true; + } + + public function toArray() { + $result = array( + 'name' => $this->name, + 'componentId' => $this->componentId, + 'capabilities' => array_keys($this->capabilities), + ); + if (!empty($this->subcategories)) { + $result['subcategories'] = array(); + foreach ($this->subcategories as $subcategory) { + $result['subcategories'][] = $subcategory->toArray(); + } + } + if (($this->slug !== '') && ($this->slug !== null)) { + $result['slug'] = $this->slug; + } + return $result; + } +} + +abstract class ameRexExtendedCategory extends ameRexCategory { + protected $variant = null; + protected $contentTypeId = null; + /** + * @var string[] + */ + public $permissions = array(); + + public function __construct($name = '', $componentId = null, $contentTypeId = null, $permissions = array()) { + parent::__construct($name, $componentId); + $this->permissions = $permissions; + $this->contentTypeId = $contentTypeId; + } + + public function hasCapability($capability) { + if (parent::hasCapability($capability)) { + return true; + } + + /* + * Check if the capability is used by one of the permissions. + * + * Implementation note: This code does not use in_array() because, in loose + * comparison mode, it will produce crazy results like treating "abc" and + * the boolean TRUE as equal. Unfortunately, strict mode also does not work + * here because some capabilities can be represented as either numbers or + * numeric strings. PHP converts numeric strings to numbers when WordPress + * uses them as array indexes. + * + * Instead, we use a standard loop with a strict comparison, plus a special + * case for numeric capabilities. + */ + + $isNeedleNumeric = is_numeric($capability); + $needleAsString = (string)$capability; + + foreach ($this->permissions as $capName) { + if ( + //Strict equality check first. + ($capName === $capability) + //Relaxed comparison for numbers. + || ($isNeedleNumeric && ($needleAsString === (string)$capName)) + ) { + return true; + } + } + + return false; + } + + public function toArray() { + $result = parent::toArray(); + $result['variant'] = $this->variant; + $result['permissions'] = $this->permissions; + $result['contentTypeId'] = $this->contentTypeId; + unset($result['capabilities']); + return $result; + } +} + + +class ameRexPostTypeCategory extends ameRexExtendedCategory { + protected $variant = 'post_type'; +} + +class ameRexTaxonomyCategory extends ameRexExtendedCategory { + protected $variant = 'taxonomy'; +} \ No newline at end of file diff --git a/extras/modules/role-editor/ameRexComponent.php b/extras/modules/role-editor/ameRexComponent.php new file mode 100644 index 0000000..473c216 --- /dev/null +++ b/extras/modules/role-editor/ameRexComponent.php @@ -0,0 +1,48 @@ +id = $id; + $this->name = ($name !== null) ? $name : $id; + } + + public function toArray() { + $result = array( + 'componentId' => $this->id, + 'name' => $this->name, + 'activeInstalls' => $this->activeInstalls, + 'isActive' => $this->isActive, + 'isInstalled' => $this->isInstalled, + ); + + if ($this->capabilityDocumentationUrl) { + $result['capabilityDocumentationUrl'] = $this->capabilityDocumentationUrl; + } + + return $result; + } +} \ No newline at end of file diff --git a/extras/modules/role-editor/ameRexComponentRegistry.php b/extras/modules/role-editor/ameRexComponentRegistry.php new file mode 100644 index 0000000..2ddbe42 --- /dev/null +++ b/extras/modules/role-editor/ameRexComponentRegistry.php @@ -0,0 +1,95 @@ +components[$component->id] = $component; + } + + public function get($componentId) { + if ( isset($this->components[$componentId]) ) { + return $this->components[$componentId]; + } + return null; + } + + public function getOrCreate($componentId, $properties = null) { + if ( isset($this->components[$componentId]) ) { + return $this->components[$componentId]; + } + if ( $properties !== null ) { + $component = new ameRexComponent($componentId, isset($properties['name']) ? $properties['name'] : null); + if ( isset($properties['activeInstalls']) ) { + $component->activeInstalls = $properties['activeInstalls']; + } + if ( isset($properties['capabilityDocumentationUrl']) ) { + $component->capabilityDocumentationUrl = $properties['capabilityDocumentationUrl']; + } + $this->register($component); + return $component; + } + return null; + } + + /** + * @param string $componentId + * @param array $properties + * @return ameRexComponent + */ + public function updateComponent($componentId, $properties) { + if ( isset($this->components[$componentId]) ) { + $component = $this->components[$componentId]; + } else { + $component = new ameRexComponent($componentId, isset($properties['name']) ? $properties['name'] : null); + $this->register($component); + } + if ( isset($properties['activeInstalls']) ) { + $component->activeInstalls = $properties['activeInstalls']; + } + if ( isset($properties['capabilityDocumentationUrl']) ) { + $component->capabilityDocumentationUrl = $properties['capabilityDocumentationUrl']; + } + return $component; + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function getIterator() { + return new ArrayIterator($this->components); + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) { + return array_key_exists($offset, $this->components); + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) { + return $this->components[$offset]; + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) { + $this->components[$offset] = $value; + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) { + unset($this->components[$offset]); + } +} \ No newline at end of file diff --git a/extras/modules/role-editor/ameRexSettingsValidator.php b/extras/modules/role-editor/ameRexSettingsValidator.php new file mode 100644 index 0000000..fabd47f --- /dev/null +++ b/extras/modules/role-editor/ameRexSettingsValidator.php @@ -0,0 +1,686 @@ +inputData = $jsonString; + $this->preExistingCapabilities = $allCapabilities; + $this->preExistingUserDefinedCaps = $userDefinedCaps; + $this->oldEditableRoleSettings = $oldEditableRoleSettings; + $this->effectiveEditableRoles = $effectiveEditableRoles; + $this->menuEditor = $menuEditor; + } + + public function validate() { + $this->errors = array(); + $data = json_decode($this->inputData, true); + + if ($data === null) { + $this->errors[] = new WP_Error( + 'ame_rex_invalid_json', + 'JSON parsing failed. Submitted settings are probably invalid or corrupted.' + ); + return $this->errors; + } + + if (!is_array($data)) { + $this->errors[] = new WP_Error( + 'ame_rex_unexpected_data_type', + sprintf('JSON parsing failed. Expected type: associative array, actual type: %s.', gettype($data)) + ); + return $this->errors; + } + + $submittedRoles = array(); + foreach ($data['roles'] as $tempRole) { + $submittedRoles[$tempRole['name']] = $tempRole; + } + + $wpRoles = ameRoleUtils::get_roles();; + $existingRoles = (isset($wpRoles->roles) && is_array($wpRoles->roles)) ? $wpRoles->roles : array(); + + $knownRoleIDs = array_fill_keys( + array_merge(array_keys($existingRoles), array_keys($submittedRoles)), + true + ); + + $editableRoles = $this->effectiveEditableRoles; + + //For validation purposes, we also need existing capabilities. + $existingCapabilities = $this->preExistingCapabilities; + + //Remove all roles that don't exist in the role list. + $rolesToDelete = array_diff_key($editableRoles, $submittedRoles); + //Don't delete the default role. The user should set a different default role first. + $defaultRole = get_option('default_role'); + if (isset($rolesToDelete[$defaultRole])) { + unset($rolesToDelete[$defaultRole]); + $this->errors[] = new WP_Error( + 'ame_rex_cannot_delete_default_role', + 'You cannot delete the default role. Set a different default role first.' + ); + } + //Don't delete roles that are currently assigned to one or more users. This check may be slow. + if (count($rolesToDelete) > 0) { + $usersByRole = count_users(); + if (isset($usersByRole['avail_roles'])) { + foreach ($usersByRole['avail_roles'] as $id => $totalUsers) { + if (($totalUsers > 0) && isset($rolesToDelete[$id])) { + unset($rolesToDelete[$id]); + $this->errors[] = new WP_Error( + 'ame_rex_deleted_role_has_users', + sprintf( + 'Role "%s" cannot be deleted because there are still %d users with that role.', + $id, + $totalUsers + ) + ); + } + } + } + } + + $rolesToCreate = array(); + $rolesToModify = array(); + + //Validate all new or modified properties. + foreach ($submittedRoles as $id => $role) { + $isNewRole = !isset($existingRoles[$id]); + $isModifiedRole = false; + + //Only modify existing roles if they're editable. + if (!$isNewRole && (!isset($editableRoles[$id]) || !isset($wpRoles->role_objects[$id]))) { + $this->errors[] = new WP_Error( + 'ame_rex_role_not_editable', + sprintf('You don\'t have permission to edit role "%s"', $id) + ); + continue; + } + + //Validate the role ID (internal name). + if ($isNewRole) { + $state = $this->validateRoleName($id, $existingRoles, $existingCapabilities); + if (is_wp_error($state)) { + $this->errors[] = $state; + continue; + } + } + + //Validate the display name. + if ($isNewRole || ($editableRoles[$id]['name'] !== $role['displayName'])) { + $state = $this->validateRoleDisplayName($role['displayName']); + if (is_wp_error($state)) { + $this->errors[] = $state; + continue; + } + $isModifiedRole = $isModifiedRole || !$isNewRole; + } + + //Validate capabilities. + $capErrors = $this->validateCapabilityAssignment( + $role['capabilities'], + $existingCapabilities, + $knownRoleIDs + ); + if (!empty($capErrors)) { + $this->errors = array_merge($this->errors, $capErrors); + continue; + } + + if (!$isNewRole) { + //Have any of the capabilities changed? + $oldCaps = $this->menuEditor->castValuesToBool($wpRoles->roles[$id]['capabilities']); + if (!$this->areAssocArraysEqual($oldCaps, $role['capabilities'])) { + $isModifiedRole = true; + } + } + + //Everything looks valid. + if ($isNewRole) { + $rolesToCreate[$id] = $role; + } else if ($isModifiedRole) { + $rolesToModify[$id] = $role; + } + } + + //TODO: Existing roles might have to include newly added roles. + + //Validate user settings. + //----------------------- + + $submittedUsers = ameUtils::get($data, 'users', array()); + if (!is_array($submittedUsers)) { + $submittedUsers = array(); + } + $existingUsers = array(); + $usersToModify = array(); + foreach ($submittedUsers as $modifiedUser) { + //Skip malformed user records that are missing required fields. + if (!isset($modifiedUser, $modifiedUser['userId'], $modifiedUser['capabilities'], $modifiedUser['roles'])) { + continue; + } + $userId = intval(ameUtils::get($modifiedUser, 'userId', 0)); + + //User must exist. + $user = get_user_by('id', $userId); + if (empty($user) || !$user->exists()) { + continue; + } + + $previousRoles = array(); + if (isset($user->roles) && is_array($user->roles)) { + $previousRoles = array_values($user->roles); + } + $previousCapsWithoutRoles = array_diff_key( + (isset($user->caps) && is_array($user->caps)) ? $user->caps : array(), + array_fill_keys($previousRoles, true) + ); + + //TODO: Allow adding newly created roles if they are editable. Tricky. Might be better to disable that option in the editor. + //Validate roles. + list($newRoles, $roleErrors) = $this->validateUserRoleChange( + $previousRoles, + $modifiedUser['roles'], + $editableRoles + ); + $modifiedUser['roles'] = $newRoles; + + //Validate capabilities. + $newCapsWithoutRoles = array_diff_key( + $modifiedUser['capabilities'], + $knownRoleIDs, + array_fill_keys($newRoles, true) + ); + $capErrors = $this->validateCapabilityAssignment( + $newCapsWithoutRoles, + $existingCapabilities, + $knownRoleIDs + ); + $modifiedUser['capabilities'] = $newCapsWithoutRoles; + + //Have any of the roles or capabilities actually changed? + $isModifiedUser = false; + $oldCaps = $this->menuEditor->castValuesToBool($previousCapsWithoutRoles); + if (!$this->areAssocArraysEqual($oldCaps, $newCapsWithoutRoles)) { + $isModifiedUser = true; + } + //Note: The order of roles is significant. + if (!$this->areAssocArraysEqual($previousRoles, $newRoles)) { + $isModifiedUser = true; + } + + //Don't check permissions if the user hasn't actually made any changes. + if (!$isModifiedUser) { + continue; + } + + //The current user must have permission to edit this user. + if (!current_user_can('edit_user', $userId)) { + $this->errors[] = new WP_Error( + 'ame_uneditable_user', + sprintf('You don\'t have sufficient permissions to edit the user with ID #%d.', $userId) + ); + continue; + } + + //Now validation errors actually matter. + if (!empty($capErrors)) { + $this->errors = array_merge($this->errors, $capErrors); + continue; + } + if (!empty($roleErrors)) { + $this->errors = array_merge($this->errors, $roleErrors); + continue; + } + + if ($isModifiedUser) { + $usersToModify[$userId] = $modifiedUser; + $existingUsers[$userId] = $user; + } + } + + //Now that we know what roles exist, we can validate and save user-defined capabilities. + $this->userDefinedCaps = null; + if (isset($data['userDefinedCaps']) && is_array($data['userDefinedCaps'])) { + $validCaps = array(); + foreach ($data['userDefinedCaps'] as $capability) { + $status = $this->validateCapabilityName($capability, $knownRoleIDs); + if (!is_wp_error($status)) { + $validCaps[$capability] = true; + } + } + $this->userDefinedCaps = $validCaps; + + $addedCaps = array_diff_key($this->userDefinedCaps, $this->preExistingUserDefinedCaps); + $deletedCaps = array_diff_key($this->preExistingUserDefinedCaps, $this->userDefinedCaps); + $this->modifiedUserDefinedCapCount = count($addedCaps) + count($deletedCaps); + } + + //Validate "editable roles" settings. + $submittedEditableRoles = ameUtils::get($data, 'editableRoles', array()); + if (!is_array($submittedEditableRoles)) { + $submittedEditableRoles = array(); + } + $allowedStrategies = array('auto', 'none', 'user-defined-list'); + $newEditableRoles = array(); + foreach($submittedEditableRoles as $actorId => $settings) { + if (!$this->userCanEditActor($actorId, $editableRoles)) { + if (isset($this->oldEditableRoleSettings[$actorId])) { + $newEditableRoles[$actorId] = $this->oldEditableRoleSettings[$actorId]; + } + continue; + } + + //Validate the strategy. + if (!isset($settings['strategy']) || !in_array($settings['strategy'], $allowedStrategies)) { + $settings['strategy'] = 'auto'; + } + //The user-defined role list, if any, must be in the form of [roleId => true]. + if ($settings['strategy'] === 'user-defined-list') { + $sanitizedList = array(); + if (is_array($settings['userDefinedList'])) { + foreach(array_keys($settings['userDefinedList']) as $roleId) { + $sanitizedList[strval($roleId)] = true; + } + } + $settings['userDefinedList'] = $sanitizedList; + } else { + $settings['userDefinedList'] = null; + } + + //"auto" is the default so we don't need to store it. + if ($settings['strategy'] === 'auto') { + $settings = null; + } + + $newEditableRoles[$actorId] = $settings; + } + //Restore removed settings if the user doesn't have permission to edit them. + $removedSettings = array_diff_key($this->oldEditableRoleSettings, $submittedEditableRoles); + foreach($removedSettings as $actorId => $settings) { + if (!$this->userCanEditActor($actorId, $editableRoles) && isset($this->oldEditableRoleSettings[$actorId]) ) { + $newEditableRoles[$actorId] = $this->oldEditableRoleSettings[$actorId]; + } + } + //Check if we have actually made any changes. + if (!$this->areAssocArraysRecursivelyEqual($newEditableRoles, $this->oldEditableRoleSettings)) { + $this->areEditableRolesModified = true; + } + + $this->rolesToModify = $rolesToModify; + $this->rolesToCreate = $rolesToCreate; + $this->rolesToDelete = $rolesToDelete; + $this->usersToModify = $usersToModify; + $this->knownRoleIDs = $knownRoleIDs; + $this->existingUsers = $existingUsers; + $this->editableRoleSettings = $newEditableRoles; + + return $this->errors; + } + + /** + * @param string $name + * @param array $roles + * @param array $capabilities + * @return bool|WP_Error + */ + private function validateRoleName($name, $roles = array(), $capabilities = array()) { + $name = trim($name); + + if ($name === '') { + return new WP_Error('ame_empty_role_name', 'Role name cannot be empty.'); + } + + //Name can only contain certain characters. + if (preg_match('/[^a-z0-9_]/', $name)) { + return new WP_Error( + 'ame_invalid_characters_in_name', + 'Role name contains invalid characters. Please use only lowercase English letters, numbers, and underscores.' + ); + } + + //Numeric names could cause problems with how PHP handles associative arrays. + if (is_numeric($name)) { + return new WP_Error('ame_numeric_role_name', 'Numeric role names are not allowed.'); + } + + //Name must not be a duplicate. + if (array_key_exists($name, $roles)) { + return new WP_Error('ame_duplicate_role', 'Duplicate role name.'); + } + + //WP stores capabilities and role names in the same associative array, + //so they must be unique with respect to each other. + if (array_key_exists($name, $capabilities)) { + return new WP_Error('ame_role_matches_capability', 'Role name can\'t be the same as a capability name.'); + } + + return true; + } + + /** + * @param string $displayName + * @return bool|WP_Error + */ + private function validateRoleDisplayName($displayName) { + $displayName = trim($displayName); + + if ($displayName === '') { + return new WP_Error('ame_empty_role_display_name', 'Role display name cannot be empty.'); + } + + if (preg_match('/[><&\r\n\t]/', $displayName)) { + return new WP_Error('ame_invalid_display_name_chars', 'Role display name contains invalid characters.'); + } + + return true; + } + + /** + * @param string $capability + * @param string[] $roles + * @return bool|WP_Error + */ + private function validateCapabilityName($capability, $roles = array()) { + if ($capability === '') { + return new WP_Error('ame_empty_cap', 'Capability name must not be an empty string.'); + } + + //WP API allows completely arbitrary capability names, but this plugin forbids some characters + //for sanity's sake and to avoid XSS. + static $invalidCharacters = '/[><&\r\n\t]/'; + if (preg_match($invalidCharacters, $capability)) { + return new WP_Error('ame_invalid_cap_characters', 'Capability name contains invalid characters.'); + } + + //PHP doesn't allow numeric string keys, and there's no conceivable reason to start the name with a space. + static $invalidFirstCharacter = '/^[\s0-9]/i'; + if (preg_match($invalidFirstCharacter, $capability)) { + return new WP_Error('ame_invalid_cap_start', 'Capability name cannot start with a number or a space.'); + } + + //Roles and caps are stored in the same array, so they must be mutually unique. + if (array_key_exists($capability, $roles)) { + return new WP_Error( + 'ame_cap_equals_role', + sprintf('Capability name "%s" cannot be the same as the name of a role.', $capability) + ); + } + + //Some capabilities are special and should never be directly assigned to roles. + static $excludedCaps = array('do_not_allow', 'exist', 'customize'); + if (in_array($capability, $excludedCaps)) { + return new WP_Error( + 'ame_create_reserved_cap', + 'Cannot create a capability that matches a meta capability or a reserved capability.' + ); + } + + return true; + } + + private function validateCapabilityAssignment($capabilities, $existingCapabilities, $roles = array()) { + //Preexisting capabilities can be granted even if they don't meet our validation requirements. + $newCaps = array_diff_key($capabilities, $existingCapabilities); + $errors = array(); + foreach ($newCaps as $capability => $isGranted) { + $validationState = $this->validateCapabilityName($capability, $roles); + if (is_wp_error($validationState)) { + $errors[] = $validationState; + } + } + return $errors; + } + + /** + * Verify that the current user has permission to change a user's roles from $oldRoles to $newRoles. + * + * Returns the roles that the selected user should have after the change. Any invalid changes - like adding + * or removing non-editable roles - will be undone. + * + * For example, lets say that the selected user has these roles: + * $oldRoles = ['administrator', 'foo', 'bar'] + * + * Then the current user tries to change their roles to this: + * $newRoles = ['foo', 'author', 'qux'] + * + * Lets assume that the current user can edit all roles except "administrator", "foo" and "qux". + * Here's what the function will return: + * ['administrator', 'foo', 'author'] + * + * Here's what it does: + * - Prevent the attempt to remove a non-editable role ('administrator'). + * - Prevent the attempt to add a non-editable role ('qux'). + * - Keep 'administrator' as the primary role because it's not editable. + * - Let the user add or remove any roles that they can edit ('author', 'bar'). + * - let the user include roles that they can't edit if the subject already had them ('foo'). + * + * @param array $oldRoles Current role IDs. Example: array('administrator', 'foo', 'bar'). + * @param array $newRoles New role IDs. Example: array('foo', 'author', 'qux'). + * @param array|null $editableRoles + * @return array [validated-new-roles, errors] + */ + private function validateUserRoleChange($oldRoles, $newRoles, $editableRoles = null) { + if ($editableRoles === null) { + $editableRoles = get_editable_roles(); + if (!is_array($editableRoles)) { + $editableRoles = array(); + } + } + $errors = array(); + + if (!is_array($newRoles)) { + return array($oldRoles, array(new WP_Error('ame_rex_invalid_argument', 'Role list must be an array.'))); + } + + $newPrimaryRole = reset($newRoles); + + //NB: It is NOT an error to select a new primary role when the old one is not editable. + //WordPress UI simply doesn't give the option to leave the role unchanged. We shouldn't penalize users for that. + $oldPrimaryRole = reset($oldRoles); + if (is_string($oldPrimaryRole) && ($oldPrimaryRole !== '') && !isset($editableRoles[$oldPrimaryRole])) { + //Keep the existing primary role. Treat the new one as a normal "other" role. + $newPrimaryRole = $oldPrimaryRole; + array_unshift($newRoles, $oldPrimaryRole); //This might duplicate the role. We'll remove duplicates later. + } + + //It's always valid to keep the same roles, even if the current user can't edit them. + $validNewRoles = array_intersect($newRoles, $oldRoles); + + //Does the current user have permission to add/remove these roles? + $changedRoles = array_merge( + array_fill_keys(array_diff($newRoles, $oldRoles), 'add'), + array_fill_keys(array_diff($oldRoles, $newRoles), 'remove') + ); + $errorMessages = array( + 'add' => 'You cannot give users the "%s" role.', + 'remove' => 'You cannot remove the "%s" role from users.', + ); + + foreach ($changedRoles as $roleId => $action) { + $isAllowed = isset($editableRoles[$roleId]); + + if (($isAllowed && ($action === 'add')) || (!$isAllowed && ($action === 'remove'))) { + $validNewRoles[] = $roleId; + } + + if (!$isAllowed && isset($errors)) { + $errors[] = new WP_Error( + sprintf('ame_rex_cannot_%1$s_role_%2$s', $action, $roleId), + sprintf($errorMessages[$action], htmlentities($roleId)) + ); + } + } + + //Move the primary role to the start of the array. + $primaryRoleIndex = array_search($newPrimaryRole, $validNewRoles, true); + if ($newPrimaryRole && ($primaryRoleIndex > 0)) { + unset($validNewRoles[$primaryRoleIndex]); + array_unshift($validNewRoles, $newPrimaryRole); + } + + //Deduplicate roles. array_unique() sorts the array but preserves keys, so we can use ksort() to restore order. + $validNewRoles = array_unique($validNewRoles, SORT_STRING); //Requires PHP >= 5.2.9 + ksort($validNewRoles); + + return array(array_values($validNewRoles), $errors); + } + + /** + * Check if the currently logged-in user can edit the settings of a specific actor. + * + * @param string $actorId + * @param array $currentEditableRoles + * @return bool + */ + private function userCanEditActor($actorId, $currentEditableRoles) { + if ($actorId === 'special:super_admin') { + return is_super_admin(); + } + + list($type, $name) = explode(':', $actorId, 2); + if ($type === 'user') { + $victim = get_user_by('login', $name); + if ($victim) { + return current_user_can('edit_user', $victim->ID); + } + return current_user_can('edit_users'); + } + + if ($type === 'role') { + return isset($currentEditableRoles[$name]); + } + + return false; + } + + /** + * Check if two arrays have the same keys and values. Arrays with string keys + * or mixed keys can be in different order and still be considered "equal". + * + * @param array $a + * @param array $b + * @return bool + */ + private function areAssocArraysEqual($a, $b) { + if (count($a) !== count($b)) { + return false; + } + $sameItems = array_intersect_assoc($a, $b); + return count($sameItems) === count($b); + } + + /** + * Like areAssocArraysEqual(), but also compares nested arrays. + * + * @param array $a + * @param array $b + * @return bool + */ + private function areAssocArraysRecursivelyEqual($a, $b) { + if (count($a) !== count($b)) { + return false; + } + $sameKeys = array_intersect_key($a, $b); + if (count($sameKeys) !== count($a)) { + return false; + } + foreach($sameKeys as $key => $valueA) { + $valueB = $b[$key]; + if ($valueA !== $valueB) { + if (is_array($valueA) && is_array($valueB)) { + if (!$this->areAssocArraysRecursivelyEqual($valueA, $valueB)) { + return false; + } + } else { + return false; + } + } + } + return true; + } + + public function getUserDefinedCaps() { + return $this->userDefinedCaps; + } + + public function getTotalChangeCount() { + //TODO: Maybe count each modified capability as a separate change. + return count($this->rolesToDelete) + count($this->rolesToCreate) + + count($this->rolesToModify) + count($this->usersToModify) + + $this->modifiedUserDefinedCapCount + ($this->areEditableRolesModified ? 1 : 0); + } + + public function getRolesToDelete() { + return $this->rolesToDelete; + } + + public function getRolesToModify() { + return $this->rolesToModify; + } + + public function getRolesToCreate() { + return $this->rolesToCreate; + } + + public function getUsersToModify() { + return $this->usersToModify; + } + + public function getNewEditableRoleSettings() { + return $this->editableRoleSettings; + } + + /** + * @param $userId + * @return WP_User + */ + public function getExistingUser($userId) { + return $this->existingUsers[$userId]; + } +} \ No newline at end of file diff --git a/extras/modules/role-editor/ameRoleEditor.php b/extras/modules/role-editor/ameRoleEditor.php new file mode 100644 index 0000000..a4b7d5b --- /dev/null +++ b/extras/modules/role-editor/ameRoleEditor.php @@ -0,0 +1,2061 @@ + $filterInstance] + */ + private $cachedEditableRoles = array(); + /** + * @var string[] Overall, most specific strategy per user. [123 => 'auto', 456 => 'user-defined-list', ...] + */ + private $cachedOverallEditableRoleStrategy = array(); + /** + * @var bool Is the hook that clears the role cache already installed? + */ + private $isRoleCacheClearingHookSet = false; + /** + * @var array + */ + private $cachedEnabledRoleCaps = array(); + + public function __construct($menuEditor) { + parent::__construct($menuEditor); + + add_filter('editable_roles', array($this, 'filterEditableRoles'), 20, 1); + add_filter('map_meta_cap', array($this, 'restrictUserEditing'), 10, 4); + + //Optimization: Only record plugins that register post types and taxonomies when the current page is an AME tab. + if (isset($_GET['sub_section'])) { + add_action('registered_post_type', array($this, 'recordPostTypeOrigin'), 10, 2); + add_action('registered_taxonomy', array($this, 'recordTaxonomyOrigin'), 10, 3); + } + + add_action('wp_ajax_' . self::UPDATE_PREFERENCES_ACTION, array($this, 'ajaxUpdateUserPreferences')); + + /** @var wpdb */ + global $wpdb; + $this->backupTable = $wpdb->base_prefix . 'ame_role_backups'; + add_action(self::BACKUP_CLEANUP_HOOK, array($this, 'deleteOldRoleBackups')); + } + + public function enqueueTabScripts() { + parent::enqueueTabScripts(); + + wp_register_auto_versioned_script( + 'ame-role-editor', + plugins_url('role-editor.js', __FILE__), + array( + 'ame-lodash', + 'knockout', + 'jquery', + 'jquery-qtip', + 'ame-actor-manager', + 'ame-actor-selector', + 'ame-ko-extensions', + ) + ); + + wp_enqueue_script('ame-role-editor'); + + $this->knownComponents = new ameRexComponentRegistry(); + $this->queryInstalledComponents(); + + $defaultCapabilities = $this->getDefaultCapabilities(); + $multisiteCapabilities = $this->getMultisiteOnlyCapabilities(); + + foreach ($this->getAllCapabilities(array(wp_get_current_user())) as $capability => $unusedValue) { + $descriptor = new ameRexCapability(); + if (isset($defaultCapabilities[$capability]) || isset($multisiteCapabilities[$capability])) { + $descriptor->componentId = self::CORE_COMPONENT_ID; + } else { + $this->uncategorizedCapabilities[$capability] = true; + } + $this->capabilities[$capability] = $descriptor; + } + //TODO: do_not_allow should never end up in a plugin category. It's part of core. + + $postTypes = $this->getPostTypeDescriptors(); + $this->analysePostTypes($postTypes); + + $taxonomies = $this->findRegisteredTaxonomies(); + $this->analyseTaxonomies($taxonomies); + + //$categorizationStartTime = microtime(true); + + //Check which menu items use what capabilities and what the corresponding components are. + $this->analyseAdminMenuCapabilities(); + + $this->queryCapabilityDatabase(); + + $this->assignCapabilitiesToComponents(); + + $probablePostTypeCategories = $this->findProbablePostTypeCategories(); + $clusteredCategories = $this->groupSimilarCapabilities(); + + foreach ($this->componentRootCategories as $category) { + //Find component roots that have both subcategories and capabilities and + //put all freestanding capabilities in a "General" subcategory. + if (!empty($category->capabilities) && !empty($category->subcategories)) { + $generalCategory = new ameRexCategory('General', $category->componentId); + $generalCategory->capabilities = $category->capabilities; + $category->capabilities = array(); + array_unshift($category->subcategories, $generalCategory); + } + } + + $coreCategory = $this->loadCoreCategories(); + + //Normally, only a Super Admin on Multisite has certain Multisite administration capabilities. + //However, there is at least one plugin that uses these capabilities even in a regular WP install, + //so we'll show them as long as they're assigned to at least one role or user. + if (!is_multisite() && isset($coreCategory->subcategories['default/multisite'])) { + $multisiteCategory = $coreCategory->subcategories['default/multisite']; + $multisiteCategory->capabilities = array_intersect_key($multisiteCategory->capabilities, $this->capabilities); + if (empty($multisiteCategory->capabilities)) { + unset($coreCategory->subcategories['default/multisite']); + } + } + + /*echo '
';
+		print_r($clusteredCategories);
+		print_r($probablePostTypeCategories);
+		print_r(array_keys($this->uncategorizedCapabilities));
+		print_r($this->capabilities);
+		exit;*/
+
+		//$elapsed = microtime(true) - $categorizationStartTime;
+		//printf('Categorization time: %.3f ms', $elapsed * 1000);
+		//exit;
+
+		$customCategories = array_merge($this->componentRootCategories, $probablePostTypeCategories, $clusteredCategories);
+		$customCategoryDescriptors = array();
+		foreach ($customCategories as $category) {
+			/** @var ameRexCategory $category */
+			$customCategoryDescriptors[] = $category->toArray();
+		}
+
+		$components = array();
+		foreach ($this->knownComponents as $id => $component) {
+			$components[$id] = $component->toArray();
+		}
+
+		$stableMetaCaps = self::loadCapabilities('stable-meta-caps.txt');
+		$metaCapMap = array();
+		$currentUserId = get_current_user_id();
+		foreach ($stableMetaCaps as $metaCap => $unused) {
+			$primitiveCaps = map_meta_cap($metaCap, $currentUserId);
+			if ((count($primitiveCaps) === 1) && !in_array('do_not_allow', $primitiveCaps)) {
+				$targetCap = reset($primitiveCaps);
+				if ($targetCap !== $metaCap) {
+					$metaCapMap[$metaCap] = $targetCap;
+				}
+			}
+		}
+
+		$userPreferences = array();
+		$userPreferenceData = get_user_meta(get_current_user_id(), self::USER_PREFERENCE_KEY, true);
+		if (is_string($userPreferenceData) && !empty($userPreferenceData)) {
+			$userPreferences = json_decode($userPreferenceData, true);
+			if (!is_array($userPreferences)) {
+				$userPreferences = array();
+			}
+		}
+
+		$query = $this->menuEditor->get_query_params();
+		$selectedActor = null;
+		if (isset($query['selected_actor'])) {
+			$selectedActor = strval($query['selected_actor']);
+		}
+
+		$scriptData = array(
+			'coreCategory'              => $coreCategory->toArray(),
+			'customCategories'          => $customCategoryDescriptors,
+			'postTypes'                 => $postTypes,
+			'taxonomies'                => $taxonomies,
+			'capabilities'              => $this->capabilities,
+			'uncategorizedCapabilities' => array_keys($this->uncategorizedCapabilities),
+			'deprecatedCapabilities'    => self::loadCapabilities('deprecated-capabilities.txt'),
+			'userDefinedCapabilities'   => $this->getUserDefinedCaps(),
+			'knownComponents'           => $components,
+			'metaCapMap'                => $metaCapMap,
+			'roles'                     => $this->getRoleData(),
+			'users'                     => array(),
+			'defaultRoleName'           => get_option('default_role'),
+			'trashedRoles'              => array(), //todo: Load trashed roles from somewhere.
+			'selectedActor'             => $selectedActor,
+			'editableRoles'             => ameUtils::get($this->loadSettings(), 'editableRoles', new stdClass()),
+
+			'userPreferences'        => $userPreferences,
+			'adminAjaxUrl'           => self_admin_url('admin-ajax.php'),
+			'updatePreferencesNonce' => wp_create_nonce(self::UPDATE_PREFERENCES_ACTION),
+		);
+
+		$jsonData = wp_json_encode($scriptData);
+
+		if ( !is_string($jsonData) ) {
+			$message = sprintf(
+				'Failed to encode role data as JSON. The encoding function returned a %s.',
+				esc_html(gettype($jsonData))
+			);
+			if ( function_exists('json_last_error_msg') ) {
+				$message .= sprintf(
+					'
JSON error message: "%s".', + esc_html(json_last_error_msg()) + ); + } + if ( function_exists('json_last_error') ) { + $message .= sprintf(' JSON error code: %d.', json_last_error()); + } + + add_action('all_admin_notices', function () use ($message, $scriptData) { + printf('

%s

', $message); + }); + } + + wp_add_inline_script( + 'ame-role-editor', + sprintf('wsRexRoleEditorData = (%s);', $jsonData) + ); + } + + public function enqueueTabStyles() { + parent::enqueueTabStyles(); + wp_enqueue_auto_versioned_style( + 'ame-role-editor-styles', + plugins_url('role-editor.css', __FILE__) + ); + } + + public function displaySettingsPage() { + if (!$this->userCanAccessModule()) { + echo 'Error: You don\'t have sufficient permissions to access these settings.'; + return; + } + + if ($this->backupsEnabled && !wp_next_scheduled(self::BACKUP_CLEANUP_HOOK)) { + wp_schedule_event(time() + 10 * 60, 'daily', self::BACKUP_CLEANUP_HOOK); + } + parent::displaySettingsPage(); + } + + private function getPostTypeDescriptors() { + $results = array(); + $wpPostTypes = get_post_types(array(), 'objects'); + + //Note: When the "map_meta_cap" option is disabled for a CPT, the values of the "cap" + //object will be treated as primitive capabilities. For example, "read_post" => "read_post" + //means that WP will actually check if the user has the literal "read_post" capability. + + //On the other hand, when "map_meta_cap" is enabled, some "cap" entries can be re-mapped + //to other "cap" entries depending on the current user, post owner, post status, etc. + + //These three meta capabilities will always be mapped to something else if "map_meta_cap" + //is enabled. We'll skip them unless mapping is off or someone has assigned them to a role. + //Note: It's possible that it would be fine to skip them even then. Not sure. + $metaCaps = array('edit_post', 'read_post', 'delete_post'); + + foreach ($wpPostTypes as $name => $postType) { + $isIncluded = $postType->public || !$postType->_builtin; + + //Skip the "attachment" post type. It only has one unique capability (upload_files), which + //is included in a default group. + if ($name === 'attachment') { + $isIncluded = false; + } + + if (!$isIncluded) { + continue; + } + + $label = $name; + $pluralLabel = $name; + if (isset($postType->labels, $postType->labels->name) && !empty($postType->labels->name)) { + $label = $postType->labels->name; + $pluralLabel = $postType->labels->name; + } + + //We want the plural in lowercase, but if there are multiple consecutive uppercase letters + //then it's probably an acronym. Stuff like "aBc" is probably a contraction or a proper noun. + if (!preg_match('@([A-Z]{2}|[a-z][A-Z])@', $pluralLabel)) { + $pluralLabel = strtolower($pluralLabel); + } + + $capabilities = array(); + foreach ((array)$postType->cap as $capType => $capability) { + //Skip meta caps unless they already exist. + if (in_array($capType, $metaCaps) && ($postType->map_meta_cap || !isset($this->capabilities[$capability]))) { + continue; + } + + //Skip the "read" cap. It's redundant - most CPTs use it, and all roles have it by default. + if (($capType === 'read') && ($capability === 'read')) { + continue; + } + + //Some plugins apparently set capability to "false". Perhaps the intention is to disable it. + if ($capability === false) { + continue; + } + + $capabilities[$capType] = $capability; + } + + $component = isset($this->postTypeRegistrants[$name]) ? $this->postTypeRegistrants[$name] : null; + + $descriptor = array( + 'label' => $label, + 'name' => $name, + 'pluralLabel' => $pluralLabel, + 'permissions' => $capabilities, + 'isDefault' => isset($postType->_builtin) && $postType->_builtin, + 'componentId' => $component, + ); + + $results[$name] = $descriptor; + } + + return $results; + } + + protected function findRegisteredTaxonomies() { + $registeredTaxonomies = array(); + $usedLabels = array('Categories' => true, 'Category' => true, 'Tags' => true); + + foreach (get_taxonomies(array(), 'object') as $taxonomy) { + $permissions = (array)($taxonomy->cap); + + //Skip "link_category" because its only cap (manage_links) is already part of a default category. + if ( + ($taxonomy->name === 'link_category') + && ($permissions['manage_terms'] === 'manage_links') + && (count(array_unique($permissions)) === 1) + ) { + continue; + } + + //Skip "nav_menu" and "post_format" because they're intended for internal use and have the same + //caps as the "Category" taxonomy. + if (in_array($taxonomy->name, array('nav_menu', 'post_format')) && $taxonomy->_builtin) { + continue; + } + + $componentId = null; + $isBuiltIn = isset($taxonomy->_builtin) && $taxonomy->_builtin; + if ($isBuiltIn) { + $componentId = self::CORE_COMPONENT_ID; + } else if (isset($this->taxonomyRegistrants[$taxonomy->name])) { + $componentId = $this->taxonomyRegistrants[$taxonomy->name]; + } + + $label = $taxonomy->name; + if (isset($taxonomy->labels, $taxonomy->labels->name) && !empty($taxonomy->labels->name)) { + $label = $taxonomy->labels->name; + } + + $uniqueLabel = $label; + if (isset($usedLabels[$uniqueLabel]) && !$isBuiltIn) { + $uniqueLabel = str_replace('_', ' ', $taxonomy->name); + } + //We want the label in lowercase unless it's an acronym. + if (!preg_match('@([A-Z]{2}|[a-z][A-Z])@', $uniqueLabel)) { + $uniqueLabel = strtolower($uniqueLabel); + } + $usedLabels[$uniqueLabel] = true; + + $registeredTaxonomies[$taxonomy->name] = array( + 'name' => $taxonomy->name, + 'label' => $label, + 'pluralLabel' => $uniqueLabel, + 'componentId' => $componentId, + 'permissions' => $permissions, + ); + + } + + return $registeredTaxonomies; + } + + protected function analysePostTypes($postTypes) { + //Record which components use which CPT capabilities. + foreach ($postTypes as $name => $postType) { + if (empty($postType['componentId']) || !isset($this->knownComponents[$postType['componentId']])) { + continue; + } + $this->knownComponents[$postType['componentId']]->registeredPostTypes[$name] = true; + foreach ($postType['permissions'] as $action => $capability) { + if (isset($this->capabilities[$capability])) { + $this->capabilities[$capability]->addUsage($postType['componentId']); + } + } + + //Add a CPT category to the component that created this post type. + if (empty($postType['isDefault']) && ($postType['componentId'] !== self::CORE_COMPONENT_ID)) { + $componentRoot = $this->getComponentCategory($postType['componentId']); + if ($componentRoot) { + $category = new ameRexPostTypeCategory( + $postType['label'], + $postType['componentId'], + $name, + $postType['permissions'] + ); + $componentRoot->subcategories[] = $category; + } + } + } + } + + protected function analyseTaxonomies($taxonomies) { + //Record taxonomy components and create taxonomy categories for those components. + foreach ($taxonomies as $name => $taxonomy) { + if (empty($taxonomy['componentId'])) { + continue; + } + foreach ($taxonomy['permissions'] as $action => $capability) { + if (isset($this->capabilities[$capability])) { + $this->capabilities[$capability]->addUsage($taxonomy['componentId']); + } + } + + //Add a taxonomy category to the component that created this taxonomy. + if ($taxonomy['componentId'] !== self::CORE_COMPONENT_ID) { + $componentRoot = $this->getComponentCategory($taxonomy['componentId']); + if ($componentRoot) { + $category = new ameRexTaxonomyCategory( + $taxonomy['label'], + $taxonomy['componentId'], + $name, + $taxonomy['permissions'] + ); + $componentRoot->subcategories[] = $category; + } + } + } + } + + /** + * Assign each capability that has known component relationships to one specific component. + * Copy component-related context information to the capability. + */ + protected function assignCapabilitiesToComponents() { + //Figure out which component each capability belongs to. + foreach ($this->capabilities as $capability => $unusedValue) { + $details = $this->capabilities[$capability]; + + if (empty($details->componentId) && !empty($details->usedByComponents)) { + //Sort related components by priority. + if (count($details->usedByComponents) > 1) { + uksort($details->usedByComponents, array($this, 'compareComponents')); + } + //Pick the first component. + $details->componentId = key($details->usedByComponents); + } + + if (!empty($details->componentId)) { + //Copy context information that's relevant to the selected component. + if (isset($details->componentContext[$details->componentId])) { + $propertiesToCopy = array('permissions', 'documentationUrl', 'notes'); + foreach ($propertiesToCopy as $property) { + if (isset($details->componentContext[$details->componentId][$property])) { + $details->$property = $details->componentContext[$details->componentId][$property]; + } + } + } + + if ($details->componentId !== self::CORE_COMPONENT_ID) { + //Add the capability to the component category unless it's already there. + $category = $this->getComponentCategory($details->componentId); + if ($category !== null) { + if (!$category->hasCapability($capability)) { + $category->capabilities[$capability] = true; + } + unset($this->uncategorizedCapabilities[$capability]); + } else { + //This should never happen. If the capability has a component ID, that component + //should already be registered. + trigger_error(sprintf( + '[AME] Capability "%s" belongs to component "%s" but that component appears to be unknown.', + $capability, + $details->componentId + ), E_USER_WARNING); + continue; + } + } + } + } + } + + /** + * @return ameRexCategory + */ + private function loadCoreCategories() { + $root = new ameRexCategory('Core', self::CORE_COMPONENT_ID); + $root->slug = 'default/core'; + + $lines = file_get_contents(__DIR__ . '/data/core-categories.txt'); + $lines = explode("\n", $lines); + + $currentCategory = new ameRexCategory('Placeholder', self::CORE_COMPONENT_ID); + + //Each category starts with a title. The title is followed by one or more indented lines listing + //capability names, one capability per line. Blank lines are ignored. + $lineNumber = 0; + foreach ($lines as $line) { + $lineNumber++; + + //Skip blank lines. + $line = rtrim($line); + if ($line === '') { + continue; + } + + $firstChar = substr($line, 0, 1); + if ($firstChar === ' ' || $firstChar === "\t") { + //Found a capability. + $capability = trim($line); + //Skip unassigned caps. Even core capabilities sometimes get removed as WP development continues. + if (isset($this->capabilities[$capability])) { + $currentCategory->capabilities[$capability] = true; + } + } else { + //Found a "Category title [optional slug]" + if (preg_match('@^(?P[^\[]+)(?:\s+\[(?P<slug>[^]]+)])?\s*$@', $line, $matches)) { + //Save the previous category if it matched any capabilities. + if (count($currentCategory->capabilities) > 0) { + $root->addSubcategory($currentCategory); + } + + $title = trim($matches['title']); + $slug = !empty($matches['slug']) ? trim($matches['slug']) : ('default/' . $title); + + $currentCategory = new ameRexCategory($title, self::CORE_COMPONENT_ID); + $currentCategory->slug = $slug; + } + } + } + + //Save the last category. + if (count($currentCategory->capabilities) > 0) { + $root->addSubcategory($currentCategory); + } + + return $root; + } + + protected function analyseAdminMenuCapabilities() { + $menu = $this->menuEditor->get_active_admin_menu(); + if (!empty($menu['tree'])) { + foreach ($menu['tree'] as $item) { + $this->analyseMenuItem($item); + } + } + } + + protected function analyseMenuItem($item, $parent = null) { + $capability = ameUtils::get($item, array('defaults', 'access_level')); + + if (empty($item['custom']) && !empty($item['defaults']) && !empty($capability) && empty($item['separator'])) { + $defaults = $item['defaults']; + $hook = get_plugin_page_hook(ameUtils::get($defaults, 'file', ''), ameUtils::get($defaults, 'parent', '')); + + $rawTitle = ameMenuItem::get($item, 'menu_title', '[Untitled]'); + $fullTitle = trim(strip_tags(ameMenuItem::remove_update_count($rawTitle))); + if ($parent) { + $parentTitle = ameMenuItem::remove_update_count(ameMenuItem::get($parent, 'menu_title', '[Untitled]')); + $fullTitle = trim(strip_tags($parentTitle)) . ' → ' . $fullTitle; + } + + $relatedComponents = array(); + if (!empty($hook)) { + $reflections = $this->getHookReflections($hook); + foreach ($reflections as $reflection) { + $path = $reflection->getFileName(); + $componentId = $this->getComponentIdFromPath($path); + if ($componentId) { + $relatedComponents[$this->getComponentIdFromPath($path)] = true; + } + } + } + + if (isset($this->capabilities[$capability])) { + $this->capabilities[$capability]->menuItems[] = $fullTitle; + $this->capabilities[$capability]->addManyUsages($relatedComponents); + } + } + + if (!empty($item['items'])) { + foreach ($item['items'] as $submenu) { + $this->analyseMenuItem($submenu, $item); + } + } + } + + /** + * @param string $tag + * @return AmeReflectionCallable[] + */ + protected function getHookReflections($tag) { + global $wp_filter; + if (!isset($wp_filter[$tag])) { + return array(); + } + + $reflections = array(); + foreach ($wp_filter[$tag] as $priority => $handlers) { + foreach ($handlers as $index => $callback) { + try { + $reflection = new AmeReflectionCallable($callback['function']); + $reflections[] = $reflection; + } catch (ReflectionException $e) { + //Invalid callback, let's just ignore it. + continue; + } + } + } + return $reflections; + } + + protected function getComponentIdFromPath($absolutePath) { + static $pluginDirectory = null, $muPluginDirectory = null, $themeDirectory = null; + if ($pluginDirectory === null) { + $pluginDirectory = wp_normalize_path(WP_PLUGIN_DIR); + $muPluginDirectory = wp_normalize_path(WPMU_PLUGIN_DIR); + $themeDirectory = wp_normalize_path(WP_CONTENT_DIR . '/themes'); + } + + $absolutePath = wp_normalize_path($absolutePath); + $pos = null; + $type = ''; + if (strpos($absolutePath, $pluginDirectory) === 0) { + $type = 'plugin'; + $pos = strlen($pluginDirectory); + } else if (strpos($absolutePath, $muPluginDirectory) === 0) { + $type = 'mu-plugin'; + $pos = strlen($muPluginDirectory); + } else if (strpos($absolutePath, $themeDirectory) === 0) { + $type = 'theme'; + $pos = strlen($themeDirectory); + } + + if ($pos !== null) { + $nextSlash = strpos($absolutePath, '/', $pos + 1); + if ($nextSlash !== false) { + $componentDirectory = substr($absolutePath, $pos + 1, $nextSlash - $pos - 1); + } else { + $componentDirectory = substr($absolutePath, $pos + 1); + } + return $type . ':' . $componentDirectory; + } + return null; + } + + protected function getRoleData() { + $wpRoles = ameRoleUtils::get_roles(); + $roles = array(); + + $usersByRole = count_users(); + + foreach ($wpRoles->role_objects as $roleId => $role) { + $capabilities = array(); + if (!empty($role->capabilities) && is_array($role->capabilities)) { + $capabilities = $this->menuEditor->castValuesToBool($role->capabilities); + } + + $hasUsers = false; + if (isset($usersByRole['avail_roles'], $usersByRole['avail_roles'][$roleId])) { + $hasUsers = ($usersByRole['avail_roles'][$roleId] > 0); + } + + //Our JS expects the capability map to be an object. It must be an object + //even if it's empty or if all of the keys (i.e. capability names) are numeric. + $capabilities = (object)$capabilities; + + $roles[] = array( + 'name' => $roleId, + 'displayName' => ameUtils::get($wpRoles->role_names, $roleId, $roleId), + 'capabilities' => $capabilities, + 'hasUsers' => $hasUsers, + ); + } + return $roles; + } + + /** + * Get a list of all known capabilities that apply to the current WordPress install. + * + * @param WP_User[] $users List of zero or more users. + * @return array Associative array indexed by capability name. + */ + protected function getAllCapabilities($users = array()) { + //Always include capabilities that are built into WordPress. + $capabilities = $this->getDefaultCapabilities(); + + //Add capabilities assigned to roles. + $capabilities = $capabilities + ameRoleUtils::get_all_capabilities(is_multisite()); + + //Add capabilities of users. + $roleNames = ameRoleUtils::get_role_names(); + foreach ($users as $user) { + $userCaps = $user->caps; + //Remove roles from the capability list. + $userCaps = array_diff_key($userCaps, $roleNames); + $capabilities = $capabilities + $userCaps; + } + + //Add custom capabilities that were created by the user. These persist until manually deleted. + $capabilities = $capabilities + $this->getUserDefinedCaps(); + + $capabilities = $this->menuEditor->castValuesToBool($capabilities); + + uksort($capabilities, 'strnatcasecmp'); + return $capabilities; + } + + protected function getDefaultCapabilities() { + static $defaults = null; + if ($defaults !== null) { + return $defaults; + } + + $defaults = self::loadCapabilities('default-capabilities.txt'); + + if (is_multisite()) { + $defaults = array_merge($defaults, $this->getMultisiteOnlyCapabilities()); + } + + return $defaults; + } + + protected function getMultisiteOnlyCapabilities() { + static $cache = null; + if ($cache === null) { + $cache = self::loadCapabilities('default-multisite-capabilities.txt'); + } + return $cache; + } + + /** + * Load a list of capabilities from a text file. + * + * @param string $fileName + * @param bool|int|string $fillValue Optional. Fill the result array with this value. Defaults to false. + * @return array Associative array with capability names as keys and $fillValue as values. + */ + public static function loadCapabilities($fileName, $fillValue = false) { + $fileName = __DIR__ . '/data/' . $fileName; + if (!is_file($fileName) || !is_readable($fileName)) { + return array(); + } + + $contents = file_get_contents($fileName); + + $capabilities = preg_split('@[\r\n]+@', $contents); + $capabilities = array_map('trim', $capabilities); + $capabilities = array_filter($capabilities, array(__CLASS__, 'isNotEmptyString')); + $capabilities = array_filter($capabilities, array(__CLASS__, 'isNotLineComment')); + + $capabilities = array_fill_keys($capabilities, $fillValue); + + return $capabilities; + } + + protected static function isNotEmptyString($input) { + return $input !== ''; + } + + protected static function isLineComment($input) { + $input = trim($input); + if ($input === '') { + return false; + } + + $firstChar = substr($input, 0, 1); + if ($firstChar === '#' || $firstChar === ';') { + return true; + } + + if (substr($input, 0, 2) === '//') { + return true; + } + + return false; + } + + protected static function isNotLineComment($input) { + return !self::isLineComment($input); + } + + /** + * Get components, capability metadata and possible categories from the capability database. + */ + private function queryCapabilityDatabase() { + $engine = new ameRexCapabilityInfoSearch(); + + $engine->addDataSource(new ameRexSqliteDataSource(__DIR__ . '/data/capability-excerpt.sqlite3')); + //$engine->addDataSource(new ameRexJsonCapabilityDataSource(__DIR__ . '/data/capability-metadata.json')); + + $results = $engine->query(array_keys($this->capabilities), $this->knownComponents); + foreach($results as $capability => $components) { + $this->capabilities[$capability]->addManyUsages($components); + } + } + + private function compareComponents($idA, $idB) { + $a = $this->knownComponents->components[$idA]; + $b = $this->knownComponents->components[$idB]; + + if ($a->isActive && !$b->isActive) { + return -1; + } + if ($b->isActive && !$a->isActive) { + return 1; + } + if ($a->isInstalled && !$b->isInstalled) { + return -1; + } + if ($b->isInstalled && !$a->isInstalled) { + return 1; + } + + return ($b->activeInstalls - $a->activeInstalls); + } + + /** + * @param string $id + * @param WP_Post_Type $postType + */ + public function recordPostTypeOrigin($id, $postType) { + if (!is_admin() || empty($postType) || empty($id)) { + return; + } + + if (isset($postType->_builtin) && $postType->_builtin) { + return; + } + + //Find the last entry that is part of a plugin or theme. + $component = $this->detectCallerComponent(); + if ($component !== null) { + $this->postTypeRegistrants[$id] = $component; + } + } + + public function recordTaxonomyOrigin( + $id, + /** @noinspection PhpUnusedParameterInspection It's part of the filter signature. We can't remove it. */ + $objectType, + $taxonomy = array() + ) { + if (!is_admin() || empty($taxonomy) || empty($id) || !is_array($taxonomy)) { + return; + } + + if (isset($taxonomy['_builtin']) && $taxonomy['_builtin']) { + return; + } + + $component = $this->detectCallerComponent(); + if ($component !== null) { + $this->taxonomyRegistrants[$id] = $component; + } + } + + /** + * Detect the plugin or theme that triggered the current hook. + * If multiple components are involved, only the earliest one will be returned + * (i.e. the one at the bottom of the call stack). + * + * @return null|string + */ + private function detectCallerComponent() { + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + //Drop the first three entries because they just contain this method, its caller, + //and an apply_filters or do_action call. + array_shift($trace); + array_shift($trace); + array_shift($trace); + + //Find the last entry that is part of a plugin or theme. + $component = null; + foreach ($trace as $item) { + if (empty($item['file'])) { + continue; + } + + $possibleComponent = $this->getComponentIdFromPath($item['file']); + if ($possibleComponent) { + $component = $possibleComponent; + } + } + return $component; + } + + private function queryInstalledComponents() { + $installedPlugins = get_plugins(); + foreach ($installedPlugins as $pluginFile => $plugin) { + $pathComponents = explode('/', $pluginFile, 2); + if (count($pathComponents) < 2) { + continue; + } + $component = new ameRexComponent( + 'plugin:' . $pathComponents[0], + ameUtils::get($plugin, 'Name', $pathComponents[0]) + ); + $component->isInstalled = true; + $component->isActive = is_plugin_active($pluginFile); + $this->knownComponents[$component->id] = $component; + } + + $installedMuPlugins = get_mu_plugins(); + foreach($installedMuPlugins as $pluginFile => $plugin) { + $component = new ameRexComponent( + 'mu-plugin:' . $pluginFile, + ameUtils::get($plugin, 'Name', $pluginFile) + ); + $component->isInstalled = true; + $component->isActive = true; //mu-plugins are always active. + $this->knownComponents[$component->id] = $component; + } + + $activeThemeSlugs = array(get_stylesheet(), get_template()); + foreach ($activeThemeSlugs as $slug) { + $componentId = 'theme:' . $slug; + if (isset($this->knownComponents[$componentId])) { + continue; + } + $theme = wp_get_theme($slug); + if (!empty($theme)) { + $component = new ameRexComponent($componentId, $theme->get('Name')); + $component->isActive = true; + $component->isInstalled = true; + $this->knownComponents[$component->id] = $component; + } + } + } + + /** + * @param $componentId + * @return ameRexCategory|null + */ + private function getComponentCategory($componentId) { + if (!isset($this->componentRootCategories[$componentId])) { + if (!isset($this->knownComponents[$componentId])) { + return null; + } + $category = new ameRexCategory($this->knownComponents[$componentId]->name, $componentId); + $category->slug = 'components/' . $componentId; + $this->componentRootCategories[$componentId] = $category; + } + return $this->componentRootCategories[$componentId]; + } + + /** + * Group capabilities that look like they belong to a post type but are not used by any registered post types. + * This could be stuff left behind by an uninstalled plugin, or just a set of similar capabilities. + * + * @return ameRexCategory[] + */ + protected function findProbablePostTypeCategories() { + $potentialPostTypes = array(); + $foundCategories = array(); + + //At the moment, WordPress database schema limits post types to 20 characters. + $namePattern = '(?P<post_type>.{1,20}?)s?'; + $cptPatterns = array( + '@^edit_(?:(?:others|private|published)_)?' . $namePattern . '$@', + '@^delete_(?:(?:others|private|published)_)?' . $namePattern . '$@', + '@^publish_' . $namePattern . '$@', + + '@^read_private_' . $namePattern . '$@', + '@^read_' . $namePattern . '$@', + + //WooCommerce stuff + '@^(assign|edit|manage|delete)_' . $namePattern . '_terms$@', + ); + + foreach ($this->uncategorizedCapabilities as $capability => $unused) { + foreach ($cptPatterns as $pattern) { + if (preg_match($pattern, $capability, $matches)) { + $postType = $matches['post_type']; + + //Unknown CPT-like capability. + if (!isset($potentialPostTypes[$postType])) { + $potentialPostTypes[$postType] = array(); + } + $potentialPostTypes[$postType][$capability] = $capability; + + break; + } + } + } + + //Empirically, real post types have at least 3 associated capabilities. + foreach ($potentialPostTypes as $postType => $typeCaps) { + if (count($typeCaps) >= 3) { + //Note that this group does not correspond to an existing post type. It's just a set of similar caps. + $title = ameUtils::ucWords(str_replace('_', ' ', $postType)); + if (substr($title, -1) !== 's') { + $title .= 's'; //Post type titles are usually plural. + } + + $category = new ameRexCategory($title); + $category->capabilities = array_fill_keys($typeCaps, true); + $foundCategories[] = $category; + + //Now that we know which group these caps belong to, remove them from consideration. + foreach ($typeCaps as $capability) { + unset($this->uncategorizedCapabilities[$capability]); + } + } + } + + return $foundCategories; + } + + private function groupSimilarCapabilities() { + $stopWords = $this->getPrefixStopWords(); + + //Find the common prefix of each component root, if there is one. + foreach ($this->componentRootCategories as $category) { + if (!empty($category->capabilities) && (count($category->capabilities) > 1)) { + $possiblePrefix = key($category->capabilities); + foreach ($category->capabilities as $capability => $unusedValue) { + for ($i = 0; $i < min(strlen($possiblePrefix), strlen($capability)); $i++) { + if ($possiblePrefix[$i] !== $capability[$i]) { + if ($i >= 1) { + $possiblePrefix = substr($possiblePrefix, 0, $i); + } else { + $possiblePrefix = ''; + } + break; + } + } + + if ($possiblePrefix === '') { + break; + } + } + + //The prefix must be at least 2 characters long and must not consist entirely of stopwords. + if (strlen($possiblePrefix) >= 2) { + $tokens = $this->tokenizeCapability($possiblePrefix); + $foundStopWords = 0; + foreach ($tokens as $token) { + if (isset($stopWords[strtolower($token)])) { + $foundStopWords++; + } + } + if ($foundStopWords === count($tokens)) { + continue; + } + + $prefix = implode(' ', array_slice($tokens, 0, 2)); + $this->componentCapPrefixes[$prefix] = $category; + } + } + } + + $possibleCategories = array(); + foreach ($this->uncategorizedCapabilities as $capability => $unusedValue) { + $tokens = $this->tokenizeCapability($capability); + $upperLimit = min(2, count($tokens) - 1); + + $prefix = null; + for ($i = 0; $i < $upperLimit; $i++) { + if ($prefix === null) { + $prefix = $tokens[$i]; + } else { + $prefix .= ' ' . $tokens[$i]; + } + if (isset($stopWords[$tokens[$i]]) || (strlen($tokens[$i]) < 2)) { + continue; + } + + //Check if one of the existing component categories has the same prefix + //and add this capability there. + if (isset($this->componentCapPrefixes[$prefix])) { + $this->componentCapPrefixes[$prefix]->addCapabilityToDefaultLocation($capability); + unset($this->uncategorizedCapabilities[$capability]); + + $componentId = $this->componentCapPrefixes[$prefix]->componentId; + if ($componentId !== null) { + $this->capabilities[$capability]->addUsage($componentId); + if (empty($this->capabilities[$capability]->componentId)) { + $this->capabilities[$capability]->componentId = $componentId; + } + } + } + + if (!isset($possibleCategories[$prefix])) { + $possibleCategories[$prefix] = array(); + } + $possibleCategories[$prefix][$capability] = true; + } + } + + uasort($possibleCategories, array($this, 'compareArraySizes')); + + $approvedCategories = array(); + foreach ($possibleCategories as $prefix => $capabilities) { + $capabilities = array_intersect_key($capabilities, $this->uncategorizedCapabilities); + if (count($capabilities) < 3) { + continue; + } + + $title = $prefix; + //Convert all-lowercase to Title Case, but preserve stuff that already has mixed case. + if (strtolower($title) === $title) { + $title = ameUtils::ucWords($title); + } + + //No vowels = probably an acronym. + if (!preg_match('@[aeuio]@', $title)) { + $title = strtoupper($title); + } + + $category = new ameRexCategory($title); + $category->capabilities = $capabilities; + $approvedCategories[] = $category; + foreach ($capabilities as $capability => $unused) { + unset($this->uncategorizedCapabilities[$capability]); + } + } + + return $approvedCategories; + } + + private function tokenizeCapability($capability) { + return preg_split('@[\s_\-]@', $capability, -1, PREG_SPLIT_NO_EMPTY); + } + + private function getPrefixStopWords() { + static $stopWords = null; + if ($stopWords === null) { + $stopWords = array( + 'edit', + 'delete', + 'add', + 'list', + 'manage', + 'read', + 'others', + 'private', + 'published', + 'publish', + 'terms', + 'view', + 'create', + 'settings', + 'options', + 'option', + 'setting', + 'update', + 'install', + ); + $stopWords = array_fill_keys($stopWords, true); + } + return $stopWords; + } + + private function compareArraySizes($a, $b) { + return count($b) - count($a); + } + + public function ajaxUpdateUserPreferences() { + check_ajax_referer(self::UPDATE_PREFERENCES_ACTION); + + @header('Content-Type: application/json; charset=' . get_option('blog_charset')); + if (!$this->userCanAccessModule()) { + echo json_encode(array('error' => 'Access denied')); + exit; + } + + $post = $this->menuEditor->get_post_params(); + if (!isset($post['preferences']) || !is_string($post['preferences'])) { + echo json_encode(array('error' => 'The "preferences" field is missing or invalid.')); + exit; + } + + $preferences = json_decode($post['preferences'], true); + if ($preferences === null) { + echo json_encode(array('error' => 'The "preferences" field is not valid JSON.')); + exit; + } + + if (!is_array($preferences)) { + echo json_encode(array('error' => 'The "preferences" field is not valid. Expected an associative array.')); + exit; + } + + update_user_meta(get_current_user_id(), self::USER_PREFERENCE_KEY, json_encode($preferences)); + + echo json_encode(array('success' => true)); + exit; + } + + public function handleSettingsForm($post = array()) { + if (!$this->userCanAccessModule()) { + wp_die("You don't have sufficient permissions to change these settings."); + } + + $redirectParams = array(); + if (!empty($post['selectedActor'])) { + $redirectParams['selected_actor'] = strval($post['selectedActor']); + } + + $validator = new ameRexSettingsValidator( + $post['settings'], + $this->getAllCapabilities(array(wp_get_current_user())), + $this->getUserDefinedCaps(), + ameUtils::get($this->loadSettings(), 'editableRoles', array()), + $this->getEffectiveEditableRoles(), + $this->menuEditor + ); + + $errors = $validator->validate(); + $this->storeSettingsErrors($errors); + + $shouldUpdateNetwork = !empty($post['isGlobalUpdate']) && is_multisite() && is_super_admin(); + $totalChanges = $validator->getTotalChangeCount(); + + //var_dump($totalChanges, $post); + //exit; + + if (($totalChanges <= 0) && !$shouldUpdateNetwork) { + $redirectParams['no-changes-made'] = 1; + wp_redirect($this->getTabUrl($redirectParams)); + exit; + } + + if ($shouldUpdateNetwork && ($totalChanges < 1)) { + $totalChanges = 1; //A network update is always at least one change. + } + + if ($this->backupsEnabled) { + //Make a backup before actually changing anything. + $currentUser = wp_get_current_user(); + $this->createRoleBackup(sprintf( + 'Automatic backup before applying %d changes made by %s', + $totalChanges, + $currentUser->user_login + )); + } + + //Save user defined capabilities. + if ($validator->getUserDefinedCaps() !== null) { + $this->saveUserDefinedCaps($validator->getUserDefinedCaps()); + } + + //Apply role changes. + //------------------- + $wpRoles = ameRoleUtils::get_roles(); + + //Delete roles. + foreach ($validator->getRolesToDelete() as $id => $role) { + $wpRoles->remove_role($id); + } + + //Create roles. + foreach ($validator->getRolesToCreate() as $id => $role) { + $wpRoles->add_role($id, $role['displayName'], $role['capabilities']); + } + + //Update role capabilities and display names. + $rolesToModify = $validator->getRolesToModify(); + if (!empty($rolesToModify)) { + foreach ($rolesToModify as $id => $role) { + //Rename the role. + if ($wpRoles->roles[$id]['name'] !== $role['displayName']) { + $wpRoles->roles[$id]['name'] = $role['displayName']; + } + $wpRoles->roles[$id]['capabilities'] = $role['capabilities']; + } + //Save role data. + update_option($wpRoles->role_key, $wpRoles->roles); + } + + //Apply role settings to all network sites if requested. We'll do that even if the settings + //weren't changed, which lets you use this feature to normalize role settings across the network. + if ($shouldUpdateNetwork) { + $result = $this->updateNetworkRoles(get_option($wpRoles->role_key)); + if (is_wp_error($result)) { + $errors[] = $result; + $this->storeSettingsErrors($errors); + } + } + + //Apply user changes. + //------------------- + + foreach ($validator->getUsersToModify() as $userId => $modifiedUser) { + $newCaps = $modifiedUser['capabilities']; + $newRoles = $modifiedUser['roles']; + $user = $validator->getExistingUser($userId); + + //We have to go through the trouble of removing/adding each role individually + //because some plugins use the "add_user_role" and "remove_user_role" hooks. + $oldRoles = (isset($user->roles) && is_array($user->roles)) ? $user->roles : array(); + $removedRoles = array_diff($oldRoles, $newRoles); + $addedRoles = array_diff($newRoles, $oldRoles); + foreach ($removedRoles as $role) { + $user->remove_role($role); + } + foreach ($addedRoles as $role) { + $user->add_role($role); + } + + //Now that the necessary hooks have been triggered, we can just overwrite the caps array. + //This is faster than calling add_cap() a bunch of times and it lets us precisely control + //the order of roles in the array. + $user->remove_all_caps(); + $user->roles = array(); + $user->caps = array_merge($newCaps, array_fill_keys($newRoles, true)); + + update_user_meta($user->ID, $user->cap_key, $user->caps); + $user->get_role_caps(); + $user->update_user_level_from_caps(); + } + + //Save editable roles. + $this->loadSettings(); + $this->settings['editableRoles'] = $validator->getNewEditableRoleSettings(); + $this->saveSettings(); + + $redirectParams['updated'] = 1; + wp_redirect($this->getTabUrl($redirectParams)); + exit; + + //TODO: Consider creating revisions in an update_option filter (flushed on shutdown), not here. Also in add_option. + //TODO: Maybe leave revision log formatting until time of display. It can be built based on change lists, I think. + + //TODO: It's still useful to take a backup/create a revision before updating if there have been no backups in X days. + //The roles could have changed while the plugin is inactive, in which case the previous revision could be out of date. + + //TODO: Save trashed roles. + + /* + * Remove all roles that don't exist in the role list. + For each existing role: + Add missing caps, remove deleted caps. + Keep a list of caps created with AME even if they're unused. + Create roles that don't exist. + Rename roles where the current name doesn't match the new one. + + Update user capabilities. + Update user roles (see validateUserRoleChange) + + (Seems straightforward. Could be done directly if API is too slow.) + Validation: + Only change editable roles. + Some capabilities are only available to super admins. + Cannot create capabilities with "<>&" characters. + Validate roles names and display names. + Don't delete the default role and used roles. + */ + } + + /** + * Update role settings across the entire Multisite network. Applies the same settings to all sites. + * + * @param mixed $roleData + * @return bool|WP_Error + */ + private function updateNetworkRoles($roleData) { + global $wpdb; + /** @var wpdb $wpdb */ + + if (!is_multisite()) { + return new WP_Error( + 'ame_not_multisite', + 'Cannot update roles on all sites because this is not a Multisite network.' + ); + } + + if (empty($roleData)) { + return new WP_Error('ame_invalid_role_data', 'Role data is invalid.'); + } + + if (!function_exists('get_sites')) { + return new WP_Error( + 'ame_wp_incompatible', + 'The plugin does not support this feature in the current WordPress version.' + ); + } + + //When a site uses an external object cache (e.g. Redis), we might need to remove the '$prefix_user_roles' + //option from the cache to force the caching plugin to load the updated roles from the database. + //This could be slow because it involves switching to each site, so it's off by default. + $shouldClearCache = is_callable('wp_cache_switch_to_blog') + && is_callable('wp_using_ext_object_cache') + && wp_using_ext_object_cache() + && apply_filters('admin_menu_editor-clear_role_cache', false); + $originalBlogId = is_callable('get_current_blog_id') ? get_current_blog_id() : null; + + $sites = get_sites(array( + /* + * As of this writing, WP documentation doesn't mention any officially supported way + * to make get_sites() return all available results. There are unofficial workarounds, + * but simply specifying a very high number should be good enough for most situations. + * We'll probably hit the PHP execution time limit before we hit the result limit. + */ + 'number' => 1000000, + 'fields' => 'ids', + )); + $serializedData = serialize($roleData); + + foreach ($sites as $siteId) { + $prefix = $wpdb->get_blog_prefix($siteId); + $tableName = $prefix . 'options'; + $optionName = $prefix . 'user_roles'; + + $query = $wpdb->prepare( + "UPDATE {$tableName} SET option_value = %s WHERE option_name = %s LIMIT 1", + array($serializedData, $optionName) + ); + + $result = $wpdb->query($query); + if ($result === false) { + $errorMessage = sprintf('Failed to update site with ID %d.', $siteId); + if (!empty($wpdb->last_error)) { + $errorMessage .= ' Database error: ' . $wpdb->last_error; + } + return new WP_Error('ame_db_error', $errorMessage); + } + + //Clear the option cache on the target site. + if ($shouldClearCache) { + wp_cache_switch_to_blog($siteId); + wp_cache_delete($optionName, 'options'); + } + } + + if ($shouldClearCache && ($originalBlogId !== null)) { + wp_cache_switch_to_blog($originalBlogId); + } + + return true; + } + + /** @noinspection PhpUnusedPrivateMethodInspection */ + private function tempGenerateChangeSummary($role = null) { + global $wpRoles; + $id = ''; + + if (!empty($rolesToDelete)) { + $deletedIds = array_keys($rolesToDelete); + if (count($deletedIds) === 1) { + $summary[] = 'Deleted ' . $deletedIds[0]; + } else if (count($deletedIds) === 2) { + $summary[] = 'Deleted ' . implode(' and ', $deletedIds); + } else { + $summary[] = 'Deleted ' . count($deletedIds) . ' roles'; + } + } + + if (!empty($rolesToCreate)) { + $createdIds = array_keys($rolesToCreate); + $summary[] = 'Created ' . $this->formatPhraseList($createdIds, '%d roles'); + } + + $renamedRoles[] = sprintf('"%1$s" to %2$s', $wpRoles->roles[$id]['name'], $role['displayName']); + + $oldGrantedCaps = array_filter($wpRoles->roles[$id]['capabilities']); + $newGrantedCaps = array_filter($role['capabilities']); + $addedCaps = array_diff_key($newGrantedCaps, $oldGrantedCaps); + $removedCaps = array_diff_key($oldGrantedCaps, $newGrantedCaps); + + $changes = array(); + if (!empty($addedCaps)) { + $changes[] = count($addedCaps) . ' added'; + } + if (!empty($removedCaps)) { + $changes[] = count($removedCaps) . ' removed'; + } + $capSummaries[] = $id . ' (' . implode(', ', $changes) . ')'; + + //Add renames and cap changes to the summary. + if (!empty($renamedRoles)) { + $summary[] = 'Renamed ' . $this->formatPhraseList($renamedRoles, '%d roles', 1); + } + if (!empty($capSummaries)) { + $summary[] = 'Changed capabilities: ' . implode(', ', $capSummaries); + } + } + + private function formatPhraseList($items, $combinedTemplate = '%d items', $limit = 2) { + $itemCount = count($items); + if ($itemCount === 1) { + return $items[0]; + } else if ($itemCount === 2) { + return implode(' and ', $items); + } else if ($itemCount <= $limit) { + return implode(', ', $items); + } + return sprintf($combinedTemplate, $itemCount); + } + + /** + * @param array $errors + */ + private function storeSettingsErrors($errors) { + if (empty($errors)) { + delete_transient(self::SETTINGS_ERROR_TRANSIENT); + return; + } + set_transient(self::SETTINGS_ERROR_TRANSIENT, $errors, 30 * 60); + $this->cachedSettingsErrors = $errors; + } + + /** + * @return array + */ + private function fetchSettingsErrors() { + $storedErrors = get_transient(self::SETTINGS_ERROR_TRANSIENT); + if ($storedErrors !== false) { + delete_transient(self::SETTINGS_ERROR_TRANSIENT); + $this->cachedSettingsErrors = (array)$storedErrors; + } + return $this->cachedSettingsErrors; + } + + protected function getTemplateVariables($templateName) { + $variables = parent::getTemplateVariables($templateName); + $variables['settingsErrors'] = $this->fetchSettingsErrors(); + return $variables; + } + + /** + * @param string $comment + * @param string|null $roleData + * @return bool + */ + private function createRoleBackup($comment = '', $roleData = null) { + //TODO: If we're going to do backups, remember to create the table if it doesn't exist. + //TODO: Delete the table when the plugin is uninstalled. + if ($roleData === null) { + $wpRoles = ameRoleUtils::get_roles(); + $roleData = get_option($wpRoles->role_key); + } + + /** @var wpdb */ + global $wpdb; + + $result = $wpdb->insert( + $this->backupTable, + array( + 'created_on' => gmdate('Y-m-d H:i:s'), + 'site_id' => get_current_blog_id(), + 'user_id' => get_current_user_id(), + 'role_data' => serialize($roleData), + 'comment' => $comment, + ), + array('%s', '%d', '%d', '%s', '%s') + ); + return ($result !== false); + } + + /** + * Delete old role data backups. + * + * We keep either the last 10 backups or the last 30 days of backups, whichever is greater. + */ + public function deleteOldRoleBackups() { + /** @var wpdb */ + global $wpdb; + + $nthItemDate = $wpdb->get_var( + 'SELECT created_on FROM ' . $this->backupTable + . ' WHERE 1 ORDER BY created_on DESC LIMIT 1 OFFSET 10' + ); + + if (empty($nthItemDate)) { + return; //There are 10 or fewer backups. Do nothing. + } + + $survivalThreshold = gmdate( + 'Y-m-d H:i:s', + min(strtotime($nthItemDate . ' UTC'), strtotime('-30 days')) + ); + + $wpdb->query($wpdb->prepare( + 'DELETE FROM ' . $this->backupTable . + ' WHERE created_on <= %s', $survivalThreshold + )); + } + + /** + * Check if a user can access the role editor module. + * + * @param int|null $userId Optional user ID. Defaults to the current user. + * @return bool + */ + private function userCanAccessModule($userId = null) { + if (($userId === null) || ($userId === get_current_user_id())) { + return $this->menuEditor->current_user_can_edit_menu() && current_user_can(self::REQUIRED_CAPABILITY); + } + $user = get_user_by('id', $userId); + if (!$user) { + return false; + } + return $this->menuEditor->user_can_edit_menu($userId) + && $user->has_cap(self::REQUIRED_CAPABILITY); + } + + private function saveUserDefinedCaps($capabilities) { + delete_site_option(self::USER_DEFINED_CAP_KEY); + + if (empty($capabilities)) { + return; + } + update_site_option(self::USER_DEFINED_CAP_KEY, $capabilities); + } + + /** + * Get a list of capabilities that were created by the user(s). + * + * @return array [capabilityName => arbitraryValue] + */ + private function getUserDefinedCaps() { + $caps = get_site_option(self::USER_DEFINED_CAP_KEY, array()); + if (!is_array($caps)) { + return array(); + } + return $caps; + } + + /** + * Apply "editable roles" settings. + * + * @param array $editableRoles + * @return array + */ + public function filterEditableRoles($editableRoles) { + //Sanity check: The role list should be an array or something array-like. + if ( !is_array($editableRoles) && !($editableRoles instanceof Traversable) ) { + return $editableRoles; + } + + //Do nothing if the core user API hasn't been loaded yet. There is at least one plugin that tries to get + //editable roles before WordPress loads the user API and determines which user is logged in. + if ( !is_callable('wp_get_current_user') ) { + return $editableRoles; + } + + // Do nothing if the overall strategy is "none" ("leave unchanged"). + $user = wp_get_current_user(); + if ( + isset($this->cachedOverallEditableRoleStrategy[$user->ID]) + && ($this->cachedOverallEditableRoleStrategy[$user->ID] === 'none') + ) { + return $editableRoles; + } + + //It's possible that another plugin has already removed some roles from the array. We'll need the full list + //so that we can restore enabled roles if the user has selected the "user-defined-list" strategy. + if (function_exists('wp_roles')) { + $allRoles = array_merge(wp_roles()->roles, $editableRoles); + } else { + $allRoles = $editableRoles; + } + + //Try the cache first. + if ( isset($this->cachedEditableRoles[$user->ID]) ) { + return $this->cachedEditableRoles[$user->ID]->filter($allRoles, $editableRoles); + } + + //A super admin always has full access to everything. Do not remove any roles. + if (is_multisite() && is_super_admin()) { + return $editableRoles; + } + + $settings = ameUtils::get($this->loadSettings(), 'editableRoles', array()); + $userRoles = $this->menuEditor->get_user_roles($user); + + //User-specific settings have precedence. + //For users, "auto" means "use role settings". + $userActorId = 'user:' . $user->user_login; + if ( ameUtils::get($settings, array($userActorId, 'strategy'), 'auto') !== 'auto' ) { + $userSettings = $settings[$userActorId]; + if ($userSettings['strategy'] === 'none') { + //Leave the roles unchanged. + $this->cachedOverallEditableRoleStrategy[$user->ID] = 'none'; + return $editableRoles; + } else if ($userSettings['strategy'] === 'user-defined-list') { + //Allow editing only those roles that are on the list. + $filteredResult = array(); + $allowedRoles = ameUtils::get($userSettings, 'userDefinedList', array()); + foreach($allRoles as $roleId => $role) { + if ( isset($allowedRoles[$roleId]) ) { + $filteredResult[$roleId] = $role; + } + } + $this->cachedEditableRoles[$user->ID] = new ameEditableRoleReplacer( + array_fill_keys(array_keys($filteredResult), true) + ); + $this->cachedOverallEditableRoleStrategy[$user->ID] = 'user-defined-list'; + return $filteredResult; + } + //We'll only reach this line if the user's strategy setting is not valid. + //In that case, let's leave the role list unchanged. + return $editableRoles; + } + + $leaveUnchanged = true; + $hasAnyUserDefinedList = false; + $autoDisabledRoles = array(); + $filteredResult = array(); + + foreach($allRoles as $roleId => $role) { + $wasEnabled = isset($editableRoles[$roleId]); + $canAutoDisable = false; + + //Include this role if at least one of the user's roles is allowed to edit it. + foreach($userRoles as $userRoleId) { + $actorId = 'role:' . $userRoleId; + + $strategy = ameUtils::get($settings, array($actorId, 'strategy'), 'auto'); + $leaveUnchanged = $leaveUnchanged && ($strategy === 'none'); + + if ($strategy === 'user-defined-list') { + $hasAnyUserDefinedList = true; + if ( isset($settings[$actorId]['userDefinedList'][$roleId]) ) { + $filteredResult[$roleId] = $role; + break; + } + } else if ( ($strategy === 'auto') ) { + //Shortcut: A user with role X can assign role X to other users (assuming that they can edit users). + if ( $roleId === $userRoleId ) { + $shouldLeaveEnabled = true; + } else { + //Does the target role have the same or fewer capabilities as the user's role? + $targetCaps = $this->getEnabledCoreCapabilitiesForRole($roleId, $role); + $sameCaps = array_intersect_key( + $targetCaps, + $this->getEnabledCoreCapabilitiesForRole($userRoleId) + ); + $shouldLeaveEnabled = (count($sameCaps) === count($targetCaps)); + } + + $canAutoDisable = !$shouldLeaveEnabled; + if ( $wasEnabled && $shouldLeaveEnabled ) { + $filteredResult[$roleId] = $role; + break; + } + } else if ($strategy === 'none') { + if ($wasEnabled) { + $filteredResult[$roleId] = $role; + } + } + } + + if ($canAutoDisable && !isset($filteredRoles[$roleId])) { + $autoDisabledRoles[] = $roleId; + } + } + + //Are all of the roles set to "none" = leave unchanged? + if ($leaveUnchanged) { + $this->cachedOverallEditableRoleStrategy[$user->ID] = 'none'; + return $editableRoles; + } + + $overallStrategy = $hasAnyUserDefinedList ? 'user-defined-list' : 'auto'; + $this->cachedOverallEditableRoleStrategy[$user->ID] = $overallStrategy; + + //We won't need the capability cache again unless something changes or replaces the current user mid-request. + //That's probably going to be rare, so we can throw away the cache to free up some RAM. + $this->cachedEnabledRoleCaps = array(); + //Update the user cache. + if ($overallStrategy === 'auto') { + $this->cachedEditableRoles[$user->ID] = new ameEditableRoleLimiter($autoDisabledRoles); + } else { + $this->cachedEditableRoles[$user->ID] = new ameEditableRoleReplacer( + array_fill_keys(array_keys($filteredResult), true) + ); + } + + if (!$this->isRoleCacheClearingHookSet) { + $this->isRoleCacheClearingHookSet = true; + //Clear cache when user roles or capabilities change. + add_action('updated_user_meta', array($this, 'clearEditableRoleCache'), 10, 0); + add_action('deleted_user_meta', array($this, 'clearEditableRoleCache'), 10, 0); + //Clear cache when switching to another site because users can have different roles + //on different sites. + add_action('switch_blog', array($this, 'clearEditableRoleCache'), 10, 0); + } + + return $filteredResult; + } + + /** + * @param string $roleId + * @param array|null $roleData + * @return boolean[] + */ + private function getEnabledCoreCapabilitiesForRole($roleId, $roleData = null) { + if (isset($this->cachedEnabledRoleCaps[$roleId])) { + return $this->cachedEnabledRoleCaps[$roleId]; + } + + if ($roleData) { + $capabilities = isset($roleData['capabilities']) ? $roleData['capabilities'] : null; + } else { + $roleObject = get_role($roleId); + $capabilities = isset($roleObject->capabilities) ? $roleObject->capabilities : null; + } + if (!isset($capabilities)) { + return array(); + } + + $enabledCaps = array_filter($capabilities); + + //Keep only core capabilities like "edit_posts" and filter out custom capabilities added by plugins or themes. + $enabledCaps = array_intersect_key($enabledCaps, $this->getDefaultCapabilities()); + + $this->cachedEnabledRoleCaps[$roleId] = $enabledCaps; + return $enabledCaps; + } + + /** + * Get roles that the current user can edit in the role editor. Unlike get_editable_roles(), this method should + * include any special roles that do not show up in the role list when editing a user, like the forum roles + * created by bbPress. + * + * @return array + */ + private function getEffectiveEditableRoles() { + $editableRoles = get_editable_roles(); + if (empty($editableRoles) || !is_array($editableRoles)) { + $editableRoles = array(); + } + if (function_exists('bbp_get_dynamic_roles')) { + $userId = get_current_user_id(); + if ( + !isset($this->cachedOverallEditableRoleStrategy[$userId]) + || ($this->cachedOverallEditableRoleStrategy[$userId] !== 'user-defined-list') + ) { + $bbPressRoles = bbp_get_dynamic_roles(); + $editableRoles = array_merge($bbPressRoles, $editableRoles); + } + } + return $editableRoles; + } + + public function clearEditableRoleCache() { + $this->cachedEditableRoles = array(); + $this->cachedOverallEditableRoleStrategy = array(); + $this->cachedEnabledRoleCaps = array(); + } + + /** + * Prevent less-privileged users from editing more privileged users. + * + * @param array $requiredCaps List of primitive capabilities (output). + * @param string $capability The meta capability (input). + * @param int $thisUserId The user that's trying to do something. + * @param array $args + * @return array + */ + public function restrictUserEditing($requiredCaps, $capability, $thisUserId, $args) { + static $editUserCaps = array('edit_user', 'delete_user', 'promote_user'); + if (!in_array($capability, $editUserCaps) || !isset($args[0])) { + return $requiredCaps; + } + + /** @var int $targetUserId The user that might be edited or deleted. */ + $targetUserId = intval($args[0]); + + $thisUserId = intval($thisUserId); + $isMultisite = is_multisite(); + $isSuperAdmin = $isMultisite && is_super_admin($thisUserId); + + //Super Admins can edit everything. + if ($isSuperAdmin) { + return $requiredCaps; + } + + $accessDenied = array_merge($requiredCaps, array('do_not_allow')); + + //Only Super Admins can edit other Super Admins. + if ($isMultisite && is_super_admin($targetUserId) && !$isSuperAdmin) { + return $accessDenied; + } + + //Users that don't have access to the role editor can't edit users that do have access. + if (!$this->userCanAccessModule($thisUserId) && $this->userCanAccessModule($targetUserId)) { + return $accessDenied; + } + + //Finally, a user can only edit those other users that have an editable role. + //This part only works with the current user because get_editable_roles() does not take a user parameter. + if (($thisUserId === get_current_user_id()) && ($thisUserId !== $targetUserId) ) { + //The file that defines get_editable_roles() is only loaded in the admin back-end even though + //the "edit_user" capability is also used in the front-end, e.g. when adding an "Edit" link + //to the Toolbar/Admin Bar on author pages. + if (!function_exists('get_editable_roles')) { + return $requiredCaps; + } + $editableRoles = get_editable_roles(); + + $strategy = 'auto'; + if ( isset($this->cachedOverallEditableRoleStrategy[$thisUserId]) ) { + $strategy = $this->cachedOverallEditableRoleStrategy[$thisUserId]; + } + + //Don't apply any further restrictions if all editable role settings are set to "leave unchanged" + //for this user. + if ($strategy === 'none') { + return $requiredCaps; + } + + if (function_exists('bbp_get_dynamic_roles')) { + $bbPressRoles = bbp_get_dynamic_roles(); + } else { + $bbPressRoles = array(); + } + + $targetUser = get_user_by('id', $targetUserId); + $roles = (isset($targetUser->roles) && is_array($targetUser->roles)) ? $targetUser->roles : array(); + foreach($roles as $roleId) { + /* + * = bbPress compatibility fix = + * + * bbPress always removes its special roles (like "Participant") from the editable role list. As far + * as I can tell, the intent is to prevent people from bypassing bbPress settings and manually giving + * those roles to users. + * + * This should not automatically prevent administrators from editing users who have any bbPress roles. + * Therefore, let's ignore bbPress roles here unless the user has custom editable role settings. + */ + if (array_key_exists($roleId, $bbPressRoles) && ($strategy !== 'user-defined-list')) { + continue; + } + + if (!array_key_exists($roleId, $editableRoles)) { + return $accessDenied; + } + } + } + + return $requiredCaps; + } + + public function getExportOptionLabel() { + return '"Editable Roles" settings'; + } +} + +interface ameEditableRoleFilter { + /** + * @param array<string, array> $allRoles + * @param array<string, array> $editableRoles + * @return array<string, array> Filtered editable roles. + */ + public function filter($allRoles, $editableRoles); +} + +/** + * Replaces the list of editable roles with the specified list. + * Any changes that were made by other plugins will be overwritten. + */ +class ameEditableRoleReplacer implements ameEditableRoleFilter { + private $enabledRoles; + + /** + * @param array<string,mixed> $enabledRoles + */ + public function __construct($enabledRoles) { + $this->enabledRoles = $enabledRoles; + } + + public function filter($allRoles, $editableRoles) { + $result = array(); + foreach ($allRoles as $roleId => $role) { + if ( isset($this->enabledRoles[$roleId]) ) { + $result[$roleId] = $role; + } + } + return $result; + } + +} + +/** + * Removes the specified roles from the list of editable roles. + */ +class ameEditableRoleLimiter implements ameEditableRoleFilter { + private $rolesToRemove; + + /** + * @param string[] $rolesToRemove + */ + public function __construct($rolesToRemove) { + $this->rolesToRemove = $rolesToRemove; + } + + public function filter($allRoles, $editableRoles) { + foreach ($this->rolesToRemove as $roleId) { + if ( array_key_exists($roleId, $editableRoles) ) { + unset($editableRoles[$roleId]); + } + } + return $editableRoles; + } +} \ No newline at end of file diff --git a/extras/modules/role-editor/data/capability-excerpt.sqlite3 b/extras/modules/role-editor/data/capability-excerpt.sqlite3 new file mode 100644 index 0000000..914c113 Binary files /dev/null and b/extras/modules/role-editor/data/capability-excerpt.sqlite3 differ diff --git a/extras/modules/role-editor/data/core-categories.txt b/extras/modules/role-editor/data/core-categories.txt new file mode 100644 index 0000000..7ef3256 --- /dev/null +++ b/extras/modules/role-editor/data/core-categories.txt @@ -0,0 +1,63 @@ +Administration + edit_dashboard + export + import + manage_options + moderate_comments + unfiltered_html + update_core + +Plugins + activate_plugins + delete_plugins + edit_plugins + install_plugins + update_plugins + +Themes + delete_themes + edit_themes + edit_theme_options + install_themes + update_themes + switch_themes + +Users + add_users + create_users + delete_users + edit_users + list_users + promote_users + remove_users + +Multisite [default/multisite] + create_sites + delete_sites + manage_sites + manage_network + manage_network_users + manage_network_themes + manage_network_plugins + manage_network_options + upgrade_network + +Other [default/other] + read + upload_files + manage_links + unfiltered_upload + +Deprecated [default/deprecated] + edit_files + level_0 + level_1 + level_2 + level_3 + level_4 + level_5 + level_6 + level_7 + level_8 + level_9 + level_10 \ No newline at end of file diff --git a/extras/modules/role-editor/data/default-capabilities.txt b/extras/modules/role-editor/data/default-capabilities.txt new file mode 100644 index 0000000..8464582 --- /dev/null +++ b/extras/modules/role-editor/data/default-capabilities.txt @@ -0,0 +1,61 @@ +activate_plugins +create_users +delete_others_pages +delete_others_posts +delete_pages +delete_plugins +delete_posts +delete_private_pages +delete_private_posts +delete_published_pages +delete_published_posts +delete_themes +delete_users +edit_dashboard +edit_files +edit_others_pages +edit_others_posts +edit_pages +edit_plugins +edit_posts +edit_private_pages +edit_private_posts +edit_published_pages +edit_published_posts +edit_theme_options +edit_themes +edit_users +export +import +install_plugins +install_themes +level_0 +level_1 +level_10 +level_2 +level_3 +level_4 +level_5 +level_6 +level_7 +level_8 +level_9 +list_users +manage_categories +manage_links +manage_options +moderate_comments +promote_users +publish_pages +publish_posts +read +read_private_pages +read_private_posts +remove_users +switch_themes +unfiltered_html +unfiltered_upload +update_core +update_plugins +update_themes +upload_files \ No newline at end of file diff --git a/extras/modules/role-editor/data/default-multisite-capabilities.txt b/extras/modules/role-editor/data/default-multisite-capabilities.txt new file mode 100644 index 0000000..a9e9fe7 --- /dev/null +++ b/extras/modules/role-editor/data/default-multisite-capabilities.txt @@ -0,0 +1,9 @@ +create_sites +delete_sites +manage_sites +manage_network +manage_network_users +manage_network_themes +manage_network_plugins +manage_network_options +upgrade_network \ No newline at end of file diff --git a/extras/modules/role-editor/data/default-role-capabilities-4.1.txt b/extras/modules/role-editor/data/default-role-capabilities-4.1.txt new file mode 100644 index 0000000..fc59dc3 --- /dev/null +++ b/extras/modules/role-editor/data/default-role-capabilities-4.1.txt @@ -0,0 +1,127 @@ +administrator + switch_themes + edit_themes + activate_plugins + edit_plugins + edit_users + edit_files + manage_options + moderate_comments + manage_categories + manage_links + upload_files + import + + //Note: On Multisite, only Super Admins have the unfiltered_html capability. + unfiltered_html + + edit_posts + edit_others_posts + edit_published_posts + publish_posts + edit_pages + read + level_10 + level_9 + level_8 + level_7 + level_6 + level_5 + level_4 + level_3 + level_2 + level_1 + level_0 + edit_others_pages + edit_published_pages + publish_pages + delete_pages + delete_others_pages + delete_published_pages + delete_posts + delete_others_posts + delete_published_posts + delete_private_posts + edit_private_posts + read_private_posts + delete_private_pages + edit_private_pages + read_private_pages + delete_users + create_users + unfiltered_upload + edit_dashboard + update_plugins + delete_plugins + install_plugins + update_themes + install_themes + update_core + list_users + remove_users + add_users + promote_users + edit_theme_options + delete_themes + export + +editor + moderate_comments + manage_categories + manage_links + upload_files + edit_posts + edit_others_posts + edit_published_posts + publish_posts + edit_pages + read + level_7 + level_6 + level_5 + level_4 + level_3 + level_2 + level_1 + level_0 + edit_others_pages + edit_published_pages + publish_pages + delete_pages + delete_others_pages + delete_published_pages + delete_posts + delete_others_posts + delete_published_posts + delete_private_posts + edit_private_posts + read_private_posts + delete_private_pages + edit_private_pages + read_private_pages + + //Note: On Multisite, only the Super Admin has unfiltered_html. + unfiltered_html + +author + upload_files + edit_posts + edit_published_posts + publish_posts + read + level_2 + level_1 + level_0 + delete_posts + delete_published_posts + +contributor + edit_posts + read + level_1 + level_0 + delete_posts + +subscriber + read + level_0 \ No newline at end of file diff --git a/extras/modules/role-editor/data/deprecated-capabilities.txt b/extras/modules/role-editor/data/deprecated-capabilities.txt new file mode 100644 index 0000000..6bbdcf3 --- /dev/null +++ b/extras/modules/role-editor/data/deprecated-capabilities.txt @@ -0,0 +1,12 @@ +level_0 +level_1 +level_10 +level_2 +level_3 +level_4 +level_5 +level_6 +level_7 +level_8 +level_9 +edit_files \ No newline at end of file diff --git a/extras/modules/role-editor/data/plugin-capabilities.csv b/extras/modules/role-editor/data/plugin-capabilities.csv new file mode 100644 index 0000000..0f336a0 --- /dev/null +++ b/extras/modules/role-editor/data/plugin-capabilities.csv @@ -0,0 +1,5482 @@ +0,len-slider,1000,Len Slider +Customer,wp-shop-original,3000,WP Shop +FlAG Add skins,flash-album-gallery,20000,Gallery – Flagallery Photo Portfolio +FlAG Change options,flash-album-gallery,20000,Gallery – Flagallery Photo Portfolio +FlAG Change skin,flash-album-gallery,20000,Gallery – Flagallery Photo Portfolio +FlAG Delete skins,flash-album-gallery,20000,Gallery – Flagallery Photo Portfolio +FlAG Import folder,flash-album-gallery,20000,Gallery – Flagallery Photo Portfolio +FlAG Manage banners,flash-album-gallery,20000,Gallery – Flagallery Photo Portfolio +FlAG Manage gallery,flash-album-gallery,20000,Gallery – Flagallery Photo Portfolio +FlAG Manage music,flash-album-gallery,20000,Gallery – Flagallery Photo Portfolio +FlAG Manage others gallery,flash-album-gallery,20000,Gallery – Flagallery Photo Portfolio +FlAG Manage video,flash-album-gallery,20000,Gallery – Flagallery Photo Portfolio +FlAG Upload images,flash-album-gallery,20000,Gallery – Flagallery Photo Portfolio +FlAG Use TinyMCE,flash-album-gallery,20000,Gallery – Flagallery Photo Portfolio +FlAG iFrame page,flash-album-gallery,20000,Gallery – Flagallery Photo Portfolio +FlAG overview,flash-album-gallery,20000,Gallery – Flagallery Photo Portfolio +NextGEN Attach Interface,nextgen-gallery,900000,WordPress Gallery Plugin – NextGEN Gallery +NextGEN Change options,nextcellent-gallery-nextgen-legacy,20000,NextCellent Gallery – NextGEN Legacy +NextGEN Change options,nextgen-gallery,900000,WordPress Gallery Plugin – NextGEN Gallery +NextGEN Change style,nextcellent-gallery-nextgen-legacy,20000,NextCellent Gallery – NextGEN Legacy +NextGEN Change style,nextgen-gallery,900000,WordPress Gallery Plugin – NextGEN Gallery +NextGEN Edit album,nextcellent-gallery-nextgen-legacy,20000,NextCellent Gallery – NextGEN Legacy +NextGEN Edit album,nextgen-gallery,900000,WordPress Gallery Plugin – NextGEN Gallery +NextGEN Gallery overview,nextcellent-gallery-nextgen-legacy,20000,NextCellent Gallery – NextGEN Legacy +NextGEN Gallery overview,nextgen-gallery,900000,WordPress Gallery Plugin – NextGEN Gallery +NextGEN Manage gallery,nextcellent-gallery-nextgen-legacy,20000,NextCellent Gallery – NextGEN Legacy +NextGEN Manage gallery,nextgen-gallery,900000,WordPress Gallery Plugin – NextGEN Gallery +NextGEN Manage others gallery,nextcellent-gallery-nextgen-legacy,20000,NextCellent Gallery – NextGEN Legacy +NextGEN Manage others gallery,nextgen-gallery,900000,WordPress Gallery Plugin – NextGEN Gallery +NextGEN Manage tags,nextcellent-gallery-nextgen-legacy,20000,NextCellent Gallery – NextGEN Legacy +NextGEN Manage tags,nextgen-gallery,900000,WordPress Gallery Plugin – NextGEN Gallery +NextGEN Upload images,nextcellent-gallery-nextgen-legacy,20000,NextCellent Gallery – NextGEN Legacy +NextGEN Upload images,nextgen-gallery,900000,WordPress Gallery Plugin – NextGEN Gallery +NextGEN Use TinyMCE,nextcellent-gallery-nextgen-legacy,20000,NextCellent Gallery – NextGEN Legacy +NextGEN Use TinyMCE,nextgen-gallery,900000,WordPress Gallery Plugin – NextGEN Gallery +Nginx Helper | Config,nginx-helper,60000,Nginx Helper +Nginx Helper | Purge cache,nginx-helper,60000,Nginx Helper +Show_Admin_Bar_in_Front,dd-roles,200,DD Roles +UM Configure Plugin,user-messages,400,User Messages +UM Ignore Public Msg,user-messages,400,User Messages +UM Receive Msg,user-messages,400,User Messages +UM Refuse Private Msg,user-messages,400,User Messages +UM Send Email Msg,user-messages,400,User Messages +UM Send Private Msg,user-messages,400,User Messages +UM Send Public Msg,user-messages,400,User Messages +UM Use Plugin,user-messages,400,User Messages +WP-CRM: Add User Messages,wp-crm,4000,WP-CRM – Customer Relations Management for WordPress +WP-CRM: Change Color Scheme,wp-crm,4000,WP-CRM – Customer Relations Management for WordPress +WP-CRM: Change Passwords,wp-crm,4000,WP-CRM – Customer Relations Management for WordPress +WP-CRM: Change Role,wp-crm,4000,WP-CRM – Customer Relations Management for WordPress +WP-CRM: Manage Settings,wp-crm,4000,WP-CRM – Customer Relations Management for WordPress +WP-CRM: Perform Advanced Functions,wp-crm,4000,WP-CRM – Customer Relations Management for WordPress +WP-CRM: Send Group Message,wp-crm,4000,WP-CRM – Customer Relations Management for WordPress +WP-CRM: View Detailed Logs,wp-crm,4000,WP-CRM – Customer Relations Management for WordPress +WP-CRM: View Messages,wp-crm,4000,WP-CRM – Customer Relations Management for WordPress +WP-CRM: View Overview,wp-crm,4000,WP-CRM – Customer Relations Management for WordPress +WP-CRM: View Profiles,wp-crm,4000,WP-CRM – Customer Relations Management for WordPress +_debug_objects,debug-objects,800,Debug Objects +_wswebinar_changesettings,wp-webinarsystem,2000,WP WebinarSystem +_wswebinar_createwebinars,wp-webinarsystem,2000,WP WebinarSystem +_wswebinar_managequestions,wp-webinarsystem,2000,WP WebinarSystem +_wswebinar_managesubscribers,wp-webinarsystem,2000,WP WebinarSystem +access_automate_mautic,automate-mautic,400,AutomatePlug – Mautic for WordPress +access_gtmetrix,gtmetrix-for-wordpress,10000,GTmetrix for WordPress +access_kingcomposer,kingcomposer,70000,Page Builder: KingComposer – Free Drag and Drop page builder by King-Theme +access_mageewp_page_layout,mageewp-page-layout,3000,Mageewp Page Layout +access_masterslider,master-slider,100000,Master Slider – Responsive Touch Slider +access_s2member_level0,s2member,30000,"s2Member Framework (Member Roles, Capabilities, Membership, PayPal Members)" +access_s2member_level1,s2member,30000,"s2Member Framework (Member Roles, Capabilities, Membership, PayPal Members)" +access_s2member_level2,s2member,30000,"s2Member Framework (Member Roles, Capabilities, Membership, PayPal Members)" +access_s2member_level3,s2member,30000,"s2Member Framework (Member Roles, Capabilities, Membership, PayPal Members)" +access_s2member_level4,s2member,30000,"s2Member Framework (Member Roles, Capabilities, Membership, PayPal Members)" +access_server_browser,download-manager,100000,WordPress Download Manager +access_touchpoints,ukuupeople-the-simple-crm,1000,CRM: Contact Management Simplified – UkuuPeople +access_ukuupeoples,ukuupeople-the-simple-crm,1000,CRM: Contact Management Simplified – UkuuPeople +access_woocommerce_pos,woocommerce-pos,7000,WooCommerce POS +access_zopim,zopim-live-chat,90000,Zendesk Chat +activate_modules,site-editor,300,Site Editor – WordPress Site Builder – Theme Builder and Page Builder +activate_wordpoints_extensions,wordpoints,700,WordPoints +activate_wordpoints_modules,wordpoints,700,WordPoints +add-to-head,per-page-add-to,20000,Per page add to head +add_book-reviews,book-review-library,700,Book Review Library +add_discount_cap,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +add_multiple_datasets,projectmanager,400,ProjectManager +add_users,wp-real-estate,400,WP Real Estate +add_yop_poll_votes,yop-poll,20000,YOP Poll +admin intel,intelligence,600,Intelligence +admin-post-highlights,post-highlights,200,post highlights +admin_albo,albo-pretorio-on-line,2000,Albo Pretorio On line +admin_simple_tags,simple-tags,100000,Simple Tags +admin_wp_cn_kit,wp-kit-cn,200,WP Kit CN +admin_wp_copy,copy-link,2000,CopyLink +admin_zerobs_customers,zero-bs-crm,1000,Zero BS WordPress CRM +admin_zerobs_customers_tags,zero-bs-crm,1000,Zero BS WordPress CRM +admin_zerobs_events,zero-bs-crm,1000,Zero BS WordPress CRM +admin_zerobs_forms,zero-bs-crm,1000,Zero BS WordPress CRM +admin_zerobs_invoices,zero-bs-crm,1000,Zero BS WordPress CRM +admin_zerobs_logs_addedit,zero-bs-crm,1000,Zero BS WordPress CRM +admin_zerobs_logs_delete,zero-bs-crm,1000,Zero BS WordPress CRM +admin_zerobs_mailcampaigns,zero-bs-crm,1000,Zero BS WordPress CRM +admin_zerobs_manage_options,zero-bs-crm,1000,Zero BS WordPress CRM +admin_zerobs_notifications,zero-bs-crm,1000,Zero BS WordPress CRM +admin_zerobs_quotes,zero-bs-crm,1000,Zero BS WordPress CRM +admin_zerobs_sendemails_contacts,zero-bs-crm,1000,Zero BS WordPress CRM +admin_zerobs_transactions,zero-bs-crm,1000,Zero BS WordPress CRM +admin_zerobs_usr,zero-bs-crm,1000,Zero BS WordPress CRM +admin_zerobs_view_customers,zero-bs-crm,1000,Zero BS WordPress CRM +admin_zerobs_view_events,zero-bs-crm,1000,Zero BS WordPress CRM +admin_zerobs_view_invoices,zero-bs-crm,1000,Zero BS WordPress CRM +admin_zerobs_view_quotes,zero-bs-crm,1000,Zero BS WordPress CRM +admin_zerobs_view_transactions,zero-bs-crm,1000,Zero BS WordPress CRM +administer_awesome_support,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +administrator,advanced-access-manager,90000,Advanced Access Manager +administrator,developer-mode,1000,Developer Mode +adrotate_ad_delete,adrotate,50000,AdRotate Banner Manager +adrotate_ad_manage,adrotate,50000,AdRotate Banner Manager +adrotate_group_delete,adrotate,50000,AdRotate Banner Manager +adrotate_group_manage,adrotate,50000,AdRotate Banner Manager +advanced_ads_edit_ads,advanced-ads,70000,Advanced Ads – Ad Manager with AdSense Integration +advanced_ads_manage_options,advanced-ads,70000,Advanced Ads – Ad Manager with AdSense Integration +advanced_ads_manage_placements,advanced-ads,70000,Advanced Ads – Ad Manager with AdSense Integration +advanced_ads_place_ads,advanced-ads,70000,Advanced Ads – Ad Manager with AdSense Integration +advanced_ads_see_interface,advanced-ads,70000,Advanced Ads – Ad Manager with AdSense Integration +aff_access,affiliates,6000,Affiliates +aff_admin_affiliates,affiliates,6000,Affiliates +aff_admin_options,affiliates,6000,Affiliates +ag_manage_advanced,age-gate,7000,Age Gate +ag_manage_appearance,age-gate,7000,Age Gate +ag_manage_messaging,age-gate,7000,Age Gate +ag_manage_restrictions,age-gate,7000,Age Gate +ag_manage_set_content_bypass,age-gate,7000,Age Gate +ag_manage_set_content_restriction,age-gate,7000,Age Gate +ag_manage_set_custom_age,age-gate,7000,Age Gate +ag_manage_settings,age-gate,7000,Age Gate +aiosp_manage_seo,all-in-one-seo-pack,2000000,All in One SEO Pack +akp_edit_five,adkingpro,600,Ad King Pro +akp_edit_four,adkingpro,600,Ad King Pro +akp_edit_one,adkingpro,600,Ad King Pro +akp_edit_three,adkingpro,600,Ad King Pro +akp_edit_two,adkingpro,600,Ad King Pro +answer_chat,yith-live-chat,1000,YITH Live Chat +ap_approve_comment,anspress-question-answer,4000,AnsPress – Question and answer +ap_change_status,anspress-question-answer,4000,AnsPress – Question and answer +ap_change_status_other,anspress-question-answer,4000,AnsPress – Question and answer +ap_delete_answer,anspress-question-answer,4000,AnsPress – Question and answer +ap_delete_comment,anspress-question-answer,4000,AnsPress – Question and answer +ap_delete_others_answer,anspress-question-answer,4000,AnsPress – Question and answer +ap_delete_others_comment,anspress-question-answer,4000,AnsPress – Question and answer +ap_delete_others_question,anspress-question-answer,4000,AnsPress – Question and answer +ap_delete_post_permanent,anspress-question-answer,4000,AnsPress – Question and answer +ap_delete_question,anspress-question-answer,4000,AnsPress – Question and answer +ap_edit_answer,anspress-question-answer,4000,AnsPress – Question and answer +ap_edit_comment,anspress-question-answer,4000,AnsPress – Question and answer +ap_edit_others_answer,anspress-question-answer,4000,AnsPress – Question and answer +ap_edit_others_comment,anspress-question-answer,4000,AnsPress – Question and answer +ap_edit_others_question,anspress-question-answer,4000,AnsPress – Question and answer +ap_edit_question,anspress-question-answer,4000,AnsPress – Question and answer +ap_new_answer,anspress-question-answer,4000,AnsPress – Question and answer +ap_new_comment,anspress-question-answer,4000,AnsPress – Question and answer +ap_new_question,anspress-question-answer,4000,AnsPress – Question and answer +ap_no_moderation,anspress-question-answer,4000,AnsPress – Question and answer +ap_read_answer,anspress-question-answer,4000,AnsPress – Question and answer +ap_read_comment,anspress-question-answer,4000,AnsPress – Question and answer +ap_read_question,anspress-question-answer,4000,AnsPress – Question and answer +ap_restore_posts,anspress-question-answer,4000,AnsPress – Question and answer +ap_toggle_best_answer,anspress-question-answer,4000,AnsPress – Question and answer +ap_toggle_featured,anspress-question-answer,4000,AnsPress – Question and answer +ap_upload_cover,anspress-question-answer,4000,AnsPress – Question and answer +ap_view_moderate,anspress-question-answer,4000,AnsPress – Question and answer +ap_view_private,anspress-question-answer,4000,AnsPress – Question and answer +ap_vote_close,anspress-question-answer,4000,AnsPress – Question and answer +ap_vote_down,anspress-question-answer,4000,AnsPress – Question and answer +ap_vote_flag,anspress-question-answer,4000,AnsPress – Question and answer +ap_vote_up,anspress-question-answer,4000,AnsPress – Question and answer +armember,armember-membership,200,ARMember – Content Restriction & Membership Plugin +asa1_delete_collections,amazonsimpleadmin,6000,AmazonSimpleAdmin +asa1_edit_cache,amazonsimpleadmin,6000,AmazonSimpleAdmin +asa1_edit_collections,amazonsimpleadmin,6000,AmazonSimpleAdmin +asa1_edit_options,amazonsimpleadmin,6000,AmazonSimpleAdmin +asa1_edit_setup,amazonsimpleadmin,6000,AmazonSimpleAdmin +assign_achievement_events,achievements,300,Achievements for WordPress +assign_ad_terms,apply-online,5000,Apply Online +assign_agent_terms,essential-real-estate,3000,Essential Real Estate +assign_archive_structure,archive,700,Archive +assign_awebooking_terms,awebooking,6000,AweBooking – Hotel Booking System +assign_book_terms,novelist,800,Novelist +assign_campaign_terms,charitable,10000,Charitable – Donation Plugin +assign_cannedresponse_category,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +assign_cannedresponse_tag,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +assign_car_listing_terms,wp-car-manager,3000,WP Car Manager +assign_classified_listing_terms,classifieds-wp,800,Classifieds WP +assign_client_terms,upstream,1000,WordPress Project Management by UpStream +assign_contact_country,wp-easy-contact,600,Best Contact Management Software for WordPress +assign_contact_state,wp-easy-contact,600,Best Contact Management Software for WordPress +assign_contact_tag,wp-easy-contact,600,Best Contact Management Software for WordPress +assign_contact_topic,wp-easy-contact,600,Best Contact Management Software for WordPress +assign_course_cats,lifterlms,8000,LifterLMS +assign_course_difficulties,lifterlms,8000,LifterLMS +assign_course_tags,lifterlms,8000,LifterLMS +assign_course_tracks,lifterlms,8000,LifterLMS +assign_cover_artist_terms,mooberry-book-manager,1000,Mooberry Book Manager +assign_customfields,catchers-helpdesk,200,Catchers Helpdesk and Ticket system for Support +assign_departments,employee-directory,400,Staff Directory – Employee Directory for WordPress +assign_editor_terms,mooberry-book-manager,1000,Mooberry Book Manager +assign_email_categories,mailer-dragon,300,Mailer Dragon – Email Marketing Plugin for WordPress +assign_employee_tags,employee-spotlight,1000,Team Members Staff Showcase Plugin – Employee Spotlight +assign_employment_type,employee-directory,400,Staff Directory – Employee Directory for WordPress +assign_event_listing_terms,wp-event-manager,1000,WP Event Manager +assign_event_magic_terms,kikfyre-events-calendar-tickets,200,"Events, Calendars & Tickets – Event Kikfyre" +assign_everest_form_terms,everest-forms,40000,Everest Forms – Easy Contact Form and Form Builder for WordPress +assign_fa_terms,featured-articles-lite,3000,FA Lite – WP responsive slider plugin +assign_feed_source_terms,wp-rss-aggregator,60000,WP RSS Aggregator +assign_feed_terms,wp-rss-aggregator,60000,WP RSS Aggregator +assign_food_group_terms,restaurantpress,3000,RestaurantPress +assign_food_menu_terms,restaurantpress,3000,RestaurantPress +assign_gender,employee-directory,400,Staff Directory – Employee Directory for WordPress +assign_genre_terms,mooberry-book-manager,1000,Mooberry Book Manager +assign_give_form_terms,give,50000,Give – Donation Plugin and Fundraising Platform +assign_give_payment_terms,give,50000,Give – Donation Plugin and Fundraising Platform +assign_groups,employee-spotlight,1000,Team Members Staff Showcase Plugin – Employee Spotlight +assign_hotel_location_terms,awebooking,6000,AweBooking – Hotel Booking System +assign_hotel_service_terms,awebooking,6000,AweBooking – Hotel Booking System +assign_illustrator_terms,mooberry-book-manager,1000,Mooberry Book Manager +assign_invoice_terms,essential-real-estate,3000,Essential Real Estate +assign_jbbrd_businesses_tags,job-board,300,Job Board by BestWebSoft +assign_jbbrd_employment_tags,job-board,300,Job Board by BestWebSoft +assign_job_listing_terms,wp-job-manager,100000,WP Job Manager +assign_jobtitles,employee-directory,400,Staff Directory – Employee Directory for WordPress +assign_klaviyo_shop_cart_terms,klaviyo-for-woocommerce,1000,Klaviyo for WooCommerce +assign_listing_terms,wpcasa,2000,WPCasa +assign_marital_status,employee-directory,400,Staff Directory – Employee Directory for WordPress +assign_membership_cats,lifterlms,8000,LifterLMS +assign_membership_tags,lifterlms,8000,LifterLMS +assign_mp_menu_item_terms,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +assign_mprm_order_terms,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +assign_office_locations,employee-spotlight,1000,Team Members Staff Showcase Plugin – Employee Spotlight +assign_opalestate_agents_terms,opal-estate,1000,Opal Estate +assign_opalestate_properties_terms,opal-estate,1000,Opal Estate +assign_package_terms,essential-real-estate,3000,Essential Real Estate +assign_person_area,campus-directory,200,Faculty Staff and Student Directory Plugin – Campus Directory +assign_person_location,campus-directory,200,Faculty Staff and Student Directory Plugin – Campus Directory +assign_person_rareas,campus-directory,200,Faculty Staff and Student Directory Plugin – Campus Directory +assign_person_title,campus-directory,200,Faculty Staff and Student Directory Plugin – Campus Directory +assign_portfolio_categories,custom-content-portfolio,1000,Custom Content Portfolio +assign_portfolio_tags,custom-content-portfolio,1000,Custom Content Portfolio +assign_portfolio_terms,flash-toolkit,30000,Flash Toolkit +assign_portfolio_terms,suffice-toolkit,5000,Suffice Toolkit +assign_pricing_rate_terms,awebooking,6000,AweBooking – Hotel Booking System +assign_product_categories,ecommerce-product-catalog,10000,eCommerce Product Catalog Plugin for WordPress +assign_product_categories,post-type-x,1000,Product Catalog X +assign_product_terms,dc-woocommerce-multi-vendor,10000,WC Marketplace +assign_product_terms,easy-digital-downloads,60000,Easy Digital Downloads +assign_product_terms,jigoshop,4000,Jigoshop +assign_product_terms,jigoshop-ecommerce,400,Jigoshop eCommerce +assign_product_terms,webmaster-user-role,8000,Webmaster User Role +assign_product_terms,woocommerce,4000000,WooCommerce +assign_product_terms,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +assign_project_terms,upstream,1000,WordPress Project Management by UpStream +assign_property_terms,essential-real-estate,3000,Essential Real Estate +assign_quote_authors,mg-quotes,300,mg Quotes +assign_quote_categories,mg-quotes,300,mg Quotes +assign_raq_services,request-a-quote,1000,Request a Quote +assign_resume_organizations,wp-resume,700,WP Resume +assign_resume_sections,wp-resume,700,WP Resume +assign_room_reservation_terms,wp-hotelier,1000,Easy WP Hotelier +assign_room_terms,wp-hotelier,1000,Easy WP Hotelier +assign_room_type_terms,awebooking,6000,AweBooking – Hotel Booking System +assign_series_terms,mooberry-book-manager,1000,Mooberry Book Manager +assign_shop_coupon_terms,jigoshop,4000,Jigoshop +assign_shop_coupon_terms,jigoshop-ecommerce,400,Jigoshop eCommerce +assign_shop_coupon_terms,webmaster-user-role,8000,Webmaster User Role +assign_shop_coupon_terms,woocommerce,4000000,WooCommerce +assign_shop_discount_terms,easy-digital-downloads,60000,Easy Digital Downloads +assign_shop_email_terms,jigoshop,4000,Jigoshop +assign_shop_email_terms,jigoshop-ecommerce,400,Jigoshop eCommerce +assign_shop_order_terms,jigoshop,4000,Jigoshop +assign_shop_order_terms,jigoshop-ecommerce,400,Jigoshop eCommerce +assign_shop_order_terms,webmaster-user-role,8000,Webmaster User Role +assign_shop_order_terms,woocommerce,4000000,WooCommerce +assign_shop_payment_terms,easy-digital-downloads,60000,Easy Digital Downloads +assign_shop_webhook_terms,woocommerce,4000000,WooCommerce +assign_sp_calendar_terms,sportspress,20000,SportsPress – Sports Club & League Manager +assign_sp_config_terms,sportspress,20000,SportsPress – Sports Club & League Manager +assign_sp_event_terms,sportspress,20000,SportsPress – Sports Club & League Manager +assign_sp_list_terms,sportspress,20000,SportsPress – Sports Club & League Manager +assign_sp_player_terms,sportspress,20000,SportsPress – Sports Club & League Manager +assign_sp_staff_terms,sportspress,20000,SportsPress – Sports Club & League Manager +assign_sp_table_terms,sportspress,20000,SportsPress – Sports Club & League Manager +assign_sp_team_terms,sportspress,20000,SportsPress – Sports Club & League Manager +assign_tag_terms,mooberry-book-manager,1000,Mooberry Book Manager +assign_ticket,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +assign_ticket,catchers-helpdesk,200,Catchers Helpdesk and Ticket system for Support +assign_ticket_creator,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +assign_ticket_priority,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +assign_ticket_status,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +assign_ticket_topic,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +assign_topic_tags,bbpress,300000,bbPress +assign_trans_log_terms,essential-real-estate,3000,Essential Real Estate +assign_user_package_terms,essential-real-estate,3000,Essential Real Estate +assign_user_registration_terms,user-registration,7000,"User Registration – Custom Registration Form, Login and User Profile for WordPress" +assign_wctrl_contents,widgets-control,1000,Widgets Control +assign_wd_ads_groups,ad-manager-wd,700,Ad Manager by WD – Advanced Ad Manager plugin +assign_wpcm_club_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +assign_wpcm_match_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +assign_wpcm_player_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +assign_wpcm_sponsor_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +assign_wpcm_staff_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +assign_wpsdeals_terms,deals-engine,200,Social Deals Engine +assign_wpsdealssales_terms,deals-engine,200,Social Deals Engine +attach_files,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +attach_files,catchers-helpdesk,200,Catchers Helpdesk and Ticket system for Support +avartan_slider_access,avartan-slider-lite,2000,Responsive WordPress Slider – Avartan Slider Lite +avh_fdas_admin,avh-first-defense-against-spam,7000,AVH First Defense Against Spam +backwpup,backwpup,600000,BackWPup – WordPress Backup Plugin +backwpup_backups,backwpup,600000,BackWPup – WordPress Backup Plugin +backwpup_backups_delete,backwpup,600000,BackWPup – WordPress Backup Plugin +backwpup_backups_download,backwpup,600000,BackWPup – WordPress Backup Plugin +backwpup_jobs,backwpup,600000,BackWPup – WordPress Backup Plugin +backwpup_jobs_edit,backwpup,600000,BackWPup – WordPress Backup Plugin +backwpup_jobs_start,backwpup,600000,BackWPup – WordPress Backup Plugin +backwpup_logs,backwpup,600000,BackWPup – WordPress Backup Plugin +backwpup_logs_delete,backwpup,600000,BackWPup – WordPress Backup Plugin +backwpup_restore,backwpup,600000,BackWPup – WordPress Backup Plugin +backwpup_settings,backwpup,600000,BackWPup – WordPress Backup Plugin +become_yop_poll_pro,yop-poll,20000,YOP Poll +birthdays_list,birthdays-widget,2000,Birthdays Widget +bnfw,bnfw,20000,Better Notifications for WordPress +bookacti_create_activities,booking-activities,900,Booking Activities +bookacti_create_bookings,booking-activities,900,Booking Activities +bookacti_create_forms,booking-activities,900,Booking Activities +bookacti_create_templates,booking-activities,900,Booking Activities +bookacti_delete_activities,booking-activities,900,Booking Activities +bookacti_delete_bookings,booking-activities,900,Booking Activities +bookacti_delete_forms,booking-activities,900,Booking Activities +bookacti_delete_templates,booking-activities,900,Booking Activities +bookacti_edit_activities,booking-activities,900,Booking Activities +bookacti_edit_bookings,booking-activities,900,Booking Activities +bookacti_edit_forms,booking-activities,900,Booking Activities +bookacti_edit_templates,booking-activities,900,Booking Activities +bookacti_manage_booking_activities,booking-activities,900,Booking Activities +bookacti_manage_booking_activities_settings,booking-activities,900,Booking Activities +bookacti_manage_bookings,booking-activities,900,Booking Activities +bookacti_manage_forms,booking-activities,900,Booking Activities +bookacti_manage_templates,booking-activities,900,Booking Activities +bookacti_read_templates,booking-activities,900,Booking Activities +brightcove_get_user_default_account,brightcove-video-connect,900,Brightcove Video Connect +brightcove_manipulate_accounts,brightcove-video-connect,900,Brightcove Video Connect +brightcove_manipulate_playlists,brightcove-video-connect,900,Brightcove Video Connect +brightcove_manipulate_videos,brightcove-video-connect,900,Brightcove Video Connect +brightcove_set_site_default_account,brightcove-video-connect,900,Brightcove Video Connect +brightcove_set_user_default_account,brightcove-video-connect,900,Brightcove Video Connect +brizy_edit_whole_page,brizy,30000,Brizy – Page Builder +btev,bluetrait-event-viewer,800,BTEV +bug_assigned_to_field,upstream,1000,WordPress Project Management by UpStream +bug_description_field,upstream,1000,WordPress Project Management by UpStream +bug_due_date_field,upstream,1000,WordPress Project Management by UpStream +bug_file_field,upstream,1000,WordPress Project Management by UpStream +bug_severity_field,upstream,1000,WordPress Project Management by UpStream +bug_status_field,upstream,1000,WordPress Project Management by UpStream +bug_title_field,upstream,1000,WordPress Project Management by UpStream +bulk_edit_roles,wpfront-user-role-editor,60000,WPFront User Role Editor +buwd_api_keys,backup-wd,8000,Backup WD – Backup and Restore Plugin +buwd_backups,backup-wd,8000,Backup WD – Backup and Restore Plugin +buwd_backups_delete,backup-wd,8000,Backup WD – Backup and Restore Plugin +buwd_backups_download,backup-wd,8000,Backup WD – Backup and Restore Plugin +buwd_edit,backup-wd,8000,Backup WD – Backup and Restore Plugin +buwd_job,backup-wd,8000,Backup WD – Backup and Restore Plugin +buwd_job_delete,backup-wd,8000,Backup WD – Backup and Restore Plugin +buwd_job_edit,backup-wd,8000,Backup WD – Backup and Restore Plugin +buwd_job_run,backup-wd,8000,Backup WD – Backup and Restore Plugin +buwd_log_delete,backup-wd,8000,Backup WD – Backup and Restore Plugin +buwd_log_download,backup-wd,8000,Backup WD – Backup and Restore Plugin +buwd_log_view,backup-wd,8000,Backup WD – Backup and Restore Plugin +buwd_logs,backup-wd,8000,Backup WD – Backup and Restore Plugin +buwd_settings,backup-wd,8000,Backup WD – Backup and Restore Plugin +buwd_settings_export,backup-wd,8000,Backup WD – Backup and Restore Plugin +buwd_settings_import,backup-wd,8000,Backup WD – Backup and Restore Plugin +buy_tax_free,pricing-deals-for-woocommerce,7000,Pricing Deals for WooCommerce +buy_wholesale,pricing-deals-for-woocommerce,7000,Pricing Deals for WooCommerce +cb_parallax_edit,cb-parallax,400,cbParallax +cbe_edit_background,custom-background-extended,3000,Custom Background Extended +cdi_gateway,colissimo-delivery-integration,2000,Colissimo Delivery Integration +cfdb7_access,contact-form-cfdb7,100000,Contact Form 7 Database Addon – CFDB7 +challonge_report_own,challonge,900,Challonge +challonge_signup,challonge,900,Challonge +challonge_view,challonge,900,Challonge +chatbro_ban_user,chatbro,1000,Chat Bro – Chat linked with Telegram or VK chat +chatbro_delete_message,chatbro,1000,Chat Bro – Chat linked with Telegram or VK chat +chatbro_view_chat,chatbro,1000,Chat Bro – Chat linked with Telegram or VK chat +che_edit_header,custom-header-extended,5000,Custom Header Extended +check_PBot,proofread-bot,400,Proofread Bot +chimpmate_cap,chimpmate,3000,ChimpMate – WordPress MailChimp Assistant +chronosly_author,chronosly-events-calendar,4000,Chronosly Events Calendar +chronosly_license,chronosly-events-calendar,4000,Chronosly Events Calendar +chronosly_options,chronosly-events-calendar,4000,Chronosly Events Calendar +clear_errors,timber,400,Timber +clone_own_yop_polls,yop-poll,20000,YOP Poll +clone_own_yop_polls_templates,yop-poll,20000,YOP Poll +clone_yop_polls,yop-poll,20000,YOP Poll +clone_yop_polls_templates,yop-poll,20000,YOP Poll +close_ticket,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +close_ticket,catchers-helpdesk,200,Catchers Helpdesk and Ticket system for Support +config_postie,postie,20000,Postie +configure_recent_dash_contacts,wp-easy-contact,600,Best Contact Management Software for WordPress +configure_recent_tickets_dashboard,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +connections_add_entry,connections,10000,Connections Business Directory +connections_add_entry_moderated,connections,10000,Connections Business Directory +connections_change_roles,connections,10000,Connections Business Directory +connections_change_settings,connections,10000,Connections Business Directory +connections_delete_entry,connections,10000,Connections Business Directory +connections_edit_categories,connections,10000,Connections Business Directory +connections_edit_entry,connections,10000,Connections Business Directory +connections_edit_entry_moderated,connections,10000,Connections Business Directory +connections_manage,connections,10000,Connections Business Directory +connections_manage_template,connections,10000,Connections Business Directory +connections_view_dashboard,connections,10000,Connections Business Directory +connections_view_menu,connections,10000,Connections Business Directory +connections_view_private,connections,10000,Connections Business Directory +connections_view_public,connections,10000,Connections Business Directory +connections_view_unlisted,connections,10000,Connections Business Directory +copy_posts,duplicate-post,2000000,Duplicate Post +copy_posts,project-panorama-lite,1000,Project Panorama +copy_posts,project-status,200,Project Status +cp_add_projects,collabpress,700,CollabPress +cp_add_task,collabpress,700,CollabPress +cp_add_task_lists,collabpress,700,CollabPress +cp_edit_projects,collabpress,700,CollabPress +cp_edit_task,collabpress,700,CollabPress +cp_edit_task_lists,collabpress,700,CollabPress +cp_view_category_permalinks,custom-permalinks,100000,Custom Permalinks +cp_view_post_permalinks,custom-permalinks,100000,Custom Permalinks +create_admincolorschemes,easy-admin-color-schemes,1000,Easy Admin Color Schemes +create_applications,apply-online,5000,Apply Online +create_blocks,gutenberg,500000,Gutenberg +create_courses,lifterlms,8000,LifterLMS +create_edit_projects,ignitiondeck,2000,IgnitionDeck Crowdfunding & Commerce +create_fep_announcements,front-end-pm,8000,Front End PM +create_fep_messages,front-end-pm,8000,Front End PM +create_forms,formlift,800,FormLift for Infusionsoft Web Forms +create_forms,pronamic-ideal,6000,Pronamic Pay +create_galleries,gallery-box,2000,Gallery Box +create_glossaries,glossary-by-codeat,1000,Glossary +create_lazyest_folder,lazyest-gallery,1000,Lazyest Gallery +create_lessons,lifterlms,8000,LifterLMS +create_masterslider,master-slider,100000,Master Slider – Responsive Touch Slider +create_memberships,lifterlms,8000,LifterLMS +create_nc_references,nelio-content,7000,Nelio Content – Social Media Marketing Automation +create_niso_slider_carousels,niso-carousel,200,Niso Carousel +create_niso_slider_carousels,niso-carousel-slider,400,Niso Carousel Slider +create_portfolio_projects,custom-content-portfolio,1000,Custom Content Portfolio +create_posts,wp2appir,300,wp2appir +create_posts,zero-bs-crm,1000,Zero BS WordPress CRM +create_questions,lifterlms,8000,LifterLMS +create_quizzes,lifterlms,8000,LifterLMS +create_restaurant_items,restaurant,300,Restaurant +create_roles,members,100000,Members +create_roles,wpfront-user-role-editor,60000,WPFront User Role Editor +create_support_tickets,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +create_tc_events,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +create_tc_orders,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +create_tc_tickets,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +create_tc_tickets_instances,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +create_ticket,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +create_ticket,catchers-helpdesk,200,Catchers Helpdesk and Ticket system for Support +create_touchpoints,ukuupeople-the-simple-crm,1000,CRM: Contact Management Simplified – UkuuPeople +create_ukuupeoples,ukuupeople-the-simple-crm,1000,CRM: Contact Management Simplified – UkuuPeople +create_whistles,whistles,2000,Whistles +create_wpp_properties,wp-property,5000,WP-Property – WordPress Powered Real Estate and Property Management +create_wpse_profiles,wp-smart-editor,900,WP Smart Editor +crm_role,wsdesk,1000,WSDesk – WordPress HelpDesk & Support Ticket System +cstm_cds_full_access,custom-codes,800,Custom Codes +cuar_access_admin_panel,customer-area,10000,WP Customer Area +cuar_edit_account,customer-area,10000,WP Customer Area +cuar_pf_assign_categories,customer-area,10000,WP Customer Area +cuar_pf_delete,customer-area,10000,WP Customer Area +cuar_pf_delete_categories,customer-area,10000,WP Customer Area +cuar_pf_edit,customer-area,10000,WP Customer Area +cuar_pf_edit_categories,customer-area,10000,WP Customer Area +cuar_pf_list_all,customer-area,10000,WP Customer Area +cuar_pf_manage_attachments,customer-area,10000,WP Customer Area +cuar_pf_manage_categories,customer-area,10000,WP Customer Area +cuar_pf_read,customer-area,10000,WP Customer Area +cuar_pp_assign_categories,customer-area,10000,WP Customer Area +cuar_pp_delete,customer-area,10000,WP Customer Area +cuar_pp_delete_categories,customer-area,10000,WP Customer Area +cuar_pp_edit,customer-area,10000,WP Customer Area +cuar_pp_edit_categories,customer-area,10000,WP Customer Area +cuar_pp_list_all,customer-area,10000,WP Customer Area +cuar_pp_manage_categories,customer-area,10000,WP Customer Area +cuar_pp_read,customer-area,10000,WP Customer Area +cuar_view_account,customer-area,10000,WP Customer Area +cuar_view_any_cuar_private_file,customer-area,10000,WP Customer Area +cuar_view_any_cuar_private_page,customer-area,10000,WP Customer Area +cuar_view_files,customer-area,10000,WP Customer Area +cuar_view_pages,customer-area,10000,WP Customer Area +cuar_view_top_bar,customer-area,10000,WP Customer Area +customize_admin,client-dash,6000,Client Dash +deactivate_modules,site-editor,300,Site Editor – WordPress Site Builder – Theme Builder and Page Builder +debug intel,intelligence,600,Intelligence +del_leagues,leaguemanager,2000,LeagueManager +del_matches,leaguemanager,2000,LeagueManager +del_seasons,leaguemanager,2000,LeagueManager +del_teams,leaguemanager,2000,LeagueManager +delete all intel visitors,intelligence,600,Intelligence +delete_acadp_field,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +delete_acadp_fields,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +delete_acadp_listing,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +delete_acadp_listings,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +delete_acadp_payment,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +delete_acadp_payments,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +delete_achievement_events,achievements,300,Achievements for WordPress +delete_achievement_progresses,achievements,300,Achievements for WordPress +delete_achievements,achievements,300,Achievements for WordPress +delete_ad_terms,apply-online,5000,Apply Online +delete_admincolorschemes,easy-admin-color-schemes,1000,Easy Admin Color Schemes +delete_ads,apply-online,5000,Apply Online +delete_aec_event,another-events-calendar,800,Another Events Calendar +delete_aec_events,another-events-calendar,800,Another Events Calendar +delete_aec_organizer,another-events-calendar,800,Another Events Calendar +delete_aec_organizers,another-events-calendar,800,Another Events Calendar +delete_aec_venue,another-events-calendar,800,Another Events Calendar +delete_aec_venues,another-events-calendar,800,Another Events Calendar +delete_affiliate_keywords,affiliate,700,Affiliate +delete_agent,essential-real-estate,3000,Essential Real Estate +delete_agent_terms,essential-real-estate,3000,Essential Real Estate +delete_agents,essential-real-estate,3000,Essential Real Estate +delete_aggregator-records,the-events-calendar,700000,The Events Calendar +delete_ai1ec_event,all-in-one-event-calendar,100000,All-in-One Event Calendar +delete_ai1ec_events,all-in-one-event-calendar,100000,All-in-One Event Calendar +delete_aiovg_video,all-in-one-video-gallery,1000,All-in-One Video Gallery +delete_aiovg_videos,all-in-one-video-gallery,1000,All-in-One Video Gallery +delete_all_touchpoints,ukuupeople-the-simple-crm,1000,CRM: Contact Management Simplified – UkuuPeople +delete_all_ukuupeoples,ukuupeople-the-simple-crm,1000,CRM: Contact Management Simplified – UkuuPeople +delete_anb_animations,alert-notice-boxes,1000,Alert Notice Boxes +delete_anb_animations_out,alert-notice-boxes,1000,Alert Notice Boxes +delete_anb_designs,alert-notice-boxes,1000,Alert Notice Boxes +delete_anb_locations,alert-notice-boxes,1000,Alert Notice Boxes +delete_anbs,alert-notice-boxes,1000,Alert Notice Boxes +delete_applications,apply-online,5000,Apply Online +delete_archiv,archive,700,Archive +delete_archive_structure,archive,700,Archive +delete_archivs,archive,700,Archive +delete_article,issuem,1000,IssueM +delete_articles,issuem,1000,IssueM +delete_at_biz_dir,directorist,500,Directorist – Business Directory Plugin +delete_at_biz_dirs,directorist,500,Directorist – Business Directory Plugin +delete_atbdp_order,directorist,500,Directorist – Business Directory Plugin +delete_atbdp_orders,directorist,500,Directorist – Business Directory Plugin +delete_attachments,wpfront-user-role-editor,60000,WPFront User Role Editor +delete_attachments,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +delete_attendees_cap,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_awebooking,awebooking,6000,AweBooking – Hotel Booking System +delete_awebooking_terms,awebooking,6000,AweBooking – Hotel Booking System +delete_awebookings,awebooking,6000,AweBooking – Hotel Booking System +delete_birs_appointment,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_birs_appointments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_birs_client,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_birs_clients,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_birs_location,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_birs_locations,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_birs_payment,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_birs_payments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_birs_service,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_birs_services,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_birs_staff,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_birs_staffs,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_blocks,gutenberg,500000,Gutenberg +delete_board_committees,nonprofit-board-management,400,Nonprofit Board Management +delete_board_events,nonprofit-board-management,400,Nonprofit Board Management +delete_book,novelist,800,Novelist +delete_book-reviews,book-review-library,700,Book Review Library +delete_book_terms,novelist,800,Novelist +delete_books,novelist,800,Novelist +delete_box,boxzilla,20000,Boxzilla +delete_bps_forms,bp-profile-search,10000,BP Profile Search +delete_calp_event,calpress-event-calendar,5000,CalPress Calendar +delete_calp_events,calpress-event-calendar,5000,CalPress Calendar +delete_campaign,charitable,10000,Charitable – Donation Plugin +delete_campaign,leyka,1000,Leyka +delete_campaign_terms,charitable,10000,Charitable – Donation Plugin +delete_campaigns,charitable,10000,Charitable – Donation Plugin +delete_campaigns,leyka,1000,Leyka +delete_cannedresponse_category,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +delete_cannedresponse_tag,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +delete_car_listing,wp-car-manager,3000,WP Car Manager +delete_car_listing_terms,wp-car-manager,3000,WP Car Manager +delete_car_listings,wp-car-manager,3000,WP Car Manager +delete_cbxaccounting,cbxwpsimpleaccounting,300,CBX Accounting +delete_cctor_coupon,coupon-creator,10000,Coupon Creator +delete_cctor_coupons,coupon-creator,10000,Coupon Creator +delete_checkins_cap,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_chronosly,chronosly-events-calendar,4000,Chronosly Events Calendar +delete_chronoslys,chronosly-events-calendar,4000,Chronosly Events Calendar +delete_classified_listing,classifieds-wp,800,Classifieds WP +delete_classified_listing_terms,classifieds-wp,800,Classifieds WP +delete_classified_listings,classifieds-wp,800,Classifieds WP +delete_client,upstream,1000,WordPress Project Management by UpStream +delete_client_terms,upstream,1000,WordPress Project Management by UpStream +delete_clients,upstream,1000,WordPress Project Management by UpStream +delete_contact_country,wp-easy-contact,600,Best Contact Management Software for WordPress +delete_contact_state,wp-easy-contact,600,Best Contact Management Software for WordPress +delete_contact_tag,wp-easy-contact,600,Best Contact Management Software for WordPress +delete_contact_topic,wp-easy-contact,600,Best Contact Management Software for WordPress +delete_content_shortcodes,wpfront-user-role-editor,60000,WPFront User Role Editor +delete_course,lifterlms,8000,LifterLMS +delete_course_cats,lifterlms,8000,LifterLMS +delete_course_difficulties,lifterlms,8000,LifterLMS +delete_course_tags,lifterlms,8000,LifterLMS +delete_course_tracks,lifterlms,8000,LifterLMS +delete_courses,lifterlms,8000,LifterLMS +delete_cupri_pay,pardakht-delkhah,1000,پلاگین پرداخت دلخواه +delete_custom_css,custom-css-js,100000,Simple Custom CSS and JS +delete_custom_csss,custom-css-js,100000,Simple Custom CSS and JS +delete_customfields,catchers-helpdesk,200,Catchers Helpdesk and Ticket system for Support +delete_datasets,projectmanager,400,ProjectManager +delete_departments,employee-directory,400,Staff Directory – Employee Directory for WordPress +delete_ditty_news_ticker,ditty-news-ticker,40000,Ditty News Ticker +delete_ditty_news_tickers,ditty-news-ticker,40000,Ditty News Ticker +delete_documents,wp-document-revisions,4000,WP Document Revisions +delete_donation,charitable,10000,Charitable – Donation Plugin +delete_donation,leyka,1000,Leyka +delete_donations,charitable,10000,Charitable – Donation Plugin +delete_donations,leyka,1000,Leyka +delete_dsn_note,admin-dashboard-site-notes,3000,Dashboard Site Notes +delete_dsn_notes,admin-dashboard-site-notes,3000,Dashboard Site Notes +delete_edr_course,educator,1000,Educator 2 +delete_edr_courses,educator,1000,Educator 2 +delete_edr_lesson,educator,1000,Educator 2 +delete_edr_lessons,educator,1000,Educator 2 +delete_edr_membership,educator,1000,Educator 2 +delete_edr_memberships,educator,1000,Educator 2 +delete_email_categories,mailer-dragon,300,Mailer Dragon – Email Marketing Plugin for WordPress +delete_email_templates,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +delete_emails,mailer-dragon,300,Mailer Dragon – Email Marketing Plugin for WordPress +delete_emd_agents,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +delete_emd_canned_responses,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +delete_emd_contacts,wp-easy-contact,600,Best Contact Management Software for WordPress +delete_emd_employees,employee-directory,400,Staff Directory – Employee Directory for WordPress +delete_emd_quotes,request-a-quote,1000,Request a Quote +delete_emd_tickets,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +delete_employee_tags,employee-spotlight,1000,Team Members Staff Showcase Plugin – Employee Spotlight +delete_employment_type,employee-directory,400,Staff Directory – Employee Directory for WordPress +delete_epa_albums,easy-photo-album,5000,Easy Photo Album +delete_event_categories,events-manager,100000,Events Manager +delete_event_listing,wp-event-manager,1000,WP Event Manager +delete_event_listing_terms,wp-event-manager,1000,WP Event Manager +delete_event_listings,wp-event-manager,1000,WP Event Manager +delete_event_magic,kikfyre-events-calendar-tickets,200,"Events, Calendars & Tickets – Event Kikfyre" +delete_event_magic_terms,kikfyre-events-calendar-tickets,200,"Events, Calendars & Tickets – Event Kikfyre" +delete_event_magics,kikfyre-events-calendar-tickets,200,"Events, Calendars & Tickets – Event Kikfyre" +delete_events,event-organiser,40000,Event Organiser +delete_events,events-maker,4000,Events Maker by dFactory +delete_events,events-manager,100000,Events Manager +delete_events,quick-event-manager,5000,Quick Event Manager +delete_everest_form,everest-forms,40000,Everest Forms – Easy Contact Form and Form Builder for WordPress +delete_everest_form_terms,everest-forms,40000,Everest Forms – Easy Contact Form and Form Builder for WordPress +delete_everest_forms,everest-forms,40000,Everest Forms – Easy Contact Form and Form Builder for WordPress +delete_fa_items,featured-articles-lite,3000,FA Lite – WP responsive slider plugin +delete_fa_terms,featured-articles-lite,3000,FA Lite – WP responsive slider plugin +delete_fbtabs,facebook-tab-manager,1000,Facebook Tab Manager +delete_feed,wp-rss-aggregator,60000,WP RSS Aggregator +delete_feed_source,wp-rss-aggregator,60000,WP RSS Aggregator +delete_feed_source_terms,wp-rss-aggregator,60000,WP RSS Aggregator +delete_feed_sources,wp-rss-aggregator,60000,WP RSS Aggregator +delete_feed_terms,wp-rss-aggregator,60000,WP RSS Aggregator +delete_feeds,wp-rss-aggregator,60000,WP RSS Aggregator +delete_fep_announcements,front-end-pm,8000,Front End PM +delete_fep_messages,front-end-pm,8000,Front End PM +delete_flexible_invoice,flexible-invoices,1000,Flexible Invoices for WordPress +delete_flexible_invoices,flexible-invoices,1000,Flexible Invoices for WordPress +delete_food_group,restaurantpress,3000,RestaurantPress +delete_food_group_terms,restaurantpress,3000,RestaurantPress +delete_food_groups,restaurantpress,3000,RestaurantPress +delete_food_menu,restaurantpress,3000,RestaurantPress +delete_food_menu_terms,restaurantpress,3000,RestaurantPress +delete_food_menus,restaurantpress,3000,RestaurantPress +delete_form,formlift,800,FormLift for Infusionsoft Web Forms +delete_form,pronamic-ideal,6000,Pronamic Pay +delete_forms,pronamic-ideal,6000,Pronamic Pay +delete_forums,bbpress,300000,bbPress +delete_galleries,gallery-box,2000,Gallery Box +delete_gallery,gallery-box,2000,Gallery Box +delete_game,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_games,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_gender,employee-directory,400,Staff Directory – Employee Directory for WordPress +delete_give_form,give,50000,Give – Donation Plugin and Fundraising Platform +delete_give_form_terms,give,50000,Give – Donation Plugin and Fundraising Platform +delete_give_forms,give,50000,Give – Donation Plugin and Fundraising Platform +delete_give_payment,give,50000,Give – Donation Plugin and Fundraising Platform +delete_give_payment_terms,give,50000,Give – Donation Plugin and Fundraising Platform +delete_give_payments,give,50000,Give – Donation Plugin and Fundraising Platform +delete_glossaries,glossary-by-codeat,1000,Glossary +delete_glossary,glossary-by-codeat,1000,Glossary +delete_groups,employee-spotlight,1000,Team Members Staff Showcase Plugin – Employee Spotlight +delete_hanaboard-post,hana-board,1000,Hana-Board 하나보드 워드프레스 게시판 +delete_hb_bookings,wp-hotel-booking,7000,WP Hotel Booking +delete_hb_rooms,wp-hotel-booking,7000,WP Hotel Booking +delete_hf_membership_plan,xa-woocommerce-memberships,400,Memberships for WooCommerce +delete_hf_membership_plans,xa-woocommerce-memberships,400,Memberships for WooCommerce +delete_hf_user_membership,xa-woocommerce-memberships,400,Memberships for WooCommerce +delete_hf_user_memberships,xa-woocommerce-memberships,400,Memberships for WooCommerce +delete_hotel_location,awebooking,6000,AweBooking – Hotel Booking System +delete_hotel_location_terms,awebooking,6000,AweBooking – Hotel Booking System +delete_hotel_locations,awebooking,6000,AweBooking – Hotel Booking System +delete_hotel_service,awebooking,6000,AweBooking – Hotel Booking System +delete_hotel_service_terms,awebooking,6000,AweBooking – Hotel Booking System +delete_hotel_services,awebooking,6000,AweBooking – Hotel Booking System +delete_ib_edu_membership,ibeducator,1000,Educator +delete_ib_edu_memberships,ibeducator,1000,Educator +delete_ib_educator_course,ibeducator,1000,Educator +delete_ib_educator_courses,ibeducator,1000,Educator +delete_ib_educator_lesson,ibeducator,1000,Educator +delete_ib_educator_lessons,ibeducator,1000,Educator +delete_ims_gallery,image-store,900,Image Store +delete_ims_gallerys,image-store,900,Image Store +delete_inbound-form,cta,10000,WordPress Calls to Action +delete_inbound-form,landing-pages,10000,WordPress Landing Pages +delete_inbound-form,leads,7000,WordPress Leads +delete_inbound-forms,cta,10000,WordPress Calls to Action +delete_inbound-forms,landing-pages,10000,WordPress Landing Pages +delete_inbound-forms,leads,7000,WordPress Leads +delete_invoice,essential-real-estate,3000,Essential Real Estate +delete_invoice_terms,essential-real-estate,3000,Essential Real Estate +delete_invoices,essential-real-estate,3000,Essential Real Estate +delete_item,gamipress,2000,GamiPress +delete_items,gamipress,2000,GamiPress +delete_jbbrd_businesses_tags,job-board,300,Job Board by BestWebSoft +delete_jbbrd_employment_tags,job-board,300,Job Board by BestWebSoft +delete_job_listing,wp-job-manager,100000,WP Job Manager +delete_job_listing_terms,wp-job-manager,100000,WP Job Manager +delete_job_listings,wp-job-manager,100000,WP Job Manager +delete_jobtitles,employee-directory,400,Staff Directory – Employee Directory for WordPress +delete_jscp_match,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +delete_jscp_player,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +delete_jscp_team,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +delete_klaviyo_shop_cart,klaviyo-for-woocommerce,1000,Klaviyo for WooCommerce +delete_klaviyo_shop_cart_terms,klaviyo-for-woocommerce,1000,Klaviyo for WooCommerce +delete_klaviyo_shop_carts,klaviyo-for-woocommerce,1000,Klaviyo for WooCommerce +delete_landing_pages,landing-pages,10000,WordPress Landing Pages +delete_language,sublanguage,1000,Sublanguage +delete_leads,cta,10000,WordPress Calls to Action +delete_leads,landing-pages,10000,WordPress Landing Pages +delete_leads,leads,7000,WordPress Leads +delete_legalpack_pages,legalpack,400,Legalpack +delete_lesson,lifterlms,8000,LifterLMS +delete_lessons,lifterlms,8000,LifterLMS +delete_listing,auto-listings,400,Auto Listings +delete_listing,wp-real-estate,400,WP Real Estate +delete_listing,wpcasa,2000,WPCasa +delete_listing_terms,wpcasa,2000,WPCasa +delete_listings,auto-listings,400,Auto Listings +delete_listings,wp-real-estate,400,WP Real Estate +delete_listings,wpcasa,2000,WPCasa +delete_locations,events-manager,100000,Events Manager +delete_login_redirects,wpfront-user-role-editor,60000,WPFront User Role Editor +delete_lp_courses,learnpress,50000,LearnPress – WordPress LMS Plugin +delete_lp_lessons,learnpress,50000,LearnPress – WordPress LMS Plugin +delete_lp_orders,learnpress,50000,LearnPress – WordPress LMS Plugin +delete_marital_status,employee-directory,400,Staff Directory – Employee Directory for WordPress +delete_masterslider,master-slider,100000,Master Slider – Responsive Touch Slider +delete_mbdb_book,mooberry-book-manager,1000,Mooberry Book Manager +delete_mbdb_book_grid,mooberry-book-manager,1000,Mooberry Book Manager +delete_mbdb_book_grids,mooberry-book-manager,1000,Mooberry Book Manager +delete_mbdb_books,mooberry-book-manager,1000,Mooberry Book Manager +delete_meals,restaurant-manager,800,Restaurant Manager +delete_membership,lifterlms,8000,LifterLMS +delete_membership_cats,lifterlms,8000,LifterLMS +delete_membership_tags,lifterlms,8000,LifterLMS +delete_memberships,lifterlms,8000,LifterLMS +delete_mp_menu_item,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +delete_mp_menu_item_terms,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +delete_mp_menu_items,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +delete_mprm_order,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +delete_mprm_order_terms,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +delete_mprm_orders,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +delete_nc_reference,nelio-content,7000,Nelio Content – Social Media Marketing Automation +delete_nc_references,nelio-content,7000,Nelio Content – Social Media Marketing Automation +delete_nemus-sliders,nemus-slider,2000,Nemus Slider +delete_news,news-manager,3000,News Manager +delete_newsletters,alo-easymail,10000,ALO EasyMail Newsletter +delete_niso_slider_carousel,niso-carousel,200,Niso Carousel +delete_niso_slider_carousel,niso-carousel-slider,400,Niso Carousel Slider +delete_niso_slider_carousels,niso-carousel,200,Niso Carousel +delete_niso_slider_carousels,niso-carousel-slider,400,Niso Carousel Slider +delete_office_locations,employee-spotlight,1000,Team Members Staff Showcase Plugin – Employee Spotlight +delete_opalestate_agents,opal-estate,1000,Opal Estate +delete_opalestate_agents_terms,opal-estate,1000,Opal Estate +delete_opalestate_agentss,opal-estate,1000,Opal Estate +delete_opalestate_properties,opal-estate,1000,Opal Estate +delete_opalestate_properties_terms,opal-estate,1000,Opal Estate +delete_opalestate_propertiess,opal-estate,1000,Opal Estate +delete_opanda-item,opt-in-panda,3000,OnePress Opt-In Panda +delete_opanda-item,social-locker,10000,OnePress Social Locker +delete_orbis_company,orbis,200,Orbis +delete_orbis_project,orbis,200,Orbis +delete_other_datasets,projectmanager,400,ProjectManager +delete_other_ticket,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +delete_other_ticket,catchers-helpdesk,200,Catchers Helpdesk and Ticket system for Support +delete_others_acadp_fields,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +delete_others_acadp_listings,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +delete_others_acadp_payments,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +delete_others_achievement_progresses,achievements,300,Achievements for WordPress +delete_others_achievements,achievements,300,Achievements for WordPress +delete_others_admincolorschemes,easy-admin-color-schemes,1000,Easy Admin Color Schemes +delete_others_ads,apply-online,5000,Apply Online +delete_others_aec_events,another-events-calendar,800,Another Events Calendar +delete_others_aec_organizers,another-events-calendar,800,Another Events Calendar +delete_others_aec_venues,another-events-calendar,800,Another Events Calendar +delete_others_affiliate_keywords,affiliate,700,Affiliate +delete_others_agents,essential-real-estate,3000,Essential Real Estate +delete_others_aggregator-records,the-events-calendar,700000,The Events Calendar +delete_others_ai1ec_events,all-in-one-event-calendar,100000,All-in-One Event Calendar +delete_others_aiovg_videos,all-in-one-video-gallery,1000,All-in-One Video Gallery +delete_others_anb_animations,alert-notice-boxes,1000,Alert Notice Boxes +delete_others_anb_animations_out,alert-notice-boxes,1000,Alert Notice Boxes +delete_others_anb_designs,alert-notice-boxes,1000,Alert Notice Boxes +delete_others_anb_locations,alert-notice-boxes,1000,Alert Notice Boxes +delete_others_anbs,alert-notice-boxes,1000,Alert Notice Boxes +delete_others_applications,apply-online,5000,Apply Online +delete_others_archivs,archive,700,Archive +delete_others_articles,issuem,1000,IssueM +delete_others_at_biz_dirs,directorist,500,Directorist – Business Directory Plugin +delete_others_atbdp_orders,directorist,500,Directorist – Business Directory Plugin +delete_others_attachments,wpfront-user-role-editor,60000,WPFront User Role Editor +delete_others_awebookings,awebooking,6000,AweBooking – Hotel Booking System +delete_others_birs_appointments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_others_birs_clients,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_others_birs_locations,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_others_birs_payments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_others_birs_services,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_others_birs_staffs,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_others_blocks,gutenberg,500000,Gutenberg +delete_others_board_committees,nonprofit-board-management,400,Nonprofit Board Management +delete_others_board_events,nonprofit-board-management,400,Nonprofit Board Management +delete_others_book-reviews,book-review-library,700,Book Review Library +delete_others_books,novelist,800,Novelist +delete_others_bps_forms,bp-profile-search,10000,BP Profile Search +delete_others_calp_events,calpress-event-calendar,5000,CalPress Calendar +delete_others_campaigns,charitable,10000,Charitable – Donation Plugin +delete_others_campaigns,leyka,1000,Leyka +delete_others_car_listings,wp-car-manager,3000,WP Car Manager +delete_others_cctor_coupons,coupon-creator,10000,Coupon Creator +delete_others_chronoslys,chronosly-events-calendar,4000,Chronosly Events Calendar +delete_others_classified_listings,classifieds-wp,800,Classifieds WP +delete_others_clients,upstream,1000,WordPress Project Management by UpStream +delete_others_courses,lifterlms,8000,LifterLMS +delete_others_ctas,cta,10000,WordPress Calls to Action +delete_others_custom_csss,custom-css-js,100000,Simple Custom CSS and JS +delete_others_ditty_news_tickers,ditty-news-ticker,40000,Ditty News Ticker +delete_others_documents,wp-document-revisions,4000,WP Document Revisions +delete_others_donations,charitable,10000,Charitable – Donation Plugin +delete_others_donations,leyka,1000,Leyka +delete_others_dsn_notes,admin-dashboard-site-notes,3000,Dashboard Site Notes +delete_others_edr_courses,educator,1000,Educator 2 +delete_others_edr_lessons,educator,1000,Educator 2 +delete_others_edr_memberships,educator,1000,Educator 2 +delete_others_email_templates,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +delete_others_emails,mailer-dragon,300,Mailer Dragon – Email Marketing Plugin for WordPress +delete_others_emd_agents,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +delete_others_emd_canned_responses,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +delete_others_emd_contacts,wp-easy-contact,600,Best Contact Management Software for WordPress +delete_others_emd_employees,employee-directory,400,Staff Directory – Employee Directory for WordPress +delete_others_emd_quotes,request-a-quote,1000,Request a Quote +delete_others_emd_tickets,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +delete_others_epa_albums,easy-photo-album,5000,Easy Photo Album +delete_others_event_listings,wp-event-manager,1000,WP Event Manager +delete_others_event_magics,kikfyre-events-calendar-tickets,200,"Events, Calendars & Tickets – Event Kikfyre" +delete_others_events,event-organiser,40000,Event Organiser +delete_others_events,events-maker,4000,Events Maker by dFactory +delete_others_events,events-manager,100000,Events Manager +delete_others_events,quick-event-manager,5000,Quick Event Manager +delete_others_everest_forms,everest-forms,40000,Everest Forms – Easy Contact Form and Form Builder for WordPress +delete_others_fa_items,featured-articles-lite,3000,FA Lite – WP responsive slider plugin +delete_others_fbtabs,facebook-tab-manager,1000,Facebook Tab Manager +delete_others_feed_sources,wp-rss-aggregator,60000,WP RSS Aggregator +delete_others_feeds,wp-rss-aggregator,60000,WP RSS Aggregator +delete_others_fep_announcements,front-end-pm,8000,Front End PM +delete_others_fep_messages,front-end-pm,8000,Front End PM +delete_others_flexible_invoices,flexible-invoices,1000,Flexible Invoices for WordPress +delete_others_food_groups,restaurantpress,3000,RestaurantPress +delete_others_food_menus,restaurantpress,3000,RestaurantPress +delete_others_forms,pronamic-ideal,6000,Pronamic Pay +delete_others_forums,bbpress,300000,bbPress +delete_others_games,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_others_give_forms,give,50000,Give – Donation Plugin and Fundraising Platform +delete_others_give_payments,give,50000,Give – Donation Plugin and Fundraising Platform +delete_others_glossaries,glossary-by-codeat,1000,Glossary +delete_others_hb_bookings,wp-hotel-booking,7000,WP Hotel Booking +delete_others_hb_rooms,wp-hotel-booking,7000,WP Hotel Booking +delete_others_hf_membership_plans,xa-woocommerce-memberships,400,Memberships for WooCommerce +delete_others_hf_user_memberships,xa-woocommerce-memberships,400,Memberships for WooCommerce +delete_others_hotel_locations,awebooking,6000,AweBooking – Hotel Booking System +delete_others_hotel_services,awebooking,6000,AweBooking – Hotel Booking System +delete_others_ib_edu_memberships,ibeducator,1000,Educator +delete_others_ib_educator_courses,ibeducator,1000,Educator +delete_others_ib_educator_lessons,ibeducator,1000,Educator +delete_others_ims_gallerys,image-store,900,Image Store +delete_others_inbound-forms,cta,10000,WordPress Calls to Action +delete_others_inbound-forms,landing-pages,10000,WordPress Landing Pages +delete_others_inbound-forms,leads,7000,WordPress Leads +delete_others_insertcodes,insert-code,300,Insert Code +delete_others_invoices,essential-real-estate,3000,Essential Real Estate +delete_others_job_listings,wp-job-manager,100000,WP Job Manager +delete_others_jscp_matchs,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +delete_others_jscp_players,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +delete_others_jscp_teams,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +delete_others_klaviyo_shop_carts,klaviyo-for-woocommerce,1000,Klaviyo for WooCommerce +delete_others_landing_pages,landing-pages,10000,WordPress Landing Pages +delete_others_leads,cta,10000,WordPress Calls to Action +delete_others_leads,landing-pages,10000,WordPress Landing Pages +delete_others_leads,leads,7000,WordPress Leads +delete_others_legalpack_pages,legalpack,400,Legalpack +delete_others_lessons,lifterlms,8000,LifterLMS +delete_others_listings,auto-listings,400,Auto Listings +delete_others_listings,wp-real-estate,400,WP Real Estate +delete_others_listings,wpcasa,2000,WPCasa +delete_others_locations,events-manager,100000,Events Manager +delete_others_lp_courses,learnpress,50000,LearnPress – WordPress LMS Plugin +delete_others_lp_lessons,learnpress,50000,LearnPress – WordPress LMS Plugin +delete_others_lp_orders,learnpress,50000,LearnPress – WordPress LMS Plugin +delete_others_mbdb_book,mooberry-book-manager,1000,Mooberry Book Manager +delete_others_mbdb_book_grid,mooberry-book-manager,1000,Mooberry Book Manager +delete_others_mbdb_book_grids,mooberry-book-manager,1000,Mooberry Book Manager +delete_others_mbdb_books,mooberry-book-manager,1000,Mooberry Book Manager +delete_others_meals,restaurant-manager,800,Restaurant Manager +delete_others_memberships,lifterlms,8000,LifterLMS +delete_others_mp_menu_items,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +delete_others_mprm_orders,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +delete_others_nc_references,nelio-content,7000,Nelio Content – Social Media Marketing Automation +delete_others_nemus-sliders,nemus-slider,2000,Nemus Slider +delete_others_news,news-manager,3000,News Manager +delete_others_newsletters,alo-easymail,10000,ALO EasyMail Newsletter +delete_others_opalestate_agentss,opal-estate,1000,Opal Estate +delete_others_opalestate_propertiess,opal-estate,1000,Opal Estate +delete_others_packages,essential-real-estate,3000,Essential Real Estate +delete_others_payments,pronamic-ideal,6000,Pronamic Pay +delete_others_players,team-rosters,800,Team Rosters +delete_others_playlists,radio-station,1000,Radio Station +delete_others_plugin_filters,plugin-organizer,10000,Plugin Organizer +delete_others_plugin_groups,plugin-organizer,10000,Plugin Organizer +delete_others_portfolio_projects,custom-content-portfolio,1000,Custom Content Portfolio +delete_others_portfolios,flash-toolkit,30000,Flash Toolkit +delete_others_portfolios,suffice-toolkit,5000,Suffice Toolkit +delete_others_pricing_rates,awebooking,6000,AweBooking – Hotel Booking System +delete_others_product_sets,datafeedr-product-sets,1000,Datafeedr Product Sets +delete_others_products,design-approval-system,500,Design Approval System +delete_others_products,easy-digital-downloads,60000,Easy Digital Downloads +delete_others_products,ecommerce-product-catalog,10000,eCommerce Product Catalog Plugin for WordPress +delete_others_products,gnucommerce,1000,GNUCommerce +delete_others_products,jigoshop,4000,Jigoshop +delete_others_products,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_others_products,post-type-x,1000,Product Catalog X +delete_others_products,products,300,WP Products +delete_others_products,webmaster-user-role,8000,Webmaster User Role +delete_others_products,woocommerce,4000000,WooCommerce +delete_others_products,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +delete_others_profile_cct,profile-custom-content-type,200,Profile CCT +delete_others_projects,upstream,1000,WordPress Project Management by UpStream +delete_others_propertys,essential-real-estate,3000,Essential Real Estate +delete_others_psp_projects,project-panorama-lite,1000,Project Panorama +delete_others_questions,lifterlms,8000,LifterLMS +delete_others_quizzes,lifterlms,8000,LifterLMS +delete_others_quotes,mg-quotes,300,mg Quotes +delete_others_recurring_events,events-manager,100000,Events Manager +delete_others_redirects,wp-redirects,700,WP Redirects +delete_others_rem_properties,real-estate-manager,1000,Real Estate Manager – Property Listing and Agent Management +delete_others_replies,bbpress,300000,bbPress +delete_others_reservations,restaurant-manager,800,Restaurant Manager +delete_others_resume_positions,wp-resume,700,WP Resume +delete_others_room_reservations,wp-hotelier,1000,Easy WP Hotelier +delete_others_room_types,awebooking,6000,AweBooking – Hotel Booking System +delete_others_rooms,wp-hotelier,1000,Easy WP Hotelier +delete_others_rsvpmakers,rsvpmaker,1000,RSVPMaker +delete_others_schedules,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_others_sgpb_popups,popup-builder,100000,Popup Builder – Responsive WordPress Pop up – Subscription & Newsletter +delete_others_shop_coupons,jigoshop,4000,Jigoshop +delete_others_shop_coupons,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_others_shop_coupons,webmaster-user-role,8000,Webmaster User Role +delete_others_shop_coupons,woocommerce,4000000,WooCommerce +delete_others_shop_discounts,easy-digital-downloads,60000,Easy Digital Downloads +delete_others_shop_emails,jigoshop,4000,Jigoshop +delete_others_shop_emails,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_others_shop_orders,jigoshop,4000,Jigoshop +delete_others_shop_orders,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_others_shop_orders,webmaster-user-role,8000,Webmaster User Role +delete_others_shop_orders,woocommerce,4000000,WooCommerce +delete_others_shop_payments,easy-digital-downloads,60000,Easy Digital Downloads +delete_others_shop_webhooks,woocommerce,4000000,WooCommerce +delete_others_shows,radio-station,1000,Radio Station +delete_others_sln_attendants,salon-booking-system,5000,Salon booking system +delete_others_sln_bookings,salon-booking-system,5000,Salon booking system +delete_others_sln_services,salon-booking-system,5000,Salon booking system +delete_others_snippets,wp-snippets,1000,WP Snippets +delete_others_sp_calendars,sportspress,20000,SportsPress – Sports Club & League Manager +delete_others_sp_configs,sportspress,20000,SportsPress – Sports Club & League Manager +delete_others_sp_events,sportspress,20000,SportsPress – Sports Club & League Manager +delete_others_sp_lists,sportspress,20000,SportsPress – Sports Club & League Manager +delete_others_sp_players,sportspress,20000,SportsPress – Sports Club & League Manager +delete_others_sp_staffs,sportspress,20000,SportsPress – Sports Club & League Manager +delete_others_sp_tables,sportspress,20000,SportsPress – Sports Club & League Manager +delete_others_sp_teams,sportspress,20000,SportsPress – Sports Club & League Manager +delete_others_sports,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_others_stm_lms_posts,masterstudy-lms-learning-management-system,400,MasterStudy LMS – Free Learning Management System WordPress Plugin for Online Courses +delete_others_store_orders,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +delete_others_stores,wp-store-locator,50000,WP Store Locator +delete_others_sunshine_galleries,sunshine-photo-cart,2000,Sunshine Photo Cart +delete_others_sunshine_orders,sunshine-photo-cart,2000,Sunshine Photo Cart +delete_others_sunshine_products,sunshine-photo-cart,2000,Sunshine Photo Cart +delete_others_support_tickets,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +delete_others_tc_events,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_others_tc_orders,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_others_tc_tickets,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_others_tc_tickets_instances,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_others_teams,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_others_tm-propertys,cherry-real-estate,600,Cherry Real Estate +delete_others_topics,bbpress,300000,bbPress +delete_others_total_slider_slides,total-slider,500,Total Slider +delete_others_trans_logs,essential-real-estate,3000,Essential Real Estate +delete_others_translations,simple-punctual-translation,200,Simple Punctual Translation +delete_others_tribe_events,the-events-calendar,700000,The Events Calendar +delete_others_tribe_organizers,the-events-calendar,700000,The Events Calendar +delete_others_tribe_venues,the-events-calendar,700000,The Events Calendar +delete_others_user_packages,essential-real-estate,3000,Essential Real Estate +delete_others_user_registrations,user-registration,7000,"User Registration – Custom Registration Form, Login and User Profile for WordPress" +delete_others_vacancies,job-board,300,Job Board by BestWebSoft +delete_others_venues,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_others_wctrl_contents,widgets-control,1000,Widgets Control +delete_others_wd_ads_adverts,ad-manager-wd,700,Ad Manager by WD – Advanced Ad Manager plugin +delete_others_wd_ads_schedules,ad-manager-wd,700,Ad Manager by WD – Advanced Ad Manager plugin +delete_others_wiki_pages,wordpress-wiki,400,WordPress Wiki +delete_others_wordlift_entities,wordlift,400,WordLift – AI powered SEO +delete_others_wpautoterms_pages,auto-terms-of-service-and-privacy-policy,100000,Auto Terms of Service and Privacy Policy (WP AutoTerms) +delete_others_wpcm_clubs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_others_wpcm_matchs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_others_wpcm_players,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_others_wpcm_sponsors,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_others_wpcm_staffs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_others_wpdiscuz_forms,wpdiscuz,40000,Comments – wpDiscuz +delete_others_wpfc_sermons,sermon-manager-for-wordpress,9000,Sermon Manager +delete_others_wpi_discounts,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_others_wpi_invoices,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_others_wpi_items,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_others_wpi_quotes,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_others_wpp_properties,wp-property,5000,WP-Property – WordPress Powered Real Estate and Property Management +delete_others_wppizzas,wppizza,2000,WPPizza +delete_others_wprm_reservations,wp-restaurant-manager,700,WP Restaurant Manager +delete_others_wpsdealss,deals-engine,200,Social Deals Engine +delete_others_wpsdealssaless,deals-engine,200,Social Deals Engine +delete_others_wpse_profiles,wp-smart-editor,900,WP Smart Editor +delete_others_ycd_countdowns,countdown-builder,1000,Countdown +delete_own_touchpoints,ukuupeople-the-simple-crm,1000,CRM: Contact Management Simplified – UkuuPeople +delete_own_ukuupeoples,ukuupeople-the-simple-crm,1000,CRM: Contact Management Simplified – UkuuPeople +delete_own_yop_polls,yop-poll,20000,YOP Poll +delete_own_yop_polls_templates,yop-poll,20000,YOP Poll +delete_package,essential-real-estate,3000,Essential Real Estate +delete_package_terms,essential-real-estate,3000,Essential Real Estate +delete_packages,essential-real-estate,3000,Essential Real Estate +delete_page_in_section,bu-section-editing,300,BU Section Editing +delete_payment,pronamic-ideal,6000,Pronamic Pay +delete_payments,pronamic-ideal,6000,Pronamic Pay +delete_person_area,campus-directory,200,Faculty Staff and Student Directory Plugin – Campus Directory +delete_person_location,campus-directory,200,Faculty Staff and Student Directory Plugin – Campus Directory +delete_person_rareas,campus-directory,200,Faculty Staff and Student Directory Plugin – Campus Directory +delete_person_title,campus-directory,200,Faculty Staff and Student Directory Plugin – Campus Directory +delete_player,team-rosters,800,Team Rosters +delete_players,team-rosters,800,Team Rosters +delete_playlists,radio-station,1000,Radio Station +delete_plugin_filter,plugin-organizer,10000,Plugin Organizer +delete_plugin_filters,plugin-organizer,10000,Plugin Organizer +delete_plugin_group,plugin-organizer,10000,Plugin Organizer +delete_plugin_groups,plugin-organizer,10000,Plugin Organizer +delete_portfolio,flash-toolkit,30000,Flash Toolkit +delete_portfolio,suffice-toolkit,5000,Suffice Toolkit +delete_portfolio,visual-portfolio,7000,Visual Portfolio +delete_portfolio_categories,custom-content-portfolio,1000,Custom Content Portfolio +delete_portfolio_projects,custom-content-portfolio,1000,Custom Content Portfolio +delete_portfolio_tags,custom-content-portfolio,1000,Custom Content Portfolio +delete_portfolio_terms,flash-toolkit,30000,Flash Toolkit +delete_portfolio_terms,suffice-toolkit,5000,Suffice Toolkit +delete_portfolios,flash-toolkit,30000,Flash Toolkit +delete_portfolios,suffice-toolkit,5000,Suffice Toolkit +delete_portfolios,visual-portfolio,7000,Visual Portfolio +delete_post_ims_gallery,image-store,900,Image Store +delete_post_in_section,bu-section-editing,300,BU Section Editing +delete_posts_ims_gallery,image-store,900,Image Store +delete_pricing_rate,awebooking,6000,AweBooking – Hotel Booking System +delete_pricing_rate_terms,awebooking,6000,AweBooking – Hotel Booking System +delete_pricing_rates,awebooking,6000,AweBooking – Hotel Booking System +delete_private_acadp_fields,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +delete_private_acadp_listings,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +delete_private_acadp_payments,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +delete_private_aec_events,another-events-calendar,800,Another Events Calendar +delete_private_aec_organizers,another-events-calendar,800,Another Events Calendar +delete_private_aec_venues,another-events-calendar,800,Another Events Calendar +delete_private_affiliate_keywords,affiliate,700,Affiliate +delete_private_agents,essential-real-estate,3000,Essential Real Estate +delete_private_aggregator-records,the-events-calendar,700000,The Events Calendar +delete_private_ai1ec_events,all-in-one-event-calendar,100000,All-in-One Event Calendar +delete_private_aiovg_videos,all-in-one-video-gallery,1000,All-in-One Video Gallery +delete_private_anb_animations,alert-notice-boxes,1000,Alert Notice Boxes +delete_private_anb_animations_out,alert-notice-boxes,1000,Alert Notice Boxes +delete_private_anb_designs,alert-notice-boxes,1000,Alert Notice Boxes +delete_private_anb_locations,alert-notice-boxes,1000,Alert Notice Boxes +delete_private_anbs,alert-notice-boxes,1000,Alert Notice Boxes +delete_private_archivs,archive,700,Archive +delete_private_articles,issuem,1000,IssueM +delete_private_at_biz_dirs,directorist,500,Directorist – Business Directory Plugin +delete_private_atbdp_orders,directorist,500,Directorist – Business Directory Plugin +delete_private_awebookings,awebooking,6000,AweBooking – Hotel Booking System +delete_private_birs_appointments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_private_birs_clients,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_private_birs_locations,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_private_birs_payments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_private_birs_services,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_private_birs_staffs,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_private_blocks,gutenberg,500000,Gutenberg +delete_private_board_committees,nonprofit-board-management,400,Nonprofit Board Management +delete_private_board_events,nonprofit-board-management,400,Nonprofit Board Management +delete_private_books,novelist,800,Novelist +delete_private_calp_events,calpress-event-calendar,5000,CalPress Calendar +delete_private_campaigns,charitable,10000,Charitable – Donation Plugin +delete_private_campaigns,leyka,1000,Leyka +delete_private_car_listings,wp-car-manager,3000,WP Car Manager +delete_private_cctor_coupons,coupon-creator,10000,Coupon Creator +delete_private_chronoslys,chronosly-events-calendar,4000,Chronosly Events Calendar +delete_private_classified_listings,classifieds-wp,800,Classifieds WP +delete_private_clients,upstream,1000,WordPress Project Management by UpStream +delete_private_courses,lifterlms,8000,LifterLMS +delete_private_ctas,cta,10000,WordPress Calls to Action +delete_private_ditty_news_tickers,ditty-news-ticker,40000,Ditty News Ticker +delete_private_documents,wp-document-revisions,4000,WP Document Revisions +delete_private_donations,charitable,10000,Charitable – Donation Plugin +delete_private_donations,leyka,1000,Leyka +delete_private_edr_courses,educator,1000,Educator 2 +delete_private_edr_lessons,educator,1000,Educator 2 +delete_private_edr_memberships,educator,1000,Educator 2 +delete_private_email_templates,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +delete_private_emails,mailer-dragon,300,Mailer Dragon – Email Marketing Plugin for WordPress +delete_private_emd_agents,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +delete_private_emd_canned_responses,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +delete_private_emd_contacts,wp-easy-contact,600,Best Contact Management Software for WordPress +delete_private_emd_employees,employee-directory,400,Staff Directory – Employee Directory for WordPress +delete_private_emd_quotes,request-a-quote,1000,Request a Quote +delete_private_emd_tickets,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +delete_private_epa_albums,easy-photo-album,5000,Easy Photo Album +delete_private_event_listings,wp-event-manager,1000,WP Event Manager +delete_private_event_magics,kikfyre-events-calendar-tickets,200,"Events, Calendars & Tickets – Event Kikfyre" +delete_private_events,quick-event-manager,5000,Quick Event Manager +delete_private_everest_forms,everest-forms,40000,Everest Forms – Easy Contact Form and Form Builder for WordPress +delete_private_fbtabs,facebook-tab-manager,1000,Facebook Tab Manager +delete_private_feed_sources,wp-rss-aggregator,60000,WP RSS Aggregator +delete_private_feeds,wp-rss-aggregator,60000,WP RSS Aggregator +delete_private_fep_announcements,front-end-pm,8000,Front End PM +delete_private_fep_messages,front-end-pm,8000,Front End PM +delete_private_flexible_invoices,flexible-invoices,1000,Flexible Invoices for WordPress +delete_private_food_groups,restaurantpress,3000,RestaurantPress +delete_private_food_menus,restaurantpress,3000,RestaurantPress +delete_private_forms,pronamic-ideal,6000,Pronamic Pay +delete_private_give_forms,give,50000,Give – Donation Plugin and Fundraising Platform +delete_private_give_payments,give,50000,Give – Donation Plugin and Fundraising Platform +delete_private_glossaries,glossary-by-codeat,1000,Glossary +delete_private_hb_bookings,wp-hotel-booking,7000,WP Hotel Booking +delete_private_hb_rooms,wp-hotel-booking,7000,WP Hotel Booking +delete_private_hf_membership_plans,xa-woocommerce-memberships,400,Memberships for WooCommerce +delete_private_hf_user_memberships,xa-woocommerce-memberships,400,Memberships for WooCommerce +delete_private_hotel_locations,awebooking,6000,AweBooking – Hotel Booking System +delete_private_hotel_services,awebooking,6000,AweBooking – Hotel Booking System +delete_private_ib_edu_memberships,ibeducator,1000,Educator +delete_private_ib_educator_courses,ibeducator,1000,Educator +delete_private_ib_educator_lessons,ibeducator,1000,Educator +delete_private_ims_gallery,image-store,900,Image Store +delete_private_inbound-forms,cta,10000,WordPress Calls to Action +delete_private_inbound-forms,landing-pages,10000,WordPress Landing Pages +delete_private_inbound-forms,leads,7000,WordPress Leads +delete_private_insertcodes,insert-code,300,Insert Code +delete_private_invoices,essential-real-estate,3000,Essential Real Estate +delete_private_job_listings,wp-job-manager,100000,WP Job Manager +delete_private_klaviyo_shop_carts,klaviyo-for-woocommerce,1000,Klaviyo for WooCommerce +delete_private_landing_pages,landing-pages,10000,WordPress Landing Pages +delete_private_leads,cta,10000,WordPress Calls to Action +delete_private_leads,landing-pages,10000,WordPress Landing Pages +delete_private_leads,leads,7000,WordPress Leads +delete_private_legalpack_pages,legalpack,400,Legalpack +delete_private_lessons,lifterlms,8000,LifterLMS +delete_private_listings,auto-listings,400,Auto Listings +delete_private_listings,wp-real-estate,400,WP Real Estate +delete_private_listings,wpcasa,2000,WPCasa +delete_private_lp_courses,learnpress,50000,LearnPress – WordPress LMS Plugin +delete_private_lp_lessons,learnpress,50000,LearnPress – WordPress LMS Plugin +delete_private_lp_orders,learnpress,50000,LearnPress – WordPress LMS Plugin +delete_private_meals,restaurant-manager,800,Restaurant Manager +delete_private_memberships,lifterlms,8000,LifterLMS +delete_private_mp_menu_items,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +delete_private_mprm_orders,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +delete_private_nc_references,nelio-content,7000,Nelio Content – Social Media Marketing Automation +delete_private_nemus-sliders,nemus-slider,2000,Nemus Slider +delete_private_opalestate_agentss,opal-estate,1000,Opal Estate +delete_private_opalestate_propertiess,opal-estate,1000,Opal Estate +delete_private_packages,essential-real-estate,3000,Essential Real Estate +delete_private_payments,pronamic-ideal,6000,Pronamic Pay +delete_private_playlists,radio-station,1000,Radio Station +delete_private_plugin_filters,plugin-organizer,10000,Plugin Organizer +delete_private_plugin_groups,plugin-organizer,10000,Plugin Organizer +delete_private_portfolio_projects,custom-content-portfolio,1000,Custom Content Portfolio +delete_private_portfolios,flash-toolkit,30000,Flash Toolkit +delete_private_portfolios,suffice-toolkit,5000,Suffice Toolkit +delete_private_pricing_rates,awebooking,6000,AweBooking – Hotel Booking System +delete_private_product_sets,datafeedr-product-sets,1000,Datafeedr Product Sets +delete_private_products,design-approval-system,500,Design Approval System +delete_private_products,easy-digital-downloads,60000,Easy Digital Downloads +delete_private_products,ecommerce-product-catalog,10000,eCommerce Product Catalog Plugin for WordPress +delete_private_products,gnucommerce,1000,GNUCommerce +delete_private_products,jigoshop,4000,Jigoshop +delete_private_products,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_private_products,post-type-x,1000,Product Catalog X +delete_private_products,products,300,WP Products +delete_private_products,webmaster-user-role,8000,Webmaster User Role +delete_private_products,woocommerce,4000000,WooCommerce +delete_private_products,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +delete_private_projects,upstream,1000,WordPress Project Management by UpStream +delete_private_propertys,essential-real-estate,3000,Essential Real Estate +delete_private_psp_projects,project-panorama-lite,1000,Project Panorama +delete_private_questions,lifterlms,8000,LifterLMS +delete_private_quizzes,lifterlms,8000,LifterLMS +delete_private_quotes,mg-quotes,300,mg Quotes +delete_private_redirects,wp-redirects,700,WP Redirects +delete_private_rem_properties,real-estate-manager,1000,Real Estate Manager – Property Listing and Agent Management +delete_private_reservations,restaurant-manager,800,Restaurant Manager +delete_private_resume_positions,wp-resume,700,WP Resume +delete_private_room_reservations,wp-hotelier,1000,Easy WP Hotelier +delete_private_room_types,awebooking,6000,AweBooking – Hotel Booking System +delete_private_rooms,wp-hotelier,1000,Easy WP Hotelier +delete_private_rsvpmakers,rsvpmaker,1000,RSVPMaker +delete_private_sgpb_popup,popup-builder,100000,Popup Builder – Responsive WordPress Pop up – Subscription & Newsletter +delete_private_sgpb_popups,popup-builder,100000,Popup Builder – Responsive WordPress Pop up – Subscription & Newsletter +delete_private_shop_coupons,jigoshop,4000,Jigoshop +delete_private_shop_coupons,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_private_shop_coupons,webmaster-user-role,8000,Webmaster User Role +delete_private_shop_coupons,woocommerce,4000000,WooCommerce +delete_private_shop_discounts,easy-digital-downloads,60000,Easy Digital Downloads +delete_private_shop_emails,jigoshop,4000,Jigoshop +delete_private_shop_emails,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_private_shop_orders,jigoshop,4000,Jigoshop +delete_private_shop_orders,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_private_shop_orders,webmaster-user-role,8000,Webmaster User Role +delete_private_shop_orders,woocommerce,4000000,WooCommerce +delete_private_shop_payments,easy-digital-downloads,60000,Easy Digital Downloads +delete_private_shop_webhooks,woocommerce,4000000,WooCommerce +delete_private_shows,radio-station,1000,Radio Station +delete_private_sln_attendants,salon-booking-system,5000,Salon booking system +delete_private_sln_bookings,salon-booking-system,5000,Salon booking system +delete_private_sln_services,salon-booking-system,5000,Salon booking system +delete_private_snippets,wp-snippets,1000,WP Snippets +delete_private_snitchs,snitch,1000,Snitch +delete_private_sp_calendars,sportspress,20000,SportsPress – Sports Club & League Manager +delete_private_sp_configs,sportspress,20000,SportsPress – Sports Club & League Manager +delete_private_sp_events,sportspress,20000,SportsPress – Sports Club & League Manager +delete_private_sp_lists,sportspress,20000,SportsPress – Sports Club & League Manager +delete_private_sp_players,sportspress,20000,SportsPress – Sports Club & League Manager +delete_private_sp_staffs,sportspress,20000,SportsPress – Sports Club & League Manager +delete_private_sp_tables,sportspress,20000,SportsPress – Sports Club & League Manager +delete_private_sp_teams,sportspress,20000,SportsPress – Sports Club & League Manager +delete_private_stm_lms_posts,masterstudy-lms-learning-management-system,400,MasterStudy LMS – Free Learning Management System WordPress Plugin for Online Courses +delete_private_store_orders,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +delete_private_stores,wp-store-locator,50000,WP Store Locator +delete_private_sunshine_galleries,sunshine-photo-cart,2000,Sunshine Photo Cart +delete_private_sunshine_orders,sunshine-photo-cart,2000,Sunshine Photo Cart +delete_private_sunshine_products,sunshine-photo-cart,2000,Sunshine Photo Cart +delete_private_support_tickets,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +delete_private_tc_events,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_private_tc_orders,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_private_tc_tickets,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_private_tc_tickets_instances,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_private_ticket,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +delete_private_tm-propertys,cherry-real-estate,600,Cherry Real Estate +delete_private_total_slider_slides,total-slider,500,Total Slider +delete_private_trans_logs,essential-real-estate,3000,Essential Real Estate +delete_private_translations,simple-punctual-translation,200,Simple Punctual Translation +delete_private_tribe_events,the-events-calendar,700000,The Events Calendar +delete_private_tribe_organizers,the-events-calendar,700000,The Events Calendar +delete_private_tribe_venues,the-events-calendar,700000,The Events Calendar +delete_private_user_packages,essential-real-estate,3000,Essential Real Estate +delete_private_user_registrations,user-registration,7000,"User Registration – Custom Registration Form, Login and User Profile for WordPress" +delete_private_vacancies,job-board,300,Job Board by BestWebSoft +delete_private_wctrl_contents,widgets-control,1000,Widgets Control +delete_private_wordlift_entities,wordlift,400,WordLift – AI powered SEO +delete_private_wpautoterms_pages,auto-terms-of-service-and-privacy-policy,100000,Auto Terms of Service and Privacy Policy (WP AutoTerms) +delete_private_wpcm_clubs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_private_wpcm_matchs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_private_wpcm_players,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_private_wpcm_sponsors,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_private_wpcm_staffs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_private_wpdiscuz_forms,wpdiscuz,40000,Comments – wpDiscuz +delete_private_wpfc_sermons,sermon-manager-for-wordpress,9000,Sermon Manager +delete_private_wpi_discounts,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_private_wpi_invoices,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_private_wpi_items,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_private_wpi_quotes,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_private_wpp_properties,wp-property,5000,WP-Property – WordPress Powered Real Estate and Property Management +delete_private_wprm_reservations,wp-restaurant-manager,700,WP Restaurant Manager +delete_private_wpsdealss,deals-engine,200,Social Deals Engine +delete_private_wpsdealssaless,deals-engine,200,Social Deals Engine +delete_private_ycd_countdown,countdown-builder,1000,Countdown +delete_private_ycd_countdowns,countdown-builder,1000,Countdown +delete_product,dc-woocommerce-multi-vendor,10000,WC Marketplace +delete_product,design-approval-system,500,Design Approval System +delete_product,easy-digital-downloads,60000,Easy Digital Downloads +delete_product,gnucommerce,1000,GNUCommerce +delete_product,jigoshop,4000,Jigoshop +delete_product,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_product,webmaster-user-role,8000,Webmaster User Role +delete_product,woocommerce,4000000,WooCommerce +delete_product,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +delete_product,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +delete_product_categories,ecommerce-product-catalog,10000,eCommerce Product Catalog Plugin for WordPress +delete_product_categories,post-type-x,1000,Product Catalog X +delete_product_set,datafeedr-product-sets,1000,Datafeedr Product Sets +delete_product_sets,datafeedr-product-sets,1000,Datafeedr Product Sets +delete_product_terms,easy-digital-downloads,60000,Easy Digital Downloads +delete_product_terms,jigoshop,4000,Jigoshop +delete_product_terms,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_product_terms,webmaster-user-role,8000,Webmaster User Role +delete_product_terms,woocommerce,4000000,WooCommerce +delete_products,dc-woocommerce-multi-vendor,10000,WC Marketplace +delete_products,design-approval-system,500,Design Approval System +delete_products,easy-digital-downloads,60000,Easy Digital Downloads +delete_products,ecommerce-product-catalog,10000,eCommerce Product Catalog Plugin for WordPress +delete_products,gnucommerce,1000,GNUCommerce +delete_products,jigoshop,4000,Jigoshop +delete_products,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_products,post-type-x,1000,Product Catalog X +delete_products,products,300,WP Products +delete_products,webmaster-user-role,8000,Webmaster User Role +delete_products,woocommerce,4000000,WooCommerce +delete_products,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +delete_products,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +delete_profile_cct,profile-custom-content-type,200,Profile CCT +delete_project,upstream,1000,WordPress Project Management by UpStream +delete_project_discussion,upstream,1000,WordPress Project Management by UpStream +delete_project_terms,upstream,1000,WordPress Project Management by UpStream +delete_projects,projectmanager,400,ProjectManager +delete_projects,upstream,1000,WordPress Project Management by UpStream +delete_property,essential-real-estate,3000,Essential Real Estate +delete_property_terms,essential-real-estate,3000,Essential Real Estate +delete_propertys,essential-real-estate,3000,Essential Real Estate +delete_psp_projects,project-panorama-lite,1000,Project Panorama +delete_published_acadp_fields,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +delete_published_acadp_listings,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +delete_published_acadp_payments,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +delete_published_ads,apply-online,5000,Apply Online +delete_published_aec_events,another-events-calendar,800,Another Events Calendar +delete_published_aec_organizers,another-events-calendar,800,Another Events Calendar +delete_published_aec_venues,another-events-calendar,800,Another Events Calendar +delete_published_affiliate_keywords,affiliate,700,Affiliate +delete_published_agents,essential-real-estate,3000,Essential Real Estate +delete_published_aggregator-records,the-events-calendar,700000,The Events Calendar +delete_published_ai1ec_events,all-in-one-event-calendar,100000,All-in-One Event Calendar +delete_published_aiovg_videos,all-in-one-video-gallery,1000,All-in-One Video Gallery +delete_published_anb_animations,alert-notice-boxes,1000,Alert Notice Boxes +delete_published_anb_animations_out,alert-notice-boxes,1000,Alert Notice Boxes +delete_published_anb_designs,alert-notice-boxes,1000,Alert Notice Boxes +delete_published_anb_locations,alert-notice-boxes,1000,Alert Notice Boxes +delete_published_anbs,alert-notice-boxes,1000,Alert Notice Boxes +delete_published_applications,apply-online,5000,Apply Online +delete_published_archivs,archive,700,Archive +delete_published_articles,issuem,1000,IssueM +delete_published_at_biz_dirs,directorist,500,Directorist – Business Directory Plugin +delete_published_atbdp_orders,directorist,500,Directorist – Business Directory Plugin +delete_published_awebookings,awebooking,6000,AweBooking – Hotel Booking System +delete_published_birs_appointments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_published_birs_clients,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_published_birs_locations,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_published_birs_payments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_published_birs_services,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_published_birs_staffs,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +delete_published_blocks,gutenberg,500000,Gutenberg +delete_published_board_committees,nonprofit-board-management,400,Nonprofit Board Management +delete_published_board_events,nonprofit-board-management,400,Nonprofit Board Management +delete_published_book-reviews,book-review-library,700,Book Review Library +delete_published_books,novelist,800,Novelist +delete_published_bps_forms,bp-profile-search,10000,BP Profile Search +delete_published_calp_events,calpress-event-calendar,5000,CalPress Calendar +delete_published_campaigns,charitable,10000,Charitable – Donation Plugin +delete_published_campaigns,leyka,1000,Leyka +delete_published_car_listings,wp-car-manager,3000,WP Car Manager +delete_published_cctor_coupons,coupon-creator,10000,Coupon Creator +delete_published_chronoslys,chronosly-events-calendar,4000,Chronosly Events Calendar +delete_published_classified_listings,classifieds-wp,800,Classifieds WP +delete_published_clients,upstream,1000,WordPress Project Management by UpStream +delete_published_courses,lifterlms,8000,LifterLMS +delete_published_ctas,cta,10000,WordPress Calls to Action +delete_published_custom_csss,custom-css-js,100000,Simple Custom CSS and JS +delete_published_ditty_news_tickers,ditty-news-ticker,40000,Ditty News Ticker +delete_published_documents,wp-document-revisions,4000,WP Document Revisions +delete_published_donations,charitable,10000,Charitable – Donation Plugin +delete_published_donations,leyka,1000,Leyka +delete_published_edr_courses,educator,1000,Educator 2 +delete_published_edr_lessons,educator,1000,Educator 2 +delete_published_edr_memberships,educator,1000,Educator 2 +delete_published_email_templates,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +delete_published_emails,mailer-dragon,300,Mailer Dragon – Email Marketing Plugin for WordPress +delete_published_emd_agents,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +delete_published_emd_canned_responses,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +delete_published_emd_contacts,wp-easy-contact,600,Best Contact Management Software for WordPress +delete_published_emd_employees,employee-directory,400,Staff Directory – Employee Directory for WordPress +delete_published_emd_quotes,request-a-quote,1000,Request a Quote +delete_published_emd_tickets,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +delete_published_epa_albums,easy-photo-album,5000,Easy Photo Album +delete_published_event_listings,wp-event-manager,1000,WP Event Manager +delete_published_event_magics,kikfyre-events-calendar-tickets,200,"Events, Calendars & Tickets – Event Kikfyre" +delete_published_events,events-maker,4000,Events Maker by dFactory +delete_published_events,quick-event-manager,5000,Quick Event Manager +delete_published_everest_forms,everest-forms,40000,Everest Forms – Easy Contact Form and Form Builder for WordPress +delete_published_fbtabs,facebook-tab-manager,1000,Facebook Tab Manager +delete_published_feed_sources,wp-rss-aggregator,60000,WP RSS Aggregator +delete_published_feeds,wp-rss-aggregator,60000,WP RSS Aggregator +delete_published_fep_announcements,front-end-pm,8000,Front End PM +delete_published_fep_messages,front-end-pm,8000,Front End PM +delete_published_flexible_invoices,flexible-invoices,1000,Flexible Invoices for WordPress +delete_published_food_groups,restaurantpress,3000,RestaurantPress +delete_published_food_menus,restaurantpress,3000,RestaurantPress +delete_published_forms,pronamic-ideal,6000,Pronamic Pay +delete_published_games,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_published_give_forms,give,50000,Give – Donation Plugin and Fundraising Platform +delete_published_give_payments,give,50000,Give – Donation Plugin and Fundraising Platform +delete_published_glossaries,glossary-by-codeat,1000,Glossary +delete_published_hb_bookings,wp-hotel-booking,7000,WP Hotel Booking +delete_published_hb_rooms,wp-hotel-booking,7000,WP Hotel Booking +delete_published_hf_membership_plans,xa-woocommerce-memberships,400,Memberships for WooCommerce +delete_published_hf_user_memberships,xa-woocommerce-memberships,400,Memberships for WooCommerce +delete_published_hotel_locations,awebooking,6000,AweBooking – Hotel Booking System +delete_published_hotel_services,awebooking,6000,AweBooking – Hotel Booking System +delete_published_ib_edu_memberships,ibeducator,1000,Educator +delete_published_ib_educator_courses,ibeducator,1000,Educator +delete_published_ib_educator_lessons,ibeducator,1000,Educator +delete_published_ims_gallery,image-store,900,Image Store +delete_published_inbound-forms,cta,10000,WordPress Calls to Action +delete_published_inbound-forms,landing-pages,10000,WordPress Landing Pages +delete_published_inbound-forms,leads,7000,WordPress Leads +delete_published_insertcodes,insert-code,300,Insert Code +delete_published_invoices,essential-real-estate,3000,Essential Real Estate +delete_published_job_listings,wp-job-manager,100000,WP Job Manager +delete_published_jscp_matchs,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +delete_published_jscp_players,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +delete_published_jscp_teams,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +delete_published_klaviyo_shop_carts,klaviyo-for-woocommerce,1000,Klaviyo for WooCommerce +delete_published_landing_pages,landing-pages,10000,WordPress Landing Pages +delete_published_leads,cta,10000,WordPress Calls to Action +delete_published_leads,landing-pages,10000,WordPress Landing Pages +delete_published_leads,leads,7000,WordPress Leads +delete_published_legalpack_pages,legalpack,400,Legalpack +delete_published_lessons,lifterlms,8000,LifterLMS +delete_published_listings,auto-listings,400,Auto Listings +delete_published_listings,wp-real-estate,400,WP Real Estate +delete_published_listings,wpcasa,2000,WPCasa +delete_published_lp_courses,learnpress,50000,LearnPress – WordPress LMS Plugin +delete_published_lp_lessons,learnpress,50000,LearnPress – WordPress LMS Plugin +delete_published_lp_orders,learnpress,50000,LearnPress – WordPress LMS Plugin +delete_published_mbdb_book,mooberry-book-manager,1000,Mooberry Book Manager +delete_published_mbdb_book_grid,mooberry-book-manager,1000,Mooberry Book Manager +delete_published_mbdb_book_grids,mooberry-book-manager,1000,Mooberry Book Manager +delete_published_mbdb_books,mooberry-book-manager,1000,Mooberry Book Manager +delete_published_meals,restaurant-manager,800,Restaurant Manager +delete_published_memberships,lifterlms,8000,LifterLMS +delete_published_mp_menu_items,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +delete_published_mprm_orders,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +delete_published_nc_references,nelio-content,7000,Nelio Content – Social Media Marketing Automation +delete_published_nemus-sliders,nemus-slider,2000,Nemus Slider +delete_published_news,news-manager,3000,News Manager +delete_published_opalestate_agentss,opal-estate,1000,Opal Estate +delete_published_opalestate_propertiess,opal-estate,1000,Opal Estate +delete_published_packages,essential-real-estate,3000,Essential Real Estate +delete_published_payments,pronamic-ideal,6000,Pronamic Pay +delete_published_players,team-rosters,800,Team Rosters +delete_published_playlists,radio-station,1000,Radio Station +delete_published_plugin_filters,plugin-organizer,10000,Plugin Organizer +delete_published_plugin_groups,plugin-organizer,10000,Plugin Organizer +delete_published_portfolio_projects,custom-content-portfolio,1000,Custom Content Portfolio +delete_published_portfolios,flash-toolkit,30000,Flash Toolkit +delete_published_portfolios,suffice-toolkit,5000,Suffice Toolkit +delete_published_pricing_rates,awebooking,6000,AweBooking – Hotel Booking System +delete_published_product_sets,datafeedr-product-sets,1000,Datafeedr Product Sets +delete_published_products,design-approval-system,500,Design Approval System +delete_published_products,easy-digital-downloads,60000,Easy Digital Downloads +delete_published_products,ecommerce-product-catalog,10000,eCommerce Product Catalog Plugin for WordPress +delete_published_products,gnucommerce,1000,GNUCommerce +delete_published_products,jigoshop,4000,Jigoshop +delete_published_products,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_published_products,post-type-x,1000,Product Catalog X +delete_published_products,products,300,WP Products +delete_published_products,webmaster-user-role,8000,Webmaster User Role +delete_published_products,woocommerce,4000000,WooCommerce +delete_published_products,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +delete_published_products,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +delete_published_projects,upstream,1000,WordPress Project Management by UpStream +delete_published_propertys,essential-real-estate,3000,Essential Real Estate +delete_published_psp_projects,project-panorama-lite,1000,Project Panorama +delete_published_questions,lifterlms,8000,LifterLMS +delete_published_quizzes,lifterlms,8000,LifterLMS +delete_published_quotes,mg-quotes,300,mg Quotes +delete_published_redirects,wp-redirects,700,WP Redirects +delete_published_rem_properties,real-estate-manager,1000,Real Estate Manager – Property Listing and Agent Management +delete_published_reservations,restaurant-manager,800,Restaurant Manager +delete_published_resume_positions,wp-resume,700,WP Resume +delete_published_room_reservations,wp-hotelier,1000,Easy WP Hotelier +delete_published_room_types,awebooking,6000,AweBooking – Hotel Booking System +delete_published_rooms,wp-hotelier,1000,Easy WP Hotelier +delete_published_rsvpmakers,rsvpmaker,1000,RSVPMaker +delete_published_schedules,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_published_sgpb_popups,popup-builder,100000,Popup Builder – Responsive WordPress Pop up – Subscription & Newsletter +delete_published_shop_coupons,jigoshop,4000,Jigoshop +delete_published_shop_coupons,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_published_shop_coupons,webmaster-user-role,8000,Webmaster User Role +delete_published_shop_coupons,woocommerce,4000000,WooCommerce +delete_published_shop_coupons,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +delete_published_shop_discounts,easy-digital-downloads,60000,Easy Digital Downloads +delete_published_shop_emails,jigoshop,4000,Jigoshop +delete_published_shop_emails,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_published_shop_orders,jigoshop,4000,Jigoshop +delete_published_shop_orders,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_published_shop_orders,webmaster-user-role,8000,Webmaster User Role +delete_published_shop_orders,woocommerce,4000000,WooCommerce +delete_published_shop_payments,easy-digital-downloads,60000,Easy Digital Downloads +delete_published_shop_webhooks,woocommerce,4000000,WooCommerce +delete_published_shows,radio-station,1000,Radio Station +delete_published_sln_attendants,salon-booking-system,5000,Salon booking system +delete_published_sln_bookings,salon-booking-system,5000,Salon booking system +delete_published_sln_services,salon-booking-system,5000,Salon booking system +delete_published_snippets,wp-snippets,1000,WP Snippets +delete_published_snitchs,snitch,1000,Snitch +delete_published_sp_calendars,sportspress,20000,SportsPress – Sports Club & League Manager +delete_published_sp_configs,sportspress,20000,SportsPress – Sports Club & League Manager +delete_published_sp_events,sportspress,20000,SportsPress – Sports Club & League Manager +delete_published_sp_lists,sportspress,20000,SportsPress – Sports Club & League Manager +delete_published_sp_players,sportspress,20000,SportsPress – Sports Club & League Manager +delete_published_sp_staffs,sportspress,20000,SportsPress – Sports Club & League Manager +delete_published_sp_tables,sportspress,20000,SportsPress – Sports Club & League Manager +delete_published_sp_teams,sportspress,20000,SportsPress – Sports Club & League Manager +delete_published_sports,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_published_stm_lms_posts,masterstudy-lms-learning-management-system,400,MasterStudy LMS – Free Learning Management System WordPress Plugin for Online Courses +delete_published_store_orders,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +delete_published_stores,wp-store-locator,50000,WP Store Locator +delete_published_sunshine_galleries,sunshine-photo-cart,2000,Sunshine Photo Cart +delete_published_sunshine_orders,sunshine-photo-cart,2000,Sunshine Photo Cart +delete_published_sunshine_products,sunshine-photo-cart,2000,Sunshine Photo Cart +delete_published_support_tickets,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +delete_published_tc_events,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_published_tc_orders,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_published_tc_tickets,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_published_tc_tickets_instances,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_published_teams,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_published_tm-propertys,cherry-real-estate,600,Cherry Real Estate +delete_published_total_slider_slides,total-slider,500,Total Slider +delete_published_trans_logs,essential-real-estate,3000,Essential Real Estate +delete_published_translations,simple-punctual-translation,200,Simple Punctual Translation +delete_published_tribe_events,the-events-calendar,700000,The Events Calendar +delete_published_tribe_organizers,the-events-calendar,700000,The Events Calendar +delete_published_tribe_venues,the-events-calendar,700000,The Events Calendar +delete_published_user_packages,essential-real-estate,3000,Essential Real Estate +delete_published_user_registrations,user-registration,7000,"User Registration – Custom Registration Form, Login and User Profile for WordPress" +delete_published_vacancies,job-board,300,Job Board by BestWebSoft +delete_published_venues,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_published_wctrl_contents,widgets-control,1000,Widgets Control +delete_published_wordlift_entities,wordlift,400,WordLift – AI powered SEO +delete_published_wpautoterms_pages,auto-terms-of-service-and-privacy-policy,100000,Auto Terms of Service and Privacy Policy (WP AutoTerms) +delete_published_wpcm_clubs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_published_wpcm_matchs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_published_wpcm_players,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_published_wpcm_sponsors,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_published_wpcm_staffs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_published_wpdiscuz_forms,wpdiscuz,40000,Comments – wpDiscuz +delete_published_wpfc_sermons,sermon-manager-for-wordpress,9000,Sermon Manager +delete_published_wpi_discounts,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_published_wpi_invoices,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_published_wpi_items,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_published_wpi_quotes,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_published_wppizzas,wppizza,2000,WPPizza +delete_published_wprm_reservations,wp-restaurant-manager,700,WP Restaurant Manager +delete_published_wpsdealss,deals-engine,200,Social Deals Engine +delete_published_wpsdealssaless,deals-engine,200,Social Deals Engine +delete_published_ycd_countdowns,countdown-builder,1000,Countdown +delete_question,lifterlms,8000,LifterLMS +delete_questions,lifterlms,8000,LifterLMS +delete_quiz,lifterlms,8000,LifterLMS +delete_quizzes,lifterlms,8000,LifterLMS +delete_quote_authors,mg-quotes,300,mg Quotes +delete_quote_categories,mg-quotes,300,mg Quotes +delete_quotes,mg-quotes,300,mg Quotes +delete_raq_services,request-a-quote,1000,Request a Quote +delete_recurring_events,events-manager,100000,Events Manager +delete_redirects,wp-redirects,700,WP Redirects +delete_replies,bbpress,300000,bbPress +delete_reply,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +delete_reply,catchers-helpdesk,200,Catchers Helpdesk and Ticket system for Support +delete_reservations,restaurant-manager,800,Restaurant Manager +delete_resume_organizations,wp-resume,700,WP Resume +delete_resume_positions,wp-resume,700,WP Resume +delete_resume_sections,wp-resume,700,WP Resume +delete_roles,members,100000,Members +delete_roles,wpfront-user-role-editor,60000,WPFront User Role Editor +delete_room,wp-hotelier,1000,Easy WP Hotelier +delete_room_reservation,wp-hotelier,1000,Easy WP Hotelier +delete_room_reservation_terms,wp-hotelier,1000,Easy WP Hotelier +delete_room_reservations,wp-hotelier,1000,Easy WP Hotelier +delete_room_terms,wp-hotelier,1000,Easy WP Hotelier +delete_room_type,awebooking,6000,AweBooking – Hotel Booking System +delete_room_type_terms,awebooking,6000,AweBooking – Hotel Booking System +delete_room_types,awebooking,6000,AweBooking – Hotel Booking System +delete_rooms,wp-hotelier,1000,Easy WP Hotelier +delete_rsvpemail,rsvpmaker,1000,RSVPMaker +delete_rsvpmakers,rsvpmaker,1000,RSVPMaker +delete_schedule,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_schedules,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_sgpb_popups,popup-builder,100000,Popup Builder – Responsive WordPress Pop up – Subscription & Newsletter +delete_shift,employee-scheduler,400,Shiftee Basic – Employee and Staff Scheduling +delete_shifts,employee-scheduler,400,Shiftee Basic – Employee and Staff Scheduling +delete_shop_coupon,jigoshop,4000,Jigoshop +delete_shop_coupon,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_shop_coupon,webmaster-user-role,8000,Webmaster User Role +delete_shop_coupon,woocommerce,4000000,WooCommerce +delete_shop_coupon_terms,jigoshop,4000,Jigoshop +delete_shop_coupon_terms,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_shop_coupon_terms,webmaster-user-role,8000,Webmaster User Role +delete_shop_coupon_terms,woocommerce,4000000,WooCommerce +delete_shop_coupons,jigoshop,4000,Jigoshop +delete_shop_coupons,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_shop_coupons,webmaster-user-role,8000,Webmaster User Role +delete_shop_coupons,woocommerce,4000000,WooCommerce +delete_shop_coupons,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +delete_shop_discount,easy-digital-downloads,60000,Easy Digital Downloads +delete_shop_discount_terms,easy-digital-downloads,60000,Easy Digital Downloads +delete_shop_discounts,easy-digital-downloads,60000,Easy Digital Downloads +delete_shop_email,jigoshop,4000,Jigoshop +delete_shop_email,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_shop_email_terms,jigoshop,4000,Jigoshop +delete_shop_email_terms,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_shop_emails,jigoshop,4000,Jigoshop +delete_shop_emails,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_shop_order,jigoshop,4000,Jigoshop +delete_shop_order,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_shop_order,webmaster-user-role,8000,Webmaster User Role +delete_shop_order,woocommerce,4000000,WooCommerce +delete_shop_order_terms,jigoshop,4000,Jigoshop +delete_shop_order_terms,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_shop_order_terms,webmaster-user-role,8000,Webmaster User Role +delete_shop_order_terms,woocommerce,4000000,WooCommerce +delete_shop_orders,jigoshop,4000,Jigoshop +delete_shop_orders,jigoshop-ecommerce,400,Jigoshop eCommerce +delete_shop_orders,webmaster-user-role,8000,Webmaster User Role +delete_shop_orders,woocommerce,4000000,WooCommerce +delete_shop_payment,easy-digital-downloads,60000,Easy Digital Downloads +delete_shop_payment_terms,easy-digital-downloads,60000,Easy Digital Downloads +delete_shop_payments,easy-digital-downloads,60000,Easy Digital Downloads +delete_shop_webhook,woocommerce,4000000,WooCommerce +delete_shop_webhook_terms,woocommerce,4000000,WooCommerce +delete_shop_webhooks,woocommerce,4000000,WooCommerce +delete_shows,radio-station,1000,Radio Station +delete_sln_attendant,salon-booking-system,5000,Salon booking system +delete_sln_attendants,salon-booking-system,5000,Salon booking system +delete_sln_booking,salon-booking-system,5000,Salon booking system +delete_sln_bookings,salon-booking-system,5000,Salon booking system +delete_sln_service,salon-booking-system,5000,Salon booking system +delete_sln_services,salon-booking-system,5000,Salon booking system +delete_snippets,wp-snippets,1000,WP Snippets +delete_snitch,snitch,1000,Snitch +delete_snitchs,snitch,1000,Snitch +delete_sola_st_tickets,sola-support-tickets,400,Sola Support Tickets +delete_sp_calendar,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_calendar_terms,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_calendars,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_config,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_config_terms,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_configs,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_event,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_event_terms,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_events,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_list,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_list_terms,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_lists,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_player,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_player_terms,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_players,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_staff,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_staff_terms,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_staffs,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_table,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_table_terms,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_tables,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_team,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_team_terms,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sp_teams,sportspress,20000,SportsPress – Sports Club & League Manager +delete_sport,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_sports,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_sprout_invoices,sprout-invoices,2000,Client Invoicing by Sprout Invoices – Easy Estimates and Invoices for WordPress +delete_stm_lms_post,masterstudy-lms-learning-management-system,400,MasterStudy LMS – Free Learning Management System WordPress Plugin for Online Courses +delete_stm_lms_posts,masterstudy-lms-learning-management-system,400,MasterStudy LMS – Free Learning Management System WordPress Plugin for Online Courses +delete_store,wp-store-locator,50000,WP Store Locator +delete_store_order,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +delete_store_orders,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +delete_stores,wp-store-locator,50000,WP Store Locator +delete_sunshine_galleries,sunshine-photo-cart,2000,Sunshine Photo Cart +delete_sunshine_gallery,sunshine-photo-cart,2000,Sunshine Photo Cart +delete_sunshine_order,sunshine-photo-cart,2000,Sunshine Photo Cart +delete_sunshine_orders,sunshine-photo-cart,2000,Sunshine Photo Cart +delete_sunshine_product,sunshine-photo-cart,2000,Sunshine Photo Cart +delete_sunshine_products,sunshine-photo-cart,2000,Sunshine Photo Cart +delete_support_tickets,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +delete_tc_event,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_tc_events,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_tc_order,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_tc_orders,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_tc_ticket,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_tc_tickets,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_tc_tickets_instance,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_tc_tickets_instances,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +delete_team,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_teams,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_ticket,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +delete_ticket,catchers-helpdesk,200,Catchers Helpdesk and Ticket system for Support +delete_ticket_priority,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +delete_ticket_status,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +delete_ticket_topic,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +delete_tm-propertys,cherry-real-estate,600,Cherry Real Estate +delete_topic_tags,bbpress,300000,bbPress +delete_topics,bbpress,300000,bbPress +delete_total_slider_slides,total-slider,500,Total Slider +delete_trans_log,essential-real-estate,3000,Essential Real Estate +delete_trans_log_terms,essential-real-estate,3000,Essential Real Estate +delete_trans_logs,essential-real-estate,3000,Essential Real Estate +delete_translation,simple-punctual-translation,200,Simple Punctual Translation +delete_translations,simple-punctual-translation,200,Simple Punctual Translation +delete_tribe_events,the-events-calendar,700000,The Events Calendar +delete_tribe_organizers,the-events-calendar,700000,The Events Calendar +delete_tribe_venues,the-events-calendar,700000,The Events Calendar +delete_un_feedback,usernoise,7000,Usernoise modal feedback / contact form +delete_user_package,essential-real-estate,3000,Essential Real Estate +delete_user_package_terms,essential-real-estate,3000,Essential Real Estate +delete_user_packages,essential-real-estate,3000,Essential Real Estate +delete_user_registration,user-registration,7000,"User Registration – Custom Registration Form, Login and User Profile for WordPress" +delete_user_registration_terms,user-registration,7000,"User Registration – Custom Registration Form, Login and User Profile for WordPress" +delete_user_registrations,user-registration,7000,"User Registration – Custom Registration Form, Login and User Profile for WordPress" +delete_users_higher_level,wpfront-user-role-editor,60000,WPFront User Role Editor +delete_vacancies,job-board,300,Job Board by BestWebSoft +delete_vacancy,job-board,300,Job Board by BestWebSoft +delete_venue,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_venues,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +delete_wbcr-snippets,insert-php,100000,PHP code snippets (Insert PHP) +delete_wctrl_contents,widgets-control,1000,Widgets Control +delete_wd_ads_advert,ad-manager-wd,700,Ad Manager by WD – Advanced Ad Manager plugin +delete_wd_ads_adverts,ad-manager-wd,700,Ad Manager by WD – Advanced Ad Manager plugin +delete_wd_ads_groups,ad-manager-wd,700,Ad Manager by WD – Advanced Ad Manager plugin +delete_wd_ads_schedule,ad-manager-wd,700,Ad Manager by WD – Advanced Ad Manager plugin +delete_wd_ads_schedules,ad-manager-wd,700,Ad Manager by WD – Advanced Ad Manager plugin +delete_wiki_page,wordpress-wiki,400,WordPress Wiki +delete_wordlift_entities,wordlift,400,WordLift – AI powered SEO +delete_wordlift_entity,wordlift,400,WordLift – AI powered SEO +delete_wordpoints_extensions,wordpoints,700,WordPoints +delete_wordpoints_modules,wordpoints,700,WordPoints +delete_wpautoterms_pages,auto-terms-of-service-and-privacy-policy,100000,Auto Terms of Service and Privacy Policy (WP AutoTerms) +delete_wpcm_club,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_wpcm_club_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_wpcm_clubs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_wpcm_match,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_wpcm_match_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_wpcm_matchs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_wpcm_player,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_wpcm_player_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_wpcm_players,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_wpcm_sponsor,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_wpcm_sponsor_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_wpcm_sponsors,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_wpcm_staff,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_wpcm_staff_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_wpcm_staffs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +delete_wpdiscuz_form,wpdiscuz,40000,Comments – wpDiscuz +delete_wpdiscuz_forms,wpdiscuz,40000,Comments – wpDiscuz +delete_wpfc_sermon,sermon-manager-for-wordpress,9000,Sermon Manager +delete_wpfc_sermons,sermon-manager-for-wordpress,9000,Sermon Manager +delete_wpi_discount,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_wpi_discounts,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_wpi_invoice,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_wpi_invoices,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_wpi_item,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_wpi_items,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_wpi_quote,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_wpi_quotes,invoicing,2000,Invoicing – Invoice & Payments Plugin +delete_wplc_quick_response,wp-live-chat-support,60000,WP Live Chat Support +delete_wpp_properties,wp-property,5000,WP-Property – WordPress Powered Real Estate and Property Management +delete_wpp_property,wp-property,5000,WP-Property – WordPress Powered Real Estate and Property Management +delete_wppizza,wppizza,2000,WPPizza +delete_wppizzas,wppizza,2000,WPPizza +delete_wpsdeals,deals-engine,200,Social Deals Engine +delete_wpsdeals_terms,deals-engine,200,Social Deals Engine +delete_wpsdealss,deals-engine,200,Social Deals Engine +delete_wpsdealssales,deals-engine,200,Social Deals Engine +delete_wpsdealssales_terms,deals-engine,200,Social Deals Engine +delete_wpsdealssaless,deals-engine,200,Social Deals Engine +delete_wpse_profiles,wp-smart-editor,900,WP Smart Editor +delete_wswebinar,wp-webinarsystem,2000,WP WebinarSystem +delete_wswebinars,wp-webinarsystem,2000,WP WebinarSystem +delete_ycd_countdowns,countdown-builder,1000,Countdown +delete_yop_polls,yop-poll,20000,YOP Poll +delete_yop_polls_logs,yop-poll,20000,YOP Poll +delete_yop_polls_templates,yop-poll,20000,YOP Poll +design_wpas,wp-app-studio,300,Professional WordPress Plugin Development – WP App Studio +developer_updates,developer-mode,1000,Developer Mode +diagnosis_read,diagnosis,300,Diagnosis +disable_h5p_security,h5p,10000,Interactive Content – H5P +dlm_manage_logs,download-monitor,100000,Download Monitor +dlm_view_reports,download-monitor,100000,Download Monitor +do_not_allow,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +dsn_notes,admin-dashboard-site-notes,3000,Dashboard Site Notes +duplicate_everest_form,everest-forms,40000,Everest Forms – Easy Contact Form and Form Builder for WordPress +duplicate_masterslider,master-slider,100000,Master Slider – Responsive Touch Slider +dwqa_can_delete_answer,dw-question-answer,10000,DW Question & Answer +dwqa_can_delete_comment,dw-question-answer,10000,DW Question & Answer +dwqa_can_delete_question,dw-question-answer,10000,DW Question & Answer +dwqa_can_edit_answer,dw-question-answer,10000,DW Question & Answer +dwqa_can_edit_comment,dw-question-answer,10000,DW Question & Answer +dwqa_can_edit_question,dw-question-answer,10000,DW Question & Answer +dwqa_can_post_answer,dw-question-answer,10000,DW Question & Answer +dwqa_can_post_comment,dw-question-answer,10000,DW Question & Answer +dwqa_can_post_question,dw-question-answer,10000,DW Question & Answer +dwqa_can_read_answer,dw-question-answer,10000,DW Question & Answer +dwqa_can_read_comment,dw-question-answer,10000,DW Question & Answer +dwqa_can_read_question,dw-question-answer,10000,DW Question & Answer +easingslider_delete_sliders,easing-slider,60000,Easing Slider +easingslider_duplicate_sliders,easing-slider,60000,Easing Slider +easingslider_edit_sliders,easing-slider,60000,Easing Slider +easingslider_manage_addons,easing-slider,60000,Easing Slider +easingslider_manage_settings,easing-slider,60000,Easing Slider +easingslider_publish_sliders,easing-slider,60000,Easing Slider +edit_acadp_field,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +edit_acadp_fields,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +edit_acadp_listing,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +edit_acadp_listings,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +edit_acadp_payment,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +edit_acadp_payments,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +edit_achievement_events,achievements,300,Achievements for WordPress +edit_achievement_progresses,achievements,300,Achievements for WordPress +edit_achievements,achievements,300,Achievements for WordPress +edit_ad_terms,apply-online,5000,Apply Online +edit_admincolorschemes,easy-admin-color-schemes,1000,Easy Admin Color Schemes +edit_ads,apply-online,5000,Apply Online +edit_aec_event,another-events-calendar,800,Another Events Calendar +edit_aec_events,another-events-calendar,800,Another Events Calendar +edit_aec_organizer,another-events-calendar,800,Another Events Calendar +edit_aec_organizers,another-events-calendar,800,Another Events Calendar +edit_aec_venue,another-events-calendar,800,Another Events Calendar +edit_aec_venues,another-events-calendar,800,Another Events Calendar +edit_affiliate_keywords,affiliate,700,Affiliate +edit_agent,essential-real-estate,3000,Essential Real Estate +edit_agent_terms,essential-real-estate,3000,Essential Real Estate +edit_agents,essential-real-estate,3000,Essential Real Estate +edit_aggregator-records,the-events-calendar,700000,The Events Calendar +edit_ai1ec_event,all-in-one-event-calendar,100000,All-in-One Event Calendar +edit_ai1ec_events,all-in-one-event-calendar,100000,All-in-One Event Calendar +edit_aiovg_video,all-in-one-video-gallery,1000,All-in-One Video Gallery +edit_aiovg_videos,all-in-one-video-gallery,1000,All-in-One Video Gallery +edit_all_touchpoints,ukuupeople-the-simple-crm,1000,CRM: Contact Management Simplified – UkuuPeople +edit_all_ukuupeoples,ukuupeople-the-simple-crm,1000,CRM: Contact Management Simplified – UkuuPeople +edit_anb_animations,alert-notice-boxes,1000,Alert Notice Boxes +edit_anb_animations_out,alert-notice-boxes,1000,Alert Notice Boxes +edit_anb_designs,alert-notice-boxes,1000,Alert Notice Boxes +edit_anb_locations,alert-notice-boxes,1000,Alert Notice Boxes +edit_anbs,alert-notice-boxes,1000,Alert Notice Boxes +edit_application,apply-online,5000,Apply Online +edit_applications,apply-online,5000,Apply Online +edit_archiv,archive,700,Archive +edit_archive_structure,archive,700,Archive +edit_archivs,archive,700,Archive +edit_article,issuem,1000,IssueM +edit_articles,issuem,1000,IssueM +edit_at_biz_dir,directorist,500,Directorist – Business Directory Plugin +edit_at_biz_dirs,directorist,500,Directorist – Business Directory Plugin +edit_atbdp_order,directorist,500,Directorist – Business Directory Plugin +edit_atbdp_orders,directorist,500,Directorist – Business Directory Plugin +edit_attachments,wpfront-user-role-editor,60000,WPFront User Role Editor +edit_awebooking,awebooking,6000,AweBooking – Hotel Booking System +edit_awebooking_terms,awebooking,6000,AweBooking – Hotel Booking System +edit_awebookings,awebooking,6000,AweBooking – Hotel Booking System +edit_birs_appointment,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_birs_appointments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_birs_client,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_birs_clients,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_birs_location,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_birs_locations,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_birs_payment,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_birs_payments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_birs_service,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_birs_services,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_birs_staff,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_birs_staffs,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_blocks,gutenberg,500000,Gutenberg +edit_board_committees,nonprofit-board-management,400,Nonprofit Board Management +edit_board_content,nonprofit-board-management,400,Nonprofit Board Management +edit_board_events,nonprofit-board-management,400,Nonprofit Board Management +edit_book,novelist,800,Novelist +edit_book-reviews,book-review-library,700,Book Review Library +edit_book_terms,novelist,800,Novelist +edit_books,novelist,800,Novelist +edit_box,boxzilla,20000,Boxzilla +edit_boxes,boxzilla,20000,Boxzilla +edit_bps_forms,bp-profile-search,10000,BP Profile Search +edit_by_site_editor,site-editor,300,Site Editor – WordPress Site Builder – Theme Builder and Page Builder +edit_calp_event,calpress-event-calendar,5000,CalPress Calendar +edit_calp_events,calpress-event-calendar,5000,CalPress Calendar +edit_campaign,charitable,10000,Charitable – Donation Plugin +edit_campaign,leyka,1000,Leyka +edit_campaign,personal-fundraiser,200,Personal Fundraiser +edit_campaign_terms,charitable,10000,Charitable – Donation Plugin +edit_campaigns,charitable,10000,Charitable – Donation Plugin +edit_campaigns,leyka,1000,Leyka +edit_cannedresponse_category,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +edit_cannedresponse_tag,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +edit_car_listing,wp-car-manager,3000,WP Car Manager +edit_car_listing_terms,wp-car-manager,3000,WP Car Manager +edit_car_listings,wp-car-manager,3000,WP Car Manager +edit_categories,projectmanager,400,ProjectManager +edit_cbxaccounting,cbxwpsimpleaccounting,300,CBX Accounting +edit_cctor_coupon,coupon-creator,10000,Coupon Creator +edit_cctor_coupons,coupon-creator,10000,Coupon Creator +edit_chronosly,chronosly-events-calendar,4000,Chronosly Events Calendar +edit_chronoslys,chronosly-events-calendar,4000,Chronosly Events Calendar +edit_classified_listing,classifieds-wp,800,Classifieds WP +edit_classified_listing_terms,classifieds-wp,800,Classifieds WP +edit_classified_listings,classifieds-wp,800,Classifieds WP +edit_client,upstream,1000,WordPress Project Management by UpStream +edit_client_terms,upstream,1000,WordPress Project Management by UpStream +edit_clients,upstream,1000,WordPress Project Management by UpStream +edit_comment,wpsite-comment-moderator,200,Comment Moderator +edit_contact_country,wp-easy-contact,600,Best Contact Management Software for WordPress +edit_contact_state,wp-easy-contact,600,Best Contact Management Software for WordPress +edit_contact_tag,wp-easy-contact,600,Best Contact Management Software for WordPress +edit_contact_topic,wp-easy-contact,600,Best Contact Management Software for WordPress +edit_content_shortcodes,wpfront-user-role-editor,60000,WPFront User Role Editor +edit_cooked_recipes,cooked,4000,Cooked – Recipe Plugin +edit_cooked_settings,cooked,4000,Cooked – Recipe Plugin +edit_course,lifterlms,8000,LifterLMS +edit_course_cats,lifterlms,8000,LifterLMS +edit_course_difficulties,lifterlms,8000,LifterLMS +edit_course_tags,lifterlms,8000,LifterLMS +edit_course_tracks,lifterlms,8000,LifterLMS +edit_courses,lifterlms,8000,LifterLMS +edit_crossword,crosswordsearch,300,crosswordsearch +edit_cta,cta,10000,WordPress Calls to Action +edit_ctas,cta,10000,WordPress Calls to Action +edit_cupri_pay,pardakht-delkhah,1000,پلاگین پرداخت دلخواه +edit_cupri_pays,pardakht-delkhah,1000,پلاگین پرداخت دلخواه +edit_custom_css,custom-css-js,100000,Simple Custom CSS and JS +edit_custom_csss,custom-css-js,100000,Simple Custom CSS and JS +edit_customfields,catchers-helpdesk,200,Catchers Helpdesk and Ticket system for Support +edit_dataset_order,projectmanager,400,ProjectManager +edit_datasets,projectmanager,400,ProjectManager +edit_departments,employee-directory,400,Staff Directory – Employee Directory for WordPress +edit_ditty_news_ticker,ditty-news-ticker,40000,Ditty News Ticker +edit_ditty_news_tickers,ditty-news-ticker,40000,Ditty News Ticker +edit_documents,wp-document-revisions,4000,WP Document Revisions +edit_domain_check,domain-check,500,Domain Check +edit_donation,charitable,10000,Charitable – Donation Plugin +edit_donation,leyka,1000,Leyka +edit_donations,charitable,10000,Charitable – Donation Plugin +edit_donations,leyka,1000,Leyka +edit_dsn_note,admin-dashboard-site-notes,3000,Dashboard Site Notes +edit_dsn_notes,admin-dashboard-site-notes,3000,Dashboard Site Notes +edit_edr_course,educator,1000,Educator 2 +edit_edr_courses,educator,1000,Educator 2 +edit_edr_lesson,educator,1000,Educator 2 +edit_edr_lessons,educator,1000,Educator 2 +edit_edr_membership,educator,1000,Educator 2 +edit_edr_memberships,educator,1000,Educator 2 +edit_email_categories,mailer-dragon,300,Mailer Dragon – Email Marketing Plugin for WordPress +edit_email_template,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +edit_email_templates,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +edit_emails,mailer-dragon,300,Mailer Dragon – Email Marketing Plugin for WordPress +edit_emd_agents,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +edit_emd_canned_responses,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +edit_emd_contacts,wp-easy-contact,600,Best Contact Management Software for WordPress +edit_emd_employees,employee-directory,400,Staff Directory – Employee Directory for WordPress +edit_emd_employees,employee-spotlight,1000,Team Members Staff Showcase Plugin – Employee Spotlight +edit_emd_persons,campus-directory,200,Faculty Staff and Student Directory Plugin – Campus Directory +edit_emd_quotes,request-a-quote,1000,Request a Quote +edit_emd_tickets,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +edit_emd_videos,youtube-showcase,7000,YouTube Gallery – Best YouTube Video Gallery for WordPress +edit_employee,hrm,200,WP Human Resource Management +edit_employee_tags,employee-spotlight,1000,Team Members Staff Showcase Plugin – Employee Spotlight +edit_employment_type,employee-directory,400,Staff Directory – Employee Directory for WordPress +edit_epa_albums,easy-photo-album,5000,Easy Photo Album +edit_event,quick-event-manager,5000,Quick Event Manager +edit_event_categories,events-manager,100000,Events Manager +edit_event_listing,wp-event-manager,1000,WP Event Manager +edit_event_listing_terms,wp-event-manager,1000,WP Event Manager +edit_event_listings,wp-event-manager,1000,WP Event Manager +edit_event_magic,kikfyre-events-calendar-tickets,200,"Events, Calendars & Tickets – Event Kikfyre" +edit_event_magic_terms,kikfyre-events-calendar-tickets,200,"Events, Calendars & Tickets – Event Kikfyre" +edit_event_magics,kikfyre-events-calendar-tickets,200,"Events, Calendars & Tickets – Event Kikfyre" +edit_events,event-organiser,40000,Event Organiser +edit_events,events-maker,4000,Events Maker by dFactory +edit_events,events-manager,100000,Events Manager +edit_events,quick-event-manager,5000,Quick Event Manager +edit_everest_form,everest-forms,40000,Everest Forms – Easy Contact Form and Form Builder for WordPress +edit_everest_form_terms,everest-forms,40000,Everest Forms – Easy Contact Form and Form Builder for WordPress +edit_everest_forms,everest-forms,40000,Everest Forms – Easy Contact Form and Form Builder for WordPress +edit_fa_items,featured-articles-lite,3000,FA Lite – WP responsive slider plugin +edit_fa_terms,featured-articles-lite,3000,FA Lite – WP responsive slider plugin +edit_fancy_news,fancy-news,300,Fancy News +edit_fbtabs,facebook-tab-manager,1000,Facebook Tab Manager +edit_feed,wp-rss-aggregator,60000,WP RSS Aggregator +edit_feed_source,wp-rss-aggregator,60000,WP RSS Aggregator +edit_feed_source_terms,wp-rss-aggregator,60000,WP RSS Aggregator +edit_feed_sources,wp-rss-aggregator,60000,WP RSS Aggregator +edit_feed_terms,wp-rss-aggregator,60000,WP RSS Aggregator +edit_feeds,wp-rss-aggregator,60000,WP RSS Aggregator +edit_fep_announcements,front-end-pm,8000,Front End PM +edit_fep_messages,front-end-pm,8000,Front End PM +edit_filter_group,plugin-organizer,10000,Plugin Organizer +edit_flexible_invoice,flexible-invoices,1000,Flexible Invoices for WordPress +edit_flexible_invoices,flexible-invoices,1000,Flexible Invoices for WordPress +edit_food_group,restaurantpress,3000,RestaurantPress +edit_food_group_terms,restaurantpress,3000,RestaurantPress +edit_food_groups,restaurantpress,3000,RestaurantPress +edit_food_menu,restaurantpress,3000,RestaurantPress +edit_food_menu_terms,restaurantpress,3000,RestaurantPress +edit_food_menus,restaurantpress,3000,RestaurantPress +edit_footer_text,footer-text,10000,Footer Text +edit_form,formlift,800,FormLift for Infusionsoft Web Forms +edit_form,pronamic-ideal,6000,Pronamic Pay +edit_formfields,projectmanager,400,ProjectManager +edit_forms,formlift,800,FormLift for Infusionsoft Web Forms +edit_forms,html-forms,1000,HTML Forms +edit_forms,pronamic-ideal,6000,Pronamic Pay +edit_forums,bbpress,300000,bbPress +edit_galleries,gallery-box,2000,Gallery Box +edit_gallery,gallery-box,2000,Gallery Box +edit_game,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_games,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_gender,employee-directory,400,Staff Directory – Employee Directory for WordPress +edit_gigya,gigya-socialize-for-wordpress,200,Gigya – Social Infrastructure +edit_gigya_secret,gigya-socialize-for-wordpress,200,Gigya – Social Infrastructure +edit_give_form,give,50000,Give – Donation Plugin and Fundraising Platform +edit_give_form_terms,give,50000,Give – Donation Plugin and Fundraising Platform +edit_give_forms,give,50000,Give – Donation Plugin and Fundraising Platform +edit_give_payment,give,50000,Give – Donation Plugin and Fundraising Platform +edit_give_payment_terms,give,50000,Give – Donation Plugin and Fundraising Platform +edit_give_payments,give,50000,Give – Donation Plugin and Fundraising Platform +edit_glossaries,glossary-by-codeat,1000,Glossary +edit_glossary,glossary-by-codeat,1000,Glossary +edit_groups,employee-spotlight,1000,Team Members Staff Showcase Plugin – Employee Spotlight +edit_h5p_contents,h5p,10000,Interactive Content – H5P +edit_hanaboard-post,hana-board,1000,Hana-Board 하나보드 워드프레스 게시판 +edit_hb_bookings,wp-hotel-booking,7000,WP Hotel Booking +edit_hb_rooms,wp-hotel-booking,7000,WP Hotel Booking +edit_hf_membership_plan,xa-woocommerce-memberships,400,Memberships for WooCommerce +edit_hf_membership_plans,xa-woocommerce-memberships,400,Memberships for WooCommerce +edit_hf_user_membership,xa-woocommerce-memberships,400,Memberships for WooCommerce +edit_hf_user_memberships,xa-woocommerce-memberships,400,Memberships for WooCommerce +edit_hotel_location,awebooking,6000,AweBooking – Hotel Booking System +edit_hotel_location_terms,awebooking,6000,AweBooking – Hotel Booking System +edit_hotel_locations,awebooking,6000,AweBooking – Hotel Booking System +edit_hotel_service,awebooking,6000,AweBooking – Hotel Booking System +edit_hotel_service_terms,awebooking,6000,AweBooking – Hotel Booking System +edit_hotel_services,awebooking,6000,AweBooking – Hotel Booking System +edit_ib_edu_membership,ibeducator,1000,Educator +edit_ib_edu_memberships,ibeducator,1000,Educator +edit_ib_educator_course,ibeducator,1000,Educator +edit_ib_educator_courses,ibeducator,1000,Educator +edit_ib_educator_lesson,ibeducator,1000,Educator +edit_ib_educator_lessons,ibeducator,1000,Educator +edit_ims_gallery,image-store,900,Image Store +edit_ims_gallerys,image-store,900,Image Store +edit_in_section,bu-section-editing,300,BU Section Editing +edit_inbound-form,cta,10000,WordPress Calls to Action +edit_inbound-form,landing-pages,10000,WordPress Landing Pages +edit_inbound-form,leads,7000,WordPress Leads +edit_inbound-forms,cta,10000,WordPress Calls to Action +edit_inbound-forms,landing-pages,10000,WordPress Landing Pages +edit_inbound-forms,leads,7000,WordPress Leads +edit_insertcode,insert-code,300,Insert Code +edit_insertcodes,insert-code,300,Insert Code +edit_invoice,essential-real-estate,3000,Essential Real Estate +edit_invoice_terms,essential-real-estate,3000,Essential Real Estate +edit_invoices,essential-real-estate,3000,Essential Real Estate +edit_issues,issuem,1000,IssueM +edit_item,gamipress,2000,GamiPress +edit_items,gamipress,2000,GamiPress +edit_jbbrd_businesses_tags,job-board,300,Job Board by BestWebSoft +edit_jbbrd_employment_tags,job-board,300,Job Board by BestWebSoft +edit_job_listing,wp-job-manager,100000,WP Job Manager +edit_job_listing_terms,wp-job-manager,100000,WP Job Manager +edit_job_listings,wp-job-manager,100000,WP Job Manager +edit_jobtitles,employee-directory,400,Staff Directory – Employee Directory for WordPress +edit_jscp_match,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +edit_jscp_matchs,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +edit_jscp_player,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +edit_jscp_players,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +edit_jscp_team,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +edit_jscp_teams,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +edit_klaviyo_shop_cart,klaviyo-for-woocommerce,1000,Klaviyo for WooCommerce +edit_klaviyo_shop_cart_terms,klaviyo-for-woocommerce,1000,Klaviyo for WooCommerce +edit_klaviyo_shop_carts,klaviyo-for-woocommerce,1000,Klaviyo for WooCommerce +edit_landing_page,landing-pages,10000,WordPress Landing Pages +edit_landing_pages,landing-pages,10000,WordPress Landing Pages +edit_language,sublanguage,1000,Sublanguage +edit_languages,sublanguage,1000,Sublanguage +edit_lazyest_fields,lazyest-gallery,1000,Lazyest Gallery +edit_lead,cta,10000,WordPress Calls to Action +edit_lead,landing-pages,10000,WordPress Landing Pages +edit_lead,leads,7000,WordPress Leads +edit_leads,cta,10000,WordPress Calls to Action +edit_leads,landing-pages,10000,WordPress Landing Pages +edit_leads,leads,7000,WordPress Leads +edit_league_settings,leaguemanager,2000,LeagueManager +edit_leagues,leaguemanager,2000,LeagueManager +edit_legalpack_pages,legalpack,400,Legalpack +edit_lesson,lifterlms,8000,LifterLMS +edit_lessons,lifterlms,8000,LifterLMS +edit_listing,auto-listings,400,Auto Listings +edit_listing,wp-real-estate,400,WP Real Estate +edit_listing,wpcasa,2000,WPCasa +edit_listing_id,wpcasa,2000,WPCasa +edit_listing_terms,wpcasa,2000,WPCasa +edit_listings,auto-listings,400,Auto Listings +edit_listings,wp-real-estate,400,WP Real Estate +edit_listings,wpcasa,2000,WPCasa +edit_locations,events-manager,100000,Events Manager +edit_login_redirects,wpfront-user-role-editor,60000,WPFront User Role Editor +edit_lp_courses,learnpress,50000,LearnPress – WordPress LMS Plugin +edit_lp_lessons,learnpress,50000,LearnPress – WordPress LMS Plugin +edit_lp_orders,learnpress,50000,LearnPress – WordPress LMS Plugin +edit_marital_status,employee-directory,400,Staff Directory – Employee Directory for WordPress +edit_matches,leaguemanager,2000,LeagueManager +edit_mbdb_book,mooberry-book-manager,1000,Mooberry Book Manager +edit_mbdb_book_grid,mooberry-book-manager,1000,Mooberry Book Manager +edit_mbdb_book_grids,mooberry-book-manager,1000,Mooberry Book Manager +edit_mbdb_books,mooberry-book-manager,1000,Mooberry Book Manager +edit_meals,restaurant-manager,800,Restaurant Manager +edit_membership,lifterlms,8000,LifterLMS +edit_membership_cats,lifterlms,8000,LifterLMS +edit_membership_tags,lifterlms,8000,LifterLMS +edit_memberships,lifterlms,8000,LifterLMS +edit_mp_menu_item,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +edit_mp_menu_item_terms,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +edit_mp_menu_items,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +edit_mprm_order,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +edit_mprm_order_terms,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +edit_mprm_orders,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +edit_mstw_tr_settings,team-rosters,800,Team Rosters +edit_nav_menu_permissions,wpfront-user-role-editor,60000,WPFront User Role Editor +edit_nc_reference,nelio-content,7000,Nelio Content – Social Media Marketing Automation +edit_nc_references,nelio-content,7000,Nelio Content – Social Media Marketing Automation +edit_nemus-sliders,nemus-slider,2000,Nemus Slider +edit_news,news-manager,3000,News Manager +edit_newsletters,alo-easymail,10000,ALO EasyMail Newsletter +edit_niso_others_carousels_slider,niso-carousel,200,Niso Carousel +edit_niso_others_carousels_slider,niso-carousel-slider,400,Niso Carousel Slider +edit_niso_slider_carousel,niso-carousel,200,Niso Carousel +edit_niso_slider_carousel,niso-carousel-slider,400,Niso Carousel Slider +edit_niso_slider_carousels,niso-carousel,200,Niso Carousel +edit_niso_slider_carousels,niso-carousel-slider,400,Niso Carousel Slider +edit_office_locations,employee-spotlight,1000,Team Members Staff Showcase Plugin – Employee Spotlight +edit_opalestate_agents,opal-estate,1000,Opal Estate +edit_opalestate_agents_terms,opal-estate,1000,Opal Estate +edit_opalestate_agentss,opal-estate,1000,Opal Estate +edit_opalestate_properties,opal-estate,1000,Opal Estate +edit_opalestate_properties_terms,opal-estate,1000,Opal Estate +edit_opalestate_propertiess,opal-estate,1000,Opal Estate +edit_opanda-item,opt-in-panda,3000,OnePress Opt-In Panda +edit_opanda-item,social-locker,10000,OnePress Social Locker +edit_opanda-items,opt-in-panda,3000,OnePress Opt-In Panda +edit_opanda-items,social-locker,10000,OnePress Social Locker +edit_orbis_companies,orbis,200,Orbis +edit_orbis_company,orbis,200,Orbis +edit_orbis_project,orbis,200,Orbis +edit_orbis_projects,orbis,200,Orbis +edit_other_boxes,boxzilla,20000,Boxzilla +edit_other_datasets,projectmanager,400,ProjectManager +edit_other_languages,sublanguage,1000,Sublanguage +edit_other_portfolios,visual-portfolio,7000,Visual Portfolio +edit_other_posts,baw-moderator-role,300,Moderator Role +edit_other_sola_st_tickets,sola-support-tickets,400,Sola Support Tickets +edit_other_ticket,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +edit_other_ticket,catchers-helpdesk,200,Catchers Helpdesk and Ticket system for Support +edit_other_wplc_quick_response,wp-live-chat-support,60000,WP Live Chat Support +edit_others_acadp_fields,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +edit_others_acadp_listings,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +edit_others_acadp_payments,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +edit_others_achievement_progresses,achievements,300,Achievements for WordPress +edit_others_achievements,achievements,300,Achievements for WordPress +edit_others_admincolorschemes,easy-admin-color-schemes,1000,Easy Admin Color Schemes +edit_others_ads,apply-online,5000,Apply Online +edit_others_aec_events,another-events-calendar,800,Another Events Calendar +edit_others_aec_organizers,another-events-calendar,800,Another Events Calendar +edit_others_aec_venues,another-events-calendar,800,Another Events Calendar +edit_others_affiliate_keywords,affiliate,700,Affiliate +edit_others_agents,essential-real-estate,3000,Essential Real Estate +edit_others_aggregator-records,the-events-calendar,700000,The Events Calendar +edit_others_ai1ec_events,all-in-one-event-calendar,100000,All-in-One Event Calendar +edit_others_aiovg_videos,all-in-one-video-gallery,1000,All-in-One Video Gallery +edit_others_anb_animations,alert-notice-boxes,1000,Alert Notice Boxes +edit_others_anb_animations_out,alert-notice-boxes,1000,Alert Notice Boxes +edit_others_anb_designs,alert-notice-boxes,1000,Alert Notice Boxes +edit_others_anb_locations,alert-notice-boxes,1000,Alert Notice Boxes +edit_others_anbs,alert-notice-boxes,1000,Alert Notice Boxes +edit_others_applications,apply-online,5000,Apply Online +edit_others_archivs,archive,700,Archive +edit_others_articles,issuem,1000,IssueM +edit_others_at_biz_dirs,directorist,500,Directorist – Business Directory Plugin +edit_others_atbdp_orders,directorist,500,Directorist – Business Directory Plugin +edit_others_attachments,wpfront-user-role-editor,60000,WPFront User Role Editor +edit_others_awebookings,awebooking,6000,AweBooking – Hotel Booking System +edit_others_birs_appointments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_others_birs_clients,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_others_birs_locations,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_others_birs_payments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_others_birs_services,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_others_birs_staffs,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_others_blocks,gutenberg,500000,Gutenberg +edit_others_board_committees,nonprofit-board-management,400,Nonprofit Board Management +edit_others_board_events,nonprofit-board-management,400,Nonprofit Board Management +edit_others_book-reviews,book-review-library,700,Book Review Library +edit_others_books,novelist,800,Novelist +edit_others_bps_forms,bp-profile-search,10000,BP Profile Search +edit_others_calp_events,calpress-event-calendar,5000,CalPress Calendar +edit_others_campaigns,charitable,10000,Charitable – Donation Plugin +edit_others_campaigns,leyka,1000,Leyka +edit_others_car_listings,wp-car-manager,3000,WP Car Manager +edit_others_cctor_coupons,coupon-creator,10000,Coupon Creator +edit_others_chronoslys,chronosly-events-calendar,4000,Chronosly Events Calendar +edit_others_classified_listings,classifieds-wp,800,Classifieds WP +edit_others_clients,upstream,1000,WordPress Project Management by UpStream +edit_others_courses,lifterlms,8000,LifterLMS +edit_others_ctas,cta,10000,WordPress Calls to Action +edit_others_cupri_pays,pardakht-delkhah,1000,پلاگین پرداخت دلخواه +edit_others_custom_csss,custom-css-js,100000,Simple Custom CSS and JS +edit_others_ditty_news_tickers,ditty-news-ticker,40000,Ditty News Ticker +edit_others_documents,wp-document-revisions,4000,WP Document Revisions +edit_others_donations,charitable,10000,Charitable – Donation Plugin +edit_others_donations,leyka,1000,Leyka +edit_others_dsn_notes,admin-dashboard-site-notes,3000,Dashboard Site Notes +edit_others_edr_courses,educator,1000,Educator 2 +edit_others_edr_lessons,educator,1000,Educator 2 +edit_others_edr_memberships,educator,1000,Educator 2 +edit_others_email_templates,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +edit_others_emails,mailer-dragon,300,Mailer Dragon – Email Marketing Plugin for WordPress +edit_others_emd_agents,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +edit_others_emd_canned_responses,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +edit_others_emd_contacts,wp-easy-contact,600,Best Contact Management Software for WordPress +edit_others_emd_employees,employee-directory,400,Staff Directory – Employee Directory for WordPress +edit_others_emd_quotes,request-a-quote,1000,Request a Quote +edit_others_emd_tickets,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +edit_others_epa_albums,easy-photo-album,5000,Easy Photo Album +edit_others_event_listings,wp-event-manager,1000,WP Event Manager +edit_others_event_magics,kikfyre-events-calendar-tickets,200,"Events, Calendars & Tickets – Event Kikfyre" +edit_others_events,event-organiser,40000,Event Organiser +edit_others_events,events-maker,4000,Events Maker by dFactory +edit_others_events,events-manager,100000,Events Manager +edit_others_events,quick-event-manager,5000,Quick Event Manager +edit_others_everest_forms,everest-forms,40000,Everest Forms – Easy Contact Form and Form Builder for WordPress +edit_others_fa_items,featured-articles-lite,3000,FA Lite – WP responsive slider plugin +edit_others_fbtabs,facebook-tab-manager,1000,Facebook Tab Manager +edit_others_feed_sources,wp-rss-aggregator,60000,WP RSS Aggregator +edit_others_feeds,wp-rss-aggregator,60000,WP RSS Aggregator +edit_others_fep_announcements,front-end-pm,8000,Front End PM +edit_others_fep_messages,front-end-pm,8000,Front End PM +edit_others_flexible_invoices,flexible-invoices,1000,Flexible Invoices for WordPress +edit_others_food_groups,restaurantpress,3000,RestaurantPress +edit_others_food_menus,restaurantpress,3000,RestaurantPress +edit_others_forms,pronamic-ideal,6000,Pronamic Pay +edit_others_forums,bbpress,300000,bbPress +edit_others_galleries,gallery-box,2000,Gallery Box +edit_others_games,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_others_give_forms,give,50000,Give – Donation Plugin and Fundraising Platform +edit_others_give_payments,give,50000,Give – Donation Plugin and Fundraising Platform +edit_others_glossaries,glossary-by-codeat,1000,Glossary +edit_others_h5p_contents,h5p,10000,Interactive Content – H5P +edit_others_hb_bookings,wp-hotel-booking,7000,WP Hotel Booking +edit_others_hb_rooms,wp-hotel-booking,7000,WP Hotel Booking +edit_others_hf_membership_plans,xa-woocommerce-memberships,400,Memberships for WooCommerce +edit_others_hf_user_memberships,xa-woocommerce-memberships,400,Memberships for WooCommerce +edit_others_hotel_locations,awebooking,6000,AweBooking – Hotel Booking System +edit_others_hotel_services,awebooking,6000,AweBooking – Hotel Booking System +edit_others_ib_edu_memberships,ibeducator,1000,Educator +edit_others_ib_educator_courses,ibeducator,1000,Educator +edit_others_ib_educator_lessons,ibeducator,1000,Educator +edit_others_ims_gallerys,image-store,900,Image Store +edit_others_inbound-forms,cta,10000,WordPress Calls to Action +edit_others_inbound-forms,landing-pages,10000,WordPress Landing Pages +edit_others_inbound-forms,leads,7000,WordPress Leads +edit_others_insertcodes,insert-code,300,Insert Code +edit_others_invoices,essential-real-estate,3000,Essential Real Estate +edit_others_issues,issuem,1000,IssueM +edit_others_items,gamipress,2000,GamiPress +edit_others_job_listings,wp-job-manager,100000,WP Job Manager +edit_others_jscp_match,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +edit_others_jscp_player,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +edit_others_jscp_team,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +edit_others_klaviyo_shop_carts,klaviyo-for-woocommerce,1000,Klaviyo for WooCommerce +edit_others_landing_pages,landing-pages,10000,WordPress Landing Pages +edit_others_leads,cta,10000,WordPress Calls to Action +edit_others_leads,landing-pages,10000,WordPress Landing Pages +edit_others_leads,leads,7000,WordPress Leads +edit_others_legalpack_pages,legalpack,400,Legalpack +edit_others_lessons,lifterlms,8000,LifterLMS +edit_others_listings,auto-listings,400,Auto Listings +edit_others_listings,wp-real-estate,400,WP Real Estate +edit_others_listings,wpcasa,2000,WPCasa +edit_others_locations,events-manager,100000,Events Manager +edit_others_lp_courses,learnpress,50000,LearnPress – WordPress LMS Plugin +edit_others_lp_lessons,learnpress,50000,LearnPress – WordPress LMS Plugin +edit_others_lp_orders,learnpress,50000,LearnPress – WordPress LMS Plugin +edit_others_mbdb_book,mooberry-book-manager,1000,Mooberry Book Manager +edit_others_mbdb_book_grid,mooberry-book-manager,1000,Mooberry Book Manager +edit_others_mbdb_book_grids,mooberry-book-manager,1000,Mooberry Book Manager +edit_others_mbdb_books,mooberry-book-manager,1000,Mooberry Book Manager +edit_others_meals,restaurant-manager,800,Restaurant Manager +edit_others_memberships,lifterlms,8000,LifterLMS +edit_others_mp_menu_items,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +edit_others_mprm_orders,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +edit_others_nc_reference,nelio-content,7000,Nelio Content – Social Media Marketing Automation +edit_others_nc_references,nelio-content,7000,Nelio Content – Social Media Marketing Automation +edit_others_nemus-sliders,nemus-slider,2000,Nemus Slider +edit_others_news,news-manager,3000,News Manager +edit_others_newsletters,alo-easymail,10000,ALO EasyMail Newsletter +edit_others_opalestate_agentss,opal-estate,1000,Opal Estate +edit_others_opalestate_propertiess,opal-estate,1000,Opal Estate +edit_others_opanda-items,opt-in-panda,3000,OnePress Opt-In Panda +edit_others_opanda-items,social-locker,10000,OnePress Social Locker +edit_others_orbis_companies,orbis,200,Orbis +edit_others_orbis_projects,orbis,200,Orbis +edit_others_packages,essential-real-estate,3000,Essential Real Estate +edit_others_payments,pronamic-ideal,6000,Pronamic Pay +edit_others_players,team-rosters,800,Team Rosters +edit_others_playlists,radio-station,1000,Radio Station +edit_others_plugin_filters,plugin-organizer,10000,Plugin Organizer +edit_others_plugin_groups,plugin-organizer,10000,Plugin Organizer +edit_others_portfolio_projects,custom-content-portfolio,1000,Custom Content Portfolio +edit_others_portfolios,flash-toolkit,30000,Flash Toolkit +edit_others_portfolios,suffice-toolkit,5000,Suffice Toolkit +edit_others_posts_tc_orders,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_others_posts_tc_tickets_instances,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_others_pricing_rates,awebooking,6000,AweBooking – Hotel Booking System +edit_others_product_sets,datafeedr-product-sets,1000,Datafeedr Product Sets +edit_others_products,design-approval-system,500,Design Approval System +edit_others_products,easy-digital-downloads,60000,Easy Digital Downloads +edit_others_products,ecommerce-product-catalog,10000,eCommerce Product Catalog Plugin for WordPress +edit_others_products,gnucommerce,1000,GNUCommerce +edit_others_products,jigoshop,4000,Jigoshop +edit_others_products,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_others_products,post-type-x,1000,Product Catalog X +edit_others_products,products,300,WP Products +edit_others_products,webmaster-user-role,8000,Webmaster User Role +edit_others_products,woocommerce,4000000,WooCommerce +edit_others_products,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +edit_others_profile_cct,profile-custom-content-type,200,Profile CCT +edit_others_projects,upstream,1000,WordPress Project Management by UpStream +edit_others_propertys,essential-real-estate,3000,Essential Real Estate +edit_others_psp_projects,project-panorama-lite,1000,Project Panorama +edit_others_questions,lifterlms,8000,LifterLMS +edit_others_quizzes,lifterlms,8000,LifterLMS +edit_others_quotes,mg-quotes,300,mg Quotes +edit_others_recurring_events,events-manager,100000,Events Manager +edit_others_redirects,wp-redirects,700,WP Redirects +edit_others_rem_properties,real-estate-manager,1000,Real Estate Manager – Property Listing and Agent Management +edit_others_replies,bbpress,300000,bbPress +edit_others_reservations,restaurant-manager,800,Restaurant Manager +edit_others_resume,wp-resume,700,WP Resume +edit_others_resume_positions,wp-resume,700,WP Resume +edit_others_room_reservations,wp-hotelier,1000,Easy WP Hotelier +edit_others_room_types,awebooking,6000,AweBooking – Hotel Booking System +edit_others_rooms,wp-hotelier,1000,Easy WP Hotelier +edit_others_rsvpemails,rsvpmaker,1000,RSVPMaker +edit_others_rsvpmakers,rsvpmaker,1000,RSVPMaker +edit_others_schedules,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_others_sgpb_popups,popup-builder,100000,Popup Builder – Responsive WordPress Pop up – Subscription & Newsletter +edit_others_shifts,employee-scheduler,400,Shiftee Basic – Employee and Staff Scheduling +edit_others_shop_coupons,jigoshop,4000,Jigoshop +edit_others_shop_coupons,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_others_shop_coupons,webmaster-user-role,8000,Webmaster User Role +edit_others_shop_coupons,woocommerce,4000000,WooCommerce +edit_others_shop_discounts,easy-digital-downloads,60000,Easy Digital Downloads +edit_others_shop_emails,jigoshop,4000,Jigoshop +edit_others_shop_emails,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_others_shop_orders,jigoshop,4000,Jigoshop +edit_others_shop_orders,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_others_shop_orders,webmaster-user-role,8000,Webmaster User Role +edit_others_shop_orders,woocommerce,4000000,WooCommerce +edit_others_shop_payments,easy-digital-downloads,60000,Easy Digital Downloads +edit_others_shop_webhooks,woocommerce,4000000,WooCommerce +edit_others_shows,radio-station,1000,Radio Station +edit_others_sln_attendants,salon-booking-system,5000,Salon booking system +edit_others_sln_bookings,salon-booking-system,5000,Salon booking system +edit_others_sln_services,salon-booking-system,5000,Salon booking system +edit_others_snippets,wp-snippets,1000,WP Snippets +edit_others_snitchs,snitch,1000,Snitch +edit_others_sp_calendars,sportspress,20000,SportsPress – Sports Club & League Manager +edit_others_sp_configs,sportspress,20000,SportsPress – Sports Club & League Manager +edit_others_sp_events,sportspress,20000,SportsPress – Sports Club & League Manager +edit_others_sp_lists,sportspress,20000,SportsPress – Sports Club & League Manager +edit_others_sp_players,sportspress,20000,SportsPress – Sports Club & League Manager +edit_others_sp_staffs,sportspress,20000,SportsPress – Sports Club & League Manager +edit_others_sp_tables,sportspress,20000,SportsPress – Sports Club & League Manager +edit_others_sp_teams,sportspress,20000,SportsPress – Sports Club & League Manager +edit_others_sports,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_others_stm_lms_posts,masterstudy-lms-learning-management-system,400,MasterStudy LMS – Free Learning Management System WordPress Plugin for Online Courses +edit_others_store_orders,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +edit_others_stores,wp-store-locator,50000,WP Store Locator +edit_others_sunshine_galleries,sunshine-photo-cart,2000,Sunshine Photo Cart +edit_others_sunshine_orders,sunshine-photo-cart,2000,Sunshine Photo Cart +edit_others_sunshine_products,sunshine-photo-cart,2000,Sunshine Photo Cart +edit_others_support_tickets,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +edit_others_tc_events,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_others_tc_tickets,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_others_teams,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_others_tm-propertys,cherry-real-estate,600,Cherry Real Estate +edit_others_topics,bbpress,300000,bbPress +edit_others_total_slider_slides,total-slider,500,Total Slider +edit_others_trans_logs,essential-real-estate,3000,Essential Real Estate +edit_others_translations,simple-punctual-translation,200,Simple Punctual Translation +edit_others_tribe_events,the-events-calendar,700000,The Events Calendar +edit_others_tribe_organizers,the-events-calendar,700000,The Events Calendar +edit_others_tribe_venues,the-events-calendar,700000,The Events Calendar +edit_others_un_feedback_items,usernoise,7000,Usernoise modal feedback / contact form +edit_others_user_packages,essential-real-estate,3000,Essential Real Estate +edit_others_user_registrations,user-registration,7000,"User Registration – Custom Registration Form, Login and User Profile for WordPress" +edit_others_vacancies,job-board,300,Job Board by BestWebSoft +edit_others_venues,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_others_video_encodes,video-embed-thumbnail-generator,30000,Video Embed & Thumbnail Generator +edit_others_wbcr-snippetss,insert-php,100000,PHP code snippets (Insert PHP) +edit_others_wctrl_contents,widgets-control,1000,Widgets Control +edit_others_wd_ads_adverts,ad-manager-wd,700,Ad Manager by WD – Advanced Ad Manager plugin +edit_others_wd_ads_schedules,ad-manager-wd,700,Ad Manager by WD – Advanced Ad Manager plugin +edit_others_wiki_pages,wordpress-wiki,400,WordPress Wiki +edit_others_wordlift_entities,wordlift,400,WordLift – AI powered SEO +edit_others_wpautoterms_pages,auto-terms-of-service-and-privacy-policy,100000,Auto Terms of Service and Privacy Policy (WP AutoTerms) +edit_others_wpcm_clubs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_others_wpcm_matchs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_others_wpcm_players,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_others_wpcm_sponsors,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_others_wpcm_staffs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_others_wpdiscuz_forms,wpdiscuz,40000,Comments – wpDiscuz +edit_others_wpfc_sermons,sermon-manager-for-wordpress,9000,Sermon Manager +edit_others_wpi_discounts,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_others_wpi_invoices,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_others_wpi_items,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_others_wpi_quotes,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_others_wpp_properties,wp-property,5000,WP-Property – WordPress Powered Real Estate and Property Management +edit_others_wppizzas,wppizza,2000,WPPizza +edit_others_wprm_reservations,wp-restaurant-manager,700,WP Restaurant Manager +edit_others_wpsdealss,deals-engine,200,Social Deals Engine +edit_others_wpsdealssaless,deals-engine,200,Social Deals Engine +edit_others_wpse_profiles,wp-smart-editor,900,WP Smart Editor +edit_others_wswebinars,wp-webinarsystem,2000,WP WebinarSystem +edit_others_ycd_countdowns,countdown-builder,1000,Countdown +edit_own_touchpoints,ukuupeople-the-simple-crm,1000,CRM: Contact Management Simplified – UkuuPeople +edit_own_ukuupeoples,ukuupeople-the-simple-crm,1000,CRM: Contact Management Simplified – UkuuPeople +edit_own_yop_polls,yop-poll,20000,YOP Poll +edit_own_yop_polls_templates,yop-poll,20000,YOP Poll +edit_package,essential-real-estate,3000,Essential Real Estate +edit_package_terms,essential-real-estate,3000,Essential Real Estate +edit_packages,essential-real-estate,3000,Essential Real Estate +edit_page_in_section,bu-section-editing,300,BU Section Editing +edit_pages_role_permissions,wpfront-user-role-editor,60000,WPFront User Role Editor +edit_payment,pronamic-ideal,6000,Pronamic Pay +edit_payments,pronamic-ideal,6000,Pronamic Pay +edit_person_area,campus-directory,200,Faculty Staff and Student Directory Plugin – Campus Directory +edit_person_location,campus-directory,200,Faculty Staff and Student Directory Plugin – Campus Directory +edit_person_rareas,campus-directory,200,Faculty Staff and Student Directory Plugin – Campus Directory +edit_person_title,campus-directory,200,Faculty Staff and Student Directory Plugin – Campus Directory +edit_player,team-rosters,800,Team Rosters +edit_players,team-rosters,800,Team Rosters +edit_playlists,radio-station,1000,Radio Station +edit_plugin_filter,plugin-organizer,10000,Plugin Organizer +edit_plugin_filters,plugin-organizer,10000,Plugin Organizer +edit_plugin_group,plugin-organizer,10000,Plugin Organizer +edit_plugin_groups,plugin-organizer,10000,Plugin Organizer +edit_portfolio,flash-toolkit,30000,Flash Toolkit +edit_portfolio,suffice-toolkit,5000,Suffice Toolkit +edit_portfolio,visual-portfolio,7000,Visual Portfolio +edit_portfolio_categories,custom-content-portfolio,1000,Custom Content Portfolio +edit_portfolio_projects,custom-content-portfolio,1000,Custom Content Portfolio +edit_portfolio_tags,custom-content-portfolio,1000,Custom Content Portfolio +edit_portfolio_terms,flash-toolkit,30000,Flash Toolkit +edit_portfolio_terms,suffice-toolkit,5000,Suffice Toolkit +edit_portfolios,flash-toolkit,30000,Flash Toolkit +edit_portfolios,suffice-toolkit,5000,Suffice Toolkit +edit_portfolios,visual-portfolio,7000,Visual Portfolio +edit_post,realia,4000,Realia +edit_post,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +edit_post_in_section,bu-section-editing,300,BU Section Editing +edit_post_subscriptions,edit-flow,10000,Edit Flow +edit_post_subscriptions,publishpress,1000,PublishPress – Professional publishing tools for WordPress +edit_posts_role_permissions,wpfront-user-role-editor,60000,WPFront User Role Editor +edit_pricing_rate,awebooking,6000,AweBooking – Hotel Booking System +edit_pricing_rate_terms,awebooking,6000,AweBooking – Hotel Booking System +edit_pricing_rates,awebooking,6000,AweBooking – Hotel Booking System +edit_private_acadp_fields,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +edit_private_acadp_listings,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +edit_private_acadp_payments,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +edit_private_ads,apply-online,5000,Apply Online +edit_private_aec_events,another-events-calendar,800,Another Events Calendar +edit_private_aec_organizers,another-events-calendar,800,Another Events Calendar +edit_private_aec_venues,another-events-calendar,800,Another Events Calendar +edit_private_affiliate_keywords,affiliate,700,Affiliate +edit_private_agents,essential-real-estate,3000,Essential Real Estate +edit_private_aggregator-records,the-events-calendar,700000,The Events Calendar +edit_private_ai1ec_events,all-in-one-event-calendar,100000,All-in-One Event Calendar +edit_private_aiovg_videos,all-in-one-video-gallery,1000,All-in-One Video Gallery +edit_private_anb_animations,alert-notice-boxes,1000,Alert Notice Boxes +edit_private_anb_animations_out,alert-notice-boxes,1000,Alert Notice Boxes +edit_private_anb_designs,alert-notice-boxes,1000,Alert Notice Boxes +edit_private_anb_locations,alert-notice-boxes,1000,Alert Notice Boxes +edit_private_anbs,alert-notice-boxes,1000,Alert Notice Boxes +edit_private_applications,apply-online,5000,Apply Online +edit_private_archivs,archive,700,Archive +edit_private_articles,issuem,1000,IssueM +edit_private_at_biz_dirs,directorist,500,Directorist – Business Directory Plugin +edit_private_atbdp_orders,directorist,500,Directorist – Business Directory Plugin +edit_private_awebookings,awebooking,6000,AweBooking – Hotel Booking System +edit_private_birs_appointments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_private_birs_clients,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_private_birs_locations,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_private_birs_payments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_private_birs_services,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_private_birs_staffs,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_private_blocks,gutenberg,500000,Gutenberg +edit_private_board_committees,nonprofit-board-management,400,Nonprofit Board Management +edit_private_board_events,nonprofit-board-management,400,Nonprofit Board Management +edit_private_books,novelist,800,Novelist +edit_private_calp_events,calpress-event-calendar,5000,CalPress Calendar +edit_private_campaigns,charitable,10000,Charitable – Donation Plugin +edit_private_campaigns,leyka,1000,Leyka +edit_private_car_listings,wp-car-manager,3000,WP Car Manager +edit_private_cctor_coupons,coupon-creator,10000,Coupon Creator +edit_private_chronoslys,chronosly-events-calendar,4000,Chronosly Events Calendar +edit_private_classified_listings,classifieds-wp,800,Classifieds WP +edit_private_clients,upstream,1000,WordPress Project Management by UpStream +edit_private_courses,lifterlms,8000,LifterLMS +edit_private_ditty_news_tickers,ditty-news-ticker,40000,Ditty News Ticker +edit_private_documents,wp-document-revisions,4000,WP Document Revisions +edit_private_donations,charitable,10000,Charitable – Donation Plugin +edit_private_donations,leyka,1000,Leyka +edit_private_edr_courses,educator,1000,Educator 2 +edit_private_edr_lessons,educator,1000,Educator 2 +edit_private_edr_memberships,educator,1000,Educator 2 +edit_private_email_templates,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +edit_private_emails,mailer-dragon,300,Mailer Dragon – Email Marketing Plugin for WordPress +edit_private_emd_agents,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +edit_private_emd_canned_responses,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +edit_private_emd_contacts,wp-easy-contact,600,Best Contact Management Software for WordPress +edit_private_emd_employees,employee-directory,400,Staff Directory – Employee Directory for WordPress +edit_private_emd_quotes,request-a-quote,1000,Request a Quote +edit_private_emd_tickets,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +edit_private_epa_albums,easy-photo-album,5000,Easy Photo Album +edit_private_event_listings,wp-event-manager,1000,WP Event Manager +edit_private_event_magics,kikfyre-events-calendar-tickets,200,"Events, Calendars & Tickets – Event Kikfyre" +edit_private_everest_forms,everest-forms,40000,Everest Forms – Easy Contact Form and Form Builder for WordPress +edit_private_fbtabs,facebook-tab-manager,1000,Facebook Tab Manager +edit_private_feed_sources,wp-rss-aggregator,60000,WP RSS Aggregator +edit_private_feeds,wp-rss-aggregator,60000,WP RSS Aggregator +edit_private_fep_announcements,front-end-pm,8000,Front End PM +edit_private_fep_messages,front-end-pm,8000,Front End PM +edit_private_flexible_invoices,flexible-invoices,1000,Flexible Invoices for WordPress +edit_private_food_groups,restaurantpress,3000,RestaurantPress +edit_private_food_menus,restaurantpress,3000,RestaurantPress +edit_private_forms,pronamic-ideal,6000,Pronamic Pay +edit_private_games,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_private_give_forms,give,50000,Give – Donation Plugin and Fundraising Platform +edit_private_give_payments,give,50000,Give – Donation Plugin and Fundraising Platform +edit_private_glossaries,glossary-by-codeat,1000,Glossary +edit_private_hb_bookings,wp-hotel-booking,7000,WP Hotel Booking +edit_private_hb_rooms,wp-hotel-booking,7000,WP Hotel Booking +edit_private_hf_membership_plans,xa-woocommerce-memberships,400,Memberships for WooCommerce +edit_private_hf_user_memberships,xa-woocommerce-memberships,400,Memberships for WooCommerce +edit_private_hotel_locations,awebooking,6000,AweBooking – Hotel Booking System +edit_private_hotel_services,awebooking,6000,AweBooking – Hotel Booking System +edit_private_ib_edu_memberships,ibeducator,1000,Educator +edit_private_ib_educator_courses,ibeducator,1000,Educator +edit_private_ib_educator_lessons,ibeducator,1000,Educator +edit_private_invoices,essential-real-estate,3000,Essential Real Estate +edit_private_job_listings,wp-job-manager,100000,WP Job Manager +edit_private_klaviyo_shop_carts,klaviyo-for-woocommerce,1000,Klaviyo for WooCommerce +edit_private_legalpack_pages,legalpack,400,Legalpack +edit_private_lessons,lifterlms,8000,LifterLMS +edit_private_listings,auto-listings,400,Auto Listings +edit_private_listings,wp-real-estate,400,WP Real Estate +edit_private_listings,wpcasa,2000,WPCasa +edit_private_lp_courses,learnpress,50000,LearnPress – WordPress LMS Plugin +edit_private_lp_lessons,learnpress,50000,LearnPress – WordPress LMS Plugin +edit_private_lp_orders,learnpress,50000,LearnPress – WordPress LMS Plugin +edit_private_meals,restaurant-manager,800,Restaurant Manager +edit_private_memberships,lifterlms,8000,LifterLMS +edit_private_mp_menu_items,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +edit_private_mprm_orders,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +edit_private_nc_references,nelio-content,7000,Nelio Content – Social Media Marketing Automation +edit_private_nemus-sliders,nemus-slider,2000,Nemus Slider +edit_private_opalestate_agentss,opal-estate,1000,Opal Estate +edit_private_opalestate_propertiess,opal-estate,1000,Opal Estate +edit_private_packages,essential-real-estate,3000,Essential Real Estate +edit_private_payments,pronamic-ideal,6000,Pronamic Pay +edit_private_players,team-rosters,800,Team Rosters +edit_private_playlists,radio-station,1000,Radio Station +edit_private_plugin_filters,plugin-organizer,10000,Plugin Organizer +edit_private_plugin_groups,plugin-organizer,10000,Plugin Organizer +edit_private_portfolio_projects,custom-content-portfolio,1000,Custom Content Portfolio +edit_private_portfolios,flash-toolkit,30000,Flash Toolkit +edit_private_portfolios,suffice-toolkit,5000,Suffice Toolkit +edit_private_pricing_rates,awebooking,6000,AweBooking – Hotel Booking System +edit_private_product_sets,datafeedr-product-sets,1000,Datafeedr Product Sets +edit_private_products,design-approval-system,500,Design Approval System +edit_private_products,easy-digital-downloads,60000,Easy Digital Downloads +edit_private_products,ecommerce-product-catalog,10000,eCommerce Product Catalog Plugin for WordPress +edit_private_products,gnucommerce,1000,GNUCommerce +edit_private_products,jigoshop,4000,Jigoshop +edit_private_products,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_private_products,post-type-x,1000,Product Catalog X +edit_private_products,products,300,WP Products +edit_private_products,webmaster-user-role,8000,Webmaster User Role +edit_private_products,woocommerce,4000000,WooCommerce +edit_private_products,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +edit_private_projects,upstream,1000,WordPress Project Management by UpStream +edit_private_propertys,essential-real-estate,3000,Essential Real Estate +edit_private_questions,lifterlms,8000,LifterLMS +edit_private_quizzes,lifterlms,8000,LifterLMS +edit_private_quotes,mg-quotes,300,mg Quotes +edit_private_redirects,wp-redirects,700,WP Redirects +edit_private_reservations,restaurant-manager,800,Restaurant Manager +edit_private_resume_positions,wp-resume,700,WP Resume +edit_private_room_reservations,wp-hotelier,1000,Easy WP Hotelier +edit_private_room_types,awebooking,6000,AweBooking – Hotel Booking System +edit_private_rooms,wp-hotelier,1000,Easy WP Hotelier +edit_private_rsvpmakers,rsvpmaker,1000,RSVPMaker +edit_private_schedules,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_private_shop_coupons,jigoshop,4000,Jigoshop +edit_private_shop_coupons,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_private_shop_coupons,webmaster-user-role,8000,Webmaster User Role +edit_private_shop_coupons,woocommerce,4000000,WooCommerce +edit_private_shop_discounts,easy-digital-downloads,60000,Easy Digital Downloads +edit_private_shop_emails,jigoshop,4000,Jigoshop +edit_private_shop_emails,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_private_shop_orders,jigoshop,4000,Jigoshop +edit_private_shop_orders,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_private_shop_orders,webmaster-user-role,8000,Webmaster User Role +edit_private_shop_orders,woocommerce,4000000,WooCommerce +edit_private_shop_payments,easy-digital-downloads,60000,Easy Digital Downloads +edit_private_shop_webhooks,woocommerce,4000000,WooCommerce +edit_private_shows,radio-station,1000,Radio Station +edit_private_sln_attendants,salon-booking-system,5000,Salon booking system +edit_private_sln_bookings,salon-booking-system,5000,Salon booking system +edit_private_sln_services,salon-booking-system,5000,Salon booking system +edit_private_snippets,wp-snippets,1000,WP Snippets +edit_private_snitchs,snitch,1000,Snitch +edit_private_sp_calendars,sportspress,20000,SportsPress – Sports Club & League Manager +edit_private_sp_configs,sportspress,20000,SportsPress – Sports Club & League Manager +edit_private_sp_events,sportspress,20000,SportsPress – Sports Club & League Manager +edit_private_sp_lists,sportspress,20000,SportsPress – Sports Club & League Manager +edit_private_sp_players,sportspress,20000,SportsPress – Sports Club & League Manager +edit_private_sp_staffs,sportspress,20000,SportsPress – Sports Club & League Manager +edit_private_sp_tables,sportspress,20000,SportsPress – Sports Club & League Manager +edit_private_sp_teams,sportspress,20000,SportsPress – Sports Club & League Manager +edit_private_sports,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_private_stm_lms_posts,masterstudy-lms-learning-management-system,400,MasterStudy LMS – Free Learning Management System WordPress Plugin for Online Courses +edit_private_store_orders,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +edit_private_stores,wp-store-locator,50000,WP Store Locator +edit_private_sunshine_galleries,sunshine-photo-cart,2000,Sunshine Photo Cart +edit_private_sunshine_orders,sunshine-photo-cart,2000,Sunshine Photo Cart +edit_private_sunshine_products,sunshine-photo-cart,2000,Sunshine Photo Cart +edit_private_support_tickets,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +edit_private_tc_events,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_private_tc_orders,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_private_tc_tickets,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_private_tc_tickets_instances,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_private_teams,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_private_ticket,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +edit_private_tm-propertys,cherry-real-estate,600,Cherry Real Estate +edit_private_total_slider_slides,total-slider,500,Total Slider +edit_private_trans_logs,essential-real-estate,3000,Essential Real Estate +edit_private_translations,simple-punctual-translation,200,Simple Punctual Translation +edit_private_tribe_events,the-events-calendar,700000,The Events Calendar +edit_private_tribe_organizers,the-events-calendar,700000,The Events Calendar +edit_private_tribe_venues,the-events-calendar,700000,The Events Calendar +edit_private_user_packages,essential-real-estate,3000,Essential Real Estate +edit_private_user_registrations,user-registration,7000,"User Registration – Custom Registration Form, Login and User Profile for WordPress" +edit_private_vacancies,job-board,300,Job Board by BestWebSoft +edit_private_venues,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_private_wctrl_contents,widgets-control,1000,Widgets Control +edit_private_wpautoterms_pages,auto-terms-of-service-and-privacy-policy,100000,Auto Terms of Service and Privacy Policy (WP AutoTerms) +edit_private_wpcm_clubs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_private_wpcm_matchs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_private_wpcm_players,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_private_wpcm_sponsors,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_private_wpcm_staffs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_private_wpfc_sermons,sermon-manager-for-wordpress,9000,Sermon Manager +edit_private_wpi_discounts,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_private_wpi_invoices,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_private_wpi_items,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_private_wpi_quotes,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_private_wpp_properties,wp-property,5000,WP-Property – WordPress Powered Real Estate and Property Management +edit_private_wpsdealss,deals-engine,200,Social Deals Engine +edit_private_wpsdealssaless,deals-engine,200,Social Deals Engine +edit_product,dc-woocommerce-multi-vendor,10000,WC Marketplace +edit_product,design-approval-system,500,Design Approval System +edit_product,easy-digital-downloads,60000,Easy Digital Downloads +edit_product,gnucommerce,1000,GNUCommerce +edit_product,jigoshop,4000,Jigoshop +edit_product,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_product,webmaster-user-role,8000,Webmaster User Role +edit_product,woocommerce,4000000,WooCommerce +edit_product,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +edit_product,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +edit_product_categories,ecommerce-product-catalog,10000,eCommerce Product Catalog Plugin for WordPress +edit_product_categories,post-type-x,1000,Product Catalog X +edit_product_set,datafeedr-product-sets,1000,Datafeedr Product Sets +edit_product_sets,datafeedr-product-sets,1000,Datafeedr Product Sets +edit_product_terms,easy-digital-downloads,60000,Easy Digital Downloads +edit_product_terms,jigoshop,4000,Jigoshop +edit_product_terms,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_product_terms,webmaster-user-role,8000,Webmaster User Role +edit_product_terms,woocommerce,4000000,WooCommerce +edit_products,dc-woocommerce-multi-vendor,10000,WC Marketplace +edit_products,design-approval-system,500,Design Approval System +edit_products,easy-digital-downloads,60000,Easy Digital Downloads +edit_products,ecommerce-product-catalog,10000,eCommerce Product Catalog Plugin for WordPress +edit_products,gnucommerce,1000,GNUCommerce +edit_products,jigoshop,4000,Jigoshop +edit_products,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_products,post-type-x,1000,Product Catalog X +edit_products,products,300,WP Products +edit_products,webmaster-user-role,8000,Webmaster User Role +edit_products,woocommerce,4000000,WooCommerce +edit_products,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +edit_products,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +edit_profile_cct,profile-custom-content-type,200,Profile CCT +edit_profiles_cct,profile-custom-content-type,200,Profile CCT +edit_project,upstream,1000,WordPress Project Management by UpStream +edit_project_author,upstream,1000,WordPress Project Management by UpStream +edit_project_terms,upstream,1000,WordPress Project Management by UpStream +edit_projects,projectmanager,400,ProjectManager +edit_projects,upstream,1000,WordPress Project Management by UpStream +edit_projects_settings,projectmanager,400,ProjectManager +edit_property,essential-real-estate,3000,Essential Real Estate +edit_property_terms,essential-real-estate,3000,Essential Real Estate +edit_propertys,essential-real-estate,3000,Essential Real Estate +edit_psp_project,project-panorama-lite,1000,Project Panorama +edit_psp_projects,project-panorama-lite,1000,Project Panorama +edit_published_acadp_fields,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +edit_published_acadp_listings,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +edit_published_acadp_payments,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +edit_published_ads,apply-online,5000,Apply Online +edit_published_aec_events,another-events-calendar,800,Another Events Calendar +edit_published_aec_organizers,another-events-calendar,800,Another Events Calendar +edit_published_aec_venues,another-events-calendar,800,Another Events Calendar +edit_published_affiliate_keywords,affiliate,700,Affiliate +edit_published_agents,essential-real-estate,3000,Essential Real Estate +edit_published_aggregator-records,the-events-calendar,700000,The Events Calendar +edit_published_ai1ec_events,all-in-one-event-calendar,100000,All-in-One Event Calendar +edit_published_aiovg_videos,all-in-one-video-gallery,1000,All-in-One Video Gallery +edit_published_anb_animations,alert-notice-boxes,1000,Alert Notice Boxes +edit_published_anb_animations_out,alert-notice-boxes,1000,Alert Notice Boxes +edit_published_anb_designs,alert-notice-boxes,1000,Alert Notice Boxes +edit_published_anb_locations,alert-notice-boxes,1000,Alert Notice Boxes +edit_published_anbs,alert-notice-boxes,1000,Alert Notice Boxes +edit_published_applications,apply-online,5000,Apply Online +edit_published_archivs,archive,700,Archive +edit_published_articles,issuem,1000,IssueM +edit_published_at_biz_dirs,directorist,500,Directorist – Business Directory Plugin +edit_published_atbdp_orders,directorist,500,Directorist – Business Directory Plugin +edit_published_awebookings,awebooking,6000,AweBooking – Hotel Booking System +edit_published_birs_appointments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_published_birs_clients,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_published_birs_locations,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_published_birs_payments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_published_birs_services,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_published_birs_staffs,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +edit_published_blocks,gutenberg,500000,Gutenberg +edit_published_board_committees,nonprofit-board-management,400,Nonprofit Board Management +edit_published_board_events,nonprofit-board-management,400,Nonprofit Board Management +edit_published_book-reviews,book-review-library,700,Book Review Library +edit_published_books,novelist,800,Novelist +edit_published_bps_forms,bp-profile-search,10000,BP Profile Search +edit_published_calp_events,calpress-event-calendar,5000,CalPress Calendar +edit_published_campaigns,charitable,10000,Charitable – Donation Plugin +edit_published_campaigns,leyka,1000,Leyka +edit_published_car_listings,wp-car-manager,3000,WP Car Manager +edit_published_cctor_coupons,coupon-creator,10000,Coupon Creator +edit_published_chronoslys,chronosly-events-calendar,4000,Chronosly Events Calendar +edit_published_classified_listings,classifieds-wp,800,Classifieds WP +edit_published_clients,upstream,1000,WordPress Project Management by UpStream +edit_published_courses,lifterlms,8000,LifterLMS +edit_published_ctas,cta,10000,WordPress Calls to Action +edit_published_cupri_pays,pardakht-delkhah,1000,پلاگین پرداخت دلخواه +edit_published_custom_csss,custom-css-js,100000,Simple Custom CSS and JS +edit_published_ditty_news_tickers,ditty-news-ticker,40000,Ditty News Ticker +edit_published_documents,wp-document-revisions,4000,WP Document Revisions +edit_published_donations,charitable,10000,Charitable – Donation Plugin +edit_published_donations,leyka,1000,Leyka +edit_published_edr_courses,educator,1000,Educator 2 +edit_published_edr_lessons,educator,1000,Educator 2 +edit_published_edr_memberships,educator,1000,Educator 2 +edit_published_email_templates,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +edit_published_emails,mailer-dragon,300,Mailer Dragon – Email Marketing Plugin for WordPress +edit_published_emd_agents,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +edit_published_emd_canned_responses,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +edit_published_emd_contacts,wp-easy-contact,600,Best Contact Management Software for WordPress +edit_published_emd_employees,employee-directory,400,Staff Directory – Employee Directory for WordPress +edit_published_emd_quotes,request-a-quote,1000,Request a Quote +edit_published_emd_tickets,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +edit_published_epa_albums,easy-photo-album,5000,Easy Photo Album +edit_published_event_listings,wp-event-manager,1000,WP Event Manager +edit_published_event_magics,kikfyre-events-calendar-tickets,200,"Events, Calendars & Tickets – Event Kikfyre" +edit_published_events,events-maker,4000,Events Maker by dFactory +edit_published_events,quick-event-manager,5000,Quick Event Manager +edit_published_everest_forms,everest-forms,40000,Everest Forms – Easy Contact Form and Form Builder for WordPress +edit_published_fbtabs,facebook-tab-manager,1000,Facebook Tab Manager +edit_published_feed_sources,wp-rss-aggregator,60000,WP RSS Aggregator +edit_published_feeds,wp-rss-aggregator,60000,WP RSS Aggregator +edit_published_fep_announcements,front-end-pm,8000,Front End PM +edit_published_fep_messages,front-end-pm,8000,Front End PM +edit_published_flexible_invoices,flexible-invoices,1000,Flexible Invoices for WordPress +edit_published_food_groups,restaurantpress,3000,RestaurantPress +edit_published_food_menus,restaurantpress,3000,RestaurantPress +edit_published_forms,pronamic-ideal,6000,Pronamic Pay +edit_published_games,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_published_give_forms,give,50000,Give – Donation Plugin and Fundraising Platform +edit_published_give_payments,give,50000,Give – Donation Plugin and Fundraising Platform +edit_published_glossaries,glossary-by-codeat,1000,Glossary +edit_published_hb_bookings,wp-hotel-booking,7000,WP Hotel Booking +edit_published_hb_rooms,wp-hotel-booking,7000,WP Hotel Booking +edit_published_hf_membership_plans,xa-woocommerce-memberships,400,Memberships for WooCommerce +edit_published_hf_user_memberships,xa-woocommerce-memberships,400,Memberships for WooCommerce +edit_published_hotel_locations,awebooking,6000,AweBooking – Hotel Booking System +edit_published_hotel_services,awebooking,6000,AweBooking – Hotel Booking System +edit_published_ib_edu_memberships,ibeducator,1000,Educator +edit_published_ib_educator_courses,ibeducator,1000,Educator +edit_published_ib_educator_lessons,ibeducator,1000,Educator +edit_published_ims_gallerys,image-store,900,Image Store +edit_published_inbound-forms,cta,10000,WordPress Calls to Action +edit_published_inbound-forms,landing-pages,10000,WordPress Landing Pages +edit_published_inbound-forms,leads,7000,WordPress Leads +edit_published_insertcodes,insert-code,300,Insert Code +edit_published_invoices,essential-real-estate,3000,Essential Real Estate +edit_published_issues,issuem,1000,IssueM +edit_published_job_listings,wp-job-manager,100000,WP Job Manager +edit_published_jscp_matchs,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +edit_published_jscp_players,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +edit_published_jscp_teams,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +edit_published_klaviyo_shop_carts,klaviyo-for-woocommerce,1000,Klaviyo for WooCommerce +edit_published_landing_pages,landing-pages,10000,WordPress Landing Pages +edit_published_leads,cta,10000,WordPress Calls to Action +edit_published_leads,landing-pages,10000,WordPress Landing Pages +edit_published_leads,leads,7000,WordPress Leads +edit_published_legalpack_pages,legalpack,400,Legalpack +edit_published_lessons,lifterlms,8000,LifterLMS +edit_published_listings,auto-listings,400,Auto Listings +edit_published_listings,wp-real-estate,400,WP Real Estate +edit_published_listings,wpcasa,2000,WPCasa +edit_published_lp_courses,learnpress,50000,LearnPress – WordPress LMS Plugin +edit_published_lp_lessons,learnpress,50000,LearnPress – WordPress LMS Plugin +edit_published_lp_orders,learnpress,50000,LearnPress – WordPress LMS Plugin +edit_published_mbdb_book,mooberry-book-manager,1000,Mooberry Book Manager +edit_published_mbdb_book_grid,mooberry-book-manager,1000,Mooberry Book Manager +edit_published_mbdb_book_grids,mooberry-book-manager,1000,Mooberry Book Manager +edit_published_mbdb_books,mooberry-book-manager,1000,Mooberry Book Manager +edit_published_meals,restaurant-manager,800,Restaurant Manager +edit_published_memberships,lifterlms,8000,LifterLMS +edit_published_mp_menu_items,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +edit_published_mprm_orders,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +edit_published_nc_references,nelio-content,7000,Nelio Content – Social Media Marketing Automation +edit_published_nemus-sliders,nemus-slider,2000,Nemus Slider +edit_published_news,news-manager,3000,News Manager +edit_published_opalestate_agentss,opal-estate,1000,Opal Estate +edit_published_opalestate_propertiess,opal-estate,1000,Opal Estate +edit_published_packages,essential-real-estate,3000,Essential Real Estate +edit_published_payments,pronamic-ideal,6000,Pronamic Pay +edit_published_players,team-rosters,800,Team Rosters +edit_published_playlists,radio-station,1000,Radio Station +edit_published_portfolio_projects,custom-content-portfolio,1000,Custom Content Portfolio +edit_published_portfolios,flash-toolkit,30000,Flash Toolkit +edit_published_portfolios,suffice-toolkit,5000,Suffice Toolkit +edit_published_pricing_rates,awebooking,6000,AweBooking – Hotel Booking System +edit_published_product_sets,datafeedr-product-sets,1000,Datafeedr Product Sets +edit_published_products,design-approval-system,500,Design Approval System +edit_published_products,easy-digital-downloads,60000,Easy Digital Downloads +edit_published_products,ecommerce-product-catalog,10000,eCommerce Product Catalog Plugin for WordPress +edit_published_products,gnucommerce,1000,GNUCommerce +edit_published_products,jigoshop,4000,Jigoshop +edit_published_products,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_published_products,post-type-x,1000,Product Catalog X +edit_published_products,products,300,WP Products +edit_published_products,webmaster-user-role,8000,Webmaster User Role +edit_published_products,woocommerce,4000000,WooCommerce +edit_published_products,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +edit_published_products,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +edit_published_projects,upstream,1000,WordPress Project Management by UpStream +edit_published_propertys,essential-real-estate,3000,Essential Real Estate +edit_published_psp_projects,project-panorama-lite,1000,Project Panorama +edit_published_questions,lifterlms,8000,LifterLMS +edit_published_quizzes,lifterlms,8000,LifterLMS +edit_published_quotes,mg-quotes,300,mg Quotes +edit_published_redirects,wp-redirects,700,WP Redirects +edit_published_rem_properties,real-estate-manager,1000,Real Estate Manager – Property Listing and Agent Management +edit_published_reservations,restaurant-manager,800,Restaurant Manager +edit_published_resume_positions,wp-resume,700,WP Resume +edit_published_room_reservations,wp-hotelier,1000,Easy WP Hotelier +edit_published_room_types,awebooking,6000,AweBooking – Hotel Booking System +edit_published_rooms,wp-hotelier,1000,Easy WP Hotelier +edit_published_rsvpmakers,rsvpmaker,1000,RSVPMaker +edit_published_schedules,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_published_sgpb_popups,popup-builder,100000,Popup Builder – Responsive WordPress Pop up – Subscription & Newsletter +edit_published_shop_coupons,jigoshop,4000,Jigoshop +edit_published_shop_coupons,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_published_shop_coupons,webmaster-user-role,8000,Webmaster User Role +edit_published_shop_coupons,woocommerce,4000000,WooCommerce +edit_published_shop_coupons,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +edit_published_shop_discounts,easy-digital-downloads,60000,Easy Digital Downloads +edit_published_shop_emails,jigoshop,4000,Jigoshop +edit_published_shop_emails,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_published_shop_orders,jigoshop,4000,Jigoshop +edit_published_shop_orders,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_published_shop_orders,webmaster-user-role,8000,Webmaster User Role +edit_published_shop_orders,woocommerce,4000000,WooCommerce +edit_published_shop_payments,easy-digital-downloads,60000,Easy Digital Downloads +edit_published_shop_webhooks,woocommerce,4000000,WooCommerce +edit_published_shows,radio-station,1000,Radio Station +edit_published_sln_attendants,salon-booking-system,5000,Salon booking system +edit_published_sln_bookings,salon-booking-system,5000,Salon booking system +edit_published_sln_services,salon-booking-system,5000,Salon booking system +edit_published_snippets,wp-snippets,1000,WP Snippets +edit_published_sp_calendars,sportspress,20000,SportsPress – Sports Club & League Manager +edit_published_sp_configs,sportspress,20000,SportsPress – Sports Club & League Manager +edit_published_sp_events,sportspress,20000,SportsPress – Sports Club & League Manager +edit_published_sp_lists,sportspress,20000,SportsPress – Sports Club & League Manager +edit_published_sp_players,sportspress,20000,SportsPress – Sports Club & League Manager +edit_published_sp_staffs,sportspress,20000,SportsPress – Sports Club & League Manager +edit_published_sp_tables,sportspress,20000,SportsPress – Sports Club & League Manager +edit_published_sp_teams,sportspress,20000,SportsPress – Sports Club & League Manager +edit_published_sports,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_published_stm_lms_posts,masterstudy-lms-learning-management-system,400,MasterStudy LMS – Free Learning Management System WordPress Plugin for Online Courses +edit_published_store_orders,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +edit_published_stores,wp-store-locator,50000,WP Store Locator +edit_published_sunshine_galleries,sunshine-photo-cart,2000,Sunshine Photo Cart +edit_published_sunshine_orders,sunshine-photo-cart,2000,Sunshine Photo Cart +edit_published_sunshine_products,sunshine-photo-cart,2000,Sunshine Photo Cart +edit_published_support_tickets,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +edit_published_tc_events,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_published_tc_orders,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_published_tc_tickets,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_published_tc_tickets_instances,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_published_teams,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_published_tm-propertys,cherry-real-estate,600,Cherry Real Estate +edit_published_total_slider_slides,total-slider,500,Total Slider +edit_published_trans_logs,essential-real-estate,3000,Essential Real Estate +edit_published_translations,simple-punctual-translation,200,Simple Punctual Translation +edit_published_tribe_events,the-events-calendar,700000,The Events Calendar +edit_published_tribe_organizers,the-events-calendar,700000,The Events Calendar +edit_published_tribe_venues,the-events-calendar,700000,The Events Calendar +edit_published_un_feedback,usernoise,7000,Usernoise modal feedback / contact form +edit_published_user_packages,essential-real-estate,3000,Essential Real Estate +edit_published_user_registrations,user-registration,7000,"User Registration – Custom Registration Form, Login and User Profile for WordPress" +edit_published_vacancies,job-board,300,Job Board by BestWebSoft +edit_published_venues,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_published_wctrl_contents,widgets-control,1000,Widgets Control +edit_published_wpautoterms_pages,auto-terms-of-service-and-privacy-policy,100000,Auto Terms of Service and Privacy Policy (WP AutoTerms) +edit_published_wpcm_clubs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_published_wpcm_matchs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_published_wpcm_players,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_published_wpcm_sponsors,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_published_wpcm_staffs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_published_wpdiscuz_forms,wpdiscuz,40000,Comments – wpDiscuz +edit_published_wpfc_sermons,sermon-manager-for-wordpress,9000,Sermon Manager +edit_published_wpi_discounts,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_published_wpi_invoices,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_published_wpi_items,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_published_wpi_quotes,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_published_wppizzas,wppizza,2000,WPPizza +edit_published_wprm_reservations,wp-restaurant-manager,700,WP Restaurant Manager +edit_published_wpsdealss,deals-engine,200,Social Deals Engine +edit_published_wpsdealssaless,deals-engine,200,Social Deals Engine +edit_published_ycd_countdowns,countdown-builder,1000,Countdown +edit_question,lifterlms,8000,LifterLMS +edit_questions,lifterlms,8000,LifterLMS +edit_quiz,lifterlms,8000,LifterLMS +edit_quizzes,lifterlms,8000,LifterLMS +edit_quote_authors,mg-quotes,300,mg Quotes +edit_quote_categories,mg-quotes,300,mg Quotes +edit_quotes,mg-quotes,300,mg Quotes +edit_raq_services,request-a-quote,1000,Request a Quote +edit_recurring_events,events-manager,100000,Events Manager +edit_redirects,wp-redirects,700,WP Redirects +edit_rem_properties,real-estate-manager,1000,Real Estate Manager – Property Listing and Agent Management +edit_rem_property,real-estate-manager,1000,Real Estate Manager – Property Listing and Agent Management +edit_replies,bbpress,300000,bbPress +edit_reservations,restaurant-manager,800,Restaurant Manager +edit_restaurant_items,restaurant,300,Restaurant +edit_resume,wp-resume,700,WP Resume +edit_resume_organizations,wp-resume,700,WP Resume +edit_resume_positions,wp-resume,700,WP Resume +edit_resume_sections,wp-resume,700,WP Resume +edit_role_menus,wpfront-user-role-editor,60000,WPFront User Role Editor +edit_roles,members,100000,Members +edit_roles,wpfront-user-role-editor,60000,WPFront User Role Editor +edit_room,wp-hotelier,1000,Easy WP Hotelier +edit_room_reservation,wp-hotelier,1000,Easy WP Hotelier +edit_room_reservation_terms,wp-hotelier,1000,Easy WP Hotelier +edit_room_reservations,wp-hotelier,1000,Easy WP Hotelier +edit_room_terms,wp-hotelier,1000,Easy WP Hotelier +edit_room_type,awebooking,6000,AweBooking – Hotel Booking System +edit_room_type_terms,awebooking,6000,AweBooking – Hotel Booking System +edit_room_types,awebooking,6000,AweBooking – Hotel Booking System +edit_rooms,wp-hotelier,1000,Easy WP Hotelier +edit_rsvpemail,rsvpmaker,1000,RSVPMaker +edit_rsvpemails,rsvpmaker,1000,RSVPMaker +edit_rsvpmakers,rsvpmaker,1000,RSVPMaker +edit_schedule,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_schedules,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_seasons,leaguemanager,2000,LeagueManager +edit_sgpb_popup,popup-builder,100000,Popup Builder – Responsive WordPress Pop up – Subscription & Newsletter +edit_sgpb_popups,popup-builder,100000,Popup Builder – Responsive WordPress Pop up – Subscription & Newsletter +edit_shift,employee-scheduler,400,Shiftee Basic – Employee and Staff Scheduling +edit_shifts,employee-scheduler,400,Shiftee Basic – Employee and Staff Scheduling +edit_shop_coupon,jigoshop,4000,Jigoshop +edit_shop_coupon,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_shop_coupon,webmaster-user-role,8000,Webmaster User Role +edit_shop_coupon,woocommerce,4000000,WooCommerce +edit_shop_coupon_terms,jigoshop,4000,Jigoshop +edit_shop_coupon_terms,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_shop_coupon_terms,webmaster-user-role,8000,Webmaster User Role +edit_shop_coupon_terms,woocommerce,4000000,WooCommerce +edit_shop_coupons,jigoshop,4000,Jigoshop +edit_shop_coupons,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_shop_coupons,webmaster-user-role,8000,Webmaster User Role +edit_shop_coupons,woocommerce,4000000,WooCommerce +edit_shop_coupons,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +edit_shop_discount,easy-digital-downloads,60000,Easy Digital Downloads +edit_shop_discount_terms,easy-digital-downloads,60000,Easy Digital Downloads +edit_shop_discounts,easy-digital-downloads,60000,Easy Digital Downloads +edit_shop_email,jigoshop,4000,Jigoshop +edit_shop_email,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_shop_email_terms,jigoshop,4000,Jigoshop +edit_shop_email_terms,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_shop_emails,jigoshop,4000,Jigoshop +edit_shop_emails,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_shop_order,jigoshop,4000,Jigoshop +edit_shop_order,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_shop_order,webmaster-user-role,8000,Webmaster User Role +edit_shop_order,woocommerce,4000000,WooCommerce +edit_shop_order_terms,jigoshop,4000,Jigoshop +edit_shop_order_terms,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_shop_order_terms,webmaster-user-role,8000,Webmaster User Role +edit_shop_order_terms,woocommerce,4000000,WooCommerce +edit_shop_orders,jigoshop,4000,Jigoshop +edit_shop_orders,jigoshop-ecommerce,400,Jigoshop eCommerce +edit_shop_orders,webmaster-user-role,8000,Webmaster User Role +edit_shop_orders,woocommerce,4000000,WooCommerce +edit_shop_payment,easy-digital-downloads,60000,Easy Digital Downloads +edit_shop_payment_terms,easy-digital-downloads,60000,Easy Digital Downloads +edit_shop_payments,easy-digital-downloads,60000,Easy Digital Downloads +edit_shop_webhook,woocommerce,4000000,WooCommerce +edit_shop_webhook_terms,woocommerce,4000000,WooCommerce +edit_shop_webhooks,woocommerce,4000000,WooCommerce +edit_shows,radio-station,1000,Radio Station +edit_sln_attendant,salon-booking-system,5000,Salon booking system +edit_sln_attendants,salon-booking-system,5000,Salon booking system +edit_sln_booking,salon-booking-system,5000,Salon booking system +edit_sln_bookings,salon-booking-system,5000,Salon booking system +edit_sln_service,salon-booking-system,5000,Salon booking system +edit_sln_services,salon-booking-system,5000,Salon booking system +edit_snippets,wp-snippets,1000,WP Snippets +edit_snitch,snitch,1000,Snitch +edit_snitchs,snitch,1000,Snitch +edit_sola_st_ticket,sola-support-tickets,400,Sola Support Tickets +edit_sola_st_tickets,sola-support-tickets,400,Sola Support Tickets +edit_sp_calendar,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_calendar_terms,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_calendars,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_config,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_config_terms,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_configs,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_event,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_event_terms,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_events,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_list,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_list_terms,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_lists,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_player,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_player_terms,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_players,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_staff,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_staff_terms,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_staffs,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_table,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_table_terms,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_tables,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_team,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_team_terms,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sp_teams,sportspress,20000,SportsPress – Sports Club & League Manager +edit_sport,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_sports,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_sprout_invoices,sprout-invoices,2000,Client Invoicing by Sprout Invoices – Easy Estimates and Invoices for WordPress +edit_stafflist,stafflist,200,StaffList +edit_stats_wd_ads_adverts,ad-manager-wd,700,Ad Manager by WD – Advanced Ad Manager plugin +edit_stm_lms_post,masterstudy-lms-learning-management-system,400,MasterStudy LMS – Free Learning Management System WordPress Plugin for Online Courses +edit_stm_lms_posts,masterstudy-lms-learning-management-system,400,MasterStudy LMS – Free Learning Management System WordPress Plugin for Online Courses +edit_store,wp-store-locator,50000,WP Store Locator +edit_store_order,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +edit_store_orders,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +edit_stores,wp-store-locator,50000,WP Store Locator +edit_sunshine_galleries,sunshine-photo-cart,2000,Sunshine Photo Cart +edit_sunshine_gallery,sunshine-photo-cart,2000,Sunshine Photo Cart +edit_sunshine_order,sunshine-photo-cart,2000,Sunshine Photo Cart +edit_sunshine_orders,sunshine-photo-cart,2000,Sunshine Photo Cart +edit_sunshine_product,sunshine-photo-cart,2000,Sunshine Photo Cart +edit_sunshine_products,sunshine-photo-cart,2000,Sunshine Photo Cart +edit_support_ticket,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +edit_support_ticket_comments,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +edit_support_tickets,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +edit_tc_event,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_tc_events,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_tc_order,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_tc_orders,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_tc_ticket,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_tc_tickets,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_tc_tickets_instance,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_tc_tickets_instances,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +edit_team,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_teams,leaguemanager,2000,LeagueManager +edit_teams,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_ticket,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +edit_ticket,catchers-helpdesk,200,Catchers Helpdesk and Ticket system for Support +edit_ticket_priority,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +edit_ticket_status,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +edit_ticket_topic,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +edit_tm-property_feature,cherry-real-estate,600,Cherry Real Estate +edit_tm-property_tag,cherry-real-estate,600,Cherry Real Estate +edit_tm-property_type,cherry-real-estate,600,Cherry Real Estate +edit_tm-propertys,cherry-real-estate,600,Cherry Real Estate +edit_topic_tags,bbpress,300000,bbPress +edit_topics,bbpress,300000,bbPress +edit_total_slider_slides,total-slider,500,Total Slider +edit_trans_log,essential-real-estate,3000,Essential Real Estate +edit_trans_log_terms,essential-real-estate,3000,Essential Real Estate +edit_trans_logs,essential-real-estate,3000,Essential Real Estate +edit_translation,simple-punctual-translation,200,Simple Punctual Translation +edit_translations,simple-punctual-translation,200,Simple Punctual Translation +edit_tribe_events,the-events-calendar,700000,The Events Calendar +edit_tribe_organizers,the-events-calendar,700000,The Events Calendar +edit_tribe_venues,the-events-calendar,700000,The Events Calendar +edit_un_feedback,usernoise,7000,Usernoise modal feedback / contact form +edit_un_feedback_items,usernoise,7000,Usernoise modal feedback / contact form +edit_user_package,essential-real-estate,3000,Essential Real Estate +edit_user_package_terms,essential-real-estate,3000,Essential Real Estate +edit_user_packages,essential-real-estate,3000,Essential Real Estate +edit_user_registration,user-registration,7000,"User Registration – Custom Registration Form, Login and User Profile for WordPress" +edit_user_registration_terms,user-registration,7000,"User Registration – Custom Registration Form, Login and User Profile for WordPress" +edit_user_registrations,user-registration,7000,"User Registration – Custom Registration Form, Login and User Profile for WordPress" +edit_usergroups,edit-flow,10000,Edit Flow +edit_usergroups,publishpress,1000,PublishPress – Professional publishing tools for WordPress +edit_users_higher_level,wpfront-user-role-editor,60000,WPFront User Role Editor +edit_vacancies,job-board,300,Job Board by BestWebSoft +edit_vacancy,job-board,300,Job Board by BestWebSoft +edit_venue,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_venues,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +edit_wbcr-snippets,insert-php,100000,PHP code snippets (Insert PHP) +edit_wbcr-snippetss,insert-php,100000,PHP code snippets (Insert PHP) +edit_wctrl_contents,widgets-control,1000,Widgets Control +edit_wd_ads_advert,ad-manager-wd,700,Ad Manager by WD – Advanced Ad Manager plugin +edit_wd_ads_adverts,ad-manager-wd,700,Ad Manager by WD – Advanced Ad Manager plugin +edit_wd_ads_groups,ad-manager-wd,700,Ad Manager by WD – Advanced Ad Manager plugin +edit_wd_ads_schedule,ad-manager-wd,700,Ad Manager by WD – Advanced Ad Manager plugin +edit_wd_ads_schedules,ad-manager-wd,700,Ad Manager by WD – Advanced Ad Manager plugin +edit_whistles,whistles,2000,Whistles +edit_widget_permissions,wpfront-user-role-editor,60000,WPFront User Role Editor +edit_wiki,wordpress-wiki,400,WordPress Wiki +edit_wiki_page,wordpress-wiki,400,WordPress Wiki +edit_wiki_pages,wordpress-wiki,400,WordPress Wiki +edit_wordlift_entities,wordlift,400,WordLift – AI powered SEO +edit_wordlift_entity,wordlift,400,WordLift – AI powered SEO +edit_wpautoterms_pages,auto-terms-of-service-and-privacy-policy,100000,Auto Terms of Service and Privacy Policy (WP AutoTerms) +edit_wpcm_club,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_wpcm_club_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_wpcm_clubs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_wpcm_match,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_wpcm_match_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_wpcm_matchs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_wpcm_player,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_wpcm_player_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_wpcm_players,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_wpcm_sponsor,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_wpcm_sponsor_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_wpcm_sponsors,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_wpcm_staff,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_wpcm_staff_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_wpcm_staffs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +edit_wpdiscuz_form,wpdiscuz,40000,Comments – wpDiscuz +edit_wpdiscuz_forms,wpdiscuz,40000,Comments – wpDiscuz +edit_wpfc_sermon,sermon-manager-for-wordpress,9000,Sermon Manager +edit_wpfc_sermons,sermon-manager-for-wordpress,9000,Sermon Manager +edit_wpi_discount,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_wpi_discounts,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_wpi_invoice,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_wpi_invoices,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_wpi_item,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_wpi_items,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_wpi_quote,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_wpi_quotes,invoicing,2000,Invoicing – Invoice & Payments Plugin +edit_wplc_quick_response,wp-live-chat-support,60000,WP Live Chat Support +edit_wpp_properties,wp-property,5000,WP-Property – WordPress Powered Real Estate and Property Management +edit_wpp_property,wp-property,5000,WP-Property – WordPress Powered Real Estate and Property Management +edit_wppizza,wppizza,2000,WPPizza +edit_wppizzas,wppizza,2000,WPPizza +edit_wprm_reservation,wp-restaurant-manager,700,WP Restaurant Manager +edit_wprm_reservations,wp-restaurant-manager,700,WP Restaurant Manager +edit_wpsdeals,deals-engine,200,Social Deals Engine +edit_wpsdeals_terms,deals-engine,200,Social Deals Engine +edit_wpsdealss,deals-engine,200,Social Deals Engine +edit_wpsdealssales,deals-engine,200,Social Deals Engine +edit_wpsdealssales_terms,deals-engine,200,Social Deals Engine +edit_wpsdealssaless,deals-engine,200,Social Deals Engine +edit_wpse_profiles,wp-smart-editor,900,WP Smart Editor +edit_wswebinar,wp-webinarsystem,2000,WP WebinarSystem +edit_wswebinars,wp-webinarsystem,2000,WP WebinarSystem +edit_ycd_countdown,countdown-builder,1000,Countdown +edit_ycd_countdowns,countdown-builder,1000,Countdown +edit_yop_polls,yop-poll,20000,YOP Poll +edit_yop_polls_templates,yop-poll,20000,YOP Poll +editor,webmaster-user-role,8000,Webmaster User Role +editore_atti_albo,albo-pretorio-on-line,2000,Albo Pretorio On line +edr_edit_quiz_grades_all,educator,1000,Educator 2 +edr_edit_quiz_grades_own,educator,1000,Educator 2 +educator_edit_entries,educator,1000,Educator 2 +educator_edit_entries,ibeducator,1000,Educator +ee_assign_event_category,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_assign_event_type,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_assign_venue_category,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_checkin,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_checkins,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_contact,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_contacts,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_default_price,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_default_price_type,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_default_price_types,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_default_prices,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_default_ticket,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_default_tickets,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_event,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_event_category,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_event_type,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_events,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_global_messages,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_message,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_messages,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_others_checkins,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_others_default_tickets,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_others_events,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_others_messages,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_others_venues,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_payment_method,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_payment_methods,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_payments,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_private_events,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_private_venues,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_published_events,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_published_venues,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_question,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_question_group,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_question_groups,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_questions,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_registration,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_registrations,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_venue,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_venue_category,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_delete_venues,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_checkin,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_checkins,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_contact,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_contacts,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_default_price,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_default_price_type,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_default_price_types,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_default_prices,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_default_ticket,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_default_tickets,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_event,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_event_category,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_event_type,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_events,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_global_messages,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_message,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_messages,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_others_checkins,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_others_default_tickets,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_others_events,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_others_messages,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_others_payment_methods,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_others_registrations,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_others_venues,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_payment_method,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_payment_methods,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_payments,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_private_events,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_private_venues,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_published_events,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_published_venues,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_question,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_question_group,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_question_groups,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_questions,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_registration,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_registrations,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_system_question_groups,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_system_questions,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_venue,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_venue_category,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_edit_venues,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_manage_event_categories,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_manage_event_types,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_manage_gateways,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_manage_venue_categories,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_payment_method_admin_only,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_payment_method_bank,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_payment_method_check,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_payment_method_invoice,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_payment_method_paypal_express,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_payment_method_paypal_standard,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_publish_events,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_publish_venues,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_checkin,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_checkins,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_contact,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_contacts,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_default_price_types,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_default_prices,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_default_ticket,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_default_tickets,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_ee,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_event,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_events,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_global_messages,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_message,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_messages,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_others_checkins,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_others_default_tickets,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_others_events,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_others_messages,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_others_payment_methods,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_others_registrations,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_others_venues,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_payment_method,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_payment_methods,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_private_events,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_private_venues,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_question_groups,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_questions,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_registration,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_registrations,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_transaction,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_transactions,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_venue,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_read_venues,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ee_send_message,event-espresso-decaf,3000,Event Espresso 4 Decaf – Event Registration Event Ticketing +ef_view_calendar,edit-flow,10000,Edit Flow +ef_view_story_budget,edit-flow,10000,Edit Flow +email_multiple_users,email-users,10000,Email Users +email_single_user,email-users,10000,Email Users +email_user_groups,email-users,10000,Email Users +email_users_notify,email-users,10000,Email Users +emu2_email_multiple_users,emu2-email-users-2,500,Emu2 – Email Users 2 +emu2_email_single_user,emu2-email-users-2,500,Emu2 – Email Users 2 +emu2_email_user_groups,emu2-email-users-2,500,Emu2 – Email Users 2 +emu2_email_users_notify,emu2-email-users-2,500,Emu2 – Email Users 2 +emu2_export_list,emu2-email-users-2,500,Emu2 – Email Users 2 +emu2_manage_options,emu2-email-users-2,500,Emu2 – Email Users 2 +enable_cometchat,cometchat,600,"Voice, Video & Text Chat by CometChat – Best WordPress Chat Plugin" +encode_videos,video-embed-thumbnail-generator,30000,Video Embed & Thumbnail Generator +enroll,lifterlms,8000,LifterLMS +erp_ac_create_account,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_create_bank_transfer,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_create_customer,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_create_expenses_credit,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_create_expenses_voucher,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_create_journal,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_create_sales_invoice,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_create_sales_payment,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_create_vendor,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_delete_account,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_delete_customer,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_delete_other_customers,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_delete_other_vendors,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_delete_vendor,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_edit_account,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_edit_customer,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_edit_other_customers,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_edit_other_vendors,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_edit_vendor,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_publish_expenses_credit,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_publish_expenses_voucher,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_publish_sales_invoice,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_publish_sales_payment,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_view_account_lists,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_view_bank_accounts,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_view_customer,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_view_dashboard,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_view_expense,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_view_expenses_summary,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_view_journal,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_view_other_customers,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_view_other_expenses,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_view_other_journals,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_view_other_sales,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_view_other_vendors,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_view_reports,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_view_sale,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_view_sales_summary,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_view_single_account,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_view_single_customer,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_view_single_vendor,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_ac_view_vendor,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_can_terminate,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_crate_announcement,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_create_attendance,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_create_dependent,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_create_document,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_create_education,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_create_employee,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_create_experience,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_create_review,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_crm_add_contact,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_crm_create_groups,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_crm_delete_contact,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_crm_delete_groups,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_crm_edit_contact,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_crm_edit_groups,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_crm_list_contact,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_crm_manage_activites,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_crm_manage_dashboard,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_crm_manage_groups,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_crm_manage_schedules,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_delete_attendance,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_delete_dependent,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_delete_document,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_delete_education,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_delete_employee,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_delete_experience,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_delete_review,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_edit_attendance,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_edit_dependent,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_edit_document,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_edit_education,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_edit_employee,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_edit_employees,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_edit_experience,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_leave_create_request,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_leave_manage,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_list_employee,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_manage_announcement,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_manage_department,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_manage_designation,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_manage_hr_settings,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_manage_jobinfo,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_manage_review,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_view_announcement,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_view_attendance,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_view_dependent,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_view_document,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_view_education,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_view_employee,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_view_experience,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_view_jobinfo,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +erp_view_list,erp,7000,"WP ERP – HRM, CRM & Accounting Solution For WordPress" +execute_php,php-execution-plugin,9000,PHP Execution +export_admincolorschemes,easy-admin-color-schemes,1000,Easy Admin Color Schemes +export_charitable_reports,charitable,10000,Charitable – Donation Plugin +export_give_reports,give,50000,Give – Donation Plugin and Fundraising Platform +export_leagues,leaguemanager,2000,LeagueManager +export_masterslider,master-slider,100000,Master Slider – Responsive Touch Slider +export_opalestate_reports,opal-estate,1000,Opal Estate +export_others_admincolorschemes,easy-admin-color-schemes,1000,Easy Admin Color Schemes +export_shop_reports,easy-digital-downloads,60000,Easy Digital Downloads +fbm_edit_background,fully-background-manager,10000,Full Background Manager +fluentform_dashboard_access,fluentform,400,WP Fluent Form – WordPress Contact Form Plugin with Advanced Form Builder Features +fluentform_entries_viewer,fluentform,400,WP Fluent Form – WordPress Contact Form Plugin with Advanced Form Builder Features +fluentform_forms_manager,fluentform,400,WP Fluent Form – WordPress Contact Form Plugin with Advanced Form Builder Features +fluentform_full_access,fluentform,400,WP Fluent Form – WordPress Contact Form Plugin with Advanced Form Builder Features +fluentform_settings_manager,fluentform,400,WP Fluent Form – WordPress Contact Form Plugin with Advanced Form Builder Features +flush_cache_all,nginx-champuru,2000,Nginx Cache Controller +flush_cache_single,nginx-champuru,2000,Nginx Cache Controller +frm_change_settings,formidable,200000,Formidable Forms – Form Builder for WordPress +frm_delete_entries,formidable,200000,Formidable Forms – Form Builder for WordPress +frm_delete_forms,formidable,200000,Formidable Forms – Form Builder for WordPress +frm_edit_forms,formidable,200000,Formidable Forms – Form Builder for WordPress +frm_view_entries,formidable,200000,Formidable Forms – Form Builder for WordPress +frm_view_entries,wp-essentials,200,WP Essentials +frm_view_forms,formidable,200000,Formidable Forms – Form Builder for WordPress +frm_view_forms,wp-essentials,200,WP Essentials +frm_view_reports,wp-essentials,200,WP Essentials +frontier_post_can_add,frontier-post,2000,Frontier Post +frontier_post_can_delete,frontier-post,2000,Frontier Post +frontier_post_can_draft,frontier-post,2000,Frontier Post +frontier_post_can_edit,frontier-post,2000,Frontier Post +frontier_post_can_media,frontier-post,2000,Frontier Post +frontier_post_can_page,frontier-post,2000,Frontier Post +frontier_post_can_pending,frontier-post,2000,Frontier Post +frontier_post_can_private,frontier-post,2000,Frontier Post +frontier_post_can_publish,frontier-post,2000,Frontier Post +frontier_post_exerpt_edit,frontier-post,2000,Frontier Post +frontier_post_redir_edit,frontier-post,2000,Frontier Post +frontier_post_show_admin_bar,frontier-post,2000,Frontier Post +frontier_post_tags_edit,frontier-post,2000,Frontier Post +gdcpttools_basic,gd-taxonomies-tools,3000,GD Custom Posts And Taxonomies Tools +gdoc_query_sql_databases,inline-google-spreadsheet-viewer,10000,Inline Google Spreadsheet Viewer +gdrts_standard,gd-rating-system,5000,GD Rating System +gest_atti_albo,albo-pretorio-on-line,2000,Albo Pretorio On line +gf_limit,gravity-forms-quantity-limits,700,Gravity Forms Quantity Limiter +gf_limit_uninstall,gravity-forms-quantity-limits,700,Gravity Forms Quantity Limiter +gmedia_album_manage,grand-media,20000,Gmedia Photo Gallery +gmedia_category_manage,grand-media,20000,Gmedia Photo Gallery +gmedia_delete_media,grand-media,20000,Gmedia Photo Gallery +gmedia_delete_others_media,grand-media,20000,Gmedia Photo Gallery +gmedia_edit_media,grand-media,20000,Gmedia Photo Gallery +gmedia_edit_others_media,grand-media,20000,Gmedia Photo Gallery +gmedia_gallery_manage,grand-media,20000,Gmedia Photo Gallery +gmedia_import,grand-media,20000,Gmedia Photo Gallery +gmedia_library,grand-media,20000,Gmedia Photo Gallery +gmedia_module_manage,grand-media,20000,Gmedia Photo Gallery +gmedia_settings,grand-media,20000,Gmedia Photo Gallery +gmedia_show_others_media,grand-media,20000,Gmedia Photo Gallery +gmedia_tag_manage,grand-media,20000,Gmedia Photo Gallery +gmedia_terms,grand-media,20000,Gmedia Photo Gallery +gmedia_terms_delete,grand-media,20000,Gmedia Photo Gallery +gmedia_upload,grand-media,20000,Gmedia Photo Gallery +gravityforms_directory,gravity-forms-addons,5000,Gravity Forms Directory +gravityforms_directory_uninstall,gravity-forms-addons,5000,Gravity Forms Directory +gravityforms_edit_forms,webmaster-user-role,8000,Webmaster User Role +gravityforms_exacttarget,gravity-forms-exacttarget,300,Gravity Forms ExactTarget Add-on +gravityforms_exacttarget_uninstall,gravity-forms-exacttarget,300,Gravity Forms ExactTarget Add-on +gravityforms_icontact,gravity-forms-icontact,600,iContact for Gravity Forms +gravityforms_icontact_uninstall,gravity-forms-icontact,600,iContact for Gravity Forms +gravityforms_infusionsoft,infusionsoft,6000,Infusionsoft Gravity Forms Add-on +gravityforms_infusionsoft_uninstall,infusionsoft,6000,Infusionsoft Gravity Forms Add-on +gravityforms_marketo,marketo,300,Marketo Gravity Forms Add-on +gravityforms_marketo_uninstall,marketo,300,Marketo Gravity Forms Add-on +gravityforms_remove,gravity-forms-remove-entries,900,Gravity Forms Remove Entries +gravityforms_remove_uninstall,gravity-forms-remove-entries,900,Gravity Forms Remove Entries +gravityforms_sendinblue,gravity-forms-sendinblue-add-on,800,Gravity Forms SendinBlue Add-On +gravityforms_sendinblue_uninstall,gravity-forms-sendinblue-add-on,800,Gravity Forms SendinBlue Add-On +gravityforms_shootq,gravity-forms-shootq-add-on,200,Gravity Forms ShootQ add-on +gravityforms_shootq_uninstall,gravity-forms-shootq-add-on,200,Gravity Forms ShootQ add-on +gravityforms_tave,gravity-forms-tave-add-on,200,Gravity Forms Táve add-on +gravityforms_tave_uninstall,gravity-forms-tave-add-on,200,Gravity Forms Táve add-on +gravityforms_view_entries,webmaster-user-role,8000,Webmaster User Role +groups_access,groups,20000,Groups +groups_admin_groups,groups,20000,Groups +groups_admin_options,groups,20000,Groups +groups_restrict_access,groups,20000,Groups +happyforms_manage_form,happyforms,4000,Contact Form to Manage and respond to conversations with customers — HappyForms +happyforms_manage_response,happyforms,4000,Contact Form to Manage and respond to conversations with customers — HappyForms +has_wallets,wallets,600,Bitcoin and Altcoin Wallets +haveown_snap_accss,social-networks-auto-poster-facebook-twitter-g,100000,NextScripts: Social Networks Auto-Poster +hclc_admin,locatoraid,1000,Locatoraid – Store Locator Plugin +help_yop_poll_page,yop-poll,20000,YOP Poll +hide_from_intercom,intercom-for-wordpress,400,Intercom for WordPress +hrm_employee,hrm,200,WP Human Resource Management +ihep_effects_settings,wp-overlays,2000,WP Overlays +ihep_how_overview,wp-overlays,2000,WP Overlays +ihep_manage_settings,wp-overlays,2000,WP Overlays +import_admincolorschemes,easy-admin-color-schemes,1000,Easy Admin Color Schemes +import_datasets,projectmanager,400,ProjectManager +import_give_forms,give,50000,Give – Donation Plugin and Fundraising Platform +import_give_payments,give,50000,Give – Donation Plugin and Fundraising Platform +import_leagues,leaguemanager,2000,LeagueManager +import_products,easy-digital-downloads,60000,Easy Digital Downloads +import_shop_discounts,easy-digital-downloads,60000,Easy Digital Downloads +import_shop_payments,easy-digital-downloads,60000,Easy Digital Downloads +import_wp_polls,yop-poll,20000,YOP Poll +ims_add_galleries,image-store,900,Image Store +ims_change_permissions,image-store,900,Image Store +ims_change_pricing,image-store,900,Image Store +ims_change_settings,image-store,900,Image Store +ims_manage_customers,image-store,900,Image Store +ims_manage_galleries,image-store,900,Image Store +ims_read_galleries,image-store,900,Image Store +ims_read_sales,image-store,900,Image Store +install_modules,site-editor,300,Site Editor – WordPress Site Builder – Theme Builder and Page Builder +install_recommended_h5p_libraries,h5p,10000,Interactive Content – H5P +install_wordpoints_extensions,wordpoints,700,WordPoints +install_wordpoints_modules,wordpoints,700,WordPoints +invoke_force_refresh,force-refresh,600,Force Refresh +join_board_committee,nonprofit-board-management,400,Nonprofit Board Management +jsjobs,js-jobs,1000,JS Job Manager +jsp_matchday_edit,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +jsp_matchday_manage,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +jsst_support_ticket,js-support-ticket,3000,JS Support Ticket +keep_gate,bbpress,300000,bbPress +launch_editor,digiwidgets-image-editor,1000,DigiWidgets Image Editor +lazyest_author,lazyest-gallery,1000,Lazyest Gallery +lazyest_editor,lazyest-gallery,1000,Lazyest Gallery +lazyest_manager,lazyest-gallery,1000,Lazyest Gallery +league_manager,leaguemanager,2000,LeagueManager +leaguemanager,leaguemanager,2000,LeagueManager +leaguemanager_settings,leaguemanager,2000,LeagueManager +leenkme_edit_user_settings,leenkme,600,leenk.me +leenkme_manage_all_settings,leenkme,600,leenk.me +lenslider_manage,len-slider,1000,Len Slider +leyka_manage_donations,leyka,1000,Leyka +leyka_manage_options,leyka,1000,Leyka +lfb_manager,lead-form-builder,10000,Contact Form & Lead Form Builder +lifterlms_instructor,lifterlms,8000,LifterLMS +limitby_author_backend_emd_tickets,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +limitby_author_frontend_emd_tickets,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +limitby_tickets_assigned_to,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +list_hanaboard-post,hana-board,1000,Hana-Board 하나보드 워드프레스 게시판 +list_roles,members,100000,Members +list_roles,wpfront-user-role-editor,60000,WPFront User Role Editor +list_wallet_transactions,wallets,600,Bitcoin and Altcoin Wallets +ljmm_control,lj-maintenance-mode,50000,Maintenance Mode +ljmm_view_site,lj-maintenance-mode,50000,Maintenance Mode +loco_admin,loco-translate,700000,Loco Translate +log_cbxaccounting,cbxwpsimpleaccounting,300,CBX Accounting +mailpoet_access_plugin_admin,mailpoet,70000,MailPoet – emails and newsletters in WordPress +mailpoet_manage_emails,mailpoet,70000,MailPoet – emails and newsletters in WordPress +mailpoet_manage_forms,mailpoet,70000,MailPoet – emails and newsletters in WordPress +mailpoet_manage_segments,mailpoet,70000,MailPoet – emails and newsletters in WordPress +mailpoet_manage_settings,mailpoet,70000,MailPoet – emails and newsletters in WordPress +mailpoet_manage_subscribers,mailpoet,70000,MailPoet – emails and newsletters in WordPress +make_video_thumbnails,video-embed-thumbnail-generator,30000,Video Embed & Thumbnail Generator +manage DB views,dbview,400,dbview +manage-post-highlights,post-highlights,200,post highlights +manage-wp-users-exporter,wp-users-exporter,2000,WP Users Exporter +manage_acadp_options,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +manage_achievement_events,achievements,300,Achievements for WordPress +manage_ad_terms,apply-online,5000,Apply Online +manage_admin_columns,codepress-admin-columns,100000,Admin Columns +manage_admincolorschemes_settings,easy-admin-color-schemes,1000,Easy Admin Color Schemes +manage_ads,pixel-caffeine,30000,Pixel Caffeine +manage_aec_options,another-events-calendar,800,Another Events Calendar +manage_agent_terms,essential-real-estate,3000,Essential Real Estate +manage_ai1ec_feeds,all-in-one-event-calendar,100000,All-in-One Event Calendar +manage_ai1ec_options,all-in-one-event-calendar,100000,All-in-One Event Calendar +manage_aiovg_options,all-in-one-video-gallery,1000,All-in-One Video Gallery +manage_amazon_listings,wp-lister-for-amazon,2000,WP-Lister Lite for Amazon +manage_amazon_options,wp-lister-for-amazon,2000,WP-Lister Lite for Amazon +manage_archive_structure,archive,700,Archive +manage_article_categories,issuem,1000,IssueM +manage_article_tags,issuem,1000,IssueM +manage_atbdp_options,directorist,500,Directorist – Business Directory Plugin +manage_attendance,hrm,200,WP Human Resource Management +manage_attendees_cap,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +manage_awebooking,awebooking,6000,AweBooking – Hotel Booking System +manage_awebooking_settings,awebooking,6000,AweBooking – Hotel Booking System +manage_awebooking_terms,awebooking,6000,AweBooking – Hotel Booking System +manage_birs_settings,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +manage_book_review_options,book-review-library,700,Book Review Library +manage_book_terms,novelist,800,Novelist +manage_bookings,events-manager,100000,Events Manager +manage_bookings,restaurant-reservations,20000,Restaurant Reservations +manage_campaign_terms,charitable,10000,Charitable – Donation Plugin +manage_cannedresponse_category,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +manage_cannedresponse_tag,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +manage_capabilities,capability-manager-enhanced,70000,Capability Manager Enhanced +manage_capabilities,capsman,10000,Capability Manager +manage_car_listing_terms,wp-car-manager,3000,WP Car Manager +manage_car_listings,wp-car-manager,3000,WP Car Manager +manage_cbxaccounting,cbxwpsimpleaccounting,300,CBX Accounting +manage_cforms,cforms2,10000,cformsII +manage_charitable_settings,charitable,10000,Charitable – Donation Plugin +manage_cimy_image_rotator,cimy-header-image-rotator,4000,Cimy Header Image Rotator +manage_circulation,weblibrarian,700,WebLibrarian +manage_classified_listing_terms,classifieds-wp,800,Classifieds WP +manage_classified_listings,classifieds-wp,800,Classifieds WP +manage_classifieds,another-wordpress-classifieds-plugin,10000,AWPCP – Classifieds Plugin +manage_classifieds_listings,another-wordpress-classifieds-plugin,10000,AWPCP – Classifieds Plugin +manage_client_terms,upstream,1000,WordPress Project Management by UpStream +manage_collection,weblibrarian,700,WebLibrarian +manage_contact_country,wp-easy-contact,600,Best Contact Management Software for WordPress +manage_contact_manager,contact-manager,500,Contact Manager +manage_contact_state,wp-easy-contact,600,Best Contact Management Software for WordPress +manage_contact_tag,wp-easy-contact,600,Best Contact Management Software for WordPress +manage_contact_topic,wp-easy-contact,600,Best Contact Management Software for WordPress +manage_content_types,cms-press,1000,CMS Press +manage_contests,and-the-winner-is,300,And The Winner Is… +manage_course_cats,lifterlms,8000,LifterLMS +manage_course_difficulties,lifterlms,8000,LifterLMS +manage_course_tags,lifterlms,8000,LifterLMS +manage_course_tracks,lifterlms,8000,LifterLMS +manage_cover_artist_terms,mooberry-book-manager,1000,Mooberry Book Manager +manage_crm,wp-smart-crm-invoices-free,300,WP smart CRM & Invoices FREE +manage_customfields,catchers-helpdesk,200,Catchers Helpdesk and Ticket system for Support +manage_database,wp-dbmanager,100000,WP-DBManager +manage_department,hrm,200,WP Human Resource Management +manage_departments,employee-directory,400,Staff Directory – Employee Directory for WordPress +manage_designation,hrm,200,WP Human Resource Management +manage_discount_codes_cap,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +manage_ditty_news_ticker_settings,ditty-news-ticker,40000,Ditty News Ticker +manage_download_attachments,download-attachments,10000,Download Attachments +manage_downloads,download-monitor,100000,Download Monitor +manage_downloads,hacklog-downloadmanager,300,Hacklog DownloadManager +manage_downloads,wp-downloadmanager,8000,WP-DownloadManager +manage_ebay_listings,wp-lister-for-ebay,5000,WP-Lister Lite for eBay +manage_ebay_options,wp-lister-for-ebay,5000,WP-Lister Lite for eBay +manage_editor_terms,mooberry-book-manager,1000,Mooberry Book Manager +manage_editorial_access,editorial-access-manager,400,Editorial Access Manager +manage_educator,educator,1000,Educator 2 +manage_educator,ibeducator,1000,Educator +manage_email,wp-email,10000,WP-EMail +manage_email_categories,mailer-dragon,300,Mailer Dragon – Email Marketing Plugin for WordPress +manage_email_logs,email-log,20000,Email Log +manage_email_settings,mailer-dragon,300,Mailer Dragon – Email Marketing Plugin for WordPress +manage_employee,hrm,200,WP Human Resource Management +manage_employee_profile,hrm,200,WP Human Resource Management +manage_employee_tags,employee-spotlight,1000,Team Members Staff Showcase Plugin – Employee Spotlight +manage_employment_type,employee-directory,400,Staff Directory – Employee Directory for WordPress +manage_event_categories,event-organiser,40000,Event Organiser +manage_event_categories,events-maker,4000,Events Maker by dFactory +manage_event_listing_terms,wp-event-manager,1000,WP Event Manager +manage_event_listings,wp-event-manager,1000,WP Event Manager +manage_event_locations,events-maker,4000,Events Maker by dFactory +manage_event_magic_terms,kikfyre-events-calendar-tickets,200,"Events, Calendars & Tickets – Event Kikfyre" +manage_event_organizers,events-maker,4000,Events Maker by dFactory +manage_event_tags,events-maker,4000,Events Maker by dFactory +manage_events_cap,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +manage_events_categories,all-in-one-event-calendar,100000,All-in-One Event Calendar +manage_everest_form_terms,everest-forms,40000,Everest Forms – Easy Contact Form and Form Builder for WordPress +manage_everest_forms,everest-forms,40000,Everest Forms – Easy Contact Form and Form Builder for WordPress +manage_ezemails,ez-emails,500,EZ Emails +manage_fa_terms,featured-articles-lite,3000,FA Lite – WP responsive slider plugin +manage_facebook_awd_opengraph,facebook-awd,1000,Facebook AWD All in one +manage_facebook_awd_plugins,facebook-awd,1000,Facebook AWD All in one +manage_facebook_awd_publish_to_pages,facebook-awd,1000,Facebook AWD All in one +manage_facebook_awd_settings,facebook-awd,1000,Facebook AWD All in one +manage_feed_settings,wp-rss-aggregator,60000,WP RSS Aggregator +manage_feed_source_terms,wp-rss-aggregator,60000,WP RSS Aggregator +manage_feed_terms,wp-rss-aggregator,60000,WP RSS Aggregator +manage_filter_groups,plugin-organizer,10000,Plugin Organizer +manage_flash_toolkit,flash-toolkit,30000,Flash Toolkit +manage_fonts,font-organizer,20000,Font Organizer +manage_food_group_terms,restaurantpress,3000,RestaurantPress +manage_food_menu_terms,restaurantpress,3000,RestaurantPress +manage_football_pool,football-pool,1000,Football Pool +manage_forms,formlift,800,FormLift for Infusionsoft Web Forms +manage_gender,employee-directory,400,Staff Directory – Employee Directory for WordPress +manage_genre_terms,mooberry-book-manager,1000,Mooberry Book Manager +manage_give_form_terms,give,50000,Give – Donation Plugin and Fundraising Platform +manage_give_payment_terms,give,50000,Give – Donation Plugin and Fundraising Platform +manage_give_settings,give,50000,Give – Donation Plugin and Fundraising Platform +manage_glossaries,glossary-by-codeat,1000,Glossary +manage_groups,employee-spotlight,1000,Team Members Staff Showcase Plugin – Employee Spotlight +manage_guiform,guiform,400,GuiForm +manage_h5p_libraries,h5p,10000,Interactive Content – H5P +manage_hami,wp2appir,300,wp2appir +manage_hb_booking,wp-hotel-booking,7000,WP Hotel Booking +manage_hotel_location_terms,awebooking,6000,AweBooking – Hotel Booking System +manage_hotel_service_terms,awebooking,6000,AweBooking – Hotel Booking System +manage_hotelier,wp-hotelier,1000,Easy WP Hotelier +manage_hrm_organization,hrm,200,WP Human Resource Management +manage_illustrator_terms,mooberry-book-manager,1000,Mooberry Book Manager +manage_invoice_terms,essential-real-estate,3000,Essential Real Estate +manage_invoicing,invoicing,2000,Invoicing – Invoice & Payments Plugin +manage_issuem_settings,issuem,1000,IssueM +manage_issues,issuem,1000,IssueM +manage_jbbrd_businesses_tags,job-board,300,Job Board by BestWebSoft +manage_jbbrd_employment_tags,job-board,300,Job Board by BestWebSoft +manage_jigoshop,jigoshop,4000,Jigoshop +manage_jigoshop,jigoshop-ecommerce,400,Jigoshop eCommerce +manage_jigoshop_coupons,jigoshop,4000,Jigoshop +manage_jigoshop_coupons,jigoshop-ecommerce,400,Jigoshop eCommerce +manage_jigoshop_emails,jigoshop,4000,Jigoshop +manage_jigoshop_orders,jigoshop,4000,Jigoshop +manage_jigoshop_orders,jigoshop-ecommerce,400,Jigoshop eCommerce +manage_jigoshop_products,jigoshop,4000,Jigoshop +manage_jigoshop_products,jigoshop-ecommerce,400,Jigoshop eCommerce +manage_job_listing_terms,wp-job-manager,100000,WP Job Manager +manage_job_listings,wp-job-manager,100000,WP Job Manager +manage_jobtitles,employee-directory,400,Staff Directory – Employee Directory for WordPress +manage_klaviyo_shop_cart_terms,klaviyo-for-woocommerce,1000,Klaviyo for WooCommerce +manage_lana_download_logs,lana-downloads-manager,1000,Lana Downloads Manager +manage_lazyest_files,lazyest-gallery,1000,Lazyest Gallery +manage_leaguemanager,leaguemanager,2000,LeagueManager +manage_leave,hrm,200,WP Human Resource Management +manage_licenses_for_awesome_support,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +manage_lifterlms,lifterlms,8000,LifterLMS +manage_listing_terms,wpcasa,2000,WPCasa +manage_loan,hrm,200,WP Human Resource Management +manage_location,hrm,200,WP Human Resource Management +manage_marital_status,employee-directory,400,Staff Directory – Employee Directory for WordPress +manage_mbdb_book_grids,mooberry-book-manager,1000,Mooberry Book Manager +manage_mbdb_books,mooberry-book-manager,1000,Mooberry Book Manager +manage_mbm,mooberry-book-manager,1000,Mooberry Book Manager +manage_membership_cats,lifterlms,8000,LifterLMS +manage_membership_tags,lifterlms,8000,LifterLMS +manage_module_skins,site-editor,300,Site Editor – WordPress Site Builder – Theme Builder and Page Builder +manage_modules,site-editor,300,Site Editor – WordPress Site Builder – Theme Builder and Page Builder +manage_mp_menu_item_terms,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +manage_mprm_order_terms,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +manage_mstw_plugins,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +manage_mstw_plugins,team-rosters,800,Team Rosters +manage_mstw_schedules,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +manage_mstw_schedules,team-rosters,800,Team Rosters +manage_network,design-approval-system,500,Design Approval System +manage_network_options,design-approval-system,500,Design Approval System +manage_network_themes,design-approval-system,500,Design Approval System +manage_network_users,design-approval-system,500,Design Approval System +manage_news_categories,news-manager,3000,News Manager +manage_news_tags,news-manager,3000,News Manager +manage_newsletter_options,alo-easymail,10000,ALO EasyMail Newsletter +manage_newsletter_subscribers,alo-easymail,10000,ALO EasyMail Newsletter +manage_notice,hrm,200,WP Human Resource Management +manage_notices,notices,400,Notices Ticker +manage_novelist_settings,novelist,800,Novelist +manage_office_locations,employee-spotlight,1000,Team Members Staff Showcase Plugin – Employee Spotlight +manage_opalestate_agents_terms,opal-estate,1000,Opal Estate +manage_opalestate_properties_terms,opal-estate,1000,Opal Estate +manage_opalestate_settings,opal-estate,1000,Opal Estate +manage_operations_emd_agents,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +manage_operations_emd_canned_responses,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +manage_operations_emd_contacts,wp-easy-contact,600,Best Contact Management Software for WordPress +manage_operations_emd_employees,employee-directory,400,Staff Directory – Employee Directory for WordPress +manage_operations_emd_employees,employee-spotlight,1000,Team Members Staff Showcase Plugin – Employee Spotlight +manage_operations_emd_persons,campus-directory,200,Faculty Staff and Student Directory Plugin – Campus Directory +manage_operations_emd_quotes,request-a-quote,1000,Request a Quote +manage_operations_emd_tickets,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +manage_operations_emd_videos,youtube-showcase,7000,YouTube Gallery – Best YouTube Video Gallery for WordPress +manage_orbis,orbis,200,Orbis +manage_orders_cap,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +manage_organization,hrm,200,WP Human Resource Management +manage_others_bookings,events-manager,100000,Events Manager +manage_package_terms,essential-real-estate,3000,Essential Real Estate +manage_patrons,weblibrarian,700,WebLibrarian +manage_payroll,hrm,200,WP Human Resource Management +manage_pbb,peanut-butter-bar-smooth-version,500,Peanut Butter Bar (smooth version) +manage_person_area,campus-directory,200,Faculty Staff and Student Directory Plugin – Campus Directory +manage_person_location,campus-directory,200,Faculty Staff and Student Directory Plugin – Campus Directory +manage_person_rareas,campus-directory,200,Faculty Staff and Student Directory Plugin – Campus Directory +manage_person_title,campus-directory,200,Faculty Staff and Student Directory Plugin – Campus Directory +manage_photocontest,wp-photocontest,200,PhotoContest Plugin +manage_photocontests,wp-photocontest,200,PhotoContest Plugin +manage_phpleague,phpleague,300,PHPLeague +manage_podcast,seriously-simple-podcasting,10000,Seriously Simple Podcasting +manage_polls,wp-polls,100000,WP-Polls +manage_popup_categories_terms,popup-builder,100000,Popup Builder – Responsive WordPress Pop up – Subscription & Newsletter +manage_popup_terms,popup-builder,100000,Popup Builder – Responsive WordPress Pop up – Subscription & Newsletter +manage_portfolio_categories,custom-content-portfolio,1000,Custom Content Portfolio +manage_portfolio_tags,custom-content-portfolio,1000,Custom Content Portfolio +manage_portfolio_terms,flash-toolkit,30000,Flash Toolkit +manage_portfolio_terms,suffice-toolkit,5000,Suffice Toolkit +manage_postman_smtp,post-smtp,70000,Post SMTP Mailer/Email Log +manage_postman_smtp,postman-smtp,100000,Postman SMTP Mailer/Email Log +manage_postmen,postmen-woo-shipping,800,Postmen WooCommerce Shipping +manage_pricing_rate_terms,awebooking,6000,AweBooking – Hotel Booking System +manage_product,dc-woocommerce-multi-vendor,10000,WC Marketplace +manage_product,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +manage_product_categories,ecommerce-product-catalog,10000,eCommerce Product Catalog Plugin for WordPress +manage_product_categories,post-type-x,1000,Product Catalog X +manage_product_categories,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +manage_product_settings,ecommerce-product-catalog,10000,eCommerce Product Catalog Plugin for WordPress +manage_product_settings,post-type-x,1000,Product Catalog X +manage_product_tags,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +manage_product_terms,easy-digital-downloads,60000,Easy Digital Downloads +manage_product_terms,jigoshop,4000,Jigoshop +manage_product_terms,jigoshop-ecommerce,400,Jigoshop eCommerce +manage_product_terms,webmaster-user-role,8000,Webmaster User Role +manage_product_terms,woocommerce,4000000,WooCommerce +manage_project_terms,upstream,1000,WordPress Project Management by UpStream +manage_property_terms,essential-real-estate,3000,Essential Real Estate +manage_propertyhive,propertyhive,900,PropertyHive +manage_pta,pta-member-directory,1000,PTA Member Directory and Contact Form +manage_quote_authors,mg-quotes,300,mg Quotes +manage_quote_categories,mg-quotes,300,mg Quotes +manage_raq_services,request-a-quote,1000,Request a Quote +manage_ratings,wp-postratings,100000,WP-PostRatings +manage_real_estate,essential-real-estate,3000,Essential Real Estate +manage_registrations,pta-member-directory,1000,PTA Member Directory and Contact Form +manage_restaurant,restaurant,300,Restaurant +manage_restaurant,restaurant-manager,800,Restaurant Manager +manage_restaurant_menu,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +manage_restaurant_options,restaurant-manager,800,Restaurant Manager +manage_restaurant_settings,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +manage_restaurant_terms,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +manage_restaurantpress,restaurantpress,3000,RestaurantPress +manage_restriction,restaurant-manager,800,Restaurant Manager +manage_resume_organizations,wp-resume,700,WP Resume +manage_resume_sections,wp-resume,700,WP Resume +manage_room_reservation_terms,wp-hotelier,1000,Easy WP Hotelier +manage_room_terms,wp-hotelier,1000,Easy WP Hotelier +manage_room_type_terms,awebooking,6000,AweBooking – Hotel Booking System +manage_rootspersona1,rootspersona,1000,Rootspersona +manage_rootspersona2,rootspersona,1000,Rootspersona +manage_rps_include_content,rps-include-content,2000,RPS Include Content +manage_sales_dash,zero-bs-crm,1000,Zero BS WordPress CRM +manage_salon,salon-booking-system,5000,Salon booking system +manage_saved_search,estatik,2000,Estatik Real Estate Plugin +manage_schema_options,schema,50000,Schema +manage_search_live,search-live,2000,Search Live +manage_sendpress,sendpress,9000,SendPress Newsletters +manage_series,organize-series,4000,Organize Series +manage_series,series,4000,Series +manage_series_terms,mooberry-book-manager,1000,Mooberry Book Manager +manage_settings,hrm,200,WP Human Resource Management +manage_settings_cap,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +manage_sh4,shiftcontroller,600,ShiftController – Employee Shift Scheduling +manage_shop_coupon_terms,jigoshop,4000,Jigoshop +manage_shop_coupon_terms,jigoshop-ecommerce,400,Jigoshop eCommerce +manage_shop_coupon_terms,webmaster-user-role,8000,Webmaster User Role +manage_shop_coupon_terms,woocommerce,4000000,WooCommerce +manage_shop_coupons,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +manage_shop_discount_terms,easy-digital-downloads,60000,Easy Digital Downloads +manage_shop_discounts,easy-digital-downloads,60000,Easy Digital Downloads +manage_shop_email_terms,jigoshop,4000,Jigoshop +manage_shop_email_terms,jigoshop-ecommerce,400,Jigoshop eCommerce +manage_shop_order_terms,jigoshop,4000,Jigoshop +manage_shop_order_terms,jigoshop-ecommerce,400,Jigoshop eCommerce +manage_shop_order_terms,webmaster-user-role,8000,Webmaster User Role +manage_shop_order_terms,woocommerce,4000000,WooCommerce +manage_shop_payment_terms,easy-digital-downloads,60000,Easy Digital Downloads +manage_shop_settings,easy-digital-downloads,60000,Easy Digital Downloads +manage_shop_webhook_terms,woocommerce,4000000,WooCommerce +manage_signup_sheets,pta-member-directory,1000,PTA Member Directory and Contact Form +manage_signup_sheets,pta-volunteer-sign-up-sheets,2000,PTA Volunteer Sign Up Sheets +manage_signup_sheets,sign-up-sheets,1000,Sign-up Sheets +manage_site_editor_preset,site-editor,300,Site Editor – WordPress Site Builder – Theme Builder and Page Builder +manage_site_media,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +manage_sites,design-approval-system,500,Design Approval System +manage_slp,store-locator-le,10000,Store Locator Plus™ for WordPress +manage_slp_admin,store-locator-le,10000,Store Locator Plus™ for WordPress +manage_slp_user,store-locator-le,10000,Store Locator Plus™ for WordPress +manage_snippets,code-snippets,100000,Code Snippets +manage_sp_calendar_terms,sportspress,20000,SportsPress – Sports Club & League Manager +manage_sp_config_terms,sportspress,20000,SportsPress – Sports Club & League Manager +manage_sp_event_terms,sportspress,20000,SportsPress – Sports Club & League Manager +manage_sp_list_terms,sportspress,20000,SportsPress – Sports Club & League Manager +manage_sp_player_terms,sportspress,20000,SportsPress – Sports Club & League Manager +manage_sp_staff_terms,sportspress,20000,SportsPress – Sports Club & League Manager +manage_sp_table_terms,sportspress,20000,SportsPress – Sports Club & League Manager +manage_sp_team_terms,sportspress,20000,SportsPress – Sports Club & League Manager +manage_sportspress,sportspress,20000,SportsPress – Sports Club & League Manager +manage_sprout_invoices_importer,sprout-invoices,2000,Client Invoicing by Sprout Invoices – Easy Estimates and Invoices for WordPress +manage_sprout_invoices_options,sprout-invoices,2000,Client Invoicing by Sprout Invoices – Easy Estimates and Invoices for WordPress +manage_sprout_invoices_payments,sprout-invoices,2000,Client Invoicing by Sprout Invoices – Easy Estimates and Invoices for WordPress +manage_sprout_invoices_records,sprout-invoices,2000,Client Invoicing by Sprout Invoices – Easy Estimates and Invoices for WordPress +manage_store_settings,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +manage_students,pta-member-directory,1000,PTA Member Directory and Contact Form +manage_suffice_toolkit,suffice-toolkit,5000,Suffice Toolkit +manage_support,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +manage_support_plus_agent,wp-support-plus-responsive-ticket-system,10000,WP Support Plus Responsive Ticket System +manage_support_plus_ticket,wp-support-plus-responsive-ticket-system,10000,WP Support Plus Responsive Ticket System +manage_support_tickets,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +manage_syn_rest_course,restaurant-manager,800,Restaurant Manager +manage_syn_rest_cuisine,restaurant-manager,800,Restaurant Manager +manage_syn_rest_diet,restaurant-manager,800,Restaurant Manager +manage_syn_rest_menu,restaurant-manager,800,Restaurant Manager +manage_tag_terms,mooberry-book-manager,1000,Mooberry Book Manager +manage_task_manager,task-manager,400,Task Manager +manage_taxonomies,cms-press,1000,CMS Press +manage_tdih_events,this-day-in-history,800,This Day In History +manage_ticket_priority,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +manage_ticket_status,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +manage_ticket_templates_cap,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +manage_ticket_topic,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +manage_ticket_types_cap,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +manage_topic_tags,bbpress,300000,bbPress +manage_tr_teams,team-rosters,800,Team Rosters +manage_trans_log_terms,essential-real-estate,3000,Essential Real Estate +manage_translations,wp-multilang,7000,WP Multilang +manage_upstream,upstream,1000,WordPress Project Management by UpStream +manage_user_karma_settings,comment-popularity,300,Comment Popularity +manage_user_package_terms,essential-real-estate,3000,Essential Real Estate +manage_user_registration,user-registration,7000,"User Registration – Custom Registration Form, Login and User Profile for WordPress" +manage_user_registration_terms,user-registration,7000,"User Registration – Custom Registration Form, Login and User Profile for WordPress" +manage_venues,event-organiser,40000,Event Organiser +manage_vosl,vo-locator-the-wp-store-locator,500,VO Store Locator – WP Store Locator Plugin +manage_wallets,wallets,600,Bitcoin and Altcoin Wallets +manage_wd_ads_groups,ad-manager-wd,700,Ad Manager by WD – Advanced Ad Manager plugin +manage_web_invoice,web-invoice,200,Web Invoice – Invoicing and billing for WordPress +manage_whistles,whistles,2000,Whistles +manage_widgets,restrict-widgets,20000,Restrict Widgets +manage_woocommerce,webmaster-user-role,8000,Webmaster User Role +manage_woocommerce,woocommerce,4000000,WooCommerce +manage_woocommerce_hf_membership_plans,xa-woocommerce-memberships,400,Memberships for WooCommerce +manage_woocommerce_hf_user_memberships,xa-woocommerce-memberships,400,Memberships for WooCommerce +manage_woocommerce_pos,woocommerce-pos,7000,WooCommerce POS +manage_woocommerce_products,design-approval-system,500,Design Approval System +manage_wordpoints_points_types,wordpoints,700,WordPoints +manage_wp2syslog,wp2syslog,200,wp2syslog +manage_wp_athletics,wp-athletics,300,WP Athletics +manage_wp_instagram_gallery,wp-instagram-gallery,500,WP Instagram Gallery +manage_wpclubmanager,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +manage_wpcm_club_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +manage_wpcm_match_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +manage_wpcm_player_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +manage_wpcm_sponsor_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +manage_wpcm_staff_terms,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +manage_wpfc_categories,sermon-manager-for-wordpress,9000,Sermon Manager +manage_wpfc_sm_settings,sermon-manager-for-wordpress,9000,Sermon Manager +manage_wpp_admintools,wp-property,5000,WP-Property – WordPress Powered Real Estate and Property Management +manage_wpp_categories,wp-property,5000,WP-Property – WordPress Powered Real Estate and Property Management +manage_wpp_make_featured,wp-property,5000,WP-Property – WordPress Powered Real Estate and Property Management +manage_wpp_settings,wp-property,5000,WP-Property – WordPress Powered Real Estate and Property Management +manage_wpsdeals_terms,deals-engine,200,Social Deals Engine +manage_wpsdealssales_terms,deals-engine,200,Social Deals Engine +manage_wpshop,wpshop,1000,=== WPshop – eCommerce +manage_wpsl_settings,wp-store-locator,50000,WP Store Locator +manage_xydac_cms,ultimate-cms,200,Ultimate CMS +manage_yop_polls_bans,yop-poll,20000,YOP Poll +manage_yop_polls_imports,yop-poll,20000,YOP Poll +manage_yop_polls_options,yop-poll,20000,YOP Poll +manage_zodiacpress_interps,zodiacpress,400,ZodiacPress +manage_zodiacpress_settings,zodiacpress,400,ZodiacPress +mc_add_events,my-calendar,30000,My Calendar +mc_approve_events,my-calendar,30000,My Calendar +mc_edit_behaviors,my-calendar,30000,My Calendar +mc_edit_cats,my-calendar,30000,My Calendar +mc_edit_locations,my-calendar,30000,My Calendar +mc_edit_settings,my-calendar,30000,My Calendar +mc_edit_styles,my-calendar,30000,My Calendar +mc_edit_templates,my-calendar,30000,My Calendar +mc_manage_events,my-calendar,30000,My Calendar +mc_view_help,my-calendar,30000,My Calendar +mdjm_employee,mobile-dj-manager,200,MDJM Event Management +mdocs-dashboard,memphis-documents-library,5000,Memphis Documents Library +mdocs_allow_upload,memphis-documents-library,5000,Memphis Documents Library +mdocs_allow_upload_frontend,memphis-documents-library,5000,Memphis Documents Library +mdocs_batch_delete,memphis-documents-library,5000,Memphis Documents Library +mdocs_batch_edit,memphis-documents-library,5000,Memphis Documents Library +mdocs_batch_move,memphis-documents-library,5000,Memphis Documents Library +mdocs_dashboard,memphis-documents-library,5000,Memphis Documents Library +mdocs_manage_options,memphis-documents-library,5000,Memphis Documents Library +mdocs_manage_settings,memphis-documents-library,5000,Memphis Documents Library +mediatags_assign_terms,media-tags,5000,Media Tags +mediatags_delete_terms,media-tags,5000,Media Tags +mediatags_edit_terms,media-tags,5000,Media Tags +mediatags_manage_role_cap,media-tags,5000,Media Tags +mediatags_manage_terms,media-tags,5000,Media Tags +mediatags_settings,media-tags,5000,Media Tags +microblogposter_who_can_auto_publish,microblog-poster,10000,Microblog Poster – Auto Publish on Social Media +milestone_assigned_to_field,upstream,1000,WordPress Project Management by UpStream +milestone_end_date_field,upstream,1000,WordPress Project Management by UpStream +milestone_milestone_field,upstream,1000,WordPress Project Management by UpStream +milestone_notes_field,upstream,1000,WordPress Project Management by UpStream +milestone_start_date_field,upstream,1000,WordPress Project Management by UpStream +moderate,bbpress,300000,bbPress +moderate_comments_hanaboard-post,hana-board,1000,Hana-Board 하나보드 워드프레스 게시판 +moderate_schreikasten,schreikasten,1000,Schreikasten +modify_ditty_news_ticker_settings,ditty-news-ticker,40000,Ditty News Ticker +mr_edit_ratings,multi-rating,6000,Multi Rating +mt-copy-cart,my-tickets,1000,My Tickets +mt-order-comps,my-tickets,1000,My Tickets +mt-order-expired,my-tickets,1000,My Tickets +mt-verify-ticket,my-tickets,1000,My Tickets +mt-view-reports,my-tickets,1000,My Tickets +mto_admin_overview,meta-tags-optimization,1000,Meta Tags Optimization +mto_how_overview,meta-tags-optimization,1000,Meta Tags Optimization +namaste,namaste-lms,1000,Namaste! LMS +namaste_manage,namaste-lms,1000,Namaste! LMS +newsletters_admin_send_sendtoroles,newsletters-lite,8000,Newsletters +newsletters_autoresponderemails,newsletters-lite,8000,Newsletters +newsletters_autoresponders,newsletters-lite,8000,Newsletters +newsletters_clicks,newsletters-lite,8000,Newsletters +newsletters_emails,newsletters-lite,8000,Newsletters +newsletters_extensions,newsletters-lite,8000,Newsletters +newsletters_extensions_settings,newsletters-lite,8000,Newsletters +newsletters_fields,newsletters-lite,8000,Newsletters +newsletters_forms,newsletters-lite,8000,Newsletters +newsletters_gdpr,newsletters-lite,8000,Newsletters +newsletters_groups,newsletters-lite,8000,Newsletters +newsletters_history,newsletters-lite,8000,Newsletters +newsletters_importexport,newsletters-lite,8000,Newsletters +newsletters_links,newsletters-lite,8000,Newsletters +newsletters_lists,newsletters-lite,8000,Newsletters +newsletters_orders,newsletters-lite,8000,Newsletters +newsletters_queue,newsletters-lite,8000,Newsletters +newsletters_send,newsletters-lite,8000,Newsletters +newsletters_settings,newsletters-lite,8000,Newsletters +newsletters_settings_api,newsletters-lite,8000,Newsletters +newsletters_settings_subscribers,newsletters-lite,8000,Newsletters +newsletters_settings_system,newsletters-lite,8000,Newsletters +newsletters_settings_tasks,newsletters-lite,8000,Newsletters +newsletters_settings_templates,newsletters-lite,8000,Newsletters +newsletters_settings_updates,newsletters-lite,8000,Newsletters +newsletters_submitserial,newsletters-lite,8000,Newsletters +newsletters_subscribers,newsletters-lite,8000,Newsletters +newsletters_support,newsletters-lite,8000,Newsletters +newsletters_templates,newsletters-lite,8000,Newsletters +newsletters_templates_save,newsletters-lite,8000,Newsletters +newsletters_themes,newsletters-lite,8000,Newsletters +newsletters_welcome,newsletters-lite,8000,Newsletters +newsman_wpNewsman,wpnewsman-newsletters,1000,WPNewsman Lite +nextend,smart-slider-3,300000,Smart Slider 3 +nextend_config,smart-slider-3,300000,Smart Slider 3 +nextend_visual_delete,smart-slider-3,300000,Smart Slider 3 +nextend_visual_edit,smart-slider-3,300000,Smart Slider 3 +nicescrollr_edit,nicescrollr,200,Nicescrollr +oQeyGalleries,oqey-gallery,3000,Plugin Name: oQey Gallery +oQeyMusic,oqey-gallery,3000,Plugin Name: oQey Gallery +oQeyRoles,oqey-gallery,3000,Plugin Name: oQey Gallery +oQeySettings,oqey-gallery,3000,Plugin Name: oQey Gallery +oQeySkins,oqey-gallery,3000,Plugin Name: oQey Gallery +oQeyTrash,oqey-gallery,3000,Plugin Name: oQey Gallery +oQeyVideo,oqey-gallery,3000,Plugin Name: oQey Gallery +olimometer_dashboard_widget,olimometer,2000,Olimometer +oqey-gallery,oqey-gallery,3000,Plugin Name: oQey Gallery +override_document_lock,wp-document-revisions,4000,WP Document Revisions +ow_abort_workflow,oasis-workflow,1000,Oasis Workflow +ow_create_workflow,oasis-workflow,1000,Oasis Workflow +ow_delete_workflow,oasis-workflow,1000,Oasis Workflow +ow_delete_workflow_history,oasis-workflow,1000,Oasis Workflow +ow_download_workflow_history,oasis-workflow,1000,Oasis Workflow +ow_edit_workflow,oasis-workflow,1000,Oasis Workflow +ow_reassign_task,oasis-workflow,1000,Oasis Workflow +ow_sign_off_step,oasis-workflow,1000,Oasis Workflow +ow_skip_workflow,oasis-workflow,1000,Oasis Workflow +ow_submit_to_workflow,oasis-workflow,1000,Oasis Workflow +ow_view_others_inbox,oasis-workflow,1000,Oasis Workflow +ow_view_reports,oasis-workflow,1000,Oasis Workflow +ow_view_workflow_history,oasis-workflow,1000,Oasis Workflow +p2pConverter,p2pconverter,2000,p2pConverter +pTypeConverter,ptypeconverter,7000,pTypeConverter +participate,bbpress,300000,bbPress +payroll_revistion,hrm,200,WP Human Resource Management +pc_manage_permalink_redirects,permalinks-customizer,3000,Permalinks Customizer +pc_manage_permalink_settings,permalinks-customizer,3000,Permalinks Customizer +pc_manage_permalinks,permalinks-customizer,3000,Permalinks Customizer +phpleague,phpleague,300,PHPLeague +picasa_dialog,photo-express-for-google,2000,Photo Express for Google +picasa_dialog,picasa-express-x2,4000,Picasa and Google Plus Express +pipe_access_embed,pipe-video-recorder,300,Pipe Video Recorder +pipe_access_plugin,pipe-video-recorder,300,Pipe Video Recorder +pipe_access_record,pipe-video-recorder,300,Pipe Video Recorder +pipe_access_recordings,pipe-video-recorder,300,Pipe Video Recorder +pipe_access_setup,pipe-video-recorder,300,Pipe Video Recorder +pmpro_addons,paid-memberships-pro,70000,Paid Memberships Pro +pmpro_advancedsettings,paid-memberships-pro,70000,Paid Memberships Pro +pmpro_discountcodes,paid-memberships-pro,70000,Paid Memberships Pro +pmpro_edit_memberships,paid-memberships-pro,70000,Paid Memberships Pro +pmpro_emailsettings,paid-memberships-pro,70000,Paid Memberships Pro +pmpro_membershiplevels,paid-memberships-pro,70000,Paid Memberships Pro +pmpro_memberships_menu,paid-memberships-pro,70000,Paid Memberships Pro +pmpro_memberslist,paid-memberships-pro,70000,Paid Memberships Pro +pmpro_memberslistcsv,paid-memberships-pro,70000,Paid Memberships Pro +pmpro_orders,paid-memberships-pro,70000,Paid Memberships Pro +pmpro_orderscsv,paid-memberships-pro,70000,Paid Memberships Pro +pmpro_pagesettings,paid-memberships-pro,70000,Paid Memberships Pro +pmpro_paymentsettings,paid-memberships-pro,70000,Paid Memberships Pro +pmpro_reports,paid-memberships-pro,70000,Paid Memberships Pro +pmpro_updates,paid-memberships-pro,70000,Paid Memberships Pro +podlove_read_analytics,podlove-podcasting-plugin-for-wordpress,4000,Podlove Podcast Publisher +podlove_read_dashboard,podlove-podcasting-plugin-for-wordpress,4000,Podlove Podcast Publisher +post_avatars,post-avatar,900,Post Avatar +post_pay_counter_access_stats,post-pay-counter,1000,Post Pay Counter +post_pay_counter_manage_options,post-pay-counter,1000,Post Pay Counter +post_via_postie,postie,20000,Postie +pp_administer_content,press-permit-core,5000,Press Permit Core +pp_assign_roles,press-permit-core,5000,Press Permit Core +pp_create_groups,press-permit-core,5000,Press Permit Core +pp_delete_groups,press-permit-core,5000,Press Permit Core +pp_edit_groups,press-permit-core,5000,Press Permit Core +pp_manage_members,press-permit-core,5000,Press Permit Core +pp_manage_roles,publishpress,1000,PublishPress – Professional publishing tools for WordPress +pp_manage_settings,press-permit-core,5000,Press Permit Core +pp_moderate_any,press-permit-core,5000,Press Permit Core +pp_set_read_exceptions,press-permit-core,5000,Press Permit Core +pp_view_calendar,publishpress,1000,PublishPress – Professional publishing tools for WordPress +pp_view_content_overview,publishpress,1000,PublishPress – Professional publishing tools for WordPress +pp_view_story_budget,publishpress,1000,PublishPress – Professional publishing tools for WordPress +prepare_ebay_listings,wp-lister-for-ebay,5000,WP-Lister Lite for eBay +project_client_field,upstream,1000,WordPress Project Management by UpStream +project_end_date_field,upstream,1000,WordPress Project Management by UpStream +project_owner_field,upstream,1000,WordPress Project Management by UpStream +project_send_newsletter,projectmanager,400,ProjectManager +project_start_date_field,upstream,1000,WordPress Project Management by UpStream +project_status_field,upstream,1000,WordPress Project Management by UpStream +project_title_field,upstream,1000,WordPress Project Management by UpStream +project_users_field,upstream,1000,WordPress Project Management by UpStream +projectmanager_send_confirmation,projectmanager,400,ProjectManager +projectmanager_settings,projectmanager,400,ProjectManager +projectmanager_user,projectmanager,400,ProjectManager +promote_users_higher_level,wpfront-user-role-editor,60000,WPFront User Role Editor +promote_users_to_higher_level,wpfront-user-role-editor,60000,WPFront User Role Editor +pronamic_client,pronamic-client,300,Pronamic Client +publish_acadp_fields,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +publish_acadp_listings,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +publish_acadp_payments,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +publish_achievement_progresses,achievements,300,Achievements for WordPress +publish_achievements,achievements,300,Achievements for WordPress +publish_ads,apply-online,5000,Apply Online +publish_aec_events,another-events-calendar,800,Another Events Calendar +publish_aec_organizers,another-events-calendar,800,Another Events Calendar +publish_aec_venues,another-events-calendar,800,Another Events Calendar +publish_affiliate_keywords,affiliate,700,Affiliate +publish_agents,essential-real-estate,3000,Essential Real Estate +publish_aggregator-records,the-events-calendar,700000,The Events Calendar +publish_ai1ec_events,all-in-one-event-calendar,100000,All-in-One Event Calendar +publish_aiovg_videos,all-in-one-video-gallery,1000,All-in-One Video Gallery +publish_anb_animations,alert-notice-boxes,1000,Alert Notice Boxes +publish_anb_animations_out,alert-notice-boxes,1000,Alert Notice Boxes +publish_anb_designs,alert-notice-boxes,1000,Alert Notice Boxes +publish_anb_locations,alert-notice-boxes,1000,Alert Notice Boxes +publish_anbs,alert-notice-boxes,1000,Alert Notice Boxes +publish_applications,apply-online,5000,Apply Online +publish_archivs,archive,700,Archive +publish_articles,issuem,1000,IssueM +publish_at_biz_dirs,directorist,500,Directorist – Business Directory Plugin +publish_atbdp_orders,directorist,500,Directorist – Business Directory Plugin +publish_awebookings,awebooking,6000,AweBooking – Hotel Booking System +publish_birs_appointments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +publish_birs_clients,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +publish_birs_locations,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +publish_birs_payments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +publish_birs_services,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +publish_birs_staffs,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +publish_blocks,gutenberg,500000,Gutenberg +publish_board_committees,nonprofit-board-management,400,Nonprofit Board Management +publish_board_events,nonprofit-board-management,400,Nonprofit Board Management +publish_book-reviews,book-review-library,700,Book Review Library +publish_books,novelist,800,Novelist +publish_boxes,boxzilla,20000,Boxzilla +publish_bps_forms,bp-profile-search,10000,BP Profile Search +publish_calp_events,calpress-event-calendar,5000,CalPress Calendar +publish_campaigns,charitable,10000,Charitable – Donation Plugin +publish_campaigns,leyka,1000,Leyka +publish_car_listings,wp-car-manager,3000,WP Car Manager +publish_cctor_coupons,coupon-creator,10000,Coupon Creator +publish_chronoslys,chronosly-events-calendar,4000,Chronosly Events Calendar +publish_classified_listings,classifieds-wp,800,Classifieds WP +publish_clients,upstream,1000,WordPress Project Management by UpStream +publish_courses,lifterlms,8000,LifterLMS +publish_ctas,cta,10000,WordPress Calls to Action +publish_cupri_pays,pardakht-delkhah,1000,پلاگین پرداخت دلخواه +publish_custom_csss,custom-css-js,100000,Simple Custom CSS and JS +publish_ditty_news_tickers,ditty-news-ticker,40000,Ditty News Ticker +publish_documents,wp-document-revisions,4000,WP Document Revisions +publish_donations,charitable,10000,Charitable – Donation Plugin +publish_donations,leyka,1000,Leyka +publish_dsn_notes,admin-dashboard-site-notes,3000,Dashboard Site Notes +publish_ebay_listings,wp-lister-for-ebay,5000,WP-Lister Lite for eBay +publish_edr_courses,educator,1000,Educator 2 +publish_edr_lessons,educator,1000,Educator 2 +publish_edr_memberships,educator,1000,Educator 2 +publish_email_templates,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +publish_emails,mailer-dragon,300,Mailer Dragon – Email Marketing Plugin for WordPress +publish_emd_agents,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +publish_emd_canned_responses,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +publish_emd_contacts,wp-easy-contact,600,Best Contact Management Software for WordPress +publish_emd_employees,employee-directory,400,Staff Directory – Employee Directory for WordPress +publish_emd_quotes,request-a-quote,1000,Request a Quote +publish_emd_tickets,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +publish_epa_albums,easy-photo-album,5000,Easy Photo Album +publish_event_listings,wp-event-manager,1000,WP Event Manager +publish_event_magics,kikfyre-events-calendar-tickets,200,"Events, Calendars & Tickets – Event Kikfyre" +publish_events,event-organiser,40000,Event Organiser +publish_events,events-maker,4000,Events Maker by dFactory +publish_events,events-manager,100000,Events Manager +publish_events,quick-event-manager,5000,Quick Event Manager +publish_everest_forms,everest-forms,40000,Everest Forms – Easy Contact Form and Form Builder for WordPress +publish_fa_items,featured-articles-lite,3000,FA Lite – WP responsive slider plugin +publish_fbtabs,facebook-tab-manager,1000,Facebook Tab Manager +publish_feed_sources,wp-rss-aggregator,60000,WP RSS Aggregator +publish_feeds,wp-rss-aggregator,60000,WP RSS Aggregator +publish_fep_announcements,front-end-pm,8000,Front End PM +publish_fep_messages,front-end-pm,8000,Front End PM +publish_flexible_invoices,flexible-invoices,1000,Flexible Invoices for WordPress +publish_food_groups,restaurantpress,3000,RestaurantPress +publish_food_menus,restaurantpress,3000,RestaurantPress +publish_foosales,foosales,400,FooSales +publish_forms,pronamic-ideal,6000,Pronamic Pay +publish_forums,bbpress,300000,bbPress +publish_galleries,gallery-box,2000,Gallery Box +publish_games,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +publish_give_forms,give,50000,Give – Donation Plugin and Fundraising Platform +publish_give_payments,give,50000,Give – Donation Plugin and Fundraising Platform +publish_glossaries,glossary-by-codeat,1000,Glossary +publish_hanaboard-post,hana-board,1000,Hana-Board 하나보드 워드프레스 게시판 +publish_hb_bookings,wp-hotel-booking,7000,WP Hotel Booking +publish_hb_rooms,wp-hotel-booking,7000,WP Hotel Booking +publish_hf_membership_plans,xa-woocommerce-memberships,400,Memberships for WooCommerce +publish_hf_user_memberships,xa-woocommerce-memberships,400,Memberships for WooCommerce +publish_hotel_locations,awebooking,6000,AweBooking – Hotel Booking System +publish_hotel_services,awebooking,6000,AweBooking – Hotel Booking System +publish_ib_edu_memberships,ibeducator,1000,Educator +publish_ib_educator_courses,ibeducator,1000,Educator +publish_ib_educator_lessons,ibeducator,1000,Educator +publish_ims_gallerys,image-store,900,Image Store +publish_inbound-form,cta,10000,WordPress Calls to Action +publish_inbound-form,landing-pages,10000,WordPress Landing Pages +publish_inbound-form,leads,7000,WordPress Leads +publish_insertcodes,insert-code,300,Insert Code +publish_invoices,essential-real-estate,3000,Essential Real Estate +publish_issues,issuem,1000,IssueM +publish_items,gamipress,2000,GamiPress +publish_job_listings,wp-job-manager,100000,WP Job Manager +publish_jscp_match,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +publish_jscp_player,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +publish_jscp_team,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +publish_klaviyo_shop_carts,klaviyo-for-woocommerce,1000,Klaviyo for WooCommerce +publish_landing_pages,landing-pages,10000,WordPress Landing Pages +publish_languages,sublanguage,1000,Sublanguage +publish_leads,cta,10000,WordPress Calls to Action +publish_leads,landing-pages,10000,WordPress Landing Pages +publish_leads,leads,7000,WordPress Leads +publish_legalpack_pages,legalpack,400,Legalpack +publish_lessons,lifterlms,8000,LifterLMS +publish_listings,auto-listings,400,Auto Listings +publish_listings,wp-real-estate,400,WP Real Estate +publish_listings,wpcasa,2000,WPCasa +publish_locations,events-manager,100000,Events Manager +publish_lp_courses,learnpress,50000,LearnPress – WordPress LMS Plugin +publish_lp_lessons,learnpress,50000,LearnPress – WordPress LMS Plugin +publish_lp_orders,learnpress,50000,LearnPress – WordPress LMS Plugin +publish_masterslider,master-slider,100000,Master Slider – Responsive Touch Slider +publish_mbdb_book,mooberry-book-manager,1000,Mooberry Book Manager +publish_mbdb_book_grid,mooberry-book-manager,1000,Mooberry Book Manager +publish_mbdb_book_grids,mooberry-book-manager,1000,Mooberry Book Manager +publish_mbdb_books,mooberry-book-manager,1000,Mooberry Book Manager +publish_meals,restaurant-manager,800,Restaurant Manager +publish_memberships,lifterlms,8000,LifterLMS +publish_mp_menu_items,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +publish_mprm_orders,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +publish_nc_references,nelio-content,7000,Nelio Content – Social Media Marketing Automation +publish_nemus-sliders,nemus-slider,2000,Nemus Slider +publish_news,news-manager,3000,News Manager +publish_newsletters,alo-easymail,10000,ALO EasyMail Newsletter +publish_niso_slider_carousels,niso-carousel,200,Niso Carousel +publish_niso_slider_carousels,niso-carousel-slider,400,Niso Carousel Slider +publish_opalestate_agentss,opal-estate,1000,Opal Estate +publish_opalestate_propertiess,opal-estate,1000,Opal Estate +publish_opanda-items,opt-in-panda,3000,OnePress Opt-In Panda +publish_opanda-items,social-locker,10000,OnePress Social Locker +publish_orbis_companies,orbis,200,Orbis +publish_orbis_projects,orbis,200,Orbis +publish_packages,essential-real-estate,3000,Essential Real Estate +publish_page_in_section,bu-section-editing,300,BU Section Editing +publish_players,team-rosters,800,Team Rosters +publish_playlists,radio-station,1000,Radio Station +publish_plugin_filters,plugin-organizer,10000,Plugin Organizer +publish_plugin_groups,plugin-organizer,10000,Plugin Organizer +publish_portfolio_projects,custom-content-portfolio,1000,Custom Content Portfolio +publish_portfolios,flash-toolkit,30000,Flash Toolkit +publish_portfolios,suffice-toolkit,5000,Suffice Toolkit +publish_portfolios,visual-portfolio,7000,Visual Portfolio +publish_post_in_section,bu-section-editing,300,BU Section Editing +publish_pricing_rates,awebooking,6000,AweBooking – Hotel Booking System +publish_product_sets,datafeedr-product-sets,1000,Datafeedr Product Sets +publish_products,design-approval-system,500,Design Approval System +publish_products,easy-digital-downloads,60000,Easy Digital Downloads +publish_products,ecommerce-product-catalog,10000,eCommerce Product Catalog Plugin for WordPress +publish_products,gnucommerce,1000,GNUCommerce +publish_products,jigoshop,4000,Jigoshop +publish_products,jigoshop-ecommerce,400,Jigoshop eCommerce +publish_products,post-type-x,1000,Product Catalog X +publish_products,products,300,WP Products +publish_products,webmaster-user-role,8000,Webmaster User Role +publish_products,woocommerce,4000000,WooCommerce +publish_products,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +publish_products,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +publish_profile_cct,profile-custom-content-type,200,Profile CCT +publish_project_bugs,upstream,1000,WordPress Project Management by UpStream +publish_project_discussion,upstream,1000,WordPress Project Management by UpStream +publish_project_files,upstream,1000,WordPress Project Management by UpStream +publish_project_milestones,upstream,1000,WordPress Project Management by UpStream +publish_project_tasks,upstream,1000,WordPress Project Management by UpStream +publish_projects,upstream,1000,WordPress Project Management by UpStream +publish_propertys,essential-real-estate,3000,Essential Real Estate +publish_psp_projects,project-panorama-lite,1000,Project Panorama +publish_questions,lifterlms,8000,LifterLMS +publish_quizzes,lifterlms,8000,LifterLMS +publish_quotes,mg-quotes,300,mg Quotes +publish_recurring_events,events-manager,100000,Events Manager +publish_redirects,wp-redirects,700,WP Redirects +publish_rem_properties,real-estate-manager,1000,Real Estate Manager – Property Listing and Agent Management +publish_replies,bbpress,300000,bbPress +publish_reservations,restaurant-manager,800,Restaurant Manager +publish_resume_positions,wp-resume,700,WP Resume +publish_room_reservations,wp-hotelier,1000,Easy WP Hotelier +publish_room_types,awebooking,6000,AweBooking – Hotel Booking System +publish_rooms,wp-hotelier,1000,Easy WP Hotelier +publish_rsvpemails,rsvpmaker,1000,RSVPMaker +publish_rsvpmakers,rsvpmaker,1000,RSVPMaker +publish_schedules,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +publish_sgpb_popups,popup-builder,100000,Popup Builder – Responsive WordPress Pop up – Subscription & Newsletter +publish_shifts,employee-scheduler,400,Shiftee Basic – Employee and Staff Scheduling +publish_shop_coupons,jigoshop,4000,Jigoshop +publish_shop_coupons,jigoshop-ecommerce,400,Jigoshop eCommerce +publish_shop_coupons,webmaster-user-role,8000,Webmaster User Role +publish_shop_coupons,woocommerce,4000000,WooCommerce +publish_shop_coupons,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +publish_shop_discounts,easy-digital-downloads,60000,Easy Digital Downloads +publish_shop_emails,jigoshop,4000,Jigoshop +publish_shop_emails,jigoshop-ecommerce,400,Jigoshop eCommerce +publish_shop_orders,jigoshop,4000,Jigoshop +publish_shop_orders,jigoshop-ecommerce,400,Jigoshop eCommerce +publish_shop_orders,webmaster-user-role,8000,Webmaster User Role +publish_shop_orders,woocommerce,4000000,WooCommerce +publish_shop_payments,easy-digital-downloads,60000,Easy Digital Downloads +publish_shop_webhooks,woocommerce,4000000,WooCommerce +publish_shows,radio-station,1000,Radio Station +publish_sln_attendants,salon-booking-system,5000,Salon booking system +publish_sln_bookings,salon-booking-system,5000,Salon booking system +publish_sln_services,salon-booking-system,5000,Salon booking system +publish_snippets,wp-snippets,1000,WP Snippets +publish_sola_st_tickets,sola-support-tickets,400,Sola Support Tickets +publish_sp_calendars,sportspress,20000,SportsPress – Sports Club & League Manager +publish_sp_configs,sportspress,20000,SportsPress – Sports Club & League Manager +publish_sp_events,sportspress,20000,SportsPress – Sports Club & League Manager +publish_sp_lists,sportspress,20000,SportsPress – Sports Club & League Manager +publish_sp_players,sportspress,20000,SportsPress – Sports Club & League Manager +publish_sp_staffs,sportspress,20000,SportsPress – Sports Club & League Manager +publish_sp_tables,sportspress,20000,SportsPress – Sports Club & League Manager +publish_sp_teams,sportspress,20000,SportsPress – Sports Club & League Manager +publish_sports,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +publish_sprout_invoices,sprout-invoices,2000,Client Invoicing by Sprout Invoices – Easy Estimates and Invoices for WordPress +publish_stm_lms_posts,masterstudy-lms-learning-management-system,400,MasterStudy LMS – Free Learning Management System WordPress Plugin for Online Courses +publish_store_orders,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +publish_stores,wp-store-locator,50000,WP Store Locator +publish_sunshine_galleries,sunshine-photo-cart,2000,Sunshine Photo Cart +publish_sunshine_gallery,sunshine-photo-cart,2000,Sunshine Photo Cart +publish_sunshine_order,sunshine-photo-cart,2000,Sunshine Photo Cart +publish_sunshine_orders,sunshine-photo-cart,2000,Sunshine Photo Cart +publish_sunshine_product,sunshine-photo-cart,2000,Sunshine Photo Cart +publish_sunshine_products,sunshine-photo-cart,2000,Sunshine Photo Cart +publish_support_tickets,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +publish_tc_events,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +publish_tc_orders,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +publish_tc_tickets,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +publish_tc_tickets_instances,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +publish_teams,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +publish_tm-propertys,cherry-real-estate,600,Cherry Real Estate +publish_topics,bbpress,300000,bbPress +publish_total_slider_slides,total-slider,500,Total Slider +publish_trans_logs,essential-real-estate,3000,Essential Real Estate +publish_translations,simple-punctual-translation,200,Simple Punctual Translation +publish_tribe_events,the-events-calendar,700000,The Events Calendar +publish_tribe_organizers,the-events-calendar,700000,The Events Calendar +publish_tribe_venues,the-events-calendar,700000,The Events Calendar +publish_un_feedback,usernoise,7000,Usernoise modal feedback / contact form +publish_un_feedback_items,usernoise,7000,Usernoise modal feedback / contact form +publish_user_packages,essential-real-estate,3000,Essential Real Estate +publish_user_registrations,user-registration,7000,"User Registration – Custom Registration Form, Login and User Profile for WordPress" +publish_vacancies,job-board,300,Job Board by BestWebSoft +publish_venues,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +publish_wbcr-snippetss,insert-php,100000,PHP code snippets (Insert PHP) +publish_wctrl_contents,widgets-control,1000,Widgets Control +publish_wd_ads_adverts,ad-manager-wd,700,Ad Manager by WD – Advanced Ad Manager plugin +publish_wd_ads_schedules,ad-manager-wd,700,Ad Manager by WD – Advanced Ad Manager plugin +publish_wiki_pages,wordpress-wiki,400,WordPress Wiki +publish_wordlift_entities,wordlift,400,WordLift – AI powered SEO +publish_wpautoterms_pages,auto-terms-of-service-and-privacy-policy,100000,Auto Terms of Service and Privacy Policy (WP AutoTerms) +publish_wpcm_clubs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +publish_wpcm_matchs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +publish_wpcm_players,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +publish_wpcm_sponsors,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +publish_wpcm_staffs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +publish_wpdiscuz_forms,wpdiscuz,40000,Comments – wpDiscuz +publish_wpfc_sermons,sermon-manager-for-wordpress,9000,Sermon Manager +publish_wpi_discounts,invoicing,2000,Invoicing – Invoice & Payments Plugin +publish_wpi_invoices,invoicing,2000,Invoicing – Invoice & Payments Plugin +publish_wpi_items,invoicing,2000,Invoicing – Invoice & Payments Plugin +publish_wpi_quotes,invoicing,2000,Invoicing – Invoice & Payments Plugin +publish_wplc_quick_response,wp-live-chat-support,60000,WP Live Chat Support +publish_wpp_properties,wp-property,5000,WP-Property – WordPress Powered Real Estate and Property Management +publish_wppizzas,wppizza,2000,WPPizza +publish_wprm_reservations,wp-restaurant-manager,700,WP Restaurant Manager +publish_wpsdealss,deals-engine,200,Social Deals Engine +publish_wpsdealssaless,deals-engine,200,Social Deals Engine +publish_wpse_profiles,wp-smart-editor,900,WP Smart Editor +publish_wswebinars,wp-webinarsystem,2000,WP WebinarSystem +publish_ycd_countdowns,countdown-builder,1000,Countdown +push_crossword,crosswordsearch,300,crosswordsearch +read_acadp_field,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +read_acadp_listing,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +read_acadp_payment,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +read_achievements,achievements,300,Achievements for WordPress +read_aec_event,another-events-calendar,800,Another Events Calendar +read_aec_organizer,another-events-calendar,800,Another Events Calendar +read_aec_venue,another-events-calendar,800,Another Events Calendar +read_agent,essential-real-estate,3000,Essential Real Estate +read_ai1ec_event,all-in-one-event-calendar,100000,All-in-One Event Calendar +read_ai1ec_events,all-in-one-event-calendar,100000,All-in-One Event Calendar +read_aiovg_video,all-in-one-video-gallery,1000,All-in-One Video Gallery +read_all_touchpoints,ukuupeople-the-simple-crm,1000,CRM: Contact Management Simplified – UkuuPeople +read_all_ukuupeoples,ukuupeople-the-simple-crm,1000,CRM: Contact Management Simplified – UkuuPeople +read_application,apply-online,5000,Apply Online +read_archiv,archive,700,Archive +read_article,issuem,1000,IssueM +read_at_biz_dir,directorist,500,Directorist – Business Directory Plugin +read_atbdp_order,directorist,500,Directorist – Business Directory Plugin +read_awebooking,awebooking,6000,AweBooking – Hotel Booking System +read_birs_appointment,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +read_birs_client,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +read_birs_location,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +read_birs_payment,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +read_birs_service,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +read_birs_staff,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +read_blocks,gutenberg,500000,Gutenberg +read_book,novelist,800,Novelist +read_book-reviews,book-review-library,700,Book Review Library +read_box,boxzilla,20000,Boxzilla +read_calp_event,calpress-event-calendar,5000,CalPress Calendar +read_campaign,charitable,10000,Charitable – Donation Plugin +read_campaign,leyka,1000,Leyka +read_car_listing,wp-car-manager,3000,WP Car Manager +read_cctor_coupon,coupon-creator,10000,Coupon Creator +read_chronosly,chronosly-events-calendar,4000,Chronosly Events Calendar +read_classified_listing,classifieds-wp,800,Classifieds WP +read_client,upstream,1000,WordPress Project Management by UpStream +read_course,lifterlms,8000,LifterLMS +read_cta,cta,10000,WordPress Calls to Action +read_cupri_pay,pardakht-delkhah,1000,پلاگین پرداخت دلخواه +read_custom_css,custom-css-js,100000,Simple Custom CSS and JS +read_ditty_news_ticker,ditty-news-ticker,40000,Ditty News Ticker +read_ditty_news_tickers,ditty-news-ticker,40000,Ditty News Ticker +read_document_revisions,wp-document-revisions,4000,WP Document Revisions +read_documents,wp-document-revisions,4000,WP Document Revisions +read_donation,charitable,10000,Charitable – Donation Plugin +read_donation,leyka,1000,Leyka +read_dsn_note,admin-dashboard-site-notes,3000,Dashboard Site Notes +read_edr_course,educator,1000,Educator 2 +read_edr_lesson,educator,1000,Educator 2 +read_edr_membership,educator,1000,Educator 2 +read_email_template,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +read_email_templates,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +read_event,quick-event-manager,5000,Quick Event Manager +read_event_listing,wp-event-manager,1000,WP Event Manager +read_event_magic,kikfyre-events-calendar-tickets,200,"Events, Calendars & Tickets – Event Kikfyre" +read_everest_form,everest-forms,40000,Everest Forms – Easy Contact Form and Form Builder for WordPress +read_feed,wp-rss-aggregator,60000,WP RSS Aggregator +read_feed_source,wp-rss-aggregator,60000,WP RSS Aggregator +read_flexible_invoice,flexible-invoices,1000,Flexible Invoices for WordPress +read_flexible_invoices,flexible-invoices,1000,Flexible Invoices for WordPress +read_food_group,restaurantpress,3000,RestaurantPress +read_food_menu,restaurantpress,3000,RestaurantPress +read_form,formlift,800,FormLift for Infusionsoft Web Forms +read_form,pronamic-ideal,6000,Pronamic Pay +read_galleries,gallery-box,2000,Gallery Box +read_game,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +read_give_form,give,50000,Give – Donation Plugin and Fundraising Platform +read_give_payment,give,50000,Give – Donation Plugin and Fundraising Platform +read_glossary,glossary-by-codeat,1000,Glossary +read_hanaboard-post,hana-board,1000,Hana-Board 하나보드 워드프레스 게시판 +read_hf_membership_plan,xa-woocommerce-memberships,400,Memberships for WooCommerce +read_hf_user_membership,xa-woocommerce-memberships,400,Memberships for WooCommerce +read_hidden_forums,bbpress,300000,bbPress +read_hotel_location,awebooking,6000,AweBooking – Hotel Booking System +read_hotel_service,awebooking,6000,AweBooking – Hotel Booking System +read_ib_edu_membership,ibeducator,1000,Educator +read_ib_educator_course,ibeducator,1000,Educator +read_ib_educator_lesson,ibeducator,1000,Educator +read_ims_gallery,image-store,900,Image Store +read_inbound-form,cta,10000,WordPress Calls to Action +read_inbound-form,landing-pages,10000,WordPress Landing Pages +read_inbound-form,leads,7000,WordPress Leads +read_insertcode,insert-code,300,Insert Code +read_invoice,essential-real-estate,3000,Essential Real Estate +read_item,gamipress,2000,GamiPress +read_job_listing,wp-job-manager,100000,WP Job Manager +read_jscp_match,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +read_jscp_player,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +read_jscp_team,joomsport-sports-league-results-management,1000,"JoomSport – for Sports: Team & League, Football, Hockey & more" +read_klaviyo_shop_cart,klaviyo-for-woocommerce,1000,Klaviyo for WooCommerce +read_landing_page,landing-pages,10000,WordPress Landing Pages +read_language,sublanguage,1000,Sublanguage +read_lead,cta,10000,WordPress Calls to Action +read_lead,landing-pages,10000,WordPress Landing Pages +read_lead,leads,7000,WordPress Leads +read_legalpack_pages,legalpack,400,Legalpack +read_lesson,lifterlms,8000,LifterLMS +read_listing,auto-listings,400,Auto Listings +read_listing,wp-real-estate,400,WP Real Estate +read_listing,wpcasa,2000,WPCasa +read_membership,lifterlms,8000,LifterLMS +read_mp_menu_item,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +read_mprm_order,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +read_nc_reference,nelio-content,7000,Nelio Content – Social Media Marketing Automation +read_nemus-slider,nemus-slider,2000,Nemus Slider +read_niso_private_carousels_slider,niso-carousel,200,Niso Carousel +read_niso_private_carousels_slider,niso-carousel-slider,400,Niso Carousel Slider +read_niso_slider_carousels,niso-carousel,200,Niso Carousel +read_niso_slider_carousels,niso-carousel-slider,400,Niso Carousel Slider +read_opalestate_agents,opal-estate,1000,Opal Estate +read_opalestate_properties,opal-estate,1000,Opal Estate +read_opanda-item,opt-in-panda,3000,OnePress Opt-In Panda +read_opanda-item,social-locker,10000,OnePress Social Locker +read_orbis_company,orbis,200,Orbis +read_orbis_project,orbis,200,Orbis +read_others_attachments,wpfront-user-role-editor,60000,WPFront User Role Editor +read_others_locations,events-manager,100000,Events Manager +read_package,essential-real-estate,3000,Essential Real Estate +read_payment,pronamic-ideal,6000,Pronamic Pay +read_player,team-rosters,800,Team Rosters +read_playlists,radio-station,1000,Radio Station +read_plugin_filters,plugin-organizer,10000,Plugin Organizer +read_plugin_groups,plugin-organizer,10000,Plugin Organizer +read_portfolio,flash-toolkit,30000,Flash Toolkit +read_portfolio,suffice-toolkit,5000,Suffice Toolkit +read_portfolio,visual-portfolio,7000,Visual Portfolio +read_post,countdown-builder,1000,Countdown +read_post,popup-builder,100000,Popup Builder – Responsive WordPress Pop up – Subscription & Newsletter +read_pricing_rate,awebooking,6000,AweBooking – Hotel Booking System +read_private_acadp_fields,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +read_private_acadp_listings,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +read_private_acadp_payments,advanced-classifieds-and-directory-pro,4000,Advanced Classifieds & Directory Pro +read_private_achievement_progresses,achievements,300,Achievements for WordPress +read_private_achievements,achievements,300,Achievements for WordPress +read_private_ads,apply-online,5000,Apply Online +read_private_aec_events,another-events-calendar,800,Another Events Calendar +read_private_aec_organizers,another-events-calendar,800,Another Events Calendar +read_private_aec_venues,another-events-calendar,800,Another Events Calendar +read_private_affiliate_keywords,affiliate,700,Affiliate +read_private_agents,essential-real-estate,3000,Essential Real Estate +read_private_aggregator-records,the-events-calendar,700000,The Events Calendar +read_private_ai1ec_events,all-in-one-event-calendar,100000,All-in-One Event Calendar +read_private_aiovg_videos,all-in-one-video-gallery,1000,All-in-One Video Gallery +read_private_anb_animations,alert-notice-boxes,1000,Alert Notice Boxes +read_private_anb_animations_out,alert-notice-boxes,1000,Alert Notice Boxes +read_private_anb_designs,alert-notice-boxes,1000,Alert Notice Boxes +read_private_anb_locations,alert-notice-boxes,1000,Alert Notice Boxes +read_private_anbs,alert-notice-boxes,1000,Alert Notice Boxes +read_private_applications,apply-online,5000,Apply Online +read_private_archivs,archive,700,Archive +read_private_articles,issuem,1000,IssueM +read_private_at_biz_dirs,directorist,500,Directorist – Business Directory Plugin +read_private_atbdp_orders,directorist,500,Directorist – Business Directory Plugin +read_private_awebookings,awebooking,6000,AweBooking – Hotel Booking System +read_private_birs_appointments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +read_private_birs_clients,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +read_private_birs_locations,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +read_private_birs_payments,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +read_private_birs_services,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +read_private_birs_staffs,birchschedule,8000,Appointment Booking Calendar – BirchPress Scheduler +read_private_blocks,gutenberg,500000,Gutenberg +read_private_board_committees,nonprofit-board-management,400,Nonprofit Board Management +read_private_board_events,nonprofit-board-management,400,Nonprofit Board Management +read_private_books,novelist,800,Novelist +read_private_box,boxzilla,20000,Boxzilla +read_private_calp_events,calpress-event-calendar,5000,CalPress Calendar +read_private_campaigns,charitable,10000,Charitable – Donation Plugin +read_private_campaigns,leyka,1000,Leyka +read_private_car_listings,wp-car-manager,3000,WP Car Manager +read_private_cctor_coupons,coupon-creator,10000,Coupon Creator +read_private_chronoslys,chronosly-events-calendar,4000,Chronosly Events Calendar +read_private_classified_listings,classifieds-wp,800,Classifieds WP +read_private_clients,upstream,1000,WordPress Project Management by UpStream +read_private_courses,lifterlms,8000,LifterLMS +read_private_ctas,cta,10000,WordPress Calls to Action +read_private_cupri_pays,pardakht-delkhah,1000,پلاگین پرداخت دلخواه +read_private_ditty_news_tickers,ditty-news-ticker,40000,Ditty News Ticker +read_private_documents,wp-document-revisions,4000,WP Document Revisions +read_private_donations,charitable,10000,Charitable – Donation Plugin +read_private_donations,leyka,1000,Leyka +read_private_dsn_notes,admin-dashboard-site-notes,3000,Dashboard Site Notes +read_private_edr_courses,educator,1000,Educator 2 +read_private_edr_lessons,educator,1000,Educator 2 +read_private_edr_memberships,educator,1000,Educator 2 +read_private_email_templates,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +read_private_emails,mailer-dragon,300,Mailer Dragon – Email Marketing Plugin for WordPress +read_private_emd_agents,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +read_private_emd_canned_responses,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +read_private_emd_contacts,wp-easy-contact,600,Best Contact Management Software for WordPress +read_private_emd_employees,employee-directory,400,Staff Directory – Employee Directory for WordPress +read_private_emd_quotes,request-a-quote,1000,Request a Quote +read_private_emd_tickets,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +read_private_epa_albums,easy-photo-album,5000,Easy Photo Album +read_private_event,quick-event-manager,5000,Quick Event Manager +read_private_event_listings,wp-event-manager,1000,WP Event Manager +read_private_event_magics,kikfyre-events-calendar-tickets,200,"Events, Calendars & Tickets – Event Kikfyre" +read_private_events,event-organiser,40000,Event Organiser +read_private_events,events-maker,4000,Events Maker by dFactory +read_private_events,events-manager,100000,Events Manager +read_private_everest_forms,everest-forms,40000,Everest Forms – Easy Contact Form and Form Builder for WordPress +read_private_fa_items,featured-articles-lite,3000,FA Lite – WP responsive slider plugin +read_private_fbtabs,facebook-tab-manager,1000,Facebook Tab Manager +read_private_feed_sources,wp-rss-aggregator,60000,WP RSS Aggregator +read_private_feeds,wp-rss-aggregator,60000,WP RSS Aggregator +read_private_fep_announcements,front-end-pm,8000,Front End PM +read_private_fep_messages,front-end-pm,8000,Front End PM +read_private_flexible_invoices,flexible-invoices,1000,Flexible Invoices for WordPress +read_private_food_groups,restaurantpress,3000,RestaurantPress +read_private_food_menus,restaurantpress,3000,RestaurantPress +read_private_forms,pronamic-ideal,6000,Pronamic Pay +read_private_forums,bbpress,300000,bbPress +read_private_galleries,gallery-box,2000,Gallery Box +read_private_games,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +read_private_give_forms,give,50000,Give – Donation Plugin and Fundraising Platform +read_private_give_payments,give,50000,Give – Donation Plugin and Fundraising Platform +read_private_glossaries,glossary-by-codeat,1000,Glossary +read_private_hf_membership_plans,xa-woocommerce-memberships,400,Memberships for WooCommerce +read_private_hf_user_memberships,xa-woocommerce-memberships,400,Memberships for WooCommerce +read_private_hotel_locations,awebooking,6000,AweBooking – Hotel Booking System +read_private_hotel_services,awebooking,6000,AweBooking – Hotel Booking System +read_private_ib_edu_memberships,ibeducator,1000,Educator +read_private_ib_educator_courses,ibeducator,1000,Educator +read_private_ib_educator_lessons,ibeducator,1000,Educator +read_private_ims_gallery,image-store,900,Image Store +read_private_inbound-forms,cta,10000,WordPress Calls to Action +read_private_inbound-forms,landing-pages,10000,WordPress Landing Pages +read_private_inbound-forms,leads,7000,WordPress Leads +read_private_insertcodes,insert-code,300,Insert Code +read_private_invoices,essential-real-estate,3000,Essential Real Estate +read_private_items,gamipress,2000,GamiPress +read_private_job_listings,wp-job-manager,100000,WP Job Manager +read_private_klaviyo_shop_carts,klaviyo-for-woocommerce,1000,Klaviyo for WooCommerce +read_private_landing_pages,landing-pages,10000,WordPress Landing Pages +read_private_languages,sublanguage,1000,Sublanguage +read_private_leads,cta,10000,WordPress Calls to Action +read_private_leads,landing-pages,10000,WordPress Landing Pages +read_private_leads,leads,7000,WordPress Leads +read_private_legalpack_pages,legalpack,400,Legalpack +read_private_lessons,lifterlms,8000,LifterLMS +read_private_listings,auto-listings,400,Auto Listings +read_private_listings,wp-real-estate,400,WP Real Estate +read_private_listings,wpcasa,2000,WPCasa +read_private_locations,events-manager,100000,Events Manager +read_private_meals,restaurant-manager,800,Restaurant Manager +read_private_memberships,lifterlms,8000,LifterLMS +read_private_mp_menu_items,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +read_private_mprm_orders,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +read_private_nc_references,nelio-content,7000,Nelio Content – Social Media Marketing Automation +read_private_nemus-sliders,nemus-slider,2000,Nemus Slider +read_private_news,news-manager,3000,News Manager +read_private_newsletters,alo-easymail,10000,ALO EasyMail Newsletter +read_private_opalestate_agentss,opal-estate,1000,Opal Estate +read_private_opalestate_propertiess,opal-estate,1000,Opal Estate +read_private_opanda-items,opt-in-panda,3000,OnePress Opt-In Panda +read_private_opanda-items,social-locker,10000,OnePress Social Locker +read_private_orbis_companies,orbis,200,Orbis +read_private_orbis_projects,orbis,200,Orbis +read_private_packages,essential-real-estate,3000,Essential Real Estate +read_private_payments,pronamic-ideal,6000,Pronamic Pay +read_private_players,team-rosters,800,Team Rosters +read_private_plugin_filters,plugin-organizer,10000,Plugin Organizer +read_private_plugin_groups,plugin-organizer,10000,Plugin Organizer +read_private_portfolio_projects,custom-content-portfolio,1000,Custom Content Portfolio +read_private_portfolios,flash-toolkit,30000,Flash Toolkit +read_private_portfolios,suffice-toolkit,5000,Suffice Toolkit +read_private_portfolios,visual-portfolio,7000,Visual Portfolio +read_private_pricing_rates,awebooking,6000,AweBooking – Hotel Booking System +read_private_product_sets,datafeedr-product-sets,1000,Datafeedr Product Sets +read_private_products,design-approval-system,500,Design Approval System +read_private_products,easy-digital-downloads,60000,Easy Digital Downloads +read_private_products,ecommerce-product-catalog,10000,eCommerce Product Catalog Plugin for WordPress +read_private_products,gnucommerce,1000,GNUCommerce +read_private_products,jigoshop,4000,Jigoshop +read_private_products,jigoshop-ecommerce,400,Jigoshop eCommerce +read_private_products,post-type-x,1000,Product Catalog X +read_private_products,products,300,WP Products +read_private_products,webmaster-user-role,8000,Webmaster User Role +read_private_products,woocommerce,4000000,WooCommerce +read_private_products,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +read_private_profile_cct,profile-custom-content-type,200,Profile CCT +read_private_projects,upstream,1000,WordPress Project Management by UpStream +read_private_propertys,essential-real-estate,3000,Essential Real Estate +read_private_psp_project,project-panorama-lite,1000,Project Panorama +read_private_psp_projects,project-panorama-lite,1000,Project Panorama +read_private_questions,lifterlms,8000,LifterLMS +read_private_quizzes,lifterlms,8000,LifterLMS +read_private_quotes,mg-quotes,300,mg Quotes +read_private_redirects,wp-redirects,700,WP Redirects +read_private_rem_properties,real-estate-manager,1000,Real Estate Manager – Property Listing and Agent Management +read_private_replies,bbpress,300000,bbPress +read_private_reservations,restaurant-manager,800,Restaurant Manager +read_private_resume_positions,wp-resume,700,WP Resume +read_private_room_reservations,wp-hotelier,1000,Easy WP Hotelier +read_private_room_types,awebooking,6000,AweBooking – Hotel Booking System +read_private_rooms,wp-hotelier,1000,Easy WP Hotelier +read_private_rsvpemails,rsvpmaker,1000,RSVPMaker +read_private_rsvpmakers,rsvpmaker,1000,RSVPMaker +read_private_schedules,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +read_private_sgpb_popups,popup-builder,100000,Popup Builder – Responsive WordPress Pop up – Subscription & Newsletter +read_private_shifts,employee-scheduler,400,Shiftee Basic – Employee and Staff Scheduling +read_private_shop_coupons,jigoshop,4000,Jigoshop +read_private_shop_coupons,jigoshop-ecommerce,400,Jigoshop eCommerce +read_private_shop_coupons,webmaster-user-role,8000,Webmaster User Role +read_private_shop_coupons,woocommerce,4000000,WooCommerce +read_private_shop_discounts,easy-digital-downloads,60000,Easy Digital Downloads +read_private_shop_emails,jigoshop,4000,Jigoshop +read_private_shop_emails,jigoshop-ecommerce,400,Jigoshop eCommerce +read_private_shop_orders,jigoshop,4000,Jigoshop +read_private_shop_orders,jigoshop-ecommerce,400,Jigoshop eCommerce +read_private_shop_orders,webmaster-user-role,8000,Webmaster User Role +read_private_shop_orders,woocommerce,4000000,WooCommerce +read_private_shop_payments,easy-digital-downloads,60000,Easy Digital Downloads +read_private_shop_webhooks,woocommerce,4000000,WooCommerce +read_private_sln_attendants,salon-booking-system,5000,Salon booking system +read_private_sln_bookings,salon-booking-system,5000,Salon booking system +read_private_sln_services,salon-booking-system,5000,Salon booking system +read_private_snippets,wp-snippets,1000,WP Snippets +read_private_snitchs,snitch,1000,Snitch +read_private_sola_st_tickets,sola-support-tickets,400,Sola Support Tickets +read_private_sp_calendars,sportspress,20000,SportsPress – Sports Club & League Manager +read_private_sp_configs,sportspress,20000,SportsPress – Sports Club & League Manager +read_private_sp_events,sportspress,20000,SportsPress – Sports Club & League Manager +read_private_sp_lists,sportspress,20000,SportsPress – Sports Club & League Manager +read_private_sp_players,sportspress,20000,SportsPress – Sports Club & League Manager +read_private_sp_staffs,sportspress,20000,SportsPress – Sports Club & League Manager +read_private_sp_tables,sportspress,20000,SportsPress – Sports Club & League Manager +read_private_sp_teams,sportspress,20000,SportsPress – Sports Club & League Manager +read_private_sports,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +read_private_stm_lms_posts,masterstudy-lms-learning-management-system,400,MasterStudy LMS – Free Learning Management System WordPress Plugin for Online Courses +read_private_store_orders,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +read_private_stores,wp-store-locator,50000,WP Store Locator +read_private_sunshine_galleries,sunshine-photo-cart,2000,Sunshine Photo Cart +read_private_sunshine_orders,sunshine-photo-cart,2000,Sunshine Photo Cart +read_private_sunshine_products,sunshine-photo-cart,2000,Sunshine Photo Cart +read_private_support_tickets,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +read_private_tc_events,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +read_private_tc_orders,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +read_private_tc_tickets,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +read_private_tc_tickets_instances,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +read_private_teams,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +read_private_tm-propertys,cherry-real-estate,600,Cherry Real Estate +read_private_topics,bbpress,300000,bbPress +read_private_total_slider_slides,total-slider,500,Total Slider +read_private_trans_logs,essential-real-estate,3000,Essential Real Estate +read_private_translations,simple-punctual-translation,200,Simple Punctual Translation +read_private_tribe_events,the-events-calendar,700000,The Events Calendar +read_private_tribe_organizers,the-events-calendar,700000,The Events Calendar +read_private_tribe_venues,the-events-calendar,700000,The Events Calendar +read_private_user_packages,essential-real-estate,3000,Essential Real Estate +read_private_user_registrations,user-registration,7000,"User Registration – Custom Registration Form, Login and User Profile for WordPress" +read_private_vacancies,job-board,300,Job Board by BestWebSoft +read_private_venues,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +read_private_wbcr-snippetss,insert-php,100000,PHP code snippets (Insert PHP) +read_private_wctrl_contents,widgets-control,1000,Widgets Control +read_private_wordlift_entities,wordlift,400,WordLift – AI powered SEO +read_private_wpautoterms_pages,auto-terms-of-service-and-privacy-policy,100000,Auto Terms of Service and Privacy Policy (WP AutoTerms) +read_private_wpcm_clubs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +read_private_wpcm_matchs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +read_private_wpcm_players,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +read_private_wpcm_sponsors,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +read_private_wpcm_staffs,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +read_private_wpfc_sermons,sermon-manager-for-wordpress,9000,Sermon Manager +read_private_wpi_discounts,invoicing,2000,Invoicing – Invoice & Payments Plugin +read_private_wpi_invoices,invoicing,2000,Invoicing – Invoice & Payments Plugin +read_private_wpi_items,invoicing,2000,Invoicing – Invoice & Payments Plugin +read_private_wpi_quotes,invoicing,2000,Invoicing – Invoice & Payments Plugin +read_private_wplc_quick_response,wp-live-chat-support,60000,WP Live Chat Support +read_private_wppizzas,wppizza,2000,WPPizza +read_private_wprm_reservations,wp-restaurant-manager,700,WP Restaurant Manager +read_private_wpsdealss,deals-engine,200,Social Deals Engine +read_private_wpsdealssaless,deals-engine,200,Social Deals Engine +read_private_wpse_profiles,wp-smart-editor,900,WP Smart Editor +read_private_wswebinars,wp-webinarsystem,2000,WP WebinarSystem +read_private_ycd_countdowns,countdown-builder,1000,Countdown +read_product,dc-woocommerce-multi-vendor,10000,WC Marketplace +read_product,design-approval-system,500,Design Approval System +read_product,easy-digital-downloads,60000,Easy Digital Downloads +read_product,gnucommerce,1000,GNUCommerce +read_product,jigoshop,4000,Jigoshop +read_product,jigoshop-ecommerce,400,Jigoshop eCommerce +read_product,webmaster-user-role,8000,Webmaster User Role +read_product,woocommerce,4000000,WooCommerce +read_product,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +read_product,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +read_product_set,datafeedr-product-sets,1000,Datafeedr Product Sets +read_project,upstream,1000,WordPress Project Management by UpStream +read_property,essential-real-estate,3000,Essential Real Estate +read_psp_project,project-panorama-lite,1000,Project Panorama +read_question,lifterlms,8000,LifterLMS +read_quiz,lifterlms,8000,LifterLMS +read_rem_property,real-estate-manager,1000,Real Estate Manager – Property Listing and Agent Management +read_resume_positions,wp-resume,700,WP Resume +read_room,wp-hotelier,1000,Easy WP Hotelier +read_room_reservation,wp-hotelier,1000,Easy WP Hotelier +read_room_type,awebooking,6000,AweBooking – Hotel Booking System +read_rsvpemail,rsvpmaker,1000,RSVPMaker +read_schedule,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +read_shift,employee-scheduler,400,Shiftee Basic – Employee and Staff Scheduling +read_shop_coupon,dc-woocommerce-multi-vendor,10000,WC Marketplace +read_shop_coupon,jigoshop,4000,Jigoshop +read_shop_coupon,jigoshop-ecommerce,400,Jigoshop eCommerce +read_shop_coupon,webmaster-user-role,8000,Webmaster User Role +read_shop_coupon,woocommerce,4000000,WooCommerce +read_shop_coupon,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +read_shop_coupons,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +read_shop_discount,easy-digital-downloads,60000,Easy Digital Downloads +read_shop_email,jigoshop,4000,Jigoshop +read_shop_email,jigoshop-ecommerce,400,Jigoshop eCommerce +read_shop_order,jigoshop,4000,Jigoshop +read_shop_order,jigoshop-ecommerce,400,Jigoshop eCommerce +read_shop_order,webmaster-user-role,8000,Webmaster User Role +read_shop_order,woocommerce,4000000,WooCommerce +read_shop_payment,easy-digital-downloads,60000,Easy Digital Downloads +read_shop_webhook,woocommerce,4000000,WooCommerce +read_shows,radio-station,1000,Radio Station +read_sln_attendant,salon-booking-system,5000,Salon booking system +read_sln_booking,salon-booking-system,5000,Salon booking system +read_sln_service,salon-booking-system,5000,Salon booking system +read_snitchs,snitch,1000,Snitch +read_sola_st_ticket,sola-support-tickets,400,Sola Support Tickets +read_sp_calendar,sportspress,20000,SportsPress – Sports Club & League Manager +read_sp_config,sportspress,20000,SportsPress – Sports Club & League Manager +read_sp_event,sportspress,20000,SportsPress – Sports Club & League Manager +read_sp_list,sportspress,20000,SportsPress – Sports Club & League Manager +read_sp_player,sportspress,20000,SportsPress – Sports Club & League Manager +read_sp_staff,sportspress,20000,SportsPress – Sports Club & League Manager +read_sp_table,sportspress,20000,SportsPress – Sports Club & League Manager +read_sp_team,sportspress,20000,SportsPress – Sports Club & League Manager +read_sport,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +read_stm_lms_posts,masterstudy-lms-learning-management-system,400,MasterStudy LMS – Free Learning Management System WordPress Plugin for Online Courses +read_store,wp-store-locator,50000,WP Store Locator +read_store_order,wordpress-ecommerce,3000,MarketPress – WordPress eCommerce +read_sunshine_gallery,sunshine-photo-cart,2000,Sunshine Photo Cart +read_sunshine_order,sunshine-photo-cart,2000,Sunshine Photo Cart +read_sunshine_product,sunshine-photo-cart,2000,Sunshine Photo Cart +read_support_ticket,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +read_support_tickets,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +read_tc_event,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +read_tc_order,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +read_tc_ticket,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +read_tc_tickets_instance,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +read_team,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +read_tm-property_feature,cherry-real-estate,600,Cherry Real Estate +read_tm-property_tag,cherry-real-estate,600,Cherry Real Estate +read_tm-property_type,cherry-real-estate,600,Cherry Real Estate +read_trans_log,essential-real-estate,3000,Essential Real Estate +read_translation,simple-punctual-translation,200,Simple Punctual Translation +read_ubn_author_notes,private-content,9000,Private Content +read_ubn_contributor_notes,private-content,9000,Private Content +read_ubn_editor_notes,private-content,9000,Private Content +read_ubn_subscriber_notes,private-content,9000,Private Content +read_user_package,essential-real-estate,3000,Essential Real Estate +read_user_registration,user-registration,7000,"User Registration – Custom Registration Form, Login and User Profile for WordPress" +read_vacancy,job-board,300,Job Board by BestWebSoft +read_venue,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +read_wbcr-snippets,insert-php,100000,PHP code snippets (Insert PHP) +read_wordlift_entity,wordlift,400,WordLift – AI powered SEO +read_wp2syslog,wp2syslog,200,wp2syslog +read_wpautoterms_pages,auto-terms-of-service-and-privacy-policy,100000,Auto Terms of Service and Privacy Policy (WP AutoTerms) +read_wpcm_club,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +read_wpcm_match,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +read_wpcm_player,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +read_wpcm_sponsor,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +read_wpcm_staff,wp-club-manager,1000,WP Club Manager – WordPress Sports Club Plugin +read_wpdiscuz_form,wpdiscuz,40000,Comments – wpDiscuz +read_wpdiscuz_forms,wpdiscuz,40000,Comments – wpDiscuz +read_wpfc_sermon,sermon-manager-for-wordpress,9000,Sermon Manager +read_wpi_discount,invoicing,2000,Invoicing – Invoice & Payments Plugin +read_wpi_invoice,invoicing,2000,Invoicing – Invoice & Payments Plugin +read_wpi_item,invoicing,2000,Invoicing – Invoice & Payments Plugin +read_wpi_quote,invoicing,2000,Invoicing – Invoice & Payments Plugin +read_wplc_quick_response,wp-live-chat-support,60000,WP Live Chat Support +read_wppizza,wppizza,2000,WPPizza +read_wprm_reservation,wp-restaurant-manager,700,WP Restaurant Manager +read_wpsdeals,deals-engine,200,Social Deals Engine +read_wpsdealssales,deals-engine,200,Social Deals Engine +read_wpse_profile,wp-smart-editor,900,WP Smart Editor +read_wswebinar,wp-webinarsystem,2000,WP WebinarSystem +reply_ticket,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +reply_ticket,catchers-helpdesk,200,Catchers Helpdesk and Ticket system for Support +reset_own_yop_polls_stats,yop-poll,20000,YOP Poll +reset_yop_polls_stats,yop-poll,20000,YOP Poll +restrict_content,members,100000,Members +rggcl_manage_galleries,responsive-grid-gallery-with-custom-links,2000,Responsive Grid Gallery with Custom Links +rggcl_manage_items,responsive-grid-gallery-with-custom-links,2000,Responsive Grid Gallery with Custom Links +role_prcheater,wp-postratings-cheater,2000,WP-PostRatings Cheater +rsvp_board_events,nonprofit-board-management,400,Nonprofit Board Management +run_adminer,ari-adminer,20000,ARI Adminer – WordPress Database Manager +s,yop-poll,20000,YOP Poll +sar_fsmtp_options,sar-friendly-smtp,2000,SAR Friendly SMTP +save_ticket_cap,tickera-event-ticketing-system,6000,Tickera – WordPress Event Ticketing +scfp_edit_settings,wcp-contact-form,9000,WCP Contact Form +scfp_menu,wcp-contact-form,9000,WCP Contact Form +scfp_view_inbox,wcp-contact-form,9000,WCP Contact Form +sed_edit_less,site-editor,300,Site Editor – WordPress Site Builder – Theme Builder and Page Builder +sed_manage_settings,site-editor,300,Site Editor – WordPress Site Builder – Theme Builder and Page Builder +see_all_menus,slash-admin,1000,Slash Admin +send_funds_to_user,wallets,600,Bitcoin and Altcoin Wallets +serve_as_volunteer,wired-impact-volunteer-management,1000,Wired Impact Volunteer Management +serve_on_board,nonprofit-board-management,400,Nonprofit Board Management +set_author_emd_tickets,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +set_wordpoints_points,wordpoints,700,WordPoints +setka_editor_use_editor,setka-editor,1000,Page builder for Posts – Setka Editor +settings_tickets,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +settings_tickets,catchers-helpdesk,200,Catchers Helpdesk and Ticket system for Support +sgpb_manage_options,popup-builder,100000,Popup Builder – Responsive WordPress Pop up – Subscription & Newsletter +shopp_capture,shopp,3000,Shopp +shopp_categories,shopp,3000,Shopp +shopp_customers,shopp,3000,Shopp +shopp_delete_customers,shopp,3000,Shopp +shopp_delete_orders,shopp,3000,Shopp +shopp_export_customers,shopp,3000,Shopp +shopp_export_orders,shopp,3000,Shopp +shopp_financials,shopp,3000,Shopp +shopp_memberships,shopp,3000,Shopp +shopp_menu,shopp,3000,Shopp +shopp_orders,shopp,3000,Shopp +shopp_products,shopp,3000,Shopp +shopp_promotions,shopp,3000,Shopp +shopp_refund,shopp,3000,Shopp +shopp_settings,shopp,3000,Shopp +shopp_settings_checkout,shopp,3000,Shopp +shopp_settings_payments,shopp,3000,Shopp +shopp_settings_presentation,shopp,3000,Shopp +shopp_settings_shipping,shopp,3000,Shopp +shopp_settings_system,shopp,3000,Shopp +shopp_settings_taxes,shopp,3000,Shopp +shopp_settings_update,shopp,3000,Shopp +shopp_void,shopp,3000,Shopp +si_delete_profile,simple-intranet-directory,600,Simple Intranet Directory +si_delete_profiles,simple-intranet-directory,600,Simple Intranet Directory +si_edit_outofoffice,simple-intranet-directory,600,Simple Intranet Directory +si_edit_profile,simple-intranet-directory,600,Simple Intranet Directory +si_edit_profiles,simple-intranet-directory,600,Simple Intranet Directory +si_publish_profile,simple-intranet-directory,600,Simple Intranet Directory +si_read_profile,simple-intranet-directory,600,Simple Intranet Directory +simple_tags,simple-tags,100000,Simple Tags +site_editor_manage,site-editor,300,Site Editor – WordPress Site Builder – Theme Builder and Page Builder +slideshow-jquery-image-gallery-add-slideshows,slideshow-jquery-image-gallery,100000,Slideshow +slideshow-jquery-image-gallery-delete-slideshows,slideshow-jquery-image-gallery,100000,Slideshow +slideshow-jquery-image-gallery-edit-slideshows,slideshow-jquery-image-gallery,100000,Slideshow +slideshow_about,slideshow-gallery,20000,Slideshow Gallery +slideshow_galleries,slideshow-gallery,20000,Slideshow Gallery +slideshow_settings,slideshow-gallery,20000,Slideshow Gallery +slideshow_slides,slideshow-gallery,20000,Slideshow Gallery +slideshow_submitserial,slideshow-gallery,20000,Slideshow Gallery +slideshow_welcome,slideshow-gallery,20000,Slideshow Gallery +smartslider,smart-slider-3,300000,Smart Slider 3 +smartslider_config,smart-slider-3,300000,Smart Slider 3 +smartslider_delete,smart-slider-3,300000,Smart Slider 3 +smartslider_edit,smart-slider-3,300000,Smart Slider 3 +snowball_delete_posts,snowball,500,Snowball +snowball_edit_others_posts,snowball,500,Snowball +snowball_edit_posts,snowball,500,Snowball +snowball_publish_posts,snowball,500,Snowball +snowball_read_private_posts,snowball,500,Snowball +sp_cdm,sp-client-document-manager,3000,SP Project & Document Manager +sp_cdm_categories,sp-client-document-manager,3000,SP Project & Document Manager +sp_cdm_forms,sp-client-document-manager,3000,SP Project & Document Manager +sp_cdm_groups,sp-client-document-manager,3000,SP Project & Document Manager +sp_cdm_help,sp-client-document-manager,3000,SP Project & Document Manager +sp_cdm_link,sp-client-document-manager,3000,SP Project & Document Manager +sp_cdm_local_import,sp-client-document-manager,3000,SP Project & Document Manager +sp_cdm_media,sp-client-document-manager,3000,SP Project & Document Manager +sp_cdm_projects,sp-client-document-manager,3000,SP Project & Document Manager +sp_cdm_settings,sp-client-document-manager,3000,SP Project & Document Manager +sp_cdm_show_folders_as_nav,sp-client-document-manager,3000,SP Project & Document Manager +sp_cdm_top_menu,sp-client-document-manager,3000,SP Project & Document Manager +sp_cdm_uploader,sp-client-document-manager,3000,SP Project & Document Manager +sp_cdm_user_logs,sp-client-document-manager,3000,SP Project & Document Manager +sp_cdm_vendors,sp-client-document-manager,3000,SP Project & Document Manager +spectate,bbpress,300000,bbPress +srm_manage_redirects,safe-redirect-manager,40000,Safe Redirect Manager +status_change_assigned,publishpress,1000,PublishPress – Professional publishing tools for WordPress +status_change_draft,publishpress,1000,PublishPress – Professional publishing tools for WordPress +status_change_future,publishpress,1000,PublishPress – Professional publishing tools for WordPress +status_change_in_progress,publishpress,1000,PublishPress – Professional publishing tools for WordPress +status_change_pending,publishpress,1000,PublishPress – Professional publishing tools for WordPress +status_change_pitch,publishpress,1000,PublishPress – Professional publishing tools for WordPress +status_change_private,publishpress,1000,PublishPress – Professional publishing tools for WordPress +status_change_publish,publishpress,1000,PublishPress – Professional publishing tools for WordPress +strong_testimonials_about,strong-testimonials,60000,Strong Testimonials +strong_testimonials_fields,strong-testimonials,60000,Strong Testimonials +strong_testimonials_options,strong-testimonials,60000,Strong Testimonials +strong_testimonials_views,strong-testimonials,60000,Strong Testimonials +sunshine_manage_options,sunshine-photo-cart,2000,Sunshine Photo Cart +swifty_change_lock,swifty-content-creator,1000,Swifty Content Creator +swifty_change_lock,swifty-page-manager,5000,Swifty Page Manager +swifty_change_lock,swifty-site,1000,SwiftySite +swifty_edit_locked,swifty-content-creator,1000,Swifty Content Creator +swifty_edit_locked,swifty-page-manager,5000,Swifty Page Manager +swifty_edit_locked,swifty-site,1000,SwiftySite +switch_ai1ec_themes,all-in-one-event-calendar,100000,All-in-One Event Calendar +tablepress_access_about_screen,tablepress,700000,TablePress +tablepress_access_about_screen,webmaster-user-role,8000,Webmaster User Role +tablepress_access_options_screen,tablepress,700000,TablePress +tablepress_access_options_screen,webmaster-user-role,8000,Webmaster User Role +tablepress_add_tables,tablepress,700000,TablePress +tablepress_add_tables,webmaster-user-role,8000,Webmaster User Role +tablepress_copy_tables,tablepress,700000,TablePress +tablepress_delete_tables,tablepress,700000,TablePress +tablepress_edit_options,tablepress,700000,TablePress +tablepress_edit_tables,tablepress,700000,TablePress +tablepress_edit_tables,webmaster-user-role,8000,Webmaster User Role +tablepress_export_tables,tablepress,700000,TablePress +tablepress_export_tables,webmaster-user-role,8000,Webmaster User Role +tablepress_import_tables,tablepress,700000,TablePress +tablepress_import_tables,webmaster-user-role,8000,Webmaster User Role +tablepress_import_tables_wptr,tablepress,700000,TablePress +tablepress_list_tables,tablepress,700000,TablePress +tablepress_list_tables,webmaster-user-role,8000,Webmaster User Role +task_assigned_to_field,upstream,1000,WordPress Project Management by UpStream +task_end_date_field,upstream,1000,WordPress Project Management by UpStream +task_milestone_field,upstream,1000,WordPress Project Management by UpStream +task_notes_field,upstream,1000,WordPress Project Management by UpStream +task_progress_field,upstream,1000,WordPress Project Management by UpStream +task_start_date_field,upstream,1000,WordPress Project Management by UpStream +task_status_field,upstream,1000,WordPress Project Management by UpStream +task_title_field,upstream,1000,WordPress Project Management by UpStream +tcp_checkout_editor,thecartpress,1000,TheCartPress eCommerce Shopping Cart +tcp_delete_product,thecartpress,1000,TheCartPress eCommerce Shopping Cart +tcp_downloadable_products,thecartpress,1000,TheCartPress eCommerce Shopping Cart +tcp_edit_address,thecartpress,1000,TheCartPress eCommerce Shopping Cart +tcp_edit_addresses,thecartpress,1000,TheCartPress eCommerce Shopping Cart +tcp_edit_orders,thecartpress,1000,TheCartPress eCommerce Shopping Cart +tcp_edit_others_products,thecartpress,1000,TheCartPress eCommerce Shopping Cart +tcp_edit_plugins,thecartpress,1000,TheCartPress eCommerce Shopping Cart +tcp_edit_product,thecartpress,1000,TheCartPress eCommerce Shopping Cart +tcp_edit_products,thecartpress,1000,TheCartPress eCommerce Shopping Cart +tcp_edit_settings,thecartpress,1000,TheCartPress eCommerce Shopping Cart +tcp_edit_taxes,thecartpress,1000,TheCartPress eCommerce Shopping Cart +tcp_edit_wish_list,thecartpress,1000,TheCartPress eCommerce Shopping Cart +tcp_publish_products,thecartpress,1000,TheCartPress eCommerce Shopping Cart +tcp_read_orders,thecartpress,1000,TheCartPress eCommerce Shopping Cart +tcp_read_product,thecartpress,1000,TheCartPress eCommerce Shopping Cart +tcp_shortcode_generator,thecartpress,1000,TheCartPress eCommerce Shopping Cart +tcp_update_price,thecartpress,1000,TheCartPress eCommerce Shopping Cart +tcp_update_stock,thecartpress,1000,TheCartPress eCommerce Shopping Cart +tcp_users_roles,thecartpress,1000,TheCartPress eCommerce Shopping Cart +throttle,bbpress,300000,bbPress +ticket_delete_channels,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +ticket_delete_departments,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +ticket_delete_priorities,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +ticket_delete_products,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +ticket_delete_tags,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +ticket_edit_channels,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +ticket_edit_departments,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +ticket_edit_priorities,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +ticket_edit_products,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +ticket_edit_tags,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +ticket_manage_channels,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +ticket_manage_departments,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +ticket_manage_priorities,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +ticket_manage_privacy,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +ticket_manage_products,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +ticket_manage_tags,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +ticket_taxonomy,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +total_slider_manage_slides,total-slider,500,Total Slider +track_cforms,cforms2,10000,cformsII +trackserver_admin,trackserver,500,Trackserver +trackserver_publish,trackserver,500,Trackserver +unenroll,lifterlms,8000,LifterLMS +update all intel visitors,intelligence,600,Intelligence +update_plugin,wp2appir,300,wp2appir +update_results,leaguemanager,2000,LeagueManager +update_wordpoints_extensions,wordpoints,700,WordPoints +upload_event_images,events-manager,100000,Events Manager +upload_lazyest_file,lazyest-gallery,1000,Lazyest Gallery +upload_media,classifieds-wp,800,Classifieds WP +upstream_comment_images,upstream,1000,WordPress Project Management by UpStream +ure_create_capabilities,user-role-editor,500000,User Role Editor +ure_create_roles,user-role-editor,500000,User Role Editor +ure_delete_capabilities,user-role-editor,500000,User Role Editor +ure_delete_roles,user-role-editor,500000,User Role Editor +ure_edit_roles,user-role-editor,500000,User Role Editor +ure_manage_options,user-role-editor,500000,User Role Editor +ure_reset_roles,user-role-editor,500000,User Role Editor +use-wp-users-exporter,wp-users-exporter,2000,WP Users Exporter +use_copyscape,copyscape-premium,1000,Copyscape Premium +use_openid_provider,openid,3000,OpenID +use_support,ucare-support-system,3000,uCare – Support Ticket System & HelpDesk +use_teachpress,teachpress,1000,teachPress +use_teachpress_courses,teachpress,1000,teachPress +use_trackserver,trackserver,500,Trackserver +use_wp_admin_microblog,wp-admin-microblog,200,WP Admin Microblog +use_wp_admin_microblog_bp,wp-admin-microblog,200,WP Admin Microblog +use_wp_admin_microblog_sticky,wp-admin-microblog,200,WP Admin Microblog +user_meta_admin,user-meta,3000,User Meta – User Profile Builder and User management plugin +vc_access_rules_post_types/enhancedcategory,enhanced-category-pages,5000,Enhanced Category Pages +view all intel emailclicks,intelligence,600,Intelligence +view all intel phonecalls,intelligence,600,Intelligence +view all intel reports,intelligence,600,Intelligence +view all intel submissions,intelligence,600,Intelligence +view all intel visitors,intelligence,600,Intelligence +view own intel reports,intelligence,600,Intelligence +view-customer-area-menu,customer-area,10000,WP Customer Area +view_admin_dashboard,weblibrarian,700,WebLibrarian +view_admin_dashboard,wsdesk,1000,WSDesk – WordPress HelpDesk & Support Ticket System +view_admin_dashboard,apply-online,5000,Apply Online +view_all_aryo_activity_log,aryo-activity-log,90000,Activity Log +view_all_tickets,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +view_board_content,nonprofit-board-management,400,Nonprofit Board Management +view_campus_directory_dashboard,campus-directory,200,Faculty Staff and Student Directory Plugin – Campus Directory +view_charitable_sensitive_data,charitable,10000,Charitable – Donation Plugin +view_cimy_extra_fields,cimy-user-extra-fields,10000,Cimy User Extra Fields +view_contact_manager,contact-manager,500,Contact Manager +view_developer_content,developer-mode,1000,Developer Mode +view_developer_menu_items,developer-mode,1000,Developer Mode +view_developer_plugins,developer-mode,1000,Developer Mode +view_directory,pta-member-directory,1000,PTA Member Directory and Contact Form +view_empd_com_dashboard,employee-directory,400,Staff Directory – Employee Directory for WordPress +view_employee_spotlight_dashboard,employee-spotlight,1000,Team Members Staff Showcase Plugin – Employee Spotlight +view_errors,timber,400,Timber +view_give_form_stats,give,50000,Give – Donation Plugin and Fundraising Platform +view_give_payment_stats,give,50000,Give – Donation Plugin and Fundraising Platform +view_give_payments,give,50000,Give – Donation Plugin and Fundraising Platform +view_give_reports,give,50000,Give – Donation Plugin and Fundraising Platform +view_give_sensitive_data,give,50000,Give – Donation Plugin and Fundraising Platform +view_h5p_results,h5p,10000,Interactive Content – H5P +view_jigoshop_reports,jigoshop,4000,Jigoshop +view_jigoshop_reports,jigoshop-ecommerce,400,Jigoshop eCommerce +view_leagues,leaguemanager,2000,LeagueManager +view_lifterlms_reports,lifterlms,8000,LifterLMS +view_mp_menu_item_stats,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +view_mprm_order_stats,mp-restaurant-menu,6000,Restaurant Menu by MotoPress +view_mstw_menus,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +view_mstw_menus,team-rosters,800,Team Rosters +view_mstw_ss_menus,mstw-schedules-scoreboards,500,MSTW Schedules & Scoreboards +view_mstw_tr_menus,team-rosters,800,Team Rosters +view_opalestate_agents_stats,opal-estate,1000,Opal Estate +view_opalestate_properties_stats,opal-estate,1000,Opal Estate +view_opalestate_reports,opal-estate,1000,Opal Estate +view_opalestate_sensitive_data,opal-estate,1000,Opal Estate +view_others_lifterlms_reports,lifterlms,8000,LifterLMS +view_others_posts,wp-admin-hide-others-posts,300,WP Admin Hide Other's Posts +view_own_yop_polls_logs,yop-poll,20000,YOP Poll +view_own_yop_polls_results,yop-poll,20000,YOP Poll +view_pdf,projectmanager,400,ProjectManager +view_private_ticket,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +view_product_stats,easy-digital-downloads,60000,Easy Digital Downloads +view_projects,projectmanager,400,ProjectManager +view_query_monitor,query-monitor,50000,Query Monitor +view_recent_contacts,wp-easy-contact,600,Best Contact Management Software for WordPress +view_recent_dash_contacts,wp-easy-contact,600,Best Contact Management Software for WordPress +view_recent_tickets_dashboard,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +view_request_a_quote_dashboard,request-a-quote,1000,Request a Quote +view_shop_discount_stats,easy-digital-downloads,60000,Easy Digital Downloads +view_shop_payment_stats,easy-digital-downloads,60000,Easy Digital Downloads +view_shop_reports,easy-digital-downloads,60000,Easy Digital Downloads +view_shop_sensitive_data,easy-digital-downloads,60000,Easy Digital Downloads +view_single_quote,request-a-quote,1000,Request a Quote +view_sportspress_reports,sportspress,20000,SportsPress – Sports Club & League Manager +view_sprout_invoices_dashboard,sprout-invoices,2000,Client Invoicing by Sprout Invoices – Easy Estimates and Invoices for WordPress +view_template_name,display-template-name,2000,Display Template Name +view_ticket,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +view_ticket,catchers-helpdesk,200,Catchers Helpdesk and Ticket system for Support +view_trash,bbpress,300000,bbPress +view_unassigned_tickets,awesome-support,9000,Awesome Support – WordPress HelpDesk & Support Plugin +view_wallets_profile,wallets,600,Bitcoin and Altcoin Wallets +view_woocommerce_reports,dc-woocommerce-multi-vendor,10000,WC Marketplace +view_woocommerce_reports,webmaster-user-role,8000,Webmaster User Role +view_woocommerce_reports,woocommerce,4000000,WooCommerce +view_woocommerce_reports,wc-multivendor-marketplace,1000,WooCommerce Multivendor Marketplace +view_wp_easy_contact_dashboard,wp-easy-contact,600,Best Contact Management Software for WordPress +view_wp_template_viewer,wp-template-viewer,200,WP Template Viewer +view_wp_ticket_com_dashboard,wp-ticket,400,Best Customer Customer Service Software & Support Ticket System for WordPress +view_wptao_reports,wp-tao,2000,"Track, Analyze & Optimize by WP Tao" +view_yop_polls_imports,yop-poll,20000,YOP Poll +view_yop_polls_logs,yop-poll,20000,YOP Poll +view_yop_polls_results,yop-poll,20000,YOP Poll +view_youtube_showcase_dashboard,youtube-showcase,7000,YouTube Gallery – Best YouTube Video Gallery for WordPress +vote_on_comments,comment-popularity,300,Comment Popularity +wcjp_addcss_code,custom-css-js-php,7000,Custom css-js-php +wcjp_addjs_code,custom-css-js-php,7000,Custom css-js-php +wcjp_addphp_code,custom-css-js-php,7000,Custom css-js-php +wcjp_admin_overview,custom-css-js-php,7000,Custom css-js-php +wcjp_how_overview,custom-css-js-php,7000,Custom css-js-php +wcjp_managecss_code,custom-css-js-php,7000,Custom css-js-php +wcjp_managejs_code,custom-css-js-php,7000,Custom css-js-php +wcjp_managephp_code,custom-css-js-php,7000,Custom css-js-php +wcup_manager,world-cup-predictor,400,World Cup Predictor +wh_admin_overview,word-highlighter,500,Word Highlighter +wh_how_overview,word-highlighter,500,Word Highlighter +wh_manage_settings,word-highlighter,500,Word Highlighter +wholesale,pricing-deals-for-woocommerce,7000,Pricing Deals for WooCommerce +withdraw_funds_from_wallet,wallets,600,Bitcoin and Altcoin Wallets +wop_admin_overview,wp-overlays,2000,WP Overlays +wop_how_overview,wp-overlays,2000,WP Overlays +wop_manage_settings,wp-overlays,2000,WP Overlays +wp-piwik_read_stats,wp-piwik,70000,WP-Matomo (WP-Piwik) +wpProQuiz_add_quiz,wp-pro-quiz,20000,Wp-Pro-Quiz +wpProQuiz_change_settings,wp-pro-quiz,20000,Wp-Pro-Quiz +wpProQuiz_delete_quiz,wp-pro-quiz,20000,Wp-Pro-Quiz +wpProQuiz_edit_quiz,wp-pro-quiz,20000,Wp-Pro-Quiz +wpProQuiz_export,wp-pro-quiz,20000,Wp-Pro-Quiz +wpProQuiz_import,wp-pro-quiz,20000,Wp-Pro-Quiz +wpProQuiz_reset_statistics,wp-pro-quiz,20000,Wp-Pro-Quiz +wpProQuiz_show,wp-pro-quiz,20000,Wp-Pro-Quiz +wpProQuiz_show_statistics,wp-pro-quiz,20000,Wp-Pro-Quiz +wpProQuiz_toplist_edit,wp-pro-quiz,20000,Wp-Pro-Quiz +wp_power_stats_configure,wp-power-stats,10000,WP Power Stats +wp_power_stats_view,wp-power-stats,10000,WP Power Stats +wp_review_description,wp-review,80000,WP Review +wp_review_features,wp-review,80000,WP Review +wp_review_global_options,wp-review,80000,WP Review +wp_review_import_reviews,wp-review,80000,WP Review +wp_review_links,wp-review,80000,WP Review +wp_review_notification_bar,wp-review,80000,WP Review +wp_review_purge_comment_ratings,wp-review,80000,WP Review +wp_review_purge_visitor_ratings,wp-review,80000,WP Review +wp_review_single_page,wp-review,80000,WP Review +wp_review_user_reviews,wp-review,80000,WP Review +wp_show_stats_visibility,wp-show-stats,2000,WP Show Stats +wpaa_set_comment_cap,wp-access-areas,1000,WordPress Access Areas +wpaa_set_edit_cap,wp-access-areas,1000,WordPress Access Areas +wpaa_set_view_cap,wp-access-areas,1000,WordPress Access Areas +wpam_admin,affiliates-manager,10000,Affiliates Manager +wpcf_custom_field_edit,types,200000,"Toolset Types – Custom Post Types, Custom Fields and Taxonomies" +wpcf_custom_field_edit_others,types,200000,"Toolset Types – Custom Post Types, Custom Fields and Taxonomies" +wpcf_custom_field_view,types,200000,"Toolset Types – Custom Post Types, Custom Fields and Taxonomies" +wpcf_custom_post_type_edit,types,200000,"Toolset Types – Custom Post Types, Custom Fields and Taxonomies" +wpcf_custom_post_type_edit_others,types,200000,"Toolset Types – Custom Post Types, Custom Fields and Taxonomies" +wpcf_custom_post_type_view,types,200000,"Toolset Types – Custom Post Types, Custom Fields and Taxonomies" +wpcf_custom_taxonomy_edit,types,200000,"Toolset Types – Custom Post Types, Custom Fields and Taxonomies" +wpcf_custom_taxonomy_edit_others,types,200000,"Toolset Types – Custom Post Types, Custom Fields and Taxonomies" +wpcf_custom_taxonomy_view,types,200000,"Toolset Types – Custom Post Types, Custom Fields and Taxonomies" +wpcf_user_meta_field_edit,types,200000,"Toolset Types – Custom Post Types, Custom Fields and Taxonomies" +wpcf_user_meta_field_edit_others,types,200000,"Toolset Types – Custom Post Types, Custom Fields and Taxonomies" +wpcf_user_meta_field_view,types,200000,"Toolset Types – Custom Post Types, Custom Fields and Taxonomies" +wpe_admin_overview,wp-prayer,800,WP Prayer +wpe_form_prayer,wp-prayer,800,WP Prayer +wpe_manage_email_settings,wp-prayer,800,WP Prayer +wpe_manage_prayer,wp-prayer,800,WP Prayer +wpe_manage_prayers_performed,wp-prayer,800,WP Prayer +wpe_manage_settings,wp-prayer,800,WP Prayer +wpe_prayers_export,wp-prayer,800,WP Prayer +wpfd_create_category,wp-smart-editor,900,WP Smart Editor +wpfd_delete_category,wp-smart-editor,900,WP Smart Editor +wpfd_edit_category,wp-smart-editor,900,WP Smart Editor +wpfd_edit_own_category,wp-smart-editor,900,WP Smart Editor +wpfd_manage_file,wp-smart-editor,900,WP Smart Editor +wpgmp_admin_overview,wp-google-map-plugin,100000,WP Google Map Plugin +wpgmp_form_group_map,wp-google-map-plugin,100000,WP Google Map Plugin +wpgmp_form_location,wp-google-map-plugin,100000,WP Google Map Plugin +wpgmp_form_map,wp-google-map-plugin,100000,WP Google Map Plugin +wpgmp_how_overview,wp-google-map-plugin,100000,WP Google Map Plugin +wpgmp_manage_group_map,wp-google-map-plugin,100000,WP Google Map Plugin +wpgmp_manage_location,wp-google-map-plugin,100000,WP Google Map Plugin +wpgmp_manage_map,wp-google-map-plugin,100000,WP Google Map Plugin +wpgmp_manage_settings,wp-google-map-plugin,100000,WP Google Map Plugin +wphr_create_document,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_create_employee,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_create_review,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_delete_document,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_delete_employee,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_delete_review,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_edit_document,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_edit_employee,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_leave_create_request,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_leave_mails,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_leave_manage,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_list_employee,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_manage_announcement,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_manage_department,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_manage_designation,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_manage_hr_settings,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_manage_jobinfo,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_manage_review,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_view_document,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_view_employee,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wphr_view_jobinfo,wp-hr-manager,300,WP-HR Manager: The Human Resources Plugin for WordPress +wplc_ma_agent,wp-live-chat-support,60000,WP Live Chat Support +wpml_manage_woocommerce_multilingual,woocommerce-multilingual,90000,WooCommerce Multilingual – run WooCommerce with WPML +wpml_operate_woocommerce_multilingual,woocommerce-multilingual,90000,WooCommerce Multilingual – run WooCommerce with WPML +wpp_admin_overview,wp-posts-master,1000,WP Posts Master +wpp_form_layout,wp-posts-master,1000,WP Posts Master +wpp_form_rules,wp-posts-master,1000,WP Posts Master +wpp_how_overview,wp-posts-master,1000,WP Posts Master +wpp_manage_layout,wp-posts-master,1000,WP Posts Master +wpp_manage_rules,wp-posts-master,1000,WP Posts Master +wppa_admin,wp-photo-album-plus,30000,WP Photo Album Plus +wppa_comments,wp-photo-album-plus,30000,WP Photo Album Plus +wppa_export,wp-photo-album-plus,30000,WP Photo Album Plus +wppa_help,wp-photo-album-plus,30000,WP Photo Album Plus +wppa_import,wp-photo-album-plus,30000,WP Photo Album Plus +wppa_moderate,wp-photo-album-plus,30000,WP Photo Album Plus +wppa_potd,wp-photo-album-plus,30000,WP Photo Album Plus +wppa_settings,wp-photo-album-plus,30000,WP Photo Album Plus +wppa_upload,wp-photo-album-plus,30000,WP Photo Album Plus +wppcp_manage_options,wp-private-content-plus,7000,WP Private Content Plus +wppizza_cap_access,wppizza,2000,WPPizza +wppizza_cap_access_rights,wppizza,2000,WPPizza +wppizza_cap_additives,wppizza,2000,WPPizza +wppizza_cap_categories,wppizza,2000,WPPizza +wppizza_cap_customers,wppizza,2000,WPPizza +wppizza_cap_delete_order,wppizza,2000,WPPizza +wppizza_cap_gateways,wppizza,2000,WPPizza +wppizza_cap_layout,wppizza,2000,WPPizza +wppizza_cap_localization,wppizza,2000,WPPizza +wppizza_cap_meal_sizes,wppizza,2000,WPPizza +wppizza_cap_menu_items,wppizza,2000,WPPizza +wppizza_cap_opening_times,wppizza,2000,WPPizza +wppizza_cap_openingtimes,wppizza,2000,WPPizza +wppizza_cap_order_form,wppizza,2000,WPPizza +wppizza_cap_order_form_settings,wppizza,2000,WPPizza +wppizza_cap_order_history,wppizza,2000,WPPizza +wppizza_cap_order_settings,wppizza,2000,WPPizza +wppizza_cap_orderhistory,wppizza,2000,WPPizza +wppizza_cap_reports,wppizza,2000,WPPizza +wppizza_cap_settings,wppizza,2000,WPPizza +wppizza_cap_templates,wppizza,2000,WPPizza +wppizza_cap_tools,wppizza,2000,WPPizza +wprc_add_new_capability,user-roles-and-capabilities,10000,User Roles and Capabilities +wprc_add_new_role,user-roles-and-capabilities,10000,User Roles and Capabilities +wprc_change_default_role,user-roles-and-capabilities,10000,User Roles and Capabilities +wprc_delete_role,user-roles-and-capabilities,10000,User Roles and Capabilities +wprc_export_role_caps,user-roles-and-capabilities,10000,User Roles and Capabilities +wprc_import_role_caps,user-roles-and-capabilities,10000,User Roles and Capabilities +wprc_manage_all_capabilities,user-roles-and-capabilities,10000,User Roles and Capabilities +wprc_manage_user_capabilities,user-roles-and-capabilities,10000,User Roles and Capabilities +wprc_remove_capability,user-roles-and-capabilities,10000,User Roles and Capabilities +wprc_rename_role,user-roles-and-capabilities,10000,User Roles and Capabilities +wpseo_bulk_edit,wordpress-seo,5000000,Yoast SEO +wpseo_edit_advanced_metadata,wordpress-seo,5000000,Yoast SEO +wpseo_manage_options,wordpress-seo,5000000,Yoast SEO +wpsg_conf,wpshopgermany-free,400,wpShopGermany Free +wpsg_lizence,wpshopgermany-free,400,wpShopGermany Free +wpsg_menu,wpshopgermany-free,400,wpShopGermany Free +wpsg_order,wpshopgermany-free,400,wpShopGermany Free +wpsg_produkt,wpshopgermany-free,400,wpShopGermany Free +wpshop_add_attribute_group,wpshop,1000,=== WPshop – eCommerce +wpshop_add_attribute_set,wpshop,1000,=== WPshop – eCommerce +wpshop_add_attributes,wpshop,1000,=== WPshop – eCommerce +wpshop_add_attributes_select_values,wpshop,1000,=== WPshop – eCommerce +wpshop_add_attributes_unit,wpshop,1000,=== WPshop – eCommerce +wpshop_add_attributes_unit_group,wpshop,1000,=== WPshop – eCommerce +wpshop_add_product,wpshop,1000,=== WPshop – eCommerce +wpshop_delete_attribute_group,wpshop,1000,=== WPshop – eCommerce +wpshop_delete_attribute_set,wpshop,1000,=== WPshop – eCommerce +wpshop_delete_attributes,wpshop,1000,=== WPshop – eCommerce +wpshop_delete_attributes_select_values,wpshop,1000,=== WPshop – eCommerce +wpshop_delete_attributes_unit,wpshop,1000,=== WPshop – eCommerce +wpshop_delete_attributes_unit_group,wpshop,1000,=== WPshop – eCommerce +wpshop_edit_advanced_options,wpshop,1000,=== WPshop – eCommerce +wpshop_edit_attribute_group,wpshop,1000,=== WPshop – eCommerce +wpshop_edit_attribute_group_details,wpshop,1000,=== WPshop – eCommerce +wpshop_edit_attribute_set,wpshop,1000,=== WPshop – eCommerce +wpshop_edit_attributes,wpshop,1000,=== WPshop – eCommerce +wpshop_edit_attributes_select_values,wpshop,1000,=== WPshop – eCommerce +wpshop_edit_attributes_unit,wpshop,1000,=== WPshop – eCommerce +wpshop_edit_attributes_unit_group,wpshop,1000,=== WPshop – eCommerce +wpshop_edit_options,wpshop,1000,=== WPshop – eCommerce +wpshop_edit_product,wpshop,1000,=== WPshop – eCommerce +wpshop_manage_product_categories,wpshop,1000,=== WPshop – eCommerce +wpshop_view_addons,wpshop,1000,=== WPshop – eCommerce +wpshop_view_advanced_options,wpshop,1000,=== WPshop – eCommerce +wpshop_view_attribute_group,wpshop,1000,=== WPshop – eCommerce +wpshop_view_attribute_group_details,wpshop,1000,=== WPshop – eCommerce +wpshop_view_attribute_set,wpshop,1000,=== WPshop – eCommerce +wpshop_view_attribute_set_details,wpshop,1000,=== WPshop – eCommerce +wpshop_view_attributes,wpshop,1000,=== WPshop – eCommerce +wpshop_view_attributes_unit,wpshop,1000,=== WPshop – eCommerce +wpshop_view_attributes_unit_group,wpshop,1000,=== WPshop – eCommerce +wpshop_view_coupons,wpshop,1000,=== WPshop – eCommerce +wpshop_view_dashboard,wpshop,1000,=== WPshop – eCommerce +wpshop_view_documentation_menu,wpshop,1000,=== WPshop – eCommerce +wpshop_view_groups,wpshop,1000,=== WPshop – eCommerce +wpshop_view_import_menu,wpshop,1000,=== WPshop – eCommerce +wpshop_view_messages,wpshop,1000,=== WPshop – eCommerce +wpshop_view_options,wpshop,1000,=== WPshop – eCommerce +wpshop_view_orders,wpshop,1000,=== WPshop – eCommerce +wpshop_view_product,wpshop,1000,=== WPshop – eCommerce +wpshop_view_shortcodes,wpshop,1000,=== WPshop – eCommerce +wpshop_view_statistics,wpshop,1000,=== WPshop – eCommerce +wpshop_view_tools_menu,wpshop,1000,=== WPshop – eCommerce +wpsms_outbox,wp-sms,7000,WP SMS +wpsms_sendsms,wp-sms,7000,WP SMS +wpsms_setting,wp-sms,7000,WP SMS +wpsms_subscribe_groups,wp-sms,7000,WP SMS +wpsms_subscribers,wp-sms,7000,WP SMS +wpsqt-manage,wp-survey-and-quiz-tool,4000,WP Survey And Quiz Tool +wpt_can_tweet,wp-to-twitter,60000,WP to Twitter +wpt_tweet_now,wp-to-twitter,60000,WP to Twitter +wpt_twitter_custom,wp-to-twitter,60000,WP to Twitter +wpt_twitter_oauth,wp-to-twitter,60000,WP to Twitter +wpt_twitter_switch,wp-to-twitter,60000,WP to Twitter +write_reply_hanaboard-post,hana-board,1000,Hana-Board 하나보드 워드프레스 게시판 +wsq_admin_overview,wp-security-questions,1000,WP Security Question +wsq_how_overview,wp-security-questions,1000,WP Security Question +wsq_manage_settings,wp-security-questions,1000,WP Security Question +wth_admin_overview,was-this-helpful,500,Was This Helpful +wth_form_helpful,was-this-helpful,500,Was This Helpful +wth_how_overview,was-this-helpful,500,Was This Helpful +wysija_config,wysija-newsletters,300000,MailPoet Newsletters (Previous) +wysija_newsletters,wysija-newsletters,300000,MailPoet Newsletters (Previous) +wysija_stats_dashboard,wysija-newsletters,300000,MailPoet Newsletters (Previous) +wysija_style_tab,wysija-newsletters,300000,MailPoet Newsletters (Previous) +wysija_subscribers,wysija-newsletters,300000,MailPoet Newsletters (Previous) +wysija_theme_tab,wysija-newsletters,300000,MailPoet Newsletters (Previous) +xili_dictionary_admin,xili-dictionary,800,xili-dictionary +xili_dictionary_edit,xili-dictionary,800,xili-dictionary +xili_dictionary_edit_save,xili-dictionary,800,xili-dictionary +xili_language_clone_tax,xili-language,3000,xili-language +xili_language_menu,xili-language,3000,xili-language +xili_language_set,xili-language,3000,xili-language +xili_tidy_admin_set,xili-tidy-tags,1000,xili-tidy-tags +xili_tidy_editor_group,xili-tidy-tags,1000,xili-tidy-tags +xili_tidy_editor_set,xili-tidy-tags,1000,xili-tidy-tags +ycd_manage_options,countdown-builder,1000,Countdown +yop_poll_add,yop-poll,20000,YOP Poll +yop_poll_delete_others,yop-poll,20000,YOP Poll +yop_poll_delete_own,yop-poll,20000,YOP Poll +yop_poll_edit_others,yop-poll,20000,YOP Poll +yop_poll_edit_own,yop-poll,20000,YOP Poll +yop_poll_results_others,yop-poll,20000,YOP Poll +yop_poll_results_own,yop-poll,20000,YOP Poll +zbs_dash,zero-bs-crm,1000,Zero BS WordPress CRM \ No newline at end of file diff --git a/extras/modules/role-editor/data/single-site-admin-caps.txt b/extras/modules/role-editor/data/single-site-admin-caps.txt new file mode 100644 index 0000000..7087ce7 --- /dev/null +++ b/extras/modules/role-editor/data/single-site-admin-caps.txt @@ -0,0 +1,18 @@ +// Only Administrators of single site installations have the following capabilities. +// In Multisite, only the Super Admin has these abilities. +// See: http://codex.wordpress.org/Roles_and_Capabilities#Additional_Admin_Capabilities + +update_core +update_plugins +update_themes +install_plugins +install_themes +delete_themes +delete_plugins +edit_plugins +edit_themes +edit_files +edit_users +create_users +delete_users +unfiltered_html \ No newline at end of file diff --git a/extras/modules/role-editor/data/stable-meta-caps.txt b/extras/modules/role-editor/data/stable-meta-caps.txt new file mode 100644 index 0000000..44b74a4 --- /dev/null +++ b/extras/modules/role-editor/data/stable-meta-caps.txt @@ -0,0 +1,26 @@ +// These are known meta capabilities that each reliably map to a single capability if they are enabled. +// Meta capabilities that have context-dependent mappings are not included in this list. + +promote_user +add_users +edit_css +upload_themes +upload_plugins +update_languages +deactivate_plugins +customize +delete_site + +edit_categories +delete_categories +assign_categories + +manage_post_tags +edit_post_tags +delete_post_tags +assign_post_tags + +update_php +export_others_personal_data +erase_others_personal_data +manage_privacy_options \ No newline at end of file diff --git a/extras/modules/role-editor/data/superadmin-only-caps.txt b/extras/modules/role-editor/data/superadmin-only-caps.txt new file mode 100644 index 0000000..02895d4 --- /dev/null +++ b/extras/modules/role-editor/data/superadmin-only-caps.txt @@ -0,0 +1,8 @@ +//These capabilities are only available to Multisite Super Admins: + +manage_network +manage_sites +manage_network_users +manage_network_plugins +manage_network_themes +manage_network_options \ No newline at end of file diff --git a/extras/modules/role-editor/data/update-cap-db.php b/extras/modules/role-editor/data/update-cap-db.php new file mode 100644 index 0000000..ccf8e22 --- /dev/null +++ b/extras/modules/role-editor/data/update-cap-db.php @@ -0,0 +1,187 @@ +<?php /** @noinspection SqlResolve */ +/** @noinspection PhpComposerExtensionStubsInspection */ +$startTime = microtime(true); + +require_once '../../../../includes/ame-utils.php'; + +$configFileName = 'D:/Dropbox/Projects/Admin Menu Editor/cap-db-generator-config.json'; +if ( !is_file($configFileName) ) { + echo "Error: Configuration file not found.\n"; + exit(1); +} + +$config = json_decode(file_get_contents($configFileName)); + +$inputFileName = ameUtils::get($config, 'inputFile'); +$outputFileName = ameUtils::get($config, 'outputFile'); +$excerptFileName = ameUtils::get($config, 'excerptFile', __DIR__ . DIRECTORY_SEPARATOR . 'capability-excerpt.sqlite3'); + +//Basic error checking. +if ( empty($inputFileName) ) { + echo "Error: Input file not specified.\n"; + exit(2); +} +if ( empty($outputFileName) ) { + echo "Error: Output file not specified.\n"; + exit(3); +} +if ( empty($excerptFileName) ) { + echo "Error: Excerpt database file name not specified.\n"; + exit(4); +} + +if ( !is_file($inputFileName) ) { + echo "Error: Input file doesn't exist.\n"; + exit(5); +} +if ( !is_file($outputFileName) ) { + echo "Error: Output file doesn't exist.\n"; + exit(6); +} + +if ( file_exists($excerptFileName) ) { + echo "Notice: The excerpt file will be overwritten.\n"; +} + +//Connect to the databases. +$pdo = new PDO('sqlite:' . $outputFileName); +$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +$pdo->exec("ATTACH DATABASE '$inputFileName' AS smokebase"); +$pdo->exec("PRAGMA foreign_keys = ON"); + +$statement = $pdo->query('SELECT COUNT(*) FROM smokebase.plugins'); +printf( + "%d plugins available\n", + $statement->fetchColumn(0) +); +$statement->closeCursor(); + +//Insert new plugins. +echo "Inserting new plugins\n"; +$pdo->exec(" + INSERT INTO components(typeId, slug, name, activeInstalls) + SELECT + componentTypes.typeId, plugins.slug, coalesce(plugins.name_header, plugins.name), plugins.active_installs + FROM + plugins + JOIN componentTypes + WHERE componentTypes.prefix = 'plugin:' + ON CONFLICT DO NOTHING +"); + +//Update plugin names and install stats. +echo "Updating plugin names\n"; +$pdo->exec(" + UPDATE components + SET name = coalesce(( + SELECT coalesce(plugins.name_header, plugins.name) + FROM plugins + WHERE plugins.slug = components.slug + ), name) + WHERE components.typeId = (SELECT componentTypes.typeId FROM componentTypes WHERE componentTypes.prefix = 'plugin:') +"); + +echo "Updating active installs\n"; +$pdo->exec(" + UPDATE components + SET activeInstalls = coalesce(( + SELECT plugins.active_installs + FROM plugins + WHERE plugins.slug = components.slug + ), activeInstalls) + WHERE components.typeId = (SELECT componentTypes.typeId FROM componentTypes WHERE componentTypes.prefix = 'plugin:') +"); + +//Insert new capabilities. +echo "Inserting new capabilities\n"; + +/** @noinspection SqlConstantCondition */ +$pdo->exec(" + INSERT INTO capabilities(name) + SELECT smokebase.capabilities.name + FROM smokebase.capabilities + WHERE 1=1 + ON CONFLICT DO NOTHING +"); + +echo "Updating plugin-capability relationships\n"; +$pdo->exec(" + INSERT INTO componentCapabilityInfo(capabilityId, numericId) + SELECT DISTINCT + capabilities.capabilityId, components.numericId + FROM + plugin_capabilities + JOIN smokebase.capabilities ON (smokebase.capabilities.entity_id = plugin_capabilities.entity_id) + JOIN reports ON (plugin_capabilities.report_id = reports.report_id) + JOIN plugins ON (plugins.slug = reports.slug) + JOIN components ON (components.slug = plugins.slug) + JOIN componentTypes ON (componentTypes.typeId = components.typeId) + JOIN main.capabilities ON (main.capabilities.name = smokebase.capabilities.name) + WHERE + componentTypes.prefix = 'plugin:' + ON CONFLICT DO NOTHING +"); + +$pdo = null; + +//Make a copy of the database. +$tempFileName = tempnam(sys_get_temp_dir(), 'rex'); +printf("Creating temporary file %s\n", $tempFileName); +copy($outputFileName, $tempFileName); + +$excerptDb = new PDO('sqlite:' . $tempFileName); +$excerptDb->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +$excerptDb->exec("PRAGMA foreign_keys = OFF"); + +//Delete plugins that don't have any associated capabilities. +echo "Deleting plugins without any capabilities (this can take a while)\n"; + +$excerptDb->exec(" + DELETE FROM components + WHERE components.numericId IN ( + select a.numericId + from components AS a LEFT JOIN componentCapabilityInfo ON (a.numericId = componentCapabilityInfo.numericId) + WHERE componentCapabilityInfo.capabilityId is NULL + ) + AND components.typeId = (SELECT typeId from componentTypes WHERE prefix = 'plugin:') +"); + +//Delete plugins with less than X installs. +echo "Deleting unpopular plugins\n"; +$excerptDb->exec("PRAGMA foreign_keys = ON"); +$excerptDb->exec(" + DELETE FROM components + WHERE activeInstalls < 100 + AND components.typeId = (SELECT typeId from componentTypes WHERE prefix = 'plugin:') +"); + +echo "Deleting unused capabilities\n"; +$excerptDb->exec(" + DELETE FROM capabilities + WHERE capabilityId IN ( + SELECT capabilities.capabilityId + FROM capabilities LEFT JOIN componentCapabilityInfo + ON (capabilities.capabilityId = componentCapabilityInfo.capabilityId) + WHERE componentCapabilityInfo.numericId IS NULL + ) +"); + +//The database probably has some empty space after deleting all of that data, +//so lets compact it to reduce its size. +$excerptDb->exec("VACUUM"); + +$excerptDb = null; +$size = filesize($tempFileName); +printf("Excerpt size: %d bytes\n", $size); + +//Move the file to the appropriate location. +if ( copy($tempFileName, $excerptFileName) ) { + printf("File moved to: %s\n", $excerptFileName); +} else { + printf("Error: Could not move the file to %s", $excerptFileName); + exit(10); +} +unlink($tempFileName); + +printf("Elapsed time: %.3f seconds\n", microtime(true) - $startTime); \ No newline at end of file diff --git a/extras/modules/role-editor/load.php b/extras/modules/role-editor/load.php new file mode 100644 index 0000000..ba9b8fa --- /dev/null +++ b/extras/modules/role-editor/load.php @@ -0,0 +1,13 @@ +<?php +require_once __DIR__ . '/../../../includes/reflection-callable.php'; + +require_once 'ameRoleEditor.php'; +require_once 'ameRexCapability.php'; +require_once 'ameRexCategory.php'; +require_once 'ameRexComponent.php'; +require_once 'ameRexSettingsValidator.php'; + +require_once 'ameRexComponentRegistry.php'; +require_once 'ameRexCapabilitySearchResultSet.php'; +require_once 'ameRexCapabilityDataSource.php'; +require_once 'ameRexCapabilityInfoSearch.php'; \ No newline at end of file diff --git a/extras/modules/role-editor/role-editor-template.php b/extras/modules/role-editor/role-editor-template.php new file mode 100644 index 0000000..7dd65e9 --- /dev/null +++ b/extras/modules/role-editor/role-editor-template.php @@ -0,0 +1,737 @@ +<?php +/** + * Variables set by ameModule when it outputs a template. + * + * @var string $moduleTabUrl + * @var array $settingsErrors + */ + +//Show errors encountered while saving changes. +if (!empty($settingsErrors)) { + echo '<div class="notice notice-error">'; + foreach ($settingsErrors as $error) { + /** @var WP_Error $error */ + if (!($error instanceof WP_Error)) { + continue; + } + printf('<p title="%s">%s</p>', esc_attr($error->get_error_code()), esc_html($error->get_error_message())); + } + echo '</div>'; +} + +if (!empty($_GET['no-changes-made'])) { + ?> + <div class="notice notice-info is-dismissible" id="ame-rex-no-changes"> + <p><strong>No changes were made.</strong></p> + </div> + <?php +} + +require AME_ROOT_DIR . '/modules/actor-selector/actor-selector-template.php'; +?> + +<div id="ame-role-editor-root"> + <div data-bind="visible: !isLoaded()" id="rex-loading-message">Loading...</div> + + <div id="rex-main-ui" data-bind="visible: isLoaded" style="display: none;"> + <div id="rex-content-container"> + <div id="rex-user-role-list" data-bind="visible: userRoleModule.isVisible"> + <div data-bind="template: {name: 'rex-user-role-list-template', data: userRoleModule}"></div> + </div> + + <div id="rex-category-sidebar"> + <div class="rex-dropdown-trigger" + data-target-dropdown-id="rex-category-list-options"> + <div class="dashicons dashicons-admin-generic"></div> + </div> + + <ul data-bind="template: {name: 'rex-nav-item-template', data: rootCategory}" + id="rex-category-navigation"></ul> + </div> + + <div id="rex-capability-view-container"> + <div id="rex-view-toolbar"> + <input type="search" title="Filter capabilities" placeholder="Search" id="rex-quick-search-query" + data-bind="textInput: searchQuery"> + <label for="rex-quick-search-query" class="screen-reader-text">Search capabilities</label> + + <label> + <input type="checkbox" data-bind="checked: readableNamesEnabled"> Readable names + </label> + + <button class="button rex-dropdown-trigger" id="rex-misc-view-options-button" + data-target-dropdown-id="rex-general-view-options"> + Options <span class="dashicons dashicons-arrow-down"></span> + </button> + + <label for="rex-category-view-selector" class="screen-reader-text">Select the category view</label> + <select id="rex-category-view-selector" title="Choose the category view" + data-bind=" + options: categoryViewOptions, + optionsText: 'label', + value: categoryViewMode"> + </select> + </div> + <div id="rex-capability-view" data-bind="class: capabilityViewClasses, + template: categoryViewMode().templateName"> + </div> + </div> + </div> + + <div id="rex-action-sidebar"> + <?php + if (is_multisite() && is_super_admin() && is_network_admin()) { + submit_button( + 'Update All Sites', + 'primary rex-action-button', + 'rex-global-save-changes-button', + false, + array( + 'disabled' => 'disabled', + 'data-bind' => 'enable: (!$root.isSaving() && $root.isLoaded()), click: updateAllSites', + ) + ); + } else { + submit_button( + 'Save Changes', + 'primary rex-action-button', + 'rex-save-changes-button', + false, + array( + 'disabled' => 'disabled', + 'data-bind' => 'enable: (!$root.isSaving() && $root.isLoaded()), click: saveChanges', + ) + ); + } + ?> + <div class="rex-action-separator"></div> + <?php + submit_button( + 'Add role', + 'rex-action-button', + 'rex-add-role-button', + false, + array('data-bind' => 'ameOpenDialog: "#rex-add-role-dialog"') + ); + + submit_button( + 'Rename role', + 'rex-action-button', + 'rex-rename-role-button', + false, + array('data-bind' => 'ameOpenDialog: "#rex-rename-role-dialog"') + ); + + submit_button( + 'Delete role', + 'rex-action-button', + 'rex-delete-role-button', + false, + array('data-bind' => 'ameOpenDialog: "#rex-delete-role-dialog"') + ); + ?> + <div class="rex-action-separator"></div> + <?php + submit_button( + 'Add capability', + 'rex-action-button', + 'rex-add-capability-button', + false, + array('data-bind' => 'ameOpenDialog: "#rex-add-capability-dialog"') + ); + + submit_button( + 'Delete capability', + 'rex-action-button', + 'rex-delete-capability-button', + false, + array('data-bind' => 'ameOpenDialog: "#rex-delete-capability-dialog"') + ); + + ?> + <div class="rex-action-separator"></div> + <?php + submit_button( + 'Editable roles', + 'rex-action-button', + 'rex-editable-roles-button', + false, + array('data-bind' => 'ameOpenDialog: "#rex-editable-roles-dialog"') + ); + ?> + + <form action="<?php echo esc_attr(add_query_arg('noheader', '1', $moduleTabUrl)); ?>" + method="post" + id="rex-save-settings-form" + style="display: none;"> + + <input type="hidden" name="action" value="ame-save-role-settings"> + <?php wp_nonce_field('ame-save-role-settings'); ?> + + <input type="hidden" name="settings" value="" data-bind="value: settingsFieldData"> + <input type="hidden" name="selectedActor" value="" + data-bind="value: selectedActor() ? selectedActor().id : ''"> + <input type="hidden" name="isGlobalUpdate" value="" + data-bind="value: (isGlobalSettingsUpdate() ? '1' : '')"> + </form> + </div> + + </div> + + <div id="rex-category-list-options" class="rex-dropdown" style="display: none;"> + <label class="rex-dropdown-item"> + <input type="checkbox" data-bind="checked: showNumberOfCapsEnabled"> Show number of capabilities + </label> + <label class="rex-dropdown-item rex-dropdown-sub-item"> + <input type="checkbox" data-bind="checked: showGrantedCapCountEnabled, enable: showNumberOfCapsEnabled"> + Show granted + </label> + <label class="rex-dropdown-item rex-dropdown-sub-item"> + <input type="checkbox" data-bind="checked: showTotalCapCountEnabled, enable: showNumberOfCapsEnabled"> Show + total + </label> + <label class="rex-dropdown-item rex-dropdown-sub-item"> + <input type="checkbox" data-bind="checked: showZerosEnabled, enable: showNumberOfCapsEnabled"> Show zeros + </label> + </div> + + <div id="rex-general-view-options" class="rex-dropdown" style="display: none;"> + <label class="rex-dropdown-item"> + <input type="checkbox" data-bind="checked: showOnlyCheckedEnabled"> Show only checked + </label> + <label class="rex-dropdown-item"> + <input type="checkbox" data-bind="checked: showDeprecatedEnabled"> Show deprecated + </label> + <label class="rex-dropdown-item"> + <input type="checkbox" data-bind="checked: showRedundantEnabled"> Show redundant + </label> + + <label class="rex-dropdown-item"> + <input type="checkbox" + data-bind="checked: inheritanceOverrideEnabled, enable: (selectedActor() instanceof RexUser)"> + Allow editing inherited capabilities + </label> + + <fieldset class="rex-dropdown-item"> + <strong class="rex-dropdown-item">Category box width</strong> + <label class="rex-dropdown-item rex-dropdown-sub-item"> + <input type="radio" value="adaptive" data-bind="checked: categoryWidthMode"> Adaptive + </label> + <label class="rex-dropdown-item rex-dropdown-sub-item"> + <input type="radio" value="full" data-bind="checked: categoryWidthMode"> Full + </label> + </fieldset> + </div> + + <!-- Permission tooltip content --> + <div style="display: none;"> + <div id="rex-permission-tip" data-bind="if: permissionTipSubject"> + <div class="rex-permission-description" + data-bind="if: permissionTipSubject().mainDescription"> + <span data-bind="text: permissionTipSubject().mainDescription"></span> + </div> + <code data-bind="text: permissionTipSubject().capability.name"></code> + + <div class="rex-tooltip-section-container"> + <!-- ko if: (selectedActor() && selectedActor().canHaveRoles) --> + <div class="rex-tooltip-section"> + <h4>Inheritance</h4> + <table class="widefat rex-capability-inheritance-breakdown"> + <tbody + data-bind="foreach: selectedActor().getInheritanceDetails(permissionTipSubject().capability)"> + <tr data-bind="css: {'rex-is-decisive-actor': isDecisive}"> + <td data-bind="text: name" class="rex-inheritance-actor-name"></td> + <td data-bind="text: description"></td> + </tr> + </tbody> + </table> + </div> + <!-- /ko --> + + <!-- ko if: permissionTipSubject().capability.notes --> + <div class="rex-tooltip-section"> + <h4>Notes</h4> + <span data-bind="text: permissionTipSubject().capability.notes"></span> + </div> + <!-- /ko --> + + <!-- ko if: permissionTipSubject().capability.grantedPermissions().length > 0 --> + <div class="rex-tooltip-section"> + <h4>Permissions</h4> + <ul data-bind="foreach: permissionTipSubject().capability.grantedPermissions()" + class="rex-tip-granted-permissions"> + <li data-bind="text: $data"></li> + </ul> + </div> + <!-- /ko --> + + <div data-bind="if: permissionTipSubject().capability.originComponent" class="rex-tooltip-section"> + <h4>Origin</h4> + <span data-bind="text: permissionTipSubject().capability.originComponent.name"></span> + </div> + + <div data-bind="if: permissionTipSubject().capability.getDocumentationUrl()" + class="rex-tooltip-section"> + <h4>See also</h4> + <span> + <a href="#" + target="_blank" + class="rex-documentation-link" + data-bind="text: permissionTipSubject().capability.getDocumentationUrl(), + attr: {href: permissionTipSubject().capability.getDocumentationUrl()}"></a> + </span> + </div> + </div> + </div> + </div> + + <div id="rex-delete-capability-dialog" + data-bind="ameDialog: deleteCapabilityDialog, ameEnableDialogButton: deleteCapabilityDialog.isDeleteButtonEnabled" + title="Delete capability" + style="display: none;" class="rex-dialog"> + <p class="rex-dialog-section"> + Select capabilities to remove from all roles: + </p> + + <div class="rex-deletable-capability-container" + data-bind="visible: (deleteCapabilityDialog.deletableItems().length > 0)"> + <ul data-bind="foreach: deleteCapabilityDialog.deletableItems" class="rex-deletable-capability-list"> + <li> + <label> + <input type="checkbox" data-bind="checked: isSelected"> + <span data-bind="text: capability.displayName" class="rex-capability-name"></span> + </label> + </li> + </ul> + </div> + + <p class="rex-dialog-section" data-bind="visible: (deleteCapabilityDialog.deletableItems().length <= 0)"> + There are no custom capabilities that can be deleted. + </p> + </div> + + <div id="rex-add-capability-dialog" + data-bind="ameDialog: addCapabilityDialog, ameEnableDialogButton: addCapabilityDialog.isAddButtonEnabled" + title="Add capability" + style="display: none;" class="rex-dialog"> + + <form data-bind="submit: addCapabilityDialog.onConfirm.bind(addCapabilityDialog)"> + <label for="rex-new-capability-name"> + Capability name: + </label> + <input type="text" data-bind="textInput: addCapabilityDialog.capabilityName" id="rex-new-capability-name" + maxlength="150"> + + <p id="rex-add-capability-validation-message"> + <span class="dashicons dashicons-dismiss" + data-bind="visible: (addCapabilityDialog.validationState() === 'error')"></span> + <span class="dashicons dashicons-info" + data-bind="visible: (addCapabilityDialog.validationState() === 'notice')"></span> + <span data-bind="html: addCapabilityDialog.validationMessage"></span> + </p> + </form> + </div> + + <div id="rex-add-role-dialog" + data-bind="ameDialog: addRoleDialog, ameEnableDialogButton: addRoleDialog.isAddButtonEnabled" + title="Add role" + style="display: none;" class="rex-dialog"> + + <!-- ko if: addRoleDialog.isRendered --> + <form data-bind="submit: addRoleDialog.onConfirm.bind(addRoleDialog)"> + <p class="rex-dialog-section"> + <label for="rex-new-role-display-name"> + Display name: + </label> + <input type="text" data-bind="textInput: addRoleDialog.roleDisplayName" id="rex-new-role-display-name" + maxlength="150" placeholder="New Role Name"> + </p> + + <p class="rex-dialog-section"> + <label for="rex-new-role-name"> + Role name (ID): + </label> + <input type="text" data-bind="textInput: addRoleDialog.roleName" id="rex-new-role-name" + maxlength="150" placeholder="new_role_name"> + </p> + + <p class="rex-dialog-section"> + <label for="rex-new-role-copy-caps"> + Copy capabilities from: + </label> + <select id="rex-new-role-copy-caps" data-bind="value: addRoleDialog.roleToCopyFrom"> + <option data-bind="value: null">None</option> + + <!-- ko if: $root.defaultRoles().length > 0 --> + <optgroup label="Built-In" data-bind="foreach: $root.defaultRoles"> + <option data-bind="text: displayName, value: $data"></option> + </optgroup> + <!-- /ko --> + + <!-- ko if: $root.customRoles().length > 0 --> + <optgroup label="Custom" data-bind="foreach: $root.customRoles"> + <option data-bind="text: displayName, value: $data"></option> + </optgroup> + <!-- /ko --> + </select> + </p> + + <!-- + As an alternative to clicking the "Add Role" button, the user can + confirm their inputs by pressing Enter. + --> + <input type="submit" name="hidden-submit-trigger" style="display: none;"> + </form> + <!-- /ko --> + </div> + + <div id="rex-delete-role-dialog" + data-bind="ameDialog: deleteRoleDialog, ameEnableDialogButton: deleteRoleDialog.isDeleteButtonEnabled" + title="Delete role" + style="display: none;" class="rex-dialog"> + + <!-- ko if: deleteRoleDialog.isRendered --> + <span>Select roles to delete:</span> + + <div class="rex-deletable-role-list-container"> + <table class="widefat rex-deletable-role-list"> + <tbody> + <!-- ko if: $root.roles().length > 0 --> + <!-- ko template: { + name: 'rex-deletable-role-template', + foreach: $root.roles + } --> + <!-- /ko --> + <!-- /ko --> + </tbody> + </table> + </div> + <!-- /ko --> + + <!-- ko if: !deleteRoleDialog.isRendered() --> + <div style="height: 400px">(Placeholder.)</div> + <!-- /ko --> + </div> + + <div id="rex-rename-role-dialog" + data-bind="ameDialog: renameRoleDialog, ameEnableDialogButton: renameRoleDialog.isConfirmButtonEnabled" + title="Rename role" + style="display: none;" class="rex-dialog"> + + <!-- ko if: renameRoleDialog.isRendered --> + <form data-bind="submit: renameRoleDialog.onConfirm.bind(renameRoleDialog)"> + <p class="rex-dialog-section"> + <label for="rex-role-to-rename"> + Select role to rename: + </label> + <select id="rex-role-to-rename" data-bind="value: renameRoleDialog.selectedRole"> + <!-- ko if: $root.defaultRoles().length > 0 --> + <optgroup label="Built-In" data-bind="foreach: $root.defaultRoles"> + <option data-bind="text: (displayName() + ' (' + name() + ')'), value: $data"></option> + </optgroup> + <!-- /ko --> + + <!-- ko if: $root.customRoles().length > 0 --> + <optgroup label="Custom" data-bind="foreach: $root.customRoles"> + <option data-bind="text: (displayName() + ' (' + name() + ')'), value: $data"></option> + </optgroup> + <!-- /ko --> + </select> + </p> + + <p class="rex-dialog-section"> + <label for="rex-edited-role-display-name"> + New display name: + </label> + <input type="text" data-bind="textInput: renameRoleDialog.newDisplayName" id="rex-edited-role-display-name" + maxlength="150" placeholder="New Role Name"> + </p> + + <input type="submit" name="hidden-submit-trigger" style="display: none;"> + </form> + <!-- /ko --> + </div> + + <div id="rex-editable-roles-dialog" title="Editable roles" class="rex-dialog" + style="display: none;" + data-bind="ameDialog: editableRolesDialog"> + <!-- ko template: { + name: 'rex-editable-roles-screen-template', + data: editableRolesDialog + } --> + <!-- /ko --> + </div> +</div> + +<script type="text/html" id="rex-nav-item-template"> + <li class="rex-nav-item" data-bind="css: navCssClasses, click: $root.selectedCategory, visible: isNavVisible"> + <span class="rex-nav-toggle" data-bind=" + visible: (parent !== null), + click: toggleSubcategories.bind($data), + clickBubble: false"> + </span> + <span data-bind="text: name, attr: { title: subtitle }" class="rex-nav-item-header"></span> + + <!-- ko if: isCapCountVisible --> + <span class="rex-capability-count" + data-bind="css: {'rex-all-capabilities-enabled': areAllPermissionsEnabled}, + attr: {title: enabledCapabilityCount() + ' of ' + totalCapabilityCount() + ' capabilities' }"><!-- + ko if: isEnabledCapCountVisible + --><span data-bind="text: enabledCapabilityCount" class="rex-enabled-capability-count"></span><!-- /ko + --><!-- + ko if: $root.showTotalCapCountEnabled() + --><span data-bind="text: totalCapabilityCount" class="rex-total-capability-count"></span><!-- /ko + --></span> + <!-- /ko --> + </li> + + <!-- ko if: (subcategories.length > 0) --> + <!-- ko template: { + name: 'rex-nav-item-template', + foreach: navSubcategories + } --> + <!-- /ko --> + <!-- /ko --> +</script> + +<script type="text/html" id="rex-category-template"> + <div class="rex-category" data-bind="css: cssClasses(), visible: isVisible, + attr: { 'id': htmlId }"> + <div class="rex-category-header"> + <div class="rex-category-name" data-bind="text: name, attr: {title: subtitle}"></div> + <div class="rex-category-subheading" data-bind="text: subheading, attr: {title: subheading}"></div> + </div> + <div class="rex-category-contents" data-bind="template: { name: contentTemplate }"> + </div> + </div> +</script> + +<script type="text/html" id="rex-default-category-content-template"> + <!-- ko if: subcategories.length > 0 --> + <!-- ko template: { + name: 'rex-category-template', + foreach: sortedSubcategories + } --> + <!-- /ko --> + <!-- /ko --> + + <!-- ko if: (permissions().length > 0) --> + <div class="rex-permission-list" data-bind="template: {name: 'rex-permission-template', foreach: permissions}"> + </div> + <!-- /ko --> +</script> + +<script type="text/html" id="rex-permission-table-template"> + <table class="widefat rex-permission-table"> + <thead> + <tr> + <th class="rex-category-name-column"></th> + <!-- ko foreach: tableColumns --> + <th scope="col" data-bind="text: title"></th> + <!-- /ko --> + </tr> + </thead> + + <tbody data-bind="foreach: {data: sortedSubcategories, as: 'category'}"> + <tr data-bind="visible: isVisible"> + <th scope="row" data-bind="attr: {title: subtitle}"> + <label> + <input type="checkbox" data-bind="checked: areAllPermissionsEnabled, enable: areAnyPermissionsEditable"> + <span data-bind="text: name"></span> + </label> + + <div data-bind="visible: (subtitle !== null)"> + <!--suppress HtmlFormInputWithoutLabel --> + <input type="checkbox" readonly disabled style="visibility: hidden" title="Hidden placeholder"> + <span class="rex-category-subtitle" data-bind="text: subtitle"></span> + </div> + </th> + + <!-- ko foreach: {data: $parent.tableColumns, as: 'column'} --> + <td data-bind="visible: !category.isBaseCapNoticeVisible()"> + <div data-bind="foreach: column.actions" class=""> + <!-- ko if: category.actions.hasOwnProperty($data) --> + <!-- ko with: category.actions[$data] --> + <!-- ko template: 'rex-permission-template' --> + <!-- /ko --> + <!-- /ko --> + <!-- /ko --> + </div> + </td> + <!-- /ko --> + + <!-- ko if: isBaseCapNoticeVisible --> + <td class="rex-base-cap-notice" data-bind="attr: {colspan: $parent.tableColumns().length}"> + Uses "<span data-bind="text: getBaseCategory().name"></span>" capabilities. + </td> + <!-- /ko --> + </tr> + </tbody> + </table> +</script> + +<script type="text/html" id="rex-permission-template"> + <div class="rex-permission" data-bind=" + visible: isVisible, + css: { + 'rex-is-redundant': isRedundant, + 'rex-is-deprecated-capability': capability.isDeprecated, + 'rex-is-explicitly-denied': capability.isExplicitlyDenied, + 'rex-is-inherited': capability.isInherited, + 'rex-is-personal-override': capability.isPersonalOverride + }"> + <label data-bind="attr: {title: capability.name}"> + <input + data-bind="checked: capability.isEnabledForSelectedActor, enable: capability.isEditable" + type="checkbox"> + <span data-bind="html: labelHtml" class="rex-capability-name"></span> + </label> + <span class="rex-permission-tip-trigger"><span class="dashicons dashicons-info"></span></span> + </div> +</script> + +<script type="text/html" id="rex-hierarchy-view-template"> + <!-- ko template: { + name: 'rex-category-template', + foreach: rootCategory.sortedSubcategories + } --> + <!-- /ko --> +</script> + +<script type="text/html" id="rex-list-view-template"> + <div class="rex-permission-list" id="rex-permission-list-view" + data-bind="template: {name: 'rex-permission-template', foreach: allCapabilitiesAsPermissions}"> + </div> +</script> + +<script type="text/html" id="rex-single-category-view-template"> + <div id="rex-category-view-spacer"></div> + <!-- ko template: { + name: 'rex-category-template', + foreach: leafCategories + } --> + <!-- /ko --> +</script> + +<script type="text/html" id="rex-deletable-role-template"> + <tr> + <td class="rex-role-name-column" data-bind="attr: { title: name }"> + <label> + <input + data-bind="enable: $root.canDeleteRole($data), + checked: $root.deleteRoleDialog.getSelectionState(name())" + type="checkbox"> + <span data-bind="text: displayName"></span> + </label> + </td> + <td class="rex-role-usage-column"> + <span data-bind="if: $root.isDefaultRoleForNewUsers($data)" + title="This is the default role for new users"> + Default role + </span> + <span data-bind="if: hasUsers && !$root.isDefaultRoleForNewUsers($data)" + title="This role is still assigned to one or more users"> + In use + </span> + </td> + </tr> +</script> + +<script type="text/html" id="rex-editable-roles-screen-template"> + <div id="rex-editable-roles-container"> + <div class="ame-role-table-container"> + <table class="widefat ame-role-table"> + <tbody data-bind="foreach: visibleActors"> + <tr data-bind="css: { + 'alternate': (($index() % 2) === 0), + 'ame-selected-role-table-row': $data === $parent.selectedActor() + }, click: $parent.selectItem.bind($parent)"> + <td class="ame-column-role-name"> + <span data-bind="text: $parent.getItemText($data)"></span> + </td> + <td class="ame-column-selected-role-tip"> + <div class="ame-selected-role-tip"> + <svg xmlns="http://www.w3.org/2000/svg" class="ame-rex-svg-triangle" viewBox="0 0 50 100"> + <polygon points="51,0 0,50 51,100"/> + </svg> + </div> + </td> + </tr> + </tbody> + </table> + </div> + <div id="rex-editable-roles-options"> + <fieldset> + <p><label> + <input type="radio" value="auto" data-bind="checked: editableRoleStrategy, enable: isAutoStrategyAllowed" + name="editable-roles-behaviour"> + Automatic + <br><span class="description"> + Only allows to assign the roles that have the same or fewer capabilities. + </span> + </label></p> + <p><label> + <input type="radio" value="none" data-bind="checked: editableRoleStrategy" + name="editable-roles-behaviour"> + Leave unchanged + <br><span class="description"> + Lets other plugins control this setting. + </span> + </label></p> + <p><label> + <input type="radio" value="user-defined-list" data-bind="checked: editableRoleStrategy, enable: isListStrategyAllowed" + name="editable-roles-behaviour"> + Custom + <br><span class="description"> + Lets you manually choose the roles that the selected role or user can + assign to other users. + </span> + </label></p> + </fieldset> + <!-- ko if: $root.roles().length > 0 --> + <ul id="rex-editable-role-list" data-bind="foreach: editor.roles"> + <li> + <label> + <input type="checkbox" + data-bind="checked: $parent.isRoleSetToEditable($data), enable: $parent.isRoleEnabled($data)"> + <span data-bind="text: displayName"></span> + </label> + </li> + </ul> + <!-- /ko --> + </div> + + </div> +</script> + +<script type="text/html" id="rex-user-role-list-template"> + <p> + <label for="rex-primary-user-role"> + <strong>Primary role</strong> + </label> + <select name="rex-primary-user-role" id="rex-primary-user-role" data-bind="value: primaryRole"> + <!-- ko if: sortedRoles().length > 0 --> + <!-- ko foreach: sortedRoles --> + <option value="" + data-bind="text: displayName, value: $data, enable: $parent.canAssignRoleToActor($data)"></option> + <!-- /ko --> + <!-- /ko --> + + <!-- Include a "no role" option because some users might have no roles, especially in Multisite. --> + <option data-bind="text: '— No role for this site —', value: null"></option> + </select> + </p> + + <strong>Other roles</strong> + <!-- ko if: sortedRoles().length > 0 --> + <ul data-bind="foreach: sortedRoles" class="rex-user-role-option-list"> + <li> + <label data-bind="attr: { title: name }"> + <input type="checkbox" + data-bind="checked: $parent.actorHasRole($data), enable: $parent.canAssignRoleToActor($data)"> + <span data-bind="text: displayName"></span> + </label> + </li> + </ul> + <!-- /ko --> +</script> \ No newline at end of file diff --git a/extras/modules/role-editor/role-editor.css b/extras/modules/role-editor/role-editor.css new file mode 100644 index 0000000..5484216 --- /dev/null +++ b/extras/modules/role-editor/role-editor.css @@ -0,0 +1,608 @@ +#rex-loading-message { + margin-top: 10px; } + +#rex-main-ui { + display: flex; + flex-direction: row; + margin-top: 10px; + width: 100%; } + +#rex-content-container, +#rex-action-sidebar { + border: 1px solid #ccd0d4; + background: #fff; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); } + +#rex-content-container { + display: flex; + flex-grow: 80; + padding: 0; + overflow-x: hidden; } + +#rex-action-sidebar { + box-sizing: border-box; + width: 170px; + flex-grow: 0; + flex-shrink: 0; + align-self: flex-start; + margin-left: 15px; + padding: 10px 8px; } + #rex-action-sidebar .rex-action-separator { + height: 10px; } + +#rex-category-sidebar { + width: 240px; + flex-grow: 0; + flex-shrink: 0; + position: relative; + border-right: 1px solid #ccd0d4; + padding: 10px 0; + background: #f8f8f8; } + #rex-category-sidebar > ul { + margin-top: 0; } + #rex-category-sidebar .rex-nav-item { + cursor: pointer; + margin: 0; + padding: 3px 8px 3px 10px; } + #rex-category-sidebar .rex-nav-item:hover { + background-color: #E5F3FF; } + #rex-category-sidebar .rex-selected-nav-item { + background-color: #CCE8FF; + box-shadow: 0px -1px 0px 0px #99D1FF, 0px 1px 0px 0px #99D1FF; } + #rex-category-sidebar .rex-selected-nav-item:hover { + background-color: #CCE8FF; } + #rex-category-sidebar .rex-nav-level-2 { + padding-left: 0px; } + #rex-category-sidebar .rex-nav-level-3 { + padding-left: 13px; } + #rex-category-sidebar .rex-nav-level-4 { + padding-left: 26px; } + #rex-category-sidebar .rex-nav-level-5 { + padding-left: 39px; } + #rex-category-sidebar .rex-nav-toggle { + visibility: hidden; + display: inline-block; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + max-height: 100%; + width: 20px; + text-align: right; + vertical-align: middle; } + #rex-category-sidebar .rex-nav-toggle:after { + font-family: dashicons, sans-serif; + content: "\f345"; } + #rex-category-sidebar .rex-nav-toggle:hover { + color: #3ECEF9; } + #rex-category-sidebar .rex-nav-is-expanded .rex-nav-toggle:after { + content: "\f347"; } + #rex-category-sidebar .rex-nav-has-children .rex-nav-toggle { + visibility: visible; } + #rex-category-sidebar .rex-dropdown-trigger { + position: absolute; + right: 0; + top: 0; + padding: 12px 10px 3px 8px; } + #rex-category-sidebar .rex-nav-item { + display: flex; + flex-wrap: nowrap; + align-items: baseline; + height: 21px; + padding-top: 4px; + padding-bottom: 2px; } + #rex-category-sidebar .rex-nav-item .rex-nav-toggle { + flex-shrink: 0; + margin-right: 0.3em; + align-self: stretch; + padding: 1px 0; } + #rex-category-sidebar .rex-nav-item .rex-capability-count { + flex-shrink: 0; + margin-left: 0.3em; + margin-right: 0.3em; } + #rex-category-sidebar .rex-nav-item .rex-nav-item-header { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; } + +#rex-capability-view-container { + flex-grow: 70; + padding: 10px 10px; + overflow-x: hidden; } + +#rex-capability-view { + width: 100%; + display: flex; + flex-direction: row; + flex-wrap: wrap; } + +.rex-category { + box-sizing: border-box; + min-width: 160px; + width: 250px; + flex-grow: 0; + flex-shrink: 0; + flex-basis: auto; + padding: 0; + margin: 0 16px 16px 0; + border: 1px solid #ccd0d4; } + .rex-category .rex-category-name { + font-weight: 600; } + .rex-category .rex-category-subheading { + display: none; + color: #666; + font-size: 12px; + font-variant: small-caps; } + .rex-category .rex-category-subtitle { + color: #888; + font-size: 0.95em; + font-family: Consolas, Monaco, monospace; } + .rex-category .rex-category-contents { + box-sizing: border-box; + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + padding: 10px; } + .rex-category.rex-has-subcategories { + width: 100%; + flex-basis: 100%; } + .rex-category .rex-category-header { + padding: 8px 10px; + border-bottom: 1px solid #ccd0d4; } + .rex-category.rex-top-category { + border: none; + margin: 0 0 10px 0; + padding: 0; } + .rex-category.rex-top-category > .rex-category-header { + color: #23282d; + font-size: 1.3em; + margin: 1em 0; + padding: 0; + border-bottom: none; } + .rex-category.rex-top-category > .rex-category-contents { + padding: 0; } + .rex-category.rex-sub-category { + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); } + .rex-category.rex-sub-category > .rex-category-header { + background: #fafafa; } + +.rex-desired-columns-1 { + width: 250px; + flex-grow: 0; + max-width: 500px; } + +.rex-desired-columns-2 { + width: 516px; + flex-grow: 0; + max-width: 1032px; } + +.rex-desired-columns-3 { + width: 782px; + flex-grow: 0; + max-width: 1564px; } + +.rex-desired-columns-max { + flex-basis: 100%; + width: 100%; } + +@media screen and (max-width: 1432px) { + .rex-desired-columns-3 { + flex-basis: 100%; + width: 100%; } } +@media screen and (max-width: 1168px) { + .rex-desired-columns-2 { + flex-basis: 100%; + width: 100%; } + + .rex-desired-columns-3 { + flex-basis: 100%; + width: 100%; } } +.rex-full-width-categories .rex-category { + width: 100%; + max-width: unset; } +.rex-full-width-categories .rex-desired-columns-1 > .rex-category-contents > .rex-permission-list { + column-count: 1; + max-width: 300px; } + +/* + * Ensure that each category contains no more than the desired number of columns. + * This is done by adding an invisible space filler element to the end of the permission list. + * + * Warning: This hack is not perfect. It can allow n+1 columns sometimes. + */ +@media screen and (min-width: 1292px) and (max-width: 1501px) { + .rex-full-width-categories .rex-desired-columns-2 > .rex-category-contents > .rex-permission-list::after { + content: 'filler'; + display: block; + background: yellowgreen; + font-size: 13px; + height: 81px; + visibility: hidden; } } +@media screen and (min-width: 1502px) and (max-width: 1711px) { + .rex-full-width-categories .rex-desired-columns-2 > .rex-category-contents > .rex-permission-list::after { + content: 'filler'; + display: block; + background: yellowgreen; + font-size: 13px; + height: 162px; + visibility: hidden; } + .rex-full-width-categories .rex-desired-columns-3 > .rex-category-contents > .rex-permission-list::after { + content: 'filler'; + display: block; + background: yellowgreen; + font-size: 13px; + height: 81px; + visibility: hidden; } } +@media screen and (min-width: 1712px) { + .rex-full-width-categories .rex-desired-columns-2 > .rex-category-contents > .rex-permission-list::after { + content: 'filler'; + display: block; + background: yellowgreen; + font-size: 13px; + height: 243px; + visibility: hidden; } + .rex-full-width-categories .rex-desired-columns-3 > .rex-category-contents > .rex-permission-list::after { + content: 'filler'; + display: block; + background: yellowgreen; + font-size: 13px; + height: 162px; + visibility: hidden; } + .rex-full-width-categories .rex-desired-columns-4 > .rex-category-contents > .rex-permission-list::after { + content: 'filler'; + display: block; + background: yellowgreen; + font-size: 13px; + height: 81px; + visibility: hidden; } } +.rex-show-category-subheadings .rex-category .rex-category-subheading { + display: block; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } + +.rex-capability-count { + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; + font-size: 12px; } + .rex-capability-count:before { + content: "("; } + .rex-capability-count:after { + content: ")"; } + +.rex-enabled-capability-count + .rex-total-capability-count:before { + content: "/"; } + +.rex-permission-list { + box-sizing: border-box; + width: 100%; + columns: 200px; + column-gap: 10px; } + +.rex-permission { + box-sizing: border-box; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 13px; + height: 27px; + vertical-align: baseline; + break-inside: avoid-column; + display: flex; } + .rex-permission label, .rex-permission .rex-permission-tip-trigger { + vertical-align: baseline; + padding-top: 3px; + padding-bottom: 3px; } + .rex-permission label { + flex-grow: 1; + flex-shrink: 1; + flex-basis: 50px; + overflow: hidden; + text-overflow: ellipsis; } + .rex-permission .rex-permission-tip-trigger { + flex-grow: 0; + flex-shrink: 0; + flex-basis: 20px; } + +.rex-is-redundant { + color: #888; } + +.rex-is-explicitly-denied input[type=checkbox] { + border-color: red; } + +.rex-is-personal-override.rex-is-explicitly-denied input[type=checkbox] { + background-color: #ffe5e5; } +.rex-is-personal-override input[type=checkbox]:checked { + background-color: #d9ffd9; + border-color: green; } + +.rex-permission-tip-trigger { + visibility: hidden; + display: inline-block; + min-width: 20px; + height: 100%; + margin: 0; + padding-left: 2px; + cursor: pointer; + color: #777; } + .rex-permission-tip-trigger:hover { + color: #0096dd; } + +.rex-permission:hover { + background-color: #fafafa; } + .rex-permission:hover .rex-permission-tip-trigger { + visibility: visible; } + +.rex-tooltip { + max-width: 700px; } + .rex-tooltip .rex-tooltip-section-container { + display: flex; + flex-direction: column; + flex-wrap: nowrap; } + .rex-tooltip .rex-tooltip-section { + max-width: 400px; } + +#rex-permission-tip { + overflow-y: auto; + max-height: 600px; } + #rex-permission-tip h4 { + margin-bottom: 0.4em; } + #rex-permission-tip .rex-tip-granted-permissions { + list-style: disc inside; + margin-top: 0; + margin-bottom: 0; } + #rex-permission-tip .rex-documentation-link { + display: inline-block; + max-width: 100%; + overflow-wrap: break-word; } + +.rex-capability-inheritance-breakdown tbody tr:nth-child(2n+1) { + background-color: #F9F9F9; } +.rex-capability-inheritance-breakdown .rex-is-decisive-actor td:first-child:after { + content: "\1f844"; + display: inline-block; + font-weight: bold; + margin-left: 0.5em; } + +#rex-view-toolbar { + background: #fcfcfc; + border-bottom: 1px solid #ddd; + padding: 0 8px 10px 8px; + margin: -10px -10px 0 -10px; + display: flex; + align-items: center; + flex-wrap: wrap; } + #rex-view-toolbar > * { + margin-top: 10px; } + #rex-view-toolbar .button { + vertical-align: middle; } + #rex-view-toolbar > label { + margin-right: 10px; } + #rex-view-toolbar .rex-dropdown-trigger .dashicons { + line-height: 1.3; } + +#rex-quick-search-query { + min-width: 250px; + max-width: 100%; + margin-right: 10px; } + +#rex-misc-view-options-button { + margin-left: auto; + margin-right: 10px; } + +.rex-search-highlight { + background-color: #ffff00; } + +.rex-permission-table th input[type="checkbox"] { + vertical-align: middle; + margin: -4px 4px -1px 0; } +.rex-permission-table tbody tr:nth-child(2n+1) { + background-color: #F9F9F9; } +.rex-permission-table td ul { + margin: 0; } +.rex-permission-table .rex-base-cap-notice { + color: #888; } + +/* Switch to fixed layout in narrow viewports to prevent overflow. */ +@media screen and (max-width: 1540px) { + .rex-permission-table { + table-layout: fixed; + max-width: 100%; } + .rex-permission-table .rex-category-name-column { + width: 20%; } + + .rex-readable-names-enabled .rex-permission-table { + table-layout: fixed; + max-width: 100%; } + .rex-readable-names-enabled .rex-permission-table .rex-category-name-column { + width: 25%; } } +/* The taxonomy table needs a wider screen because it has more columns. */ +@media screen and (max-width: 1650px) { + #rex-taxonomy-summary-category .rex-permission-table { + table-layout: fixed; + max-width: 100%; } + #rex-taxonomy-summary-category .rex-permission-table .rex-category-name-column { + width: 25%; } } +/* +When in "human readable" mode, the taxonomy table doesn't show capability names, +so it won't overflow its container unless the viewport is very small. +*/ +.rex-readable-names-enabled #rex-taxonomy-summary-category .rex-permission-table { + table-layout: auto; + max-width: 600px; } + .rex-readable-names-enabled #rex-taxonomy-summary-category .rex-permission-table .rex-capability-name, .rex-readable-names-enabled #rex-taxonomy-summary-category .rex-permission-table .rex-permission-tip-trigger { + display: none; } + .rex-readable-names-enabled #rex-taxonomy-summary-category .rex-permission-table .rex-permission, .rex-readable-names-enabled #rex-taxonomy-summary-category .rex-permission-table th[scope="col"] { + text-align: center; } + .rex-readable-names-enabled #rex-taxonomy-summary-category .rex-permission-table .rex-category-name-column { + width: unset; } + +@media screen and (max-width: 1200px) { + .rex-readable-names-enabled #rex-taxonomy-summary-category .rex-permission-table { + table-layout: fixed; + max-width: 100%; } + .rex-readable-names-enabled #rex-taxonomy-summary-category .rex-permission-table .rex-category-name-column { + width: 40%; } } +#rex-action-sidebar .rex-action-button { + display: block; + margin-bottom: 4px; + width: 100%; } + +#rex-permission-list-view { + column-width: 240px; + column-gap: 16px; + padding-top: 8px; } + +#rex-category-view-spacer { + width: 100%; + height: 10px; } + +.rex-dropdown-trigger { + display: inline-block; + box-sizing: border-box; + cursor: pointer; + padding: 2px; + color: #aaa; + text-decoration: none; } + .rex-dropdown-trigger:hover, .rex-dropdown-trigger:focus { + color: #777; + text-decoration: none; } + +.rex-dropdown { + position: absolute; + border: 1px solid #ccd0d4; + background: #fff; + box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + padding: 10px 8px; + z-index: 100; } + .rex-dropdown .rex-dropdown-item { + display: block; + margin-bottom: 10px; } + .rex-dropdown .rex-dropdown-item:last-child { + margin-bottom: 0; } + .rex-dropdown .rex-dropdown-sub-item { + margin-left: 1em; } + .rex-dropdown .rex-dropdown-item > .rex-dropdown-item { + margin-bottom: 6px; } + .rex-dropdown .rex-dropdown-item > .rex-dropdown-item:last-child { + margin-bottom: 0; } + +.ui-dialog .ui-dialog-buttonpane { + background: #fcfcfc; + border-top: 1px solid #dfdfdf; + padding: 8px; } + .ui-dialog .ui-dialog-buttonpane:after { + clear: both; + content: ""; + min-height: 0; + display: table; + border-collapse: collapse; } +.ui-dialog .ui-dialog-buttonset { + width: 100%; } + .ui-dialog .ui-dialog-buttonset .ui-button.rex-dialog-cancel-button, .ui-dialog .ui-dialog-buttonset .ui-button.ame-dialog-cancel-button { + float: right; + margin-right: 0 !important; } + .ui-dialog .ui-dialog-buttonset .ui-button { + float: left; } + +.rex-dialog input[type=text], .rex-dialog select { + box-sizing: border-box; + display: block; + width: 100%; } + +.rex-dialog-section { + margin-top: 0; } + +#rex-delete-capability-dialog .rex-deletable-capability-container { + max-height: 400px; + overflow-y: auto; } +#rex-delete-capability-dialog .rex-deletable-capability-list { + margin-top: 0; + list-style-type: none; } + +#rex-add-capability-dialog #rex-new-capability-name { + box-sizing: border-box; + width: 100%; } +#rex-add-capability-dialog #rex-add-capability-validation-message { + min-height: 40px; + margin-bottom: 6px; } + +#rex-delete-role-dialog .rex-deletable-role-list-container { + max-height: 380px; + overflow-y: auto; + margin-top: 10px; } +#rex-delete-role-dialog .rex-deletable-role-list { + table-layout: fixed; } + #rex-delete-role-dialog .rex-deletable-role-list tbody tr:nth-child(2n+1) { + background-color: #F9F9F9; } +#rex-delete-role-dialog .rex-role-name-column > label { + display: inline-block; + width: 100%; } +#rex-delete-role-dialog .rex-role-usage-column { + width: 6em; + max-width: 30%; + color: #888; + text-align: right; } + +#rex-editable-roles-container { + display: flex; } + #rex-editable-roles-container .ame-role-table { + min-width: 190px; + border: 1px solid #ccd0d4; + border-right-style: none; } + #rex-editable-roles-container .ame-role-table td { + cursor: pointer; } + #rex-editable-roles-container .ame-selected-role-table-row { + background: #CCE8FF; } + #rex-editable-roles-container .ame-selected-role-table-row .ame-selected-role-tip { + visibility: visible; } + #rex-editable-roles-container .ame-selected-role-table-row .ame-column-role-name { + font-weight: bold; } + #rex-editable-roles-container .ame-column-selected-role-tip { + position: relative; + padding: 0; + min-width: 30px; } + #rex-editable-roles-container .ame-selected-role-tip { + visibility: hidden; + height: 100%; + width: 100%; + box-sizing: border-box; + position: absolute; + top: 0; + right: -2px; + border-right: 1px solid white; } + #rex-editable-roles-container .ame-selected-role-tip .ame-rex-svg-triangle { + box-sizing: border-box; + position: absolute; + right: 0; + height: 100%; } + #rex-editable-roles-container .ame-selected-role-tip .ame-rex-svg-triangle polygon { + fill: white; + stroke: white; + stroke-width: 1px; } + +#rex-editable-roles-options { + padding: 4px 10px 10px 10px; + border: 1px solid #ccd0d4; } + #rex-editable-roles-options fieldset > p:first-of-type { + margin-top: 0; } + +#rex-editable-role-list { + margin-left: 1em; + margin-top: 0; } + +#rex-user-role-list { + border-right: 1px solid #ccd0d4; + padding: 10px 8px; + background: #f8f8f8; } + #rex-user-role-list p:first-child { + margin-top: 0; } + +#rex-primary-user-role { + display: block; } + +.rex-user-role-option-list { + margin-top: 0; } + +/*# sourceMappingURL=role-editor.css.map */ diff --git a/extras/modules/role-editor/role-editor.css.map b/extras/modules/role-editor/role-editor.css.map new file mode 100644 index 0000000..e58a17b --- /dev/null +++ b/extras/modules/role-editor/role-editor.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAwBA,oBAAqB;EACpB,UAAU,EAAE,IAAI;;AAGjB,YAAa;EACZ,OAAO,EAAE,IAAI;EACb,cAAc,EAAE,GAAG;EACnB,UAAU,EAAE,IAAI;EAChB,KAAK,EAAE,IAAI;;AAGZ;mBACoB;EAvBnB,MAAM,EAXK,iBAAgC;EAY3C,UAAU,EAAE,IAAI;EAChB,UAAU,ECdQ,6BAA6B;;ADuChD,sBAAuB;EACtB,OAAO,EAAE,IAAI;EACb,SAAS,EAAE,EAAE;EACb,OAAO,EAAE,CAAC;EACV,UAAU,EAAE,MAAM;;AAGnB,mBAAoB;EACnB,UAAU,EAAE,UAAU;EACtB,KAAK,EAAE,KAAK;EACZ,SAAS,EAAE,CAAC;EACZ,WAAW,EAAE,CAAC;EACd,UAAU,EAAE,UAAU;EAEtB,WAAW,EAAE,IAAI;EACjB,OAAO,EAlDK,QAA8B;EAoD1C,yCAAsB;IACrB,MAAM,EAAE,IAAI;;AAId,qBAAsB;EACrB,KAAK,EAAE,KAAK;EACZ,SAAS,EAAE,CAAC;EACZ,WAAW,EAAE,CAAC;EACd,QAAQ,EAAE,QAAQ;EAElB,YAAY,EAlED,iBAAgC;EAmE3C,OAAO,EAAE,MAAgB;EAEzB,UAAU,EAAE,OAAO;EAEnB,0BAAO;IACN,UAAU,EAAE,CAAC;EAGd,mCAAc;IACb,MAAM,EAAE,OAAO;IACf,MAAM,EAAE,CAAC;IACT,OAAO,EAAE,gBAAoD;EAG9D,yCAAoB;IACnB,gBAAgB,EAAE,OAAO;EAG1B,4CAAuB;IACtB,gBAAgB,EAAE,OAAO;IACzB,UAAU,EAAE,iDAAiD;IAG7D,kDAAQ;MACP,gBAAgB,EAAE,OAAO;EAM1B,sCAAyB;IACxB,YAAY,EAAE,GAAmC;EADlD,sCAAyB;IACxB,YAAY,EAAE,IAAmC;EADlD,sCAAyB;IACxB,YAAY,EAAE,IAAmC;EADlD,sCAAyB;IACxB,YAAY,EAAE,IAAmC;EAInD,qCAAgB;IACf,UAAU,EAAE,MAAM;IAClB,OAAO,EAAE,YAAY;IAErB,kBAAkB,EAAE,UAAU;IAC9B,eAAe,EAAE,UAAU;IAC3B,UAAU,EAAE,UAAU;IAEtB,UAAU,EAAE,IAAI;IAChB,KAAK,EAAE,IAAI;IAEX,UAAU,EAAE,KAAK;IACjB,cAAc,EAAE,MAAM;IAEtB,2CAAQ;MACP,WAAW,EAAE,qBAAqB;MAClC,OAAO,EAAE,OAAO;IAGjB,2CAAQ;MACP,KAAK,EAAE,OAAO;EAMf,gEAAQ;IACP,OAAO,EAAE,OAAO;EAIlB,2DAAsC;IACrC,UAAU,EAAE,OAAO;EAGpB,2CAAsB;IACrB,QAAQ,EAAE,QAAQ;IAClB,KAAK,EAAE,CAAC;IACR,GAAG,EAAE,CAAC;IAEN,OAAO,EAAE,iBAA8D;EAIxE,mCAAc;IACb,OAAO,EAAE,IAAI;IACb,SAAS,EAAE,MAAM;IACjB,WAAW,EAAE,QAAQ;IAErB,MAAM,EAAE,IAAI;IACZ,WAAW,EAAE,GAAG;IAChB,cAAc,EAAE,GAAG;IAInB,mDAAgB;MACf,WAAW,EAAE,CAAC;MACd,YAAY,EAJA,KAAK;MAKjB,UAAU,EAAE,OAAO;MACnB,OAAO,EAAE,KAAK;IAGf,yDAAsB;MACrB,WAAW,EAAE,CAAC;MACd,WAAW,EAXC,KAAK;MAYjB,YAAY,EAZA,KAAK;IAelB,wDAAqB;MACpB,WAAW,EAAE,MAAM;MACnB,aAAa,EAAE,QAAQ;MACvB,QAAQ,EAAE,MAAM;;AAKnB,8BAA+B;EAC9B,SAAS,EAAE,EAAE;EACb,OAAO,EAAE,SAAwD;EACjE,UAAU,EAAE,MAAM;;AAGnB,oBAAqB;EACpB,KAAK,EAAE,IAAI;EAEX,OAAO,EAAE,IAAI;EACb,cAAc,EAAE,GAAG;EACnB,SAAS,EAAE,IAAI;;AAMhB,aAAc;EACb,UAAU,EAAE,UAAU;EACtB,SAAS,EAAE,KAAK;EAChB,KAAK,EANa,KAAK;EAQvB,SAAS,EAAE,CAAC;EACZ,WAAW,EAAE,CAAC;EACd,UAAU,EAAE,IAAI;EAEhB,OAAO,EAAE,CAAC;EACV,MAAM,EAAE,aAAuC;EAE/C,MAAM,EAAE,iBAAgC;EAExC,gCAAmB;IAClB,WAAW,EAAE,GAAG;EAGjB,sCAAyB;IACxB,OAAO,EAAE,IAAI;IACb,KAAK,EAAE,IAAI;IACX,SAAS,EAAE,IAAI;IACf,YAAY,EAAE,UAAU;EAGzB,oCAAuB;IACtB,KAAK,EAAE,IAAI;IACX,SAAS,EAAE,MAAM;IACjB,WAAW,EAAE,2BAA2B;EAGzC,oCAAuB;IACtB,UAAU,EAAE,UAAU;IAEtB,OAAO,EAAE,IAAI;IACb,SAAS,EAAE,IAAI;IACf,eAAe,EAAE,UAAU;IAE3B,OAAO,EAAE,IAAI;EAGd,mCAAwB;IACvB,KAAK,EAAE,IAAI;IACX,UAAU,EAAE,IAAI;EAGjB,kCAAqB;IACpB,OAAO,EAAE,QAAQ;IACjB,aAAa,EAAE,iBAAgC;EAGhD,8BAAmB;IAClB,MAAM,EAAE,IAAI;IACZ,MAAM,EAAE,UAAU;IAClB,OAAO,EAAE,CAAC;IAEV,qDAAyB;MACxB,KAAK,EAAE,OAAO;MACd,SAAS,EAAE,KAAK;MAChB,MAAM,EAAE,KAAK;MACb,OAAO,EAAE,CAAC;MACV,aAAa,EAAE,IAAI;IAGpB,uDAA2B;MAC1B,OAAO,EAAE,CAAC;EAIZ,8BAAmB;IAClB,UAAU,EC1QO,6BAA6B;ID4Q9C,qDAAyB;MACxB,UAAU,EAAE,OAAO;;AAWrB,sBAA8B;EAE7B,KAAK,EADU,KAA2B;EAE1C,SAAS,EAAE,CAAC;EACZ,SAAS,EAAE,KAAiB;;AAJ7B,sBAA8B;EAE7B,KAAK,EADU,KAA2B;EAE1C,SAAS,EAAE,CAAC;EACZ,SAAS,EAAE,MAAiB;;AAJ7B,sBAA8B;EAE7B,KAAK,EADU,KAA2B;EAE1C,SAAS,EAAE,CAAC;EACZ,SAAS,EAAE,MAAiB;;AAI9B,wBAAyB;EACxB,UAAU,EAAE,IAAI;EAChB,KAAK,EAAE,IAAI;;AAKZ,qCAAsC;EAKnC,sBAA8B;IAC7B,UAAU,EAAE,IAAI;IAChB,KAAK,EAAE,IAAI;AAMf,qCAAsC;EAKnC,sBAA8B;IAC7B,UAAU,EAAE,IAAI;IAChB,KAAK,EAAE,IAAI;;EAFZ,sBAA8B;IAC7B,UAAU,EAAE,IAAI;IAChB,KAAK,EAAE,IAAI;AASd,wCAAc;EACb,KAAK,EAAE,IAAI;EACX,SAAS,EAAE,KAAK;AAGjB,iGAAuE;EACtE,YAAY,EAAE,CAAC;EACf,SAAS,EAAE,KAAK;;AAIlB;;;;;GAKG;AAqBH,6DAA8D;EAd1D,wGAAqF;IACpF,OAAO,EAAE,QAAQ;IACjB,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,WAAW;IACvB,SAAS,EAAE,IAAI;IAEf,MAAM,EAAE,IAA4E;IACpF,UAAU,EAAE,MAAM;AAWvB,6DAA8D;EAlB1D,wGAAqF;IACpF,OAAO,EAAE,QAAQ;IACjB,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,WAAW;IACvB,SAAS,EAAE,IAAI;IAEf,MAAM,EAAE,KAA4E;IACpF,UAAU,EAAE,MAAM;EAPnB,wGAAqF;IACpF,OAAO,EAAE,QAAQ;IACjB,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,WAAW;IACvB,SAAS,EAAE,IAAI;IAEf,MAAM,EAAE,IAA4E;IACpF,UAAU,EAAE,MAAM;AAevB,qCAAsC;EAtBlC,wGAAqF;IACpF,OAAO,EAAE,QAAQ;IACjB,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,WAAW;IACvB,SAAS,EAAE,IAAI;IAEf,MAAM,EAAE,KAA4E;IACpF,UAAU,EAAE,MAAM;EAPnB,wGAAqF;IACpF,OAAO,EAAE,QAAQ;IACjB,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,WAAW;IACvB,SAAS,EAAE,IAAI;IAEf,MAAM,EAAE,KAA4E;IACpF,UAAU,EAAE,MAAM;EAPnB,wGAAqF;IACpF,OAAO,EAAE,QAAQ;IACjB,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,WAAW;IACvB,SAAS,EAAE,IAAI;IAEf,MAAM,EAAE,IAA4E;IACpF,UAAU,EAAE,MAAM;AAqBvB,qEAAsE;EACrE,OAAO,EAAE,KAAK;EACd,KAAK,EAAE,IAAI;EAEX,WAAW,EAAE,MAAM;EACnB,QAAQ,EAAE,MAAM;EAChB,aAAa,EAAE,QAAQ;;AAGxB,qBAAsB;EAOrB,qBAAqB,EAAE,IAAI;EAC3B,kBAAkB,EAAE,IAAI;EACxB,aAAa,EAAE,IAAI;EAEnB,SAAS,EAAE,IAAI;EAUf,4BAAS;IACR,OAAO,EAAE,GAAG;EAGb,2BAAQ;IACP,OAAO,EAAE,GAAG;;AAKb,kEAAS;EACR,OAAO,EAAE,GAAG;;AAId,oBAAqB;EACpB,UAAU,EAAE,UAAU;EACtB,KAAK,EAAE,IAAI;EAEX,OAAO,EAAE,KAAK;EACd,UAAU,EAAE,IAAI;;AAQjB,eAAgB;EACf,UAAU,EAAE,UAAU;EAEtB,WAAW,EAAE,MAAM;EACnB,QAAQ,EAAE,MAAM;EAChB,aAAa,EAAE,QAAQ;EAEvB,SAAS,EAAE,IAAI;EACf,MAAM,EAhbe,IAAI;EAibzB,cAAc,EAAE,QAAQ;EAExB,YAAY,EAAE,YAAY;EAE1B,OAAO,EAAE,IAAI;EAEb,kEAAmC;IAClC,cAAc,EAAE,QAAQ;IACxB,WAAW,EAAE,GAAG;IAChB,cAAc,EAAE,GAAG;EAGpB,qBAAM;IACL,SAAS,EAAE,CAAC;IACZ,WAAW,EAAE,CAAC;IACd,UAAU,EAAE,IAAI;IAEhB,QAAQ,EAAE,MAAM;IAChB,aAAa,EAAE,QAAQ;EAGxB,2CAA4B;IAC3B,SAAS,EAAE,CAAC;IACZ,WAAW,EAAE,CAAC;IACd,UAAU,EAAE,IAAI;;AAIlB,iBAAkB;EACjB,KAAK,EAAE,IAAI;;AAGZ,8CAA+C;EAC9C,YAAY,EAAE,GAAG;;AAQjB,uEAAgD;EAJhD,gBAAgB,EAKY,OAAO;AAGnC,sDAA6B;EAR7B,gBAAgB,EASY,OAAO;EAClC,YAAY,EAAE,KAAK;;AASrB,2BAA4B;EAE3B,UAAU,EAAE,MAAM;EAElB,OAAO,EAAE,YAAY;EACrB,SAAS,EAAE,IAAI;EACf,MAAM,EAAE,IAAI;EACZ,MAAM,EAAE,CAAC;EACT,YAAY,EAAE,GAAG;EAEjB,MAAM,EAAE,OAAO;EACf,KAAK,EAAE,IAAI;EAEX,iCAAQ;IACP,KAAK,EAAE,OAAO;;AAIhB,qBAAsB;EACrB,gBAAgB,EAAE,OAAO;EAEzB,iDAA4B;IAC3B,UAAU,EAAE,OAAO;;AAIrB,YAAa;EACZ,SAAS,EAAE,KAAK;EAEhB,2CAA+B;IAC9B,OAAO,EAAE,IAAI;IACb,cAAc,EAAE,MAAM;IACtB,SAAS,EAAE,MAAM;EAGlB,iCAAqB;IACpB,SAAS,EAAE,KAAK;;AAIlB,mBAAoB;EACnB,UAAU,EAAE,IAAI;EAChB,UAAU,EAAE,KAAK;EAEjB,sBAAG;IACF,aAAa,EAAE,KAAK;EAGrB,gDAA6B;IAC5B,UAAU,EAAE,WAAW;IACvB,UAAU,EAAE,CAAC;IACb,aAAa,EAAE,CAAC;EAGjB,2CAAwB;IACvB,OAAO,EAAE,YAAY;IACrB,SAAS,EAAE,IAAI;IACf,aAAa,EAAE,UAAU;;AAzhB1B,8DAAyB;EACxB,gBAAgB,EAAE,OAAO;AAgiBzB,iFAAqB;EACpB,OAAO,EAAE,QAAQ;EACjB,OAAO,EAAE,YAAY;EACrB,WAAW,EAAE,IAAI;EACjB,WAAW,EAAE,KAAK;;AAOrB,iBAAkB;EACjB,UAAU,EAAE,OAAO;EACnB,aAAa,EAAE,cAAc;EAE7B,OAAO,EAAE,cAAgD;EACzD,MAAM,EAAE,mBAAkG;EAE1G,OAAO,EAAE,IAAI;EACb,WAAW,EAAE,MAAM;EACnB,SAAS,EAAE,IAAI;EAEf,qBAAM;IACL,UAAU,EAxkBI,IAAI;EA2kBnB,yBAAQ;IACP,cAAc,EAAE,MAAM;EAGvB,yBAAQ;IACP,YAAY,EAAE,IAAI;EAGnB,kDAAiC;IAChC,WAAW,EAAE,GAAG;;AAIlB,uBAAwB;EACvB,SAAS,EAAE,KAAK;EAChB,SAAS,EAAE,IAAI;EACf,YAAY,EAAE,IAAI;;AAGnB,6BAA8B;EAC7B,WAAW,EAAE,IAAI;EACjB,YAAY,EAAE,IAAI;;AAQnB,qBAAsB;EACrB,gBAAgB,EAAE,OAAO;;AAKzB,+CAA0B;EACzB,cAAc,EAAE,MAAM;EACtB,MAAM,EAAE,eAAe;AAhmBxB,8CAAyB;EACxB,gBAAgB,EAAE,OAAO;AAomB1B,2BAAM;EACL,MAAM,EAAE,CAAC;AAGV,0CAAqB;EACpB,KAAK,EAAE,IAAI;;AAIb,qEAAqE;AAUrE,qCAAsC;EACrC,qBAAsB;IATtB,YAAY,EAAE,KAAK;IACnB,SAAS,EAAE,IAAI;IAEf,+CAA0B;MACzB,KAAK,EAMqB,GAAG;;EAG9B,iDAAkD;IAblD,YAAY,EAAE,KAAK;IACnB,SAAS,EAAE,IAAI;IAEf,2EAA0B;MACzB,KAAK,EAUqB,GAAG;AAI/B,0EAA0E;AAC1E,qCAAsC;EACrC,oDAAqD;IApBrD,YAAY,EAAE,KAAK;IACnB,SAAS,EAAE,IAAI;IAEf,8EAA0B;MACzB,KAAK,EAiBqB,GAAG;AAI/B;;;EAGE;AACF,gFAAiF;EAChF,YAAY,EAAE,IAAI;EAClB,SAAS,EAAE,KAAK;EAEhB,mNAAkD;IACjD,OAAO,EAAE,IAAI;EAGd,kMAAiC;IAChC,UAAU,EAAE,MAAM;EAGnB,0GAA0B;IACzB,KAAK,EAAE,KAAK;;AAId,qCAAsC;EACrC,gFAAiF;IA/CjF,YAAY,EAAE,KAAK;IACnB,SAAS,EAAE,IAAI;IAEf,0GAA0B;MACzB,KAAK,EA4CqB,GAAG;AAO9B,sCAAmB;EAClB,OAAO,EAAE,KAAK;EACd,aAAa,EAAE,GAAG;EAClB,KAAK,EAAE,IAAI;;AAIb,yBAA0B;EACzB,YAAY,EAAE,KAAK;EACnB,UAAU,EAAE,IAAI;EAChB,WAAW,EAhsBK,GAAG;;AAmsBpB,yBAA0B;EACzB,KAAK,EAAE,IAAI;EACX,MAAM,EAjsBsB,IAAc;;AAosB3C,qBAAsB;EACrB,OAAO,EAAE,YAAY;EACrB,UAAU,EAAE,UAAU;EACtB,MAAM,EAAE,OAAO;EAEf,OAAO,EAAE,GAAG;EACZ,KAAK,EAAE,IAAI;EACX,eAAe,EAAE,IAAI;EAErB,wDAAiB;IAChB,KAAK,EAAE,IAAI;IACX,eAAe,EAAE,IAAI;;AAIvB,aAAc;EACb,QAAQ,EAAE,QAAQ;EAElB,MAAM,EA5tBK,iBAAgC;EA6tB3C,UAAU,EAAE,IAAI;EAChB,UAAU,EAAE,4BAA4B;EAExC,OAAO,EA7tBK,QAA8B;EA8tB1C,OAAO,EAAE,GAAG;EAEZ,gCAAmB;IAClB,OAAO,EAAE,KAAK;IACd,aAAa,EAAE,IAAI;IAEnB,2CAAa;MACZ,aAAa,EAAE,CAAC;EAIlB,oCAAuB;IACtB,WAAW,EAAE,GAAG;EAGjB,qDAAwC;IACvC,aAAa,EAAE,GAAG;IAElB,gEAAa;MACZ,aAAa,EAAE,CAAC;;AAMlB,gCAAsB;EACrB,UAAU,EAAE,OAAO;EACnB,UAAU,EAAE,iBAAiB;EAC7B,OAAO,EAAE,GAAG;EAEZ,sCAAQ;IACP,KAAK,EAAE,IAAI;IACX,OAAO,EAAE,EAAE;IACX,UAAU,EAAE,CAAC;IACb,OAAO,EAAE,KAAK;IACd,eAAe,EAAE,QAAQ;AAM3B,+BAAqB;EACpB,KAAK,EAAE,IAAI;EAEX,wIAAyE;IACxE,KAAK,EAAE,KAAK;IACZ,YAAY,EAAE,YAAY;EAG3B,0CAAW;IACV,KAAK,EAAE,IAAI;;AAMb,gDAAyB;EACxB,UAAU,EAAE,UAAU;EACtB,OAAO,EAAE,KAAK;EACd,KAAK,EAAE,IAAI;;AAIb,mBAAoB;EACnB,UAAU,EAAE,CAAC;;AAIb,iEAAoC;EACnC,UAAU,EAAE,KAAK;EACjB,UAAU,EAAE,IAAI;AAGjB,4DAA+B;EAC9B,UAAU,EAAE,CAAC;EACb,eAAe,EAAE,IAAI;;AAKtB,mDAAyB;EACxB,UAAU,EAAE,UAAU;EACtB,KAAK,EAAE,IAAI;AAGZ,iEAAuC;EACtC,UAAU,EAAE,IAAI;EAChB,aAAa,EAAE,GAAG;;AAKnB,0DAAmC;EAClC,UAAU,EAAE,KAAK;EAEjB,UAAU,EAAE,IAAI;EAChB,UAAU,EAAE,IAAI;AAGjB,gDAAyB;EACxB,YAAY,EAAE,KAAK;EApzBpB,yEAAyB;IACxB,gBAAgB,EAAE,OAAO;AAuzB1B,qDAA8B;EAC7B,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAI;AAGZ,8CAAuB;EACtB,KAAK,EAAE,GAAG;EACV,SAAS,EAAE,GAAG;EACd,KAAK,EAAE,IAAI;EACX,UAAU,EAAE,KAAK;;AAMnB,6BAA8B;EAC7B,OAAO,EAAE,IAAI;EAEb,6CAAgB;IACf,SAAS,EAAE,KAAK;IAChB,MAAM,EAAE,iBAAqC;IAC7C,kBAAkB,EAAE,IAAI;IAExB,gDAAG;MACF,MAAM,EAAE,OAAO;EAMjB,0DAA6B;IAC5B,UAAU,EAAE,OAAO;IAEnB,iFAAuB;MACtB,UAAU,EAAE,OAAO;IAGpB,gFAAsB;MACrB,WAAW,EAAE,IAAI;EAInB,2DAA8B;IAC7B,QAAQ,EAAE,QAAQ;IAClB,OAAO,EAAE,CAAC;IACV,SAAS,EAAE,IAAI;EAGhB,oDAAuB;IACtB,UAAU,EAAE,MAAM;IAElB,MAAM,EAAE,IAAI;IACZ,KAAK,EAAE,IAAI;IACX,UAAU,EAAE,UAAU;IAEtB,QAAQ,EAAE,QAAQ;IAClB,GAAG,EAAE,CAAC;IACN,KAAK,EAAE,IAAI;IAEX,YAAY,EAAE,eAA8B;IAE5C,0EAAsB;MACrB,UAAU,EAAE,UAAU;MACtB,QAAQ,EAAE,QAAQ;MAClB,KAAK,EAAE,CAAC;MACR,MAAM,EAAE,IAAI;MAEZ,kFAAQ;QACP,IAAI,EAxCe,KAAK;QAyCxB,MAAM,EAzCa,KAAK;QA0CxB,YAAY,EAAE,GAAG;;AAMrB,2BAA4B;EAC3B,OAAO,EAAE,kBAAgD;EACzD,MAAM,EAAE,iBAAqC;EAE7C,sDAA2B;IAC1B,UAAU,EAAE,CAAC;;AAIf,uBAAwB;EACvB,WAAW,EAAE,GAAG;EAChB,UAAU,EAAE,CAAC;;AAMd,mBAAoB;EACnB,YAAY,EAv6BD,iBAAgC;EAw6B3C,OAAO,EAr6BK,QAA8B;EAu6B1C,UAAU,EAAE,OAAO;EAEnB,iCAAc;IACb,UAAU,EAAE,CAAC;;AAIf,sBAAuB;EACtB,OAAO,EAAE,KAAK;;AAGf,0BAA2B;EAC1B,UAAU,EAAE,CAAC", +"sources": ["role-editor.scss","../../../css/_boxes.scss"], +"names": [], +"file": "role-editor.css" +} \ No newline at end of file diff --git a/extras/modules/role-editor/role-editor.js b/extras/modules/role-editor/role-editor.js new file mode 100644 index 0000000..521bd0d --- /dev/null +++ b/extras/modules/role-editor/role-editor.js @@ -0,0 +1,3072 @@ +/// <reference path="../../../js/knockout.d.ts" /> +/// <reference path="../../../js/lodash-3.10.d.ts" /> +/// <reference path="../../../js/common.d.ts" /> +/// <reference path="../../../js/actor-manager.ts" /> +/// <reference path="../../../js/jquery.d.ts" /> +/// <reference path="../../../js/jqueryui.d.ts" /> +/// <reference path="../../../modules/actor-selector/actor-selector.ts" /> +/// <reference path="../../ko-extensions.ts" /> +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var RexPermission = /** @class */ (function () { + function RexPermission(editor, capability) { + this.readableAction = null; + this.mainDescription = ''; + this.isRedundant = false; + this.editor = editor; + this.capability = capability; + var self = this; + this.labelHtml = ko.pureComputed({ + read: self.getLabelHtml, + deferEvaluation: true, + owner: this + }); + //Prevent freezing when entering a search query. Highlighting keywords in hundreds of capabilities can be slow. + this.labelHtml.extend({ rateLimit: { timeout: 50, method: "notifyWhenChangesStop" } }); + this.isVisible = ko.computed({ + read: function () { + if (!editor.capabilityMatchesFilters(self.capability)) { + return false; + } + //When in list view, check if the capability is inside the selected category. + if (editor.categoryViewMode() === RexRoleEditor.listView) { + if (!editor.isInSelectedCategory(self.capability.name)) { + return false; + } + } + if (self.capability.isDeleted()) { + return false; + } + return !(self.isRedundant && !editor.showRedundantEnabled()); + }, + owner: this, + deferEvaluation: true + }); + } + RexPermission.prototype.getLabelHtml = function () { + var text; + if ((this.readableAction !== null) && this.editor.readableNamesEnabled()) { + text = this.readableAction; + } + else { + text = this.capability.displayName(); + } + var html = wsAmeLodash.escape(text); + if (this.isVisible()) { + html = this.editor.highlightSearchKeywords(html); + } + //Let the browser break words on underscores. + html = html.replace(/_/g, '_<wbr>'); + return html; + }; + return RexPermission; +}()); +/** + * A basic representation of any component or extension that can add new capabilities. + * This includes plugins, themes, and the WordPress core. + */ +var RexWordPressComponent = /** @class */ (function () { + function RexWordPressComponent(id, name) { + this.id = id; + this.name = name; + } + RexWordPressComponent.fromJs = function (id, details) { + var instance = new RexWordPressComponent(id, details.name ? details.name : id); + if (details.capabilityDocumentationUrl) { + instance.capabilityDocumentationUrl = details.capabilityDocumentationUrl; + } + return instance; + }; + return RexWordPressComponent; +}()); +var RexObservableCapabilityMap = /** @class */ (function () { + function RexObservableCapabilityMap(initialCapabilities) { + this.capabilities = {}; + if (initialCapabilities) { + this.initialCapabilities = wsAmeLodash.clone(initialCapabilities); + } + else { + this.initialCapabilities = {}; + } + } + RexObservableCapabilityMap.prototype.getCapabilityState = function (capabilityName) { + var observable = this.getObservable(capabilityName); + return observable(); + }; + RexObservableCapabilityMap.prototype.setCapabilityState = function (capabilityName, state) { + var observable = this.getObservable(capabilityName); + observable(state); + }; + RexObservableCapabilityMap.prototype.getAllCapabilities = function () { + var _ = wsAmeLodash; + var result = this.initialCapabilities ? _.clone(this.initialCapabilities) : {}; + _.forEach(this.capabilities, function (observable, name) { + var isGranted = observable(); + if (isGranted === null) { + delete result[name]; + } + else { + result[name] = isGranted; + } + }); + return result; + }; + RexObservableCapabilityMap.prototype.getObservable = function (capabilityName) { + if (!this.capabilities.hasOwnProperty(capabilityName)) { + var initialValue = null; + if (this.initialCapabilities.hasOwnProperty(capabilityName)) { + initialValue = this.initialCapabilities[capabilityName]; + } + this.capabilities[capabilityName] = ko.observable(initialValue); + } + return this.capabilities[capabilityName]; + }; + return RexObservableCapabilityMap; +}()); +var RexBaseActor = /** @class */ (function () { + function RexBaseActor(id, name, displayName, capabilities) { + this.canHaveRoles = false; + this.id = ko.observable(id); + this.name = ko.observable(name); + this.displayName = ko.observable(displayName); + this.capabilities = new RexObservableCapabilityMap(capabilities || {}); + } + RexBaseActor.prototype.hasCap = function (capability) { + return (this.capabilities.getCapabilityState(capability) === true); + }; + RexBaseActor.prototype.getCapabilityState = function (capability) { + return this.getOwnCapabilityState(capability); + }; + RexBaseActor.prototype.getOwnCapabilityState = function (capability) { + return this.capabilities.getCapabilityState(capability); + }; + RexBaseActor.prototype.setCap = function (capability, enabled) { + this.capabilities.setCapabilityState(capability, enabled); + }; + RexBaseActor.prototype.deleteCap = function (capability) { + this.capabilities.setCapabilityState(capability, null); + }; + RexBaseActor.prototype.getDisplayName = function () { + return this.displayName(); + }; + RexBaseActor.prototype.getId = function () { + return this.id(); + }; + /** + * Get capabilities that are explicitly assigned/denied to this actor. + * Does not include capabilities that a user inherits from their role(s). + */ + RexBaseActor.prototype.getOwnCapabilities = function () { + return this.capabilities.getAllCapabilities(); + }; + return RexBaseActor; +}()); +var RexRole = /** @class */ (function (_super) { + __extends(RexRole, _super); + function RexRole(name, displayName, capabilities) { + var _this = _super.call(this, 'role:' + name, name, displayName, capabilities) || this; + _this.hasUsers = false; + return _this; + } + RexRole.fromRoleData = function (data) { + var role = new RexRole(data.name, data.displayName, data.capabilities); + role.hasUsers = data.hasUsers; + return role; + }; + /** + * Is this one of the default roles that are part of WordPress core? + * + * Note: I'm calling this property "built-in" instead of "default" to distinguish it + * from the default role for new users. + */ + RexRole.prototype.isBuiltIn = function () { + return RexRole.builtInRoleNames.indexOf(this.name()) >= 0; + }; + RexRole.prototype.toJs = function () { + return { + name: this.name(), + displayName: this.displayName(), + capabilities: this.getOwnCapabilities() + }; + }; + RexRole.builtInRoleNames = ['administrator', 'editor', 'author', 'subscriber', 'contributor']; + return RexRole; +}(RexBaseActor)); +var RexSuperAdmin = /** @class */ (function (_super) { + __extends(RexSuperAdmin, _super); + function RexSuperAdmin() { + return _super.call(this, 'special:super_admin', 'Super Admin', 'Super Admin') || this; + } + RexSuperAdmin.getInstance = function () { + if (RexSuperAdmin.instance === null) { + RexSuperAdmin.instance = new RexSuperAdmin(); + } + return RexSuperAdmin.instance; + }; + RexSuperAdmin.instance = null; + return RexSuperAdmin; +}(RexBaseActor)); +var RexUser = /** @class */ (function (_super) { + __extends(RexUser, _super); + function RexUser(login, displayName, capabilities, userId) { + var _this = _super.call(this, 'user:' + login, login, displayName, capabilities) || this; + _this.isSuperAdmin = false; + _this.userLogin = login; + _this.canHaveRoles = true; + _this.roles = ko.observableArray([]); + _this.userId = userId; + return _this; + } + RexUser.prototype.hasCap = function (capability, outGrantedBy) { + return (this.getCapabilityState(capability, outGrantedBy) === true); + }; + RexUser.prototype.getCapabilityState = function (capability, outGrantedBy) { + if (capability === 'do_not_allow') { + return false; + } + if (this.isSuperAdmin) { + if (outGrantedBy) { + outGrantedBy.push(RexSuperAdmin.getInstance()); + } + return (capability !== 'do_not_allow'); + } + var result = _super.prototype.getCapabilityState.call(this, capability); + if (result !== null) { + if (outGrantedBy) { + outGrantedBy.push(this); + } + return result; + } + wsAmeLodash.each(this.roles(), function (role) { + var roleHasCap = role.getCapabilityState(capability); + if (roleHasCap !== null) { + if (outGrantedBy) { + outGrantedBy.push(role); + } + result = roleHasCap; + } + }); + return result; + }; + // noinspection JSUnusedGlobalSymbols Used in KO templates. + RexUser.prototype.getInheritanceDetails = function (capability) { + var _ = wsAmeLodash; + var results = []; + //Note: Alternative terms include "Assigned", "Granted", "Yes"/"No". + if (this.isSuperAdmin) { + var superAdmin = RexSuperAdmin.getInstance(); + var description_1 = 'Allow everything'; + if (capability.name === 'do_not_allow') { + description_1 = 'Deny'; + } + results.push({ + actor: superAdmin, + name: superAdmin.displayName(), + description: description_1 + }); + } + _.each(this.roles(), function (role) { + var roleHasCap = role.getCapabilityState(capability.name); + var description; + if (roleHasCap) { + description = 'Allow'; + } + else if (roleHasCap === null) { + description = '—'; + } + else { + description = 'Deny'; + } + results.push({ + actor: role, + name: role.displayName(), + description: description, + }); + }); + var hasOwnCap = _super.prototype.getCapabilityState.call(this, capability.name); + var description; + if (hasOwnCap) { + description = 'Allow'; + } + else if (hasOwnCap === null) { + description = '—'; + } + else { + description = 'Deny'; + } + results.push({ + actor: this, + name: 'User-specific setting', + description: description, + }); + var relevantActors = []; + this.getCapabilityState(capability.name, relevantActors); + var decidingActor = _.last(relevantActors); + _.each(results, function (item) { + item.isDecisive = (item.actor === decidingActor); + }); + return results; + }; + RexUser.fromAmeUser = function (data, editor) { + var user = new RexUser(data.userLogin, data.displayName, data.capabilities, data.userId); + wsAmeLodash.forEach(data.roles, function (roleId) { + var role = editor.getRole(roleId); + if (role) { + user.roles.push(role); + } + }); + return user; + }; + RexUser.fromAmeUserProperties = function (properties, editor) { + var user = new RexUser(properties.user_login, properties.display_name, properties.capabilities); + if (properties.id) { + user.userId = properties.id; + } + wsAmeLodash.forEach(properties.roles, function (roleId) { + var role = editor.getRole(roleId); + if (role) { + user.roles.push(role); + } + }); + return user; + }; + RexUser.prototype.toJs = function () { + var _ = wsAmeLodash; + var roles = _.invoke(this.roles(), 'name'); + return { + userId: this.userId, + userLogin: this.userLogin, + displayName: this.displayName(), + capabilities: this.getOwnCapabilities(), + roles: roles + }; + }; + return RexUser; +}(RexBaseActor)); +var RexCategory = /** @class */ (function () { + function RexCategory(name, editor, slug, capabilities) { + if (slug === void 0) { slug = null; } + if (capabilities === void 0) { capabilities = []; } + var _this = this; + this.slug = null; + this.origin = null; + this.subtitle = null; + this.htmlId = null; + this.parent = null; + this.subcategories = []; + this.duplicates = []; + var _ = wsAmeLodash; + var self = this; + this.editor = editor; + this.name = name; + this.slug = slug; + if ((this.slug !== null) && (this.slug !== '')) { + editor.categoriesBySlug[this.slug] = this; + } + var initialPermissions = _.map(capabilities, function (capabilityName) { + return new RexPermission(editor, editor.getCapability(capabilityName)); + }); + this.permissions = ko.observableArray(initialPermissions); + this.sortPermissions(); + this.contentTemplate = ko.observable('rex-default-category-content-template'); + this.isSelected = ko.observable(false); + this.enabledCapabilityCount = ko.pureComputed({ + read: function () { + return self.countUniqueCapabilities({}, function (capability) { + return capability.isEnabledForSelectedActor(); + }); + }, + deferEvaluation: true, + owner: this + }); + this.enabledCapabilityCount.extend({ rateLimit: { timeout: 5, method: "notifyWhenChangesStop" } }); + this.totalCapabilityCount = ko.pureComputed({ + read: function () { + return self.countUniqueCapabilities(); + }, + deferEvaluation: true, + owner: this + }); + this.isCapCountVisible = ko.pureComputed({ + read: function () { + if (!editor.showNumberOfCapsEnabled()) { + return false; + } + var totalCaps = self.totalCapabilityCount(), enabledCaps = self.enabledCapabilityCount(); + if (!editor.showZerosEnabled() && ((totalCaps === 0) || (enabledCaps === 0))) { + return false; + } + return editor.showTotalCapCountEnabled() || self.isEnabledCapCountVisible(); + }, + deferEvaluation: true, + owner: this + }); + this.isEnabledCapCountVisible = ko.pureComputed({ + read: function () { + if (!editor.showGrantedCapCountEnabled()) { + return false; + } + return (self.enabledCapabilityCount() > 0) || editor.showZerosEnabled(); + }, + deferEvaluation: true, + owner: this + }); + this.areAllPermissionsEnabled = ko.computed({ + read: function () { + var items = self.permissions(); + var len = items.length; + for (var i = 0; i < len; i++) { + if (!items[i].capability.isEnabledForSelectedActor() && items[i].capability.isEditable()) { + return false; + } + } + for (var i = 0; i < self.subcategories.length; i++) { + if (!self.subcategories[i].areAllPermissionsEnabled()) { + return false; + } + } + return true; + }, + write: function (enabled) { + var items = self.permissions(); + for (var i = 0; i < items.length; i++) { + var item = items[i]; + if (item.capability.isEditable()) { + item.capability.isEnabledForSelectedActor(enabled); + } + } + for (var i = 0; i < self.subcategories.length; i++) { + self.subcategories[i].areAllPermissionsEnabled(enabled); + } + }, + deferEvaluation: true, + owner: this + }); + this.areAllPermissionsEnabled.extend({ rateLimit: { timeout: 5, method: 'notifyWhenChangesStop' } }); + this.areAnyPermissionsEditable = ko.pureComputed({ + read: function () { + var items = self.permissions(); + var len = items.length; + for (var i = 0; i < len; i++) { + if (items[i].capability.isEditable()) { + return true; + } + } + for (var i = 0; i < self.subcategories.length; i++) { + if (!self.subcategories[i].areAnyPermissionsEditable()) { + return true; + } + } + return false; + }, + deferEvaluation: true, + owner: this + }); + this.areAnyPermissionsEditable.extend({ rateLimit: { timeout: 5, method: 'notifyWhenChangesStop' } }); + this.isVisible = ko.computed({ + read: function () { + var visible = false; + var hasVisibleSubcategories = false; + _.forEach(self.subcategories, function (category) { + if (category.isVisible()) { + hasVisibleSubcategories = true; + return false; + } + }); + //Hide it if not inside the selected category. + var isInSelectedCategory = false, temp = self; + while (temp !== null) { + if (temp.isSelected()) { + isInSelectedCategory = true; + break; + } + temp = temp.parent; + } + //In single-category view, the category also counts as "selected" + //if one of its duplicates is selected. + if (!isInSelectedCategory + && (self.duplicates.length > 0) + && (editor.categoryViewMode() === RexRoleEditor.singleCategoryView)) { + for (var i = 0; i < self.duplicates.length; i++) { + temp = self.duplicates[i]; + while (temp !== null) { + if (temp.isSelected()) { + isInSelectedCategory = true; + break; + } + temp = temp.parent; + } + if (isInSelectedCategory) { + break; + } + } + } + if (!isInSelectedCategory && !hasVisibleSubcategories) { + return false; + } + //Stay visible as long as at least one subcategory or permission is visible. + visible = hasVisibleSubcategories; + _.forEach(self.permissions(), function (permission) { + if (permission.isVisible()) { + visible = true; + return false; + } + }); + return visible; + }, + deferEvaluation: true, + owner: this, + }); + this.isVisible.extend({ + rateLimit: { + timeout: 10, + method: 'notifyWhenChangesStop' + } + }); + this.desiredColumnCount = ko.computed({ + read: function () { + var visiblePermissions = 0; + _.forEach(self.permissions(), function (permission) { + if (permission.isVisible()) { + visiblePermissions++; + } + }); + var minItemsPerColumn = 12; + if (editor.categoryWidthMode() === 'full') { + minItemsPerColumn = 3; + } + var desiredColumns = Math.max(Math.ceil(visiblePermissions / minItemsPerColumn), 1); + //Avoid situations where the last column has only one item (an orphan). + if ((desiredColumns >= 2) && (visiblePermissions % minItemsPerColumn === 1)) { + desiredColumns--; + } + if (desiredColumns > 3) { + return 'max'; + } + return desiredColumns.toString(10); + }, + deferEvaluation: true + }); + this.nestingDepth = ko.pureComputed({ + read: function () { + if (self.parent !== null) { + return self.parent.nestingDepth() + 1; + } + return 1; + }, + deferEvaluation: true + }); + this.isNavExpanded = ko.observable((this.slug !== null) ? !editor.userPreferences.collapsedCategories.peek(this.slug) : true); + if (this.slug) { + this.isNavExpanded.subscribe(function (newValue) { + editor.userPreferences.collapsedCategories.toggle(_this.slug, !newValue); + }); + } + this.isNavVisible = ko.pureComputed({ + read: function () { + if (self.parent === null) { + return true; + } + return self.parent.isNavVisible() && self.parent.isNavExpanded(); + //Idea: We could hide it if all of the capabilities it contains have been deleted. + }, + deferEvaluation: true + }); + this.cssClasses = ko.computed({ + read: function () { + var classes = []; + if (self.subcategories.length > 0) { + classes.push('rex-has-subcategories'); + } + if (self.parent) { + if (self.parent === editor.rootCategory) { + classes.push('rex-top-category'); + } + else { + classes.push('rex-sub-category'); + } + } + if (self.permissions().length > 0) { + classes.push('rex-desired-columns-' + self.desiredColumnCount()); + } + return classes.join(' '); + }, + deferEvaluation: true + }); + this.navCssClasses = ko.pureComputed({ + read: function () { + var classes = []; + if (self.isSelected()) { + classes.push('rex-selected-nav-item'); + } + if (self.isNavExpanded()) { + classes.push('rex-nav-is-expanded'); + } + if (self.subcategories.length > 0) { + classes.push('rex-nav-has-children'); + } + classes.push('rex-nav-level-' + self.nestingDepth()); + return classes.join(' '); + }, + deferEvaluation: true + }); + this.subcategoryModificationFlag = ko.observable(this.subcategories.length); + this.sortedSubcategories = ko.pureComputed({ + read: function () { + //Refresh the sorted list when categories are added or removed. + _this.subcategoryModificationFlag(); + return _this.getSortedSubcategories(); + }, + deferEvaluation: true + }); + this.navSubcategories = ko.pureComputed({ + read: function () { + _this.subcategoryModificationFlag(); + return _this.subcategories; + }, + deferEvaluation: true + }); + this.subheading = ko.pureComputed({ + read: function () { + return _this.getSubheadingItems().join(', '); + }, + deferEvaluation: true + }); + } + RexCategory.prototype.addSubcategory = function (category, afterName) { + category.parent = this; + if (afterName) { + var index = wsAmeLodash.findIndex(this.subcategories, { 'name': afterName }); + if (index > -1) { + this.subcategories.splice(index + 1, 0, category); + this.subcategoryModificationFlag(this.subcategories.length); + return; + } + } + this.subcategories.push(category); + this.subcategoryModificationFlag(this.subcategories.length); + }; + // noinspection JSUnusedGlobalSymbols Used in KO templates. + RexCategory.prototype.toggleSubcategories = function () { + this.isNavExpanded(!this.isNavExpanded()); + }; + RexCategory.prototype.getSortedSubcategories = function () { + //In most cases, the subcategory list is already sorted either alphabetically or in a predefined order + //chosen for specific category. Subcategories can override this method to change the sort order. + return this.subcategories; + }; + /** + * Sort the permissions in this category. Doesn't affect subcategories. + * The default sort is alphabetical, but subclasses can override this method to specify a custom order. + */ + RexCategory.prototype.sortPermissions = function () { + this.permissions.sort(function (a, b) { + return a.capability.name.toLowerCase().localeCompare(b.capability.name.toLowerCase()); + }); + }; + RexCategory.prototype.countUniqueCapabilities = function (accumulator, predicate) { + if (accumulator === void 0) { accumulator = {}; } + if (predicate === void 0) { predicate = null; } + var total = 0; + var permissions = this.permissions(); + for (var i = 0; i < permissions.length; i++) { + var capability = permissions[i].capability; + if (accumulator.hasOwnProperty(capability.name)) { + continue; + } + if (predicate && !predicate(capability)) { + continue; + } + if (capability.isDeleted()) { + continue; + } + accumulator[capability.name] = true; + total++; + } + for (var i = 0; i < this.subcategories.length; i++) { + total = total + this.subcategories[i].countUniqueCapabilities(accumulator, predicate); + } + return total; + }; + RexCategory.prototype.findCategoryBySlug = function (slug) { + if (this.editor.categoriesBySlug.hasOwnProperty(slug)) { + return this.editor.categoriesBySlug[slug]; + } + return null; + }; + RexCategory.fromJs = function (details, editor) { + var category; + if (details.variant && details.variant === 'post_type') { + category = new RexPostTypeCategory(details.name, editor, details.contentTypeId, details.slug, details.permissions); + } + else if (details.variant && details.variant === 'taxonomy') { + category = new RexTaxonomyCategory(details.name, editor, details.contentTypeId, details.slug, details.permissions); + } + else { + category = new RexCategory(details.name, editor, details.slug, details.capabilities); + } + if (details.componentId) { + category.origin = editor.getComponent(details.componentId); + } + if (details.subcategories) { + wsAmeLodash.forEach(details.subcategories, function (childDetails) { + var subcategory = RexCategory.fromJs(childDetails, editor); + category.addSubcategory(subcategory); + }); + } + return category; + }; + RexCategory.prototype.usesBaseCapabilities = function () { + return false; + }; + RexCategory.prototype.getDeDuplicationKey = function () { + var _a; + var key = (_a = this.slug) !== null && _a !== void 0 ? _a : this.name; + if (this.parent) { + key = this.parent.getDeDuplicationKey() + '>' + key; + } + return key; + }; + RexCategory.prototype.addDuplicate = function (category) { + if (this.duplicates.indexOf(category) === -1) { + this.duplicates.push(category); + } + }; + RexCategory.prototype.getSubheadingItems = function () { + var items = []; + if (this.parent !== null) { + items.push(this.parent.name); + } + if (this.duplicates.length > 0) { + for (var i = 0; i < this.duplicates.length; i++) { + var category = this.duplicates[i]; + if (category.parent) { + items.push(category.parent.name); + } + } + } + return items; + }; + RexCategory.prototype.getAbsoluteName = function () { + var components = [this.name]; + var parent = this.parent; + while (parent !== null) { + components.unshift(parent.name); + parent = parent.parent; + } + return components.join(' > '); + }; + RexCategory.defaultSubcategoryComparison = function (a, b) { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }; + return RexCategory; +}()); +var RexContentTypeCategory = /** @class */ (function (_super) { + __extends(RexContentTypeCategory, _super); + function RexContentTypeCategory(name, editor, slug) { + if (slug === void 0) { slug = null; } + var _this = _super.call(this, name, editor, slug) || this; + _this.actions = {}; + _this.baseCategorySlug = null; + _this.isBaseCapNoticeVisible = ko.pureComputed({ + read: function () { + if (editor.showBaseCapsEnabled()) { + return false; + } + return _this.usesBaseCapabilities(); + }, + deferEvaluation: true + }); + return _this; + } + /** + * Check if the post type or taxonomy represented by this category uses the same capabilities + * as the built-in "post" type or the "category" taxonomy. + */ + RexContentTypeCategory.prototype.usesBaseCapabilities = function () { + var baseCategory = this.getBaseCategory(); + if (baseCategory === null || this === baseCategory) { + return false; + } + var allCapsMatch = true; + wsAmeLodash.forEach(this.actions, function (item) { + var isMatch = item.action + && baseCategory.actions.hasOwnProperty(item.action) + && (item.capability === baseCategory.actions[item.action].capability); + if (!isMatch) { + allCapsMatch = false; + return false; + } + }); + return allCapsMatch; + }; + RexContentTypeCategory.prototype.getBaseCategory = function () { + if (this.baseCategorySlug !== null) { + var result = this.findCategoryBySlug(this.baseCategorySlug); + if (result instanceof RexContentTypeCategory) { + return result; + } + } + return null; + }; + return RexContentTypeCategory; +}(RexCategory)); +var RexPostTypePermission = /** @class */ (function (_super) { + __extends(RexPostTypePermission, _super); + function RexPostTypePermission(editor, capability, action, pluralNoun) { + if (pluralNoun === void 0) { pluralNoun = ''; } + var _this = _super.call(this, editor, capability) || this; + _this.action = action; + _this.readableAction = wsAmeLodash.capitalize(_this.action.replace('_posts', '').replace('_', ' ')); + if (RexPostTypePermission.actionDescriptions.hasOwnProperty(action) && pluralNoun) { + _this.mainDescription = RexPostTypePermission.actionDescriptions[action].replace('%s', pluralNoun); + } + return _this; + } + RexPostTypePermission.actionDescriptions = { + 'edit_and_create': 'Edit and create %s', + 'edit_posts': 'Edit %s', + 'create_posts': 'Create new %s', + 'edit_published_posts': 'Edit published %s', + 'edit_others_posts': 'Edit %s created by others', + 'edit_private_posts': 'Edit private %s created by others', + 'publish_posts': 'Publish %s', + 'read_private_posts': 'Read private %s', + 'delete_posts': 'Delete %s', + 'delete_published_posts': 'Delete published %s', + 'delete_others_posts': 'Delete %s created by others', + 'delete_private_posts': 'Delete private %s created by others', + }; + return RexPostTypePermission; +}(RexPermission)); +var RexPostTypeCategory = /** @class */ (function (_super) { + __extends(RexPostTypeCategory, _super); + function RexPostTypeCategory(name, editor, postTypeId, slug, permissions, isDefault) { + if (slug === void 0) { slug = null; } + if (isDefault === void 0) { isDefault = false; } + var _this = _super.call(this, name, editor, slug) || this; + _this.pluralLabel = ''; + _this.actions = {}; + var _ = wsAmeLodash; + _this.baseCategorySlug = 'postTypes/post'; + _this.postType = postTypeId; + _this.isDefault = isDefault; + _this.subtitle = _this.postType; + if (editor.postTypes[postTypeId].pluralLabel) { + _this.pluralLabel = editor.postTypes[postTypeId].pluralLabel; + } + else { + _this.pluralLabel = name.toLowerCase(); + } + _this.permissions = ko.observableArray(_.map(permissions, function (capability, action) { + var permission = new RexPostTypePermission(editor, editor.getCapability(capability), action, _this.pluralLabel); + //The "read" capability is already shown in the core category and every role has it by default. + if (capability === 'read') { + permission.isRedundant = true; + } + _this.actions[action] = permission; + return permission; + })); + _this.sortPermissions(); + //The "create" capability is often the same as the "edit" capability. + var editPerm = _.get(_this.actions, 'edit_posts', null), createPerm = _.get(_this.actions, 'create_posts', null); + if (editPerm && createPerm && (createPerm.capability.name === editPerm.capability.name)) { + createPerm.isRedundant = true; + } + return _this; + } + RexPostTypeCategory.prototype.getDeDuplicationKey = function () { + return 'postType:' + this.postType; + }; + RexPostTypeCategory.prototype.sortPermissions = function () { + this.permissions.sort(function (a, b) { + var priorityA = RexPostTypeCategory.desiredActionOrder.hasOwnProperty(a.action) ? RexPostTypeCategory.desiredActionOrder[a.action] : 1000; + var priorityB = RexPostTypeCategory.desiredActionOrder.hasOwnProperty(b.action) ? RexPostTypeCategory.desiredActionOrder[b.action] : 1000; + var delta = priorityA - priorityB; + if (delta !== 0) { + return delta; + } + return a.capability.name.localeCompare(b.capability.name); + }); + }; + RexPostTypeCategory.prototype.getSubheadingItems = function () { + var items = _super.prototype.getSubheadingItems.call(this); + items.push(this.postType); + return items; + }; + RexPostTypeCategory.desiredActionOrder = { + 'edit_posts': 1, + 'edit_others_posts': 2, + 'edit_published_posts': 3, + 'edit_private_posts': 4, + 'publish_posts': 5, + 'delete_posts': 6, + 'delete_others_posts': 7, + 'delete_published_posts': 8, + 'delete_private_posts': 9, + 'read_private_posts': 10, + 'create_posts': 11, + }; + return RexPostTypeCategory; +}(RexContentTypeCategory)); +var RexTaxonomyPermission = /** @class */ (function (_super) { + __extends(RexTaxonomyPermission, _super); + function RexTaxonomyPermission(editor, capability, action, pluralNoun) { + if (pluralNoun === void 0) { pluralNoun = ''; } + var _this = _super.call(this, editor, capability) || this; + _this.action = action; + _this.readableAction = wsAmeLodash.capitalize(_this.action.replace('_terms', '').replace('_', ' ')); + if (RexTaxonomyPermission.actionDescriptions.hasOwnProperty(action) && pluralNoun) { + _this.mainDescription = RexTaxonomyPermission.actionDescriptions[action].replace('%s', pluralNoun); + } + return _this; + } + RexTaxonomyPermission.actionDescriptions = { + 'manage_terms': 'Manage %s', + 'edit_terms': 'Edit %s', + 'delete_terms': 'Delete %s', + 'assign_terms': 'Assign %s', + }; + return RexTaxonomyPermission; +}(RexPermission)); +var RexTaxonomyCategory = /** @class */ (function (_super) { + __extends(RexTaxonomyCategory, _super); + function RexTaxonomyCategory(name, editor, taxonomyId, slug, permissions) { + if (slug === void 0) { slug = null; } + var _this = _super.call(this, name, editor, slug) || this; + _this.actions = {}; + var _ = wsAmeLodash; + _this.baseCategorySlug = 'taxonomies/category'; + _this.taxonomy = taxonomyId; + _this.subtitle = taxonomyId; + var noun = name.toLowerCase(); + _this.permissions = ko.observableArray(_.map(permissions, function (capability, action) { + var permission = new RexTaxonomyPermission(editor, editor.getCapability(capability), action, noun); + _this.actions[action] = permission; + return permission; + })); + _this.sortPermissions(); + //Permissions that use the same capability as the "manage_terms" permission are redundant. + if (_this.actions.manage_terms) { + var manageCap = _this.actions.manage_terms.capability.name; + for (var action in _this.actions) { + if (!_this.actions.hasOwnProperty(action)) { + continue; + } + if ((action !== 'manage_terms') && (_this.actions[action].capability.name === manageCap)) { + _this.actions[action].isRedundant = true; + } + } + } + return _this; + } + RexTaxonomyCategory.prototype.getDeDuplicationKey = function () { + return 'taxonomy:' + this.taxonomy; + }; + RexTaxonomyCategory.prototype.sortPermissions = function () { + this.permissions.sort(function (a, b) { + var priorityA = RexTaxonomyCategory.desiredActionOrder.hasOwnProperty(a.action) ? RexTaxonomyCategory.desiredActionOrder[a.action] : 1000; + var priorityB = RexTaxonomyCategory.desiredActionOrder.hasOwnProperty(b.action) ? RexTaxonomyCategory.desiredActionOrder[b.action] : 1000; + var delta = priorityA - priorityB; + if (delta !== 0) { + return delta; + } + return a.capability.name.localeCompare(b.capability.name); + }); + }; + RexTaxonomyCategory.prototype.getSubheadingItems = function () { + var items = _super.prototype.getSubheadingItems.call(this); + items.push(this.taxonomy); + return items; + }; + RexTaxonomyCategory.desiredActionOrder = { + 'manage_terms': 1, + 'edit_terms': 2, + 'delete_terms': 3, + 'assign_terms': 4, + }; + return RexTaxonomyCategory; +}(RexContentTypeCategory)); +var RexTableViewCategory = /** @class */ (function (_super) { + __extends(RexTableViewCategory, _super); + function RexTableViewCategory(name, editor, slug) { + if (slug === void 0) { slug = null; } + var _this = _super.call(this, name, editor, slug) || this; + _this.subcategoryComparisonCallback = null; + _this.contentTemplate = ko.pureComputed(function () { + if (editor.categoryViewMode() === RexRoleEditor.hierarchyView) { + return 'rex-permission-table-template'; + } + return 'rex-default-category-content-template'; + }); + _this.subcategoryComparisonCallback = RexCategory.defaultSubcategoryComparison; + return _this; + } + RexTableViewCategory.prototype.getSortedSubcategories = function () { + var _this = this; + if (this.editor.showBaseCapsEnabled()) { + return _super.prototype.getSortedSubcategories.call(this); + } + var cats = wsAmeLodash.clone(this.subcategories); + return cats.sort(function (a, b) { + //Special case: Put categories that use base capabilities at the end. + var aEqualsBase = a.usesBaseCapabilities(); + var bEqualsBase = b.usesBaseCapabilities(); + if (aEqualsBase && !bEqualsBase) { + return 1; + } + else if (!aEqualsBase && bEqualsBase) { + return -1; + } + //Otherwise just sort in the default order. + return _this.subcategoryComparisonCallback(a, b); + }); + }; + /** + * Sort the underlying category array. + */ + RexTableViewCategory.prototype.sortSubcategories = function () { + this.subcategories.sort(this.subcategoryComparisonCallback); + }; + return RexTableViewCategory; +}(RexCategory)); +var RexTaxonomyContainerCategory = /** @class */ (function (_super) { + __extends(RexTaxonomyContainerCategory, _super); + function RexTaxonomyContainerCategory(name, editor, slug) { + if (slug === void 0) { slug = null; } + var _this = _super.call(this, name, editor, slug) || this; + _this.htmlId = 'rex-taxonomy-summary-category'; + _this.tableColumns = ko.pureComputed({ + read: function () { + var _ = wsAmeLodash; + var defaultTaxonomyActions = ['manage_terms', 'assign_terms', 'edit_terms', 'delete_terms']; + var columns = [ + { + title: 'Manage', + actions: ['manage_terms'] + }, + { + title: 'Assign', + actions: ['assign_terms'] + }, + { + title: 'Edit', + actions: ['edit_terms'] + }, + { + title: 'Delete', + actions: ['delete_terms'] + } + ]; + var misColumnExists = false, miscColumn = null; + for (var i = 0; i < _this.subcategories.length; i++) { + var category = _this.subcategories[i]; + if (!(category instanceof RexTaxonomyCategory)) { + continue; + } + //Display any unrecognized actions in a "Misc" column. + var customActions = _.omit(category.actions, defaultTaxonomyActions); + if (!_.isEmpty(customActions)) { + if (!misColumnExists) { + miscColumn = { title: 'Misc', actions: [] }; + columns.push(miscColumn); + } + miscColumn.actions = _.union(miscColumn.actions, _.keys(customActions)); + } + } + return columns; + }, + deferEvaluation: true, + }); + return _this; + } + return RexTaxonomyContainerCategory; +}(RexTableViewCategory)); +var RexPostTypeContainerCategory = /** @class */ (function (_super) { + __extends(RexPostTypeContainerCategory, _super); + function RexPostTypeContainerCategory(name, editor, slug) { + if (slug === void 0) { slug = null; } + var _this = _super.call(this, name, editor, slug) || this; + /* Note: This seems like poor design because the superclass overrides subclass + * behaviour (subcategory comparison) in some situations. Unfortunately, I haven't + * come up with anything better so far. Might be something to revisit later. + */ + _this.subcategoryComparisonCallback = function (a, b) { + //Special case: Put "Posts" at the top. + if (a.postType === 'post') { + return -1; + } + else if (b.postType === 'post') { + return 1; + } + //Put other built-in post types above custom post types. + if (a.isDefault && !b.isDefault) { + return -1; + } + else if (b.isDefault && !a.isDefault) { + return 1; + } + var labelA = a.name.toLowerCase(), labelB = b.name.toLowerCase(); + return labelA.localeCompare(labelB); + }; + _this.tableColumns = ko.pureComputed({ + read: function () { + var _ = wsAmeLodash; + var defaultPostTypeActions = _.keys(RexPostTypePermission.actionDescriptions); + var columns = [ + { + title: 'Own items', + actions: ['create_posts', 'edit_posts', 'delete_posts', 'publish_posts', 'edit_published_posts', 'delete_published_posts'] + }, + { + title: 'Other\'s items', + actions: ['edit_others_posts', 'delete_others_posts', 'edit_private_posts', 'delete_private_posts', 'read_private_posts'] + } + ]; + var metaColumn = { + title: 'Meta', + actions: ['edit_post', 'delete_post', 'read_post'] + }; + columns.push(metaColumn); + for (var i = 0; i < _this.subcategories.length; i++) { + var category = _this.subcategories[i]; + if (!(category instanceof RexPostTypeCategory)) { + continue; + } + //Display any unrecognized actions in a "Misc" column. + var customActions = _.omit(category.actions, defaultPostTypeActions); + if (!_.isEmpty(customActions)) { + metaColumn.actions = _.union(metaColumn.actions, _.keys(customActions)); + } + } + return columns; + }, + deferEvaluation: true, + }); + return _this; + } + return RexPostTypeContainerCategory; +}(RexTableViewCategory)); +var RexCapability = /** @class */ (function () { + function RexCapability(name, editor) { + var _this = this; + this.originComponent = null; + this.usedByComponents = []; + this.menuItems = []; + this.usedByPostTypeActions = {}; + this.usedByTaxonomyActions = {}; + this.predefinedPermissions = []; + this.documentationUrl = null; + this.notes = null; + this.name = String(name); + this.editor = editor; + var self = this; + this.readableName = wsAmeLodash.capitalize(this.name.replace(/[_\-\s]+/g, ' ')); + this.displayName = ko.pureComputed({ + read: function () { + return editor.readableNamesEnabled() ? self.readableName : self.name; + }, + deferEvaluation: true, + owner: this + }); + this.isDeleted = ko.observable(false); + this.responsibleActors = ko.computed({ + read: function () { + var actor = editor.selectedActor(), list = []; + if (actor instanceof RexUser) { + actor.hasCap(self.name, list); + } + return list; + }, + owner: this, + deferEvaluation: true + }); + this.isInherited = ko.computed({ + read: function () { + var actor = editor.selectedActor(); + if (!actor.canHaveRoles) { + return false; + } + var responsibleActors = self.responsibleActors(); + return responsibleActors + && (responsibleActors.length > 0) + && (wsAmeLodash.indexOf(responsibleActors, actor) < (responsibleActors.length - 1)); + }, + owner: this, + deferEvaluation: true + }); + this.isPersonalOverride = ko.pureComputed({ + read: function () { + //This flag applies only to actors that can inherit permissions. + var actor = editor.selectedActor(); + if (!actor.canHaveRoles) { + return false; + } + return !self.isInherited(); + }, + owner: this, + deferEvaluation: true + }); + this.isEditable = ko.pureComputed({ + read: function () { + if (self.isInherited() && !editor.inheritanceOverrideEnabled()) { + return false; + } + return !self.isDeleted(); + }, + deferEvaluation: true + }); + this.isEnabledForSelectedActor = ko.computed({ + read: function () { + return editor.selectedActor().hasCap(self.name); + }, + write: function (newState) { + var actor = editor.selectedActor(); + if (editor.isShiftKeyDown()) { + //Hold the shift key while clicking to cycle the capability between 3 states: + //Granted -> Denied -> Not granted. + var oldState = actor.getOwnCapabilityState(self.name); + if (newState) { + if (oldState === false) { + actor.deleteCap(self.name); //Denied -> Not granted. + } + else if (oldState === null) { + actor.setCap(self.name, true); //Not granted -> Granted. + } + } + else { + if (oldState === true) { + actor.setCap(self.name, false); //Granted -> Denied. + } + else if (oldState === null) { + actor.setCap(self.name, true); //Not granted (inherited = Granted) -> Granted. + } + } + //Update the checkbox state. + if (actor.hasCap(self.name) !== newState) { + self.isEnabledForSelectedActor.notifySubscribers(); + } + return; + } + if (newState) { + //TODO: If it's a user and the cap is explicitly negated, consider removing that state. + actor.setCap(self.name, newState); + } + else { + //The default is to remove the capability instead of explicitly setting it to false. + actor.deleteCap(self.name); + //If we're removing a capability from a user but one of their roles also has it, + //we have to set it to false after all or it will stay enabled. + if (actor.canHaveRoles && actor.hasCap(self.name)) { + actor.setCap(self.name, newState); + } + } + }, + owner: this, + deferEvaluation: true + }); + //this.isEnabledForSelectedActor.extend({rateLimit: {timeout: 10, method: "notifyWhenChangesStop"}}); + this.isExplicitlyDenied = ko.pureComputed({ + read: function () { + var actor = editor.selectedActor(); + if (actor) { + return (actor.getCapabilityState(self.name) === false); + } + return false; + }, + deferEvaluation: true + }); + this.grantedPermissions = ko.computed({ + read: function () { + var _ = wsAmeLodash; + var results = []; + if (_this.predefinedPermissions.length > 0) { + results = _this.predefinedPermissions.slice(); + } + function localeAwareCompare(a, b) { + return a.localeCompare(b); + } + function actionsToPermissions(actionGroups, labelMap, descriptions) { + return _.map(actionGroups, function (ids, action) { + var labels = _.map(ids, function (id) { return labelMap[id].pluralLabel; }) + .sort(localeAwareCompare); + var template = descriptions[action]; + if (!template) { + template = action + ': %s'; + } + return template.replace('%s', RexCapability.formatNounList(labels)); + }).sort(localeAwareCompare); + } + //Post permissions. + var postActionGroups = _.transform(_this.usedByPostTypeActions, function (accumulator, actions, postType) { + var actionKeys = _.keys(actions); + //Combine "edit" and "create" permissions because they usually use the same capability. + var editEqualsCreate = actions.hasOwnProperty('edit_posts') && actions.hasOwnProperty('create_posts'); + if (editEqualsCreate) { + actionKeys = _.without(actionKeys, 'edit_posts', 'create_posts'); + actionKeys.unshift('edit_and_create'); + } + _.forEach(actionKeys, function (action) { + if (!accumulator.hasOwnProperty(action)) { + accumulator[action] = []; + } + accumulator[action].push(postType); + }); + }, {}); + var postPermissions = actionsToPermissions(postActionGroups, _this.editor.postTypes, RexPostTypePermission.actionDescriptions); + Array.prototype.push.apply(results, postPermissions); + //Taxonomy permissions. + var taxonomyActionGroups = _.transform(_this.usedByTaxonomyActions, function (accumulator, actions, taxonomy) { + var actionKeys = _.keys(actions); + //Most taxonomies use the same capability for manage_terms, edit_terms, and delete_terms. + //In those cases, let's show only manage_terms. + if (actions.hasOwnProperty('manage_terms')) { + actionKeys = _.without(actionKeys, 'edit_terms', 'delete_terms'); + } + _.forEach(actionKeys, function (action) { + if (!accumulator.hasOwnProperty(action)) { + accumulator[action] = []; + } + accumulator[action].push(taxonomy); + }); + }, {}); + var taxonomyPermissions = actionsToPermissions(taxonomyActionGroups, _this.editor.taxonomies, RexTaxonomyPermission.actionDescriptions); + Array.prototype.push.apply(results, taxonomyPermissions); + Array.prototype.push.apply(results, _this.menuItems); + return results; + }, + deferEvaluation: true, + owner: this, + }); + } + // noinspection JSUnusedGlobalSymbols Used in KO templates. + RexCapability.prototype.getDocumentationUrl = function () { + if (this.documentationUrl) { + return this.documentationUrl; + } + if (this.originComponent && this.originComponent.capabilityDocumentationUrl) { + this.documentationUrl = this.originComponent.capabilityDocumentationUrl; + return this.documentationUrl; + } + return null; + }; + RexCapability.fromJs = function (name, data, editor) { + var capability = new RexCapability(name, editor); + capability.menuItems = data.menuItems.sort(function (a, b) { + return a.localeCompare(b); + }); + if (data.componentId) { + capability.originComponent = editor.getComponent(data.componentId); + } + if (data.usedByComponents) { + for (var id in data.usedByComponents) { + var component = editor.getComponent(id); + if (component) { + capability.usedByComponents.push(component); + } + } + } + if (data.documentationUrl) { + capability.documentationUrl = data.documentationUrl; + } + if (data.permissions && (data.permissions.length > 0)) { + capability.predefinedPermissions = data.permissions; + } + if ((capability.originComponent === editor.coreComponent) && (capability.documentationUrl === null)) { + capability.documentationUrl = 'https://wordpress.org/support/article/roles-and-capabilities/#' + + encodeURIComponent(capability.name); + } + if (data.readableName) { + capability.readableName = data.readableName; + } + return capability; + }; + RexCapability.formatNounList = function (items) { + if (items.length <= 2) { + return items.join(' and '); + } + return items.slice(0, -1).join(', ') + ', and ' + items[items.length - 1]; + }; + return RexCapability; +}()); +var RexDoNotAllowCapability = /** @class */ (function (_super) { + __extends(RexDoNotAllowCapability, _super); + function RexDoNotAllowCapability(editor) { + var _this = _super.call(this, 'do_not_allow', editor) || this; + _this.notes = '"do_not_allow" is a special capability. ' + + 'WordPress uses it internally to indicate that access is denied. ' + + 'Normally, it should not be assigned to any roles or users.'; + //Normally, it's impossible to grant this capability to anyone. Doing so would break things. + //However, if it's already granted, you can remove it. + _this.isEditable = ko.computed(function () { + return _this.isEnabledForSelectedActor(); + }); + return _this; + } + return RexDoNotAllowCapability; +}(RexCapability)); +var RexExistCapability = /** @class */ (function (_super) { + __extends(RexExistCapability, _super); + function RexExistCapability(editor) { + var _this = _super.call(this, 'exist', editor) || this; + _this.notes = '"exist" is a special capability. ' + + 'WordPress uses it internally to indicate that a role or user exists. ' + + 'Normally, everyone has this capability by default, and it is not necessary ' + + '(or possible) to assign it directly.'; + //Everyone must have this capability. However, if it has somehow become disabled, + //we'll let the user enable it. + _this.isEditable = ko.computed(function () { + return !_this.isEnabledForSelectedActor(); + }); + return _this; + } + return RexExistCapability; +}(RexCapability)); +var RexInvalidCapability = /** @class */ (function (_super) { + __extends(RexInvalidCapability, _super); + function RexInvalidCapability(fakeName, value, editor) { + var _this = _super.call(this, fakeName, editor) || this; + var startsWithVowel = /^[aeiou]/i; + var theType = (typeof value); + var nounPhrase = (startsWithVowel.test(theType) ? 'an' : 'a') + ' ' + theType; + _this.notes = 'This is not a valid capability. A capability name must be a string (i.e. text),' + + ' but this is ' + nounPhrase + '. It was probably created by a bug in another plugin or theme.'; + _this.isEditable = ko.computed(function () { + return false; + }); + return _this; + } + return RexInvalidCapability; +}(RexCapability)); +var RexUserPreferences = /** @class */ (function () { + function RexUserPreferences(initialPreferences, ajaxUrl, updateNonce) { + var _this = this; + var _ = wsAmeLodash; + initialPreferences = initialPreferences || {}; + if (_.isArray(initialPreferences)) { + initialPreferences = {}; + } + this.preferenceObservables = _.mapValues(initialPreferences, ko.observable, ko); + this.preferenceCount = ko.observable(_.size(this.preferenceObservables)); + this.collapsedCategories = new RexCollapsedCategorySet(_.get(initialPreferences, 'collapsedCategories', [])); + this.plainPreferences = ko.computed(function () { + //By creating a dependency on the number of preferences, we ensure that the observable will be re-evaluated + //whenever a preference is added or removed. + _this.preferenceCount(); + //This converts preferences to a plain JS object and establishes dependencies on all individual observables. + var result = _.mapValues(_this.preferenceObservables, function (observable) { + return observable(); + }); + result.collapsedCategories = _this.collapsedCategories.toJs(); + return result; + }); + //Avoid excessive AJAX requests. + this.plainPreferences.extend({ rateLimit: { timeout: 5000, method: "notifyWhenChangesStop" } }); + //Save preferences when they change. + if (ajaxUrl && updateNonce) { + this.plainPreferences.subscribe(function (preferences) { + //console.info('Saving user preferences', preferences); + jQuery.post(ajaxUrl, { + action: 'ws_ame_rex_update_user_preferences', + _ajax_nonce: updateNonce, + preferences: ko.toJSON(preferences) + }); + }); + } + } + RexUserPreferences.prototype.getObservable = function (name, defaultValue) { + if (defaultValue === void 0) { defaultValue = null; } + if (this.preferenceObservables.hasOwnProperty(name)) { + return this.preferenceObservables[name]; + } + var newPreference = ko.observable(defaultValue || null); + this.preferenceObservables[name] = newPreference; + this.preferenceCount(this.preferenceCount() + 1); + return newPreference; + }; + return RexUserPreferences; +}()); +/** + * An observable collection of unique strings. In this case, they're category slugs. + */ +var RexCollapsedCategorySet = /** @class */ (function () { + function RexCollapsedCategorySet(items) { + if (items === void 0) { items = []; } + this.isItemInSet = {}; + items = wsAmeLodash.uniq(items); + for (var i = 0; i < items.length; i++) { + this.isItemInSet[items[i]] = ko.observable(true); + } + this.items = ko.observableArray(items); + } + RexCollapsedCategorySet.prototype.getItemObservable = function (item) { + if (!this.isItemInSet.hasOwnProperty(item)) { + this.isItemInSet[item] = ko.observable(false); + } + return this.isItemInSet[item]; + }; + RexCollapsedCategorySet.prototype.add = function (item) { + if (!this.contains(item)) { + this.getItemObservable(item)(true); + this.items.push(item); + } + }; + RexCollapsedCategorySet.prototype.remove = function (item) { + if (this.contains(item)) { + this.isItemInSet[item](false); + this.items.remove(item); + } + }; + RexCollapsedCategorySet.prototype.toggle = function (item, addToSet) { + if (addToSet) { + this.add(item); + } + else { + this.remove(item); + } + }; + RexCollapsedCategorySet.prototype.contains = function (item) { + return this.getItemObservable(item)(); + }; + RexCollapsedCategorySet.prototype.peek = function (item) { + if (!this.isItemInSet.hasOwnProperty(item)) { + return false; + } + return this.isItemInSet[item].peek(); + }; + RexCollapsedCategorySet.prototype.toJs = function () { + return this.items(); + }; + return RexCollapsedCategorySet; +}()); +var RexBaseDialog = /** @class */ (function () { + function RexBaseDialog() { + var _this = this; + this.isOpen = ko.observable(false); + this.isRendered = ko.observable(false); + this.title = null; + this.options = { + buttons: [] + }; + this.isOpen.subscribe(function (isOpenNow) { + if (isOpenNow && !_this.isRendered()) { + _this.isRendered(true); + } + }); + } + RexBaseDialog.prototype.setupValidationTooltip = function (inputSelector, message) { + //Display validation messages next to the input field. + var element = this.jQueryWidget.find(inputSelector).qtip({ + overwrite: false, + content: '(Validation errors will appear here.)', + //Show the tooltip when the input is focused. + show: { + event: '', + ready: false, + effect: false + }, + hide: { + event: '', + effect: false + }, + position: { + my: 'center left', + at: 'center right', + effect: false + }, + style: { + classes: 'qtip-bootstrap qtip-shadow rex-tooltip' + } + }); + message.subscribe(function (newMessage) { + if (newMessage == '') { + element.qtip('option', 'content.text', 'OK'); + element.qtip('option', 'show.event', ''); + element.qtip('hide'); + } + else { + element.qtip('option', 'content.text', newMessage); + element.qtip('option', 'show.event', 'focus'); + element.qtip('show'); + } + }); + //Hide the tooltip when the dialog is closed and prevent it from automatically re-appearing. + this.isOpen.subscribe(function (isDialogOpen) { + if (!isDialogOpen) { + element.qtip('option', 'show.event', ''); + element.qtip('hide'); + } + }); + }; + ; + return RexBaseDialog; +}()); +var RexDeleteCapDialog = /** @class */ (function (_super) { + __extends(RexDeleteCapDialog, _super); + function RexDeleteCapDialog(editor) { + var _this = _super.call(this) || this; + _this.options = { + buttons: [], + minWidth: 380 + }; + _this.wasEverOpen = ko.observable(false); + var _ = wsAmeLodash; + _this.options.buttons.push({ + text: 'Delete Capability', + 'class': 'button button-primary rex-delete-selected-caps', + click: function () { + var selectedCapabilities = _.chain(_this.deletableItems()) + .filter(function (item) { + return item.isSelected(); + }) + .pluck('capability') + .value(); + //Note: We could remove confirmation if we get an "Undo" feature. + var noun = (selectedCapabilities.length === 1) ? 'capability' : 'capabilities'; + var warning = 'Caution: Deleting capabilities could break plugins that use those capabilities. ' + + 'Delete ' + selectedCapabilities.length + ' ' + noun + '?'; + if (!confirm(warning)) { + return; + } + _this.isOpen(false); + editor.deleteCapabilities(selectedCapabilities); + alert(selectedCapabilities.length + ' capabilities deleted'); + }, + disabled: true + }); + _this.isOpen.subscribe(function (open) { + if (open && !_this.wasEverOpen()) { + _this.wasEverOpen(true); + } + }); + _this.deletableItems = ko.pureComputed({ + read: function () { + var wpCore = editor.getComponent(':wordpress:'); + return _.chain(editor.capabilities) + .filter(function (capability) { + if (capability.originComponent === wpCore) { + return false; + } + return !capability.isDeleted(); + }) + //Pre-populate part of the list when the dialog is closed to ensure it has a non-zero height. + .take(_this.wasEverOpen() ? 1000000 : 30) + .sortBy(function (capability) { + return capability.name.toLowerCase(); + }) + .map(function (capability) { + return { + 'capability': capability, + 'isSelected': ko.observable(false) + }; + }) + .value(); + }, + deferEvaluation: true + }); + _this.selectedItemCount = ko.pureComputed({ + read: function () { return _.filter(_this.deletableItems(), function (item) { + return item.isSelected(); + }).length; }, + deferEvaluation: true + }); + var deleteButtonText = ko.pureComputed({ + read: function () { + var count = _this.selectedItemCount(); + if (count <= 0) { + return 'Delete Capability'; + } + else { + if (count === 1) { + return 'Delete 1 Capability'; + } + else { + return ('Delete ' + count + ' Capabilities'); + } + } + }, + deferEvaluation: true + }); + deleteButtonText.subscribe(function (newText) { + _this.jQueryWidget + .closest('.ui-dialog') + .find('.ui-dialog-buttonset .button-primary .ui-button-text') + .text(newText); + }); + _this.isDeleteButtonEnabled = ko.pureComputed({ + read: function () { + return _this.selectedItemCount() > 0; + }, + deferEvaluation: true + }); + return _this; + } + RexDeleteCapDialog.prototype.onOpen = function () { + //Deselect all items when the dialog is opened. + var items = this.deletableItems(); + for (var i = 0; i < items.length; i++) { + if (items[i].isSelected()) { + items[i].isSelected(false); + } + } + }; + return RexDeleteCapDialog; +}(RexBaseDialog)); +var RexAddCapabilityDialog = /** @class */ (function (_super) { + __extends(RexAddCapabilityDialog, _super); + function RexAddCapabilityDialog(editor) { + var _this = _super.call(this) || this; + _this.autoCancelButton = true; + _this.options = { + minWidth: 380 + }; + _this.validationState = ko.observable(RexAddCapabilityDialog.states.empty); + _this.validationMessage = ko.observable(''); + var _ = wsAmeLodash; + _this.editor = editor; + var excludedCaps = ['do_not_allow', 'exist', 'customize']; + var newCapabilityName = ko.observable(''); + _this.capabilityName = ko.computed({ + read: function () { + return newCapabilityName(); + }, + write: function (value) { + value = _.trimRight(value); + //Validate and sanitize the capability name. + var state = _this.validationState, message = _this.validationMessage; + //WP API allows completely arbitrary capability names, but this plugin forbids some characters + //for sanity's sake and to avoid XSS. + var invalidCharacters = /[><&\r\n\t]/g; + //While all other characters are allowed, it's recommended to stick to alphanumerics, + //underscores and dashes. Spaces are also OK because some other plugins use them. + var suspiciousCharacters = /[^a-z0-9_ -]/ig; + //PHP doesn't allow numeric string keys, and there's no conceivable reason to start the name with a space. + var invalidFirstCharacter = /^[\s0-9]/i; + var foundInvalid = value.match(invalidCharacters); + var foundSuspicious = value.match(suspiciousCharacters); + if (foundInvalid !== null) { + state(RexAddCapabilityDialog.states.error); + message('Sorry, <code>' + _.escape(_.last(foundInvalid)) + '</code> is not allowed here.'); + } + else if (value.match(invalidFirstCharacter) !== null) { + state(RexAddCapabilityDialog.states.error); + message('Capability name should start with a letter or an underscore.'); + } + else if (editor.capabilityExists(value)) { + //Duplicates are not allowed. + state(RexAddCapabilityDialog.states.error); + message('That capability already exists.'); + } + else if (editor.getRole(value) !== null) { + state(RexAddCapabilityDialog.states.error); + message('Capability name can\'t be the same as the name of a role.'); + } + else if (excludedCaps.indexOf(value) >= 0) { + state(RexAddCapabilityDialog.states.error); + message('That is a meta capability or a reserved capability name.'); + } + else if (foundSuspicious !== null) { + state(RexAddCapabilityDialog.states.notice); + message('For best compatibility, we recommend using only English letters, numbers, and underscores.'); + } + else if (value === '') { + //Empty input, nothing to validate. + state(RexAddCapabilityDialog.states.empty); + message(''); + } + else { + state(RexAddCapabilityDialog.states.valid); + message(''); + } + newCapabilityName(value); + } + }); + var acceptableStates = [RexAddCapabilityDialog.states.valid, RexAddCapabilityDialog.states.notice]; + _this.isAddButtonEnabled = ko.pureComputed(function () { + return (acceptableStates.indexOf(_this.validationState()) >= 0); + }); + _this.options.buttons = [{ + text: 'Add Capability', + 'class': 'button button-primary', + click: function () { + _this.onConfirm(); + }, + disabled: true + }]; + return _this; + } + RexAddCapabilityDialog.prototype.onOpen = function (event, ui) { + //Clear the input when the dialog is opened. + this.capabilityName(''); + }; + RexAddCapabilityDialog.prototype.onConfirm = function () { + if (!this.isAddButtonEnabled()) { + return; + } + var category = this.editor.addCapability(this.capabilityName().trim()); + this.isOpen(false); + //Note: Maybe the user doesn't need this alert? Hmm. + if (!category || (this.editor.categoryViewMode() === RexRoleEditor.listView)) { + alert('Capability added'); + } + else { + alert('Capability added to the "' + category.getAbsoluteName() + '" category.'); + } + }; + RexAddCapabilityDialog.states = { + valid: 'valid', + empty: 'empty', + notice: 'notice', + error: 'error' + }; + return RexAddCapabilityDialog; +}(RexBaseDialog)); +var RexAddRoleDialog = /** @class */ (function (_super) { + __extends(RexAddRoleDialog, _super); + function RexAddRoleDialog(editor) { + var _this = _super.call(this) || this; + _this.roleName = ko.observable(''); + _this.roleDisplayName = ko.observable(''); + _this.roleToCopyFrom = ko.observable(null); + _this.nameValidationMessage = ko.observable(''); + _this.displayNameValidationMessage = ko.observable(''); + _this.areTooltipsInitialised = false; + var _ = wsAmeLodash; + _this.editor = editor; + _this.options.minWidth = 380; + _this.options.buttons.push({ + text: 'Add Role', + 'class': 'button button-primary', + click: _this.onConfirm.bind(_this), + disabled: true + }); + _this.roleDisplayName.extend({ rateLimit: 10 }); + _this.roleName.extend({ rateLimit: 10 }); + //Role names are restricted - you can only use lowercase Latin letters, numbers and underscores. + var roleNameCharacterGroup = 'a-z0-9_'; + var invalidCharacterRegex = new RegExp('[^' + roleNameCharacterGroup + ']', 'g'); + var numbersOnlyRegex = /^[0-9]+$/; + _this.isNameValid = ko.computed(function () { + var name = _this.roleName().trim(); + var message = _this.nameValidationMessage; + //Name must not be empty. + if (name === '') { + message(''); + return false; + } + //Name can only contain certain characters. + var invalidChars = name.match(invalidCharacterRegex); + if (invalidChars !== null) { + var lastInvalidChar = _.last(invalidChars); + if (lastInvalidChar === ' ') { + lastInvalidChar = 'space'; + } + message('Sorry, <code>' + _.escape(lastInvalidChar) + '</code> is not allowed here.<br>' + + 'Please enter only lowercase English letters, numbers, and underscores.'); + return false; + } + //Numeric names could cause problems with how PHP handles associative arrays. + if (numbersOnlyRegex.test(name)) { + message('Numeric names are not allowed. Please add at least one letter or underscore.'); + return false; + } + //Name must not be a duplicate. + var existingRole = editor.getRole(name); + if (existingRole) { + message('Duplicate role name.'); + return false; + } + //WP stores capabilities and role names in the same associative array, + //so they must be unique with respect to each other. + if (editor.capabilityExists(name)) { + message('Role name can\'t be the same as a capability name.'); + return false; + } + message(''); + return true; + }); + _this.isDisplayNameValid = ko.computed(function () { + var name = _this.roleDisplayName(); + var message = _this.displayNameValidationMessage; + return RexAddRoleDialog.validateDisplayName(name, message); + }); + //Automatically generate a role name from the display name. Basically, turn it into a slug. + var lastAutoRoleName = null; + _this.roleDisplayName.subscribe(function (displayName) { + var slug = _.snakeCase(displayName); + //Use the auto-generated role name only if the user hasn't entered their own. + var currentValue = _this.roleName(); + if ((currentValue === '') || (currentValue === lastAutoRoleName)) { + _this.roleName(slug); + } + lastAutoRoleName = slug; + }); + _this.isAddButtonEnabled = ko.pureComputed({ + read: function () { + return (_this.roleName() !== '') && (_this.roleDisplayName() !== '') + && _this.isNameValid() && _this.isDisplayNameValid(); + }, + deferEvaluation: true + }); + return _this; + } + RexAddRoleDialog.validateDisplayName = function (name, validationMessage) { + name = name.trim(); + if (name === '') { + validationMessage(''); + return false; + } + //You can choose pretty much any display name you like, but we'll forbid special characters + //that might cause problems for plugins that don't escape output for HTML. + if (RexAddRoleDialog.invalidDisplayNameRegex.test(name)) { + validationMessage('Sorry, these characters are not allowed: <code>< > &</code>'); + return false; + } + validationMessage(''); + return true; + }; + RexAddRoleDialog.prototype.onOpen = function (event, ui) { + //Clear dialog fields when it's opened. + this.roleName(''); + this.roleDisplayName(''); + this.roleToCopyFrom(null); + if (!this.areTooltipsInitialised) { + this.setupValidationTooltip('#rex-new-role-display-name', this.displayNameValidationMessage); + this.setupValidationTooltip('#rex-new-role-name', this.nameValidationMessage); + this.areTooltipsInitialised = true; + } + }; + RexAddRoleDialog.prototype.onConfirm = function () { + if (!this.isAddButtonEnabled()) { + return; + } + this.isOpen(false); + var caps = {}; + if (this.roleToCopyFrom()) { + caps = this.roleToCopyFrom().getOwnCapabilities(); + } + this.editor.addRole(this.roleName(), this.roleDisplayName(), caps); + }; + RexAddRoleDialog.invalidDisplayNameRegex = /[><&\r\n\t]/; + return RexAddRoleDialog; +}(RexBaseDialog)); +var RexDeleteRoleDialog = /** @class */ (function (_super) { + __extends(RexDeleteRoleDialog, _super); + function RexDeleteRoleDialog(editor) { + var _this = _super.call(this) || this; + _this.isRoleSelected = {}; + _this.editor = editor; + _this.options.minWidth = 420; + _this.options.buttons.push({ + text: 'Delete Role', + 'class': 'button button-primary', + click: _this.onConfirm.bind(_this), + disabled: true + }); + _this.isDeleteButtonEnabled = ko.pureComputed({ + read: function () { + return _this.getSelectedRoles().length > 0; + }, + deferEvaluation: true + }); + return _this; + } + RexDeleteRoleDialog.prototype.onConfirm = function () { + var _ = wsAmeLodash; + var rolesToDelete = this.getSelectedRoles(); + //Warn about the dangers of deleting built-in roles. + var selectedBuiltInRoles = _.filter(rolesToDelete, _.method('isBuiltIn')); + if (selectedBuiltInRoles.length > 0) { + var warning = 'Caution: Deleting default roles like ' + _.first(selectedBuiltInRoles).displayName() + + ' can prevent you from using certain plugins. This is because some plugins look for specific' + + ' role names to determine if a user is allowed to access the plugin.' + + '\nDelete ' + selectedBuiltInRoles.length + ' default role(s)?'; + if (!confirm(warning)) { + return; + } + } + this.editor.deleteRoles(rolesToDelete); + this.isOpen(false); + }; + RexDeleteRoleDialog.prototype.onOpen = function (event, ui) { + //Deselect all previously selected roles. + wsAmeLodash.forEach(this.isRoleSelected, function (isSelected) { + isSelected(false); + }); + }; + RexDeleteRoleDialog.prototype.getSelectionState = function (roleName) { + if (!this.isRoleSelected.hasOwnProperty(roleName)) { + this.isRoleSelected[roleName] = ko.observable(false); + } + return this.isRoleSelected[roleName]; + }; + RexDeleteRoleDialog.prototype.getSelectedRoles = function () { + var _this = this; + var _ = wsAmeLodash; + var rolesToDelete = []; + _.forEach(this.editor.roles(), function (role) { + if (_this.getSelectionState(role.name())()) { + rolesToDelete.push(role); + } + }); + return rolesToDelete; + }; + return RexDeleteRoleDialog; +}(RexBaseDialog)); +var RexRenameRoleDialog = /** @class */ (function (_super) { + __extends(RexRenameRoleDialog, _super); + function RexRenameRoleDialog(editor) { + var _this = _super.call(this) || this; + _this.selectedRole = ko.observable(null); + _this.newDisplayName = ko.observable(''); + _this.displayNameValidationMessage = ko.observable(''); + _this.isTooltipInitialised = false; + _this.editor = editor; + _this.options.minWidth = 380; + _this.options.buttons.push({ + text: 'Rename Role', + 'class': 'button button-primary', + click: _this.onConfirm.bind(_this), + disabled: true + }); + _this.selectedRole.subscribe(function (role) { + if (role) { + _this.newDisplayName(role.displayName()); + } + }); + _this.isConfirmButtonEnabled = ko.computed({ + read: function () { + return RexAddRoleDialog.validateDisplayName(_this.newDisplayName(), _this.displayNameValidationMessage); + }, + deferEvaluation: true + }); + return _this; + } + RexRenameRoleDialog.prototype.onOpen = function (event, ui) { + var _ = wsAmeLodash; + if (!this.isTooltipInitialised) { + this.setupValidationTooltip('#rex-edited-role-display-name', this.displayNameValidationMessage); + this.isTooltipInitialised = true; + } + //Select either the currently selected role or the first available role. + var selectedActor = this.editor.selectedActor(); + if (selectedActor && (selectedActor instanceof RexRole)) { + this.selectedRole(selectedActor); + } + else { + this.selectedRole(_.first(this.editor.roles())); + } + }; + RexRenameRoleDialog.prototype.onConfirm = function () { + if (!this.isConfirmButtonEnabled()) { + return; + } + if (this.selectedRole()) { + var name_1 = this.newDisplayName().trim(); + this.selectedRole().displayName(name_1); + this.editor.actorSelector.repopulate(); + } + this.isOpen(false); + }; + return RexRenameRoleDialog; +}(RexBaseDialog)); +var RexEagerObservableStringSet = /** @class */ (function () { + function RexEagerObservableStringSet() { + this.items = {}; + } + RexEagerObservableStringSet.prototype.contains = function (item) { + if (!this.items.hasOwnProperty(item)) { + this.items[item] = ko.observable(false); + return false; + } + return this.items[item](); + }; + RexEagerObservableStringSet.prototype.add = function (item) { + if (!this.items.hasOwnProperty(item)) { + this.items[item] = ko.observable(true); + } + else { + this.items[item](true); + } + }; + RexEagerObservableStringSet.prototype.remove = function (item) { + if (this.items.hasOwnProperty(item)) { + this.items[item](false); + } + }; + RexEagerObservableStringSet.prototype.clear = function () { + var _ = wsAmeLodash; + _.forEach(this.items, function (isInSet) { + isInSet(false); + }); + }; + RexEagerObservableStringSet.prototype.getPresenceObservable = function (item) { + if (!this.items.hasOwnProperty(item)) { + this.items[item] = ko.observable(false); + } + return this.items[item]; + }; + RexEagerObservableStringSet.prototype.getAsObject = function (fillValue) { + if (fillValue === void 0) { fillValue = true; } + var _ = wsAmeLodash; + var output = {}; + _.forEach(this.items, function (isInSet, item) { + if (isInSet()) { + output[item] = fillValue; + } + }); + return output; + }; + return RexEagerObservableStringSet; +}()); +var RexObservableEditableRoleSettings = /** @class */ (function () { + function RexObservableEditableRoleSettings() { + this.strategy = ko.observable('auto'); + this.userDefinedList = new RexEagerObservableStringSet(); + } + RexObservableEditableRoleSettings.prototype.toPlainObject = function () { + var roleList = this.userDefinedList.getAsObject(true); + if (wsAmeLodash.isEmpty(roleList)) { + roleList = null; + } + return { + strategy: this.strategy(), + userDefinedList: roleList + }; + }; + return RexObservableEditableRoleSettings; +}()); +var RexUserRoleModule = /** @class */ (function () { + function RexUserRoleModule(selectedActor, roles) { + var _this = this; + this.roleObservables = {}; + this.selectedActor = selectedActor; + this.sortedRoles = ko.computed(function () { + return roles(); + }); + this.primaryRole = ko.computed({ + read: function () { + var actor = selectedActor(); + if ((actor === null) || !actor.canHaveRoles) { + return null; + } + if (actor instanceof RexUser) { + var roles_1 = actor.roles(); + if (roles_1.length < 1) { + return null; + } + return roles_1[0]; + } + return null; + }, + write: function (newRole) { + var actor = selectedActor(); + if ((actor === null) || !actor.canHaveRoles || !(actor instanceof RexUser)) { + return; + } + //No primary role = no roles at all. + if (newRole === null) { + actor.roles.removeAll(); + return; + } + //Sanity check. + if (!(newRole instanceof RexRole)) { + return; + } + if (!_this.canAssignRoleToActor(newRole)) { + return; + } + //Remove the previous primary role. + var oldPrimaryRole = (actor.roles().length > 0) ? actor.roles()[0] : null; + if (oldPrimaryRole !== null) { + actor.roles.remove(oldPrimaryRole); + } + //If the user already has the new role, remove it from its old position first. + if (actor.roles.indexOf(newRole) !== -1) { + actor.roles.remove(newRole); + } + //Add the role to the top of the list. + actor.roles.unshift(newRole); + } + }); + this.isVisible = ko.pureComputed(function () { + var actor = _this.selectedActor(); + return (actor !== null) && actor.canHaveRoles; + }); + } + // noinspection JSUnusedGlobalSymbols Used in Knockout templates. + RexUserRoleModule.prototype.actorHasRole = function (role) { + var _this = this; + var roleActorId = role.getId(); + if (this.roleObservables.hasOwnProperty(roleActorId) && (this.roleObservables[roleActorId].role === role)) { + return this.roleObservables[roleActorId].selectedActorHasRole; + } + var selectedActorHasRole = ko.computed({ + read: function () { + var actor = _this.selectedActor(); + if ((actor === null) || !actor.canHaveRoles) { + return false; + } + if (actor instanceof RexUser) { + return (actor.roles.indexOf(role) !== -1); + } + return false; + }, + write: function (shouldHaveRole) { + var actor = _this.selectedActor(); + if ((actor === null) || !actor.canHaveRoles || !(actor instanceof RexUser)) { + return; + } + if (!_this.canAssignRoleToActor(role)) { + return; + } + var alreadyHasRole = (actor.roles.indexOf(role) !== -1); + if (shouldHaveRole !== alreadyHasRole) { + if (shouldHaveRole) { + actor.roles.push(role); + } + else { + actor.roles.remove(role); + } + } + } + }); + this.roleObservables[roleActorId] = { + role: role, + selectedActorHasRole: selectedActorHasRole + }; + return selectedActorHasRole; + }; + RexUserRoleModule.prototype.canAssignRoleToActor = function (role) { + //This is a stub. The role editor currently doesn't check editable role settings at edit time. + var actor = this.selectedActor(); + if ((actor === null) || !actor.canHaveRoles) { + return false; + } + return (role instanceof RexRole); + }; + return RexUserRoleModule; +}()); +var RexEditableRolesDialog = /** @class */ (function (_super) { + __extends(RexEditableRolesDialog, _super); + function RexEditableRolesDialog(editor) { + var _this = _super.call(this) || this; + _this.selectedActor = ko.observable(null); + _this.actorSettings = {}; + _this.editor = editor; + _this.visibleActors = ko.observableArray([]); + _this.options.minWidth = 600; + _this.options.buttons.push({ + text: 'Save Changes', + 'class': 'button button-primary', + click: _this.onConfirm.bind(_this), + disabled: false + }); + //Super Admin is always set to "leave unchanged" because + //they can edit all roles. + var superAdmin = editor.getSuperAdmin(); + var superAdminSettings = new RexObservableEditableRoleSettings(); + superAdminSettings.strategy('none'); + var dummySettings = new RexObservableEditableRoleSettings(); + _this.selectedActorSettings = ko.computed(function () { + if (_this.selectedActor() === null) { + return dummySettings; + } + if (_this.selectedActor() === superAdmin) { + return superAdminSettings; + } + var actorId = _this.selectedActor().getId(); + if (!_this.actorSettings.hasOwnProperty(actorId)) { + //This should never happen; the dictionary should be initialised when opening the dialog. + _this.actorSettings[actorId] = new RexObservableEditableRoleSettings(); + } + return _this.actorSettings[actorId]; + }); + _this.editableRoleStrategy = ko.computed({ + read: function () { + return _this.selectedActorSettings().strategy(); + }, + write: function (newValue) { + _this.selectedActorSettings().strategy(newValue); + } + }); + _this.isAutoStrategyAllowed = ko.computed(function () { + var actor = _this.selectedActor(); + if (actor == null) { + return true; + } + return !((actor === superAdmin) + || ((actor instanceof RexUser) && actor.isSuperAdmin)); + }); + _this.isListStrategyAllowed = _this.isAutoStrategyAllowed; + return _this; + } + RexEditableRolesDialog.prototype.onOpen = function (event, ui) { + var _this = this; + var _ = wsAmeLodash; + //Copy editable role settings into observables. + _.forEach(this.editor.actorEditableRoles, function (settings, actorId) { + if (!_this.actorSettings.hasOwnProperty(actorId)) { + _this.actorSettings[actorId] = new RexObservableEditableRoleSettings(); + } + var observableSettings = _this.actorSettings[actorId]; + observableSettings.strategy(settings.strategy); + observableSettings.userDefinedList.clear(); + if (settings.userDefinedList !== null) { + _.forEach(settings.userDefinedList, function (ignored, roleId) { + observableSettings.userDefinedList.add(roleId); + }); + } + }); + this.visibleActors(this.editor.actorSelector.getVisibleActors()); + //Select either the currently selected actor or the first role. + var selectedActor = this.editor.selectedActor(); + if (selectedActor) { + this.selectedActor(selectedActor); + } + else { + this.selectedActor(_.first(this.editor.roles())); + } + }; + RexEditableRolesDialog.prototype.onConfirm = function () { + //Save editable roles + var _ = wsAmeLodash; + var settings = this.editor.actorEditableRoles; + _.forEach(this.actorSettings, function (observableSettings, actorId) { + if (observableSettings.strategy() === 'auto') { + //"auto" is the default so we don't need to store anything. + delete settings[actorId]; + } + else { + settings[actorId] = observableSettings.toPlainObject(); + } + }); + this.isOpen(false); + }; + RexEditableRolesDialog.prototype.isRoleSetToEditable = function (role) { + return this.selectedActorSettings().userDefinedList.getPresenceObservable(role.name()); + }; + RexEditableRolesDialog.prototype.isRoleEnabled = function (role) { + return this.editableRoleStrategy() === 'user-defined-list'; + }; + RexEditableRolesDialog.prototype.selectItem = function (actor) { + this.selectedActor(actor); + }; + RexEditableRolesDialog.prototype.getItemText = function (actor) { + return this.editor.actorSelector.getNiceName(actor); + }; + return RexEditableRolesDialog; +}(RexBaseDialog)); +var RexRoleEditor = /** @class */ (function () { + function RexRoleEditor(data) { + var _this = this; + // noinspection JSUnusedGlobalSymbols + this.categoryViewOptions = [ + RexRoleEditor.hierarchyView, + RexRoleEditor.singleCategoryView, + RexRoleEditor.listView + ]; + this.deprecatedCapabilities = {}; + this.userDefinedCapabilities = {}; + this.categoriesBySlug = {}; + this.actorLookup = {}; + var self = this; + var _ = wsAmeLodash; + this.areBindingsApplied = ko.observable(false); + this.isLoaded = ko.computed(function () { + return _this.areBindingsApplied(); + }); + this.userPreferences = new RexUserPreferences(data.userPreferences, data.adminAjaxUrl, data.updatePreferencesNonce); + var preferences = this.userPreferences; + this.showDeprecatedEnabled = preferences.getObservable('showDeprecatedEnabled', true); + this.showRedundantEnabled = preferences.getObservable('showRedundantEnabled', false); + this.showBaseCapsEnabled = ko.computed(this.showRedundantEnabled); + this.showOnlyCheckedEnabled = preferences.getObservable('showOnlyCheckedEnabled', false); + this.categoryWidthMode = preferences.getObservable('categoryWidthMode', 'adaptive'); + this.readableNamesEnabled = preferences.getObservable('readableNamesEnabled', true); + this.showNumberOfCapsEnabled = preferences.getObservable('showNumberOfCapsEnabled', true); + this.showGrantedCapCountEnabled = preferences.getObservable('showGrantedCapCountEnabled', true); + this.showTotalCapCountEnabled = preferences.getObservable('showTotalCapCountEnabled', true); + this.showZerosEnabled = preferences.getObservable('showZerosEnabled', false); + this.inheritanceOverrideEnabled = preferences.getObservable('inheritanceOverrideEnabled', false); + //Remember and restore the selected view mode. + var viewModeId = preferences.getObservable('categoryVewMode', 'hierarchy'); + var initialViewMode = _.find(this.categoryViewOptions, 'id', viewModeId()); + if (!initialViewMode) { + initialViewMode = RexRoleEditor.hierarchyView; + } + this.categoryViewMode = ko.observable(initialViewMode); + this.categoryViewMode.subscribe(function (newMode) { + viewModeId(newMode.id); + }); + this.isShiftKeyDown = ko.observable(false); + this.capabilityViewClasses = ko.pureComputed({ + read: function () { + var viewMode = _this.categoryViewMode(); + var classes = ['rex-category-view-mode-' + viewMode.id]; + if (viewMode === RexRoleEditor.singleCategoryView) { + classes.push('rex-show-category-subheadings'); + } + if (_this.readableNamesEnabled()) { + classes.push('rex-readable-names-enabled'); + } + if (_this.categoryWidthMode() === 'full') { + classes.push('rex-full-width-categories'); + } + return classes.join(' '); + }, + deferEvaluation: true + }); + this.searchQuery = ko.observable('').extend({ rateLimit: { timeout: 100, method: "notifyWhenChangesStop" } }); + this.searchKeywords = ko.computed(function () { + var query = self.searchQuery().trim(); + if (query === '') { + return []; + } + return wsAmeLodash(query.split(' ')) + .map(function (keyword) { + return keyword.trim(); + }) + .filter(function (keyword) { + return (keyword !== ''); + }) + .value(); + }); + this.components = _.mapValues(data.knownComponents, function (details, id) { + return RexWordPressComponent.fromJs(id, details); + }); + this.coreComponent = new RexWordPressComponent(':wordpress:', 'WordPress core'); + this.components[':wordpress:'] = this.coreComponent; + //Populate roles and users. + var tempRoleList = []; + _.forEach(data.roles, function (roleData) { + var role = new RexRole(roleData.name, roleData.displayName, roleData.capabilities); + role.hasUsers = roleData.hasUsers; + tempRoleList.push(role); + _this.actorLookup[role.id()] = role; + }); + this.roles = ko.observableArray(tempRoleList); + var tempUserList = []; + _.forEach(AmeActors.getUsers(), function (data) { + var user = RexUser.fromAmeUser(data, self); + tempUserList.push(user); + _this.actorLookup[user.id()] = user; + }); + this.users = ko.observableArray(tempUserList); + this.dummyActor = new RexRole('rex-invalid-role', 'Invalid Role'); + this.defaultNewUserRoleName = data.defaultRoleName; + this.trashedRoles = ko.observableArray(_.map(data.trashedRoles, function (roleData) { + return RexRole.fromRoleData(roleData); + })); + this.actorSelector = new AmeActorSelector(this, true, false); + //Wrap the selected actor in a computed observable so that it can be used with Knockout. + var _selectedActor = ko.observable(this.getActor(this.actorSelector.selectedActor)); + this.selectedActor = ko.computed({ + read: function () { + return _selectedActor(); + }, + write: function (newActor) { + _this.actorSelector.setSelectedActor(newActor.id()); + } + }); + this.actorSelector.onChange(function (newSelectedActor) { + _selectedActor(_this.getActor(newSelectedActor)); + }); + //Refresh the actor selector when roles are added or removed. + this.roles.subscribe(function () { + _this.actorSelector.repopulate(); + }); + //Re-select the previously selected actor if possible. + var initialActor = null; + if (data.selectedActor) { + initialActor = this.getActor(data.selectedActor); + } + if (!initialActor || (initialActor === this.dummyActor)) { + initialActor = this.roles()[0]; + } + this.selectedActor(initialActor); + //Populate capabilities. + this.deprecatedCapabilities = data.deprecatedCapabilities; + this.metaCapabilityMap = data.metaCapMap; + this.userDefinedCapabilities = data.userDefinedCapabilities; + this.capabilities = _.mapValues(data.capabilities, function (metadata, name) { + return RexCapability.fromJs(name, metadata, self); + }); + //Add the special "do_not_allow" capability. Normally, it's impossible to assign it to anyone, + //but it can still be used in post type permissions and other places. + var doNotAllow = new RexDoNotAllowCapability(this); + doNotAllow.originComponent = this.components[':wordpress:']; + this.capabilities['do_not_allow'] = doNotAllow; + //Similarly, "exist" is always enabled for all roles and users. Everyone can exist. + if (this.capabilities.hasOwnProperty('exist')) { + this.capabilities['exist'] = new RexExistCapability(this); + this.capabilities['exist'].originComponent = this.components[':wordpress:']; + } + //Store editable roles. + this.actorEditableRoles = (!_.isEmpty(data.editableRoles)) ? data.editableRoles : {}; + this.rootCategory = new RexCategory('All', this); + var coreCategory = RexCategory.fromJs(data.coreCategory, this); + this.rootCategory.addSubcategory(coreCategory); + var postTypeCategory = new RexPostTypeContainerCategory('Post Types', this, 'postTypes'); + this.postTypes = _.indexBy(data.postTypes, 'name'); + _.forEach(this.postTypes, function (details, id) { + var category = new RexPostTypeCategory(details.label, self, id, 'postTypes/' + id, details.permissions, details.isDefault); + if (details.componentId) { + category.origin = _this.getComponent(details.componentId); + } + postTypeCategory.addSubcategory(category); + //Record the post type actions associated with each capability. + for (var action in details.permissions) { + var capability = self.getCapability(details.permissions[action]); + _.set(capability.usedByPostTypeActions, [details.name, action], true); + } + }); + //Sort the actual subcategory array. + postTypeCategory.sortSubcategories(); + this.rootCategory.addSubcategory(postTypeCategory); + //Taxonomies. + this.taxonomies = data.taxonomies; + var taxonomyCategory = new RexTaxonomyContainerCategory('Taxonomies', this, 'taxonomies'); + _.forEach(data.taxonomies, function (details, id) { + var category = new RexTaxonomyCategory(details.label, self, id, 'taxonomies/' + id, details.permissions); + taxonomyCategory.addSubcategory(category); + //Record taxonomy type actions associated with each capability. + for (var action in details.permissions) { + var capability = self.getCapability(details.permissions[action]); + _.set(capability.usedByTaxonomyActions, [details.name, action], true); + } + }); + taxonomyCategory.subcategories.sort(function (a, b) { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }); + this.rootCategory.addSubcategory(taxonomyCategory); + var customParentCategory = new RexCategory('Plugins', this, 'custom'); + function initCustomCategory(details, parent) { + var category = RexCategory.fromJs(details, self); + //Sort subcategories by title. + category.subcategories.sort(function (a, b) { + //Keep the "General" category at the top if there is one. + if (a.name === b.name) { + return 0; + } + else if (a.name === 'General') { + return -1; + } + else if (b.name === 'General') { + return 1; + } + return a.name.localeCompare(b.name); + }); + parent.addSubcategory(category); + } + _.forEach(data.customCategories, function (details) { + initCustomCategory(details, customParentCategory); + }); + customParentCategory.subcategories.sort(function (a, b) { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }); + this.rootCategory.addSubcategory(customParentCategory); + //Make a category for uncategorized capabilities. This one is always at the bottom. + var uncategorizedCategory = new RexCategory('Uncategorized', self, 'custom/uncategorized', data.uncategorizedCapabilities); + customParentCategory.addSubcategory(uncategorizedCategory); + var _selectedCategory = ko.observable(null); + this.selectedCategory = ko.computed({ + read: function () { + return _selectedCategory(); + }, + write: function (newSelection) { + var oldSelection = _selectedCategory(); + if (newSelection === oldSelection) { + return; + } + if (newSelection) { + newSelection.isSelected(true); + } + if (oldSelection) { + oldSelection.isSelected(false); + } + _selectedCategory(newSelection); + } + }); + this.selectedCategory(this.rootCategory); + this.permissionTipSubject = ko.observable(null); + this.allCapabilitiesAsPermissions = ko.pureComputed({ + read: function () { + //Create a permission for each unique, non-deleted capability. + //Exclude special caps like do_not_allow and exist because they can't be enabled. + var excludedCaps = ['do_not_allow', 'exist']; + return _.chain(_this.capabilities) + .map(function (capability) { + if (excludedCaps.indexOf(capability.name) >= 0) { + return null; + } + return new RexPermission(self, capability); + }) + .filter(function (value) { + return value !== null; + }) + .value(); + }, + deferEvaluation: true + }); + this.capsInSelectedCategory = ko.pureComputed({ + read: function () { + var category = _this.selectedCategory(); + if (!category) { + return {}; + } + var caps = {}; + category.countUniqueCapabilities(caps); + return caps; + }, + deferEvaluation: true + }); + this.leafCategories = ko.computed({ + read: function () { + //So what we want here is a depth-first traversal of the category tree. + var results = []; + var addedUniqueCategories = {}; + function traverse(category) { + if (category.subcategories.length < 1) { + //Eliminate duplicates, like CPTs that show up in the post type category and a plugin category. + var key = category.getDeDuplicationKey(); + if (!addedUniqueCategories.hasOwnProperty(key)) { + results.push(category); + addedUniqueCategories[key] = category; + } + else { + addedUniqueCategories[key].addDuplicate(category); + } + return; + } + for (var i = 0; i < category.subcategories.length; i++) { + traverse(category.subcategories[i]); + } + } + traverse(_this.rootCategory); + results.sort(function (a, b) { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }); + return results; + }, + deferEvaluation: true + }); + var compareRoleDisplayNames = function (a, b) { + return a.displayName().toLowerCase().localeCompare(b.displayName().toLowerCase()); + }; + this.defaultRoles = ko.pureComputed({ + read: function () { + return _.filter(self.roles(), function (role) { + return role.isBuiltIn(); + }).sort(compareRoleDisplayNames); + }, + deferEvaluation: true + }); + this.customRoles = ko.computed({ + read: function () { + return _.difference(self.roles(), self.defaultRoles()).sort(compareRoleDisplayNames); + }, + deferEvaluation: true + }); + this.deleteCapabilityDialog = new RexDeleteCapDialog(this); + this.addCapabilityDialog = new RexAddCapabilityDialog(this); + this.addRoleDialog = new RexAddRoleDialog(this); + this.deleteRoleDialog = new RexDeleteRoleDialog(this); + this.renameRoleDialog = new RexRenameRoleDialog(this); + this.editableRolesDialog = new RexEditableRolesDialog(this); + this.userRoleModule = new RexUserRoleModule(this.selectedActor, this.roles); + this.settingsFieldData = ko.observable(''); + this.isSaving = ko.observable(false); + this.isGlobalSettingsUpdate = ko.observable(false); + } + RexRoleEditor.prototype.capabilityMatchesFilters = function (capability) { + if (!this.showDeprecatedEnabled() && this.isDeprecated(capability.name)) { + return false; + } + if (this.showOnlyCheckedEnabled() && !capability.isEnabledForSelectedActor()) { + return false; + } + var keywords = this.searchKeywords(), capabilityName = capability.name; + if (keywords.length > 0) { + var haystack_1 = capabilityName.toLowerCase(); + var matchesKeywords = wsAmeLodash.all(keywords, function (keyword) { + return haystack_1.indexOf(keyword) >= 0; + }); + if (!matchesKeywords) { + return false; + } + } + return true; + }; + RexRoleEditor.prototype.isDeprecated = function (capability) { + return this.deprecatedCapabilities.hasOwnProperty(capability); + }; + RexRoleEditor.prototype.getComponent = function (componentId) { + if (this.components.hasOwnProperty(componentId)) { + return this.components[componentId]; + } + return null; + }; + /** + * Get or create a capability instance. + */ + RexRoleEditor.prototype.getCapability = function (capabilityName, recursionDepth) { + if (recursionDepth === void 0) { recursionDepth = 0; } + //Un-map meta capabilities where possible. + if (this.metaCapabilityMap.hasOwnProperty(capabilityName) && (recursionDepth < 10)) { + return this.getCapability(this.metaCapabilityMap[capabilityName], recursionDepth + 1); + } + if (!this.capabilities.hasOwnProperty(capabilityName)) { + var _1 = wsAmeLodash; + if (!_1.isString(capabilityName) && !_1.isFinite(capabilityName)) { + return this.getInvalidCapability(capabilityName); + } + if (console && console.info) { + console.info('Capability not found: "' + capabilityName + '". It will be created.'); + } + capabilityName = String(capabilityName); + this.capabilities[capabilityName] = new RexCapability(capabilityName, this); + } + return this.capabilities[capabilityName]; + }; + RexRoleEditor.prototype.getInvalidCapability = function (invalidName) { + var capabilityName = '[Invalid capability: ' + String(invalidName) + ']'; + if (!this.capabilities.hasOwnProperty(capabilityName)) { + if (console && console.error) { + console.error('Invalid capability detected - expected a string but got this: ', invalidName); + } + this.capabilities[capabilityName] = new RexInvalidCapability(capabilityName, invalidName, this); + } + return this.capabilities[capabilityName]; + }; + RexRoleEditor.prototype.getActor = function (actorId) { + if (this.actorLookup.hasOwnProperty(actorId)) { + return this.actorLookup[actorId]; + } + return this.dummyActor; + }; + RexRoleEditor.prototype.getRole = function (name) { + var actorId = 'role:' + name; + if (this.actorLookup.hasOwnProperty(actorId)) { + var role = this.actorLookup[actorId]; + if (role instanceof RexRole) { + return role; + } + } + return null; + }; + // noinspection JSUnusedGlobalSymbols Testing method used in KO templates. + RexRoleEditor.prototype.setSubjectPermission = function (permission) { + this.permissionTipSubject(permission); + }; + /** + * Search a string for the current search keywords and add the "rex-search-highlight" CSS class to each match. + * + * @param inputString + */ + RexRoleEditor.prototype.highlightSearchKeywords = function (inputString) { + var _ = wsAmeLodash; + var keywordList = this.searchKeywords(); + if (keywordList.length === 0) { + return inputString; + } + var keywordGroup = _.map(keywordList, _.escapeRegExp).join('|'); + var regex = new RegExp('((?:' + keywordGroup + ')(?:\\s*))+', 'gi'); + return inputString.replace(regex, function (foundKeywords) { + //Don't highlight the trailing space after the keyword(s). + var trailingSpace = ''; + var parts = foundKeywords.match(/^(.+?)(\s+)$/); + if (parts) { + foundKeywords = parts[1]; + trailingSpace = parts[2]; + } + return '<mark class="rex-search-highlight">' + foundKeywords + '</mark>' + trailingSpace; + }); + }; + RexRoleEditor.prototype.actorExists = function (actorId) { + return this.actorLookup.hasOwnProperty(actorId); + }; + RexRoleEditor.prototype.addUsers = function (newUsers) { + var _this = this; + wsAmeLodash.forEach(newUsers, function (user) { + if (!(user instanceof RexUser)) { + if (console.error) { + console.error('Cannot add a user. Expected an instance of RexUser, got this:', user); + } + return; + } + if (!_this.actorLookup.hasOwnProperty(user.getId())) { + _this.users.push(user); + _this.actorLookup[user.getId()] = user; + } + }); + }; + RexRoleEditor.prototype.createUserFromProperties = function (properties) { + return RexUser.fromAmeUserProperties(properties, this); + }; + RexRoleEditor.prototype.getRoles = function () { + return wsAmeLodash.indexBy(this.roles(), function (role) { + return role.name(); + }); + }; + RexRoleEditor.prototype.getSuperAdmin = function () { + return RexSuperAdmin.getInstance(); + }; + RexRoleEditor.prototype.getUser = function (login) { + var actorId = 'user:' + login; + if (this.actorLookup.hasOwnProperty(actorId)) { + var user = this.actorLookup[actorId]; + if (user instanceof RexUser) { + return user; + } + } + return null; + }; + RexRoleEditor.prototype.getUsers = function () { + return wsAmeLodash.indexBy(this.users(), 'userLogin'); + }; + RexRoleEditor.prototype.isInSelectedCategory = function (capabilityName) { + var caps = this.capsInSelectedCategory(); + return caps.hasOwnProperty(capabilityName); + }; + RexRoleEditor.prototype.addCapability = function (capabilityName) { + var capability; + if (this.capabilities.hasOwnProperty(capabilityName)) { + capability = this.capabilities[capabilityName]; + if (!capability.isDeleted()) { + throw 'Cannot add capability "' + capabilityName + '" because it already exists.'; + } + capability.isDeleted(false); + this.userDefinedCapabilities[capabilityName] = true; + return null; + } + else { + capability = new RexCapability(capabilityName, this); + capability.notes = 'This capability has not been saved yet. Click the "Save Changes" button to save it.'; + this.capabilities[capabilityName] = capability; + //Add the new capability to the "Other" or "Uncategorized" category. + var category = this.categoriesBySlug['custom/uncategorized']; + var permission = new RexPermission(this, capability); + category.permissions.push(permission); + category.sortPermissions(); + this.userDefinedCapabilities[capabilityName] = true; + return category; + } + }; + RexRoleEditor.prototype.deleteCapabilities = function (selectedCapabilities) { + var self = this, _ = wsAmeLodash; + var targetActors = _.union(this.roles(), this.users()); + _.forEach(selectedCapabilities, function (capability) { + //Remove it from all roles and visible users. + _.forEach(targetActors, function (actor) { + actor.deleteCap(capability.name); + }); + capability.isDeleted(true); + delete self.userDefinedCapabilities[capability.name]; + }); + }; + RexRoleEditor.prototype.capabilityExists = function (capabilityName) { + return this.capabilities.hasOwnProperty(capabilityName) && !this.capabilities[capabilityName].isDeleted(); + }; + RexRoleEditor.prototype.addRole = function (name, displayName, capabilities) { + if (capabilities === void 0) { capabilities = {}; } + var role = new RexRole(name, displayName, capabilities); + this.actorLookup[role.id()] = role; + this.roles.push(role); + //Select the new role. + this.selectedActor(role); + return role; + }; + RexRoleEditor.prototype.deleteRoles = function (roles) { + var _this = this; + var _ = wsAmeLodash; + _.forEach(roles, function (role) { + if (!_this.canDeleteRole(role)) { + throw 'Cannot delete role "' + role.name() + '"'; + } + }); + this.roles.removeAll(roles); + this.trashedRoles.push.apply(this.trashedRoles, roles); + //TODO: Later, add an option to restore deleted roles. + }; + RexRoleEditor.prototype.canDeleteRole = function (role) { + //Was the role already assigned to any users when the editor was opened? + if (role.hasUsers) { + return false; + } + //We also need to take into account any unsaved user role changes. + //Is the role assigned to any of the users currently loaded in the editor? + var _ = wsAmeLodash; + if (_.some(this.users(), function (user) { + return (user.roles.indexOf(role) !== -1); + })) { + return false; + } + return !this.isDefaultRoleForNewUsers(role); + }; + RexRoleEditor.prototype.isDefaultRoleForNewUsers = function (role) { + return (role.name() === this.defaultNewUserRoleName); + }; + // noinspection JSUnusedGlobalSymbols Used in KO templates. + RexRoleEditor.prototype.saveChanges = function () { + this.isSaving(true); + var _ = wsAmeLodash; + var data = { + 'roles': _.invoke(this.roles(), 'toJs'), + 'users': _.invoke(this.users(), 'toJs'), + 'trashedRoles': _.invoke(this.trashedRoles(), 'toJs'), + 'userDefinedCaps': _.keys(this.userDefinedCapabilities), + 'editableRoles': this.actorEditableRoles + }; + this.settingsFieldData(ko.toJSON(data)); + jQuery('#rex-save-settings-form').submit(); + }; + RexRoleEditor.prototype.updateAllSites = function () { + if (!confirm('Apply these role settings to ALL sites? Any changes that you\'ve made to individual sites will be lost.')) { + return false; + } + this.isGlobalSettingsUpdate(true); + this.saveChanges(); + }; + RexRoleEditor.hierarchyView = { + label: 'Hierarchy view', + id: 'hierarchy', + templateName: 'rex-hierarchy-view-template' + }; + RexRoleEditor.singleCategoryView = { + label: 'Category view', + id: 'category', + templateName: 'rex-single-category-view-template' + }; + RexRoleEditor.listView = { label: 'List view', id: 'list', templateName: 'rex-list-view-template' }; + return RexRoleEditor; +}()); +(function () { + jQuery(function ($) { + var rootElement = jQuery('#ame-role-editor-root'); + //Initialize the application. + var app = new RexRoleEditor(wsRexRoleEditorData); + //The input data can be quite large, so let's give the browser a chance to free up that memory. + wsRexRoleEditorData = null; + window['ameRoleEditor'] = app; + //console.time('Apply Knockout bindings'); + //ko.options.deferUpdates = true; + ko.applyBindings(app, rootElement.get(0)); + app.areBindingsApplied(true); + //console.timeEnd('Apply Knockout bindings'); + //Track the state of the Shift key. + var isShiftKeyDown = false; + function handleKeyboardEvent(event) { + var newState = !!(event.shiftKey); + if (newState !== isShiftKeyDown) { + isShiftKeyDown = newState; + app.isShiftKeyDown(isShiftKeyDown); + } + } + $(document).on('keydown.adminMenuEditorRex keyup.adminMenuEditorRex mousedown.adminMenuEditorRex', handleKeyboardEvent); + //Initialize permission tooltips. + var visiblePermissionTooltips = []; + rootElement.find('#rex-capability-view').on('mouseenter click', '.rex-permission-tip-trigger', function (event) { + $(this).qtip({ + overwrite: false, + content: { + text: 'Loading...' + }, + //Show the tooltip on focus. + show: { + event: 'click mouseenter', + delay: 80, + solo: '#ame-role-editor-root', + ready: true, + effect: false + }, + hide: { + event: 'mouseleave unfocus', + fixed: true, + delay: 300, + leave: false, + effect: false + }, + position: { + my: 'center left', + at: 'center right', + effect: false, + viewport: $(window), + adjust: { + method: 'flipinvert shift', + scroll: false, + } + }, + style: { + classes: 'qtip-bootstrap qtip-shadow rex-tooltip' + }, + events: { + show: function (event, api) { + //Immediately hide all other permission tooltips. + for (var i = visiblePermissionTooltips.length - 1; i >= 0; i--) { + visiblePermissionTooltips[i].hide(); + } + var permission = ko.dataFor(api.target.get(0)); + if (permission && (permission instanceof RexPermission)) { + app.permissionTipSubject(permission); + } + //Move the content container to the current tooltip. + var tipContent = $('#rex-permission-tip'); + if (!$.contains(api.elements.content.get(0), tipContent.get(0))) { + api.elements.content.empty().append(tipContent); + } + visiblePermissionTooltips.push(api); + }, + hide: function (event, api) { + var index = visiblePermissionTooltips.indexOf(api); + if (index >= 0) { + visiblePermissionTooltips.splice(index, 1); + } + } + } + }, event); + }); + //Tooltips must have a higher z-index than the modal widget overlay and the Toolbar. + jQuery.fn.qtip.zindex = 100101 + 5000; + //Set up dropdown menus. + $('.rex-dropdown-trigger').on('click', function (event) { + var $trigger = $(this); + var $dropdown = $('#' + $trigger.data('target-dropdown-id')); + event.stopPropagation(); + event.preventDefault(); + function hideThisDropdown(event) { + //Only do it if the user clicked something outside the dropdown. + var $clickedDropdown = $(event.target).closest($dropdown.get(0)); + if ($clickedDropdown.length < 1) { + $dropdown.hide(); + $(document).off('click', hideThisDropdown); + } + } + if ($dropdown.is(':visible')) { + $dropdown.hide(); + $(document).off('click', hideThisDropdown); + return; + } + $dropdown.show().position({ + my: 'left top', + at: 'left bottom', + of: $trigger + }); + $(document).on('click', hideThisDropdown); + }); + }); +})(); +//# sourceMappingURL=role-editor.js.map \ No newline at end of file diff --git a/extras/modules/role-editor/role-editor.js.map b/extras/modules/role-editor/role-editor.js.map new file mode 100644 index 0000000..17de9a2 --- /dev/null +++ b/extras/modules/role-editor/role-editor.js.map @@ -0,0 +1 @@ +{"version":3,"file":"role-editor.js","sourceRoot":"","sources":["role-editor.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/extras/modules/role-editor/role-editor.scss b/extras/modules/role-editor/role-editor.scss new file mode 100644 index 0000000..fdc277b --- /dev/null +++ b/extras/modules/role-editor/role-editor.scss @@ -0,0 +1,955 @@ +@import "../../../css/boxes"; + +$boxBorder: 1px solid $amePostboxBorderColor; +$boxTopPadding: 10px; +$boxLeftPadding: 8px; +$boxPadding: $boxTopPadding $boxLeftPadding; + +$capViewContainerPaddingLeft: $boxTopPadding; +$capViewContainerPaddingTop: $boxTopPadding; + +$onePermissionHeight: 27px; + +@mixin ame-postbox { + border: $boxBorder; + background: #fff; + box-shadow: $amePostboxShadow; +} + +@mixin ame-striped-table { + tbody tr:nth-child(2n+1) { + background-color: #F9F9F9; + } +} + +#rex-loading-message { + margin-top: 10px; +} + +#rex-main-ui { + display: flex; + flex-direction: row; + margin-top: 10px; + width: 100%; +} + +#rex-content-container, +#rex-action-sidebar { + @include ame-postbox; +} + +#rex-content-container { + display: flex; + flex-grow: 80; + padding: 0; + overflow-x: hidden; +} + +#rex-action-sidebar { + box-sizing: border-box; + width: 170px; + flex-grow: 0; + flex-shrink: 0; + align-self: flex-start; + + margin-left: 15px; + padding: $boxPadding; + + .rex-action-separator { + height: 10px; + } +} + +#rex-category-sidebar { + width: 240px; + flex-grow: 0; + flex-shrink: 0; + position: relative; + + border-right: $boxBorder; + padding: $boxTopPadding 0; + + background: #f8f8f8; + + & > ul { + margin-top: 0; + } + + .rex-nav-item { + cursor: pointer; + margin: 0; + padding: 3px $boxLeftPadding 3px $capViewContainerPaddingLeft; + } + + .rex-nav-item:hover { + background-color: #E5F3FF; + } + + .rex-selected-nav-item { + background-color: #CCE8FF; + box-shadow: 0px -1px 0px 0px #99D1FF, 0px 1px 0px 0px #99D1FF; + + //Don't change the color when hovering over a selected item. + &:hover { + background-color: #CCE8FF; + } + } + + $navItemLevelPadding: 13px; + @for $level from 2 through 5 { + .rex-nav-level-#{$level} { + padding-left: ($level - 2) * $navItemLevelPadding; + } + } + + .rex-nav-toggle { + visibility: hidden; + display: inline-block; + + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + + max-height: 100%; + width: 20px; + //background: #A27D35; + text-align: right; + vertical-align: middle; + + &:after { + font-family: dashicons, sans-serif; + content: "\f345"; + } + + &:hover { + color: #3ECEF9; + } + } + + .rex-nav-is-expanded .rex-nav-toggle { + //background: #00aa00; + &:after { + content: "\f347"; + } + } + + .rex-nav-has-children .rex-nav-toggle { + visibility: visible; + } + + .rex-dropdown-trigger { + position: absolute; + right: 0; + top: 0; + + padding: ($boxTopPadding + 2) ($boxLeftPadding + 2) 3px $boxLeftPadding; + } + + //Test styles for flexboxy appearance. + .rex-nav-item { + display: flex; + flex-wrap: nowrap; + align-items: baseline; + + height: 21px; + padding-top: 4px; + padding-bottom: 2px; + + $spaceWidth: 0.3em; + + .rex-nav-toggle { + flex-shrink: 0; + margin-right: $spaceWidth; + align-self: stretch; + padding: 1px 0; + } + + .rex-capability-count { + flex-shrink: 0; + margin-left: $spaceWidth; + margin-right: $spaceWidth; + } + + .rex-nav-item-header { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } +} + +#rex-capability-view-container { + flex-grow: 70; + padding: $capViewContainerPaddingTop $capViewContainerPaddingLeft; + overflow-x: hidden; +} + +#rex-capability-view { + width: 100%; + + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +$oneCategoryWidth: 250px; +$categoryMargin: 8px; + +.rex-category { + box-sizing: border-box; + min-width: 160px; + width: $oneCategoryWidth; + + flex-grow: 0; + flex-shrink: 0; + flex-basis: auto; + + padding: 0; + margin: 0 $categoryMargin*2 $categoryMargin*2 0; + + border: 1px solid $amePostboxBorderColor; + + .rex-category-name { + font-weight: 600; + } + + .rex-category-subheading { + display: none; + color: #666; + font-size: 12px; + font-variant: small-caps; + } + + .rex-category-subtitle { + color: #888; + font-size: 0.95em; + font-family: Consolas, Monaco, monospace; + } + + .rex-category-contents { + box-sizing: border-box; + + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + + padding: 10px; + } + + &.rex-has-subcategories { + width: 100%; + flex-basis: 100%; + } + + .rex-category-header { + padding: 8px 10px; + border-bottom: 1px solid $amePostboxBorderColor; + } + + &.rex-top-category { + border: none; + margin: 0 0 10px 0; + padding: 0; + + & > .rex-category-header { + color: #23282d; + font-size: 1.3em; + margin: 1em 0; + padding: 0; + border-bottom: none; + } + + & > .rex-category-contents { + padding: 0; + } + } + + &.rex-sub-category { + box-shadow: $amePostboxShadow; + + & > .rex-category-header { + background: #fafafa; + } + } +} + +@function desiredCategoryWidth($columns) { + @return $oneCategoryWidth * $columns + ($categoryMargin * 2 * ($columns - 1)); +} + +$maxFixedColumns: 3; +@for $cols from 1 through $maxFixedColumns { + .rex-desired-columns-#{$cols} { + $desiredWidth: desiredCategoryWidth($cols); + width: $desiredWidth; + flex-grow: 0; + max-width: $desiredWidth * 2; + } +} + +.rex-desired-columns-max { + flex-basis: 100%; + width: 100%; +} + +//Switch fixed-size categories to full width on smaller screens. The breakpoints were +//determined empirically and might need to change if other parts of the UI change. +@media screen and (max-width: 1432px) { + $availableWidth: 780; + @for $cols from 2 through $maxFixedColumns { + $desiredWidth: desiredCategoryWidth($cols); + @if $desiredWidth >= $availableWidth { + .rex-desired-columns-#{$cols} { + flex-basis: 100%; + width: 100%; + } + } + } +} + +@media screen and (max-width: 1168px) { + $availableWidth: 516; + @for $cols from 2 through $maxFixedColumns { + $desiredWidth: desiredCategoryWidth($cols); + @if $desiredWidth >= $availableWidth { + .rex-desired-columns-#{$cols} { + flex-basis: 100%; + width: 100%; + } + } + } +} + +//region Full-width categories +//A "full-width" category spans the entire width of the container and displays permissions in columns. +.rex-full-width-categories { + .rex-category { + width: 100%; + max-width: unset; + } + + .rex-desired-columns-1 > .rex-category-contents > .rex-permission-list { + column-count: 1; + max-width: 300px; + } +} + +/* + * Ensure that each category contains no more than the desired number of columns. + * This is done by adding an invisible space filler element to the end of the permission list. + * + * Warning: This hack is not perfect. It can allow n+1 columns sometimes. + */ +$minPermissionsPerColumn: 3; //Note: This must match the minItemsPerColumn variable in role-editor.ts. + +@mixin addColumnFiller($expectedColumns) { + .rex-full-width-categories { + @for $cols from 2 through 5 { + @if $cols < $expectedColumns { + .rex-desired-columns-#{$cols} > .rex-category-contents > .rex-permission-list::after { + content: 'filler'; + display: block; + background: yellowgreen; + font-size: 13px; //Same as permission font size. Might be unnecessary. + + height: ($expectedColumns - $cols) * $minPermissionsPerColumn * $onePermissionHeight; + visibility: hidden; + } + } + } + } +} + +@media screen and (min-width: 1292px) and (max-width: 1501px) { + @include addColumnFiller(3); +} + +@media screen and (min-width: 1502px) and (max-width: 1711px) { + @include addColumnFiller(4); +} + +@media screen and (min-width: 1712px) { + @include addColumnFiller(5); +} + +//endregion + +.rex-show-category-subheadings .rex-category .rex-category-subheading { + display: block; + width: 100%; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.rex-capability-count { + //In the future, we could show the number of caps in a bubble and style it depending + //on how many capabilities are enabled (none/some/all). + //background-color: #eee; + //border: 1px solid #ddd; + //color: #666; + + -webkit-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; + + font-size: 12px; + //padding: 0 7px; + + &.rex-all-capabilities-enabled { + //font-weight: bold; + //color: #494; + //border-color: green; + //background-color: #ddffcc; + } + + &:before { + content: "("; + } + + &:after { + content: ")"; + } +} + +.rex-enabled-capability-count + .rex-total-capability-count { + &:before { + content: "/"; + } +} + +.rex-permission-list { + box-sizing: border-box; + width: 100%; + + columns: 200px; + column-gap: 10px; + + .rex-permission { + //margin-bottom: 6px; + //background: #fafafa; //For development. The color helps to visually estimate the size of the element. + } +} + +.rex-permission { + box-sizing: border-box; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + font-size: 13px; + height: $onePermissionHeight; + vertical-align: baseline; + + break-inside: avoid-column; + + display: flex; + + label, .rex-permission-tip-trigger { + vertical-align: baseline; + padding-top: 3px; + padding-bottom: 3px; + } + + label { + flex-grow: 1; + flex-shrink: 1; + flex-basis: 50px; + + overflow: hidden; + text-overflow: ellipsis; + } + + .rex-permission-tip-trigger { + flex-grow: 0; + flex-shrink: 0; + flex-basis: 20px; + } +} + +.rex-is-redundant { + color: #888; +} + +.rex-is-explicitly-denied input[type=checkbox] { + border-color: red; +} + +@mixin capCheckboxEffect($color) { + background-color: $color; +} + +.rex-is-personal-override { + &.rex-is-explicitly-denied input[type=checkbox] { + @include capCheckboxEffect(#ffe5e5); + } + + input[type=checkbox]:checked { + @include capCheckboxEffect(#d9ffd9); + border-color: green; + } +} + +.rex-is-inherited input[type=checkbox] { + +} + +//region Permission tooltips +.rex-permission-tip-trigger { + //We could hide the trigger by default, but I'm not sure if that's the best option. + visibility: hidden; + + display: inline-block; + min-width: 20px; + height: 100%; + margin: 0; + padding-left: 2px; + + cursor: pointer; + color: #777; //Unsure. Even this softer color still draws more attention than a tooltip should. + + &:hover { + color: #0096dd; + } +} + +.rex-permission:hover { + background-color: #fafafa; + + .rex-permission-tip-trigger { + visibility: visible; + } +} + +.rex-tooltip { + max-width: 700px; + + .rex-tooltip-section-container { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + } + + .rex-tooltip-section { + max-width: 400px; + } +} + +#rex-permission-tip { + overflow-y: auto; + max-height: 600px; + + h4 { + margin-bottom: 0.4em; + } + + .rex-tip-granted-permissions { + list-style: disc inside; + margin-top: 0; + margin-bottom: 0; + } + + .rex-documentation-link { + display: inline-block; + max-width: 100%; + overflow-wrap: break-word; + } +} + +.rex-capability-inheritance-breakdown { + @include ame-striped-table; + + .rex-is-decisive-actor { + td:first-child:after { + content: "\1f844"; //Wide left arrow. + display: inline-block; + font-weight: bold; + margin-left: 0.5em; + } + } +} + +//endregion + +#rex-view-toolbar { + background: #fcfcfc; //Consider #f5f5f5 and #fcfcfc as alternatives. + border-bottom: 1px solid #ddd; + + padding: 0 $boxLeftPadding $boxTopPadding $boxLeftPadding; + margin: (-$capViewContainerPaddingTop) (-$capViewContainerPaddingLeft) (0) (-$capViewContainerPaddingLeft); + + display: flex; + align-items: center; + flex-wrap: wrap; + + & > * { + margin-top: $boxTopPadding; + } + + .button { + vertical-align: middle; + } + + > label { + margin-right: 10px; + } + + .rex-dropdown-trigger .dashicons { + line-height: 1.3; //This isn't quite right, but it will do for now. + } +} + +#rex-quick-search-query { + min-width: 250px; + max-width: 100%; + margin-right: 10px; +} + +#rex-misc-view-options-button { + margin-left: auto; + margin-right: 10px; +} + +#rex-category-view-selector { + +} + + +.rex-search-highlight { + background-color: #ffff00; +} + +//region CPT & Taxonomy tables +.rex-permission-table { + th input[type="checkbox"] { + vertical-align: middle; + margin: -4px 4px -1px 0; + } + + @include ame-striped-table; + + td ul { + margin: 0; + } + + .rex-base-cap-notice { + color: #888; + } +} + +/* Switch to fixed layout in narrow viewports to prevent overflow. */ +@mixin fixedTableLayout($nameColumnWidth: 25%) { + table-layout: fixed; + max-width: 100%; + + .rex-category-name-column { + width: $nameColumnWidth; + } +} + +@media screen and (max-width: 1540px) { + .rex-permission-table { + @include fixedTableLayout(20%); + } + + .rex-readable-names-enabled .rex-permission-table { + @include fixedTableLayout(25%); + } +} + +/* The taxonomy table needs a wider screen because it has more columns. */ +@media screen and (max-width: 1650px) { + #rex-taxonomy-summary-category .rex-permission-table { + @include fixedTableLayout(25%); + } +} + +/* +When in "human readable" mode, the taxonomy table doesn't show capability names, +so it won't overflow its container unless the viewport is very small. +*/ +.rex-readable-names-enabled #rex-taxonomy-summary-category .rex-permission-table { + table-layout: auto; + max-width: 600px; + + .rex-capability-name, .rex-permission-tip-trigger { + display: none; + } + + .rex-permission, th[scope="col"] { + text-align: center; + } + + .rex-category-name-column { + width: unset; + } +} + +@media screen and (max-width: 1200px) { + .rex-readable-names-enabled #rex-taxonomy-summary-category .rex-permission-table { + @include fixedTableLayout(40%); + } +} + +//endregion + +#rex-action-sidebar { + .rex-action-button { + display: block; + margin-bottom: 4px; + width: 100%; + } +} + +#rex-permission-list-view { + column-width: 240px; + column-gap: 16px; + padding-top: $boxLeftPadding; +} + +#rex-category-view-spacer { + width: 100%; + height: $capViewContainerPaddingTop; +} + +.rex-dropdown-trigger { + display: inline-block; + box-sizing: border-box; + cursor: pointer; + + padding: 2px; + color: #aaa; + text-decoration: none; + + &:hover, &:focus { + color: #777; + text-decoration: none; + } +} + +.rex-dropdown { + position: absolute; + + border: $boxBorder; + background: #fff; + box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); + + padding: $boxPadding; + z-index: 100; + + .rex-dropdown-item { + display: block; + margin-bottom: 10px; + + &:last-child { + margin-bottom: 0; + } + } + + .rex-dropdown-sub-item { + margin-left: 1em; + } + + .rex-dropdown-item > .rex-dropdown-item { + margin-bottom: 6px; + + &:last-child { + margin-bottom: 0; + } + } +} + +.ui-dialog { + .ui-dialog-buttonpane { + background: #fcfcfc; + border-top: 1px solid #dfdfdf; + padding: 8px; + + &:after { + clear: both; + content: ""; + min-height: 0; + display: table; + border-collapse: collapse; + } + } + + //In WordPress the "Cancel" option is usually on the left side, + //but AME historically puts it on the right. + .ui-dialog-buttonset { + width: 100%; + + .ui-button.rex-dialog-cancel-button, .ui-button.ame-dialog-cancel-button { + float: right; + margin-right: 0 !important; + } + + .ui-button { + float: left; + } + } +} + +.rex-dialog { + input[type=text], select { + box-sizing: border-box; + display: block; + width: 100%; + } +} + +.rex-dialog-section { + margin-top: 0; +} + +#rex-delete-capability-dialog { + .rex-deletable-capability-container { + max-height: 400px; + overflow-y: auto; + } + + .rex-deletable-capability-list { + margin-top: 0; + list-style-type: none; + } +} + +#rex-add-capability-dialog { + #rex-new-capability-name { + box-sizing: border-box; + width: 100%; + } + + #rex-add-capability-validation-message { + min-height: 40px; + margin-bottom: 6px; + } +} + +#rex-delete-role-dialog { + .rex-deletable-role-list-container { + max-height: 380px; + + overflow-y: auto; + margin-top: 10px; + } + + .rex-deletable-role-list { + table-layout: fixed; + @include ame-striped-table; + } + + .rex-role-name-column > label { + display: inline-block; + width: 100%; + } + + .rex-role-usage-column { + width: 6em; + max-width: 30%; + color: #888; + text-align: right; + } +} + +//region Editable roles dialog +$editableRolesSectionBorder: #ccd0d4; +#rex-editable-roles-container { + display: flex; + + .ame-role-table { + min-width: 190px; + border: 1px solid $editableRolesSectionBorder; + border-right-style: none; + + td { + cursor: pointer; + } + } + + $selectedRowTipColor: white; + + .ame-selected-role-table-row { + background: #CCE8FF; + + .ame-selected-role-tip { + visibility: visible; + } + + .ame-column-role-name { + font-weight: bold; + } + } + + .ame-column-selected-role-tip { + position: relative; + padding: 0; + min-width: 30px; + } + + .ame-selected-role-tip { + visibility: hidden; + + height: 100%; + width: 100%; + box-sizing: border-box; + + position: absolute; + top: 0; + right: -2px; + + border-right: 1px solid $selectedRowTipColor; + + .ame-rex-svg-triangle { + box-sizing: border-box; + position: absolute; + right: 0; + height: 100%; + + polygon { + fill: $selectedRowTipColor; + stroke: $selectedRowTipColor; + stroke-width: 1px; + } + } + } +} + +#rex-editable-roles-options { + padding: 4px $boxTopPadding $boxTopPadding $boxTopPadding; + border: 1px solid $editableRolesSectionBorder; + + fieldset > p:first-of-type { + margin-top: 0; + } +} + +#rex-editable-role-list { + margin-left: 1em; + margin-top: 0; +} + +//endregion + +//region User role list +#rex-user-role-list { + border-right: $boxBorder; + padding: $boxPadding; + + background: #f8f8f8; + + p:first-child { + margin-top: 0; + } +} + +#rex-primary-user-role { + display: block; +} + +.rex-user-role-option-list { + margin-top: 0; +} +//endregion \ No newline at end of file diff --git a/extras/modules/role-editor/role-editor.ts b/extras/modules/role-editor/role-editor.ts new file mode 100644 index 0000000..8ae7a13 --- /dev/null +++ b/extras/modules/role-editor/role-editor.ts @@ -0,0 +1,3845 @@ +/// <reference path="../../../js/knockout.d.ts" /> +/// <reference path="../../../js/lodash-3.10.d.ts" /> +/// <reference path="../../../js/common.d.ts" /> +/// <reference path="../../../js/actor-manager.ts" /> +/// <reference path="../../../js/jquery.d.ts" /> +/// <reference path="../../../js/jqueryui.d.ts" /> +/// <reference path="../../../modules/actor-selector/actor-selector.ts" /> +/// <reference path="../../ko-extensions.ts" /> + +class RexPermission { + public readonly capability: RexCapability; + + labelHtml: KnockoutComputed<string>; + protected readableAction: string = null; + + mainDescription: string = ''; + + isRedundant: boolean = false; + + readonly isVisible: KnockoutComputed<boolean>; + + private editor: RexRoleEditor; + + constructor(editor: RexRoleEditor, capability: RexCapability) { + this.editor = editor; + this.capability = capability; + + const self = this; + + this.labelHtml = ko.pureComputed({ + read: self.getLabelHtml, + deferEvaluation: true, + owner: this + }); + //Prevent freezing when entering a search query. Highlighting keywords in hundreds of capabilities can be slow. + this.labelHtml.extend({rateLimit: {timeout: 50, method: "notifyWhenChangesStop"}}); + + this.isVisible = ko.computed({ + read: function () { + if (!editor.capabilityMatchesFilters(self.capability)) { + return false; + } + + //When in list view, check if the capability is inside the selected category. + if (editor.categoryViewMode() === RexRoleEditor.listView) { + if (!editor.isInSelectedCategory(self.capability.name)) { + return false; + } + } + + if (self.capability.isDeleted()) { + return false; + } + + return !(self.isRedundant && !editor.showRedundantEnabled()); + + }, + owner: this, + deferEvaluation: true + }); + } + + protected getLabelHtml(): string { + let text; + + if ((this.readableAction !== null) && this.editor.readableNamesEnabled()) { + text = this.readableAction; + } else { + text = this.capability.displayName(); + } + + let html = wsAmeLodash.escape(text); + if (this.isVisible()) { + html = this.editor.highlightSearchKeywords(html); + } + + //Let the browser break words on underscores. + html = html.replace(/_/g, '_<wbr>'); + + return html; + } +} + +interface RexComponentData { + name: string; + type?: string; + capabilityDocumentationUrl?: string; +} + +/** + * A basic representation of any component or extension that can add new capabilities. + * This includes plugins, themes, and the WordPress core. + */ +class RexWordPressComponent { + readonly name: string; + readonly id: string; + capabilityDocumentationUrl?: string; + + constructor(id: string, name: string) { + this.id = id; + this.name = name; + } + + static fromJs(id: string, details: RexComponentData): RexWordPressComponent { + const instance = new RexWordPressComponent(id, details.name ? details.name : id); + if (details.capabilityDocumentationUrl) { + instance.capabilityDocumentationUrl = details.capabilityDocumentationUrl; + } + return instance; + } +} + +class RexObservableCapabilityMap { + private readonly initialCapabilities: CapabilityMap; + private capabilities: { [capabilityName: string]: KnockoutObservable<boolean | null> } = {}; + + constructor(initialCapabilities: CapabilityMap) { + if (initialCapabilities) { + this.initialCapabilities = wsAmeLodash.clone(initialCapabilities); + } else { + this.initialCapabilities = {}; + } + } + + getCapabilityState(capabilityName: string): boolean { + const observable = this.getObservable(capabilityName); + return observable(); + } + + setCapabilityState(capabilityName: string, state: boolean | null) { + const observable = this.getObservable(capabilityName); + observable(state); + } + + getAllCapabilities(): CapabilityMap { + const _ = wsAmeLodash; + + let result = this.initialCapabilities ? _.clone(this.initialCapabilities) : {}; + _.forEach(this.capabilities, function (observable, name) { + const isGranted = observable(); + if (isGranted === null) { + delete result[name]; + } else { + result[name] = isGranted; + } + }); + return result; + } + + private getObservable(capabilityName: string): KnockoutObservable<boolean | null> { + if (!this.capabilities.hasOwnProperty(capabilityName)) { + let initialValue = null; + if (this.initialCapabilities.hasOwnProperty(capabilityName)) { + initialValue = this.initialCapabilities[capabilityName]; + } + this.capabilities[capabilityName] = ko.observable(initialValue); + } + return this.capabilities[capabilityName]; + } +} + +abstract class RexBaseActor implements IAmeActor { + /** + * Actor ID. Usually in the form of "prefix:name". + */ + id: KnockoutObservable<string>; + + /** + * Internal role name or user login. + */ + name: KnockoutObservable<string>; + + /** + * Formatted, human-readable name. + */ + displayName: KnockoutObservable<string>; + + canHaveRoles: boolean = false; + + private capabilities: RexObservableCapabilityMap; + + protected constructor(id: string, name: string, displayName: string, capabilities?: CapabilityMap) { + this.id = ko.observable(id); + this.name = ko.observable(name); + this.displayName = ko.observable(displayName); + this.capabilities = new RexObservableCapabilityMap(capabilities || {}); + } + + hasCap(capability: string): boolean { + return (this.capabilities.getCapabilityState(capability) === true); + } + + getCapabilityState(capability: string) { + return this.getOwnCapabilityState(capability); + } + + getOwnCapabilityState(capability: string): boolean | null { + return this.capabilities.getCapabilityState(capability); + } + + setCap(capability: string, enabled: boolean) { + this.capabilities.setCapabilityState(capability, enabled); + } + + deleteCap(capability: string) { + this.capabilities.setCapabilityState(capability, null); + } + + getDisplayName(): string { + return this.displayName(); + } + + getId(): string { + return this.id(); + } + + /** + * Get capabilities that are explicitly assigned/denied to this actor. + * Does not include capabilities that a user inherits from their role(s). + */ + getOwnCapabilities(): CapabilityMap { + return this.capabilities.getAllCapabilities(); + } +} + +class RexRole extends RexBaseActor { + static readonly builtInRoleNames = ['administrator', 'editor', 'author', 'subscriber', 'contributor']; + + hasUsers: boolean = false; + + public constructor(name: string, displayName: string, capabilities?: CapabilityMap) { + super('role:' + name, name, displayName, capabilities); + } + + public static fromRoleData(data: RexRoleData) { + const role = new RexRole(data.name, data.displayName, data.capabilities); + role.hasUsers = data.hasUsers; + return role; + } + + /** + * Is this one of the default roles that are part of WordPress core? + * + * Note: I'm calling this property "built-in" instead of "default" to distinguish it + * from the default role for new users. + */ + isBuiltIn() { + return RexRole.builtInRoleNames.indexOf(this.name()) >= 0; + } + + toJs(): RexStorableRoleData { + return { + name: this.name(), + displayName: this.displayName(), + capabilities: this.getOwnCapabilities() + }; + } +} + +class RexSuperAdmin extends RexBaseActor { + private static instance: RexSuperAdmin = null; + + protected constructor() { + super('special:super_admin', 'Super Admin', 'Super Admin'); + } + + static getInstance(): RexSuperAdmin { + if (RexSuperAdmin.instance === null) { + RexSuperAdmin.instance = new RexSuperAdmin(); + } + return RexSuperAdmin.instance; + } +} + +class RexUser extends RexBaseActor implements IAmeUser { + roles: KnockoutObservableArray<RexRole>; + isSuperAdmin: boolean = false; + userLogin: string; + userId: number; + + constructor(login: string, displayName: string, capabilities?: CapabilityMap, userId?: number) { + super('user:' + login, login, displayName, capabilities); + this.userLogin = login; + this.canHaveRoles = true; + this.roles = ko.observableArray([]); + this.userId = userId; + } + + hasCap(capability: string, outGrantedBy?: RexBaseActor[]): boolean { + return (this.getCapabilityState(capability, outGrantedBy) === true); + } + + getCapabilityState(capability: string, outGrantedBy?: RexBaseActor[]): boolean { + if (capability === 'do_not_allow') { + return false; + } + + if (this.isSuperAdmin) { + if (outGrantedBy) { + outGrantedBy.push(RexSuperAdmin.getInstance()); + } + return (capability !== 'do_not_allow'); + } + + let result = super.getCapabilityState(capability); + if (result !== null) { + if (outGrantedBy) { + outGrantedBy.push(this); + } + return result; + } + + wsAmeLodash.each(this.roles(), (role) => { + const roleHasCap = role.getCapabilityState(capability); + if (roleHasCap !== null) { + if (outGrantedBy) { + outGrantedBy.push(role); + } + result = roleHasCap; + } + }); + + return result; + } + + // noinspection JSUnusedGlobalSymbols Used in KO templates. + getInheritanceDetails(capability: RexCapability): any[] { + const _ = wsAmeLodash; + let results = []; + //Note: Alternative terms include "Assigned", "Granted", "Yes"/"No". + + if (this.isSuperAdmin) { + const superAdmin = RexSuperAdmin.getInstance(); + let description = 'Allow everything'; + if (capability.name === 'do_not_allow') { + description = 'Deny'; + } + results.push({ + actor: superAdmin, + name: superAdmin.displayName(), + description: description + }); + } + + _.each(this.roles(), (role) => { + const roleHasCap = role.getCapabilityState(capability.name); + let description; + if (roleHasCap) { + description = 'Allow'; + } else if (roleHasCap === null) { + description = '—'; + } else { + description = 'Deny'; + } + results.push({ + actor: role, + name: role.displayName(), + description: description, + }); + }); + + let hasOwnCap = super.getCapabilityState(capability.name); + let description; + if (hasOwnCap) { + description = 'Allow'; + } else if (hasOwnCap === null) { + description = '—'; + } else { + description = 'Deny'; + } + results.push({ + actor: this, + name: 'User-specific setting', + description: description, + }); + + let relevantActors = []; + this.getCapabilityState(capability.name, relevantActors); + const decidingActor = _.last(relevantActors); + _.each(results, function (item) { + item.isDecisive = (item.actor === decidingActor); + }); + + return results; + } + + static fromAmeUser(data: AmeUser, editor: RexRoleEditor) { + const user = new RexUser(data.userLogin, data.displayName, data.capabilities, data.userId); + wsAmeLodash.forEach(data.roles, function (roleId) { + const role = editor.getRole(roleId); + if (role) { + user.roles.push(role); + } + }); + return user; + } + + static fromAmeUserProperties(properties: AmeUserPropertyMap, editor: RexRoleEditor) { + const user = new RexUser(properties.user_login, properties.display_name, properties.capabilities); + if (properties.id) { + user.userId = properties.id; + } + wsAmeLodash.forEach(properties.roles, function (roleId) { + const role = editor.getRole(roleId); + if (role) { + user.roles.push(role); + } + }); + return user; + } + + toJs(): RexStorableUserData { + const _ = wsAmeLodash; + let roles = _.invoke(this.roles(), 'name'); + return { + userId: this.userId, + userLogin: this.userLogin, + displayName: this.displayName(), + capabilities: this.getOwnCapabilities(), + roles: roles + }; + } +} + +interface RexStorableRoleData { + name: string; + displayName: string; + capabilities: CapabilityMap; +} + +interface RexRoleData extends RexStorableRoleData { + hasUsers: boolean; +} + +interface RexStorableUserData { + userId?: number; + userLogin: string; + displayName: string; + roles: string[]; + capabilities: CapabilityMap; +} + +interface RexUserData extends RexStorableUserData { + isSuperAdmin: boolean; +} + +type RexCategoryComparisonCallback = (a: RexCategory, b: RexCategory) => number; + +class RexCategory { + slug: string = null; + + name: string; + permissions: KnockoutObservableArray<RexPermission>; + origin: RexWordPressComponent = null; + subtitle: string = null; + htmlId: string = null; + + parent: RexCategory = null; + subheading: KnockoutObservable<string>; + subcategories: RexCategory[] = []; + + sortedSubcategories: KnockoutComputed<RexCategory[]>; + static readonly defaultSubcategoryComparison: RexCategoryComparisonCallback = function (a, b) { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }; + + navSubcategories: KnockoutComputed<RexCategory[]>; + protected subcategoryModificationFlag: KnockoutObservable<number>; + + protected editor: RexRoleEditor; + + areAllPermissionsEnabled: KnockoutComputed<boolean>; + areAnyPermissionsEditable: KnockoutComputed<boolean>; + + /** + * Number of unique capabilities in this category and all of its subcategories. + */ + totalCapabilityCount: KnockoutComputed<number>; + /** + * Number of capabilities that are granted to the currently selected actor. + */ + enabledCapabilityCount: KnockoutComputed<number>; + + isVisible: KnockoutComputed<boolean>; + isSelected: KnockoutObservable<boolean>; + desiredColumnCount: KnockoutComputed<string>; + + isNavExpanded: KnockoutObservable<boolean>; + isNavVisible: KnockoutComputed<boolean>; + + cssClasses: KnockoutComputed<string>; + navCssClasses: KnockoutComputed<string>; + readonly nestingDepth: KnockoutComputed<number>; + contentTemplate: KnockoutObservable<string>; + + isCapCountVisible: KnockoutComputed<boolean>; + isEnabledCapCountVisible: KnockoutComputed<boolean>; + + protected duplicates: RexCategory[] = []; + + constructor(name: string, editor: RexRoleEditor, slug: string = null, capabilities: string[] = []) { + const _ = wsAmeLodash; + const self = this; + this.editor = editor; + + this.name = name; + this.slug = slug; + if ((this.slug !== null) && (this.slug !== '')) { + editor.categoriesBySlug[this.slug] = this; + } + + let initialPermissions = _.map(capabilities, (capabilityName) => { + return new RexPermission(editor, editor.getCapability(capabilityName)); + }); + this.permissions = ko.observableArray(initialPermissions); + this.sortPermissions(); + this.contentTemplate = ko.observable('rex-default-category-content-template'); + + this.isSelected = ko.observable(false); + + this.enabledCapabilityCount = ko.pureComputed({ + read: function () { + return self.countUniqueCapabilities({}, function (capability: RexCapability) { + return capability.isEnabledForSelectedActor(); + }); + }, + deferEvaluation: true, + owner: this + }); + this.enabledCapabilityCount.extend({rateLimit: {timeout: 5, method: "notifyWhenChangesStop"}}); + + this.totalCapabilityCount = ko.pureComputed({ + read: function () { + return self.countUniqueCapabilities(); + }, + deferEvaluation: true, + owner: this + }); + + this.isCapCountVisible = ko.pureComputed({ + read: function () { + if (!editor.showNumberOfCapsEnabled()) { + return false; + } + + const totalCaps = self.totalCapabilityCount(), + enabledCaps = self.enabledCapabilityCount(); + if (!editor.showZerosEnabled() && ((totalCaps === 0) || (enabledCaps === 0))) { + return false; + } + + return editor.showTotalCapCountEnabled() || self.isEnabledCapCountVisible(); + }, + deferEvaluation: true, + owner: this + }); + this.isEnabledCapCountVisible = ko.pureComputed({ + read: function () { + if (!editor.showGrantedCapCountEnabled()) { + return false; + } + return (self.enabledCapabilityCount() > 0) || editor.showZerosEnabled(); + }, + deferEvaluation: true, + owner: this + }); + + this.areAllPermissionsEnabled = ko.computed({ + read: function () { + const items = self.permissions(); + const len = items.length; + + for (let i = 0; i < len; i++) { + if (!items[i].capability.isEnabledForSelectedActor() && items[i].capability.isEditable()) { + return false; + } + } + + for (let i = 0; i < self.subcategories.length; i++) { + if (!self.subcategories[i].areAllPermissionsEnabled()) { + return false; + } + } + + return true; + }, + write: function (enabled) { + const items = self.permissions(); + for (let i = 0; i < items.length; i++) { + let item = items[i]; + if (item.capability.isEditable()) { + item.capability.isEnabledForSelectedActor(enabled); + } + } + + for (let i = 0; i < self.subcategories.length; i++) { + self.subcategories[i].areAllPermissionsEnabled(enabled); + } + }, + deferEvaluation: true, + owner: this + }); + this.areAllPermissionsEnabled.extend({rateLimit: {timeout: 5, method: 'notifyWhenChangesStop'}}); + + this.areAnyPermissionsEditable = ko.pureComputed({ + read: () => { + const items = self.permissions(); + const len = items.length; + + for (let i = 0; i < len; i++) { + if (items[i].capability.isEditable()) { + return true; + } + } + + for (let i = 0; i < self.subcategories.length; i++) { + if (!self.subcategories[i].areAnyPermissionsEditable()) { + return true; + } + } + + return false; + }, + deferEvaluation: true, + owner: this + }); + this.areAnyPermissionsEditable.extend({rateLimit: {timeout: 5, method: 'notifyWhenChangesStop'}}); + + this.isVisible = ko.computed({ + read: function () { + let visible = false; + + let hasVisibleSubcategories = false; + _.forEach(self.subcategories, function (category) { + if (category.isVisible()) { + hasVisibleSubcategories = true; + return false; + } + }); + + //Hide it if not inside the selected category. + let isInSelectedCategory = false, + temp: RexCategory = self; + while (temp !== null) { + if (temp.isSelected()) { + isInSelectedCategory = true; + break; + } + temp = temp.parent; + } + + //In single-category view, the category also counts as "selected" + //if one of its duplicates is selected. + if ( + !isInSelectedCategory + && (self.duplicates.length > 0) + && (editor.categoryViewMode() === RexRoleEditor.singleCategoryView) + ) { + for (let i = 0; i < self.duplicates.length; i++) { + temp = self.duplicates[i]; + while (temp !== null) { + if (temp.isSelected()) { + isInSelectedCategory = true; + break; + } + temp = temp.parent; + } + if (isInSelectedCategory) { + break; + } + } + } + + if (!isInSelectedCategory && !hasVisibleSubcategories) { + return false; + } + + //Stay visible as long as at least one subcategory or permission is visible. + visible = hasVisibleSubcategories; + _.forEach(self.permissions(), function (permission) { + if (permission.isVisible()) { + visible = true; + return false; + } + }); + + return visible; + }, + deferEvaluation: true, + owner: this, + }); + this.isVisible.extend({ + rateLimit: { + timeout: 10, + method: 'notifyWhenChangesStop' + } + }); + + this.desiredColumnCount = ko.computed({ + read: function () { + let visiblePermissions = 0; + _.forEach(self.permissions(), function (permission) { + if (permission.isVisible()) { + visiblePermissions++; + } + }); + + let minItemsPerColumn = 12; + if (editor.categoryWidthMode() === 'full') { + minItemsPerColumn = 3; + } + + let desiredColumns = Math.max(Math.ceil(visiblePermissions / minItemsPerColumn), 1); + //Avoid situations where the last column has only one item (an orphan). + if ((desiredColumns >= 2) && (visiblePermissions % minItemsPerColumn === 1)) { + desiredColumns--; + } + if (desiredColumns > 3) { + return 'max'; + } + + return desiredColumns.toString(10); + }, + deferEvaluation: true + }); + + this.nestingDepth = ko.pureComputed({ + read: function () { + if (self.parent !== null) { + return self.parent.nestingDepth() + 1; + } + return 1; + }, + deferEvaluation: true + }); + + this.isNavExpanded = ko.observable( + (this.slug !== null) ? !editor.userPreferences.collapsedCategories.peek(this.slug) : true + ); + + if (this.slug) { + this.isNavExpanded.subscribe((newValue: boolean) => { + editor.userPreferences.collapsedCategories.toggle(this.slug, !newValue); + }); + } + + this.isNavVisible = ko.pureComputed({ + read: function () { + if (self.parent === null) { + return true; + } + return self.parent.isNavVisible() && self.parent.isNavExpanded(); + //Idea: We could hide it if all of the capabilities it contains have been deleted. + }, + deferEvaluation: true + }); + + this.cssClasses = ko.computed({ + read: function () { + let classes = []; + if (self.subcategories.length > 0) { + classes.push('rex-has-subcategories'); + } + if (self.parent) { + if (self.parent === editor.rootCategory) { + classes.push('rex-top-category'); + } else { + classes.push('rex-sub-category'); + } + } + + if (self.permissions().length > 0) { + classes.push('rex-desired-columns-' + self.desiredColumnCount()); + } + return classes.join(' '); + }, + deferEvaluation: true + }); + + this.navCssClasses = ko.pureComputed({ + read: function () { + let classes = []; + if (self.isSelected()) { + classes.push('rex-selected-nav-item'); + } + if (self.isNavExpanded()) { + classes.push('rex-nav-is-expanded'); + } + if (self.subcategories.length > 0) { + classes.push('rex-nav-has-children'); + } + classes.push('rex-nav-level-' + self.nestingDepth()); + return classes.join(' '); + }, + deferEvaluation: true + }); + + this.subcategoryModificationFlag = ko.observable(this.subcategories.length); + this.sortedSubcategories = ko.pureComputed({ + read: () => { + //Refresh the sorted list when categories are added or removed. + this.subcategoryModificationFlag(); + return this.getSortedSubcategories(); + }, + deferEvaluation: true + }); + + this.navSubcategories = ko.pureComputed({ + read: () => { + this.subcategoryModificationFlag(); + return this.subcategories; + }, + deferEvaluation: true + }); + + this.subheading = ko.pureComputed({ + read: () => { + return this.getSubheadingItems().join(', '); + }, + deferEvaluation: true + }); + } + + addSubcategory(category: RexCategory, afterName?: string) { + category.parent = this; + if (afterName) { + const index = wsAmeLodash.findIndex(this.subcategories, {'name': afterName}); + if (index > -1) { + this.subcategories.splice(index + 1, 0, category); + this.subcategoryModificationFlag(this.subcategories.length); + return; + } + } + this.subcategories.push(category); + this.subcategoryModificationFlag(this.subcategories.length); + } + + // noinspection JSUnusedGlobalSymbols Used in KO templates. + toggleSubcategories() { + this.isNavExpanded(!this.isNavExpanded()); + } + + protected getSortedSubcategories(): RexCategory[] { + //In most cases, the subcategory list is already sorted either alphabetically or in a predefined order + //chosen for specific category. Subcategories can override this method to change the sort order. + return this.subcategories; + } + + /** + * Sort the permissions in this category. Doesn't affect subcategories. + * The default sort is alphabetical, but subclasses can override this method to specify a custom order. + */ + sortPermissions() { + this.permissions.sort(function (a, b) { + return a.capability.name.toLowerCase().localeCompare(b.capability.name.toLowerCase()); + }); + } + + countUniqueCapabilities(accumulator: AmeDictionary<boolean> = {}, predicate: Function = null): number { + let total = 0; + const permissions = this.permissions(); + + for (let i = 0; i < permissions.length; i++) { + const capability = permissions[i].capability; + if (accumulator.hasOwnProperty(capability.name)) { + continue; + } + if (predicate && !predicate(capability)) { + continue; + } + if (capability.isDeleted()) { + continue; + } + + accumulator[capability.name] = true; + total++; + } + + for (let i = 0; i < this.subcategories.length; i++) { + total = total + this.subcategories[i].countUniqueCapabilities(accumulator, predicate); + } + + return total; + } + + protected findCategoryBySlug(slug: string): RexCategory { + if (this.editor.categoriesBySlug.hasOwnProperty(slug)) { + return this.editor.categoriesBySlug[slug]; + } + return null; + } + + static fromJs(details: RexCategoryData, editor: RexRoleEditor): RexCategory { + let category; + if (details.variant && details.variant === 'post_type') { + category = new RexPostTypeCategory(details.name, editor, details.contentTypeId, details.slug, details.permissions); + } else if (details.variant && details.variant === 'taxonomy') { + category = new RexTaxonomyCategory(details.name, editor, details.contentTypeId, details.slug, details.permissions); + } else { + category = new RexCategory(details.name, editor, details.slug, details.capabilities); + } + + if (details.componentId) { + category.origin = editor.getComponent(details.componentId); + } + + if (details.subcategories) { + wsAmeLodash.forEach(details.subcategories, (childDetails) => { + const subcategory = RexCategory.fromJs(childDetails, editor); + category.addSubcategory(subcategory); + }); + } + + return category; + } + + usesBaseCapabilities(): boolean { + return false; + } + + getDeDuplicationKey(): string { + let key = this.slug ?? this.name; + if (this.parent) { + key = this.parent.getDeDuplicationKey() + '>' + key; + } + return key; + } + + addDuplicate(category: RexCategory) { + if (this.duplicates.indexOf(category) === -1) { + this.duplicates.push(category); + } + } + + protected getSubheadingItems(): string[] { + let items = []; + if (this.parent !== null) { + items.push(this.parent.name); + } + if (this.duplicates.length > 0) { + for (let i = 0; i < this.duplicates.length; i++) { + let category = this.duplicates[i]; + if (category.parent) { + items.push(category.parent.name); + } + } + } + return items; + } + + getAbsoluteName() { + let components = [this.name]; + let parent = this.parent; + while (parent !== null) { + components.unshift(parent.name); + parent = parent.parent; + } + return components.join(' > '); + } +} + +interface RexContentTypePermission extends RexPermission { + readonly action: string; +} + +abstract class RexContentTypeCategory extends RexCategory { + public actions: { [action: string]: RexContentTypePermission } = {}; + protected baseCategorySlug: string = null; + isBaseCapNoticeVisible: KnockoutComputed<boolean>; + + protected constructor(name: string, editor: RexRoleEditor, slug: string = null) { + super(name, editor, slug); + + this.isBaseCapNoticeVisible = ko.pureComputed({ + read: () => { + if (editor.showBaseCapsEnabled()) { + return false; + } + return this.usesBaseCapabilities(); + }, + deferEvaluation: true + }); + } + + /** + * Check if the post type or taxonomy represented by this category uses the same capabilities + * as the built-in "post" type or the "category" taxonomy. + */ + usesBaseCapabilities(): boolean { + const baseCategory = this.getBaseCategory(); + if (baseCategory === null || this === baseCategory) { + return false; + } + + let allCapsMatch = true; + wsAmeLodash.forEach(this.actions, function (item) { + let isMatch = item.action + && baseCategory.actions.hasOwnProperty(item.action) + && (item.capability === baseCategory.actions[item.action].capability); + + if (!isMatch) { + allCapsMatch = false; + return false; + } + }); + return allCapsMatch; + } + + protected getBaseCategory(): RexContentTypeCategory { + if (this.baseCategorySlug !== null) { + let result = this.findCategoryBySlug(this.baseCategorySlug); + if (result instanceof RexContentTypeCategory) { + return result; + } + } + return null; + } +} + +class RexPostTypePermission extends RexPermission implements RexContentTypePermission { + public readonly action: string; + + public static readonly actionDescriptions: { [cptAction: string]: string } = { + 'edit_and_create': 'Edit and create %s', + 'edit_posts': 'Edit %s', + 'create_posts': 'Create new %s', + 'edit_published_posts': 'Edit published %s', + 'edit_others_posts': 'Edit %s created by others', + 'edit_private_posts': 'Edit private %s created by others', + 'publish_posts': 'Publish %s', + 'read_private_posts': 'Read private %s', + 'delete_posts': 'Delete %s', + 'delete_published_posts': 'Delete published %s', + 'delete_others_posts': 'Delete %s created by others', + 'delete_private_posts': 'Delete private %s created by others', + }; + + constructor(editor: RexRoleEditor, capability: RexCapability, action: string, pluralNoun: string = '') { + super(editor, capability); + + this.action = action; + this.readableAction = wsAmeLodash.capitalize(this.action.replace('_posts', '').replace('_', ' ')); + + if (RexPostTypePermission.actionDescriptions.hasOwnProperty(action) && pluralNoun) { + this.mainDescription = RexPostTypePermission.actionDescriptions[action].replace('%s', pluralNoun); + } + } +} + +class RexPostTypeCategory extends RexContentTypeCategory { + readonly pluralLabel: string = ''; + + public actions: { [action: string]: RexPostTypePermission } = {}; + public readonly postType: string; + public readonly isDefault: boolean; + + private static readonly desiredActionOrder: { [cptAction: string]: number } = { + 'edit_posts': 1, + 'edit_others_posts': 2, + 'edit_published_posts': 3, + 'edit_private_posts': 4, + 'publish_posts': 5, + 'delete_posts': 6, + 'delete_others_posts': 7, + 'delete_published_posts': 8, + 'delete_private_posts': 9, + 'read_private_posts': 10, + 'create_posts': 11, + }; + + constructor( + name: string, + editor: RexRoleEditor, + postTypeId: string, + slug: string = null, + permissions: { [action: string]: string }, + isDefault: boolean = false + ) { + super(name, editor, slug); + const _ = wsAmeLodash; + + this.baseCategorySlug = 'postTypes/post'; + this.postType = postTypeId; + this.isDefault = isDefault; + this.subtitle = this.postType; + if (editor.postTypes[postTypeId].pluralLabel) { + this.pluralLabel = editor.postTypes[postTypeId].pluralLabel; + } else { + this.pluralLabel = name.toLowerCase(); + } + + this.permissions = ko.observableArray(_.map(permissions, (capability, action) => { + const permission = new RexPostTypePermission(editor, editor.getCapability(capability), action, this.pluralLabel); + + //The "read" capability is already shown in the core category and every role has it by default. + if (capability === 'read') { + permission.isRedundant = true; + } + + this.actions[action] = permission; + return permission; + })); + + this.sortPermissions(); + + //The "create" capability is often the same as the "edit" capability. + const editPerm = _.get(this.actions, 'edit_posts', null), + createPerm = _.get(this.actions, 'create_posts', null); + if (editPerm && createPerm && (createPerm.capability.name === editPerm.capability.name)) { + createPerm.isRedundant = true; + } + } + + + getDeDuplicationKey(): string { + return 'postType:' + this.postType; + } + + sortPermissions() { + this.permissions.sort(function (a: RexPostTypePermission, b: RexPostTypePermission) { + const priorityA = RexPostTypeCategory.desiredActionOrder.hasOwnProperty(a.action) ? RexPostTypeCategory.desiredActionOrder[a.action] : 1000; + const priorityB = RexPostTypeCategory.desiredActionOrder.hasOwnProperty(b.action) ? RexPostTypeCategory.desiredActionOrder[b.action] : 1000; + + let delta = priorityA - priorityB; + if (delta !== 0) { + return delta; + } + + return a.capability.name.localeCompare(b.capability.name); + }); + } + + protected getSubheadingItems(): string[] { + let items = super.getSubheadingItems(); + items.push(this.postType); + return items; + } +} + +class RexTaxonomyPermission extends RexPermission implements RexContentTypePermission { + public readonly action: string; + + public static readonly actionDescriptions: { [cptAction: string]: string } = { + 'manage_terms': 'Manage %s', + 'edit_terms': 'Edit %s', + 'delete_terms': 'Delete %s', + 'assign_terms': 'Assign %s', + }; + + constructor(editor: RexRoleEditor, capability: RexCapability, action: string, pluralNoun: string = '') { + super(editor, capability); + + this.action = action; + this.readableAction = wsAmeLodash.capitalize(this.action.replace('_terms', '').replace('_', ' ')); + + if (RexTaxonomyPermission.actionDescriptions.hasOwnProperty(action) && pluralNoun) { + this.mainDescription = RexTaxonomyPermission.actionDescriptions[action].replace('%s', pluralNoun); + } + } +} + +class RexTaxonomyCategory extends RexContentTypeCategory { + public actions: { [action: string]: RexTaxonomyPermission } = {}; + private readonly taxonomy: string; + + private static readonly desiredActionOrder: { [cptAction: string]: number } = { + 'manage_terms': 1, + 'edit_terms': 2, + 'delete_terms': 3, + 'assign_terms': 4, + }; + + constructor( + name: string, + editor: RexRoleEditor, + taxonomyId: string, + slug: string = null, + permissions: { [action: string]: string } + ) { + super(name, editor, slug); + const _ = wsAmeLodash; + + this.baseCategorySlug = 'taxonomies/category'; + this.taxonomy = taxonomyId; + this.subtitle = taxonomyId; + + const noun = name.toLowerCase(); + this.permissions = ko.observableArray(_.map(permissions, (capability, action) => { + const permission = new RexTaxonomyPermission(editor, editor.getCapability(capability), action, noun); + this.actions[action] = permission; + return permission; + })); + + this.sortPermissions(); + + //Permissions that use the same capability as the "manage_terms" permission are redundant. + if (this.actions.manage_terms) { + const manageCap = this.actions.manage_terms.capability.name; + for (let action in this.actions) { + if (!this.actions.hasOwnProperty(action)) { + continue; + } + if ((action !== 'manage_terms') && (this.actions[action].capability.name === manageCap)) { + this.actions[action].isRedundant = true; + } + } + } + } + + getDeDuplicationKey(): string { + return 'taxonomy:' + this.taxonomy; + } + + sortPermissions(): void { + this.permissions.sort(function (a: RexTaxonomyPermission, b: RexTaxonomyPermission) { + const priorityA = RexTaxonomyCategory.desiredActionOrder.hasOwnProperty(a.action) ? RexTaxonomyCategory.desiredActionOrder[a.action] : 1000; + const priorityB = RexTaxonomyCategory.desiredActionOrder.hasOwnProperty(b.action) ? RexTaxonomyCategory.desiredActionOrder[b.action] : 1000; + + let delta = priorityA - priorityB; + if (delta !== 0) { + return delta; + } + + return a.capability.name.localeCompare(b.capability.name); + }); + } + + protected getSubheadingItems(): string[] { + let items = super.getSubheadingItems(); + items.push(this.taxonomy); + return items; + } +} + +interface RexPermissionTableColumn { + title: string; + actions: string[]; +} + +class RexTableViewCategory extends RexCategory { + tableColumns: KnockoutComputed<RexPermissionTableColumn[]>; + subcategoryComparisonCallback: RexCategoryComparisonCallback = null; + + constructor(name: string, editor: RexRoleEditor, slug: string = null) { + super(name, editor, slug); + + this.contentTemplate = ko.pureComputed(function () { + if (editor.categoryViewMode() === RexRoleEditor.hierarchyView) { + return 'rex-permission-table-template'; + } + return 'rex-default-category-content-template'; + }); + + this.subcategoryComparisonCallback = RexCategory.defaultSubcategoryComparison; + } + + protected getSortedSubcategories(): RexCategory[] { + if (this.editor.showBaseCapsEnabled()) { + return super.getSortedSubcategories(); + } + + let cats = wsAmeLodash.clone(this.subcategories); + return cats.sort((a, b) => { + //Special case: Put categories that use base capabilities at the end. + const aEqualsBase = a.usesBaseCapabilities(); + const bEqualsBase = b.usesBaseCapabilities(); + if (aEqualsBase && !bEqualsBase) { + return 1; + } else if (!aEqualsBase && bEqualsBase) { + return -1; + } + + //Otherwise just sort in the default order. + return this.subcategoryComparisonCallback(a, b); + }); + } + + /** + * Sort the underlying category array. + */ + public sortSubcategories() { + this.subcategories.sort(this.subcategoryComparisonCallback); + } +} + +class RexTaxonomyContainerCategory extends RexTableViewCategory { + constructor(name: string, editor: RexRoleEditor, slug: string = null) { + super(name, editor, slug); + + this.htmlId = 'rex-taxonomy-summary-category'; + + this.tableColumns = ko.pureComputed({ + read: () => { + const _ = wsAmeLodash; + const defaultTaxonomyActions = ['manage_terms', 'assign_terms', 'edit_terms', 'delete_terms']; + + let columns = [ + { + title: 'Manage', + actions: ['manage_terms'] + }, + { + title: 'Assign', + actions: ['assign_terms'] + }, + { + title: 'Edit', + actions: ['edit_terms'] + }, + { + title: 'Delete', + actions: ['delete_terms'] + } + ]; + let misColumnExists = false, miscColumn: RexPermissionTableColumn = null; + + for (let i = 0; i < this.subcategories.length; i++) { + const category = this.subcategories[i]; + if (!(category instanceof RexTaxonomyCategory)) { + continue; + } + + //Display any unrecognized actions in a "Misc" column. + const customActions = _.omit(category.actions, defaultTaxonomyActions); + if (!_.isEmpty(customActions)) { + if (!misColumnExists) { + miscColumn = {title: 'Misc', actions: []}; + columns.push(miscColumn); + } + miscColumn.actions = _.union(miscColumn.actions, _.keys(customActions)); + } + } + + return columns; + }, + deferEvaluation: true, + }); + } +} + +class RexPostTypeContainerCategory extends RexTableViewCategory { + constructor(name: string, editor: RexRoleEditor, slug: string = null) { + super(name, editor, slug); + + /* Note: This seems like poor design because the superclass overrides subclass + * behaviour (subcategory comparison) in some situations. Unfortunately, I haven't + * come up with anything better so far. Might be something to revisit later. + */ + + this.subcategoryComparisonCallback = function (a: RexPostTypeCategory, b: RexPostTypeCategory) { + //Special case: Put "Posts" at the top. + if (a.postType === 'post') { + return -1; + } else if (b.postType === 'post') { + return 1; + } + + //Put other built-in post types above custom post types. + if (a.isDefault && !b.isDefault) { + return -1; + } else if (b.isDefault && !a.isDefault) { + return 1; + } + + let labelA = a.name.toLowerCase(), labelB = b.name.toLowerCase(); + return labelA.localeCompare(labelB); + }; + + this.tableColumns = ko.pureComputed({ + read: () => { + const _ = wsAmeLodash; + const defaultPostTypeActions = _.keys(RexPostTypePermission.actionDescriptions); + + let columns = [ + { + title: 'Own items', + actions: ['create_posts', 'edit_posts', 'delete_posts', 'publish_posts', 'edit_published_posts', 'delete_published_posts'] + }, + { + title: 'Other\'s items', + actions: ['edit_others_posts', 'delete_others_posts', 'edit_private_posts', 'delete_private_posts', 'read_private_posts'] + } + ]; + let metaColumn = { + title: 'Meta', + actions: ['edit_post', 'delete_post', 'read_post'] + }; + columns.push(metaColumn); + + for (let i = 0; i < this.subcategories.length; i++) { + const category = this.subcategories[i]; + if (!(category instanceof RexPostTypeCategory)) { + continue; + } + + //Display any unrecognized actions in a "Misc" column. + const customActions = _.omit(category.actions, defaultPostTypeActions); + if (!_.isEmpty(customActions)) { + metaColumn.actions = _.union(metaColumn.actions, _.keys(customActions)); + } + } + + return columns; + }, + deferEvaluation: true, + }); + } +} + + +interface RexCapabilityData { + componentId?: string; + menuItems: string[]; + usedByComponents: string[]; + + documentationUrl?: string; + permissions?: string[]; + readableName?: string; +} + +class RexCapability { + readonly name: string; + + private readableName: string; + readonly displayName: KnockoutComputed<string>; + + private readonly editor: RexRoleEditor; + + readonly isEnabledForSelectedActor: KnockoutComputed<boolean>; + isEditable: KnockoutComputed<boolean>; + + readonly responsibleActors: KnockoutComputed<RexBaseActor[]>; + readonly isInherited: KnockoutComputed<boolean>; + readonly isPersonalOverride: KnockoutComputed<boolean>; + readonly isExplicitlyDenied: KnockoutComputed<boolean>; + + originComponent: RexWordPressComponent = null; + usedByComponents: RexWordPressComponent[] = []; + + menuItems: string[] = []; + usedByPostTypeActions: { [postType: string]: { [action: string]: boolean } } = {}; + usedByTaxonomyActions: { [taxonomy: string]: { [action: string]: boolean } } = {}; + predefinedPermissions: string[] = []; + + grantedPermissions: KnockoutComputed<string[]>; + protected documentationUrl: string = null; + notes: string = null; + + readonly isDeleted: KnockoutObservable<boolean>; + + constructor(name: string, editor: RexRoleEditor) { + this.name = String(name); + this.editor = editor; + + const self = this; + + this.readableName = wsAmeLodash.capitalize(this.name.replace(/[_\-\s]+/g, ' ')); + this.displayName = ko.pureComputed({ + read: function () { + return editor.readableNamesEnabled() ? self.readableName : self.name; + }, + deferEvaluation: true, + owner: this + }); + this.isDeleted = ko.observable(false); + + this.responsibleActors = ko.computed({ + read: function () { + let actor = editor.selectedActor(), list = []; + if (actor instanceof RexUser) { + actor.hasCap(self.name, list); + } + return list; + }, + owner: this, + deferEvaluation: true + }); + + this.isInherited = ko.computed({ + read: function () { + const actor = editor.selectedActor(); + if (!actor.canHaveRoles) { + return false; + } + + const responsibleActors = self.responsibleActors(); + return responsibleActors + && (responsibleActors.length > 0) + && (wsAmeLodash.indexOf(responsibleActors, actor) < (responsibleActors.length - 1)) + }, + owner: this, + deferEvaluation: true + }); + + this.isPersonalOverride = ko.pureComputed({ + read: function () { + //This flag applies only to actors that can inherit permissions. + const actor = editor.selectedActor(); + if (!actor.canHaveRoles) { + return false; + } + return !self.isInherited(); + }, + owner: this, + deferEvaluation: true + }); + + this.isEditable = ko.pureComputed({ + read: function () { + if (self.isInherited() && !editor.inheritanceOverrideEnabled()) { + return false; + } + + return !self.isDeleted(); + }, + deferEvaluation: true + }); + + this.isEnabledForSelectedActor = ko.computed({ + read: function () { + return editor.selectedActor().hasCap(self.name); + }, + write: function (newState: boolean) { + const actor = editor.selectedActor(); + if (editor.isShiftKeyDown()) { + //Hold the shift key while clicking to cycle the capability between 3 states: + //Granted -> Denied -> Not granted. + const oldState = actor.getOwnCapabilityState(self.name); + if (newState) { + if (oldState === false) { + actor.deleteCap(self.name); //Denied -> Not granted. + } else if (oldState === null) { + actor.setCap(self.name, true); //Not granted -> Granted. + } + } else { + if (oldState === true) { + actor.setCap(self.name, false); //Granted -> Denied. + } else if (oldState === null) { + actor.setCap(self.name, true); //Not granted (inherited = Granted) -> Granted. + } + } + //Update the checkbox state. + if (actor.hasCap(self.name) !== newState) { + self.isEnabledForSelectedActor.notifySubscribers(); + } + return; + } + + if (newState) { + //TODO: If it's a user and the cap is explicitly negated, consider removing that state. + actor.setCap(self.name, newState); + } else { + //The default is to remove the capability instead of explicitly setting it to false. + actor.deleteCap(self.name); + + //If we're removing a capability from a user but one of their roles also has it, + //we have to set it to false after all or it will stay enabled. + if (actor.canHaveRoles && actor.hasCap(self.name)) { + actor.setCap(self.name, newState); + } + } + }, + owner: this, + deferEvaluation: true + }); + //this.isEnabledForSelectedActor.extend({rateLimit: {timeout: 10, method: "notifyWhenChangesStop"}}); + + this.isExplicitlyDenied = ko.pureComputed({ + read: function () { + const actor = editor.selectedActor(); + if (actor) { + return (actor.getCapabilityState(self.name) === false); + } + return false; + }, + deferEvaluation: true + }); + + this.grantedPermissions = ko.computed({ + read: () => { + const _ = wsAmeLodash; + let results = []; + + if (this.predefinedPermissions.length > 0) { + results = this.predefinedPermissions.slice(); + } + + function localeAwareCompare(a: string, b: string) { + return a.localeCompare(b); + } + + function actionsToPermissions( + actionGroups: AmeDictionary<string[]>, + labelMap: AmeDictionary<RexBaseContentData>, + descriptions: AmeDictionary<string> + ): string[] { + return _.map(actionGroups, (ids, action) => { + let labels = _.map(ids, (id) => labelMap[id].pluralLabel) + .sort(localeAwareCompare); + let template = descriptions[action]; + if (!template) { + template = action + ': %s'; + } + return template.replace('%s', RexCapability.formatNounList(labels)); + }).sort(localeAwareCompare); + } + + //Post permissions. + let postActionGroups = _.transform( + this.usedByPostTypeActions, + function (accumulator: { [action: string]: string[] }, actions, postType) { + let actionKeys = _.keys(actions); + + //Combine "edit" and "create" permissions because they usually use the same capability. + const editEqualsCreate = actions.hasOwnProperty('edit_posts') && actions.hasOwnProperty('create_posts'); + if (editEqualsCreate) { + actionKeys = _.without(actionKeys, 'edit_posts', 'create_posts'); + actionKeys.unshift('edit_and_create'); + } + + _.forEach(actionKeys, function (action) { + if (!accumulator.hasOwnProperty(action)) { + accumulator[action] = []; + } + accumulator[action].push(postType); + }); + }, {} + ); + + let postPermissions = actionsToPermissions( + postActionGroups, + this.editor.postTypes, + RexPostTypePermission.actionDescriptions + ); + Array.prototype.push.apply(results, postPermissions); + + //Taxonomy permissions. + let taxonomyActionGroups = _.transform( + this.usedByTaxonomyActions, + function (accumulator: { [action: string]: string[] }, actions, taxonomy) { + let actionKeys = _.keys(actions); + + //Most taxonomies use the same capability for manage_terms, edit_terms, and delete_terms. + //In those cases, let's show only manage_terms. + if (actions.hasOwnProperty('manage_terms')) { + actionKeys = _.without(actionKeys, 'edit_terms', 'delete_terms'); + } + + _.forEach(actionKeys, function (action) { + if (!accumulator.hasOwnProperty(action)) { + accumulator[action] = []; + } + accumulator[action].push(taxonomy); + }); + }, {} + ); + + let taxonomyPermissions = actionsToPermissions( + taxonomyActionGroups, + this.editor.taxonomies, + RexTaxonomyPermission.actionDescriptions + ); + Array.prototype.push.apply(results, taxonomyPermissions); + + Array.prototype.push.apply(results, this.menuItems); + return results; + }, + deferEvaluation: true, + owner: this, + }) + } + + // noinspection JSUnusedGlobalSymbols Used in KO templates. + getDocumentationUrl(): string | null { + if (this.documentationUrl) { + return this.documentationUrl; + } + if (this.originComponent && this.originComponent.capabilityDocumentationUrl) { + this.documentationUrl = this.originComponent.capabilityDocumentationUrl; + return this.documentationUrl; + } + return null; + } + + static fromJs(name: string, data: RexCapabilityData, editor: RexRoleEditor): RexCapability { + const capability = new RexCapability(name, editor); + capability.menuItems = data.menuItems.sort(function (a, b) { + return a.localeCompare(b); + }); + + if (data.componentId) { + capability.originComponent = editor.getComponent(data.componentId); + } + if (data.usedByComponents) { + for (let id in data.usedByComponents) { + const component = editor.getComponent(id); + if (component) { + capability.usedByComponents.push(component); + } + } + } + + if (data.documentationUrl) { + capability.documentationUrl = data.documentationUrl; + } + + if (data.permissions && (data.permissions.length > 0)) { + capability.predefinedPermissions = data.permissions; + } + + if ((capability.originComponent === editor.coreComponent) && (capability.documentationUrl === null)) { + capability.documentationUrl = 'https://wordpress.org/support/article/roles-and-capabilities/#' + + encodeURIComponent(capability.name); + } + + if (data.readableName) { + capability.readableName = data.readableName; + } + + return capability; + } + + static formatNounList(items: string[]): string { + if (items.length <= 2) { + return items.join(' and '); + } + return items.slice(0, -1).join(', ') + ', and ' + items[items.length - 1]; + } +} + +class RexDoNotAllowCapability extends RexCapability { + constructor(editor: RexRoleEditor) { + super('do_not_allow', editor); + this.notes = '"do_not_allow" is a special capability. ' + + 'WordPress uses it internally to indicate that access is denied. ' + + 'Normally, it should not be assigned to any roles or users.'; + + //Normally, it's impossible to grant this capability to anyone. Doing so would break things. + //However, if it's already granted, you can remove it. + this.isEditable = ko.computed(() => { + return this.isEnabledForSelectedActor(); + }); + } +} + +class RexExistCapability extends RexCapability { + constructor(editor: RexRoleEditor) { + super('exist', editor); + this.notes = '"exist" is a special capability. ' + + 'WordPress uses it internally to indicate that a role or user exists. ' + + 'Normally, everyone has this capability by default, and it is not necessary ' + + '(or possible) to assign it directly.'; + + //Everyone must have this capability. However, if it has somehow become disabled, + //we'll let the user enable it. + this.isEditable = ko.computed(() => { + return !this.isEnabledForSelectedActor(); + }); + } +} + +class RexInvalidCapability extends RexCapability { + constructor(fakeName: string, value: any, editor: RexRoleEditor) { + super(fakeName, editor); + const startsWithVowel = /^[aeiou]/i; + let theType = (typeof value); + const nounPhrase = (startsWithVowel.test(theType) ? 'an' : 'a') + ' ' + theType; + + this.notes = 'This is not a valid capability. A capability name must be a string (i.e. text),' + + ' but this is ' + nounPhrase + '. It was probably created by a bug in another plugin or theme.'; + + this.isEditable = ko.computed(() => { + return false; + }); + } +} + +class RexUserPreferences { + private readonly preferenceObservables: AmeDictionary<KnockoutObservable<any>>; + private readonly preferenceCount: KnockoutObservable<number>; + + private readonly plainPreferences: KnockoutComputed<any>; + + collapsedCategories: RexCollapsedCategorySet; + + constructor(initialPreferences?: AmeDictionary<any>, ajaxUrl?: string, updateNonce?: string) { + const _ = wsAmeLodash; + + initialPreferences = initialPreferences || {}; + if (_.isArray(initialPreferences)) { + initialPreferences = {}; + } + + this.preferenceObservables = _.mapValues(initialPreferences, ko.observable, ko); + this.preferenceCount = ko.observable(_.size(this.preferenceObservables)); + + this.collapsedCategories = new RexCollapsedCategorySet(_.get(initialPreferences, 'collapsedCategories', [])); + + this.plainPreferences = ko.computed(() => { + //By creating a dependency on the number of preferences, we ensure that the observable will be re-evaluated + //whenever a preference is added or removed. + this.preferenceCount(); + + //This converts preferences to a plain JS object and establishes dependencies on all individual observables. + let result = _.mapValues(this.preferenceObservables, function (observable) { + return observable(); + }); + + result.collapsedCategories = this.collapsedCategories.toJs(); + + return result; + }); + + //Avoid excessive AJAX requests. + this.plainPreferences.extend({rateLimit: {timeout: 5000, method: "notifyWhenChangesStop"}}); + + //Save preferences when they change. + if (ajaxUrl && updateNonce) { + this.plainPreferences.subscribe((preferences) => { + //console.info('Saving user preferences', preferences); + jQuery.post( + ajaxUrl, + { + action: 'ws_ame_rex_update_user_preferences', + _ajax_nonce: updateNonce, + preferences: ko.toJSON(preferences) + } + ) + }); + } + } + + getObservable<T>(name: string, defaultValue: T = null): KnockoutObservable<T> { + if (this.preferenceObservables.hasOwnProperty(name)) { + return this.preferenceObservables[name]; + } + + const newPreference = ko.observable(defaultValue || null); + this.preferenceObservables[name] = newPreference; + this.preferenceCount(this.preferenceCount() + 1); + + return newPreference; + } +} + +/** + * An observable collection of unique strings. In this case, they're category slugs. + */ +class RexCollapsedCategorySet { + readonly items: KnockoutObservableArray<string>; + private isItemInSet: AmeDictionary<KnockoutObservable<boolean>> = {}; + + constructor(items: string[] = []) { + items = wsAmeLodash.uniq(items); + for (let i = 0; i < items.length; i++) { + this.isItemInSet[items[i]] = ko.observable(true); + } + this.items = ko.observableArray(items); + } + + private getItemObservable(item: string) { + if (!this.isItemInSet.hasOwnProperty(item)) { + this.isItemInSet[item] = ko.observable(false); + } + return this.isItemInSet[item]; + } + + add(item: string) { + if (!this.contains(item)) { + this.getItemObservable(item)(true); + this.items.push(item); + } + } + + remove(item: string) { + if (this.contains(item)) { + this.isItemInSet[item](false); + this.items.remove(item); + } + } + + toggle(item: string, addToSet: boolean) { + if (addToSet) { + this.add(item); + } else { + this.remove(item); + } + } + + contains(item: string): boolean { + return this.getItemObservable(item)(); + } + + peek(item: string): boolean { + if (!this.isItemInSet.hasOwnProperty(item)) { + return false; + } + return this.isItemInSet[item].peek(); + } + + toJs() { + return this.items(); + } +} + +interface RexCategoryData { + name: string; + componentId?: string; + slug?: string; + + capabilities?: string[]; + permissions?: { [action: string]: string }; + subcategories?: RexCategoryData[]; + + variant?: string; + contentTypeId?: string; //Post type or taxonomy name (internal ID, not display name). +} + +interface RexBaseContentData { + name: string; + label: string; + pluralLabel: string; + permissions: { [action: string]: string }; + componentId?: string; +} + +interface RexPostTypeData extends RexBaseContentData { + isDefault: boolean; +} + +interface RexTaxonomyData extends RexBaseContentData { +} + +type RexEditableRoleStrategy = 'auto' | 'none' | 'user-defined-list'; + +interface RexEditableRoleSettings { + strategy: RexEditableRoleStrategy; + userDefinedList: null | { [roleId: string]: true; }; +} + +interface RexActorEditableRoles { + [actorId: string]: RexEditableRoleSettings; +} + +interface RexAppData { + knownComponents: { [id: string]: RexComponentData }; + + capabilities: { [capabilityName: string]: RexCapabilityData }; + deprecatedCapabilities: CapabilityMap; + roles: RexRoleData[]; + users: RexUserData[]; + defaultRoleName: string; + trashedRoles: RexRoleData[]; + + coreCategory: RexCategoryData; + customCategories: RexCategoryData[]; + uncategorizedCapabilities: string[]; + userDefinedCapabilities: CapabilityMap; + + postTypes: { [postType: string]: RexPostTypeData }; + taxonomies: { [taxonomy: string]: RexTaxonomyData }; + + metaCapMap: AmeDictionary<string>; + + editableRoles: RexActorEditableRoles; + + selectedActor: string | null; + userPreferences: AmeDictionary<any>; + adminAjaxUrl: string; + updatePreferencesNonce: string; +} + +interface RexCategoryViewOption { + id: string; + label: string; +} + +class RexBaseDialog implements AmeKnockoutDialog { + isOpen: KnockoutObservable<boolean> = ko.observable(false); + isRendered: KnockoutObservable<boolean> = ko.observable(false); + jQueryWidget: JQuery; + title: KnockoutObservable<string> = null; + options: AmeDictionary<any> = { + buttons: [] + }; + + constructor() { + this.isOpen.subscribe((isOpenNow) => { + if (isOpenNow && !this.isRendered()) { + this.isRendered(true); + } + }); + } + + setupValidationTooltip(inputSelector: string, message: KnockoutObservable<string>) { + //Display validation messages next to the input field. + const element = this.jQueryWidget.find(inputSelector).qtip({ + overwrite: false, + content: '(Validation errors will appear here.)', + + //Show the tooltip when the input is focused. + show: { + event: '', + ready: false, + effect: false + }, + hide: { + event: '', + effect: false + }, + + position: { + my: 'center left', + at: 'center right', + effect: false + }, + style: { + classes: 'qtip-bootstrap qtip-shadow rex-tooltip' + } + }); + + message.subscribe((newMessage) => { + if (newMessage == '') { + element.qtip('option', 'content.text', 'OK'); + element.qtip('option', 'show.event', ''); + element.qtip('hide'); + } else { + element.qtip('option', 'content.text', newMessage); + element.qtip('option', 'show.event', 'focus'); + element.qtip('show'); + } + }); + + //Hide the tooltip when the dialog is closed and prevent it from automatically re-appearing. + this.isOpen.subscribe((isDialogOpen) => { + if (!isDialogOpen) { + element.qtip('option', 'show.event', ''); + element.qtip('hide'); + } + }); + }; +} + +interface RexDeletableCapItem { + capability: RexCapability; + isSelected: KnockoutObservable<boolean>; +} + +class RexDeleteCapDialog extends RexBaseDialog { + options = { + buttons: [], + minWidth: 380 + }; + + deletableItems: KnockoutComputed<RexDeletableCapItem[]>; + selectedItemCount: KnockoutComputed<number>; + isDeleteButtonEnabled: KnockoutComputed<boolean>; + + private wasEverOpen: KnockoutObservable<boolean> = ko.observable(false); + + constructor(editor: RexRoleEditor) { + super(); + const _ = wsAmeLodash; + + this.options.buttons.push({ + text: 'Delete Capability', + 'class': 'button button-primary rex-delete-selected-caps', + click: () => { + let selectedCapabilities = _.chain(this.deletableItems()) + .filter(function (item) { + return item.isSelected(); + }) + .pluck<RexCapability>('capability') + .value(); + + //Note: We could remove confirmation if we get an "Undo" feature. + const noun = (selectedCapabilities.length === 1) ? 'capability' : 'capabilities'; + const warning = 'Caution: Deleting capabilities could break plugins that use those capabilities. ' + + 'Delete ' + selectedCapabilities.length + ' ' + noun + '?'; + if (!confirm(warning)) { + return; + } + + this.isOpen(false); + + editor.deleteCapabilities(selectedCapabilities); + + alert(selectedCapabilities.length + ' capabilities deleted'); + }, + disabled: true + }); + + this.isOpen.subscribe((open) => { + if (open && !this.wasEverOpen()) { + this.wasEverOpen(true); + } + }); + + this.deletableItems = ko.pureComputed({ + read: () => { + const wpCore = editor.getComponent(':wordpress:'); + return _.chain(editor.capabilities) + .filter(function (capability) { + if (capability.originComponent === wpCore) { + return false; + } + return !capability.isDeleted(); + }) + //Pre-populate part of the list when the dialog is closed to ensure it has a non-zero height. + .take(this.wasEverOpen() ? 1000000 : 30) + .sortBy(function (capability) { + return capability.name.toLowerCase(); + }) + .map(function (capability) { + return { + 'capability': capability, + 'isSelected': ko.observable(false) + }; + }) + .value(); + }, + deferEvaluation: true + }); + + this.selectedItemCount = ko.pureComputed({ + read: () => _.filter(this.deletableItems(), function (item) { + return item.isSelected(); + }).length, + deferEvaluation: true + }); + + const deleteButtonText = ko.pureComputed({ + read: () => { + const count = this.selectedItemCount(); + if (count <= 0) { + return 'Delete Capability'; + } else { + if (count === 1) { + return 'Delete 1 Capability'; + } else { + return ('Delete ' + count + ' Capabilities'); + } + } + }, + deferEvaluation: true + }); + + deleteButtonText.subscribe((newText) => { + this.jQueryWidget + .closest('.ui-dialog') + .find('.ui-dialog-buttonset .button-primary .ui-button-text') + .text(newText); + }); + + this.isDeleteButtonEnabled = ko.pureComputed({ + read: () => { + return this.selectedItemCount() > 0; + }, + deferEvaluation: true + }) + } + + onOpen() { + //Deselect all items when the dialog is opened. + const items = this.deletableItems(); + for (let i = 0; i < items.length; i++) { + if (items[i].isSelected()) { + items[i].isSelected(false); + } + } + } +} + +class RexAddCapabilityDialog extends RexBaseDialog { + public static readonly states = { + valid: 'valid', + empty: 'empty', + notice: 'notice', + error: 'error' + }; + + autoCancelButton: boolean = true; + options: AmeDictionary<any> = { + minWidth: 380 + }; + + capabilityName: KnockoutComputed<string>; + validationState: KnockoutObservable<string> = ko.observable(RexAddCapabilityDialog.states.empty); + validationMessage: KnockoutObservable<string> = ko.observable(''); + isAddButtonEnabled: KnockoutComputed<boolean>; + + private readonly editor: RexRoleEditor; + + constructor(editor: RexRoleEditor) { + super(); + const _ = wsAmeLodash; + this.editor = editor; + + const excludedCaps = ['do_not_allow', 'exist', 'customize']; + + let newCapabilityName = ko.observable(''); + this.capabilityName = ko.computed({ + read: function () { + return newCapabilityName(); + }, + write: (value) => { + value = _.trimRight(value); + + //Validate and sanitize the capability name. + let state = this.validationState, + message = this.validationMessage; + + //WP API allows completely arbitrary capability names, but this plugin forbids some characters + //for sanity's sake and to avoid XSS. + const invalidCharacters = /[><&\r\n\t]/g; + //While all other characters are allowed, it's recommended to stick to alphanumerics, + //underscores and dashes. Spaces are also OK because some other plugins use them. + const suspiciousCharacters = /[^a-z0-9_ -]/ig; + //PHP doesn't allow numeric string keys, and there's no conceivable reason to start the name with a space. + const invalidFirstCharacter = /^[\s0-9]/i; + + let foundInvalid = value.match(invalidCharacters); + let foundSuspicious = value.match(suspiciousCharacters); + + if (foundInvalid !== null) { + state(RexAddCapabilityDialog.states.error); + message('Sorry, <code>' + _.escape(_.last(foundInvalid)) + '</code> is not allowed here.'); + + } else if (value.match(invalidFirstCharacter) !== null) { + state(RexAddCapabilityDialog.states.error); + message('Capability name should start with a letter or an underscore.'); + + } else if (editor.capabilityExists(value)) { + //Duplicates are not allowed. + state(RexAddCapabilityDialog.states.error); + message('That capability already exists.'); + + } else if (editor.getRole(value) !== null) { + state(RexAddCapabilityDialog.states.error); + message('Capability name can\'t be the same as the name of a role.'); + + } else if (excludedCaps.indexOf(value) >= 0) { + state(RexAddCapabilityDialog.states.error); + message('That is a meta capability or a reserved capability name.'); + + } else if (foundSuspicious !== null) { + state(RexAddCapabilityDialog.states.notice); + message('For best compatibility, we recommend using only English letters, numbers, and underscores.'); + + } else if (value === '') { + //Empty input, nothing to validate. + state(RexAddCapabilityDialog.states.empty); + message(''); + + } else { + state(RexAddCapabilityDialog.states.valid); + message(''); + } + + newCapabilityName(value); + } + }); + + const acceptableStates = [RexAddCapabilityDialog.states.valid, RexAddCapabilityDialog.states.notice]; + this.isAddButtonEnabled = ko.pureComputed(() => { + return (acceptableStates.indexOf(this.validationState()) >= 0); + }); + + this.options.buttons = [{ + text: 'Add Capability', + 'class': 'button button-primary', + click: () => { + this.onConfirm(); + }, + disabled: true + }]; + } + + onOpen(event, ui) { + //Clear the input when the dialog is opened. + this.capabilityName(''); + } + + onConfirm() { + if (!this.isAddButtonEnabled()) { + return; + } + const category = this.editor.addCapability(this.capabilityName().trim()); + this.isOpen(false); + + //Note: Maybe the user doesn't need this alert? Hmm. + if (!category || (this.editor.categoryViewMode() === RexRoleEditor.listView)) { + alert('Capability added'); + } else { + alert('Capability added to the "' + category.getAbsoluteName() + '" category.'); + } + } +} + +class RexAddRoleDialog extends RexBaseDialog { + roleName: KnockoutObservable<string> = ko.observable(''); + roleDisplayName: KnockoutObservable<string> = ko.observable(''); + roleToCopyFrom: KnockoutObservable<RexRole> = ko.observable(null); + + isAddButtonEnabled: KnockoutComputed<boolean>; + + isNameValid: KnockoutComputed<boolean>; + nameValidationMessage: KnockoutObservable<string> = ko.observable(''); + isDisplayNameValid: KnockoutComputed<boolean>; + displayNameValidationMessage: KnockoutObservable<string> = ko.observable(''); + + private readonly editor: RexRoleEditor; + private areTooltipsInitialised: boolean = false; + + private static readonly invalidDisplayNameRegex = /[><&\r\n\t]/; + + constructor(editor: RexRoleEditor) { + super(); + const _ = wsAmeLodash; + this.editor = editor; + + this.options.minWidth = 380; + this.options.buttons.push({ + text: 'Add Role', + 'class': 'button button-primary', + click: this.onConfirm.bind(this), + disabled: true + }); + + this.roleDisplayName.extend({rateLimit: 10}); + this.roleName.extend({rateLimit: 10}); + + //Role names are restricted - you can only use lowercase Latin letters, numbers and underscores. + const roleNameCharacterGroup = 'a-z0-9_'; + const invalidCharacterRegex = new RegExp('[^' + roleNameCharacterGroup + ']', 'g'); + const numbersOnlyRegex = /^[0-9]+$/; + + this.isNameValid = ko.computed(() => { + let name = this.roleName().trim(); + let message = this.nameValidationMessage; + + //Name must not be empty. + if (name === '') { + message(''); + return false; + } + + //Name can only contain certain characters. + const invalidChars = name.match(invalidCharacterRegex); + if (invalidChars !== null) { + let lastInvalidChar = _.last(invalidChars); + if (lastInvalidChar === ' ') { + lastInvalidChar = 'space'; + } + message( + 'Sorry, <code>' + _.escape(lastInvalidChar) + '</code> is not allowed here.<br>' + + 'Please enter only lowercase English letters, numbers, and underscores.' + ); + return false; + } + + //Numeric names could cause problems with how PHP handles associative arrays. + if (numbersOnlyRegex.test(name)) { + message('Numeric names are not allowed. Please add at least one letter or underscore.'); + return false; + } + + //Name must not be a duplicate. + let existingRole = editor.getRole(name); + if (existingRole) { + message('Duplicate role name.'); + return false; + } + + //WP stores capabilities and role names in the same associative array, + //so they must be unique with respect to each other. + if (editor.capabilityExists(name)) { + message('Role name can\'t be the same as a capability name.'); + return false; + } + + message(''); + return true; + }); + + this.isDisplayNameValid = ko.computed(() => { + let name = this.roleDisplayName(); + let message = this.displayNameValidationMessage; + return RexAddRoleDialog.validateDisplayName(name, message); + }); + + //Automatically generate a role name from the display name. Basically, turn it into a slug. + let lastAutoRoleName = null; + this.roleDisplayName.subscribe((displayName) => { + let slug = _.snakeCase(displayName); + + //Use the auto-generated role name only if the user hasn't entered their own. + let currentValue = this.roleName(); + if ((currentValue === '') || (currentValue === lastAutoRoleName)) { + this.roleName(slug); + } + lastAutoRoleName = slug; + }); + + this.isAddButtonEnabled = ko.pureComputed({ + read: () => { + return (this.roleName() !== '') && (this.roleDisplayName() !== '') + && this.isNameValid() && this.isDisplayNameValid(); + }, + deferEvaluation: true + }); + } + + static validateDisplayName(name: string, validationMessage: KnockoutObservable<string>): boolean { + name = name.trim(); + + if (name === '') { + validationMessage(''); + return false; + } + + //You can choose pretty much any display name you like, but we'll forbid special characters + //that might cause problems for plugins that don't escape output for HTML. + if (RexAddRoleDialog.invalidDisplayNameRegex.test(name)) { + validationMessage('Sorry, these characters are not allowed: <code>< > &</code>'); + return false; + } + + validationMessage(''); + return true; + } + + onOpen(event, ui) { + //Clear dialog fields when it's opened. + this.roleName(''); + this.roleDisplayName(''); + this.roleToCopyFrom(null); + + if (!this.areTooltipsInitialised) { + this.setupValidationTooltip('#rex-new-role-display-name', this.displayNameValidationMessage); + this.setupValidationTooltip('#rex-new-role-name', this.nameValidationMessage); + this.areTooltipsInitialised = true; + } + } + + onConfirm() { + if (!this.isAddButtonEnabled()) { + return; + } + + this.isOpen(false); + + let caps = {}; + if (this.roleToCopyFrom()) { + caps = this.roleToCopyFrom().getOwnCapabilities(); + } + + this.editor.addRole(this.roleName(), this.roleDisplayName(), caps); + } +} + +class RexDeleteRoleDialog extends RexBaseDialog { + isDeleteButtonEnabled: KnockoutComputed<boolean>; + + private editor: RexRoleEditor; + private isRoleSelected: AmeDictionary<KnockoutObservable<boolean>> = {}; + + constructor(editor: RexRoleEditor) { + super(); + this.editor = editor; + + this.options.minWidth = 420; + this.options.buttons.push({ + text: 'Delete Role', + 'class': 'button button-primary', + click: this.onConfirm.bind(this), + disabled: true + }); + + this.isDeleteButtonEnabled = ko.pureComputed({ + read: () => { + return this.getSelectedRoles().length > 0; + }, + deferEvaluation: true + }); + } + + onConfirm() { + const _ = wsAmeLodash; + let rolesToDelete = this.getSelectedRoles(); + + //Warn about the dangers of deleting built-in roles. + let selectedBuiltInRoles = _.filter(rolesToDelete, _.method('isBuiltIn')); + if (selectedBuiltInRoles.length > 0) { + const warning = 'Caution: Deleting default roles like ' + _.first(selectedBuiltInRoles).displayName() + + ' can prevent you from using certain plugins. This is because some plugins look for specific' + + ' role names to determine if a user is allowed to access the plugin.' + + '\nDelete ' + selectedBuiltInRoles.length + ' default role(s)?'; + if (!confirm(warning)) { + return; + } + } + this.editor.deleteRoles(rolesToDelete); + this.isOpen(false); + } + + onOpen(event, ui) { + //Deselect all previously selected roles. + wsAmeLodash.forEach(this.isRoleSelected, function (isSelected) { + isSelected(false); + }); + } + + getSelectionState(roleName: string): KnockoutObservable<boolean> { + if (!this.isRoleSelected.hasOwnProperty(roleName)) { + this.isRoleSelected[roleName] = ko.observable(false); + } + return this.isRoleSelected[roleName]; + } + + private getSelectedRoles(): RexRole[] { + const _ = wsAmeLodash; + let rolesToDelete = []; + _.forEach(this.editor.roles(), (role) => { + if (this.getSelectionState(role.name())()) { + rolesToDelete.push(role); + } + }); + return rolesToDelete; + } +} + +class RexRenameRoleDialog extends RexBaseDialog { + private editor: RexRoleEditor; + isConfirmButtonEnabled: KnockoutComputed<boolean>; + selectedRole: KnockoutObservable<RexRole> = ko.observable(null); + + newDisplayName: KnockoutObservable<string> = ko.observable(''); + displayNameValidationMessage: KnockoutObservable<string> = ko.observable(''); + isTooltipInitialised: boolean = false; + + constructor(editor: RexRoleEditor) { + super(); + this.editor = editor; + + this.options.minWidth = 380; + this.options.buttons.push({ + text: 'Rename Role', + 'class': 'button button-primary', + click: this.onConfirm.bind(this), + disabled: true + }); + + this.selectedRole.subscribe((role) => { + if (role) { + this.newDisplayName(role.displayName()); + } + }); + + this.isConfirmButtonEnabled = ko.computed({ + read: () => { + return RexAddRoleDialog.validateDisplayName(this.newDisplayName(), this.displayNameValidationMessage); + }, + deferEvaluation: true + }); + } + + onOpen(event, ui) { + const _ = wsAmeLodash; + + if (!this.isTooltipInitialised) { + this.setupValidationTooltip('#rex-edited-role-display-name', this.displayNameValidationMessage); + this.isTooltipInitialised = true; + } + + //Select either the currently selected role or the first available role. + const selectedActor = this.editor.selectedActor(); + if (selectedActor && (selectedActor instanceof RexRole)) { + this.selectedRole(selectedActor); + } else { + this.selectedRole(_.first(this.editor.roles())); + } + } + + onConfirm() { + if (!this.isConfirmButtonEnabled()) { + return; + } + if (this.selectedRole()) { + const name = this.newDisplayName().trim(); + this.selectedRole().displayName(name); + this.editor.actorSelector.repopulate(); + } + this.isOpen(false); + } +} + +class RexEagerObservableStringSet { + private items: Record<string, KnockoutObservable<boolean>> = {}; + + public contains(item: string): boolean { + if (!this.items.hasOwnProperty(item)) { + this.items[item] = ko.observable(false); + return false; + } + return this.items[item](); + } + + public add(item: string) { + if (!this.items.hasOwnProperty(item)) { + this.items[item] = ko.observable(true); + } else { + this.items[item](true); + } + } + + public remove(item: string) { + if (this.items.hasOwnProperty(item)) { + this.items[item](false); + } + } + + public clear() { + const _ = wsAmeLodash; + _.forEach(this.items, (isInSet) => { + isInSet(false); + }); + } + + public getPresenceObservable(item: string): KnockoutObservable<boolean> { + if (!this.items.hasOwnProperty(item)) { + this.items[item] = ko.observable(false); + } + return this.items[item]; + } + + public getAsObject<T>(fillValue: T | boolean = true): Record<string, T> { + const _ = wsAmeLodash; + let output = {}; + _.forEach(this.items, (isInSet, item) => { + if (isInSet()) { + output[item] = fillValue; + } + }); + return output; + } +} + +class RexObservableEditableRoleSettings { + strategy: KnockoutObservable<RexEditableRoleStrategy>; + userDefinedList: RexEagerObservableStringSet; + + constructor() { + this.strategy = ko.observable('auto'); + this.userDefinedList = new RexEagerObservableStringSet(); + } + + toPlainObject(): RexEditableRoleSettings { + let roleList = this.userDefinedList.getAsObject<true>(true); + if (wsAmeLodash.isEmpty(roleList)) { + roleList = null; + } + return { + strategy: this.strategy(), + userDefinedList: roleList + }; + } +} + +class RexUserRoleModule { + primaryRole: KnockoutObservable<RexRole | null>; + private roleObservables: { + [roleId: string]: { + role: RexRole; + selectedActorHasRole: KnockoutComputed<boolean>; + } + } = {}; + + private readonly selectedActor: KnockoutObservable<RexBaseActor>; + public readonly sortedRoles: KnockoutObservable<RexRole[]>; + + public readonly isVisible: KnockoutObservable<boolean>; + + constructor(selectedActor: KnockoutObservable<RexBaseActor>, roles: KnockoutObservableArray<RexRole>) { + this.selectedActor = selectedActor; + this.sortedRoles = ko.computed(() => { + return roles(); + }); + this.primaryRole = ko.computed({ + read: () => { + const actor = selectedActor(); + if ((actor === null) || !actor.canHaveRoles) { + return null; + } + if (actor instanceof RexUser) { + const roles = actor.roles(); + if (roles.length < 1) { + return null; + } + return roles[0]; + } + return null; + }, + write: (newRole: RexRole | null) => { + const actor = selectedActor(); + if ((actor === null) || !actor.canHaveRoles || !(actor instanceof RexUser)) { + return; + } + + //No primary role = no roles at all. + if (newRole === null) { + actor.roles.removeAll(); + return; + } + + //Sanity check. + if (!(newRole instanceof RexRole)) { + return; + } + + if (!this.canAssignRoleToActor(newRole)) { + return; + } + + //Remove the previous primary role. + const oldPrimaryRole = (actor.roles().length > 0) ? actor.roles()[0] : null; + if (oldPrimaryRole !== null) { + actor.roles.remove(oldPrimaryRole); + } + + //If the user already has the new role, remove it from its old position first. + if (actor.roles.indexOf(newRole) !== -1) { + actor.roles.remove(newRole); + } + //Add the role to the top of the list. + actor.roles.unshift(newRole); + } + }); + + this.isVisible = ko.pureComputed(() => { + const actor = this.selectedActor(); + return (actor !== null) && actor.canHaveRoles; + }); + } + + // noinspection JSUnusedGlobalSymbols Used in Knockout templates. + actorHasRole(role: RexRole): KnockoutObservable<boolean> { + const roleActorId = role.getId(); + if (this.roleObservables.hasOwnProperty(roleActorId) && (this.roleObservables[roleActorId].role === role)) { + return this.roleObservables[roleActorId].selectedActorHasRole; + } + + let selectedActorHasRole = ko.computed<boolean>({ + read: () => { + const actor = this.selectedActor(); + if ((actor === null) || !actor.canHaveRoles) { + return false; + } + if (actor instanceof RexUser) { + return (actor.roles.indexOf(role) !== -1); + } + return false; + }, + write: (shouldHaveRole) => { + const actor = this.selectedActor(); + if ((actor === null) || !actor.canHaveRoles || !(actor instanceof RexUser)) { + return; + } + if (!this.canAssignRoleToActor(role)) { + return; + } + + const alreadyHasRole = (actor.roles.indexOf(role) !== -1); + if (shouldHaveRole !== alreadyHasRole) { + if (shouldHaveRole) { + actor.roles.push(role); + } else { + actor.roles.remove(role); + } + } + } + }); + + this.roleObservables[roleActorId] = { + role: role, + selectedActorHasRole: selectedActorHasRole + }; + + return selectedActorHasRole; + } + + canAssignRoleToActor(role: RexRole): boolean { + //This is a stub. The role editor currently doesn't check editable role settings at edit time. + const actor = this.selectedActor(); + if ((actor === null) || !actor.canHaveRoles) { + return false; + } + return (role instanceof RexRole); + } +} + +class RexEditableRolesDialog extends RexBaseDialog { + private editor: RexRoleEditor; + private readonly visibleActors: KnockoutObservableArray<IAmeActor>; + selectedActor: KnockoutObservable<RexBaseActor> = ko.observable(null); + + private actorSettings: { [actorId: string]: RexObservableEditableRoleSettings; } = {}; + private selectedActorSettings: KnockoutObservable<RexObservableEditableRoleSettings>; + + editableRoleStrategy: KnockoutComputed<string>; + isAutoStrategyAllowed: KnockoutComputed<boolean>; + isListStrategyAllowed: KnockoutComputed<boolean>; + + constructor(editor: RexRoleEditor) { + super(); + this.editor = editor; + this.visibleActors = ko.observableArray([]); + + this.options.minWidth = 600; + this.options.buttons.push({ + text: 'Save Changes', + 'class': 'button button-primary', + click: this.onConfirm.bind(this), + disabled: false + }); + + //Super Admin is always set to "leave unchanged" because + //they can edit all roles. + const superAdmin = editor.getSuperAdmin(); + const superAdminSettings = new RexObservableEditableRoleSettings(); + superAdminSettings.strategy('none'); + + const dummySettings = new RexObservableEditableRoleSettings(); + this.selectedActorSettings = ko.computed(() => { + if (this.selectedActor() === null) { + return dummySettings; + } + if (this.selectedActor() === superAdmin) { + return superAdminSettings; + } + const actorId = this.selectedActor().getId(); + if (!this.actorSettings.hasOwnProperty(actorId)) { + //This should never happen; the dictionary should be initialised when opening the dialog. + this.actorSettings[actorId] = new RexObservableEditableRoleSettings(); + } + return this.actorSettings[actorId]; + }); + + this.editableRoleStrategy = ko.computed({ + read: () => { + return this.selectedActorSettings().strategy(); + }, + write: (newValue: string) => { + this.selectedActorSettings().strategy(newValue as RexEditableRoleStrategy); + } + }); + + this.isAutoStrategyAllowed = ko.computed(() => { + const actor = this.selectedActor(); + if (actor == null) { + return true; + } + return !( + (actor === superAdmin) + || ((actor instanceof RexUser) && actor.isSuperAdmin) + ); + }); + this.isListStrategyAllowed = this.isAutoStrategyAllowed; + } + + onOpen(event, ui) { + const _ = wsAmeLodash; + + //Copy editable role settings into observables. + _.forEach(this.editor.actorEditableRoles, (settings: RexEditableRoleSettings, actorId: string) => { + if (!this.actorSettings.hasOwnProperty(actorId)) { + this.actorSettings[actorId] = new RexObservableEditableRoleSettings(); + } + const observableSettings = this.actorSettings[actorId]; + observableSettings.strategy(settings.strategy); + observableSettings.userDefinedList.clear(); + if (settings.userDefinedList !== null) { + _.forEach(settings.userDefinedList, (ignored, roleId: string) => { + observableSettings.userDefinedList.add(roleId); + }); + } + }); + + this.visibleActors(this.editor.actorSelector.getVisibleActors()); + + //Select either the currently selected actor or the first role. + const selectedActor = this.editor.selectedActor(); + if (selectedActor) { + this.selectedActor(selectedActor); + } else { + this.selectedActor(_.first(this.editor.roles())); + } + } + + onConfirm() { + //Save editable roles + const _ = wsAmeLodash; + let settings = this.editor.actorEditableRoles; + _.forEach(this.actorSettings, (observableSettings, actorId) => { + if (observableSettings.strategy() === 'auto') { + //"auto" is the default so we don't need to store anything. + delete settings[actorId]; + } else { + settings[actorId] = observableSettings.toPlainObject(); + } + }); + + this.isOpen(false); + } + + isRoleSetToEditable(role: RexBaseActor) { + return this.selectedActorSettings().userDefinedList.getPresenceObservable(role.name()); + } + + isRoleEnabled(role: RexBaseActor) { + return this.editableRoleStrategy() === 'user-defined-list'; + } + + selectItem(actor: RexBaseActor) { + this.selectedActor(actor); + } + + getItemText(actor: RexBaseActor): string { + return this.editor.actorSelector.getNiceName(actor); + } +} + +class RexRoleEditor implements AmeActorManagerInterface { + public static readonly hierarchyView = { + label: 'Hierarchy view', + id: 'hierarchy', + templateName: 'rex-hierarchy-view-template' + }; + public static readonly singleCategoryView = { + label: 'Category view', + id: 'category', + templateName: 'rex-single-category-view-template' + }; + public static readonly listView = {label: 'List view', id: 'list', templateName: 'rex-list-view-template'}; + // noinspection JSUnusedGlobalSymbols + readonly categoryViewOptions: RexCategoryViewOption[] = [ + RexRoleEditor.hierarchyView, + RexRoleEditor.singleCategoryView, + RexRoleEditor.listView + ]; + + readonly selectedActor: KnockoutComputed<RexBaseActor>; + readonly readableNamesEnabled: KnockoutObservable<boolean>; + + roles: KnockoutObservableArray<RexRole>; + users: KnockoutObservableArray<RexUser>; + capabilities: { [capability: string]: RexCapability }; + + postTypes: { [name: string]: RexPostTypeData }; + taxonomies: { [name: string]: RexTaxonomyData }; + + private deprecatedCapabilities: CapabilityMap = {}; + private readonly metaCapabilityMap: AmeDictionary<string>; + + private readonly userDefinedCapabilities: CapabilityMap = {}; + + private readonly components: { [id: string]: RexWordPressComponent }; + readonly coreComponent: RexWordPressComponent; + + rootCategory: RexCategory; + selectedCategory: KnockoutComputed<RexCategory>; + categoriesBySlug: AmeDictionary<RexCategory> = {}; + categoryViewMode: KnockoutObservable<RexCategoryViewOption>; + capabilityViewClasses: KnockoutObservable<string>; + readonly leafCategories: KnockoutComputed<RexCategory[]>; + allCapabilitiesAsPermissions: KnockoutComputed<RexPermission[]>; + /** + * Index of capabilities that are in the selected category or its subcategories. + */ + private readonly capsInSelectedCategory: KnockoutComputed<AmeDictionary<boolean>>; + + private readonly defaultNewUserRoleName: string; + defaultRoles: KnockoutComputed<RexRole[]>; + customRoles: KnockoutComputed<RexRole[]>; + trashedRoles: KnockoutObservableArray<RexRole>; + + actorSelector: AmeActorSelector; + private actorLookup: AmeDictionary<RexBaseActor> = {}; + private readonly dummyActor: RexRole; + permissionTipSubject: KnockoutObservable<RexPermission>; + + searchQuery: KnockoutObservable<string>; + searchKeywords: KnockoutComputed<string[]>; + + public readonly userPreferences: RexUserPreferences; + + public actorEditableRoles: RexActorEditableRoles; + + public readonly isLoaded: KnockoutObservable<boolean>; + public readonly areBindingsApplied: KnockoutObservable<boolean>; + + /** + * Show deprecated capabilities. + */ + showDeprecatedEnabled: KnockoutObservable<boolean>; + /** + * Show CPT or taxonomy permissions that use the same capability as another permission on that CPT/taxonomy. + */ + showRedundantEnabled: KnockoutObservable<boolean>; + /** + * Show CPT or taxonomy capabilities that match the built-in "Posts" post type or the "Categories" taxonomy. + */ + showBaseCapsEnabled: KnockoutObservable<boolean>; + + /** + * Show only checked (granted) capabilities. + */ + showOnlyCheckedEnabled: KnockoutObservable<boolean>; + + showNumberOfCapsEnabled: KnockoutObservable<boolean>; + showTotalCapCountEnabled: KnockoutObservable<boolean>; + showGrantedCapCountEnabled: KnockoutObservable<boolean>; + showZerosEnabled: KnockoutObservable<boolean>; + + categoryWidthMode: KnockoutObservable<string>; + + inheritanceOverrideEnabled: KnockoutObservable<boolean>; + + deleteCapabilityDialog: AmeKnockoutDialog; + addCapabilityDialog: RexAddCapabilityDialog; + addRoleDialog: AmeKnockoutDialog; + deleteRoleDialog: AmeKnockoutDialog; + renameRoleDialog: AmeKnockoutDialog; + editableRolesDialog: AmeKnockoutDialog; + + userRoleModule: RexUserRoleModule; + + settingsFieldData: KnockoutObservable<string>; + isSaving: KnockoutObservable<boolean>; + isGlobalSettingsUpdate: KnockoutObservable<boolean>; + + public readonly isShiftKeyDown: KnockoutObservable<boolean>; + + constructor(data: RexAppData) { + const self = this; + const _ = wsAmeLodash; + + this.areBindingsApplied = ko.observable(false); + this.isLoaded = ko.computed(() => { + return this.areBindingsApplied(); + }); + + this.userPreferences = new RexUserPreferences(data.userPreferences, data.adminAjaxUrl, data.updatePreferencesNonce); + + const preferences = this.userPreferences; + this.showDeprecatedEnabled = preferences.getObservable('showDeprecatedEnabled', true); + this.showRedundantEnabled = preferences.getObservable('showRedundantEnabled', false); + this.showBaseCapsEnabled = ko.computed<boolean>(this.showRedundantEnabled); + this.showOnlyCheckedEnabled = preferences.getObservable('showOnlyCheckedEnabled', false); + this.categoryWidthMode = preferences.getObservable<string>('categoryWidthMode', 'adaptive'); + + this.readableNamesEnabled = preferences.getObservable<boolean>('readableNamesEnabled', true); + + this.showNumberOfCapsEnabled = preferences.getObservable('showNumberOfCapsEnabled', true); + this.showGrantedCapCountEnabled = preferences.getObservable('showGrantedCapCountEnabled', true); + this.showTotalCapCountEnabled = preferences.getObservable('showTotalCapCountEnabled', true); + this.showZerosEnabled = preferences.getObservable('showZerosEnabled', false); + this.inheritanceOverrideEnabled = preferences.getObservable('inheritanceOverrideEnabled', false); + + //Remember and restore the selected view mode. + let viewModeId = preferences.getObservable('categoryVewMode', 'hierarchy'); + let initialViewMode = _.find(this.categoryViewOptions, 'id', viewModeId()); + if (!initialViewMode) { + initialViewMode = RexRoleEditor.hierarchyView; + } + this.categoryViewMode = ko.observable(initialViewMode); + this.categoryViewMode.subscribe(function (newMode) { + viewModeId(newMode.id); + }); + + this.isShiftKeyDown = ko.observable(false); + + this.capabilityViewClasses = ko.pureComputed({ + read: () => { + const viewMode = this.categoryViewMode(); + let classes = ['rex-category-view-mode-' + viewMode.id]; + if (viewMode === RexRoleEditor.singleCategoryView) { + classes.push('rex-show-category-subheadings'); + } + if (this.readableNamesEnabled()) { + classes.push('rex-readable-names-enabled'); + } + if (this.categoryWidthMode() === 'full') { + classes.push('rex-full-width-categories'); + } + return classes.join(' '); + }, + deferEvaluation: true + }); + + this.searchQuery = ko.observable('').extend({rateLimit: {timeout: 100, method: "notifyWhenChangesStop"}}); + this.searchKeywords = ko.computed(function () { + let query = self.searchQuery().trim(); + if (query === '') { + return []; + } + + return wsAmeLodash(query.split(' ')) + .map((keyword) => { + return keyword.trim() + }) + .filter((keyword) => { + return (keyword !== ''); + }) + .value(); + }); + + this.components = _.mapValues(data.knownComponents, (details, id) => { + return RexWordPressComponent.fromJs(id, details); + }); + this.coreComponent = new RexWordPressComponent(':wordpress:', 'WordPress core'); + this.components[':wordpress:'] = this.coreComponent; + + //Populate roles and users. + const tempRoleList = []; + _.forEach(data.roles, (roleData) => { + const role = new RexRole(roleData.name, roleData.displayName, roleData.capabilities); + role.hasUsers = roleData.hasUsers; + tempRoleList.push(role); + this.actorLookup[role.id()] = role; + }); + this.roles = ko.observableArray(tempRoleList); + + const tempUserList = []; + _.forEach(AmeActors.getUsers(), (data) => { + const user = RexUser.fromAmeUser(data, self); + tempUserList.push(user); + this.actorLookup[user.id()] = user; + }); + this.users = ko.observableArray(tempUserList); + + this.dummyActor = new RexRole('rex-invalid-role', 'Invalid Role'); + this.defaultNewUserRoleName = data.defaultRoleName; + this.trashedRoles = ko.observableArray(_.map(data.trashedRoles, function (roleData) { + return RexRole.fromRoleData(roleData); + })); + + this.actorSelector = new AmeActorSelector(this, true, false); + //Wrap the selected actor in a computed observable so that it can be used with Knockout. + let _selectedActor = ko.observable(this.getActor(this.actorSelector.selectedActor)); + this.selectedActor = ko.computed({ + read: function () { + return _selectedActor(); + }, + write: (newActor: RexBaseActor) => { + this.actorSelector.setSelectedActor(newActor.id()); + } + }); + this.actorSelector.onChange((newSelectedActor: string) => { + _selectedActor(this.getActor(newSelectedActor)); + }); + + //Refresh the actor selector when roles are added or removed. + this.roles.subscribe(() => { + this.actorSelector.repopulate(); + }); + + //Re-select the previously selected actor if possible. + let initialActor: RexBaseActor = null; + if (data.selectedActor) { + initialActor = this.getActor(data.selectedActor); + } + if (!initialActor || (initialActor === this.dummyActor)) { + initialActor = this.roles()[0]; + } + this.selectedActor(initialActor); + + //Populate capabilities. + this.deprecatedCapabilities = data.deprecatedCapabilities; + this.metaCapabilityMap = data.metaCapMap; + this.userDefinedCapabilities = data.userDefinedCapabilities; + + this.capabilities = _.mapValues(data.capabilities, (metadata: RexCapabilityData, name: string) => { + return RexCapability.fromJs(name, metadata, self); + }); + + //Add the special "do_not_allow" capability. Normally, it's impossible to assign it to anyone, + //but it can still be used in post type permissions and other places. + const doNotAllow = new RexDoNotAllowCapability(this); + doNotAllow.originComponent = this.components[':wordpress:']; + this.capabilities['do_not_allow'] = doNotAllow; + + //Similarly, "exist" is always enabled for all roles and users. Everyone can exist. + if (this.capabilities.hasOwnProperty('exist')) { + this.capabilities['exist'] = new RexExistCapability(this); + this.capabilities['exist'].originComponent = this.components[':wordpress:']; + } + + //Store editable roles. + this.actorEditableRoles = (!_.isEmpty(data.editableRoles)) ? data.editableRoles : {}; + + this.rootCategory = new RexCategory('All', this); + + const coreCategory = RexCategory.fromJs(data.coreCategory, this); + this.rootCategory.addSubcategory(coreCategory); + + const postTypeCategory = new RexPostTypeContainerCategory('Post Types', this, 'postTypes'); + this.postTypes = _.indexBy(data.postTypes, 'name'); + _.forEach(this.postTypes, (details: RexPostTypeData, id) => { + const category = new RexPostTypeCategory(details.label, self, id, 'postTypes/' + id, details.permissions, details.isDefault); + if (details.componentId) { + category.origin = this.getComponent(details.componentId); + } + postTypeCategory.addSubcategory(category); + + //Record the post type actions associated with each capability. + for (let action in details.permissions) { + const capability = self.getCapability(details.permissions[action]); + _.set(capability.usedByPostTypeActions, [details.name, action], true); + } + }); + //Sort the actual subcategory array. + postTypeCategory.sortSubcategories(); + this.rootCategory.addSubcategory(postTypeCategory); + + //Taxonomies. + this.taxonomies = data.taxonomies; + const taxonomyCategory = new RexTaxonomyContainerCategory('Taxonomies', this, 'taxonomies'); + _.forEach(data.taxonomies, (details, id) => { + const category = new RexTaxonomyCategory(details.label, self, id, 'taxonomies/' + id, details.permissions); + taxonomyCategory.addSubcategory(category); + + //Record taxonomy type actions associated with each capability. + for (let action in details.permissions) { + const capability = self.getCapability(details.permissions[action]); + _.set(capability.usedByTaxonomyActions, [details.name, action], true); + } + }); + taxonomyCategory.subcategories.sort((a, b) => { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }); + + this.rootCategory.addSubcategory(taxonomyCategory); + + const customParentCategory = new RexCategory('Plugins', this, 'custom'); + + function initCustomCategory(details: RexCategoryData, parent: RexCategory) { + let category = RexCategory.fromJs(details, self); + + //Sort subcategories by title. + category.subcategories.sort((a, b) => { + //Keep the "General" category at the top if there is one. + if (a.name === b.name) { + return 0 + } else if (a.name === 'General') { + return -1; + } else if (b.name === 'General') { + return 1; + } + return a.name.localeCompare(b.name); + }); + + parent.addSubcategory(category); + } + + _.forEach(data.customCategories, (details) => { + initCustomCategory(details, customParentCategory); + }); + customParentCategory.subcategories.sort((a, b) => { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }); + this.rootCategory.addSubcategory(customParentCategory); + + //Make a category for uncategorized capabilities. This one is always at the bottom. + const uncategorizedCategory = new RexCategory( + 'Uncategorized', + self, + 'custom/uncategorized', + data.uncategorizedCapabilities + ); + customParentCategory.addSubcategory(uncategorizedCategory); + + let _selectedCategory: KnockoutObservable<RexCategory> = ko.observable(null); + this.selectedCategory = ko.computed({ + read: function () { + return _selectedCategory(); + }, + write: function (newSelection: RexCategory) { + const oldSelection = _selectedCategory(); + if (newSelection === oldSelection) { + return; + } + + if (newSelection) { + newSelection.isSelected(true); + } + if (oldSelection) { + oldSelection.isSelected(false); + } + _selectedCategory(newSelection); + } + }); + this.selectedCategory(this.rootCategory); + + this.permissionTipSubject = ko.observable(null); + + this.allCapabilitiesAsPermissions = ko.pureComputed({ + read: () => { + //Create a permission for each unique, non-deleted capability. + //Exclude special caps like do_not_allow and exist because they can't be enabled. + const excludedCaps = ['do_not_allow', 'exist']; + return _.chain(this.capabilities) + .map(function (capability) { + if (excludedCaps.indexOf(capability.name) >= 0) { + return null; + } + return new RexPermission(self, capability); + }) + .filter(function (value) { + return value !== null + }) + .value(); + }, + deferEvaluation: true + }); + + this.capsInSelectedCategory = ko.pureComputed({ + read: () => { + const category = this.selectedCategory(); + if (!category) { + return {}; + } + + let caps = {}; + category.countUniqueCapabilities(caps); + return caps; + }, + deferEvaluation: true + }); + + this.leafCategories = ko.computed({ + read: () => { + //So what we want here is a depth-first traversal of the category tree. + let results: RexCategory[] = []; + let addedUniqueCategories: AmeDictionary<RexCategory> = {}; + + function traverse(category: RexCategory) { + if (category.subcategories.length < 1) { + //Eliminate duplicates, like CPTs that show up in the post type category and a plugin category. + let key = category.getDeDuplicationKey(); + if (!addedUniqueCategories.hasOwnProperty(key)) { + results.push(category); + addedUniqueCategories[key] = category; + } else { + addedUniqueCategories[key].addDuplicate(category); + } + return; + } + + for (let i = 0; i < category.subcategories.length; i++) { + traverse(category.subcategories[i]); + } + } + + traverse(this.rootCategory); + + results.sort(function (a, b) { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + }); + + return results; + }, + deferEvaluation: true + }); + + const compareRoleDisplayNames = function (a: RexRole, b: RexRole): number { + return a.displayName().toLowerCase().localeCompare(b.displayName().toLowerCase()); + }; + this.defaultRoles = ko.pureComputed({ + read: function () { + return _.filter(self.roles(), function (role: RexRole) { + return role.isBuiltIn(); + }).sort(compareRoleDisplayNames); + }, + deferEvaluation: true + }); + this.customRoles = ko.computed({ + read: function () { + return _.difference(self.roles(), self.defaultRoles()).sort(compareRoleDisplayNames); + }, + deferEvaluation: true + }); + + this.deleteCapabilityDialog = new RexDeleteCapDialog(this); + this.addCapabilityDialog = new RexAddCapabilityDialog(this); + this.addRoleDialog = new RexAddRoleDialog(this); + this.deleteRoleDialog = new RexDeleteRoleDialog(this); + this.renameRoleDialog = new RexRenameRoleDialog(this); + this.editableRolesDialog = new RexEditableRolesDialog(this); + + this.userRoleModule = new RexUserRoleModule(this.selectedActor, this.roles); + + this.settingsFieldData = ko.observable(''); + this.isSaving = ko.observable(false); + this.isGlobalSettingsUpdate = ko.observable(false); + } + + capabilityMatchesFilters(capability: RexCapability): boolean { + if (!this.showDeprecatedEnabled() && this.isDeprecated(capability.name)) { + return false; + } + + if (this.showOnlyCheckedEnabled() && !capability.isEnabledForSelectedActor()) { + return false; + } + + const keywords = this.searchKeywords(), + capabilityName = capability.name; + if (keywords.length > 0) { + const haystack = capabilityName.toLowerCase(); + const matchesKeywords = wsAmeLodash.all( + keywords, + function (keyword) { + return haystack.indexOf(keyword) >= 0; + } + ); + + if (!matchesKeywords) { + return false; + } + } + + return true; + } + + isDeprecated(capability: string): boolean { + return this.deprecatedCapabilities.hasOwnProperty(capability); + } + + getComponent(componentId: string): RexWordPressComponent | null { + if (this.components.hasOwnProperty(componentId)) { + return this.components[componentId]; + } + return null; + } + + /** + * Get or create a capability instance. + */ + getCapability(capabilityName: string, recursionDepth: number = 0): RexCapability { + //Un-map meta capabilities where possible. + if (this.metaCapabilityMap.hasOwnProperty(capabilityName) && (recursionDepth < 10)) { + return this.getCapability(this.metaCapabilityMap[capabilityName], recursionDepth + 1); + } + + if (!this.capabilities.hasOwnProperty(capabilityName)) { + const _ = wsAmeLodash; + if (!_.isString(capabilityName) && !_.isFinite(capabilityName)) { + return this.getInvalidCapability(capabilityName); + } + + if (console && console.info) { + console.info('Capability not found: "' + capabilityName + '". It will be created.'); + } + capabilityName = String(capabilityName); + this.capabilities[capabilityName] = new RexCapability(capabilityName, this); + } + return this.capabilities[capabilityName]; + } + + private getInvalidCapability(invalidName: any): RexCapability { + const capabilityName = '[Invalid capability: ' + String(invalidName) + ']'; + if (!this.capabilities.hasOwnProperty(capabilityName)) { + if (console && console.error) { + console.error('Invalid capability detected - expected a string but got this: ', invalidName); + } + this.capabilities[capabilityName] = new RexInvalidCapability(capabilityName, invalidName, this); + } + return this.capabilities[capabilityName]; + } + + getActor(actorId: string): RexBaseActor { + if (this.actorLookup.hasOwnProperty(actorId)) { + return this.actorLookup[actorId]; + } + return this.dummyActor; + } + + getRole(name: string): RexRole { + const actorId = 'role:' + name; + if (this.actorLookup.hasOwnProperty(actorId)) { + const role = this.actorLookup[actorId]; + if (role instanceof RexRole) { + return role; + } + } + return null; + } + + // noinspection JSUnusedGlobalSymbols Testing method used in KO templates. + setSubjectPermission(permission: RexPermission) { + this.permissionTipSubject(permission); + } + + /** + * Search a string for the current search keywords and add the "rex-search-highlight" CSS class to each match. + * + * @param inputString + */ + highlightSearchKeywords(inputString: string): string { + const _ = wsAmeLodash; + const keywordList = this.searchKeywords(); + if (keywordList.length === 0) { + return inputString; + } + + let keywordGroup = _.map(keywordList, _.escapeRegExp).join('|'); + let regex = new RegExp('((?:' + keywordGroup + ')(?:\\s*))+', 'gi'); + + return inputString.replace( + regex, + function (foundKeywords) { + //Don't highlight the trailing space after the keyword(s). + let trailingSpace = ''; + let parts = foundKeywords.match(/^(.+?)(\s+)$/); + if (parts) { + foundKeywords = parts[1]; + trailingSpace = parts[2]; + } + + return '<mark class="rex-search-highlight">' + foundKeywords + '</mark>' + trailingSpace; + } + ); + } + + actorExists(actorId: string): boolean { + return this.actorLookup.hasOwnProperty(actorId); + } + + addUsers(newUsers: IAmeUser[]) { + wsAmeLodash.forEach(newUsers, (user) => { + if (!(user instanceof RexUser)) { + if (console.error) { + console.error('Cannot add a user. Expected an instance of RexUser, got this:', user); + } + return; + } + if (!this.actorLookup.hasOwnProperty(user.getId())) { + this.users.push(user); + this.actorLookup[user.getId()] = user; + } + }); + } + + createUserFromProperties(properties: AmeUserPropertyMap): RexUser { + return RexUser.fromAmeUserProperties(properties, this); + } + + getRoles(): AmeDictionary<RexRole> { + return wsAmeLodash.indexBy(this.roles(), function (role) { + return role.name(); + }); + } + + getSuperAdmin(): RexSuperAdmin { + return RexSuperAdmin.getInstance(); + } + + getUser(login: string): RexUser { + const actorId = 'user:' + login; + if (this.actorLookup.hasOwnProperty(actorId)) { + const user = this.actorLookup[actorId]; + if (user instanceof RexUser) { + return user; + } + } + return null; + } + + getUsers(): AmeDictionary<RexUser> { + return wsAmeLodash.indexBy(this.users(), 'userLogin'); + } + + isInSelectedCategory(capabilityName: string): boolean { + let caps = this.capsInSelectedCategory(); + return caps.hasOwnProperty(capabilityName); + } + + addCapability(capabilityName: string): RexCategory { + let capability: RexCapability; + if (this.capabilities.hasOwnProperty(capabilityName)) { + capability = this.capabilities[capabilityName]; + if (!capability.isDeleted()) { + throw 'Cannot add capability "' + capabilityName + '" because it already exists.'; + } + capability.isDeleted(false); + + this.userDefinedCapabilities[capabilityName] = true; + return null; + } else { + capability = new RexCapability(capabilityName, this); + capability.notes = 'This capability has not been saved yet. Click the "Save Changes" button to save it.'; + this.capabilities[capabilityName] = capability; + + //Add the new capability to the "Other" or "Uncategorized" category. + const category = this.categoriesBySlug['custom/uncategorized']; + const permission = new RexPermission(this, capability); + category.permissions.push(permission); + category.sortPermissions(); + + this.userDefinedCapabilities[capabilityName] = true; + return category; + } + } + + deleteCapabilities(selectedCapabilities: RexCapability[]) { + const self = this, _ = wsAmeLodash; + const targetActors = _.union<RexBaseActor>(this.roles(), this.users()); + + _.forEach(selectedCapabilities, function (capability) { + //Remove it from all roles and visible users. + _.forEach(targetActors, function (actor) { + actor.deleteCap(capability.name); + }); + capability.isDeleted(true); + + delete self.userDefinedCapabilities[capability.name]; + }); + } + + capabilityExists(capabilityName: string): boolean { + return this.capabilities.hasOwnProperty(capabilityName) && !this.capabilities[capabilityName].isDeleted(); + } + + addRole(name: string, displayName: string, capabilities: CapabilityMap = {}): RexRole { + let role = new RexRole(name, displayName, capabilities); + this.actorLookup[role.id()] = role; + this.roles.push(role); + + //Select the new role. + this.selectedActor(role); + + return role; + } + + deleteRoles(roles: RexRole[]) { + const _ = wsAmeLodash; + _.forEach(roles, (role) => { + if (!this.canDeleteRole(role)) { + throw 'Cannot delete role "' + role.name() + '"'; + } + }); + + this.roles.removeAll(roles); + this.trashedRoles.push.apply(this.trashedRoles, roles); + //TODO: Later, add an option to restore deleted roles. + } + + canDeleteRole(role: RexRole): boolean { + //Was the role already assigned to any users when the editor was opened? + if (role.hasUsers) { + return false; + } + //We also need to take into account any unsaved user role changes. + //Is the role assigned to any of the users currently loaded in the editor? + const _ = wsAmeLodash; + if (_.some(this.users(), function (user) { + return (user.roles.indexOf(role) !== -1); + })) { + return false; + } + return !this.isDefaultRoleForNewUsers(role); + } + + isDefaultRoleForNewUsers(role: RexRole): boolean { + return (role.name() === this.defaultNewUserRoleName); + } + + // noinspection JSUnusedGlobalSymbols Used in KO templates. + saveChanges() { + this.isSaving(true); + const _ = wsAmeLodash; + + let data = { + 'roles': _.invoke(this.roles(), 'toJs'), + 'users': _.invoke(this.users(), 'toJs'), + 'trashedRoles': _.invoke(this.trashedRoles(), 'toJs'), + 'userDefinedCaps': _.keys(this.userDefinedCapabilities), + 'editableRoles': this.actorEditableRoles + }; + + this.settingsFieldData(ko.toJSON(data)); + jQuery('#rex-save-settings-form').submit(); + } + + updateAllSites() { + if (!confirm('Apply these role settings to ALL sites? Any changes that you\'ve made to individual sites will be lost.')) { + return false; + } + this.isGlobalSettingsUpdate(true); + this.saveChanges(); + } +} + +declare var wsRexRoleEditorData: RexAppData; + +(function () { + jQuery(function ($) { + const rootElement = jQuery('#ame-role-editor-root'); + + //Initialize the application. + const app = new RexRoleEditor(wsRexRoleEditorData); + //The input data can be quite large, so let's give the browser a chance to free up that memory. + wsRexRoleEditorData = null; + + window['ameRoleEditor'] = app; + + //console.time('Apply Knockout bindings'); + //ko.options.deferUpdates = true; + ko.applyBindings(app, rootElement.get(0)); + app.areBindingsApplied(true); + //console.timeEnd('Apply Knockout bindings'); + + //Track the state of the Shift key. + let isShiftKeyDown = false; + + function handleKeyboardEvent(event) { + const newState = !!(event.shiftKey); + if (newState !== isShiftKeyDown) { + isShiftKeyDown = newState; + app.isShiftKeyDown(isShiftKeyDown); + } + } + + $(document).on( + 'keydown.adminMenuEditorRex keyup.adminMenuEditorRex mousedown.adminMenuEditorRex', + handleKeyboardEvent + ); + + //Initialize permission tooltips. + let visiblePermissionTooltips = []; + + rootElement.find('#rex-capability-view').on('mouseenter click', '.rex-permission-tip-trigger', function (event) { + $(this).qtip({ + overwrite: false, + content: { + text: 'Loading...' + }, + + //Show the tooltip on focus. + show: { + event: 'click mouseenter', + delay: 80, + solo: '#ame-role-editor-root', + ready: true, + effect: false + }, + hide: { + event: 'mouseleave unfocus', + fixed: true, + delay: 300, + leave: false, + effect: false + }, + + position: { + my: 'center left', + at: 'center right', + effect: false, + viewport: $(window), + adjust: { + method: 'flipinvert shift', + scroll: false, + } + }, + style: { + classes: 'qtip-bootstrap qtip-shadow rex-tooltip' + }, + + events: { + show: function (event, api) { + //Immediately hide all other permission tooltips. + for (let i = visiblePermissionTooltips.length - 1; i >= 0; i--) { + visiblePermissionTooltips[i].hide(); + } + + let permission = ko.dataFor(api.target.get(0)); + if (permission && (permission instanceof RexPermission)) { + app.permissionTipSubject(permission); + } + + //Move the content container to the current tooltip. + const tipContent = $('#rex-permission-tip'); + if (!$.contains(api.elements.content.get(0), tipContent.get(0))) { + api.elements.content.empty().append(tipContent); + } + + visiblePermissionTooltips.push(api); + }, + hide: function (event, api) { + const index = visiblePermissionTooltips.indexOf(api); + if (index >= 0) { + visiblePermissionTooltips.splice(index, 1); + } + } + } + }, event); + }); + + //Tooltips must have a higher z-index than the modal widget overlay and the Toolbar. + jQuery.fn.qtip.zindex = 100101 + 5000; + + //Set up dropdown menus. + $('.rex-dropdown-trigger').on('click', function (event) { + const $trigger = $(this); + const $dropdown = $('#' + $trigger.data('target-dropdown-id')); + + event.stopPropagation(); + event.preventDefault(); + + function hideThisDropdown(event) { + //Only do it if the user clicked something outside the dropdown. + const $clickedDropdown = $(event.target).closest($dropdown.get(0)); + if ($clickedDropdown.length < 1) { + $dropdown.hide(); + $(document).off('click', hideThisDropdown); + } + } + + if ($dropdown.is(':visible')) { + $dropdown.hide(); + $(document).off('click', hideThisDropdown); + return; + } + + $dropdown.show().position({ + my: 'left top', + at: 'left bottom', + of: $trigger + }); + + $(document).on('click', hideThisDropdown); + }); + }); +})(); diff --git a/extras/modules/role-editor/uninstall.php b/extras/modules/role-editor/uninstall.php new file mode 100644 index 0000000..6faedfd --- /dev/null +++ b/extras/modules/role-editor/uninstall.php @@ -0,0 +1,6 @@ +<?php + +if ( defined('ABSPATH') && defined('WP_UNINSTALL_PLUGIN') ) { + delete_site_option('ws_ame_role_editor'); + delete_option('ws_ame_role_editor'); +} \ No newline at end of file diff --git a/extras/modules/separator-styles/ameMenuSeparatorStyler.php b/extras/modules/separator-styles/ameMenuSeparatorStyler.php new file mode 100644 index 0000000..f4938eb --- /dev/null +++ b/extras/modules/separator-styles/ameMenuSeparatorStyler.php @@ -0,0 +1,234 @@ +<?php + +class ameMenuSeparatorStyler { + const CSS_AJAX_ACTION = 'ame_output_separator_css'; + + private $menuEditor; + + /** + * ameMenuSeparatorStyler constructor. + * + * @param WPMenuEditor $menuEditor + */ + public function __construct($menuEditor) { + $this->menuEditor = $menuEditor; + ameMenu::add_custom_loader(array($this, 'loadSeparatorSettings')); + + if ( !is_admin() ) { + return; + } + + add_action('admin_menu_editor-footer-editor', array($this, 'outputDialog')); + add_action('admin_menu_editor-enqueue_styles-editor', array($this, 'enqueueStyles')); + + add_filter('ame_pre_set_custom_menu', array($this, 'addSeparatorCssToConfiguration')); + add_action('admin_enqueue_scripts', array($this, 'enqueueCustomSeparatorStyle')); + add_action('wp_ajax_' . self::CSS_AJAX_ACTION, array($this, 'ajaxOutputCss')); + } + + public function outputDialog() { + require __DIR__ . '/separator-styles-template.php'; + + wp_enqueue_auto_versioned_script( + 'ame-separator-settings-js', + plugins_url('separator-settings.js', __FILE__), + array('jquery', 'knockout', 'jquery-ui-dialog', 'jquery-ui-tabs', 'ame-ko-extensions', 'ame-lodash'), + true + ); + } + + public function enqueueStyles() { + wp_enqueue_style( + 'ame-separator-settings', + plugins_url('separator-settings.css', __FILE__), + array('menu-editor-base-style', 'wp-color-picker') + ); + } + + public function addSeparatorCssToConfiguration($customMenu) { + if ( empty($customMenu) || !is_array($customMenu) ) { + return $customMenu; + } + + if ( empty($customMenu['separators']) ) { + unset($customMenu['separator_css']); + unset($customMenu['separator_css_modified']); + return $customMenu; + } + + $css = $this->generateCss($customMenu['separators']); + $customMenu['separator_css'] = $css; + $customMenu['separator_css_modified'] = time(); + + return $customMenu; + } + + private function generateCss($settings) { + if ( empty($settings['customSettingsEnabled']) ) { + return ''; + } + + $css = $this->generateSeparatorTypeCss( + $settings['topLevelSeparators'], + '#adminmenumain #adminmenu li.wp-menu-separator .separator', + '#adminmenumain #adminmenu li.wp-menu-separator' + ); + + $css .= "\n" . '#adminmenumain #adminmenu .wp-submenu a.wp-menu-separator {' + . 'padding: 0;' + . 'margin: 0;' + . '}' . "\n"; + + $css .= $this->generateSeparatorTypeCss( + !empty($settings['useTopLevelSettingsForSubmenus']) + ? $settings['topLevelSeparators'] + : $settings['submenuSeparators'], + '#adminmenumain #adminmenu .wp-submenu .ws-submenu-separator', + '#adminmenumain #adminmenu .wp-submenu .ws-submenu-separator-wrap' + ); + + return $css; + } + + private function generateSeparatorTypeCss($settings, $nodeSelector, $parentSelector) { + $nodeSelector = trim($nodeSelector); + $parentSelector = trim($parentSelector); + + $shouldClearFloats = false; + + $parentLines = array( + 'height: auto', + 'margin: 0', + 'padding: 0', + 'width: 100%', + ); + $lines = array(); + + $separatorColor = 'transparent'; + if ( $settings['colorType'] !== 'transparent' ) { + $separatorColor = $settings['customColor']; + if ( $separatorColor === '' ) { + $separatorColor = 'transparent'; + } + } + + if ( $settings['borderStyle'] === 'solid' ) { + $lines[] = 'border: none'; + $lines[] = 'background-color: ' . $separatorColor; + $lines[] = 'height: ' . $settings['height'] . 'px'; + } else { + $lines[] = 'border-top-style: ' . $settings['borderStyle']; + + $lines[] = 'border-top-width: ' . $settings['height'] . 'px'; + $lines[] = 'height: 0'; + + $lines[] = 'border-color: ' . $separatorColor; + $lines[] = 'background: transparent'; + } + + if ( $settings['widthStrategy'] === 'percentage' ) { + $lines[] = 'width: ' . $settings['widthInPercent'] . '%'; + } else if ( $settings['widthStrategy'] === 'fixed' ) { + $lines[] = 'width: ' . $settings['widthInPixels'] . 'px'; + } + + $effectiveMargins = array( + 'top' => $settings['marginTop'] . 'px', + 'bottom' => $settings['marginBottom'] . 'px', + 'left' => $settings['marginLeft'] . 'px', + 'right' => $settings['marginRight'] . 'px', + ); + + if ( $settings['widthStrategy'] !== 'full' ) { + if ( $settings['alignment'] === 'center' ) { + $effectiveMargins['left'] = 'auto'; + $effectiveMargins['right'] = 'auto'; + } else if ( ($settings['alignment'] === 'left') || ($settings['alignment'] === 'right') ) { + $lines[] = 'float: ' . $settings['alignment']; + $shouldClearFloats = true; + } + } + + $lines[] = 'margin: ' . $effectiveMargins['top'] . ' ' . $effectiveMargins['right'] . ' ' + . $effectiveMargins['bottom'] . ' ' . $effectiveMargins['left']; + + $result = ( + $nodeSelector . " {\n" . implode(";", $lines) . ";}\n" + . $parentSelector . " {\n" . implode(";", $parentLines) . ";}\n" + ); + if ( $shouldClearFloats ) { + $result .= $parentSelector . '::after { content: ""; display: block; clear: both; height: 0; }'; + } + return $result; + } + + public function loadSeparatorSettings($menuConfig, $storedConfig) { + //Copy separator settings. + if ( isset($storedConfig['separators']) ) { + $menuConfig['separators'] = $storedConfig['separators']; + } + //Copy the pre-generated CSS. + if ( isset($storedConfig['separator_css']) && is_string($storedConfig['separator_css']) ) { + $menuConfig['separator_css'] = $storedConfig['separator_css']; + $menuConfig['separator_css_modified'] = isset($storedConfig['separator_css_modified']) + ? intval($storedConfig['separator_css_modified']) + : 0; + } + return $menuConfig; + } + + public function enqueueCustomSeparatorStyle() { + $customMenu = $this->menuEditor->load_custom_menu(); + if ( empty($customMenu) || empty($customMenu['separator_css']) ) { + return; + } + + wp_enqueue_style( + 'ame-custom-separator-styles', + add_query_arg( + 'ame_config_id', + $this->menuEditor->get_loaded_menu_config_id(), + admin_url('admin-ajax.php?action=' . urlencode(self::CSS_AJAX_ACTION)) + ), + array(), + $customMenu['separator_css_modified'] + ); + } + + public function ajaxOutputCss() { + $configId = null; + if ( isset($_GET['ame_config_id']) && !empty($_GET['ame_config_id']) ) { + $configId = (string)($_GET['ame_config_id']); + } + + $customMenu = $this->menuEditor->load_custom_menu($configId); + if ( empty($customMenu) || empty($customMenu['separator_css']) ) { + echo '/* No CSS found. */'; + return; + } + + $timestamp = $customMenu['separator_css_modified']; + //Support the If-Modified-Since header. + $omitResponseBody = false; + if ( isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && !empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) ) { + $threshold = strtotime((string)$_SERVER['HTTP_IF_MODIFIED_SINCE']); + if ( $timestamp <= $threshold ) { + header('HTTP/1.1 304 Not Modified'); + $omitResponseBody = true; + } + } + + //Enable browser caching. + header('Cache-Control: public, max-age=5184000'); + header('Last-Modified: ' . gmdate('D, d M Y H:i:s ', $timestamp) . 'GMT'); + if ( $omitResponseBody ) { + exit(); + } + + header('Content-Type: text/css'); + header('X-Content-Type-Options: nosniff'); + + echo $customMenu['separator_css']; + exit(); + } +} \ No newline at end of file diff --git a/extras/modules/separator-styles/separator-settings.css b/extras/modules/separator-styles/separator-settings.css new file mode 100644 index 0000000..da67e49 --- /dev/null +++ b/extras/modules/separator-styles/separator-settings.css @@ -0,0 +1,70 @@ +#ws-ame-separator-style-settings { + background-color: white; + padding-top: 0; } + #ws-ame-separator-style-settings input[type=number] { + width: 6em; } + #ws-ame-separator-style-settings .ame-sp-label-text { + display: inline-block; + min-width: 6em; } + #ws-ame-separator-style-settings .ame-sp-flexbox-break { + flex-basis: 100%; + height: 0; } + #ws-ame-separator-style-settings .wp-picker-container { + display: inline-block; } + #ws-ame-separator-style-settings .ame-small-tab-container { + list-style: none; + position: relative; + padding-left: 10px; + padding-top: 8px; + border-bottom: 1px solid #ddd; + background: #f9f9f9; + margin: 0 -8px 0.5em -8px; } + #ws-ame-separator-style-settings .ame-small-tab-container .ame-small-tab { + display: inline-block; + position: relative; + bottom: -1px; + margin: 0; + border: solid 1px transparent; } + #ws-ame-separator-style-settings .ame-small-tab-container .ame-small-tab a { + display: inline-block; + text-decoration: none; + padding: 5px 8px 6px 8px; } + #ws-ame-separator-style-settings .ame-small-tab-container .ame-small-tab.ame-active-tab { + border: 1px solid #ddd; + border-bottom-color: white; + background: white; } + #ws-ame-separator-style-settings .ame-small-tab-container .ame-small-tab.ame-active-tab a { + color: #444; } + #ws-ame-separator-style-settings .ame-separator-settings-container { + max-height: 500px; + overflow-y: auto; + padding-top: 0.5em; } + #ws-ame-separator-style-settings .ws_dialog_subpanel > p { + margin-top: 0; } + +#ame-separator-margins { + display: flex; + flex-wrap: wrap; + max-width: 800px; } + #ame-separator-margins .ame-sp-label-text { + min-width: 4em; } + #ame-separator-margins label { + margin-right: 2.5em; } + #ame-separator-margins input { + margin-bottom: 0.4em; } + +#ame-separator-border-styles .ame-sp-label-text { + min-width: 5em; } +#ame-separator-border-styles .ame-border-sample-container { + display: inline-block; + vertical-align: top; + min-height: 28px; } +#ame-separator-border-styles .ame-border-sample { + display: inline-block; + width: 14em; + border-top: 0.3em solid #444; } + +#ame-separator-width-options input[type=number] { + margin-top: 0.4em; } + +/*# sourceMappingURL=separator-settings.css.map */ diff --git a/extras/modules/separator-styles/separator-settings.css.map b/extras/modules/separator-styles/separator-settings.css.map new file mode 100644 index 0000000..db7398d --- /dev/null +++ b/extras/modules/separator-styles/separator-settings.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAEA,gCAAiC;EAChC,gBAAgB,EAAE,KAAK;EACvB,WAAW,EAAE,CAAC;EAEd,mDAAmB;IAClB,KAAK,EAAE,GAAG;EAGX,mDAAmB;IAClB,OAAO,EAAE,YAAY;IACrB,SAAS,EAAE,GAAG;EAGf,sDAAsB;IACrB,UAAU,EAAE,IAAI;IAChB,MAAM,EAAE,CAAC;EAGV,qDAAqB;IACpB,OAAO,EAAE,YAAY;EAGtB,yDAAyB;IAGxB,UAAU,EAAE,IAAI;IAChB,QAAQ,EAAE,QAAQ;IAClB,YAAY,EAAE,IAAI;IAClB,WAAW,EAAE,GAAG;IAEhB,aAAa,EAAE,cAAyB;IACxC,UAAU,EAAE,OAAO;IAGnB,MAAM,EAAE,iBAA2D;IAEnE,wEAAe;MACd,OAAO,EAAE,YAAY;MACrB,QAAQ,EAAE,QAAQ;MAClB,MAAM,EAAE,IAAI;MAEZ,MAAM,EAAE,CAAC;MACT,MAAM,EAAE,qBAAqB;MAE7B,0EAAE;QACD,OAAO,EAAE,YAAY;QACrB,eAAe,EAAE,IAAI;QACrB,OAAO,EAAE,eAAe;MAGzB,uFAAiB;QAChB,MAAM,EAAE,cAAyB;QACjC,mBAAmB,EAAE,KAAK;QAC1B,UAAU,EAAE,KAAK;QAEjB,yFAAE;UACD,KAAK,EAAE,IAAI;EAMf,kEAAkC;IACjC,UAAU,EAAE,KAAK;IACjB,UAAU,EAAE,IAAI;IAChB,WAAW,EAAE,KAAK;EAGnB,wDAAwB;IACvB,UAAU,EAAE,CAAC;;AAIf,sBAAuB;EACtB,OAAO,EAAE,IAAI;EACb,SAAS,EAAE,IAAI;EACf,SAAS,EAAE,KAAK;EAEhB,yCAAmB;IAClB,SAAS,EAAE,GAAG;EAGf,4BAAM;IACL,YAAY,EAAE,KAAK;EAGpB,4BAAM;IACL,aAAa,EAzFa,KAAK;;AA8FhC,+CAAmB;EAClB,SAAS,EAAE,GAAG;AAGf,yDAA6B;EAC5B,OAAO,EAAE,YAAY;EACrB,cAAc,EAAE,GAAG;EACnB,UAAU,EAAE,IAAI;AAGjB,+CAAmB;EAClB,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAI;EACX,UAAU,EAAE,gBAAgB;;AAK7B,+CAAmB;EAClB,UAAU,EAjHgB,KAAK", +"sources": ["separator-settings.scss"], +"names": [], +"file": "separator-settings.css" +} \ No newline at end of file diff --git a/extras/modules/separator-styles/separator-settings.js b/extras/modules/separator-styles/separator-settings.js new file mode 100644 index 0000000..43d74ee --- /dev/null +++ b/extras/modules/separator-styles/separator-settings.js @@ -0,0 +1,377 @@ +///<reference path="../../../js/jquery.d.ts"/> +///<reference path="../../../js/common.d.ts"/> +///<reference path="../../../js/lodash-3.10.d.ts"/> +ko.extenders['boundedInteger'] = function (target, options) { + if (options.minValue > options.maxValue) { + throw new Error('Minimum value must be smaller than the maximum value'); + } + //Create a writable computed observable to intercept writes to our observable. + var result = ko.pureComputed({ + read: target, + write: function (newValue) { + var current = target(), newInteger = parseInt(newValue); + if (isNaN(newInteger)) { + newInteger = current; + } + if (newInteger < options.minValue) { + newInteger = options.minValue; + } + if (newInteger > options.maxValue) { + newInteger = options.maxValue; + } + if (newInteger !== current) { + target(newInteger); + } + else { + //If the parsed number is the same as the current value but different from the new value, + //the new value is probably invalid or incorrectly formatted. Trigger a notification to update + //the input field with the old number. + if (String(newValue) !== String(newInteger)) { + target.notifySubscribers(current); + } + } + } + }).extend({ notify: 'always' }); + //Initialize with the current value. + result(target()); + return result; +}; +var AmeSeparatorTypeSettings = /** @class */ (function () { + function AmeSeparatorTypeSettings() { + this.defaults = { + alignment: 'none', + borderStyle: 'solid', + colorType: 'transparent', + customColor: '', + height: 5, + marginBottom: 6, + marginLeft: 0, + marginRight: 0, + marginTop: 0, + widthInPercent: 100, + widthInPixels: 160, + widthStrategy: 'full' + }; + this.colorType = ko.observable(this.defaults.colorType); + this.customColor = ko.observable(this.defaults.customColor); + this.borderStyle = ko.observable(this.defaults.borderStyle); + this.height = ko.observable(this.defaults.height).extend({ + boundedInteger: { minValue: 0, maxValue: 300 } + }); + this.widthStrategy = ko.observable(this.defaults.widthStrategy); + this.widthInPercent = ko.observable(this.defaults.widthInPercent).extend({ + boundedInteger: { minValue: 1, maxValue: 100 } + }); + this.widthInPixels = ko.observable(this.defaults.widthInPixels).extend({ + boundedInteger: { minValue: 1, maxValue: 300 } + }); + this.marginTop = ko.observable(this.defaults.marginTop).extend({ + boundedInteger: { minValue: 0, maxValue: 300 } + }); + this.marginBottom = ko.observable(this.defaults.marginBottom).extend({ + boundedInteger: { minValue: 0, maxValue: 300 } + }); + this.marginLeft = ko.observable(this.defaults.marginLeft).extend({ + boundedInteger: { minValue: 0, maxValue: 300 } + }); + this.marginRight = ko.observable(this.defaults.marginRight).extend({ + boundedInteger: { minValue: 0, maxValue: 300 } + }); + this.alignment = ko.observable(this.defaults.alignment); + } + AmeSeparatorTypeSettings.prototype.setAll = function (settings) { + var newSettings = wsAmeLodash.defaults({}, settings, this.defaults); + this.colorType(newSettings.colorType); + this.customColor(newSettings.customColor); + this.borderStyle(newSettings.borderStyle); + this.height(newSettings.height); + this.widthStrategy(newSettings.widthStrategy); + this.widthInPixels(newSettings.widthInPixels); + this.widthInPercent(newSettings.widthInPercent); + this.marginTop(newSettings.marginTop); + this.marginBottom(newSettings.marginBottom); + this.marginLeft(newSettings.marginLeft); + this.marginRight(newSettings.marginRight); + this.alignment(newSettings.alignment); + }; + AmeSeparatorTypeSettings.prototype.getAll = function () { + return { + colorType: this.colorType(), + customColor: this.customColor(), + borderStyle: this.borderStyle(), + height: this.height(), + widthStrategy: this.widthStrategy(), + widthInPercent: this.widthInPercent(), + widthInPixels: this.widthInPixels(), + marginBottom: this.marginBottom(), + marginLeft: this.marginLeft(), + marginRight: this.marginRight(), + marginTop: this.marginTop(), + alignment: this.alignment() + }; + }; + AmeSeparatorTypeSettings.prototype.resetToDefault = function () { + this.colorType(this.defaults.colorType); + this.customColor(this.defaults.customColor); + this.borderStyle(this.defaults.borderStyle); + this.height(this.defaults.height); + this.widthStrategy(this.defaults.widthStrategy); + this.widthInPixels(this.defaults.widthInPixels); + this.widthInPercent(this.defaults.widthInPercent); + this.marginTop(this.defaults.marginTop); + this.marginBottom(this.defaults.marginBottom); + this.marginLeft(this.defaults.marginLeft); + this.marginRight(this.defaults.marginRight); + this.alignment(this.defaults.alignment); + }; + return AmeSeparatorTypeSettings; +}()); +var AmeSeparatorSettingsScreen = /** @class */ (function () { + function AmeSeparatorSettingsScreen() { + var _this = this; + this.currentSavedSettings = null; + this.dialog = null; + this.customSettingsEnabled = ko.observable(false); + this.previewEnabled = ko.observable(true); + this.useTopLevelSettingsForSubmenus = ko.observable(false); + this.activeTab = ko.observable('top'); + this.topLevelSeparators = new AmeSeparatorTypeSettings(); + this.submenuSeparators = new AmeSeparatorTypeSettings(); + //As an aesthetic choice, the default margins of submenu separators shall match + //the default padding of submenu items. + this.submenuSeparators.marginTop(5); + this.submenuSeparators.marginBottom(5); + this.submenuSeparators.marginLeft(12); + this.submenuSeparators.marginRight(12); + this.currentTypeSettings = ko.computed(function () { + if (_this.activeTab() === 'top') { + return _this.topLevelSeparators; + } + else { + if (_this.useTopLevelSettingsForSubmenus()) { + return _this.topLevelSeparators; + } + else { + return _this.submenuSeparators; + } + } + }); + this.tabSettingsEnabled = ko.pureComputed(function () { + return (_this.activeTab() === 'top') || (!_this.useTopLevelSettingsForSubmenus()); + }); + this.isOpen = ko.observable(false); + this.previewCss = ko.pureComputed(function () { + if (!_this.previewEnabled() || !_this.isOpen()) { + return ''; + } + var css = _this.generatePreviewCss(_this.topLevelSeparators, '#adminmenu li.wp-menu-separator .separator', '#adminmenu li.wp-menu-separator'); + //Unlike top level separators, each submenu submenu separator is inside an <a> element that has some + //default styles. Let's get rid of those to ensure that the separator has the correct size with respect to + //its list item parent. + css += '\n#adminmenu .wp-submenu a.wp-menu-separator {' + + 'padding: 0 !important;' + + 'margin: 0 !important;' + + '}\n'; + css += '\n' + _this.generatePreviewCss(_this.useTopLevelSettingsForSubmenus() ? _this.topLevelSeparators : _this.submenuSeparators, '#adminmenu .wp-submenu .ws-submenu-separator', '#adminmenu .wp-submenu .ws-submenu-separator-wrap'); + return css; + }); + var previewStyleTag = jQuery('<style></style>').appendTo('head'); + this.previewCss.subscribe(function (css) { + previewStyleTag.text(css); + }); + } + // noinspection JSUnusedGlobalSymbols Is actually used in Knockout templates. + AmeSeparatorSettingsScreen.prototype.selectTab = function (tabId) { + if ((tabId === 'top') || (tabId === 'submenu')) { + this.activeTab(tabId); + } + return false; + }; + AmeSeparatorSettingsScreen.prototype.generatePreviewCss = function (settings, nodeSelector, parentSelector) { + nodeSelector = wsAmeLodash.trimRight(nodeSelector); + parentSelector = wsAmeLodash.trimRight(parentSelector); + var shouldClearFloats = false; + var parentLines = [ + 'height: auto', + 'margin: 0', + 'padding: 0', + 'width: 100%' + ]; + var lines = []; + var separatorColor = 'transparent'; + if (settings.colorType() !== 'transparent') { + separatorColor = settings.customColor(); + if (separatorColor === '') { + separatorColor = 'transparent'; + } + } + if (settings.borderStyle() === 'solid') { + lines.push('border: none'); + lines.push('background-color: ' + separatorColor); + lines.push('height: ' + settings.height() + 'px'); + } + else { + lines.push('border-top-style: ' + settings.borderStyle()); + lines.push('border-top-width: ' + settings.height() + 'px'); + lines.push('height: 0'); + lines.push('border-color: ' + separatorColor); + lines.push('background: transparent'); + } + if (settings.widthStrategy() === 'percentage') { + lines.push('width: ' + settings.widthInPercent() + '%'); + } + else if (settings.widthStrategy() === 'fixed') { + lines.push('width: ' + settings.widthInPixels() + 'px'); + } + var effectiveMargins = { + top: settings.marginTop() + 'px', + bottom: settings.marginBottom() + 'px', + left: settings.marginLeft() + 'px', + right: settings.marginRight() + 'px' + }; + //Alignment has no meaning for separators that take the full width of the container. Also, applying float + //would prevent the element from expanding and make it zero-width. So we apply alignment only to separators + //that have an explicitly specified width. + if (settings.widthStrategy() !== 'full') { + if (settings.alignment() === 'center') { + effectiveMargins.left = 'auto'; + effectiveMargins.right = 'auto'; + } + else if ((settings.alignment() === 'left') || (settings.alignment() === 'right')) { + lines.push('float: ' + settings.alignment()); + shouldClearFloats = true; + } + } + lines.push('margin: ' + effectiveMargins.top + ' ' + effectiveMargins.right + ' ' + + effectiveMargins.bottom + ' ' + effectiveMargins.left); + var result = (nodeSelector + ' {\n' + lines.join(' !important;\n') + ' !important;\n}\n' + + parentSelector + ' {\n' + parentLines.join(' !important;\n') + ' !important;\n}'); + if (shouldClearFloats) { + result += parentSelector + '::after { content: ""; display: block; clear: both; height: 0; }'; + } + return result; + }; + AmeSeparatorSettingsScreen.prototype.setSettings = function (settings) { + if (settings === null) { + this.applyDefaultSettings(); + return; + } + this.currentSavedSettings = wsAmeLodash.clone(settings, true); + this.topLevelSeparators.setAll(settings.topLevelSeparators); + this.submenuSeparators.setAll(settings.submenuSeparators); + this.useTopLevelSettingsForSubmenus(settings.useTopLevelSettingsForSubmenus); + this.customSettingsEnabled(settings.customSettingsEnabled); + }; + AmeSeparatorSettingsScreen.prototype.applyDefaultSettings = function () { + this.currentSavedSettings = null; + this.customSettingsEnabled(false); + this.previewEnabled(false); + this.useTopLevelSettingsForSubmenus(true); + this.topLevelSeparators.resetToDefault(); + this.submenuSeparators.resetToDefault(); + this.submenuSeparators.marginTop(5); + this.submenuSeparators.marginBottom(5); + this.submenuSeparators.marginLeft(12); + this.submenuSeparators.marginRight(12); + this.activeTab('top'); + }; + AmeSeparatorSettingsScreen.prototype.getConfirmedSettings = function () { + return this.currentSavedSettings; + }; + AmeSeparatorSettingsScreen.prototype.getDisplayedSettings = function () { + return { + topLevelSeparators: this.topLevelSeparators.getAll(), + submenuSeparators: this.submenuSeparators.getAll(), + useTopLevelSettingsForSubmenus: this.useTopLevelSettingsForSubmenus(), + customSettingsEnabled: this.customSettingsEnabled() + }; + }; + AmeSeparatorSettingsScreen.prototype.discardChanges = function () { + this.setSettings(this.currentSavedSettings); + }; + // noinspection JSUnusedGlobalSymbols + AmeSeparatorSettingsScreen.prototype.onConfirm = function () { + this.currentSavedSettings = this.getDisplayedSettings(); + if (this.dialog) { + this.dialog.dialog('close'); + } + }; + // noinspection JSUnusedGlobalSymbols + AmeSeparatorSettingsScreen.prototype.onCancel = function () { + this.discardChanges(); + if (this.dialog) { + this.dialog.dialog('close'); + } + }; + AmeSeparatorSettingsScreen.prototype.setDialog = function ($dialog) { + var _this = this; + this.dialog = $dialog; + $dialog.on('dialogopen', function () { + _this.isOpen(true); + }); + $dialog.on('dialogclose', function () { + _this.isOpen(false); + }); + }; + return AmeSeparatorSettingsScreen; +}()); +(function ($) { + var lastLoadedConfig = null; + var screen = null; + $(document) + .on('menuConfigurationLoaded.adminMenuEditor', function (event, menuConfiguration) { + //Load separator settings from the menu configuration. + if (typeof menuConfiguration['separators'] !== 'undefined') { + lastLoadedConfig = menuConfiguration['separators']; + } + else { + lastLoadedConfig = null; + } + if (screen) { + screen.setSettings(lastLoadedConfig); + } + }) + .on('getMenuConfiguration.adminMenuEditor', function (event, menuConfiguration) { + //Store separator settings in the menu configuration. + var settings = (screen !== null) ? screen.getConfirmedSettings() : lastLoadedConfig; + if (settings !== null) { + menuConfiguration['separators'] = settings; + } + else { + if (typeof menuConfiguration['separators'] !== 'undefined') { + delete menuConfiguration['separators']; + } + } + }); + jQuery(function ($) { + var separatorDialog = $('#ws-ame-separator-style-settings'); + var isDialogInitialized = false; + function initializeSeparatorDialog() { + screen = new AmeSeparatorSettingsScreen(); + if (lastLoadedConfig !== null) { + screen.setSettings(lastLoadedConfig); + } + separatorDialog.dialog({ + autoOpen: false, + closeText: ' ', + draggable: false, + modal: true, + minHeight: 400, + minWidth: 520 + }); + isDialogInitialized = true; + ko.applyBindings(screen, separatorDialog.get(0)); + screen.setDialog(separatorDialog); + } + $('#ws_edit_separator_styles').on('click', function () { + if (!isDialogInitialized) { + initializeSeparatorDialog(); + } + screen.discardChanges(); + separatorDialog.dialog('open'); + //Reset the scroll position. + separatorDialog.find('.ame-separator-settings-container').scrollTop(0); + }); + }); +})(jQuery); +//# sourceMappingURL=separator-settings.js.map \ No newline at end of file diff --git a/extras/modules/separator-styles/separator-settings.js.map b/extras/modules/separator-styles/separator-settings.js.map new file mode 100644 index 0000000..363ba86 --- /dev/null +++ b/extras/modules/separator-styles/separator-settings.js.map @@ -0,0 +1 @@ +{"version":3,"file":"separator-settings.js","sourceRoot":"","sources":["separator-settings.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/extras/modules/separator-styles/separator-settings.scss b/extras/modules/separator-styles/separator-settings.scss new file mode 100644 index 0000000..cadea44 --- /dev/null +++ b/extras/modules/separator-styles/separator-settings.scss @@ -0,0 +1,116 @@ +$numberInputVerticalMargin: 0.4em; + +#ws-ame-separator-style-settings { + background-color: white; + padding-top: 0; + + input[type=number] { + width: 6em; + } + + .ame-sp-label-text { + display: inline-block; + min-width: 6em; + } + + .ame-sp-flexbox-break { + flex-basis: 100%; + height: 0; + } + + .wp-picker-container { + display: inline-block; + } + + .ame-small-tab-container { + $tabBorderColor: #ddd; + + list-style: none; + position: relative; + padding-left: 10px; + padding-top: 8px; + + border-bottom: 1px solid $tabBorderColor; + background: #f9f9f9; + + $defaultDialogPadding: 8px; + margin: (0) (-$defaultDialogPadding) 0.5em (-$defaultDialogPadding); + + .ame-small-tab { + display: inline-block; + position: relative; + bottom: -1px; + + margin: 0; + border: solid 1px transparent; + + a { + display: inline-block; + text-decoration: none; + padding: 5px 8px 6px 8px; + } + + &.ame-active-tab { + border: 1px solid $tabBorderColor; + border-bottom-color: white; + background: white; + + a { + color: #444; + } + } + } + } + + .ame-separator-settings-container { + max-height: 500px; + overflow-y: auto; + padding-top: 0.5em; + } + + .ws_dialog_subpanel > p { + margin-top: 0; + } +} + +#ame-separator-margins { + display: flex; + flex-wrap: wrap; + max-width: 800px; + + .ame-sp-label-text { + min-width: 4em; + } + + label { + margin-right: 2.5em; + } + + input { + margin-bottom: $numberInputVerticalMargin; + } +} + +#ame-separator-border-styles { + .ame-sp-label-text { + min-width: 5em; + } + + .ame-border-sample-container { + display: inline-block; + vertical-align: top; + min-height: 28px; + } + + .ame-border-sample { + display: inline-block; + width: 14em; + border-top: 0.3em solid #444; + } +} + +#ame-separator-width-options { + input[type=number] { + margin-top: $numberInputVerticalMargin; + } +} \ No newline at end of file diff --git a/extras/modules/separator-styles/separator-settings.ts b/extras/modules/separator-styles/separator-settings.ts new file mode 100644 index 0000000..718b925 --- /dev/null +++ b/extras/modules/separator-styles/separator-settings.ts @@ -0,0 +1,517 @@ +///<reference path="../../../js/jquery.d.ts"/> +///<reference path="../../../js/common.d.ts"/> +///<reference path="../../../js/lodash-3.10.d.ts"/> + +declare var wsAmeLodash: _.LoDashStatic; + +ko.extenders['boundedInteger'] = function ( + target: KnockoutObservable<any>, + options: { minValue: number, maxValue: number } +) { + if (options.minValue > options.maxValue) { + throw new Error('Minimum value must be smaller than the maximum value'); + } + + //Create a writable computed observable to intercept writes to our observable. + let result = ko.pureComputed({ + read: target, + write: function (newValue) { + let current = target(), + newInteger = parseInt(newValue); + + if (isNaN(newInteger)) { + newInteger = current; + } + + if (newInteger < options.minValue) { + newInteger = options.minValue; + } + if (newInteger > options.maxValue) { + newInteger = options.maxValue; + } + + if (newInteger !== current) { + target(newInteger); + } else { + //If the parsed number is the same as the current value but different from the new value, + //the new value is probably invalid or incorrectly formatted. Trigger a notification to update + //the input field with the old number. + if (String(newValue) !== String(newInteger)) { + target.notifySubscribers(current); + } + } + } + }).extend({notify: 'always'}); + + //Initialize with the current value. + result(target()); + + return result; +} + +type AmeSeparatorColorType = 'transparent' | 'custom'; +type AmeSeparatorBorderStyle = 'solid' | 'dashed' | 'dotted' | 'double'; +type AmeSeparatorWidthOption = 'full' | 'percentage' | 'fixed'; +type AmeSeparatorAlignmentOption = 'none' | 'left' | 'center' | 'right'; + +interface AmePlainSeparatorTypeSettings { + colorType: AmeSeparatorColorType; + customColor: string; + + borderStyle: AmeSeparatorBorderStyle; + height: number; + widthStrategy: AmeSeparatorWidthOption; + widthInPercent: number; + widthInPixels: number; + + marginTop: number; + marginBottom: number; + marginLeft: number; + marginRight: number; + + alignment: AmeSeparatorAlignmentOption; +} + +interface AmePlainSeparatorSettings { + topLevelSeparators: AmePlainSeparatorTypeSettings; + submenuSeparators: AmePlainSeparatorTypeSettings; + useTopLevelSettingsForSubmenus: boolean; + customSettingsEnabled: boolean; +} + +class AmeSeparatorTypeSettings implements AmeObservablePropertiesOf<AmePlainSeparatorTypeSettings> { + private readonly defaults: AmePlainSeparatorTypeSettings = { + alignment: 'none', + borderStyle: 'solid', + colorType: 'transparent', + customColor: '', + height: 5, + marginBottom: 6, + marginLeft: 0, + marginRight: 0, + marginTop: 0, + widthInPercent: 100, + widthInPixels: 160, + widthStrategy: 'full' + } + + colorType: KnockoutObservable<AmePlainSeparatorTypeSettings["colorType"]>; + customColor: KnockoutObservable<AmePlainSeparatorTypeSettings["customColor"]>; + borderStyle: KnockoutObservable<AmePlainSeparatorTypeSettings["borderStyle"]>; + + height: KnockoutObservable<AmePlainSeparatorTypeSettings["height"]>; + widthStrategy: KnockoutObservable<AmePlainSeparatorTypeSettings["widthStrategy"]>; + widthInPercent: KnockoutObservable<AmePlainSeparatorTypeSettings["widthInPercent"]>; + widthInPixels: KnockoutObservable<AmePlainSeparatorTypeSettings["widthInPixels"]>; + + marginTop: KnockoutObservable<AmePlainSeparatorTypeSettings["marginTop"]>; + marginLeft: KnockoutObservable<AmePlainSeparatorTypeSettings["marginLeft"]>; + marginBottom: KnockoutObservable<AmePlainSeparatorTypeSettings["marginBottom"]>; + marginRight: KnockoutObservable<AmePlainSeparatorTypeSettings["marginRight"]>; + + alignment: KnockoutObservable<AmePlainSeparatorTypeSettings["alignment"]>; + + constructor() { + this.colorType = ko.observable(this.defaults.colorType); + this.customColor = ko.observable(this.defaults.customColor); + this.borderStyle = ko.observable(this.defaults.borderStyle); + + this.height = ko.observable(this.defaults.height).extend({ + boundedInteger: {minValue: 0, maxValue: 300} + }); + + this.widthStrategy = ko.observable(this.defaults.widthStrategy); + this.widthInPercent = ko.observable(this.defaults.widthInPercent).extend({ + boundedInteger: {minValue: 1, maxValue: 100} + }); + this.widthInPixels = ko.observable(this.defaults.widthInPixels).extend({ + boundedInteger: {minValue: 1, maxValue: 300} + }); + + this.marginTop = ko.observable(this.defaults.marginTop).extend({ + boundedInteger: {minValue: 0, maxValue: 300} + }); + this.marginBottom = ko.observable(this.defaults.marginBottom).extend({ + boundedInteger: {minValue: 0, maxValue: 300} + }); + this.marginLeft = ko.observable(this.defaults.marginLeft).extend({ + boundedInteger: {minValue: 0, maxValue: 300} + }); + this.marginRight = ko.observable(this.defaults.marginRight).extend({ + boundedInteger: {minValue: 0, maxValue: 300} + }); + + this.alignment = ko.observable(this.defaults.alignment); + } + + setAll(settings: AmePlainSeparatorTypeSettings) { + const newSettings: AmePlainSeparatorTypeSettings = wsAmeLodash.defaults({}, settings, this.defaults); + + this.colorType(newSettings.colorType); + this.customColor(newSettings.customColor); + this.borderStyle(newSettings.borderStyle); + + this.height(newSettings.height); + this.widthStrategy(newSettings.widthStrategy) + this.widthInPixels(newSettings.widthInPixels); + this.widthInPercent(newSettings.widthInPercent); + + this.marginTop(newSettings.marginTop); + this.marginBottom(newSettings.marginBottom); + this.marginLeft(newSettings.marginLeft); + this.marginRight(newSettings.marginRight); + + this.alignment(newSettings.alignment); + } + + getAll(): AmePlainSeparatorTypeSettings { + return { + colorType: this.colorType(), + customColor: this.customColor(), + borderStyle: this.borderStyle(), + + height: this.height(), + widthStrategy: this.widthStrategy(), + widthInPercent: this.widthInPercent(), + widthInPixels: this.widthInPixels(), + + marginBottom: this.marginBottom(), + marginLeft: this.marginLeft(), + marginRight: this.marginRight(), + marginTop: this.marginTop(), + + alignment: this.alignment() + }; + } + + resetToDefault() { + this.colorType(this.defaults.colorType); + this.customColor(this.defaults.customColor); + this.borderStyle(this.defaults.borderStyle); + + this.height(this.defaults.height); + this.widthStrategy(this.defaults.widthStrategy) + this.widthInPixels(this.defaults.widthInPixels); + this.widthInPercent(this.defaults.widthInPercent); + + this.marginTop(this.defaults.marginTop); + this.marginBottom(this.defaults.marginBottom); + this.marginLeft(this.defaults.marginLeft); + this.marginRight(this.defaults.marginRight); + + this.alignment(this.defaults.alignment); + } +} + +class AmeSeparatorSettingsScreen { + customSettingsEnabled: KnockoutObservable<boolean>; + previewEnabled: KnockoutObservable<boolean>; + + topLevelSeparators: AmeSeparatorTypeSettings; + submenuSeparators: AmeSeparatorTypeSettings; + useTopLevelSettingsForSubmenus: KnockoutObservable<boolean>; + + currentSavedSettings: AmePlainSeparatorSettings = null; + + currentTypeSettings: KnockoutComputed<AmeSeparatorTypeSettings>; + + activeTab: KnockoutObservable<'top' | 'submenu'>; + /** Are the settings in the currently active tab enabled? */ + tabSettingsEnabled: KnockoutObservable<boolean>; + + previewCss: KnockoutObservable<string>; + + dialog: JQuery = null; + isOpen: KnockoutObservable<boolean>; + + constructor() { + this.customSettingsEnabled = ko.observable(false); + this.previewEnabled = ko.observable(true); + this.useTopLevelSettingsForSubmenus = ko.observable(false); + + this.activeTab = ko.observable('top'); + + this.topLevelSeparators = new AmeSeparatorTypeSettings(); + + this.submenuSeparators = new AmeSeparatorTypeSettings(); + //As an aesthetic choice, the default margins of submenu separators shall match + //the default padding of submenu items. + this.submenuSeparators.marginTop(5); + this.submenuSeparators.marginBottom(5); + this.submenuSeparators.marginLeft(12); + this.submenuSeparators.marginRight(12); + + this.currentTypeSettings = ko.computed(() => { + if (this.activeTab() === 'top') { + return this.topLevelSeparators; + } else { + if (this.useTopLevelSettingsForSubmenus()) { + return this.topLevelSeparators; + } else { + return this.submenuSeparators; + } + } + }); + + this.tabSettingsEnabled = ko.pureComputed(() => { + return (this.activeTab() === 'top') || (!this.useTopLevelSettingsForSubmenus()); + }); + + this.isOpen = ko.observable(false); + this.previewCss = ko.pureComputed(() => { + if (!this.previewEnabled() || !this.isOpen()) { + return ''; + } + + let css = this.generatePreviewCss( + this.topLevelSeparators, + '#adminmenu li.wp-menu-separator .separator', + '#adminmenu li.wp-menu-separator' + ); + + //Unlike top level separators, each submenu submenu separator is inside an <a> element that has some + //default styles. Let's get rid of those to ensure that the separator has the correct size with respect to + //its list item parent. + css += '\n#adminmenu .wp-submenu a.wp-menu-separator {' + + 'padding: 0 !important;' + + 'margin: 0 !important;' + + '}\n'; + + css += '\n' + this.generatePreviewCss( + this.useTopLevelSettingsForSubmenus() ? this.topLevelSeparators : this.submenuSeparators, + '#adminmenu .wp-submenu .ws-submenu-separator', + '#adminmenu .wp-submenu .ws-submenu-separator-wrap' + ); + + return css; + }); + + let previewStyleTag = jQuery('<style></style>').appendTo('head'); + this.previewCss.subscribe((css) => { + previewStyleTag.text(css); + }); + } + + // noinspection JSUnusedGlobalSymbols Is actually used in Knockout templates. + selectTab(tabId: string) { + if ((tabId === 'top') || (tabId === 'submenu')) { + this.activeTab(tabId); + } + return false; + } + + generatePreviewCss(settings: AmeSeparatorTypeSettings, nodeSelector: string, parentSelector: string): string { + nodeSelector = wsAmeLodash.trimRight(nodeSelector); + parentSelector = wsAmeLodash.trimRight(parentSelector); + + let shouldClearFloats = false; + + let parentLines = [ + 'height: auto', + 'margin: 0', + 'padding: 0', + 'width: 100%' + ]; + let lines = []; + + let separatorColor = 'transparent'; + if (settings.colorType() !== 'transparent') { + separatorColor = settings.customColor(); + if (separatorColor === '') { + separatorColor = 'transparent'; + } + } + + if (settings.borderStyle() === 'solid') { + lines.push('border: none'); + lines.push('background-color: ' + separatorColor); + lines.push('height: ' + settings.height() + 'px'); + } else { + lines.push('border-top-style: ' + settings.borderStyle()); + + lines.push('border-top-width: ' + settings.height() + 'px'); + lines.push('height: 0'); + + lines.push('border-color: ' + separatorColor); + lines.push('background: transparent'); + } + + if (settings.widthStrategy() === 'percentage') { + lines.push('width: ' + settings.widthInPercent() + '%'); + } else if (settings.widthStrategy() === 'fixed') { + lines.push('width: ' + settings.widthInPixels() + 'px'); + } + + let effectiveMargins = { + top: settings.marginTop() + 'px', + bottom: settings.marginBottom() + 'px', + left: settings.marginLeft() + 'px', + right: settings.marginRight() + 'px' + }; + + //Alignment has no meaning for separators that take the full width of the container. Also, applying float + //would prevent the element from expanding and make it zero-width. So we apply alignment only to separators + //that have an explicitly specified width. + if (settings.widthStrategy() !== 'full') { + if (settings.alignment() === 'center') { + effectiveMargins.left = 'auto'; + effectiveMargins.right = 'auto'; + } else if ((settings.alignment() === 'left') || (settings.alignment() === 'right')) { + lines.push('float: ' + settings.alignment()); + shouldClearFloats = true; + } + } + + lines.push('margin: ' + effectiveMargins.top + ' ' + effectiveMargins.right + ' ' + + effectiveMargins.bottom + ' ' + effectiveMargins.left); + + let result = ( + nodeSelector + ' {\n' + lines.join(' !important;\n') + ' !important;\n}\n' + + parentSelector + ' {\n' + parentLines.join(' !important;\n') + ' !important;\n}' + ); + if (shouldClearFloats) { + result += parentSelector + '::after { content: ""; display: block; clear: both; height: 0; }'; + } + return result; + } + + setSettings(settings: AmePlainSeparatorSettings) { + if (settings === null) { + this.applyDefaultSettings(); + return; + } + + this.currentSavedSettings = wsAmeLodash.clone(settings, true); + + this.topLevelSeparators.setAll(settings.topLevelSeparators); + this.submenuSeparators.setAll(settings.submenuSeparators); + this.useTopLevelSettingsForSubmenus(settings.useTopLevelSettingsForSubmenus); + this.customSettingsEnabled(settings.customSettingsEnabled); + } + + private applyDefaultSettings() { + this.currentSavedSettings = null; + + this.customSettingsEnabled(false); + this.previewEnabled(false); + this.useTopLevelSettingsForSubmenus(true); + + this.topLevelSeparators.resetToDefault(); + this.submenuSeparators.resetToDefault(); + + this.submenuSeparators.marginTop(5); + this.submenuSeparators.marginBottom(5); + this.submenuSeparators.marginLeft(12); + this.submenuSeparators.marginRight(12); + + this.activeTab('top'); + } + + getConfirmedSettings(): AmePlainSeparatorSettings { + return this.currentSavedSettings; + } + + getDisplayedSettings(): AmePlainSeparatorSettings { + return { + topLevelSeparators: this.topLevelSeparators.getAll(), + submenuSeparators: this.submenuSeparators.getAll(), + useTopLevelSettingsForSubmenus: this.useTopLevelSettingsForSubmenus(), + customSettingsEnabled: this.customSettingsEnabled() + }; + } + + discardChanges() { + this.setSettings(this.currentSavedSettings); + } + + // noinspection JSUnusedGlobalSymbols + onConfirm() { + this.currentSavedSettings = this.getDisplayedSettings(); + if (this.dialog) { + this.dialog.dialog('close'); + } + } + + // noinspection JSUnusedGlobalSymbols + onCancel() { + this.discardChanges(); + if (this.dialog) { + this.dialog.dialog('close'); + } + } + + setDialog($dialog: JQuery) { + this.dialog = $dialog; + $dialog.on('dialogopen', () => { + this.isOpen(true); + }); + $dialog.on('dialogclose', () => { + this.isOpen(false); + }); + } +} + +(function ($) { + let lastLoadedConfig = null; + let screen: AmeSeparatorSettingsScreen = null; + + $(document) + .on('menuConfigurationLoaded.adminMenuEditor', function (event, menuConfiguration) { + //Load separator settings from the menu configuration. + if (typeof menuConfiguration['separators'] !== 'undefined') { + lastLoadedConfig = menuConfiguration['separators']; + } else { + lastLoadedConfig = null; + } + if (screen) { + screen.setSettings(lastLoadedConfig); + } + }) + .on('getMenuConfiguration.adminMenuEditor', function (event, menuConfiguration) { + //Store separator settings in the menu configuration. + const settings = (screen !== null) ? screen.getConfirmedSettings() : lastLoadedConfig; + if (settings !== null) { + menuConfiguration['separators'] = settings; + } else { + if (typeof menuConfiguration['separators'] !== 'undefined') { + delete menuConfiguration['separators']; + } + } + }); + + jQuery(function ($) { + const separatorDialog = $('#ws-ame-separator-style-settings'); + let isDialogInitialized = false; + + function initializeSeparatorDialog() { + screen = new AmeSeparatorSettingsScreen(); + if (lastLoadedConfig !== null) { + screen.setSettings(lastLoadedConfig); + } + + separatorDialog.dialog({ + autoOpen: false, + closeText: ' ', + draggable: false, + modal: true, + minHeight: 400, + minWidth: 520 + }); + isDialogInitialized = true; + + ko.applyBindings(screen, separatorDialog.get(0)); + screen.setDialog(separatorDialog); + } + + $('#ws_edit_separator_styles').on('click', function () { + if (!isDialogInitialized) { + initializeSeparatorDialog(); + } + screen.discardChanges(); + separatorDialog.dialog('open'); + + //Reset the scroll position. + separatorDialog.find('.ame-separator-settings-container').scrollTop(0); + }); + }); +})(jQuery); diff --git a/extras/modules/separator-styles/separator-styles-template.php b/extras/modules/separator-styles/separator-styles-template.php new file mode 100644 index 0000000..3dab85b --- /dev/null +++ b/extras/modules/separator-styles/separator-styles-template.php @@ -0,0 +1,188 @@ +<?php +//This line reserved for future use. +?> +<div id="ws-ame-separator-style-settings" title="Menu Separators" style="display: none;"> + <div class="ws_dialog_panel"> + <ul class="ame-small-tab-container"> + <li class="ame-small-tab ame-active-tab" data-bind="css: {'ame-active-tab': (activeTab() === 'top')}"> + <!--suppress HtmlUnknownAnchorTarget --> + <a href="#topmenu-separators" data-bind="click: selectTab.bind($root, 'top')">Top Level Separators</a> + </li> + <li class="ame-small-tab" data-bind="css: {'ame-active-tab': (activeTab() === 'submenu')}"> + <!--suppress HtmlUnknownAnchorTarget --> + <a href="#submenu-separators" data-bind="click: selectTab.bind($root, 'submenu')">Submenu Separators</a> + </li> + </ul> + + <div class="ame-separator-settings-container"> + <div class="ws_dialog_subpanel" data-bind="visible: activeTab() === 'top'"> + <fieldset> + <p> + <label> + <input type="checkbox" data-bind="checked: customSettingsEnabled"> + Use custom separator styles + </label> + </p> + <p><label><input type="checkbox" data-bind="checked: previewEnabled"> Live preview</label></p> + </fieldset> + </div> + + <div class="ws_dialog_subpanel" data-bind="visible: activeTab() === 'submenu'" style="display: none;"> + <p> + <label> + <input type="checkbox" data-bind="checked: useTopLevelSettingsForSubmenus"> + Use the same settings as top level separators + </label> + </p> + </div> + + <div class="ws_dialog_subpanel"> + <h3 class="ws-ame-dialog-subheading">Color</h3> + <fieldset data-bind="enable: tabSettingsEnabled"> + <p> + <label> + <input type="radio" name="ame-separator-color-type" value="transparent" + data-bind="checked: currentTypeSettings().colorType"> + Transparent + </label> + </p> + <p> + <label> + <input type="radio" name="ame-separator-color-type" value="custom" + data-bind="checked: currentTypeSettings().colorType"> + <span class="ame-sp-label-text">Custom</span> + </label> + <label for="ame-custom-separator-color" class="hidden">Custom separator color</label> + <input type="text" id="ame-custom-separator-color" + data-bind="ameColorPicker: currentTypeSettings().customColor"> + </p> + + </fieldset> + </div> + + <div class="ws_dialog_subpanel"> + <h3 class="ws-ame-dialog-subheading">Line style</h3> + <fieldset id="ame-separator-border-styles" data-bind="enable: tabSettingsEnabled"> + <?php + $styleOptions = array( + 'solid' => 'Solid', + 'dashed' => 'Dashed', + 'double' => 'Double', + 'dotted' => 'Dotted', + ); + foreach ($styleOptions as $style => $label): + ?> + <p> + <label> + <input type="radio" name="ame-separator-style" value="<?php echo esc_attr($style); ?>" + data-bind="checked: currentTypeSettings().borderStyle"> + <span class="ame-sp-label-text"><?php echo $label; ?></span> + <span class="ame-border-sample-container"> + <span class="ame-border-sample" + style="border-top-style: <?php echo esc_attr($style); ?>"></span> + </span> + </label> + </p> + <?php + endforeach; + ?> + </fieldset> + </div> + + <div class="ws_dialog_subpanel"> + <h3 class="ws-ame-dialog-subheading"><label for="ws-ame-separator-height">Height</label></h3> + <input type="number" id="ws-ame-separator-height" min="1" max="300" + data-bind="value: currentTypeSettings().height, enable: tabSettingsEnabled"> px + </div> + + <div class="ws_dialog_subpanel" id="ame-separator-width-options"> + <h3 class="ws-ame-dialog-subheading">Width</h3> + <fieldset data-bind="enable: tabSettingsEnabled"> + <p> + <label> + <input type="radio" name="ame-separator-width" value="full" + data-bind="checked: currentTypeSettings().widthStrategy"> + Full width + </label> + </p> + <p> + <label> + <input type="radio" name="ame-separator-width" value="percentage" + data-bind="checked: currentTypeSettings().widthStrategy"> + <span class="ame-sp-label-text">Percentage</span> + </label> + <label for="ws-ame-separator-width-pct" class="hidden"> + Separator width as a percentage of menu width + </label> + <input type="number" id="ws-ame-separator-width-pct" min="1" max="100" + data-bind="value: currentTypeSettings().widthInPercent, + enable: (currentTypeSettings().widthStrategy() === 'percentage')"> % + </p> + <p> + <label> + <input type="radio" name="ame-separator-width" value="fixed" + data-bind="checked: currentTypeSettings().widthStrategy"> + <span class="ame-sp-label-text">Fixed width</span> + </label> + <label for="ws-ame-separator-width-px" class="hidden">Separator width in pixels</label> + <input type="number" id="ws-ame-separator-width-px" min="1" max="300" + data-bind="value: currentTypeSettings().widthInPixels, + enable: (currentTypeSettings().widthStrategy() === 'fixed')"> px + </p> + </fieldset> + </div> + + <div class="ws_dialog_subpanel"> + <h3 class="ws-ame-dialog-subheading">Margins</h3> + <fieldset id="ame-separator-margins" data-bind="enable: tabSettingsEnabled"> + <label> + <span class="ame-sp-label-text">Top:</span> + <input type="number" min="0" max="300" data-bind="value: currentTypeSettings().marginTop"> px + </label> + <label> + <span class="ame-sp-label-text">Bottom:</span> + <input type="number" min="0" max="300" data-bind="value: currentTypeSettings().marginBottom"> px + </label> + <div class="ame-sp-flexbox-break"></div> + <label> + <span class="ame-sp-label-text">Left:</span> + <input type="number" min="0" max="300" data-bind="value: currentTypeSettings().marginLeft"> px + </label> + <label> + <span class="ame-sp-label-text">Right:</span> + <input type="number" min="0" max="300" data-bind="value: currentTypeSettings().marginRight"> px + </label> + </fieldset> + </div> + + <div class="ws_dialog_subpanel"> + <h3 class="ws-ame-dialog-subheading">Alignment</h3> + <fieldset class="ws-ame-icon-radio-button-group" data-bind="enable: tabSettingsEnabled"> + <?php + $options = array( + 'none' => array('title' => 'None', 'icon' => 'dashicons-editor-justify'), + 'left' => array('title' => 'Left', 'icon' => 'dashicons-editor-alignleft'), + 'center' => array('title' => 'Center', 'icon' => 'dashicons-editor-aligncenter'), + 'right' => array('title' => 'Right', 'icon' => 'dashicons-editor-alignright'), + ); + foreach ($options as $key => $properties): + ?> + <label title="<?php echo esc_attr($properties['title']); ?>"> + <input type="radio" name="ame-separator-alignment" value="<?php echo esc_attr($key); ?>" + data-bind="checked: currentTypeSettings().alignment"> + <span class="dashicons <?php echo esc_attr($properties['icon']); ?>"></span> + </label> + <?php + endforeach; + ?> + </fieldset> + </div> + </div> + </div> + + <div class="ws_dialog_buttons"> + <input type="button" class="button-primary" value="Save Changes" id="ws_save_separator_settings" + data-bind="click: onConfirm.bind($root)"> + <input type="button" class="button ws_close_dialog" value="Cancel" data-bind="click: onCancel.bind($root)"> + </div> +</div> diff --git a/extras/modules/super-users/super-users-template.php b/extras/modules/super-users/super-users-template.php new file mode 100644 index 0000000..8cfa77c --- /dev/null +++ b/extras/modules/super-users/super-users-template.php @@ -0,0 +1,101 @@ +<?php +/** + * Variables set by ameModule when it outputs a template. + * + * @var string $moduleTabUrl + * @see ameModule::getTabUrl + */ +?> +<div id="ame-super-user-settings"> + <h3> + Hidden Users + <a class="page-title-action" href="#" + data-bind="click: $root.selectHiddenUsers.bind($root), text: addButtonText">Add</a> + </h3> + + <table class="wp-list-table widefat fixed striped"> + <thead> + <tr> + <th scope="col">Username</th> + <th scope="col">Name</th> + <th scope="col">Role</th> + <th class="ame-column-user-id num" scope="col">ID</th> + </tr> + </thead> + + <!-- ko if: (superUsers().length > 0) --> + <tbody data-bind="foreach: superUsers"> + <tr> + <td class="column-username"> + <span data-bind="html: avatarHTML"></span> + <strong><a data-bind="text: userLogin, attr: {href: $root.getEditLink($data)}"></a></strong> + + <div class="row-actions"> + <span><a href="#" data-bind="click: $root.removeUser.bind($root, $data)">Remove</a></span> + </div> + </td> + <td data-bind="text: displayName"></td> + <td data-bind="text: $root.formatUserRoles($data)"></td> + <td data-bind="text: userId" class="num"></td> + </tr> + </tbody> + <!-- /ko --> + + <!-- ko if: (superUsers().length <= 0) --> + <tbody> + <tr> + <td colspan="4"> + No users selected. Click "<span data-bind="text: addButtonText"></span>" to hide one or more users. + </td> + </tr> + </tbody> + <!-- /ko --> + + <tfoot> + <tr> + <th>Username</th> + <th>Name</th> + <th>Role</th> + <th class="ame-column-user-id num">ID</th> + </tr> + </tfoot> + </table> + + <form action="<?php echo esc_attr(add_query_arg('noheader', 1, $moduleTabUrl)); ?>" method="post"> + <input type="hidden" name="settings" value="" data-bind="value: settingsData"> + <input type="hidden" name="action" value="ame_save_super_users"> + <?php + wp_nonce_field('ame_save_super_users'); + submit_button('Save Changes', 'primary', 'submit', true); + ?> + </form> + + <div class="metabox-holder"> + <div class="postbox ws_ame_doc_box" data-bind="css: {closed: !isInfoBoxOpen()}"> + <button type="button" class="handlediv button-link" data-bind="click: toggleInfoBox.bind($root)"> + <span class="toggle-indicator"></span> + </button> + <h2 class="hndle" data-bind="click: toggleInfoBox.bind($root)">How It Works</h2> + <div class="inside"> + <ul> + <li>Hidden users don't show up + on the <a href="<?php echo esc_attr(self_admin_url('users.php')); ?>">Users → All Users</a> + page. + </li> + <li>They can't be edited or deleted by normal users.</li> + <li>However, they still show up in other places like the "Author" column on the "Posts" page, and + their posts and comments are not specially protected. + </li> + <li>Hidden users can see other hidden users. + <ul> + <li>So if you hide your own user account, you will still see it under "All Users" + unless you switch to another user.</li> + </ul> + </li> + </ul> + + </div> + </div> + </div> + +</div> \ No newline at end of file diff --git a/extras/modules/super-users/super-users.css b/extras/modules/super-users/super-users.css new file mode 100644 index 0000000..830eaf1 --- /dev/null +++ b/extras/modules/super-users/super-users.css @@ -0,0 +1,4 @@ +.ame-column-user-id { + width: 70px; } + +/*# sourceMappingURL=super-users.css.map */ diff --git a/extras/modules/super-users/super-users.css.map b/extras/modules/super-users/super-users.css.map new file mode 100644 index 0000000..f5d5d7d --- /dev/null +++ b/extras/modules/super-users/super-users.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAAA,mBAAoB;EACnB,KAAK,EAAE,IAAI", +"sources": ["super-users.scss"], +"names": [], +"file": "super-users.css" +} \ No newline at end of file diff --git a/extras/modules/super-users/super-users.js b/extras/modules/super-users/super-users.js new file mode 100644 index 0000000..96692a8 --- /dev/null +++ b/extras/modules/super-users/super-users.js @@ -0,0 +1,88 @@ +/// <reference path="../../../js/knockout.d.ts" /> +/// <reference path="../../../js/jquery.d.ts" /> +/// <reference path="../../../js/jquery.biscuit.d.ts" /> +/// <reference path="../../../js/lodash-3.10.d.ts" /> +/// <reference path="../../../modules/actor-selector/actor-selector.ts" /> +var AmeSuperUsers = /** @class */ (function () { + function AmeSuperUsers(settings) { + var _this = this; + this.addButtonText = 'Add User'; + this.userEditUrl = settings.userEditUrl; + this.currentUserLogin = settings.currentUserLogin; + this.superUsers = ko.observableArray([]); + AmeSuperUsers._.forEach(settings.superUsers, function (userDetails) { + var user = AmeUser.createFromProperties(userDetails); + if (!AmeActors.getUser(user.userLogin)) { + AmeActors.addUsers([user]); + } + _this.superUsers.push(user); + }); + this.superUsers.sort(AmeSuperUsers.compareLogins); + this.settingsData = ko.computed(function () { + return AmeSuperUsers._.map(_this.superUsers(), 'userId').join(','); + }); + //Store the state of the info box in a cookie. + var initialState = jQuery.cookie('ame_su_info_box_open'); + var _isBoxOpen = ko.observable((typeof initialState === 'undefined') ? true : (initialState === '1')); + this.isInfoBoxOpen = ko.computed({ + read: function () { + return _isBoxOpen(); + }, + write: function (value) { + jQuery.cookie('ame_su_info_box_open', value ? '1' : '0', { expires: 90 }); + _isBoxOpen(value); + } + }); + } + AmeSuperUsers.prototype.removeUser = function (user) { + this.superUsers.remove(user); + }; + AmeSuperUsers.prototype.getEditLink = function (user) { + return this.userEditUrl + '?user_id=' + user.userId; + }; + AmeSuperUsers.prototype.selectHiddenUsers = function () { + var _this = this; + AmeSelectUsersDialog.open({ + selectedUsers: AmeSuperUsers._.map(this.superUsers(), 'userLogin'), + users: AmeSuperUsers._.indexBy(this.superUsers(), 'userLogin'), + actorManager: AmeActors, + currentUserLogin: this.currentUserLogin, + alwaysIncludeCurrentUser: false, + save: function (selectedUsers) { + selectedUsers.sort(AmeSuperUsers.compareLogins); + _this.superUsers(selectedUsers); + } + }); + }; + AmeSuperUsers.compareLogins = function (a, b) { + if (a.userLogin > b.userLogin) { + return 1; + } + else if (a.userLogin < b.userLogin) { + return -1; + } + return 0; + }; + AmeSuperUsers.prototype.formatUserRoles = function (user) { + var displayNames = AmeSuperUsers._.map(user.roles, function (roleId) { + var actor = AmeActors.getActor('role:' + roleId); + if (actor) { + return actor.displayName; + } + else { + return '[Unknown role]'; + } + }); + return displayNames.join(', '); + }; + AmeSuperUsers.prototype.toggleInfoBox = function () { + this.isInfoBoxOpen(!this.isInfoBoxOpen()); + }; + AmeSuperUsers._ = wsAmeLodash; + return AmeSuperUsers; +}()); +jQuery(function () { + var superUserVM = new AmeSuperUsers(wsAmeSuperUserSettings); + ko.applyBindings(superUserVM, document.getElementById('ame-super-user-settings')); +}); +//# sourceMappingURL=super-users.js.map \ No newline at end of file diff --git a/extras/modules/super-users/super-users.js.map b/extras/modules/super-users/super-users.js.map new file mode 100644 index 0000000..9f9285a --- /dev/null +++ b/extras/modules/super-users/super-users.js.map @@ -0,0 +1 @@ +{"version":3,"file":"super-users.js","sourceRoot":"","sources":["super-users.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/extras/modules/super-users/super-users.php b/extras/modules/super-users/super-users.php new file mode 100644 index 0000000..dd3c91f --- /dev/null +++ b/extras/modules/super-users/super-users.php @@ -0,0 +1,243 @@ +<?php + +class ameSuperUsers extends ameModule { + protected $tabSlug = 'hidden-users'; + protected $tabTitle = 'Users'; + + private $isInsideCountFilter = false; + + public function __construct($menuEditor) { + parent::__construct($menuEditor); + + add_filter('users_list_table_query_args', array($this, 'filterUserQueryArgs')); + add_filter('map_meta_cap', array($this, 'restrictUserEditing'), 10, 4); + add_filter('pre_count_users', array($this, 'filterUserCounts'), 10, 3); + + /* + * Why not use the "pre_get_users" filter to hide users? + * + * This filter is called in WP_User_Query and, by extension, get_users(). The problem is that + * WordPress uses get_users() in at least one place where hiding specific users could cause + * problems. In edit-form-advanced.php, WordPress calls get_users() to determine if it should + * check if another user is already editing the current post. It only checks locks if get_users() + * returns more than one user. This means that removing users from get_users() results could + * make WordPress ignore post locks. + */ + + add_action('admin_menu_editor-header', array($this, 'handleFormSubmission'), 10, 2); + } + + public function filterUserQueryArgs($args) { + //Exclude superusers if the current user is not a superuser. + $superUsers = $this->getSuperUserIDs(); + if ( empty($superUsers) ) { + return $args; + } + + if ( !$this->isCurrentUserSuper() ) { + $args['exclude'] = array_merge( + isset($args['exclude']) ? $args['exclude'] : array(), + $superUsers + ); + + //Exclude hidden users even if specifically included. This can happen + //when looking at the "None" view on the "Users" page (this view shows + //users that have no role on the current site). + if ( isset($args['include']) && !empty($args['include']) ) { + $args['include'] = array_diff($args['include'], $superUsers); + if ( empty($args['include']) ) { + unset($args['include']); + } + } + } + + return $args; + } + + /** + * Prevent normal users from editing superusers. + * + * @param string[] $requiredCaps List of primitive capabilities (output). + * @param string $capability The meta capability (input). + * @param int $thisUserId The user that's trying to do something. + * @param array $args + * @return string[] + */ + public function restrictUserEditing($requiredCaps, $capability, $thisUserId, $args) { + static $editUserCaps = array('edit_user', 'delete_user', 'promote_user', 'remove_user'); + if ( !in_array($capability, $editUserCaps) || !isset($args[0]) ) { + return $requiredCaps; + } + + /** @var int The user that might be edited or deleted. */ + $targetUserId = intval($args[0]); + $thisUserId = intval($thisUserId); + + if ( $this->isSuperUser($targetUserId) && !$this->isSuperUser($thisUserId) ) { + return array_merge($requiredCaps, array('do_not_allow')); + } + + return $requiredCaps; + } + + /** + * Filter the user counts shown in the list of roles at the top of the "Users" page. + * + * @param $result + * @param $strategy + * @param $siteId + * @return array|null + */ + public function filterUserCounts($result = null, $strategy = 'time', $siteId = null) { + //We're going to call count_users() which will trigger the 'pre_count_users' filter + //again, so we need to avoid infinite recursion. + if ( $this->isInsideCountFilter ) { + return $result; + } + + //Perform this filtering only on the "Users" page. + if ( !isset($GLOBALS['parent_file']) || ($GLOBALS['parent_file'] !== 'users.php') ) { + return $result; + } + + if ( $this->isCurrentUserSuper() ) { + //This user can see other hidden users. + return $result; + } + + $superUsers = $this->getSuperUsers($siteId); + //Note the $siteId. We want the roles that each user has on the specified site. + //This should normally be the current site, but it doesn't have to be. + + if ( empty($superUsers) ) { + //There are no users that need to be hidden. + return $result; + } + + /** @noinspection PhpFieldImmediatelyRewrittenInspection Recursive filters! */ + $this->isInsideCountFilter = true; + $result = count_users($strategy, $siteId); + + //Adjust the total number of users. + $result['total_users'] -= count($superUsers); + + //For each hidden user, subtract one from each of the roles that the user has. + foreach ($superUsers as $user) { + if ( !empty($user->roles) && is_array($user->roles) ) { + foreach ($user->roles as $roleId) { + if ( isset($result['avail_roles'][$roleId]) ) { + $result['avail_roles'][$roleId]--; + if ( $result['avail_roles'][$roleId] <= 0 ) { + unset($result['avail_roles'][$roleId]); + } + } + } + } else if ( isset($result['avail_roles']['none']) ) { + $result['avail_roles']['none']--; + } + } + + $this->isInsideCountFilter = false; + return $result; + } + + /** + * @return int[] + */ + private function getSuperUserIDs() { + $result = $this->menuEditor->get_plugin_option('super_users'); + if ( $result === null ) { + return array(); + } + return $result; + } + + /** + * @param int|null $siteId + * @return WP_User[] + */ + private function getSuperUsers($siteId = null) { + $ids = $this->getSuperUserIDs(); + if ( empty($ids) ) { + return array(); + } + + if ( !is_numeric($siteId) ) { + $siteId = get_current_blog_id(); + } + + //Caution: If you pass an empty array as "include", get_users() will return *all* users from the current site. + return get_users( array( + 'include' => $ids, + 'blog_id' => $siteId, + ) ); + } + + /** + * Is the current user one of the superusers? + * + * @return bool + */ + private function isCurrentUserSuper() { + $user = wp_get_current_user(); + return $user && $this->isSuperUser($user->ID); + } + + private function isSuperUser($userId) { + return in_array($userId, $this->getSuperUserIDs()); + } + + public function enqueueTabScripts() { + parent::enqueueTabScripts(); + + wp_enqueue_auto_versioned_script( + 'ame-super-users', + plugins_url('super-users.js', __FILE__), + array('knockout', 'jquery', 'ame-visible-users', 'ame-actor-manager', 'ame-jquery-cookie') + ); + + //Pass users to JS. + $users = array(); + foreach($this->getSuperUsers() as $user) { + $properties = $this->menuEditor->user_to_property_map($user); + $properties['avatar_html'] = get_avatar($user->ID, 32); + $users[$user->user_login] = $properties; + } + + $currentUser = wp_get_current_user(); + wp_localize_script( + 'ame-super-users', + 'wsAmeSuperUserSettings', + array( + 'superUsers' => $users, + 'userEditUrl' => admin_url('user-edit.php'), + 'currentUserLogin' => $currentUser->get('user_login'), + ) + ); + } + + public function enqueueTabStyles() { + parent::enqueueTabStyles(); + + wp_enqueue_auto_versioned_style( + 'ame-super-users-css', + plugins_url('super-users.css', __FILE__) + ); + } + + public function handleFormSubmission($action, $post = array()) { + //Note: We don't need to check user permissions here because plugin core already did. + if ( $action === 'ame_save_super_users' ) { + check_admin_referer($action); + + $userIDs = array_map('intval', explode(',', $post['settings'], 100)); + $userIDs = array_unique(array_filter($userIDs)); + + //Save settings. + $this->menuEditor->set_plugin_option('super_users', $userIDs); + + wp_redirect($this->getTabUrl(array('message' => 1))); + exit; + } + } +} \ No newline at end of file diff --git a/extras/modules/super-users/super-users.scss b/extras/modules/super-users/super-users.scss new file mode 100644 index 0000000..9976714 --- /dev/null +++ b/extras/modules/super-users/super-users.scss @@ -0,0 +1,3 @@ +.ame-column-user-id { + width: 70px; +} \ No newline at end of file diff --git a/extras/modules/super-users/super-users.ts b/extras/modules/super-users/super-users.ts new file mode 100644 index 0000000..b67e606 --- /dev/null +++ b/extras/modules/super-users/super-users.ts @@ -0,0 +1,107 @@ +/// <reference path="../../../js/knockout.d.ts" /> +/// <reference path="../../../js/jquery.d.ts" /> +/// <reference path="../../../js/jquery.biscuit.d.ts" /> +/// <reference path="../../../js/lodash-3.10.d.ts" /> +/// <reference path="../../../modules/actor-selector/actor-selector.ts" /> + +declare var wsAmeSuperUserSettings: Object; +declare var AmeSelectUsersDialog: any; + +class AmeSuperUsers { + private static _ = wsAmeLodash; + public superUsers: KnockoutObservableArray<AmeUser>; + public settingsData: KnockoutComputed<string>; + + private userEditUrl: string; + private currentUserLogin: string; + + public addButtonText: string = 'Add User'; + public isInfoBoxOpen: KnockoutComputed<boolean>; + + constructor(settings) { + this.userEditUrl = settings.userEditUrl; + this.currentUserLogin = settings.currentUserLogin; + + this.superUsers = ko.observableArray([]); + AmeSuperUsers._.forEach(settings.superUsers, (userDetails) => { + var user = AmeUser.createFromProperties(userDetails); + if (!AmeActors.getUser(user.userLogin)) { + AmeActors.addUsers([user]); + } + this.superUsers.push(user); + }); + this.superUsers.sort(AmeSuperUsers.compareLogins); + + this.settingsData = ko.computed<string>((): string => { + return AmeSuperUsers._.map(this.superUsers(), 'userId').join(','); + }); + + //Store the state of the info box in a cookie. + let initialState = jQuery.cookie('ame_su_info_box_open'); + let _isBoxOpen = ko.observable<boolean>((typeof initialState === 'undefined') ? true : (initialState === '1')); + + this.isInfoBoxOpen = ko.computed<boolean>({ + read: (): boolean => { + return _isBoxOpen(); + }, + write: (value: boolean) => { + jQuery.cookie('ame_su_info_box_open', value ? '1' : '0', {expires: 90}); + _isBoxOpen(value); + } + }); + } + + public removeUser(user: AmeUser) { + this.superUsers.remove(user); + } + + public getEditLink(user: AmeUser) { + return this.userEditUrl + '?user_id=' + user.userId; + } + + public selectHiddenUsers() { + AmeSelectUsersDialog.open({ + selectedUsers: AmeSuperUsers._.map(this.superUsers(), 'userLogin'), + users: AmeSuperUsers._.indexBy(this.superUsers(), 'userLogin'), + actorManager: AmeActors, + + currentUserLogin: this.currentUserLogin, + alwaysIncludeCurrentUser: false, + + save: (selectedUsers: AmeUser[]) => { + selectedUsers.sort(AmeSuperUsers.compareLogins); + this.superUsers(selectedUsers); + } + }); + } + + private static compareLogins(a: AmeUser, b: AmeUser): number { + if (a.userLogin > b.userLogin) { + return 1; + } else if (a. userLogin < b.userLogin) { + return -1; + } + return 0; + } + + public formatUserRoles(user: AmeUser): string { + let displayNames = AmeSuperUsers._.map(user.roles, (roleId) => { + var actor = AmeActors.getActor('role:' + roleId); + if (actor) { + return actor.displayName; + } else { + return '[Unknown role]'; + } + }); + return displayNames.join(', '); + } + + public toggleInfoBox() { + this.isInfoBoxOpen(!this.isInfoBoxOpen()); + } +} + +jQuery(function() { + var superUserVM = new AmeSuperUsers(wsAmeSuperUserSettings); + ko.applyBindings(superUserVM, document.getElementById('ame-super-user-settings')); +}); diff --git a/extras/modules/tweaks/ameAdminCssTweakManager.php b/extras/modules/tweaks/ameAdminCssTweakManager.php new file mode 100644 index 0000000..cb025c3 --- /dev/null +++ b/extras/modules/tweaks/ameAdminCssTweakManager.php @@ -0,0 +1,70 @@ +<?php + +use YahnisElsts\AdminMenuEditor\Configurable\StringSetting; + +class ameAdminCssTweakManager { + private $isOutputHookRegistered = false; + private $pendingCss = array(); + + private $cachedUserInput = null; + + public function __construct() { + add_action('admin-menu-editor-register_tweaks', array($this, 'registerDefaultTweak'), 10, 1); + } + + public function enqueueCss($settings = null) { + if ( ($settings === null) || (empty($settings['css'])) ) { + return; + } + $this->pendingCss[] = $settings['css']; + if ( !$this->isOutputHookRegistered ) { + add_action('admin_print_scripts', array($this, 'outputCss')); + $this->isOutputHookRegistered = true; + } + } + + public function outputCss() { + if ( empty($this->pendingCss) ) { + return; + } + echo '<!-- Admin Menu Editor: Admin CSS tweaks -->', "\n"; + echo '<style type="text/css" id="ame-admin-css-tweaks">', "\n"; + echo implode("\n", $this->pendingCss); + echo "\n", '</style>', "\n"; + } + + /** + * Create a CSS tweak instance with the specified properties. + * + * @param array $properties + * @return ameDelegatedTweak + */ + public function createTweak($properties) { + if ( $this->cachedUserInput === null ) { + $this->cachedUserInput = (new StringSetting('css'))->textarea('css'); + } + + $cssTweak = new ameDelegatedTweak( + $properties['id'], + $properties['label'], + array($this, 'enqueueCss') + ); + $cssTweak->setSectionId('admin-css'); + $cssTweak->add($this->cachedUserInput); + + return $cssTweak; + } + + /** + * @param ameTweakManager $tweakManager + */ + public function registerDefaultTweak($tweakManager) { + $tweakManager->addSection('admin-css', 'Admin CSS', 20); + + $defaultTweak = $this->createTweak(array( + 'id' => 'default-admin-css', + 'label' => 'Add custom admin CSS', + )); + $tweakManager->addTweak($defaultTweak); + } +} \ No newline at end of file diff --git a/extras/modules/tweaks/ameBaseTweak.php b/extras/modules/tweaks/ameBaseTweak.php new file mode 100644 index 0000000..e4069cc --- /dev/null +++ b/extras/modules/tweaks/ameBaseTweak.php @@ -0,0 +1,123 @@ +<?php + +use YahnisElsts\AdminMenuEditor\Configurable\ActorFeature; + +abstract class ameBaseTweak extends ActorFeature { + protected $parentId; + protected $sectionId; + + /** + * @var string[]|null List of admin screen IDs that the tweak applies to. + */ + protected $screens = null; + + /** + * @var string|null + */ + protected $hideableLabel = null; + + /** + * @var string|null + */ + protected $hideableCategory = null; + + /** + * @param array|null $settings User settings for this tweak. + * @return mixed + */ + abstract public function apply($settings = null); + + public function getId() { + return $this->id; + } + + public function getLabel() { + return $this->label; + } + + public function getParentId() { + return $this->parentId; + } + + public function setParentId($id) { + $this->parentId = $id; + return $this; + } + + public function setSectionId($id) { + $this->sectionId = $id; + return $this; + } + + public function getSectionId() { + return $this->sectionId; + } + + public function hasScreenFilter() { + return ($this->screens !== null); + } + + public function isEnabledForCurrentScreen() { + if ( !$this->hasScreenFilter() ) { + return true; + } + if ( !function_exists('get_current_screen') ) { + return false; + } + $screen = get_current_screen(); + if ( isset($screen, $screen->id) ) { + return $this->isEnabledForScreen($screen->id); + } + return false; + } + + public function isEnabledForScreen($screenId) { + if ( $this->screens === null ) { + return true; + } + return in_array($screenId, $this->screens); + } + + public function setScreens($screens) { + $this->screens = $screens; + } + + public function isIndependentlyHideable() { + return ($this->hideableCategory !== null); + } + + public function getHideableCategoryId() { + return $this->hideableCategory; + } + + public function setHideableCategoryId($categoryId) { + $this->hideableCategory = $categoryId; + } + + public function getHideableLabel() { + if ( $this->hideableLabel !== null ) { + return $this->hideableLabel; + } + return $this->getLabel(); + } + + public function setHideableLabel($text) { + $this->hideableLabel = $text; + } + + public function toArray() { + return array_merge( + parent::toArray(), + array( + 'parentId' => $this->getParentId(), + 'sectionId' => $this->getSectionId(), + ) + ); + } + + public function supportsUserInput() { + return $this->hasAnySettings(); + } + + //todo: getEditableProperties(). Or maybe we don't need it at all? Just merge the settings. +} \ No newline at end of file diff --git a/extras/modules/tweaks/ameDelegatedTweak.php b/extras/modules/tweaks/ameDelegatedTweak.php new file mode 100644 index 0000000..4ac10e3 --- /dev/null +++ b/extras/modules/tweaks/ameDelegatedTweak.php @@ -0,0 +1,32 @@ +<?php + +class ameDelegatedTweak extends ameBaseTweak { + protected $callback; + protected $callbackArgs; + + /** + * ameDelegatedTweak constructor. + * + * @param string $id + * @param string $label + * @param callable $callback + * @param array $callbackArgs + */ + public function __construct($id, $label, $callback, $callbackArgs = array()) { + parent::__construct($id, $label); + $this->callback = $callback; + + if ( !is_array($callbackArgs) ) { + throw new LogicException('$callbackArgs must be an array'); + } + $this->callbackArgs = $callbackArgs; + } + + public function apply($settings = null) { + $theArgs = $this->callbackArgs; + if ( $settings !== null ) { + $theArgs[] = $settings; + } + call_user_func_array($this->callback, $theArgs); + } +} \ No newline at end of file diff --git a/extras/modules/tweaks/ameEnvironmentColorTweak.php b/extras/modules/tweaks/ameEnvironmentColorTweak.php new file mode 100644 index 0000000..5f335e1 --- /dev/null +++ b/extras/modules/tweaks/ameEnvironmentColorTweak.php @@ -0,0 +1,187 @@ +<?php + +use YahnisElsts\AdminMenuEditor\Configurable\ActorFeature; +use YahnisElsts\AdminMenuEditor\Configurable\ColorSetting; +use YahnisElsts\AdminMenuEditor\Configurable\SettingsGroup; + +class ameEnvironmentColorTweak extends ameBaseTweak { + const DEFAULT_ID = 'environment-dependent-colors'; + + private $chosenColor = ''; + private $colorizeComponent = array(); + + public function __construct($id = null, $label = 'Change menu color depending on the environment') { + if ( $id === null ) { + $id = self::DEFAULT_ID; + } + parent::__construct($id, $label); + + $toolbarDefaults = array('role:administrator' => true); + if ( function_exists('is_multisite') && is_multisite() ) { + $toolbarDefaults['special:super_admin'] = true; + } + + $this + ->add( + (new SettingsGroup('colors', 'Environments:')) + ->add(new ColorSetting('production', 'Production')) + ->add(new ColorSetting('staging', 'Staging')) + ->add(new ColorSetting('development', 'Development')) + ->add(new ColorSetting('local', 'Local')) + ) + ->add( + (new SettingsGroup('targets', 'Apply color to:', null)) + ->add( + (new ActorFeature('colorizeToolbar', 'Toolbar (a.k.a Admin Bar)')) + ->setDefaultAccessMap($toolbarDefaults) + ) + ->add(new ActorFeature('colorizeAdminMenu', 'Admin menu')) + ); + } + + public function apply($settings = null) { + if ( !function_exists('wp_get_environment_type') ) { + return; + } + $environment = wp_get_environment_type(); + if ( empty($environment) ) { + return; + } + + if ( empty($settings['colors'][$environment]) ) { + return; + } + + $this->chosenColor = trim($settings['colors'][$environment]); + if ( !$this->isValidCssColor($this->chosenColor) ) { + $this->chosenColor = ''; + return; + } + + if ( + isset($GLOBALS['wsMenuEditorExtras']) + && method_exists($GLOBALS['wsMenuEditorExtras'], 'check_current_user_access') + ) { + $extras = $GLOBALS['wsMenuEditorExtras']; + /** @var wsMenuEditorExtras $extras */ + + $this->colorizeComponent['toolbar'] = $extras->check_current_user_access( + ameUtils::get($settings, 'colorizeToolbar', array()) + ); + $this->colorizeComponent['adminMenu'] = $extras->check_current_user_access( + ameUtils::get($settings, 'colorizeAdminMenu', array()) + ); + } + + if ( did_action('admin_bar_init') ) { + $this->enqueueEnvironmentStyle(); + } else { + add_action('admin_bar_init', array($this, 'enqueueEnvironmentStyle')); + } + } + + public function enqueueEnvironmentStyle() { + $customizations = array( + 'toolbar' => array( + 'background' => array( + '#wpadminbar', + '#wpadminbar .ab-item', + '#wpadminbar .ab-sub-wrapper', + '#wpadminbar .ab-sub-secondary', + ), + 'text' => array( + '#wpadminbar .ab-empty-item', + '#wpadminbar a.ab-item', + '#wpadminbar > #wp-toolbar span.ab-label', + '#wpadminbar > #wp-toolbar span.noticon', + + '#wpadminbar .ab-icon::before', + '#wpadminbar .ab-item::before', + '#wpadminbar #adminbarsearch::before', + ), + 'styleHandle' => 'admin-bar', + ), + 'adminMenu' => array( + 'background' => array( + '#adminmenuback', + '#adminmenuwrap', + '#adminmenu', + '#adminmenu .wp-submenu', + ), + 'text' => array( + '#adminmenu a', + '#adminmenu div.wp-menu-image::before', + 'div.wp-menu-image::before', + '#adminmenu .wp-submenu a', + '#collapse-button', + ), + 'styleHandle' => 'admin-menu', + ), + ); + + $textColor = $this->getContrastingTextColor($this->chosenColor); + + foreach ($customizations as $component => $details) { + if ( empty($this->colorizeComponent[$component]) ) { + continue; + } + + $css = sprintf( + '%1$s { background-color: %2$s !important; }', + implode(',', $details['background']), + $this->chosenColor + ); + + if ( $textColor !== null ) { + $css .= sprintf( + '%1$s { color: %2$s !important; }', + implode(', ', $details['text']), + $textColor + ); + } + + wp_add_inline_style($details['styleHandle'], $css); + } + } + + private function isValidCssColor($color) { + if ( !is_string($color) ) { + return false; + } + return (preg_match('@^#[0-9a-f]{3,8}$@i', $color) === 1); + } + + /** + * @param string $backgroundColor + * @return string|null A hex color value, or NULL to leave the color unchanged. + */ + private function getContrastingTextColor($backgroundColor) { + $colorLibraryPath = AME_ROOT_DIR . '/extras/phpColors/src/color.php'; + if ( !class_exists('phpColor', false) && file_exists($colorLibraryPath) ) { + /** @noinspection PhpIncludeInspection */ + include($colorLibraryPath); + } + if ( !class_exists('phpColor') ) { + return null; + } + + //The default admin color scheme uses a very light grey as the text color for the Toolbar + //and the admin menu. If the user chooses a light background color, this could make text + //difficult to read. To avoid that, let's automatically change the text color to dark grey + //if the background color is too light. + + //TODO: This doesn't really work correctly because phpColor doesn't seem to use the same HSL space as other tools. + //Maybe replace it with something else. + + try { + $background = phpColor::hexToHsl($backgroundColor); + if ( $background['L'] > 0.4 ) { + //Text needs to be darker. + return '#101010'; + } + } catch (Exception $e) { + return null; + } + return null; + } +} \ No newline at end of file diff --git a/extras/modules/tweaks/ameEnvironmentNameTweak.php b/extras/modules/tweaks/ameEnvironmentNameTweak.php new file mode 100644 index 0000000..acd3775 --- /dev/null +++ b/extras/modules/tweaks/ameEnvironmentNameTweak.php @@ -0,0 +1,74 @@ +<?php + + +class ameEnvironmentNameTweak extends ameBaseTweak { + private $currentEnvironment = ''; + private $toolbarIconCss = null; + + public function __construct($id = 'show-environment-in-toolbar', $label = 'Show environment type in the Toolbar') { + parent::__construct($id, $label); + } + + public function apply($settings = null) { + if ( !function_exists('wp_get_environment_type') ) { + return; + } + $this->currentEnvironment = wp_get_environment_type(); + if ( empty($this->currentEnvironment) ) { + return; + } + + add_action('admin_bar_menu', array($this, 'addEnvironmentToToolbar')); + } + + /** + * @param WP_Admin_Bar|null $adminBar + */ + public function addEnvironmentToToolbar($adminBar = null) { + if ( !$adminBar ) { + return; + } + + //Dashicons for different environments. Some icons are not aligned in the same way as others, + //so we also store a relative offset (position) from the top of the box. + $iconsByEnvironment = array( + 'production' => array('f11f', 3), + 'staging' => array('f463', 3), + 'development' => array('f107', 3), + 'local' => array('f102', 2), + ); + + $icon = 'f159'; + $offsetTop = 3; + if ( !empty($iconsByEnvironment[$this->currentEnvironment]) ) { + list($icon, $offsetTop) = $iconsByEnvironment[$this->currentEnvironment]; + } + + $itemId = 'ame-tweak-environment-type'; + $this->toolbarIconCss = sprintf( + '#wp-admin-bar-%s .ab-icon::before { + content: "\\%s"; + top: %dpx; + }', + $itemId, + $icon, + $offsetTop + ); + + $iconHtml = '<span class="ab-icon"></span>'; + $adminBar->add_node(array( + 'id' => $itemId, + 'title' => $iconHtml . esc_html(ameUtils::ucWords($this->currentEnvironment)), + 'parent' => 'top-secondary', + 'meta' => array( + 'title' => 'Current environment type', + ), + )); + + add_action('wp_before_admin_bar_render', array($this, 'printToolbarIconCss')); + } + + public function printToolbarIconCss() { + printf('<style type="text/css">%s</style>', $this->toolbarIconCss); + } +} \ No newline at end of file diff --git a/extras/modules/tweaks/ameGutenbergBlockManager.php b/extras/modules/tweaks/ameGutenbergBlockManager.php new file mode 100644 index 0000000..5021f72 --- /dev/null +++ b/extras/modules/tweaks/ameGutenbergBlockManager.php @@ -0,0 +1,235 @@ +<?php + +class ameGutenbergBlockManager { + const DETECTED_BLOCK_OPTION = 'ws_ame_detected_gtb_blocks'; + const SECTION_ID = 'gutenberg-blocks'; + + const SCRIPT_HANDLE = 'ame-gtb-block-detector'; + const UPDATE_BLOCKS_ACTION = 'ws_ame_update_gtb_blocks'; + + const TWEAK_PREFIX = 'hide-gtb-'; + const PARENT_PREFIX = 'gtb-block-section-'; + + /** + * @var WPMenuEditor + */ + private $menuEditor; + + private $hiddenBlocks = array(); + + public function __construct($menuEditor) { + $this->menuEditor = $menuEditor; + if ( is_admin() ) { + add_action('enqueue_block_editor_assets', array($this, 'enqueueGutenbergAssets'), 10000); + add_action('wp_ajax_' . self::UPDATE_BLOCKS_ACTION, array($this, 'ajaxUpdateBlocks')); + } + + add_action('admin-menu-editor-register_tweaks', array($this, 'registerBlockTweaks'), 10, 2); + + //The "allowed_block_types" filter was deprecated in WP 5.8 and a new "allowed_block_types_all" + //filter was introduced. Note that the filters take different arguments, but we can ignore that + //in this case because the first argument is the same and that's all we need here. + global $wp_version; + if ( isset($wp_version) && is_string($wp_version) && version_compare($wp_version, '5.8', '<') ) { + $blockFilter = 'allowed_block_types'; //Deprecated since WP 5.8.0. + } else { + $blockFilter = 'allowed_block_types_all'; + } + add_filter($blockFilter, array($this, 'filterAllowedBlocks'), 10000, 1); + } + + public function enqueueGutenbergAssets() { + //To reduce the performance impact of this feature, we only detect new Gutenberg blocks + //for users that can access the plugin. + if ( !$this->menuEditor->current_user_can_edit_menu() ) { + return; + } + + wp_enqueue_script( + self::SCRIPT_HANDLE, + plugins_url('gutenberg-block-detector.js', __FILE__), + array('wp-blocks', 'wp-dom-ready', 'wp-edit-post', 'jquery'), + '20210218-4', + true + ); + + $detectedItems = $this->getDetectedItems(); + $scriptData = array( + 'knownBlocks' => array_fill_keys(array_keys($detectedItems['blocks']), true), + 'knownCategories' => array_fill_keys(array_keys($detectedItems['categories']), true), + 'ajaxUrl' => self_admin_url('admin-ajax.php'), + 'ajaxAction' => self::UPDATE_BLOCKS_ACTION, + 'updateNonce' => wp_create_nonce('ws_ame_update_gtb_blocks'), + ); + + //Make sure to encode associative arrays as objects (dictionaries) even when they're empty. + if ( empty($scriptData['knownBlocks']) ) { + $scriptData['knownBlocks'] = new stdClass(); + } + if ( empty($scriptData['knownCategories']) ) { + $scriptData['knownCategories'] = new stdClass(); + } + + wp_localize_script(self::SCRIPT_HANDLE, 'wsAmeGutenbergBlockData', $scriptData); + } + + /** @noinspection PhpComposerExtensionStubsInspection */ + public function ajaxUpdateBlocks() { + check_ajax_referer(self::UPDATE_BLOCKS_ACTION); + + @header('Content-Type: application/json; charset=' . get_option('blog_charset')); + if ( !$this->menuEditor->current_user_can_edit_menu() ) { + echo json_encode(array('error' => 'Access denied')); + exit; + } + + //Basic validation. + $post = $this->menuEditor->get_post_params(); + if ( !isset($post['blocks'], $post['categories']) ) { + echo json_encode(array('error' => 'The "blocks" or "categories" field is missing.')); + exit; + } + $blocks = json_decode($post['blocks'], true); + $categories = json_decode($post['categories'], true); + if ( ($blocks === null) || ($categories === null) ) { + echo json_encode(array('error' => 'The "blocks" or "categories" field is not valid JSON.')); + exit; + } + + $this->saveDetectedItems($blocks, $categories); + echo json_encode(array('success' => true)); + exit; + } + + private function saveDetectedItems($blocks, $categories) { + //Index the lists by name or slug. + $blockIndex = array(); + foreach ($blocks as $block) { + $name = $block['name']; + unset($block['name']); + $blockIndex[$name] = $block; + } + $categoryIndex = array(); + foreach ($categories as $category) { + $slug = $category['slug']; + unset($category['slug']); + $categoryIndex[$slug] = $category; + } + + $data = array( + 'blocks' => $blockIndex, + 'categories' => $categoryIndex, + ); + + $lock = ameFileLock::create(__FILE__); + if ( !$lock->acquire() ) { + return; + } + + if ( is_multisite() ) { + update_site_option(self::DETECTED_BLOCK_OPTION, $data); + } else { + update_option(self::DETECTED_BLOCK_OPTION, $data, true); + } + + $lock->release(); + } + + private function getDetectedItems() { + $default = array('blocks' => array(), 'categories' => array()); + if ( is_multisite() ) { + $data = get_site_option(self::DETECTED_BLOCK_OPTION, $default); + } else { + $data = get_option(self::DETECTED_BLOCK_OPTION, $default); + } + if ( !is_array($data) ) { + return $default; + } + return $data; + } + + /** + * @param ameTweakManager $manager + * @param null|array $tweakFilter + */ + public function registerBlockTweaks($manager, $tweakFilter = null) { + $data = $this->getDetectedItems(); + $blocks = ameUtils::get($data, 'blocks', array()); + if ( empty($blocks) ) { + //The user must first open the Gutenberg editor so that we can detect registered blocks. + return; + } + + $manager->addSection(self::SECTION_ID, 'Hide Gutenberg Blocks', 40); + + if ( $tweakFilter !== null ) { + $filteredBlocks = array(); + foreach ($blocks as $id => $data) { + if ( isset($tweakFilter[self::TWEAK_PREFIX . $id]) ) { + $filteredBlocks[$id] = $data; + } + } + $blocks = $filteredBlocks; + } + + if ( $tweakFilter === null ) { + //Create stub tweaks that represent each block category. + $categories = ameUtils::get($data, 'categories', array()); + foreach ($categories as $catId => $category) { + $parentTweak = new ameDelegatedTweak( + self::PARENT_PREFIX . $catId, + ameUtils::get($category, 'title', $catId), + '__return_false' //This tweak is just a presentation tool. It doesn't do anything. + ); + $parentTweak->setSectionId(self::SECTION_ID); + $manager->addTweak($parentTweak); + } + } + + $theCallback = array($this, 'flagBlockAsHidden'); + foreach ($blocks as $id => $block) { + $tweak = new ameDelegatedTweak( + self::TWEAK_PREFIX . $id, + ameUtils::get($block, 'title', $id), + $theCallback, + array($id) + ); + $tweak->setSectionId(self::SECTION_ID); + if ( !empty($block['category']) ) { + $tweak->setParentId(self::PARENT_PREFIX . $block['category']); + } + $manager->addTweak($tweak); + } + } + + /** @noinspection PhpUnused Actually used as a tweak callback. */ + public function flagBlockAsHidden($blockId) { + $this->hiddenBlocks[] = $blockId; + } + + public function filterAllowedBlocks($allowedBlocks) { + if ( empty($this->hiddenBlocks) ) { + return $allowedBlocks; + } + + if ( $allowedBlocks === true ) { + //All blocks are allowed by default. We need to turn our blacklist into a whitelist. + //Unfortunately, we can't get all available blocks via PHP, so we rely on the cached + //list of registered blocks that was supplied by our JS script. + $registeredBlocks = array_keys(ameUtils::get($this->getDetectedItems(), 'blocks', array())); + $result = array_diff($registeredBlocks, $this->hiddenBlocks); + + //Reindex the array. array_diff() can create "holes" in the array, which means that + //json_encode() will encode it as an object with numeric keys and not a real array. + //As of WP 5.4-alpha, Gutenberg requires a plain array. + return array_values($result); + } else if ( is_array($allowedBlocks) ) { + //Another plugin has already filtered the list of allowed block types. + //Let's remove any blocks that are hidden by AME settings. + return array_values(array_diff($allowedBlocks, $this->hiddenBlocks)); + } + //Either all blocks were hidden by another plugin, or the data type of $allowedBlocks + //is not recognized. + return $allowedBlocks; + } +} \ No newline at end of file diff --git a/extras/modules/tweaks/ameHideSelectorTweak.php b/extras/modules/tweaks/ameHideSelectorTweak.php new file mode 100644 index 0000000..98b603e --- /dev/null +++ b/extras/modules/tweaks/ameHideSelectorTweak.php @@ -0,0 +1,45 @@ +<?php + + +class ameHideSelectorTweak extends ameBaseTweak { + const OUTPUT_HOOK = 'admin_head'; + + /** + * @var string A CSS selector. + */ + protected $selector; + + protected static $pendingSelectors = array(); + protected static $isOutputHookSet = false; + + public function __construct($id, $label, $selector) { + parent::__construct($id, $label); + $this->selector = $selector; + } + + public function apply($settings = null) { + self::$pendingSelectors[] = $this->selector; + + if (did_action(self::OUTPUT_HOOK)) { + self::outputPendingSelectors(); + } else if (!self::$isOutputHookSet) { + add_action(self::OUTPUT_HOOK, array(__CLASS__, 'outputPendingSelectors')); + self::$isOutputHookSet = true; + } + } + + public static function outputPendingSelectors() { + if (empty(self::$pendingSelectors)) { + return; + } + + $css = sprintf( + '<style type="text/css">%s { display: none !important; }</style>', + implode(',', self::$pendingSelectors) + ); + + echo '<!-- AME selector tweaks -->', "\n", $css, "\n"; + + self::$pendingSelectors = array(); + } +} \ No newline at end of file diff --git a/extras/modules/tweaks/ameHideSidebarTweak.php b/extras/modules/tweaks/ameHideSidebarTweak.php new file mode 100644 index 0000000..dea933a --- /dev/null +++ b/extras/modules/tweaks/ameHideSidebarTweak.php @@ -0,0 +1,23 @@ +<?php + + +class ameHideSidebarTweak extends ameBaseTweak { + private $sidebarId; + + protected $sectionId = 'sidebars'; + + /** + * @param array $sidebar Sidebar data from $wp_registered_sidebars + */ + public function __construct($sidebar) { + $this->sidebarId = $sidebar['id']; + parent::__construct( + 'hide-sidebar-' . $this->sidebarId, + esc_html($sidebar['name']) + ); + } + + public function apply($settings = null) { + unregister_sidebar($this->sidebarId); + } +} \ No newline at end of file diff --git a/extras/modules/tweaks/ameHideSidebarWidgetTweak.php b/extras/modules/tweaks/ameHideSidebarWidgetTweak.php new file mode 100644 index 0000000..e9e85a4 --- /dev/null +++ b/extras/modules/tweaks/ameHideSidebarWidgetTweak.php @@ -0,0 +1,26 @@ +<?php + +class ameHideSidebarWidgetTweak extends ameBaseTweak { + private $widget; + private $widgetClass; + + protected $sectionId = 'sidebar-widgets'; + + /** + * ameHideSidebarWidgetTweak constructor. + * + * @param WP_Widget $widget + */ + public function __construct($widget) { + $this->widgetClass = get_class($widget); + $this->widget = $widget; + parent::__construct( + 'hide-sidebar-widget-' . $this->widgetClass, + esc_html($widget->name) + ); + } + + public function apply($settings = null) { + unregister_widget($this->widgetClass); + } +} \ No newline at end of file diff --git a/extras/modules/tweaks/ameTinyMceButtonManager.php b/extras/modules/tweaks/ameTinyMceButtonManager.php new file mode 100644 index 0000000..d1bee52 --- /dev/null +++ b/extras/modules/tweaks/ameTinyMceButtonManager.php @@ -0,0 +1,231 @@ +<?php + +class ameTinyMceButtonManager { + const DETECTED_BUTTON_OPTION = 'ws_ame_detected_tmce_buttons'; + const SECTION_ID = 'tmce-buttons'; + + private $detectionEnabled = false; + private $storageHookSet = false; + + private $newDetectedButtons = array(); + private $cachedKnownButtons = null; + + private $hiddenButtons = array(); + + private $builtInButtons = array( + 'kitchensink' => array('title' => 'More buttons'), + 'wp_add_media' => array('title' => 'Add Media'), + 'formatselect' => array('title' => 'Format dropdown'), + 'alignleft' => array('title' => 'Align left'), + 'aligncenter' => array('title' => 'Align center'), + 'alignright' => array('title' => 'Align right'), + 'alignjustify' => array('title' => 'Justify'), + 'alignnone' => array('title' => 'No alignment'), + 'bold' => array('title' => 'Bold'), + 'italic' => array('title' => 'Italic'), + 'underline' => array('title' => 'Underline'), + 'strikethrough' => array('title' => 'Strikethrough'), + 'subscript' => array('title' => 'Subscript'), + 'superscript' => array('title' => 'Superscript'), + 'outdent' => array('title' => 'Decrease indent'), + 'indent' => array('title' => 'Increase indent'), + 'cut' => array('title' => 'Cut'), + 'copy' => array('title' => 'Copy'), + 'paste' => array('title' => 'Paste'), + 'help' => array('title' => 'Help'), + 'selectall' => array('title' => 'Select all'), + 'visualaid' => array('title' => 'Visual aids'), + 'newdocument' => array('title' => 'New document'), + 'removeformat' => array('title' => 'Clear formatting'), + 'remove' => array('title' => 'Remove'), + 'blockquote' => array('title' => 'Blockquote'), + 'undo' => array('title' => 'Undo'), + 'redo' => array('title' => 'Redo'), + 'fontsizeselect' => array('title' => 'Font Size'), + 'fontselect' => array('title' => 'Font Family'), + 'styleselect' => array('title' => 'Style dropdown'), + 'insert' => array('title' => 'Insert menu'), + 'charmap' => array('title' => 'Special character'), + 'hr' => array('title' => 'Horizontal line'), + 'numlist' => array('title' => 'Numbered list'), + 'bullist' => array('title' => 'Bulleted list'), + 'media' => array('title' => 'Insert/edit media'), + 'pastetext' => array('title' => 'Paste as text'), + 'forecolor' => array('title' => 'Text color'), + 'backcolor' => array('title' => 'Background color'), + 'wp_adv' => array('title' => 'Toolbar Toggle'), + 'wp_more' => array('title' => 'Insert Read More tag'), + 'wp_page' => array('title' => 'Page break'), + 'wp_help' => array('title' => 'Keyboard Shortcuts'), + 'wp_code' => array('title' => 'Code'), + 'link' => array('title' => 'Insert/edit link'), + 'unlink' => array('title' => 'Remove link'), + 'spellchecker' => array('title' => 'Toggle spellchecker'), + ); + + public function __construct() { + add_action('admin_init', array($this, 'toggleButtonDetection')); + + $buttonFilters = array('mce_buttons', 'mce_buttons_2', 'mce_buttons_3', 'mce_buttons_4'); + foreach ($buttonFilters as $filter) { + add_filter($filter, array($this, 'filterButtons'), 9000, 1); + } + + add_action('admin-menu-editor-register_tweaks', array($this, 'registerButtonTweaks'), 10, 1); + } + + public function toggleButtonDetection() { + $this->detectionEnabled = current_user_can('activate_plugins') || current_user_can('edit_others_pages'); + } + + public function filterButtons($buttons) { + //Sanity check: $buttons should be an array or something array-like. + if ( !is_array($buttons) && !($buttons instanceof ArrayAccess) ) { + return $buttons; + } + + if ( $this->detectionEnabled ) { + $this->detectNewButtons($buttons); + } + $buttons = $this->removeHiddenButtons($buttons); + return $buttons; + } + + private function detectNewButtons($buttons) { + $newButtons = array_diff($buttons, $this->getKnownButtonIds()); + if ( !empty($newButtons) ) { + $this->newDetectedButtons = array_merge($this->newDetectedButtons, $newButtons); + if ( !$this->storageHookSet ) { + add_action('shutdown', array($this, 'storeNewButtons')); + $this->storageHookSet = true; + } + } + } + + public function storeNewButtons() { + $newButtons = array_fill_keys($this->newDetectedButtons, time()); + $buttons = array_merge($this->getDetectedButtons(), $newButtons); + + //Filter out built-in buttons. We already know they exist, so there's no need + //to store them in the database. + $buttons = array_diff_key($buttons, $this->getBuiltInButtons()); + $this->saveDetectedButtons($buttons); + + $this->newDetectedButtons = array(); + } + + private function saveDetectedButtons($buttons) { + $this->cachedKnownButtons = null; + + $handle = null; + if ( function_exists('flock') ) { + $handle = @fopen(__FILE__, 'r'); + if ( !$handle ) { + return; + } + $success = @flock($handle, LOCK_EX | LOCK_NB); + if ( !$success ) { + fclose($handle); + return; + } + } + + if ( is_multisite() ) { + update_site_option(self::DETECTED_BUTTON_OPTION, $buttons); + } else { + update_option(self::DETECTED_BUTTON_OPTION, $buttons, 'yes'); + } + + if ( $handle !== null ) { + @flock($handle, LOCK_UN); + fclose($handle); + } + } + + /** + * @param string[] $buttons + * @return string[] + */ + private function removeHiddenButtons($buttons) { + if ( empty($this->hiddenButtons) ) { + return $buttons; + } + return array_diff($buttons, $this->hiddenButtons); + } + + private function getKnownButtons() { + if ( $this->cachedKnownButtons === null ) { + $this->cachedKnownButtons = array_merge($this->getDetectedButtons(), $this->getBuiltInButtons()); + } + return $this->cachedKnownButtons; + } + + /** + * @return string[] + */ + private function getKnownButtonIds() { + return array_keys($this->getKnownButtons()); + } + + /** + * @return array + */ + private function getDetectedButtons() { + if ( is_multisite() ) { + $buttons = get_site_option(self::DETECTED_BUTTON_OPTION, array()); + } else { + $buttons = get_option(self::DETECTED_BUTTON_OPTION, array()); + } + if ( !is_array($buttons) ) { + return array(); + } + return $buttons; + } + + private function getBuiltInButtons() { + return $this->builtInButtons; + } + + /** + * @param ameTweakManager $tweakManager + */ + public function registerButtonTweaks($tweakManager) { + $tweakManager->addSection(self::SECTION_ID, 'Hide TinyMCE Buttons', 150); + $theCallback = array($this, 'flagButtonAsHidden'); + + $buttons = $this->getKnownButtons(); + $buttonTweaks = array(); + foreach ($buttons as $id => $details) { + $label = $id; + if ( isset($details['title']) ) { + $label = sprintf('%s (%s)', $details['title'], $id); + } + + $tweak = new ameDelegatedTweak('hide-tmce-' . $id, $label, $theCallback, array($id)); + $tweak->setSectionId(self::SECTION_ID); + $buttonTweaks[] = $tweak; + } + + //Sort tweaks by label. + uasort( + $buttonTweaks, + /** + * @param ameBaseTweak $a + * @param ameBaseTweak $b + * @return int + */ + function ($a, $b) { + return strnatcasecmp($a->getLabel(), $b->getLabel()); + } + ); + + foreach ($buttonTweaks as $tweak) { + $tweakManager->addTweak($tweak); + } + } + + /** @noinspection PhpUnused Actually used in registerButtonTweaks(). */ + public function flagButtonAsHidden($buttonId) { + $this->hiddenButtons[] = $buttonId; + } +} \ No newline at end of file diff --git a/extras/modules/tweaks/configurables.php b/extras/modules/tweaks/configurables.php new file mode 100644 index 0000000..d5dd132 --- /dev/null +++ b/extras/modules/tweaks/configurables.php @@ -0,0 +1,284 @@ +<?php + +namespace YahnisElsts\AdminMenuEditor\Configurable; + +use ArrayAccess; +use ArrayIterator; +use InvalidArgumentException; +use IteratorAggregate; + +interface IterableSettingsCollection extends IteratorAggregate, ArrayAccess { + /** + * @param NamedNode $child + * @return $this + */ + public function add(NamedNode $child); + + /** + * Check if the collection contains any setting objects. + * + * @return bool + */ + public function hasAnySettings(); +} + +abstract class NamedNode { + /** + * @var string + */ + protected $id = ''; + + /** + * @var string + */ + protected $label = ''; + + /** + * NamedNode constructor. + * + * @param string $id + * @param string $label + */ + public function __construct($id, $label = '') { + $this->id = $id; + $this->label = ($label !== null) ? $label : $id; + } + + public function getId() { + return $this->id; + } + + public function getLabel() { + return $this->label; + } + + public function toArray() { + $result = array( + 'id' => $this->getId(), + 'label' => $this->getLabel(), + ); + + if ( $this instanceof IterableSettingsCollection ) { + $children = []; + foreach ($this as $item) { + $children[] = $item->toArray(); + } + $result['children'] = $children; + } + + return $result; + } +} + +class SettingsGroup extends NamedNode implements IterableSettingsCollection { + const ID_AS_PATH = '.'; + + /** + * @var NamedNode[] + */ + protected $items = []; + + /** + * @var string|null + */ + protected $propertyPath = null; + + /** + * SettingsGroup constructor. + * + * @param string $id + * @param string|null $label + * @param string|null $propertyPath + */ + public function __construct($id, $label = null, $propertyPath = self::ID_AS_PATH) { + parent::__construct($id, $label); + if ( $propertyPath === self::ID_AS_PATH ) { + $this->propertyPath = $this->id; + } else { + $this->propertyPath = $propertyPath; + } + } + + #[\ReturnTypeWillChange] + public function getIterator() { + return new ArrayIterator($this->items); + } + + #[\ReturnTypeWillChange] + public function offsetExists($offset) { + return array_key_exists($offset, $this->items); + } + + #[\ReturnTypeWillChange] + public function offsetGet($offset) { + return $this->items[$offset]; + } + + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) { + if ( !($value instanceof NamedNode) ) { + throw new InvalidArgumentException( + 'Tried to add an invalid item to ' . __CLASS__ . '. Expected a NamedNode.' + ); + } + $this->items[$offset] = $value; + } + + #[\ReturnTypeWillChange] + public function offsetUnset($offset) { + unset($this->items[$offset]); + } + + public function toArray() { + $result = parent::toArray(); + $result['type'] = 'group'; + $result['propertyPath'] = $this->propertyPath; + return $result; + } + + public function add(NamedNode $child) { + $id = $child->getId(); + if ( ($id !== '') && ($id !== null) ) { + $this->items[$child->getId()] = $child; + } else { + $this->items[] = $child; + } + return $this; + } + + public function hasAnySettings() { + if ( empty($this->items) ) { + return false; + } + foreach ($this->items as $item) { + if ( $item instanceof Setting ) { + return true; + } else if ( ($item instanceof IterableSettingsCollection) && $item->hasAnySettings() ) { + return true; + } + } + return false; + } +} + +class ActorFeature extends NamedNode implements IterableSettingsCollection { + /** + * @var SettingsGroup + */ + protected $children; + protected $defaultAccessMap = null; + + public function __construct($id, $label = null) { + parent::__construct($id, $label); + $this->children = new SettingsGroup('', '', null); + } + + /** + * @param array<string,boolean>|null $accessMap + * @return $this + */ + public function setDefaultAccessMap($accessMap) { + $this->defaultAccessMap = $accessMap; + return $this; + } + + #[\ReturnTypeWillChange] + public function getIterator() { + return $this->children->getIterator(); + } + + #[\ReturnTypeWillChange] + public function offsetExists($offset) { + return $this->children->offsetExists($offset); + } + + #[\ReturnTypeWillChange] + public function offsetGet($offset) { + return $this->children[$offset]; + } + + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) { + $this->children[$offset] = $value; + } + + #[\ReturnTypeWillChange] + public function offsetUnset($offset) { + unset($this->children[$offset]); + } + + public function add(NamedNode $child) { + $this->children->add($child); + return $this; + } + + public function toArray() { + $result = parent::toArray(); + $result['hasAccessMap'] = true; + if ( $this->defaultAccessMap !== null ) { + $result['defaultAccessMap'] = $this->defaultAccessMap; + } + return $result; + } + + public function hasAnySettings() { + return $this->children->hasAnySettings(); + } +} + +class Setting extends NamedNode { + /** + * @var mixed|null + */ + public $defaultValue = null; + + /** + * @var string + */ + protected $dataType = 'null'; + + /** + * @var string|null + */ + protected $inputType = null; + + public function toArray() { + $result = parent::toArray(); + $result['dataType'] = $this->dataType; + $result['inputType'] = $this->inputType; + if ( $this->defaultValue !== null ) { + $result['defaultValue'] = $this->defaultValue; + } + return $result; + } +} + +class StringSetting extends Setting { + protected $dataType = 'string'; + protected $inputType = 'text'; + + public $syntaxHighlighting = null; + + public function textarea($syntaxHighlighting = null) { + $this->inputType = 'textarea'; + $this->syntaxHighlighting = $syntaxHighlighting; + return $this; + } + + public function toArray() { + $result = parent::toArray(); + if ( $this->syntaxHighlighting !== null ) { + $result['syntaxHighlighting'] = $this->syntaxHighlighting; + } + return $result; + } +} + +class ColorSetting extends Setting { + protected $dataType = 'string'; + protected $inputType = 'color'; +} + +class BooleanSetting extends Setting { + protected $dataType = 'boolean'; +} \ No newline at end of file diff --git a/extras/modules/tweaks/default-tweaks.php b/extras/modules/tweaks/default-tweaks.php new file mode 100644 index 0000000..4ef6032 --- /dev/null +++ b/extras/modules/tweaks/default-tweaks.php @@ -0,0 +1,85 @@ +<?php +return array( + 'sections' => array( + 'profile' => array('label' => 'Hide Profile Fields', 'priority' => 80), + 'sidebar-widgets' => array('label' => 'Hide Sidebar Widgets', 'priority' => 100), + 'sidebars' => array('label' => 'Hide Sidebars', 'priority' => 120), + 'environment-type' => array('label' => 'Environment Type', 'priority' => 30), + ), + + 'tweaks' => array( + 'hide-screen-meta-links' => array( + 'label' => 'Hide screen meta links', + 'selector' => '#screen-meta-links', + 'hideableLabel' => 'Screen meta links', + 'hideableCategory' => 'admin-ui', + ), + 'hide-screen-options' => array( + 'label' => 'Hide the "Screen Options" button', + 'selector' => '#screen-options-link-wrap', + 'parent' => 'hide-screen-meta-links', + 'hideableLabel' => '"Screen Options" button', + 'hideableCategory' => 'admin-ui', + ), + 'hide-help-panel' => array( + 'label' => 'Hide the "Help" button', + 'selector' => '#contextual-help-link-wrap', + 'parent' => 'hide-screen-meta-links', + 'hideableLabel' => '"Help" button', + 'hideableCategory' => 'admin-ui', + ), + 'hide-all-admin-notices' => array( + 'label' => 'Hide ALL admin notices', + 'selector' => '#wpbody-content .notice, #wpbody-content .updated, #wpbody-content .update-nag', + 'hideableLabel' => 'All admin notices', + 'hideableCategory' => 'admin-ui', + ), + + 'hide-gutenberg-options' => array( + 'label' => 'Hide the Gutenberg options menu (three vertical dots)', + 'selector' => '#editor .edit-post-header__settings .edit-post-more-menu', + ), + 'hide-gutenberg-fs-wp-logo' => array( + 'label' => 'Hide the WordPress logo in Gutenberg fullscreen mode', + 'selector' => '#editor .edit-post-header a.components-button[href^="edit.php"]', + ), + + 'hide-profile-visual-editor' => array( + 'label' => 'Visual Editor', + 'selector' => 'tr.user-rich-editing-wrap', + 'section' => 'profile', + 'screens' => array('profile'), + ), + 'hide-profile-syntax-higlighting' => array( + 'label' => 'Syntax Highlighting', + 'selector' => 'tr.user-syntax-highlighting-wrap', + 'section' => 'profile', + 'screens' => array('profile'), + ), + 'hide-profile-color-scheme-selector' => array( + 'label' => 'Admin Color Scheme', + 'selector' => 'tr.user-admin-color-wrap', + 'section' => 'profile', + 'screens' => array('profile'), + ), + 'hide-profile-toolbar-toggle' => array( + 'label' => 'Toolbar', + 'selector' => 'tr.show-admin-bar.user-admin-bar-front-wrap', + 'section' => 'profile', + 'screens' => array('profile'), + ), + + 'show-environment-in-toolbar' => array( + 'label' => 'Show environment type in the Toolbar', + 'section' => 'environment-type', + 'className' => 'ameEnvironmentNameTweak', + 'includeFile' => __DIR__ . '/ameEnvironmentNameTweak.php', + ), + 'environment-dependent-colors' => array( + 'label' => 'Change menu color depending on the environment', + 'section' => 'environment-type', + 'className' => 'ameEnvironmentColorTweak', + 'includeFile' => __DIR__ . '/ameEnvironmentColorTweak.php', + ), + ), +); \ No newline at end of file diff --git a/extras/modules/tweaks/gutenberg-block-detector.js b/extras/modules/tweaks/gutenberg-block-detector.js new file mode 100644 index 0000000..74af337 --- /dev/null +++ b/extras/modules/tweaks/gutenberg-block-detector.js @@ -0,0 +1,84 @@ +/** + * @property {Object} window.wsAmeGutenbergBlockData + * @property wp.domReady + * @property wp.blocks + * @property wp.blocks.getBlockTypes + * @property wp.blocks.getCategories + * @property window._wpLoadBlockEditor + * @property window._wpLoadGutenbergEditor + */ + +if (typeof wp !== 'undefined' && typeof wp.domReady !== 'undefined') { + wp.domReady(function () { + let loadGutenberg = null; + if (typeof window._wpLoadBlockEditor !== 'undefined') { + loadGutenberg = window._wpLoadBlockEditor; + } else if (typeof window._wpLoadGutenbergEditor !== 'undefined') { + loadGutenberg = window._wpLoadGutenbergEditor; + } + + if ((loadGutenberg === null) || (typeof loadGutenberg.then === 'undefined')) { + return; + } + + const scriptData = (typeof window.wsAmeGutenbergBlockData !== 'undefined') + ? window.wsAmeGutenbergBlockData + : null; + + //We must have the AJAX URL and an update nonce to save detected blocks. + //If we don't have those, this script can't do anything. + if (!scriptData) { + return; + } + + //Wait for Gutenberg to load. + loadGutenberg.then(function () { + setTimeout(function () { + let hasNewData = false; + + //We're using arrays instead of objects because we want to preserve item order. + let registeredBlocks = []; + const blocks = wp.blocks.getBlockTypes(); + for (let i = 0; i < blocks.length; i++) { + const block = blocks[i]; + registeredBlocks.push({ + name: block.name, + title: block.title, + category: block.category + }); + + if (!scriptData.knownBlocks.hasOwnProperty(block.name)) { + hasNewData = true; + } + } + + let registeredCategories = [], + categories = wp.blocks.getCategories(); + for (let j = 0; j < categories.length; j++) { + registeredCategories.push({ + slug: categories[j].slug, + title: categories[j].title, + }); + + if (!scriptData.knownCategories.hasOwnProperty(categories[j].slug)) { + hasNewData = true; + } + } + + if (hasNewData && scriptData.updateNonce && scriptData.ajaxAction) { + //Save the registered blocks and categories. + jQuery.post( + scriptData.ajaxUrl, + { + action: scriptData.ajaxAction, + _ajax_nonce: scriptData.updateNonce, + blocks: JSON.stringify(registeredBlocks), + categories: JSON.stringify(registeredCategories) + } + ); + } + }, 50); + }); + + }); +} \ No newline at end of file diff --git a/extras/modules/tweaks/tweak-manager.js b/extras/modules/tweaks/tweak-manager.js new file mode 100644 index 0000000..77c3a2c --- /dev/null +++ b/extras/modules/tweaks/tweak-manager.js @@ -0,0 +1,791 @@ +/// <reference path="../../../js/knockout.d.ts" /> +/// <reference path="../../../js/jquery.d.ts" /> +/// <reference path="../../../js/lodash-3.10.d.ts" /> +/// <reference path="../../../modules/actor-selector/actor-selector.ts" /> +/// <reference path="../../../js/jquery.biscuit.d.ts" /> +/// <reference path="../../ko-extensions.ts" /> +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var AmeNamedNode = /** @class */ (function () { + function AmeNamedNode(properties) { + this.htmlId = ''; + this.id = properties.id; + this.label = properties.label; + } + return AmeNamedNode; +}()); +function isAmeSettingsGroupProperties(thing) { + var group = thing; + return (typeof group.children !== 'undefined'); +} +function isAmeSettingProperties(thing) { + return (typeof thing.dataType === 'string'); +} +var AmeSetting = /** @class */ (function (_super) { + __extends(AmeSetting, _super); + function AmeSetting(properties, store, path) { + if (path === void 0) { path = []; } + var _this = _super.call(this, properties) || this; + var defaultValue = null; + if (typeof properties.defaultValue !== 'undefined') { + defaultValue = properties.defaultValue; + } + _this.inputValue = store.getObservableProperty(properties.id, defaultValue, path); + AmeSetting.idCounter++; + _this.uniqueInputId = 'ws-ame-gen-setting-' + AmeSetting.idCounter; + return _this; + } + AmeSetting.idCounter = 0; + return AmeSetting; +}(AmeNamedNode)); +var AmeStringSetting = /** @class */ (function (_super) { + __extends(AmeStringSetting, _super); + function AmeStringSetting(properties, module, store, path) { + if (path === void 0) { path = []; } + var _this = _super.call(this, properties, store, path) || this; + _this.syntaxHighlightingOptions = null; + _this.templateName = 'ame-tweak-textarea-input-template'; + if (properties.syntaxHighlighting && module) { + _this.syntaxHighlightingOptions = module.getCodeMirrorOptions(properties.syntaxHighlighting); + } + return _this; + } + return AmeStringSetting; +}(AmeSetting)); +var AmeColorSetting = /** @class */ (function (_super) { + __extends(AmeColorSetting, _super); + function AmeColorSetting(properties, store, path) { + if (path === void 0) { path = []; } + var _this = _super.call(this, properties, store, path) || this; + _this.templateName = 'ame-tweak-color-input-template'; + return _this; + } + return AmeColorSetting; +}(AmeSetting)); +var AmeBooleanSetting = /** @class */ (function (_super) { + __extends(AmeBooleanSetting, _super); + function AmeBooleanSetting(properties, store, path) { + if (path === void 0) { path = []; } + var _this = _super.call(this, properties, store, path) || this; + _this.templateName = 'ame-tweak-boolean-input-template'; + //Ensure that the value is always a boolean. + var _internalValue = _this.inputValue; + if (typeof _internalValue() !== 'boolean') { + _internalValue(!!_internalValue()); + } + _this.inputValue = ko.computed({ + read: function () { + return _internalValue(); + }, + write: function (newValue) { + if (typeof newValue !== 'boolean') { + newValue = !!newValue; + } + _internalValue(newValue); + }, + owner: _this + }); + return _this; + } + return AmeBooleanSetting; +}(AmeSetting)); +function isAmeActorFeatureProperties(thing) { + return (typeof thing.hasAccessMap === 'boolean'); +} +var AmeSettingStore = /** @class */ (function () { + function AmeSettingStore(initialProperties) { + if (initialProperties === void 0) { initialProperties = {}; } + this.observableProperties = {}; + this.accessMaps = {}; + this.initialProperties = initialProperties; + } + AmeSettingStore.prototype.getObservableProperty = function (name, defaultValue, path) { + if (path === void 0) { path = []; } + path = this.getFullPath(name, path); + if (this.observableProperties.hasOwnProperty(path)) { + return this.observableProperties[path]; + } + var _ = AmeTweakManagerModule._; + var value = _.get(this.initialProperties, path, defaultValue); + var observable = ko.observable(value); + this.observableProperties[path] = observable; + return observable; + }; + AmeSettingStore.prototype.getFullPath = function (name, path) { + if (typeof path !== 'string') { + path = path.join('.'); + } + if (path === '') { + path = name; + } + else { + path = path + '.' + name; + } + return path; + }; + AmeSettingStore.prototype.propertiesToJs = function () { + var _ = AmeTweakManagerModule._; + var newProps = {}; + _.forOwn(this.observableProperties, function (observable, path) { + _.set(newProps, path, observable()); + }); + _.forOwn(this.accessMaps, function (map, path) { + //Since all tweaks are disabled by default, having a tweak disabled for a role is the same + //as not having a setting, so we can save some space by removing it. This does not always + //apply to users/Super Admins because they can have precedence over roles. + var temp = map.getAll(); + var enabled = {}; + var areAllFalse = true; + for (var actorId in temp) { + if (!temp.hasOwnProperty(actorId)) { + continue; + } + areAllFalse = areAllFalse && (!temp[actorId]); + if (!temp[actorId]) { + var actor = AmeActors.getActor(actorId); + if (actor instanceof AmeRole) { + continue; + } + } + enabled[actorId] = temp[actorId]; + } + if (areAllFalse) { + enabled = {}; + } + _.set(newProps, path, enabled); + }); + return newProps; + }; + AmeSettingStore.prototype.getAccessMap = function (name, path, defaultAccessMap) { + if (path === void 0) { path = []; } + if (defaultAccessMap === void 0) { defaultAccessMap = null; } + path = this.getFullPath(name, path); + var _ = AmeTweakManagerModule._; + var value = _.get(this.initialProperties, path, defaultAccessMap); + if (!this.accessMaps.hasOwnProperty(path)) { + this.accessMaps[path] = new AmeObservableActorSettings(value); + } + return this.accessMaps[path]; + }; + return AmeSettingStore; +}()); +function isSettingStore(thing) { + var maybe = thing; + return (typeof maybe.getObservableProperty !== 'undefined') && (typeof maybe.propertiesToJs !== 'undefined'); +} +var AmeCompositeNode = /** @class */ (function (_super) { + __extends(AmeCompositeNode, _super); + function AmeCompositeNode(properties, module, store, path) { + if (store === void 0) { store = null; } + if (path === void 0) { path = []; } + var _this = _super.call(this, properties) || this; + _this.children = null; + _this.id = properties.id; + _this.label = properties.label; + if (store === 'self') { + if (!_this.properties) { + _this.properties = new AmeSettingStore(properties); + } + store = _this.properties; + } + if (isAmeSettingsGroupProperties(properties)) { + if ((typeof properties.propertyPath === 'string') && (properties.propertyPath !== '')) { + _this.propertyPath = properties.propertyPath.split('.'); + } + else { + _this.propertyPath = []; + } + if (path.length > 0) { + _this.propertyPath = path.concat(_this.propertyPath); + } + var children = []; + if (properties.children && (properties.children.length > 0)) { + for (var i = 0; i < properties.children.length; i++) { + var props = properties.children[i]; + var child = void 0; + if (isAmeSettingProperties(props)) { + child = AmeCompositeNode.createSetting(props, module, store, _this.propertyPath); + } + else { + child = new AmeCompositeNode(props, module, store, _this.propertyPath); + } + if (child) { + children.push(child); + } + } + } + _this.children = ko.observableArray(children); + } + if (isAmeActorFeatureProperties(properties)) { + var name_1 = (store === _this.properties) ? 'enabledForActor' : _this.id; + var defaultAccess = (typeof properties.defaultAccessMap !== 'undefined') ? properties.defaultAccessMap : null; + _this.actorAccess = new AmeActorAccess(store.getAccessMap(name_1, path, defaultAccess), module, _this.children); + } + return _this; + } + AmeCompositeNode.createSetting = function (properties, module, store, path) { + if (path === void 0) { path = []; } + var inputType = properties.inputType ? properties.inputType : properties.dataType; + switch (inputType) { + case 'text': + case 'textarea': + case 'string': + return new AmeStringSetting(properties, module, store, path); + case 'color': + return new AmeColorSetting(properties, store, path); + case 'boolean': + return new AmeBooleanSetting(properties, store, path); + default: + if (console && console.error) { + console.error('Unknown setting input type "%s"', inputType); + } + return null; + } + }; + return AmeCompositeNode; +}(AmeNamedNode)); +var AmeActorAccess = /** @class */ (function () { + function AmeActorAccess(actorSettings, module, children) { + if (children === void 0) { children = null; } + var _this = this; + this.module = module; + this.enabledForActor = actorSettings; + var _isIndeterminate = ko.observable(false); + this.isIndeterminate = ko.computed(function () { + if (module.selectedActor() !== null) { + return false; + } + return _isIndeterminate(); + }); + this.isChecked = ko.computed({ + read: function () { + var selectedActor = _this.module.selectedActor(); + if (selectedActor === null) { + //All: Checked only if it's checked for all actors. + var allActors = _this.module.actorSelector.getVisibleActors(); + var isEnabledForAll = true, isEnabledForAny = false; + for (var index = 0; index < allActors.length; index++) { + if (_this.enabledForActor.get(allActors[index].getId(), false)) { + isEnabledForAny = true; + } + else { + isEnabledForAll = false; + } + } + _isIndeterminate(isEnabledForAny && !isEnabledForAll); + return isEnabledForAll; + } + //Is there an explicit setting for this actor? + var ownSetting = _this.enabledForActor.get(selectedActor.getId(), null); + if (ownSetting !== null) { + return ownSetting; + } + if (selectedActor instanceof AmeUser) { + //The "Super Admin" setting takes precedence over regular roles. + if (selectedActor.isSuperAdmin) { + var superAdminSetting = _this.enabledForActor.get(AmeSuperAdmin.permanentActorId, null); + if (superAdminSetting !== null) { + return superAdminSetting; + } + } + //Is it enabled for any of the user's roles? + for (var i = 0; i < selectedActor.roles.length; i++) { + var groupSetting = _this.enabledForActor.get('role:' + selectedActor.roles[i], null); + if (groupSetting === true) { + return true; + } + } + } + //All tweaks are unchecked by default. + return false; + }, + write: function (checked) { + var selectedActor = _this.module.selectedActor(); + if (selectedActor === null) { + //Enable/disable this tweak for all actors. + if (checked === false) { + //Since false is the default, this is the same as removing/resetting all values. + _this.enabledForActor.resetAll(); + } + else { + var allActors = _this.module.actorSelector.getVisibleActors(); + for (var i = 0; i < allActors.length; i++) { + _this.enabledForActor.set(allActors[i].getId(), checked); + } + } + } + else { + _this.enabledForActor.set(selectedActor.getId(), checked); + } + //Apply the same setting to all children. + if (children) { + var childrenArray = children(); + for (var i = 0; i < childrenArray.length; i++) { + var child = childrenArray[i]; + if ((child instanceof AmeCompositeNode) && child.actorAccess) { + child.actorAccess.isChecked(checked); + } + } + } + } + }); + } + return AmeActorAccess; +}()); +var AmeTweakItem = /** @class */ (function (_super) { + __extends(AmeTweakItem, _super); + function AmeTweakItem(properties, module) { + var _this = _super.call(this, properties, module, 'self') || this; + _this.initialProperties = null; + _this.section = null; + _this.parent = null; + _this.isUserDefined = properties.isUserDefined ? properties.isUserDefined : false; + if (_this.isUserDefined) { + _this.initialProperties = properties; + } + if (_this.isUserDefined) { + _this.label = ko.observable(properties.label); + } + else { + _this.label = ko.pureComputed(function () { + return properties.label; + }); + } + _this.htmlId = 'ame-tweak-' + AmeTweakManagerModule.slugify(_this.id); + return _this; + } + AmeTweakItem.prototype.toJs = function () { + var result = { + id: this.id + }; + var _ = AmeTweakManagerModule._; + if (this.properties) { + result = _.defaults(result, this.properties.propertiesToJs()); + } + if (!this.isUserDefined) { + return result; + } + else { + var props = result; + props.isUserDefined = this.isUserDefined; + props.label = this.label(); + props.sectionId = this.section ? this.section.id : null; + props.parentId = this.parent ? this.parent.id : null; + props = _.defaults(props, _.omit(this.initialProperties, 'userInputValue', 'enabledForActor')); + return props; + } + }; + AmeTweakItem.prototype.setSection = function (section) { + this.section = section; + return this; + }; + AmeTweakItem.prototype.setParent = function (tweak) { + this.parent = tweak; + return this; + }; + AmeTweakItem.prototype.getSection = function () { + return this.section; + }; + AmeTweakItem.prototype.getParent = function () { + return this.parent; + }; + AmeTweakItem.prototype.addChild = function (tweak) { + this.children.push(tweak); + tweak.setParent(this); + return this; + }; + AmeTweakItem.prototype.removeChild = function (tweak) { + this.children.remove(tweak); + }; + AmeTweakItem.prototype.getEditableProperty = function (key) { + if (this.properties) { + return this.properties.getObservableProperty(key, ''); + } + }; + AmeTweakItem.prototype.getTypeId = function () { + if (!this.isUserDefined || !this.initialProperties) { + return null; + } + if (this.initialProperties.typeId) { + return this.initialProperties.typeId; + } + return null; + }; + return AmeTweakItem; +}(AmeCompositeNode)); +var AmeTweakSection = /** @class */ (function () { + function AmeTweakSection(properties) { + this.footerTemplateName = null; + this.id = properties.id; + this.label = properties.label; + this.isOpen = ko.observable(true); + this.tweaks = ko.observableArray([]); + } + AmeTweakSection.prototype.addTweak = function (tweak) { + this.tweaks.push(tweak); + tweak.setSection(this); + }; + AmeTweakSection.prototype.removeTweak = function (tweak) { + this.tweaks.remove(tweak); + }; + AmeTweakSection.prototype.hasContent = function () { + return this.tweaks().length > 0; + }; + AmeTweakSection.prototype.toggle = function () { + this.isOpen(!this.isOpen()); + }; + return AmeTweakSection; +}()); +var AmeTweakManagerModule = /** @class */ (function () { + function AmeTweakManagerModule(scriptData) { + var _this = this; + this.tweaksById = {}; + this.sectionsById = {}; + this.sections = []; + this.lastUserTweakSuffix = 0; + var _ = AmeTweakManagerModule._; + this.actorSelector = new AmeActorSelector(AmeActors, scriptData.isProVersion); + this.selectedActorId = this.actorSelector.createKnockoutObservable(ko); + this.selectedActor = ko.computed(function () { + var id = _this.selectedActorId(); + if (id === null) { + return null; + } + return AmeActors.getActor(id); + }); + //Reselect the previously selected actor. + this.selectedActorId(scriptData.selectedActor); + //Set syntax highlighting options. + this.cssHighlightingOptions = _.merge({}, scriptData.defaultCodeEditorSettings, { + 'codemirror': { + 'mode': 'css', + 'lint': true, + 'autoCloseBrackets': true, + 'matchBrackets': true + } + }); + //Sort sections by priority, then by label. + var sectionData = _.sortByAll(scriptData.sections, ['priority', 'label']); + //Register sections. + _.forEach(sectionData, function (properties) { + var section = new AmeTweakSection(properties); + _this.sectionsById[section.id] = section; + _this.sections.push(section); + }); + var firstSection = this.sections[0]; + _.forEach(scriptData.tweaks, function (properties) { + var tweak = new AmeTweakItem(properties, _this); + _this.tweaksById[tweak.id] = tweak; + if (properties.parentId && _this.tweaksById.hasOwnProperty(properties.parentId)) { + _this.tweaksById[properties.parentId].addChild(tweak); + } + else { + var ownerSection = firstSection; + if (properties.sectionId && _this.sectionsById.hasOwnProperty(properties.sectionId)) { + ownerSection = _this.sectionsById[properties.sectionId]; + } + ownerSection.addTweak(tweak); + } + }); + //Remove empty sections. + this.sections = _.filter(this.sections, function (section) { + return section.hasContent(); + }); + //Add the tweak creation button to the Admin CSS section. + if (this.sectionsById.hasOwnProperty('admin-css')) { + this.sectionsById['admin-css'].footerTemplateName = 'ame-admin-css-section-footer'; + } + //By default, all sections except the first one are closed. + //The user can open/close sections and we automatically remember their state. + this.openSectionIds = ko.computed({ + read: function () { + var result = []; + _.forEach(_this.sections, function (section) { + if (section.isOpen()) { + result.push(section.id); + } + }); + return result; + }, + write: function (sectionIds) { + var openSections = _.indexBy(sectionIds); + _.forEach(_this.sections, function (section) { + section.isOpen(openSections.hasOwnProperty(section.id)); + }); + } + }); + this.openSectionIds.extend({ rateLimit: { timeout: 1000, method: 'notifyWhenChangesStop' } }); + var initialState = null; + var cookieValue = jQuery.cookie(AmeTweakManagerModule.openSectionCookieName); + if ((typeof cookieValue === 'string') && JSON && JSON.parse) { + var storedState = JSON.parse(cookieValue); + if (_.isArray(storedState)) { + initialState = _.intersection(_.keys(this.sectionsById), storedState); + } + } + if (initialState !== null) { + this.openSectionIds(initialState); + } + else { + this.openSectionIds([_.first(this.sections).id]); + } + this.openSectionIds.subscribe(function (sectionIds) { + jQuery.cookie(AmeTweakManagerModule.openSectionCookieName, ko.toJSON(sectionIds), { expires: 90 }); + }); + if (scriptData.lastUserTweakSuffix) { + this.lastUserTweakSuffix = scriptData.lastUserTweakSuffix; + } + this.adminCssEditorDialog = new AmeEditAdminCssDialog(this); + this.settingsData = ko.observable(''); + this.isSaving = ko.observable(false); + } + AmeTweakManagerModule.prototype.saveChanges = function () { + this.isSaving(true); + var _ = wsAmeLodash; + var data = { + 'tweaks': _.indexBy(_.invoke(this.tweaksById, 'toJs'), 'id'), + 'lastUserTweakSuffix': this.lastUserTweakSuffix + }; + this.settingsData(ko.toJSON(data)); + return true; + }; + AmeTweakManagerModule.prototype.addAdminCssTweak = function (label, css) { + this.lastUserTweakSuffix++; + var slug = AmeTweakManagerModule.slugify(label); + if (slug !== '') { + slug = '-' + slug; + } + var props = { + label: label, + id: 'utw-' + this.lastUserTweakSuffix + slug, + isUserDefined: true, + sectionId: 'admin-css', + typeId: 'admin-css', + children: [], + hasAccessMap: true + }; + props['css'] = css; + var cssInput = { + id: 'css', + label: '', + dataType: 'string', + inputType: 'textarea', + syntaxHighlighting: 'css' + }; + props.children.push(cssInput); + var newTweak = new AmeTweakItem(props, this); + this.tweaksById[newTweak.id] = newTweak; + this.sectionsById['admin-css'].addTweak(newTweak); + }; + AmeTweakManagerModule.slugify = function (input) { + var _ = AmeTweakManagerModule._; + var output = _.deburr(input); + output = output.replace(/[^a-zA-Z0-9 \-]/, ''); + return _.kebabCase(output); + }; + AmeTweakManagerModule.prototype.launchTweakEditor = function (tweak) { + // noinspection JSRedundantSwitchStatement + switch (tweak.getTypeId()) { + case 'admin-css': + this.adminCssEditorDialog.selectedTweak = tweak; + this.adminCssEditorDialog.open(); + break; + default: + alert('Error: Editor not implemented! This is probably a bug.'); + } + }; + AmeTweakManagerModule.prototype.confirmDeleteTweak = function (tweak) { + if (!tweak.isUserDefined || !confirm('Delete this tweak?')) { + return; + } + this.deleteTweak(tweak); + }; + AmeTweakManagerModule.prototype.deleteTweak = function (tweak) { + var section = tweak.getSection(); + if (section) { + section.removeTweak(tweak); + } + var parent = tweak.getParent(); + if (parent) { + parent.removeChild(tweak); + } + delete this.tweaksById[tweak.id]; + }; + AmeTweakManagerModule.prototype.getCodeMirrorOptions = function (mode) { + if (mode === 'css') { + return this.cssHighlightingOptions; + } + return null; + }; + AmeTweakManagerModule._ = wsAmeLodash; + AmeTweakManagerModule.openSectionCookieName = 'ame_tmce_open_sections'; + return AmeTweakManagerModule; +}()); +var AmeEditAdminCssDialog = /** @class */ (function () { + function AmeEditAdminCssDialog(manager) { + var _this = this; + this.autoCancelButton = false; + this.options = { + minWidth: 400 + }; + this.selectedTweak = null; + var _ = AmeTweakManagerModule._; + this.manager = manager; + this.tweakLabel = ko.observable(''); + this.cssCode = ko.observable(''); + this.confirmButtonText = ko.observable('Add Snippet'); + this.title = ko.observable(null); + this.isAddButtonEnabled = ko.computed(function () { + return !((_.trim(_this.tweakLabel()) === '') || (_.trim(_this.cssCode()) === '')); + }); + this.isOpen = ko.observable(false); + } + AmeEditAdminCssDialog.prototype.onOpen = function (event, ui) { + if (this.selectedTweak) { + this.tweakLabel(this.selectedTweak.label()); + this.title('Edit admin CSS snippet'); + this.confirmButtonText('Save Changes'); + var cssProperty = this.selectedTweak.getEditableProperty('css'); + this.cssCode(cssProperty ? cssProperty() : ''); + } + else { + this.tweakLabel(''); + this.cssCode(''); + this.title('Add admin CSS snippet'); + this.confirmButtonText('Add Snippet'); + } + }; + AmeEditAdminCssDialog.prototype.onConfirm = function () { + if (this.selectedTweak) { + //Update the existing tweak. + this.selectedTweak.label(this.tweakLabel()); + this.selectedTweak.getEditableProperty('css')(this.cssCode()); + } + else { + //Create a new tweak. + this.manager.addAdminCssTweak(this.tweakLabel(), this.cssCode()); + } + this.close(); + }; + AmeEditAdminCssDialog.prototype.onClose = function () { + this.selectedTweak = null; + }; + AmeEditAdminCssDialog.prototype.close = function () { + this.isOpen(false); + }; + AmeEditAdminCssDialog.prototype.open = function () { + this.isOpen(true); + }; + return AmeEditAdminCssDialog; +}()); +ko.bindingHandlers.ameCodeMirror = { + init: function (element, valueAccessor, allBindings) { + if (!wp.hasOwnProperty('codeEditor') || !wp.codeEditor.initialize) { + return; + } + var parameters = ko.unwrap(valueAccessor()); + if (!parameters) { + return; + } + var options; + var refreshTrigger; + if (parameters.options) { + options = parameters.options; + if (parameters.refreshTrigger) { + refreshTrigger = parameters.refreshTrigger; + } + } + else { + options = parameters; + } + var result = wp.codeEditor.initialize(element, options); + var cm = result.codemirror; + //Synchronise the editor contents with the observable passed to the "value" binding. + var valueObservable = allBindings.get('value'); + if (!ko.isObservable(valueObservable)) { + valueObservable = null; + } + var subscription = null; + var changeHandler = null; + if (valueObservable !== null) { + //Update the observable when the contents of the editor change. + var ignoreNextUpdate_1 = false; + changeHandler = function () { + //This will trigger our observable subscription (see below). + //We need to ignore that trigger to avoid recursive or duplicated updates. + ignoreNextUpdate_1 = true; + valueObservable(cm.doc.getValue()); + }; + cm.on('changes', changeHandler); + //Update the editor when the observable changes. + subscription = valueObservable.subscribe(function (newValue) { + if (ignoreNextUpdate_1) { + ignoreNextUpdate_1 = false; + return; + } + cm.doc.setValue(newValue); + ignoreNextUpdate_1 = false; + }); + } + //Refresh the size of the editor element when an observable changes value. + var refreshSubscription = null; + if (refreshTrigger) { + refreshSubscription = refreshTrigger.subscribe(function () { + cm.refresh(); + }); + } + //If the editor starts out hidden - for example, because it's inside a collapsed section - it will + //render incorrectly. To fix that, let's refresh it the first time it becomes visible. + if (!jQuery(element).is(':visible') && (typeof IntersectionObserver !== 'undefined')) { + var observer_1 = new IntersectionObserver(function (entries) { + for (var i = 0; i < entries.length; i++) { + if (entries[i].isIntersecting) { + //The editor is at least partially visible now. + observer_1.disconnect(); + cm.refresh(); + break; + } + } + }, { + //Use the browser viewport. + root: null, + //The threshold is somewhat arbitrary. Any value will work, but a lower setting means + //that the user is less likely to see an incorrectly rendered editor. + threshold: 0.05 + }); + observer_1.observe(cm.getWrapperElement()); + } + ko.utils.domNodeDisposal.addDisposeCallback(element, function () { + //Remove subscriptions and event handlers. + if (subscription) { + subscription.dispose(); + } + if (refreshSubscription) { + refreshSubscription.dispose(); + } + if (changeHandler) { + cm.off('changes', changeHandler); + } + //Destroy the CodeMirror instance. + jQuery(cm.getWrapperElement()).remove(); + }); + } +}; +jQuery(function () { + ameTweakManager = new AmeTweakManagerModule(wsTweakManagerData); + ko.applyBindings(ameTweakManager, document.getElementById('ame-tweak-manager')); +}); +//# sourceMappingURL=tweak-manager.js.map \ No newline at end of file diff --git a/extras/modules/tweaks/tweak-manager.js.map b/extras/modules/tweaks/tweak-manager.js.map new file mode 100644 index 0000000..87cb76c --- /dev/null +++ b/extras/modules/tweaks/tweak-manager.js.map @@ -0,0 +1 @@ +{"version":3,"file":"tweak-manager.js","sourceRoot":"","sources":["tweak-manager.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/extras/modules/tweaks/tweak-manager.ts b/extras/modules/tweaks/tweak-manager.ts new file mode 100644 index 0000000..64feb72 --- /dev/null +++ b/extras/modules/tweaks/tweak-manager.ts @@ -0,0 +1,1016 @@ +/// <reference path="../../../js/knockout.d.ts" /> +/// <reference path="../../../js/jquery.d.ts" /> +/// <reference path="../../../js/lodash-3.10.d.ts" /> +/// <reference path="../../../modules/actor-selector/actor-selector.ts" /> +/// <reference path="../../../js/jquery.biscuit.d.ts" /> +/// <reference path="../../ko-extensions.ts" /> + +declare let ameTweakManager: AmeTweakManagerModule; +declare const wsTweakManagerData: AmeTweakManagerScriptData; + +declare const wp: { + codeEditor: { + initialize: (textarea: string, options: object) => any; + }; +}; + +interface AmeTweakManagerScriptData { + selectedActor: string; + isProVersion: boolean; + tweaks: AmeTweakProperties[]; + sections: AmeSectionProperties[]; + lastUserTweakSuffix: number; + defaultCodeEditorSettings: Record<string, any>; +} + +interface AmeNamedNodeProperties { + id: string; + label: string; +} + +abstract class AmeNamedNode { + id: string; + label: string | KnockoutObservable<string>; + htmlId: string = ''; + + protected constructor(properties: AmeNamedNodeProperties) { + this.id = properties.id; + this.label = properties.label; + } +} + +interface AmeSettingsGroupProperties extends AmeNamedNodeProperties { + children: ConfigurationNodeProperties[]; + propertyPath?: string | null; +} + +function isAmeSettingsGroupProperties(thing: AmeNamedNodeProperties): thing is AmeSettingsGroupProperties { + const group = thing as AmeSettingsGroupProperties; + return (typeof group.children !== 'undefined'); +} + +interface AmeSettingProperties extends AmeNamedNodeProperties { + dataType: string; + inputType: string | null; + defaultValue?: any; +} + +function isAmeSettingProperties(thing: AmeNamedNodeProperties): thing is AmeSettingProperties { + return (typeof (thing as AmeSettingProperties).dataType === 'string'); +} + +abstract class AmeSetting extends AmeNamedNode { + protected static idCounter = 0; + + // noinspection JSUnusedGlobalSymbols Used in Knockout templates. + templateName: string; + inputValue: KnockoutObservable<any>; + readonly uniqueInputId: string; + + protected constructor(properties: AmeSettingProperties, store: AmeSettingStore, path: string[] = []) { + super(properties); + let defaultValue = null; + if (typeof properties.defaultValue !== 'undefined') { + defaultValue = properties.defaultValue; + } + this.inputValue = store.getObservableProperty(properties.id, defaultValue, path); + + AmeSetting.idCounter++; + this.uniqueInputId = 'ws-ame-gen-setting-' + AmeSetting.idCounter; + } +} + +interface AmeStringSettingProperties extends AmeSettingProperties { + syntaxHighlighting?: string; +} + +class AmeStringSetting extends AmeSetting { + syntaxHighlightingOptions: object = null; + + constructor( + properties: AmeStringSettingProperties, + module: AmeTweakManagerModule, + store: AmeSettingStore, + path: string[] = [] + ) { + super(properties, store, path); + this.templateName = 'ame-tweak-textarea-input-template'; + + if (properties.syntaxHighlighting && module) { + this.syntaxHighlightingOptions = module.getCodeMirrorOptions(properties.syntaxHighlighting); + } + } +} + +class AmeColorSetting extends AmeSetting { + constructor( + properties: AmeStringSettingProperties, + store: AmeSettingStore, + path: string[] = [] + ) { + super(properties, store, path); + this.templateName = 'ame-tweak-color-input-template'; + } +} + +class AmeBooleanSetting extends AmeSetting { + public templateName: string = 'ame-tweak-boolean-input-template'; + + constructor( + properties: AmeStringSettingProperties, + store: AmeSettingStore, + path: string[] = [] + ) { + super(properties, store, path); + + //Ensure that the value is always a boolean. + let _internalValue = this.inputValue; + if (typeof _internalValue() !== 'boolean') { + _internalValue(!!_internalValue()); + } + + this.inputValue = ko.computed<boolean>({ + read: function () { + return _internalValue(); + }, + write: function (newValue) { + if (typeof newValue !== 'boolean') { + newValue = !!newValue; + } + _internalValue(newValue); + }, + owner: this + }); + } +} + +interface AmeActorFeatureProperties extends AmeSettingsGroupProperties { + hasAccessMap: true; + defaultAccessMap?: AmeDictionary<boolean>; + enabledForActor?: AmeDictionary<boolean>; +} + +function isAmeActorFeatureProperties(thing: AmeNamedNodeProperties): thing is AmeActorFeatureProperties { + return (typeof (thing as AmeActorFeatureProperties).hasAccessMap === 'boolean'); +} + +type ConfigurationNodeProperties = AmeActorFeatureProperties | AmeSettingProperties | AmeSettingsGroupProperties; + +class AmeSettingStore { + private observableProperties: Record<string, KnockoutObservable<any>> = {}; + private accessMaps: Record<string, AmeObservableActorSettings> = {}; + private readonly initialProperties: Record<string, any>; + + constructor(initialProperties: Record<string, any> = {}) { + this.initialProperties = initialProperties; + } + + getObservableProperty<T>(name: string, defaultValue: T, path: string | string[] = []): KnockoutObservable<T> { + path = this.getFullPath(name, path); + + if (this.observableProperties.hasOwnProperty(path)) { + return this.observableProperties[path]; + } + + const _ = AmeTweakManagerModule._; + const value = _.get(this.initialProperties, path, defaultValue); + const observable = ko.observable(value); + this.observableProperties[path] = observable; + return observable; + } + + protected getFullPath(name: string, path: string | string[]): string { + if (typeof path !== 'string') { + path = path.join('.'); + } + if (path === '') { + path = name; + } else { + path = path + '.' + name; + } + return path; + } + + propertiesToJs(): Record<string, any> { + const _ = AmeTweakManagerModule._; + let newProps = {}; + _.forOwn(this.observableProperties, function (observable, path) { + _.set(newProps, path, observable()); + }); + + _.forOwn(this.accessMaps, function (map, path: string) { + //Since all tweaks are disabled by default, having a tweak disabled for a role is the same + //as not having a setting, so we can save some space by removing it. This does not always + //apply to users/Super Admins because they can have precedence over roles. + let temp = map.getAll(); + let enabled: AmeDictionary<boolean> = {}; + let areAllFalse = true; + for (let actorId in temp) { + if (!temp.hasOwnProperty(actorId)) { + continue; + } + + areAllFalse = areAllFalse && (!temp[actorId]); + if (!temp[actorId]) { + const actor = AmeActors.getActor(actorId); + if (actor instanceof AmeRole) { + continue; + } + } + enabled[actorId] = temp[actorId]; + } + + if (areAllFalse) { + enabled = {}; + } + + _.set(newProps, path, enabled); + }); + + return newProps; + } + + getAccessMap( + name: string, + path: string | string[] = [], + defaultAccessMap: AmeDictionary<boolean> | null = null + ): AmeObservableActorSettings { + path = this.getFullPath(name, path); + const _ = AmeTweakManagerModule._; + const value = _.get(this.initialProperties, path, defaultAccessMap); + + if (!this.accessMaps.hasOwnProperty(path)) { + this.accessMaps[path] = new AmeObservableActorSettings(value); + + } + return this.accessMaps[path]; + } +} + +function isSettingStore(thing: object): thing is AmeSettingStore { + const maybe = thing as AmeSettingStore; + return (typeof maybe.getObservableProperty !== 'undefined') && (typeof maybe.propertiesToJs !== 'undefined'); +} + +class AmeCompositeNode extends AmeNamedNode { + children: KnockoutObservableArray<AmeNamedNode> = null; + propertyPath: string[]; + actorAccess: AmeActorAccess; + properties: AmeSettingStore; + + constructor( + properties: ConfigurationNodeProperties, + module: AmeTweakManagerModule, + store: AmeSettingStore | 'self' = null, + path: string[] = [] + ) { + super(properties); + this.id = properties.id; + this.label = properties.label; + + if (store === 'self') { + if (!this.properties) { + this.properties = new AmeSettingStore(properties); + } + store = this.properties; + } + + if (isAmeSettingsGroupProperties(properties)) { + if ((typeof properties.propertyPath === 'string') && (properties.propertyPath !== '')) { + this.propertyPath = properties.propertyPath.split('.'); + } else { + this.propertyPath = []; + } + if (path.length > 0) { + this.propertyPath = path.concat(this.propertyPath); + } + + let children = []; + if (properties.children && (properties.children.length > 0)) { + for (let i = 0; i < properties.children.length; i++) { + const props = properties.children[i]; + let child; + if (isAmeSettingProperties(props)) { + child = AmeCompositeNode.createSetting(props, module, store, this.propertyPath); + } else { + child = new AmeCompositeNode(props, module, store, this.propertyPath); + } + if (child) { + children.push(child); + } + } + } + + this.children = ko.observableArray(children); + } + + if (isAmeActorFeatureProperties(properties)) { + let name = (store === this.properties) ? 'enabledForActor' : this.id; + const defaultAccess = (typeof properties.defaultAccessMap !== 'undefined') ? properties.defaultAccessMap : null; + this.actorAccess = new AmeActorAccess( + store.getAccessMap(name, path, defaultAccess), + module, + this.children + ); + } + } + + static createSetting( + properties: AmeSettingProperties, + module: AmeTweakManagerModule, + store: AmeSettingStore, + path: string[] = [] + ): AmeSetting { + const inputType = properties.inputType ? properties.inputType : properties.dataType; + + switch (inputType) { + case 'text': + case 'textarea': + case 'string': + return new AmeStringSetting(properties, module, store, path); + case 'color': + return new AmeColorSetting(properties, store, path); + case 'boolean': + return new AmeBooleanSetting(properties, store, path); + default: + if (console && console.error) { + console.error('Unknown setting input type "%s"', inputType); + } + return null; + } + } +} + +class AmeActorAccess { + isChecked: KnockoutComputed<boolean>; + protected enabledForActor: AmeObservableActorSettings; + protected module: AmeTweakManagerModule; + isIndeterminate: KnockoutComputed<boolean>; + + constructor( + actorSettings: AmeObservableActorSettings, + module: AmeTweakManagerModule, + children: AmeCompositeNode['children'] = null + ) { + this.module = module; + this.enabledForActor = actorSettings; + + let _isIndeterminate = ko.observable<boolean>(false); + this.isIndeterminate = ko.computed<boolean>(() => { + if (module.selectedActor() !== null) { + return false; + } + return _isIndeterminate(); + }); + + this.isChecked = ko.computed<boolean>({ + read: () => { + const selectedActor = this.module.selectedActor(); + + if (selectedActor === null) { + //All: Checked only if it's checked for all actors. + const allActors = this.module.actorSelector.getVisibleActors(); + let isEnabledForAll = true, isEnabledForAny = false; + for (let index = 0; index < allActors.length; index++) { + if (this.enabledForActor.get(allActors[index].getId(), false)) { + isEnabledForAny = true; + } else { + isEnabledForAll = false; + } + } + + _isIndeterminate(isEnabledForAny && !isEnabledForAll); + + return isEnabledForAll; + } + + //Is there an explicit setting for this actor? + let ownSetting = this.enabledForActor.get(selectedActor.getId(), null); + if (ownSetting !== null) { + return ownSetting; + } + + if (selectedActor instanceof AmeUser) { + //The "Super Admin" setting takes precedence over regular roles. + if (selectedActor.isSuperAdmin) { + let superAdminSetting = this.enabledForActor.get(AmeSuperAdmin.permanentActorId, null); + if (superAdminSetting !== null) { + return superAdminSetting; + } + } + + //Is it enabled for any of the user's roles? + for (let i = 0; i < selectedActor.roles.length; i++) { + let groupSetting = this.enabledForActor.get('role:' + selectedActor.roles[i], null); + if (groupSetting === true) { + return true; + } + } + } + + //All tweaks are unchecked by default. + return false; + }, + write: (checked: boolean) => { + const selectedActor = this.module.selectedActor(); + if (selectedActor === null) { + //Enable/disable this tweak for all actors. + if (checked === false) { + //Since false is the default, this is the same as removing/resetting all values. + this.enabledForActor.resetAll(); + } else { + const allActors = this.module.actorSelector.getVisibleActors(); + for (let i = 0; i < allActors.length; i++) { + this.enabledForActor.set(allActors[i].getId(), checked); + } + } + } else { + this.enabledForActor.set(selectedActor.getId(), checked); + } + + //Apply the same setting to all children. + if (children) { + const childrenArray = children(); + for (let i = 0; i < childrenArray.length; i++) { + const child = childrenArray[i]; + if ((child instanceof AmeCompositeNode) && child.actorAccess) { + child.actorAccess.isChecked(checked); + } + } + } + } + }); + } +} + +interface AmeSavedTweakProperties { + id: string; + enabledForActor?: AmeDictionary<boolean>; +} + +interface AmeTweakProperties extends AmeSavedTweakProperties, AmeActorFeatureProperties { + description?: string; + parentId?: string; + sectionId?: string; + + isUserDefined?: boolean; + typeId?: string; + + //User-defined tweaks can have additional arbitrary properties. + [key: string]: any; +} + +class AmeTweakItem extends AmeCompositeNode { + label: KnockoutObservable<string>; + + public readonly isUserDefined: boolean; + private readonly initialProperties: AmeSavedTweakProperties = null; + + private section: AmeTweakSection = null; + private parent: AmeTweakItem = null; + + constructor(properties: AmeTweakProperties, module: AmeTweakManagerModule) { + super(properties, module, 'self'); + + this.isUserDefined = properties.isUserDefined ? properties.isUserDefined : false; + if (this.isUserDefined) { + this.initialProperties = properties; + } + + if (this.isUserDefined) { + this.label = ko.observable(properties.label); + } else { + this.label = ko.pureComputed(function () { + return properties.label; + }); + } + + this.htmlId = 'ame-tweak-' + AmeTweakManagerModule.slugify(this.id); + } + + toJs(): AmeSavedTweakProperties { + let result: AmeSavedTweakProperties = { + id: this.id + }; + + const _ = AmeTweakManagerModule._; + if (this.properties) { + result = _.defaults(result, this.properties.propertiesToJs()); + } + + if (!this.isUserDefined) { + return result; + } else { + let props: AmeTweakProperties = result as AmeTweakProperties; + props.isUserDefined = this.isUserDefined; + props.label = this.label(); + props.sectionId = this.section ? this.section.id : null; + props.parentId = this.parent ? this.parent.id : null; + + props = _.defaults( + props, + _.omit(this.initialProperties, 'userInputValue', 'enabledForActor') + ); + return props; + } + } + + setSection(section: AmeTweakSection) { + this.section = section; + return this; + } + + setParent(tweak: AmeTweakItem) { + this.parent = tweak; + return this; + } + + getSection(): AmeTweakSection { + return this.section; + } + + getParent(): AmeTweakItem { + return this.parent; + } + + addChild(tweak: AmeTweakItem) { + this.children.push(tweak); + tweak.setParent(this); + return this; + } + + removeChild(tweak: AmeTweakItem) { + this.children.remove(tweak); + } + + getEditableProperty(key: string): KnockoutObservable<any> { + if (this.properties) { + return this.properties.getObservableProperty(key, ''); + } + } + + getTypeId(): string | null { + if (!this.isUserDefined || !this.initialProperties) { + return null; + } + if ((this.initialProperties as AmeTweakProperties).typeId) { + return (this.initialProperties as AmeTweakProperties).typeId; + } + return null; + } +} + +interface AmeSectionProperties { + id: string; + label: string; + priority: number | null; +} + +class AmeTweakSection { + id: string; + label: string; + tweaks: KnockoutObservableArray<AmeTweakItem>; + isOpen: KnockoutObservable<boolean>; + + footerTemplateName: string = null; + + constructor(properties: AmeSectionProperties) { + this.id = properties.id; + this.label = properties.label; + this.isOpen = ko.observable<boolean>(true); + this.tweaks = ko.observableArray([]); + } + + addTweak(tweak: AmeTweakItem) { + this.tweaks.push(tweak); + tweak.setSection(this); + } + + removeTweak(tweak: AmeTweakItem) { + this.tweaks.remove(tweak); + } + + hasContent() { + return this.tweaks().length > 0; + } + + toggle() { + this.isOpen(!this.isOpen()); + } +} + +class AmeTweakManagerModule { + static _ = wsAmeLodash; + static readonly openSectionCookieName = 'ame_tmce_open_sections'; + + readonly actorSelector: AmeActorSelector; + selectedActorId: KnockoutComputed<string>; + selectedActor: KnockoutComputed<IAmeActor>; + + private tweaksById: { [id: string]: AmeTweakItem } = {}; + private sectionsById: AmeDictionary<AmeTweakSection> = {}; + sections: AmeTweakSection[] = []; + + settingsData: KnockoutObservable<string>; + isSaving: KnockoutObservable<boolean>; + + private readonly openSectionIds: KnockoutComputed<string[]>; + + readonly adminCssEditorDialog: AmeEditAdminCssDialog; + private lastUserTweakSuffix: number = 0; + + public readonly cssHighlightingOptions: Record<string, any>; + + constructor(scriptData: AmeTweakManagerScriptData) { + const _ = AmeTweakManagerModule._; + + this.actorSelector = new AmeActorSelector(AmeActors, scriptData.isProVersion); + this.selectedActorId = this.actorSelector.createKnockoutObservable(ko); + this.selectedActor = ko.computed<IAmeActor>(() => { + const id = this.selectedActorId(); + if (id === null) { + return null; + } + return AmeActors.getActor(id); + }); + + //Reselect the previously selected actor. + this.selectedActorId(scriptData.selectedActor); + + //Set syntax highlighting options. + this.cssHighlightingOptions = _.merge( + {}, + scriptData.defaultCodeEditorSettings, + { + 'codemirror': { + 'mode': 'css', + 'lint': true, + 'autoCloseBrackets': true, + 'matchBrackets': true + } + } + ); + + //Sort sections by priority, then by label. + let sectionData = _.sortByAll(scriptData.sections, ['priority', 'label']); + //Register sections. + _.forEach(sectionData, (properties) => { + let section = new AmeTweakSection(properties); + this.sectionsById[section.id] = section; + this.sections.push(section); + }); + const firstSection = this.sections[0]; + + _.forEach(scriptData.tweaks, (properties) => { + const tweak = new AmeTweakItem(properties, this); + this.tweaksById[tweak.id] = tweak; + + if (properties.parentId && this.tweaksById.hasOwnProperty(properties.parentId)) { + this.tweaksById[properties.parentId].addChild(tweak); + } else { + let ownerSection = firstSection; + if (properties.sectionId && this.sectionsById.hasOwnProperty(properties.sectionId)) { + ownerSection = this.sectionsById[properties.sectionId]; + } + ownerSection.addTweak(tweak); + } + }); + + //Remove empty sections. + this.sections = _.filter(this.sections, function (section) { + return section.hasContent(); + }); + + //Add the tweak creation button to the Admin CSS section. + if (this.sectionsById.hasOwnProperty('admin-css')) { + this.sectionsById['admin-css'].footerTemplateName = 'ame-admin-css-section-footer'; + } + + //By default, all sections except the first one are closed. + //The user can open/close sections and we automatically remember their state. + this.openSectionIds = ko.computed<string[]>({ + read: () => { + let result = []; + _.forEach(this.sections, section => { + if (section.isOpen()) { + result.push(section.id); + } + }); + return result; + }, + write: (sectionIds: string[]) => { + const openSections = _.indexBy(sectionIds); + _.forEach(this.sections, section => { + section.isOpen(openSections.hasOwnProperty(section.id)); + }); + } + }); + this.openSectionIds.extend({rateLimit: {timeout: 1000, method: 'notifyWhenChangesStop'}}); + + let initialState: string[] = null; + let cookieValue = jQuery.cookie(AmeTweakManagerModule.openSectionCookieName); + if ((typeof cookieValue === 'string') && JSON && JSON.parse) { + let storedState = JSON.parse(cookieValue); + if (_.isArray<string>(storedState)) { + initialState = _.intersection(_.keys(this.sectionsById), storedState); + } + } + + if (initialState !== null) { + this.openSectionIds(initialState); + } else { + this.openSectionIds([_.first(this.sections).id]); + } + + this.openSectionIds.subscribe((sectionIds) => { + jQuery.cookie(AmeTweakManagerModule.openSectionCookieName, ko.toJSON(sectionIds), {expires: 90}); + }); + + if (scriptData.lastUserTweakSuffix) { + this.lastUserTweakSuffix = scriptData.lastUserTweakSuffix; + } + + this.adminCssEditorDialog = new AmeEditAdminCssDialog(this); + + this.settingsData = ko.observable<string>(''); + this.isSaving = ko.observable<boolean>(false); + } + + saveChanges() { + this.isSaving(true); + const _ = wsAmeLodash; + + let data = { + 'tweaks': _.indexBy(_.invoke(this.tweaksById, 'toJs'), 'id'), + 'lastUserTweakSuffix': this.lastUserTweakSuffix + }; + this.settingsData(ko.toJSON(data)); + return true; + } + + addAdminCssTweak(label: string, css: string) { + this.lastUserTweakSuffix++; + + let slug = AmeTweakManagerModule.slugify(label); + if (slug !== '') { + slug = '-' + slug; + } + + let props: AmeTweakProperties = { + label: label, + id: 'utw-' + this.lastUserTweakSuffix + slug, + isUserDefined: true, + sectionId: 'admin-css', + typeId: 'admin-css', + children: [], + hasAccessMap: true + }; + props['css'] = css; + + const cssInput: AmeStringSettingProperties = { + id: 'css', + label: '', + dataType: 'string', + inputType: 'textarea', + syntaxHighlighting: 'css' + }; + props.children.push(cssInput); + + const newTweak = new AmeTweakItem(props, this); + this.tweaksById[newTweak.id] = newTweak; + this.sectionsById['admin-css'].addTweak(newTweak) + } + + static slugify(input: string): string { + const _ = AmeTweakManagerModule._; + let output = _.deburr(input); + output = output.replace(/[^a-zA-Z0-9 \-]/, ''); + return _.kebabCase(output); + } + + launchTweakEditor(tweak: AmeTweakItem) { + // noinspection JSRedundantSwitchStatement + switch (tweak.getTypeId()) { + case 'admin-css': + this.adminCssEditorDialog.selectedTweak = tweak; + this.adminCssEditorDialog.open(); + break; + default: + alert('Error: Editor not implemented! This is probably a bug.'); + } + } + + confirmDeleteTweak(tweak: AmeTweakItem) { + if (!tweak.isUserDefined || !confirm('Delete this tweak?')) { + return; + } + this.deleteTweak(tweak); + } + + protected deleteTweak(tweak: AmeTweakItem) { + const section = tweak.getSection(); + if (section) { + section.removeTweak(tweak); + } + const parent = tweak.getParent(); + if (parent) { + parent.removeChild(tweak); + } + delete this.tweaksById[tweak.id]; + } + + getCodeMirrorOptions(mode: string) { + if (mode === 'css') { + return this.cssHighlightingOptions; + } + return null; + } +} + +class AmeEditAdminCssDialog implements AmeKnockoutDialog { + jQueryWidget: JQuery; + isOpen: KnockoutObservable<boolean>; + autoCancelButton: boolean = false; + + options: AmeDictionary<any> = { + minWidth: 400 + }; + + isAddButtonEnabled: KnockoutComputed<boolean>; + tweakLabel: KnockoutObservable<string>; + cssCode: KnockoutObservable<string>; + confirmButtonText: KnockoutObservable<string>; + title: KnockoutObservable<string>; + + selectedTweak: AmeTweakItem = null; + + private manager: AmeTweakManagerModule; + + constructor(manager: AmeTweakManagerModule) { + const _ = AmeTweakManagerModule._; + this.manager = manager; + + this.tweakLabel = ko.observable(''); + this.cssCode = ko.observable(''); + this.confirmButtonText = ko.observable('Add Snippet'); + this.title = ko.observable(null); + + this.isAddButtonEnabled = ko.computed(() => { + return !((_.trim(this.tweakLabel()) === '') || (_.trim(this.cssCode()) === '')); + }); + this.isOpen = ko.observable(false); + } + + onOpen(event, ui) { + if (this.selectedTweak) { + this.tweakLabel(this.selectedTweak.label()); + this.title('Edit admin CSS snippet'); + this.confirmButtonText('Save Changes'); + + const cssProperty = this.selectedTweak.getEditableProperty('css'); + this.cssCode(cssProperty ? cssProperty() : ''); + } else { + this.tweakLabel(''); + this.cssCode(''); + this.title('Add admin CSS snippet'); + this.confirmButtonText('Add Snippet'); + } + } + + onConfirm() { + if (this.selectedTweak) { + //Update the existing tweak. + this.selectedTweak.label(this.tweakLabel()); + this.selectedTweak.getEditableProperty('css')(this.cssCode()); + } else { + //Create a new tweak. + this.manager.addAdminCssTweak( + this.tweakLabel(), + this.cssCode() + ); + } + this.close(); + } + + onClose() { + this.selectedTweak = null; + } + + close() { + this.isOpen(false); + } + + open() { + this.isOpen(true); + } +} + +ko.bindingHandlers.ameCodeMirror = { + init: function (element, valueAccessor, allBindings) { + if (!wp.hasOwnProperty('codeEditor') || !wp.codeEditor.initialize) { + return; + } + let parameters = ko.unwrap(valueAccessor()); + if (!parameters) { + return; + } + + let options; + let refreshTrigger: KnockoutObservable<any>; + if (parameters.options) { + options = parameters.options; + if (parameters.refreshTrigger) { + refreshTrigger = parameters.refreshTrigger; + } + } else { + options = parameters; + } + + let result = wp.codeEditor.initialize(element, options); + const cm = result.codemirror; + + //Synchronise the editor contents with the observable passed to the "value" binding. + let valueObservable: KnockoutObservable<any> = allBindings.get('value'); + if (!ko.isObservable(valueObservable)) { + valueObservable = null; + } + + let subscription = null; + let changeHandler = null; + if (valueObservable !== null) { + //Update the observable when the contents of the editor change. + let ignoreNextUpdate = false; + changeHandler = function () { + //This will trigger our observable subscription (see below). + //We need to ignore that trigger to avoid recursive or duplicated updates. + ignoreNextUpdate = true; + valueObservable(cm.doc.getValue()); + }; + cm.on('changes', changeHandler); + + //Update the editor when the observable changes. + subscription = valueObservable.subscribe(function (newValue) { + if (ignoreNextUpdate) { + ignoreNextUpdate = false; + return; + } + cm.doc.setValue(newValue); + ignoreNextUpdate = false; + }); + } + + //Refresh the size of the editor element when an observable changes value. + let refreshSubscription: KnockoutSubscription = null; + if (refreshTrigger) { + refreshSubscription = refreshTrigger.subscribe(function () { + cm.refresh(); + }); + } + + //If the editor starts out hidden - for example, because it's inside a collapsed section - it will + //render incorrectly. To fix that, let's refresh it the first time it becomes visible. + if (!jQuery(element).is(':visible') && (typeof IntersectionObserver !== 'undefined')) { + const observer = new IntersectionObserver( + function (entries) { + for (let i = 0; i < entries.length; i++) { + if (entries[i].isIntersecting) { + //The editor is at least partially visible now. + observer.disconnect(); + cm.refresh(); + break; + } + } + }, + { + //Use the browser viewport. + root: null, + //The threshold is somewhat arbitrary. Any value will work, but a lower setting means + //that the user is less likely to see an incorrectly rendered editor. + threshold: 0.05 + } + ); + observer.observe(cm.getWrapperElement()); + } + + ko.utils.domNodeDisposal.addDisposeCallback(element, function () { + //Remove subscriptions and event handlers. + if (subscription) { + subscription.dispose(); + } + if (refreshSubscription) { + refreshSubscription.dispose(); + } + if (changeHandler) { + cm.off('changes', changeHandler); + } + + //Destroy the CodeMirror instance. + jQuery(cm.getWrapperElement()).remove(); + }); + } +}; + +jQuery(function () { + ameTweakManager = new AmeTweakManagerModule(wsTweakManagerData); + ko.applyBindings(ameTweakManager, document.getElementById('ame-tweak-manager')); +}); \ No newline at end of file diff --git a/extras/modules/tweaks/tweaks-template.php b/extras/modules/tweaks/tweaks-template.php new file mode 100644 index 0000000..62004eb --- /dev/null +++ b/extras/modules/tweaks/tweaks-template.php @@ -0,0 +1,230 @@ +<?php +/** + * @var string $moduleTabUrl Fully qualified URL of the tab. + */ + +?> +<div id="ame-tweak-manager"> + <?php require AME_ROOT_DIR . '/modules/actor-selector/actor-selector-template.php'; ?> + + <div data-bind="foreach: sections"> + <div class="ame-twm-section" data-bind="css: { 'ws-ame-closed-postbox': !isOpen() }"> + <div class="ws-ame-postbox-header"> + <h3 data-bind="text: label"></h3> + <button class="ws-ame-postbox-toggle" data-bind="click: toggle"></button> + </div> + <div class="ws-ame-postbox-content"> + <div data-bind="template: {name: 'ame-named-node-template', foreach: tweaks}"></div> + <!-- ko if: footerTemplateName --> + <!-- ko template: { + name: $data.footerTemplateName, + data: $data + } --> + <!-- /ko --> + <!-- /ko --> + </div> + </div> + </div> + + <form method="post" data-bind="submit: saveChanges" class="ame-twm-save-form" action="<?php + echo esc_attr(add_query_arg(array('noheader' => '1'), $moduleTabUrl)); + ?>"> + + <?php + submit_button( + 'Save Changes', + 'primary', + 'submit', + true, + array( + 'data-bind' => 'disable: isSaving', + 'disabled' => 'disabled', + ) + ); + ?> + + <input type="hidden" name="action" value="ame-save-tweak-settings"> + <?php wp_nonce_field('ame-save-tweak-settings'); ?> + + <input type="hidden" name="settings" value="" data-bind="value: settingsData"> + <input type="hidden" name="selected_actor" value="" data-bind="value: selectedActorId"> + </form> + + <div id="ame-twm-add-admin-css-dialog" + data-bind="ameDialog: adminCssEditorDialog" + title="Add admin CSS snippet" + style="display: none;" class="ame-twm-dialog"> + <div class="ws_dialog_subpanel"> + <label for="ame-twm-new-css-tweak-label"><strong>Name</strong></label><br> + <input type="text" id="ame-twm-new-css-tweak-label" class="large-text" + data-bind="textInput: adminCssEditorDialog.tweakLabel"> + </div> + + <div class="ws_dialog_subpanel"> + <label for="ame-twm-new-css-tweak-code"><strong>CSS code</strong></label><br> + <textarea id="ame-twm-new-css-tweak-code" cols="40" rows="6" + data-bind="value: adminCssEditorDialog.cssCode, + ameCodeMirror: {options: $root.cssHighlightingOptions, refreshTrigger: adminCssEditorDialog.isOpen}"></textarea> + </div> + + <div class="ws_dialog_buttons"> + <?php + submit_button( + 'Add Snippet', + 'primary', + 'ame-twm-confirm-css-addition', + false, + array( + 'data-bind' => + 'enable : adminCssEditorDialog.isAddButtonEnabled, ' + . 'click: adminCssEditorDialog.onConfirm.bind(adminCssEditorDialog), ' + . 'value: adminCssEditorDialog.confirmButtonText', + ) + ); + + submit_button( + 'Cancel', + 'secondary', + 'ame-twm-cancel-css-addition', + false, + array('data-bind' => 'click: adminCssEditorDialog.close.bind(adminCssEditorDialog)') + ); + ?> + </div> + </div> +</div> + +<div style="display: none;"> + <template id="ame-named-node-template"> + <!-- ko if: ($data.templateName) --> + <!-- ko template: { + name: $data.templateName, + data: $data + } --> + <!-- /ko --> + <!-- /ko --> + + <!-- ko ifnot: ($data.templateName) --> + <div class="ame-twm-named-node" + data-bind="css: {'ame-twm-tweak': ($data instanceof AmeTweakItem)}, attr: {'id': $data.htmlId}"> + <label class="ame-twm-tweak-label"> + <!-- ko if: $data.actorAccess --> + <input type="checkbox" + data-bind="checked: actorAccess.isChecked, indeterminate: actorAccess.isIndeterminate"> + <!-- /ko --> + <span data-bind="text: label"></span> + </label> + <!-- ko if: $data.isUserDefined --> + <span class="ame-twm-tweak-actions"> + <a href="#" class="ame-twm-action ame-twm-edit-tweak" title="Edit tweak" + data-bind="click: $root.launchTweakEditor.bind($root)" + ><span class="dashicons dashicons-edit"> + </span></a + ><a href="#" + class="ame-twm-action ame-twm-delete-tweak" + title="Delete tweak" + data-bind="click: $root.confirmDeleteTweak.bind($root)" + ><span class="dashicons dashicons-trash"></span></a> + </span> + <!-- /ko --> + + <!-- ko if: ($data.userInput) --> + <!-- ko template: { + name: $data.userInput.templateName, + data: $data.userInput + } --> + <!-- /ko --> + <!-- /ko --> + + <!-- ko if: $data.children && (children().length > 0) --> + <div class="ame-twm-tweak-children" + data-bind="template: {name: 'ame-named-node-template', foreach: children}"></div> + <!-- /ko --> + </div> + <!-- /ko --> + </template> + + <template id="ame-tweak-item-template"> + <div class="ame-twm-tweak"> + <label class="ame-twm-tweak-label"> + <!-- ko if: $data.actorAccess --> + <input type="checkbox" + data-bind="checked: actorAccess.isChecked, indeterminate: actorAccess.isIndeterminate"> + <!-- /ko --> + <span data-bind="text: label"></span> + </label> + <!-- ko if: $data.isUserDefined --> + <span class="ame-twm-tweak-actions"> + <a href="#" class="ame-twm-action ame-twm-edit-tweak" title="Edit tweak" + data-bind="click: $root.launchTweakEditor.bind($root)" + ><span class="dashicons dashicons-edit"> + </span></a + ><a href="#" + class="ame-twm-action ame-twm-delete-tweak" + title="Delete tweak" + data-bind="click: $root.confirmDeleteTweak.bind($root)" + ><span class="dashicons dashicons-trash"></span></a> + </span> + <!-- /ko --> + + <!-- ko if: ($data.userInput) --> + <!-- ko template: { + name: $data.userInput.templateName, + data: $data.userInput + } --> + <!-- /ko --> + <!-- /ko --> + + <!-- ko if: $data.children && (children().length > 0) --> + <div class="ame-twm-tweak-children" + data-bind="template: {name: 'ame-tweak-item-template', foreach: children}"></div> + <!-- /ko --> + </div> + </template> + + <template id="ame-tweak-textarea-input-template"> + <div class="ame-twm-user-input"> + <label> + <span class="screen-reader-text" data-bind="text: $data.label"></span> + <textarea cols="80" rows="5" class="large-text code" + data-bind="value: $data.inputValue, + ameCodeMirror: $data.syntaxHighlightingOptions"></textarea> + </label> + </div> + </template> + + <template id="ame-tweak-color-input-template"> + <div class="ame-twm-user-input ame-twm-color-input"> + <label data-bind="attr: {'for': $data.uniqueInputId}" class="ame-twm-color-label"> + <span data-bind="text: $data.label"></span> + </label> + <!--suppress HtmlFormInputWithoutLabel --> + <input type="text" data-bind="ameColorPicker: $data.inputValue, attr: {'id': $data.uniqueInputId}"> + </div> + </template> + + <template id="ame-tweak-boolean-input-template"> + <div class="ame-twm-user-input ame-twm-boolean-input"> + <label> + <input type="checkbox" data-bind="checked: $data.inputValue"> + <span data-bind="text: $data.label"></span> + </label> + </div> + </template> + + <template id="ame-admin-css-section-footer"> + <p> + <?php + submit_button( + 'Add CSS snippet', + 'secondary', + 'ame-twm-add-css-tweak', + false, + array( + 'data-bind' => 'ameOpenDialog: "#ame-twm-add-admin-css-dialog"', + ) + ); + ?> + </p> + </template> +</div> \ No newline at end of file diff --git a/extras/modules/tweaks/tweaks.css b/extras/modules/tweaks/tweaks.css new file mode 100644 index 0000000..c2fc37f --- /dev/null +++ b/extras/modules/tweaks/tweaks.css @@ -0,0 +1,174 @@ +@charset "UTF-8"; +.ame-twm-section { + position: relative; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); + background: #fff; + margin-bottom: 20px; + max-width: 630px; +} +.ame-twm-section .ws-ame-postbox-header { + position: relative; + font-size: 14px; + margin: 0; + line-height: 1.4; + border: 1px solid #ccd0d4; +} +.ame-twm-section .ws-ame-postbox-header h3 { + padding: 10px 12px; + margin: 0; + font-size: 1em; + line-height: 1; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} +.ame-twm-section .ws-ame-postbox-toggle { + color: #72777c; + background: #fff; + display: block; + font: normal 20px/1 dashicons; + text-align: center; + cursor: pointer; + border: none; + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 36px; + height: 100%; + padding: 0; +} +.ame-twm-section .ws-ame-postbox-toggle:hover { + color: #23282d; +} +.ame-twm-section .ws-ame-postbox-toggle:active, .ame-twm-section .ws-ame-postbox-toggle:focus { + outline: none; + padding: 0; +} +.ame-twm-section .ws-ame-postbox-toggle:before { + content: "\f142"; + display: inline-block; + vertical-align: middle; +} +.ame-twm-section .ws-ame-postbox-toggle:after { + display: inline-block; + content: ""; + vertical-align: middle; + height: 100%; +} +.ame-twm-section .ws-ame-postbox-content { + border: 1px solid #ccd0d4; + border-top: none; + padding: 12px; +} +.ame-twm-section.ws-ame-closed-postbox .ws-ame-postbox-content { + display: none; +} +.ame-twm-section.ws-ame-closed-postbox .ws-ame-postbox-toggle:before { + content: "\f140"; +} +.ame-twm-section .ws-ame-postbox-content { + padding-top: 8px; +} + +.ame-twm-tweak-children { + margin-left: 1.7em; +} +.ame-twm-tweak-children > .ame-twm-tweak { + margin-left: 0.3em; +} + +.ame-twm-named-node input[type=checkbox]:indeterminate:before { + content: "■"; + color: #1e8cbe; + margin: -3px 0 0 -1px; + font: 400 14px/1 dashicons; + float: left; + display: inline-block; + vertical-align: middle; + width: 16px; + -webkit-font-smoothing: antialiased; +} +@media screen and (max-width: 782px) { + .ame-twm-named-node input[type=checkbox]:indeterminate:before { + height: 1.5625rem; + width: 1.5625rem; + line-height: 1.5625rem; + margin: -1px; + font-size: 18px; + font-family: unset; + font-weight: normal; + } +} + +.ame-twm-named-node { + font-size: 14px; + line-height: 1.65; +} + +#ame-tweak-manager #ws_actor_selector_container { + margin-bottom: 8px; +} + +#ame-twm-new-css-tweak-code { + width: 99%; +} + +.ame-twm-tweak-actions { + vertical-align: middle; + margin-left: 1em; + display: none; +} + +.ame-twm-tweak:hover .ame-twm-tweak-actions { + display: inline-block; +} + +.ame-twm-action { + text-decoration: none; + color: #595959; + cursor: pointer; + padding: 0 0.5em; +} +.ame-twm-action > .dashicons { + vertical-align: text-bottom; +} +.ame-twm-action:hover { + text-decoration: none; +} +.ame-twm-action.ame-twm-delete-tweak:hover { + color: #a00; +} + +.ame-twm-user-input label .CodeMirror { + cursor: auto; +} + +.ame-twm-user-input .CodeMirror, .ame-twm-dialog .CodeMirror { + box-sizing: border-box; + border: 1px solid #ddd; + border-radius: 4px; + margin-bottom: 4px; + height: auto; +} +.ame-twm-user-input .CodeMirror-scroll, .ame-twm-dialog .CodeMirror-scroll { + overflow-y: hidden; + overflow-x: auto; + min-height: 100px; + max-height: 500px; +} +.ame-twm-user-input .CodeMirror:focus-within, .ame-twm-dialog .CodeMirror:focus-within { + border-color: #007cba; + box-shadow: 0 0 0 1px #007cba; +} + +.ame-twm-color-label { + margin-right: 0.5em; +} + +#ame-tweak-environment-dependent-colors .ame-twm-color-label { + display: inline-block; + min-width: 7em; +} + +/*# sourceMappingURL=tweaks.css.map */ diff --git a/extras/modules/tweaks/tweaks.css.map b/extras/modules/tweaks/tweaks.css.map new file mode 100644 index 0000000..32b37c2 --- /dev/null +++ b/extras/modules/tweaks/tweaks.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["tweaks.scss","../../../css/_boxes.scss","../../../css/_indeterminate-checkbox.scss"],"names":[],"mappings":";AAMA;ECCC;EACA,YAPkB;EAQlB,YAJmB;EAMnB;EDEA;;ACAA;EACC;EACA;EACA;EACA;EAEA;;AAEA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;AAIF;EACC;EACA,YA7BkB;EA+BlB;EACA;EACA;EACA;EACA;EAEA;EACA;EACA;EACA;EACA,OA3CwC;EA4CxC;EACA;;AAEA;EACC;;AAGD;EACC;EACA;;AAGD;EACC;EACA;EACA;;AAGD;EACC;EACA;EACA;EACA;;AAIF;EACC;EACA;EAEA,SA1EkE;;AA6EnE;EACC;;AAGD;EACC;;AD5ED;EACC;;;AAMF;EACC,aAbgB;;AAehB;EACC;;;AEnBD;EACC;EACA,OAH4C;EAU5C;EACA;EAMA;EACA;EACA;EACA;EACA;;AAGD;EACC;IAEC,QADU;IAEV,OAFU;IAGV,aAHU;IAIV;IAEA;IACA;IACA;;;;AFNH;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AASD;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;EACA;;AAEA;EACC;;AAGD;EACC;;AAGD;EACC;;;AAIF;EACC;;;AAIA;EACC;EACA;EACA;EAEA;EAGA;;AAID;EACC;EACA;EACA;EACA;;AAID;EACC;EACA;;;AAIF;EACC;;;AAGD;EACC;EACA","file":"tweaks.css"} \ No newline at end of file diff --git a/extras/modules/tweaks/tweaks.php b/extras/modules/tweaks/tweaks.php new file mode 100644 index 0000000..f1c69bd --- /dev/null +++ b/extras/modules/tweaks/tweaks.php @@ -0,0 +1,678 @@ +<?php + +/* + * Idea: Show tweaks as options in menu properties, e.g. in a "Tweaks" section styled like the collapsible + * property sheets in Delphi. + */ + +require_once __DIR__ . '/configurables.php'; +require_once __DIR__ . '/ameBaseTweak.php'; +require_once __DIR__ . '/ameHideSelectorTweak.php'; +require_once __DIR__ . '/ameHideSidebarTweak.php'; +require_once __DIR__ . '/ameHideSidebarWidgetTweak.php'; +require_once __DIR__ . '/ameDelegatedTweak.php'; +require_once __DIR__ . '/ameTinyMceButtonManager.php'; +require_once __DIR__ . '/ameAdminCssTweakManager.php'; +require_once __DIR__ . '/ameGutenbergBlockManager.php'; + +/** @noinspection PhpUnused The class is actually used in extras.php */ + +//TODO: When importing tweak settings, pick the largest of lastUserTweakSuffix. See mergeSettingsWith(). + +class ameTweakManager extends amePersistentModule { + const APPLY_TWEAK_AUTO = 'auto'; + const APPLY_TWEAK_MANUALLY = 'manual'; + + const HIDEABLE_ITEM_COMPONENT = 'tw'; + const HIDEABLE_ITEM_PREFIX = 'tweaks/'; + + protected $tabSlug = 'tweaks'; + protected $tabTitle = 'Tweaks'; + protected $optionName = 'ws_ame_tweak_settings'; + + protected $settingsFormAction = 'ame-save-tweak-settings'; + + /** + * @var ameBaseTweak[] + */ + private $tweaks = array(); + + /** + * @var ameBaseTweak[] + */ + private $pendingTweaks = array(); + + /** + * @var ameBaseTweak[] + */ + private $postponedTweaks = array(); + + /** + * @var ameTweakSection[] + */ + private $sections = array(); + + private $editorButtonManager; + private $adminCssManager; + private $gutenbergBlockManager; + + /** + * @var null|array + */ + private $cachedEnabledTweakSettings = null; + + /** + * @var callable[] + */ + private $tweakBuilders = array(); + + public function __construct($menuEditor) { + parent::__construct($menuEditor); + + add_action('init', array($this, 'onInit'), PHP_INT_MAX - 1000); + + //We need to process widgets after they've been registered (usually priority 10) + //but before WordPress has populated the $wp_registered_widgets global (priority 95 or 100). + add_action('widgets_init', array($this, 'processSidebarWidgets'), 50); + //Sidebars are simpler: we can just use a really late priority. + add_action('widgets_init', array($this, 'processSidebars'), 1000); + + $this->editorButtonManager = new ameTinyMceButtonManager(); + $this->adminCssManager = new ameAdminCssTweakManager(); + $this->gutenbergBlockManager = new ameGutenbergBlockManager($menuEditor); + + $this->tweakBuilders['admin-css'] = array($this->adminCssManager, 'createTweak'); + + add_action('admin_menu_editor-register_hideable_items', array($this, 'registerHideableItems'), 20); + add_filter( + 'admin_menu_editor-save_hideable_items-' . self::HIDEABLE_ITEM_COMPONENT, + array($this, 'saveHideableItems'), + 10, 2 + ); + } + + public function onInit() { + $this->addSection('general', 'General'); + $this->registerTweaks(); + + $tweaksToProcess = $this->pendingTweaks; + $this->pendingTweaks = array(); + $this->processTweaks($tweaksToProcess); + } + + private function registerTweaks() { + //We may be able to improve performance by only registering tweaks that are enabled + //for the current user. However, we still need to show all tweaks in the "Tweaks" tab. + $isTweaksTab = is_admin() + && isset($_GET['page'], $_GET['sub_section']) + && ($_GET['page'] === 'menu_editor') + && ($_GET['sub_section'] === $this->tabSlug); + $isEasyHidePage = is_admin() && isset($_GET['page']) + && ($_GET['page'] === 'ame-easy-hide'); + + if ( $isTweaksTab || $isEasyHidePage ) { + $tweakFilter = null; + } else { + $tweakFilter = $this->getEnabledTweakSettings(); + } + + $tweakData = require(__DIR__ . '/default-tweaks.php'); + + foreach (ameUtils::get($tweakData, 'sections', array()) as $id => $section) { + $this->addSection($id, ameUtils::get($section, 'label', $id), ameUtils::get($section, 'priority', 10)); + } + + $defaultTweaks = ameUtils::get($tweakData, 'tweaks', array()); + if ( $tweakFilter !== null ) { + $defaultTweaks = array_intersect_key($defaultTweaks, $tweakFilter); + } + + foreach ($defaultTweaks as $id => $properties) { + if ( isset($properties['selector']) ) { + $tweak = new ameHideSelectorTweak( + $id, + isset($properties['label']) ? $properties['label'] : null, + $properties['selector'] + ); + + if ( isset($properties['screens']) ) { + $tweak->setScreens($properties['screens']); + } + } else if ( isset($properties['className']) ) { + if ( isset($properties['includeFile']) ) { + require_once $properties['includeFile']; + } + + $className = $properties['className']; + $tweak = new $className( + $id, + isset($properties['label']) ? $properties['label'] : null + ); + } else { + throw new LogicException('Unknown tweak type in default-tweaks.php for tweak "' . $id . '"'); + } + + if ( isset($properties['parent']) ) { + $tweak->setParentId($properties['parent']); + } + if ( isset($properties['section']) ) { + $tweak->setSectionId($properties['section']); + } + + if ( isset($properties['hideableLabel']) ) { + $tweak->setHideableLabel($properties['hideableLabel']); + } + if ( isset($properties['hideableCategory']) ) { + $tweak->setHideableCategoryId($properties['hideableCategory']); + } + + $this->addTweak($tweak); + } + + do_action('admin-menu-editor-register_tweaks', $this, $tweakFilter); + + //Register user-defined tweaks. + $settings = $this->loadSettings(); + $userDefinedTweakIds = ameUtils::get($settings, 'userDefinedTweaks', array()); + if ( !empty($userDefinedTweakIds) ) { + $tweakSettings = isset($settings['tweaks']) ? $settings['tweaks'] : array(); + foreach ($userDefinedTweakIds as $id => $unused) { + if ( !isset($tweakSettings[$id]['typeId']) ) { + continue; + } + $properties = $tweakSettings[$id]; + if ( isset($this->tweakBuilders[$properties['typeId']]) ) { + $tweak = call_user_func($this->tweakBuilders[$properties['typeId']], $properties); + if ( $tweak ) { + $this->addTweak($tweak); + } + } + } + } + } + + /** + * @param ameBaseTweak $tweak + * @param string $applicationMode + */ + public function addTweak($tweak, $applicationMode = self::APPLY_TWEAK_AUTO) { + $this->tweaks[$tweak->getId()] = $tweak; + if ( $applicationMode === self::APPLY_TWEAK_AUTO ) { + $this->pendingTweaks[$tweak->getId()] = $tweak; + } + } + + /** + * @param ameBaseTweak[] $tweaks + */ + protected function processTweaks($tweaks) { + $settings = $this->getEnabledTweakSettings(); + + foreach ($tweaks as $tweak) { + if ( empty($settings[$tweak->getId()]) ) { + continue; //This tweak is not enabled for the current user. + } + + if ( $tweak->hasScreenFilter() ) { + if ( !did_action('current_screen') ) { + $this->postponedTweaks[$tweak->getId()] = $tweak; + continue; + } else if ( !$tweak->isEnabledForCurrentScreen() ) { + continue; + } + } + + $settingsForThisTweak = null; + if ( $tweak->supportsUserInput() ) { + $settingsForThisTweak = ameUtils::get($settings, array($tweak->getId()), array()); + } + $tweak->apply($settingsForThisTweak); + } + + if ( !empty($this->postponedTweaks) ) { + add_action('current_screen', array($this, 'processPostponedTweaks'), 10, 1); + } + } + + /** + * Get settings associated with tweaks that are enabled for the current user. + */ + protected function getEnabledTweakSettings() { + if ( $this->cachedEnabledTweakSettings !== null ) { + return $this->cachedEnabledTweakSettings; + } + + $settings = ameUtils::get($this->loadSettings(), 'tweaks'); + if ( !is_array($settings) ) { + $settings = array(); + } + + $currentUser = wp_get_current_user(); + $roles = $this->menuEditor->get_user_roles($currentUser); + $isSuperAdmin = is_multisite() && is_super_admin($currentUser->ID); + + $results = array(); + foreach ($settings as $id => $tweakSettings) { + $enabledForActor = ameUtils::get($tweakSettings, 'enabledForActor', array()); + if ( !$this->appliesToUser($enabledForActor, $currentUser, $roles, $isSuperAdmin) ) { + continue; + } + + $results[$id] = $tweakSettings; + } + + $this->cachedEnabledTweakSettings = $results; + return $results; + } + + /** + * @param array $enabledForActor + * @param WP_User $user + * @param array $roles + * @param bool $isSuperAdmin + * @return bool + */ + private function appliesToUser($enabledForActor, $user, $roles, $isSuperAdmin = false) { + //User-specific settings have priority over everything else. + $userActor = 'user:' . $user->user_login; + if ( isset($enabledForActor[$userActor]) ) { + return $enabledForActor[$userActor]; + } + + //The "Super Admin" flag has priority over regular roles. + if ( $isSuperAdmin && isset($enabledForActor['special:super_admin']) ) { + return $enabledForActor['special:super_admin']; + } + + //If it's enabled for any role, it's enabled for the user. + foreach ($roles as $role) { + if ( !empty($enabledForActor['role:' . $role]) ) { + return true; + } + } + + //By default, all tweaks are disabled. + return false; + } + + /** + * @param WP_Screen $screen + */ + public function processPostponedTweaks($screen = null) { + if ( empty($screen) && function_exists('get_current_screen') ) { + $screen = get_current_screen(); + } + $screenId = isset($screen, $screen->id) ? $screen->id : null; + + foreach ($this->postponedTweaks as $id => $tweak) { + if ( !$tweak->isEnabledForScreen($screenId) ) { + continue; + } + $tweak->apply(); + } + + $this->postponedTweaks = array(); + } + + public function processSidebarWidgets() { + global $wp_widget_factory; + global $pagenow; + if ( !isset($wp_widget_factory, $wp_widget_factory->widgets) || !is_array($wp_widget_factory->widgets) ) { + return; + } + + $widgetTweaks = array(); + foreach ($wp_widget_factory->widgets as $id => $widget) { + $tweak = new ameHideSidebarWidgetTweak($widget); + $widgetTweaks[$tweak->getId()] = $tweak; + } + + //Sort the tweaks in alphabetic order. + uasort( + $widgetTweaks, + /** + * @param ameBaseTweak $a + * @param ameBaseTweak $b + * @return int + */ + function ($a, $b) { + return strnatcasecmp($a->getLabel(), $b->getLabel()); + } + ); + + foreach ($widgetTweaks as $tweak) { + $this->addTweak($tweak, self::APPLY_TWEAK_MANUALLY); + } + + if ( is_admin() && ($pagenow === 'widgets.php') ) { + $this->processTweaks($widgetTweaks); + } + } + + public function processSidebars() { + global $wp_registered_sidebars; + global $pagenow; + if ( !isset($wp_registered_sidebars) || !is_array($wp_registered_sidebars) ) { + return; + } + + $sidebarTweaks = array(); + foreach ($wp_registered_sidebars as $id => $sidebar) { + $tweak = new ameHideSidebarTweak($sidebar); + $this->addTweak($tweak, self::APPLY_TWEAK_MANUALLY); + $sidebarTweaks[$tweak->getId()] = $tweak; + } + + if ( is_admin() && ($pagenow === 'widgets.php') ) { + $this->processTweaks($sidebarTweaks); + } + } + + public function addSection($id, $label, $priority = null) { + $section = new ameTweakSection($id, $label); + if ( $priority !== null ) { + $section->setPriority($priority); + } + $this->sections[$section->getId()] = $section; + } + + protected function getTemplateVariables($templateName) { + $variables = parent::getTemplateVariables($templateName); + $variables['tweaks'] = $this->tweaks; + return $variables; + } + + public function enqueueTabScripts() { + $codeEditorSettings = null; + if ( function_exists('wp_enqueue_code_editor') ) { + $codeEditorSettings = wp_enqueue_code_editor(array('type' => 'text/html')); + } + + wp_register_auto_versioned_script( + 'ame-tweak-manager', + plugins_url('tweak-manager.js', __FILE__), + array( + 'ame-lodash', + 'knockout', + 'ame-actor-selector', + 'ame-jquery-cookie', + 'ame-ko-extensions', + ) + ); + wp_enqueue_script('ame-tweak-manager'); + + //Reselect the same actor. + $query = $this->menuEditor->get_query_params(); + $selectedActor = null; + if ( isset($query['selected_actor']) ) { + $selectedActor = strval($query['selected_actor']); + } + + $scriptData = $this->getScriptData(); + $scriptData['selectedActor'] = $selectedActor; + $scriptData['defaultCodeEditorSettings'] = $codeEditorSettings; + wp_localize_script('ame-tweak-manager', 'wsTweakManagerData', $scriptData); + } + + protected function getScriptData() { + $settings = $this->loadSettings(); + $tweakSettings = ameUtils::get($settings, 'tweaks', array()); + + $tweakData = array(); + foreach ($this->tweaks as $id => $tweak) { + $item = $tweak->toArray(); + $item = array_merge(ameUtils::get($tweakSettings, $id, array()), $item); + $tweakData[] = $item; + } + + $sectionData = array(); + foreach ($this->sections as $section) { + $sectionData[] = array( + 'id' => $section->getId(), + 'label' => $section->getLabel(), + 'priority' => $section->getPriority(), + ); + } + + return array( + 'tweaks' => $tweakData, + 'sections' => $sectionData, + 'isProVersion' => $this->menuEditor->is_pro_version(), + 'lastUserTweakSuffix' => ameUtils::get($settings, 'lastUserTweakSuffix', 0), + ); + } + + public function enqueueTabStyles() { + parent::enqueueTabStyles(); + wp_enqueue_auto_versioned_style( + 'ame-tweak-manager-css', + plugins_url('tweaks.css', __FILE__) + ); + } + + public function handleSettingsForm($post = array()) { + parent::handleSettingsForm($post); + + $submittedSettings = json_decode($post['settings'], true); + + //To save space, filter out tweaks that are not enabled for anyone and have no other settings. + //Most tweaks only have "id" and "enabledForActor" properties. + $basicProperties = array('id' => true, 'enabledForActor' => true); + $submittedSettings['tweaks'] = array_filter( + $submittedSettings['tweaks'], + function ($settings) use ($basicProperties) { + if ( !empty($settings['enabledForActor']) ) { + return true; + } + $additionalProperties = array_diff_key($settings, $basicProperties); + return !empty($additionalProperties); + } + ); + + //User-defined tweaks must have a type. + $submittedSettings['tweaks'] = array_filter( + $submittedSettings['tweaks'], + function ($settings) { + return empty($settings['isUserDefined']) || !empty($settings['typeId']); + } + ); + + //TODO: Give other components an opportunity to validate and sanitize tweak settings. E.g. a filter. + //Sanitize CSS with FILTER_SANITIZE_FULL_SPECIAL_CHARS if unfiltered_html is not enabled. Always strip </style>. + + //Build a lookup array of user-defined tweaks so that we can register them later + //without iterating through the entire list. + $userDefinedTweakIds = array(); + foreach ($submittedSettings['tweaks'] as $properties) { + if ( !empty($properties['isUserDefined']) && !empty($properties['id']) ) { + $userDefinedTweakIds[$properties['id']] = true; + } + } + + //We use an incrementing suffix to ensure each user-defined tweak gets a unique ID. + $lastUserTweakSuffix = ameUtils::get($this->loadSettings(), 'lastUserTweakSuffix', 0); + $newSuffix = ameUtils::get($submittedSettings, 'lastUserTweakSuffix', 0); + if ( is_scalar($newSuffix) && is_numeric($newSuffix) ) { + $newSuffix = max(intval($newSuffix), 0); + if ( $newSuffix < 10000000 ) { + $lastUserTweakSuffix = $newSuffix; + } + } + + $this->settings['tweaks'] = $submittedSettings['tweaks']; + $this->settings['userDefinedTweaks'] = $userDefinedTweakIds; + $this->settings['lastUserTweakSuffix'] = $lastUserTweakSuffix; + $this->saveSettings(); + + $params = array('updated' => 1); + if ( !empty($post['selected_actor']) ) { + $params['selected_actor'] = strval($post['selected_actor']); + } + + wp_redirect($this->getTabUrl($params)); + exit; + } + + /** + * @param \YahnisElsts\AdminMenuEditor\EasyHide\HideableItemStore $store + */ + public function registerHideableItems($store) { + $settings = ameUtils::get($this->loadSettings(), 'tweaks'); + + $enabledSections = array( + ameGutenbergBlockManager::SECTION_ID => 'Gutenberg Blocks', + ameTinyMceButtonManager::SECTION_ID => 'TinyMCE Buttons', + 'profile' => null, + 'sidebar-widgets' => null, + 'sidebars' => null, + ); + + $postEditorCategory = $store->getOrCreateCategory('post-editor', 'Editor', null, false, 0, 0); + $parentCategories = array( + ameGutenbergBlockManager::SECTION_ID => $postEditorCategory, + ameTinyMceButtonManager::SECTION_ID => $postEditorCategory, + ); + + $categoriesBySection = array(); + foreach ($enabledSections as $sectionId => $customLabel) { + if ( !isset($this->sections[$sectionId]) ) { + continue; + } + $section = $this->sections[$sectionId]; + + $parent = null; + if ( isset($parentCategories[$sectionId]) ) { + $parent = $parentCategories[$sectionId]; + } + + $category = $store->getOrCreateCategory( + 'tw/' . $sectionId, + !empty($customLabel) ? $customLabel : str_replace('Hide ', '', $section->getLabel()), + $parent, + false, + 0, + 0 + ); + + $categoriesBySection[$sectionId] = $category; + } + + $generalCat = $store->getOrCreateCategory('admin-ui', 'General', null, true); + $generalCat->setSortPriority(1); + + foreach ($this->tweaks as $tweak) { + $sectionCategoryExists = isset($categoriesBySection[$tweak->getSectionId()]); + + $isHideable = ($sectionCategoryExists || $tweak->isIndependentlyHideable()); + if ( !$isHideable ) { + continue; + } + + $tweakParent = $tweak->getParentId(); + if ( !empty($tweakParent) ) { + $parent = $store->getItemById(self::getHideableIdForTweak($tweakParent)); + } else { + $parent = null; + } + + $enabled = ameUtils::get($settings, array($tweak->getId(), 'enabledForActor'), array()); + $inverted = null; + + $categories = array(); + if ( $sectionCategoryExists ) { + $categories[] = $categoriesBySection[$tweak->getSectionId()]; + } + $customCategoryId = $tweak->getHideableCategoryId(); + if ( $customCategoryId ) { + $customCategory = $store->getCategory($customCategoryId); + if ( $customCategory ) { + $categories[] = $customCategory; + //Tweak state should not be inverted, so if the category does that, + //we'll need to override that setting. + if ($customCategory->isInvertingItemState()) { + $inverted = false; + } + } + } + + $store->addItem( + self::getHideableIdForTweak($tweak->getId()), + $tweak->getHideableLabel(), + $categories, + $parent, + $enabled, + self::HIDEABLE_ITEM_COMPONENT, + null, + $inverted + ); + } + } + + private static function getHideableIdForTweak($tweakId) { + return self::HIDEABLE_ITEM_PREFIX . $tweakId; + } + + public function saveHideableItems($errors, $items) { + $tweakSettings = ameUtils::get($this->loadSettings(), 'tweaks', array()); + $prefixLength = strlen(self::HIDEABLE_ITEM_PREFIX); + $anyTweaksModified = false; + + foreach ($items as $id => $item) { + $tweakId = substr($id, $prefixLength); + + $enabled = isset($item['enabled']) ? $item['enabled'] : array(); + $oldEnabled = ameUtils::get($tweakSettings, array($tweakId, 'enabledForActor'), array()); + + if ( !ameUtils::areAssocArraysEqual($enabled, $oldEnabled) ) { + if ( !empty($enabled) ) { + if ( !isset($tweakSettings[$tweakId]) ) { + $tweakSettings[$tweakId] = array(); + } + $tweakSettings[$tweakId]['enabledForActor'] = $enabled; + } else { + //To save space, we can simply remove the array if it's empty. + if ( isset($tweakSettings[$tweakId]['enabledForActor']) ) { + unset($tweakSettings[$tweakId]['enabledForActor']); + } + } + $anyTweaksModified = true; + } + } + + if ( $anyTweaksModified ) { + $this->settings['tweaks'] = $tweakSettings; + $this->saveSettings(); + } + + return $errors; + } +} + +class ameTweakSection { + private $id; + private $label; + + private $priority = 0; + + public function __construct($id, $label) { + $this->id = $id; + $this->label = $label; + } + + public function getId() { + return $this->id; + } + + public function getLabel() { + return $this->label; + } + + public function getPriority() { + return $this->priority; + } + + public function setPriority($priority) { + $this->priority = $priority; + return $this; + } +} diff --git a/extras/modules/tweaks/tweaks.scss b/extras/modules/tweaks/tweaks.scss new file mode 100644 index 0000000..91ab753 --- /dev/null +++ b/extras/modules/tweaks/tweaks.scss @@ -0,0 +1,115 @@ +@import "../../../css/indeterminate-checkbox"; +@import "../../../css/boxes"; + +$desiredChildTweakOffset: 2em; +$childrenOffset: 1.7em; + +.ame-twm-section { + @include ame-emulated-postbox(); + + .ws-ame-postbox-content { + padding-top: 8px; + } + + max-width: 630px; +} + +.ame-twm-tweak-children { + margin-left: $childrenOffset; + + & > .ame-twm-tweak { + margin-left: $desiredChildTweakOffset - $childrenOffset; + } +} + +.ame-twm-named-node input[type=checkbox] { + @include ame-indeterminate-checkbox; +} + +.ame-twm-named-node { + font-size: 14px; + line-height: 1.65; +} + +#ame-tweak-manager #ws_actor_selector_container { + margin-bottom: 8px; +} + +#ame-twm-new-css-tweak-code { + width: 99%; +} + +#ame-twm-add-admin-css-dialog { + .CodeMirror-scroll { + //min-height: 160px; + } +} + +.ame-twm-tweak-actions { + vertical-align: middle; + margin-left: 1em; + display: none; +} + +.ame-twm-tweak:hover .ame-twm-tweak-actions { + display: inline-block; +} + +.ame-twm-action { + text-decoration: none; + color: #595959; + cursor: pointer; + padding: 0 0.5em; + + & > .dashicons { + vertical-align: text-bottom; + } + + &:hover { + text-decoration: none; + } + + &.ame-twm-delete-tweak:hover { + color: #a00; + } +} + +.ame-twm-user-input label .CodeMirror { + cursor: auto; +} + +.ame-twm-user-input, .ame-twm-dialog { + .CodeMirror { + box-sizing: border-box; + border: 1px solid #ddd; + border-radius: 4px; + + margin-bottom: 4px; + + //Automatically resize CodeMirror text fields to fit the contents. + height: auto; + } + + //This is also part of the resizing code. + .CodeMirror-scroll { + overflow-y: hidden; + overflow-x: auto; + min-height: 100px; + max-height: 500px; + } + + //Emulate WP textarea focus styles. + .CodeMirror:focus-within { + border-color: #007cba; + box-shadow: 0 0 0 1px #007cba; + } +} + +.ame-twm-color-label { + margin-right: 0.5em; +} + +#ame-tweak-environment-dependent-colors .ame-twm-color-label { + display: inline-block; + min-width: 7em; +} \ No newline at end of file diff --git a/extras/modules/visible-users/visible-users-template.php b/extras/modules/visible-users/visible-users-template.php new file mode 100644 index 0000000..1aae44c --- /dev/null +++ b/extras/modules/visible-users/visible-users-template.php @@ -0,0 +1,32 @@ +<div id="ws_visible_users_dialog" title="Select Visible Users" class="hidden"> + + <div id="ws_user_selection_panels"> + + <div id="ws_user_selection_source_panel" class="ws_user_selection_panel"> + <label for="ws_available_user_query" class="hidden">Search users</label> + <input type="text" name="ws_available_user_query" id="ws_available_user_query" + placeholder="Search and hit Enter to add a user"> + + <div class="ws_user_list_wrapper"> + <table id="ws_available_users" class="widefat striped ws_user_selection_list" title="Add user"></table> + </div> + + <div id="ws_loading_users_indicator" class="spinner"></div> + </div> + + <div id="ws_user_selection_target_panel" class="ws_user_selection_panel"> + <div id="ws_selected_users_caption">Selected users</div> + + <div class="ws_user_list_wrapper" title=""> + <table id="ws_selected_users" class="widefat ws_user_selection_list"></table> + </div> + </div> + + </div> + + <div class="ws_dialog_buttons"> + <?php submit_button('Save Changes', 'primary', 'ws_ame_save_visible_users', false); ?> + <input type="button" class="button ws_close_dialog" value="Cancel"> + </div> + +</div> \ No newline at end of file diff --git a/extras/modules/visible-users/visible-users.js b/extras/modules/visible-users/visible-users.js new file mode 100644 index 0000000..1c22447 --- /dev/null +++ b/extras/modules/visible-users/visible-users.js @@ -0,0 +1,433 @@ +/* globals jQuery, wsAmeLodash, ameVisibleUsersScriptData */ + +window.AmeSelectUsersDialog = (function($, _) { + 'use strict'; + + var maxUsersToShow = 30, + maxLoadedUsers = 150, + adminAjaxUrl, + searchUsersNonce = ''; + + var /** @var {IAmeUser[]} */ + selectedUsers = [], + /** @var {IAmeUser[]} */ + loadedUsers = [], + searchQuery = '', + searchKeywords = [], + bestMatch = null, + currentUserLogin = '', + alwaysIncludeCurrentUser = false, + saveCallback = null, + actorManager = null, + + $dialog, + $selectedUsersTable, + $availableUsersTable, + $searchBox, + $spinner; + + + /** + * @param {IAmeUser} user + */ + function addSelectedUser(user) { + //Don't add the same user twice. + if (_.includes(selectedUsers, user)) { + return; + } + + //The list should stay sorted by username. + var index = _.sortedIndex(selectedUsers, user, 'userLogin'); + selectedUsers.splice(index, 0, user); + + //Add a new row at the same index. + var row = buildTableRow(user, 'deselect_user'); + if (index === 0) { + $selectedUsersTable.prepend(row); + } else { + row.insertAfter($selectedUsersTable.find('tr').eq(index - 1)); + } + + searchUsers(); + } + + /** + * @param {IAmeUser} user + * @param {JQuery} tableRow + */ + function removeSelectedUser(user, tableRow) { + //You can't remove the current user, ever. + if (alwaysIncludeCurrentUser && (user.userLogin === currentUserLogin)) { + return; + } + + if (!tableRow) { + tableRow = $selectedUsersTable.find('tr').filter(function() { + return ($(this).data('user') === user); + }).first(); + } + tableRow.remove(); + selectedUsers = _.without(selectedUsers, user); + + updateSearchResults(); + } + + function searchUsers(newQuery) { + if (typeof newQuery !== 'undefined') { + searchQuery = newQuery; + searchKeywords = _.uniq(_.words(searchQuery)); + } + + requestUsersFromServer(); + updateSearchResults(); + } + + var requestUsersFromServer = _.debounce( + function() { + $spinner.addClass('is-active'); + + $.getJSON( + adminAjaxUrl, + { + 'action' : 'ws_ame_search_users', + '_ajax_nonce' : searchUsersNonce, + 'query' : searchQuery, + 'limit' : maxLoadedUsers + }, + function(response) { + $spinner.removeClass('is-active'); + + if (_.has(response, 'error')) { + if (_.has(console, 'error')) { + console.error(_.get(response, 'error')); + } + return; + } + + if (_.has(response, 'users')) { + //Add new results to loaded users. + var userIndex = _.indexBy(loadedUsers, 'userLogin'); + _.forEach(response.users, function(userDetails) { + if (!userIndex.hasOwnProperty(userDetails.user_login)) { + loadedUsers.push(actorManager.createUserFromProperties(userDetails)); + } + }); + } + updateSearchResults(); + } + ); + }, + 1000, + { + maxWait: 5000 + } + ); + + function updateSearchResults() { + loadedUsers = _(loadedUsers) + .forEach(function(user) { + //Update search score. + user.searchScore = calculateSearchScore(user, searchQuery, searchKeywords); + }) + .sortByOrder(['searchScore', 'userLogin'], ['desc', 'asc']) + .take(maxLoadedUsers) //Conserve memory and keep searches fast. + .value(); + + var matchesToShow = _(loadedUsers) + .filter(function(user) { + return user.searchScore > 0; + }) + .difference(selectedUsers) + .take(maxUsersToShow) + .value(); + + //Keep the same best match if possible, or just pick the first one. + if (!_.includes(matchesToShow, bestMatch)) { + bestMatch = (matchesToShow.length > 0) ? matchesToShow[0] : null; + } + + //Show the new matches in the table. + $availableUsersTable.empty(); + _.forEach(matchesToShow, function(user) { + $availableUsersTable.append(buildTableRow(user, 'select_available_user')); + }); + + scrollRowIntoView($availableUsersTable.find('.ws_user_best_match').first()); + } + + /** + * + * @param {IAmeUser} user + * @param {string} query + * @param {string[]} keywords + * @returns {number} + */ + function calculateSearchScore(user, query, keywords) { + if (query === '') { + return 1; //Include all users when there's no query. + } + + var haystack = user.userLogin.toLowerCase() + '\n' + user.getDisplayName().toLowerCase(); + if (haystack.indexOf(query) >= 0) { + return 2; + } else if (_.all(keywords, function(keyword) { return (haystack.indexOf(keyword) >= 0); })) { + return 1; + } + return 0; + } + + /** + * + * @param {IAmeUser} user + * @param {string} action + * @returns {*|void} + */ + function buildTableRow(user, action) { + if (typeof action === 'undefined') { + action = 'select_available_user'; + } + + return $('<tr></tr>') + .data('user', user) + .toggleClass('ws_user_best_match', user === bestMatch) + .toggleClass( + 'ws_user_must_be_selected', + alwaysIncludeCurrentUser && (user.userLogin === currentUserLogin) + ).append($('<td></td>', { + 'class': 'ws_user_action_column', + 'html': '<div class="dashicons dashicons-plus ws_user_action_button ws_' + action + '"></div>' + })).append($('<td></td>', { + 'text': user.userLogin + ((user.userLogin === currentUserLogin) ? ' (current user)' : ''), + 'class': 'ws_user_username_column' + })).append($('<td></td>', { + 'text': user.getDisplayName(), + 'class': 'ws_user_display_name_column' + })); + } + + function scrollRowIntoView(row) { + if (row.length < 1) { + return; + } + + var rowTop = row.position().top || 0, + scrollableContainer = $availableUsersTable.closest('.ws_user_list_wrapper'), + containerScrollTop = scrollableContainer.scrollTop(), + containerHeight = scrollableContainer.height(), + rowHeight = row.height(), + desiredVisibleHeight = Math.min(rowHeight, containerHeight); + + var scrollAmount = 0, visibleHeight = 0; + if (rowTop > 0) { + visibleHeight = containerHeight - rowTop; + if (visibleHeight < desiredVisibleHeight) { + scrollAmount = desiredVisibleHeight - visibleHeight; + } + } else { + scrollAmount = rowTop; + } + + if (Math.abs(scrollAmount) >= 1) { + scrollableContainer.scrollTop(containerScrollTop + scrollAmount); + } + } + + + $(function() { + searchUsersNonce = _.get(ameVisibleUsersScriptData, 'searchUsersNonce', null); + adminAjaxUrl = _.get(ameVisibleUsersScriptData, 'adminAjaxUrl', null); + + $dialog = $('#ws_visible_users_dialog'); + $selectedUsersTable = $('#ws_selected_users'); + $availableUsersTable = $('#ws_available_users'); + $spinner = $('#ws_loading_users_indicator'); + + $dialog.dialog({ + autoOpen: false, + closeText: ' ', + modal: true, + minHeight: 100, + width: 726, + draggable: false + }); + + $searchBox = $dialog.find('#ws_available_user_query'); + $searchBox.on('change keyup input paste click propertychange ', _.debounce(function() { + //Normalize query: lowercase, condense whitespace, trim. + var newQuery = $searchBox.val(); + + function jsTrim(str){ + return str.replace(/^\s+|\s+$/g, ""); + } + + newQuery = jsTrim(newQuery.toLowerCase().replace(/\s{2,}/, ' ')); + + if (newQuery !== searchQuery) { + searchUsers(newQuery); + } + }, 200, {maxWait: 1000})); + + //Search box keyboard shortcuts. + $searchBox.on('keydown', function(event) { + var currentRow = $availableUsersTable.find('tr.ws_user_best_match').first(), + nextRow = currentRow; + if (currentRow.length === 0) { + return; + } + + switch(event.which) { + //Up: Select the previous row. + case 38: + nextRow = currentRow.prev('tr'); + if (nextRow.length === 0) { + nextRow = $availableUsersTable.find('tr').last(); + } + break; + + //Down: Select the next row. + case 40: + nextRow = currentRow.next('tr'); + if (nextRow.length === 0) { + nextRow = $availableUsersTable.find('tr').first(); + } + break; + + //Enter: Add the current selection to the list of selected users. + case 13: + addSelectedUser(currentRow.data('user')); + + currentRow.remove(); + bestMatch = null; + nextRow = null; + + $searchBox.val(''); + searchUsers(''); + break; + } + + if (nextRow && nextRow.length > 0) { + bestMatch = nextRow.data('user'); + $availableUsersTable.find('tr').removeClass('ws_user_best_match'); + nextRow.addClass('ws_user_best_match'); + scrollRowIntoView(nextRow); + } + }); + + + //Add a user. + $availableUsersTable.on('click', 'tr', function() { + var row = $(this).closest('tr'), + user = row.data('user'); + row.remove(); + addSelectedUser(user); + searchUsers(); + }); + + //Remove a user. + $selectedUsersTable.on('click', '.ws_user_action_button', function() { + var row = $(this).closest('tr'); + removeSelectedUser(row.data('user'), row); + }); + + + //The save button. + $dialog.find('#ws_ame_save_visible_users').on('click', function() { + if (saveCallback) { + saveCallback(selectedUsers, _.pluck(selectedUsers, 'userLogin')); + } + $dialog.dialog('close'); + }); + + //The cancel button. + $dialog.find('.ws_close_dialog').on('click', function() { + $dialog.dialog('close'); + }); + }); + + return { + /** + * @param {Object} options + * @param {String} options.currentUserLogin + * @param {Boolean} [options.alwaysIncludeCurrentUser] + * @param {Function} options.save + * @param {String[]} options.selectedUsers + * @param {Object.<String, IAmeUser>} options.users + * @param {AmeActorManagerInterface} options.actorManager + * @param {String} [options.dialogTitle] + */ + open: function(options) { + currentUserLogin = options.currentUserLogin; + alwaysIncludeCurrentUser = _.get(options, 'alwaysIncludeCurrentUser', false); + saveCallback = options.save; + actorManager = options.actorManager; + + var knownUsers = options.users, + initialSelectedUsers = [].concat(options.selectedUsers); //Don't modify the input array. + + //Always include the current user. + if (!_.includes(initialSelectedUsers, currentUserLogin) && alwaysIncludeCurrentUser) { + initialSelectedUsers.unshift(currentUserLogin); + } + + selectedUsers = _.map(initialSelectedUsers, function(login) { + return knownUsers[login]; + }); + + //Use the user objects provided by the plugin whenever possible. + //We don't want to have two different instances for the same user. + loadedUsers = _(loadedUsers) + .map(function(user) { + if (knownUsers.hasOwnProperty(user.userLogin)) { + return knownUsers[user.userLogin]; + } else { + return user; + } + }) + .union(_.values(knownUsers)) + .value(); + + //Populate the "selected users" table. + $selectedUsersTable.empty(); + _.forEach(selectedUsers, function(user) { + $selectedUsersTable.append(buildTableRow(user, 'deselect_user')); + }); + + bestMatch = null; + $searchBox.val(''); + searchUsers(''); + + $dialog.dialog('option', 'title', _.get(options, 'dialogTitle', 'Select Users')); + $dialog.dialog('open'); + $searchBox.trigger('focus'); + } + }; + +})(jQuery, wsAmeLodash); + +window.AmeVisibleUserDialog = (function($, _) { + 'use strict'; + + return { + /** + * @param {Object} options + * @param {String} options.currentUserLogin + * @param {Function} options.save + * @param {String[]} options.visibleUsers + * @param {Object.<String, IAmeUser>} options.users + * @param {AmeActorManagerInterface} options.actorManager + */ + open: function(options) { + options = _.assign( + { + selectedUsers: options.visibleUsers, + dialogTitle: 'Select Visible Users', + alwaysIncludeCurrentUser: true + }, + options + ); + + window.AmeSelectUsersDialog.open(options); + } + }; + +})(jQuery, wsAmeLodash); \ No newline at end of file diff --git a/extras/modules/visible-users/visible-users.php b/extras/modules/visible-users/visible-users.php new file mode 100644 index 0000000..60222e9 --- /dev/null +++ b/extras/modules/visible-users/visible-users.php @@ -0,0 +1,130 @@ +<?php + +class ameVisibleUsers extends ameModule { + private $isTemplateDone = false; + + public function __construct($menuEditor) { + parent::__construct($menuEditor); + + add_action('wp_ajax_ws_ame_search_users', array($this, 'ajaxSearchUsers')); + add_filter('admin_menu_editor-editor_script_dependencies', array($this, 'addEditorScript')); + add_action('admin_menu_editor-footer', array($this, 'outputDialogTemplate')); + add_action('admin_menu_editor-visible_users_template', array($this, 'outputDialogTemplate')); + } + + public function ajaxSearchUsers() { + global $wpdb; /** @var wpdb $wpdb */ + global $wp_roles; + + if ( !$this->menuEditor->current_user_can_edit_menu() ) { + die($this->menuEditor->json_encode(array( + 'error' => __("You don't have permission to use Admin Menu Editor Pro.", 'admin-menu-editor') + ))); + } + + if ( !check_ajax_referer('search_users', false, false) ){ + die($this->menuEditor->json_encode(array( + 'error' => __("Access denied. Invalid nonce.", 'admin-menu-editor') + ))); + } + + $query = strval($_GET['query']); + $limit = intval($_GET['limit']); + if ( $limit > 50 ) { + $limit = 50; + } + + $capability_key = $wpdb->prefix . 'capabilities'; + $sql = + "SELECT ID, user_login, display_name, meta_value as capabilities + FROM {$wpdb->users} LEFT JOIN {$wpdb->usermeta} + ON ({$wpdb->users}.ID = {$wpdb->usermeta}.user_id AND {$wpdb->usermeta}.meta_key = \"$capability_key\") "; + + if ( !empty($query) ) { + $like = '%' . $wpdb->esc_like($query) . '%'; + $sql .= $wpdb->prepare( + ' WHERE (user_login LIKE %s) OR (display_name LIKE %s) ', + $like, $like + ); + } + + $sql .= ' LIMIT ' . ($limit + 1); //Ask for +1 result so that we know if there are additional results. + + $users = $wpdb->get_results($sql, ARRAY_A); + + $is_multisite = is_multisite(); + if ( !isset($wp_roles) ) { + $wp_roles = new WP_Roles(); + } + + $results = array(); + foreach($users as $user) { + //Capabilities (when present) are stored as serialized PHP arrays. + if ( !empty($user['capabilities']) ) { + $capabilities = $this->menuEditor->castValuesToBool(unserialize($user['capabilities'])); + } else { + $capabilities = array(); + } + + //Get roles from capabilities. + $roles = array_filter(array_keys($capabilities), array($wp_roles, 'is_role')); + + $results[] = array( + 'id' => $user['ID'], + 'user_login' => $user['user_login'], + 'capabilities' => $capabilities, + 'roles' => $roles, + 'is_super_admin' => $is_multisite && is_super_admin($user['ID']), + 'display_name' => strval($user['display_name']), + 'avatar_html' => get_avatar($user['ID'], 32), + ); + } + + $more_results_available = false; + if ( count($results) > $limit ) { + $more_results_available = true; + array_pop($results); + } + + $response = array( + 'users' => $results, + 'moreResultsAvailable' => $more_results_available, + ); + die($this->menuEditor->json_encode($response)); + } + + public function registerScripts() { + parent::registerScripts(); + + wp_register_auto_versioned_script( + 'ame-visible-users', + plugins_url('extras/modules/visible-users/visible-users.js', $this->menuEditor->plugin_file), + array('jquery', 'ame-lodash', 'jquery-ui-dialog', 'ame-actor-manager',) + ); + + wp_localize_script( + 'ame-visible-users', + 'ameVisibleUsersScriptData', + array( + 'searchUsersNonce' => wp_create_nonce('search_users'), + 'adminAjaxUrl' => admin_url('admin-ajax.php'), + ) + ); + } + + public function addEditorScript($dependencies) { + $dependencies[] = 'ame-visible-users'; + return $dependencies; + } + + public function outputDialogTemplate() { + if ( $this->isTemplateDone ) { + return; + } + + if ( wp_script_is('ame-visible-users', 'enqueued') || wp_script_is('ame-visible-users', 'done') ) { + $this->isTemplateDone = true; + include dirname(__FILE__) . '/visible-users-template.php'; + } + } +} \ No newline at end of file diff --git a/extras/page-dropdown.php b/extras/page-dropdown.php new file mode 100644 index 0000000..cdbe80a --- /dev/null +++ b/extras/page-dropdown.php @@ -0,0 +1,43 @@ +<div style="visibility: hidden;"> + +<div id="ws_embedded_page_selector"> + <ul class="ws_page_selector_tab_nav"> + <li class="ws_active_tab"><a href="#ws_select_page_tab">Pages</a></li> + <li><a href="#ws_custom_embedded_page_tab">Custom</a></li> + </ul> + + <div id="ws_select_page_tab" class="ws_page_selector_tab"> + <label for="ws_current_site_pages" class="hidden"> + <span>Select page</span> + </label> + <select id="ws_current_site_pages" size="10"> + <option>Loading pages...</option> + </select> + </div> + <div id="ws_custom_embedded_page_tab" class="ws_page_selector_tab"> + <form> + <p> + <label for="ws_embedded_page_id">Post ID</label><br> + <input type="number" value="0" id="ws_embedded_page_id" min="0"> + </p> + + <p> + <label for="ws_embedded_page_blog_id">Blog ID</label><br> + <input type="number" value="<?php echo get_current_blog_id(); ?>" id="ws_embedded_page_blog_id" min="0" <?php + if ( !is_multisite() || !is_super_admin() ) { + echo ' readonly="readonly"'; + } + ?>> + </p> + + <div> + <?php + submit_button('Apply', 'primary', 'ws_set_custom_embedded_page', false); + ?> + <div class="clear"></div> + </div> + </form> + </div> +</div> + +</div> \ No newline at end of file diff --git a/extras/persistent-pro-module.php b/extras/persistent-pro-module.php new file mode 100644 index 0000000..f28ecb6 --- /dev/null +++ b/extras/persistent-pro-module.php @@ -0,0 +1,40 @@ +<?php +class amePersistentProModule extends amePersistentModule implements ameExportableModule { + /** + * @internal + * @param array $importedData + */ + public function handleDataImport($importedData) { + //Action: admin_menu_editor-import_data + if ( !empty($this->moduleId) && isset($importedData, $importedData[$this->moduleId]) ) { + $this->importSettings($importedData[$this->moduleId]); + } + } + + public function exportSettings() { + if ( isset($this->moduleId) ) { + return $this->loadSettings(); + } + return null; + } + + public function importSettings($newSettings) { + if ( !is_array($newSettings) || empty($newSettings) ) { + return; + } + + $this->mergeSettingsWith($newSettings); + $this->saveSettings(); + } + + /** + * @return string + */ + public function getExportOptionLabel() { + return $this->getTabTitle(); + } + + public function getExportOptionDescription() { + return ''; + } +} \ No newline at end of file diff --git a/extras/phpColors/README.md b/extras/phpColors/README.md new file mode 100644 index 0000000..f1ced7e --- /dev/null +++ b/extras/phpColors/README.md @@ -0,0 +1,153 @@ +### Last Build: [![Build Status](https://secure.travis-ci.org/mexitek/phpColors.png)](http://travis-ci.org/mexitek/phpColors) + +## How it works +Instantiate an object of the color class with a hex color string `$foo = new Color("336699")`. That's it! Now, call the methods you need for different color variants. + +## Available Methods +- <strong>darken( [$amount] )</strong> : Allows you to obtain a darker shade of your color. Optionally you can decide to darken using a desired percentage. +- <strong>lighten( [$amount] )</strong> : Allows you to obtain a lighter shade of your color. Optionally you can decide to lighten using a desired percentage. +- <strong>mix($hex, [$amount] )</strong> : Allows you to mix another color to your color. Optionally you can decide to set the percent of second color or original color amount is ranged -100..0.100. +- <strong>isLight( [$hex] )</strong> : Determins whether your color (or the provide param) is considered a "light" color. Returns `TRUE` if color is light. +- <strong>isDark( [$hex] )</strong> : Determins whether your color (or the provide param) is considered a "dark" color. Returns `TRUE` if color is dark. +- <strong>makeGradient( [$amount] )</strong> : Returns an array with 2 indices `light` and `dark`, the initial color will either be selected for `light` or `dark` depending on its brightness, then the other color will be generated. The optional param allows for a static lighten or darkened amount. +- <strong>complementary()</strong> : Returns the color "opposite" or complementary to your color. +- <strong>getHex()</strong> : Returns the original hex color. +- <strong>getHsl()</strong> : Returns HSL array for your color. +- <strong>getRgb()</strong> : Returns RGB array for your color. + +> Auto lightens/darkens by 10% for sexily-subtle gradients + +```php +/** + * Using The Class + */ + +using phpColors\Color; + +// Initialize my color +$myBlue = new Color("#336699"); + +echo $myBlue->darken(); +// 1a334d + +echo $myBlue->lighten(); +// 8cb3d9 + +echo $myBlue->isLight(); +// false + +echo $myBlue->isDark(); +// true + +echo $myBlue->complementary(); +// 996633 + +echo $myBlue->getHex(); +// 336699 + +print_r( $myBlue->getHsl() ); +// array( "H"=> 210, "S"=> 0.5, "L"=>0.4 ); + +print_r( $myBlue->getRgb() ); +// array( "R"=> 51, "G"=> 102, "B"=>153 ); + +print_r($myBlue->makeGradient()); +// array( "light"=>"8cb3d9" ,"dark"=>"336699" ) + +``` + + +## Static Methods +- <strong>hslToHex( $hsl )</strong> : Convert a HSL array to a HEX string. +- <strong>hexToHsl( $hex )</strong> : Convert a HEX string into an HSL array. +- <strong>hexToRgb( $hex )</strong> : Convert a HEX string into an RGB array. +- <strong>rgbToHex( $rgb )</strong> : Convert an RGB array into a HEX string. + +```php +/** + * On The Fly Custom Calculations + */ + +using phpColors\Color; + + // Convert my HEX + $myBlue = Color::hexToHsl("#336699"); + + // Get crazy with the HUE + $myBlue["H"] = 295; + + // Gimme my new color!! + echo Color::hslToHex($myBlue); + // 913399 + +``` + +## CSS Helpers +- <strong>getCssGradient( [$amount] [, $vintageBrowsers] )</strong> : Generates the CSS3 gradients for safari, chrome, opera, firefox and IE10. Optional percentage amount for lighter/darker shade. Optional boolean for older gradient CSS support. + +> Would like to add support to custom gradient stops + +```php + +using phpColors\Color; + +// Initialize my color +$myBlue = new Color("#336699"); + +// Get CSS +echo $myBlue->getCssGradient(); +/* - Actual output doesn't have comments and is single line + + // fallback background + background: #336699; + + // IE Browsers + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#8cb3d9', endColorstr='#336699'); + + // Safari 5.1+, Mobile Safari, Chrome 10+ + background-image: -webkit-linear-gradient(top, #8cb3d9, #336699); + + // Standards + background-image: linear-gradient(to bottom, #8cb3d9, #336699); + +*/ + +``` + +However, if you want to support the ancient browsers (which has negligible market share and almost died out), you can set the second parameter to `TRUE`. This will output: + +```php + +using phpColors\Color; +$myBlue = new Color("#336699"); + +// Get CSS +echo $myBlue->getCssGradient(10, TRUE); +/* - Actual output doesn't have comments and is single line + + background: #336699; // fallback background + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#8cb3d9', endColorstr='#336699'); // IE Browsers + background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#8cb3d9), to(#336699)); // Safari 4+, Chrome 1-9 + background-image: -webkit-linear-gradient(top, #8cb3d9, #336699); // Safari 5.1+, Mobile Safari, Chrome 10+ + background-image: -moz-linear-gradient(top, #8cb3d9, #336699); // Firefox 3.6+ + background-image: -o-linear-gradient(top, #8cb3d9, #336699); // Opera 11.10+ + background-image: linear-gradient(to bottom, #8cb3d9, #336699); // Standards + +*/ + +``` + +## Github Contributors +- mexitek +- danielpataki +- alexmglover +- intuxicated +- pborreli +- curtisgibby +- matthewpatterson +- there4 +- alex-humphreys +- zaher +- primozcigler + +# License: [arlo.mit-license.org](http://arlo.mit-license.org) diff --git a/extras/phpColors/src/color.php b/extras/phpColors/src/color.php new file mode 100644 index 0000000..329939f --- /dev/null +++ b/extras/phpColors/src/color.php @@ -0,0 +1,487 @@ +<?php + +/** + * Author: Arlo Carreon <http://arlocarreon.com> + * Info: http://mexitek.github.io/phpColors/ + * License: http://arlo.mit-license.org/ + */ + +/** + * A color utility that helps manipulate HEX colors + */ +class phpColor { + + private $_hex; + private $_hsl; + private $_rgb; + + /** + * Auto darkens/lightens by 10% for sexily-subtle gradients. + * Set this to FALSE to adjust automatic shade to be between given color + * and black (for darken) or white (for lighten) + */ + const DEFAULT_ADJUST = 10; + + /** + * Instantiates the class with a HEX value + * @param string $hex + */ + function __construct( $hex ) { + // Strip # sign is present + $color = str_replace("#", "", $hex); + + // Make sure it's 6 digits + if( strlen($color) === 3 ) { + $color = $color[0].$color[0].$color[1].$color[1].$color[2].$color[2]; + } else if( strlen($color) != 6 ) { + throw new Exception("HEX color needs to be 6 or 3 digits long"); + } + + $this->_hsl = self::hexToHsl( $color ); + $this->_hex = $color; + $this->_rgb = self::hexToRgb( $color ); + } + + // ==================== + // = Public Interface = + // ==================== + + /** + * Given a HEX string returns a HSL array equivalent. + * @param string $color + * @return array HSL associative array + */ + public static function hexToHsl( $color ){ + + // Sanity check + $color = self::_checkHex($color); + + // Convert HEX to DEC + $R = hexdec($color[0].$color[1]); + $G = hexdec($color[2].$color[3]); + $B = hexdec($color[4].$color[5]); + + $HSL = array(); + + $var_R = ($R / 255); + $var_G = ($G / 255); + $var_B = ($B / 255); + + $var_Min = min($var_R, $var_G, $var_B); + $var_Max = max($var_R, $var_G, $var_B); + $del_Max = $var_Max - $var_Min; + + $L = ($var_Max + $var_Min)/2; + + if ($del_Max == 0) + { + $H = 0; + $S = 0; + } + else + { + if ( $L < 0.5 ) $S = $del_Max / ( $var_Max + $var_Min ); + else $S = $del_Max / ( 2 - $var_Max - $var_Min ); + + $del_R = ( ( ( $var_Max - $var_R ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max; + $del_G = ( ( ( $var_Max - $var_G ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max; + $del_B = ( ( ( $var_Max - $var_B ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max; + + if ($var_R == $var_Max) $H = $del_B - $del_G; + else if ($var_G == $var_Max) $H = ( 1 / 3 ) + $del_R - $del_B; + else if ($var_B == $var_Max) $H = ( 2 / 3 ) + $del_G - $del_R; + + if ($H<0) $H++; + if ($H>1) $H--; + } + + $HSL['H'] = ($H*360); + $HSL['S'] = $S; + $HSL['L'] = $L; + + return $HSL; + } + + /** + * Given a HSL associative array returns the equivalent HEX string + * @param array $hsl + * @return string HEX string + * @throws Exception "Bad HSL Array" + */ + public static function hslToHex( $hsl = array() ){ + // Make sure it's HSL + if(empty($hsl) || !isset($hsl["H"]) || !isset($hsl["S"]) || !isset($hsl["L"]) ) { + throw new Exception("Param was not an HSL array"); + } + + list($H,$S,$L) = array( $hsl['H']/360,$hsl['S'],$hsl['L'] ); + + if( $S == 0 ) { + $r = $L * 255; + $g = $L * 255; + $b = $L * 255; + } else { + + if($L<0.5) { + $var_2 = $L*(1+$S); + } else { + $var_2 = ($L+$S) - ($S*$L); + } + + $var_1 = 2 * $L - $var_2; + + $r = round(255 * self::_huetorgb( $var_1, $var_2, $H + (1/3) )); + $g = round(255 * self::_huetorgb( $var_1, $var_2, $H )); + $b = round(255 * self::_huetorgb( $var_1, $var_2, $H - (1/3) )); + + } + + // Convert to hex + $r = dechex($r); + $g = dechex($g); + $b = dechex($b); + + // Make sure we get 2 digits for decimals + $r = (strlen("".$r)===1) ? "0".$r:$r; + $g = (strlen("".$g)===1) ? "0".$g:$g; + $b = (strlen("".$b)===1) ? "0".$b:$b; + + return $r.$g.$b; + } + + + /** + * Given a HEX string returns a RGB array equivalent. + * @param string $color + * @return array RGB associative array + */ + public static function hexToRgb( $color ){ + + // Sanity check + $color = self::_checkHex($color); + + // Convert HEX to DEC + $R = hexdec($color[0].$color[1]); + $G = hexdec($color[2].$color[3]); + $B = hexdec($color[4].$color[5]); + + $RGB['R'] = $R; + $RGB['G'] = $G; + $RGB['B'] = $B; + + return $RGB; + } + + + /** + * Given an RGB associative array returns the equivalent HEX string + * @param array $rgb + * @return string RGB string + * @throws Exception "Bad RGB Array" + */ + public static function rgbToHex( $rgb = array() ){ + // Make sure it's RGB + if(empty($rgb) || !isset($rgb["R"]) || !isset($rgb["G"]) || !isset($rgb["B"]) ) { + throw new Exception("Param was not an RGB array"); + } + + // Convert RGB to HEX + $hex[0] = str_pad(dechex( $rgb['R'] ), 2, '0', STR_PAD_LEFT); + $hex[1] = str_pad(dechex( $rgb['G'] ), 2, '0', STR_PAD_LEFT); + $hex[2] = str_pad(dechex( $rgb['B'] ), 2, '0', STR_PAD_LEFT); + + return implode( '', $hex ); + + } + + + /** + * Given a HEX value, returns a darker color. If no desired amount provided, then the color halfway between + * given HEX and black will be returned. + * @param int $amount + * @return string Darker HEX value + */ + public function darken( $amount = self::DEFAULT_ADJUST ){ + // Darken + $darkerHSL = $this->_darken($this->_hsl, $amount); + // Return as HEX + return self::hslToHex($darkerHSL); + } + + /** + * Given a HEX value, returns a lighter color. If no desired amount provided, then the color halfway between + * given HEX and white will be returned. + * @param int $amount + * @return string Lighter HEX value + */ + public function lighten( $amount = self::DEFAULT_ADJUST ){ + // Lighten + $lighterHSL = $this->_lighten($this->_hsl, $amount); + // Return as HEX + return self::hslToHex($lighterHSL); + } + + /** + * Given a HEX value, returns a mixed color. If no desired amount provided, then the color mixed by this ratio + * @param int $amount = -100..0..+100 + * @return string mixed HEX value + */ + public function mix($hex2, $amount = 0){ + $rgb2 = self::hexToRgb($hex2); + $mixed = $this->_mix($this->_rgb, $rgb2, $amount); + // Return as HEX + return self::rgbToHex($mixed); + } + + /** + * Creates an array with two shades that can be used to make a gradient + * @param int $amount Optional percentage amount you want your contrast color + * @return array An array with a 'light' and 'dark' index + */ + public function makeGradient( $amount = self::DEFAULT_ADJUST ) { + // Decide which color needs to be made + if( $this->isLight() ) { + $lightColor = $this->_hex; + $darkColor = $this->darken($amount); + } else { + $lightColor = $this->lighten($amount); + $darkColor = $this->_hex; + } + + // Return our gradient array + return array( "light" => $lightColor, "dark" => $darkColor ); + } + + + /** + * Returns whether or not given color is considered "light" + * @param string|Boolean $color + * @return boolean + */ + public function isLight( $color = FALSE ){ + // Get our color + $color = ($color) ? $color : $this->_hex; + + // Calculate straight from rbg + $r = hexdec($color[0].$color[1]); + $g = hexdec($color[2].$color[3]); + $b = hexdec($color[4].$color[5]); + + return (( $r*299 + $g*587 + $b*114 )/1000 > 130); + } + + /** + * Returns whether or not a given color is considered "dark" + * @param string|Boolean $color + * @return boolean + */ + public function isDark( $color = FALSE ){ + // Get our color + $color = ($color) ? $color:$this->_hex; + + // Calculate straight from rbg + $r = hexdec($color[0].$color[1]); + $g = hexdec($color[2].$color[3]); + $b = hexdec($color[4].$color[5]); + + return (( $r*299 + $g*587 + $b*114 )/1000 <= 130); + } + + /** + * Returns the complimentary color + * @return string Complementary hex color + * + */ + public function complementary() { + // Get our HSL + $hsl = $this->_hsl; + + // Adjust Hue 180 degrees + $hsl['H'] += ($hsl['H']>180) ? -180:180; + + // Return the new value in HEX + return self::hslToHex($hsl); + } + + /** + * Returns your color's HSL array + */ + public function getHsl() { + return $this->_hsl; + } + /** + * Returns your original color + */ + public function getHex() { + return $this->_hex; + } + /** + * Returns your color's RGB array + */ + public function getRgb() { + return $this->_rgb; + } + + /** + * Returns the cross browser CSS3 gradient + * @param int Optional: percentage amount to light/darken the gradient + * @param boolean $vintageBrowsers Optional: include vendor prefixes for browsers that almost died out already + * @param string $prefix Optional: prefix for every lines + * @param string $suffix Optional: suffix for every lines + * @link http://caniuse.com/css-gradients Resource for the browser support + * @return string CSS3 gradient for chrome, safari, firefox, opera and IE10 + */ + public function getCssGradient( $amount = self::DEFAULT_ADJUST, $vintageBrowsers = FALSE, $suffix = "" , $prefix = "" ) { + + // Get the recommended gradient + $g = $this->makeGradient($amount); + + $css = ""; + /* fallback/image non-cover color */ + $css .= "{$prefix}background-color: #".$this->_hex.";{$suffix}"; + + /* IE Browsers */ + $css .= "{$prefix}filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#".$g['light']."', endColorstr='#".$g['dark']."');{$suffix}"; + + /* Safari 4+, Chrome 1-9 */ + if ( $vintageBrowsers ) { + $css .= "{$prefix}background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#".$g['light']."), to(#".$g['dark']."));{$suffix}"; + } + + /* Safari 5.1+, Mobile Safari, Chrome 10+ */ + $css .= "{$prefix}background-image: -webkit-linear-gradient(top, #".$g['light'].", #".$g['dark'].");{$suffix}"; + + /* Firefox 3.6+ */ + if ( $vintageBrowsers ) { + $css .= "{$prefix}background-image: -moz-linear-gradient(top, #".$g['light'].", #".$g['dark'].");{$suffix}"; + } + + /* Opera 11.10+ */ + if ( $vintageBrowsers ) { + $css .= "{$prefix}background-image: -o-linear-gradient(top, #".$g['light'].", #".$g['dark'].");{$suffix}"; + } + + /* Unprefixed version (standards): FF 16+, IE10+, Chrome 26+, Safari 7+, Opera 12.1+ */ + $css .= "{$prefix}background-image: linear-gradient(to bottom, #".$g['light'].", #".$g['dark'].");{$suffix}"; + + // Return our CSS + return $css; + } + + // =========================== + // = Private Functions Below = + // =========================== + + + /** + * Darkens a given HSL array + * @param array $hsl + * @param int $amount + * @return array $hsl + */ + private function _darken( $hsl, $amount = self::DEFAULT_ADJUST){ + // Check if we were provided a number + if( $amount ) { + $hsl['L'] = ($hsl['L'] * 100) - $amount; + $hsl['L'] = ($hsl['L'] < 0) ? 0:$hsl['L']/100; + } else { + // We need to find out how much to darken + $hsl['L'] = $hsl['L']/2 ; + } + + return $hsl; + } + + /** + * Lightens a given HSL array + * @param array $hsl + * @param int $amount + * @return array $hsl + */ + private function _lighten( $hsl, $amount = self::DEFAULT_ADJUST){ + // Check if we were provided a number + if( $amount ) { + $hsl['L'] = ($hsl['L'] * 100) + $amount; + $hsl['L'] = ($hsl['L'] > 100) ? 1:$hsl['L']/100; + } else { + // We need to find out how much to lighten + $hsl['L'] += (1-$hsl['L'])/2; + } + + return $hsl; + } + + /** + * Mix 2 rgb colors and return an rgb color + * @param array $rgb1 + * @param array $rgb2 + * @param int $amount ranged -100..0..+100 + * @return array $rgb + * + * ported from http://phpxref.pagelines.com/nav.html?includes/class.colors.php.source.html + */ + private function _mix($rgb1, $rgb2, $amount = 0) { + + $r1 = ($amount + 100) / 100; + $r2 = 2 - $r1; + + $rmix = (($rgb1['R'] * $r1) + ($rgb2['R'] * $r2)) / 2; + $gmix = (($rgb1['G'] * $r1) + ($rgb2['G'] * $r2)) / 2; + $bmix = (($rgb1['B'] * $r1) + ($rgb2['B'] * $r2)) / 2; + + return array('R' => $rmix, 'G' => $gmix, 'B' => $bmix); + } + + /** + * Given a Hue, returns corresponding RGB value + * @param type $v1 + * @param type $v2 + * @param type $vH + * @return int + */ + private static function _huetorgb( $v1,$v2,$vH ) { + if( $vH < 0 ) { + $vH += 1; + } + + if( $vH > 1 ) { + $vH -= 1; + } + + if( (6*$vH) < 1 ) { + return ($v1 + ($v2 - $v1) * 6 * $vH); + } + + if( (2*$vH) < 1 ) { + return $v2; + } + + if( (3*$vH) < 2 ) { + return ($v1 + ($v2-$v1) * ( (2/3)-$vH ) * 6); + } + + return $v1; + + } + + /** + * You need to check if you were given a good hex string + * @param string $hex + * @return string Color + * @throws Exception "Bad color format" + */ + private static function _checkHex( $hex ) { + // Strip # sign is present + $color = str_replace("#", "", $hex); + + // Make sure it's 6 digits + if( strlen($color) == 3 ) { + $color = $color[0].$color[0].$color[1].$color[1].$color[2].$color[2]; + } else if( strlen($color) != 6 ) { + throw new Exception("HEX color needs to be 6 or 3 digits long"); + } + + return $color; + } + +} diff --git a/extras/phpColors/test/colorTest.php b/extras/phpColors/test/colorTest.php new file mode 100644 index 0000000..343ed30 --- /dev/null +++ b/extras/phpColors/test/colorTest.php @@ -0,0 +1,50 @@ +<?php + +require_once __DIR__ . "/../src/color.php"; +use phpColors\Color; + +class ColorTest extends PHPUnit_Framework_TestCase { + + protected function setUp() {} + + public function testDarkenWithDefaultAdjustment() { + + $expected = array( + "336699" => "264d73", + "913399" => "6d2673" + ); + + foreach ($expected as $original => $darker) { + + $color = new Color($original); + + $this->assertEquals( + $darker, + $color->darken(), + "Incorrect darker color returned." + ); + } + } + + public function testLightenWithDefaultAdjustment() { + + $expected = array( + "336699" => "4080bf", + "913399" => "b540bf" + ); + + foreach ($expected as $original => $darker) { + + $color = new Color($original); + + $this->assertEquals( + $darker, + $color->lighten(), + "Incorrect lighter color returned." + ); + } + } + +} + +/* End of file color.php */ diff --git a/extras/pro-admin-helpers.js b/extras/pro-admin-helpers.js new file mode 100644 index 0000000..254dd19 --- /dev/null +++ b/extras/pro-admin-helpers.js @@ -0,0 +1,153 @@ +///<reference path="../js/jquery.d.ts"/> +///<reference path="../js/jquery.biscuit.d.ts"/> +'use strict'; +(function ($) { + var isHeadingStateRestored = false; + function setCollapsedState($heading, isCollapsed) { + $heading.toggleClass('ame-is-collapsed-heading', isCollapsed); + //Show/hide all menu items between this heading and the next one. + var containedItems = $heading.nextUntil('li.ame-menu-heading-item, #collapse-menu', 'li.menu-top,li.wp-menu-separator'); + containedItems.toggle(!isCollapsed); + } + /** + * Save the collapsed/expanded state of menu headings. + */ + function saveCollapsedHeadings($adminMenu) { + var collapsedHeadings = loadCollapsedHeadings(); + var currentTime = Date.now(); + $adminMenu.find('li[id].ame-collapsible-heading').each(function () { + var $heading = $(this), id = $heading.attr('id'); + if (id) { + if ($heading.hasClass('ame-is-collapsed-heading')) { + collapsedHeadings[id] = currentTime; + } + else if (collapsedHeadings.hasOwnProperty(id)) { + delete collapsedHeadings[id]; + } + } + }); + //Discard stored data associated with headings that haven't been seen in a long time. + //It's likely that the headings no longer exist. + if (Object.keys) { + var threshold = currentTime - (90 * 24 * 3600 * 1000); + var headingIds = Object.keys(collapsedHeadings); + for (var i = 0; i < headingIds.length; i++) { + var id = headingIds[i]; + if (!collapsedHeadings.hasOwnProperty(id)) { + continue; + } + if (collapsedHeadings[id] < threshold) { + delete collapsedHeadings[id]; + } + } + } + $.cookie('ame-collapsed-menu-headings', JSON.stringify(collapsedHeadings), { expires: 90 }); + } + function loadCollapsedHeadings() { + var defaultValue = {}; + if (!$.cookie) { + return defaultValue; + } + try { + var settings = JSON.parse($.cookie('ame-collapsed-menu-headings')); + if (typeof settings === 'object') { + return settings; + } + return defaultValue; + } + catch (_a) { + return defaultValue; + } + } + /** + * Restore the previous collapsed/expanded state of menu headings. + */ + function restoreCollapsedHeadings() { + isHeadingStateRestored = true; + var previouslyCollapsedHeadings = loadCollapsedHeadings(); + var $adminMenu = $('#adminmenumain #adminmenu'); + for (var id in previouslyCollapsedHeadings) { + if (!previouslyCollapsedHeadings.hasOwnProperty(id)) { + continue; + } + var $heading = $adminMenu.find('#' + id); + if ($heading.length > 0) { + setCollapsedState($heading, true); + } + } + } + $(document).on('restoreCollapsedHeadings.adminMenuEditor', function () { + if (!isHeadingStateRestored) { + restoreCollapsedHeadings(); + } + }); + jQuery(function ($) { + //Menu headings: Handle clicks. + var $adminMenu = $('#adminmenumain #adminmenu'); + $adminMenu.find('li.ame-menu-heading-item a').on('click', function () { + var $heading = $(this).closest('li'); + var canBeCollapsed = $heading.hasClass('ame-collapsible-heading'); + if (!canBeCollapsed) { + //By default, do nothing. The heading is implemented as a link due to how the admin menu + //works, but we don't want it to go to a different URL on click. + return false; + } + var isCollapsed = !$heading.hasClass('ame-is-collapsed-heading'); + setCollapsedState($heading, isCollapsed); + //Remember the collapsed/expanded state. + if ($.cookie) { + setTimeout(saveCollapsedHeadings.bind(window, $adminMenu), 50); + } + return false; + }); + if (!isHeadingStateRestored) { + restoreCollapsedHeadings(); + } + if (typeof wsAmeProAdminHelperData === 'undefined') { + return; + } + //Menu headings: If the user hasn't specified a custom text color, make sure the color + //doesn't change on hover/focus. + if (wsAmeProAdminHelperData.setHeadingHoverColor && Array.prototype.map) { + var baseTextColor = void 0; + //Look at the first N menu items to discover the default text color. + var $menus = $('#adminmenumain #adminmenu li.menu-top') + .not('.wp-menu-separator') + .not('.ame-menu-heading-item') + .slice(0, 10) + .find('> a .wp-menu-name'); + var mostCommonColor_1 = '#eeeeee', seenColors_1 = {}; + seenColors_1[mostCommonColor_1] = 0; + $menus.each(function () { + var color = $(this).css('color'); + if (color) { + if (seenColors_1.hasOwnProperty(color)) { + seenColors_1[color] = seenColors_1[color] + 1; + } + else { + seenColors_1[color] = 1; + } + if (seenColors_1[color] > seenColors_1[mostCommonColor_1]) { + mostCommonColor_1 = color; + } + } + }); + baseTextColor = mostCommonColor_1; + //We want to override the default menu colors, but not per-item styles. + var parentSelector_1 = '#adminmenu li.ame-menu-heading-item'; + var selectors = [':hover', ':active', ':focus', ' a:hover', ' a:active', ' a:focus'].map(function (suffix) { + return parentSelector_1 + suffix; + }); + var $newStyle = $('<style type="text/css">') + .text(selectors.join(',\n') + ' { color: ' + baseTextColor + '; }'); + var $adminCssNode = $('link#admin-menu-css').first(); + if ($adminCssNode.length === 1) { + $newStyle.insertAfter($adminCssNode); + } + else { + $newStyle.appendTo('head'); + } + } + }); +})(jQuery); +//# sourceMappingURL=pro-admin-helpers.js.map \ No newline at end of file diff --git a/extras/pro-admin-helpers.js.map b/extras/pro-admin-helpers.js.map new file mode 100644 index 0000000..a70a774 --- /dev/null +++ b/extras/pro-admin-helpers.js.map @@ -0,0 +1 @@ +{"version":3,"file":"pro-admin-helpers.js","sourceRoot":"","sources":["pro-admin-helpers.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/extras/pro-admin-helpers.ts b/extras/pro-admin-helpers.ts new file mode 100644 index 0000000..f58f8ad --- /dev/null +++ b/extras/pro-admin-helpers.ts @@ -0,0 +1,178 @@ +///<reference path="../js/jquery.d.ts"/> +///<reference path="../js/jquery.biscuit.d.ts"/> +'use strict'; + +declare var wsAmeProAdminHelperData: Record<string, any>; + +(function ($) { + let isHeadingStateRestored = false; + + function setCollapsedState($heading: JQuery, isCollapsed: boolean) { + $heading.toggleClass('ame-is-collapsed-heading', isCollapsed); + + //Show/hide all menu items between this heading and the next one. + const containedItems = $heading.nextUntil('li.ame-menu-heading-item, #collapse-menu', 'li.menu-top,li.wp-menu-separator'); + containedItems.toggle(!isCollapsed); + } + + /** + * Save the collapsed/expanded state of menu headings. + */ + function saveCollapsedHeadings($adminMenu: JQuery) { + let collapsedHeadings = loadCollapsedHeadings(); + const currentTime = Date.now(); + + $adminMenu.find('li[id].ame-collapsible-heading').each(function () { + const $heading = $(this), id = $heading.attr('id'); + if (id) { + if ($heading.hasClass('ame-is-collapsed-heading')) { + collapsedHeadings[id] = currentTime; + } else if (collapsedHeadings.hasOwnProperty(id)) { + delete collapsedHeadings[id]; + } + } + }); + + //Discard stored data associated with headings that haven't been seen in a long time. + //It's likely that the headings no longer exist. + if (Object.keys) { + const threshold = currentTime - (90 * 24 * 3600 * 1000); + let headingIds = Object.keys(collapsedHeadings); + for (let i = 0; i < headingIds.length; i++) { + const id = headingIds[i]; + if (!collapsedHeadings.hasOwnProperty(id)) { + continue; + } + if (collapsedHeadings[id] < threshold) { + delete collapsedHeadings[id]; + } + } + } + + $.cookie('ame-collapsed-menu-headings', JSON.stringify(collapsedHeadings), {expires: 90}); + } + + function loadCollapsedHeadings(): Record<string, number> { + let defaultValue = {}; + if (!$.cookie) { + return defaultValue; + } + + try { + let settings = JSON.parse($.cookie('ame-collapsed-menu-headings')); + if (typeof settings === 'object') { + return settings; + } + return defaultValue; + } catch { + return defaultValue; + } + } + + /** + * Restore the previous collapsed/expanded state of menu headings. + */ + function restoreCollapsedHeadings() { + isHeadingStateRestored = true; + + const previouslyCollapsedHeadings = loadCollapsedHeadings(); + const $adminMenu = $('#adminmenumain #adminmenu'); + for (let id in previouslyCollapsedHeadings) { + if (!previouslyCollapsedHeadings.hasOwnProperty(id)) { + continue; + } + const $heading = $adminMenu.find('#' + id); + if ($heading.length > 0) { + setCollapsedState($heading, true); + } + } + } + + $(document).on('restoreCollapsedHeadings.adminMenuEditor', function () { + if (!isHeadingStateRestored) { + restoreCollapsedHeadings(); + } + }); + + jQuery(function ($) { + //Menu headings: Handle clicks. + const $adminMenu = $('#adminmenumain #adminmenu'); + + $adminMenu.find('li.ame-menu-heading-item a').on('click', function () { + const $heading = $(this).closest('li'); + const canBeCollapsed = $heading.hasClass('ame-collapsible-heading'); + + if (!canBeCollapsed) { + //By default, do nothing. The heading is implemented as a link due to how the admin menu + //works, but we don't want it to go to a different URL on click. + return false; + } + + let isCollapsed = !$heading.hasClass('ame-is-collapsed-heading'); + setCollapsedState($heading, isCollapsed); + + //Remember the collapsed/expanded state. + if ($.cookie) { + setTimeout(saveCollapsedHeadings.bind(window, $adminMenu), 50); + } + + return false; + }); + + if (!isHeadingStateRestored) { + restoreCollapsedHeadings(); + } + + if (typeof wsAmeProAdminHelperData === 'undefined') { + return; + } + + //Menu headings: If the user hasn't specified a custom text color, make sure the color + //doesn't change on hover/focus. + if (wsAmeProAdminHelperData.setHeadingHoverColor && Array.prototype.map) { + let baseTextColor; + + //Look at the first N menu items to discover the default text color. + const $menus = $('#adminmenumain #adminmenu li.menu-top') + .not('.wp-menu-separator') + .not('.ame-menu-heading-item') + .slice(0, 10) + .find('> a .wp-menu-name'); + + let mostCommonColor = '#eeeeee', seenColors = {}; + seenColors[mostCommonColor] = 0; + + $menus.each(function () { + const color = $(this).css('color'); + if (color) { + if (seenColors.hasOwnProperty(color)) { + seenColors[color] = seenColors[color] + 1; + } else { + seenColors[color] = 1; + } + + if (seenColors[color] > seenColors[mostCommonColor]) { + mostCommonColor = color; + } + } + }); + + baseTextColor = mostCommonColor; + + //We want to override the default menu colors, but not per-item styles. + const parentSelector = '#adminmenu li.ame-menu-heading-item'; + let selectors = [':hover', ':active', ':focus', ' a:hover', ' a:active', ' a:focus'].map(function (suffix) { + return parentSelector + suffix; + }); + + const $newStyle = $('<style type="text/css">') + .text(selectors.join(',\n') + ' { color: ' + baseTextColor + '; }'); + const $adminCssNode = $('link#admin-menu-css').first(); + if ($adminCssNode.length === 1) { + $newStyle.insertAfter($adminCssNode); + } else { + $newStyle.appendTo('head'); + } + } + }); +})(jQuery); \ No newline at end of file diff --git a/extras/pro-admin-styles.css b/extras/pro-admin-styles.css new file mode 100644 index 0000000..e36d4c8 --- /dev/null +++ b/extras/pro-admin-styles.css @@ -0,0 +1,69 @@ +/* + * Third level menus. + */ +#adminmenu .ame-deep-submenu, .folded #adminmenu .ame-deep-submenu { + position: absolute; +} +#adminmenu li.menu-top.opensub .ame-deep-submenu, #adminmenu li.menu-top.wp-has-current-submenu .ame-deep-submenu, .folded #adminmenu li.menu-top.opensub .ame-deep-submenu, .folded #adminmenu li.menu-top.wp-has-current-submenu .ame-deep-submenu { + top: -1000em; + position: absolute; +} +#adminmenu li.ame-has-deep-submenu.ame-has-highlighted-item > a:first-of-type, #adminmenu li.ame-has-deep-submenu.ame-has-current-deep-submenu > a:first-of-type, .folded #adminmenu li.ame-has-deep-submenu.ame-has-highlighted-item > a:first-of-type, .folded #adminmenu li.ame-has-deep-submenu.ame-has-current-deep-submenu > a:first-of-type { + background: #0073aa; + color: #fff; +} +#adminmenu li.ame-has-deep-submenu.ame-has-highlighted-item > a:first-of-type::after, #adminmenu li.ame-has-deep-submenu.ame-has-current-deep-submenu > a:first-of-type::after, .folded #adminmenu li.ame-has-deep-submenu.ame-has-highlighted-item > a:first-of-type::after, .folded #adminmenu li.ame-has-deep-submenu.ame-has-current-deep-submenu > a:first-of-type::after { + content: "\f140"; + margin-top: -9px; +} +#adminmenu li.ame-has-deep-submenu.ame-has-highlighted-item > .ame-deep-submenu, #adminmenu li.ame-has-deep-submenu.ame-has-current-deep-submenu > .ame-deep-submenu, .folded #adminmenu li.ame-has-deep-submenu.ame-has-highlighted-item > .ame-deep-submenu, .folded #adminmenu li.ame-has-deep-submenu.ame-has-current-deep-submenu > .ame-deep-submenu { + top: 0; + position: relative; + padding-top: 0; + padding-bottom: 0; +} +#adminmenu li.ame-has-deep-submenu:not(.ame-has-submenu-icons).ame-has-highlighted-item > .ame-deep-submenu > li > a, #adminmenu li.ame-has-deep-submenu:not(.ame-has-submenu-icons).ame-has-current-deep-submenu > .ame-deep-submenu > li > a, .folded #adminmenu li.ame-has-deep-submenu:not(.ame-has-submenu-icons).ame-has-highlighted-item > .ame-deep-submenu > li > a, .folded #adminmenu li.ame-has-deep-submenu:not(.ame-has-submenu-icons).ame-has-current-deep-submenu > .ame-deep-submenu > li > a { + padding-left: 24px; +} +#adminmenu li.ame-has-deep-submenu > a, .folded #adminmenu li.ame-has-deep-submenu > a { + position: relative; +} +#adminmenu li.ame-has-deep-submenu > a::after, .folded #adminmenu li.ame-has-deep-submenu > a::after { + position: absolute; + right: 6px; + top: 50%; + margin-top: -9px; + height: 18px; + width: 20px; + font-family: dashicons, serif; + content: "\f139"; + font-size: 23px; + line-height: 18px; + text-align: right; +} + +#adminmenu .wp-submenu li.opensub > ul.ame-deep-submenu, +.folded #adminmenu .wp-submenu li.opensub > ul.ame-deep-submenu { + top: -1px; +} + +.folded #adminmenu li.opensub > ul.ame-deep-submenu, +.folded #adminmenu .wp-has-current-submenu.opensub > ul.ame-deep-submenu, +.no-js.folded #adminmenu .ame-has-deep-submenu:hover > ul.ame-deep-submenu { + top: 0; + left: 160px; +} + +.folded #adminmenu li.opensub li.ame-has-highlighted-item > ul.ame-deep-submenu { + left: 0; + box-shadow: none; +} +.folded #adminmenu li.opensub li.ame-has-highlighted-item > ul.ame-deep-submenu > li.wp-submenu-head { + display: none; +} + +.folded #adminmenu li.ame-has-deep-submenu.ame-has-highlighted-item > .ame-deep-submenu, .folded #adminmenu li.ame-has-deep-submenu.ame-has-current-deep-submenu > .ame-deep-submenu { + border-left-width: 0; +} + +/*# sourceMappingURL=pro-admin-styles.css.map */ diff --git a/extras/pro-admin-styles.scss b/extras/pro-admin-styles.scss new file mode 100644 index 0000000..3bef4c4 --- /dev/null +++ b/extras/pro-admin-styles.scss @@ -0,0 +1,109 @@ +@use "sass:color"; +@use "sass:math"; + +/* + * Third level menus. + */ + +#adminmenu, .folded #adminmenu { + .ame-deep-submenu { + position: absolute; + } + + li.menu-top { + &.opensub, &.wp-has-current-submenu { + .ame-deep-submenu { + top: -1000em; + position: absolute; + } + } + } + + li.ame-has-deep-submenu { + $triangleSize: 23px; + $pointerSize: 18px; + + &.ame-has-highlighted-item, &.ame-has-current-deep-submenu { + > a:first-of-type { + background: #0073aa; //Default active background color in WP 5.6.x. + color: #fff; + + &::after { + content: "\f140"; + margin-top: -$pointerSize/2; + } + } + + > .ame-deep-submenu { + top: 0; + position: relative; + + padding-top: 0; + padding-bottom: 0; + } + } + + //Don't override the left padding of submenus with icons. Those already have custom padding. + &:not(.ame-has-submenu-icons) { + &.ame-has-highlighted-item, &.ame-has-current-deep-submenu { + > .ame-deep-submenu > li > a { + padding-left: 24px; + } + } + } + + > a { + position: relative; + } + + > a::after { + position: absolute; + right: 6px; + top: 50%; + margin-top: -$pointerSize/2; + + height: $pointerSize; + width: math.max($pointerSize, 20px); + + font-family: dashicons, serif; + content: "\f139"; + font-size: $triangleSize; + line-height: $pointerSize; + text-align: right; + } + } +} + +#adminmenu .wp-submenu li.opensub > ul.ame-deep-submenu, +.folded #adminmenu .wp-submenu li.opensub > ul.ame-deep-submenu { + top: -1px; +} + +.folded #adminmenu li.opensub > ul.ame-deep-submenu, +.folded #adminmenu .wp-has-current-submenu.opensub > ul.ame-deep-submenu, +.no-js.folded #adminmenu .ame-has-deep-submenu:hover > ul.ame-deep-submenu { + top: 0; + left: 160px; +} + +.folded #adminmenu li.opensub li.ame-has-highlighted-item > ul.ame-deep-submenu { + //Fix deep submenu layout in the folded state. + left: 0; + box-shadow: none; + + //Hide the submenu head. The parent item will serve as the head. + > li.wp-submenu-head { + display: none; + } +} + +//Folded state: WP adds a transparent left border to the current submenu, and +//that gets inherited by the current deep submenu. This messes up text alignment, +//so let's remove the border. +.folded #adminmenu li.ame-has-deep-submenu { + &.ame-has-highlighted-item, &.ame-has-current-deep-submenu { + > .ame-deep-submenu { + border-left-width: 0; + } + } +} \ No newline at end of file diff --git a/extras/third-level-menus.js b/extras/third-level-menus.js new file mode 100644 index 0000000..2e271ec --- /dev/null +++ b/extras/third-level-menus.js @@ -0,0 +1,286 @@ +//Multi-level admin menu support. +(function ($) { + let hasInitStarted = false; + + let $window; + let $wpwrap; + let $adminMenu; + let $adminBar; + + function init() { + if (hasInitStarted) { + return; + } + hasInitStarted = true; + + $window = $(window); + $wpwrap = $('#wpwrap'); + $adminMenu = $('#adminmenu'); + $adminBar = $('#wpadminbar'); + + //Adjust the colors of the current deep submenu's parent(s) to match the active admin color scheme. + //Find a highlighted top-level menu. + const $highlightedMenuLink = $adminMenu.find('li.menu-top.wp-has-current-submenu,li.menu-top.current') + .find('> a').first(); + if ($highlightedMenuLink.length > 0) { + //Get the background and text color from that menu. + const background = $highlightedMenuLink.css('backgroundColor'); + const textColor = $highlightedMenuLink.css('color'); + if (background && textColor) { + //Create a new style block that will apply these colors to deep submenu parents. + let selectors = [ + '#adminmenu li.ame-has-deep-submenu.ame-has-highlighted-item > a:first-of-type', + '#adminmenu li.ame-has-deep-submenu.ame-has-current-deep-submenu > a:first-of-type', + '.folded #adminmenu li.ame-has-deep-submenu.ame-has-highlighted-item > a:first-of-type', + '.folded #adminmenu li.ame-has-deep-submenu.ame-has-current-deep-submenu > a:first-of-type' + ]; + const $newStyle = $('<style>') + .text(selectors.join(',\n') + ' { background: ' + background + '; color: ' + textColor + '; }'); + const $proStylesheetNode = $('link#ame-pro-admin-styles-css').first(); + if ($proStylesheetNode.length === 1) { + $newStyle.insertAfter($proStylesheetNode); + } else { + $newStyle.appendTo('head'); + } + } + } + + /* + * Known issue: When a submenu item is highlighted as the current item and it has a nested + * submenu, all of its children will inherit the "current item" style. This happens because + * all admin color schemes that I've seen use an "all children" selector ("li.current a") when + * setting submenu colors, not an "immediate children" selector ("li.current > a"). + * + * I've investigated several ways to fix that, but found no practical solution. + * - Overriding menu styles doesn't work due to selector specificity problems. + * - Editing the loaded admin-menu.css stylesheet with JS isn't reliable because the user + * might use a custom admin color scheme that has its own stylesheet(s). + * - Searching through all loaded stylesheets for rules that apply to submenu items seems + * too slow to do it on every page load. + */ + + //Find all items that have deep submenus and move the submenus to the items. + const $deepParents = $adminMenu.find('li.ame-has-deep-submenu'); + //The parent item should have a unique class that has the prefix "ame-ds-m". + const dsClassPrefix = 'ame-ds-m'; + + $deepParents.each(function () { + const $deepParent = $(this) + const $deepLink = $deepParent.find('> a').first(); + + const parentElement = $deepParent.get(0); + if (!parentElement || (typeof parentElement.classList === 'undefined')) { + return; + } + + const classes = parentElement.classList; + let uniqueClass = null; + for (let i = 0; i < classes.length; i++) { + if (classes[i].substr(0, dsClassPrefix.length) === dsClassPrefix) { + uniqueClass = classes[i]; + break; + } + } + + if (!uniqueClass) { + return; + } + + const $tempContainer = $adminMenu.find('li.ame-ds-child-of-' + uniqueClass).first(); + if ($tempContainer.length < 1) { + //The submenu doesn't exist, bail. + $deepParent.removeClass('ame-has-deep-submenu'); + return; + } + + const $deepSubmenu = $tempContainer.find('> ul.wp-submenu').first(); + + //Move the submenu into the parent item. + $deepLink.after($deepSubmenu); + $deepParent.css({ + position: 'relative', + overflow: 'visible' + }); + $deepSubmenu.addClass('ame-deep-submenu'); + + //If the submenu has icons, the parent needs the appropriate class. + if ($tempContainer.hasClass('ame-has-submenu-icons')) { + $deepParent.addClass('ame-has-submenu-icons'); + } + + //Remove the temporary container, we don't need it anymore. + $tempContainer.remove(); + + $deepParent.hoverIntent({ + over: function () { + let $menuItem = $(this), + $submenu = $menuItem.find('.wp-submenu'), + top = parseInt($submenu.css('top'), 10); + + if (isNaN(top) || top > -5) { //The submenu is already visible, don't try to open it again. + return; + } + + if ($adminMenu.data('wp-responsive')) { + return; + } + + //console.log('AME: Open menu on hover'); + //Close any sibling menus. + $menuItem.closest('ul').find('li.opensub').removeClass('opensub'); + //Open this menu. + $menuItem.addClass('opensub'); + readjustSubmenu($menuItem, true); + }, + out: function () { + if ($adminMenu.data('wp-responsive')) { + return; + } + + //console.log('AME: Close menu on hover out'); + $(this).removeClass('opensub').find('.ame-deep-submenu').first().css('margin-top', ''); + }, + timeout: 200, + sensitivity: 7, + interval: 90 + }); + }); + + //The current menu item might be in a deeply nested submenu. Let's highlight it right now + //to reduce the risk that menus will visibly jump around and change as the page loads. + $(document).trigger('adminMenuEditor:highlightCurrentMenu'); + } + + /** + * Ensure a deeply nested admin submenu is within the visual viewport. + * Based on the adjustSubmenu function found in /wp-admin/js/common.js. + * + * @param {JQuery} $menuItem The parent menu item containing the submenu(s). + * @param {Boolean} [onlyIncludeOpen] Set to true to include only submenus that are currently open. + */ + function readjustSubmenu($menuItem, onlyIncludeOpen) { + let bottomOffset, adjustment, minTop, maxBottomOffset, maxPageBottomOffset, maxWindowBottomOffset, + $submenus, + adminBarHeight = (Math.ceil($adminBar.height()) || 32), + submenuSelector = '.wp-submenu.ame-deep-submenu'; + + //These constraints were chosen to emulate WordPress 5.6.1 behaviour. + const minDistance = { + fromWindowTop: adminBarHeight, + fromWindowBottom: 36, + fromPageBottom: 78 + }; + + //Optimization: Adjust only open submenus. + if (onlyIncludeOpen) { + $submenus = $menuItem.find('.opensub > ' + submenuSelector); + if ($menuItem.hasClass('opensub')) { + $submenus = $submenus.add('> ' + submenuSelector, $menuItem.get(0)); + } + } else { + $submenus = $menuItem.find(submenuSelector); + } + + //Keep the menu below the top edge of the window and below the admin bar. + minTop = $window.scrollTop() + minDistance.fromWindowTop; + //Keep the menu above the bottom edge of the window/page. + maxPageBottomOffset = ($wpwrap.height() + Math.ceil($wpwrap.offset().top)) - minDistance.fromPageBottom; + maxWindowBottomOffset = $window.scrollTop() + $window.height() - minDistance.fromWindowBottom; + maxBottomOffset = Math.min(maxWindowBottomOffset, maxPageBottomOffset); + + $submenus.each(function () { + const $submenu = $(this), + $directParentMenuItem = $submenu.closest('.ame-has-deep-submenu'), + parentItemTop = $directParentMenuItem.offset().top, + parentItemWidth = $directParentMenuItem.outerWidth(), + $firstSubmenuItem = $submenu.find('> li').not('.wp-submenu-head').first(), + submenuHeight = $submenu.outerHeight(); + + let submenuTop = parseInt($submenu.css('top'), 10); + if (isNaN(submenuTop) || (submenuTop < -50)) { + submenuTop = -1; + } + + const menuTop = parentItemTop + submenuTop; //Top offset of the menu. + bottomOffset = menuTop + submenuHeight + 1; //Bottom offset of the menu. I don't know why the +1 is required. + adjustment = 0; + + if ($firstSubmenuItem.length === 1) { + //Align the first item of the submenu with the parent item. This is the default adjustment + //when the submenu isn't close to either the top or the bottom of the window. + const firstItemDistance = $firstSubmenuItem.position().top; + if (Math.abs(firstItemDistance) < 200) { //Sanity check. The largest value I've seen is 38. + adjustment = -firstItemDistance - submenuTop; + } + } + if ((bottomOffset + adjustment) > maxBottomOffset) { + adjustment = maxBottomOffset - bottomOffset; + } + if ((menuTop + adjustment) < minTop) { + adjustment = minTop - menuTop; + } + + if (adjustment !== 0) { + $submenu.css('margin-top', adjustment + 'px'); + } else { + $submenu.css('margin-top', ''); + } + + /*console.log({ + menuTop: menuTop, + submenuTop: submenuTop, + submenuHeight: submenuHeight, + itemOffset: $directParentMenuItem.offset() + });*/ + + //Align the submenu with the right edge of its parent. In addition to simplifying CSS styles (fewer + //special cases for the folded state), this also improves compatibility with non-standard menu widths. + const submenuLeft = $submenu.position().left; + if (Math.abs(submenuLeft - parentItemWidth) > 1) { + $submenu.css('left', Math.ceil(parentItemWidth) + 'px'); + } + }); + } + + //Process the menu as soon as possible to reduce the risk that the user will see the submenu containers + //in the top level before they're moved to the right places. This is similar to the FOUC problem. + $(document).one('adminMenuEditor:menuDomReady', init); + + $(function () { + //In case our custom event wasn't triggered for some reason, let's call the init function again. + //The function should be designed to avoid duplicate initialisation. + init(); + + window.setTimeout(function () { + $adminMenu.on('focus.adminmenueditor', '.ame-deep-submenu a, li.ame-has-deep-submenu > a', function (event) { + if ($adminMenu.data('wp-responsive')) { + return; + } + + const $self = $(event.target); + //If this submenu is already visible because it contains the currently highlighted item, do nothing. + const $immediateParentItem = $self.closest('li.ame-has-deep-submenu'); + if ($immediateParentItem.hasClass('ame-has-highlighted-item')) { + return; + } + + //Expand all parents of this item. + const parentItems = $self.parentsUntil($adminMenu, 'li.ame-has-deep-submenu, li.menu-top'); + parentItems.addClass('opensub'); + readjustSubmenu(parentItems.last(), true); + }).on('blur.adminmenueditor', '.ame-deep-submenu a, li.ame-has-deep-submenu > a', function (event) { + if ($adminMenu.data('wp-responsive')) { + return; + } + + const $self = $(event.target); + const $immediateParentItem = $self.closest('li.ame-has-deep-submenu'); + if ($immediateParentItem.hasClass('ame-has-highlighted-item')) { + return; + } + + $immediateParentItem.removeClass('opensub'); + }); + }, 1); + }) +})(jQuery); \ No newline at end of file diff --git a/extras/wp-cli-integration.php b/extras/wp-cli-integration.php new file mode 100644 index 0000000..d9ace5d --- /dev/null +++ b/extras/wp-cli-integration.php @@ -0,0 +1,346 @@ +<?php /** @noinspection PhpComposerExtensionStubsInspection */ + +/** + * Implements the "admin-menu-editor" WP CLI command. + */ +class ameWpCliCommand extends WP_CLI_Command { + + /** + * Activate a license key on the site. + * + * ## OPTIONS + * + * <license-key> + * : The license key to use. This is usually a 32 character code, all uppercase. + * + * @synopsis <license-key> + * + * @subcommand activate-license + * @param array $args + * @throws \WP_CLI\ExitException + */ + public function activateLicense($args) { + if (count($args) < 1) { + WP_CLI::error('You must specify a license key'); + return; + } + + $licenseManager = $this->getLicenseManager(); + $result = $licenseManager->licenseThisSite($args[0]); + if ( is_wp_error($result) ) { + WP_CLI::error(sprintf('%s [%s]', $result->get_error_message(), $result->get_error_code())); + } else { + WP_CLI::success('Success! This site is now licensed.'); + } + } + + /** + * Remove the current license from the site. + * @subcommand deactivate-license + * @throws \WP_CLI\ExitException + */ + public function deactivateLicense() { + $licenseManager = $this->getLicenseManager(); + if ( !$licenseManager->hasExistingLicense() ) { + WP_CLI::error('This site does not have a license, so you can\'t deactivate the license.'); + return; + } + + $result = $licenseManager->unlicenseThisSite(); + if ( is_wp_error($result) ) { + WP_CLI::error(sprintf('%s [%s]', $result->get_error_message(), $result->get_error_code())); + } else { + WP_CLI::success('Success! The current license has been removed.'); + } + } + + /** + * Display information about the currently active license. + * @subcommand license-status + */ + public function licenseStatus() { + $licenseManager = $this->getLicenseManager(); + + if ( $licenseManager->hasExistingLicense() ) { + $license = $licenseManager->getLicense(); + $expiresOn = $license->get('expires_on'); + $licenseKey = $licenseManager->getLicenseKey(); + $token = $licenseManager->getSiteToken(); + + $info = array( + 'License found' => 'Yes', + 'Licensed URL' => $license->get('site_url', 'N/A'), + 'Status' => $license->getStatus(), + 'Expires' => empty($expiresOn) ? 'Never' : $expiresOn, + 'License key' => $licenseKey ? $licenseKey : 'Not stored in the WordPress database', + 'Site token' => $token ? $token : 'None', + ); + } else { + $info = array( + 'License found' => 'No' + ); + } + + foreach($info as $name => $value) { + WP_CLI::line(sprintf( + '%s: %s', + str_pad($name, 14, ' '), + $value + )); + } + } + + /** + * Export the plugin configuration as JSON. + * + * ## OPTIONS + * + * [<file>] + * : Export file name. + * + * [--all] + * : Export all settings. For backwards compatibility reasons, the default is to + * export only the admin menu configuration. + * + * [--output] + * : Dump the export data to the console instead of saving it as a file. + * + * [--file=<file>] + * : An alternative way to specify the export file name. + * + * [--pretty] + * : Indent the JSON output to make it more readable. + * + * @param array $args + * @param array $assoc_args + * @throws InvalidMenuException + * @throws \WP_CLI\ExitException + */ + public function export($args, $assoc_args) { + if ( empty($assoc_args['file']) && !empty($args[0]) ) { + $assoc_args['file'] = $args[0]; + } + + if ( empty($assoc_args['file']) && empty($assoc_args['output']) ) { + WP_CLI::error('You must specify either a file name or the "--output" parameter.'); + return; + } + + $menuEditor = $this->getMenuEditor(); + + if ( empty($assoc_args['all']) ) { + $customMenu = $menuEditor->load_custom_menu(); + + if ( empty($customMenu) ) { + WP_CLI::error('Nothing to export. This site is using the default admin menu.'); + return; + } + + $json = ameMenu::to_json(ameMenu::compress($customMenu)); + } else { + $exportedData = $this->getPorter()->export_data(); + $json = json_encode($exportedData, !empty($assoc_args['pretty']) ? JSON_PRETTY_PRINT : 0); + } + + if ( !empty($assoc_args['output']) ) { + WP_CLI::line($json); + } else { + $fileName = $assoc_args['file']; + $bytesWritten = file_put_contents($fileName, $json); + if ( $bytesWritten > 0 ) { + WP_CLI::success(sprintf('Export completed. %d bytes written.', $bytesWritten)); + } else { + WP_CLI::error('Failed to write the file.'); + } + } + } + + /** + * Import plugin configuration from a JSON file. + * + * ## OPTIONS + * + * <file> + * : Import file name. + * + * [--what=<comma-separated-list>] + * : Which settings to import. The default is to import everything that's in + * the input file. Run "wp admin-menu-editor list-exportable-modules" for a list + * of options. + * + * ## EXAMPLES + * + * # Import a file. + * wp admin-menu-editor import config.json + * + * # Import only specific parts of the configuration. + * wp admin-menu-editor import config.json --what=admin-menu,metaboxes + * + * @param array $args + * @param array $assoc_args + * @throws \WP_CLI\ExitException + */ + public function import($args, $assoc_args = array()) { + $fileName = $args[0]; + if ( !is_readable($fileName) ) { + WP_CLI::error('The file doesn\'t exist or isn\'t readable.'); + return; + } + + $enabledModules = null; + if ( !empty($assoc_args['what']) ) { + $enabledModules = explode(',', $assoc_args['what'], 100); + $enabledModules = array_map('trim', $enabledModules); + $enabledModules = array_fill_keys($enabledModules, true); + } + + $json = file_get_contents($fileName); + + //Is this valid JSON? + $decoded = json_decode($json, true); + if ( function_exists('json_last_error') && (json_last_error() !== JSON_ERROR_NONE) ) { + WP_CLI::error('Failed to parse JSON: ' . json_last_error_msg()); + return; + } + if ( empty($decoded) || !is_array($decoded) ) { + WP_CLI::error('Unexpected JSON value. This is probably not an Admin Menu Editor export file.'); + return; + } + + //Is it the unified configuration format? + if ( isset($decoded['format'], $decoded['format']['name']) ) { + if ( $decoded['format']['name'] === wsAmeImportExportFeature::$export_container_format_name ) { + $this->importUnifiedSettings($decoded, $enabledModules); + return; + } + } + + //It could also be an admin menu. + $this->importAdminMenu($json); + } + + /** + * @param array $container + * @param null $enabledModules + * @throws \WP_CLI\ExitException + */ + private function importUnifiedSettings($container, $enabledModules = null) { + WP_CLI::log('Importing settings...'); + + $moduleStatus = $this->getPorter()->import_data($container, $enabledModules); + $successfulImports = 0; + + foreach($moduleStatus as $id => $status) { + if ( isset($status['message']) ) { + $message = $status['message']; + } else if ( !empty($status['success']) ) { + $message = 'OK'; + } else if ( !empty($status['skipped']) ) { + $message = 'Skipped'; + } else { + $message = 'Error'; + } + + WP_CLI::log($id . ': ' . $message); + + if ( !empty($status['success']) ) { + $successfulImports++; + } + } + + if ( $successfulImports > 0 ) { + WP_CLI::success('Import completed.'); + } else { + WP_CLI::error('All modules either failed or were skipped.'); + } + } + + /** + * @param $json + * @throws \WP_CLI\ExitException + */ + private function importAdminMenu($json) { + try { + $loadedMenu = ameMenu::load_json($json); + } catch (Exception $ex) { + WP_CLI::error($ex->getMessage()); + return; + } + + $menuEditor = $this->getMenuEditor(); + + try { + $menuEditor->set_custom_menu($loadedMenu); + } catch (InvalidMenuException $ex) { + WP_CLI::error($ex->getMessage()); + return; + } + WP_CLI::success('Import completed.'); + } + + /** + * List settings that can be exported or imported. + * + * @subcommand list-exportable-modules + */ + public function listExportableModules() { + $modules = $this->getPorter()->get_exportable_components(); + if ( is_callable('WP_CLI\Utils\format_items') ) { + $items = array(); + foreach($modules as $id => $info) { + $items[] = array( + 'id' => $id, + 'label' => isset($info['label']) ? $info['label'] : '(No label)' + ); + } + WP_CLI\Utils\format_items('table', $items, array('id', 'label')); + } + } + + /** + * Reset the "who can access this plugin" setting to the default value. + * + * @subcommand reset-plugin-access + */ + public function resetPluginAccess() { + $menuEditor = $this->getMenuEditor(); + + $oldSetting = $menuEditor->get_plugin_option('plugin_access'); + $newSetting = $menuEditor->is_super_plugin() ? 'super_admin' : 'manage_options'; + $menuEditor->set_plugin_option('plugin_access', $newSetting); + + WP_CLI::success('Access permissions changed from "' . $oldSetting . '" to "' . $newSetting . '".'); + } + + /** + * @return WPMenuEditor + */ + private function getMenuEditor() { + return $GLOBALS['wp_menu_editor']; + } + + /** + * @return Wslm_LicenseManagerClient + */ + private function getLicenseManager() { + return $GLOBALS['ameProLicenseManager']; + } + + /** @noinspection PhpUnusedPrivateMethodInspection */ + /** + * @return wsMenuEditorExtras + */ + private function getExtras() { + return $GLOBALS['wsMenuEditorExtras']; + } + + /** + * @return wsAmeImportExportFeature + */ + private function getPorter() { + return wsAmeImportExportFeature::get_instance(); + } +} + +/** @noinspection PhpUnhandledExceptionInspection */ +WP_CLI::add_command('admin-menu-editor', 'ameWpCliCommand'); \ No newline at end of file diff --git a/images/arrows-dark.png b/images/arrows-dark.png new file mode 100644 index 0000000..29f814d Binary files /dev/null and b/images/arrows-dark.png differ diff --git a/images/arrows.png b/images/arrows.png new file mode 100644 index 0000000..775a7a0 Binary files /dev/null and b/images/arrows.png differ diff --git a/images/bullet_arrow_down2.png b/images/bullet_arrow_down2.png new file mode 100644 index 0000000..c81d5a4 Binary files /dev/null and b/images/bullet_arrow_down2.png differ diff --git a/images/bullet_error.png b/images/bullet_error.png new file mode 100644 index 0000000..bca2b49 Binary files /dev/null and b/images/bullet_error.png differ diff --git a/images/check-all.png b/images/check-all.png new file mode 100644 index 0000000..aba9d4d Binary files /dev/null and b/images/check-all.png differ diff --git a/images/copy-permissions.png b/images/copy-permissions.png new file mode 100644 index 0000000..7028bc6 Binary files /dev/null and b/images/copy-permissions.png differ diff --git a/images/delete.png b/images/delete.png new file mode 100644 index 0000000..08f2493 Binary files /dev/null and b/images/delete.png differ diff --git a/images/external.png b/images/external.png new file mode 100644 index 0000000..419c06f Binary files /dev/null and b/images/external.png differ diff --git a/images/font-awesome/angle-double-down.png b/images/font-awesome/angle-double-down.png new file mode 100644 index 0000000..a2edc1c Binary files /dev/null and b/images/font-awesome/angle-double-down.png differ diff --git a/images/font-awesome/eye-slash-color.png b/images/font-awesome/eye-slash-color.png new file mode 100644 index 0000000..0d3fe37 Binary files /dev/null and b/images/font-awesome/eye-slash-color.png differ diff --git a/images/font-awesome/eye-slash.png b/images/font-awesome/eye-slash.png new file mode 100644 index 0000000..06c7531 Binary files /dev/null and b/images/font-awesome/eye-slash.png differ diff --git a/images/font-awesome/readme.txt b/images/font-awesome/readme.txt new file mode 100644 index 0000000..026d4d9 --- /dev/null +++ b/images/font-awesome/readme.txt @@ -0,0 +1,10 @@ +This directory contains icons from the Font Awesome 4.4.0 icon font: +http://fortawesome.github.io/Font-Awesome/ + +The Font Awesome font is licensed under SIL OFL 1.1: +http://scripts.sil.org/OFL + +------------------------------------------------------------------------------ + +The icons have been converted to PNG and in some cases minor adjustments have +been made, such as adding a background colour or a drop shadow. \ No newline at end of file diff --git a/images/font-awesome/undo.png b/images/font-awesome/undo.png new file mode 100644 index 0000000..ea6980d Binary files /dev/null and b/images/font-awesome/undo.png differ diff --git a/images/gion/AUTHORS b/images/gion/AUTHORS new file mode 100644 index 0000000..787e4ec --- /dev/null +++ b/images/gion/AUTHORS @@ -0,0 +1,11 @@ +Name: Silvestre Herrera + +Nickname: ertz + +Location: La Plata, Buenos Aires. ARGENTINA. + +E-mail: silvestre.herrera(at)gmail.com + +Website(s): http://www.silvestre.com.ar/ + +_______________________________________________ diff --git a/images/gion/COPYING b/images/gion/COPYING new file mode 100644 index 0000000..c48d90f --- /dev/null +++ b/images/gion/COPYING @@ -0,0 +1,342 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. + + diff --git a/images/gion/edit-copy.png b/images/gion/edit-copy.png new file mode 100644 index 0000000..900d953 Binary files /dev/null and b/images/gion/edit-copy.png differ diff --git a/images/gnome-icon-theme/AUTHORS b/images/gnome-icon-theme/AUTHORS new file mode 100644 index 0000000..bf53816 --- /dev/null +++ b/images/gnome-icon-theme/AUTHORS @@ -0,0 +1,15 @@ +Ulisse Perusin <uli.peru@gmail.com> +Riccardo Buzzotta <raozuzu@yahoo.it> +Josef Vybíral <cornelius@vybiral.info> +Hylke Bons <h.bons@gmail.com> +Ricardo González <rick@jinlabs.com> +Lapo Calamandrei <calamandrei@gmail.com> +Rodney Dawes <dobey@novell.com> +Luca Ferretti <elle.uca@libero.it> +Tuomas Kuosmanen <tigert@gimp.org> +Andreas Nilsson <nisses.mail@home.se> +Jakub Steiner <jimmac@novell.com> + +Some external 3D Assets used: +Geraldo Cockerhan - http://www.blendswap.com/blends/view/40495 CCBYSA + diff --git a/images/gnome-icon-theme/COPYING_LGPL b/images/gnome-icon-theme/COPYING_LGPL new file mode 100644 index 0000000..65c5ca8 --- /dev/null +++ b/images/gnome-icon-theme/COPYING_LGPL @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/images/gnome-icon-theme/edit-cut-blue.png b/images/gnome-icon-theme/edit-cut-blue.png new file mode 100644 index 0000000..4e8412c Binary files /dev/null and b/images/gnome-icon-theme/edit-cut-blue.png differ diff --git a/images/gnome-icon-theme/edit-paste.png b/images/gnome-icon-theme/edit-paste.png new file mode 100644 index 0000000..4458622 Binary files /dev/null and b/images/gnome-icon-theme/edit-paste.png differ diff --git a/images/icon-extension-grey.png b/images/icon-extension-grey.png new file mode 100644 index 0000000..6151f43 Binary files /dev/null and b/images/icon-extension-grey.png differ diff --git a/images/logo-medium.png b/images/logo-medium.png new file mode 100644 index 0000000..73b4dd3 Binary files /dev/null and b/images/logo-medium.png differ diff --git a/images/menu-arrows.png b/images/menu-arrows.png new file mode 100644 index 0000000..f1c7a75 Binary files /dev/null and b/images/menu-arrows.png differ diff --git a/images/new-menu-badge.png b/images/new-menu-badge.png new file mode 100644 index 0000000..7b0a390 Binary files /dev/null and b/images/new-menu-badge.png differ diff --git a/images/page-add.png b/images/page-add.png new file mode 100644 index 0000000..be2379b Binary files /dev/null and b/images/page-add.png differ diff --git a/images/page-delete.png b/images/page-delete.png new file mode 100644 index 0000000..a2d1b14 Binary files /dev/null and b/images/page-delete.png differ diff --git a/images/page-invisible.png b/images/page-invisible.png new file mode 100644 index 0000000..29c4e38 Binary files /dev/null and b/images/page-invisible.png differ diff --git a/images/pencil_delete.png b/images/pencil_delete.png new file mode 100644 index 0000000..d8944e6 Binary files /dev/null and b/images/pencil_delete.png differ diff --git a/images/pencil_delete_gray.png b/images/pencil_delete_gray.png new file mode 100644 index 0000000..2757e2e Binary files /dev/null and b/images/pencil_delete_gray.png differ diff --git a/images/plugin_add.png b/images/plugin_add.png new file mode 100644 index 0000000..ae43690 Binary files /dev/null and b/images/plugin_add.png differ diff --git a/images/plugin_disabled.png b/images/plugin_disabled.png new file mode 100644 index 0000000..f4f6be5 Binary files /dev/null and b/images/plugin_disabled.png differ diff --git a/images/plugin_error.png b/images/plugin_error.png new file mode 100644 index 0000000..cff65d7 Binary files /dev/null and b/images/plugin_error.png differ diff --git a/images/reset-permissions.png b/images/reset-permissions.png new file mode 100644 index 0000000..2809c48 Binary files /dev/null and b/images/reset-permissions.png differ diff --git a/images/separator-add.png b/images/separator-add.png new file mode 100644 index 0000000..ea462fa Binary files /dev/null and b/images/separator-add.png differ diff --git a/images/sort_ascending.png b/images/sort_ascending.png new file mode 100644 index 0000000..c3746d8 Binary files /dev/null and b/images/sort_ascending.png differ diff --git a/images/sort_descending.png b/images/sort_descending.png new file mode 100644 index 0000000..26cb4e8 Binary files /dev/null and b/images/sort_descending.png differ diff --git a/images/spinner.gif b/images/spinner.gif new file mode 100644 index 0000000..e10b97f Binary files /dev/null and b/images/spinner.gif differ diff --git a/images/submenu-tip.png b/images/submenu-tip.png new file mode 100644 index 0000000..c0ecd0c Binary files /dev/null and b/images/submenu-tip.png differ diff --git a/images/transparent16.png b/images/transparent16.png new file mode 100644 index 0000000..86e8064 Binary files /dev/null and b/images/transparent16.png differ diff --git a/images/x-light.png b/images/x-light.png new file mode 100644 index 0000000..8b03bfe Binary files /dev/null and b/images/x-light.png differ diff --git a/images/x.png b/images/x.png new file mode 100644 index 0000000..c5cc1fa Binary files /dev/null and b/images/x.png differ diff --git a/includes/.htaccess b/includes/.htaccess new file mode 100644 index 0000000..5db1317 --- /dev/null +++ b/includes/.htaccess @@ -0,0 +1,11 @@ +# Apache < 2.3 +<IfModule !mod_authz_core.c> + Order allow,deny + Deny from all + Satisfy All +</IfModule> + +# Apache >= 2.3 +<IfModule mod_authz_core.c> + Require all denied +</IfModule> \ No newline at end of file diff --git a/includes/PHP-CSS-Parser/CHANGELOG.md b/includes/PHP-CSS-Parser/CHANGELOG.md new file mode 100644 index 0000000..7d3141d --- /dev/null +++ b/includes/PHP-CSS-Parser/CHANGELOG.md @@ -0,0 +1,169 @@ +# Revision History + +## 7.0 + +### 7.0.0 (2015-08-24) + +* Compatibility with PHP 7. Well timed, eh? + +#### Deprecations + +* The `Sabberworm\CSS\Value\String` class has been renamed to `Sabberworm\CSS\Value\CSSString`. + +### 7.0.1 (2015-12-25) + +* No more suppressed `E_NOTICE` +* *No deprecations* + +## 6.0 + +### 6.0.0 (2014-07-03) + +* Format output using Sabberworm\CSS\OutputFormat +* *No backwards-incompatible changes* + +#### Deprecations + +* The parse() method replaces __toString with an optional argument (instance of the OutputFormat class) + +### 6.0.1 (2015-08-24) + +* Remove some declarations in interfaces incompatible with PHP 5.3 (< 5.3.9) +* *No deprecations* + +## 5.0 + +### 5.0.0 (2013-03-20) + +* Correctly parse all known CSS 3 units (including Hz and kHz). +* Output RGB colors in short (#aaa or #ababab) notation +* Be case-insensitive when parsing identifiers. +* *No deprecations* + +#### Backwards-incompatible changes + +* `Sabberworm\CSS\Value\Color`’s `__toString` method overrides `CSSList`’s to maybe return something other than `type(value, …)` (see above). + +### 5.0.1 (2013-03-20) + +* Internal cleanup +* *No backwards-incompatible changes* +* *No deprecations* + +### 5.0.2 (2013-03-21) + +* CHANGELOG.md file added to distribution +* *No backwards-incompatible changes* +* *No deprecations* + +### 5.0.3 (2013-03-21) + +* More size units recognized +* *No backwards-incompatible changes* +* *No deprecations* + +### 5.0.4 (2013-03-21) + +* Don’t output floats with locale-aware separator chars +* *No backwards-incompatible changes* +* *No deprecations* + +### 5.0.5 (2013-04-17) + +* Initial support for lenient parsing (setting this parser option will catch some exceptions internally and recover the parser’s state as neatly as possible). +* *No backwards-incompatible changes* +* *No deprecations* + +### 5.0.6 (2013-05-31) + +* Fix broken unit test +* *No backwards-incompatible changes* +* *No deprecations* + +### 5.0.7 (2013-08-04) + +* Fix broken decimal point output optimization +* *No backwards-incompatible changes* +* *No deprecations* + +### 5.0.8 (2013-08-15) + +* Make default settings’ multibyte parsing option dependent on whether or not the mbstring extension is actually installed. +* *No backwards-incompatible changes* +* *No deprecations* + +### 5.1.0 (2013-10-24) + +* Performance enhancements by Michael M Slusarz +* More rescue entry points for lenient parsing (unexpected tokens between declaration blocks and unclosed comments) +* *No backwards-incompatible changes* +* *No deprecations* + +### 5.1.1 (2013-10-28) + +* Updated CHANGELOG.md to reflect changes since 5.0.4 +* *No backwards-incompatible changes* +* *No deprecations* + +### 5.1.2 (2013-10-30) + +* Remove the use of consumeUntil in comment parsing. This makes it possible to parse comments such as “/** Perfectly valid **/” +* *No backwards-incompatible changes* +* *No deprecations* + +### 5.2.0 (2014-06-30) + +* Support removing a selector from a declaration block using `$oBlock->removeSelector($mSelector)` +* Introduce a specialized exception (Sabberworm\CSS\Parsing\OuputException) for exceptions during output rendering + +* *No deprecations* + +#### Backwards-incompatible changes + +* Outputting a declaration block that has no selectors throws an OuputException instead of outputting an invalid ` {…}` into the CSS document. + +## 4.0 + +### 4.0.0 (2013-03-19) + +* Support for more @-rules +* Generic interface `Sabberworm\CSS\Property\AtRule`, implemented by all @-rule classes +* *No deprecations* + +#### Backwards-incompatible changes + +* `Sabberworm\CSS\RuleSet\AtRule` renamed to `Sabberworm\CSS\RuleSet\AtRuleSet` +* `Sabberworm\CSS\CSSList\MediaQuery` renamed to `Sabberworm\CSS\RuleSet\CSSList\AtRuleBlockList` with differing semantics and API (which also works for other block-list-based @-rules like `@supports`). + +## 3.0 + +### 3.0.0 (2013-03-06) + +* Support for lenient parsing (on by default) +* *No deprecations* + +#### Backwards-incompatible changes + +* All properties (like whether or not to use `mb_`-functions, which default charset to use and – new – whether or not to be forgiving when parsing) are now encapsulated in an instance of `Sabberworm\CSS\Settings` which can be passed as the second argument to `Sabberworm\CSS\Parser->__construct()`. +* Specifying a charset as the second argument to `Sabberworm\CSS\Parser->__construct()` is no longer supported. Use `Sabberworm\CSS\Settings::create()->withDefaultCharset('some-charset')` instead. +* Setting `Sabberworm\CSS\Parser->bUseMbFunctions` has no effect. Use `Sabberworm\CSS\Settings::create()->withMultibyteSupport(true/false)` instead. +* `Sabberworm\CSS\Parser->parse()` may throw a `Sabberworm\CSS\Parsing\UnexpectedTokenException` when in strict parsing mode. + +## 2.0 + +### 2.0.0 (2013-01-29) + +* Allow multiple rules of the same type per rule set + +#### Backwards-incompatible changes + +* `Sabberworm\CSS\RuleSet->getRules()` returns an index-based array instead of an associative array. Use `Sabberworm\CSS\RuleSet->getRulesAssoc()` (which eliminates duplicate rules and lets the later rule of the same name win). +* `Sabberworm\CSS\RuleSet->removeRule()` works as it did before except when passed an instance of `Sabberworm\CSS\Rule\Rule`, in which case it would only remove the exact rule given instead of all the rules of the same type. To get the old behaviour, use `Sabberworm\CSS\RuleSet->removeRule($oRule->getRule()`; + +## 1.0 + +Initial release of a stable public API. + +## 0.9 + +Last version not to use PSR-0 project organization semantics. diff --git a/includes/PHP-CSS-Parser/README.md b/includes/PHP-CSS-Parser/README.md new file mode 100644 index 0000000..68833f6 --- /dev/null +++ b/includes/PHP-CSS-Parser/README.md @@ -0,0 +1,532 @@ +PHP CSS Parser +-------------- + +[![build status](https://travis-ci.org/sabberworm/PHP-CSS-Parser.png)](https://travis-ci.org/sabberworm/PHP-CSS-Parser) [![HHVM Status](http://hhvm.h4cc.de/badge/sabberworm/php-css-parser.png)](http://hhvm.h4cc.de/package/sabberworm/php-css-parser) + +A Parser for CSS Files written in PHP. Allows extraction of CSS files into a data structure, manipulation of said structure and output as (optimized) CSS. + +## Usage + +### Installation using composer + +Add php-css-parser to your composer.json + + { + "require": { + "sabberworm/php-css-parser": "*" + } + } + +### Extraction + +To use the CSS Parser, create a new instance. The constructor takes the following form: + + new Sabberworm\CSS\Parser($sText); + +To read a file, for example, you’d do the following: + + $oCssParser = new Sabberworm\CSS\Parser(file_get_contents('somefile.css')); + $oCssDocument = $oCssParser->parse(); + +The resulting CSS document structure can be manipulated prior to being output. + +### Options + +#### Charset + +The charset option is used only if no @charset declaration is found in the CSS file. UTF-8 is the default, so you won’t have to create a settings object at all if you don’t intend to change that. + + $oSettings = Sabberworm\CSS\Settings::create()->withDefaultCharset('windows-1252'); + new Sabberworm\CSS\Parser($sText, $oSettings); + +#### Strict parsing + +To have the parser choke on invalid rules, supply a thusly configured Sabberworm\CSS\Settings object: + + $oCssParser = new Sabberworm\CSS\Parser(file_get_contents('somefile.css'), Sabberworm\CSS\Settings::create()->beStrict()); + +#### Disable multibyte functions + +To achieve faster parsing, you can choose to have PHP-CSS-Parser use regular string functions instead of `mb_*` functions. This should work fine in most cases, even for UTF-8 files, as all the multibyte characters are in string literals. Still it’s not recommended to use this with input you have no control over as it’s not thoroughly covered by test cases. + + $oSettings = Sabberworm\CSS\Settings::create()->withMultibyteSupport(false); + new Sabberworm\CSS\Parser($sText, $oSettings); + +### Manipulation + +The resulting data structure consists mainly of five basic types: `CSSList`, `RuleSet`, `Rule`, `Selector` and `Value`. There are two additional types used: `Import` and `Charset` which you won’t use often. + +#### CSSList + +`CSSList` represents a generic CSS container, most likely containing declaration blocks (rule sets with a selector) but it may also contain at-rules, charset declarations, etc. `CSSList` has the following concrete subtypes: + +* `Document` – representing the root of a CSS file. +* `MediaQuery` – represents a subsection of a CSSList that only applies to a output device matching the contained media query. + +To access the items stored in a `CSSList` – like the document you got back when calling `$oCssParser->parse()` –, use `getContents()`, then iterate over that collection and use instanceof to check whether you’re dealing with another `CSSList`, a `RuleSet`, a `Import` or a `Charset`. + +To append a new item (selector, media query, etc.) to an existing `CSSList`, construct it using the constructor for this class and use the `append($oItem)` method. + +#### RuleSet + +`RuleSet` is a container for individual rules. The most common form of a rule set is one constrained by a selector. The following concrete subtypes exist: + +* `AtRuleSet` – for generic at-rules which do not match the ones specifically mentioned like @import, @charset or @media. A common example for this is @font-face. +* `DeclarationBlock` – a RuleSet constrained by a `Selector`; contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the matching elements. + +Note: A `CSSList` can contain other `CSSList`s (and `Import`s as well as a `Charset`) while a `RuleSet` can only contain `Rule`s. + +If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $oRule)`, `getRules()` and `removeRule($mRule)` (which accepts either a Rule instance or a rule name; optionally suffixed by a dash to remove all related rules). + +#### Rule + +`Rule`s just have a key (the rule) and a value. These values are all instances of a `Value`. + +#### Value + +`Value` is an abstract class that only defines the `render` method. The concrete subclasses for atomic value types are: + +* `Size` – consists of a numeric `size` value and a unit. +* `Color` – colors can be input in the form #rrggbb, #rgb or schema(val1, val2, …) but are always stored as an array of ('s' => val1, 'c' => val2, 'h' => val3, …) and output in the second form. +* `CSSString` – this is just a wrapper for quoted strings to distinguish them from keywords; always output with double quotes. +* `URL` – URLs in CSS; always output in URL("") notation. + +There is another abstract subclass of `Value`, `ValueList`. A `ValueList` represents a lists of `Value`s, separated by some separation character (mostly `,`, whitespace, or `/`). There are two types of `ValueList`s: + +* `RuleValueList` – The default type, used to represent all multi-valued rules like `font: bold 12px/3 Helvetica, Verdana, sans-serif;` (where the value would be a whitespace-separated list of the primitive value `bold`, a slash-separated list and a comma-separated list). +* `CSSFunction` – A special kind of value that also contains a function name and where the values are the function’s arguments. Also handles equals-sign-separated argument lists like `filter: alpha(opacity=90);`. + +#### Convenience methods + +There are a few convenience methods on Document to ease finding, manipulating and deleting rules: + +* `getAllDeclarationBlocks()` – does what it says; no matter how deeply nested your selectors are. Aliased as `getAllSelectors()`. +* `getAllRuleSets()` – does what it says; no matter how deeply nested your rule sets are. +* `getAllValues()` – finds all `Value` objects inside `Rule`s. + +## To-Do + +* More convenience methods [like `selectorsWithElement($sId/Class/TagName)`, `attributesOfType($sType)`, `removeAttributesOfType($sType)`] +* Real multibyte support. Currently only multibyte charsets whose first 255 code points take up only one byte and are identical with ASCII are supported (yes, UTF-8 fits this description). +* Named color support (using `Color` instead of an anonymous string literal) + +## Use cases + +### Use `Parser` to prepend an id to all selectors + + $sMyId = "#my_id"; + $oParser = new Sabberworm\CSS\Parser($sText); + $oCss = $oParser->parse(); + foreach($oCss->getAllDeclarationBlocks() as $oBlock) { + foreach($oBlock->getSelectors() as $oSelector) { + //Loop over all selector parts (the comma-separated strings in a selector) and prepend the id + $oSelector->setSelector($sMyId.' '.$oSelector->getSelector()); + } + } + +### Shrink all absolute sizes to half + + $oParser = new Sabberworm\CSS\Parser($sText); + $oCss = $oParser->parse(); + foreach($oCss->getAllValues() as $mValue) { + if($mValue instanceof CSSSize && !$mValue->isRelative()) { + $mValue->setSize($mValue->getSize()/2); + } + } + +### Remove unwanted rules + + $oParser = new Sabberworm\CSS\Parser($sText); + $oCss = $oParser->parse(); + foreach($oCss->getAllRuleSets() as $oRuleSet) { + $oRuleSet->removeRule('font-'); //Note that the added dash will make this remove all rules starting with font- (like font-size, font-weight, etc.) as well as a potential font-rule + $oRuleSet->removeRule('cursor'); + } + +### Output + +To output the entire CSS document into a variable, just use `->render()`: + + $oCssParser = new Sabberworm\CSS\Parser(file_get_contents('somefile.css')); + $oCssDocument = $oCssParser->parse(); + print $oCssDocument->render(); + +If you want to format the output, pass an instance of type `Sabberworm\CSS\OutputFormat`: + + $oFormat = Sabberworm\CSS\OutputFormat::create()->indentWithSpaces(4)->setSpaceBetweenRules("\n"); + print $oCssDocument->render($oFormat); + +Or use one of the predefined formats: + + print $oCssDocument->render(Sabberworm\CSS\OutputFormat::createPretty()); + print $oCssDocument->render(Sabberworm\CSS\OutputFormat::createCompact()); + +To see what you can do with output formatting, look at the tests in `tests/Sabberworm/CSS/OutputFormatTest.php`. + +## Examples + +### Example 1 (At-Rules) + +#### Input + + @charset "utf-8"; + + @font-face { + font-family: "CrassRoots"; + src: url("../media/cr.ttf") + } + + html, body { + font-size: 1.6em + } + + @keyframes mymove { + from { top: 0px; } + to { top: 200px; } + } + +#### Structure (`var_dump()`) + + object(Sabberworm\CSS\CSSList\Document)#4 (1) { + ["aContents":protected]=> + array(4) { + [0]=> + object(Sabberworm\CSS\Property\Charset)#6 (1) { + ["sCharset":"Sabberworm\CSS\Property\Charset":private]=> + object(Sabberworm\CSS\Value\CSSString)#5 (1) { + ["sString":"Sabberworm\CSS\Value\CSSString":private]=> + string(5) "utf-8" + } + } + [1]=> + object(Sabberworm\CSS\RuleSet\AtRuleSet)#7 (2) { + ["sType":"Sabberworm\CSS\RuleSet\AtRuleSet":private]=> + string(9) "font-face" + ["aRules":"Sabberworm\CSS\RuleSet\RuleSet":private]=> + array(2) { + ["font-family"]=> + array(1) { + [0]=> + object(Sabberworm\CSS\Rule\Rule)#8 (3) { + ["sRule":"Sabberworm\CSS\Rule\Rule":private]=> + string(11) "font-family" + ["mValue":"Sabberworm\CSS\Rule\Rule":private]=> + object(Sabberworm\CSS\Value\CSSString)#9 (1) { + ["sString":"Sabberworm\CSS\Value\CSSString":private]=> + string(10) "CrassRoots" + } + ["bIsImportant":"Sabberworm\CSS\Rule\Rule":private]=> + bool(false) + } + } + ["src"]=> + array(1) { + [0]=> + object(Sabberworm\CSS\Rule\Rule)#10 (3) { + ["sRule":"Sabberworm\CSS\Rule\Rule":private]=> + string(3) "src" + ["mValue":"Sabberworm\CSS\Rule\Rule":private]=> + object(Sabberworm\CSS\Value\URL)#11 (1) { + ["oURL":"Sabberworm\CSS\Value\URL":private]=> + object(Sabberworm\CSS\Value\CSSString)#12 (1) { + ["sString":"Sabberworm\CSS\Value\CSSString":private]=> + string(15) "../media/cr.ttf" + } + } + ["bIsImportant":"Sabberworm\CSS\Rule\Rule":private]=> + bool(false) + } + } + } + } + [2]=> + object(Sabberworm\CSS\RuleSet\DeclarationBlock)#13 (2) { + ["aSelectors":"Sabberworm\CSS\RuleSet\DeclarationBlock":private]=> + array(2) { + [0]=> + object(Sabberworm\CSS\Property\Selector)#14 (2) { + ["sSelector":"Sabberworm\CSS\Property\Selector":private]=> + string(4) "html" + ["iSpecificity":"Sabberworm\CSS\Property\Selector":private]=> + NULL + } + [1]=> + object(Sabberworm\CSS\Property\Selector)#15 (2) { + ["sSelector":"Sabberworm\CSS\Property\Selector":private]=> + string(4) "body" + ["iSpecificity":"Sabberworm\CSS\Property\Selector":private]=> + NULL + } + } + ["aRules":"Sabberworm\CSS\RuleSet\RuleSet":private]=> + array(1) { + ["font-size"]=> + array(1) { + [0]=> + object(Sabberworm\CSS\Rule\Rule)#16 (3) { + ["sRule":"Sabberworm\CSS\Rule\Rule":private]=> + string(9) "font-size" + ["mValue":"Sabberworm\CSS\Rule\Rule":private]=> + object(Sabberworm\CSS\Value\Size)#17 (3) { + ["fSize":"Sabberworm\CSS\Value\Size":private]=> + float(1.6) + ["sUnit":"Sabberworm\CSS\Value\Size":private]=> + string(2) "em" + ["bIsColorComponent":"Sabberworm\CSS\Value\Size":private]=> + bool(false) + } + ["bIsImportant":"Sabberworm\CSS\Rule\Rule":private]=> + bool(false) + } + } + } + } + [3]=> + object(Sabberworm\CSS\CSSList\KeyFrame)#18 (3) { + ["vendorKeyFrame":"Sabberworm\CSS\CSSList\KeyFrame":private]=> + string(9) "keyframes" + ["animationName":"Sabberworm\CSS\CSSList\KeyFrame":private]=> + string(6) "mymove" + ["aContents":protected]=> + array(2) { + [0]=> + object(Sabberworm\CSS\RuleSet\DeclarationBlock)#19 (2) { + ["aSelectors":"Sabberworm\CSS\RuleSet\DeclarationBlock":private]=> + array(1) { + [0]=> + object(Sabberworm\CSS\Property\Selector)#20 (2) { + ["sSelector":"Sabberworm\CSS\Property\Selector":private]=> + string(4) "from" + ["iSpecificity":"Sabberworm\CSS\Property\Selector":private]=> + NULL + } + } + ["aRules":"Sabberworm\CSS\RuleSet\RuleSet":private]=> + array(1) { + ["top"]=> + array(1) { + [0]=> + object(Sabberworm\CSS\Rule\Rule)#21 (3) { + ["sRule":"Sabberworm\CSS\Rule\Rule":private]=> + string(3) "top" + ["mValue":"Sabberworm\CSS\Rule\Rule":private]=> + object(Sabberworm\CSS\Value\Size)#22 (3) { + ["fSize":"Sabberworm\CSS\Value\Size":private]=> + float(0) + ["sUnit":"Sabberworm\CSS\Value\Size":private]=> + string(2) "px" + ["bIsColorComponent":"Sabberworm\CSS\Value\Size":private]=> + bool(false) + } + ["bIsImportant":"Sabberworm\CSS\Rule\Rule":private]=> + bool(false) + } + } + } + } + [1]=> + object(Sabberworm\CSS\RuleSet\DeclarationBlock)#23 (2) { + ["aSelectors":"Sabberworm\CSS\RuleSet\DeclarationBlock":private]=> + array(1) { + [0]=> + object(Sabberworm\CSS\Property\Selector)#24 (2) { + ["sSelector":"Sabberworm\CSS\Property\Selector":private]=> + string(2) "to" + ["iSpecificity":"Sabberworm\CSS\Property\Selector":private]=> + NULL + } + } + ["aRules":"Sabberworm\CSS\RuleSet\RuleSet":private]=> + array(1) { + ["top"]=> + array(1) { + [0]=> + object(Sabberworm\CSS\Rule\Rule)#25 (3) { + ["sRule":"Sabberworm\CSS\Rule\Rule":private]=> + string(3) "top" + ["mValue":"Sabberworm\CSS\Rule\Rule":private]=> + object(Sabberworm\CSS\Value\Size)#26 (3) { + ["fSize":"Sabberworm\CSS\Value\Size":private]=> + float(200) + ["sUnit":"Sabberworm\CSS\Value\Size":private]=> + string(2) "px" + ["bIsColorComponent":"Sabberworm\CSS\Value\Size":private]=> + bool(false) + } + ["bIsImportant":"Sabberworm\CSS\Rule\Rule":private]=> + bool(false) + } + } + } + } + } + } + } + } + +#### Output (`render()`) + + @charset "utf-8";@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");}html, body {font-size: 1.6em;} + @keyframes mymove {from {top: 0px;} + to {top: 200px;} + } + +### Example 2 (Values) + +#### Input + + #header { + margin: 10px 2em 1cm 2%; + font-family: Verdana, Helvetica, "Gill Sans", sans-serif; + color: red !important; + } + +#### Structure (`var_dump()`) + + object(Sabberworm\CSS\CSSList\Document)#4 (1) { + ["aContents":protected]=> + array(1) { + [0]=> + object(Sabberworm\CSS\RuleSet\DeclarationBlock)#5 (2) { + ["aSelectors":"Sabberworm\CSS\RuleSet\DeclarationBlock":private]=> + array(1) { + [0]=> + object(Sabberworm\CSS\Property\Selector)#6 (2) { + ["sSelector":"Sabberworm\CSS\Property\Selector":private]=> + string(7) "#header" + ["iSpecificity":"Sabberworm\CSS\Property\Selector":private]=> + NULL + } + } + ["aRules":"Sabberworm\CSS\RuleSet\RuleSet":private]=> + array(3) { + ["margin"]=> + array(1) { + [0]=> + object(Sabberworm\CSS\Rule\Rule)#7 (3) { + ["sRule":"Sabberworm\CSS\Rule\Rule":private]=> + string(6) "margin" + ["mValue":"Sabberworm\CSS\Rule\Rule":private]=> + object(Sabberworm\CSS\Value\RuleValueList)#12 (2) { + ["aComponents":protected]=> + array(4) { + [0]=> + object(Sabberworm\CSS\Value\Size)#8 (3) { + ["fSize":"Sabberworm\CSS\Value\Size":private]=> + float(10) + ["sUnit":"Sabberworm\CSS\Value\Size":private]=> + string(2) "px" + ["bIsColorComponent":"Sabberworm\CSS\Value\Size":private]=> + bool(false) + } + [1]=> + object(Sabberworm\CSS\Value\Size)#9 (3) { + ["fSize":"Sabberworm\CSS\Value\Size":private]=> + float(2) + ["sUnit":"Sabberworm\CSS\Value\Size":private]=> + string(2) "em" + ["bIsColorComponent":"Sabberworm\CSS\Value\Size":private]=> + bool(false) + } + [2]=> + object(Sabberworm\CSS\Value\Size)#10 (3) { + ["fSize":"Sabberworm\CSS\Value\Size":private]=> + float(1) + ["sUnit":"Sabberworm\CSS\Value\Size":private]=> + string(2) "cm" + ["bIsColorComponent":"Sabberworm\CSS\Value\Size":private]=> + bool(false) + } + [3]=> + object(Sabberworm\CSS\Value\Size)#11 (3) { + ["fSize":"Sabberworm\CSS\Value\Size":private]=> + float(2) + ["sUnit":"Sabberworm\CSS\Value\Size":private]=> + string(1) "%" + ["bIsColorComponent":"Sabberworm\CSS\Value\Size":private]=> + bool(false) + } + } + ["sSeparator":protected]=> + string(1) " " + } + ["bIsImportant":"Sabberworm\CSS\Rule\Rule":private]=> + bool(false) + } + } + ["font-family"]=> + array(1) { + [0]=> + object(Sabberworm\CSS\Rule\Rule)#13 (3) { + ["sRule":"Sabberworm\CSS\Rule\Rule":private]=> + string(11) "font-family" + ["mValue":"Sabberworm\CSS\Rule\Rule":private]=> + object(Sabberworm\CSS\Value\RuleValueList)#15 (2) { + ["aComponents":protected]=> + array(4) { + [0]=> + string(7) "Verdana" + [1]=> + string(9) "Helvetica" + [2]=> + object(Sabberworm\CSS\Value\CSSString)#14 (1) { + ["sString":"Sabberworm\CSS\Value\CSSString":private]=> + string(9) "Gill Sans" + } + [3]=> + string(10) "sans-serif" + } + ["sSeparator":protected]=> + string(1) "," + } + ["bIsImportant":"Sabberworm\CSS\Rule\Rule":private]=> + bool(false) + } + } + ["color"]=> + array(1) { + [0]=> + object(Sabberworm\CSS\Rule\Rule)#16 (3) { + ["sRule":"Sabberworm\CSS\Rule\Rule":private]=> + string(5) "color" + ["mValue":"Sabberworm\CSS\Rule\Rule":private]=> + string(3) "red" + ["bIsImportant":"Sabberworm\CSS\Rule\Rule":private]=> + bool(true) + } + } + } + } + } + } + +#### Output (`render()`) + + #header {margin: 10px 2em 1cm 2%;font-family: Verdana,Helvetica,"Gill Sans",sans-serif;color: red !important;} + +## Contributors/Thanks to + +* [ju1ius](https://github.com/ju1ius) for the specificity parsing code and the ability to expand/compact shorthand properties. +* [GaryJones](https://github.com/GaryJones) for lots of input and [http://css-specificity.info/](http://css-specificity.info/). +* [docteurklein](https://github.com/docteurklein) for output formatting and `CSSList->remove()` inspiration. +* [nicolopignatelli](https://github.com/nicolopignatelli) for PSR-0 compatibility. +* [diegoembarcadero](https://github.com/diegoembarcadero) for keyframe at-rule parsing. +* [goetas](https://github.com/goetas) for @namespace at-rule support. +* [View full list](https://github.com/sabberworm/PHP-CSS-Parser/contributors) + +## Misc + +* Legacy Support: The latest pre-PSR-0 version of this project can be checked with the `0.9.0` tag. +* Running Tests: To run all unit tests for this project, have `phpunit` installed and run `phpunit .`. + +## License + +PHP-CSS-Parser is freely distributable under the terms of an MIT-style license. + +Copyright (c) 2011 Raphael Schweikert, http://sabberworm.com/ + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/includes/PHP-CSS-Parser/autoloader.php b/includes/PHP-CSS-Parser/autoloader.php new file mode 100644 index 0000000..c2f5389 --- /dev/null +++ b/includes/PHP-CSS-Parser/autoloader.php @@ -0,0 +1,10 @@ +<?php + +spl_autoload_register(function($class) +{ + $file = __DIR__.'/lib/'.strtr($class, '\\', '/').'.php'; + if (file_exists($file)) { + require $file; + return true; + } +}); \ No newline at end of file diff --git a/includes/PHP-CSS-Parser/composer.json b/includes/PHP-CSS-Parser/composer.json new file mode 100644 index 0000000..bd3830c --- /dev/null +++ b/includes/PHP-CSS-Parser/composer.json @@ -0,0 +1,17 @@ +{ + "name": "sabberworm/php-css-parser", + "type": "library", + "description": "Parser for CSS Files written in PHP", + "keywords": ["parser", "css", "stylesheet"], + "homepage": "http://www.sabberworm.com/blog/2010/6/10/php-css-parser", + "license": "MIT", + "authors": [ + {"name": "Raphael Schweikert"} + ], + "require": { + "php": ">=5.3.2" + }, + "autoload": { + "psr-0": { "Sabberworm\\CSS": "lib/" } + } +} diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/CSSList/AtRuleBlockList.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/CSSList/AtRuleBlockList.php new file mode 100644 index 0000000..70b9c40 --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/CSSList/AtRuleBlockList.php @@ -0,0 +1,48 @@ +<?php + +namespace Sabberworm\CSS\CSSList; + +use Sabberworm\CSS\Property\AtRule; + +/** + * A BlockList constructed by an unknown @-rule. @media rules are rendered into AtRuleBlockList objects. + */ +class AtRuleBlockList extends CSSBlockList implements AtRule { + + private $sType; + private $sArgs; + + public function __construct($sType, $sArgs = '') { + parent::__construct(); + $this->sType = $sType; + $this->sArgs = $sArgs; + } + + public function atRuleName() { + return $this->sType; + } + + public function atRuleArgs() { + return $this->sArgs; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + $sArgs = $this->sArgs; + if($sArgs) { + $sArgs = ' ' . $sArgs; + } + $sResult = "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; + $sResult .= parent::render($oOutputFormat); + $sResult .= '}'; + return $sResult; + } + + public function isRootList() { + return false; + } + +} \ No newline at end of file diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/CSSList/CSSBlockList.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/CSSList/CSSBlockList.php new file mode 100644 index 0000000..b533bdb --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/CSSList/CSSBlockList.php @@ -0,0 +1,88 @@ +<?php + +namespace Sabberworm\CSS\CSSList; + +use Sabberworm\CSS\RuleSet\DeclarationBlock; +use Sabberworm\CSS\RuleSet\RuleSet; +use Sabberworm\CSS\Property\Selector; +use Sabberworm\CSS\Rule\Rule; +use Sabberworm\CSS\Value\ValueList; +use Sabberworm\CSS\Value\CSSFunction; + +/** + * A CSSBlockList is a CSSList whose DeclarationBlocks are guaranteed to contain valid declaration blocks or at-rules. + * Most CSSLists conform to this category but some at-rules (such as @keyframes) do not. + */ +abstract class CSSBlockList extends CSSList { + protected function allDeclarationBlocks(&$aResult) { + foreach ($this->aContents as $mContent) { + if ($mContent instanceof DeclarationBlock) { + $aResult[] = $mContent; + } else if ($mContent instanceof CSSBlockList) { + $mContent->allDeclarationBlocks($aResult); + } + } + } + + protected function allRuleSets(&$aResult) { + foreach ($this->aContents as $mContent) { + if ($mContent instanceof RuleSet) { + $aResult[] = $mContent; + } else if ($mContent instanceof CSSBlockList) { + $mContent->allRuleSets($aResult); + } + } + } + + protected function allValues($oElement, &$aResult, $sSearchString = null, $bSearchInFunctionArguments = false) { + if ($oElement instanceof CSSBlockList) { + foreach ($oElement->getContents() as $oContent) { + $this->allValues($oContent, $aResult, $sSearchString, $bSearchInFunctionArguments); + } + } else if ($oElement instanceof RuleSet) { + foreach ($oElement->getRules($sSearchString) as $oRule) { + $this->allValues($oRule, $aResult, $sSearchString, $bSearchInFunctionArguments); + } + } else if ($oElement instanceof Rule) { + $this->allValues($oElement->getValue(), $aResult, $sSearchString, $bSearchInFunctionArguments); + } else if ($oElement instanceof ValueList) { + if ($bSearchInFunctionArguments || !($oElement instanceof CSSFunction)) { + foreach ($oElement->getListComponents() as $mComponent) { + $this->allValues($mComponent, $aResult, $sSearchString, $bSearchInFunctionArguments); + } + } + } else { + //Non-List Value or CSSString (CSS identifier) + $aResult[] = $oElement; + } + } + + protected function allSelectors(&$aResult, $sSpecificitySearch = null) { + $aDeclarationBlocks = array(); + $this->allDeclarationBlocks($aDeclarationBlocks); + foreach ($aDeclarationBlocks as $oBlock) { + foreach ($oBlock->getSelectors() as $oSelector) { + if ($sSpecificitySearch === null) { + $aResult[] = $oSelector; + } else { + //WSH: The original implementation used eval to compare specificity. I rewrote it + //to use version_compare instead to prevent false positives in vulnerability scanners. + //It's a bit of a hack, but it's shorter than a big switch statement. + $specificity = $oSelector->getSpecificity(); + $isMatch = false; + if ( preg_match( + '/^(?P<operator>(?:[<>]=?)|(?:!==?)|={2,3})\s*?(?P<number>\d++)$/', + trim($sSpecificitySearch), + $matches + ) ) { + $isMatch = version_compare((string)$specificity, $matches['number'], $matches['operator']); + } + if ($isMatch) { + $aResult[] = $oSelector; + } + } + } + } + } + +} diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/CSSList/CSSList.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/CSSList/CSSList.php new file mode 100644 index 0000000..c9ec931 --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/CSSList/CSSList.php @@ -0,0 +1,114 @@ +<?php + +namespace Sabberworm\CSS\CSSList; + +use Sabberworm\CSS\RuleSet\DeclarationBlock; +use Sabberworm\CSS\RuleSet\RuleSet; +use Sabberworm\CSS\Property\Selector; +use Sabberworm\CSS\Rule\Rule; +use Sabberworm\CSS\Value\ValueList; +use Sabberworm\CSS\Value\CSSFunction; + +/** + * A CSSList is the most generic container available. Its contents include RuleSet as well as other CSSList objects. + * Also, it may contain Import and Charset objects stemming from @-rules. + */ +abstract class CSSList { + + protected $aContents; + + public function __construct() { + $this->aContents = array(); + } + + public function append($oItem) { + $this->aContents[] = $oItem; + } + + /** + * Removes an item from the CSS list. + * @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery) + */ + public function remove($oItemToRemove) { + $iKey = array_search($oItemToRemove, $this->aContents, true); + if ($iKey !== false) { + unset($this->aContents[$iKey]); + return true; + } + return false; + } + + /** + * Removes a declaration block from the CSS list if it matches all given selectors. + * @param array|string $mSelector The selectors to match. + * @param boolean $bRemoveAll Whether to stop at the first declaration block found or remove all blocks + */ + public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false) { + if ($mSelector instanceof DeclarationBlock) { + $mSelector = $mSelector->getSelectors(); + } + if (!is_array($mSelector)) { + $mSelector = explode(',', $mSelector); + } + foreach ($mSelector as $iKey => &$mSel) { + if (!($mSel instanceof Selector)) { + $mSel = new Selector($mSel); + } + } + foreach ($this->aContents as $iKey => $mItem) { + if (!($mItem instanceof DeclarationBlock)) { + continue; + } + if ($mItem->getSelectors() == $mSelector) { + unset($this->aContents[$iKey]); + if (!$bRemoveAll) { + return; + } + } + } + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + $sResult = ''; + $bIsFirst = true; + $oNextLevel = $oOutputFormat; + if(!$this->isRootList()) { + $oNextLevel = $oOutputFormat->nextLevel(); + } + foreach ($this->aContents as $oContent) { + $sRendered = $oOutputFormat->safely(function() use ($oNextLevel, $oContent) { + return $oContent->render($oNextLevel); + }); + if($sRendered === null) { + continue; + } + if($bIsFirst) { + $bIsFirst = false; + $sResult .= $oNextLevel->spaceBeforeBlocks(); + } else { + $sResult .= $oNextLevel->spaceBetweenBlocks(); + } + $sResult .= $sRendered; + } + + if(!$bIsFirst) { + // Had some output + $sResult .= $oOutputFormat->spaceAfterBlocks(); + } + + return $sResult; + } + + /** + * Return true if the list can not be further outdented. Only important when rendering. + */ + public abstract function isRootList(); + + public function getContents() { + return $this->aContents; + } +} diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/CSSList/Document.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/CSSList/Document.php new file mode 100644 index 0000000..0349886 --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/CSSList/Document.php @@ -0,0 +1,98 @@ +<?php + +namespace Sabberworm\CSS\CSSList; + +/** + * The root CSSList of a parsed file. Contains all top-level css contents, mostly declaration blocks, but also any @-rules encountered. + */ +class Document extends CSSBlockList { + + /** + * Gets all DeclarationBlock objects recursively. + */ + public function getAllDeclarationBlocks() { + $aResult = array(); + $this->allDeclarationBlocks($aResult); + return $aResult; + } + + /** + * @deprecated use getAllDeclarationBlocks() + */ + public function getAllSelectors() { + return $this->getAllDeclarationBlocks(); + } + + /** + * Returns all RuleSet objects found recursively in the tree. + */ + public function getAllRuleSets() { + $aResult = array(); + $this->allRuleSets($aResult); + return $aResult; + } + + /** + * Returns all Value objects found recursively in the tree. + * @param (object|string) $mElement the CSSList or RuleSet to start the search from (defaults to the whole document). If a string is given, it is used as rule name filter (@see{RuleSet->getRules()}). + * @param (bool) $bSearchInFunctionArguments whether to also return Value objects used as Function arguments. + */ + public function getAllValues($mElement = null, $bSearchInFunctionArguments = false) { + $sSearchString = null; + if ($mElement === null) { + $mElement = $this; + } else if (is_string($mElement)) { + $sSearchString = $mElement; + $mElement = $this; + } + $aResult = array(); + $this->allValues($mElement, $aResult, $sSearchString, $bSearchInFunctionArguments); + return $aResult; + } + + /** + * Returns all Selector objects found recursively in the tree. + * Note that this does not yield the full DeclarationBlock that the selector belongs to (and, currently, there is no way to get to that). + * @param $sSpecificitySearch An optional filter by specificity. May contain a comparison operator and a number or just a number (defaults to "=="). + * @example getSelectorsBySpecificity('>= 100') + */ + public function getSelectorsBySpecificity($sSpecificitySearch = null) { + if (is_numeric($sSpecificitySearch) || is_numeric($sSpecificitySearch[0])) { + $sSpecificitySearch = "== $sSpecificitySearch"; + } + $aResult = array(); + $this->allSelectors($aResult, $sSpecificitySearch); + return $aResult; + } + + /** + * Expands all shorthand properties to their long value + */ + public function expandShorthands() { + foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->expandShorthands(); + } + } + + /** + * Create shorthands properties whenever possible + */ + public function createShorthands() { + foreach ($this->getAllDeclarationBlocks() as $oDeclaration) { + $oDeclaration->createShorthands(); + } + } + + // Override render() to make format argument optional + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat = null) { + if($oOutputFormat === null) { + $oOutputFormat = new \Sabberworm\CSS\OutputFormat(); + } + return parent::render($oOutputFormat); + } + + public function isRootList() { + return true; + } + +} \ No newline at end of file diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/CSSList/KeyFrame.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/CSSList/KeyFrame.php new file mode 100644 index 0000000..3faf2e3 --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/CSSList/KeyFrame.php @@ -0,0 +1,56 @@ +<?php + +namespace Sabberworm\CSS\CSSList; + +use Sabberworm\CSS\Property\AtRule; + +class KeyFrame extends CSSList implements AtRule { + + private $vendorKeyFrame; + private $animationName; + + public function __construct() { + parent::__construct(); + $this->vendorKeyFrame = null; + $this->animationName = null; + } + + public function setVendorKeyFrame($vendorKeyFrame) { + $this->vendorKeyFrame = $vendorKeyFrame; + } + + public function getVendorKeyFrame() { + return $this->vendorKeyFrame; + } + + public function setAnimationName($animationName) { + $this->animationName = $animationName; + } + + public function getAnimationName() { + return $this->animationName; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + $sResult = "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{"; + $sResult .= parent::render($oOutputFormat); + $sResult .= '}'; + return $sResult; + } + + public function isRootList() { + return false; + } + + public function atRuleName() { + return $this->vendorKeyFrame; + } + + public function atRuleArgs() { + return $this->animationName; + } +} diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/OutputFormat.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/OutputFormat.php new file mode 100644 index 0000000..1b17984 --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/OutputFormat.php @@ -0,0 +1,289 @@ +<?php + +namespace Sabberworm\CSS; + +use Sabberworm\CSS\Parsing\OutputException; + +class OutputFormat { + /** + * Value format + */ + // " means double-quote, ' means single-quote + public $sStringQuotingType = '"'; + // Output RGB colors in hash notation if possible + public $bRGBHashNotation = true; + + /** + * Declaration format + */ + // Semicolon after the last rule of a declaration block can be omitted. To do that, set this false. + public $bSemicolonAfterLastRule = true; + + /** + * Spacing + * Note that these strings are not sanity-checked: the value should only consist of whitespace + * Any newline character will be indented according to the current level. + * The triples (After, Before, Between) can be set using a wildcard (e.g. `$oFormat->set('Space*Rules', "\n");`) + */ + public $sSpaceAfterRuleName = ' '; + + public $sSpaceBeforeRules = ''; + public $sSpaceAfterRules = ''; + public $sSpaceBetweenRules = ''; + + public $sSpaceBeforeBlocks = ''; + public $sSpaceAfterBlocks = ''; + public $sSpaceBetweenBlocks = "\n"; + + // This is what’s printed before and after the comma if a declaration block contains multiple selectors. + public $sSpaceBeforeSelectorSeparator = ''; + public $sSpaceAfterSelectorSeparator = ' '; + // This is what’s printed after the comma of value lists + public $sSpaceBeforeListArgumentSeparator = ''; + public $sSpaceAfterListArgumentSeparator = ''; + + public $sSpaceBeforeOpeningBrace = ' '; + + /** + * Indentation + */ + // Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings. + public $sIndentation = "\t"; + + /** + * Output exceptions. + */ + public $bIgnoreExceptions = false; + + + private $oFormatter = null; + private $oNextLevelFormat = null; + private $iIndentationLevel = 0; + + public function __construct() { + } + + public function get($sName) { + $aVarPrefixes = array('a', 's', 'm', 'b', 'f', 'o', 'c', 'i'); + foreach($aVarPrefixes as $sPrefix) { + $sFieldName = $sPrefix.ucfirst($sName); + if(isset($this->$sFieldName)) { + return $this->$sFieldName; + } + } + return null; + } + + public function set($aNames, $mValue) { + $aVarPrefixes = array('a', 's', 'm', 'b', 'f', 'o', 'c', 'i'); + if(is_string($aNames) && strpos($aNames, '*') !== false) { + $aNames = array(str_replace('*', 'Before', $aNames), str_replace('*', 'Between', $aNames), str_replace('*', 'After', $aNames)); + } else if(!is_array($aNames)) { + $aNames = array($aNames); + } + foreach($aVarPrefixes as $sPrefix) { + $bDidReplace = false; + foreach($aNames as $sName) { + $sFieldName = $sPrefix.ucfirst($sName); + if(isset($this->$sFieldName)) { + $this->$sFieldName = $mValue; + $bDidReplace = true; + } + } + if($bDidReplace) { + return $this; + } + } + // Break the chain so the user knows this option is invalid + return false; + } + + public function __call($sMethodName, $aArguments) { + if(strpos($sMethodName, 'set') === 0) { + return $this->set(substr($sMethodName, 3), $aArguments[0]); + } else if(strpos($sMethodName, 'get') === 0) { + return $this->get(substr($sMethodName, 3)); + } else if(method_exists('\\Sabberworm\\CSS\\OutputFormatter', $sMethodName)) { + return call_user_func_array(array($this->getFormatter(), $sMethodName), $aArguments); + } else { + throw new \Exception('Unknown OutputFormat method called: '.$sMethodName); + } + } + + public function indentWithTabs($iNumber = 1) { + return $this->setIndentation(str_repeat("\t", $iNumber)); + } + + public function indentWithSpaces($iNumber = 2) { + return $this->setIndentation(str_repeat(" ", $iNumber)); + } + + public function nextLevel() { + if($this->oNextLevelFormat === null) { + $this->oNextLevelFormat = clone $this; + $this->oNextLevelFormat->iIndentationLevel++; + $this->oNextLevelFormat->oFormatter = null; + } + return $this->oNextLevelFormat; + } + + public function beLenient() { + $this->bIgnoreExceptions = true; + } + + public function getFormatter() { + if($this->oFormatter === null) { + $this->oFormatter = new OutputFormatter($this); + } + return $this->oFormatter; + } + + public function level() { + return $this->iIndentationLevel; + } + + public static function create() { + return new OutputFormat(); + } + + public static function createCompact() { + return self::create()->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('')->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator(''); + } + + public static function createPretty() { + return self::create()->set('Space*Rules', "\n")->set('Space*Blocks', "\n")->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' ')); + } +} + +class OutputFormatter { + private $oFormat; + + public function __construct(OutputFormat $oFormat) { + $this->oFormat = $oFormat; + } + + public function space($sName, $sType = null) { + $sSpaceString = $this->oFormat->get("Space$sName"); + // If $sSpaceString is an array, we have multple values configured depending on the type of object the space applies to + if(is_array($sSpaceString)) { + if($sType !== null && isset($sSpaceString[$sType])) { + $sSpaceString = $sSpaceString[$sType]; + } else { + $sSpaceString = reset($sSpaceString); + } + } + return $this->prepareSpace($sSpaceString); + } + + public function spaceAfterRuleName() { + return $this->space('AfterRuleName'); + } + + public function spaceBeforeRules() { + return $this->space('BeforeRules'); + } + + public function spaceAfterRules() { + return $this->space('AfterRules'); + } + + public function spaceBetweenRules() { + return $this->space('BetweenRules'); + } + + public function spaceBeforeBlocks() { + return $this->space('BeforeBlocks'); + } + + public function spaceAfterBlocks() { + return $this->space('AfterBlocks'); + } + + public function spaceBetweenBlocks() { + return $this->space('BetweenBlocks'); + } + + public function spaceBeforeSelectorSeparator() { + return $this->space('BeforeSelectorSeparator'); + } + + public function spaceAfterSelectorSeparator() { + return $this->space('AfterSelectorSeparator'); + } + + public function spaceBeforeListArgumentSeparator($sSeparator) { + return $this->space('BeforeListArgumentSeparator', $sSeparator); + } + + public function spaceAfterListArgumentSeparator($sSeparator) { + return $this->space('AfterListArgumentSeparator', $sSeparator); + } + + public function spaceBeforeOpeningBrace() { + return $this->space('BeforeOpeningBrace'); + } + + /** + * Runs the given code, either swallowing or passing exceptions, depending on the bIgnoreExceptions setting. + */ + public function safely($cCode) { + if($this->oFormat->get('IgnoreExceptions')) { + // If output exceptions are ignored, run the code with exception guards + try { + return $cCode(); + } catch (OutputException $e) { + return null; + } //Do nothing + } else { + // Run the code as-is + return $cCode(); + } + } + + /** + * Clone of the implode function but calls ->render with the current output format instead of __toString() + */ + public function implode($sSeparator, $aValues, $bIncreaseLevel = false) { + $sResult = ''; + $oFormat = $this->oFormat; + if($bIncreaseLevel) { + $oFormat = $oFormat->nextLevel(); + } + $bIsFirst = true; + foreach($aValues as $mValue) { + if($bIsFirst) { + $bIsFirst = false; + } else { + $sResult .= $sSeparator; + } + if($mValue instanceof \Sabberworm\CSS\Renderable) { + $sResult .= $mValue->render($oFormat); + } else { + $sResult .= $mValue; + } + } + return $sResult; + } + + public function removeLastSemicolon($sString) { + if($this->oFormat->get('SemicolonAfterLastRule')) { + return $sString; + } + $sString = explode(';', $sString); + if(count($sString) < 2) { + return $sString[0]; + } + $sLast = array_pop($sString); + $sNextToLast = array_pop($sString); + array_push($sString, $sNextToLast.$sLast); + return implode(';', $sString); + } + + private function prepareSpace($sSpaceString) { + return str_replace("\n", "\n".$this->indent(), $sSpaceString); + } + + private function indent() { + return str_repeat($this->oFormat->sIndentation, $this->oFormat->level()); + } +} \ No newline at end of file diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Parser.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Parser.php new file mode 100644 index 0000000..fd9f7bc --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Parser.php @@ -0,0 +1,633 @@ +<?php + +namespace Sabberworm\CSS; + +use Sabberworm\CSS\CSSList\CSSList; +use Sabberworm\CSS\CSSList\Document; +use Sabberworm\CSS\CSSList\KeyFrame; +use Sabberworm\CSS\Property\AtRule; +use Sabberworm\CSS\Property\Import; +use Sabberworm\CSS\Property\Charset; +use Sabberworm\CSS\Property\CSSNamespace; +use Sabberworm\CSS\RuleSet\AtRuleSet; +use Sabberworm\CSS\CSSList\AtRuleBlockList; +use Sabberworm\CSS\RuleSet\DeclarationBlock; +use Sabberworm\CSS\Value\CSSFunction; +use Sabberworm\CSS\Value\RuleValueList; +use Sabberworm\CSS\Value\Size; +use Sabberworm\CSS\Value\Color; +use Sabberworm\CSS\Value\URL; +use Sabberworm\CSS\Value\CSSString; +use Sabberworm\CSS\Rule\Rule; +use Sabberworm\CSS\Parsing\UnexpectedTokenException; + +/** + * Parser class parses CSS from text into a data structure. + */ +class Parser { + + private $sText; + private $iCurrentPosition; + private $oParserSettings; + private $sCharset; + private $iLength; + private $peekCache = null; + private $blockRules; + private $aSizeUnits; + + public function __construct($sText, Settings $oParserSettings = null) { + $this->sText = $sText; + $this->iCurrentPosition = 0; + if ($oParserSettings === null) { + $oParserSettings = Settings::create(); + } + $this->oParserSettings = $oParserSettings; + $this->blockRules = explode('/', AtRule::BLOCK_RULES); + + foreach (explode('/', Size::ABSOLUTE_SIZE_UNITS.'/'.Size::RELATIVE_SIZE_UNITS.'/'.Size::NON_SIZE_UNITS) as $val) { + $iSize = strlen($val); + if(!isset($this->aSizeUnits[$iSize])) { + $this->aSizeUnits[$iSize] = array(); + } + $this->aSizeUnits[$iSize][strtolower($val)] = $val; + } + ksort($this->aSizeUnits, SORT_NUMERIC); + } + + public function setCharset($sCharset) { + $this->sCharset = $sCharset; + $this->iLength = $this->strlen($this->sText); + } + + public function getCharset() { + return $this->sCharset; + } + + public function parse() { + $this->setCharset($this->oParserSettings->sDefaultCharset); + $oResult = new Document(); + $this->parseDocument($oResult); + return $oResult; + } + + private function parseDocument(Document $oDocument) { + $this->consumeWhiteSpace(); + $this->parseList($oDocument, true); + } + + private function parseList(CSSList $oList, $bIsRoot = false) { + while (!$this->isEnd()) { + if ($this->comes('@')) { + $oList->append($this->parseAtRule()); + } else if ($this->comes('}')) { + $this->consume('}'); + if ($bIsRoot) { + throw new \Exception("Unopened {"); + } else { + return; + } + } else { + if($this->oParserSettings->bLenientParsing) { + try { + $oList->append($this->parseSelector()); + } catch (UnexpectedTokenException $e) {} + } else { + $oList->append($this->parseSelector()); + } + } + $this->consumeWhiteSpace(); + } + if (!$bIsRoot) { + throw new \Exception("Unexpected end of document"); + } + } + + private function parseAtRule() { + $this->consume('@'); + $sIdentifier = $this->parseIdentifier(); + $this->consumeWhiteSpace(); + if ($sIdentifier === 'import') { + $oLocation = $this->parseURLValue(); + $this->consumeWhiteSpace(); + $sMediaQuery = null; + if (!$this->comes(';')) { + $sMediaQuery = $this->consumeUntil(';'); + } + $this->consume(';'); + return new Import($oLocation, $sMediaQuery); + } else if ($sIdentifier === 'charset') { + $sCharset = $this->parseStringValue(); + $this->consumeWhiteSpace(); + $this->consume(';'); + $this->setCharset($sCharset->getString()); + return new Charset($sCharset); + } else if ($this->identifierIs($sIdentifier, 'keyframes')) { + $oResult = new KeyFrame(); + $oResult->setVendorKeyFrame($sIdentifier); + $oResult->setAnimationName(trim($this->consumeUntil('{', false, true))); + $this->consumeWhiteSpace(); + $this->parseList($oResult); + return $oResult; + } else if ($sIdentifier === 'namespace') { + $sPrefix = null; + $mUrl = $this->parsePrimitiveValue(); + if (!$this->comes(';')) { + $sPrefix = $mUrl; + $mUrl = $this->parsePrimitiveValue(); + } + $this->consume(';'); + if ($sPrefix !== null && !is_string($sPrefix)) { + throw new \Exception('Wrong namespace prefix '.$sPrefix); + } + if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) { + throw new \Exception('Wrong namespace url of invalid type '.$mUrl); + } + return new CSSNamespace($mUrl, $sPrefix); + } else { + //Unknown other at rule (font-face or such) + $sArgs = trim($this->consumeUntil('{', false, true)); + $this->consumeWhiteSpace(); + $bUseRuleSet = true; + foreach($this->blockRules as $sBlockRuleName) { + if($this->identifierIs($sIdentifier, $sBlockRuleName)) { + $bUseRuleSet = false; + break; + } + } + if($bUseRuleSet) { + $oAtRule = new AtRuleSet($sIdentifier, $sArgs); + $this->parseRuleSet($oAtRule); + } else { + $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs); + $this->parseList($oAtRule); + } + return $oAtRule; + } + } + + private function parseIdentifier($bAllowFunctions = true, $bIgnoreCase = true) { + $sResult = $this->parseCharacter(true); + if ($sResult === null) { + throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier'); + } + $sCharacter = null; + while (($sCharacter = $this->parseCharacter(true)) !== null) { + $sResult .= $sCharacter; + } + if ($bIgnoreCase) { + $sResult = $this->strtolower($sResult); + } + if ($bAllowFunctions && $this->comes('(')) { + $this->consume('('); + $aArguments = $this->parseValue(array('=', ' ', ',')); + $sResult = new CSSFunction($sResult, $aArguments); + $this->consume(')'); + } + return $sResult; + } + + private function parseStringValue() { + $sBegin = $this->peek(); + $sQuote = null; + if ($sBegin === "'") { + $sQuote = "'"; + } else if ($sBegin === '"') { + $sQuote = '"'; + } + if ($sQuote !== null) { + $this->consume($sQuote); + } + $sResult = ""; + $sContent = null; + if ($sQuote === null) { + //Unquoted strings end in whitespace or with braces, brackets, parentheses + while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $this->peek())) { + $sResult .= $this->parseCharacter(false); + } + } else { + while (!$this->comes($sQuote)) { + $sContent = $this->parseCharacter(false); + if ($sContent === null) { + throw new \Exception("Non-well-formed quoted string {$this->peek(3)}"); + } + $sResult .= $sContent; + } + $this->consume($sQuote); + } + return new CSSString($sResult); + } + + private function parseCharacter($bIsForIdentifier) { + if ($this->peek() === '\\') { + $this->consume('\\'); + if ($this->comes('\n') || $this->comes('\r')) { + return ''; + } + if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) { + return $this->consume(1); + } + $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u'); + if ($this->strlen($sUnicode) < 6) { + //Consume whitespace after incomplete unicode escape + if (preg_match('/\\s/isSu', $this->peek())) { + if ($this->comes('\r\n')) { + $this->consume(2); + } else { + $this->consume(1); + } + } + } + $iUnicode = intval($sUnicode, 16); + $sUtf32 = ""; + for ($i = 0; $i < 4; ++$i) { + $sUtf32 .= chr($iUnicode & 0xff); + $iUnicode = $iUnicode >> 8; + } + return iconv('utf-32le', $this->sCharset, $sUtf32); + } + if ($bIsForIdentifier) { + $peek = ord($this->peek()); + // Ranges: a-z A-Z 0-9 - _ + if (($peek >= 97 && $peek <= 122) || + ($peek >= 65 && $peek <= 90) || + ($peek >= 48 && $peek <= 57) || + ($peek === 45) || + ($peek === 95) || + ($peek > 0xa1)) { + return $this->consume(1); + } + } else { + return $this->consume(1); + } + return null; + } + + private function parseSelector() { + $oResult = new DeclarationBlock(); + $oResult->setSelector($this->consumeUntil('{', false, true)); + $this->consumeWhiteSpace(); + $this->parseRuleSet($oResult); + return $oResult; + } + + private function parseRuleSet($oRuleSet) { + while ($this->comes(';')) { + $this->consume(';'); + $this->consumeWhiteSpace(); + } + while (!$this->comes('}')) { + $oRule = null; + if($this->oParserSettings->bLenientParsing) { + try { + $oRule = $this->parseRule(); + } catch (UnexpectedTokenException $e) { + try { + $sConsume = $this->consumeUntil(array("\n", ";", '}'), true); + // We need to “unfind” the matches to the end of the ruleSet as this will be matched later + if($this->streql($this->substr($sConsume, $this->strlen($sConsume)-1, 1), '}')) { + --$this->iCurrentPosition; + $this->peekCache = null; + } else { + $this->consumeWhiteSpace(); + while ($this->comes(';')) { + $this->consume(';'); + } + } + } catch (UnexpectedTokenException $e) { + // We’ve reached the end of the document. Just close the RuleSet. + return; + } + } + } else { + $oRule = $this->parseRule(); + } + if($oRule) { + $oRuleSet->addRule($oRule); + } + $this->consumeWhiteSpace(); + } + $this->consume('}'); + } + + private function parseRule() { + $oRule = new Rule($this->parseIdentifier()); + $this->consumeWhiteSpace(); + $this->consume(':'); + $oValue = $this->parseValue(self::listDelimiterForRule($oRule->getRule())); + $oRule->setValue($oValue); + if ($this->comes('!')) { + $this->consume('!'); + $this->consumeWhiteSpace(); + $this->consume('important'); + $oRule->setIsImportant(true); + } + while ($this->comes(';')) { + $this->consume(';'); + $this->consumeWhiteSpace(); + } + return $oRule; + } + + private function parseValue($aListDelimiters) { + $aStack = array(); + $this->consumeWhiteSpace(); + //Build a list of delimiters and parsed values + while (!($this->comes('}') || $this->comes(';') || $this->comes('!') || $this->comes(')'))) { + if (count($aStack) > 0) { + $bFoundDelimiter = false; + foreach ($aListDelimiters as $sDelimiter) { + if ($this->comes($sDelimiter)) { + array_push($aStack, $this->consume($sDelimiter)); + $this->consumeWhiteSpace(); + $bFoundDelimiter = true; + break; + } + } + if (!$bFoundDelimiter) { + //Whitespace was the list delimiter + array_push($aStack, ' '); + } + } + array_push($aStack, $this->parsePrimitiveValue()); + $this->consumeWhiteSpace(); + } + //Convert the list to list objects + foreach ($aListDelimiters as $sDelimiter) { + if (count($aStack) === 1) { + return $aStack[0]; + } + $iStartPosition = null; + while (($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) { + $iLength = 2; //Number of elements to be joined + for ($i = $iStartPosition + 2; $i < count($aStack); $i+=2, ++$iLength) { + if ($sDelimiter !== $aStack[$i]) { + break; + } + } + $oList = new RuleValueList($sDelimiter); + for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i+=2) { + $oList->addListComponent($aStack[$i]); + } + array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, array($oList)); + } + } + return $aStack[0]; + } + + private static function listDelimiterForRule($sRule) { + if (preg_match('/^font($|-)/', $sRule)) { + return array(',', '/', ' '); + } + return array(',', ' ', '/'); + } + + private function parsePrimitiveValue() { + $oValue = null; + $this->consumeWhiteSpace(); + if (is_numeric($this->peek()) || ($this->comes('-.') && is_numeric($this->peek(1, 2))) || (($this->comes('-') || $this->comes('.')) && is_numeric($this->peek(1, 1)))) { + $oValue = $this->parseNumericValue(); + } else if ($this->comes('#') || $this->comes('rgb', true) || $this->comes('hsl', true)) { + $oValue = $this->parseColorValue(); + } else if ($this->comes('url', true)) { + $oValue = $this->parseURLValue(); + } else if ($this->comes("'") || $this->comes('"')) { + $oValue = $this->parseStringValue(); + } else { + $oValue = $this->parseIdentifier(true, false); + } + $this->consumeWhiteSpace(); + return $oValue; + } + + private function parseNumericValue($bForColor = false) { + $sSize = ''; + if ($this->comes('-')) { + $sSize .= $this->consume('-'); + } + while (is_numeric($this->peek()) || $this->comes('.')) { + if ($this->comes('.')) { + $sSize .= $this->consume('.'); + } else { + $sSize .= $this->consume(1); + } + } + + $sUnit = null; + foreach ($this->aSizeUnits as $iLength => &$aValues) { + $sKey = strtolower($this->peek($iLength)); + if(array_key_exists($sKey, $aValues)) { + if (($sUnit = $aValues[$sKey]) !== null) { + $this->consume($iLength); + break; + } + } + } + return new Size(floatval($sSize), $sUnit, $bForColor); + } + + private function parseColorValue() { + $aColor = array(); + if ($this->comes('#')) { + $this->consume('#'); + $sValue = $this->parseIdentifier(false); + if ($this->strlen($sValue) === 3) { + $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2]; + } + $aColor = array('r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true), 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true), 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true)); + } else { + $sColorMode = $this->parseIdentifier(false); + $this->consumeWhiteSpace(); + $this->consume('('); + $iLength = $this->strlen($sColorMode); + for ($i = 0; $i < $iLength; ++$i) { + $this->consumeWhiteSpace(); + $aColor[$sColorMode[$i]] = $this->parseNumericValue(true); + $this->consumeWhiteSpace(); + if ($i < ($iLength - 1)) { + $this->consume(','); + } + } + $this->consume(')'); + } + return new Color($aColor); + } + + private function parseURLValue() { + $bUseUrl = $this->comes('url', true); + if ($bUseUrl) { + $this->consume('url'); + $this->consumeWhiteSpace(); + $this->consume('('); + } + $this->consumeWhiteSpace(); + $oResult = new URL($this->parseStringValue()); + if ($bUseUrl) { + $this->consumeWhiteSpace(); + $this->consume(')'); + } + return $oResult; + } + + /** + * Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. We need to check for these versions too. + */ + private function identifierIs($sIdentifier, $sMatch) { + return (strcasecmp($sIdentifier, $sMatch) === 0) + ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1; + } + + private function comes($sString, $bCaseInsensitive = false) { + $sPeek = $this->peek(strlen($sString)); + return ($sPeek == '') + ? false + : $this->streql($sPeek, $sString, $bCaseInsensitive); + } + + private function peek($iLength = 1, $iOffset = 0) { + if (($peek = (!$iOffset && ($iLength === 1))) && + !is_null($this->peekCache)) { + return $this->peekCache; + } + $iOffset += $this->iCurrentPosition; + if ($iOffset >= $this->iLength) { + return ''; + } + $iLength = min($iLength, $this->iLength-$iOffset); + $out = $this->substr($this->sText, $iOffset, $iLength); + if ($peek) { + $this->peekCache = $out; + } + return $out; + } + + private function consume($mValue = 1) { + if (is_string($mValue)) { + $iLength = $this->strlen($mValue); + if (!$this->streql($this->substr($this->sText, $this->iCurrentPosition, $iLength), $mValue)) { + throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5))); + } + $this->iCurrentPosition += $this->strlen($mValue); + $this->peekCache = null; + return $mValue; + } else { + if ($this->iCurrentPosition + $mValue > $this->iLength) { + throw new UnexpectedTokenException($mValue, $this->peek(5), 'count'); + } + $sResult = $this->substr($this->sText, $this->iCurrentPosition, $mValue); + $this->iCurrentPosition += $mValue; + $this->peekCache = null; + return $sResult; + } + } + + private function consumeExpression($mExpression) { + $aMatches = null; + if (preg_match($mExpression, $this->inputLeft(), $aMatches, PREG_OFFSET_CAPTURE) === 1) { + return $this->consume($aMatches[0][0]); + } + throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression'); + } + + private function consumeWhiteSpace() { + do { + while (preg_match('/\\s/isSu', $this->peek()) === 1) { + $this->consume(1); + } + if($this->oParserSettings->bLenientParsing) { + try { + $bHasComment = $this->consumeComment(); + } catch(UnexpectedTokenException $e) { + // When we can’t find the end of a comment, we assume the document is finished. + $this->iCurrentPosition = $this->iLength; + return; + } + } else { + $bHasComment = $this->consumeComment(); + } + } while($bHasComment); + } + + private function consumeComment() { + if ($this->comes('/*')) { + $this->consume(1); + while ($this->consume(1) !== '') { + if ($this->comes('*/')) { + $this->consume(2); + return true; + } + } + } + return false; + } + + private function isEnd() { + return $this->iCurrentPosition >= $this->iLength; + } + + private function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false) { + $aEnd = is_array($aEnd) ? $aEnd : array($aEnd); + $out = ''; + $start = $this->iCurrentPosition; + + while (($char = $this->consume(1)) !== '') { + $this->consumeComment(); + if (in_array($char, $aEnd)) { + if ($bIncludeEnd) { + $out .= $char; + } elseif (!$consumeEnd) { + $this->iCurrentPosition -= $this->strlen($char); + } + return $out; + } + $out .= $char; + } + + $this->iCurrentPosition = $start; + throw new UnexpectedTokenException('One of ("'.implode('","', $aEnd).'")', $this->peek(5), 'search'); + } + + private function inputLeft() { + return $this->substr($this->sText, $this->iCurrentPosition, -1); + } + + private function substr($sString, $iStart, $iLength) { + if ($this->oParserSettings->bMultibyteSupport) { + return mb_substr($sString, $iStart, $iLength, $this->sCharset); + } else { + return substr($sString, $iStart, $iLength); + } + } + + private function strlen($sString) { + if ($this->oParserSettings->bMultibyteSupport) { + return mb_strlen($sString, $this->sCharset); + } else { + return strlen($sString); + } + } + + private function streql($sString1, $sString2, $bCaseInsensitive = true) { + if($bCaseInsensitive) { + return $this->strtolower($sString1) === $this->strtolower($sString2); + } else { + return $sString1 === $sString2; + } + } + + private function strtolower($sString) { + if ($this->oParserSettings->bMultibyteSupport) { + return mb_strtolower($sString, $this->sCharset); + } else { + return strtolower($sString); + } + } + + private function strpos($sString, $sNeedle, $iOffset) { + if ($this->oParserSettings->bMultibyteSupport) { + return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset); + } else { + return strpos($sString, $sNeedle, $iOffset); + } + } + +} diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Parsing/OutputException.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Parsing/OutputException.php new file mode 100644 index 0000000..5dc4b2c --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Parsing/OutputException.php @@ -0,0 +1,9 @@ +<?php + +namespace Sabberworm\CSS\Parsing; + +/** +* Thrown if the CSS parsers attempts to print something invalid +*/ +class OutputException extends \Exception { +} \ No newline at end of file diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Parsing/UnexpectedTokenException.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Parsing/UnexpectedTokenException.php new file mode 100644 index 0000000..410f5de --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Parsing/UnexpectedTokenException.php @@ -0,0 +1,28 @@ +<?php + +namespace Sabberworm\CSS\Parsing; + +/** +* Thrown if the CSS parsers encounters a token it did not expect +*/ +class UnexpectedTokenException extends \Exception { + private $sExpected; + private $sFound; + // Possible values: literal, identifier, count, expression, search + private $sMatchType; + + public function __construct($sExpected, $sFound, $sMatchType = 'literal') { + $this->sExpected = $sExpected; + $this->sFound = $sFound; + $this->sMatchType = $sMatchType; + $sMessage = "Token “{$sExpected}” ({$sMatchType}) not found. Got “{$sFound}”."; + if($this->sMatchType === 'search') { + $sMessage = "Search for “{$sExpected}” returned no results. Context: “{$sFound}”."; + } else if($this->sMatchType === 'count') { + $sMessage = "Next token was expected to have {$sExpected} chars. Context: “{$sFound}”."; + } else if($this->sMatchType === 'identifier') { + $sMessage = "Identifier expected. Got “{$sFound}”"; + } + parent::__construct($sMessage); + } +} \ No newline at end of file diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Property/AtRule.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Property/AtRule.php new file mode 100644 index 0000000..e9009cc --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Property/AtRule.php @@ -0,0 +1,14 @@ +<?php + +namespace Sabberworm\CSS\Property; + +use Sabberworm\CSS\Renderable; + +interface AtRule extends Renderable { + const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values'; + // Since there are more set rules than block rules, we’re whitelisting the block rules and have anything else be treated as a set rule. + const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation'; //…and more font-specific ones (to be used inside font-feature-values) + + public function atRuleName(); + public function atRuleArgs(); +} \ No newline at end of file diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Property/CSSNamespace.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Property/CSSNamespace.php new file mode 100644 index 0000000..3d5a80d --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Property/CSSNamespace.php @@ -0,0 +1,52 @@ +<?php + +namespace Sabberworm\CSS\Property; + +/** +* CSSNamespace represents an @namespace rule. +*/ +class CSSNamespace implements AtRule { + private $mUrl; + private $sPrefix; + + public function __construct($mUrl, $sPrefix = null) { + $this->mUrl = $mUrl; + $this->sPrefix = $sPrefix; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + return '@namespace '.($this->sPrefix === null ? '' : $this->sPrefix.' ').$this->mUrl->render($oOutputFormat).';'; + } + + public function getUrl() { + return $this->mUrl; + } + + public function getPrefix() { + return $this->sPrefix; + } + + public function setUrl($mUrl) { + $this->mUrl = $mUrl; + } + + public function setPrefix($sPrefix) { + $this->sPrefix = $sPrefix; + } + + public function atRuleName() { + return 'namespace'; + } + + public function atRuleArgs() { + $aResult = array($this->mUrl); + if($this->sPrefix) { + array_unshift($aResult, $this->sPrefix); + } + return $aResult; + } +} \ No newline at end of file diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Property/Charset.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Property/Charset.php new file mode 100644 index 0000000..6960422 --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Property/Charset.php @@ -0,0 +1,43 @@ +<?php + +namespace Sabberworm\CSS\Property; + +/** + * Class representing an @charset rule. + * The following restrictions apply: + * • May not be found in any CSSList other than the Document. + * • May only appear at the very top of a Document’s contents. + * • Must not appear more than once. + */ +class Charset implements AtRule { + + private $sCharset; + + public function __construct($sCharset) { + $this->sCharset = $sCharset; + } + + public function setCharset($sCharset) { + $this->sCharset = $sCharset; + } + + public function getCharset() { + return $this->sCharset; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + return "@charset {$this->sCharset->render($oOutputFormat)};"; + } + + public function atRuleName() { + return 'charset'; + } + + public function atRuleArgs() { + return $this->sCharset; + } +} \ No newline at end of file diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Property/Import.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Property/Import.php new file mode 100644 index 0000000..4fd08b1 --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Property/Import.php @@ -0,0 +1,46 @@ +<?php + +namespace Sabberworm\CSS\Property; + +use Sabberworm\CSS\Value\URL; + +/** +* Class representing an @import rule. +*/ +class Import implements AtRule { + private $oLocation; + private $sMediaQuery; + + public function __construct(URL $oLocation, $sMediaQuery) { + $this->oLocation = $oLocation; + $this->sMediaQuery = $sMediaQuery; + } + + public function setLocation($oLocation) { + $this->oLocation = $oLocation; + } + + public function getLocation() { + return $this->oLocation; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + return "@import ".$this->oLocation->render($oOutputFormat).($this->sMediaQuery === null ? '' : ' '.$this->sMediaQuery).';'; + } + + public function atRuleName() { + return 'import'; + } + + public function atRuleArgs() { + $aResult = array($this->oLocation); + if($this->sMediaQuery) { + array_push($aResult, $this->sMediaQuery); + } + return $aResult; + } +} \ No newline at end of file diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Property/Selector.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Property/Selector.php new file mode 100644 index 0000000..d84171f --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Property/Selector.php @@ -0,0 +1,74 @@ +<?php + +namespace Sabberworm\CSS\Property; + +/** + * Class representing a single CSS selector. Selectors have to be split by the comma prior to being passed into this class. + */ +class Selector { + + //Regexes for specificity calculations + const NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = '/ + (\.[\w]+) # classes + | + \[(\w+) # attributes + | + (\:( # pseudo classes + link|visited|active + |hover|focus + |lang + |target + |enabled|disabled|checked|indeterminate + |root + |nth-child|nth-last-child|nth-of-type|nth-last-of-type + |first-child|last-child|first-of-type|last-of-type + |only-child|only-of-type + |empty|contains + )) + /ix'; + + const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/ + ((^|[\s\+\>\~]+)[\w]+ # elements + | + \:{1,2}( # pseudo-elements + after|before|first-letter|first-line|selection + )) + /ix'; + + private $sSelector; + private $iSpecificity; + + public function __construct($sSelector, $bCalculateSpecificity = false) { + $this->setSelector($sSelector); + if ($bCalculateSpecificity) { + $this->getSpecificity(); + } + } + + public function getSelector() { + return $this->sSelector; + } + + public function setSelector($sSelector) { + $this->sSelector = trim($sSelector); + $this->iSpecificity = null; + } + + public function __toString() { + return $this->getSelector(); + } + + public function getSpecificity() { + if ($this->iSpecificity === null) { + $a = 0; + /// @todo should exclude \# as well as "#" + $aMatches = null; + $b = substr_count($this->sSelector, '#'); + $c = preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $this->sSelector, $aMatches); + $d = preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $this->sSelector, $aMatches); + $this->iSpecificity = ($a * 1000) + ($b * 100) + ($c * 10) + $d; + } + return $this->iSpecificity; + } + +} diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Renderable.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Renderable.php new file mode 100644 index 0000000..626613a --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Renderable.php @@ -0,0 +1,8 @@ +<?php + +namespace Sabberworm\CSS; + +interface Renderable { + public function __toString(); + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat); +} \ No newline at end of file diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Rule/Rule.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Rule/Rule.php new file mode 100644 index 0000000..7996727 --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Rule/Rule.php @@ -0,0 +1,146 @@ +<?php + +namespace Sabberworm\CSS\Rule; + +use Sabberworm\CSS\Value\RuleValueList; +use Sabberworm\CSS\Value\Value; + +/** + * RuleSets contains Rule objects which always have a key and a value. + * In CSS, Rules are expressed as follows: “key: value[0][0] value[0][1], value[1][0] value[1][1];” + */ +class Rule { + + private $sRule; + private $mValue; + private $bIsImportant; + + public function __construct($sRule) { + $this->sRule = $sRule; + $this->mValue = null; + $this->bIsImportant = false; + } + + public function setRule($sRule) { + $this->sRule = $sRule; + } + + public function getRule() { + return $this->sRule; + } + + public function getValue() { + return $this->mValue; + } + + public function setValue($mValue) { + $this->mValue = $mValue; + } + + /** + * @deprecated Old-Style 2-dimensional array given. Retained for (some) backwards-compatibility. Use setValue() instead and wrapp the value inside a RuleValueList if necessary. + */ + public function setValues($aSpaceSeparatedValues) { + $oSpaceSeparatedList = null; + if (count($aSpaceSeparatedValues) > 1) { + $oSpaceSeparatedList = new RuleValueList(' '); + } + foreach ($aSpaceSeparatedValues as $aCommaSeparatedValues) { + $oCommaSeparatedList = null; + if (count($aCommaSeparatedValues) > 1) { + $oCommaSeparatedList = new RuleValueList(','); + } + foreach ($aCommaSeparatedValues as $mValue) { + if (!$oSpaceSeparatedList && !$oCommaSeparatedList) { + $this->mValue = $mValue; + return $mValue; + } + if ($oCommaSeparatedList) { + $oCommaSeparatedList->addListComponent($mValue); + } else { + $oSpaceSeparatedList->addListComponent($mValue); + } + } + if (!$oSpaceSeparatedList) { + $this->mValue = $oCommaSeparatedList; + return $oCommaSeparatedList; + } else { + $oSpaceSeparatedList->addListComponent($oCommaSeparatedList); + } + } + $this->mValue = $oSpaceSeparatedList; + return $oSpaceSeparatedList; + } + + /** + * @deprecated Old-Style 2-dimensional array returned. Retained for (some) backwards-compatibility. Use getValue() instead and check for the existance of a (nested set of) ValueList object(s). + */ + public function getValues() { + if (!$this->mValue instanceof RuleValueList) { + return array(array($this->mValue)); + } + if ($this->mValue->getListSeparator() === ',') { + return array($this->mValue->getListComponents()); + } + $aResult = array(); + foreach ($this->mValue->getListComponents() as $mValue) { + if (!$mValue instanceof RuleValueList || $mValue->getListSeparator() !== ',') { + $aResult[] = array($mValue); + continue; + } + if ($this->mValue->getListSeparator() === ' ' || count($aResult) === 0) { + $aResult[] = array(); + } + foreach ($mValue->getListComponents() as $mValue) { + $aResult[count($aResult) - 1][] = $mValue; + } + } + return $aResult; + } + + /** + * Adds a value to the existing value. Value will be appended if a RuleValueList exists of the given type. Otherwise, the existing value will be wrapped by one. + */ + public function addValue($mValue, $sType = ' ') { + if (!is_array($mValue)) { + $mValue = array($mValue); + } + if (!$this->mValue instanceof RuleValueList || $this->mValue->getListSeparator() !== $sType) { + $mCurrentValue = $this->mValue; + $this->mValue = new RuleValueList($sType); + if ($mCurrentValue) { + $this->mValue->addListComponent($mCurrentValue); + } + } + foreach ($mValue as $mValueItem) { + $this->mValue->addListComponent($mValueItem); + } + } + + public function setIsImportant($bIsImportant) { + $this->bIsImportant = $bIsImportant; + } + + public function getIsImportant() { + return $this->bIsImportant; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + $sResult = "{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}"; + if ($this->mValue instanceof Value) { //Can also be a ValueList + $sResult .= $this->mValue->render($oOutputFormat); + } else { + $sResult .= $this->mValue; + } + if ($this->bIsImportant) { + $sResult .= ' !important'; + } + $sResult .= ';'; + return $sResult; + } + +} diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/RuleSet/AtRuleSet.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/RuleSet/AtRuleSet.php new file mode 100644 index 0000000..a21737d --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/RuleSet/AtRuleSet.php @@ -0,0 +1,44 @@ +<?php + +namespace Sabberworm\CSS\RuleSet; + +use Sabberworm\CSS\Property\AtRule; + +/** + * A RuleSet constructed by an unknown @-rule. @font-face rules are rendered into AtRuleSet objects. + */ +class AtRuleSet extends RuleSet implements AtRule { + + private $sType; + private $sArgs; + + public function __construct($sType, $sArgs = '') { + parent::__construct(); + $this->sType = $sType; + $this->sArgs = $sArgs; + } + + public function atRuleName() { + return $this->sType; + } + + public function atRuleArgs() { + return $this->sArgs; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + $sArgs = $this->sArgs; + if($sArgs) { + $sArgs = ' ' . $sArgs; + } + $sResult = "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{"; + $sResult .= parent::render($oOutputFormat); + $sResult .= '}'; + return $sResult; + } + +} \ No newline at end of file diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php new file mode 100644 index 0000000..c59e601 --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php @@ -0,0 +1,608 @@ +<?php + +namespace Sabberworm\CSS\RuleSet; + +use Sabberworm\CSS\Property\Selector; +use Sabberworm\CSS\Rule\Rule; +use Sabberworm\CSS\Value\RuleValueList; +use Sabberworm\CSS\Value\Value; +use Sabberworm\CSS\Value\Size; +use Sabberworm\CSS\Value\Color; +use Sabberworm\CSS\Value\URL; +use Sabberworm\CSS\Parsing\OutputException; + +/** + * Declaration blocks are the parts of a css file which denote the rules belonging to a selector. + * Declaration blocks usually appear directly inside a Document or another CSSList (mostly a MediaQuery). + */ +class DeclarationBlock extends RuleSet { + + private $aSelectors; + + public function __construct() { + parent::__construct(); + $this->aSelectors = array(); + } + + public function setSelectors($mSelector) { + if (is_array($mSelector)) { + $this->aSelectors = $mSelector; + } else { + $this->aSelectors = explode(',', $mSelector); + } + foreach ($this->aSelectors as $iKey => $mSelector) { + if (!($mSelector instanceof Selector)) { + $this->aSelectors[$iKey] = new Selector($mSelector); + } + } + } + + // remove one of the selector of the block + public function removeSelector($mSelector) { + if($mSelector instanceof Selector) { + $mSelector = $mSelector->getSelector(); + } + foreach($this->aSelectors as $iKey => $oSelector) { + if($oSelector->getSelector() === $mSelector) { + unset($this->aSelectors[$iKey]); + return true; + } + } + return false; + } + + /** + * @deprecated use getSelectors() + */ + public function getSelector() { + return $this->getSelectors(); + } + + /** + * @deprecated use setSelectors() + */ + public function setSelector($mSelector) { + $this->setSelectors($mSelector); + } + + public function getSelectors() { + return $this->aSelectors; + } + + /** + * Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts. + * */ + public function expandShorthands() { + // border must be expanded before dimensions + $this->expandBorderShorthand(); + $this->expandDimensionsShorthand(); + $this->expandFontShorthand(); + $this->expandBackgroundShorthand(); + $this->expandListStyleShorthand(); + } + + /** + * Create shorthand declarations (e.g. +margin+ or +font+) whenever possible. + * */ + public function createShorthands() { + $this->createBackgroundShorthand(); + $this->createDimensionsShorthand(); + // border must be shortened after dimensions + $this->createBorderShorthand(); + $this->createFontShorthand(); + $this->createListStyleShorthand(); + } + + /** + * Split shorthand border declarations (e.g. <tt>border: 1px red;</tt>) + * Additional splitting happens in expandDimensionsShorthand + * Multiple borders are not yet supported as of 3 + * */ + public function expandBorderShorthand() { + $aBorderRules = array( + 'border', 'border-left', 'border-right', 'border-top', 'border-bottom' + ); + $aBorderSizes = array( + 'thin', 'medium', 'thick' + ); + $aRules = $this->getRulesAssoc(); + foreach ($aBorderRules as $sBorderRule) { + if (!isset($aRules[$sBorderRule])) + continue; + $oRule = $aRules[$sBorderRule]; + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if (!$mRuleValue instanceof RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + foreach ($aValues as $mValue) { + if ($mValue instanceof Value) { + $mNewValue = clone $mValue; + } else { + $mNewValue = $mValue; + } + if ($mValue instanceof Size) { + $sNewRuleName = $sBorderRule . "-width"; + } else if ($mValue instanceof Color) { + $sNewRuleName = $sBorderRule . "-color"; + } else { + if (in_array($mValue, $aBorderSizes)) { + $sNewRuleName = $sBorderRule . "-width"; + } else/* if(in_array($mValue, $aBorderStyles)) */ { + $sNewRuleName = $sBorderRule . "-style"; + } + } + $oNewRule = new Rule($sNewRuleName); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $oNewRule->addValue(array($mNewValue)); + $this->addRule($oNewRule); + } + $this->removeRule($sBorderRule); + } + } + + /** + * Split shorthand dimensional declarations (e.g. <tt>margin: 0px auto;</tt>) + * into their constituent parts. + * Handles margin, padding, border-color, border-style and border-width. + * */ + public function expandDimensionsShorthand() { + $aExpansions = array( + 'margin' => 'margin-%s', + 'padding' => 'padding-%s', + 'border-color' => 'border-%s-color', + 'border-style' => 'border-%s-style', + 'border-width' => 'border-%s-width' + ); + $aRules = $this->getRulesAssoc(); + foreach ($aExpansions as $sProperty => $sExpanded) { + if (!isset($aRules[$sProperty])) + continue; + $oRule = $aRules[$sProperty]; + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if (!$mRuleValue instanceof RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + $top = $right = $bottom = $left = null; + switch (count($aValues)) { + case 1: + $top = $right = $bottom = $left = $aValues[0]; + break; + case 2: + $top = $bottom = $aValues[0]; + $left = $right = $aValues[1]; + break; + case 3: + $top = $aValues[0]; + $left = $right = $aValues[1]; + $bottom = $aValues[2]; + break; + case 4: + $top = $aValues[0]; + $right = $aValues[1]; + $bottom = $aValues[2]; + $left = $aValues[3]; + break; + } + foreach (array('top', 'right', 'bottom', 'left') as $sPosition) { + $oNewRule = new Rule(sprintf($sExpanded, $sPosition)); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $oNewRule->addValue(${$sPosition}); + $this->addRule($oNewRule); + } + $this->removeRule($sProperty); + } + } + + /** + * Convert shorthand font declarations + * (e.g. <tt>font: 300 italic 11px/14px verdana, helvetica, sans-serif;</tt>) + * into their constituent parts. + * */ + public function expandFontShorthand() { + $aRules = $this->getRulesAssoc(); + if (!isset($aRules['font'])) + return; + $oRule = $aRules['font']; + // reset properties to 'normal' per http://www.w3.org/TR/21/fonts.html#font-shorthand + $aFontProperties = array( + 'font-style' => 'normal', + 'font-variant' => 'normal', + 'font-weight' => 'normal', + 'font-size' => 'normal', + 'line-height' => 'normal' + ); + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if (!$mRuleValue instanceof RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + foreach ($aValues as $mValue) { + if (!$mValue instanceof Value) { + $mValue = mb_strtolower($mValue); + } + if (in_array($mValue, array('normal', 'inherit'))) { + foreach (array('font-style', 'font-weight', 'font-variant') as $sProperty) { + if (!isset($aFontProperties[$sProperty])) { + $aFontProperties[$sProperty] = $mValue; + } + } + } else if (in_array($mValue, array('italic', 'oblique'))) { + $aFontProperties['font-style'] = $mValue; + } else if ($mValue == 'small-caps') { + $aFontProperties['font-variant'] = $mValue; + } else if ( + in_array($mValue, array('bold', 'bolder', 'lighter')) + || ($mValue instanceof Size + && in_array($mValue->getSize(), range(100, 900, 100))) + ) { + $aFontProperties['font-weight'] = $mValue; + } else if ($mValue instanceof RuleValueList && $mValue->getListSeparator() == '/') { + list($oSize, $oHeight) = $mValue->getListComponents(); + $aFontProperties['font-size'] = $oSize; + $aFontProperties['line-height'] = $oHeight; + } else if ($mValue instanceof Size && $mValue->getUnit() !== null) { + $aFontProperties['font-size'] = $mValue; + } else { + $aFontProperties['font-family'] = $mValue; + } + } + foreach ($aFontProperties as $sProperty => $mValue) { + $oNewRule = new Rule($sProperty); + $oNewRule->addValue($mValue); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $this->addRule($oNewRule); + } + $this->removeRule('font'); + } + + /* + * Convert shorthand background declarations + * (e.g. <tt>background: url("chess.png") gray 50% repeat fixed;</tt>) + * into their constituent parts. + * @see http://www.w3.org/TR/21/colors.html#propdef-background + * */ + + public function expandBackgroundShorthand() { + $aRules = $this->getRulesAssoc(); + if (!isset($aRules['background'])) + return; + $oRule = $aRules['background']; + $aBgProperties = array( + 'background-color' => array('transparent'), 'background-image' => array('none'), + 'background-repeat' => array('repeat'), 'background-attachment' => array('scroll'), + 'background-position' => array(new Size(0, '%'), new Size(0, '%')) + ); + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if (!$mRuleValue instanceof RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + if (count($aValues) == 1 && $aValues[0] == 'inherit') { + foreach ($aBgProperties as $sProperty => $mValue) { + $oNewRule = new Rule($sProperty); + $oNewRule->addValue('inherit'); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $this->addRule($oNewRule); + } + $this->removeRule('background'); + return; + } + $iNumBgPos = 0; + foreach ($aValues as $mValue) { + if (!$mValue instanceof Value) { + $mValue = mb_strtolower($mValue); + } + if ($mValue instanceof URL) { + $aBgProperties['background-image'] = $mValue; + } else if ($mValue instanceof Color) { + $aBgProperties['background-color'] = $mValue; + } else if (in_array($mValue, array('scroll', 'fixed'))) { + $aBgProperties['background-attachment'] = $mValue; + } else if (in_array($mValue, array('repeat', 'no-repeat', 'repeat-x', 'repeat-y'))) { + $aBgProperties['background-repeat'] = $mValue; + } else if (in_array($mValue, array('left', 'center', 'right', 'top', 'bottom')) + || $mValue instanceof Size + ) { + if ($iNumBgPos == 0) { + $aBgProperties['background-position'][0] = $mValue; + $aBgProperties['background-position'][1] = 'center'; + } else { + $aBgProperties['background-position'][$iNumBgPos] = $mValue; + } + $iNumBgPos++; + } + } + foreach ($aBgProperties as $sProperty => $mValue) { + $oNewRule = new Rule($sProperty); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $oNewRule->addValue($mValue); + $this->addRule($oNewRule); + } + $this->removeRule('background'); + } + + public function expandListStyleShorthand() { + $aListProperties = array( + 'list-style-type' => 'disc', + 'list-style-position' => 'outside', + 'list-style-image' => 'none' + ); + $aListStyleTypes = array( + 'none', 'disc', 'circle', 'square', 'decimal-leading-zero', 'decimal', + 'lower-roman', 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', + 'upper-alpha', 'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic', + 'hiragana', 'hira-gana-iroha', 'katakana-iroha', 'katakana' + ); + $aListStylePositions = array( + 'inside', 'outside' + ); + $aRules = $this->getRulesAssoc(); + if (!isset($aRules['list-style'])) + return; + $oRule = $aRules['list-style']; + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if (!$mRuleValue instanceof RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + if (count($aValues) == 1 && $aValues[0] == 'inherit') { + foreach ($aListProperties as $sProperty => $mValue) { + $oNewRule = new Rule($sProperty); + $oNewRule->addValue('inherit'); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $this->addRule($oNewRule); + } + $this->removeRule('list-style'); + return; + } + foreach ($aValues as $mValue) { + if (!$mValue instanceof Value) { + $mValue = mb_strtolower($mValue); + } + if ($mValue instanceof Url) { + $aListProperties['list-style-image'] = $mValue; + } else if (in_array($mValue, $aListStyleTypes)) { + $aListProperties['list-style-types'] = $mValue; + } else if (in_array($mValue, $aListStylePositions)) { + $aListProperties['list-style-position'] = $mValue; + } + } + foreach ($aListProperties as $sProperty => $mValue) { + $oNewRule = new Rule($sProperty); + $oNewRule->setIsImportant($oRule->getIsImportant()); + $oNewRule->addValue($mValue); + $this->addRule($oNewRule); + } + $this->removeRule('list-style'); + } + + public function createShorthandProperties(array $aProperties, $sShorthand) { + $aRules = $this->getRulesAssoc(); + $aNewValues = array(); + foreach ($aProperties as $sProperty) { + if (!isset($aRules[$sProperty])) + continue; + $oRule = $aRules[$sProperty]; + if (!$oRule->getIsImportant()) { + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if (!$mRuleValue instanceof RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + foreach ($aValues as $mValue) { + $aNewValues[] = $mValue; + } + $this->removeRule($sProperty); + } + } + if (count($aNewValues)) { + $oNewRule = new Rule($sShorthand); + foreach ($aNewValues as $mValue) { + $oNewRule->addValue($mValue); + } + $this->addRule($oNewRule); + } + } + + public function createBackgroundShorthand() { + $aProperties = array( + 'background-color', 'background-image', 'background-repeat', + 'background-position', 'background-attachment' + ); + $this->createShorthandProperties($aProperties, 'background'); + } + + public function createListStyleShorthand() { + $aProperties = array( + 'list-style-type', 'list-style-position', 'list-style-image' + ); + $this->createShorthandProperties($aProperties, 'list-style'); + } + + /** + * Combine border-color, border-style and border-width into border + * Should be run after create_dimensions_shorthand! + * */ + public function createBorderShorthand() { + $aProperties = array( + 'border-width', 'border-style', 'border-color' + ); + $this->createShorthandProperties($aProperties, 'border'); + } + + /* + * Looks for long format CSS dimensional properties + * (margin, padding, border-color, border-style and border-width) + * and converts them into shorthand CSS properties. + * */ + + public function createDimensionsShorthand() { + $aPositions = array('top', 'right', 'bottom', 'left'); + $aExpansions = array( + 'margin' => 'margin-%s', + 'padding' => 'padding-%s', + 'border-color' => 'border-%s-color', + 'border-style' => 'border-%s-style', + 'border-width' => 'border-%s-width' + ); + $aRules = $this->getRulesAssoc(); + foreach ($aExpansions as $sProperty => $sExpanded) { + $aFoldable = array(); + foreach ($aRules as $sRuleName => $oRule) { + foreach ($aPositions as $sPosition) { + if ($sRuleName == sprintf($sExpanded, $sPosition)) { + $aFoldable[$sRuleName] = $oRule; + } + } + } + // All four dimensions must be present + if (count($aFoldable) == 4) { + $aValues = array(); + foreach ($aPositions as $sPosition) { + $oRule = $aRules[sprintf($sExpanded, $sPosition)]; + $mRuleValue = $oRule->getValue(); + $aRuleValues = array(); + if (!$mRuleValue instanceof RuleValueList) { + $aRuleValues[] = $mRuleValue; + } else { + $aRuleValues = $mRuleValue->getListComponents(); + } + $aValues[$sPosition] = $aRuleValues; + } + $oNewRule = new Rule($sProperty); + if ((string) $aValues['left'][0] == (string) $aValues['right'][0]) { + if ((string) $aValues['top'][0] == (string) $aValues['bottom'][0]) { + if ((string) $aValues['top'][0] == (string) $aValues['left'][0]) { + // All 4 sides are equal + $oNewRule->addValue($aValues['top']); + } else { + // Top and bottom are equal, left and right are equal + $oNewRule->addValue($aValues['top']); + $oNewRule->addValue($aValues['left']); + } + } else { + // Only left and right are equal + $oNewRule->addValue($aValues['top']); + $oNewRule->addValue($aValues['left']); + $oNewRule->addValue($aValues['bottom']); + } + } else { + // No sides are equal + $oNewRule->addValue($aValues['top']); + $oNewRule->addValue($aValues['left']); + $oNewRule->addValue($aValues['bottom']); + $oNewRule->addValue($aValues['right']); + } + $this->addRule($oNewRule); + foreach ($aPositions as $sPosition) { + $this->removeRule(sprintf($sExpanded, $sPosition)); + } + } + } + } + + /** + * Looks for long format CSS font properties (e.g. <tt>font-weight</tt>) and + * tries to convert them into a shorthand CSS <tt>font</tt> property. + * At least font-size AND font-family must be present in order to create a shorthand declaration. + * */ + public function createFontShorthand() { + $aFontProperties = array( + 'font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family' + ); + $aRules = $this->getRulesAssoc(); + if (!isset($aRules['font-size']) || !isset($aRules['font-family'])) { + return; + } + $oNewRule = new Rule('font'); + foreach (array('font-style', 'font-variant', 'font-weight') as $sProperty) { + if (isset($aRules[$sProperty])) { + $oRule = $aRules[$sProperty]; + $mRuleValue = $oRule->getValue(); + $aValues = array(); + if (!$mRuleValue instanceof RuleValueList) { + $aValues[] = $mRuleValue; + } else { + $aValues = $mRuleValue->getListComponents(); + } + if ($aValues[0] !== 'normal') { + $oNewRule->addValue($aValues[0]); + } + } + } + // Get the font-size value + $oRule = $aRules['font-size']; + $mRuleValue = $oRule->getValue(); + $aFSValues = array(); + if (!$mRuleValue instanceof RuleValueList) { + $aFSValues[] = $mRuleValue; + } else { + $aFSValues = $mRuleValue->getListComponents(); + } + // But wait to know if we have line-height to add it + if (isset($aRules['line-height'])) { + $oRule = $aRules['line-height']; + $mRuleValue = $oRule->getValue(); + $aLHValues = array(); + if (!$mRuleValue instanceof RuleValueList) { + $aLHValues[] = $mRuleValue; + } else { + $aLHValues = $mRuleValue->getListComponents(); + } + if ($aLHValues[0] !== 'normal') { + $val = new RuleValueList('/'); + $val->addListComponent($aFSValues[0]); + $val->addListComponent($aLHValues[0]); + $oNewRule->addValue($val); + } + } else { + $oNewRule->addValue($aFSValues[0]); + } + $oRule = $aRules['font-family']; + $mRuleValue = $oRule->getValue(); + $aFFValues = array(); + if (!$mRuleValue instanceof RuleValueList) { + $aFFValues[] = $mRuleValue; + } else { + $aFFValues = $mRuleValue->getListComponents(); + } + $oFFValue = new RuleValueList(','); + $oFFValue->setListComponents($aFFValues); + $oNewRule->addValue($oFFValue); + + $this->addRule($oNewRule); + foreach ($aFontProperties as $sProperty) { + $this->removeRule($sProperty); + } + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + if(count($this->aSelectors) === 0) { + // If all the selectors have been removed, this declaration block becomes invalid + throw new OutputException("Attempt to print declaration block with missing selector"); + } + $sResult = $oOutputFormat->implode($oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(), $this->aSelectors) . $oOutputFormat->spaceBeforeOpeningBrace() . '{'; + $sResult .= parent::render($oOutputFormat); + $sResult .= '}'; + return $sResult; + } + +} diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/RuleSet/RuleSet.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/RuleSet/RuleSet.php new file mode 100644 index 0000000..d33f9cd --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/RuleSet/RuleSet.php @@ -0,0 +1,119 @@ +<?php + +namespace Sabberworm\CSS\RuleSet; + +use Sabberworm\CSS\Rule\Rule; +use Sabberworm\CSS\Renderable; + +/** + * RuleSet is a generic superclass denoting rules. The typical example for rule sets are declaration block. + * However, unknown At-Rules (like @font-face) are also rule sets. + */ +abstract class RuleSet implements Renderable { + + private $aRules; + + public function __construct() { + $this->aRules = array(); + } + + public function addRule(Rule $oRule) { + $sRule = $oRule->getRule(); + if(!isset($this->aRules[$sRule])) { + $this->aRules[$sRule] = array(); + } + $this->aRules[$sRule][] = $oRule; + } + + /** + * Returns all rules matching the given rule name + * @param (null|string|Rule) $mRule pattern to search for. If null, returns all rules. if the pattern ends with a dash, all rules starting with the pattern are returned as well as one matching the pattern with the dash excluded. passing a Rule behaves like calling getRules($mRule->getRule()). + * @example $oRuleSet->getRules('font-') //returns an array of all rules either beginning with font- or matching font. + * @example $oRuleSet->getRules('font') //returns array(0 => $oRule, …) or array(). + */ + public function getRules($mRule = null) { + if ($mRule instanceof Rule) { + $mRule = $mRule->getRule(); + } + $aResult = array(); + foreach($this->aRules as $sName => $aRules) { + // Either no search rule is given or the search rule matches the found rule exactly or the search rule ends in “-” and the found rule starts with the search rule. + if(!$mRule || $sName === $mRule || (strrpos($mRule, '-') === strlen($mRule) - strlen('-') && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)))) { + $aResult = array_merge($aResult, $aRules); + } + } + return $aResult; + } + + /** + * Returns all rules matching the given pattern and returns them in an associative array with the rule’s name as keys. This method exists mainly for backwards-compatibility and is really only partially useful. + * @param (string) $mRule pattern to search for. If null, returns all rules. if the pattern ends with a dash, all rules starting with the pattern are returned as well as one matching the pattern with the dash excluded. passing a Rule behaves like calling getRules($mRule->getRule()). + * Note: This method loses some information: Calling this (with an argument of 'background-') on a declaration block like { background-color: green; background-color; rgba(0, 127, 0, 0.7); } will only yield an associative array containing the rgba-valued rule while @link{getRules()} would yield an indexed array containing both. + */ + public function getRulesAssoc($mRule = null) { + $aResult = array(); + foreach($this->getRules($mRule) as $oRule) { + $aResult[$oRule->getRule()] = $oRule; + } + return $aResult; + } + + /** + * Remove a rule from this RuleSet. This accepts all the possible values that @link{getRules()} accepts. If given a Rule, it will only remove this particular rule (by identity). If given a name, it will remove all rules by that name. Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would remove all rules with the same name. To get the old behvaiour, use removeRule($oRule->getRule()). + * @param (null|string|Rule) $mRule pattern to remove. If $mRule is null, all rules are removed. If the pattern ends in a dash, all rules starting with the pattern are removed as well as one matching the pattern with the dash excluded. Passing a Rule behaves matches by identity. + */ + public function removeRule($mRule) { + if($mRule instanceof Rule) { + $sRule = $mRule->getRule(); + if(!isset($this->aRules[$sRule])) { + return; + } + foreach($this->aRules[$sRule] as $iKey => $oRule) { + if($oRule === $mRule) { + unset($this->aRules[$sRule][$iKey]); + } + } + } else { + foreach($this->aRules as $sName => $aRules) { + // Either no search rule is given or the search rule matches the found rule exactly or the search rule ends in “-” and the found rule starts with the search rule or equals it (without the trailing dash). + if(!$mRule || $sName === $mRule || (strrpos($mRule, '-') === strlen($mRule) - strlen('-') && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)))) { + unset($this->aRules[$sName]); + } + } + } + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + $sResult = ''; + $bIsFirst = true; + foreach ($this->aRules as $aRules) { + foreach($aRules as $oRule) { + $sRendered = $oOutputFormat->safely(function() use ($oRule, $oOutputFormat) { + return $oRule->render($oOutputFormat->nextLevel()); + }); + if($sRendered === null) { + continue; + } + if($bIsFirst) { + $bIsFirst = false; + $sResult .= $oOutputFormat->nextLevel()->spaceBeforeRules(); + } else { + $sResult .= $oOutputFormat->nextLevel()->spaceBetweenRules(); + } + $sResult .= $sRendered; + } + } + + if(!$bIsFirst) { + // Had some output + $sResult .= $oOutputFormat->spaceAfterRules(); + } + + return $oOutputFormat->removeLastSemicolon($sResult); + } + +} diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Settings.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Settings.php new file mode 100644 index 0000000..cb89a86 --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Settings.php @@ -0,0 +1,54 @@ +<?php + +namespace Sabberworm\CSS; + +use Sabberworm\CSS\Rule\Rule; + +/** + * Parser settings class. + * + * Configure parser behaviour here. + */ +class Settings { + /** + * Multi-byte string support. If true (mbstring extension must be enabled), will use (slower) mb_strlen, mb_convert_case, mb_substr and mb_strpos functions. Otherwise, the normal (ASCII-Only) functions will be used. + */ + public $bMultibyteSupport; + + /** + * The default charset for the CSS if no `@charset` rule is found. Defaults to utf-8. + */ + public $sDefaultCharset = 'utf-8'; + + /** + * Lenient parsing. When used (which is true by default), the parser will not choke on unexpected tokens but simply ignore them. + */ + public $bLenientParsing = true; + + private function __construct() { + $this->bMultibyteSupport = extension_loaded('mbstring'); + } + + public static function create() { + return new Settings(); + } + + public function withMultibyteSupport($bMultibyteSupport = true) { + $this->bMultibyteSupport = $bMultibyteSupport; + return $this; + } + + public function withDefaultCharset($sDefaultCharset) { + $this->sDefaultCharset = $sDefaultCharset; + return $this; + } + + public function withLenientParsing($bLenientParsing = true) { + $this->bLenientParsing = $bLenientParsing; + return $this; + } + + public function beStrict() { + return $this->withLenientParsing(false); + } +} \ No newline at end of file diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/CSSFunction.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/CSSFunction.php new file mode 100644 index 0000000..29c4374 --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/CSSFunction.php @@ -0,0 +1,39 @@ +<?php + +namespace Sabberworm\CSS\Value; + +class CSSFunction extends ValueList { + + private $sName; + + public function __construct($sName, $aArguments, $sSeparator = ',') { + if($aArguments instanceof RuleValueList) { + $sSeparator = $aArguments->getListSeparator(); + $aArguments = $aArguments->getListComponents(); + } + $this->sName = $sName; + parent::__construct($aArguments, $sSeparator); + } + + public function getName() { + return $this->sName; + } + + public function setName($sName) { + $this->sName = $sName; + } + + public function getArguments() { + return $this->aComponents; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + $aArguments = parent::render($oOutputFormat); + return "{$this->sName}({$aArguments})"; + } + +} \ No newline at end of file diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/CSSString.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/CSSString.php new file mode 100644 index 0000000..c583efd --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/CSSString.php @@ -0,0 +1,31 @@ +<?php + +namespace Sabberworm\CSS\Value; + +class CSSString extends PrimitiveValue { + + private $sString; + + public function __construct($sString) { + $this->sString = $sString; + } + + public function setString($sString) { + $this->sString = $sString; + } + + public function getString() { + return $this->sString; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + $sString = addslashes($this->sString); + $sString = str_replace("\n", '\A', $sString); + return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType(); + } + +} \ No newline at end of file diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/Color.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/Color.php new file mode 100644 index 0000000..3ee43cd --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/Color.php @@ -0,0 +1,41 @@ +<?php + +namespace Sabberworm\CSS\Value; + +class Color extends CSSFunction { + + public function __construct($aColor) { + parent::__construct(implode('', array_keys($aColor)), $aColor); + } + + public function getColor() { + return $this->aComponents; + } + + public function setColor($aColor) { + $this->setName(implode('', array_keys($aColor))); + $this->aComponents = $aColor; + } + + public function getColorDescription() { + return $this->getName(); + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + // Shorthand RGB color values + if($oOutputFormat->getRGBHashNotation() && implode('', array_keys($this->aComponents)) === 'rgb') { + $sResult = sprintf( + '%02x%02x%02x', + $this->aComponents['r']->getSize(), + $this->aComponents['g']->getSize(), + $this->aComponents['b']->getSize() + ); + return '#'.(($sResult[0] == $sResult[1]) && ($sResult[2] == $sResult[3]) && ($sResult[4] == $sResult[5]) ? "$sResult[0]$sResult[2]$sResult[4]" : $sResult); + } + return parent::render($oOutputFormat); + } +} diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/PrimitiveValue.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/PrimitiveValue.php new file mode 100644 index 0000000..2e6e2ab --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/PrimitiveValue.php @@ -0,0 +1,7 @@ +<?php + +namespace Sabberworm\CSS\Value; + +abstract class PrimitiveValue extends Value { + +} \ No newline at end of file diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/RuleValueList.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/RuleValueList.php new file mode 100644 index 0000000..fdb2a41 --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/RuleValueList.php @@ -0,0 +1,11 @@ +<?php + +namespace Sabberworm\CSS\Value; + +class RuleValueList extends ValueList { + + public function __construct($sSeparator = ',') { + parent::__construct(array(), $sSeparator); + } + +} \ No newline at end of file diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/Size.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/Size.php new file mode 100644 index 0000000..6d995a0 --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/Size.php @@ -0,0 +1,72 @@ +<?php + +namespace Sabberworm\CSS\Value; + +class Size extends PrimitiveValue { + + const ABSOLUTE_SIZE_UNITS = 'px/cm/mm/mozmm/in/pt/pc/vh/vw/vm/vmin/vmax/rem'; //vh/vw/vm(ax)/vmin/rem are absolute insofar as they don’t scale to the immediate parent (only the viewport) + const RELATIVE_SIZE_UNITS = '%/em/ex/ch/fr'; + const NON_SIZE_UNITS = 'deg/grad/rad/s/ms/turns/Hz/kHz'; + + private $fSize; + private $sUnit; + private $bIsColorComponent; + + public function __construct($fSize, $sUnit = null, $bIsColorComponent = false) { + $this->fSize = floatval($fSize); + $this->sUnit = $sUnit; + $this->bIsColorComponent = $bIsColorComponent; + } + + public function setUnit($sUnit) { + $this->sUnit = $sUnit; + } + + public function getUnit() { + return $this->sUnit; + } + + public function setSize($fSize) { + $this->fSize = floatval($fSize); + } + + public function getSize() { + return $this->fSize; + } + + public function isColorComponent() { + return $this->bIsColorComponent; + } + + /** + * Returns whether the number stored in this Size really represents a size (as in a length of something on screen). + * @return false if the unit an angle, a duration, a frequency or the number is a component in a Color object. + */ + public function isSize() { + if (in_array($this->sUnit, explode('/', self::NON_SIZE_UNITS))) { + return false; + } + return !$this->isColorComponent(); + } + + public function isRelative() { + if (in_array($this->sUnit, explode('/', self::RELATIVE_SIZE_UNITS))) { + return true; + } + if ($this->sUnit === null && $this->fSize != 0) { + return true; + } + return false; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + $l = localeconv(); + $sPoint = preg_quote($l['decimal_point'], '/'); + return preg_replace(array("/$sPoint/", "/^(-?)0\./"), array('.', '$1.'), $this->fSize) . ($this->sUnit === null ? '' : $this->sUnit); + } + +} diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/URL.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/URL.php new file mode 100644 index 0000000..9ececd5 --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/URL.php @@ -0,0 +1,30 @@ +<?php + +namespace Sabberworm\CSS\Value; + + +class URL extends PrimitiveValue { + + private $oURL; + + public function __construct(CSSString $oURL) { + $this->oURL = $oURL; + } + + public function setURL(CSSString $oURL) { + $this->oURL = $oURL; + } + + public function getURL() { + return $this->oURL; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + return "url({$this->oURL->render($oOutputFormat)})"; + } + +} \ No newline at end of file diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/Value.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/Value.php new file mode 100644 index 0000000..3b511bd --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/Value.php @@ -0,0 +1,11 @@ +<?php + +namespace Sabberworm\CSS\Value; + +use Sabberworm\CSS\Renderable; + +abstract class Value implements Renderable { + //Methods are commented out because re-declaring them here is a fatal error in PHP < 5.3.9 + //public abstract function __toString(); + //public abstract function render(\Sabberworm\CSS\OutputFormat $oOutputFormat); +} diff --git a/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/ValueList.php b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/ValueList.php new file mode 100644 index 0000000..64b1024 --- /dev/null +++ b/includes/PHP-CSS-Parser/lib/Sabberworm/CSS/Value/ValueList.php @@ -0,0 +1,46 @@ +<?php + +namespace Sabberworm\CSS\Value; + +abstract class ValueList extends Value { + + protected $aComponents; + protected $sSeparator; + + public function __construct($aComponents = array(), $sSeparator = ',') { + if (!is_array($aComponents)) { + $aComponents = array($aComponents); + } + $this->aComponents = $aComponents; + $this->sSeparator = $sSeparator; + } + + public function addListComponent($mComponent) { + $this->aComponents[] = $mComponent; + } + + public function getListComponents() { + return $this->aComponents; + } + + public function setListComponents($aComponents) { + $this->aComponents = $aComponents; + } + + public function getListSeparator() { + return $this->sSeparator; + } + + public function setListSeparator($sSeparator) { + $this->sSeparator = $sSeparator; + } + + public function __toString() { + return $this->render(new \Sabberworm\CSS\OutputFormat()); + } + + public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { + return $oOutputFormat->implode($oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator . $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator), $this->aComponents); + } + +} diff --git a/includes/access-test-runner.php b/includes/access-test-runner.php new file mode 100644 index 0000000..1cffbb9 --- /dev/null +++ b/includes/access-test-runner.php @@ -0,0 +1,272 @@ +<?php + +class ameAccessTestRunner implements ArrayAccess { + const TEST_DATA_META_KEY = 'ws_ame_access_test_data'; + + /** + * @var WPMenuEditor + */ + private $menuEditor; + + private $get = array(); + + private $test_menu = null; + private $test_target_item = null; + private $test_target_parent = null; + private $test_relevant_role = null; + + private $original_wp_die_handler = null; + private $access_test_results = array(); + + public function __construct($menuEditor, $queryParameters) { + $this->menuEditor = $menuEditor; + $this->get = $queryParameters; + + add_filter('admin_menu_editor-script_data', array($this, 'addEditorScriptData')); + + add_action('wp_ajax_ws_ame_set_test_configuration', array($this, 'ajax_set_test_configuration')); + add_action('set_current_user', array($this, 'init_access_test')); + } + + public function addEditorScriptData($scriptData) { + $scriptData = array_merge( + $scriptData, + array( + 'setTestConfigurationNonce' => wp_create_nonce('ws_ame_set_test_configuration'), + 'testAccessNonce' => wp_create_nonce('ws_ame_test_access'), + ) + ); + return $scriptData; + } + + public function ajax_set_test_configuration() { + check_ajax_referer('ws_ame_set_test_configuration'); + if ( !$this->menuEditor->current_user_can_edit_menu() ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Outputs JSON, not HTML. + exit($this->menuEditor->json_encode(array( + 'error' => 'You don\'t have permission to test menu settings.', + ))); + } + + $post = $this->menuEditor->get_post_params(); + $menuData = strval($post['data']); + + $metaId = add_user_meta(get_current_user_id(), self::TEST_DATA_META_KEY, wp_slash($menuData), false); + if ( $metaId === false ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Outputs JSON, not HTML. + exit($this->menuEditor->json_encode(array( + 'error' => 'Failed to store test data. add_user_meta() returned FALSE.', + ))); + } + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Outputs JSON, not HTML. + exit($this->menuEditor->json_encode(array('success' => true, 'meta_id' => $metaId))); + } + + public function init_access_test() { + //We want to do this only once per page load: specifically, when WP authenticates + //the user at the start of the request. + static $is_user_already_set = false; + if ( $is_user_already_set || $this->menuEditor->is_access_test || did_action('init') ) { + return; + } + $is_user_already_set = true; + + if ( + !isset( + $this->get['ame-test-menu-access-as'], + $this->get['ame-test-target-item'] + ) + || !check_admin_referer('ws_ame_test_access') + ) { + return; + } + + + $configurations = get_user_meta(get_current_user_id(), self::TEST_DATA_META_KEY, false); + if ( empty($configurations) ) { + exit('Error: Test data not found.'); + } + + //Use the most recent config. It's usually the last one. + $json = array_pop($configurations); + //Clean up the database. + delete_user_meta(get_current_user_id(), self::TEST_DATA_META_KEY, wp_slash($json)); + + try { + $test_menu = ameMenu::load_json($json); + } catch (InvalidMenuException $e) { + exit(esc_html($e->getMessage())); + } + $this->test_menu = $test_menu; + + $user = get_user_by('login', strval($this->get['ame-test-menu-access-as'])); + if ( !$user ) { + exit('Error: User not found.'); + } + + //Everything looks good, proceed with the test. + $this->menuEditor->is_access_test = true; + + $this->access_test_results = array(); + $this->test_target_item = strval($this->get['ame-test-target-item']); + $this->test_target_parent = ameUtils::get($this->get, 'ame-test-target-parent', null); + $this->test_relevant_role = ameUtils::get($this->get, 'ame-test-relevant-role', null); + + if ( $this->test_target_parent === '' ) { + $this->test_target_parent = null; + } + if ( $this->test_relevant_role === null ) { + $this->test_relevant_role = null; + } + + wp_set_current_user($user->ID, $user->user_login); + + $this->menuEditor->set_plugin_option('security_logging_enabled', true); + add_action('admin_print_scripts', array($this, 'output_access_test_results')); + add_filter('wp_die_handler', array($this, 'replace_die_handler_for_access_test'), 25, 1); + } + + public function output_access_test_results() { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Intentionally outputs generated JS. + echo $this->get_access_test_result_script(); + } + + private function get_access_test_result_script() { + $response = array_merge( + $this->access_test_results, + array( + 'securityLog' => $this->menuEditor->get_security_log(), + ) + ); + + return '<script type="text/javascript"> + window.parent.postMessage((' . $this->menuEditor->json_encode($response) . '), "*"); + </script>'; + } + + public function replace_die_handler_for_access_test($callback = null) { + $this->original_wp_die_handler = $callback; + return array($this, 'die_during_an_access_test'); + } + + public function die_during_an_access_test($message, $title = '', $args = array()) { + if ( $this->original_wp_die_handler ) { + $script = $this->get_access_test_result_script(); + if ( $message instanceof WP_Error ) { + $message->add('ame-access-test-response', '[Access test]' . $script); + } else if ( is_string($message) ) { + $message .= $script; + } + + call_user_func($this->original_wp_die_handler, $message, $title, $args); + } else { + exit('Unexpected error: wp_die() was called but there is no default handler.'); + } + } + + private function find_target_menu_item($items, $item_file, $parent_file = null, $current_parent = null) { + foreach ($items as $item) { + $this_file = ameMenuItem::get($item, 'file', null); + if ( ($this_file === $item_file) && ($parent_file === $current_parent) ) { + return $item; + } + + if ( !empty($item['items']) ) { + $result = $this->find_target_menu_item($item['items'], $item_file, $parent_file, $this_file); + if ( $result !== null ) { + return $result; + } + } + } + return null; + } + + public function setCurrentMenuItem($menuItem) { + $this->access_test_results['currentMenuItem'] = $menuItem; + + $this->access_test_results['currentMenuItemIsTarget'] = + isset($this->access_test_results['currentMenuItem']) + && (ameMenuItem::get($this->access_test_results['currentMenuItem'], 'file', null) === $this->test_target_item) + && (ameMenuItem::get($this->access_test_results['currentMenuItem'], 'parent', null) === $this->test_target_parent); + + $this->access_test_results['isIdentity'] = + ($this->access_test_results['currentMenuItem'] === $this->access_test_results['targetMenuItem']); + } + + public function onFinalTreeReady($tree) { + //Find the target item. It might not be the same as the current item. + $this->access_test_results['targetMenuItem'] = $this->find_target_menu_item( + $tree, + $this->test_target_item, + $this->test_target_parent + ); + } + + + /** + * Whether a offset exists + * + * @link http://php.net/manual/en/arrayaccess.offsetexists.php + * @param mixed $offset <p> + * An offset to check for. + * </p> + * @return boolean true on success or false on failure. + * </p> + * <p> + * The return value will be casted to boolean if non-boolean was returned. + * @since 5.0.0 + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) { + return array_key_exists($offset, $this->access_test_results); + } + + /** + * Offset to retrieve + * + * @link http://php.net/manual/en/arrayaccess.offsetget.php + * @param mixed $offset <p> + * The offset to retrieve. + * </p> + * @return mixed Can return all value types. + * @since 5.0.0 + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) { + return $this->access_test_results[$offset]; + } + + /** + * Offset to set + * + * @link http://php.net/manual/en/arrayaccess.offsetset.php + * @param mixed $offset <p> + * The offset to assign the value to. + * </p> + * @param mixed $value <p> + * The value to set. + * </p> + * @return void + * @since 5.0.0 + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) { + $this->access_test_results[$offset] = $value; + } + + /** + * Offset to unset + * + * @link http://php.net/manual/en/arrayaccess.offsetunset.php + * @param mixed $offset <p> + * The offset to unset. + * </p> + * @return void + * @since 5.0.0 + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) { + unset($this->access_test_results[$offset]); + } +} \ No newline at end of file diff --git a/includes/admin-menu-editor-mu.php b/includes/admin-menu-editor-mu.php new file mode 100644 index 0000000..95d6f66 --- /dev/null +++ b/includes/admin-menu-editor-mu.php @@ -0,0 +1,61 @@ +<?php +/* +Plugin Name: Admin Menu Editor [Multisite module] +Plugin URI: http://adminmenueditor.com/ +Description: Lets you edit the WordPress admin menu. To access the editor, go to the Dashboard of one of your network sites and open the Settings -> Menu Editor page. +Author: Janis Elsts +Author URI: http://w-shadow.com/ +*/ + +/** +To install Admin Menu Editor as a global plugin in WPMU : + 1) Place the "admin-menu-editor" directory into your "mu-plugins" directory. + 2) Move this file, admin-menu-editor-mu.php, from the "admin-menu-editor" directory + to your "mu-plugins" directory. + +The resulting directory structure should look like this : + +mu-plugins/ + admin-menu-editor-mu.php + admin-menu-editor/ + menu-editor.php + menu-editor-core.php + ...and other Admin Menu Editor files + +**/ + +//Load the plugin +$ws_menu_editor_filename = dirname(__FILE__) . '/admin-menu-editor/menu-editor.php'; +$ws_menu_editor_pro_filename = dirname(__FILE__) . '/admin-menu-editor-pro/menu-editor.php'; +if ( file_exists($ws_menu_editor_filename) ) { + require $ws_menu_editor_filename; +} elseif ( file_exists($ws_menu_editor_pro_filename) ) { + require $ws_menu_editor_pro_filename; +} else { + add_action('admin_notices', 'ws_ame_installation_error'); +} + +function ws_ame_installation_error(){ + if ( !is_super_admin() ) return; +?> +<div class="error fade"><p> + <strong>Admin Menu Editor is installed incorrectly!</strong> + </p> + <p> + Please copy the entire <code>admin-menu-editor</code> directory to your <code>mu-plugins</code> + directory, then move only the admin-menu-editor-mu.php file from + <code>admin-menu-editor/includes</code> to <code>mu-plugins</code>. + </p> +</div> +<?php +} + +//Add the license management link(s) to our must-use module. +function ws_ame_add_mu_license_link($actions) { + global $ameLicensingUi; + if ( isset($ameLicensingUi) && is_callable(array($ameLicensingUi, 'addLicenseActionLink')) ) { + $actions = $ameLicensingUi->addLicenseActionLink($actions); + } + return $actions; +} +add_filter('network_admin_plugin_action_links_' . basename(__FILE__), 'ws_ame_add_mu_license_link'); diff --git a/includes/ame-option.php b/includes/ame-option.php new file mode 100644 index 0000000..4d2d1cf --- /dev/null +++ b/includes/ame-option.php @@ -0,0 +1,315 @@ +<?php + +namespace YahnisElsts\AdminMenuEditor\Options; + +/** + * A simplified PHP version of the Option class from Scala. + * + * It is also heavily inspired by the phpoption/phpoption package by Johannes M. Schmitt, + * though this version is designed to support PHP 5.6 and does not require PHP 7.0+. + * + * @template T + */ +abstract class Option implements \IteratorAggregate { + /** + * @return boolean + */ + abstract public function isDefined(); + + /** + * @return boolean + */ + public function isEmpty() { + return !$this->isDefined(); + } + + public function nonEmpty() { + return $this->isDefined(); + } + + /** + * @return T + * @throws \RuntimeException If the option is empty. + */ + abstract public function get(); + + /** + * @param T $default + * @return T + */ + abstract public function getOrElse($default); + + /** + * @param callable():T $callable + * @return T + */ + abstract public function getOrCall($callable); + + /** + * @param Option<T> $alternative + * @return Option<T> + */ + abstract public function orElse(self $alternative); + + /** + * @template R + * @param callable(T):R $callable + * @return Option<R> + */ + abstract public function map($callable); + + /** + * @template R + * @param callable(T):Option<R> $callable + * @return Option<R> + */ + abstract public function flatMap($callable); + + /** + * Apply the given function to the option's value, if it's not empty. + * + * This is called "each" and not "forEach" because "foreach" is a keyword, + * which means it can't be used as a function name before PHP 7.0. + * + * @param callable(T):void $callable + * @return $this The same option instance. + */ + abstract public function each($callable); + + /** + * Check if the option contains the specified value. + * + * @param mixed $value + * @return boolean + */ + abstract public function contains($value); + + + /** + * @template A + * @param A $value + * @param mixed $emptyValue + * @return Option<A> + */ + public static function fromValue($value, $emptyValue = null) { + if ( $value === $emptyValue ) { + return None::getInstance(); + } else { + return new Some($value); + } + } + + /** + * @template A + * @param callable():A $callable + * @return Option<A> + */ + public static function fromCallable($callable, $arguments = array()) { + return new LazyOption($callable, $arguments); + } +} + +/** + * @template T + * @extends Option<T> + */ +final class Some extends Option { + /** + * @var mixed + */ + private $value; + + public function __construct($value) { + $this->value = $value; + } + + public function isDefined() { + return true; + } + + public function get() { + return $this->value; + } + + public function getOrElse($default) { + return $this->value; + } + + public function getOrCall($callable) { + return $this->value; + } + + public function orElse(Option $alternative) { + return $this; + } + + public function map($callable) { + return new self($callable($this->value)); + } + + public function flatMap($callable) { + return $callable($this->value); + } + + public function each($callable) { + $callable($this->value); + return $this; + } + + public function contains($value) { + return ($this->value === $value); + } + + #[\ReturnTypeWillChange] + public function getIterator() { + return new \ArrayIterator([$this->value]); + } +} + +final class None extends Option { + /** + * @var null|self + */ + private static $instance = null; + + private function __construct() { + //Prevent others from instantiating this class. + } + + public static function getInstance() { + if ( self::$instance === null ) { + self::$instance = new self(); + } + return self::$instance; + } + + public function isDefined() { + return false; + } + + public function get() { + throw new \RuntimeException('Option is empty.'); + } + + public function getOrElse($default) { + return $default; + } + + public function getOrCall($callable) { + return $callable(); + } + + public function orElse(Option $alternative) { + return $alternative; + } + + public function map($callable) { + return $this; + } + + public function flatMap($callable) { + return $this; + } + + public function each($callable) { + //Intentionally does nothing. + return $this; + } + + public function contains($value) { + return false; + } + + #[\ReturnTypeWillChange] + public function getIterator() { + return new \EmptyIterator(); + } +} + +/** + * Lazy version of the Option class. + * + * This class just has an internal, lazy-initialized option, and it forwards all + * method calls to that option. + * + * @template T + * @extends Option<T> + */ +class LazyOption extends Option { + /** + * @var callable + */ + private $callback; + /** + * @var array + */ + private $arguments; + + /** + * @var Option<T>|null + */ + private $innerOption = null; + + public function __construct($callback, $arguments = array()) { + if ( !is_callable($callback) ) { + throw new \InvalidArgumentException('$callback must be a valid callable.'); + } + + $this->callback = $callback; + $this->arguments = $arguments; + } + + private function resolve() { + if ( $this->innerOption === null ) { + $value = call_user_func_array($this->callback, $this->arguments); + if ( $value instanceof Option ) { + $this->innerOption = $value; + } else { + $this->innerOption = Option::fromValue($value); + } + } + return $this->innerOption; + } + + public function isDefined() { + return $this->resolve()->isDefined(); + } + + public function get() { + return $this->resolve()->get(); + } + + public function getOrElse($default) { + return $this->resolve()->getOrElse($default); + } + + public function getOrCall($callable) { + return $this->resolve()->getOrCall($callable); + } + + public function orElse(Option $alternative) { + return $this->resolve()->orElse($alternative); + } + + public function map($callable) { + return $this->resolve()->map($callable); + } + + public function flatMap($callable) { + return $this->resolve()->flatMap($callable); + } + + public function each($callable) { + $this->resolve()->each($callable); + return $this; + } + + public function contains($value) { + return $this->resolve()->contains($value); + } + + #[\ReturnTypeWillChange] + public function getIterator() { + return $this->resolve()->getIterator(); + } +} \ No newline at end of file diff --git a/includes/ame-utils.php b/includes/ame-utils.php new file mode 100644 index 0000000..d05406d --- /dev/null +++ b/includes/ame-utils.php @@ -0,0 +1,735 @@ +<?php + +/** + * Miscellaneous utility functions. + */ +class ameUtils { + /** + * HTML tags allowed in WP_Error messages and titles. + * + * This is based on the default list of allowed tags in /wp-includes/kses.php. + */ + const ALLOWED_WP_ERROR_TAGS = array( + 'abbr' => array( + 'title' => true, + ), + 'acronym' => array( + 'title' => true, + ), + 'b' => array(), + 'blockquote' => array( + 'cite' => true, + ), + 'cite' => array(), + 'code' => array(), + 'del' => array( + 'datetime' => true, + ), + 'em' => array(), + 'i' => array(), + 'q' => array( + 'cite' => true, + ), + 's' => array(), + 'strong' => array(), + ); + + /** + * Get a value from a nested array or object based on a path. + * + * @param array|object $array Get an entry from this array. + * @param array|string $path A list of array keys in hierarchy order, or a string path like "foo.bar.baz". + * @param mixed $default The value to return if the specified path is not found. Defaults to NULL. + * @param string $separator Path element separator. Only applies to string paths. + * @return mixed + */ + public static function get($array, $path, $default = null, $separator = '.') { + if ( is_string($path) ) { + $path = explode($separator, $path); + } + if ( empty($path) ) { + return $default; + } + + //Follow the $path into $input as far as possible. + $currentValue = $array; + $pathExists = true; + foreach ($path as $node) { + if ( ($currentValue instanceof ArrayAccess) && $currentValue->offsetExists($node) ) { + $currentValue = $currentValue[$node]; + } else if ( is_array($currentValue) && array_key_exists($node, $currentValue) ) { + $currentValue = $currentValue[$node]; + } else if ( is_object($currentValue) && property_exists($currentValue, $node) ) { + $currentValue = $currentValue->$node; + } else { + $pathExists = false; + break; + } + } + + if ( $pathExists ) { + return $currentValue; + } + return $default; + } + + /** + * Get the first non-root directory from a path. + * + * Examples: + * "foo/bar" => "foo" + * "/foo/bar/baz.txt" => "foo" + * "bar" => null + * "baz/" => "baz" + * "/" => null + * + * @param string $fileName + * @return string|null + */ + public static function getFirstDirectory($fileName) { + $fileName = ltrim($fileName, '/'); + + $segments = explode('/', $fileName, 2); + if ( (count($segments) > 1) && ($segments[0] !== '') ) { + return $segments[0]; + } + return null; + } + + /** + * Capitalize the first character of every word. Supports UTF-8. + * + * @param string $input + * @return string + */ + public static function ucWords($input) { + static $hasUnicodeSupport = null, $charset = 'UTF-8'; + if ( $hasUnicodeSupport === null ) { + //We need the mbstring extension and PCRE UTF-8 support. + $hasUnicodeSupport = function_exists('mb_list_encodings') + && (@preg_match('/\pL/u', 'a') === 1) + && function_exists('get_bloginfo'); + + if ( $hasUnicodeSupport ) { + //Technically, the encoding can change if something switches WP to a different site + //in the middle of a request, but we'll ignore that possibility. + $charset = get_bloginfo('charset'); + $hasUnicodeSupport = in_array($charset, mb_list_encodings()) && ($charset === 'UTF-8'); + } + } + + if ( $hasUnicodeSupport ) { + $totalLength = mb_strlen($input); + $words = preg_split('/([\s\-_]++)/u', $input, -1, PREG_SPLIT_DELIM_CAPTURE); + $output = array(); + foreach ($words as $word) { + $firstCharacter = mb_substr($word, 0, 1, $charset); + //In old PHP versions, you must specify a non-null length to get the rest of the string. + $remainder = mb_substr($word, 1, $totalLength, $charset); + $output[] = mb_strtoupper($firstCharacter, $charset) . $remainder; + } + return implode('', $output); + } + return ucwords($input); + } + + /** + * Check if two arrays have the same keys and values. Arrays with string keys + * or mixed keys can be in different order and still be considered "equal". + * + * @param array $a + * @param array $b + * @return bool + */ + public static function areAssocArraysEqual($a, $b) { + $secondArraySize = count($b); + if ( count($a) !== $secondArraySize ) { + return false; + } + $sameItems = array_intersect_assoc($a, $b); + return count($sameItems) === $secondArraySize; + } + + /** + * Escape a WP_Error object for passing it to wp_die(). + * + * Converts special characters in error messages to HTML entities. + * Returns a new WP_Error instance. Does not modify the input object. + * + * @param WP_Error $error + * @return WP_Error New WP_Error instance. + */ + public static function escapeWpError($error) { + return self::copyErrorWithFilter($error, 'esc_html'); + } + + /** + * Strip disallowed HTML from a WP_Error object. + * + * @param WP_Error $error + * @return WP_Error New WP_Error instance. + */ + public static function ksesWpError($error) { + return self::copyErrorWithFilter($error, array(__CLASS__, 'ksesCallbackForErrors')); + } + + protected static function ksesCallbackForErrors($message) { + return wp_kses($message, self::ALLOWED_WP_ERROR_TAGS); + } + + /** + * Copy a WP_Error object and apply a filter callback to each message. + * + * Also, if an error has a data item that's an array with a 'title' key, + * this escapes HTML in the title. + * + * @param \WP_Error $error + * @param callable $callback + * @return \WP_Error + */ + protected static function copyErrorWithFilter($error, $callback) { + $result = new WP_Error(); + $canGetAllData = method_exists($error, 'get_all_error_data'); //WP 5.6+ + + foreach ($error->get_error_codes() as $code) { + foreach ($error->get_error_messages($code) as $message) { + $result->add($code, call_user_func($callback, $message)); + } + + if ( $canGetAllData ) { + $dataItems = $error->get_all_error_data($code); + } else { + $data = $error->get_error_data($code); + if ( $data !== null ) { + $dataItems = array($data); + } else { + $dataItems = array(); + } + } + + foreach ($dataItems as $data) { + //Page titles should never contain unescaped HTML tags. + //As of this writing, this plugin doesn't put titles in error data, + //but other code might, and wp_die() supports it. + if ( isset($data['title']) ) { + $data['title'] = esc_html($data['title']); + } + $result->add_data($data, $code); + } + } + + return $result; + } + + /** + * Get the first element of an iterable collection. + * + * @param iterable $collection Array, Traversable, Generator, etc. + * @param mixed $defaultValue Value to return if the collection is empty. + * @return mixed + */ + public static function getFirstItem($collection, $defaultValue = null) { + foreach ($collection as $value) { + return $value; + } + return $defaultValue; + } +} + +/** + * @see ameUtils::escapeWpError + * + * This function exists because the "EscapeOutput" sniff in the WordPress coding standards + * doesn't understand class methods. + * + * @param \WP_Error $error + * @return \WP_Error + */ +function wsAmeEscapeWpError($error) { + return ameUtils::escapeWpError($error); +} + +class ameFileLock { + protected $fileName; + protected $handle = null; + + public function __construct($fileName) { + $this->fileName = $fileName; + } + + //fopen() and flock() should be fine here because we only need read permissions. + //phpcs:disable WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_flock,WordPress.WP.AlternativeFunctions.file_system_read_fopen + public function acquire($timeout = null) { + if ( $this->handle !== null ) { + throw new RuntimeException('Cannot acquire a lock that is already held.'); + } + if ( !function_exists('flock') ) { + return false; + } + + $this->handle = @fopen(__FILE__, 'r'); + if ( !$this->handle ) { + $this->handle = null; + return false; + } + + $success = @flock($this->handle, LOCK_EX | LOCK_NB, $wouldBlock); + + if ( !$success && $wouldBlock && ($timeout !== null) ) { + $timeout = max(min($timeout, 0.1), 600); + $endTime = microtime(true) + $timeout; + //Wait for a short, random time and try again. + do { + $canWaitMore = $this->waitRandom($endTime); + $success = @flock($this->handle, LOCK_EX | LOCK_NB, $wouldBlock); + } while (!$success && $wouldBlock && $canWaitMore); + } + + if ( !$success ) { + fclose($this->handle); + $this->handle = null; + return false; + } + return true; + } + + public function release() { + if ( $this->handle !== null ) { + @flock($this->handle, LOCK_UN); + fclose($this->handle); + $this->handle = null; + } + } + //phpcs:enable + + /** + * Wait for a random interval without going over $endTime. + * + * @param float|int $endTime Unix timestamp. + * @return bool TRUE if there's still time until $endTime, FALSE otherwise. + */ + protected function waitRandom($endTime) { + $now = microtime(true); + if ( $now >= $endTime ) { + return false; + } + + $delayMs = wp_rand(80, 300); + $remainingTimeMs = ($endTime - $now) * 1000; + if ( $delayMs < $remainingTimeMs ) { + usleep($delayMs * 1000); + return true; + } else { + usleep($remainingTimeMs * 1000); + return false; + } + } + + public static function create($fileName) { + return new self($fileName); + } + + public function __destruct() { + $this->release(); + } +} + +class ameOrderedMap implements Iterator, Countable { + /** + * @var ameLinkedListNode[] + */ + private $nodesByKey = array(); + + /** + * @var ameLinkedListNode|null + */ + private $head = null; + /** + * @var ameLinkedListNode|null + */ + private $tail = null; + /** + * @var ameLinkedListNode|null + */ + private $currentNode = null; + + /** + * @param array $items + * @return $this + */ + public function addAll($items) { + foreach ($items as $key => $item) { + $this->set($key, $item); + } + return $this; + } + + /** + * @param string $previousKey + * @param array $items + * @return $this + */ + public function insertAllAfter($previousKey, $items) { + if ( !isset($this->nodesByKey[$previousKey]) ) { + return $this->addAll($items); + } + + $previousNode = $this->nodesByKey[$previousKey]; + foreach ($items as $key => $value) { + if ( isset($this->nodesByKey[$key]) ) { + $node = $this->nodesByKey[$key]; + } else { + $node = new ameLinkedListNode($value, $key); + $this->nodesByKey[$key] = $node; + } + + $this->insertNodeAfter($previousNode, $node); + $previousNode = $node; + } + + return $this; + } + + /** + * @param string $previousKey + * @param string $key + * @param mixed $item + * @return $this + */ + public function insertAfter($previousKey, $key, $item) { + return $this->insertAllAfter($previousKey, array($key => $item)); + } + + private function insertNodeAfter($previousNode, $newNode) { + $newNode->previous = $previousNode; + $newNode->next = $previousNode->next; + if ( $newNode->next !== null ) { + $newNode->next->previous = $newNode; + } + + $previousNode->next = $newNode; + + if ( $this->tail === $previousNode ) { + $this->tail = $newNode; + } + } + + /** + * @param string $nextKey + * @param string $key + * @param $item + * @return $this + */ + public function insertBefore($nextKey, $key, $item) { + if ( !isset($this->nodesByKey[$nextKey]) ) { + return $this->set($key, $item); + } + + $nextNode = $this->nodesByKey[$nextKey]; + $previousNode = $nextNode->previous; + + if ( isset($this->nodesByKey[$key]) ) { + $node = $this->nodesByKey[$key]; + } else { + $node = new ameLinkedListNode($item, $key); + $this->nodesByKey[$key] = $node; + } + + $node->next = $nextNode; + $node->previous = $previousNode; + + $nextNode->previous = $node; + if ( $previousNode !== null ) { + $previousNode->next = $node; + } + + if ( $this->head === $nextNode ) { + $this->head = $node; + } + + return $this; + } + + public function set($key, $item) { + if ( isset($this->nodesByKey[$key]) ) { + $this->nodesByKey[$key]->value = $item; + } else { + $this->append($key, $item); + } + return $this; + } + + private function append($key, $item) { + $node = new ameLinkedListNode($item, $key); + $this->nodesByKey[$key] = $node; + + if ( $this->tail === null ) { + $this->head = $node; + $this->tail = $node; + } else { + $this->insertNodeAfter($this->tail, $node); + $this->tail = $node; + } + + return $this; + } + + #[\ReturnTypeWillChange] + public function current() { + return $this->currentNode->value; + } + + #[\ReturnTypeWillChange] + public function next() { + if ( $this->currentNode !== null ) { + $this->currentNode = $this->currentNode->next; + } + } + + #[\ReturnTypeWillChange] + public function key() { + return $this->currentNode->key; + } + + #[\ReturnTypeWillChange] + public function valid() { + return ($this->currentNode !== null); + } + + #[\ReturnTypeWillChange] + public function rewind() { + $this->currentNode = $this->head; + } + + #[\ReturnTypeWillChange] + public function count() { + return count($this->nodesByKey); + } + + /** + * Filter the map using a callback function. + * Returns a new map that contains only the items for which the callback function returns a truthy value. + * + * @param callable $predicate + * @return ameOrderedMap + */ + public function filter($predicate) { + $result = new self(); + foreach ($this as $key => $value) { + if ( call_user_func($predicate, $value, $key) ) { + $result->append($key, $value); + } + } + return $result; + } +} + +class ameLinkedListNode { + /** + * @var string + */ + public $key; + + /** + * @var mixed + */ + public $value; + + /** + * @var self|null + */ + public $next = null; + /** + * @var self|null + */ + public $previous = null; + + public function __construct($value, $key = '') { + $this->value = $value; + $this->key = $key; + } +} + +class ameMultiDictionary { + const PATH_SEPARATOR = '.'; + const MAX_PATH_DEPTH = 64; + + /** + * Get a value from an array or object using a path. + * + * Supports multidimensional/nested arrays and objects. + * + * @param array|object $collection + * @param string|string[] $path + * @param mixed $defaultValue + * @param string $separator + * @return mixed|null The value at the specified path, or the default value + * if the path does not exist. + */ + public static function get($collection, $path, $defaultValue = null, $separator = self::PATH_SEPARATOR) { + $path = self::parsePath($path, $separator); + if ( empty($path) ) { + return $collection; + } + + //Follow the $path into the $collection as far as possible. + $currentValue = $collection; + $pathExists = true; + foreach ($path as $key) { + if ( ($currentValue instanceof ArrayAccess) && $currentValue->offsetExists($key) ) { + //Caution: offsetExists() may return false if the key exists but is null. + $currentValue = $currentValue[$key]; + } else if ( is_array($currentValue) && array_key_exists($key, $currentValue) ) { + $currentValue = $currentValue[$key]; + } else if ( is_object($currentValue) && property_exists($currentValue, $key) ) { + $currentValue = $currentValue->{$key}; + } else { + $pathExists = false; + break; + } + } + + if ( $pathExists ) { + return $currentValue; + } + return $defaultValue; + } + + public static function set( + &$collection, + $path, + $value, + $createArrays = true, + $overwriteScalars = false, + $separator = self::PATH_SEPARATOR + ) { + $path = self::parsePath($path, $separator); + if ( empty($path) ) { + //An empty path doesn't make sense, we can't replace the collection itself. + throw new InvalidArgumentException('Cannot set a value because the path is empty.'); + } + + if ( !self::isCollection($collection) ) { + //The collection is not an array or an object, so we can't set a value in it. + throw new InvalidArgumentException('Collection must be an array or an object.'); + } + + $lastKey = array_pop($path); + if ( empty($path) ) { + $target = &$collection; + } else { + $target = &self::acquireNestedCollection( + $collection, + $path, + $createArrays, + $overwriteScalars + ); + if ( $target === null ) { + return false; + } + } + + if ( is_array($target) || ($target instanceof ArrayAccess) ) { + $target[$lastKey] = $value; + } else if ( is_object($target) ) { + $target->{$lastKey} = $value; + } + return true; + } + + public static function delete(&$collection, $path, $separator = self::PATH_SEPARATOR) { + $path = self::parsePath($path, $separator); + if ( empty($path) ) { + throw new InvalidArgumentException('Cannot delete an item because the path is empty.'); + } + if ( !self::isCollection($collection) ) { + throw new InvalidArgumentException('Collection must be an array or an object.'); + } + + $lastKey = array_pop($path); + $target = &self::acquireNestedCollection($collection, $path, false); + if ( $target !== null ) { + if ( is_array($target) || ($target instanceof ArrayAccess) ) { + unset($target[$lastKey]); + } else if ( is_object($target) ) { + unset($target->{$lastKey}); + } + } + } + + public static function parsePath($path, $separator = self::PATH_SEPARATOR) { + if ( is_array($path) ) { + return $path; + } else if ( ($path === '') || ($path === $separator) ) { + return array(); + } + return explode($separator, $path, self::MAX_PATH_DEPTH); + } + + /** + * @param array $prefix + * @param string|array $path + * @return array + */ + public static function addPrefixToPath($prefix, $path, $separator = self::PATH_SEPARATOR) { + return array_merge($prefix, self::parsePath($path, $separator)); + } + + protected static function isCollection($collection) { + return is_array($collection) || is_object($collection); + } + + protected static function &acquireNestedCollection( + &$collection, + $parsedPath, + $createArrays = true, + $overwriteScalars = false + ) { + $current = &$collection; + $notFound = null; + $previousNode = null; + $previousKey = null; + foreach ($parsedPath as $key) { + //The array and object branches are functionally identical, + //but they must be separated due to syntax differences. + if ( is_array($current) || ($current instanceof ArrayAccess) ) { + if ( !isset($current[$key]) ) { + if ( $createArrays ) { + $current[$key] = array(); + } else { + return $notFound; + } + } + $current = &$current[$key]; + } else if ( is_object($current) ) { + if ( !isset($current->{$key}) ) { + if ( $createArrays ) { + $current->{$key} = array(); + } else { + return $notFound; + } + } + $current = &$current->{$key}; + } + + //Overwrite scalar values with associative arrays if necessary. + if ( !is_array($current) && !is_object($current) ) { + if ( $overwriteScalars && ($previousNode !== null) ) { + if ( is_array($previousNode) || ($previousNode instanceof ArrayAccess) ) { + $previousNode[$previousKey] = array(); + } else if ( is_object($previousNode) ) { + $previousNode->{$previousKey} = array(); + } + $current = &$previousNode[$previousKey]; + } else { + return $notFound; + } + } + + $previousNode = &$current; + $previousKey = $key; + } + + return $current; + } +} \ No newline at end of file diff --git a/includes/auto-versioning.php b/includes/auto-versioning.php new file mode 100644 index 0000000..eded348 --- /dev/null +++ b/includes/auto-versioning.php @@ -0,0 +1,139 @@ +<?php + +if ( !class_exists('AutoVersioning') ) { + +/** + * This class enables automatic versioning of CSS/JS by adding file modification time to the URLs. + * @see http://stackoverflow.com/questions/118884/ + */ +class AutoVersioning { + private static $version_in_filename = false; + + /** + * An auto-versioning wrapper for wp_register_s*() and wp_enqueue_s*() dependency APIs. + * + * @static + * @param string $wp_api_function The name of the WP dependency API to call. + * @param string $handle Script or stylesheet handle. + * @param string $src Script or stylesheet URL. + * @param array $deps Dependencies. + * @param bool|string $last_param Either $media (for wp_register_style) or $in_footer (for wp_register_script). + * @param bool $add_ver_to_filename TRUE = add version to filename, FALSE = add it to the query string. + */ + public static function add_dependency($wp_api_function, $handle, $src, $deps, $last_param, $add_ver_to_filename = false ) { + list($src, $version) = self::auto_version($src, $add_ver_to_filename); + call_user_func($wp_api_function, $handle, $src, $deps, $version, $last_param); + } + + /** + * Automatically version a script or style sheet URL based on file modification time. + * + * Returns auto-versioned $src and $ver values suitable for use with WordPress dependency APIs like + * wp_register_script() and wp_register_style(). + * + * @static + * @param string $url + * @param bool $add_ver_to_filename + * @return array array($url, $version) + */ + private static function auto_version($url, $add_ver_to_filename = false) { + $version = false; + $filename = self::guess_filename_from_url($url); + + if ( ($filename !== null) && is_file($filename) ) { + $mtime = filemtime($filename); + if ( $add_ver_to_filename ) { + $url = preg_replace('@\.([^./\?]+)(\?.*)?$@', '.' . $mtime . '.$1', $url); + $version = null; + } else { + $version = $mtime; + } + } + + return array($url, $version); + } + + private static function guess_filename_from_url($url) { + static $url_mappings = null; + if ( $url_mappings === null ) { + $url_mappings = array( + plugins_url() => WP_PLUGIN_DIR, + plugins_url('', WPMU_PLUGIN_DIR . '/dummy') => WPMU_PLUGIN_DIR, + get_stylesheet_directory_uri() => get_stylesheet_directory(), + get_template_directory_uri() => get_template_directory(), + content_url() => WP_CONTENT_DIR, + site_url('/' . WPINC) => ABSPATH . WPINC, + ); + } + + $filename = null; + foreach($url_mappings as $root_url => $directory) { + if ( strpos($url, $root_url) === 0 ) { + $filename = $directory . '/' . substr($url, strlen($root_url)); + //Get rid of the query string, if any. + list($filename, ) = explode('?', $filename, 2); + break; + } + } + + return $filename; + } + + /** + * Apply automatic versioning to all scripts and style sheets added using WP dependency APIs. + * + * If you set $add_ver_to_filename to TRUE, make sure to also add the following code to your + * .htaccess file or your site may break: + * + * <IfModule mod_rewrite.c> + * RewriteEngine On + * RewriteRule ^(.*)\.[\d]{10}\.(css|js)$ $1.$2 [L] + * </IfModule> + * + * @static + * @param bool $add_ver_to_filename + */ + public static function apply_to_all_dependencies($add_ver_to_filename = false) { + self::$version_in_filename = $add_ver_to_filename; + foreach(array('script_loader_src', 'style_loader_src') as $hook) { + add_filter($hook, __CLASS__ . '::_filter_dependency_src', 10, 1); + } + } + + public static function _filter_dependency_src($src) { + //Only add version info to CSS/JS files that don't already have it in the file name. + if ( preg_match('@(?<!\.\d{10})\.(css|js)(\?|$)@i', $src) ) { + list($src, $version) = self::auto_version($src, self::$version_in_filename); + if ( !empty($version) ) { + $src = add_query_arg('ver', $version, $src); + } + } + return $src; + } +} + +} // End of class exists check + +if ( !function_exists('wp_register_auto_versioned_script') ) { + function wp_register_auto_versioned_script($handle, $src, $deps = array(), $in_footer = false, $add_ver_to_filename = false) { + AutoVersioning::add_dependency('wp_register_script', $handle, $src, $deps, $in_footer, $add_ver_to_filename); + } +} + +if ( !function_exists('wp_register_auto_versioned_style') ) { + function wp_register_auto_versioned_style( $handle, $src, $deps = array(), $media = 'all', $add_ver_to_filename = false) { + AutoVersioning::add_dependency('wp_register_style', $handle, $src, $deps, $media, $add_ver_to_filename); + } +} + +if ( !function_exists('wp_enqueue_auto_versioned_script') ) { + function wp_enqueue_auto_versioned_script( $handle, $src, $deps = array(), $in_footer = false, $add_ver_to_filename = false ) { + AutoVersioning::add_dependency('wp_enqueue_script', $handle, $src, $deps, $in_footer, $add_ver_to_filename); + } +} + +if ( !function_exists('wp_enqueue_auto_versioned_style') ) { + function wp_enqueue_auto_versioned_style( $handle, $src, $deps = array(), $media = 'all', $add_ver_to_filename = false ) { + AutoVersioning::add_dependency('wp_enqueue_style', $handle, $src, $deps, $media, $add_ver_to_filename); + } +} \ No newline at end of file diff --git a/includes/basic-dependencies.php b/includes/basic-dependencies.php new file mode 100644 index 0000000..122732f --- /dev/null +++ b/includes/basic-dependencies.php @@ -0,0 +1,20 @@ +<?php +if ( !defined('AME_ROOT_DIR') ) { + define('AME_ROOT_DIR', dirname(dirname(__FILE__))); +} + +$thisDirectory = dirname(__FILE__); +require_once $thisDirectory . '/shadow_plugin_framework.php'; +require_once $thisDirectory . '/role-utils.php'; +require_once $thisDirectory . '/ame-utils.php'; +require_once $thisDirectory . '/menu-item.php'; +require_once $thisDirectory . '/menu.php'; +require_once $thisDirectory . '/auto-versioning.php'; +require_once $thisDirectory . '/../ajax-wrapper/AjaxWrapper.php'; +require_once $thisDirectory . '/module.php'; +require_once $thisDirectory . '/persistent-module.php'; +require_once $thisDirectory . '/shortcodes.php'; + +if ( !class_exists('WPMenuEditor', false) ) { + require_once $thisDirectory . '/menu-editor-core.php'; +} diff --git a/includes/bbpress-role-override.php b/includes/bbpress-role-override.php new file mode 100644 index 0000000..7047576 --- /dev/null +++ b/includes/bbpress-role-override.php @@ -0,0 +1,56 @@ +<?php +class ameBBPressRoleOverride { + private $customRoleSettings = array(); + private $propertiesToSave = array('roles', 'role_objects', 'role_names'); + + public function __construct() { + //Save a local copy of bbPress roles before bbPress overwrites them, then restore that saved copy later. + //Note that the priority number here must be higher than the priority of the bbPress::roles_init() callback + //and lower than the priority of the bbp_add_forums_roles() callback. + add_action('bbp_roles_init', array($this, 'maybePreserveCustomSettings'), 6); + } + + public function maybePreserveCustomSettings($wp_roles = null) { + $priority = has_action('bbp_roles_init', 'bbp_add_forums_roles'); + if ( ($priority === false) || !function_exists('bbp_get_dynamic_roles') || (empty($wp_roles)) ) { + //bbPress is not active or the current bbPress version is not supported. + return $wp_roles; + } + + $bbPressRoles = bbp_get_dynamic_roles(); + if ( !is_array($bbPressRoles) || empty($bbPressRoles) ) { + return $wp_roles; + } + + foreach (array_keys($bbPressRoles) as $id) { + $settings = array(); + foreach ($this->propertiesToSave as $property) { + if ( isset($wp_roles->{$property}[$id]) ) { + $settings[$property] = $wp_roles->{$property}[$id]; + } + } + if ( !empty($settings) ) { + $this->customRoleSettings[$id] = $settings; + } + } + + if ( !empty($this->customRoleSettings) ) { + add_action('bbp_roles_init', array($this, 'restoreCustomSettings'), $priority + 5); + } + + return $wp_roles; + } + + public function restoreCustomSettings($wp_roles = null) { + if ( empty($wp_roles) ) { + return $wp_roles; + } + foreach ($this->customRoleSettings as $id => $properties) { + foreach ($properties as $property => $value) { + $wp_roles->{$property}[$id] = $value; + } + } + $this->customRoleSettings = array(); + return $wp_roles; + } +} \ No newline at end of file diff --git a/includes/cap-suggestion-box.php b/includes/cap-suggestion-box.php new file mode 100644 index 0000000..f25b5a0 --- /dev/null +++ b/includes/cap-suggestion-box.php @@ -0,0 +1,19 @@ +<?php +if ( !defined('ABSPATH') ) { + exit('Direct access denied.'); +} +?> +<div id="ws_capability_suggestions" style="display: none;"> + <p id="ws_previewed_caps"> </p> + <table class="widefat striped"> + <thead> + <tr> + <th class="ws_ame_role_name">Role</th> + <th>Suggestion</th> + </tr> + </thead> + <tbody> + <tr><td colspan="2">This table will be populated by JavaScript</td></tr> + </tbody> + </table> +</div> \ No newline at end of file diff --git a/includes/capabilities/cap-power.csv b/includes/capabilities/cap-power.csv new file mode 100644 index 0000000..6979838 --- /dev/null +++ b/includes/capabilities/cap-power.csv @@ -0,0 +1,54 @@ +Capability;Power;Super Admin;Administrator;Editor;Author;Contributor;Subscriber +manage_network;20;Y;;;;; +manage_sites;20;Y;;;;; +manage_network_users;20;Y;;;;; +manage_network_plugins;20;Y;;;;; +manage_network_themes;20;Y;;;;; +manage_network_options;20;Y;;;;; +install_plugins;10;Y;Y (single site);;;; +install_themes;10;Y;Y (single site);;;; +edit_plugins;10;Y;Y (single site);;;; +edit_themes;10;Y;Y (single site);;;; +delete_plugins;8;Y;Y (single site);;;; +delete_themes;8;Y;Y (single site);;;; +update_core;7;Y;Y (single site);;;; +update_plugins;7;Y;Y (single site);;;; +update_themes;7;Y;Y (single site);;;; +create_users;7;Y;Y (single site);;;; +delete_users;7;Y;Y (single site);;;; +edit_users;7;Y;Y (single site);;;; +activate_plugins;6;Y;Y (single site or enabled by network setting);;;; +edit_theme_options;5;Y;Y;;;; +export;5;Y;Y;;;; +import;5;Y;Y;;;; +list_users;5;Y;Y;;;; +manage_options;5;Y;Y;;;; +promote_users;5;Y;Y;;;; +remove_users;5;Y;Y;;;; +switch_themes;5;Y;Y;;;; +moderate_comments;4;Y;Y;Y;;; +manage_categories;4;Y;Y;Y;;; +manage_links;4;Y;Y;Y;;; +edit_others_posts;4;Y;Y;Y;;; +edit_pages;4;Y;Y;Y;;; +edit_others_pages;4;Y;Y;Y;;; +edit_published_pages;4;Y;Y;Y;;; +publish_pages;4;Y;Y;Y;;; +delete_pages;4;Y;Y;Y;;; +delete_others_pages;4;Y;Y;Y;;; +delete_published_pages;4;Y;Y;Y;;; +delete_others_posts;4;Y;Y;Y;;; +delete_private_posts;4;Y;Y;Y;;; +edit_private_posts;4;Y;Y;Y;;; +read_private_posts;4;Y;Y;Y;;; +delete_private_pages;4;Y;Y;Y;;; +edit_private_pages;4;Y;Y;Y;;; +read_private_pages;4;Y;Y;Y;;; +unfiltered_html;4,2;Y;Y;Y;;; +edit_published_posts;3;Y;Y;Y;Y;; +upload_files;3;Y;Y;Y;Y;; +publish_posts;3;Y;Y;Y;Y;; +delete_published_posts;3;Y;Y;Y;Y;; +edit_posts;2;Y;Y;Y;Y;Y; +delete_posts;2;Y;Y;Y;Y;Y; +read;1;Y;Y;Y;Y;Y;Y diff --git a/includes/consistency-check.php b/includes/consistency-check.php new file mode 100644 index 0000000..0c0e705 --- /dev/null +++ b/includes/consistency-check.php @@ -0,0 +1,159 @@ +<?php +if ( !defined('ABSPATH') ) { + die(); +} + +/** @var string $pluginFile Should be provided by the including file. */ + +$log = array(); +$log[] = sprintf( + '[OK] Main plugin file: %s', + $pluginFile +); + +$log[] = sprintf( + '[Info] WordPress version: %s', + $GLOBALS['wp_version'] +); + +$log[] = sprintf( + '[Info] WP_PLUGIN_DIR: %s', + WP_PLUGIN_DIR +); + +$log[] = sprintf( + '[Info] WP_PLUGIN_URL: %s', + WP_PLUGIN_URL +); + +$log[] = sprintf( + '[Info] WPMU_PLUGIN_DIR: %s', + WPMU_PLUGIN_DIR +); + +$log[] = sprintf( + '[Info] WPMU_PLUGIN_URL: %s', + WPMU_PLUGIN_URL +); + +$expectedPluginRoot = dirname(dirname(__FILE__)); +$actualPluginRoot = dirname($pluginFile); + +if ( $expectedPluginRoot === $actualPluginRoot ) { + $log[] = sprintf( + '[OK] Plugin root directory is "%s"', + $actualPluginRoot + ); +} else { + $log[] = sprintf( + '[Error] Actual plugin directory: "%s", expected: "%s"', + $actualPluginRoot, + $expectedPluginRoot + ); +} + +$requiredFiles = array( + 'css/menu-editor.css', + 'css/jquery.qtip.min.css', + 'js/menu-editor.js', + 'js/menu-highlight-fix.js', + 'js/jquery.sort.js', + 'js/jquery.qtip.min.js', + 'images/cut.png', + 'images/delete.png', + 'images/page_white_add.png', + 'images/spinner.gif', + 'includes/editor-page.php', + 'includes/menu-editor-core.php', + 'modules/access-editor/access-editor-template.php', + 'includes/menu-item.php', + 'menu-editor.php', + 'uninstall.php', +); + +foreach($requiredFiles as $filename) { + $fullPath = dirname($pluginFile) . '/' . $filename; + if ( is_readable($fullPath) ) { + $log[] = sprintf( + '[OK] File exists: %s', + $fullPath + ); + } else { + $log[] = sprintf( + '[Error] File does not exist: %s', + $fullPath + ); + } +} + +foreach($requiredFiles as $filename) { + if ( !preg_match('@\.(css|js|png)$@', $filename) ) { + continue; + } + + $url = plugins_url($filename, $pluginFile); + $log[] = ame_test_url_access($url, $filename); +} + +echo '<pre>'; +$divider = str_repeat('-', 50); +echo "File consistency checks:\n", esc_html($divider), "\n"; +foreach($log as $message) { + echo esc_html($message), "\n"; +} + +//Test for buggy plugins_url filters. +echo esc_html($divider), "\nTesting for problems with the 'plugins_url' hook...\n"; +add_filter('plugins_url', 'ame_plugins_url_test_first', -9999, 3); +add_filter('plugins_url', 'ame_plugins_url_test_last', 9999, 3); + +$url = plugins_url('css/menu-editor.css', $pluginFile); + +remove_filter('plugins_url', 'ame_plugins_url_test_first', -9999, 3); +remove_filter('plugins_url', 'ame_plugins_url_test_last', 9999, 3); + +function ame_plugins_url_test_first($url, $path = '', $plugin = '') { + printf( + '[Info] plugins_url() output before plugin hooks: %s' . "\n", + esc_html($url) + ); + echo esc_html(ame_test_url_access($url, 'css/menu-editor.css')), "\n"; + return $url; +} + +function ame_plugins_url_test_last($url, $path = '', $plugin = '') { + printf( + '[Info] plugins_url() output after plugin hooks: %s' . "\n", + esc_html($url) + ); + echo esc_html(ame_test_url_access($url, 'css/menu-editor.css')), "\n"; + return $url; +} + +function ame_test_url_access($url, $filename) { + $result = wp_remote_get($url); + + if ( is_wp_error($result) ) { + return sprintf( + '[Error] Can not load URL: %s (%s)', + $url, + $result->get_error_message() + ); + } else if ( $result['response']['code'] == 200 ) { + return sprintf( + '[OK] URL is accessible: %s', + $url + ); + } else { + return sprintf( + '[Error] Can not load "%s", URL : %s (%d %s)', + $filename, + $url, + $result['response']['code'], + $result['response']['message'] + ); + } +} + +echo esc_html($divider); +echo '</pre>'; \ No newline at end of file diff --git a/includes/editor-page.php b/includes/editor-page.php new file mode 100644 index 0000000..fff777b --- /dev/null +++ b/includes/editor-page.php @@ -0,0 +1,728 @@ +<?php +/** + * @var array $editor_data Various pieces of data passed by the plugin. + */ +$ame_current_user = wp_get_current_user(); +$images_url = $editor_data['images_url']; +$is_pro_version = apply_filters('admin_menu_editor_is_pro', false); +$is_second_toolbar_visible = isset($_COOKIE['ame-show-second-toolbar']) && (intval($_COOKIE['ame-show-second-toolbar']) === 1); +$is_compact_layout_enabled = isset($_COOKIE['ame-compact-layout']) && (intval($_COOKIE['ame-compact-layout']) === 1); +$is_multisite = is_multisite(); + +$icons = array( + 'cut' => '/gnome-icon-theme/edit-cut-blue.png', + 'copy' => '/gion/edit-copy.png', + 'paste' => '/gnome-icon-theme/edit-paste.png', + 'hide' => '/page-invisible.png', + 'hide-and-deny' => '/font-awesome/eye-slash-color.png', + 'new' => '/page-add.png', + 'delete' => '/page-delete.png', + 'new-separator' => '/separator-add.png', + 'toggle-all' => '/check-all.png', + 'copy-permissions' => '/copy-permissions.png', + 'toggle-toolbar' => '/font-awesome/angle-double-down.png', + 'sort-ascending' => '/sort_ascending.png', + 'sort-descending' => '/sort_descending.png', +); +foreach($icons as $name => $url) { + $icons[$name] = $images_url . $url; +} +$icons = apply_filters('admin_menu_editor-toolbar_icons', $icons, $images_url); + +$toolbarButtons = new ameOrderedMap(); +$toolbarButtons->addAll(array( + 'cut' => array( + 'title' => 'Cut', + ), + 'copy' => array( + 'title' => 'Copy', + ), + 'paste' => array( + 'title' => 'Paste', + ), + 'separator-1' => null, + 'new-menu' => array( + 'title' => 'New menu', + 'iconName' => 'new', + ), + 'new-separator' => array( + 'title' => 'New separator', + 'topLevelOnly' => !$is_pro_version, + ), + 'delete' => array( + 'title' => 'Delete menu', + 'class' => array('ws_delete_menu_button'), + ), + 'separator-2' => null, +)); + +if ( !$is_pro_version ) { + ame_register_sort_buttons($toolbarButtons); +} + +if ( $editor_data['show_deprecated_hide_button'] ) { + $toolbarButtons->insertBefore( + 'delete', + 'hide', + array( + 'title' => 'Hide without preventing access (cosmetic)', + 'alt' => 'Hide (cosmetic)', + ) + ); +} + +$secondToolbarRow = new ameOrderedMap(); +if ( $is_pro_version ) { + //In the Pro version, the sort buttons are on the second row. + ame_register_sort_buttons($secondToolbarRow); +} + +$secondToolbarRowClasses = array('ws_second_toolbar_row'); +if ( !$is_second_toolbar_visible ) { + $secondToolbarRowClasses[] = 'hidden'; +} + +do_action('admin_menu_editor-register_toolbar_buttons', $toolbarButtons, $secondToolbarRow, $icons); + +if ( count($secondToolbarRow) > 0 ) { + $toolbarButtons->set( + 'toggle-toolbar', + array( + 'title' => 'Toggle second toolbar', + 'alt' => 'Toolbar toggle', + 'class' => array('ws_toggle_toolbar_button'), + 'topLevelOnly' => true, + ) + ); +} + +/** + * @param ameOrderedMap $buttons + * @param array $icons + * @param array $classes CSS classes to add to the toolbar row. + */ +function ame_output_toolbar_row($buttons, $icons, $classes = array()) { + $classes = array_merge(array('ws_button_container'), $classes); + printf('<div class="%s">', esc_attr(implode(' ', $classes))); + + foreach ($buttons as $key => $settings) { + if ( $settings === null ) { + echo '<div class="ws_separator"> </div>'; + continue; + } + + if ( !isset($settings['title']) ) { + $settings['title'] = $key; + } + $action = isset($settings['action']) ? $settings['action'] : $key; + + $buttonClasses = array('ws_button'); + if ( !empty($settings['class']) ) { + $buttonClasses = array_merge($buttonClasses, $settings['class']); + } + + $attributes = array( + 'data-ame-button-action' => $action, + 'class' => implode(' ', $buttonClasses), + 'href' => '#', + 'title' => $settings['title'], + ); + if ( isset($settings['attributes']) ) { + $attributes = array_merge($attributes, $settings['attributes']); + } + + $iconName = isset($settings['iconName']) ? $settings['iconName'] : $key; + $icon = ''; + if ( isset($icons[$iconName]) ) { + $icon = sprintf( + '<img src="%s" alt="%s">', + esc_attr($icons[$iconName]), + esc_attr(isset($settings['alt']) ? $settings['alt'] : $settings['title']) + ); + } + + $pairs = array(); + foreach ($attributes as $name => $value) { + $pairs[] = $name . '="' . esc_attr($value) . '"'; + } + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Attribute $pairs and $icon attributes were escaped with esc_attr() above. + printf('<a %s>%s</a>' . "\n", implode(' ', $pairs), $icon); + } + + echo '<div class="clear"></div>' . "\n"; + echo '</div>'; +} + +//Output the "Upgrade to Pro" message +if ( !apply_filters('admin_menu_editor_is_pro', false) ){ + ?> + <script type="text/javascript"> + (function($){ + var screenLinks = $('#screen-meta-links'); + screenLinks.append( + '<div id="ws-pro-version-notice" class="custom-screen-meta-link-wrap">' + + '<a href="https://adminmenueditor.com/upgrade-to-pro/?utm_source=Admin%2BMenu%2BEditor%2Bfree&utm_medium=text_link&utm_content=top_upgrade_link&utm_campaign=Plugins" id="ws-pro-version-notice-link" class="show-settings custom-screen-meta-link" target="_blank" title="View Pro version details">Upgrade to Pro</a>' + + '</div>' + ); + })(jQuery); + </script> + <?php +} + +?> + +<?php do_action('admin_menu_editor-display_header'); ?> + +<?php +// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Don't need that here, just showing a notice. +if ( !empty($_GET['message']) && (intval($_GET['message']) == 2) ){ + echo '<div id="message" class="error"><p><strong>Failed to decode input! The menu wasn\'t modified.</strong></p></div>'; +} + +include dirname(__FILE__) . '/../modules/access-editor/access-editor-template.php'; +$extrasDirectory = dirname(__FILE__) . '/../extras'; +if ( $is_pro_version ) { + include $extrasDirectory . '/menu-color-dialog.php'; + include $extrasDirectory . '/copy-permissions-dialog.php'; +} + +/** + * @param ameOrderedMap $toolbar + */ +function ame_register_sort_buttons($toolbar) { + $toolbar->addAll(array( + 'sort-ascending' => array( + 'title' => 'Sort ascending', + 'action' => 'sort', + 'attributes' => array( + 'data-sort-direction' => 'asc', + ), + ), + 'sort-descending' => array( + 'title' => 'Sort descending', + 'action' => 'sort', + 'attributes' => array( + 'data-sort-direction' => 'desc', + ), + ), + )); +} + +?> + +<div id='ws_menu_editor' style="visibility: hidden;" class="<?php + if ( $is_compact_layout_enabled ) { + echo 'ws_compact_layout'; + } else { + echo 'ws_large_layout'; + } +?>"> + + <?php include dirname(__FILE__) . '/../modules/actor-selector/actor-selector-template.php'; ?> + + <div> + + <div class='ws_main_container'> + <div class='ws_toolbar'> + <?php + ame_output_toolbar_row($toolbarButtons, $icons); + ame_output_toolbar_row($secondToolbarRow, $icons, $secondToolbarRowClasses); + ?> + </div> + + <div id='ws_menu_box' class="ws_box"> + </div> + + <?php do_action('admin_menu_editor-container', 'menu'); ?> + </div> + + <div class='ws_main_container' id="ame-submenu-column-template" style="display: none;"> + <div class='ws_toolbar'> + <?php + function ame_button_can_be_in_submenu_toolbar($settings) { + return empty($settings['topLevelOnly']); + } + + ame_output_toolbar_row( + $toolbarButtons->filter('ame_button_can_be_in_submenu_toolbar'), + $icons + ); + + ame_output_toolbar_row( + $secondToolbarRow->filter('ame_button_can_be_in_submenu_toolbar'), + $icons, + $secondToolbarRowClasses + ); + ?> + </div> + + <div id='ws_submenu_box' class="ws_box"> + </div> + + <?php do_action('admin_menu_editor-container', 'submenu'); ?> + </div> + + <div class="ws_basic_container"> + + <div class="ws_main_container" id="ws_editor_sidebar"> + <form method="post" action="<?php echo esc_url(add_query_arg('noheader', '1', $editor_data['current_tab_url'])); ?>" id='ws_main_form' name='ws_main_form'> + <?php wp_nonce_field('menu-editor-form'); ?> + <input type="hidden" name="action" value="save_menu"> + <?php + printf('<input type="hidden" name="config_id" value="%s">', esc_attr($editor_data['menu_config_id'])); + ?> + <input type="hidden" name="data" id="ws_data" value=""> + <input type="hidden" name="data_length" id="ws_data_length" value=""> + <input type="hidden" name="selected_actor" id="ws_selected_actor" value=""> + + <input type="hidden" name="selected_menu_url" id="ws_selected_menu_url" value=""> + <input type="hidden" name="selected_submenu_url" id="ws_selected_submenu_url" value=""> + + <input type="hidden" name="expand_menu" id="ws_expand_selected_menu" value=""> + <input type="hidden" name="expand_submenu" id="ws_expand_selected_submenu" value=""> + + <input type="hidden" name="deep_nesting_enabled" id="ws_is_deep_nesting_enabled" value=""> + + <input type="button" id='ws_save_menu' class="button-primary ws_main_button" value="Save Changes" /> + </form> + + <input type="button" id='ws_reset_menu' value="Undo changes" class="button ws_main_button" /> + <input type="button" id='ws_load_menu' value="Load default menu" class="button ws_main_button" /> + + <!-- + <input type="button" id='ws_test_access' value="Test access..." class="button ws_main_button" /> + --> + + <?php + $compact_layout_title = 'Compact layout'; + if ( $is_compact_layout_enabled ) { + $compact_layout_title = '✓ ' . $compact_layout_title; + } + ?> + <input type="button" + id='ws_toggle_editor_layout' + value="<?php echo esc_attr($compact_layout_title); ?>" + class="button ws_main_button" /> + + <?php + do_action('admin_menu_editor-sidebar'); + ?> + </div> + + <div class="clear"></div> + <div class="metabox-holder"> + <?php + if ( apply_filters('admin_menu_editor-show_general_box', false) ) : + $is_general_box_open = true; + if ( isset($_COOKIE['ame_vis_box_open']) ) { + $is_general_box_open = ($_COOKIE['ame_vis_box_open'] === '1'); + } + $box_class = $is_general_box_open ? '' : 'closed'; + + ?> + <div class="postbox ws_ame_custom_postbox <?php echo esc_attr($box_class); ?>" id="ws_ame_general_vis_box"> + <button type="button" class="handlediv button-link"> + <span class="toggle-indicator"></span> + </button> + <h2 class="hndle">General</h2> + <div class="inside"> + <?php do_action('admin_menu_editor-general_box'); ?> + </div> + </div> + <?php + endif; + + $is_how_to_box_open = true; + if ( isset($_COOKIE['ame_how_to_box_open']) ) { + $is_how_to_box_open = ($_COOKIE['ame_how_to_box_open'] === '1'); + } + $box_class = $is_how_to_box_open ? '' : 'closed'; + + if ( $is_pro_version ) { + $tutorial_base_url = 'https://adminmenueditor.com/documentation/'; + } else { + $tutorial_base_url = 'https://adminmenueditor.com/free-version-docs/'; + } + + /** @noinspection HtmlUnknownTarget */ + $how_to_link_template = '<a href="' . esc_url($tutorial_base_url) . '%1$s" target="_blank" title="Opens in a new tab">%2$s</a>'; + $how_to_item_template = '<li>' . $how_to_link_template . '</li>'; + + ?> + <div class="postbox ws_ame_custom_postbox <?php echo esc_attr($box_class); ?>" id="ws_ame_how_to_box"> + <button type="button" class="handlediv button-link"> + <span class="toggle-indicator"></span> + </button> + <h2 class="hndle">How To</h2> + <div class="inside"> + <ul class="ame-tutorial-list"> + <?php + if ( $is_pro_version ): + //Pro version tutorials. + ?> + <li><?php + printf( + //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML template contains HTML. + $how_to_link_template, + 'how-to-hide-a-menu-item/', + 'Hide a Menu...' + ); + ?> + <ul class="ame-tutorial-list"> + <?php + foreach ( + array( + 'how-to-hide-a-menu-item/#how-to-hide-a-menu-from-a-role' => 'From a Role', + 'how-to-hide-a-menu-item/#how-to-hide-a-menu-from-a-user' => 'From a User', + 'how-to-hide-a-menu-item/#how-to-hide-a-menu-from-everyone-except-yourself' => 'From Everyone Except You', + 'how-to-hide-menu-without-preventing-access/' => 'Without Preventing Access', + ) + as $how_to_url => $how_to_title + ) { + printf( + //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML template. + $how_to_item_template, + esc_attr($how_to_url), + esc_html($how_to_title) + ); + } + ?> + </ul> + </li> + <?php + foreach ( + array( + 'how-to-give-access-to-menu/' => 'Show a Menu', + 'how-to-move-and-sort-menus/' => 'Move and Sort Menus', + 'how-to-add-a-new-menu-item/' => 'Add a New Menu', + ) + as $how_to_url => $how_to_title + ) { + printf( + //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML template + $how_to_item_template, + esc_attr($how_to_url), + esc_html($how_to_title) + ); + } + + else: + //Free version tutorials. + foreach ( + array( + 'how-to-hide-menus/' => 'Hide a Menu Item', + 'how-to-hide-menus-cosmetic/' => 'Hide Without Blocking Access', + 'how-to-add-new-menu/' => 'Add a New Menu', + ) + as $how_to_url => $how_to_title + ) { + printf( + //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML template + $how_to_item_template, + esc_attr($how_to_url), + esc_html($how_to_title) + ); + } + endif; + ?> + </ul> + </div> + </div> + </div> <!-- / .metabox-holder --> + + <?php + $hint_id = 'ws_sidebar_pro_ad'; + $show_pro_benefits = !apply_filters('admin_menu_editor_is_pro', false) && (!isset($editor_data['show_hints'][$hint_id]) || $editor_data['show_hints'][$hint_id]); + + if ( $show_pro_benefits ): + $benefit_variations = array( + 'Hide dashboard widgets.', + 'More menu icons.', + 'Make menus open in a new tab or an iframe.', + 'Prevent users from deleting a specific user.', + ); + //Pseudo-randomly select one phrase based on the site URL. + $variation_index = hexdec( substr(md5(get_site_url() . 'ab'), -2) ) % count($benefit_variations); + $selected_variation = $benefit_variations[$variation_index]; + + $pro_version_link = 'http://adminmenueditor.com/upgrade-to-pro/?utm_source=Admin%2BMenu%2BEditor%2Bfree&utm_medium=text_link&utm_content=sidebar_link_nv' . $variation_index . '&utm_campaign=Plugins'; + ?> + <div class="clear"></div> + + <div class="ws_hint" id="<?php echo esc_attr($hint_id); ?>"> + <div class="ws_hint_close" title="Close">x</div> + <div class="ws_hint_content"> + <strong>Upgrade to Pro:</strong> + <ul> + <li>Role-based menu permissions.</li> + <li>Hide items from specific users.</li> + <li>Menu import and export.</li> + <li>Change menu colors.</li> + <li><?php echo esc_html($selected_variation); ?></li> + </ul> + <a href="<?php echo esc_url($pro_version_link); ?>" target="_blank">Learn more</a> + | + <a href="https://amedemo.com/" target="_blank">Try online demo</a> + </div> + </div> + <?php + endif; + ?> + + </div> <!-- / .ws_basic_container --> + + </div> + + <div class="clear"></div> + +</div> <!-- / .ws_menu_editor --> + +<?php do_action('admin_menu_editor-display_footer'); ?> + + + +<?php + //Create a pop-up capability selector + $capSelector = array('<select id="ws_cap_selector" class="ws_dropdown" size="10">'); + + $capSelector[] = '<optgroup label="Roles">'; + foreach($editor_data['all_roles'] as $role_id => $role_name){ + $capSelector[] = sprintf( + '<option value="%s">%s</option>', + esc_attr($role_id), + esc_html($role_name) + ); + } + $capSelector[] = '</optgroup>'; + + $capSelector[] = '<optgroup label="Capabilities">'; + foreach($editor_data['all_capabilities'] as $cap){ + $capSelector[] = sprintf( + '<option value="%s">%s</option>', + esc_attr($cap), + esc_html($cap) + ); + } + $capSelector[] = '</optgroup>'; + $capSelector[] = '</select>'; + + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Generated HTML, should be escaped above. + echo implode("\n", $capSelector); +?> + +<!-- Menu icon selector widget --> +<div id="ws_icon_selector" class="ws_with_more_icons" style="display: none;"> + + <div id="ws_icon_source_tabs"> + <ul class="ws_tool_tab_nav"> + <?php + $iconSelectorTabs = apply_filters( + 'admin_menu_editor-icon_selector_tabs', + array('ws_core_icons_tab' => 'Dashicons') + ); + foreach($iconSelectorTabs as $tabId => $caption) { + printf('<li><a href="#%s">%s</a></li>', esc_attr($tabId), esc_html($caption)); + } + ?> + </ul> + + <?php + //Let the user select a custom icon via the media uploader. + //We only support the new WP 3.5+ media API. Hence, the function_exists() check. + if ( function_exists('wp_enqueue_media') ): + ?> + <input type="button" class="button" + id="ws_choose_icon_from_media" + title="Upload an image or choose one from your media library" + value="Media Library"> + <div class="clear"></div> + <?php + endif; + ?> + + <div class="ws_tool_tab" id="ws_core_icons_tab"> + + <?php + //These dashicons are used in the default admin menu. + $defaultDashicons = array( + 'admin-generic', 'dashboard', 'admin-post', 'admin-media', 'admin-links', 'admin-page', 'admin-comments', + 'admin-appearance', 'admin-plugins', 'admin-users', 'admin-tools', 'admin-settings', 'admin-network', + ); + + //The rest of Dashicons. Some icons were manually removed as they wouldn't look good as menu icons. + $dashicons = array( + 'admin-site', 'admin-home', + 'album', 'align-center', 'align-left', 'align-none', 'align-right', + 'analytics', 'archive', 'art', 'awards', 'backup', 'book', 'book-alt', + 'building', 'businessman', 'calendar', 'calendar-alt', 'camera', 'carrot', + 'cart', 'category', 'chart-area', 'chart-bar', 'chart-line', 'chart-pie', + 'clipboard', 'clock', 'cloud', 'desktop', 'dismiss', 'download', 'edit', 'editor-code', 'editor-contract', 'editor-customchar', + 'editor-distractionfree', 'editor-help', 'editor-insertmore', + 'editor-justify', 'editor-kitchensink', 'editor-ol', 'editor-paste-text', + 'editor-paste-word', 'editor-quote', 'editor-removeformatting', 'editor-rtl', 'editor-spellcheck', + 'editor-ul', 'editor-unlink', 'editor-video', + 'email', 'email-alt', 'exerpt-view', 'external', 'facebook', + 'facebook-alt', 'feedback', 'filter', 'flag', 'format-aside', + 'format-audio', 'format-chat', 'format-gallery', 'format-image', 'format-quote', 'format-status', + 'format-video', 'forms', 'googleplus', 'grid-view', 'groups', + 'hammer', 'heart', 'hidden', 'id', 'id-alt', 'image-crop', 'image-filter', + 'image-flip-horizontal', 'image-flip-vertical', 'image-rotate', + 'image-rotate-left', 'image-rotate-right', 'images-alt', + 'images-alt2', 'index-card', 'info', 'leftright', 'lightbulb', 'list-view', + 'location', 'location-alt', 'lock', 'marker', + 'media-archive', 'media-audio', 'media-code', 'media-default', 'media-video', 'megaphone', + 'menu', 'microphone', 'migrate', 'minus', 'money', 'nametag', 'networking', 'no', + 'no-alt', 'palmtree', 'performance', 'phone', 'playlist-audio', + 'playlist-video', 'plus', 'plus-alt', 'portfolio', 'post-status', 'post-trash', + 'pressthis', 'products', 'redo', 'rss', 'schedule', + 'screenoptions', 'search', 'share', 'share-alt', + 'share-alt2', 'share1', 'shield', 'shield-alt', 'slides', 'smartphone', 'smiley', 'sort', 'sos', 'star-empty', + 'star-filled', 'star-half', 'sticky', 'store', 'tablet', 'tag', + 'tagcloud', 'testimonial', 'text', 'thumbs-down', 'thumbs-up', 'translation', 'twitter', 'undo', + 'universal-access', 'universal-access-alt', 'unlock', + 'update', 'upload', 'vault', 'video-alt', 'video-alt2', 'video-alt3', 'visibility', 'warning', 'welcome-add-page', + 'welcome-comments', 'welcome-learn-more', 'welcome-view-site', 'welcome-widgets-menus', 'welcome-write-blog', + 'wordpress', 'wordpress-alt', 'yes' + ); + + if ($editor_data['dashicons_available']) { + function ws_ame_print_dashicon_option($icon, $isExtraIcon = false) { + printf( + '<div class="ws_icon_option%3$s" title="%1$s" data-icon-url="dashicons-%2$s"> + <div class="ws_icon_image dashicons dashicons-%2$s"></div> + </div>', + esc_attr(ucwords(str_replace('-', ' ', $icon))), + $icon, + $isExtraIcon ? ' ws_icon_extra' : '' + ); + } + + foreach($defaultDashicons as $icon) { + ws_ame_print_dashicon_option($icon); + } + foreach($dashicons as $icon) { + ws_ame_print_dashicon_option($icon, true); + } + } + + $defaultIconImages = array( + admin_url('images/generic.png'), + ); + foreach($defaultIconImages as $icon) { + printf( + '<div class="ws_icon_option" data-icon-url="%1$s"> + <img src="%1$s"> + </div>', + esc_attr($icon) + ); + } + + ?> + <div class="ws_icon_option ws_custom_image_icon" title="Custom image" style="display: none;"> + <img src="<?php echo esc_url(admin_url('images/loading.gif')); ?>" alt="Loading indicator"> + </div> + + <div class="clear"></div> + </div> + + <?php do_action('admin_menu_editor-icon_selector'); ?> + + </div><!-- tab container --> + +</div> + +<span id="ws-ame-screen-meta-contents" style="display:none;"> + <label for="ws-hide-advanced-settings"> + <input type="checkbox" id="ws-hide-advanced-settings"<?php + if ( $this->options['hide_advanced_settings'] ){ + echo ' checked="checked"'; + } + ?> /> Hide advanced options + </label><br> +</span> + + +<!-- Confirmation dialog when hiding "Dashboard -> Home" --> +<div id="ws-ame-dashboard-hide-confirmation" style="display: none;"> + <span> + Hiding <em>Dashboard -> Home</em> may prevent users with the selected role from logging in! + Are you sure you want to do it? + </span> + + <h4>Explanation</h4> + <p> + WordPress automatically redirects users to the <em>Dashboard -> Home</em> page upon successful login. + If you hide this page, users will get an "insufficient permissions" error when they log in + due to being redirected to a hidden page. As a result, it will look like their login failed. + </p> + + <h4>Recommendations</h4> + <p> + You can use a plugin like <a href="http://wordpress.org/plugins/peters-login-redirect/">Peter's Login Redirect</a> + to redirect specific roles to different pages. + </p> + + <div class="ws_dialog_buttons"> + <?php + submit_button('Hide the menu', 'primary', 'ws_confirm_menu_hiding', false); + submit_button('Leave it visible', 'secondary', 'ws_cancel_menu_hiding', false); + ?> + </div> + + <label class="ws_dont_show_again"> + <input type="checkbox" id="ws-ame-disable-dashboard-hide-confirmation"> + Don't show this message again + </label> +</div> + +<!-- Confirmation dialog when trying to delete a non-custom item. --> +<div id="ws-ame-menu-deletion-error" title="Error" style="display: none;"> + <div class="ws_dialog_panel"> + Sorry, it's not possible to permanently delete + <span id="ws-ame-menu-type-desc">{a built-in menu item|an item added by another plugin}</span>. + Would you like to hide it instead? + </div> + + <div class="ws_dialog_buttons ame-vertical-button-list"> + <?php + submit_button('Hide it from all users', 'secondary', 'ws_hide_menu_from_everyone', false); + submit_button( + sprintf('Hide it from everyone except "%s"', $ame_current_user->get('user_login')), + 'secondary', + 'ws_hide_menu_except_current_user', + false + ); + submit_button( + 'Hide it from everyone except Administrator', + 'secondary', + 'ws_hide_menu_except_administrator', + false + ); + submit_button('Cancel', 'secondary', 'ws_cancel_menu_deletion', false); + ?> + </div> +</div> + +<?php include dirname(__FILE__) . '/cap-suggestion-box.php'; ?> + +<?php include dirname(__FILE__) . '/test-access-screen.php'; ?> + +<?php +if ( $is_pro_version ) { + include $extrasDirectory . '/page-dropdown.php'; +} +?> + + +<!--suppress JSUnusedLocalSymbols These variables are actually used by menu-editor.js --> +<script type='text/javascript'> + var defaultMenu = <?php + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Should be JSON. + echo $editor_data['default_menu_js']; + ?>; + var customMenu = <?php + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Should also be JSON. + echo $editor_data['custom_menu_js']; + ?>; +</script> diff --git a/includes/generate-menu-dashicons.php b/includes/generate-menu-dashicons.php new file mode 100644 index 0000000..570d666 --- /dev/null +++ b/includes/generate-menu-dashicons.php @@ -0,0 +1,127 @@ +<?php +/** + * This utility script generates menu icons metadata based on the Dashicons icon font included in WordPress. + */ + +if ( !defined('ABSPATH') ) { + die('No direct script access'); +} +if ( !constant('WP_DEBUG') || !current_user_can('edit_plugins') ) { + echo "Permission denied. You need the edit_plugins cap to run this script and WP_DEBUG must be enabled."; + return; +} + +require_once dirname(__FILE__) . '/PHP-CSS-Parser/autoloader.php'; +$dashiconsStylesheet = ABSPATH . WPINC . '/css/dashicons.css'; + +$icons = array(); + +$allDashiconDefinitions = ''; + +$ignoreIcons = array('dashboard', 'editor-bold', 'editor-italic'); +$ignoreIcons = array_flip($ignoreIcons); + +// phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown -- Not fetching remote data. +$parser = new Sabberworm\CSS\Parser(file_get_contents($dashiconsStylesheet)); +$cssDocument = $parser->parse(); + +$blocks = $cssDocument->getAllDeclarationBlocks(); +foreach($blocks as $block) { + /** @var Sabberworm\CSS\RuleSet\DeclarationBlock $block */ + + //We want the ".dashicons-*:before" selectors. + $selectors = $block->getSelectors(); + foreach($selectors as $selector) { + /** @var Sabberworm\CSS\Property\Selector $selector */ + + if ( preg_match('/\.dashicons-(?P<name>[\w\-]+):before/', $selector->getSelector(), $matches) ) { + $name = $matches['name']; + $char = null; + + //The arrow icons aren't really suitable as menu icons. + if ( preg_match('/^(arrow)-/', $name) ) { + break; + } + + //Some icons are duplicates of the "admin-" icons or just wouldn't look very good in a menu. + if ( array_key_exists($name, $ignoreIcons) ) { + break; + } + + $rules = $block->getRules('content'); //Expect something like "content: '\f123'". + foreach($rules as $rule) { + /** @var Sabberworm\CSS\Rule\Rule $rule */ + $value = $rule->getValue(); + if ($value instanceof Sabberworm\CSS\Value\CSSString) { + //The parser defaults to UTF-8. Convert the char to a hexadecimal escape code + //so we don't have to worry about our CSS charset. + $char = ltrim(bin2hex(iconv('UTF-8', 'UCS-4', $value->getString())), '0'); + $icons[$name] = '\\' . $char; + } + } + + if (isset($char) && ($name !== 'before')) { + $allDashiconDefinitions .= sprintf( + '%s { content: "\%s" !important; }', + implode(', ', $selectors), + $char + ) . "\n"; + } + + break; + } + } +} + +$dashiconComment = sprintf( + "/*\nThis file was automatically generated from /wp-includes/css/dashicons.css.\nLast update: %s\n*/", + gmdate('c') +); +// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_file_put_contents -- Dev stuff; WP_DEBUG must be enabled to get here. +file_put_contents( + dirname(__FILE__) . '/../css/_dashicons.scss', + $dashiconComment . "\n" . $allDashiconDefinitions +); + +?> +<div class="wrap"> +<h2>Dashicons to Menu Icons</h2> +<style type="text/css" scoped="scoped"> + .ame-debug-dashicon { + display: inline-block; + margin: 2px; + min-width: 180px; + } +</style> +<?php + +ksort($icons); +$arrayDefinition = "array(\n"; +$currentLine = "\t"; + +foreach($icons as $name => $character) { + //Output each icon for visual verification. + printf( + '<div class="ame-debug-dashicon"><div class="dashicons dashicons-%1$s"></div> %1$s</div>', + esc_html($name) + ); + + //Wrap the array definition at about 80 characters for legibility. + $item = "'" . $name . "', "; + if ( strlen($currentLine . $item) > 80 ) { + $arrayDefinition .= $currentLine . "\n"; + $currentLine = "\t"; + } + + $currentLine .= $item; +} + +if (strlen($currentLine) > 1) { + $arrayDefinition .= $currentLine . "\n"; +} +$arrayDefinition .= ')'; + +echo '<div class="clear"></div><br>'; +echo '<textarea cols="100" rows="20">', esc_textarea($arrayDefinition), '</textarea>'; + +echo '</div>'; \ No newline at end of file diff --git a/includes/menu-editor-core.php b/includes/menu-editor-core.php new file mode 100644 index 0000000..ba2ae53 --- /dev/null +++ b/includes/menu-editor-core.php @@ -0,0 +1,5261 @@ +<?php + +class WPMenuEditor extends MenuEd_ShadowPluginFramework { + const WPML_CONTEXT = 'admin-menu-editor menu texts'; + + const VERBOSITY_LOW = 1; + const VERBOSITY_NORMAL = 2; + const VERBOSITY_VERBOSE = 5; + + const DIRECTLY_GRANTED_VIRTUAL_CAPS = 2; + const ALL_VIRTUAL_CAPS = 3; + + /** + * @var string The heading tag to use for admin pages. + */ + public static $admin_heading_tag = 'h1'; + + private $plugin_db_version = 140; + + /** @var array The default WordPress menu, before display-specific filtering. */ + protected $default_wp_menu; + /** @var array The default WordPress submenu. */ + protected $default_wp_submenu; + + /** + * We also keep track of the final, ready-for-display version of the default WP menu + * and submenu. These values are captured *just* before the admin menu HTML is output + * by _wp_menu_output() in /wp-admin/menu-header.php, and are restored afterwards. + */ + private $old_wp_menu; + private $old_wp_submenu; + + private $title_lookups = array(); //A list of page titles indexed by $item['file']. Used to + //fix the titles of moved plugin pages. + private $reverse_item_lookup = array(); //Contains the final (merged & filtered) list of admin menu items, + //indexed by URL. + + /** + * @var array List of per-URL capabilities, indexed by priority. Used while merging and + * building the final admin menu. + */ + private $page_access_lookup = array(); + + /** + * @var array A log of menu access checks. + */ + private $security_log = array(); + + /** + * @var array The current custom menu with defaults merged in. + */ + private $merged_custom_menu = null; + + /** + * @var array The custom menu in WP-compatible format (top-level). + */ + private $custom_wp_menu = null; + + /** + * @var array The custom menu in WP-compatible format (sub-menu). + */ + private $custom_wp_submenu = null; + + private $item_templates = array(); //A lookup list of default menu items, used as templates for the custom menu. + private $relative_template_order = array(); + + private $cached_custom_menu = null; //Cached, non-merged version of the custom menu. Used by load_custom_menu(). + private $loaded_menu_config_id = null; + private $cached_virtual_caps = null;//List of virtual caps. Used by get_virtual_caps(). + + private $cached_user_caps = array(); //A cache of the current user's capabilities. Used only in very specific places. + private $user_cap_cache_enabled = false; + + //Our personal copy of the request vars, without any "magic quotes". + private $post = array(); + private $get = array(); + private $originalPost = array(); + + /** + * @var array A cache of user role names indexed by user ID. E.g. [123 => array("administrator", "foo")] + */ + private $cached_user_roles = array(); + + private $cached_virtual_user_caps = array(); + private $virtual_caps_for_this_call = array(); + + public $disable_virtual_caps = false; + public $virtual_cap_mode = 3; //self::ALL_VIRTUAL_CAPS + + /** + * @var array<string,true|string> An index of URLs relative to /wp-admin/. Any menus that match the index will be ignored. + */ + private $menu_url_blacklist = array(); + + /** + * @var array Menu editor page tabs. + */ + private $tabs = array(); + + /** + * @var string The slug of the current settings tab, if any. + */ + private $current_tab = ''; + + /** + * @var ameModule[] List of modules that were loaded for the current request. + */ + private $loaded_modules = array(); + private $are_modules_loaded = false; + + /** + * @var array List of capabilities that are used in the default admin menu. Used to detect meta capabilities. + */ + private $caps_used_in_menu = array(); + + /** + * @var bool Tue if the last displayed custom menu had more than two levels. + */ + private $custom_menu_is_deep = false; + + public $is_access_test = false; + private $test_menu = null; + /** + * @var ameAccessTestRunner|null + */ + private $access_test_runner = null; + + /** + * @var Exception|null + */ + private $last_menu_exception = null; + + private static $jquery_plugins = array( + //jQuery JSON plugin + 'jquery-json' => 'js/jquery.json.js', + //jQuery sort plugin + 'jquery-sort' => 'js/jquery.sort.js', + //qTip2 - jQuery tooltip plugin + 'jquery-qtip' => 'js/jquery.qtip.min.js', + //jQuery Form plugin. This is a more recent version than the one included with WP. + 'ame-jquery-form' => 'js/jquery.form.js', + //jQuery cookie plugin + 'ame-jquery-cookie' => 'js/jquery.biscuit.js', + ); + private $registered_jquery_plugins = array(); + + function init(){ + $this->sitewide_options = true; + + //Set some plugin-specific options + if ( empty($this->option_name) ){ + $this->option_name = 'ws_menu_editor'; + } + $this->defaults = array( + 'hide_advanced_settings' => true, + 'show_extra_icons' => false, + 'custom_menu' => null, + 'custom_network_menu' => null, + 'first_install_time' => null, + 'display_survey_notice' => true, + 'plugin_db_version' => 0, + 'security_logging_enabled' => false, + + 'menu_config_scope' => ($this->is_super_plugin() || !is_multisite()) ? 'global' : 'site', + + //super_admin, specific_user, or a capability. + 'plugin_access' => $this->is_super_plugin() ? 'super_admin' : 'manage_options', + //The ID of the user who is allowed to use this plugin. Only used when plugin_access == specific_user. + 'allowed_user_id' => null, + //The user who can see this plugin on the "Plugins" page. By default all admins can see it. + 'plugins_page_allowed_user_id' => null, + + 'show_deprecated_hide_button' => true, //Note: Un-deprecated as of 2015.10.01. + 'dashboard_hiding_confirmation_enabled' => true, + + //When to show submenu icons. + 'submenu_icons_enabled' => 'if_custom', //"never", "if_custom" or "always". + + //Enable/disable CSS workaround that helps override menu icons set by other plugins. + 'force_custom_dashicons' => true, + + //Menu editor UI colour scheme. "Classic" is the old blue/yellow scheme, and "wp-grey" is more WP-like. + 'ui_colour_scheme' => 'classic', + + //User logins that will show up in the actor list at the top of the editor. + 'visible_users' => array(), + + //Enable/disable the admin notice that tells the user where the plugin settings menu is. + 'show_plugin_menu_notice' => true, + + //Where to place menu items that are not part of the last saved menu configuration. + //This usually applies to new items added by other plugins and, in Multisite, items that exist on + //the current site but did not exist on the site where the user last edited the menu configuration. + 'unused_item_position' => 'relative', //"relative" or "bottom". + + //Permissions for menu items that are not part of the save menu configuration. + //The default is to leave the permissions unchanged. + 'unused_item_permissions' => 'unchanged', //"unchanged" or "match_plugin_access". + + //Verbosity level of menu permission errors. + 'error_verbosity' => self::VERBOSITY_NORMAL, + + //Enable/disable menu configuration compression. Enabling it makes the DB row much smaller, + //but adds decompression overhead to very admin page. + 'compress_custom_menu' => false, + + //Make custom menu and page titles translatable with WPML. They will appear in the "Strings" section. + //This only applies to custom (i.e. changed) titles. + 'wpml_support_enabled' => true, + //Prevent bbPress from resetting its own roles. This should allow the user to edit bbPress roles + //with any role editing plugin. Disabled by default due to risk of conflicts and the performance impact. + 'bbpress_override_enabled' => false, + + //Experimental: Allow more than two levels of menus. + 'deep_nesting_enabled' => null, + 'was_nesting_ever_changed' => false, + + //Which modules are active or inactive. Format: ['module-id' => true/false]. + 'is_active_module' => array( + 'highlight-new-menus' => false, + ), + ); + $this->serialize_with_json = false; //(Don't) store the options in JSON format + + //WP 4.3+ uses H1 headings for admin pages. Older versions use H2 instead. + self::$admin_heading_tag = version_compare($GLOBALS['wp_version'], '4.3', '<') ? 'h2' : 'h1'; + + $this->settings_link = (is_network_admin() ? 'settings.php' : 'options-general.php') . '?page=menu_editor'; + + $this->magic_hooks = true; + //Run our hooks last (almost). Priority is less than PHP_INT_MAX mostly for defensive programming purposes. + //Old PHP versions have known bugs related to large array keys, and WP might have undiscovered edge cases. + $this->magic_hook_priority = PHP_INT_MAX - 10; + + /* + * Menu blacklist. Any menu items that *exactly* match one of the URLs on this list will be ignored. + * They won't show up in the editor or the admin menu, but they will remain accessible (caps permitting). + * + * This is a workaround for plugins that add a menu item and then remove it. Most plugins do this + * to create "Welcome" or "What's New" pages that are accessible but don't appear in the admin menu. + * + * We can't automatically detect menus like that. Here's why: + * 1) Most plugins remove them too late, e.g. in admin_head. By that point, output has already started. + * We need to finalize the list of menu items and their permissions before that. + * 2) It's hard to automatically determine *why* a menu item was removed. We can't distinguish between + * cosmetic changes like the hidden "welcome" items and people removing menus to deny access. + */ + $this->menu_url_blacklist = array( + //WP RSS Aggregator 4.7.7 + 'index.php?page=wprss-welcome' => true, + //AffiliateWP 1.7.8 + 'index.php?page=affwp-getting-started' => true, + 'index.php?page=affwp-what-is-new' => true, + 'index.php?page=affwp-credits' => true, + //BuddyPress 2.3.4 + 'index.php?page=bp-about' => true, + 'index.php?page=bp-credits' => true, + //BuddyBoss 1.5.9 + 'admin.php?page=buddyboss-platform' => 'submenu', + //DW Question Answer 1.3.8.1 + 'index.php?page=dwqa-about' => true, + 'index.php?page=dwqa-changelog' => true, + 'index.php?page=dwqa-credits' => true, + //Ninja Forms 2.9.41 + 'index.php?page=nf-about' => true, + 'index.php?page=nf-changelog' => true, + 'index.php?page=nf-getting-started' => true, + 'index.php?page=nf-credits' => true, + //All in One SEO Pack 2.3.9.2 + 'index.php?page=aioseop-about' => true, + //WP Courseware 4.1.2 + //'wpcw' => true, //This is commented out due to a bug. The Courseware top level menu and its first submenu + //both have the URL "wpcw", but the top level menu also has some visible, non-blacklisted items. AME would + //still hide the entire menu because the template builder doesn't check if a menu has submenu items. + 'admin.php?page=wpcw-course-classroom' => true, + 'admin.php?page=wpcw-student' => true, + 'admin.php?page=WPCW_showPage_ConvertPage' => true, + 'admin.php?page=WPCW_showPage_CourseOrdering' => true, + 'admin.php?page=WPCW_showPage_GradeBook' => true, + 'admin.php?page=WPCW_showPage_ModifyCourse' => true, + 'admin.php?page=WPCW_showPage_ModifyModule' => true, + 'admin.php?page=WPCW_showPage_ModifyQuestion' => true, + 'admin.php?page=WPCW_showPage_ModifyQuiz' => true, + 'admin.php?page=WPCW_showPage_UserCourseAccess' => true, + 'admin.php?page=WPCW_showPage_UserProgess' => true, + 'admin.php?page=WPCW_showPage_UserProgess_quizAnswers' => true, + //Extended Widget Options + 'index.php?page=extended-widget-opts-getting-started' => true, + //Snax + 'options-general.php?page=snax-pages-settings' => true, + 'options-general.php?page=snax-lists-settings' => true, + 'options-general.php?page=snax-quizzes-settings' => true, + 'options-general.php?page=snax-polls-settings' => true, + 'options-general.php?page=snax-stories-settings' => true, + 'options-general.php?page=snax-memes-settings' => true, + 'options-general.php?page=snax-audios-settings' => true, + 'options-general.php?page=snax-videos-settings' => true, + 'options-general.php?page=snax-images-settings' => true, + 'options-general.php?page=snax-galleries-settings' => true, + 'options-general.php?page=snax-embeds-settings' => true, + 'options-general.php?page=snax-voting-settings' => true, + 'options-general.php?page=snax-limits-settings' => true, + 'options-general.php?page=snax-auth-settings' => true, + 'options-general.php?page=snax-moderation-settings' => true, + 'options-general.php?page=snax-embedly-settings' => true, + 'options-general.php?page=snax-demo-settings' => true, + 'index.php?page=snax-about' => true, + 'options-general.php?page=snax-collections-settings' => true, + 'options-general.php?page=snax-links-settings' => true, + 'options-general.php?page=snax-extproduct-settings' => true, + 'options-general.php?page=snax-slog-settings' => true, + 'options-general.php?page=snax-slog-networks-settings' => true, + 'options-general.php?page=snax-slog-locations-settings' => true, + 'options-general.php?page=snax-slog-log-settings' => true, + 'options-general.php?page=snax-slog-gdpr-settings' => true, + 'options-general.php?page=snax-shares-settings' => true, + 'options-general.php?page=snax-shares-positions-settings' => true, + //Media Ace + 'options-general.php?page=mace-image-bulk-settings' => true, + 'options-general.php?page=mace-lazy_load-settings' => true, + 'options-general.php?page=mace-watermarks-settings' => true, + 'options-general.php?page=mace-hotlink-settings' => true, + 'options-general.php?page=mace-gif-settings' => true, + 'options-general.php?page=mace-auto-featured-image-settings' => true, + 'options-general.php?page=mace-expiry-settings' => true, + 'options-general.php?page=mace-video-settings' => true, + 'options-general.php?page=mace-gallery-settings' => true, + 'options-general.php?page=mace-general-settings' => true, + //"What's Your Reaction" + 'options-general.php?page=wyr-fakes-settings' => true, + //WP-Job-Manager 1.34.1 + 'index.php?page=job-manager-setup' => true, + //Simple Calendar 3.1.33 + 'index.php?page=simple-calendar_about' => true, + 'index.php?page=simple-calendar_credits' => true, + 'index.php?page=simple-calendar_translators' => true, + //Stripe For WooCommerce 3.2.12 + 'wc_stripe' => 'submenu', + //WP Grid Builder 1.5.9 + 'admin.php?page=wpgb-card-builder' => true, + 'admin.php?page=wpgb-grid-settings' => true, + 'admin.php?page=wpgb-facet-settings' => true, + //Google Analytics for WordPress by MonsterInsights 8.4.0 + 'index.php?page=monsterinsights-getting-started' => true, + ); + + //AJAXify screen options + add_action('wp_ajax_ws_ame_save_screen_options', array($this,'ajax_save_screen_options')); + + //AJAXify hints and warnings + add_action('wp_ajax_ws_ame_hide_hint', array($this, 'ajax_hide_hint')); + add_action( + 'wp_ajax_ws_ame_disable_dashboard_hiding_confirmation', + array($this, 'ajax_disable_dashboard_hiding_confirmation') + ); + + //Retrieve a list of pages via AJAX. + add_action('wp_ajax_ws_ame_get_pages', array($this, 'ajax_get_pages')); + //Get details about a specific page via AJAX. + add_action('wp_ajax_ws_ame_get_page_details', array($this, 'ajax_get_page_details')); + + //Make sure we have access to the original, un-mangled request data. + //This is necessary because WordPress will stupidly apply "magic quotes" + //to the request vars even if this PHP misfeature is disabled. + $this->capture_request_vars(); + + add_action('admin_enqueue_scripts', array($this, 'enqueue_menu_fix_script'), 8); + + //Enqueue miscellaneous helper scripts and styles. + add_action('admin_enqueue_scripts', array($this, 'enqueue_helper_scripts')); + add_action('admin_print_styles', array($this, 'enqueue_helper_styles')); + + //Make sure our scripts load before other plugins' scripts. + add_action('admin_print_scripts', array($this, 'move_editor_scripts_to_top')); + + //User survey + add_action('admin_notices', array($this, 'display_survey_notice')); + + //Tell first-time users where they can find the plugin settings page. + add_action('all_admin_notices', array($this, 'display_plugin_menu_notice')); + + //Reset plugin access if the only allowed user gets deleted or their ID changes. + add_action('wp_login', array($this, 'maybe_reset_plugin_access'), 10, 2); + + //Grant virtual capabilities like "super_user" to users. + add_filter('user_has_cap', array($this, 'grant_virtual_caps_to_user'), 9, 3); + add_filter('user_has_cap', array($this, 'regrant_virtual_caps_to_user'), 200, 1); + + //Update caches when the current user changes. + add_action('set_current_user', array($this, 'update_current_user_cache'), 1, 0); //Run before most plugins. + //Clear or refresh per-user caches when the user's roles or capabilities change. + add_action('updated_user_meta', array($this, 'on_user_metadata_changed'), 10, 3); + add_action('deleted_user_meta', array($this, 'on_user_metadata_changed'), 10, 3); + //There's also a "set_user_role" hook, but it's only called by WP_User::set_role and not WP_User::add_role. + //It's also redundant - WP_User::set_role updates user meta, so the above hooks already cover it. + + //Multisite: Clear role and capability caches when switching to another site. + add_action('switch_blog', array($this, 'clear_site_specific_caches'), 10, 0); + + //"Test Access" feature. + if ( (defined('DOING_AJAX') && DOING_AJAX) || isset($this->get['ame-test-menu-access-as']) ) { + require_once 'access-test-runner.php'; + $this->access_test_runner = new ameAccessTestRunner($this, $this->get); + } + + //Additional links below the plugin description. + add_filter('plugin_row_meta', array($this, 'add_plugin_row_meta_links'), 10, 2); + + //Utility actions. Modules can use them in their templates. + add_action('admin_menu_editor-display_tabs', array($this, 'display_editor_tabs')); + add_action('admin_menu_editor-display_header', array($this, 'display_settings_page_header')); + add_action('admin_menu_editor-display_footer', array($this, 'display_settings_page_footer')); + + } + + function init_finish() { + parent::init_finish(); + $should_save_options = false; + + //If we have no stored settings for this version of the plugin, try importing them + //from other versions (i.e. the free or the Pro version). + if ( !$this->load_options() ){ + $this->import_settings(); + $should_save_options = true; + } + $this->zlib_compression = $this->options['compress_custom_menu']; + + //Track first install time. + if ( !isset($this->options['first_install_time']) ) { + $this->options['first_install_time'] = time(); + $should_save_options = true; + } + + if ( $this->options['plugin_db_version'] < $this->plugin_db_version ) { + /* Put any activation code here. */ + + $this->options['plugin_db_version'] = $this->plugin_db_version; + $should_save_options = true; + } + + if ( $should_save_options ) { + //Skip saving options if the plugin hasn't been fully activated yet. + if ( $this->is_plugin_active($this->plugin_basename) ) { + $this->save_options(); + } else { + //Yes, this method can actually run before WP updates the list of active plugins. That means functions + //like is_plugin_active_for_network() will return false. As as result, we can't determine whether + //the plugin has been network-activated yet, so lets skip setting up the default config until + //the next page load. + } + } + + //This is here and not in init() because it relies on $options being initialized. + if ( $this->options['security_logging_enabled'] ) { + add_action('admin_notices', array($this, 'display_security_log')); + } + + //Compatibility fix for MailPoet 3. + $this->apply_mailpoet_compat_fix(); + + //bbPress role override. + if ( !empty($this->options['bbpress_override_enabled']) ) { + require_once __DIR__ . '/bbpress-role-override.php'; + new ameBBPressRoleOverride(); + } + + if ( did_action('plugins_loaded') ) { + $this->load_modules(); + } else { + add_action('plugins_loaded', array($this, 'load_modules'), 11); + } + } + + public function load_modules() { + //Load any active modules that haven't been loaded yet. + foreach($this->get_active_modules() as $id => $module) { + if ( array_key_exists($id, $this->loaded_modules) ) { + continue; + } + + /** @noinspection PhpIncludeInspection */ + include ($module['path']); + if ( !empty($module['className']) ) { + $instance = new $module['className']($this); + $this->loaded_modules[$id] = $instance; + } else { + $this->loaded_modules[$id] = true; + } + } + $this->are_modules_loaded = true; + + //Set up the tabs for the menu editor page. Many tabs are provided by modules. + $firstTabs = array('editor' => 'Admin Menu'); + if ( is_network_admin() ) { + //TODO: This could be in extras.php + $firstTabs = array('network-admin-menu' => 'Network Admin Menu'); + } + $this->tabs = apply_filters('admin_menu_editor-tabs', $firstTabs); + //The "Settings" tab is always last. + $this->tabs['settings'] = 'Settings'; + } + + /** + * @return ameModule[] + */ + public function get_loaded_modules() { + return $this->loaded_modules; + } + + /** + * Import settings from a different version of the plugin. + * + * @return bool True if settings were imported successfully, False otherwise + */ + function import_settings(){ + $possible_names = array('ws_menu_editor', 'ws_menu_editor_pro'); + foreach($possible_names as $option_name){ + if ( $this->load_options($option_name) ){ + return true; + } + } + return false; + } + + /** + * Create a configuration page and load the custom menu + * + * @return void + */ + function hook_admin_menu(){ + global $menu, $submenu; + + //Compatibility fix for Shopp 1.2.9. This plugin has an "admin_menu" hook (Flow::menu) that adds another + //"admin_menu" hook (AdminFlow::taxonomies) when it runs. Basically, it indirectly modifies the global + //$wp_filters['admin_menu'] array while WordPress is iterating it (nasty!). Due to how PHP arrays are + //implemented and how do_action() works, this second hook is the very last one to run, even after hooks + //with a lower priority. + //The only way we can see the changes made by the second hook is to do the same thing. + static $firstRunSkipped = false; + if ( !$firstRunSkipped && class_exists('Flow') ) { + add_action(current_filter(), array($this, 'hook_admin_menu'), $this->magic_hook_priority + 1); + $firstRunSkipped = true; + return; + } + + //Menu reset (for emergencies). Executed by accessing http://example.com/wp-admin/?reset_admin_menu=1 + $reset_requested = isset($this->get['reset_admin_menu']) && $this->get['reset_admin_menu']; + if ( $reset_requested && $this->current_user_can_edit_menu() ){ + $this->set_custom_menu(null); + } + + //The menu editor is only visible to users with the manage_options privilege. + //Or, if the plugin is installed in mu-plugins, only to the site administrator(s). + if ( $this->current_user_can_edit_menu() ){ + $this->log_security_note('Current user can edit the admin menu.'); + + //Determine the current menu editor page tab. + reset($this->tabs); + $this->current_tab = isset($this->get['sub_section']) ? strval($this->get['sub_section']) : key($this->tabs); + $tab_title = ''; + if ($this->current_tab !== 'editor' && isset($this->tabs[$this->current_tab])) { + $tab_title = ' - ' . $this->tabs[$this->current_tab]; + } + + $parent_slug = is_network_admin() ? 'settings.php' : 'options-general.php'; + + $page = add_submenu_page( + $parent_slug, + apply_filters('admin_menu_editor-self_page_title', 'Menu Editor') . $tab_title, + apply_filters('admin_menu_editor-self_menu_title', 'Menu Editor'), + apply_filters('admin_menu_editor-capability', 'manage_options'), + 'menu_editor', + array($this, 'page_menu_editor') + ); + //Output our JS & CSS on that page only + add_action("admin_print_scripts-$page", array($this, 'enqueue_scripts'), 1); + add_action("admin_print_styles-$page", array($this, 'enqueue_styles')); + + //Make sure Lodash doesn't conflict with the copy of Underscore that's bundled with WordPress. + add_filter('script_loader_tag', array($this, 'lodash_noconflict'), 10, 2); //Filter exists since WP 4.1. + + //Let modules do something when loading a specific tab but before output starts. + add_action('load-' . $page, array($this, 'trigger_tab_load_event')); + + //Notify modules that the menu item has been registered. + do_action('admin_menu_editor-editor_menu_registered'); + + //Compatibility fix for All In One Event Calendar; see the callback for details. + add_action("admin_print_scripts-$page", array($this, 'dequeue_ai1ec_scripts')); + + //Compatibility fix for Participants Database. + add_action("admin_print_scripts-$page", array($this, 'dequeue_pd_scripts')); + + //Experimental compatibility fix for Ultimate TinyMCE + add_action("admin_print_scripts-$page", array($this, 'remove_ultimate_tinymce_qtags')); + + //Make a placeholder for our screen options (hacky) + $screen_hook_name = $page; + if ( is_network_admin() ) { + $screen_hook_name .= '-network'; + } + if ( $this->current_tab === 'editor' ) { + add_meta_box("ws-ame-screen-options", "[AME placeholder]", '__return_false', $screen_hook_name); + } + } + + //Compatibility fix for the WooCommerce order count bubble. Must be run before storing or processing $submenu. + $this->apply_woocommerce_order_count_fix(); + + //Store the "original" menus for later use in the editor + $this->default_wp_menu = $menu; + $this->default_wp_submenu = $submenu; + + //Compatibility fix for bbPress. + $this->apply_bbpress_compat_fix(); + //Compatibility fix for WooCommerce (woo). + $this->apply_woocommerce_compat_fix(); + //Compatibility fix for WordPress Mu Domain Mapping. + $this->apply_wpmu_domain_mapping_fix(); + //Compatibility fix for Divi Training. + $this->apply_divi_training_fix(); + //As of WP 3.5, the "Links" menu is hidden by default. + if ( !current_user_can('manage_links') ) { + $this->remove_link_manager_menus(); + } + + //Generate item templates from the default menu. + $templateBuilder = new ameMenuTemplateBuilder(); + $this->item_templates = $templateBuilder->build( + $this->default_wp_menu, + $this->default_wp_submenu, + $this->menu_url_blacklist + ); + + //Store the default order for later. It will be used when (re)inserting unused items into the menu. + $this->relative_template_order = $templateBuilder->getRelativeTemplateOrder(); + + //Add extra templates that are not part of the normal menu. + $this->item_templates = $this->add_special_templates($this->item_templates); + //TODO: It would be nice to add the "Delete Site" item on multisite when on the main site. + + //Is there a custom menu to use? + $custom_menu = $this->load_custom_menu(); + if ( $custom_menu !== null ){ + //Merge in data from the default menu + $custom_menu['tree'] = $this->menu_merge($custom_menu['tree']); + + //Save the merged menu for later - the editor page will need it + $this->merged_custom_menu = $custom_menu; + + do_action('admin_menu_editor-menu_merged', $this->merged_custom_menu); + + //Convert our custom menu to the $menu + $submenu structure used by WP. + //Note: This method sets up multiple internal fields and may cause side-effects. + $this->user_cap_cache_enabled = true; + $this->build_custom_wp_menu($this->merged_custom_menu['tree']); + $this->user_cap_cache_enabled = false; + + do_action('admin_menu_editor-menu_built', $this->merged_custom_menu, $this); + + if ( $this->is_access_test ) { + $this->access_test_runner['wasCustomMenuApplied'] = true; + $this->access_test_runner->setCurrentMenuItem($this->get_current_menu_item()); + } + + if ( !$this->user_can_access_current_page() ) { + $this->log_security_note('DENY access.'); + if ( $this->is_access_test ) { + $this->access_test_runner['userCanAccessCurrentPage'] = false; + } + + $message = 'You do not have sufficient permissions to access this admin page.'; + + if ( ($this->options['error_verbosity'] >= self::VERBOSITY_NORMAL) ) { + $current_item = $this->get_current_menu_item(); + if ( isset($current_item, $current_item['access_decision_reason']) ) { + $message .= sprintf( + '<p>Reason: %s</p>', + htmlentities($current_item['access_decision_reason']) + ); + } + } + + if ($this->options['security_logging_enabled'] + || ($this->options['error_verbosity'] >= self::VERBOSITY_VERBOSE) + ) { + $message .= '<p><strong>Admin Menu Editor security log</strong></p>'; + $message .= $this->get_formatted_security_log(); + } + do_action('admin_page_access_denied'); + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- HTML should already be escaped as necessary. + wp_die($message); + } else { + $this->log_security_note('ALLOW access.'); + if ( $this->is_access_test ) { + $this->access_test_runner['userCanAccessCurrentPage'] = + ($this->access_test_runner['currentMenuItem'] !== null); + } + } + + //Replace the admin menu just before it is displayed and restore it afterwards. + //The fact that replace_wp_menu() is attached to the 'parent_file' hook is incidental; + //there just wasn't any other, more suitable hook available. + add_filter('parent_file', array($this, 'replace_wp_menu'), 1001); + add_action('adminmenu', array($this, 'restore_wp_menu')); + + //A compatibility hack for Ozh's Admin Drop Down Menu. Make sure it also sees the modified menu. + $ozh_adminmenu_priority = has_action('in_admin_header', 'wp_ozh_adminmenu'); + if ( $ozh_adminmenu_priority !== false ) { + add_action('in_admin_header', array($this, 'replace_wp_menu'), $ozh_adminmenu_priority - 1); + add_action('in_admin_header', array($this, 'restore_wp_menu'), $ozh_adminmenu_priority + 1); + } + } else { + do_action('admin_menu_editor-menu_replacement_skipped'); + } + + add_action( + 'admin_menu_editor-register_hideable_items', + array($this, 'register_hideable_items'), + 10, + 1 + ); + + add_filter( + 'admin_menu_editor-save_hideable_items-admin-menu', + array($this, 'save_hideable_items'), + 10, + 2 + ); + } + + /** + * Replace the current WP menu with our custom one. + * + * @param string $parent_file Ignored. Required because this method is a hook for the 'parent_file' filter. + * @return string Returns the $parent_file argument. + */ + public function replace_wp_menu($parent_file = '') { + global $menu, $submenu; + + $this->old_wp_menu = $menu; + $this->old_wp_submenu = $submenu; + + // phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited -- Overriding menus is the whole point of this plugin. + $menu = $this->custom_wp_menu; + $submenu = $this->custom_wp_submenu; + // phpcs:enable + + $this->user_cap_cache_enabled = true; + $this->filter_global_menu(); + $this->user_cap_cache_enabled = false; + + do_action('admin_menu_editor-menu_replaced'); + return $parent_file; + } + + /** + * Restore the default WordPress menu that was replaced using replace_wp_menu(). + * + * @return void + */ + public function restore_wp_menu() { + // phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited + global $menu, $submenu; + $menu = $this->old_wp_menu; + $submenu = $this->old_wp_submenu; + // phpcs:enable + } + + /** + * Filter a menu so that it can be handed to _wp_menu_output(). This method basically + * emulates the filtering that WordPress does in /wp-admin/includes/menu.php, with a few + * additions of our own. + * + * - Removes inaccessible items and superfluous separators. + * + * - Sets accessible items to a capability that the user is guaranteed to have to prevent + * _wp_menu_output() from choking on plugin-specific capabilities like "cap1,cap2+not:cap3". + * + * - Adds position-dependent CSS classes. + * + * @global array $menu + * @global array $submenu + * + * @return void + */ + private function filter_global_menu() { + // phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited + global $menu, $submenu; + global $_wp_menu_nopriv; //Caution: Modifying this array could lead to unexpected consequences. + + //Remove sub-menus which the user shouldn't be able to access, + //and ensure the rest are visible. + foreach ($submenu as $parent => $items) { + foreach ($items as $index => $data) { + if ( ! $this->current_user_can($data[1]) ) { + unset($submenu[$parent][$index]); + $_wp_submenu_nopriv[$parent][$data[2]] = true; + } else { + //The menu might be set to some kind of special capability that is only valid + //within this plugin and not WP in general. Ensure WP doesn't choke on it. + //(This is safe - we'll double-check the caps when the user tries to access a page.) + $submenu[$parent][$index][1] = 'exist'; //All users have the 'exist' cap. + } + } + + if ( empty($submenu[$parent]) ) { + unset($submenu[$parent]); + } + } + + //Remove consecutive submenu separators. This can happen if there are separators around a menu item + //that is not accessible to the current user. + foreach ($submenu as $parent => $items) { + $found_separator = false; + foreach ($items as $index => $item) { + //Separator have a dummy #anchor as a URL. See wsMenuEditorExtras::create_submenu_separator(). + if (strpos($item[2], '#submenu-separator-') === 0) { + if ( $found_separator ) { + unset($submenu[$parent][$index]); + } + $found_separator = true; + } else { + $found_separator = false; + } + } + } + + //Remove menus that have no accessible sub-menus and require privileges that the user does not have. + //Ensure the rest are visible. Run re-parent loop again. + foreach ( $menu as $id => $data ) { + if ( ! $this->current_user_can($data[1]) ) { + $_wp_menu_nopriv[$data[2]] = true; + } else { + $menu[$id][1] = 'exist'; + } + + //If there is only one submenu and it is has same destination as the parent, + //remove the submenu. + if ( ! empty( $submenu[$data[2]] ) && 1 == count ( $submenu[$data[2]] ) ) { + $subs = $submenu[$data[2]]; + $first_sub = array_shift($subs); + if ( $data[2] == $first_sub[2] ) { + unset( $submenu[$data[2]] ); + } + } + + //If submenu is empty... + if ( empty($submenu[$data[2]]) ) { + // And user doesn't have privs, remove menu. + if ( isset( $_wp_menu_nopriv[$data[2]] ) ) { + unset($menu[$id]); + } + } + } + unset($id, $data, $subs, $first_sub); + + //Remove any duplicated separators + $separator_found = false; + foreach ( $menu as $id => $data ) { + if ( 0 == strcmp('wp-menu-separator', $data[4] ) ) { + if ($separator_found) { + unset($menu[$id]); + } + $separator_found = true; + } else { + $separator_found = false; + } + } + unset($id, $data); + + //Remove the last menu item if it is a separator. + $last_menu_key = array_keys( $menu ); + $last_menu_key = array_pop( $last_menu_key ); + if (!empty($menu) && 'wp-menu-separator' == $menu[$last_menu_key][4]) { + unset($menu[$last_menu_key]); + } + unset( $last_menu_key ); + + //Add display-specific classes like "menu-top-first" and others. + $menu = add_menu_classes($menu); + // phpcs:enable + } + + public function register_base_dependencies() { + static $done = false; + if ( $done ) { + return; + } + $done = true; + + $this->register_jquery_plugins(); + + //Base styles. + wp_register_auto_versioned_style('menu-editor-base-style', plugins_url('css/menu-editor.css', $this->plugin_file)); + + //Lodash library + wp_register_auto_versioned_script('ame-lodash', plugins_url('js/lodash.min.js', $this->plugin_file)); + + //Knockout + wp_register_auto_versioned_script('knockout', plugins_url('js/knockout.js', $this->plugin_file)); + + //Actor manager. + wp_register_auto_versioned_script( + 'ame-actor-manager', + plugins_url('js/actor-manager.js', $this->plugin_file), + array('ame-lodash') + ); + + $roles = array(); + + $wp_roles = ameRoleUtils::get_roles(); + foreach($wp_roles->roles as $role_id => $role) { + //There is at least one plugin that creates a custom role without a "capabilities" key. + //We need to check for that to avoid an "undefined array key" warning. + if ( array_key_exists('capabilities', $role) ) { + //Some plugins use 1, 0, null, or other truthy/falsy values for capability settings. + //AME uses booleans. It helps avoid bugs and it's also what WordPress core does. + $role['capabilities'] = $this->castValuesToBool($role['capabilities']); + } else { + $role['capabilities'] = array(); + } + $roles[$role_id] = $role; + } + + //Known users. + $users = array(); + $current_user = wp_get_current_user(); + $logins_to_include = apply_filters('admin_menu_editor-users_to_load', array()); + + //Always include the current user. + $logins_to_include[] = $current_user->get('user_login'); + $logins_to_include = array_unique($logins_to_include); + + //Load user details. + foreach($logins_to_include as $login) { + $user = get_user_by('login', $login); + if ( !empty($user) ) { + $users[$login] = $this->user_to_property_map($user); + } + } + + //Compatibility workaround: Get the real roles of the current user even if other plugins corrupt the list. + $users[$current_user->get('user_login')]['roles'] = array_values($this->get_user_roles($current_user)); + + $suspected_meta_caps = $this->detect_meta_caps($roles, $users); + + //The current user has all of the meta caps. That's how we know they're meta caps and not just regular + //capabilities that simply haven't been granted to anyone. + $users[$current_user->get('user_login')]['meta_capabilities'] = $suspected_meta_caps; + + //TODO: Include currentUserLogin + $actor_data = array( + 'roles' => $roles, + 'users' => $users, + 'isMultisite' => is_multisite(), + 'capPower' => $this->load_cap_power(), + 'suspectedMetaCaps' => $suspected_meta_caps, + ); + wp_localize_script('ame-actor-manager', 'wsAmeActorData', $actor_data); + + //Modules + wp_register_auto_versioned_script( + 'ame-access-editor', + plugins_url('modules/access-editor/access-editor.js', $this->plugin_file), + array('jquery', 'ame-lodash') + ); + + //Let extras register their scripts. + do_action('admin_menu_editor-register_scripts'); + } + + /** + * @access private + * @param string[]|null $handles + */ + public function register_jquery_plugins($handles = null) { + if ( $handles === null ) { + $handles = array_keys(self::$jquery_plugins); + } + + foreach ($handles as $handle) { + if ( !isset(self::$jquery_plugins[$handle]) || !empty($this->registered_jquery_plugins[$handle]) ) { + continue; + } + + wp_register_auto_versioned_script( + $handle, + plugins_url(self::$jquery_plugins[$handle], $this->plugin_file), + array('jquery') + ); + $this->registered_jquery_plugins[$handle] = true; + } + } + + /** + * Detect meta capabilities. + * This only works if the current user is an admin. In Multisite, they must be a Super Admin. + * + * @param array $roles + * @param array $users + * @return array [capability => true] + */ + private function detect_meta_caps($roles, $users) { + if ( !$this->current_user_can_edit_menu() || !is_super_admin() ) { + return array(); + } + + //Any capability that's assigned to a role probably isn't a meta capability. + $allRealCaps = ameRoleUtils::get_all_capabilities(true); + //Similarly, capabilities that are directly assigned to users are probably real. + foreach($users as $user) { + $allRealCaps = $allRealCaps + $user['capabilities']; + } + //Role IDs can also be used as capabilities. + foreach($roles as $roleId => $role) { + $allRealCaps[$roleId] = true; + } + + //Collect all of the required capabilities from the admin menu. + $menu = $this->get_default_menu(); + ameMenu::for_each($menu['tree'], array($this, 'collect_menu_cap')); + + //Any capability that's part of the admin menu but not assigned to any role or user + //is probably a meta capability. + $suspectedMetaCaps = array_diff_key($this->caps_used_in_menu, $allRealCaps); + + //The current user is an admin and should have access to everything. If they don't have a cap, + //that's probably a non-meta cap that isn't enabled for *anyone*. + $suspectedMetaCaps = array_filter(array_keys($suspectedMetaCaps), 'current_user_can'); + + return array_fill_keys($suspectedMetaCaps, true); + } + + /** + * @access private + * @param array $item + */ + public function collect_menu_cap($item) { + if ( isset($item['defaults'], $item['defaults']['access_level']) ) { + $this->caps_used_in_menu[$item['defaults']['access_level']] = true; + } + } + + /** @noinspection PhpUnusedPrivateMethodInspection */ + /** + * Unfinished feature: Detect which roles have which meta capabilities. + * + * Create a temp. user for each role, test which meta caps they have, then cache the results in a site option. + * Put this part in an AJAX request to avoid a massive slowdown (takes several seconds even on a fast PC). + * + * @param array $suspected_meta_caps + * @param string[] $roleIds + * @return array + */ + private function analyse_role_meta_caps($suspected_meta_caps, $roleIds) { + //$start = microtime(true); + $results = array(); + $real_current_user = wp_get_current_user(); + + foreach($roleIds as $role_id) { + $id = wp_insert_user(array( + 'role' => $role_id, + 'user_login' => wp_slash('ametemp_' . wp_generate_password(14)), + 'user_pass' => wp_generate_password(20), + 'display_name' => 'Temporary user created by AME', + )); + $user = new WP_User($id); + + //Some plugins only check the current user and ignore the user ID passed to the "user_has_cap" filter. + //To account for cases like that, we need to also change the current user. + wp_set_current_user($user->ID); + + $results[$role_id] = array(); + foreach($suspected_meta_caps as $meta_cap => $ignored) { + $results[$role_id][$meta_cap] = $user->has_cap($meta_cap); + } + + wp_delete_user($id); + } + + //Restore the original user. + wp_set_current_user($real_current_user->ID); + + /*$elapsed = microtime(true) - $start; + printf('Meta cap analysis: %.2f ms<br>', $elapsed * 1000);*/ + return $results; + } + + /** + * Add the JS required by the editor to the page header + * + * @return void + */ + function enqueue_scripts() { + //Optimization: Remove wp-emoji.js from the plugin page. wpEmoji makes DOM manipulation slow because + //it tracks *all* DOM changes using MutationObserver. + remove_action('admin_print_scripts', 'print_emoji_detection_script'); + + //Workaround: Suppress a buggy "lets add a 'defer' attribute to all <script> tags" filter. + //It's been going around the web and breaking AME installations by producing invalid HTML. + remove_filter('clean_url', 'defer_parsing_of_js', 11); + + $this->register_base_dependencies(); + + //Tab utilities and fixes. + //This is a separate script because some of it has to run after common.js, which is loaded in the page footer. + wp_enqueue_auto_versioned_script( + 'ame-settings-tab-utils', + plugins_url('js/tab-utils.js', $this->plugin_file), + array('jquery', 'ame-lodash', 'common'), + true + ); + + //Editor's scripts + $editor_dependencies = array( + 'jquery', 'jquery-ui-sortable', 'jquery-ui-dialog', 'jquery-ui-tabs', + 'ame-jquery-form', 'jquery-ui-droppable', 'jquery-qtip', + 'jquery-sort', 'ame-jquery-cookie', + 'wp-color-picker', 'ame-lodash', 'ame-access-editor', 'ame-actor-manager', + 'ame-actor-selector', + ); + wp_register_auto_versioned_script( + 'menu-editor', + plugins_url('js/menu-editor.js', $this->plugin_file), + apply_filters('admin_menu_editor-editor_script_dependencies', $editor_dependencies) + ); + + do_action('admin_menu_editor-enqueue_scripts-' . $this->current_tab); + + //Actors (roles and users) are used in the permissions UI, so we need to pass them along. + //TODO: This is redundant. Consider using the actor manager or selector instead. + $actors = array(); + + $wp_roles = ameRoleUtils::get_roles(); + foreach($wp_roles->roles as $role_id => $role) { + $actors['role:' . $role_id] = $role['name']; + } + + if ( is_multisite() && is_super_admin() ) { + $actors['special:super_admin'] = 'Super Admin'; + } + + $current_user = wp_get_current_user(); + $actors['user:' . $current_user->get('user_login')] = sprintf( + 'Current user (%s)', + $current_user->get('user_login') + ); + + //Add only certain scripts to the settings sub-section. + if ( $this->is_settings_page() ) { + wp_enqueue_script('jquery-qtip'); + return; + } + + //Add all scripts to our editor page, but not the settings sub-section + //that shares the same page slug. Some of the scripts would crash otherwise. + if ( !$this->is_editor_page() ) { + return; + } + + wp_enqueue_script('menu-editor'); + + //We use WordPress media uploader to let the user upload custom menu icons (WP 3.5+). + if ( function_exists('wp_enqueue_media') ) { + wp_enqueue_media(); + } + + //Remove the default jQuery Form plugin to prevent conflicts with our custom version. + wp_dequeue_script('jquery-form'); + + //The editor will need access to some of the plugin data and WP data. + $script_data = array( + 'imagesUrl' => plugins_url('images', $this->plugin_file), + 'adminAjaxUrl' => admin_url('admin-ajax.php'), + 'hideAdvancedSettings' => (boolean)$this->options['hide_advanced_settings'], + 'showExtraIcons' => true, //No longer used. + 'submenuIconsEnabled' => $this->options['submenu_icons_enabled'], + + 'hideAdvancedSettingsNonce' => wp_create_nonce('ws_ame_save_screen_options'), + 'dashiconsAvailable' => wp_style_is('dashicons', 'registered'), + 'captionShowAdvanced' => 'Show advanced options', + 'captionHideAdvanced' => 'Hide advanced options', + 'wsMenuEditorPro' => $this->is_pro_version(), //Will be overwritten if extras are loaded + 'menuFormatName' => ameMenu::format_name, + 'menuFormatVersion' => ameMenu::format_version, + + 'blankMenuItem' => ameMenuItem::blank_menu(), + 'itemTemplates' => $this->item_templates, + 'customItemTemplate' => array( + 'name' => '< Custom URL >', + 'defaults' => ameMenuItem::custom_item_defaults(), + ), + + 'unclickableTemplateId' => ameMenuItem::unclickableTemplateId, + 'unclickableTemplateClass' => ameMenuItem::unclickableTemplateClass, + + 'embeddedPageTemplateId' => ameMenuItem::embeddedPageTemplateId, + + 'actors' => $actors, + 'currentUserLogin' => $current_user->get('user_login'), + 'selectedActor' => isset($this->get['selected_actor']) ? strval($this->get['selected_actor']) : null, + + 'postTypes' => $this->get_post_type_details(), + 'taxonomies' => $this->get_taxonomy_details(), + + 'showHints' => $this->get_hint_visibility(), + 'dashboardHidingConfirmationEnabled' => $this->options['dashboard_hiding_confirmation_enabled'], + 'disableDashboardConfirmationNonce' => wp_create_nonce('ws_ame_disable_dashboard_hiding_confirmation'), + + 'getPagesNonce' => wp_create_nonce('ws_ame_get_pages'), + 'getPageDetailsNonce' => wp_create_nonce('ws_ame_get_page_details'), + + 'selectedMenu' => isset($this->get['selected_menu_url']) ? strval($this->get['selected_menu_url']) : null, + 'selectedSubmenu' => isset($this->get['selected_submenu_url']) ? strval($this->get['selected_submenu_url']) : null, + 'expandSelectedMenu' => isset($this->get['expand_menu']) && ($this->get['expand_menu'] === '1'), + 'expandSelectedSubmenu' => isset($this->get['expand_submenu']) && ($this->get['expand_submenu'] === '1'), + + 'deepNestingEnabled' => $this->options['deep_nesting_enabled'], + + 'isDemoMode' => defined('IS_DEMO_MODE'), + 'isMasterMode' => defined('IS_MASTER_MODE'), + ); + $script_data = apply_filters('admin_menu_editor-script_data', $script_data); + wp_localize_script('menu-editor', 'wsEditorData', $script_data); + } + + /** + * Convert a WP_User instance to an associative array with the keys defined + * in the AmeUserPropertyMap interface in actor-manager.ts. + * + * @param WP_User $user + * @return array + */ + public function user_to_property_map($user) { + return array( + 'user_login' => $user->get('user_login'), + 'id' => $user->ID, + 'roles' => !empty($user->roles) ? array_values((array)($user->roles)) : array(), + 'capabilities' => $this->castValuesToBool($user->caps), + 'meta_capabilities' => array(), + 'display_name' => $user->display_name, + 'is_super_admin' => is_multisite() && is_super_admin($user->ID), + ); + } + + /** + * Move editor scripts closer to the top of the script queue. + * + * This reduces the chances that JavaScript bugs in other plugins will crash the menu editor. + * For example, if another plugin's script loads first and crashes in a $(document).ready() + * handler, the editor's $(document).ready() handler will never be run. This will make the UI + * unusable because the menu list will not render, etc. Loading our scripts first makes that + * less likely. + */ + public function move_editor_scripts_to_top() { + $wp_scripts = wp_scripts(); //Requires WordPress 4.2.0+ + + //Sanity check. If the wp_scripts implementation has changed significantly, don't touch it. + if ( !isset($wp_scripts->queue) || (!is_array($wp_scripts->queue) || ($wp_scripts->queue instanceof Traversable)) ) { + return; + } + + //We want to load our scripts *after* WordPress core scripts in case we depend on some core feature. + $common_key = array_search('common', $wp_scripts->queue); + $admin_bar_key = array_search('admin-bar', $wp_scripts->queue); + if ( ($common_key === false) && ($admin_bar_key === false) ) { + return; + } + $last_core_key = max($admin_bar_key, $common_key); + + //Move only those scripts that are actually in the queue. + $handles_to_move = array(); + foreach(array('menu-editor', 'ame-helper-script') as $handle) { + $key = array_search($handle, $wp_scripts->queue); + if ($key !== false) { + $handles_to_move[] = $handle; + unset($wp_scripts->queue[$key]); //Remove the script from its old position. + } + } + + //Insert the scripts after core script(s). + array_splice($wp_scripts->queue, $last_core_key + 1, 0, $handles_to_move); + } + + /** + * Revert the "_" variable to its original value and store Lodash in "wsAmeLodash" instead. + * + * @param string $tag + * @param string $script_handle + * @return string + */ + public function lodash_noconflict($tag, $script_handle) { + if ($script_handle === 'ame-lodash') { + $tag .= '<script type="text/javascript">wsAmeLodash = _.noConflict();</script>'; + } + return $tag; + } + + /** + * Compatibility workaround for All In One Event Calendar 1.8.3-premium. + * + * The event calendar plugin is known to crash Admin Menu Editor Pro 1.40. The exact cause + * of the crash is unknown, but we can prevent it by removing AIOEC scripts from the menu + * editor page. + * + * This should not affect the functionality of the event calendar plugin. The scripts + * in question don't seem to do anything on pages not related to the event calendar. AIOEC + * just loads them indiscriminately on all pages. + */ + public function dequeue_ai1ec_scripts() { + wp_dequeue_script('ai1ec_requirejs'); + wp_dequeue_script('ai1ec_common_backend'); + wp_dequeue_script('ai1ec_add_new_event_require'); + } + + /** + * Compatibility workaround for Participants Database 1.4.5.2. + * + * Participants Database loads its settings JavaScript on every page in the "Settings" menu, + * not just its own. It doesn't bother to also load the script's dependencies, though, so + * the script crashes *and* it breaks the menu editor by way of collateral damage. + * + * Fix by forcibly removing the offending script from the queue. + */ + public function dequeue_pd_scripts() { + if ( is_plugin_active('participants-database/participants-database.php') ) { + wp_dequeue_script('settings_script'); + } + } + + public function remove_ultimate_tinymce_qtags() { + remove_action('admin_print_footer_scripts', 'jwl_ult_quicktags'); + } + + /** + * Add the editor's CSS file to the page header + * + * @return void + */ + function enqueue_styles(){ + wp_enqueue_auto_versioned_style('jquery-qtip-syle', plugins_url('css/jquery.qtip.min.css', $this->plugin_file), array()); + + wp_register_auto_versioned_style( + 'menu-editor-colours-classic', + plugins_url('css/style-classic.css', $this->plugin_file), + array('menu-editor-base-style') + ); + wp_register_auto_versioned_style( + 'menu-editor-colours-wp-grey', + plugins_url('css/style-wp-grey.css', $this->plugin_file), + array('menu-editor-base-style') + ); + wp_register_auto_versioned_style( + 'menu-editor-colours-modern-one', + plugins_url('css/style-modern-one.css', $this->plugin_file), + array('menu-editor-base-style') + ); + + //WordPress introduced a new screen meta button style in WP 3.8. + //We have two different stylesheets - one for 3.8+ and one for backwards compatibility. + wp_register_auto_versioned_style('menu-editor-screen-meta', plugins_url('css/screen-meta.css', $this->plugin_file)); + wp_register_auto_versioned_style('menu-editor-screen-meta-old', plugins_url('css/screen-meta-old-wp.css', $this->plugin_file)); + + if ( isset($GLOBALS['wp_version']) && version_compare($GLOBALS['wp_version'], '3.8-RC1', '<') ) { + wp_enqueue_style('menu-editor-screen-meta-old'); + } else { + wp_enqueue_style('menu-editor-screen-meta'); + } + + $scheme = $this->options['ui_colour_scheme']; + wp_enqueue_style('menu-editor-colours-' . $scheme); + wp_enqueue_style('wp-color-picker'); + + do_action('admin_menu_editor-enqueue_styles-' . $this->current_tab); + } + + /** + * Set and save a new custom menu for the current site. + * + * @param array|null $custom_menu + * @param string|null $config_id Supported values: 'network-admin', 'global' or 'site' + * @return bool True if the database entry was updated, false if not. + */ + function set_custom_menu($custom_menu, $config_id = null) { + if ( $config_id === null ) { + $config_id = $this->guess_menu_config_id(); + } + + $custom_menu = apply_filters('ame_pre_set_custom_menu', $custom_menu); + + $previous_custom_menu = $this->load_custom_menu($config_id); + if ( !empty($this->options['wpml_support_enabled']) ) { + $this->update_wpml_strings($previous_custom_menu, $custom_menu); + } + + if ( !empty($custom_menu) ) { + $custom_menu['prebuilt_virtual_caps'] = $this->build_virtual_capability_list($custom_menu); + } + + if ( !empty($custom_menu) && $this->options['compress_custom_menu'] ) { + $custom_menu = ameMenu::compress($custom_menu); + } + + if ($config_id === 'site') { + $site_specific_options = get_option($this->option_name); + if ( !is_array($site_specific_options) ) { + $site_specific_options = array(); + } + $site_specific_options['custom_menu'] = $custom_menu; + $updated = update_option($this->option_name, $site_specific_options); + } else if ($config_id === 'global') { + $this->options['custom_menu'] = $custom_menu; + $updated = $this->save_options(); + } else if ($config_id === 'network-admin' ) { + $this->options['custom_network_menu'] = $custom_menu; + $updated = $this->save_options(); + } else { + throw new LogicException(sprintf('Invalid menu configuration ID: "%s"', $config_id)); + } + + $this->loaded_menu_config_id = null; + $this->cached_custom_menu = null; + $this->cached_virtual_caps = null; + $this->cached_user_caps = array(); + + return $updated; + } + + /** + * Load the current custom menu for this site, if any. + * + * @param null $config_id + * @return array|null Either a menu in the internal format, or NULL if there is no custom menu available. + */ + public function load_custom_menu($config_id = null) { + if ( $config_id === null ) { + $config_id = $this->guess_menu_config_id(); + } + + if ( ($this->cached_custom_menu !== null) && ($this->loaded_menu_config_id === $config_id) ) { + return $this->cached_custom_menu; + } + + //Modules may include custom hooks that change how menu settings are loaded, so we need to load active modules + //before we load the menu configuration. Usually that happens automatically, but there are some plugins that + //trigger AME filters that need menu data before modules would normally be loaded. + if ( !$this->are_modules_loaded ) { + $this->load_modules(); + } + + $this->loaded_menu_config_id = $config_id; + + if ( $this->is_access_test ) { + return $this->test_menu; + } + + try { + if ( $config_id === 'network-admin' ) { + if ( empty($this->options['custom_network_menu']) ) { + return null; + } + $this->cached_custom_menu = ameMenu::load_array($this->options['custom_network_menu']); + } else if ( $config_id === 'site' ) { + $site_specific_options = get_option($this->option_name, null); + if ( is_array($site_specific_options) && isset($site_specific_options['custom_menu']) ) { + $this->cached_custom_menu = ameMenu::load_array($site_specific_options['custom_menu']); + } + } else { + if ( empty($this->options['custom_menu']) ) { + return null; + } + $this->cached_custom_menu = ameMenu::load_array($this->options['custom_menu']); + } + } catch (InvalidMenuException $exception) { + if ( is_admin() && is_user_logged_in() && !did_action('all_admin_notices') ) { + add_action('all_admin_notices', array($this, 'show_config_corruption_error')); + $this->last_menu_exception = $exception; + } + return null; + } + + return $this->cached_custom_menu; + } + + /** + * Display a notice about the exception that was thrown when loading the menu configuration. + */ + public function show_config_corruption_error() { + if ( !$this->current_user_can_edit_menu() || is_null($this->last_menu_exception) ) { + return; + } + printf( + '<div class="notice notice-error"><p>%s</p></div>', + '<strong>Admin Menu Editor encountered an error while trying to load the menu configuration!</strong><br> ' + . esc_html($this->last_menu_exception->getMessage()) + ); + } + + private function guess_menu_config_id() { + if ( is_network_admin() ) { + return 'network-admin'; + } elseif ( $this->should_use_site_specific_menu() ) { + return 'site'; + } else { + return 'global'; + } + } + + /** + * @return string|null + */ + public function get_loaded_menu_config_id() { + return $this->loaded_menu_config_id; + } + + /** + * Determine if we should use a site-specific admin menu configuration + * for the current site, or fall back to the global config. + * + * @return bool True = use the site-specific config (if any), false = use the global config. + */ + protected function should_use_site_specific_menu() { + if ( !is_multisite() ) { + //If this is a single-site WP installation then there's really + //no difference between "site-specific" and "global". + return false; + } + return ($this->options['menu_config_scope'] === 'site'); + } + + function save_options() { + if ( $this->is_access_test ) { + //Don't change live settings during an access test. + return false; + } + return parent::save_options(); + } + + /** + * Determine if the current user may use the menu editor. + * + * @return bool + */ + public function current_user_can_edit_menu(){ + $access = $this->options['plugin_access']; + + if ( $access === 'super_admin' ) { + return is_super_admin(); + } else if ( $access === 'specific_user' ) { + return get_current_user_id() == $this->options['allowed_user_id']; + } else { + $capability = apply_filters('admin_menu_editor-capability', $access); + return current_user_can($capability); + } + } + + /** + * Determine if a specific user can access the menu editor. + * + * @param int $userId + * @return bool + */ + public function user_can_edit_menu($userId) { + $access = $this->options['plugin_access']; + if ( $access === 'super_admin' ) { + return is_super_admin($userId); + } else if ( $access === 'specific_user' ) { + return $userId == $this->options['allowed_user_id']; + } else { + $capability = apply_filters('admin_menu_editor-capability', $access); + $user = get_user_by('id', $userId); + if ( !$user ) { + return false; + } + return $user->has_cap($capability); + } + } + + /** + * Reset plugin access if the only allowed user no longer exists. + * + * Some people use security plugins like iThemes Security to replace the default admin account + * with a new one or change the user ID. This can be a problem when AME is configured to allow + * only one user to edit the admin menu. Deleting that user ID makes the plugin inaccessible. + * As a workaround, allow any admin if the configured user is missing. + * + * @internal + * @param string $login + * @param WP_User $current_user + */ + public function maybe_reset_plugin_access(/** @noinspection PhpUnusedParameterInspection */ $login = null, $current_user = null) { + if ( ($this->options['plugin_access'] !== 'specific_user') || !$current_user || !$current_user->exists() ) { + return; + } + + //For performance, only run this check when an admin logs in. + //Note that current_user_can() and friends don't work at this point in the login flow. + $current_user_is_admin = is_multisite() + ? is_super_admin($current_user->ID) + : $current_user->has_cap('manage_options'); + + if ( !$current_user_is_admin ) { + return; + } + + $allowed_user = get_user_by('id', $this->options['allowed_user_id']); + if ( !$allowed_user || !$allowed_user->exists() ) { + //The allowed user no longer exists. Allow any administrator to use the plugin. + $this->options['plugin_access'] = 'manage_options'; + $this->save_options(); + } + } + + /** + * Apply the custom page title, if any. + * + * This is a callback for the "admin_title" filter. It will change the browser window/tab + * title (i.e. <title>), but not the title displayed on the admin page itself. + * + * @param string $admin_title The current admin title (full). + * @param string $title The current page title. + * @return string New admin title. + */ + function hook_admin_title($admin_title, $title){ + $item = $this->get_current_menu_item(); + if ( $item === null ) { + return $admin_title; + } + + $custom_title = null; + + //Check if the we have a custom title for this page. + $default_title = isset($item['defaults']['page_title']) ? $item['defaults']['page_title'] : ''; + if ( !empty($item['page_title']) && $item['page_title'] != $default_title ) { + $custom_title = $item['page_title']; + } + + //Alternatively, use the custom menu title if the default page title is empty (as is usually + //the case with core menus) or matches the default menu title (which is typical for plugins). + //This saves the user a little bit of time, and, presumably, they'd want the titles to match. + $default_menu_title = isset($item['defaults']['menu_title']) ? $item['defaults']['menu_title'] : ''; + if ( + !isset($custom_title) + && !empty($item['menu_title']) + && ($item['menu_title'] !== $default_menu_title) + && (($default_menu_title === $default_title) || ($default_title === '')) + ) { + $custom_title = wp_strip_all_tags($item['menu_title']); + } + + if ( isset($custom_title) ) { + if ( empty($title) ) { + $admin_title = $custom_title . $admin_title; + } else { + //Replace the first occurrence of the default title with the custom one. + $title_pos = strpos($admin_title, $title); + $admin_title = substr_replace($admin_title, $custom_title, $title_pos, strlen($title)); + } + } + + return $admin_title; + } + + /** + * Generate special menu templates and add them to the input template list. + * + * @param array $templates Template list. + * @return array Modified template list. + */ + private function add_special_templates($templates) { + //Add a special template for unclickable menu items. These can be used as headers and such. + $itemDefaults = ameMenuItem::custom_item_defaults(); + $unclickableDefaults = array_merge( + $itemDefaults, + array( + 'file' => '#' . ameMenuItem::unclickableTemplateClass, + 'url' => '#' . ameMenuItem::unclickableTemplateClass, + 'css_class' => $itemDefaults['css_class'] . ' ' . ameMenuItem::unclickableTemplateClass, + 'menu_title' => 'Unclickable Menu', + ) + ); + $templates[ameMenuItem::unclickableTemplateId] = array( + 'name' => '< None >', + 'used' => true, + 'defaults' => $unclickableDefaults, + ); + + if ( $this->is_pro_version() ) { + $templates[ameMenuItem::embeddedPageTemplateId] = array( + 'name' => '< Embed WP page >', + 'used' => true, + 'defaults' => array_merge( + $itemDefaults, + array( + 'file' => '#automatically-generated', + 'url' => '#automatically-generated', + 'menu_title' => 'Embedded Page', + 'page_heading' => ameMenuItem::embeddedPagePlaceholderHeading, + ) + ) + ); + + //The Pro version has a [wp-logout-url] shortcode. Lets make it easier o use + //by adding it to the "Target page" dropdown. + $logoutDefaults = array_merge( + ameMenuItem::basic_defaults(), + array( + 'menu_title' => 'Logout', + 'file' => '[wp-logout-url]', + 'url' => '[wp-logout-url]', + 'icon_url' => 'dashicons-migrate', + ) + ); + $templates['>logout'] = array( + 'name' => 'Logout', + 'used' => true, + 'defaults' => $logoutDefaults, + ); + } + + return $templates; + } + + /** + * Merge a custom menu with the current default WordPress menu. Adds/replaces defaults, + * inserts new items and removes missing items. + * + * @uses self::$item_templates + * + * @param array $tree A menu in plugin's internal form + * @return array Updated menu tree + */ + function menu_merge($tree){ + //Iterate over all menus and submenus and look up default values + //Also flag used and missing items. + $orphans = array(); + + //Build an index of menu positions so that we can quickly pick the right position for new/unused items. + $positions_by_template = array(); + $following_separator_position = array(); + $previous_default_top_menu = null; + + foreach ($tree as &$topmenu){ + + if ( !empty($topmenu['separator']) && isset($previous_default_top_menu) ) { + $following_separator_position[$previous_default_top_menu] = ameMenuItem::get($topmenu, 'position', 0); + } + $previous_default_top_menu = null; + + if ( !ameMenuItem::get($topmenu, 'custom') ) { + $template_id = ameMenuItem::template_id($topmenu); + //Is this menu present in the default WP menu? + if (isset($this->item_templates[$template_id])){ + //Yes, load defaults from that item + $topmenu['defaults'] = $this->item_templates[$template_id]['defaults']; + //Note that the original item was used + $this->item_templates[$template_id]['used'] = true; + //Add valid, non-custom items to the position index. + $positions_by_template[$template_id] = ameMenuItem::get($topmenu, 'position', 0); + $previous_default_top_menu = $template_id; + } else { + //Record the menu as missing, unless it's a menu separator + if ( empty($topmenu['separator']) ){ + $topmenu['missing'] = true; + + $temp = ameMenuItem::apply_defaults($topmenu); + $temp = $this->set_final_menu_capability($temp); + $this->add_access_lookup($temp, 'menu', true); + } + //Don't add missing menus to the index because they won't show up anyway. + } + } + + if (!empty($topmenu['items'])) { + //Iterate over submenu items + $this->merge_children($topmenu, $positions_by_template, $orphans); + } + } + + //If we don't unset these they will fuck up the next two loops where the same names are used. + unset($topmenu); + + //Now we have some items marked as missing, and some items in lookup arrays + //that are not marked as used. Lets remove the missing items from the tree. + $tree = ameMenu::remove_missing_items($tree); + //TODO: What would happen if we kept missing items? + + //Lets merge in the unused items. + $max_menu_position = !empty($positions_by_template) ? max($positions_by_template) : 100; + $new_grant_access = $this->get_new_menu_grant_access(); + foreach ($this->item_templates as $template_id => $template){ + //Skip used menus and separators + if ( !empty($template['used']) || !empty($template['defaults']['separator'])) { + continue; + } + + //Found an unused item. Build the tree entry. + $entry = ameMenuItem::blank_menu(); + $entry['template_id'] = $template_id; + $entry['defaults'] = $template['defaults']; + $entry['unused'] = true; //Note that this item is unused + + $entry['grant_access'] = $new_grant_access; + + if ($this->options['unused_item_position'] === 'relative') { + + //Attempt to maintain relative menu order. + $previous_item = $was_separated = null; + if ( isset($this->relative_template_order[$template_id]) ) { + $previous_item = $this->relative_template_order[$template_id]['previous_item']; + $was_separated = $this->relative_template_order[$template_id]['was_previous_item_separated']; + } + + if ( isset($previous_item, $positions_by_template[$previous_item]) ) { + if ( $was_separated && isset($following_separator_position[$previous_item]) ) { + //Desired order: previous item -> separator -> this item. + $entry['position'] = $following_separator_position[$previous_item]; + } else { + //Desired order: previous item -> this item. + $entry['position'] = $positions_by_template[$previous_item]; + if ( isset($following_separator_position[$previous_item]) ) { + //Now the separator is after this item, not the previous one. + $following_separator_position[$template_id] = $following_separator_position[$previous_item]; + unset($following_separator_position[$previous_item]); + } + } + $entry['position'] = strval(floatval($entry['position']) + 0.01); + } else if ( $previous_item === '' ) { + //Empty string = this was originally the first item. + $entry['position'] = -1; + } else { + //Previous item is unknown or doesn't exist. Leave this item in its current, incorrect position. + } + + } else { + //Move unused entries to the bottom. + $max_menu_position = $max_menu_position + 1; + $entry['position'] = $max_menu_position; + } + $positions_by_template[$template_id] = ameMenuItem::get($entry, 'position', 0); + + //Add the new entry to the menu tree + if ( isset($template['defaults']['parent']) ) { + if ( isset($tree[$template['defaults']['parent']]) ) { + //Okay, insert the item. + $tree[$template['defaults']['parent']]['items'][] = $entry; + } else { + //This can happen if the original parent menu has been moved to a submenu. + $tree[$template['defaults']['file']] = $entry; + } + } else { + $tree[$template['defaults']['file']] = $entry; + } + } + + //Move orphaned items back to their original parents. + foreach($orphans as $item) { + $defaultParent = $item['defaults']['parent']; + //TODO: Apparently 'parent' might not exist in some configurations. Unknown bug. + if ( isset($defaultParent) && isset($tree[$defaultParent]) ) { + $tree[$defaultParent]['items'][] = $item; + } else { + //This can happen if the parent has been moved to a submenu. + //Just put the orphan at the bottom of the menu. + $tree[$item['defaults']['file']] = $item; + } + } + + //Resort the tree to ensure the found items are in the right spots + $tree = ameMenu::sort_menu_tree($tree); + + //Order data is no longer necessary. + $this->relative_template_order = null; + + return $tree; + } + + /** + * Merge the children of a menu item with the default values from the WordPress menu. + * This section was extracted to a method just to make it possible to call it recursively. + * + * @param array $menu + * @param array $positions_by_template + * @param array $orphans + */ + private function merge_children(&$menu, &$positions_by_template, &$orphans) { + foreach ($menu['items'] as &$item){ + if ( !ameMenuItem::get($item, 'custom') ) { + $template_id = ameMenuItem::template_id($item); + + //Is this item present in the default WP menu? + if (isset($this->item_templates[$template_id])){ + //Yes, load defaults from that item + $item['defaults'] = $this->item_templates[$template_id]['defaults']; + $this->item_templates[$template_id]['used'] = true; + //Add valid, non-custom items to the position index. + $positions_by_template[$template_id] = ameMenuItem::get($item, 'position', 0); + //We must move orphaned items elsewhere. Use the default location if possible. + if ( isset($menu['missing']) && $menu['missing'] ) { + $orphans[] = $item; + } + } else if ( empty($item['separator']) ) { + //Record as missing, unless it's a menu separator + $item['missing'] = true; + + $temp = ameMenuItem::apply_defaults($item); + $temp = $this->set_final_menu_capability($temp); + $this->add_access_lookup($temp, 'submenu', true); + } + } else { + //What if the parent of this custom item is missing? + //Right now the custom item will just disappear. + } + + if ( !empty($item['items']) ) { + //Recursively merge children of submenu items. + $this->merge_children($item, $positions_by_template, $orphans); + } + } + } + + /** + * Add a page and its required capability to the page access lookup. + * + * The lookup array is indexed by priority. Priorities (highest to lowest): + * - Has custom permissions and a known template. + * - Has custom permissions, template missing or can't be determined correctly. + * - Default permissions. + * - Everything else. + * Additionally, submenu items have slightly higher priority that top level menus. + * The desired end result is for menu items with custom permissions to override + * default menus. + * + * Note to self: If we were to keep items with an unknown template instead of throwing + * them away during the merge phase, we could simplify this considerably. + * + * @param array $item Menu item (with defaults already applied). + * @param string $item_type 'menu' or 'submenu'. + * @param bool $missing Whether the item template is missing or unknown. + */ + private function add_access_lookup($item, $item_type = 'menu', $missing = false) { + if ( empty($item['url']) ) { + return; + } + + $has_custom_settings = !empty($item['grant_access']) || !empty($item['extra_capability']); + $priority = 6; + if ( $missing ) { + if ( $has_custom_settings ) { + $priority = 4; + } else { + return; //Don't even consider missing menus without custom access settings. + } + } else if ( $has_custom_settings ) { + $priority = 2; + } + + if ( $item_type == 'submenu' ) { + $priority--; + } + + //TODO: Include more details like menu title and template ID for debugging purposes (log output). + $this->page_access_lookup[$item['url']][$priority] = $item['access_level']; + } + + /** + * Get the access settings for menu items that are not part of the saved menu configuration. + * + * Typically, this applies to new menus that were added by recently activated plugins. + * + * @return array + */ + public function get_new_menu_grant_access() { + if ( $this->options['unused_item_permissions'] === 'unchanged' ) { + return array(); + } + return apply_filters('admin_menu_editor-new_menu_grant_access', array()); + } + + /** + * Generate WP-compatible $menu and $submenu arrays from a custom menu tree. + * + * Side-effects: This function executes several filters that may modify global state. + * Specifically, IFrame-handling callbacks in 'extras.php' will add add new hooks + * and other menu-related structures. + * + * @uses WPMenuEditor::$custom_wp_menu Stores the generated top-level menu here. + * @uses WPMenuEditor::$custom_wp_submenu Stores the generated sub-menu here. + * + * @uses WPMenuEditor::$title_lookups Generates a lookup list of page titles. + * @uses WPMenuEditor::$reverse_item_lookup Generates a lookup list of url => menu item relationships. + * + * @param array $tree The new menu, in the internal tree format. + * @return void + */ + function build_custom_wp_menu($tree){ + $new_tree = array(); + $new_menu = array(); + $new_submenu = array(); + $this->title_lookups = array(); + $this->custom_menu_is_deep = false; + + //Prepare the top menu + $first_nonseparator_found = false; + foreach ($tree as $topmenu){ + + //Skip leading menu separators. Fixes a superfluous separator showing up + //in WP 3.0 (multisite mode) when there's a custom menu and the current user + //can't access its first item ("Super Admin"). + if ( !empty($topmenu['separator']) && !$first_nonseparator_found ) { + continue; + } + $first_nonseparator_found = true; + + $topmenu = $this->prepare_for_output($topmenu, 'menu'); + + if ( empty($topmenu['separator']) ) { + $this->title_lookups[$topmenu['file']] = !empty($topmenu['page_title']) ? $topmenu['page_title'] : $topmenu['menu_title']; + } + + //Prepare the submenu of this menu + $topmenu['items'] = $this->prepare_children_for_output($topmenu); + $new_tree[] = $topmenu; + } + + //Sort the menu by position + uasort($new_tree, 'ameMenuItem::compare_position'); + + //Use only the highest-priority capability for each URL. + foreach($this->page_access_lookup as $url => $capabilities) { + ksort($capabilities); + $this->page_access_lookup[$url] = reset($capabilities); + } + + if ( $this->is_access_test ) { + $this->access_test_runner->onFinalTreeReady($new_tree); + } + + //Convert the prepared tree to the internal WordPress format. + foreach($new_tree as $topmenu) { + $this->build_top_level_item($topmenu, $new_menu, $new_submenu); + } + + $this->custom_wp_menu = $new_menu; + $this->custom_wp_submenu = $new_submenu; + } + + /** + * Prepare all the children (i.e. submenu items) of a menu for output. + * + * @param array $menu A menu item. + * @param null|bool $is_deep + * @return array + */ + private function prepare_children_for_output($menu, $is_deep = null) { + if ( empty($menu['items']) ) { + return array(); + } + + $new_items = array(); + + foreach ($menu['items'] as $item) { + $item = $this->prepare_for_output($item, 'submenu', $menu, ($is_deep === true)); + + //Make a note of the page's correct title so we can fix it later if necessary. + $this->title_lookups[$item['file']] = !empty($item['page_title']) ? $item['page_title'] : $item['menu_title']; + + if ( !empty($item['items']) ) { + $item['items'] = $this->prepare_children_for_output($item, true); + } + + $new_items[] = $item; + } + + //Sort by position + usort($new_items, 'ameMenuItem::compare_position'); + + return $new_items; + } + + /** + * Convert one top level menu and all of its submenu items to the WP menu format. + * + * @param array $topmenu A menu item. + * @param array $menu Top level menu list. The converted item will be added to this list. + * @param array $submenu Submenu list. The converted submenus (if any) will be added to this list. + */ + private function build_top_level_item($topmenu, &$menu, &$submenu) { + $trueAccess = isset($this->page_access_lookup[$topmenu['url']]) ? $this->page_access_lookup[$topmenu['url']] : null; + if ( ($trueAccess === 'do_not_allow') && ($topmenu['access_level'] !== $trueAccess) ) { + $topmenu['access_level'] = $trueAccess; + $reason = sprintf( + 'There is a hidden menu item with the same URL (%1$s) but a higher priority.', + $topmenu['url'] + ); + $item['access_decision_reason'] = $reason; + + if ( isset($topmenu['access_check_log']) ) { + $topmenu['access_check_log'][] = sprintf( + '+ Override: %1$s Setting the capability to "%2$s".', + $reason, + $trueAccess + ); + $topmenu['access_check_log'][] = str_repeat('=', 79); + } + } + + if ( !isset($this->reverse_item_lookup[$topmenu['url']]) ) { //Prefer sub-menus. + if ( $this->is_item_visitable($topmenu) ) { + $this->reverse_item_lookup[$topmenu['url']] = $topmenu; + } + } + + $has_submenu_icons = false; + foreach($topmenu['items'] as $item) { + $trueAccess = isset($this->page_access_lookup[$item['url']]) ? $this->page_access_lookup[$item['url']] : null; + if ( ($trueAccess === 'do_not_allow') && ($item['access_level'] !== $trueAccess) ) { + $item['access_level'] = $trueAccess; + $reason = sprintf( + 'There is a hidden menu item with the same URL (%1$s) but a higher priority.', + $item['url'] + ); + $item['access_decision_reason'] = $reason; + + if ( isset($item['access_check_log']) ) { + $item['access_check_log'][] = sprintf( + '+ Override: %1$s Setting the capability to "%2$s".', + $reason, + $trueAccess + ); + $item['access_check_log'][] = str_repeat('=', 79); + } + } + + if ( $this->is_item_visitable($item) ) { + $this->reverse_item_lookup[$item['url']] = $item; + } + + //Skip missing and hidden items + if ( !empty($item['missing']) || !empty($item['hidden']) ) { + continue; + } + + //Keep track of which menus have items with icons. Ignore hidden items. + $has_submenu_icons = $has_submenu_icons + || (!empty($item['has_submenu_icon']) && $item['access_level'] !== 'do_not_allow'); + + if ( !empty($item['items']) ) { + $this->build_nested_submenu($item, $menu, $submenu); + } + + $submenu[$topmenu['file']][] = $this->convert_to_wp_format($item); + } + + //Skip missing and hidden menus. + if ( !empty($topmenu['missing']) || !empty($topmenu['hidden']) ) { + return; + } + + //The ame-has-submenu-icons class lets us change the appearance of all submenu items at once, + //without having to add classes/styles to each item individually. + if ( $has_submenu_icons && (strpos($topmenu['css_class'], 'ame-has-submenu-icons') === false) ) { + $topmenu['css_class'] .= ' ame-has-submenu-icons'; + } + + $menu[] = $this->convert_to_wp_format($topmenu); + } + + /** + * Generate WP-compatible menu items for deeply nested submenus - that is, third level and beyond. + * + * @param array $item + * @param array $wpMenu + * @param array $wpSubmenu + */ + private function build_nested_submenu(&$item, &$wpMenu, &$wpSubmenu) { + static $uniquePrefix = null, $submenuCounter = 0; + if ( empty($item['items']) ) { + return; + } + + $this->custom_menu_is_deep = true; + + if ( $uniquePrefix === null ) { + $uniquePrefix = (string) wp_rand(1000, 9999); + } + + $submenuCounter++; + $uniqueClass = 'ame-ds-m' . $uniquePrefix . $submenuCounter; + $submenuClass = 'ame-ds-child-of-' . $uniqueClass; + + //Flag the parent item as having a submenu. + $item['css_class'] .= ' ame-has-deep-submenu ' . $uniqueClass; + + //Output the submenu itself as a separate top level menu. The Pro version will then use JS to move it + //to the right place in the DOM and make it work like a nested submenu. The free version doesn't have + //that feature, but the menu will still be usable. + $containerTopLevelMenu = array_merge( + $item, + array( + 'css_class' => 'menu-top ' . $submenuClass, + 'icon_url' => 'dashicons-menu', + + //To avoid submenu key collisions and ID clashes, let's give each menu a unique slug/URL. + 'file' => '#ame-uds-p' . $submenuCounter . '-' . $item['file'], + ) + ); + + $this->build_top_level_item($containerTopLevelMenu, $wpMenu, $wpSubmenu); + } + + /** + * Convert a menu item from the internal format used by this plugin to the format + * used by WP. The menu should be prepared using the prepare... function beforehand. + * + * @see self::prepare_for_output() + * + * @param array $item + * @return array + */ + private function convert_to_wp_format($item) { + //Build the menu structure that WP expects + $wp_item = array( + $item['menu_title'], + $item['access_level'], + $item['file'], + $item['page_title'], + $item['css_class'], + $item['hookname'], //ID + isset($item['wp_icon_url']) ? $item['wp_icon_url'] : $item['icon_url'], + ); + + return $wp_item; + } + + /** + * Prepare a menu item to be converted to the WordPress format and added to the current + * WordPress admin menu. This function applies menu defaults and templates, calls filters + * that allow other components to tweak the menu, decides on what capability/-ies to use, + * and so on. + * + * Caution: The filters called by this function may cause side-effects. Specifically, the Pro-only feature + * for displaying menu pages in a frame does this. See wsMenuEditorExtras::create_framed_menu(). + * Therefore, it is not safe to call this function more than once for the same item. + * + * @param array $item Menu item in the internal format. + * @param string $item_type Either 'menu' or 'submenu'. + * @param array $parent Optional. The parent of this sub-menu item. Top level menus have no parent. + * @param bool $is_deep Optional. Whether this is a deeply nested menu item. + * @return array Menu item in the internal format. + */ + private function prepare_for_output($item, $item_type = 'menu', $parent = array(), $is_deep = false) { + $parent_file = isset($parent['file']) ? $parent['file'] : null; + + /* + * Special case: Items that use hooks and whose parent file has changed. + * We'll need to set the "file" field to the fully qualified menu URL. This is required + * because WP generates menu URLs using *both* the item file and the parent file. + * + * Applies to: + * 1) Items that have been moved from one sub-menu to another, or to the top level. + * 2) Deeply nested items. In this case, the parent slug is randomly generated. + */ + if ( $item['template_id'] !== '' && empty($item['separator']) ) { + $template = $this->item_templates[$item['template_id']]; + if ( $template['defaults']['is_plugin_page'] ) { + $default_parent = $template['defaults']['parent']; + if ( ($parent_file != $default_parent) || $is_deep ) { + $item['file'] = $template['defaults']['url']; + } + } + } + + //Give each unclickable item a unique URL. + if ( $item['template_id'] === ameMenuItem::unclickableTemplateId ) { + static $unclickableCounter = 0; + $unclickableCounter++; + $unclickableUrl = '#' . ameMenuItem::unclickableTemplateClass . '-' . $unclickableCounter; + $item['file'] = $item['url'] = $unclickableUrl; + + //The item must have the special "unclickable" class even if the user overrides the class. + $cssClass = ameMenuItem::get($item, 'css_class', ''); + if ( strpos($cssClass, ameMenuItem::unclickableTemplateClass) === false ) { + $item['css_class'] = ameMenuItem::unclickableTemplateClass . ' ' . $cssClass; + } + + //Mark unclickable items as not visitable. The submenus (if any) can be visited, + //but the item itself doesn't link to anything. + $item['is_unvisitable'] = true; + } + + //Make the default submenu icon the same as the parent icon. + if ( !empty($parent) && isset($item['defaults']) ) { + $parent_icon = ameMenuItem::get($parent, 'icon_url', ''); + if ( !empty($parent_icon) ) { + $item['defaults']['icon_url'] = $parent_icon; + } + } + + //Menus that have both a custom icon URL and a "menu-icon-*" class will get two overlapping icons. + //Fix this by automatically removing the class. The user can set a custom class attr. to override. + $hasCustomIconUrl = !ameMenuItem::is_default($item, 'icon_url'); + $tempIconUrl = ameMenuItem::get($item, 'icon_url', ''); + $hasIcon = !in_array($tempIconUrl, array('', 'none', 'div')); + if ( + ameMenuItem::is_default($item, 'css_class') + && $hasCustomIconUrl + && $hasIcon //Skip "no icon" settings. + ) { + $new_classes = preg_replace('@\bmenu-icon-[^\s]+\b@', '', $item['defaults']['css_class']); + if ( $new_classes !== $item['defaults']['css_class'] ) { + $item['css_class'] = $new_classes; + } + } + + if ( $hasCustomIconUrl ) { + //Is it a Dashicon? + if ( (strpos($tempIconUrl, 'dashicons-') === 0) ) { + $item['css_class'] = ameMenuItem::get($item, 'css_class', '') . ' ame-has-custom-dashicon'; + //Is it a URL-looking thing and not an inline image? + } else if ( (strpos($tempIconUrl, '/') !== false) && (strpos($tempIconUrl, 'data:image') === false) ) { + $item['css_class'] = ameMenuItem::get($item, 'css_class', '') . ' ame-has-custom-image-url'; + } + } + + //WPML support: Translate only custom titles. See further below. + $hasCustomMenuTitle = isset($item['menu_title']); + + //Apply defaults & filters + $item = ameMenuItem::apply_defaults($item); + $item = ameMenuItem::apply_filters($item, $item_type, $parent_file); //may cause side-effects + + //Store the hierarchical menu title for errors and debugging messages. + $item['full_title'] = $item['menu_title']; + if ( isset($parent, $parent['menu_title']) ) { + $item['full_title'] = $parent['menu_title'] . ' → ' . $item['full_title']; + } + + $item = $this->set_final_menu_capability($item, $parent); + if ( !$this->should_store_security_log() ) { + unset($item['access_check_log']); //Throw away the log to conserve memory. + } + $this->add_access_lookup($item, $item_type); + + //Menus without a custom icon image should have it set to "none" (or "div" in older WP versions). + //See /wp-admin/menu-header.php for details on how this works. + if ( !isset($item['icon_url']) || ($item['icon_url'] === '') ) { + $item['icon_url'] = 'none'; + } + + //Set a flag on top level menus. It's used when determining the current + //menu item based on the current URL. + if ( $item_type === 'menu' ) { + $item['is_top'] = true; + } + + //Submenus must not have the "menu-top" class(-es). In WP versions that support submenu CSS classes, + //it can break menu display. + if ( !empty($item['css_class']) && ($item_type === 'submenu') ) { + $item['css_class'] = preg_replace('@\bmenu-top(?:-[\w\-]+)?\b@', '', $item['css_class']); + } elseif ( ($item_type === 'menu') && (!$item['separator']) && (!preg_match('@\bmenu-top\b@', $item['css_class'])) ) { + //Top-level menus should always have the "menu-top" class. + $item['css_class'] = 'menu-top ' . $item['css_class']; + } + + //Add a flag to menus that will be kept open. + if ( !empty($item['is_always_open']) && ($item_type === 'menu') && (!$item['separator']) ) { + $item['css_class'] .= ' ws-ame-has-always-open-submenu'; + } + + //Add submenu icons if necessary. + if ( ($item_type === 'submenu') && $hasIcon ) { + $item = apply_filters('admin_menu_editor-submenu_with_icon', $item, $hasCustomIconUrl); + } + + //Used later to determine the current page based on URL. + if ( empty($item['url']) ) { + $original_parent = isset($item['defaults']['parent']) ? $item['defaults']['parent'] : $parent_file; + $item['url'] = ameMenuItem::generate_url($item['file'], $original_parent); + } + + //Convert relative URls to fully qualified ones. This prevents problems with WordPress + //incorrectly converting "index.php?page=xyz" to, say, "tools.php?page=index.php?page=xyz" + //if the menu item was moved from "Dashboard" to "Tools". + $itemFile = ameMenuItem::remove_query_from($item['file']); + $shouldMakeAbsolute = + (strpos($item['file'], '://') === false) + && (substr($item['file'], 0, 1) != '/') + && ($itemFile == 'index.php') + && (strpos($item['file'], '?') !== false); + + if ( $shouldMakeAbsolute ) { + $item['file'] = admin_url($item['url']); + } + + //WPML support: Use translated menu titles where available. + if ( + empty($item['separator']) && $hasCustomMenuTitle && function_exists('icl_t') + && !empty($this->options['wpml_support_enabled']) + ) { + $item['menu_title'] = icl_t( + self::WPML_CONTEXT, + $this->get_wpml_name_for($item, 'menu_title'), + $item['menu_title'] + ); + } + + return $item; + } + + /** + * Figure out if the current user can access a menu item and what capability they would need. + * + * This method takes into account the default capability set by WordPress as well as any + * custom role and capability settings specified by the user. It will set "access_level" + * to the required capability, or set it to 'do_not_allow' if the current user can't access + * this menu. + * + * @param array $item Menu item (with defaults applied). + * @param array $parent_item Parent menu item, if any. + * @return array + */ + private function set_final_menu_capability($item, $parent_item = null) { + $item['access_check_log'] = array( + str_repeat('=', 79), + 'Figuring out what capability the user will need to access this item...' + ); + $debug_title = ameMenuItem::get($item, 'full_title', ameMenuItem::get($item, 'menu_title', '[untitled menu]')); + + //The user can configure the plugin to automatically hide all submenu items if the parent menu is hidden. + //This is the opposite of how WordPress usually handles submenu permissions, so it's optional. + $is_parent_denied = !empty($parent_item) && ($parent_item['access_level'] === 'do_not_allow'); + if ( $is_parent_denied && !empty($parent_item['restrict_access_to_items']) ) { + $item['access_check_log'][] = '-----'; + $item['access_check_log'][] = 'WARNING: The parent menu overrides submenu permissions.'; + $item['access_check_log'][] = sprintf( + 'The current user doesn\'t have access to the parent menu ("%s"). Because the "Hide all submenu items + when this item is hidden" option is enabled, this item will also be hidden. Setting capability to + "do_not_allow".', + htmlentities($parent_item['menu_title']) + ); + + $item['access_check_log'][] = str_repeat('=', 79); + if ( !empty($parent_item['access_check_log']) ) { + $item['access_check_log'][] = 'For reference, here\'s the log for the parent menu:'; + $item['access_check_log'] = array_merge($item['access_check_log'], $parent_item['access_check_log']); + } + + $item['access_level'] = 'do_not_allow'; + return $item; + } + + //TODO: A direct call to apply_custom_access would be faster. + $item = apply_filters('custom_admin_menu_capability', $item); + + $item['access_check_log'][] = '-----'; + + //Check if the current user can access this menu. + $user_has_access = true; + $cap_to_use = ''; + + $user_has_default_cap = null; + $reason = isset($item['access_decision_reason']) ? $item['access_decision_reason'] : null; + + if ( !empty($item['access_level']) ) { + $cap_to_use = $item['access_level']; + + if ( isset($item['user_has_access_level']) ) { + //The "custom_admin_menu_capability" filter has already determined whether this user should + //have the required capability, so checking it again would be redundant. This usually only + //applies to the Pro version which uses that filter in extras.php. + $user_has_cap = $item['user_has_access_level']; + + $item['access_check_log'][] = sprintf( + 'Skipping a "%1$s" capability check because we\'ve already determined that the current user %2$s access.', + htmlentities($cap_to_use), + $user_has_cap ? 'should have' : 'should not have' + ); + } else { + $user_has_cap = $this->current_user_can($cap_to_use); + $item['access_check_log'][] = sprintf( + 'Required capability: %1$s. User %2$s this capability.', + htmlentities($cap_to_use), + $user_has_cap ? 'HAS' : 'DOES NOT have' + ); + + $user_has_default_cap = $user_has_cap; + if ( is_null($reason) ) { + $reason = sprintf( + 'The current user %1$s the "%2$s" capability that is required to access the "%3$s" menu item.', + $user_has_cap ? 'has' : 'doesn\'t have', + $cap_to_use, + $debug_title + ); + } + } + + $user_has_access = $user_has_access && $user_has_cap; + + } else { + $item['access_check_log'][] = '- No required capability set.'; + } + + if ( !empty($item['extra_capability']) ) { + $had_access_before_extra_cap = $user_has_access; + + $user_has_cap = $this->current_user_can($item['extra_capability']); + $user_has_access = $user_has_access && $user_has_cap; + $cap_to_use = $item['extra_capability']; + + $item['access_check_log'][] = sprintf( + 'Extra capability: %1$s. User %2$s this capability.', + htmlentities($cap_to_use), + $user_has_cap ? 'HAS' : 'DOES NOT have' + ); + + //Provide a more detailed reason for situations where the extra cap disagrees. + if ( !$user_has_access ) { + if ( $had_access_before_extra_cap && !$user_has_cap ) { + $reason = sprintf( + 'The current user doesn\'t have the extra capability "%1$s" that is required to access the "%2$s" menu item.', + $item['extra_capability'], + $debug_title + ); + } else if ( $user_has_cap && !$user_has_default_cap && !is_null($user_has_default_cap) ) { + //Note: Will this ever show up? If the user doesn't have the required cap, + //WordPress won't even register the menu. AME won't be able to identify the menu for that user. + $reason = sprintf( + 'The current user has the extra capability "%1$s". However, they don\'t ' . + 'have the "%2$s" capability that is also required to access "%3$s".', + $item['extra_capability'], + $item['access_level'], + $debug_title + ); + } + } + } else { + $item['access_check_log'][] = 'No "extra capability" set.'; + } + + if ( !is_null($reason) ) { + $item['access_decision_reason'] = $reason; + } + + $capability = $user_has_access ? $cap_to_use : 'do_not_allow'; + $item['access_check_log'][] = 'Final capability setting: ' . $capability; + $item['access_check_log'][] = str_repeat('=', 79); + + $item['access_level'] = $capability; + return $item; + } + + /** + * Check if a menu item can be visited/navigated to. + * Most regular items can be visited. Separators and some special item types cannot. + * + * @param array $item + * @return bool + */ + private function is_item_visitable($item) { + return empty($item['separator']) && empty($item['is_unvisitable']); + } + + /** + * Output the menu editor page + * + * @return void + */ + function page_menu_editor(){ + if ( !$this->current_user_can_edit_menu() ){ + wp_die(sprintf( + 'You do not have sufficient permissions to use Admin Menu Editor. Required: <code>%s</code>.', + esc_html($this->options['plugin_access']) + )); + } + + $action = isset($this->post['action']) ? $this->post['action'] : (isset($this->get['action']) ? $this->get['action'] : ''); + do_action('admin_menu_editor-header', $action, $this->post); + + if ( !empty($action) ) { + $this->handle_form_submission($this->post, $action); + } + + //By default, show the "Hide" button only if the user has already hidden something with it, + //or if they're using the free version. Pro users should use role permissions instead, but can + //explicitly enable the button if they want. + if ( !isset($this->options['show_deprecated_hide_button']) ) { + if ( $this->is_pro_version() ) { + $this->options['show_deprecated_hide_button'] = ameMenu::has_hidden_items($this->merged_custom_menu); + $this->save_options(); + } else { + $this->options['show_deprecated_hide_button'] = true; + } + } + + if ( $this->current_tab === 'settings' ) { + $this->display_plugin_settings_ui(); + } else if ( $this->current_tab == 'generate-menu-dashicons' ) { + require dirname(__FILE__) . '/generate-menu-dashicons.php'; + } else if ( $this->current_tab === 'repair-database' ) { + $this->repair_database(); + } else if ( $this->is_editor_page() ) { + $this->display_editor_ui(); + } else { + do_action('admin_menu_editor-section-' . $this->current_tab); + } + + //Let the Pro version script output it's extra HTML & scripts. + do_action('admin_menu_editor-footer'); + do_action('admin_menu_editor-footer-' . $this->current_tab, $action); + } + + public function trigger_tab_load_event() { + //Modules can use this hook in place of the "load-$page_hook" action. This way a module + //doesn't need to know what the page hook is, and it can easily target a specific tab. + if ( !empty($this->current_tab) ) { + do_action('admin_menu_editor-load_tab-' . $this->current_tab); + } + } + + private function repair_database() { + global $wpdb; /** @var wpdb $wpdb */ + + if ( !is_multisite() ) { + echo 'This is not Multisite. The "repair" function does not apply to your site.'; + return; + } + + echo '<div class="wrap"><h1>Repairing database...</h1><p></p>'; + // phpcs:disable WordPress.DB.DirectDatabaseQuery,WordPress.DB.SlowDBQuery -- Special case: Data recovery attempt. + + $options_to_repair = array( + $this->option_name, + 'wsh_license_manager-admin-menu-editor-pro', + 'ws_abe_admin_bar_nodes', + 'ws_abe_admin_bar_settings', + ); + + printf("Repair %s<br>", esc_html($wpdb->sitemeta)); + $wpdb->query('REPAIR TABLE ' . $wpdb->sitemeta); + + printf("Lock %s<br>", esc_html($wpdb->sitemeta)); + $wpdb->query('LOCK TABLES ' . $wpdb->sitemeta); + + foreach($options_to_repair as $option) { + if ( empty($option) ) { + continue; //Sanity check. + } + + printf("Fetch option %s<br>", esc_html($option)); + /** @noinspection SqlResolve */ + $row = $wpdb->get_row($wpdb->prepare( + "SELECT * FROM {$wpdb->sitemeta} WHERE meta_key = %s LIMIT 1", + $option + )); + + if ( empty($row) || empty($row->site_id) ) { + echo "Option doesn't exist, skipping it.<br>"; + continue; + } + + printf("Delete all rows where meta_key = %s<br>", esc_html($option)); + $wpdb->delete($wpdb->sitemeta, array('meta_key' => $option), '%s'); + + printf("Recreate the first copy of %s<br>", esc_html($option)); + $wpdb->insert( + $wpdb->sitemeta, + array( + 'site_id' => $row->site_id, + 'meta_key' => $option, + 'meta_value' => $row->meta_value, + ), + array('%d', '%s', '%s') + ); + } + + printf("Unlock %s<br>", esc_html($wpdb->sitemeta)); + $wpdb->query('UNLOCK TABLES'); + // phpcs:enable + + echo "Done.<br>"; + echo '<div>'; + } + + private function handle_form_submission($post, $action = '') { + if ( $action == 'save_menu' ) { + //Save the admin menu configuration. + if ( isset($post['data']) ){ + check_admin_referer('menu-editor-form'); + + //Try to decode a menu tree encoded as JSON + $url = remove_query_arg(array('noheader')); + try { + $menu = ameMenu::load_json($post['data'], true); + } catch (InvalidMenuException $ex) { + // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_print_r -- Debug output for exceptional cases. + $debugData = "Exception:\n" . $ex->getMessage() . "\n\n"; + $debugData .= "Used POST data:\n" . print_r($this->post, true) . "\n\n"; + $debugData .= "Original POST:\n" . print_r($this->originalPost, true) . "\n\n"; + $debugData .= "\$_POST global:\n" . print_r($_POST, true); + + $debugOutput = sprintf( + "<textarea rows=\"30\" cols=\"100\">%s</textarea>", + esc_textarea($debugData) + ); + + wp_die( + "Error: Failed to decode menu data!<br><br>\n" + . "Please send this debugging information to the developer: <br>" + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaped above, see sprintf() call. + . $debugOutput + ); + + return; + // phpcs:enable + } + + //Sanitize menu item properties. + $menu['tree'] = ameMenu::sanitize($menu['tree']); + + //Discard capabilities that refer to unregistered post types or taxonomies. + if ( !empty($menu['granted_capabilities']) ) { + $capFilter = new ameGrantedCapabilityFilter(); + $menu['granted_capabilities'] = $capFilter->clean_up($menu['granted_capabilities']); + } + + //Remember if the user has changed any menu icons to different Dashicons. + $menu['has_modified_dashicons'] = ameModifiedIconDetector::detect($menu); + + //Add a modification timestamp to help ensure that the new menu data will be different. + //This way update_option() and similar functions should only return false when there is + //an actual error, not just because the data hasn't changed. + $menu['last_modified_on'] = gmdate('c'); + + //Which menu configuration are we changing? + $config_id = isset($post['config_id']) ? $post['config_id'] : null; + if ( !in_array($config_id, array('site', 'global', 'network-admin')) ) { + $config_id = $this->get_loaded_menu_config_id(); + } + + //Save the custom menu + if ( !$this->set_custom_menu($menu, $config_id) ) { + $messages = array('Error: Could not save menu settings.'); + + global $wpdb; + if ( !empty($wpdb->last_error) ) { + $messages[] = 'Last database error: "' . esc_html($wpdb->last_error) . '"'; + } + + //Check the character set of the wp_options and wp_sitemeta tables. + $bad_charsets = array('utf8', 'utf8mb3'); + $tables_to_check = array(array($wpdb->options, 'option_value')); + if ( is_multisite() ) { + $tables_to_check[] = array($wpdb->sitemeta, 'meta_value'); + } + foreach ($tables_to_check as $item) { + list($table, $column) = $item; + if ( empty($table) ) { + continue; + } + $current_charset = $wpdb->get_col_charset($table, $column); + if ( in_array($current_charset, $bad_charsets) ) { + $messages[] = sprintf( + '<p>Warning: The <code>%s</code> database table uses the outdated <code>%s</code> ' . + 'character set. This can prevent you from saving settings that contain emojis, ' . + 'certain Chinese characters, and so on. It is recommended to convert the table ' . + 'to the <code>utf8mb4</code> character set.</p>', + esc_html($wpdb->options), + esc_html($current_charset) + ); + } + } + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Should be escaped before. + wp_die(implode("<br>\n", $messages)); + } + + //Save nesting settings. + if ( $this->update_nesting_settings($post) ) { + $this->save_options(); + } + + //Redirect back to the editor and display the success message. + $query = array('message' => 1); + + //Also, automatically select the last selected actor and menu (convenience feature). + $pass_through_params = array( + 'selected_actor', 'selected_menu_url', 'selected_submenu_url', + 'expand_menu', 'expand_submenu', + ); + foreach($pass_through_params as $param) { + if ( isset($post[$param]) && !empty($post[$param]) ) { + $query[$param] = rawurlencode(strval($post[$param])); + } + } + + wp_redirect( add_query_arg($query, $url) ); + die(); + } else { + $message = "Failed to save the menu. "; + if ( isset($this->post['data_length']) && is_numeric($this->post['data_length']) ) { + $message .= sprintf( + 'Expected to receive %d bytes of menu data in $_POST[\'data\'], but got nothing.', + intval($this->post['data_length']) + ); + } + wp_die(esc_html($message)); + } + + } else if ( $action == 'save_settings' ) { + + //Save overall plugin configuration (permissions, etc). + check_admin_referer('save_settings'); + + //Plugin access setting. + $valid_access_settings = array('super_admin', 'manage_options'); + //On Multisite only Super Admins can choose the "Only the current user" option. + if ( !is_multisite() || is_super_admin() ) { + $valid_access_settings[] = 'specific_user'; + } + if ( isset($this->post['plugin_access']) && in_array($this->post['plugin_access'], $valid_access_settings) ) { + $this->options['plugin_access'] = $this->post['plugin_access']; + + if ( $this->options['plugin_access'] === 'specific_user' ) { + $this->options['allowed_user_id'] = get_current_user_id(); + } else { + $this->options['allowed_user_id'] = null; + } + } + + //Whether to hide the plugin on the "Plugins" admin page. + if ( !is_multisite() || is_super_admin() ) { + if ( !empty($this->post['hide_plugin_from_others']) ) { + $this->options['plugins_page_allowed_user_id'] = get_current_user_id(); + } else { + $this->options['plugins_page_allowed_user_id'] = null; + } + } + + //Configuration scope. The Super Admin is the only one who can change it since it affects all sites. + if ( is_multisite() && is_super_admin() ) { + $valid_scopes = array('global', 'site'); + if ( isset($this->post['menu_config_scope']) && in_array($this->post['menu_config_scope'], $valid_scopes) ) { + $this->options['menu_config_scope'] = $this->post['menu_config_scope']; + } + } + + //Security logging. + $this->options['security_logging_enabled'] = !empty($this->post['security_logging_enabled']); + + //Hide some menu options by default. + $this->options['hide_advanced_settings'] = !empty($this->post['hide_advanced_settings']); + + //Enable the now-obsolete "Hide" button. + if ( $this->is_pro_version() ) { + $this->options['show_deprecated_hide_button'] = !empty($this->post['show_deprecated_hide_button']); + } + + //Menu editor colour scheme. + if ( !empty($this->post['ui_colour_scheme']) ) { + $valid_colour_schemes = array('classic', 'wp-grey', 'modern-one'); + $scheme = strval($this->post['ui_colour_scheme']); + if ( in_array($scheme, $valid_colour_schemes) ) { + $this->options['ui_colour_scheme'] = $scheme; + } + } + + //Enable submenu icons. + if ( !empty($this->post['submenu_icons_enabled']) ) { + $submenu_icons_enabled = strval($this->post['submenu_icons_enabled']); + $valid_icon_settings = array('never', 'if_custom', 'always'); + if ( in_array($submenu_icons_enabled, $valid_icon_settings, true) ) { + $this->options['submenu_icons_enabled'] = $submenu_icons_enabled; + } + } + + //Work around icon CSS problems. + $this->options['force_custom_dashicons'] = !empty($this->post['force_custom_dashicons']); + + //Where to put new or unused menu items. + if ( !empty($this->post['unused_item_position']) ) { + $unused_item_position = strval($this->post['unused_item_position']); + $valid_position_settings = array('relative', 'bottom'); + if ( in_array($unused_item_position, $valid_position_settings, true) ) { + $this->options['unused_item_position'] = $unused_item_position; + } + } + + //Permissions for unused menu items. + if ( + isset($this->post['unused_item_permissions']) + && in_array($this->post['unused_item_permissions'], array('unchanged', 'match_plugin_access'), true) + ) { + $this->options['unused_item_permissions'] = strval($this->post['unused_item_permissions']); + } + + //How verbose "access denied" errors should be. + if ( !empty($this->post['error_verbosity']) ) { + $error_verbosity = intval($this->post['error_verbosity']); + $valid_verbosity_levels = array(self::VERBOSITY_LOW, self::VERBOSITY_NORMAL, self::VERBOSITY_VERBOSE); + if ( in_array($error_verbosity, $valid_verbosity_levels) ) { + $this->options['error_verbosity'] = $error_verbosity; + } + } + + //Menu data compression. + $this->options['compress_custom_menu'] = !empty($this->post['compress_custom_menu']); + + //WPML support. + $this->options['wpml_support_enabled'] = !empty($this->post['wpml_support_enabled']); + + //bbPress override support. + $this->options['bbpress_override_enabled'] = !empty($this->post['bbpress_override_enabled']); + + //Three level menus / deep nesting. + $this->update_nesting_settings($this->post); + + //Active modules. + $activeModules = isset($this->post['active_modules']) ? (array)$this->post['active_modules'] : array(); + $activeModules = array_fill_keys(array_map('strval', $activeModules), true); + $this->options['is_active_module'] = array_merge( + array_map('__return_false', $this->get_available_modules()), + $activeModules + ); + + $this->save_options(); + wp_redirect(add_query_arg('message', 1, $this->get_settings_page_url())); + exit; + } + } + + /** + * Update menu nesting/three level settings. + * + * Note: This method does not actually save the new settings to the database, + * it just modifies them in memory. + * + * @param array $post + * @return boolean True if settings were changed, false otherwise. + */ + private function update_nesting_settings($post) { + if ( !isset($post['deep_nesting_enabled']) ) { + return false; + } + + $nesting_enabled = $this->json_decode($post['deep_nesting_enabled']); + $valid_nesting_settings = array(null, true, false); + if ( + in_array($nesting_enabled, $valid_nesting_settings, true) + && ($nesting_enabled !== $this->options['deep_nesting_enabled']) + ) { + $this->options['deep_nesting_enabled'] = $nesting_enabled; + if ( $nesting_enabled !== null ) { + $this->options['was_nesting_ever_changed'] = true; + } + return true; + } + + return false; + } + + private function display_editor_ui() { + //Prepare a bunch of parameters for the editor. + $editor_data = array( + 'message' => isset($this->get['message']) ? intval($this->get['message']) : null, + 'images_url' => plugins_url('images', $this->plugin_file), + 'hide_advanced_settings' => $this->options['hide_advanced_settings'], + 'show_extra_icons' => $this->options['show_extra_icons'], + 'current_tab_url' => $this->get_plugin_page_url(array('sub_section' => $this->current_tab)), + 'settings_page_url' => $this->get_settings_page_url(), + 'show_deprecated_hide_button' => $this->options['show_deprecated_hide_button'], + 'dashicons_available' => wp_style_is('dashicons', 'done'), + 'menu_config_id' => $this->get_loaded_menu_config_id(), + ); + + //Build a tree struct. for the default menu + $default_menu = $this->get_default_menu(); + + //Is there a custom menu? + if (!empty($this->merged_custom_menu)){ + $custom_menu = $this->merged_custom_menu; + } else { + //Start out with the default menu if there is no user-created one + $custom_menu = $default_menu; + } + + //The editor doesn't use the color CSS. Including it would just make the page bigger and waste bandwidth. + unset($custom_menu['color_css']); + unset($custom_menu['color_css_modified']); + + //Encode both menus as JSON + $editor_data['default_menu_js'] = ameMenu::to_json($default_menu); + $editor_data['custom_menu_js'] = ameMenu::to_json($custom_menu); + + //Create a list of all known capabilities and roles. Used for the drop-down list on the access field. + $all_capabilities = ameRoleUtils::get_all_capabilities(is_multisite()); + //"level_X" capabilities are deprecated so we don't want people using them. + //This would look better with array_filter() and an anonymous function as a callback. + for($level = 0; $level <= 10; $level++){ + $cap = 'level_' . $level; + if ( isset($all_capabilities[$cap]) ){ + unset($all_capabilities[$cap]); + } + } + $all_capabilities = array_keys($all_capabilities); + natcasesort($all_capabilities); + + //Multi-site installs also get the virtual "Super Admin" cap, but only the Super Admin sees it. + if ( is_multisite() && !isset($all_capabilities['super_admin']) && is_super_admin() ){ + array_unshift($all_capabilities, 'super_admin'); + } + $editor_data['all_capabilities'] = $all_capabilities; + + //Create a list of all roles, too. + $all_roles = ameRoleUtils::get_role_names(); + asort($all_roles); + $editor_data['all_roles'] = $all_roles; + + //Include hint visibility settings + $editor_data['show_hints'] = $this->get_hint_visibility(); + + require dirname(__FILE__) . '/editor-page.php'; + } + + /** + * Get the default admin menu configuration. + * + * @return array + */ + private function get_default_menu() { + $default_tree = ameMenu::wp2tree($this->default_wp_menu, $this->default_wp_submenu, $this->menu_url_blacklist); + try { + $default_menu = ameMenu::load_array($default_tree); + } catch (InvalidMenuException $e) { + throw new LogicException( + 'An unexpected exception was thrown while loading the default admin menu. ' + . 'This is most likely a bug. The default menu should always be valid.' + ); + } + return $default_menu; + } + + /** + * Get the admin menu configuration that was used during this page load. + * + * @return array + */ + public function get_active_admin_menu() { + if ( !did_action('admin_menu') && !did_action('network_admin_menu') ) { + throw new LogicException(__METHOD__ . ' was called too early. You must only call it after the admin menu is ready.'); + } + + if (!empty($this->merged_custom_menu)){ + return $this->merged_custom_menu; + } else { + return $this->get_default_menu(); + } + } + + /** + * Display the header of the "Menu Editor" page. + * This includes the page heading and tab list. + */ + public function display_settings_page_header() { + $wrap_classes = array('wrap'); + if ( $this->is_pro_version() ) { + $wrap_classes[] = 'ame-is-pro-version'; + } else { + $wrap_classes[] = 'ame-is-free-version'; + } + if ( isset($GLOBALS['wp_version']) && version_compare($GLOBALS['wp_version'], '5.3-RC1', '>=') ) { + $wrap_classes[] = 'ame-is-wp53-plus'; + } + + echo '<div class="', esc_attr(implode(' ', $wrap_classes)), '">'; + printf( + '<%1$s id="ws_ame_editor_heading">%2$s</%1$s>', + //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Should only ever be "h1" or "h2". + self::$admin_heading_tag, + esc_html(apply_filters('admin_menu_editor-self_page_title', 'Menu Editor')) + ); + + do_action('admin_menu_editor-display_tabs'); + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Just showing a "settings saved" notice. + if ( isset($_GET['message']) && (intval($_GET['message']) === 1) ) { + add_settings_error('ame-settings-page', 'settings_updated', __('Settings saved.'), 'updated'); + } + settings_errors('ame-settings-page'); + } + + public function display_settings_page_footer() { + echo '</div>'; //div.wrap + } + + /** + * Display the tabs for the settings page. + */ + public function display_editor_tabs() { + echo '<h2 class="nav-tab-wrapper ws-ame-nav-tab-list">'; + foreach($this->tabs as $slug => $title) { + printf( + '<a href="%s" id="%s" class="nav-tab%s">%s</a>', + esc_attr(add_query_arg('sub_section', $slug, self_admin_url($this->settings_link))), + esc_attr('ws_ame_' . $slug . '_tab'), + $slug === $this->current_tab ? ' nav-tab-active' : '', + esc_html($title) + ); + } + echo '</h2>'; + echo '<div class="clear"></div>'; + } + + /** + * Display the plugin settings page. + */ + private function display_plugin_settings_ui() { + //These variables are used by settings-page.php. + /** @noinspection PhpUnusedLocalVariableInspection */ + $settings = $this->options; + /** @noinspection PhpUnusedLocalVariableInspection */ + $settings_page_url = $this->get_settings_page_url(); + /** @noinspection PhpUnusedLocalVariableInspection */ + $editor_page_url = admin_url($this->settings_link); + /** @noinspection PhpUnusedLocalVariableInspection */ + $db_option_name = $this->option_name; + + require dirname(__FILE__) . '/settings-page.php'; + } + + /** + * Get the fully qualified URL of the plugin page, i.e. "Settings -> Menu Editor [Pro]". + * + * @param array $extra_query_args List of query arguments to append to the URL. Format: [param => value]. + * @return string + */ + public function get_plugin_page_url($extra_query_args = array()) { + $url = self_admin_url($this->settings_link); + if ( !empty($extra_query_args) ) { + $url = add_query_arg($extra_query_args, $url); + } + return $url; + } + + /** + * Get the fully qualified URL of the "Settings" sub-section of our plugin page. + * + * @return string + */ + public function get_settings_page_url() { + return $this->get_plugin_page_url(array('sub_section' => 'settings')); + } + + /** + * Check if the current page is the "Menu Editor" admin page. + * + * @return bool + */ + public function is_editor_page() { + return $this->is_tab_open('editor') || $this->is_tab_open('network-admin-menu'); + } + + /** + * Check if the current page is the "Settings" sub-section of our admin page. + * + * @return bool + */ + protected function is_settings_page() { + return $this->is_tab_open('settings'); + } + + /** + * Check if the specified AME settings tab is currently open. + * + * @param string $tab_slug + * @return bool + */ + public function is_tab_open($tab_slug) { + return is_admin() + && ($this->current_tab === $tab_slug) + && isset($this->get['page']) && ($this->get['page'] == 'menu_editor'); + } + + /** + * Get the list of virtual capabilities. + * + * @uses self::$cached_virtual_caps to cache the generated list of caps. + * + * @param int|null $mode + * @return array A list of capability => [role1 => true, ... roleN => true] assignments. + */ + function get_virtual_caps($mode = null) { + if ( $mode === null ) { + $mode = self::ALL_VIRTUAL_CAPS; + } + + if ( $this->cached_virtual_caps !== null ) { + return $this->cached_virtual_caps[$mode]; + } + + try { + $custom_menu = $this->load_custom_menu(); + } catch (InvalidMenuException $e) { + return array(); + } + if ( $custom_menu === null ){ + return array(); + } + + if ( isset($custom_menu['prebuilt_virtual_caps']) ) { + $this->cached_virtual_caps = $custom_menu['prebuilt_virtual_caps']; + } else { + $this->cached_virtual_caps = $this->build_virtual_capability_list($custom_menu); + } + + return $this->cached_virtual_caps[$mode]; + } + + /** + * Generate a list of "virtual" capabilities that should be granted to specific actors. + * + * This is based on grant_access settings for the custom menu and enables selected + * roles and users to access menu items that they ordinarily would not be able to. + * + * @uses self::get_virtual_caps_for() to actually generate the caps. + * + * @param array $custom_menu + * @return array + */ + private function build_virtual_capability_list($custom_menu) { + //Include directly granted capabilities. + $grantedCaps = array(); + if ( !empty($custom_menu['granted_capabilities']) ) { + foreach ($custom_menu['granted_capabilities'] as $actor => $capabilities) { + foreach ($capabilities as $capability => $allow) { + $grantedCaps[$actor][$capability] = (bool)(is_array($allow) ? $allow[0] : $allow); + } + } + } + + //Include caps that are required to access menu items (grant_access). + $menuCaps = array(); + foreach($custom_menu['tree'] as $item) { + $menuCaps = self::array_replace_recursive($menuCaps, $this->get_virtual_caps_for($item)); + } + + //grant_access settings on individual items have precedence. + $allCaps = self::array_replace_recursive($grantedCaps, $menuCaps); + + return array( + self::DIRECTLY_GRANTED_VIRTUAL_CAPS => $grantedCaps, + self::ALL_VIRTUAL_CAPS => $allCaps, + ); + } + + private function get_virtual_caps_for($item) { + $caps = array(); + + if ( $item['template_id'] !== '' ) { + $required_cap = ameMenuItem::get($item, 'access_level'); + + $required_cap = self::map_basic_meta_cap($required_cap); + //Why not just call map_meta_cap? Because it needs a user ID and we may be working on a role. + //Also, map_meta_cap is complex and filter-able, so it's hard to verify that it will work reliably + //in a non-standard context. + + foreach ($item['grant_access'] as $grant => $has_access) { + if ( $has_access ) { + if ( !isset($caps[$grant]) ) { + $caps[$grant] = array(); + } + $caps[$grant][$required_cap] = true; + } + } + } + + foreach($item['items'] as $sub_item) { + $caps = self::array_replace_recursive($caps, $this->get_virtual_caps_for($sub_item)); + } + + return $caps; + } + + private static function array_replace_recursive($array1, $array2) { + if ( function_exists('array_replace_recursive') ) { + return array_replace_recursive($array1, $array2); + } + foreach($array2 as $key => $value) { + if ( is_array($value) && isset($array1[$key]) && is_array($array1[$key]) ) { + $value = self::array_replace_recursive($array1[$key], $value); + } + $array1[$key] = $value; + } + return $array1; + } + + private static function map_basic_meta_cap($capability) { + if ( $capability === 'customize' ) { + return 'edit_theme_options'; + } elseif ( $capability === 'delete_site' ) { + return 'manage_options'; + } + + static $category_caps = array( + 'manage_post_tags' => true, + 'edit_categories' => true, + 'edit_post_tags' => true, + 'delete_categories' => true, + 'delete_post_tags' => true, + ); + if ( isset($category_caps[$capability]) ) { + return 'manage_categories'; + } + + if (($capability === 'assign_categories') || ($capability === 'assign_post_tags')) { + return 'edit_posts'; + } + + return $capability; + } + + /** + * Clear all internal caches that can vary depending on the current site. + * + * For example, the same user can have different roles on different sites, + * so we must clear the role cache when WordPress switches the active site. + */ + public function clear_site_specific_caches() { + $this->cached_virtual_caps = null; + $this->cached_user_caps = array(); + $this->cached_user_roles = array(); + $this->cached_virtual_user_caps = array(); + + if ($this->options['menu_config_scope'] === 'site') { + $this->cached_custom_menu = null; + $this->loaded_menu_config_id = null; + } + } + + /** + * AJAX callback for saving screen options (whether to show or to hide advanced menu options). + * + * Handles the 'ws_ame_save_screen_options' action. The new option value + * is read from $_POST['hide_advanced_settings']. + * + * @return void + */ + function ajax_save_screen_options(){ + if (!$this->current_user_can_edit_menu() || !check_ajax_referer('ws_ame_save_screen_options', false, false)){ + //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Outputs JSON, not HTML. + die( $this->json_encode( array( + 'error' => "You're not allowed to do that!" + ))); + } + + $this->options['hide_advanced_settings'] = !empty($this->post['hide_advanced_settings']); + $this->options['show_extra_icons'] = !empty($this->post['show_extra_icons']); + $this->save_options(); + die('1'); + } + + public function ajax_hide_hint() { + if ( !isset($this->post['hint']) || !$this->current_user_can_edit_menu() ){ + die("You're not allowed to do that!"); + } + + $show_hints = $this->get_hint_visibility(); + $show_hints[strval($this->post['hint'])] = false; + $this->set_hint_visibility($show_hints); + + die("OK"); + } + + private function get_hint_visibility() { + $user = wp_get_current_user(); + $show_hints = get_user_meta($user->ID, 'ame_show_hints', true); + if ( !is_array($show_hints) ) { + $show_hints = array(); + } + + $defaults = array( + 'ws_sidebar_pro_ad' => true, + 'ws_whats_new_120' => false, + 'ws_hint_menu_permissions' => false, + ); + + return array_merge($defaults, $show_hints); + } + + private function set_hint_visibility($show_hints) { + $user = wp_get_current_user(); + update_user_meta($user->ID, 'ame_show_hints', $show_hints); + } + + /** + * AJAX callback for permanently hiding the "are you sure you want to hide the Dashboard?" warning. + */ + public function ajax_disable_dashboard_hiding_confirmation() { + if (!check_ajax_referer('ws_ame_disable_dashboard_hiding_confirmation', false, false) || !$this->current_user_can_edit_menu()){ + die("You don't have sufficient permissions to do that."); + } + $this->options['dashboard_hiding_confirmation_enabled'] = false; + $this->save_options(); + } + + /** + * Retrieve a list of recently modified pages. + */ + public function ajax_get_pages() { + if ( !check_ajax_referer('ws_ame_get_pages', false, false) ) { + exit(wp_json_encode(array('error' => 'Invalid nonce.'))); + } else if ( !$this->current_user_can_edit_menu() ) { + exit(wp_json_encode(array('error' => 'You don\'t have sufficient permissions to edit the admin menu.'))); + } + + $pages = get_pages(array( + 'sort_column' => 'post_modified', + 'sort_order' => 'DESC', + 'hierarchical' => false, + 'post_status' => array('publish', 'private'), + 'number' => 50, //Semi-arbitrary. We do need a limit - some users could have thousands of pages. + )); + /** @var WP_Post[] $pages */ + $blog_id = get_current_blog_id(); + + $results = array(); + foreach($pages as $page) { + $results[] = array( + 'post_id' => $page->ID, + 'blog_id' => $blog_id, + 'post_title' => $page->post_title, + 'post_modified' => $page->post_modified + ); + } + + exit(wp_json_encode($results)); + } + + /** + * Get details about a specific page or post. CPTs also work. + */ + public function ajax_get_page_details() { + if ( !check_ajax_referer('ws_ame_get_page_details', false, false) ) { + exit(wp_json_encode(array('error' => 'Invalid nonce.'))); + } else if ( !$this->current_user_can_edit_menu() ) { + exit(wp_json_encode(array('error' => 'You don\'t have sufficient permissions to edit the admin menu.'))); + } + + $post_id = !empty($_GET['post_id']) ? intval($_GET['post_id']) : 0; + $blog_id = !empty($_GET['blog_id']) ? intval($_GET['blog_id']) : 0; + $should_switch = function_exists('get_current_blog_id') && ($blog_id !== get_current_blog_id()); + + if ( $should_switch ) { + switch_to_blog($blog_id); + } + + $page = get_post($post_id); + if ( !$page ) { + exit(wp_json_encode(array('error' => 'Not found'))); + } + + if ( $should_switch ) { + restore_current_blog(); + } + + $response = array( + 'post_id' => $page->ID, + 'blog_id' => $blog_id, + 'post_title' => $page->post_title, + ); + exit(wp_json_encode($response)); + } + + /** + * Enqueue a script that fixes a bug where pages moved to a different menu + * would not be highlighted properly when the user visits them. + */ + public function enqueue_menu_fix_script() { + $inFooter = !$this->is_custom_menu_deep(); + + //Compatibility fix for PRO Theme 1.1.5. + //This custom admin theme expands the current admin menu via JavaScript by using a "ready" handler. + //We need to ensure that we highlight the correct current menu before that happens. This means we + //have to enqueue the script in the header and with a higher priority than the PRO Theme script. + if ( class_exists('PROTheme', false) ) { + $inFooter = false; + } + + wp_enqueue_auto_versioned_script( + 'ame-menu-fix', + plugins_url('js/menu-highlight-fix.js', $this->plugin_file), + array('jquery'), + $inFooter + ); + } + + /** + * Check if the current user can access the current admin menu page. + * + * @return bool + */ + private function user_can_access_current_page() { + $current_item = $this->get_current_menu_item(); + if ( $current_item === null ) { + $this->log_security_note('Could not determine the current menu item. We won\'t do any custom permission checks.'); + return true; //Let WordPress handle it. + } + + $this->log_security_note(sprintf( + 'The current menu item is "%s", menu template ID: "%s"', + esc_html($current_item['menu_title']), + esc_html(ameMenuItem::get($current_item, 'template_id', 'N/A')) + )); + if ( isset($current_item['access_check_log']) ) { + $this->log_security_note($current_item['access_check_log']); + } + + //Note: Per-role and per-user virtual caps will be applied by has_cap filters. + $allow = $this->current_user_can($current_item['access_level']); + $this->log_security_note(sprintf( + 'The current user %1$s the "%2$s" capability.', + $allow ? 'has' : 'does not have', + esc_html($current_item['access_level']) + )); + + return $allow; + } + + /** + * Check if the current user has the specified capability. + * If the Pro version installed, you can use special syntax to perform complex capability checks. + * + * @param string $capability + * @return bool + */ + private function current_user_can($capability) { + //WP core uses a special "do_not_allow" capability in a dozen or so places to explicitly deny access. + //Even multisite super admins do not have this cap. We can return early here. + if ( $capability === 'do_not_allow' ) { + return false; + } + + //Everybody has the "exist" cap. + if ( $capability === 'exist' ) { + return true; + } + + if ( $this->user_cap_cache_enabled && isset($this->cached_user_caps[$capability]) ) { + return $this->cached_user_caps[$capability]; + } + + /* + * Some meta caps require an object ID to be passed as the second argument. WordPress core will + * unintentionally trigger a notice if we don't provide that argument. We use a non-existent ID + * to prevent that notice. + * + * NULL, FALSE and 0 are not good alternatives because some WordPress APIs (e.g. get_post) take + * those values as a sign to return the current post/page/taxonomy. + */ + + $user_can = apply_filters( + 'admin_menu_editor-current_user_can', + current_user_can($capability, -1), + $capability + ); + $this->cached_user_caps[$capability] = $user_can; + return $user_can; + } + + /** + * Determine which menu item matches the currently open admin page. + * + * @uses self::$reverse_item_lookup + * @return array|null Menu item in the internal format, or NULL if no matching item can be found. + */ + private function get_current_menu_item() { + if ( !is_admin() || empty($this->reverse_item_lookup)) { + if ( !is_admin() ) { + $this->log_security_note('This is not an admin page. is_admin() returns false.'); + } else if ( empty($this->reverse_item_lookup) ) { + $this->log_security_note('Warning: reverse_item_lookup is empty!'); + } + return null; + } + + //The current menu item doesn't change during a request, so we can cache it + //and avoid searching the entire menu every time. + static $cached_item = null; + if ( $cached_item !== null ) { + return $cached_item; + } + + //Find an item where *all* query params match the current ones, with as few extraneous params as possible, + //preferring sub-menu items. This is intentionally more strict than what we do in menu-highlight-fix.js, + //since this function is used to check menu access. + //TODO: Use get_current_screen() to determine the current post type and taxonomy. + + $best_item = null; + $best_extra_params = PHP_INT_MAX; + $best_is_submenu = false; + + $base_site_url = get_site_url(); + if ( preg_match('@(^\w+://[^/]+)@', $base_site_url, $matches) ) { //Extract scheme + hostname. + $base_site_url = $matches[1]; + } + + //Calling admin_url() once and then manually appending each page's path is measurably faster than calling it + //for each menu, but it means the "admin_url" filter is only called once. If there is a plugin that changes + //the admin_url for some pages but not others, this could lead to bugs (no such plugins are known at this time). + $base_admin_url = admin_url(); + $admin_url_is_filtered = has_filter('admin_url'); + + $current_url = $base_site_url . remove_query_arg('___ame_dummy_param___'); + $this->log_security_note(sprintf('Current URL: "%s"', esc_html($current_url))); + + $current_url = $this->parse_url($current_url); + + //Special case: if post_type is not specified for edit.php and post-new.php, + //WordPress assumes it is "post". Here we make this explicit. + if ( $this->endsWith($current_url['path'], '/wp-admin/edit.php') || $this->endsWith($current_url['path'], '/wp-admin/post-new.php') ) { + if ( !isset($current_url['params']['post_type']) ) { + $current_url['params']['post_type'] = 'post'; + } + } + + //Hook-based submenu pages can be accessed via both "parent-page.php?page=foo" and "admin.php?page=foo". + //WP has a private API function for determining the canonical parent page for the current request. + if ( $this->endsWith($current_url['path'], '/admin.php') && is_callable('get_admin_page_parent') ) { + $real_parent = get_admin_page_parent('admin.php'); + if ( !empty($real_parent) && ($real_parent !== 'admin.php') ) { + $current_url['alt_path'] = str_replace('/admin.php', '/' . $real_parent, $current_url['path']); + } + } + + foreach($this->reverse_item_lookup as $url => $item) { + $item_url = $url; + //Convert to absolute URL. Caution: directory traversal (../, etc) is not handled. + if (strpos($item_url, '://') === false) { + if ( substr($item_url, 0, 1) == '/' ) { + $item_url = $base_site_url . $item_url; + } else { + if ( $admin_url_is_filtered ) { + $item_url = admin_url($item_url); + } else { + $item_url = $base_admin_url . ltrim( $item_url, '/' ); + } + } + } + $item_url = $this->parse_url($item_url); + + //Must match scheme, host, port, user, pass and path or alt_path. + $components = array('scheme', 'host', 'port', 'user', 'pass'); + $is_close_match = $this->urlPathsMatch($current_url['path'], $item_url['path']); + if ( !$is_close_match && isset($current_url['alt_path']) ) { + $is_close_match = $this->urlPathsMatch($current_url['alt_path'], $item_url['path']); + //Technically, we should also compare current[path] vs item[alt_path], + //but generating the alt_path for each menu item would be complicated. + } + foreach($components as $component) { + $is_close_match = $is_close_match && ($current_url[$component] == $item_url[$component]); + if ( !$is_close_match ) { + break; + } + } + + //Same as above - default post type is "post". + if ( $this->endsWith($item_url['path'], '/wp-admin/edit.php') || $this->endsWith($item_url['path'], '/wp-admin/post-new.php') ) { + if ( !isset($item_url['params']['post_type']) ) { + $item_url['params']['post_type'] = 'post'; + } + } + + //Special case: In WP 4.0+ the URL of the "Customize" menu changes often due to a "return" query parameter + //that contains the current page URL. To reliably recognize this item, we should ignore that parameter. + if ( $this->endsWith($item_url['path'], 'customize.php') ) { + unset($item_url['params']['return']); + } + + //The current URL must match all query parameters of the item URL. + $different_params = $this->arrayDiffAssocRecursive($item_url['params'], $current_url['params']); + + //The current URL must have as few extra parameters as possible. + $extra_params = $this->arrayDiffAssocRecursive($current_url['params'], $item_url['params']); + + $is_submenu = empty($item['is_top']); + + if ( + $is_close_match + && (count($different_params) == 0) + && ( + (count($extra_params) < $best_extra_params) + //When all else is equal, prefer submenu items. + || ( + (count($extra_params) === $best_extra_params) + && ($is_submenu && !$best_is_submenu) + ) + ) + ) { + $best_item = $item; + $best_extra_params = count($extra_params); + $best_is_submenu = $is_submenu; + } + } + + //Special case for CPTs: When the "Add New" menu is disabled by CPT settings (show_ui, etc), and someone goes + //to add a new item, WordPress highlights the "$CPT-Name" item as the current one. Lets do the same for + //consistency. See also: /wp-admin/post-new.php, lines #20 to #40. + if ( + ($best_item === null) + && isset($current_url['params']['post_type']) + && (!empty($current_url['params']['post_type'])) + && $this->endsWith($current_url['path'], '/wp-admin/post-new.php') + && isset($this->reverse_item_lookup['edit.php?post_type=' . $current_url['params']['post_type']]) + ) { + $best_item = $this->reverse_item_lookup['edit.php?post_type=' . $current_url['params']['post_type']]; + } + + $cached_item = $best_item; + return $best_item; + } + + /** + * Parse a URL and return its components. + * + * Returns an array that contains all of these components: 'scheme', 'host', 'port', 'user', 'pass', + * 'path', 'query', 'fragment' and 'params'. All entries are strings, except 'params' which is + * an associative array of query parameters and their values. + * + * @param string $url + * @return array + */ + private function parse_url($url) { + static $url_defaults = array( + 'scheme' => '', + 'host' => '', + 'port' => '80', + 'user' => '', + 'pass' => '', + 'path' => '', + 'query' => '', + 'fragment' => '', + ); + + $parsed = wp_parse_url($url); //Requires WP 4.7+ for full functionality. + if ( !is_array($parsed) ) { + $parsed = array(); + } + $parsed = array_merge($url_defaults, $parsed); + + $params = array(); + if ( !empty($parsed['query']) ) { + wp_parse_str($parsed['query'], $params); + }; + $parsed['params'] = $params; + + return $parsed; + } + + /** + * Get the difference of two arrays. + * + * This methods works like array_diff_assoc(), except it also supports nested arrays by comparing them recursively. + * + * @param array $array1 The base array. + * @param array $array2 The array to compare to. + * @return array An associative array of values from $array1 that are not present in $array2. + */ + private function arrayDiffAssocRecursive($array1, $array2) { + $difference = array(); + + foreach($array1 as $key => $value) { + if ( !array_key_exists($key, $array2) ) { + $difference[$key] = $value; + continue; + } + + $otherValue = $array2[$key]; + if ( is_array($value) !== is_array($otherValue) ) { + //If only one of the two values is an array then they can't be equal. + $difference[$key] = $value; + } elseif ( is_array($value) ) { + //Compare array values recursively. + $subDiff = $this->arrayDiffAssocRecursive($value, $otherValue); + if( !empty($subDiff) ) { + $difference[$key] = $subDiff; + } + + //Like the original array_diff_assoc(), we compare the values as strings. + } elseif ( (string)$value !== (string)$array2[$key] ) { + $difference[$key] = $value; + } + } + + return $difference; + } + + /** + * Check if two paths match. Intended for comparing WP admin URLs. + * + * @param string $path1 + * @param string $path2 + * @return bool + */ + private function urlPathsMatch($path1, $path2) { + if ( $path1 == $path2 ) { + return true; + } + + // "/wp-admin/index.php" should match "/wp-admin/". + static $wpAdminDir = null; + if ( $wpAdminDir === null ) { + $wpAdminDir = '/wp-admin/'; + if ( has_filter('admin_url') ) { + //Detect modified admin base URLs. For example, some security and branding plugins + //replace "wp-admin" with "something-else". + $suffix = 'ame-4425-admin-path-test'; + $testUrl = self_admin_url($suffix); + $lastSlash = strrpos($testUrl, '/', -strlen($suffix) + 1); + if ( $lastSlash !== false ) { + $firstSlash = strrpos($testUrl, '/', -strlen($suffix) - 2); + if ( ($firstSlash !== false) && ($firstSlash !== $lastSlash) ) { + $wpAdminDir = substr($testUrl, $firstSlash, $lastSlash - $firstSlash + 1); + } + } + } + } + + if ( + ($this->endsWith($path1, $wpAdminDir . 'index.php') && $this->endsWith($path2, $wpAdminDir)) + || ($this->endsWith($path2, $wpAdminDir . 'index.php') && $this->endsWith($path1, $wpAdminDir)) + ) { + return true; + } + + return false; + } + + /** + * Determine if the input $string ends with the specified $suffix. + * + * @param string $string + * @param string $suffix + * @return bool + */ + private function endsWith($string, $suffix) { + $len = strlen($suffix); + if ( $len == 0 ) { + return true; + } + $inputLen = strlen($string); + if ( $len > $inputLen ) { + return false; + } + return substr_compare($string, $suffix, $inputLen - $len) === 0; + } + + public function castValuesToBool($capabilities) { + if ( !is_array($capabilities) ) { + if ( empty($capabilities) ) { + $capabilities = array(); + } else { + //phpcs:disable WordPress.PHP.DevelopmentFunctions + //This should never happen, but if it does, it's not a critical error, so an exception + //doesn't seem warranted. We'll log a warning so that technical users can investigate. + trigger_error( + //WP coding standard thinks some users will have display_errors enabled, + //so, regrettably, the error message needs to be escaped. + esc_html("Unexpected capability array: " . print_r($capabilities, true)), + E_USER_WARNING + ); + return array(); + //phpcs:enable + } + } + foreach($capabilities as $capability => $value) { + $capabilities[$capability] = (bool)$value; + } + return $capabilities; + } + + public function display_survey_notice() { + //Handle the survey notice + $hide_param_name = 'ame_hide_survey_notice'; + if ( isset($this->get[$hide_param_name]) ) { + $this->options['display_survey_notice'] = empty($this->get[$hide_param_name]); + $this->save_options(); + } + + $display_notice = $this->options['display_survey_notice'] && $this->current_user_can_edit_menu(); + if ( isset($this->options['first_install_time']) ) { + $minimum_usage_period = 7*24*3600; + $display_notice = $display_notice && ((time() - $this->options['first_install_time']) > $minimum_usage_period); + } + + //Only display the notice on the Menu Editor (Pro) page. + $display_notice = $display_notice && isset($this->get['page']) && ($this->get['page'] == 'menu_editor'); + + //Let the user override this completely (useful for client sites). + if ( $display_notice && file_exists(dirname($this->plugin_file) . '/never-display-surveys.txt') ) { + $display_notice = false; + $this->options['display_survey_notice'] = false; + $this->save_options(); + } + + if ( $display_notice ) { + $free_survey_url = 'https://docs.google.com/spreadsheet/viewform?formkey=dERyeDk0OWhlbkxYcEY4QTNaMnlTQUE6MQ'; + $pro_survey_url = 'https://docs.google.com/spreadsheet/viewform?formkey=dHl4MnlHaVI3NE5JdVFDWG01SkRKTWc6MA'; + + if ( $this->is_pro_version() ) { + $survey_url = $pro_survey_url; + } else { + $survey_url = $free_survey_url; + } + + $hide_url = add_query_arg($hide_param_name, 1); + printf( + '<div class="updated"> + <p><strong>Help improve Admin Menu Editor - take the user survey!</strong></p> + <p><!--suppress HtmlUnknownTarget --><a href="%s" target="_blank" title="Opens in a new window">Take the survey</a></p> + <p><!--suppress HtmlUnknownTarget --><a href="%s">Hide this notice</a></p> + </div>', + esc_attr($survey_url), + esc_attr($hide_url) + ); + } + } + + /** + * Capture $_GET and $_POST in $this->get and $this->post. + * Slashes added by "magic quotes" will be stripped. + * + * @return void + */ + function capture_request_vars(){ + //phpcs:disable WordPress.Security.NonceVerification -- This just captures the request vars. Any verification happens later. + $this->post = $this->originalPost = $_POST; + $this->get = $_GET; + + if ( + version_compare(phpversion(), '7.4.0alpha1', '<') + && function_exists('get_magic_quotes_gpc') + && get_magic_quotes_gpc() + ) { + $this->post = stripslashes_deep($this->post); + $this->get = stripslashes_deep($this->get); + } + //phpcs:enable + } + + /** + * Get POST parameters for the current request. + * + * @return array + */ + public function get_post_params() { + return $this->post; + } + + /** + * Get query parameters for the current request. + * + * @return array + */ + public function get_query_params() { + return $this->get; + } + + public function enqueue_helper_scripts() { + wp_enqueue_script( + 'ame-helper-script', + plugins_url('js/admin-helpers.js', $this->plugin_file), + array('jquery'), + '20160407-2' + ); + + //The helper script needs to know the custom page heading (if any) to apply it. + $currentItem = $this->get_current_menu_item(); + if ( $currentItem && !empty($currentItem['page_heading']) ) { + wp_localize_script( + 'ame-helper-script', + 'wsAmeCurrentMenuItem', + array( + 'customPageHeading' => $currentItem['page_heading'], + 'pageHeadingSelector' => + version_compare(self::get_wp_version(), '4.3', '<') ? '.wrap > h2:first' : '.wrap > h1:first', + ) + ); + } + } + + public function enqueue_helper_styles() { + wp_enqueue_style( + 'ame-helper-style', + plugins_url('css/admin.css', $this->plugin_file), + array(), + '20220912' + ); + + if ( $this->options['force_custom_dashicons'] ) { + //Optimization: Only add the stylesheet if the menu actually has custom dashicons. + $menu = $this->load_custom_menu(); + if ( $menu && !empty($menu['has_modified_dashicons']) ) { + wp_enqueue_style( + 'ame-force-dashicons', + plugins_url('css/force-dashicons.css', $this->plugin_file), + array(), + '20210226' + ); + } + } + } + + /** + * Get one of the plugin configuration values. + * + * @param string $name Option name. + * @return mixed|null + */ + public function get_plugin_option($name) { + if ( array_key_exists($name, $this->options) ) { + return $this->options[$name]; + } + return null; + } + + /** + * Update a plugin configuration value. Saves immediately. + * + * @param string $name + * @param mixed $value + */ + public function set_plugin_option($name, $value) { + $this->options[$name] = $value; + $this->save_options(); + } + + /** + * Update multiple plugin configuration values. Saves immediately. + * + * @param array $options An dictionary of key => value pairs. + */ + public function set_many_plugin_options($options) { + foreach($options as $key => $value) { + $this->options[$key] = $value; + } + $this->save_options(); + } + + /** + * Log a security-related message. + * + * @param string|array $message The message to add to the log, or an array of messages. Should be HTML safe. + */ + private function log_security_note($message) { + if ( !$this->should_store_security_log() ) { + return; + } + if ( is_array($message) ) { + $this->security_log = array_merge($this->security_log, $message); + } else { + $this->security_log[] = $message; + } + } + + private function should_store_security_log() { + return ( + $this->options['security_logging_enabled'] + || ($this->options['error_verbosity'] >= self::VERBOSITY_VERBOSE) + ); + } + + /** + * Callback for "admin_notices". + */ + public function display_security_log() { + ?> + <div class="updated"> + <h3>Admin Menu Editor security log</h3> + <?php + //Log formatting uses HTML, and log contents should already be escaped. + //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $this->get_formatted_security_log(); + ?> + </div> + <?php + } + + /** + * Get the security log in HTML format. + * + * @return string + */ + private function get_formatted_security_log() { + $log = '<div style="font: 12px/17px Consolas, monospace; margin-bottom: 1em;">'; + $log .= implode("<br>\n", $this->security_log); + $log .= '</div>'; + return $log; + } + + public function get_security_log() { + return $this->security_log; + } + + /** + * WPML support: Update strings that need translation. + * + * @param array $old_menu The old custom menu, if any. + * @param array $custom_menu The new custom menu. + */ + private function update_wpml_strings($old_menu, $custom_menu) { + if ( !function_exists('icl_register_string') ) { + return; + } + + $previous_strings = $this->get_wpml_strings($old_menu); + $new_strings = $this->get_wpml_strings($custom_menu); + + //Delete strings that are no longer valid. + if ( function_exists('icl_unregister_string') ) { + $removed_strings = array_diff_key($previous_strings, $new_strings); + foreach($removed_strings as $name => $value) { + icl_unregister_string(self::WPML_CONTEXT, $name); + } + } + + //Register/update the new menu strings. + foreach($new_strings as $name => $value) { + icl_register_string(self::WPML_CONTEXT, $name, $value); + } + } + + /** + * Prepare WPML translation strings for all menu and page titles + * in the specified menu. Includes only custom titles. + * + * @param array $custom_menu + * @return array Associative array of strings that can be translated, indexed by unique name. + */ + private function get_wpml_strings($custom_menu) { + if ( empty($custom_menu) ) { + return array(); + } + + $strings = array(); + $translatable_fields = array('menu_title', 'page_title'); + foreach($custom_menu['tree'] as $top_menu) { + if ( $top_menu['separator'] ) { + continue; + } + + foreach($translatable_fields as $field) { + if ( isset($top_menu[$field]) ) { + $name = $this->get_wpml_name_for($top_menu, $field); + $strings[$name] = ameMenuItem::get($top_menu, $field); + } + } + + if ( isset($top_menu['items']) && !empty($top_menu['items']) ) { + foreach($top_menu['items'] as $item) { + if ( $item['separator'] ) { + continue; + } + + foreach($translatable_fields as $field) { + if ( isset($item[$field]) ) { + $name = $this->get_wpml_name_for($item, $field); + $strings[$name] = ameMenuItem::get($item, $field); + } + } + } + } + } + + return $strings; + } + + /** + * Create a unique name for a specific field of a specific menu item. + * Intended for use with the icl_register_string() function. + * + * @param array $item Admin menu item in the internal format. + * @param string $field Field name. + * @return string + */ + private function get_wpml_name_for($item, $field = '') { + $name = ameMenuItem::get($item, 'template_id'); + if ( empty($name) ) { + $name = 'custom: ' . ameMenuItem::get($item, 'file'); + } + if ( !empty($field) ) { + $name = $name . '[' . $field. ']'; + } + return $name; + } + + /** + * Compatibility fix for bbPress 2.5.3. + * + * bbPress creates a bunch of "hidden" menu items in the admin_menu action only to remove them + * later in an admin_head hook. This results in apparently duplicated menus showing up when AME is + * active because AME processes the items before they get removed. + * + * This method works around the issue by explicitly removing those bbPress menus. + * + * @uses $this->default_wp_submenu + */ + private function apply_bbpress_compat_fix() { + if ( !isset($this->default_wp_submenu, $this->default_wp_submenu['index.php']) ) { + return; + } + + //Note to self: This would be easier if we could rely on anonymous function support being available. + //Then we could just array_filter() the submenu with a closure as the callback. + $items_to_remove = array('bbp-about' => null, 'bbp-credits' => null); + foreach($this->default_wp_submenu['index.php'] as $index => $menu) { + if ( array_key_exists($menu[2], $items_to_remove) ) { + $items_to_remove[$menu[2]] = $index; + } + } + + foreach($items_to_remove as $index) { + if ( isset($index, $this->default_wp_submenu['index.php'][$index]) ) { + unset($this->default_wp_submenu['index.php'][$index]); + } + } + } + + /** + * Compatibility fix for WooCommerce 2.2.1+. + * Summary: When AME is active, an unusable WooCommerce -> WooCommerce menu item shows up. Here we remove it. + * + * WooCommerce creates a top level "WooCommerce" menu with no callback. By default, WordPress automatically adds + * a submenu item with the same name. However, since the item doesn't have a callback, it is unusable and clicking + * it just triggers a "Cannot load woocommerce" error. So WooCommerce removes this item in an admin_head hook to + * hide it. With AME active, the item shows up anyway, and users get confused by the error. + * + * Fix it by removing the problematic menu item. + * + * Caution: If the user hides all WooCommerce submenus but not the top level menu, the WooCommerce menu will still + * show up but be inaccessible. This may be slightly counter-intuitive, but seems reasonable. + */ + private function apply_woocommerce_compat_fix() { + if ( !isset($this->default_wp_submenu, $this->default_wp_submenu['woocommerce']) ) { + return; + } + + $badSubmenuExists = isset($this->default_wp_submenu['woocommerce'][0]) + && isset($this->default_wp_submenu['woocommerce'][0][2]) + && ($this->default_wp_submenu['woocommerce'][0][2] === 'woocommerce'); + $anotherSubmenuExists = isset($this->default_wp_submenu['woocommerce'][1]); + + if ( $badSubmenuExists && $anotherSubmenuExists ) { + $this->default_wp_submenu['woocommerce'][0] = $this->default_wp_submenu['woocommerce'][1]; + unset($this->default_wp_submenu['woocommerce'][1]); + } + } + + /** + * Compatibility fix for WooCommerce 2.6.8+. + * + * Summary: The "WooCommerce -> Orders" menu item includes an info bubble showing the number of new orders. + * When AME is active, this number doesn't show up. This workaround re-adds the info bubble. + * + * For some inexplicable reason, WooCommerce first creates the "Orders" menu item without the info bubble. + * Then it adds the number of new orders later by modifying the global $submenu array in a separate "admin_head" + * hook. However, by that time AME has already processed the admin menu, so it doesn't see the change. + * + * Workaround: Run the relevant WooCommerce callback during the "admin_menu" action (before processing the menu). + * The now-redundant"admin_head" hook is then removed. + */ + private function apply_woocommerce_order_count_fix() { + global $wp_filter; + if ( !class_exists('WC_Admin_Menus', false) || !isset($wp_filter['admin_head'][10]) || did_action('admin_head') ) { + return; + } + + //Find the WooCommerce callback that adds order count to the menu. + //It's the menu_order_count method defined in /woocommerce/includes/admin/class-wc-admin-menus.php. + foreach($wp_filter['admin_head'][10] as $key => $filter) { + if (!isset($filter['function']) || !is_array($filter['function'])) { + continue; + } + + $callback = $filter['function']; + if ( + (count($callback) === 2) + && ($callback[1] === 'menu_order_count') + && (get_class($callback[0]) === 'WC_Admin_Menus') + ) { + //Run it now, not in admin_head. + call_user_func($callback); + remove_action('admin_head', $callback, 10); + break; + } + } + } + + /** + * Compatibility fix for WordPress Mu Domain Mapping 0.5.4.3. + * + * The aforementioned domain mapping plugin has a bug that makes the plugins_url() function + * return incorrect URLs for plugins installed in /mu-plugins. Fixed by removing the offending + * filter callback. + * + * Note that this won't break domain mapping. Domain Mapping adds two 'plugins_url' filters. + * The buggy one is completely redundant and can be removed with no ill effects. + */ + private function apply_wpmu_domain_mapping_fix() { + $priority = has_filter('plugins_url', 'domain_mapping_plugins_uri'); + if ( ($priority !== false) && (has_filter('plugins_url', 'domain_mapping_post_content') !== false) ) { + remove_filter('plugins_url', 'domain_mapping_plugins_uri', $priority); + } + } + + /** + * Compatibility fix for Divi Training 1.3.5. + * + * The Divi Training plugin adds a whole lot of "hidden" submenu items to the Dashboard menu + * and then removes them later. Lets get rid of them. + */ + private function apply_divi_training_fix() { + if ( !class_exists('Wm_Divi_Training_Admin', false) ) { + return; + } + if ( !isset($this->default_wp_submenu, $this->default_wp_submenu['index.php']) ) { + return; + } + + $items_to_remove = array(); + foreach($this->default_wp_submenu['index.php'] as $index => $menu) { + //There's a lot of items, so we search for a common prefix instead of of including an explicit list. + // + if ( (strpos($menu[2], 'wm-divi-training-the-divi-') === 0) || ($menu[2] === 'wm-divi-training-updates')) { + $items_to_remove[] = $index; + } + } + foreach($items_to_remove as $index) { + if ( isset($index, $this->default_wp_submenu['index.php'][$index]) ) { + unset($this->default_wp_submenu['index.php'][$index]); + } + } + } + + /** + * Compatibility fix for MailPoet 3. Last tested with MailPoet 3.44.0. + * + * MailPoet deliberately removes all third-party stylesheets from its admin pages. + * As a result, some AME features that use stylesheets - like custom menu icons and admin + * menu colors - don't work on those pages. Let's fix that by whitelisting our styles. + */ + private function apply_mailpoet_compat_fix() { + add_filter('mailpoet_conflict_resolver_whitelist_style', array($this, '_whitelist_ame_styles_for_mailpoet')); + } + + /** + * @internal + * @param array $styles + * @return array + */ + public function _whitelist_ame_styles_for_mailpoet($styles) { + $styles[] = 'ame_output_menu_color_css'; + $styles[] = 'font-awesome\.css'; + $styles[] = 'force-dashicons\.css'; + return $styles; + } + + /** + * As of WP 3.5, the Links Manager is hidden by default. It's only visible if the user has existing links + * or they choose to enable it by installing the Links Manager plugin. + * + * However, the "Links" menu still exists. This can be confusing to users who will now see an apparently + * useless menu item that can't be enabled (since they don't have the Links Manager plugin) and can't be + * deleted either (since it's a default menu). To remedy that, hide the default "Links" menu. + */ + private function remove_link_manager_menus() { + //Find the "Links" menu. + $links_index = null; + $links_slug = null; + foreach($this->default_wp_menu as $index => $menu) { + if ( ($menu[1] === 'manage_links') && isset($menu[5]) && ($menu[5] === 'menu-links') ) { + $links_index = $index; + $links_slug = $menu[2]; + } + } + + //Remove the default "Links" submenus, but leave custom items created by other plugins. + if ( isset($this->default_wp_submenu[$links_slug]) ) { + $this->default_wp_submenu[$links_slug] = array_filter( + $this->default_wp_submenu[$links_slug], + array($this, 'filter_default_links_submenus') + ); + if ( empty($this->default_wp_submenu[$links_slug]) ) { + unset($this->default_wp_submenu[$links_slug]); + } + } + + //Remove the "Links" menu itself if it no longer has any children. + if ( !isset($this->default_wp_submenu[$links_slug]) ) { + unset($this->default_wp_menu[$links_index]); + } + } + + private function filter_default_links_submenus($item) { + $default_items = array('link-manager.php', 'link-add.php', 'edit-tags.php?taxonomy=link_category'); + $is_default = isset($item[2]) && in_array($item[2], $default_items); + return !$is_default; + } + + /** + * Get a user's roles. + * + * "Why not just read the $user->roles array directly?", you may ask. Because some popular plugins have a really + * nasty bug where they inadvertently remove entries from that array. Specifically, they retrieve the first user + * role like this: + * + * $roleName = array_shift($currentUser->roles); + * + * What some plugin developers fail to realize is that, in addition to returning the first entry, array_shift() + * also *removes* it from the array. As a result, $user->roles is now missing one of the user's roles. This bug + * doesn't cause major problems only because most plugins check capabilities and don't care about roles as such. + * AME needs to know the roles because some menu permissions are set per role. + * + * Known buggy plugins: + * - W3 Total Cache 0.9.4.1 + * + * The current workaround is to cache the role list before it can get corrupted by other plugins. This approach + * has its own risks (cache invalidation is hard), but it should be reasonably safe assuming that everyone uses + * only standard WP APIs to modify user roles (e.g. @see WP_User::add_role ). + * + * @param WP_User $user + * @return array + */ + public function get_user_roles($user) { + if ( empty($user) ) { + return array(); + } + if ( !$user->exists() ) { + //Note: In rare cases, WP_User::$roles can be false. For AME it's more convenient to have an empty list. + return (!empty($user->roles) ? $user->roles : array()); + } + + if ( !isset($this->cached_user_roles[$user->ID]) ) { + $this->cached_user_roles[$user->ID] = $this->extract_user_roles($user); + } + return $this->cached_user_roles[$user->ID]; + } + + /** + * The current user has changed; update role and capability caches. + */ + public function update_current_user_cache() { + $user = wp_get_current_user(); + if ( empty($user) || !$user->exists() ) { + return; + } + + //Workaround for buggy plugins that unintentionally remove user roles. + /** @see WPMenuEditor::get_user_roles */ + $this->cached_user_roles[$user->ID] = $this->extract_user_roles($user); + + $this->update_virtual_cap_cache($user); + } + + /** + * @param WP_User $user + */ + private function update_virtual_cap_cache($user) { + if ( $user === null ) { + return; + } + + $virtual_caps = array( + self::ALL_VIRTUAL_CAPS => array(), + self::DIRECTLY_GRANTED_VIRTUAL_CAPS => array(), + ); + + //Create a virtual 'super_admin' capability that only super admins have. Be careful not to overwrite + //the same cap added by other plugins. For example, Advanced Access Manager also adds this capability. + if ( !isset($user->allcaps['super_admin']) ) { + $virtual_caps[self::ALL_VIRTUAL_CAPS]['super_admin'] = is_multisite() && is_super_admin($user->ID); + } + + $virtual_caps = apply_filters('admin_menu_editor-virtual_caps', $virtual_caps, $user); + $this->cached_virtual_user_caps[$user->ID] = $virtual_caps; + } + + /** + * Grant virtual caps to the user. + * + * @param array $capabilities All capabilities belonging to the specified user, cap => true/false. + * @param array $required_caps The required capabilities. + * @param array $args The capability passed to current_user_can, the user's ID, and other args. + * @return array Filtered list of capabilities. + */ + function grant_virtual_caps_to_user($capabilities, /** @noinspection PhpUnusedParameterInspection */ $required_caps, $args){ + $this->virtual_caps_for_this_call = array(); + + if ( $this->disable_virtual_caps ) { + return $capabilities; + } + + //The second entry of the $args array should be the user ID + if ( count($args) < 2 ) { + return $capabilities; + } + $user_id = intval($args[1]); + + if ( !isset($this->cached_virtual_user_caps[$user_id]) ) { + $this->update_virtual_cap_cache($this->get_user_by_id($user_id)); + } + + if ( empty($this->cached_virtual_user_caps[$user_id][$this->virtual_cap_mode]) ) { + return $capabilities; + } + + $this->virtual_caps_for_this_call = $this->cached_virtual_user_caps[$user_id][$this->virtual_cap_mode]; + + $capabilities = array_merge($capabilities, $this->virtual_caps_for_this_call); + return $capabilities; + } + + /** + * Set the capabilities that were already set by grant_virtual_caps_to_user() again. + * + * The goal of granting the same capabilities twice at different hook priorities is to: + * 1) Make sure meta caps that rely on the granted caps are enabled. + * 2) Reduce the risk that the granted caps will be overridden by other plugins. + * + * @param array $capabilities + * @return array + */ + public function regrant_virtual_caps_to_user($capabilities) { + if ( !empty($this->virtual_caps_for_this_call) ) { + $capabilities = array_merge($capabilities, $this->virtual_caps_for_this_call); + $this->virtual_caps_for_this_call = array(); + } + return $capabilities; + } + + /** + * Get user roles by parsing their capabilities. + * + * This method is reliable because it determines user roles the same way that WordPress does. However, it's also + * relatively "slow" (~ 25 microseconds on my dev. system). Don't call it directly. Use get_user_roles() instead - + * it caches results. + * + * @see WP_User::get_role_caps + * + * @param WP_User $user + * @return array + */ + private function extract_user_roles($user) { + if ( empty($user->caps) || !is_array($user->caps) ) { + return (!empty($user->roles) ? $user->roles : array()); + } + $wp_roles = ameRoleUtils::get_roles(); + return array_filter(array_keys($user->caps), array($wp_roles, 'is_role')); + } + + /** + * User metadata was updated or deleted; refresh or invalidate the associated role/capability caches. + * + * Not all metadata updates are related to role changes, but filtering them is non-trivial (meta keys change). + * + * @param int|array $unused_meta_id + * @param int $user_id + * @param string $meta_key + * @noinspection PhpUnusedParameterInspection + */ + public function on_user_metadata_changed($unused_meta_id, $user_id, $meta_key) { + if ( empty($user_id) || !is_numeric($user_id) ) { + return; + } + //Clear the user role cache. + unset($this->cached_user_roles[$user_id]); + + $this->virtual_caps_for_this_call = array(); + + //Did this update change user capabilities or roles? If so, refresh virtual caps. + $user = $this->get_user_by_id($user_id); + if ( $meta_key === $user->cap_key ) { + $this->update_virtual_cap_cache($user); + } + } + + /** + * Get the user object based on a user ID. + * + * In most cases, when this plugin needs to retrieve a user, it is the current user. This method + * attempts to make that common case faster. + * + * @param int $user_id + * @return WP_User|null + */ + private function get_user_by_id($user_id) { + //Usually, pluggable functions will already be loaded by this point, + //but there is at least one plugin that indirectly triggers this method + //before wp_get_current_user() is available by checking user caps early. + if ( function_exists('wp_get_current_user') ) { + $current_user = wp_get_current_user(); + if ( $current_user && ($current_user->ID == $user_id) ) { + return $current_user; + } + } + + if ( function_exists('get_user_by') ) { + $user = get_user_by('id', $user_id); + if ( $user === false ) { + return null; + } else { + return $user; + } + } + + return null; + } + + /** + * Get registered public post types. + * @return array + */ + private function get_post_type_details() { + $results = array(); + + $post_types = get_post_types(array('public' => true, 'show_ui' => true), 'objects', 'or'); + $meta_caps = array('edit_post', 'read_post', 'delete_post'); + + foreach($post_types as $id => $post_type) { + $title = $id; + if (isset($post_type->labels, $post_type->labels->name) && !empty($post_type->labels->name)) { + $title = $post_type->labels->name; + } + + $capabilities = array(); + foreach((array)$post_type->cap as $cap_type => $capability) { + //Skip meta caps. + if ($post_type->map_meta_cap && in_array($cap_type, $meta_caps)) { + continue; + } + + //Skip the "read" cap. It's redundant - most CPTs use it, and all roles have it by default. + if (($cap_type === 'read') && ($capability === 'read')) { + continue; + } + + $capabilities[$cap_type] = $capability; + } + + $results[$id] = array( + 'id' => $id, + 'title' => $title, + 'capabilities' => $capabilities, + ); + } + + return $results; + } + + /** + * Get registered taxonomies. + * @return array + */ + private function get_taxonomy_details() { + $results = array(); + $taxonomies = get_taxonomies(array('public' => true, 'show_ui' => true), 'objects', 'or'); + + foreach($taxonomies as $id => $taxonomy) { + $title = $id; + if (isset($taxonomy->labels, $taxonomy->labels->name) && !empty($taxonomy->labels->name)) { + $title = $taxonomy->labels->name; + } + + $capabilities = array(); + foreach((array)$taxonomy->cap as $cap_type => $capability) { + //Skip the "read" cap. It's redundant - most CPTs use it, and all roles have it by default. + if (($cap_type === 'read') && ($capability === 'read')) { + continue; + } + $capabilities[$cap_type] = $capability; + } + + $results[$id] = array( + 'id' => $id, + 'title' => $title, + 'capabilities' => $capabilities, + ); + } + + return $results; + } + + /** + * Tell new users how to access the plugin settings page. + */ + public function display_plugin_menu_notice() { + //Display the notice only if it's enabled, the current user can access our settings page, + //and there is no custom menu (if a custom menu already exists, chances are the user knows + //where the settings page is). + $showNotice = $this->options['show_plugin_menu_notice'] && ($this->load_custom_menu() === null); + $showNotice = $showNotice && $this->current_user_can_edit_menu(); + if ( !$showNotice ) { + return; + } + + //Disable the notice when the user hides it or visits any of our admin pages. + $hideNoticeParameter = 'ame-plugin-menu-notice'; + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Just hiding an optional help message. + if ( !empty($_GET[$hideNoticeParameter]) || $this->is_editor_page() || $this->is_settings_page() ) { + $this->options['show_plugin_menu_notice'] = false; + $this->save_options(); + return; + } + + $dismissUrl = add_query_arg($hideNoticeParameter, 'hide'); + $dismissUrl = remove_query_arg(array('message', 'activate'), $dismissUrl); + + if ( is_multisite() && is_network_admin() ) { + if ( $this->is_pro_version() ) { + $message = 'Tip: Go to any subsite to edit the regular admin menu. Or go to <a href="%1$s">Settings -> %2$s</a> ' + . 'in the network admin to edit the network admin menu, roles, and so on.'; + } else { + $message = 'Tip: Go to any subsite to access Admin Menu Editor. It will not show up in the network admin.'; + } + } else { + $message = 'Tip: Go to <a href="%1$s">Settings -> %2$s</a> to start customizing the admin menu.'; + } + printf( + //phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- $message can be a HTML template. + '<div class="updated" id="ame-plugin-menu-notice"> + <p>' . $message . '</p> + <p><a href="%3$s" id="ame-hide-plugin-menu-notice">Hide this message</a></p> + </div>', + //phpcs:enable + esc_url(self_admin_url($this->settings_link)), + esc_html(apply_filters('admin_menu_editor-self_menu_title', 'Menu Editor')), + esc_url($dismissUrl) + ); + + } + + public function is_pro_version() { + return apply_filters('admin_menu_editor_is_pro', false); + } + + /** + * Get the WordPress version number. + * + * Warning: Some plugins change the WordPress version number to hide the installed version from visitors. + * It's a security-by-obscurity technique. This means you can't rely on the number being correct. + * + * @return string Either the version number or an empty string. + */ + private static function get_wp_version() { + if ( isset($GLOBALS['wp_version']) ) { + return $GLOBALS['wp_version']; + } + return ''; + } + + /** + * @return array + */ + private function load_cap_power() { + $cap_power = array(); + + $power_filename = AME_ROOT_DIR . '/includes/capabilities/cap-power.csv'; + if ( is_file($power_filename) && is_readable($power_filename) ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen -- Should be fine, we only need read permissions. + $csv = fopen($power_filename, 'r'); + $firstLineSkipped = false; + + while ($csv && !feof($csv)) { + $line = fgetcsv($csv, 1000, ';'); + if ( !$firstLineSkipped ) { + $firstLineSkipped = true; + continue; + } + + if ( is_array($line) && (count($line) >= 2) ) { + $cap_power[strval($line[0])] = floatval(str_replace(',', '.', $line[1])); + } + } + fclose($csv); + + arsort($cap_power); + } + + return $cap_power; + } + + public function add_plugin_row_meta_links($pluginMeta, $pluginFile) { + $isRelevant = ($pluginFile == $this->plugin_basename); + + if ( $isRelevant && $this->current_user_can_edit_menu() ) { + $documentationUrl = $this->is_pro_version() + ? 'https://adminmenueditor.com/documentation/' + : 'https://adminmenueditor.com/free-version-docs/'; + $pluginMeta[] = sprintf( + '<a href="%s">%s</a>', + esc_attr($documentationUrl), + 'Documentation' + ); + } + + return $pluginMeta; + } + + private function get_active_modules() { + $modules = $this->get_available_modules(); + + $activeModules = array(); + foreach ($modules as $id => $module) { + if ( $this->is_module_active($id, $module) ) { + $activeModules[$id] = $module; + } + } + + return $activeModules; + } + + public function get_available_modules() { + $modules = array( + 'actor-selector' => array( + 'relativePath' => 'modules/actor-selector/actor-selector.php', + 'className' => 'ameActorSelector', + 'isAlwaysActive' => true, + ), + 'visible-users' => array( + 'relativePath' => 'extras/modules/visible-users/visible-users.php', + 'className' => 'ameVisibleUsers', + 'isAlwaysActive' => true, + ), + 'metaboxes' => array( + 'relativePath' => 'extras/modules/metaboxes/load.php', + 'className' => 'ameMetaBoxEditor', + 'requiredPhpVersion' => '5.3', + 'title' => 'Meta Boxes', + ), + 'dashboard-widget-editor' => array( + 'relativePath' => 'extras/modules/dashboard-widget-editor/load.php', + 'className' => 'ameWidgetEditor', + 'requiredPhpVersion' => '5.3', + 'title' => 'Dashboard Widgets', + ), + 'redirector' => array( + 'relativePath' => 'modules/redirector/redirector.php', + 'className' => '\\YahnisElsts\\AdminMenuEditor\\Redirects\\Module', + 'title' => 'Redirects', + 'requiredPhpVersion' => '5.6.20', //Same as WP 5.8. + ), + 'plugin-visibility' => array( + 'relativePath' => 'modules/plugin-visibility/plugin-visibility.php', + 'className' => 'amePluginVisibility', + 'title' => 'Plugins', + ), + 'super-users' => array( + 'relativePath' => 'extras/modules/super-users/super-users.php', + 'className' => 'ameSuperUsers', + 'title' => 'Hidden Users', + ), + /*'admin-css' => array( + 'relativePath' => 'modules/admin-css/admin-css.php', + 'className' => 'ameAdminCss', + 'title' => 'Admin CSS', + ),*/ + 'hide-admin-menu' => array( + 'relativePath' => 'extras/modules/hide-admin-menu/hide-admin-menu.php', + 'className' => 'ameAdminMenuHider', + 'title' => '"Show the admin menu" checkbox', + ), + 'hide-admin-bar' => array( + 'relativePath' => 'extras/modules/hide-admin-bar/hide-admin-bar.php', + 'className' => 'ameAdminBarHider', + 'title' => '"Show the Toolbar" checkbox', + ), + 'easy-hide' => array( + 'relativePath' => 'extras/modules/easy-hide/easy-hide.php', + 'className' => '\\YahnisElsts\\AdminMenuEditor\\EasyHide\\Core', + 'title' => 'Easy Hide', + 'requiredPhpVersion' => '5.6.20', + 'requiredMethods' => array(array('WP_Error', 'merge_from')), + ), + 'highlight-new-menus' => array( + 'relativePath' => 'modules/highlight-new-menus/highlight-new-menus.php', + 'className' => 'ameMenuHighlighterWrapper', + 'title' => 'Highlight new menu items', + 'requiredPhpVersion' => '5.3', + ), + ); + + foreach($modules as &$module) { + if (!empty($module['relativePath'])) { + $module['path'] = AME_ROOT_DIR . '/' . $module['relativePath']; + } + } + unset($module); + + $modules = apply_filters('admin_menu_editor-available_modules', $modules); + + $modules = array_filter($modules, array($this, 'module_path_exists')); + + return $modules; + } + + private function module_path_exists($module) { + return !empty($module['path']) && file_exists($module['path']); + } + + public function is_module_compatible($module) { + if ( !empty($module['requiredPhpVersion']) ) { + if ( !version_compare(phpversion(), $module['requiredPhpVersion'], '>=') ) { + return false; + } + } + if ( !empty($module['requiredMethods']) ) { + foreach ($module['requiredMethods'] as $item) { + if ( !method_exists($item[0], $item[1]) ) { + return false; + } + } + } + return true; + } + + public function is_module_active($id, $module) { + if ( !$this->is_module_compatible($module) ) { + return false; + } + if ( !empty($module['isAlwaysActive']) ) { + return true; + } + if ( isset($this->options['is_active_module'][$id]) ) { + return $this->options['is_active_module'][$id]; + } + return true; + } + + /** + * @return bool + */ + public function is_custom_menu_deep() { + return $this->custom_menu_is_deep; + } + + /** + * @param \YahnisElsts\AdminMenuEditor\EasyHide\HideableItemStore $store + * @return void + */ + public function register_hideable_items($store) { + try { + $menu = $this->get_active_admin_menu(); + if ( empty($menu['tree']) ) { + return; + } + } catch (LogicException $ex) { + //This should never happen because the hiding module should + //run after the menu is done, but let's not crash if it happens. + return; + } + + $cat = $store->getOrCreateCategory( + 'admin-menu', + 'Admin Menu', + null, + true + ); + + $this->register_menus_as_hideable($store, $menu['tree'], null, 1, $cat); + + //Also, register visible components. + //The word "component" is used in at least two distinct senses here, which is not ideal. + $componentsByItemId = apply_filters('admin_menu_editor-hideable_vis_components', array()); + foreach($componentsByItemId as $itemId => $properties) { + $store->addItem( + $itemId, + $properties['label'], + array($store->getOrCreateCategory( + 'admin-ui', + 'General', + null, + true + )->setSortPriority(1)), + null, + ameUtils::get($menu, array('component_visibility', $properties['component']), array()), + 'admin-menu' + ); + } + } + + /** + * @param \YahnisElsts\AdminMenuEditor\EasyHide\HideableItemStore $store + * @param array[] $menus + */ + private function register_menus_as_hideable( + $store, + $menus, + $parent, + $level, + $category + ) { + foreach ($menus as $key => $item) { + $id = $this->make_hideable_item_id($item, $level); + + $label = ameMenuItem::get($item, 'menu_title', ''); + if ( $label !== '' ) { + $label = trim(wp_strip_all_tags(ameMenuItem::remove_update_count($label))); + } else { + $label = '[' . $key . ']'; + } + + $hideableItem = $store->addItem( + $id, + $label, + array($category), + $parent, + isset($item['grant_access']) ? $item['grant_access'] : array(), + 'admin-menu' + ); + + if ( !empty($item['items']) ) { + $this->register_menus_as_hideable( + $store, + $item['items'], + $hideableItem, + $level + 1, + $category + ); + } + } + } + + /** + * @param array $errors + * @param array $items + * @return array + */ + public function save_hideable_items($errors, $items) { + try { + $menu = $this->get_active_admin_menu(); + if ( empty($menu['tree']) ) { + return $errors; + } + } catch (LogicException $ex) { + $errors[] = new WP_Error('no_admin_menu', 'Admin menu configuration is not initialised yet.'); + return $errors; + } + + $hasChanged = $this->update_hideable_menu_items($items, $menu['tree'], 1); + + //Update component visibility. It's more efficient to do it here because we + //don't need to re-save the whole menu configuration multiple times. + if ( !isset($menu['component_visibility']) ) { + $menu['component_visibility'] = array(); + } + $componentsByItemId = apply_filters('admin_menu_editor-hideable_vis_components', array()); + + foreach($componentsByItemId as $itemId => $properties) { + $component = $properties['component']; + if ( isset($items[$itemId]) ) { + $enabled = ameUtils::get($items[$itemId], 'enabled', array()); + $oldAccess = ameUtils::get($menu, array('component_visibility', $component), array()); + if ( !ameUtils::areAssocArraysEqual($enabled, $oldAccess) ) { + $menu['component_visibility'][$component] = $enabled; + $hasChanged = true; + } + } + } + + if ( $hasChanged ) { + if ( !$this->set_custom_menu($menu) ) { + $errors[] = new WP_Error('menu_update_failed', 'Failed to save the admin menu.'); + } + } + + return $errors; + } + + private function update_hideable_menu_items($hideableItems, &$menus, $level) { + $hasChanged = false; + + //Iterate over all admin menus and find the corresponding hideable items. + //We could do it the other way around, but parsing IDs is more complex. + foreach ($menus as &$menuItem) { + $id = $this->make_hideable_item_id($menuItem, $level); + if ( isset($hideableItems[$id]) ) { + $settings = $hideableItems[$id]; + $newAccess = !empty($settings['enabled']) ? $settings['enabled'] : array(); + $oldAccess = isset($menuItem['grant_access']) ? $menuItem['grant_access'] : array(); + + $changes1 = array_diff_assoc($oldAccess, $newAccess); + $changes2 = array_diff_assoc($newAccess, $oldAccess); + + if ( !empty($changes1) || !empty($changes2) ) { + $menuItem['grant_access'] = $newAccess; + $hasChanged = true; + } + } + + if ( !empty($menuItem['items']) ) { + $submenusChanged = $this->update_hideable_menu_items( + $hideableItems, + $menuItem['items'], + $level + 1 + ); + $hasChanged = $hasChanged || $submenusChanged; + } + } + unset($menuItem); //Not strictly necessary. Just guarding against future bugs. + + return $hasChanged; + } + + private function make_hideable_item_id($menuItem, $level) { + $templateId = ameMenuItem::template_id($menuItem); + if ( !empty($templateId) ) { + $suffix = 't/' . $templateId; + } else { + $suffix = 'u/' . ameMenuItem::get($menuItem, 'file', ''); + } + return 'am/' . $level . '/' . $suffix; + } + +} //class + + +class ameMenuTemplateBuilder { + private $templates = array(); + + private $parentNames = array(); + private $blacklist = array(); + + private $templateOrder = array(); + private $previousItemId = ''; + private $wasPreviousItemSeparated = false; + + /** + * Populate a lookup array with default values (templates) from $menu and $submenu. + * Used later to merge a custom menu with the native WordPress menu structure. + * + * @param array $menu + * @param array $submenu + * @param array $blacklist + * @return array An array of menu templates and their default values. + */ + public function build($menu, $submenu, $blacklist = array()){ + $this->templates = array(); + $this->blacklist = $blacklist; + + if ( !empty($menu) ) { + //At this point, the menu might not be sorted yet, especially if other plugins have made changes to it. + //We need to know the relative order of menus to insert new items in the right place. + ksort($menu, SORT_NUMERIC); + + foreach($menu as $pos => $item){ + $this->addItem($item, $pos); + } + } + + if ( !empty($submenu) ) { + foreach($submenu as $parent => $items){ + //Skip NULL's and empty arrays. + if ( empty($items) ) { + continue; + } + + //Skip sub-menus attached to non-existent parents. This should theoretically never happen, + //but a buggy plugin can cause such a situation. + if ( !isset($this->parentNames[$parent]) ) { + continue; + } + + ksort($items, SORT_NUMERIC); + $this->previousItemId = ''; + $this->wasPreviousItemSeparated = false; + + foreach($items as $pos => $item) { + $this->addItem($item, $pos, $parent); + } + } + } + + return $this->templates; + } + + /** + * Add a menu item as a template. + * + * @param array $wpItem + * @param int $position + * @param string|null $parent + */ + private function addItem($wpItem, $position, $parent = null) { + $item = ameMenuItem::fromWpItem($wpItem, $position, $parent); + + //Skip separators. + if ( $item['separator'] ) { + $this->wasPreviousItemSeparated = true; + return; + } + + //Skip blacklisted menus. + //BUG: We shouldn't skip top level menus that have non-blacklisted submenu items. + if ( isset($item['url'], $this->blacklist[$item['url']]) ) { + $filter = $this->blacklist[$item['url']]; + if ( $filter === true ) { + return; + } else if ( ($filter === 'submenu') && ($parent !== null) ) { + return; + } + } + + $name = $this->sanitizeMenuTitle($item['menu_title']); + if ( $parent === null ) { + $this->parentNames[$item['file']] = $name; + } else { + $name = $this->parentNames[$parent] . ' -> ' . $name; + } + + $templateId = ameMenuItem::template_id($item); + unset($item['template_id']); + + $this->templates[$templateId] = array( + 'name' => $name, + 'used' => false, + 'defaults' => $item, + ); + + //Remember the relative order of menu items. It's a bit like a linked list. + $this->templateOrder[$templateId] = array( + 'previous_item' => $this->previousItemId, + 'was_previous_item_separated' => $this->wasPreviousItemSeparated, + ); + $this->previousItemId = $templateId; + $this->wasPreviousItemSeparated = false; + } + + /** + * Sanitize a menu title for display. + * Removes HTML tags and update notification bubbles. Truncates long titles. + * + * @param string $title + * @return string + */ + private function sanitizeMenuTitle($title) { + $title = wp_strip_all_tags( preg_replace('@<span[^>]*>.*</span>@i', '', $title) ); + + //Compact whitespace. + $title = rtrim(preg_replace('@[\s\t\r\n]+@', ' ', $title)); + + $maxLength = 50; + if ( strlen($title) > $maxLength ) { + $title = rtrim(substr($title, 0, $maxLength)) . '...'; + } + + return $title; + } + + public function getRelativeTemplateOrder() { + return $this->templateOrder; + } +} \ No newline at end of file diff --git a/includes/menu-item.php b/includes/menu-item.php new file mode 100644 index 0000000..103700f --- /dev/null +++ b/includes/menu-item.php @@ -0,0 +1,735 @@ +<?php + +/** + * This class contains a number of static methods for working with individual menu items. + * + * Note: This class is not fully self-contained. Some of the methods will query global state. + * This is necessary because the interpretation of certain menu fields depends on things like + * currently registered hooks and the presence of specific files in admin/plugin folders. + */ +abstract class ameMenuItem { + const unclickableTemplateId = '>special:none'; + const unclickableTemplateClass = 'ame-unclickable-menu-item'; + + const embeddedPageTemplateId = '>special:embed_page'; + const embeddedPagePlaceholderHeading = '[Same as menu title]'; + + /** + * @var array A partial list of files in /wp-admin/. Correct as of WP 3.8-RC1, 2013.12.04. + * When trying to determine if a menu links to one of the default WP admin pages, it's faster + * to check this list than to hit the disk. + */ + private static $known_wp_admin_files = array( + 'customize.php' => true, 'edit-comments.php' => true, 'edit-tags.php' => true, 'edit.php' => true, + 'export.php' => true, 'import.php' => true, 'index.php' => true, 'link-add.php' => true, + 'link-manager.php' => true, 'media-new.php' => true, 'nav-menus.php' => true, 'options-discussion.php' => true, + 'options-general.php' => true, 'options-media.php' => true, 'options-permalink.php' => true, + 'options-reading.php' => true, 'options-writing.php' => true, 'plugin-editor.php' => true, + 'plugin-install.php' => true, 'plugins.php' => true, 'post-new.php' => true, 'profile.php' => true, + 'privacy.php' => true, + 'theme-editor.php' => true, 'themes.php' => true, 'tools.php' => true, 'update-core.php' => true, + 'upload.php' => true, 'user-new.php' => true, 'users.php' => true, 'widgets.php' => true, + ); + + private static $mappable_parent_whitelist = '@^(?:profile|import|post-new|edit-tags)\.php@'; + + private static $cached_site_url = null; + private static $is_switch_hook_set = false; + + /** + * Convert a WP menu structure to an associative array. + * + * @param array $item An menu item. + * @param int|string $position The position (index) of the the menu item. + * @param string|null $parent The slug of the parent menu that owns this item. Null for top level menus. + * @return array + */ + public static function fromWpItem($item, $position = 0, $parent = null) { + static $separator_count = 0; + $default_css_class = ($parent === null) ? 'menu-top' : ''; + $item = array( + 'menu_title' => strval($item[0]), + 'access_level' => strval($item[1]), //= required capability + 'file' => $item[2], + 'page_title' => (isset($item[3]) ? strval($item[3]) : ''), + 'css_class' => (isset($item[4]) ? strval($item[4]) : $default_css_class), + 'hookname' => (isset($item[5]) ? strval($item[5]) : ''), //Used as the ID attr. of the generated HTML tag. + 'icon_url' => (isset($item[6]) ? strval($item[6]) : 'dashicons-admin-generic'), + 'position' => $position, + 'parent' => $parent, + ); + + if ( is_numeric($item['access_level']) ) { + $dummyUser = new WP_User; + $item['access_level'] = $dummyUser->translate_level_to_cap($item['access_level']); + } + + if ( $parent === null ) { + $item['separator'] = (strpos($item['css_class'], 'wp-menu-separator') !== false); + //WP 3.0 in multisite mode has two separators with the same filename. Fix by reindexing separators. + if ( $item['separator'] ) { + $item['file'] = 'separator_' . ($separator_count++); + } + } else { + //Submenus can't contain separators. + $item['separator'] = false; + } + + //Flag plugin pages + $has_hook = (get_plugin_page_hook($item['file'], strval($parent)) != null); + $item['is_plugin_page'] = $has_hook; + + if ( !$item['separator'] ) { + $item['url'] = self::generate_url($item['file'], strval($parent), $has_hook); + } + + $item['template_id'] = self::template_id($item, $parent); + + return array_merge(self::basic_defaults(), $item); + } + + public static function basic_defaults() { + static $basic_defaults = null; + if ( $basic_defaults !== null ) { + return $basic_defaults; + } + + $basic_defaults = array( + //Fields that apply to all menu items. + 'page_title' => '', + 'menu_title' => '', + 'access_level' => 'read', + 'extra_capability' => '', + 'file' => '', + 'page_heading' => '', + 'position' => 0, + 'parent' => null, + + //Fields that apply only to top level menus. + 'css_class' => 'menu-top', + 'hookname' => '', + 'icon_url' => 'dashicons-admin-generic', + 'separator' => false, + 'colors' => false, + 'is_always_open' => false, + + //Internal fields that may not map directly to WP menu structures. + 'open_in' => 'same_window', //'new_window', 'iframe' or 'same_window' (the default) + 'iframe_height' => 0, + 'is_iframe_scroll_disabled' => false, + 'template_id' => '', //The default menu item that this item is based on. + 'is_plugin_page' => false, + 'custom' => false, + 'url' => '', + 'embedded_page_id' => 0, + 'embedded_page_blog_id' => function_exists('get_current_blog_id') ? get_current_blog_id() : 1, + ); + + return $basic_defaults; + } + + public static function blank_menu() { + static $blank_menu = null; + if ( $blank_menu !== null ) { + return $blank_menu; + } + + //Template for a basic menu item. + $blank_menu = array_fill_keys(array_keys(self::basic_defaults()), null); + $blank_menu = array_merge($blank_menu, array( + 'items' => array(), //List of sub-menu items. + 'grant_access' => array(), //Per-role and per-user access. Supersedes role_access. + 'colors' => null, + 'is_always_open' => null, + + 'custom' => false, //True if item is made-from-scratch and has no template. + 'missing' => false, //True if our template is no longer present in the default admin menu. Note: Stored values will be ignored. Set upon merging. + 'unused' => false, //True if this item was generated from an unused default menu. Note: Stored values will be ignored. Set upon merging. + 'hidden' => false, //Hide/show the item. Hiding is purely cosmetic, the item remains accessible. + 'hidden_from_actor' => array(), //Like the "hidden" flag, but per-role. Lets the user hide an item without changing its permissions. + 'separator' => false, //True if the item is a menu separator. + + 'restrict_access_to_items' => false, //True = Deny access to all submenu items if the user doesn't have access to this item. + + 'had_access_before_hiding' => null, //Roles who had access to this item before the user clicked the "hide" button. Usually empty. + + 'defaults' => self::basic_defaults(), + )); + return $blank_menu; + } + + public static function custom_item_defaults() { + return array( + 'menu_title' => 'Custom Menu', + 'access_level' => 'read', + 'extra_capability' => '', + 'page_title' => '', + 'css_class' => 'menu-top', + 'hookname' => '', + 'icon_url' => 'dashicons-admin-generic', + 'open_in' => 'same_window', + 'iframe_height' => 0, + 'is_iframe_scroll_disabled' => false, + 'is_plugin_page' => false, + 'page_heading' => '', + 'colors' => false, + 'embedded_page_id' => 0, + 'embedded_page_blog_id' => function_exists('get_current_blog_id') ? get_current_blog_id() : 1, + 'is_always_open' => false, + ); + } + + /** + * Get the value of a menu/submenu field. + * Will return the corresponding value from the 'defaults' entry of $item if the + * specified field is not set in the item itself. + * + * @param array $item + * @param string $field_name + * @param mixed $default Returned if the requested field is not set and is not listed in $item['defaults']. Defaults to null. + * @return mixed Field value. + */ + public static function get($item, $field_name, $default = null){ + if ( isset($item[$field_name]) ){ + return $item[$field_name]; + } else { + if ( isset($item['defaults'], $item['defaults'][$field_name]) ){ + return $item['defaults'][$field_name]; + } else { + return $default; + } + } + } + + /** + * Generate or retrieve an ID that semi-uniquely identifies the template + * of the given menu item. + * + * Note that custom items (i.e. those that do not point to any of the default + * admin menu pages) have no template IDs. + * + * The ID is generated from the item's and its parent's file attributes. + * Since WordPress technically allows two copies of the same menu to exist + * in the same sub-menu, this combination is not necessarily unique. + * + * @param array|string $item The menu item in question. + * @param string|null $parent_file The parent menu. If omitted, $item['defaults']['parent'] will be used. + * @return string Template ID, or an empty string if this is a custom item. + */ + public static function template_id($item, $parent_file = null){ + if (is_string($item)) { + return strval($parent_file) . '>' . $item; + } + + if ( !empty($item['custom']) ) { + return ''; + } + + //Maybe it already has an ID? + $template_id = self::get($item, 'template_id'); + if ( !empty($template_id) ) { + return $template_id; + } + + if ( isset($item['defaults']['file']) ) { + $item_file = $item['defaults']['file']; + } else { + $item_file = self::get($item, 'file'); + } + + if ( $parent_file === null ) { + if ( isset($item['defaults']['parent']) ) { + $parent_file = $item['defaults']['parent']; + } else { + $parent_file = self::get($item, 'parent'); + } + } + + //Map known alternative parents to admin parent menus. This is necessary to ensure that + //certain menu items have the same template ID both for admins and for regular users. + static $inverse_parent_map = null; + global $_wp_real_parent_file; + if ( ($inverse_parent_map === null) && !empty($_wp_real_parent_file) && is_array($_wp_real_parent_file) ) { + $inverse_parent_map = array_flip($_wp_real_parent_file); + } + if ( isset($inverse_parent_map[$parent_file]) && (preg_match(self::$mappable_parent_whitelist, $parent_file) === 1) ) { + $parent_file = $inverse_parent_map[$parent_file]; + } + + //Special case: In WP 4.0+ the URL of the "Appearance -> Customize" item is different on every admin page. + //This is because the URL includes a "return" parameter that contains the current page's URL. It also makes + //the template ID different on every page, so it's impossible to identify the menu. To fix that, lets remove + //the "return" parameter from the ID. + if ( strpos($item_file, 'customize.php?') === 0 ) { + $item_file = remove_query_arg('return', $item_file); + } + + //Special case: Remove the current site URL from fully qualified URLs. + //This way template IDs will still match if the menu configuration is imported on a different site. + if ( strpos($item_file, '://') !== false ) { + $site_url = self::get_site_url(); + $site_url_length = strlen($site_url); + if ( + ($site_url_length > 0) + //Does the item URL start with the site URL? + && (strncmp($item_file, $site_url, $site_url_length) === 0) + //Only cut off the site URL if there will be something left. + //We don't want the ID to be an empty string. + && (strlen($item_file) > $site_url_length) + ) { + //The site URL usually doesn't have a trailing slash, but sometimes it does, + //so we could end up either with "/wp-admin/foo.php" or "wp-admin/foo.php". + //For consistency, let's always prepend a slash to the result. + $item_file = '/' .ltrim(substr($item_file, $site_url_length), '/'); + } + } + + //Special case: A menu item can have an empty slug. This is technically very wrong, but it works (sort of) + //as long as the item has at least one submenu. This has happened at least once in practice. A user had + //a theme based on the Redux framework, and inexplicably the framework was configured to use an empty page slug. + if ( empty($item['separator']) ) { + if ( $item_file === '' ) { + $item_file = '[ame-no-slug]'; + } + if ( $parent_file === '' ) { + $parent_file = '[ame-no-slug]'; + } + } + + return strval($parent_file) . '>' . $item_file; + } + + /** + * Set all undefined menu fields to the default value. + * + * @param array $item Menu item in the plugin's internal form + * @return array + */ + public static function apply_defaults($item){ + foreach($item as $key => $value){ + //Is the field set? + if ($value === null){ + //Use default, if available + if (isset($item['defaults'], $item['defaults'][$key])){ + $item[$key] = $item['defaults'][$key]; + } + } + } + return $item; + } + + /** + * Apply custom menu filters to an item of the custom menu. + * + * Calls a 'custom_admin_$item_type' filter with the entire $item passed as the argument. + * Used when converting the current custom menu to a WP-format menu. + * + * @param array $item Associative array representing one menu item (either top-level or submenu). + * @param string $item_type 'menu' or 'submenu' + * @param mixed $extra Optional extra data to pass to hooks. + * @return array Filtered menu item. + */ + public static function apply_filters($item, $item_type, $extra = null){ + return apply_filters("custom_admin_{$item_type}", $item, $extra); + } + + /** + * Recursively normalize a menu item and all of its sub-items. + * + * This will also ensure that the item has all the required fields. + * + * @static + * @param array $item + * @return array + */ + public static function normalize($item) { + if ( isset($item['defaults']) ) { + $item['defaults'] = array_merge( + empty($item['custom']) ? self::basic_defaults() : self::custom_item_defaults(), + $item['defaults'] + ); + } + $item = array_merge(self::blank_menu(), $item); + + $item['unused'] = false; + $item['missing'] = false; + $item['template_id'] = self::template_id($item); + + //Items pointing to a default page can't have a custom file/URL. + if ( ($item['template_id'] !== '') && ($item['file'] !== null) ) { + if ( $item['file'] == $item['defaults']['file'] ) { + //Identical to default, so just set it to use that. + $item['file'] = null; + } else { + //Different file = convert to a custom item. Need to call fix_defaults() + //to fix other fields that are currently set to defaults custom items don't have. + $item['template_id'] = ''; + } + } + + $item['custom'] = $item['custom'] || ($item['template_id'] == ''); + $item = self::fix_defaults($item); + + //Older versions would allow the user to set the required capability directly. + //This was incorrect since for default menu items the default cap was *always* + //applied anyway, and the new cap was applied on top of that. We make that explicit + //by storing the custom cap in a separate field - extra_capability - and keeping + //access_level (required cap) at the default value. + if ( isset($item['defaults']) && $item['access_level'] !== null ) { + if ( empty($item['extra_capability']) ) { + $item['extra_capability'] = $item['access_level']; + } + $item['access_level'] = null; + } + + //Convert per-role access settings to the more general grant_access format. + if ( isset($item['role_access']) ) { + foreach($item['role_access'] as $role_id => $has_access) { + $item['grant_access']['role:' . $role_id] = $has_access; + } + unset($item['role_access']); + } + + //There's no need to store the default position if a custom position is set. + //The default position will not be used, and there's no option to reset the position to default. + if ( isset($item['position'], $item['defaults']['position']) && ($item['defaults']['position'] === $item['position'])) { + unset($item['defaults']['position']); + } + //The same goes for template ID. + if ( isset($item['template_id']) ) { + unset($item['defaults']['template_id']); + } + + if ( isset($item['items']) ) { + foreach($item['items'] as $index => $sub_item) { + $item['items'][$index] = self::normalize($sub_item); + } + } + + return $item; + } + + /** + * Fix obsolete default values on custom items. + * + * In older versions of the plugin, each custom item had its own set of defaults. + * It was also possible to create a pseudo-custom item from a default item by + * freely overwriting its fields with custom values. + * + * The current version uses the same defaults for all custom items. To avoid data + * loss, we'll check for any mismatches and make such defaults explicit. + * + * @static + * @param array $item + * @return array + */ + private static function fix_defaults($item) { + if ( $item['custom'] && isset($item['defaults']) ) { + $new_defaults = self::custom_item_defaults(); + foreach($item as $field => $value) { + $is_mismatch = is_null($value) + && array_key_exists($field, $item['defaults']) + && ( + !array_key_exists($field, $new_defaults) //No default. + || ($item['defaults'][$field] != $new_defaults[$field]) //Different default. + ); + + if ( $is_mismatch ) { + $item[$field] = $item['defaults'][$field]; + } + } + $item['defaults'] = $new_defaults; + } + return $item; + } + + /** + * Sanitize item properties. + * + * Strips disallowed HTML and invalid characters from many fields. For example, only users who + * have the "unfiltered_html" capability can use arbitrary HTML in menu titles. + * + * To avoid the performance hit of calling current_user_can('unfiltered_html') for every item, + * you can call it once and pass the result to this function. + * + * @param array $item Menu item in the internal format. + * @param bool|null $user_can_unfiltered_html + * @return array Sanitized menu item. + */ + public static function sanitize($item, $user_can_unfiltered_html = null) { + if ( $user_can_unfiltered_html === null ) { + $user_can_unfiltered_html = current_user_can('unfiltered_html'); + } + + if ( !$user_can_unfiltered_html ) { + $kses_fields = array('menu_title', 'page_title', 'file', 'page_heading'); + foreach($kses_fields as $field) { + $value = self::get($item, $field); + if ( is_string($value) && !empty($value) && !self::is_default($item, $field) ) { + $item[$field] = wp_kses_post($value); + } + } + } + + //Sanitize CSS class names. Note that the WP implementation of sanitize_html_class() is very basic + //and doesn't comply with the CSS2 spec, but that's probably OK in this case. + $css_class = self::get($item, 'css_class'); + if ( !self::is_default($item, 'css_class') && is_string($css_class) && function_exists('sanitize_html_class') ) { + $item['css_class'] = implode(' ', array_map('sanitize_html_class', explode(' ', $css_class))); + } + + //While menu capabilities are generally not displayed anywhere except this plugin (which already + //escapes them properly), lets sanitize them anyway in case another plugin displays them as-is. + $capability_fields = array('access_level', 'extra_capability'); + foreach($capability_fields as $field) { + $value = self::get($item, $field); + if ( !self::is_default($item, $field) && is_string($value) ) { + $item[$field] = wp_strip_all_tags($value); + } + } + + //Menu icons can be all kinds of stuff (dashicons, data URIs, etc), but they can't contain HTML. + //See /wp-admin/menu-header.php line #90 and onwards for how WordPress handles icons. + if ( !self::is_default($item, 'icon_url') ) { + $item['icon_url'] = wp_strip_all_tags($item['icon_url']); + } + + //WordPress already sanitizes the menu ID (hookname) on display, but, again, lets clean it just in case. + if ( !self::is_default($item, 'hookname') ) { + //Regex from menu-header.php, WP 4.1. + $item['hookname'] = preg_replace('@[^a-zA-Z0-9_:.]@', '-', self::get($item, 'hookname')); + } + + return $item; + } + + /** + * Custom comparison function that compares menu items based on their position in the menu. + * + * @param array $a + * @param array $b + * @return int + */ + public static function compare_position($a, $b){ + $result = floatval(self::get($a, 'position', 0)) - floatval(self::get($b, 'position', 0)); + //Support for non-integer positions. + if ($result > 0) { + return 1; + } else if ($result < 0) { + return -1; + } + return 0; + } + + /** + * Generate a URL for a menu item. + * + * @param string $item_slug + * @param string $parent_slug + * @param bool|null $has_hook + * @return string An URL relative to the /wp-admin/ directory. + */ + public static function generate_url($item_slug, $parent_slug = '', $has_hook = null) { + $menu_url = is_array($item_slug) ? self::get($item_slug, 'file') : $item_slug; + $parent_url = !empty($parent_slug) ? $parent_slug : 'admin.php'; + + //Workaround for components that HTML-entity-encode menu URLs. Most plugins and themes don't do that, but there's + //at least one plugin that does (WooCommerce). WP core also encodes some menu URLs (e.g. Appearance -> Header). + //We need a raw URL here. + $menu_url = html_entity_decode($menu_url); + + if ( strpos($menu_url, '://') !== false ) { + return $menu_url; + } + + if ( self::is_hook_or_plugin_page($menu_url, $parent_url, $has_hook) ) { + $parent_file = self::remove_query_from($parent_url); + $base_file = self::is_wp_admin_file($parent_file) ? html_entity_decode($parent_url) : 'admin.php'; + //add_query_arg() might be more robust, but it's significantly slower. + $url = $base_file + . ((strpos($base_file, '?') === false) ? '?' : '&') + . 'page=' . $menu_url; + //Surprisingly, WordPress does NOT encode the menu slug when using it in a query parameter ("?page=slug"). + //This allows plugins to use tricks like passing additional query parameters by simply appending them + //to the slug. For example, this works: "something¶m=foo". + } else { + $url = $menu_url; + } + return $url; + } + + private static function is_hook_or_plugin_page($page_url, $parent_page_url = '', $hasHook = null) { + if ( empty($parent_page_url) ) { + $parent_page_url = 'admin.php'; + } + $pageFile = self::remove_query_from($page_url); + + if ( $hasHook === null ) { + $hasHook = (get_plugin_page_hook($page_url, $parent_page_url) !== null); + } + if ( $hasHook ) { + return true; + } + + //Files in /wp-admin are part of WP core so they're not plugin pages. + if ( self::is_wp_admin_file($pageFile) ) { + return false; + } + + /* + * Special case: Absolute paths. + * + * - add_submenu_page() applies plugin_basename() to the menu slug, so we don't need to worry about plugin + * paths. However, absolute paths that *don't* point point to the plugins directory can be a problem. + * + * - Due to a known PHP bug, certain invalid paths can crash PHP. See self::is_safe_to_append(). + * + * - WP 3.9.2 and 4.0+ unintentionally break menu URLs like "foo.php?page=c:\a\b.php" because esc_url() + * interprets the part before the colon as an invalid protocol. As a result, such links have an empty URL + * on Windows (but they might still work on other OS). + * + * - Recent versions of WP won't let you load a PHP file from outside the plugins and mu-plugins directories + * with "admin.php?page=filename". See the validate_file() call in /wp-admin/admin.php. However, such filenames + * can still be used as unique slugs for menus with hook callbacks, so we shouldn't reject them outright. + * Related: https://core.trac.wordpress.org/ticket/10011 + */ + $allowPathConcatenation = self::is_safe_to_append($pageFile); + + $pluginFileExists = $allowPathConcatenation + && ($page_url != 'index.php') + && is_file(WP_PLUGIN_DIR . '/' . $pageFile); + if ( $pluginFileExists ) { + return true; + } + + return false; + } + + /** + * Check if a file exists inside the /wp-admin subdirectory. + * + * @param string $filename + * @return bool + */ + private static function is_wp_admin_file($filename) { + //Check our hard-coded list of admin pages first. It's measurably faster than + //hitting the disk with is_file(). + if ( isset(self::$known_wp_admin_files[$filename]) ) { + return self::$known_wp_admin_files[$filename]; + } + + //Now actually check the filesystem. + $adminFileExists = self::is_safe_to_append($filename) + && is_file(ABSPATH . 'wp-admin/' . $filename); + + //Cache the result for later. We can generally expect more than one call per top level menu URL. + self::$known_wp_admin_files[$filename] = $adminFileExists; + + return $adminFileExists; + } + + /** + * Verify that it's safe to append a given filename to another path. + * + * If we blindly append an absolute path to another path, we can get something like "C:\a\b/wp-admin/C:\c\d.php". + * PHP 5.2.5 has a known bug where calling file_exists() on that kind of an invalid filename will cause + * a timeout and a crash in some configurations. See: https://bugs.php.net/bug.php?id=44412 + * + * @param string $filename + * @return bool + */ + private static function is_safe_to_append($filename) { + return (substr($filename, 1, 1) !== ':'); //Reject "C:\whatever" and similar. + } + + /** + * Check if a field is currently set to its default value. + * + * @param array $item + * @param string $field_name + * @return bool + */ + public static function is_default($item, $field_name) { + if ( isset($item[$field_name]) ){ + return false; + } else { + return isset($item['defaults'], $item['defaults'][$field_name]); + } + } + + public static function remove_query_from($url) { + $pos = strpos($url, '?'); + if ( $pos !== false ) { + return substr($url, 0, $pos); + } + return $url; + } + + /** + * Remove the number of pending updates or other count elements from a menu title. + * + * @param string $menuTitle + * @return string + */ + public static function remove_update_count($menuTitle) { + if ( (stripos($menuTitle, '<span') === false) || !class_exists('DOMDocument', false) ) { + return $menuTitle; + } + + $dom = new DOMDocument(); + $uniqueId = 'ame-rex-title-wrapper-' . time(); + if ( @$dom->loadHTML('<div id="' . $uniqueId . '">' . $menuTitle . '</div>') ) { + /** @noinspection PhpComposerExtensionStubsInspection */ + $xpath = new DOMXpath($dom); + $result = $xpath->query('//span[contains(@class,"update-plugins") or contains(@class,"awaiting-mod")]'); + if ( $result->length > 0 ) { + //Remove all matched nodes. We must iterate backwards to prevent messing up the DOMNodeList. + for ($i = $result->length - 1; $i >= 0; $i--) { + $span = $result->item(0); + $span->parentNode->removeChild($span); + } + + $innerHtml = ''; + $children = $dom->getElementById($uniqueId)->childNodes; + //In theory, $children should always be a DOMNodeList, but there has been + //at least one report about the foreach() statement throwing a warning + //because $children was not iterable. + if ( $children instanceof Traversable ) { + foreach ($children as $child) { + $innerHtml .= $child->ownerDocument->saveHTML($child); + } + } + + return $innerHtml; + } + } + return $menuTitle; + } + + /** + * Get the current site URL. + * + * This is equivalent to calling the get_site_url() WordPress API function without + * arguments, except this method caches the result and doesn't run filters every time. + * + * @return string + */ + private static function get_site_url() { + if ( self::$cached_site_url !== null ) { + return self::$cached_site_url; + } + self::$cached_site_url = get_site_url(); + + //Clear the cache when WordPress switches to a different site. + if ( !self::$is_switch_hook_set ) { + self::$is_switch_hook_set = true; + add_action('switch_blog', array(__CLASS__, 'clear_per_site_cache'), 10, 0); + } + + return self::$cached_site_url; + } + + public static function clear_per_site_cache() { + self::$cached_site_url = null; + } +} \ No newline at end of file diff --git a/includes/menu.php b/includes/menu.php new file mode 100644 index 0000000..d465624 --- /dev/null +++ b/includes/menu.php @@ -0,0 +1,619 @@ +<?php +abstract class ameMenu { + const format_name = 'Admin Menu Editor menu'; + const format_version = '7.0'; + + protected static $custom_loaders = array(); + + /** + * Load an admin menu from a JSON string. + * + * @static + * + * @param string $json A JSON-encoded menu structure. + * @param bool $assume_correct_format Skip the format header check and assume everything is fine. Defaults to false. + * @param bool $always_normalize Always normalize the menu structure, even if format[is_normalized] is true. + * @throws InvalidMenuException + * @return array + */ + public static function load_json($json, $assume_correct_format = false, $always_normalize = false) { + $arr = json_decode($json, true); //TODO: Consider ignoring or substituting invalid UTF-8 characters. + if ( !is_array($arr) ) { + $message = 'The input is not a valid JSON-encoded admin menu.'; + if ( function_exists('json_last_error_msg') ) { + $message .= ' ' . json_last_error_msg(); + } + throw new InvalidMenuException($message); + } + return self::load_array($arr, $assume_correct_format, $always_normalize); + } + + /** + * Load an admin menu structure from an associative array. + * + * @static + * + * @param array $arr + * @param bool $assume_correct_format + * @param bool $always_normalize + * @throws InvalidMenuException + * @return array + */ + public static function load_array($arr, $assume_correct_format = false, $always_normalize = false){ + $is_normalized = false; + if ( !$assume_correct_format ) { + if ( isset($arr['format']) && ($arr['format']['name'] == self::format_name) ) { + $compared = version_compare($arr['format']['version'], self::format_version); + if ( $compared > 0 ) { + throw new InvalidMenuException(sprintf( + "Can't load a menu created by a newer version of the plugin. Menu format: '%s', newest supported format: '%s'. Try updating the plugin.", + $arr['format']['version'], + self::format_version + )); + } + //We can skip normalization if the version number matches exactly and the menu is already normalized. + if ( ($compared === 0) && isset($arr['format']['is_normalized']) ) { + $is_normalized = $arr['format']['is_normalized']; + } + } else if ( isset($arr['format'], $arr['format']['name']) ) { + //This is not an admin menu configuration. It's something else with a "format" header. + throw new InvalidMenuException(sprintf( + 'Unknown menu configuration format: "%s".', + esc_html($arr['format']['name']) + )); + } else { + return self::load_menu_40($arr); + } + } + + if ( !(isset($arr['tree']) && is_array($arr['tree'])) ) { + throw new InvalidMenuException("Failed to load a menu - the menu tree is missing."); + } + + if ( isset($arr['format']) && !empty($arr['format']['compressed']) ) { + $arr = self::decompress($arr); + } + + $menu = array('tree' => array()); + $menu = self::add_format_header($menu); + + if ( $is_normalized && !$always_normalize ) { + $menu['tree'] = $arr['tree']; + } else { + foreach($arr['tree'] as $file => $item) { + $menu['tree'][$file] = ameMenuItem::normalize($item); + } + $menu['format']['is_normalized'] = true; + } + + if ( isset($arr['color_css']) && is_string($arr['color_css']) ) { + $menu['color_css'] = $arr['color_css']; + $menu['color_css_modified'] = isset($arr['color_css_modified']) ? intval($arr['color_css_modified']) : 0; + $menu['icon_color_overrides'] = isset($arr['icon_color_overrides']) ? $arr['icon_color_overrides'] : null; + } + + //Sanitize color presets. + if ( isset($arr['color_presets']) && is_array($arr['color_presets']) ) { + $color_presets = array(); + + foreach($arr['color_presets'] as $name => $preset) { + $name = substr(trim(wp_strip_all_tags(strval($name))), 0, 250); + if ( empty($name) || !is_array($preset) ) { + continue; + } + + //Each color must be a hexadecimal HTML color code. For example: "#12456" + $is_valid_preset = true; + foreach($preset as $property => $color) { + //Note: It would good to check $property against a list of known color names. + if ( !is_string($property) || !is_string($color) || !preg_match('/^#[0-9a-f]{6}$/i', $color) ) { + $is_valid_preset = false; + break; + } + } + + if ( $is_valid_preset ) { + $color_presets[$name] = $preset; + } + } + + $menu['color_presets'] = $color_presets; + } + + //Copy directly granted capabilities. + if ( isset($arr['granted_capabilities']) && is_array($arr['granted_capabilities']) ) { + $granted_capabilities = array(); + foreach($arr['granted_capabilities'] as $actor => $capabilities) { + //Skip empty lists to avoid problems with {} => [] and to save space. + if ( !empty($capabilities) ) { + $granted_capabilities[strval($actor)] = $capabilities; + } + } + if (!empty($granted_capabilities)) { + $menu['granted_capabilities'] = $granted_capabilities; + } + } + + //Copy component visibility. + if ( isset($arr['component_visibility']) ) { + $visibility = array(); + + foreach(array('toolbar', 'adminMenu') as $component) { + if ( + isset($arr['component_visibility'][$component]) + && is_array($arr['component_visibility'][$component]) + && !empty($arr['component_visibility'][$component]) + ) { + //Expected: actorId => boolean. + $visibility[$component] = array(); + foreach($arr['component_visibility'][$component] as $actorId => $allow) { + $visibility[$component][strval($actorId)] = (bool)($allow); + } + } + } + + $menu['component_visibility'] = $visibility; + } + + //Copy heading settings. + if ( isset($arr['menu_headings']) ) { + $menu['menu_headings'] = $arr['menu_headings']; + } + + //Copy the "modified icons" flag. + if ( isset($arr['has_modified_dashicons']) ) { + $menu['has_modified_dashicons'] = (bool)$arr['has_modified_dashicons']; + } + + //Copy the pre-generated list of virtual capabilities. + if ( isset($arr['prebuilt_virtual_caps']) ) { + $menu['prebuilt_virtual_caps'] = $arr['prebuilt_virtual_caps']; + } + + //Copy the modification timestamp. + if ( isset($arr['last_modified_on']) ) { + $menu['last_modified_on'] = substr(strval($arr['last_modified_on']), 0, 100); + } + + foreach(self::$custom_loaders as $callback) { + $menu = call_user_func($callback, $menu, $arr); + } + + return $menu; + } + + /** + * "Pre-load" an old menu structure. + * + * In older versions of the plugin, the entire menu consisted of + * just the menu tree and nothing else. This was internally known as + * menu format "4". + * + * To improve portability and forward-compatibility, newer versions + * use a simple dictionary-based container instead, with the menu tree + * being one of the possible entries. + * + * @static + * @param array $arr + * @return array + * @throws InvalidMenuException + */ + private static function load_menu_40($arr) { + //This is *very* basic and might need to be improved. + $menu = array('tree' => $arr); + return self::load_array($menu, true); + } + + public static function add_format_header($menu) { + if ( !isset($menu['format']) || !is_array($menu['format']) ) { + $menu['format'] = array(); + } + $menu['format'] = array_merge( + $menu['format'], + array( + 'name' => self::format_name, + 'version' => self::format_version, + ) + ); + return $menu; + } + + /** + * Serialize an admin menu as JSON. + * + * @static + * @param array $menu + * @return string + */ + public static function to_json($menu) { + $menu = self::add_format_header($menu); + //todo: Maybe use wp_json_encode() instead. At least one user had invalid UTF-8 characters in their menu. + $result = wp_json_encode($menu); + if ( !is_string($result) ) { + $message = sprintf( + 'Failed to encode the menu configuration as JSON. json_encode returned a %s.', + gettype($result) + ); + if ( function_exists('json_last_error') ) { + $message .= sprintf(' JSON error code: %d.', json_last_error()); + } + if ( function_exists('json_last_error_msg') ) { + $message .= sprintf(' JSON error message: %s', json_last_error_msg()); + } + throw new RuntimeException($message); + } + return $result; + } + + /** + * Sort the menus and menu items of a given menu according to their positions + * + * @param array $tree A menu structure in the internal format (just the tree). + * @return array Sorted menu in the internal format + */ + public static function sort_menu_tree($tree){ + //Resort the tree to ensure the found items are in the right spots + uasort($tree, 'ameMenuItem::compare_position'); + //Resort all submenus as well + foreach ($tree as &$topmenu){ + if (!empty($topmenu['items'])){ + usort($topmenu['items'], 'ameMenuItem::compare_position'); + } + } + + return $tree; + } + + /** + * Convert the WP menu structure to the internal representation. All properties set as defaults. + * + * @param array $menu + * @param array $submenu + * @param array $blacklist + * @return array Menu in the internal tree format. + */ + public static function wp2tree($menu, $submenu, $blacklist = array()){ + $tree = array(); + foreach ($menu as $pos => $item){ + + $tree_item = ameMenuItem::blank_menu(); + $tree_item['defaults'] = ameMenuItem::fromWpItem($item, $pos); + $tree_item['separator'] = $tree_item['defaults']['separator']; + + //Attach sub-menu items + $parent = $tree_item['defaults']['file']; + if ( isset($submenu[$parent]) ){ + foreach($submenu[$parent] as $position => $subitem){ + $defaults = ameMenuItem::fromWpItem($subitem, $position, $parent); + + //Skip blacklisted items. + if ( isset($defaults['url'], $blacklist[$defaults['url']]) ) { + continue; + } + + $tree_item['items'][] = array_merge( + ameMenuItem::blank_menu(), + array('defaults' => $defaults) + ); + } + } + + //Skip blacklisted top level menus (only if they have no submenus). + if ( + empty($tree_item['items']) + && isset($tree_item['defaults']['url'], $blacklist[$tree_item['defaults']['url']]) + ) { + continue; + } + + $tree[$parent] = $tree_item; + } + + $tree = self::sort_menu_tree($tree); + + return $tree; + } + + /** + * Check if a menu contains any items with the "hidden" flag set to true. + * + * @param array $menu + * @return bool + */ + public static function has_hidden_items($menu) { + if ( !is_array($menu) || empty($menu) || empty($menu['tree']) ) { + return false; + } + + foreach($menu['tree'] as $item) { + if ( ameMenuItem::get($item, 'hidden') ) { + return true; + } + if ( !empty($item['items']) ) { + foreach($item['items'] as $child) { + if ( ameMenuItem::get($child, 'hidden') ) { + return true; + } + } + } + } + + return false; + } + + /** + * Sanitize a list of menu items. Array indexes will be preserved. + * + * @param array $treeItems A list of menu items. + * @param bool $unfiltered_html Whether the current user has the unfiltered_html capability. + * @return array List of sanitized items. + */ + public static function sanitize($treeItems, $unfiltered_html = null) { + if ( $unfiltered_html === null ) { + $unfiltered_html = current_user_can('unfiltered_html'); + } + + $result = array(); + foreach($treeItems as $key => $item) { + $item = ameMenuItem::sanitize($item, $unfiltered_html); + + if ( !empty($item['items']) ) { + $item['items'] = self::sanitize($item['items'], $unfiltered_html); + } + $result[$key] = $item; + } + + return $result; + } + + /** + * Recursively filter a list of menu items and remove items flagged as missing. + * + * @param array $items An array of menu items to filter. + * @return array + */ + public static function remove_missing_items($items) { + $items = array_filter($items, array(__CLASS__, 'is_not_missing')); + + foreach($items as &$item) { + if ( !empty($item['items']) ) { + $item['items'] = self::remove_missing_items($item['items']); + } + } + + return $items; + } + + protected static function is_not_missing($item) { + return empty($item['missing']); + } + + /** + * Compress menu configuration (lossless). + * + * Reduces data size by storing commonly used properties and defaults in one place + * instead of in every menu item. + * + * @param array $menu + * @return array + */ + public static function compress($menu) { + $property_dict = ameMenuItem::blank_menu(); + unset($property_dict['defaults']); + + $common = array( + 'properties' => $property_dict, + 'basic_defaults' => ameMenuItem::basic_defaults(), + 'custom_item_defaults' => ameMenuItem::custom_item_defaults(), + ); + + $menu['tree'] = self::map_items( + $menu['tree'], + array(__CLASS__, 'compress_item'), + array($common) + ); + + $menu = self::add_format_header($menu); + $menu['format']['compressed'] = true; + $menu['format']['common'] = $common; + + return $menu; + } + + protected static function compress_item($item, $common) { + //These empty arrays can be dropped. They'll be restored either by merging common properties, + //or by ameMenuItem::normalize(). + if ( empty($item['grant_access']) ) { + unset($item['grant_access']); + } + if ( empty($item['items']) ) { + unset($item['items']); + } + + //Normal and custom menu items have different defaults. + //Remove defaults that are the same for all items of that type. + $defaults = !empty($item['custom']) ? $common['custom_item_defaults'] : $common['basic_defaults']; + if ( isset($item['defaults']) ) { + foreach($defaults as $key => $value) { + if ( array_key_exists($key, $item['defaults']) && $item['defaults'][$key] === $value ) { + unset($item['defaults'][$key]); + } + } + } + + //Remove properties that match the common values. + foreach($common['properties'] as $key => $value) { + if ( array_key_exists($key, $item) && $item[$key] === $value ) { + unset($item[$key]); + } + } + + return $item; + } + + /** + * Decompress menu configuration that was previously compressed by ameMenu::compress(). + * + * If the input $menu is not compressed, this method will return it unchanged. + * + * @param array $menu + * @return array + */ + public static function decompress($menu) { + if ( !isset($menu['format']) || empty($menu['format']['compressed']) ) { + return $menu; + } + + $common = $menu['format']['common']; + $menu['tree'] = self::decompress_list($menu['tree'], $common); + + unset($menu['format']['compressed'], $menu['format']['common']); + return $menu; + } + + protected static function decompress_list($list, $common) { + //Optimization: Direct iteration is about 40% faster than map_items. + $result = array(); + foreach($list as $key => $item) { + $item = self::decompress_item($item, $common); + if ( !empty($item['items']) ) { + $item['items'] = self::decompress_list($item['items'], $common); + } + $result[$key] = $item; + } + return $result; + } + + protected static function decompress_item($item, $common) { + $item = array_merge($common['properties'], $item); + + $defaults = !empty($item['custom']) ? $common['custom_item_defaults'] : $common['basic_defaults']; + $item['defaults'] = array_merge($defaults, $item['defaults']); + + return $item; + } + + /** + * Recursively apply a callback to every menu item in an array and return the results. + * Array keys are preserved. + * + * @param array $items + * @param callable $callback + * @param array|null $extra_params Optional. An array of additional parameters to pass to the callback. + * @return array + */ + protected static function map_items($items, $callback, $extra_params = null) { + if ( $extra_params === null ) { + $extra_params = array(); + } + $args = array_merge(array(null), $extra_params); + + $result = array(); + foreach($items as $key => $item) { + $args[0] = $item; + $item = call_user_func_array($callback, $args); + + if ( !empty($item['items']) ) { + $item['items'] = self::map_items($item['items'], $callback, $extra_params); + } + $result[$key] = $item; + } + return $result; + } + + /** + * @param array $items + * @param callable $callback + */ + public static function for_each($items, $callback) { + foreach($items as $key => $item) { + call_user_func($callback, $item); + if ( !empty($item['items']) ) { + self::for_each($item['items'], $callback); + } + } + } + + /** + * @param callable $callback + */ + public static function add_custom_loader($callback) { + self::$custom_loaders[] = $callback; + } +} + +class ameGrantedCapabilityFilter { + /** + * @var string[] + */ + private $post_types; + /** + * @var string[] + */ + private $taxonomies; + + public function __construct() { + $this->post_types = get_post_types(array('public' => true, 'show_ui' => true), 'names', 'or'); + $this->taxonomies = get_taxonomies(array('public' => true, 'show_ui' => true), 'names', 'or'); + } + + /** + * Remove capabilities that refer to unregistered post types or taxonomies. + * + * @param array $granted_capabilities + * @return array + */ + public function clean_up($granted_capabilities) { + $clean = array(); + foreach($granted_capabilities as $actor => $capabilities) { + $clean[$actor] = array_filter($capabilities, array($this, 'is_registered_source')); + } + return $clean; + } + + private function is_registered_source($grant) { + if ( !is_array($grant) || !isset($grant[1]) ) { + return true; + } + + if ( isset($grant[2]) ) { + if ( $grant[1] === 'post_type' ) { + return array_key_exists($grant[2], $this->post_types); + } else if ( $grant[1] === 'taxonomy' ) { + return array_key_exists($grant[2], $this->taxonomies); + } + } + return false; + } +} + +/** + * This could just be a closure, but we want to support PHP 5.2. + */ +class ameModifiedIconDetector { + private $result = false; + + public static function detect($menu) { + $detector = new self(); + ameMenu::for_each($menu['tree'], array($detector, 'checkItem')); + return $detector->getResult(); + } + + public function checkItem($item) { + $this->result = $this->result || $this->hasModifiedDashicon($item); + } + + private function hasModifiedDashicon($item) { + return !ameMenuItem::is_default($item, 'icon_url') + && (strpos(ameMenuItem::get($item, 'icon_url'), 'dashicons-') === 0); + } + + private function getResult() { + return $this->result; + } +} + + +class InvalidMenuException extends Exception {} + +class ameInvalidJsonException extends RuntimeException {} \ No newline at end of file diff --git a/includes/module.php b/includes/module.php new file mode 100644 index 0000000..a8197e5 --- /dev/null +++ b/includes/module.php @@ -0,0 +1,164 @@ +<?php +abstract class ameModule { + protected $tabSlug = ''; + protected $tabTitle = ''; + protected $tabOrder = 10; + + protected $moduleId = ''; + protected $moduleDir = ''; + + protected $settingsFormAction = ''; + + /** + * @var WPMenuEditor + */ + protected $menuEditor; + + public function __construct($menuEditor) { + $this->menuEditor = $menuEditor; + + if ( class_exists('ReflectionClass', false) ) { + //This should never throw an exception since the current class must exist for this constructor to be run. + /** @noinspection PhpUnhandledExceptionInspection */ + $reflector = new ReflectionClass(get_class($this)); + $this->moduleDir = dirname($reflector->getFileName()); + $this->moduleId = basename($this->moduleDir); + } + + if ( !$this->isEnabledForRequest() ) { + return; + } + + add_action('admin_menu_editor-register_scripts', array($this, 'registerScripts')); + + //Register the module tab. + if ( ($this->tabSlug !== '') && is_string($this->tabSlug) ) { + add_action('admin_menu_editor-tabs', array($this, 'addTab'), $this->tabOrder); + add_action('admin_menu_editor-section-' . $this->tabSlug, array($this, 'displaySettingsPage')); + + add_action('admin_menu_editor-enqueue_scripts-' . $this->tabSlug, array($this, 'enqueueTabScripts')); + add_action('admin_menu_editor-enqueue_styles-' . $this->tabSlug, array($this, 'enqueueTabStyles')); + + //Optionally, handle settings form submission. + if ( $this->settingsFormAction !== '' ) { + add_action('admin_menu_editor-header', array($this, '_processAction'), 10, 2); + } + } + } + + /** + * Does this module need to do anything for the current request? + * + * For example, some modules work in the normal dashboard but not in the network admin. + * Other modules don't need to run during AJAX requests or when WP is running Cron jobs. + */ + protected function isEnabledForRequest() { + return true; + } + + public function addTab($tabs) { + $tabs[$this->tabSlug] = !empty($this->tabTitle) ? $this->tabTitle : $this->tabSlug; + return $tabs; + } + + public function displaySettingsPage() { + $this->menuEditor->display_settings_page_header(); + + if ( !$this->outputMainTemplate() ) { + printf( + "[ %1\$s : Module \"%2\$s\" doesn't have a primary template. ]", + esc_html(__METHOD__), + esc_html($this->moduleId) + ); + } + + $this->menuEditor->display_settings_page_footer(); + } + + protected function getTabUrl($queryParameters = array()) { + $queryParameters = array_merge( + array('sub_section' => $this->tabSlug), + $queryParameters + ); + return $this->menuEditor->get_plugin_page_url($queryParameters); + } + + protected function outputMainTemplate() { + return $this->outputTemplate($this->moduleId); + } + + protected function outputTemplate($name) { + $templateFile = $this->moduleDir . '/' . $name . '-template.php'; + if ( file_exists($templateFile) ) { + /** @noinspection PhpUnusedLocalVariableInspection Used in some templates. */ + $moduleTabUrl = $this->getTabUrl(); + + $templateVariables = $this->getTemplateVariables($name); + if ( !empty($templateVariables) ) { + extract($templateVariables, EXTR_SKIP); + } + + /** @noinspection PhpIncludeInspection */ + require $templateFile; + return true; + } + return false; + } + + protected function getTemplateVariables(/** @noinspection PhpUnusedParameterInspection */ $templateName) { + //Override this method to pass variables to a template. + return array(); + } + + public function registerScripts() { + //Override this method to register scripts. + } + + public function enqueueTabScripts() { + //Override this method to add scripts to the $this->tabSlug tab. + } + + public function enqueueTabStyles() { + //Override this method to add stylesheets to the $this->tabSlug tab. + } + + /** + * @access private + * @param string $action + * @param array $post + */ + public function _processAction($action, $post = array()) { + if ( $action === $this->settingsFormAction ) { + check_admin_referer($action); + $this->handleSettingsForm($post); + } + } + + public function handleSettingsForm($post = array()) { + //Override this method to process a form submitted from the module's tab. + } + + protected function getScopedOption($name, $defaultValue = null) { + if ( $this->menuEditor->get_plugin_option('menu_config_scope') === 'site' ) { + return get_option($name, $defaultValue); + } else { + return get_site_option($name, $defaultValue); + } + } + + protected function setScopedOption($name, $value, $autoload = null) { + if ( $this->menuEditor->get_plugin_option('menu_config_scope') === 'site' ) { + update_option($name, $value, $autoload); + } else { + WPMenuEditor::atomic_update_site_option($name, $value); + } + } + + public function getModuleId() { + return $this->moduleId; + } + + public function getTabTitle() { + return $this->tabTitle; + } +} \ No newline at end of file diff --git a/includes/persistent-module.php b/includes/persistent-module.php new file mode 100644 index 0000000..1c1463d --- /dev/null +++ b/includes/persistent-module.php @@ -0,0 +1,67 @@ +<?php + +abstract class amePersistentModule extends ameModule { + /** + * @var string Database option where module settings are stored. + */ + protected $optionName = ''; + + /** + * @var array|null Module settings. NULL when settings haven't been loaded yet. + */ + protected $settings = null; + + /** + * @var array Default module settings. + */ + protected $defaultSettings = array(); + + public function __construct($menuEditor) { + if ( $this->optionName === '' ) { + throw new LogicException(__CLASS__ . '::$optionName is an empty string. You must set it to a valid option name.'); + } + + parent::__construct($menuEditor); + } + + public function loadSettings() { + if ( isset($this->settings) ) { + return $this->settings; + } + + $json = $this->getScopedOption($this->optionName, null); + if ( is_string($json) && !empty($json) ) { + $settings = json_decode($json, true); + } else { + $settings = array(); + } + + $this->settings = array_merge($this->defaultSettings, $settings); + + return $this->settings; + } + + public function saveSettings() { + $settings = wp_json_encode($this->settings); + //Save per site or site-wide based on plugin configuration. + $this->setScopedOption($this->optionName, $settings); + } + + public function mergeSettingsWith($newSettings) { + $this->settings = array_merge($this->loadSettings(), $newSettings); + return $this->settings; + } + + protected function getTemplateVariables($templateName) { + $variables = parent::getTemplateVariables($templateName); + if ( $templateName === $this->moduleId ) { + $variables = array_merge( + $variables, + array( + 'settings' => $this->loadSettings(), + ) + ); + } + return $variables; + } +} \ No newline at end of file diff --git a/includes/reflection-callable.php b/includes/reflection-callable.php new file mode 100644 index 0000000..78561a8 --- /dev/null +++ b/includes/reflection-callable.php @@ -0,0 +1,78 @@ +<?php + +class AmeReflectionCallable { + /** + * @var callable + */ + private $callback; + + /** + * @var ReflectionFunctionAbstract + */ + private $reflection; + + + /** + * AmeReflectionCallable constructor. + * + * @param callable $callback + * @throws ReflectionException + */ + public function __construct($callback) { + $this->callback = $callback; + $this->reflection = $this->getReflectionFunction($callback); + } + + /** + * @param callable $callback + * @return ReflectionFunctionAbstract + * @throws ReflectionException + */ + private function getReflectionFunction($callback) { + //Closure or a simple function name. + if ( $callback instanceof Closure || (is_string($callback) && strpos($callback, '::') === false) ) { + return new ReflectionFunction($callback); + } + + if ( is_string($callback) ) { + //ClassName::method + $callback = explode('::', $callback, 2); + } elseif ( is_object($callback) && method_exists($callback, '__invoke') ) { + //A callable object that has the magical __invoke method. + $callback = array($callback, '__invoke'); + } + + if ( is_object($callback[0]) ) { + $reflectionObject = new ReflectionObject($callback[0]); + } else { + $reflectionObject = new ReflectionClass($callback[0]); + } + + $methodName = $callback[1]; + if ( !$reflectionObject->hasMethod($methodName) ) { + //The callback appears to use magic methods. + if ( is_string($callback[0]) && $reflectionObject->hasMethod('__callStatic') ) { + $methodName = '__callStatic'; + } else if (is_object($callback[0]) && $reflectionObject->hasMethod('__call')) { + $methodName = '__call'; + } else { + //Probably an invalid callback. It could be a relative static method call, + //but we don't support those at the moment. + //See http://php.net/manual/en/language.types.callable.php + } + } + + return $reflectionObject->getMethod($methodName); + } + + /** + * Get the file name where the callable was defined. + * + * May return false for native PHP functions like 'strlen'. + * + * @return string|false + */ + public function getFileName() { + return $this->reflection->getFileName(); + } +} \ No newline at end of file diff --git a/includes/role-utils.php b/includes/role-utils.php new file mode 100644 index 0000000..85a20fa --- /dev/null +++ b/includes/role-utils.php @@ -0,0 +1,81 @@ +<?php +class ameRoleUtils { + /** + * Retrieve a list of all known, non-meta capabilities of all roles. + * + * @param bool $include_multisite_caps + * @return array Associative array with capability names as keys + */ + public static function get_all_capabilities($include_multisite_caps = null){ + if ( $include_multisite_caps === null ) { + $include_multisite_caps = is_multisite(); + } + + //Cache the results. + static $regular_cache = null, $multisite_cache = null; + if ( $include_multisite_caps ) { + if ( isset($multisite_cache) ) { + return $multisite_cache; + } + } else if ( isset($regular_cache) ) { + return $regular_cache; + } + + $wp_roles = self::get_roles(); + $capabilities = array(); + + //Iterate over all known roles and collect their capabilities + foreach($wp_roles->roles as $role){ + if ( !empty($role['capabilities']) && is_array($role['capabilities']) ){ //Being defensive here + //We use the "+" operator instead of array_merge() to combine arrays because we don't want + //integer keys to be renumbered. Technically, capabilities should be strings and not integers, + //but in practice some plugins do create integer capabilities. + $capabilities = $capabilities + $role['capabilities']; + } + } + $regular_cache = $capabilities; + + //Add multisite-specific capabilities (not listed in any roles in WP 3.0) + if ($include_multisite_caps) { + $multisite_caps = array( + 'manage_sites' => 1, + 'manage_network' => 1, + 'manage_network_users' => 1, + 'manage_network_themes' => 1, + 'manage_network_options' => 1, + 'manage_network_plugins' => 1, + ); + $capabilities = $capabilities + $multisite_caps; + $multisite_cache = $capabilities; + } + + return $capabilities; + } + + /** + * Retrieve a list of all known roles and their names. + * + * @return array Associative array with role IDs as keys and role display names as values + */ + public static function get_role_names(){ + $wp_roles = self::get_roles(); + $roles = array(); + + foreach($wp_roles->roles as $role_id => $role){ + $roles[$role_id] = $role['name']; + } + + return $roles; + } + + /** + * Get the global WP_Roles instance. + * + * @global WP_Roles $wp_roles + * @return WP_Roles + */ + public static function get_roles() { + //Requires WP 4.3.0. + return wp_roles(); + } +} \ No newline at end of file diff --git a/includes/settings-page.php b/includes/settings-page.php new file mode 100644 index 0000000..fac1eb7 --- /dev/null +++ b/includes/settings-page.php @@ -0,0 +1,582 @@ +<?php +/** + * This is the HTML template for the plugin settings page. + * + * These variables are provided by the plugin: + * @var array $settings Plugin settings. + * @var string $editor_page_url A fully qualified URL of the admin menu editor page. + * @var string $settings_page_url + * @var string $db_option_name + */ + +$currentUser = wp_get_current_user(); +$isMultisite = is_multisite(); +$isSuperAdmin = is_super_admin(); +$formActionUrl = add_query_arg('noheader', 1, $settings_page_url); +$isProVersion = apply_filters('admin_menu_editor_is_pro', false); +?> + +<?php do_action('admin_menu_editor-display_header'); ?> + + <form method="post" action="<?php echo esc_url($formActionUrl); ?>" id="ws_plugin_settings_form"> + + <table class="form-table"> + <tbody> + <tr> + <th scope="row"> + Who can access this plugin + </th> + <td> + <fieldset> + <p> + <label> + <input type="radio" name="plugin_access" value="super_admin" + <?php checked('super_admin', $settings['plugin_access']); ?> + <?php disabled( !$isSuperAdmin ); ?>> + Super Admin + + <?php if ( !$isMultisite ) : ?> + <br><span class="description"> + On a single site installation this is usually + the same as the Administrator role. + </span> + <?php endif; ?> + </label> + </p> + + <p> + <label> + <input type="radio" name="plugin_access" value="manage_options" + <?php checked('manage_options', $settings['plugin_access']); ?> + <?php disabled( !current_user_can('manage_options') ); ?>> + Anyone with the "manage_options" capability + + <br><span class="description"> + By default only Administrators have this capability. + </span> + </label> + </p> + + <p> + <label> + <input type="radio" name="plugin_access" value="specific_user" + <?php checked('specific_user', $settings['plugin_access']); ?> + <?php disabled( $isMultisite && !$isSuperAdmin ); ?>> + Only the current user + + <br> + <span class="description"> + Login: <?php echo esc_html($currentUser->user_login); ?>, + user ID: <?php echo esc_html(get_current_user_id()); ?> + </span> + </label> + </p> + </fieldset> + + <p> + <label> + <input type="checkbox" name="hide_plugin_from_others" value="1" + <?php checked( $settings['plugins_page_allowed_user_id'] !== null ); ?> + <?php disabled( !$isProVersion || ($isMultisite && !is_super_admin()) ); ?> + > + Hide "Admin Menu Editor<?php if ( $isProVersion ) { echo ' Pro'; } ?>" + <?php if ( defined('WS_ADMIN_BAR_EDITOR_FILE') || defined('AME_BRANDING_ADD_ON_FILE') ) { + echo 'and its add-ons'; + } ?> + from the "Plugins" page for other users + <?php if ( !$isProVersion ) { + echo '(Pro version only)'; + } ?> + </label> + </p> + </td> + </tr> + + <tr> + <th scope="row"> + Multisite settings + </th> + <td> + <fieldset id="ame-menu-scope-settings"> + <p> + <label> + <input type="radio" name="menu_config_scope" value="global" + id="ame-menu-config-scope-global" + <?php checked('global', $settings['menu_config_scope']); ?> + <?php disabled(!$isMultisite || !$isSuperAdmin); ?>> + Global — + Use the same admin menu settings for all network sites. + </label> + </p> + + <p> + <label> + <input type="radio" name="menu_config_scope" value="site" + <?php checked('site', $settings['menu_config_scope']); ?> + <?php disabled(!$isMultisite || !$isSuperAdmin); ?>> + Per-site — + Use different admin menu settings for each site. + </label> + </p> + </fieldset> + </td> + </tr> + + <?php do_action('admin-menu-editor-display_addons'); ?> + + <tr id="ame-available-modules"> + <th scope="row"> + Modules + <a class="ws_tooltip_trigger" + title="Modules are plugin features that can be turned on or off. + <br> + Turning off unused features will slightly increase performance and may help with certain compatibility issues. + "> + <div class="dashicons dashicons-info"></div> + </a> + </th> + <td> + <fieldset> + <?php + global $wp_menu_editor; + foreach ($wp_menu_editor->get_available_modules() as $moduleId => $module) { + if ( !empty($module['isAlwaysActive']) ) { + continue; + } + + $isCompatible = $wp_menu_editor->is_module_compatible($module); + $compatibilityNote = ''; + if ( !$isCompatible && !empty($module['requiredPhpVersion']) ) { + if ( version_compare(phpversion(), $module['requiredPhpVersion'], '<') ) { + $compatibilityNote = sprintf( + 'Required PHP version: %1$s or later. Installed PHP version: %2$s', + esc_html($module['requiredPhpVersion']), + esc_html(phpversion()) + ); + } + } + + echo '<p>'; + /** @noinspection HtmlUnknownAttribute */ + printf( + '<label> + <input type="checkbox" name="active_modules[]" value="%1$s" %2$s %3$s> + %4$s + </label>', + esc_attr($moduleId), + $wp_menu_editor->is_module_active($moduleId, $module) ? 'checked="checked"' : '', + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Constant strings. + $isCompatible ? '' : 'disabled="disabled"', + esc_html(!empty($module['title']) ? $module['title'] : $moduleId) + ); + + if ( !empty($compatibilityNote) ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $compatibilityNote was escaped when generated. + printf('<br><span class="description">%s</span>', $compatibilityNote); + } + + echo '</p>'; + } + ?> + </fieldset> + </td> + </tr> + + <tr> + <th scope="row">Interface</th> + <td> + <p> + <label> + <input type="checkbox" name="hide_advanced_settings" + <?php checked($settings['hide_advanced_settings']); ?>> + Hide advanced menu options by default + </label> + </p> + + <?php if ($isProVersion): ?> + <p> + <label> + <input type="checkbox" name="show_deprecated_hide_button" + <?php checked($settings['show_deprecated_hide_button']); ?>> + Enable the "Hide (cosmetic)" toolbar button + </label> + <br><span class="description"> + This button hides the selected menu item without making it inaccessible. + </span> + </p> + <?php endif; ?> + </td> + </tr> + + <tr> + <th scope="row">Editor colour scheme</th> + <td> + <fieldset> + <p> + <label> + <input type="radio" name="ui_colour_scheme" value="classic" + <?php checked('classic', $settings['ui_colour_scheme']); ?>> + Blue and yellow + </label> + </p> + + <p> + <label> + <input type="radio" name="ui_colour_scheme" value="modern-one" + <?php checked('modern-one', $settings['ui_colour_scheme']); ?>> + Modern + </label> + </p> + + <p> + <label> + <input type="radio" name="ui_colour_scheme" value="wp-grey" + <?php checked('wp-grey', $settings['ui_colour_scheme']); ?>> + Grey + </label> + </p> + </fieldset> + </td> + </tr> + + <?php if ($isProVersion): ?> + <tr> + <th scope="row">Show submenu icons</th> + <td> + <fieldset id="ame-submenu-icons-settings"> + <p> + <label> + <input type="radio" name="submenu_icons_enabled" value="always" + <?php checked('always', $settings['submenu_icons_enabled']); ?>> + Always + </label> + </p> + + <p> + <label> + <input type="radio" name="submenu_icons_enabled" value="if_custom" + <?php checked('if_custom', $settings['submenu_icons_enabled']); ?>> + Only when manually selected + </label> + </p> + + <p> + <label> + <input type="radio" name="submenu_icons_enabled" value="never" + <?php checked('never', $settings['submenu_icons_enabled']); ?>> + Never + </label> + </p> + </fieldset> + </td> + </tr> + + <tr> + <th scope="row"> + New menu visibility + <a class="ws_tooltip_trigger" + title="This setting controls the default permissions of menu items that are + not present in the last saved menu configuration. + <br><br> + This includes new menus added by plugins and themes. + In Multisite, it also applies to menus that exist on some sites but not others. + It doesn't affect menu items that you add through the Admin Menu Editor interface."> + <div class="dashicons dashicons-info"></div> + </a> + </th> + <td> + <fieldset> + <p> + <label> + <input type="radio" name="unused_item_permissions" value="unchanged" + <?php checked('unchanged', $settings['unused_item_permissions']); ?>> + Leave unchanged (default) + + <br><span class="description"> + No special restrictions. Visibility will depend on the plugin + that added the menus. + </span> + </label> + </p> + + <p> + <label> + <input type="radio" name="unused_item_permissions" value="match_plugin_access" + <?php checked('match_plugin_access', $settings['unused_item_permissions']); ?>> + Show only to users who can access this plugin + + <br><span class="description"> + Automatically hides all new and unrecognized menus from regular users. + To make new menus visible, you have to manually enable them in the menu editor. + </span> + </label> + </p> + + </fieldset> + </td> + </tr> + <?php endif; ?> + + <tr> + <th scope="row"> + New menu position + <a class="ws_tooltip_trigger" + title="This setting controls the position of menu items that are not present in the last saved menu + configuration. + <br><br> + This includes new menus added by plugins and themes. + In Multisite, it also applies to menus that exist only on certain sites but not on all sites. + It doesn't affect menu items that you add through the Admin Menu Editor interface."> + <div class="dashicons dashicons-info"></div> + </a> + </th> + <td> + <fieldset> + <p> + <label> + <input type="radio" name="unused_item_position" value="relative" + <?php checked('relative', $settings['unused_item_position']); ?>> + Maintain relative order + + <br><span class="description"> + Attempts to put new items in the same relative positions + as they would be in in the default admin menu. + </span> + </label> + </p> + + <p> + <label> + <input type="radio" name="unused_item_position" value="bottom" + <?php checked('bottom', $settings['unused_item_position']); ?>> + Bottom + + <br><span class="description"> + Puts new items at the bottom of the admin menu. + </span> + </label> + </p> + + </fieldset> + </td> + </tr> + + <?php + //The free version lacks the ability to render deeply nested menus in the dashboard, so the nesting + //options are hidden by default. However, if the user somehow acquires a configuration where this + //feature is enabled (e.g. by importing config from the Pro version), the free version can display + //and even edit that configuration to a limited extent. + if ( $isProVersion || !empty($settings['was_nesting_ever_changed']) ): + ?> + <tr> + <th scope="row"> + Three level menus + <a class="ws_tooltip_trigger ame-warning-tooltip" + title="Caution: Experimental feature.<br> + This feature might not work as expected and it could cause conflicts with other plugins or themes."> + <div class="dashicons dashicons-admin-tools"></div> + </a> + </th> + <td> + <fieldset> + <?php + $nestingOptions = array( + 'Ask on first use' => null, + 'Enabled' . ($isProVersion ? '' : ' (only in editor)') => true, + 'Disabled' => false, + ); + foreach ($nestingOptions as $label => $nestingSetting): + ?> + <p> + <label> + <input type="radio" name="deep_nesting_enabled" + value="<?php echo esc_attr(wp_json_encode($nestingSetting)); ?>" + <?php + if ( $settings['deep_nesting_enabled'] === $nestingSetting ) { + echo ' checked="checked"'; + } + ?>> + <?php echo esc_html($label); ?> + </label> + </p> + <?php endforeach; ?> + </fieldset> + </td> + </tr> + <?php endif; ?> + + <tr> + <th scope="row"> + WPML support + </th> + <td> + <p> + <label> + <input type="checkbox" name="wpml_support_enabled" + <?php checked($settings['wpml_support_enabled']); ?>> + Make edited menu titles translatable with WPML + + <br><span class="description"> + The titles will appear in the "Strings" section in WPML. + If you don't use WPML or a similar translation plugin, + you can safely disable this option. + </span> + </label> + </p> + </td> + </tr> + + <tr> + <th scope="row"> + bbPress override + </th> + <td> + <p> + <label> + <input type="checkbox" name="bbpress_override_enabled" + <?php checked($settings['bbpress_override_enabled']); ?>> + Prevent bbPress from resetting role capabilities + + <br><span class="description"> + By default, bbPress will automatically undo any changes that are made to dynamic + bbPress roles. Enable this option to override that behaviour and make it possible + to change bbPress role capabilities. + </span> + </label> + </p> + </td> + </tr> + + <tr> + <th scope="row">Error verbosity level</th> + <td> + <fieldset id="ame-submenu-icons-settings"> + <p> + <label> + <input type="radio" name="error_verbosity" value="<?php echo esc_attr(WPMenuEditor::VERBOSITY_LOW); ?>>" + <?php checked(WPMenuEditor::VERBOSITY_LOW, $settings['error_verbosity']); ?>> + Low + + <br><span class="description"> + Shows a generic error message without any details. + </span> + </label> + </p> + + <p> + <label> + <input type="radio" name="error_verbosity" value="<?php echo esc_attr(WPMenuEditor::VERBOSITY_NORMAL); ?>>" + <?php checked(WPMenuEditor::VERBOSITY_NORMAL, $settings['error_verbosity']); ?>> + Normal + + <br><span class="description"> + Shows a one or two sentence explanation. For example: "The current user doesn't have + the "manage_options" capability that is required to access the "Settings" menu item." + </span> + </label> + </p> + + <p> + <label> + <input type="radio" name="error_verbosity" value="<?php echo esc_attr(WPMenuEditor::VERBOSITY_VERBOSE); ?>>" + <?php checked(WPMenuEditor::VERBOSITY_VERBOSE, $settings['error_verbosity']); ?>> + Verbose + + <br><span class="description"> + Like "normal", but also includes a log of menu settings and permissions that + caused the current menu to be hidden. Useful for debugging. + </span> + </label> + </p> + </fieldset> + </td> + </tr> + + <tr> + <th scope="row">Debugging</th> + <td> + <p> + <label> + <input type="checkbox" name="security_logging_enabled" + <?php checked($settings['security_logging_enabled']); ?>> + Show menu access checks performed by the plugin on every admin page + </label> + <br><span class="description"> + This can help track down configuration problems and figure out why + your menu permissions don't work the way they should. + + Note: It's not recommended to use this option on a live site as + it can reveal information about your menu configuration. + </span> + </p> + + <p> + <label> + <input type="checkbox" name="force_custom_dashicons" + <?php checked($settings['force_custom_dashicons']); ?>> + Attempt to override menu icon CSS that was added by other plugins + </label> + </p> + + <p> + <label> + <input type="checkbox" name="compress_custom_menu" + <?php checked($settings['compress_custom_menu']); ?>> + Compress menu configuration data that's stored in the database + </label> + <br><span class="description"> + Significantly reduces the size of + the <code><?php echo esc_html($db_option_name); ?></code> DB option, + but adds decompression overhead to every page. + </span> + </p> + </td> + </tr> + + <tr> + <th scope="row">Server info</th> + <td> + <figure> + <figcaption>PHP error log:</figcaption> + + <code><?php + echo esc_html(ini_get('error_log')); + ?></code> + </figure> + + <figure> + <figcaption>PHP memory usage:</figcaption> + + <?php + printf( + '%.2f MiB of %s', + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- We're using sprintf() to format a float. + memory_get_peak_usage() / (1024 * 1024), + esc_html(ini_get('memory_limit')) + ); + ?> + </figure> + </td> + </tr> + </tbody> + </table> + + <input type="hidden" name="action" value="save_settings"> + <?php + wp_nonce_field('save_settings'); + submit_button(); + ?> + </form> + +<?php do_action('admin_menu_editor-display_footer'); ?> + +<script type="text/javascript"> + jQuery(function($) { + //Set up tooltips + $('.ws_tooltip_trigger').qtip({ + style: { + classes: 'qtip qtip-rounded ws_tooltip_node ws_wide_tooltip' + } + }); + }); +</script> \ No newline at end of file diff --git a/includes/shadow_plugin_framework.php b/includes/shadow_plugin_framework.php new file mode 100644 index 0000000..43b4f6d --- /dev/null +++ b/includes/shadow_plugin_framework.php @@ -0,0 +1,415 @@ +<?php + +/** + * @author W-Shadow + * @copyright 2008-2012 + */ + +//Load JSON functions for PHP < 5.2 +if ( !(function_exists('json_encode') && function_exists('json_decode')) && !class_exists('Services_JSON') ){ + $class_json_path = ABSPATH . WPINC . '/class-json.php'; + if ( file_exists($class_json_path) ){ + require $class_json_path; + } +} + +class MenuEd_ShadowPluginFramework { + public static $framework_version = '0.4.1'; + + public $is_mu_plugin = null; //True if installed in the mu-plugins directory, false otherwise + + protected $options = array(); + public $option_name = ''; //should be set or overridden by the plugin + protected $defaults = array(); //should be set or overridden by the plugin + protected $sitewide_options = false; //WPMU only : save the setting in a site-wide option + protected $serialize_with_json = false; //Use the JSON format for option storage + protected $zlib_compression = false; + + public $plugin_file = ''; //Filename of the plugin. + public $plugin_basename = ''; //Basename of the plugin, as returned by plugin_basename(). + public $plugin_dir_url = ''; //The URL of the plugin's folder + + protected $magic_hooks = false; //Automagically set up hooks for all methods named "hook_[hookname]" . + protected $magic_hook_priority = 10; //Priority for magically set hooks. + + protected $settings_link = ''; //If set, this will be automatically added after "Deactivate"/"Edit". + + /** + * Class constructor. Populates some internal fields, then calls the plugin's own + * initializer (if any). + * + * @param string $plugin_file Plugin's filename. Usually you can just use __FILE__. + * @param string $option_name + */ + function __construct( $plugin_file = '', $option_name = null ){ + if ($plugin_file == ''){ + //Try to guess the name of the file that included this file. + //Not implemented yet. + } + $this->option_name = $option_name; + + if ( is_null($this->is_mu_plugin) ) + $this->is_mu_plugin = $this->is_in_wpmu_plugin_dir($plugin_file); + + $this->plugin_file = $plugin_file; + $this->plugin_basename = plugin_basename($this->plugin_file); + + $this->plugin_dir_url = rtrim(plugin_dir_url($this->plugin_file), '/'); + + /************************************ + Add the default hooks + ************************************/ + add_action('activate_'.$this->plugin_basename, array($this,'activate')); + add_action('deactivate_'.$this->plugin_basename, array($this,'deactivate')); + + $this->init(); //Call the plugin's init() function + $this->init_finish(); //Complete initialization by loading settings, etc + } + + /** + * Init the plugin. Should be overridden in a sub-class. + * Called by the class constructor. + * + * @return void + */ + function init(){ + //Do nothing. + } + + /** + * Initialize settings and set up magic hooks. + * + * @return void + */ + function init_finish(){ + /************************************ + Load settings + ************************************/ + //The provided $option_name overrides the default only if it is set to something useful + if ( $this->option_name == '' ) { + //Generate a unique name + $this->option_name = 'plugin_'.md5($this->plugin_basename); + } + + //Do we need to load the plugin's settings? + if ($this->option_name != null){ + $this->load_options(); + } + + //Add a "Settings" action link + if ($this->settings_link) + add_filter('plugin_action_links', array($this, 'plugin_action_links'), 10, 2); + + if ($this->magic_hooks) + $this->set_magic_hooks(); + } + + /** + * Load the plugin's configuration. + * Loads the specified option into $this->options, substituting defaults where necessary. + * + * @param string $option_name Optional. The slug of the option to load. If not set, the value of $this->option_name will be used instead. + * @return boolean TRUE if options were loaded okay and FALSE otherwise. + */ + function load_options($option_name = null){ + if ( empty($option_name) ){ + $option_name = $this->option_name; + } + + if ( $this->sitewide_options ) { + $this->options = get_site_option($option_name); + } else { + $this->options = get_option($option_name); + } + + $prefix = 'gzcompress:'; + if ( + is_string($this->options) + && (substr($this->options, 0, strlen($prefix)) === $prefix) + && function_exists('gzuncompress') + ) { + //TODO: Maybe this would be faster if we stored the flag separately? + $this->options = unserialize(gzuncompress(base64_decode(substr($this->options, strlen($prefix))))); + } + + if ( $this->serialize_with_json || is_string($this->options) ){ + $this->options = $this->json_decode($this->options, true); + } + + if(!is_array($this->options)){ + $this->options = $this->defaults; + return false; + } else { + $this->options = array_merge($this->defaults, $this->options); + return true; + } + } + + /** + * ShadowPluginFramework::save_options() + * Saves the $options array to the database. + * + * @return bool + */ + function save_options(){ + if ($this->option_name) { + $stored_options = $this->options; + if ( $this->serialize_with_json ){ + $stored_options = $this->json_encode($stored_options); + } + + if ( $this->zlib_compression && function_exists('gzcompress') ) { + $stored_options = 'gzcompress:' . base64_encode(gzcompress(serialize($stored_options))); + } + + if ( $this->sitewide_options && is_multisite() ) { + return self::atomic_update_site_option($this->option_name, $stored_options); + } else { + return update_option($this->option_name, $stored_options); + } + } + return false; + } + + /** + * Like update_site_option, but simulates record locking by using the MySQL GET_LOCK() function. + * + * The goal is to reduce the risk of triggering a race condition in update_site_option. + * It would be better to use real transactions, but many (most?) WordPress sites use storage engines + * that don't support transactions, like MyISAM. + * + * @param string $option_name + * @param mixed $data + * @return bool + */ + public static function atomic_update_site_option($option_name, $data) { + // phpcs:disable WordPress.DB.DirectDatabaseQuery -- WPDB doesn't have utility methods for locking, so direct queries are needed. + global $wpdb; /** @var wpdb $wpdb */ + $lock = 'ame.' . (is_multisite() ? $wpdb->sitemeta : $wpdb->options ) . '.' . $option_name; + + //Lock. Note that we're being really optimistic and not checking the return value. + $wpdb->query($wpdb->prepare("SELECT GET_LOCK(%s, %d)", $lock, 5)); + //Update. + $updated = update_site_option($option_name, $data); + //Unlock. + $wpdb->query($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock)); + + return $updated; + // phpcs:enable + } + + + /** + * Backwards compatible json_decode. + * + * @param string $data + * @param bool $assoc Decode objects as associative arrays. + * @return mixed + */ + function json_decode($data, $assoc=false){ + if ( function_exists('json_decode') ){ + return json_decode($data, $assoc); + } + if ( class_exists('Services_JSON') ){ + $flag = $assoc?SERVICES_JSON_LOOSE_TYPE:0; + $json = new Services_JSON($flag); + return( $json->decode($data) ); + } else { + throw new RuntimeException('No JSON parser available'); + } + } + + /** + * Backwards compatible json_encode. + * + * @param mixed $data + * @return string + */ + function json_encode($data) { + if ( function_exists('wp_json_encode') ) { + return wp_json_encode($data); + } else if ( function_exists('json_encode') ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- Fallback. + return json_encode($data); + } + + if ( class_exists('Services_JSON') ){ + $json = new Services_JSON(); + return( $json->encodeUnsafe($data) ); + } else { + throw new RuntimeException('No JSON parser available'); + } + } + + + /** + * ShadowPluginFramework::set_magic_hooks() + * Automagically sets up hooks for all methods named "hook_[tag]". Uses the Reflection API. + * + * @return void + */ + function set_magic_hooks(){ + $class = new ReflectionClass(get_class($this)); + $methods = $class->getMethods(); + + foreach ($methods as $method){ /** @var ReflectionMethod $method */ + //Check if the method name starts with "hook_" + if (strpos($method->name, 'hook_') === 0){ + //Get the hook's tag from the method name + $hook = substr($method->name, 5); + //Add the hook. Uses add_filter because add_action is simply a wrapper of the same. + add_filter($hook, array($this, $method->name), + $this->get_magic_hook_priority(), $method->getNumberOfParameters()); + } + } + + unset($class); + } + + public function get_magic_hook_priority() { + return $this->magic_hook_priority; + } + + + /** + * ShadowPluginFramework::activate() + * Stub function for the activation hook. + * + * @return void + */ + function activate(){ + + } + + /** + * ShadowPluginFramework::deactivate() + * Stub function for the deactivation hook. Does nothing. + * + * @return void + */ + function deactivate(){ + + } + + /** + * ShadowPluginFramework::plugin_action_links() + * Adds a "Settings" link to the plugin's action links. Default handler for the 'plugin_action_links' hook. + * + * @param array $links + * @param string $file + * @return array + */ + function plugin_action_links($links, $file) { + if (($file == $this->plugin_basename) && is_array($links)) { + $links[] = "<a href='" . $this->settings_link . "'>" . __('Settings') . "</a>"; + } + return $links; + } + + /** + * ShadowPluginFramework::uninstall() + * Default uninstaller. Removes the plugins configuration record (if available). + * + * @return void + */ + function uninstall(){ + if ($this->option_name) + delete_option($this->option_name); + } + + /** + * Checks if the specified file is inside the mu-plugins directory. + * + * @param string $filename The filename to check. Leave blank to use the current plugin's filename. + * @return bool + */ + function is_in_wpmu_plugin_dir( $filename = '' ){ + if ( !defined('WPMU_PLUGIN_DIR') ) { + return false; + } + + if ( empty($filename) ){ + $filename = $this->plugin_file; + } + + $normalizedMuPluginDir = realpath(WPMU_PLUGIN_DIR); + $normalizedFileName = realpath($filename); + + //If realpath() fails, just normalize the syntax instead. + if ( empty($normalizedFileName) || empty($normalizedMuPluginDir) ) { + $normalizedMuPluginDir = wp_normalize_path(WPMU_PLUGIN_DIR); + $normalizedFileName = wp_normalize_path($filename); + } + //Yet another fallback if the above also fails. + if ( !is_string($normalizedMuPluginDir) || empty($normalizedMuPluginDir) ) { + if ( is_string(WPMU_PLUGIN_DIR) ) { + $normalizedMuPluginDir = WPMU_PLUGIN_DIR; + } else { + return false; + } + } + return (strpos( $normalizedFileName, $normalizedMuPluginDir ) !== false); + } + + /** + * Check if the plugin is active for the entire network. + * Will return true when the plugin is installed in /mu-plugins/ (WPMU, pre-3.0) + * or has been activated via "Network Activate" (WP 3.0+). + * + * Blame the ridiculous blog/site/network confusion perpetrated by + * the WP API for the silly name. + * + * @return bool + */ + function is_super_plugin(){ + if ( is_null($this->is_mu_plugin) ){ + $this->is_mu_plugin = $this->is_in_wpmu_plugin_dir($this->plugin_file); + } + + if ( $this->is_mu_plugin ){ + return true; + } else { + return $this->is_plugin_active_for_network($this->plugin_basename); + } + } + + /** + * Check whether the plugin is active for the entire network. + * + * Silly WP doesn't load the file that contains this native function until *after* + * all plugins are loaded, so until then we use a copy-pasted version of the same. + * + * @param string $plugin + * @return bool + */ + function is_plugin_active_for_network( $plugin ) { + if ( function_exists('is_plugin_active_for_network') ){ + return is_plugin_active_for_network($plugin); + } + + if ( !is_multisite() ) + return false; + + $plugins = get_site_option( 'active_sitewide_plugins'); + if ( isset($plugins[$plugin]) ) + return true; + + return false; + } + + /** + * Check whether the plugin is active. + * + * @see self::is_plugin_active_for_network + * + * @param string $plugin + * @return bool + */ + function is_plugin_active($plugin) { + if ( function_exists('is_plugin_active') ) { + return is_plugin_active($plugin); + } + return in_array( $plugin, (array) get_option('active_plugins', array()) ) || $this->is_plugin_active_for_network($plugin); + } + +} \ No newline at end of file diff --git a/includes/shortcodes.php b/includes/shortcodes.php new file mode 100644 index 0000000..d258414 --- /dev/null +++ b/includes/shortcodes.php @@ -0,0 +1,132 @@ +<?php + +class ameCoreShortcodes { + protected static $allowedUserFields = array( + 'ID', + 'user_login', + 'display_name', + 'first_name', + 'last_name', + 'nickname', + 'description', + 'locale', + 'user_nicename', + 'user_url', + 'user_registered', + 'user_status', + ); + + public function register() { + add_shortcode('ame-wp-admin', array($this, 'handleAdminUrl')); + add_shortcode('ame-home-url', array($this, 'handleHomeUrl')); + add_shortcode('ame-user-info', array($this, 'handleUserInfo')); + //todo: Maybe a "current post id" shortcode? Would be useful for toolbar links. + } + + /** @noinspection PhpUnusedParameterInspection Parameters are required by the shortcode API. */ + public function handleAdminUrl($attributes = array(), $content = null, $tag = 'ame-wp-admin') { + if ( is_callable('self_admin_url') ) { + return self_admin_url(); + } + return '[' . $tag . ']'; + } + + /** @noinspection PhpUnusedParameterInspection */ + public function handleHomeUrl($attributes = array(), $content = null, $tag = 'ame-home-url') { + if ( is_callable('home_url') ) { + return home_url(); + } + return '[' . $tag . ']'; + } + + public function handleUserInfo( + $attributes = array(), /** @noinspection PhpUnusedParameterInspection */ + $content = null, + $tag = 'ame-user-info' + ) { + $attributes = shortcode_atts( + array( + 'field' => 'user_login', + 'placeholder' => '(No user)', + 'escape' => 'auto', + ), + $attributes, + $tag + ); + + $placeholder = $attributes['placeholder']; + + $field = strtolower($attributes['field']); + if ( $field === 'id' ) { + $field = 'ID'; + } + if ( !in_array($field, self::$allowedUserFields) ) { + return '(Error: Unsupported field)'; + } + + //Get the currently logged-in user. + $user = null; + if ( is_callable('wp_get_current_user') ) { + $user = wp_get_current_user(); + } + + //wp_get_current_user() won't work when this shortcode is used in a login redirect (for example), + //but we can try to get the current user from our "Redirects" module. + if ( !self::couldBeValidUserObject($user) ) { + $user = apply_filters('admin_menu_editor-redirected_user', null); + } + + //Display the placeholder text if nobody is logged in or the user doesn't exist. + if ( !self::couldBeValidUserObject($user) ) { + return $placeholder; + } + + $escapingHandlers = array( + 'html' => 'esc_html', + 'attr' => 'esc_attr', + 'js' => 'esc_js', + 'none' => array($this, 'identity'), + ); + + $escape = $attributes['escape']; + //By default, escape HTML special characters only if in the Loop. + if ( $escape === 'auto' ) { + if ( is_callable('in_the_loop') && in_the_loop() ) { + $escape = 'html'; + } else { + $escape = 'none'; + } + } + if ( !array_key_exists($escape, $escapingHandlers) ) { + return '(Error: Unsupported escape setting)'; + } + if ( is_callable($escapingHandlers[$escape]) ) { + $escapeCallback = $escapingHandlers[$escape]; + } else { + return '(Error: The specified escape function is not available)'; + } + + if ( isset($user->$field) ) { + return call_user_func($escapeCallback, $user->$field); + } + return $placeholder; + } + + /** + * @param WP_User|null $user + * @return bool + */ + protected static function couldBeValidUserObject($user) { + if ( empty($user) || !isset($user->ID) || ($user->ID === 0) ) { + return false; + } + return true; + } + + protected function identity($value) { + return $value; + } +} + +$wsAmeShortcodes = new ameCoreShortcodes(); +$wsAmeShortcodes->register(); diff --git a/includes/test-access-screen.php b/includes/test-access-screen.php new file mode 100644 index 0000000..e00c507 --- /dev/null +++ b/includes/test-access-screen.php @@ -0,0 +1,72 @@ +<?php +if ( !defined('ABSPATH') ) { + exit('Direct access denied'); +} +?> +<div id="ws_ame_test_access_screen"> + <div id="ws_ame_test_inputs"> + <label class="ws_ame_test_input"> + <span class="ws_ame_test_input_name">Menu item</span> + <select name="ws_ame_test_menu_item" id="ws_ame_test_menu_item" class="ws_ame_test_input_value"></select> + </label> + + <label class="ws_ame_test_input"> + <span class="ws_ame_test_input_name">Log in as user</span> + <input type="text" class="ws_ame_test_input_value" id="ws_ame_test_access_username"> + </label> + + <label class="ws_ame_test_input"> + <span class="ws_ame_test_input_name">Relevant role (optional)</span> + <select name="ws_ame_test_relevant_actor" id="ws_ame_test_relevant_actor" class="ws_ame_test_input_value"></select> + </label> + + <div class="ws_ame_test_input"> + <span class="ws_ame_test_input_name">What you want to happen</span> + <fieldset class="ws_ame_test_input_value"> + <label> + <input type="radio" name="ws_ame_desired_test_outcome" value="visible" checked> + Menu is <strong>visible</strong> + </label><br> + <label> + <input type="radio" name="ws_ame_desired_test_outcome" value="hidden"> + Menu is <strong>hidden</strong> + </label><br> + </fieldset> + </div> + + <div id="ws_ame_test_actions"> + <div id="ws_ame_test_button_container"> + <input type="button" class="button-primary" value="Start Test" id="ws_ame_start_access_test"> + </div> + <div id="ws_ame_test_progress"> + <span class="spinner is-active"></span> + <span id="ws_ame_test_progress_text"> + Test hasn't started yet. + </span> + </div> + </div> + + <div class="clear"></div> + </div> + + <div id="ws_ame_test_access_body"> + <div id="ws_ame_test_frame_container"> + <span id="ws_ame_test_frame_placeholder"> + <em>Test page will appear here.</em><br> + </span> + <iframe src="" frameborder="0" sandbox="allow-scripts" id="ws_ame_test_access_frame"></iframe> + </div> + + <div id="ws_ame_test_access_sidebar"> + <span id="ws_ame_test_output_placeholder"> + <em>Analysis will appear here.</em> + </span> + <div id="ws_ame_test_output"> + <h4>Result</h4> + + <h4>Analysis</h4> + <h4>Suggestions</h4> + </div> + </div> + </div> +</div> diff --git a/includes/version-conflict-check.php b/includes/version-conflict-check.php new file mode 100644 index 0000000..111c6d7 --- /dev/null +++ b/includes/version-conflict-check.php @@ -0,0 +1,25 @@ +<?php +//It's not possible to have two versions of this plugin active at the same time. Abort plugin load +//and display an error if we detect that another version has already been loaded. +if ( class_exists('WPMenuEditor') ) { + + function ws_ame_activation_conflict() { + if ( !current_user_can('activate_plugins') ) { + return; //The current user can't do anything about the problem. + } + ?> + <div class="error fade"> + <p> + <strong>Error: Another version of Admin Menu Editor is already active.</strong><br> + Please deactivate the older version. It is not possible to run two different versions + of this plugin at the same time. + </p> + </div> + <?php + } + + add_action('admin_notices', 'ws_ame_activation_conflict'); + return true; //Conflict detected. +} + +return false; //No conflict. \ No newline at end of file diff --git a/js/actor-manager.js b/js/actor-manager.js new file mode 100644 index 0000000..9fea149 --- /dev/null +++ b/js/actor-manager.js @@ -0,0 +1,669 @@ +/// <reference path="lodash-3.10.d.ts" /> +/// <reference path="knockout.d.ts" /> +/// <reference path="common.d.ts" /> +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var AmeBaseActor = /** @class */ (function () { + function AmeBaseActor(id, displayName, capabilities, metaCapabilities) { + if (metaCapabilities === void 0) { metaCapabilities = {}; } + this.displayName = '[Error: No displayName set]'; + this.groupActors = []; + this.id = id; + this.displayName = displayName; + this.capabilities = capabilities; + this.metaCapabilities = metaCapabilities; + } + /** + * Get the capability setting directly from this actor, ignoring capabilities + * granted by roles, the Super Admin flag, or the grantedCapabilities feature. + * + * Returns NULL for capabilities that are neither explicitly granted nor denied. + * + * @param {string} capability + * @returns {boolean|null} + */ + AmeBaseActor.prototype.hasOwnCap = function (capability) { + if (this.capabilities.hasOwnProperty(capability)) { + return this.capabilities[capability]; + } + if (this.metaCapabilities.hasOwnProperty(capability)) { + return this.metaCapabilities[capability]; + } + return null; + }; + AmeBaseActor.getActorSpecificity = function (actorId) { + var actorType = actorId.substring(0, actorId.indexOf(':')), specificity; + switch (actorType) { + case 'role': + specificity = 1; + break; + case 'special': + specificity = 2; + break; + case 'user': + specificity = 10; + break; + default: + specificity = 0; + } + return specificity; + }; + AmeBaseActor.prototype.toString = function () { + return this.displayName + ' [' + this.id + ']'; + }; + AmeBaseActor.prototype.getId = function () { + return this.id; + }; + AmeBaseActor.prototype.getDisplayName = function () { + return this.displayName; + }; + return AmeBaseActor; +}()); +var AmeRole = /** @class */ (function (_super) { + __extends(AmeRole, _super); + function AmeRole(roleId, displayName, capabilities, metaCapabilities) { + if (metaCapabilities === void 0) { metaCapabilities = {}; } + var _this = _super.call(this, 'role:' + roleId, displayName, capabilities, metaCapabilities) || this; + _this.name = roleId; + return _this; + } + AmeRole.prototype.hasOwnCap = function (capability) { + //In WordPress, a role name is also a capability name. Users that have the role "foo" always + //have the "foo" capability. It's debatable whether the role itself actually has that capability + //(WP_Role says no), but it's convenient to treat it that way. + if (capability === this.name) { + return true; + } + return _super.prototype.hasOwnCap.call(this, capability); + }; + return AmeRole; +}(AmeBaseActor)); +var AmeUser = /** @class */ (function (_super) { + __extends(AmeUser, _super); + function AmeUser(userLogin, displayName, capabilities, roles, isSuperAdmin, userId, metaCapabilities) { + if (isSuperAdmin === void 0) { isSuperAdmin = false; } + if (metaCapabilities === void 0) { metaCapabilities = {}; } + var _this = _super.call(this, 'user:' + userLogin, displayName, capabilities, metaCapabilities) || this; + _this.userId = 0; + _this.isSuperAdmin = false; + _this.avatarHTML = ''; + _this.userLogin = userLogin; + _this.roles = roles; + _this.isSuperAdmin = isSuperAdmin; + _this.userId = userId || 0; + if (_this.isSuperAdmin) { + _this.groupActors.push(AmeSuperAdmin.permanentActorId); + } + for (var i = 0; i < _this.roles.length; i++) { + _this.groupActors.push('role:' + _this.roles[i]); + } + return _this; + } + AmeUser.createFromProperties = function (properties) { + var user = new AmeUser(properties.user_login, properties.display_name, properties.capabilities, properties.roles, properties.is_super_admin, properties.hasOwnProperty('id') ? properties.id : null, properties.meta_capabilities); + if (properties.avatar_html) { + user.avatarHTML = properties.avatar_html; + } + return user; + }; + return AmeUser; +}(AmeBaseActor)); +var AmeSuperAdmin = /** @class */ (function (_super) { + __extends(AmeSuperAdmin, _super); + function AmeSuperAdmin() { + return _super.call(this, AmeSuperAdmin.permanentActorId, 'Super Admin', {}) || this; + } + AmeSuperAdmin.prototype.hasOwnCap = function (capability) { + //The Super Admin has all possible capabilities except the special "do_not_allow" flag. + return (capability !== 'do_not_allow'); + }; + AmeSuperAdmin.permanentActorId = 'special:super_admin'; + return AmeSuperAdmin; +}(AmeBaseActor)); +var AmeActorManager = /** @class */ (function () { + function AmeActorManager(roles, users, isMultisite, suspectedMetaCaps) { + if (isMultisite === void 0) { isMultisite = false; } + if (suspectedMetaCaps === void 0) { suspectedMetaCaps = {}; } + var _this = this; + this.roles = {}; + this.users = {}; + this.grantedCapabilities = {}; + this.isMultisite = false; + this.exclusiveSuperAdminCapabilities = {}; + this.tagMetaCaps = {}; + this.suggestedCapabilities = []; + this.isMultisite = !!isMultisite; + AmeActorManager._.forEach(roles, function (roleDetails, id) { + var role = new AmeRole(id, roleDetails.name, roleDetails.capabilities, AmeActorManager._.get(roleDetails, 'meta_capabilities', {})); + _this.roles[role.name] = role; + }); + AmeActorManager._.forEach(users, function (userDetails) { + var user = AmeUser.createFromProperties(userDetails); + _this.users[user.userLogin] = user; + }); + this.superAdmin = new AmeSuperAdmin(); + this.suspectedMetaCaps = suspectedMetaCaps; + var exclusiveCaps = [ + 'update_core', 'update_plugins', 'delete_plugins', 'install_plugins', 'upload_plugins', 'update_themes', + 'delete_themes', 'install_themes', 'upload_themes', 'update_core', 'edit_css', 'unfiltered_html', + 'edit_files', 'edit_plugins', 'edit_themes', 'delete_user', 'delete_users' + ]; + for (var i = 0; i < exclusiveCaps.length; i++) { + this.exclusiveSuperAdminCapabilities[exclusiveCaps[i]] = true; + } + var tagMetaCaps = [ + 'manage_post_tags', 'edit_categories', 'edit_post_tags', 'delete_categories', + 'delete_post_tags' + ]; + for (var i = 0; i < tagMetaCaps.length; i++) { + this.tagMetaCaps[tagMetaCaps[i]] = true; + } + } + // noinspection JSUnusedGlobalSymbols + AmeActorManager.prototype.actorCanAccess = function (actorId, grantAccess, defaultCapability) { + if (defaultCapability === void 0) { defaultCapability = null; } + if (grantAccess.hasOwnProperty(actorId)) { + return grantAccess[actorId]; + } + if (defaultCapability !== null) { + return this.hasCap(actorId, defaultCapability, grantAccess); + } + return true; + }; + AmeActorManager.prototype.getActor = function (actorId) { + if (actorId === AmeSuperAdmin.permanentActorId) { + return this.superAdmin; + } + var separator = actorId.indexOf(':'), actorType = actorId.substring(0, separator), actorKey = actorId.substring(separator + 1); + if (actorType === 'role') { + return this.roles.hasOwnProperty(actorKey) ? this.roles[actorKey] : null; + } + else if (actorType === 'user') { + return this.users.hasOwnProperty(actorKey) ? this.users[actorKey] : null; + } + throw { + name: 'InvalidActorException', + message: "There is no actor with that ID, or the ID is invalid.", + value: actorId + }; + }; + AmeActorManager.prototype.actorExists = function (actorId) { + try { + return (this.getActor(actorId) !== null); + } + catch (exception) { + if (exception.hasOwnProperty('name') && (exception.name === 'InvalidActorException')) { + return false; + } + else { + throw exception; + } + } + }; + AmeActorManager.prototype.hasCap = function (actorId, capability, context) { + context = context || {}; + return this.actorHasCap(actorId, capability, [context, this.grantedCapabilities]); + }; + AmeActorManager.prototype.hasCapByDefault = function (actorId, capability) { + return this.actorHasCap(actorId, capability); + }; + AmeActorManager.prototype.actorHasCap = function (actorId, capability, contextList) { + //It's like the chain-of-responsibility pattern. + //Everybody has the "exist" cap and it can't be removed or overridden by plugins. + if (capability === 'exist') { + return true; + } + capability = this.mapMetaCap(capability); + var result = null; + //Step #1: Check temporary context - unsaved caps, etc. Optional. + //Step #2: Check granted capabilities. Default on, but can be skipped. + if (contextList) { + //Check for explicit settings first. + var actorValue = void 0, len = contextList.length; + for (var i = 0; i < len; i++) { + if (contextList[i].hasOwnProperty(actorId)) { + actorValue = contextList[i][actorId]; + if (typeof actorValue === 'boolean') { + //Context: grant_access[actorId] = boolean. Necessary because enabling a menu item for a role + //should also enable it for all users who have that role (unless explicitly disabled for a user). + return actorValue; + } + else if (actorValue.hasOwnProperty(capability)) { + //Context: grantedCapabilities[actor][capability] = boolean|[boolean, ...] + result = actorValue[capability]; + return (typeof result === 'boolean') ? result : result[0]; + } + } + } + } + //Step #3: Check owned/default capabilities. Always checked. + var actor = this.getActor(actorId); + if (actor === null) { + return false; + } + var hasOwnCap = actor.hasOwnCap(capability); + if (hasOwnCap !== null) { + return hasOwnCap; + } + //Step #4: Users can get a capability through their roles or the "super admin" flag. + //Only users can have inherited capabilities, so if this actor is not a user, we're done. + if (actor instanceof AmeUser) { + //Note that Super Admin has priority. If the user is a super admin, their roles are ignored. + if (actor.isSuperAdmin) { + return this.actorHasCap('special:super_admin', capability, contextList); + } + //Check if any of the user's roles have the capability. + result = null; + for (var index = 0; index < actor.roles.length; index++) { + var roleHasCap = this.actorHasCap('role:' + actor.roles[index], capability, contextList); + if (roleHasCap !== null) { + result = result || roleHasCap; + } + } + if (result !== null) { + return result; + } + } + if (this.suspectedMetaCaps.hasOwnProperty(capability)) { + return null; + } + return false; + }; + AmeActorManager.prototype.mapMetaCap = function (capability) { + if (capability === 'customize') { + return 'edit_theme_options'; + } + else if (capability === 'delete_site') { + return 'manage_options'; + } + //In Multisite, some capabilities are only available to Super Admins. + if (this.isMultisite && this.exclusiveSuperAdminCapabilities.hasOwnProperty(capability)) { + return AmeSuperAdmin.permanentActorId; + } + if (this.tagMetaCaps.hasOwnProperty(capability)) { + return 'manage_categories'; + } + if ((capability === 'assign_categories') || (capability === 'assign_post_tags')) { + return 'edit_posts'; + } + return capability; + }; + /* ------------------------------- + * Roles + * ------------------------------- */ + AmeActorManager.prototype.getRoles = function () { + return this.roles; + }; + AmeActorManager.prototype.roleExists = function (roleId) { + return this.roles.hasOwnProperty(roleId); + }; + ; + AmeActorManager.prototype.getSuperAdmin = function () { + return this.superAdmin; + }; + /* ------------------------------- + * Users + * ------------------------------- */ + AmeActorManager.prototype.getUsers = function () { + return this.users; + }; + AmeActorManager.prototype.getUser = function (login) { + return this.users.hasOwnProperty(login) ? this.users[login] : null; + }; + AmeActorManager.prototype.addUsers = function (newUsers) { + var _this = this; + AmeActorManager._.forEach(newUsers, function (user) { + _this.users[user.userLogin] = user; + }); + }; + AmeActorManager.prototype.getGroupActorsFor = function (userLogin) { + return this.users[userLogin].groupActors; + }; + /* ------------------------------- + * Granted capability manipulation + * ------------------------------- */ + AmeActorManager.prototype.setGrantedCapabilities = function (newGrants) { + this.grantedCapabilities = AmeActorManager._.cloneDeep(newGrants); + }; + AmeActorManager.prototype.getGrantedCapabilities = function () { + return this.grantedCapabilities; + }; + /** + * Grant or deny a capability to an actor. + */ + AmeActorManager.prototype.setCap = function (actor, capability, hasCap, sourceType, sourceName) { + this.setCapInContext(this.grantedCapabilities, actor, capability, hasCap, sourceType, sourceName); + }; + AmeActorManager.prototype.setCapInContext = function (context, actor, capability, hasCap, sourceType, sourceName) { + capability = this.mapMetaCap(capability); + var grant = sourceType ? [hasCap, sourceType, sourceName || null] : hasCap; + AmeActorManager._.set(context, [actor, capability], grant); + }; + AmeActorManager.prototype.resetCapInContext = function (context, actor, capability) { + capability = this.mapMetaCap(capability); + if (AmeActorManager._.has(context, [actor, capability])) { + delete context[actor][capability]; + } + }; + /** + * Reset all capabilities granted to an actor. + * @param actor + * @return boolean TRUE if anything was reset or FALSE if the actor didn't have any granted capabilities. + */ + AmeActorManager.prototype.resetActorCaps = function (actor) { + if (AmeActorManager._.has(this.grantedCapabilities, actor)) { + delete this.grantedCapabilities[actor]; + return true; + } + return false; + }; + /** + * Remove redundant granted capabilities. + * + * For example, if user "jane" has been granted the "edit_posts" capability both directly and via the Editor role, + * the direct grant is redundant. We can remove it. Jane will still have "edit_posts" because she's an editor. + */ + AmeActorManager.prototype.pruneGrantedUserCapabilities = function () { + var _this = this; + var _ = AmeActorManager._, pruned = _.cloneDeep(this.grantedCapabilities), context = [pruned]; + var actorKeys = _(pruned).keys().filter(function (actorId) { + //Skip users that are not loaded. + var actor = _this.getActor(actorId); + if (actor === null) { + return false; + } + return (actor instanceof AmeUser); + }).value(); + _.forEach(actorKeys, function (actor) { + _.forEach(_.keys(pruned[actor]), function (capability) { + var grant = pruned[actor][capability]; + delete pruned[actor][capability]; + var hasCap = _.isArray(grant) ? grant[0] : grant, hasCapWhenPruned = !!_this.actorHasCap(actor, capability, context); + if (hasCap !== hasCapWhenPruned) { + pruned[actor][capability] = grant; //Restore. + } + }); + }); + this.setGrantedCapabilities(pruned); + return pruned; + }; + ; + /** + * Compare the specificity of two actors. + * + * Returns 1 if the first actor is more specific than the second, 0 if they're both + * equally specific, and -1 if the second actor is more specific. + * + * @return {Number} + */ + AmeActorManager.compareActorSpecificity = function (actor1, actor2) { + var delta = AmeBaseActor.getActorSpecificity(actor1) - AmeBaseActor.getActorSpecificity(actor2); + if (delta !== 0) { + delta = (delta > 0) ? 1 : -1; + } + return delta; + }; + ; + AmeActorManager.prototype.generateCapabilitySuggestions = function (capPower) { + var _ = AmeActorManager._; + var capsByPower = _.memoize(function (role) { + var sortedCaps = _.reduce(role.capabilities, function (result, hasCap, capability) { + if (hasCap) { + result.push({ + capability: capability, + power: _.get(capPower, [capability], 0) + }); + } + return result; + }, []); + sortedCaps = _.sortBy(sortedCaps, function (item) { return -item.power; }); + return sortedCaps; + }); + var rolesByPower = _.values(this.getRoles()).sort(function (a, b) { + var aCaps = capsByPower(a), bCaps = capsByPower(b); + //Prioritise roles with the highest number of the most powerful capabilities. + var i = 0, limit = Math.min(aCaps.length, bCaps.length); + for (; i < limit; i++) { + var delta_1 = bCaps[i].power - aCaps[i].power; + if (delta_1 !== 0) { + return delta_1; + } + } + //Give a tie to the role that has more capabilities. + var delta = bCaps.length - aCaps.length; + if (delta !== 0) { + return delta; + } + //Failing that, just sort alphabetically. + if (a.displayName > b.displayName) { + return 1; + } + else if (a.displayName < b.displayName) { + return -1; + } + return 0; + }); + var preferredCaps = [ + 'manage_network_options', + 'install_plugins', 'edit_plugins', 'delete_users', + 'manage_options', 'switch_themes', + 'edit_others_pages', 'edit_others_posts', 'edit_pages', + 'unfiltered_html', + 'publish_posts', 'edit_posts', + 'read' + ]; + var deprecatedCaps = _(_.range(0, 10)).map(function (level) { return 'level_' + level; }).value(); + deprecatedCaps.push('edit_files'); + var findDiscriminant = function (caps, includeRoles, excludeRoles) { + var getEnabledCaps = function (role) { + return _.keys(_.pick(role.capabilities, _.identity)); + }; + //Find caps that all of the includeRoles have and excludeRoles don't. + var includeCaps = _.intersection.apply(_, _.map(includeRoles, getEnabledCaps)), excludeCaps = _.union.apply(_, _.map(excludeRoles, getEnabledCaps)), possibleCaps = _.without.apply(_, [includeCaps].concat(excludeCaps).concat(deprecatedCaps)); + var bestCaps = _.intersection(preferredCaps, possibleCaps); + if (bestCaps.length > 0) { + return bestCaps[0]; + } + else if (possibleCaps.length > 0) { + return possibleCaps[0]; + } + return null; + }; + var suggestedCapabilities = []; + for (var i = 0; i < rolesByPower.length; i++) { + var role = rolesByPower[i]; + var cap = findDiscriminant(preferredCaps, _.slice(rolesByPower, 0, i + 1), _.slice(rolesByPower, i + 1, rolesByPower.length)); + suggestedCapabilities.push({ role: role, capability: cap }); + } + var previousSuggestion = null; + for (var i = suggestedCapabilities.length - 1; i >= 0; i--) { + if (suggestedCapabilities[i].capability === null) { + suggestedCapabilities[i].capability = + previousSuggestion ? previousSuggestion : 'exist'; + } + else { + previousSuggestion = suggestedCapabilities[i].capability; + } + } + this.suggestedCapabilities = suggestedCapabilities; + }; + AmeActorManager.prototype.getSuggestedCapabilities = function () { + return this.suggestedCapabilities; + }; + AmeActorManager.prototype.createUserFromProperties = function (properties) { + return AmeUser.createFromProperties(properties); + }; + AmeActorManager._ = wsAmeLodash; + return AmeActorManager; +}()); +var AmeObservableActorSettings = /** @class */ (function () { + function AmeObservableActorSettings(initialData) { + this.items = {}; + this.numberOfObservables = ko.observable(0); + if (initialData) { + this.setAll(initialData); + } + } + AmeObservableActorSettings.prototype.get = function (actor, defaultValue) { + if (defaultValue === void 0) { defaultValue = null; } + if (this.items.hasOwnProperty(actor)) { + var value = this.items[actor](); + if (value === null) { + return defaultValue; + } + return value; + } + this.numberOfObservables(); //Establish a dependency. + return defaultValue; + }; + AmeObservableActorSettings.prototype.set = function (actor, value) { + if (!this.items.hasOwnProperty(actor)) { + this.items[actor] = ko.observable(value); + this.numberOfObservables(this.numberOfObservables() + 1); + } + else { + this.items[actor](value); + } + }; + AmeObservableActorSettings.prototype.getAll = function () { + var result = {}; + for (var actorId in this.items) { + if (this.items.hasOwnProperty(actorId)) { + var value = this.items[actorId](); + if (value !== null) { + result[actorId] = value; + } + } + } + return result; + }; + AmeObservableActorSettings.prototype.setAll = function (values) { + for (var actorId in values) { + if (values.hasOwnProperty(actorId)) { + this.set(actorId, values[actorId]); + } + } + }; + /** + * Reset all values to null. + */ + AmeObservableActorSettings.prototype.resetAll = function () { + for (var actorId in this.items) { + if (this.items.hasOwnProperty(actorId)) { + this.items[actorId](null); + } + } + }; + AmeObservableActorSettings.prototype.isEnabledFor = function (selectedActor, allActors, roleDefault, superAdminDefault, noValueDefault, outIsIndeterminate) { + if (allActors === void 0) { allActors = null; } + if (roleDefault === void 0) { roleDefault = false; } + if (superAdminDefault === void 0) { superAdminDefault = null; } + if (noValueDefault === void 0) { noValueDefault = false; } + if (outIsIndeterminate === void 0) { outIsIndeterminate = null; } + if ((selectedActor === null) && (allActors === null)) { + throw 'When the selected actor is NULL, you must provide ' + + 'a list of all visible actors to determine if the item is enabled for all/any of them'; + } + if (selectedActor === null) { + //All: Enabled only if it's enabled for all actors. + //Handle the theoretically impossible case where the actor list is empty. + var actorCount = allActors.length; + if (actorCount <= 0) { + return noValueDefault; + } + var isEnabledForSome = false, isDisabledForSome = false; + for (var index = 0; index < actorCount; index++) { + if (this.isEnabledFor(allActors[index], allActors, roleDefault, superAdminDefault, noValueDefault)) { + isEnabledForSome = true; + } + else { + isDisabledForSome = true; + } + } + if (outIsIndeterminate !== null) { + outIsIndeterminate(isEnabledForSome && isDisabledForSome); + } + return isEnabledForSome && (!isDisabledForSome); + } + //Is there an explicit setting for this actor? + var ownSetting = this.get(selectedActor.getId(), null); + if (ownSetting !== null) { + return ownSetting; + } + if (selectedActor instanceof AmeUser) { + //The "Super Admin" setting takes precedence over regular roles. + if (selectedActor.isSuperAdmin) { + var superAdminSetting = this.get(AmeSuperAdmin.permanentActorId, superAdminDefault); + if (superAdminSetting !== null) { + return superAdminSetting; + } + } + //Use role settings. + //Enabled for at least one role = enabled. + //Disabled for at least one role and no settings for other roles = disabled. + var isEnabled = null; + for (var i = 0; i < selectedActor.roles.length; i++) { + var roleSetting = this.get('role:' + selectedActor.roles[i], roleDefault); + if (roleSetting !== null) { + if (isEnabled === null) { + isEnabled = roleSetting; + } + else { + isEnabled = isEnabled || roleSetting; + } + } + } + if (isEnabled !== null) { + return isEnabled; + } + //If we get this far, it means that none of the user's roles have + //a setting for this item. Fall through to the final default. + } + return noValueDefault; + }; + AmeObservableActorSettings.prototype.setEnabledFor = function (selectedActor, enabled, allActors, defaultValue) { + if (allActors === void 0) { allActors = null; } + if (defaultValue === void 0) { defaultValue = null; } + if ((selectedActor === null) && (allActors === null)) { + throw 'When the selected actor is NULL, you must provide ' + + 'a list of all visible actors so that the item can be enabled or disabled for all of them'; + } + if (selectedActor === null) { + //Enable/disable the item for all actors. + if (enabled === defaultValue) { + //Since the new value is the same as the default, + //this is equivalent to removing all settings. + this.resetAll(); + } + else { + for (var i = 0; i < allActors.length; i++) { + this.set(allActors[i].getId(), enabled); + } + } + } + else { + this.set(selectedActor.getId(), enabled); + } + }; + return AmeObservableActorSettings; +}()); +if (typeof wsAmeActorData !== 'undefined') { + AmeActors = new AmeActorManager(wsAmeActorData.roles, wsAmeActorData.users, wsAmeActorData.isMultisite, wsAmeActorData.suspectedMetaCaps); + if (typeof wsAmeActorData['capPower'] !== 'undefined') { + AmeActors.generateCapabilitySuggestions(wsAmeActorData['capPower']); + } +} +//# sourceMappingURL=actor-manager.js.map \ No newline at end of file diff --git a/js/actor-manager.js.map b/js/actor-manager.js.map new file mode 100644 index 0000000..0084038 --- /dev/null +++ b/js/actor-manager.js.map @@ -0,0 +1 @@ +{"version":3,"file":"actor-manager.js","sourceRoot":"","sources":["actor-manager.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/js/actor-manager.ts b/js/actor-manager.ts new file mode 100644 index 0000000..dab491c --- /dev/null +++ b/js/actor-manager.ts @@ -0,0 +1,854 @@ +/// <reference path="lodash-3.10.d.ts" /> +/// <reference path="knockout.d.ts" /> +/// <reference path="common.d.ts" /> + +declare let wsAmeActorData: any; +declare var wsAmeLodash: _.LoDashStatic; +declare let AmeActors: AmeActorManager; + +type Falsy = false | null | '' | undefined | 0; +type Truthy = true | string | 1; + +interface CapabilityMap { + [capabilityName: string] : boolean; +} + +interface IAmeActor { + getId(): string; + getDisplayName(): string; +} + +interface IAmeUser extends IAmeActor { + userLogin: string; + isSuperAdmin: boolean; +} + +abstract class AmeBaseActor implements IAmeActor { + public id: string; + public displayName: string = '[Error: No displayName set]'; + public capabilities: CapabilityMap; + public metaCapabilities: CapabilityMap; + + groupActors: string[] = []; + + protected constructor(id: string, displayName: string, capabilities: CapabilityMap, metaCapabilities: CapabilityMap = {}) { + this.id = id; + this.displayName = displayName; + this.capabilities = capabilities; + this.metaCapabilities = metaCapabilities; + } + + /** + * Get the capability setting directly from this actor, ignoring capabilities + * granted by roles, the Super Admin flag, or the grantedCapabilities feature. + * + * Returns NULL for capabilities that are neither explicitly granted nor denied. + * + * @param {string} capability + * @returns {boolean|null} + */ + hasOwnCap(capability: string): boolean { + if (this.capabilities.hasOwnProperty(capability)) { + return this.capabilities[capability]; + } + if (this.metaCapabilities.hasOwnProperty(capability)) { + return this.metaCapabilities[capability]; + } + return null; + } + + static getActorSpecificity(actorId: string) { + let actorType = actorId.substring(0, actorId.indexOf(':')), + specificity; + switch (actorType) { + case 'role': + specificity = 1; + break; + case 'special': + specificity = 2; + break; + case 'user': + specificity = 10; + break; + default: + specificity = 0; + } + return specificity; + } + + toString(): string { + return this.displayName + ' [' + this.id + ']'; + } + + getId(): string { + return this.id; + } + + getDisplayName(): string { + return this.displayName; + } +} + +class AmeRole extends AmeBaseActor { + name: string; + + constructor(roleId: string, displayName: string, capabilities: CapabilityMap, metaCapabilities: CapabilityMap = {}) { + super('role:' + roleId, displayName, capabilities, metaCapabilities); + this.name = roleId; + } + + hasOwnCap(capability: string): boolean { + //In WordPress, a role name is also a capability name. Users that have the role "foo" always + //have the "foo" capability. It's debatable whether the role itself actually has that capability + //(WP_Role says no), but it's convenient to treat it that way. + if (capability === this.name) { + return true; + } + return super.hasOwnCap(capability); + } +} + +interface AmeUserPropertyMap { + user_login: string; + display_name: string; + capabilities: CapabilityMap; + meta_capabilities: CapabilityMap; + roles : string[]; + is_super_admin: boolean; + id?: number; + avatar_html?: string; +} + +class AmeUser extends AmeBaseActor implements IAmeUser { + userLogin: string; + userId: number = 0; + roles: string[]; + isSuperAdmin: boolean = false; + groupActors: string[]; + avatarHTML: string = ''; + + constructor( + userLogin: string, + displayName: string, + capabilities: CapabilityMap, + roles: string[], + isSuperAdmin: boolean = false, + userId?: number, + metaCapabilities: CapabilityMap = {} + ) { + super('user:' + userLogin, displayName, capabilities, metaCapabilities); + + this.userLogin = userLogin; + this.roles = roles; + this.isSuperAdmin = isSuperAdmin; + this.userId = userId || 0; + + if (this.isSuperAdmin) { + this.groupActors.push(AmeSuperAdmin.permanentActorId); + } + for (let i = 0; i < this.roles.length; i++) { + this.groupActors.push('role:' + this.roles[i]); + } + } + + static createFromProperties(properties: AmeUserPropertyMap): AmeUser { + let user = new AmeUser( + properties.user_login, + properties.display_name, + properties.capabilities, + properties.roles, + properties.is_super_admin, + properties.hasOwnProperty('id') ? properties.id : null, + properties.meta_capabilities + ); + + if (properties.avatar_html) { + user.avatarHTML = properties.avatar_html; + } + + return user; + } +} + +class AmeSuperAdmin extends AmeBaseActor { + static permanentActorId = 'special:super_admin'; + + constructor() { + super(AmeSuperAdmin.permanentActorId, 'Super Admin', {}); + } + + hasOwnCap(capability: string): boolean { + //The Super Admin has all possible capabilities except the special "do_not_allow" flag. + return (capability !== 'do_not_allow'); + } +} + +interface AmeGrantedCapabilityMap { + [actorId: string]: { + [capability: string] : any + } +} + +interface AmeCapabilitySuggestion { + role: AmeRole; + capability: string; +} + +class AmeActorManager implements AmeActorManagerInterface { + private static _ = wsAmeLodash; + + private roles: {[roleId: string] : AmeRole} = {}; + private users: {[userLogin: string] : AmeUser} = {}; + private grantedCapabilities: AmeGrantedCapabilityMap = {}; + + public readonly isMultisite: boolean = false; + private readonly superAdmin: AmeSuperAdmin; + private exclusiveSuperAdminCapabilities = {}; + + private tagMetaCaps = {}; + private suspectedMetaCaps: CapabilityMap; + + private suggestedCapabilities: AmeCapabilitySuggestion[] = []; + + constructor(roles, users, isMultisite: Truthy | Falsy = false, suspectedMetaCaps: CapabilityMap = {}) { + this.isMultisite = !!isMultisite; + + AmeActorManager._.forEach(roles, (roleDetails, id) => { + const role = new AmeRole( + id, + roleDetails.name, + roleDetails.capabilities, + AmeActorManager._.get(roleDetails, 'meta_capabilities', {}) + ); + this.roles[role.name] = role; + }); + + AmeActorManager._.forEach(users, (userDetails: AmeUserPropertyMap) => { + const user = AmeUser.createFromProperties(userDetails); + this.users[user.userLogin] = user; + }); + + this.superAdmin = new AmeSuperAdmin(); + + this.suspectedMetaCaps = suspectedMetaCaps; + + const exclusiveCaps: string[] = [ + 'update_core', 'update_plugins', 'delete_plugins', 'install_plugins', 'upload_plugins', 'update_themes', + 'delete_themes', 'install_themes', 'upload_themes', 'update_core', 'edit_css', 'unfiltered_html', + 'edit_files', 'edit_plugins', 'edit_themes', 'delete_user', 'delete_users' + ]; + for (let i = 0; i < exclusiveCaps.length; i++) { + this.exclusiveSuperAdminCapabilities[exclusiveCaps[i]] = true; + } + + const tagMetaCaps = [ + 'manage_post_tags', 'edit_categories', 'edit_post_tags', 'delete_categories', + 'delete_post_tags' + ]; + for (let i = 0; i < tagMetaCaps.length; i++) { + this.tagMetaCaps[tagMetaCaps[i]] = true; + } + } + + // noinspection JSUnusedGlobalSymbols + actorCanAccess( + actorId: string, + grantAccess: {[actorId: string] : boolean}, + defaultCapability: string = null + ): boolean { + if (grantAccess.hasOwnProperty(actorId)) { + return grantAccess[actorId]; + } + if (defaultCapability !== null) { + return this.hasCap(actorId, defaultCapability, grantAccess); + } + return true; + } + + getActor(actorId): AmeBaseActor { + if (actorId === AmeSuperAdmin.permanentActorId) { + return this.superAdmin; + } + + const separator = actorId.indexOf(':'), + actorType = actorId.substring(0, separator), + actorKey = actorId.substring(separator + 1); + + if (actorType === 'role') { + return this.roles.hasOwnProperty(actorKey) ? this.roles[actorKey] : null; + } else if (actorType === 'user') { + return this.users.hasOwnProperty(actorKey) ? this.users[actorKey] : null; + } + + throw { + name: 'InvalidActorException', + message: "There is no actor with that ID, or the ID is invalid.", + value: actorId + }; + } + + actorExists(actorId: string): boolean { + try { + return (this.getActor(actorId) !== null); + } catch (exception) { + if (exception.hasOwnProperty('name') && (exception.name === 'InvalidActorException')) { + return false; + } else { + throw exception; + } + } + } + + hasCap(actorId: string, capability, context?: {[actor: string] : any}): boolean { + context = context || {}; + return this.actorHasCap(actorId, capability, [context, this.grantedCapabilities]); + } + + hasCapByDefault(actorId, capability) { + return this.actorHasCap(actorId, capability); + } + + private actorHasCap(actorId: string, capability: string, contextList?: Array<Object>): (boolean | null) { + //It's like the chain-of-responsibility pattern. + + //Everybody has the "exist" cap and it can't be removed or overridden by plugins. + if (capability === 'exist') { + return true; + } + + capability = this.mapMetaCap(capability); + let result = null; + + //Step #1: Check temporary context - unsaved caps, etc. Optional. + //Step #2: Check granted capabilities. Default on, but can be skipped. + if (contextList) { + //Check for explicit settings first. + let actorValue, len = contextList.length; + for (let i = 0; i < len; i++) { + if (contextList[i].hasOwnProperty(actorId)) { + actorValue = contextList[i][actorId]; + if (typeof actorValue === 'boolean') { + //Context: grant_access[actorId] = boolean. Necessary because enabling a menu item for a role + //should also enable it for all users who have that role (unless explicitly disabled for a user). + return actorValue; + } else if (actorValue.hasOwnProperty(capability)) { + //Context: grantedCapabilities[actor][capability] = boolean|[boolean, ...] + result = actorValue[capability]; + return (typeof result === 'boolean') ? result : result[0]; + } + } + } + } + + //Step #3: Check owned/default capabilities. Always checked. + let actor = this.getActor(actorId); + if (actor === null) { + return false; + } + let hasOwnCap = actor.hasOwnCap(capability); + if (hasOwnCap !== null) { + return hasOwnCap; + } + + //Step #4: Users can get a capability through their roles or the "super admin" flag. + //Only users can have inherited capabilities, so if this actor is not a user, we're done. + if (actor instanceof AmeUser) { + //Note that Super Admin has priority. If the user is a super admin, their roles are ignored. + if (actor.isSuperAdmin) { + return this.actorHasCap('special:super_admin', capability, contextList); + } + + //Check if any of the user's roles have the capability. + result = null; + for (let index = 0; index < actor.roles.length; index++) { + let roleHasCap = this.actorHasCap('role:' + actor.roles[index], capability, contextList); + if (roleHasCap !== null) { + result = result || roleHasCap; + } + } + if (result !== null) { + return result; + } + } + + if (this.suspectedMetaCaps.hasOwnProperty(capability)) { + return null; + } + return false; + } + + private mapMetaCap(capability: string): string { + if (capability === 'customize') { + return 'edit_theme_options'; + } else if (capability === 'delete_site') { + return 'manage_options'; + } + //In Multisite, some capabilities are only available to Super Admins. + if (this.isMultisite && this.exclusiveSuperAdminCapabilities.hasOwnProperty(capability)) { + return AmeSuperAdmin.permanentActorId; + } + if (this.tagMetaCaps.hasOwnProperty(capability)) { + return 'manage_categories'; + } + if ((capability === 'assign_categories') || (capability === 'assign_post_tags')) { + return 'edit_posts'; + } + return capability; + } + + /* ------------------------------- + * Roles + * ------------------------------- */ + + getRoles() { + return this.roles; + } + + roleExists(roleId: string): boolean { + return this.roles.hasOwnProperty(roleId); + }; + + getSuperAdmin() : AmeSuperAdmin { + return this.superAdmin; + } + + /* ------------------------------- + * Users + * ------------------------------- */ + + getUsers() { + return this.users; + } + + getUser(login: string) { + return this.users.hasOwnProperty(login) ? this.users[login] : null; + } + + addUsers(newUsers: AmeUser[]) { + AmeActorManager._.forEach(newUsers, (user) => { + this.users[user.userLogin] = user; + }); + } + + getGroupActorsFor(userLogin: string) { + return this.users[userLogin].groupActors; + } + + /* ------------------------------- + * Granted capability manipulation + * ------------------------------- */ + + setGrantedCapabilities(newGrants) { + this.grantedCapabilities = AmeActorManager._.cloneDeep(newGrants); + } + + getGrantedCapabilities(): AmeGrantedCapabilityMap { + return this.grantedCapabilities; + } + + /** + * Grant or deny a capability to an actor. + */ + setCap(actor: string, capability: string, hasCap: boolean, sourceType?, sourceName?) { + this.setCapInContext(this.grantedCapabilities, actor, capability, hasCap, sourceType, sourceName); + } + + public setCapInContext( + context: AmeGrantedCapabilityMap, + actor: string, + capability: string, + hasCap: boolean, + sourceType?: string, + sourceName?: string + ) { + capability = this.mapMetaCap(capability); + + const grant = sourceType ? [hasCap, sourceType, sourceName || null] : hasCap; + AmeActorManager._.set(context, [actor, capability], grant); + } + + public resetCapInContext(context: AmeGrantedCapabilityMap, actor: string, capability: string) { + capability = this.mapMetaCap(capability); + + if (AmeActorManager._.has(context, [actor, capability])) { + delete context[actor][capability]; + } + } + + /** + * Reset all capabilities granted to an actor. + * @param actor + * @return boolean TRUE if anything was reset or FALSE if the actor didn't have any granted capabilities. + */ + resetActorCaps(actor: string): boolean { + if (AmeActorManager._.has(this.grantedCapabilities, actor)) { + delete this.grantedCapabilities[actor]; + return true; + } + return false; + } + + /** + * Remove redundant granted capabilities. + * + * For example, if user "jane" has been granted the "edit_posts" capability both directly and via the Editor role, + * the direct grant is redundant. We can remove it. Jane will still have "edit_posts" because she's an editor. + */ + pruneGrantedUserCapabilities(): AmeGrantedCapabilityMap { + let _ = AmeActorManager._, + pruned = _.cloneDeep(this.grantedCapabilities), + context = [pruned]; + + let actorKeys = _(pruned).keys().filter((actorId) => { + //Skip users that are not loaded. + const actor = this.getActor(actorId); + if (actor === null) { + return false; + } + return (actor instanceof AmeUser); + }).value(); + + _.forEach(actorKeys, (actor) => { + _.forEach(_.keys(pruned[actor]), (capability) => { + const grant = pruned[actor][capability]; + delete pruned[actor][capability]; + + const hasCap = _.isArray(grant) ? grant[0] : grant, + hasCapWhenPruned = !!this.actorHasCap(actor, capability, context); + + if (hasCap !== hasCapWhenPruned) { + pruned[actor][capability] = grant; //Restore. + } + }); + }); + + this.setGrantedCapabilities(pruned); + return pruned; + }; + + + /** + * Compare the specificity of two actors. + * + * Returns 1 if the first actor is more specific than the second, 0 if they're both + * equally specific, and -1 if the second actor is more specific. + * + * @return {Number} + */ + static compareActorSpecificity(actor1: string, actor2: string): Number { + let delta = AmeBaseActor.getActorSpecificity(actor1) - AmeBaseActor.getActorSpecificity(actor2); + if (delta !== 0) { + delta = (delta > 0) ? 1 : -1; + } + return delta; + }; + + generateCapabilitySuggestions(capPower): void { + let _ = AmeActorManager._; + + let capsByPower = _.memoize((role: AmeRole): {capability: string, power: number}[] => { + let sortedCaps = _.reduce(role.capabilities, (result, hasCap, capability) => { + if (hasCap) { + result.push({ + capability: capability, + power: _.get(capPower, [capability], 0) + }); + } + return result; + }, []); + + sortedCaps = _.sortBy(sortedCaps, (item) => -item.power); + return sortedCaps; + }); + + let rolesByPower: AmeRole[] = _.values<AmeRole>(this.getRoles()).sort(function(a: AmeRole, b: AmeRole) { + let aCaps = capsByPower(a), + bCaps = capsByPower(b); + + //Prioritise roles with the highest number of the most powerful capabilities. + let i = 0, limit = Math.min(aCaps.length, bCaps.length); + for (; i < limit; i++) { + let delta = bCaps[i].power - aCaps[i].power; + if (delta !== 0) { + return delta; + } + } + + //Give a tie to the role that has more capabilities. + let delta = bCaps.length - aCaps.length; + if (delta !== 0) { + return delta; + } + + //Failing that, just sort alphabetically. + if (a.displayName > b.displayName) { + return 1; + } else if (a.displayName < b.displayName) { + return -1; + } + return 0; + }); + + let preferredCaps = [ + 'manage_network_options', + 'install_plugins', 'edit_plugins', 'delete_users', + 'manage_options', 'switch_themes', + 'edit_others_pages', 'edit_others_posts', 'edit_pages', + 'unfiltered_html', + 'publish_posts', 'edit_posts', + 'read' + ]; + + let deprecatedCaps = _(_.range(0, 10)).map((level) => 'level_' + level).value(); + deprecatedCaps.push('edit_files'); + + let findDiscriminant = (caps: string[], includeRoles: AmeRole[], excludeRoles): string => { + let getEnabledCaps = (role: AmeRole): string[] => { + return _.keys(_.pick(role.capabilities, _.identity)); + }; + + //Find caps that all of the includeRoles have and excludeRoles don't. + let includeCaps = _.intersection.apply(_, _.map(includeRoles, getEnabledCaps)), + excludeCaps = _.union.apply(_, _.map(excludeRoles, getEnabledCaps)), + possibleCaps = _.without.apply(_, [includeCaps].concat(excludeCaps).concat(deprecatedCaps)); + + let bestCaps = _.intersection(preferredCaps, possibleCaps); + + if (bestCaps.length > 0) { + return bestCaps[0]; + } else if (possibleCaps.length > 0) { + return possibleCaps[0]; + } + return null; + }; + + let suggestedCapabilities = []; + for (let i = 0; i < rolesByPower.length; i++) { + let role = rolesByPower[i]; + + let cap = findDiscriminant( + preferredCaps, + _.slice(rolesByPower, 0, i + 1), + _.slice(rolesByPower, i + 1, rolesByPower.length) + ); + suggestedCapabilities.push({role: role, capability: cap}); + } + + let previousSuggestion = null; + for (let i = suggestedCapabilities.length - 1; i >= 0; i--) { + if (suggestedCapabilities[i].capability === null) { + suggestedCapabilities[i].capability = + previousSuggestion ? previousSuggestion : 'exist'; + } else { + previousSuggestion = suggestedCapabilities[i].capability; + } + } + + this.suggestedCapabilities = suggestedCapabilities; + } + + public getSuggestedCapabilities(): AmeCapabilitySuggestion[] { + return this.suggestedCapabilities; + } + + createUserFromProperties(properties: AmeUserPropertyMap): IAmeUser { + return AmeUser.createFromProperties(properties); + } +} + +interface AmeActorManagerInterface { + getUsers(): AmeDictionary<IAmeUser>; + getUser(login: string): IAmeUser; + addUsers(newUsers: IAmeUser[]); + createUserFromProperties(properties: AmeUserPropertyMap): IAmeUser; + + getRoles(): AmeDictionary<IAmeActor>; + getSuperAdmin(): IAmeActor; + + getActor(actorId): IAmeActor; + actorExists(actorId: string): boolean; +} + +class AmeObservableActorSettings { + private items: { [actorId: string] : KnockoutObservable<boolean>; } = {}; + private readonly numberOfObservables: KnockoutObservable<number>; + + constructor(initialData?: AmeDictionary<boolean>) { + this.numberOfObservables = ko.observable(0); + if (initialData) { + this.setAll(initialData); + } + } + + get(actor: string, defaultValue = null): boolean { + if (this.items.hasOwnProperty(actor)) { + let value = this.items[actor](); + if (value === null) { + return defaultValue; + } + return value; + } + this.numberOfObservables(); //Establish a dependency. + return defaultValue; + } + + set(actor: string, value: boolean) { + if (!this.items.hasOwnProperty(actor)) { + this.items[actor] = ko.observable(value); + this.numberOfObservables(this.numberOfObservables() + 1); + } else { + this.items[actor](value); + } + } + + getAll(): AmeDictionary<boolean> { + let result: AmeDictionary<boolean> = {}; + for (let actorId in this.items) { + if (this.items.hasOwnProperty(actorId)) { + let value = this.items[actorId](); + if (value !== null) { + result[actorId] = value; + } + } + } + return result; + } + + setAll(values: AmeDictionary<boolean>) { + for (let actorId in values) { + if (values.hasOwnProperty(actorId)) { + this.set(actorId, values[actorId]); + } + } + } + + /** + * Reset all values to null. + */ + resetAll() { + for (let actorId in this.items) { + if (this.items.hasOwnProperty(actorId)) { + this.items[actorId](null); + } + } + } + + isEnabledFor( + selectedActor: IAmeActor | null, + allActors: IAmeActor[] | null = null, + roleDefault: boolean | null = false, + superAdminDefault: boolean | null = null, + noValueDefault: boolean = false, + outIsIndeterminate: KnockoutObservable<boolean> = null + ): boolean { + if ((selectedActor === null) && (allActors === null)) { + throw 'When the selected actor is NULL, you must provide ' + + 'a list of all visible actors to determine if the item is enabled for all/any of them'; + } + + if (selectedActor === null) { + //All: Enabled only if it's enabled for all actors. + + //Handle the theoretically impossible case where the actor list is empty. + const actorCount = allActors.length; + if (actorCount <= 0) { + return noValueDefault; + } + + let isEnabledForSome = false, isDisabledForSome = false; + for (let index = 0; index < actorCount; index++) { + if (this.isEnabledFor(allActors[index], allActors, roleDefault, superAdminDefault, noValueDefault)) { + isEnabledForSome = true; + } else { + isDisabledForSome = true; + } + } + + if (outIsIndeterminate !== null) { + outIsIndeterminate(isEnabledForSome && isDisabledForSome); + } + + return isEnabledForSome && (!isDisabledForSome); + } + + //Is there an explicit setting for this actor? + let ownSetting = this.get(selectedActor.getId(), null); + if (ownSetting !== null) { + return ownSetting; + } + + if (selectedActor instanceof AmeUser) { + //The "Super Admin" setting takes precedence over regular roles. + if (selectedActor.isSuperAdmin) { + let superAdminSetting = this.get(AmeSuperAdmin.permanentActorId, superAdminDefault); + if (superAdminSetting !== null) { + return superAdminSetting; + } + } + + //Use role settings. + //Enabled for at least one role = enabled. + //Disabled for at least one role and no settings for other roles = disabled. + let isEnabled: boolean|null = null; + for (let i = 0; i < selectedActor.roles.length; i++) { + let roleSetting = this.get('role:' + selectedActor.roles[i], roleDefault); + if (roleSetting !== null) { + if (isEnabled === null) { + isEnabled = roleSetting; + } else { + isEnabled = isEnabled || roleSetting; + } + } + } + + if (isEnabled !== null) { + return isEnabled; + } + + //If we get this far, it means that none of the user's roles have + //a setting for this item. Fall through to the final default. + } + + return noValueDefault; + } + + setEnabledFor( + selectedActor: IAmeActor|null, + enabled: boolean, + allActors: IAmeActor[]|null = null, + defaultValue: boolean|null = null + ) { + if ((selectedActor === null) && (allActors === null)) { + throw 'When the selected actor is NULL, you must provide ' + + 'a list of all visible actors so that the item can be enabled or disabled for all of them'; + } + + if (selectedActor === null) { + //Enable/disable the item for all actors. + if (enabled === defaultValue) { + //Since the new value is the same as the default, + //this is equivalent to removing all settings. + this.resetAll(); + } else { + for (let i = 0; i < allActors.length; i++) { + this.set(allActors[i].getId(), enabled); + } + } + } else { + this.set(selectedActor.getId(), enabled); + } + } +} + +if (typeof wsAmeActorData !== 'undefined') { + AmeActors = new AmeActorManager( + wsAmeActorData.roles, + wsAmeActorData.users, + wsAmeActorData.isMultisite, + wsAmeActorData.suspectedMetaCaps + ); + + if (typeof wsAmeActorData['capPower'] !== 'undefined') { + AmeActors.generateCapabilitySuggestions(wsAmeActorData['capPower']); + } +} diff --git a/js/admin-helpers.js b/js/admin-helpers.js new file mode 100644 index 0000000..bb7ba9d --- /dev/null +++ b/js/admin-helpers.js @@ -0,0 +1,77 @@ +/*global wsAmeCurrentMenuItem*/ + +(function($) { + var currentItem = (typeof wsAmeCurrentMenuItem !== 'undefined') ? wsAmeCurrentMenuItem : {}; + + //The page heading is typically hardcoded and/or not configurable, so we need to use JS to change it. + var customPageHeading = currentItem.hasOwnProperty('customPageHeading') ? currentItem['customPageHeading'] : null; + var pageHeadingSelector = currentItem.hasOwnProperty('pageHeadingSelector') ? currentItem['pageHeadingSelector'] : '.wrap > h2:first'; + var ameHideHeading = null; + if ( customPageHeading ) { + //Temporarily hide the heading to prevent the original text from showing up briefly + //before being replaced when the DOM is ready (see below). + ameHideHeading = $('<style type="text/css">.wrap > h2:first-child,.wrap > h1:first-child { visibility: hidden; }</style>') + .appendTo('head'); + } + + jQuery(function($) { + var adminMenu = $('#adminmenu'); + + //Menu separators shouldn't be clickable and should have a custom class. + adminMenu + .find('.ws-submenu-separator') + .closest('a').on('click', function() { + return false; + }) + .closest('li').addClass('ws-submenu-separator-wrap'); + + //Menus with the target "< None >" also shouldn't be clickable. + adminMenu + .find( + 'a.menu-top.ame-unclickable-menu-item, ul.wp-submenu > li > a[href^="#ame-unclickable-menu-item"]' + ) + .on('click', function() { + //Exception: At small viewport sizes, WordPress changes how top level menus work: clicking a menu + //now expands its submenu. We must not ignore that click or it will be impossible to expand the menu. + var viewportWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); + if ((viewportWidth <= 782) && $(this).closest('li').hasClass('wp-has-submenu')) { + return; + } + + //Ignore the click. + return false; + }); + + //Open submenus that have the "always open" flag. + adminMenu + .find('li.ws-ame-has-always-open-submenu.wp-has-submenu') + .addClass('wp-has-current-submenu') + .removeClass('wp-not-current-submenu'); + + //Replace the original page heading with the custom heading. + if ( customPageHeading ) { + function replaceAdminPageHeading(newText) { + var headingText = $(pageHeadingSelector) + .contents() + .filter(function() { + //Find text nodes. + if ((this.nodeType != 3) || (!this.nodeValue)) { + return false; + } + //Skip whitespace. + return /\S/.test(this.nodeValue); + }).get(0); + + if (headingText && headingText.nodeValue) { + headingText.nodeValue = newText; + } + } + + //Normal headings have at least one tab's worth of trailing whitespace. We need to replicate that + //to keep the page layout roughly the same. + replaceAdminPageHeading(customPageHeading + '\t'); + ameHideHeading.remove(); //Make the heading visible. + } + }); + +})(jQuery); \ No newline at end of file diff --git a/js/common.d.ts b/js/common.d.ts new file mode 100644 index 0000000..932778a --- /dev/null +++ b/js/common.d.ts @@ -0,0 +1,25 @@ +///<reference path="knockout.d.ts"/> + +interface AmeDictionary<T> { + [mapKey: string]: T; +} + +// noinspection JSUnusedGlobalSymbols +type KeysMatchingType<T, V> = { [K in keyof T]: T[K] extends V ? K : never }[keyof T]; + +type AmeCssBorderStyle = 'none' | 'solid' | 'dashed' | 'dotted' | 'double' | 'groove' | 'ridge' | 'outset'; + +interface AmeCssBorderSettings { + style: AmeCssBorderStyle; + color: string; + width: number; +} + +type AmeObservablePropertiesOf<T> = { + [P in keyof T]: KnockoutObservable<T[P]>; +} + +type AmeRecursiveObservablePropertiesOf<T> = { + [P in keyof T]: T[P] extends object ? AmeRecursiveObservablePropertiesOf<T[P]> : KnockoutObservable<T[P]>; +} + diff --git a/js/jquery-json.d.ts b/js/jquery-json.d.ts new file mode 100644 index 0000000..a62ec61 --- /dev/null +++ b/js/jquery-json.d.ts @@ -0,0 +1,4 @@ +interface JQueryStatic { + //This method is added by the jquery-json plugin. + toJSON: (data: any) => string; +} \ No newline at end of file diff --git a/js/jquery.biscuit.d.ts b/js/jquery.biscuit.d.ts new file mode 100644 index 0000000..4839271 --- /dev/null +++ b/js/jquery.biscuit.d.ts @@ -0,0 +1,5 @@ +interface JQueryStatic { + //These methods are added by the jquery-cookie plugin. + cookie: (name: string, value?: string, options?: {}) => string; + removeCookie: (name: string, options?: {}) => boolean; +} \ No newline at end of file diff --git a/js/jquery.biscuit.js b/js/jquery.biscuit.js new file mode 100644 index 0000000..ec4a7b6 --- /dev/null +++ b/js/jquery.biscuit.js @@ -0,0 +1,120 @@ +/*! + * jQuery Cookie Plugin v1.4.0 + * https://github.com/carhartl/jquery-cookie + * + * Copyright 2013 Klaus Hartl + * Released under the MIT license + */ +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as anonymous module. + define(['jquery'], factory); + } else { + // Browser globals. + factory(jQuery); + } +}(function ($) { + + var pluses = /\+/g; + + function encode(s) { + return config.raw ? s : encodeURIComponent(s); + } + + function decode(s) { + return config.raw ? s : decodeURIComponent(s); + } + + function stringifyCookieValue(value) { + return encode(config.json ? JSON.stringify(value) : String(value)); + } + + function parseCookieValue(s) { + if (s.indexOf('"') === 0) { + // This is a quoted cookie as according to RFC2068, unescape... + s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); + } + + try { + // Replace server-side written pluses with spaces. + // If we can't decode the cookie, ignore it, it's unusable. + s = decodeURIComponent(s.replace(pluses, ' ')); + } catch(e) { + return; + } + + try { + // If we can't parse the cookie, ignore it, it's unusable. + return config.json ? JSON.parse(s) : s; + } catch(e) {} + } + + function read(s, converter) { + var value = config.raw ? s : parseCookieValue(s); + return (typeof converter === 'function') ? converter(value) : value; + } + + var config = $.cookie = function (key, value, options) { + + // Write + if (value !== undefined && !(typeof value === 'function')) { + options = $.extend({}, config.defaults, options); + + if (typeof options.expires === 'number') { + var days = options.expires, t = options.expires = new Date(); + t.setDate(t.getDate() + days); + } + + return (document.cookie = [ + encode(key), '=', stringifyCookieValue(value), + options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE + options.path ? '; path=' + options.path : '', + options.domain ? '; domain=' + options.domain : '', + options.samesite ? '; samesite=' + options.samesite : '', + options.secure ? '; secure' : '' + ].join('')); + } + + // Read + + var result = key ? undefined : {}; + + // To prevent the for loop in the first place assign an empty array + // in case there are no cookies at all. Also prevents odd result when + // calling $.cookie(). + var cookies = document.cookie ? document.cookie.split('; ') : []; + + for (var i = 0, l = cookies.length; i < l; i++) { + var parts = cookies[i].split('='); + var name = decode(parts.shift()); + var cookie = parts.join('='); + + if (key && key === name) { + // If second argument (value) is a function it's a converter... + result = read(cookie, value); + break; + } + + // Prevent storing a cookie that we couldn't decode. + if (!key && (cookie = read(cookie)) !== undefined) { + result[name] = cookie; + } + } + + return result; + }; + + config.defaults = { + 'samesite' : 'lax' + }; + + $.removeCookie = function (key, options) { + if ($.cookie(key) !== undefined) { + // Must not alter options, thus extending a fresh object... + $.cookie(key, '', $.extend({}, options, { expires: -1 })); + return true; + } + return false; + }; + +})); diff --git a/js/jquery.d.ts b/js/jquery.d.ts new file mode 100644 index 0000000..ff259e7 --- /dev/null +++ b/js/jquery.d.ts @@ -0,0 +1,3203 @@ +// Type definitions for jQuery 1.10.x / 2.0.x +// Project: http://jquery.com/ +// Definitions by: Boris Yankov <https://github.com/borisyankov/>, Christian Hoffmeister <https://github.com/choffmeister>, Steve Fenton <https://github.com/Steve-Fenton>, Diullei Gomes <https://github.com/Diullei>, Tass Iliopoulos <https://github.com/tasoili>, Jason Swearingen <https://github.com/jasons-novaleaf>, Sean Hill <https://github.com/seanski>, Guus Goossens <https://github.com/Guuz>, Kelly Summerlin <https://github.com/ksummerlin>, Basarat Ali Syed <https://github.com/basarat>, Nicholas Wolverson <https://github.com/nwolverson>, Derek Cicerone <https://github.com/derekcicerone>, Andrew Gaspar <https://github.com/AndrewGaspar>, James Harrison Fisher <https://github.com/jameshfisher>, Seikichi Kondo <https://github.com/seikichi>, Benjamin Jackman <https://github.com/benjaminjackman>, Poul Sorensen <https://github.com/s093294>, Josh Strobl <https://github.com/JoshStrobl>, John Reilly <https://github.com/johnnyreilly/>, Dick van den Brink <https://github.com/DickvdBrink> +// Definitions: https://github.com/borisyankov/DefinitelyTyped + +/* ***************************************************************************** +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 + +THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. + +See the Apache Version 2.0 License for specific language governing permissions +and limitations under the License. +***************************************************************************** */ + + +/** + * Interface for the AJAX setting that will configure the AJAX request + */ +interface JQueryAjaxSettings { + /** + * The content type sent in the request header that tells the server what kind of response it will accept in return. If the accepts setting needs modification, it is recommended to do so once in the $.ajaxSetup() method. + */ + accepts?: any; + /** + * By default, all requests are sent asynchronously (i.e. this is set to true by default). If you need synchronous requests, set this option to false. Cross-domain requests and dataType: "jsonp" requests do not support synchronous operation. Note that synchronous requests may temporarily lock the browser, disabling any actions while the request is active. As of jQuery 1.8, the use of async: false with jqXHR ($.Deferred) is deprecated; you must use the success/error/complete callback options instead of the corresponding methods of the jqXHR object such as jqXHR.done() or the deprecated jqXHR.success(). + */ + async?: boolean; + /** + * A pre-request callback function that can be used to modify the jqXHR (in jQuery 1.4.x, XMLHTTPRequest) object before it is sent. Use this to set custom headers, etc. The jqXHR and settings objects are passed as arguments. This is an Ajax Event. Returning false in the beforeSend function will cancel the request. As of jQuery 1.5, the beforeSend option will be called regardless of the type of request. + */ + beforeSend? (jqXHR: JQueryXHR, settings: JQueryAjaxSettings): any; + /** + * If set to false, it will force requested pages not to be cached by the browser. Note: Setting cache to false will only work correctly with HEAD and GET requests. It works by appending "_={timestamp}" to the GET parameters. The parameter is not needed for other types of requests, except in IE8 when a POST is made to a URL that has already been requested by a GET. + */ + cache?: boolean; + /** + * A function to be called when the request finishes (after success and error callbacks are executed). The function gets passed two arguments: The jqXHR (in jQuery 1.4.x, XMLHTTPRequest) object and a string categorizing the status of the request ("success", "notmodified", "error", "timeout", "abort", or "parsererror"). As of jQuery 1.5, the complete setting can accept an array of functions. Each function will be called in turn. This is an Ajax Event. + */ + complete? (jqXHR: JQueryXHR, textStatus: string): any; + /** + * An object of string/regular-expression pairs that determine how jQuery will parse the response, given its content type. (version added: 1.5) + */ + contents?: { [key: string]: any; }; + //According to jQuery.ajax source code, ajax's option actually allows contentType to set to "false" + // https://github.com/borisyankov/DefinitelyTyped/issues/742 + /** + * When sending data to the server, use this content type. Default is "application/x-www-form-urlencoded; charset=UTF-8", which is fine for most cases. If you explicitly pass in a content-type to $.ajax(), then it is always sent to the server (even if no data is sent). The W3C XMLHttpRequest specification dictates that the charset is always UTF-8; specifying another charset will not force the browser to change the encoding. + */ + contentType?: any; + /** + * This object will be made the context of all Ajax-related callbacks. By default, the context is an object that represents the ajax settings used in the call ($.ajaxSettings merged with the settings passed to $.ajax). + */ + context?: any; + /** + * An object containing dataType-to-dataType converters. Each converter's value is a function that returns the transformed value of the response. (version added: 1.5) + */ + converters?: { [key: string]: any; }; + /** + * If you wish to force a crossDomain request (such as JSONP) on the same domain, set the value of crossDomain to true. This allows, for example, server-side redirection to another domain. (version added: 1.5) + */ + crossDomain?: boolean; + /** + * Data to be sent to the server. It is converted to a query string, if not already a string. It's appended to the url for GET-requests. See processData option to prevent this automatic processing. Object must be Key/Value pairs. If value is an Array, jQuery serializes multiple values with same key based on the value of the traditional setting (described below). + */ + data?: any; + /** + * A function to be used to handle the raw response data of XMLHttpRequest.This is a pre-filtering function to sanitize the response. You should return the sanitized data. The function accepts two arguments: The raw data returned from the server and the 'dataType' parameter. + */ + dataFilter? (data: any, ty: any): any; + /** + * The type of data that you're expecting back from the server. If none is specified, jQuery will try to infer it based on the MIME type of the response (an XML MIME type will yield XML, in 1.4 JSON will yield a JavaScript object, in 1.4 script will execute the script, and anything else will be returned as a string). + */ + dataType?: string; + /** + * A function to be called if the request fails. The function receives three arguments: The jqXHR (in jQuery 1.4.x, XMLHttpRequest) object, a string describing the type of error that occurred and an optional exception object, if one occurred. Possible values for the second argument (besides null) are "timeout", "error", "abort", and "parsererror". When an HTTP error occurs, errorThrown receives the textual portion of the HTTP status, such as "Not Found" or "Internal Server Error." As of jQuery 1.5, the error setting can accept an array of functions. Each function will be called in turn. Note: This handler is not called for cross-domain script and cross-domain JSONP requests. This is an Ajax Event. + */ + error? (jqXHR: JQueryXHR, textStatus: string, errorThrown: string): any; + /** + * Whether to trigger global Ajax event handlers for this request. The default is true. Set to false to prevent the global handlers like ajaxStart or ajaxStop from being triggered. This can be used to control various Ajax Events. + */ + global?: boolean; + /** + * An object of additional header key/value pairs to send along with requests using the XMLHttpRequest transport. The header X-Requested-With: XMLHttpRequest is always added, but its default XMLHttpRequest value can be changed here. Values in the headers setting can also be overwritten from within the beforeSend function. (version added: 1.5) + */ + headers?: { [key: string]: any; }; + /** + * Allow the request to be successful only if the response has changed since the last request. This is done by checking the Last-Modified header. Default value is false, ignoring the header. In jQuery 1.4 this technique also checks the 'etag' specified by the server to catch unmodified data. + */ + ifModified?: boolean; + /** + * Allow the current environment to be recognized as "local," (e.g. the filesystem), even if jQuery does not recognize it as such by default. The following protocols are currently recognized as local: file, *-extension, and widget. If the isLocal setting needs modification, it is recommended to do so once in the $.ajaxSetup() method. (version added: 1.5.1) + */ + isLocal?: boolean; + /** + * Override the callback function name in a jsonp request. This value will be used instead of 'callback' in the 'callback=?' part of the query string in the url. So {jsonp:'onJSONPLoad'} would result in 'onJSONPLoad=?' passed to the server. As of jQuery 1.5, setting the jsonp option to false prevents jQuery from adding the "?callback" string to the URL or attempting to use "=?" for transformation. In this case, you should also explicitly set the jsonpCallback setting. For example, { jsonp: false, jsonpCallback: "callbackName" } + */ + jsonp?: any; + /** + * Specify the callback function name for a JSONP request. This value will be used instead of the random name automatically generated by jQuery. It is preferable to let jQuery generate a unique name as it'll make it easier to manage the requests and provide callbacks and error handling. You may want to specify the callback when you want to enable better browser caching of GET requests. As of jQuery 1.5, you can also use a function for this setting, in which case the value of jsonpCallback is set to the return value of that function. + */ + jsonpCallback?: any; + /** + * The HTTP method to use for the request (e.g. "POST", "GET", "PUT"). (version added: 1.9.0) + */ + method?: string; + /** + * A mime type to override the XHR mime type. (version added: 1.5.1) + */ + mimeType?: string; + /** + * A password to be used with XMLHttpRequest in response to an HTTP access authentication request. + */ + password?: string; + /** + * By default, data passed in to the data option as an object (technically, anything other than a string) will be processed and transformed into a query string, fitting to the default content-type "application/x-www-form-urlencoded". If you want to send a DOMDocument, or other non-processed data, set this option to false. + */ + processData?: boolean; + /** + * Only applies when the "script" transport is used (e.g., cross-domain requests with "jsonp" or "script" dataType and "GET" type). Sets the charset attribute on the script tag used in the request. Used when the character set on the local page is not the same as the one on the remote script. + */ + scriptCharset?: string; + /** + * An object of numeric HTTP codes and functions to be called when the response has the corresponding code. f the request is successful, the status code functions take the same parameters as the success callback; if it results in an error (including 3xx redirect), they take the same parameters as the error callback. (version added: 1.5) + */ + statusCode?: { [key: string]: any; }; + /** + * A function to be called if the request succeeds. The function gets passed three arguments: The data returned from the server, formatted according to the dataType parameter; a string describing the status; and the jqXHR (in jQuery 1.4.x, XMLHttpRequest) object. As of jQuery 1.5, the success setting can accept an array of functions. Each function will be called in turn. This is an Ajax Event. + */ + success? (data: any, textStatus: string, jqXHR: JQueryXHR): any; + /** + * Set a timeout (in milliseconds) for the request. This will override any global timeout set with $.ajaxSetup(). The timeout period starts at the point the $.ajax call is made; if several other requests are in progress and the browser has no connections available, it is possible for a request to time out before it can be sent. In jQuery 1.4.x and below, the XMLHttpRequest object will be in an invalid state if the request times out; accessing any object members may throw an exception. In Firefox 3.0+ only, script and JSONP requests cannot be cancelled by a timeout; the script will run even if it arrives after the timeout period. + */ + timeout?: number; + /** + * Set this to true if you wish to use the traditional style of param serialization. + */ + traditional?: boolean; + /** + * The type of request to make ("POST" or "GET"), default is "GET". Note: Other HTTP request methods, such as PUT and DELETE, can also be used here, but they are not supported by all browsers. + */ + type?: string; + /** + * A string containing the URL to which the request is sent. + */ + url?: string; + /** + * A username to be used with XMLHttpRequest in response to an HTTP access authentication request. + */ + username?: string; + /** + * Callback for creating the XMLHttpRequest object. Defaults to the ActiveXObject when available (IE), the XMLHttpRequest otherwise. Override to provide your own implementation for XMLHttpRequest or enhancements to the factory. + */ + xhr?: any; + /** + * An object of fieldName-fieldValue pairs to set on the native XHR object. For example, you can use it to set withCredentials to true for cross-domain requests if needed. In jQuery 1.5, the withCredentials property was not propagated to the native XHR and thus CORS requests requiring it would ignore this flag. For this reason, we recommend using jQuery 1.5.1+ should you require the use of it. (version added: 1.5.1) + */ + xhrFields?: { [key: string]: any; }; +} + +/** + * Interface for the jqXHR object + */ +interface JQueryXHR extends XMLHttpRequest, JQueryPromise<any> { + /** + * The .overrideMimeType() method may be used in the beforeSend() callback function, for example, to modify the response content-type header. As of jQuery 1.5.1, the jqXHR object also contains the overrideMimeType() method (it was available in jQuery 1.4.x, as well, but was temporarily removed in jQuery 1.5). + */ + overrideMimeType(mimeType: string): any; + /** + * Cancel the request. + * + * @param statusText A string passed as the textStatus parameter for the done callback. Default value: "canceled" + */ + abort(statusText?: string): void; + /** + * Incorporates the functionality of the .done() and .fail() methods, allowing (as of jQuery 1.8) the underlying Promise to be manipulated. Refer to deferred.then() for implementation details. + */ + then(doneCallback: (data: any, textStatus: string, jqXHR: JQueryXHR) => void, failCallback?: (jqXHR: JQueryXHR, textStatus: string, errorThrown: any) => void): JQueryPromise<any>; + /** + * Property containing the parsed response if the response Content-Type is json + */ + responseJSON?: any; + /** + * A function to be called if the request fails. + */ + error(xhr: JQueryXHR, textStatus: string, errorThrown: string): void; +} + +/** + * Interface for the JQuery callback + */ +interface JQueryCallback { + /** + * Add a callback or a collection of callbacks to a callback list. + * + * @param callbacks A function, or array of functions, that are to be added to the callback list. + */ + add(callbacks: Function): JQueryCallback; + /** + * Add a callback or a collection of callbacks to a callback list. + * + * @param callbacks A function, or array of functions, that are to be added to the callback list. + */ + add(callbacks: Function[]): JQueryCallback; + + /** + * Disable a callback list from doing anything more. + */ + disable(): JQueryCallback; + + /** + * Determine if the callbacks list has been disabled. + */ + disabled(): boolean; + + /** + * Remove all of the callbacks from a list. + */ + empty(): JQueryCallback; + + /** + * Call all of the callbacks with the given arguments + * + * @param arguments The argument or list of arguments to pass back to the callback list. + */ + fire(...arguments: any[]): JQueryCallback; + + /** + * Determine if the callbacks have already been called at least once. + */ + fired(): boolean; + + /** + * Call all callbacks in a list with the given context and arguments. + * + * @param context A reference to the context in which the callbacks in the list should be fired. + * @param arguments An argument, or array of arguments, to pass to the callbacks in the list. + */ + fireWith(context?: any, args?: any[]): JQueryCallback; + + /** + * Determine whether a supplied callback is in a list + * + * @param callback The callback to search for. + */ + has(callback: Function): boolean; + + /** + * Lock a callback list in its current state. + */ + lock(): JQueryCallback; + + /** + * Determine if the callbacks list has been locked. + */ + locked(): boolean; + + /** + * Remove a callback or a collection of callbacks from a callback list. + * + * @param callbacks A function, or array of functions, that are to be removed from the callback list. + */ + remove(callbacks: Function): JQueryCallback; + /** + * Remove a callback or a collection of callbacks from a callback list. + * + * @param callbacks A function, or array of functions, that are to be removed from the callback list. + */ + remove(callbacks: Function[]): JQueryCallback; +} + +/** + * Allows jQuery Promises to interop with non-jQuery promises + */ +interface JQueryGenericPromise<T> { + /** + * Add handlers to be called when the Deferred object is resolved, rejected, or still in progress. + * + * @param doneFilter A function that is called when the Deferred is resolved. + * @param failFilter An optional function that is called when the Deferred is rejected. + */ + then<U>(doneFilter: (value?: T, ...values: any[]) => U|JQueryPromise<U>, failFilter?: (...reasons: any[]) => any, progressFilter?: (...progression: any[]) => any): JQueryPromise<U>; + + /** + * Add handlers to be called when the Deferred object is resolved, rejected, or still in progress. + * + * @param doneFilter A function that is called when the Deferred is resolved. + * @param failFilter An optional function that is called when the Deferred is rejected. + */ + then(doneFilter: (value?: T, ...values: any[]) => void, failFilter?: (...reasons: any[]) => any, progressFilter?: (...progression: any[]) => any): JQueryPromise<void>; +} + +/** + * Interface for the JQuery promise/deferred callbacks + */ +interface JQueryPromiseCallback<T> { + (value?: T, ...args: any[]): void; +} + +interface JQueryPromiseOperator<T, U> { + (callback1: JQueryPromiseCallback<T>|JQueryPromiseCallback<T>[], ...callbacksN: Array<JQueryPromiseCallback<any>|JQueryPromiseCallback<any>[]>): JQueryPromise<U>; +} + +/** + * Interface for the JQuery promise, part of callbacks + */ +interface JQueryPromise<T> extends JQueryGenericPromise<T> { + /** + * Determine the current state of a Deferred object. + */ + state(): string; + /** + * Add handlers to be called when the Deferred object is either resolved or rejected. + * + * @param alwaysCallbacks1 A function, or array of functions, that is called when the Deferred is resolved or rejected. + * @param alwaysCallbacks2 Optional additional functions, or arrays of functions, that are called when the Deferred is resolved or rejected. + */ + always(alwaysCallback1?: JQueryPromiseCallback<any>|JQueryPromiseCallback<any>[], ...alwaysCallbacksN: Array<JQueryPromiseCallback<any>|JQueryPromiseCallback<any>[]>): JQueryPromise<T>; + /** + * Add handlers to be called when the Deferred object is resolved. + * + * @param doneCallbacks1 A function, or array of functions, that are called when the Deferred is resolved. + * @param doneCallbacks2 Optional additional functions, or arrays of functions, that are called when the Deferred is resolved. + */ + done(doneCallback1?: JQueryPromiseCallback<T>|JQueryPromiseCallback<T>[], ...doneCallbackN: Array<JQueryPromiseCallback<T>|JQueryPromiseCallback<T>[]>): JQueryPromise<T>; + /** + * Add handlers to be called when the Deferred object is rejected. + * + * @param failCallbacks1 A function, or array of functions, that are called when the Deferred is rejected. + * @param failCallbacks2 Optional additional functions, or arrays of functions, that are called when the Deferred is rejected. + */ + fail(failCallback1?: JQueryPromiseCallback<any>|JQueryPromiseCallback<any>[], ...failCallbacksN: Array<JQueryPromiseCallback<any>|JQueryPromiseCallback<any>[]>): JQueryPromise<T>; + /** + * Add handlers to be called when the Deferred object generates progress notifications. + * + * @param progressCallbacks A function, or array of functions, to be called when the Deferred generates progress notifications. + */ + progress(progressCallback1?: JQueryPromiseCallback<any>|JQueryPromiseCallback<any>[], ...progressCallbackN: Array<JQueryPromiseCallback<any>|JQueryPromiseCallback<any>[]>): JQueryPromise<T>; + + // Deprecated - given no typings + pipe(doneFilter?: (x: any) => any, failFilter?: (x: any) => any, progressFilter?: (x: any) => any): JQueryPromise<any>; +} + +/** + * Interface for the JQuery deferred, part of callbacks + */ +interface JQueryDeferred<T> extends JQueryGenericPromise<T> { + /** + * Determine the current state of a Deferred object. + */ + state(): string; + /** + * Add handlers to be called when the Deferred object is either resolved or rejected. + * + * @param alwaysCallbacks1 A function, or array of functions, that is called when the Deferred is resolved or rejected. + * @param alwaysCallbacks2 Optional additional functions, or arrays of functions, that are called when the Deferred is resolved or rejected. + */ + always(alwaysCallback1?: JQueryPromiseCallback<any>|JQueryPromiseCallback<any>[], ...alwaysCallbacksN: Array<JQueryPromiseCallback<any>|JQueryPromiseCallback<any>[]>): JQueryDeferred<T>; + /** + * Add handlers to be called when the Deferred object is resolved. + * + * @param doneCallbacks1 A function, or array of functions, that are called when the Deferred is resolved. + * @param doneCallbacks2 Optional additional functions, or arrays of functions, that are called when the Deferred is resolved. + */ + done(doneCallback1?: JQueryPromiseCallback<T>|JQueryPromiseCallback<T>[], ...doneCallbackN: Array<JQueryPromiseCallback<T>|JQueryPromiseCallback<T>[]>): JQueryDeferred<T>; + /** + * Add handlers to be called when the Deferred object is rejected. + * + * @param failCallbacks1 A function, or array of functions, that are called when the Deferred is rejected. + * @param failCallbacks2 Optional additional functions, or arrays of functions, that are called when the Deferred is rejected. + */ + fail(failCallback1?: JQueryPromiseCallback<any>|JQueryPromiseCallback<any>[], ...failCallbacksN: Array<JQueryPromiseCallback<any>|JQueryPromiseCallback<any>[]>): JQueryDeferred<T>; + /** + * Add handlers to be called when the Deferred object generates progress notifications. + * + * @param progressCallbacks A function, or array of functions, to be called when the Deferred generates progress notifications. + */ + progress(progressCallback1?: JQueryPromiseCallback<any>|JQueryPromiseCallback<any>[], ...progressCallbackN: Array<JQueryPromiseCallback<any>|JQueryPromiseCallback<any>[]>): JQueryDeferred<T>; + + /** + * Call the progressCallbacks on a Deferred object with the given args. + * + * @param args Optional arguments that are passed to the progressCallbacks. + */ + notify(value?: any, ...args: any[]): JQueryDeferred<T>; + + /** + * Call the progressCallbacks on a Deferred object with the given context and args. + * + * @param context Context passed to the progressCallbacks as the this object. + * @param args Optional arguments that are passed to the progressCallbacks. + */ + notifyWith(context: any, value?: any[]): JQueryDeferred<T>; + + /** + * Reject a Deferred object and call any failCallbacks with the given args. + * + * @param args Optional arguments that are passed to the failCallbacks. + */ + reject(value?: any, ...args: any[]): JQueryDeferred<T>; + /** + * Reject a Deferred object and call any failCallbacks with the given context and args. + * + * @param context Context passed to the failCallbacks as the this object. + * @param args An optional array of arguments that are passed to the failCallbacks. + */ + rejectWith(context: any, value?: any[]): JQueryDeferred<T>; + + /** + * Resolve a Deferred object and call any doneCallbacks with the given args. + * + * @param value First argument passed to doneCallbacks. + * @param args Optional subsequent arguments that are passed to the doneCallbacks. + */ + resolve(value?: T, ...args: any[]): JQueryDeferred<T>; + + /** + * Resolve a Deferred object and call any doneCallbacks with the given context and args. + * + * @param context Context passed to the doneCallbacks as the this object. + * @param args An optional array of arguments that are passed to the doneCallbacks. + */ + resolveWith(context: any, value?: T[]): JQueryDeferred<T>; + + /** + * Return a Deferred's Promise object. + * + * @param target Object onto which the promise methods have to be attached + */ + promise(target?: any): JQueryPromise<T>; + + // Deprecated - given no typings + pipe(doneFilter?: (x: any) => any, failFilter?: (x: any) => any, progressFilter?: (x: any) => any): JQueryPromise<any>; +} + +/** + * Interface of the JQuery extension of the W3C event object + */ +interface BaseJQueryEventObject extends Event { + data: any; + delegateTarget: Element; + isDefaultPrevented(): boolean; + isImmediatePropagationStopped(): boolean; + isPropagationStopped(): boolean; + namespace: string; + originalEvent: Event; + preventDefault(): any; + relatedTarget: Element; + result: any; + stopImmediatePropagation(): void; + stopPropagation(): void; + target: Element; + pageX: number; + pageY: number; + which: number; + metaKey: boolean; +} + +interface JQueryInputEventObject extends BaseJQueryEventObject { + altKey: boolean; + ctrlKey: boolean; + metaKey: boolean; + shiftKey: boolean; +} + +interface JQueryMouseEventObject extends JQueryInputEventObject { + button: number; + clientX: number; + clientY: number; + offsetX: number; + offsetY: number; + pageX: number; + pageY: number; + screenX: number; + screenY: number; +} + +interface JQueryKeyEventObject extends JQueryInputEventObject { + char: any; + charCode: number; + key: any; + keyCode: number; +} + +interface JQueryEventObject extends BaseJQueryEventObject, JQueryInputEventObject, JQueryMouseEventObject, JQueryKeyEventObject{ +} + +/* + Collection of properties of the current browser +*/ + +interface JQuerySupport { + ajax?: boolean; + boxModel?: boolean; + changeBubbles?: boolean; + checkClone?: boolean; + checkOn?: boolean; + cors?: boolean; + cssFloat?: boolean; + hrefNormalized?: boolean; + htmlSerialize?: boolean; + leadingWhitespace?: boolean; + noCloneChecked?: boolean; + noCloneEvent?: boolean; + opacity?: boolean; + optDisabled?: boolean; + optSelected?: boolean; + scriptEval? (): boolean; + style?: boolean; + submitBubbles?: boolean; + tbody?: boolean; +} + +interface JQueryParam { + /** + * Create a serialized representation of an array or object, suitable for use in a URL query string or Ajax request. + * + * @param obj An array or object to serialize. + */ + (obj: any): string; + + /** + * Create a serialized representation of an array or object, suitable for use in a URL query string or Ajax request. + * + * @param obj An array or object to serialize. + * @param traditional A Boolean indicating whether to perform a traditional "shallow" serialization. + */ + (obj: any, traditional: boolean): string; +} + +/** + * The interface used to construct jQuery events (with $.Event). It is + * defined separately instead of inline in JQueryStatic to allow + * overriding the construction function with specific strings + * returning specific event objects. + */ +interface JQueryEventConstructor { + (name: string, eventProperties?: any): JQueryEventObject; + new (name: string, eventProperties?: any): JQueryEventObject; +} + +/** + * The interface used to specify coordinates. + */ +interface JQueryCoordinates { + left: number; + top: number; +} + +/** + * Elements in the array returned by serializeArray() + */ +interface JQuerySerializeArrayElement { + name: string; + value: string; +} + +interface JQueryAnimationOptions { + /** + * A string or number determining how long the animation will run. + */ + duration?: any; + /** + * A string indicating which easing function to use for the transition. + */ + easing?: string; + /** + * A function to call once the animation is complete. + */ + complete?: Function; + /** + * A function to be called for each animated property of each animated element. This function provides an opportunity to modify the Tween object to change the value of the property before it is set. + */ + step?: (now: number, tween: any) => any; + /** + * A function to be called after each step of the animation, only once per animated element regardless of the number of animated properties. (version added: 1.8) + */ + progress?: (animation: JQueryPromise<any>, progress: number, remainingMs: number) => any; + /** + * A function to call when the animation begins. (version added: 1.8) + */ + start?: (animation: JQueryPromise<any>) => any; + /** + * A function to be called when the animation completes (its Promise object is resolved). (version added: 1.8) + */ + done?: (animation: JQueryPromise<any>, jumpedToEnd: boolean) => any; + /** + * A function to be called when the animation fails to complete (its Promise object is rejected). (version added: 1.8) + */ + fail?: (animation: JQueryPromise<any>, jumpedToEnd: boolean) => any; + /** + * A function to be called when the animation completes or stops without completing (its Promise object is either resolved or rejected). (version added: 1.8) + */ + always?: (animation: JQueryPromise<any>, jumpedToEnd: boolean) => any; + /** + * A Boolean indicating whether to place the animation in the effects queue. If false, the animation will begin immediately. As of jQuery 1.7, the queue option can also accept a string, in which case the animation is added to the queue represented by that string. When a custom queue name is used the animation does not automatically start; you must call .dequeue("queuename") to start it. + */ + queue?: any; + /** + * A map of one or more of the CSS properties defined by the properties argument and their corresponding easing functions. (version added: 1.4) + */ + specialEasing?: Object; +} + +interface JQueryEasingFunction { + ( percent: number ): number; +} + +interface JQueryEasingFunctions { + [ name: string ]: JQueryEasingFunction; + linear: JQueryEasingFunction; + swing: JQueryEasingFunction; +} + +/** + * Static members of jQuery (those on $ and jQuery themselves) + */ +interface JQueryStatic { + + /** + * Perform an asynchronous HTTP (Ajax) request. + * + * @param settings A set of key/value pairs that configure the Ajax request. All settings are optional. A default can be set for any option with $.ajaxSetup(). + */ + ajax(settings: JQueryAjaxSettings): JQueryXHR; + /** + * Perform an asynchronous HTTP (Ajax) request. + * + * @param url A string containing the URL to which the request is sent. + * @param settings A set of key/value pairs that configure the Ajax request. All settings are optional. A default can be set for any option with $.ajaxSetup(). + */ + ajax(url: string, settings?: JQueryAjaxSettings): JQueryXHR; + + /** + * Handle custom Ajax options or modify existing options before each request is sent and before they are processed by $.ajax(). + * + * @param dataTypes An optional string containing one or more space-separated dataTypes + * @param handler A handler to set default values for future Ajax requests. + */ + ajaxPrefilter(dataTypes: string, handler: (opts: any, originalOpts: JQueryAjaxSettings, jqXHR: JQueryXHR) => any): void; + /** + * Handle custom Ajax options or modify existing options before each request is sent and before they are processed by $.ajax(). + * + * @param handler A handler to set default values for future Ajax requests. + */ + ajaxPrefilter(handler: (opts: any, originalOpts: JQueryAjaxSettings, jqXHR: JQueryXHR) => any): void; + + ajaxSettings: JQueryAjaxSettings; + + /** + * Set default values for future Ajax requests. Its use is not recommended. + * + * @param options A set of key/value pairs that configure the default Ajax request. All options are optional. + */ + ajaxSetup(options: JQueryAjaxSettings): void; + + /** + * Load data from the server using a HTTP GET request. + * + * @param url A string containing the URL to which the request is sent. + * @param success A callback function that is executed if the request succeeds. + * @param dataType The type of data expected from the server. Default: Intelligent Guess (xml, json, script, or html). + */ + get(url: string, success?: (data: any, textStatus: string, jqXHR: JQueryXHR) => any, dataType?: string): JQueryXHR; + /** + * Load data from the server using a HTTP GET request. + * + * @param url A string containing the URL to which the request is sent. + * @param data A plain object or string that is sent to the server with the request. + * @param success A callback function that is executed if the request succeeds. + * @param dataType The type of data expected from the server. Default: Intelligent Guess (xml, json, script, or html). + */ + get(url: string, data?: Object|string, success?: (data: any, textStatus: string, jqXHR: JQueryXHR) => any, dataType?: string): JQueryXHR; + /** + * Load JSON-encoded data from the server using a GET HTTP request. + * + * @param url A string containing the URL to which the request is sent. + * @param success A callback function that is executed if the request succeeds. + */ + getJSON(url: string, success?: (data: any, textStatus: string, jqXHR: JQueryXHR) => any): JQueryXHR; + /** + * Load JSON-encoded data from the server using a GET HTTP request. + * + * @param url A string containing the URL to which the request is sent. + * @param data A plain object or string that is sent to the server with the request. + * @param success A callback function that is executed if the request succeeds. + */ + getJSON(url: string, data?: Object|string, success?: (data: any, textStatus: string, jqXHR: JQueryXHR) => any): JQueryXHR; + /** + * Load a JavaScript file from the server using a GET HTTP request, then execute it. + * + * @param url A string containing the URL to which the request is sent. + * @param success A callback function that is executed if the request succeeds. + */ + getScript(url: string, success?: (script: string, textStatus: string, jqXHR: JQueryXHR) => any): JQueryXHR; + + /** + * Create a serialized representation of an array or object, suitable for use in a URL query string or Ajax request. + */ + param: JQueryParam; + + /** + * Load data from the server using a HTTP POST request. + * + * @param url A string containing the URL to which the request is sent. + * @param success A callback function that is executed if the request succeeds. Required if dataType is provided, but can be null in that case. + * @param dataType The type of data expected from the server. Default: Intelligent Guess (xml, json, script, text, html). + */ + post(url: string, success?: (data: any, textStatus: string, jqXHR: JQueryXHR) => any, dataType?: string): JQueryXHR; + /** + * Load data from the server using a HTTP POST request. + * + * @param url A string containing the URL to which the request is sent. + * @param data A plain object or string that is sent to the server with the request. + * @param success A callback function that is executed if the request succeeds. Required if dataType is provided, but can be null in that case. + * @param dataType The type of data expected from the server. Default: Intelligent Guess (xml, json, script, text, html). + */ + post(url: string, data?: Object|string, success?: (data: any, textStatus: string, jqXHR: JQueryXHR) => any, dataType?: string): JQueryXHR; + + /** + * A multi-purpose callbacks list object that provides a powerful way to manage callback lists. + * + * @param flags An optional list of space-separated flags that change how the callback list behaves. + */ + Callbacks(flags?: string): JQueryCallback; + + /** + * Holds or releases the execution of jQuery's ready event. + * + * @param hold Indicates whether the ready hold is being requested or released + */ + holdReady(hold: boolean): void; + + /** + * Accepts a string containing a CSS selector which is then used to match a set of elements. + * + * @param selector A string containing a selector expression + * @param context A DOM Element, Document, or jQuery to use as context + */ + (selector: string, context?: Element|JQuery): JQuery; + + /** + * Accepts a string containing a CSS selector which is then used to match a set of elements. + * + * @param element A DOM element to wrap in a jQuery object. + */ + (element: Element): JQuery; + + /** + * Accepts a string containing a CSS selector which is then used to match a set of elements. + * + * @param elementArray An array containing a set of DOM elements to wrap in a jQuery object. + */ + (elementArray: Element[]): JQuery; + + /** + * Binds a function to be executed when the DOM has finished loading. + * + * @param callback A function to execute after the DOM is ready. + */ + (callback: (jQueryAlias?: JQueryStatic) => any): JQuery; + + /** + * Accepts a string containing a CSS selector which is then used to match a set of elements. + * + * @param object A plain object to wrap in a jQuery object. + */ + (object: {}): JQuery; + + /** + * Accepts a string containing a CSS selector which is then used to match a set of elements. + * + * @param object An existing jQuery object to clone. + */ + (object: JQuery): JQuery; + + /** + * Specify a function to execute when the DOM is fully loaded. + */ + (): JQuery; + + /** + * Creates DOM elements on the fly from the provided string of raw HTML. + * + * @param html A string of HTML to create on the fly. Note that this parses HTML, not XML. + * @param ownerDocument A document in which the new elements will be created. + */ + (html: string, ownerDocument?: Document): JQuery; + + /** + * Creates DOM elements on the fly from the provided string of raw HTML. + * + * @param html A string defining a single, standalone, HTML element (e.g. <div/> or <div></div>). + * @param attributes An object of attributes, events, and methods to call on the newly-created element. + */ + (html: string, attributes: Object): JQuery; + + /** + * Relinquish jQuery's control of the $ variable. + * + * @param removeAll A Boolean indicating whether to remove all jQuery variables from the global scope (including jQuery itself). + */ + noConflict(removeAll?: boolean): Object; + + /** + * Provides a way to execute callback functions based on one or more objects, usually Deferred objects that represent asynchronous events. + * + * @param deferreds One or more Deferred objects, or plain JavaScript objects. + */ + when<T>(...deferreds: Array<T|JQueryPromise<T>/* as JQueryDeferred<T> */>): JQueryPromise<T>; + + /** + * Hook directly into jQuery to override how particular CSS properties are retrieved or set, normalize CSS property naming, or create custom properties. + */ + cssHooks: { [key: string]: any; }; + cssNumber: any; + + /** + * Store arbitrary data associated with the specified element. Returns the value that was set. + * + * @param element The DOM element to associate with the data. + * @param key A string naming the piece of data to set. + * @param value The new data value. + */ + data<T>(element: Element, key: string, value: T): T; + /** + * Returns value at named data store for the element, as set by jQuery.data(element, name, value), or the full data store for the element. + * + * @param element The DOM element to associate with the data. + * @param key A string naming the piece of data to set. + */ + data(element: Element, key: string): any; + /** + * Returns value at named data store for the element, as set by jQuery.data(element, name, value), or the full data store for the element. + * + * @param element The DOM element to associate with the data. + */ + data(element: Element): any; + + /** + * Execute the next function on the queue for the matched element. + * + * @param element A DOM element from which to remove and execute a queued function. + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + */ + dequeue(element: Element, queueName?: string): void; + + /** + * Determine whether an element has any jQuery data associated with it. + * + * @param element A DOM element to be checked for data. + */ + hasData(element: Element): boolean; + + /** + * Show the queue of functions to be executed on the matched element. + * + * @param element A DOM element to inspect for an attached queue. + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + */ + queue(element: Element, queueName?: string): any[]; + /** + * Manipulate the queue of functions to be executed on the matched element. + * + * @param element A DOM element where the array of queued functions is attached. + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + * @param newQueue An array of functions to replace the current queue contents. + */ + queue(element: Element, queueName: string, newQueue: Function[]): JQuery; + /** + * Manipulate the queue of functions to be executed on the matched element. + * + * @param element A DOM element on which to add a queued function. + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + * @param callback The new function to add to the queue. + */ + queue(element: Element, queueName: string, callback: Function): JQuery; + + /** + * Remove a previously-stored piece of data. + * + * @param element A DOM element from which to remove data. + * @param name A string naming the piece of data to remove. + */ + removeData(element: Element, name?: string): JQuery; + + /** + * A constructor function that returns a chainable utility object with methods to register multiple callbacks into callback queues, invoke callback queues, and relay the success or failure state of any synchronous or asynchronous function. + * + * @param beforeStart A function that is called just before the constructor returns. + */ + Deferred<T>(beforeStart?: (deferred: JQueryDeferred<T>) => any): JQueryDeferred<T>; + + /** + * Effects + */ + + easing: JQueryEasingFunctions; + + fx: { + tick: () => void; + /** + * The rate (in milliseconds) at which animations fire. + */ + interval: number; + stop: () => void; + speeds: { slow: number; fast: number; }; + /** + * Globally disable all animations. + */ + off: boolean; + step: any; + }; + + /** + * Takes a function and returns a new one that will always have a particular context. + * + * @param fnction The function whose context will be changed. + * @param context The object to which the context (this) of the function should be set. + * @param additionalArguments Any number of arguments to be passed to the function referenced in the function argument. + */ + proxy(fnction: (...args: any[]) => any, context: Object, ...additionalArguments: any[]): any; + /** + * Takes a function and returns a new one that will always have a particular context. + * + * @param context The object to which the context (this) of the function should be set. + * @param name The name of the function whose context will be changed (should be a property of the context object). + * @param additionalArguments Any number of arguments to be passed to the function named in the name argument. + */ + proxy(context: Object, name: string, ...additionalArguments: any[]): any; + + Event: JQueryEventConstructor; + + /** + * Takes a string and throws an exception containing it. + * + * @param message The message to send out. + */ + error(message: any): JQuery; + + expr: any; + fn: any; //TODO: Decide how we want to type this + + isReady: boolean; + + // Properties + support: JQuerySupport; + + /** + * Check to see if a DOM element is a descendant of another DOM element. + * + * @param container The DOM element that may contain the other element. + * @param contained The DOM element that may be contained by (a descendant of) the other element. + */ + contains(container: Element, contained: Element): boolean; + + /** + * A generic iterator function, which can be used to seamlessly iterate over both objects and arrays. Arrays and array-like objects with a length property (such as a function's arguments object) are iterated by numeric index, from 0 to length-1. Other objects are iterated via their named properties. + * + * @param collection The object or array to iterate over. + * @param callback The function that will be executed on every object. + */ + each<T>( + collection: T[], + callback: (indexInArray: number, valueOfElement: T) => any + ): any; + + /** + * A generic iterator function, which can be used to seamlessly iterate over both objects and arrays. Arrays and array-like objects with a length property (such as a function's arguments object) are iterated by numeric index, from 0 to length-1. Other objects are iterated via their named properties. + * + * @param collection The object or array to iterate over. + * @param callback The function that will be executed on every object. + */ + each( + collection: any, + callback: (indexInArray: any, valueOfElement: any) => any + ): any; + + /** + * Merge the contents of two or more objects together into the first object. + * + * @param target An object that will receive the new properties if additional objects are passed in or that will extend the jQuery namespace if it is the sole argument. + * @param object1 An object containing additional properties to merge in. + * @param objectN Additional objects containing properties to merge in. + */ + extend(target: any, object1?: any, ...objectN: any[]): any; + /** + * Merge the contents of two or more objects together into the first object. + * + * @param deep If true, the merge becomes recursive (aka. deep copy). + * @param target The object to extend. It will receive the new properties. + * @param object1 An object containing additional properties to merge in. + * @param objectN Additional objects containing properties to merge in. + */ + extend(deep: boolean, target: any, object1?: any, ...objectN: any[]): any; + + /** + * Execute some JavaScript code globally. + * + * @param code The JavaScript code to execute. + */ + globalEval(code: string): any; + + /** + * Finds the elements of an array which satisfy a filter function. The original array is not affected. + * + * @param array The array to search through. + * @param func The function to process each item against. The first argument to the function is the item, and the second argument is the index. The function should return a Boolean value. this will be the global window object. + * @param invert If "invert" is false, or not provided, then the function returns an array consisting of all elements for which "callback" returns true. If "invert" is true, then the function returns an array consisting of all elements for which "callback" returns false. + */ + grep<T>(array: T[], func: (elementOfArray: T, indexInArray: number) => boolean, invert?: boolean): T[]; + + /** + * Search for a specified value within an array and return its index (or -1 if not found). + * + * @param value The value to search for. + * @param array An array through which to search. + * @param fromIndex he index of the array at which to begin the search. The default is 0, which will search the whole array. + */ + inArray<T>(value: T, array: T[], fromIndex?: number): number; + + /** + * Determine whether the argument is an array. + * + * @param obj Object to test whether or not it is an array. + */ + isArray(obj: any): boolean; + /** + * Check to see if an object is empty (contains no enumerable properties). + * + * @param obj The object that will be checked to see if it's empty. + */ + isEmptyObject(obj: any): boolean; + /** + * Determine if the argument passed is a Javascript function object. + * + * @param obj Object to test whether or not it is a function. + */ + isFunction(obj: any): boolean; + /** + * Determines whether its argument is a number. + * + * @param obj The value to be tested. + */ + isNumeric(value: any): boolean; + /** + * Check to see if an object is a plain object (created using "{}" or "new Object"). + * + * @param obj The object that will be checked to see if it's a plain object. + */ + isPlainObject(obj: any): boolean; + /** + * Determine whether the argument is a window. + * + * @param obj Object to test whether or not it is a window. + */ + isWindow(obj: any): boolean; + /** + * Check to see if a DOM node is within an XML document (or is an XML document). + * + * @param node he DOM node that will be checked to see if it's in an XML document. + */ + isXMLDoc(node: Node): boolean; + + /** + * Convert an array-like object into a true JavaScript array. + * + * @param obj Any object to turn into a native Array. + */ + makeArray(obj: any): any[]; + + /** + * Translate all items in an array or object to new array of items. + * + * @param array The Array to translate. + * @param callback The function to process each item against. The first argument to the function is the array item, the second argument is the index in array The function can return any value. Within the function, this refers to the global (window) object. + */ + map<T, U>(array: T[], callback: (elementOfArray: T, indexInArray: number) => U): U[]; + /** + * Translate all items in an array or object to new array of items. + * + * @param arrayOrObject The Array or Object to translate. + * @param callback The function to process each item against. The first argument to the function is the value; the second argument is the index or key of the array or object property. The function can return any value to add to the array. A returned array will be flattened into the resulting array. Within the function, this refers to the global (window) object. + */ + map(arrayOrObject: any, callback: (value: any, indexOrKey: any) => any): any; + + /** + * Merge the contents of two arrays together into the first array. + * + * @param first The first array to merge, the elements of second added. + * @param second The second array to merge into the first, unaltered. + */ + merge<T>(first: T[], second: T[]): T[]; + + /** + * An empty function. + */ + noop(): any; + + /** + * Return a number representing the current time. + */ + now(): number; + + /** + * Takes a well-formed JSON string and returns the resulting JavaScript object. + * + * @param json The JSON string to parse. + */ + parseJSON(json: string): any; + + /** + * Parses a string into an XML document. + * + * @param data a well-formed XML string to be parsed + */ + parseXML(data: string): XMLDocument; + + /** + * Remove the whitespace from the beginning and end of a string. + * + * @param str Remove the whitespace from the beginning and end of a string. + */ + trim(str: string): string; + + /** + * Determine the internal JavaScript [[Class]] of an object. + * + * @param obj Object to get the internal JavaScript [[Class]] of. + */ + type(obj: any): string; + + /** + * Sorts an array of DOM elements, in place, with the duplicates removed. Note that this only works on arrays of DOM elements, not strings or numbers. + * + * @param array The Array of DOM elements. + */ + unique(array: Element[]): Element[]; + + /** + * Parses a string into an array of DOM nodes. + * + * @param data HTML string to be parsed + * @param context DOM element to serve as the context in which the HTML fragment will be created + * @param keepScripts A Boolean indicating whether to include scripts passed in the HTML string + */ + parseHTML(data: string, context?: HTMLElement, keepScripts?: boolean): any[]; + + /** + * Parses a string into an array of DOM nodes. + * + * @param data HTML string to be parsed + * @param context DOM element to serve as the context in which the HTML fragment will be created + * @param keepScripts A Boolean indicating whether to include scripts passed in the HTML string + */ + parseHTML(data: string, context?: Document, keepScripts?: boolean): any[]; +} + +/** + * The jQuery instance members + */ +interface JQuery { + /** + * Register a handler to be called when Ajax requests complete. This is an AjaxEvent. + * + * @param handler The function to be invoked. + */ + ajaxComplete(handler: (event: JQueryEventObject, XMLHttpRequest: XMLHttpRequest, ajaxOptions: any) => any): JQuery; + /** + * Register a handler to be called when Ajax requests complete with an error. This is an Ajax Event. + * + * @param handler The function to be invoked. + */ + ajaxError(handler: (event: JQueryEventObject, jqXHR: JQueryXHR, ajaxSettings: JQueryAjaxSettings, thrownError: any) => any): JQuery; + /** + * Attach a function to be executed before an Ajax request is sent. This is an Ajax Event. + * + * @param handler The function to be invoked. + */ + ajaxSend(handler: (event: JQueryEventObject, jqXHR: JQueryXHR, ajaxOptions: JQueryAjaxSettings) => any): JQuery; + /** + * Register a handler to be called when the first Ajax request begins. This is an Ajax Event. + * + * @param handler The function to be invoked. + */ + ajaxStart(handler: () => any): JQuery; + /** + * Register a handler to be called when all Ajax requests have completed. This is an Ajax Event. + * + * @param handler The function to be invoked. + */ + ajaxStop(handler: () => any): JQuery; + /** + * Attach a function to be executed whenever an Ajax request completes successfully. This is an Ajax Event. + * + * @param handler The function to be invoked. + */ + ajaxSuccess(handler: (event: JQueryEventObject, XMLHttpRequest: XMLHttpRequest, ajaxOptions: JQueryAjaxSettings) => any): JQuery; + + /** + * Load data from the server and place the returned HTML into the matched element. + * + * @param url A string containing the URL to which the request is sent. + * @param data A plain object or string that is sent to the server with the request. + * @param complete A callback function that is executed when the request completes. + */ + load(url: string, data?: string|Object, complete?: (responseText: string, textStatus: string, XMLHttpRequest: XMLHttpRequest) => any): JQuery; + + /** + * Encode a set of form elements as a string for submission. + */ + serialize(): string; + /** + * Encode a set of form elements as an array of names and values. + */ + serializeArray(): JQuerySerializeArrayElement[]; + + /** + * Adds the specified class(es) to each of the set of matched elements. + * + * @param className One or more space-separated classes to be added to the class attribute of each matched element. + */ + addClass(className: string): JQuery; + /** + * Adds the specified class(es) to each of the set of matched elements. + * + * @param function A function returning one or more space-separated class names to be added to the existing class name(s). Receives the index position of the element in the set and the existing class name(s) as arguments. Within the function, this refers to the current element in the set. + */ + addClass(func: (index: number, className: string) => string): JQuery; + + /** + * Add the previous set of elements on the stack to the current set, optionally filtered by a selector. + */ + addBack(selector?: string): JQuery; + + /** + * Get the value of an attribute for the first element in the set of matched elements. + * + * @param attributeName The name of the attribute to get. + */ + attr(attributeName: string): string; + /** + * Set one or more attributes for the set of matched elements. + * + * @param attributeName The name of the attribute to set. + * @param value A value to set for the attribute. + */ + attr(attributeName: string, value: string|number): JQuery; + /** + * Set one or more attributes for the set of matched elements. + * + * @param attributeName The name of the attribute to set. + * @param func A function returning the value to set. this is the current element. Receives the index position of the element in the set and the old attribute value as arguments. + */ + attr(attributeName: string, func: (index: number, attr: string) => string|number): JQuery; + /** + * Set one or more attributes for the set of matched elements. + * + * @param attributes An object of attribute-value pairs to set. + */ + attr(attributes: Object): JQuery; + + /** + * Determine whether any of the matched elements are assigned the given class. + * + * @param className The class name to search for. + */ + hasClass(className: string): boolean; + + /** + * Get the HTML contents of the first element in the set of matched elements. + */ + html(): string; + /** + * Set the HTML contents of each element in the set of matched elements. + * + * @param htmlString A string of HTML to set as the content of each matched element. + */ + html(htmlString: string): JQuery; + /** + * Set the HTML contents of each element in the set of matched elements. + * + * @param func A function returning the HTML content to set. Receives the index position of the element in the set and the old HTML value as arguments. jQuery empties the element before calling the function; use the oldhtml argument to reference the previous content. Within the function, this refers to the current element in the set. + */ + html(func: (index: number, oldhtml: string) => string): JQuery; + /** + * Set the HTML contents of each element in the set of matched elements. + * + * @param func A function returning the HTML content to set. Receives the index position of the element in the set and the old HTML value as arguments. jQuery empties the element before calling the function; use the oldhtml argument to reference the previous content. Within the function, this refers to the current element in the set. + */ + + /** + * Get the value of a property for the first element in the set of matched elements. + * + * @param propertyName The name of the property to get. + */ + prop(propertyName: string): any; + /** + * Set one or more properties for the set of matched elements. + * + * @param propertyName The name of the property to set. + * @param value A value to set for the property. + */ + prop(propertyName: string, value: string|number|boolean): JQuery; + /** + * Set one or more properties for the set of matched elements. + * + * @param properties An object of property-value pairs to set. + */ + prop(properties: Object): JQuery; + /** + * Set one or more properties for the set of matched elements. + * + * @param propertyName The name of the property to set. + * @param func A function returning the value to set. Receives the index position of the element in the set and the old property value as arguments. Within the function, the keyword this refers to the current element. + */ + prop(propertyName: string, func: (index: number, oldPropertyValue: any) => any): JQuery; + + /** + * Remove an attribute from each element in the set of matched elements. + * + * @param attributeName An attribute to remove; as of version 1.7, it can be a space-separated list of attributes. + */ + removeAttr(attributeName: string): JQuery; + + /** + * Remove a single class, multiple classes, or all classes from each element in the set of matched elements. + * + * @param className One or more space-separated classes to be removed from the class attribute of each matched element. + */ + removeClass(className?: string): JQuery; + /** + * Remove a single class, multiple classes, or all classes from each element in the set of matched elements. + * + * @param function A function returning one or more space-separated class names to be removed. Receives the index position of the element in the set and the old class value as arguments. + */ + removeClass(func: (index: number, className: string) => string): JQuery; + + /** + * Remove a property for the set of matched elements. + * + * @param propertyName The name of the property to remove. + */ + removeProp(propertyName: string): JQuery; + + /** + * Add or remove one or more classes from each element in the set of matched elements, depending on either the class's presence or the value of the switch argument. + * + * @param className One or more class names (separated by spaces) to be toggled for each element in the matched set. + * @param swtch A Boolean (not just truthy/falsy) value to determine whether the class should be added or removed. + */ + toggleClass(className: string, swtch?: boolean): JQuery; + /** + * Add or remove one or more classes from each element in the set of matched elements, depending on either the class's presence or the value of the switch argument. + * + * @param swtch A boolean value to determine whether the class should be added or removed. + */ + toggleClass(swtch?: boolean): JQuery; + /** + * Add or remove one or more classes from each element in the set of matched elements, depending on either the class's presence or the value of the switch argument. + * + * @param func A function that returns class names to be toggled in the class attribute of each element in the matched set. Receives the index position of the element in the set, the old class value, and the switch as arguments. + * @param swtch A boolean value to determine whether the class should be added or removed. + */ + toggleClass(func: (index: number, className: string, swtch: boolean) => string, swtch?: boolean): JQuery; + + /** + * Get the current value of the first element in the set of matched elements. + */ + val(): any; + /** + * Set the value of each element in the set of matched elements. + * + * @param value A string of text, an array of strings or number corresponding to the value of each matched element to set as selected/checked. + */ + val(value: string|string[]|number): JQuery; + /** + * Set the value of each element in the set of matched elements. + * + * @param func A function returning the value to set. this is the current element. Receives the index position of the element in the set and the old value as arguments. + */ + val(func: (index: number, value: string) => string): JQuery; + + + /** + * Get the value of style properties for the first element in the set of matched elements. + * + * @param propertyName A CSS property. + */ + css(propertyName: string): string; + /** + * Set one or more CSS properties for the set of matched elements. + * + * @param propertyName A CSS property name. + * @param value A value to set for the property. + */ + css(propertyName: string, value: string|number): JQuery; + /** + * Set one or more CSS properties for the set of matched elements. + * + * @param propertyName A CSS property name. + * @param value A function returning the value to set. this is the current element. Receives the index position of the element in the set and the old value as arguments. + */ + css(propertyName: string, value: (index: number, value: string) => string|number): JQuery; + /** + * Set one or more CSS properties for the set of matched elements. + * + * @param properties An object of property-value pairs to set. + */ + css(properties: Object): JQuery; + + /** + * Get the current computed height for the first element in the set of matched elements. + */ + height(): number; + /** + * Set the CSS height of every matched element. + * + * @param value An integer representing the number of pixels, or an integer with an optional unit of measure appended (as a string). + */ + height(value: number|string): JQuery; + /** + * Set the CSS height of every matched element. + * + * @param func A function returning the height to set. Receives the index position of the element in the set and the old height as arguments. Within the function, this refers to the current element in the set. + */ + height(func: (index: number, height: number) => number|string): JQuery; + + /** + * Get the current computed height for the first element in the set of matched elements, including padding but not border. + */ + innerHeight(): number; + + /** + * Sets the inner height on elements in the set of matched elements, including padding but not border. + * + * @param value An integer representing the number of pixels, or an integer along with an optional unit of measure appended (as a string). + */ + innerHeight(height: number|string): JQuery; + + /** + * Get the current computed width for the first element in the set of matched elements, including padding but not border. + */ + innerWidth(): number; + + /** + * Sets the inner width on elements in the set of matched elements, including padding but not border. + * + * @param value An integer representing the number of pixels, or an integer along with an optional unit of measure appended (as a string). + */ + innerWidth(width: number|string): JQuery; + + /** + * Get the current coordinates of the first element in the set of matched elements, relative to the document. + */ + offset(): JQueryCoordinates; + /** + * An object containing the properties top and left, which are integers indicating the new top and left coordinates for the elements. + * + * @param coordinates An object containing the properties top and left, which are integers indicating the new top and left coordinates for the elements. + */ + offset(coordinates: JQueryCoordinates): JQuery; + /** + * An object containing the properties top and left, which are integers indicating the new top and left coordinates for the elements. + * + * @param func A function to return the coordinates to set. Receives the index of the element in the collection as the first argument and the current coordinates as the second argument. The function should return an object with the new top and left properties. + */ + offset(func: (index: number, coords: JQueryCoordinates) => JQueryCoordinates): JQuery; + + /** + * Get the current computed height for the first element in the set of matched elements, including padding, border, and optionally margin. Returns an integer (without "px") representation of the value or null if called on an empty set of elements. + * + * @param includeMargin A Boolean indicating whether to include the element's margin in the calculation. + */ + outerHeight(includeMargin?: boolean): number; + + /** + * Sets the outer height on elements in the set of matched elements, including padding and border. + * + * @param value An integer representing the number of pixels, or an integer along with an optional unit of measure appended (as a string). + */ + outerHeight(height: number|string): JQuery; + + /** + * Get the current computed width for the first element in the set of matched elements, including padding and border. + * + * @param includeMargin A Boolean indicating whether to include the element's margin in the calculation. + */ + outerWidth(includeMargin?: boolean): number; + + /** + * Sets the outer width on elements in the set of matched elements, including padding and border. + * + * @param value An integer representing the number of pixels, or an integer along with an optional unit of measure appended (as a string). + */ + outerWidth(width: number|string): JQuery; + + /** + * Get the current coordinates of the first element in the set of matched elements, relative to the offset parent. + */ + position(): JQueryCoordinates; + + /** + * Get the current horizontal position of the scroll bar for the first element in the set of matched elements or set the horizontal position of the scroll bar for every matched element. + */ + scrollLeft(): number; + /** + * Set the current horizontal position of the scroll bar for each of the set of matched elements. + * + * @param value An integer indicating the new position to set the scroll bar to. + */ + scrollLeft(value: number): JQuery; + + /** + * Get the current vertical position of the scroll bar for the first element in the set of matched elements or set the vertical position of the scroll bar for every matched element. + */ + scrollTop(): number; + /** + * Set the current vertical position of the scroll bar for each of the set of matched elements. + * + * @param value An integer indicating the new position to set the scroll bar to. + */ + scrollTop(value: number): JQuery; + + /** + * Get the current computed width for the first element in the set of matched elements. + */ + width(): number; + /** + * Set the CSS width of each element in the set of matched elements. + * + * @param value An integer representing the number of pixels, or an integer along with an optional unit of measure appended (as a string). + */ + width(value: number|string): JQuery; + /** + * Set the CSS width of each element in the set of matched elements. + * + * @param func A function returning the width to set. Receives the index position of the element in the set and the old width as arguments. Within the function, this refers to the current element in the set. + */ + width(func: (index: number, width: number) => number|string): JQuery; + + /** + * Remove from the queue all items that have not yet been run. + * + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + */ + clearQueue(queueName?: string): JQuery; + + /** + * Store arbitrary data associated with the matched elements. + * + * @param key A string naming the piece of data to set. + * @param value The new data value; it can be any Javascript type including Array or Object. + */ + data(key: string, value: any): JQuery; + /** + * Return the value at the named data store for the first element in the jQuery collection, as set by data(name, value) or by an HTML5 data-* attribute. + * + * @param key Name of the data stored. + */ + data(key: string): any; + /** + * Store arbitrary data associated with the matched elements. + * + * @param obj An object of key-value pairs of data to update. + */ + data(obj: { [key: string]: any; }): JQuery; + /** + * Return the value at the named data store for the first element in the jQuery collection, as set by data(name, value) or by an HTML5 data-* attribute. + */ + data(): any; + + /** + * Execute the next function on the queue for the matched elements. + * + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + */ + dequeue(queueName?: string): JQuery; + + /** + * Remove a previously-stored piece of data. + * + * @param name A string naming the piece of data to delete or space-separated string naming the pieces of data to delete. + */ + removeData(name: string): JQuery; + /** + * Remove a previously-stored piece of data. + * + * @param list An array of strings naming the pieces of data to delete. + */ + removeData(list: string[]): JQuery; + /** + * Remove all previously-stored piece of data. + */ + removeData(): JQuery; + + /** + * Return a Promise object to observe when all actions of a certain type bound to the collection, queued or not, have finished. + * + * @param type The type of queue that needs to be observed. (default: fx) + * @param target Object onto which the promise methods have to be attached + */ + promise(type?: string, target?: Object): JQueryPromise<any>; + + /** + * Perform a custom animation of a set of CSS properties. + * + * @param properties An object of CSS properties and values that the animation will move toward. + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + */ + animate(properties: Object, duration?: string|number, complete?: Function): JQuery; + /** + * Perform a custom animation of a set of CSS properties. + * + * @param properties An object of CSS properties and values that the animation will move toward. + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. (default: swing) + * @param complete A function to call once the animation is complete. + */ + animate(properties: Object, duration?: string|number, easing?: string, complete?: Function): JQuery; + /** + * Perform a custom animation of a set of CSS properties. + * + * @param properties An object of CSS properties and values that the animation will move toward. + * @param options A map of additional options to pass to the method. + */ + animate(properties: Object, options: JQueryAnimationOptions): JQuery; + + /** + * Set a timer to delay execution of subsequent items in the queue. + * + * @param duration An integer indicating the number of milliseconds to delay execution of the next item in the queue. + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + */ + delay(duration: number, queueName?: string): JQuery; + + /** + * Display the matched elements by fading them to opaque. + * + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + */ + fadeIn(duration?: number|string, complete?: Function): JQuery; + /** + * Display the matched elements by fading them to opaque. + * + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + */ + fadeIn(duration?: number|string, easing?: string, complete?: Function): JQuery; + /** + * Display the matched elements by fading them to opaque. + * + * @param options A map of additional options to pass to the method. + */ + fadeIn(options: JQueryAnimationOptions): JQuery; + + /** + * Hide the matched elements by fading them to transparent. + * + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + */ + fadeOut(duration?: number|string, complete?: Function): JQuery; + /** + * Hide the matched elements by fading them to transparent. + * + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + */ + fadeOut(duration?: number|string, easing?: string, complete?: Function): JQuery; + /** + * Hide the matched elements by fading them to transparent. + * + * @param options A map of additional options to pass to the method. + */ + fadeOut(options: JQueryAnimationOptions): JQuery; + + /** + * Adjust the opacity of the matched elements. + * + * @param duration A string or number determining how long the animation will run. + * @param opacity A number between 0 and 1 denoting the target opacity. + * @param complete A function to call once the animation is complete. + */ + fadeTo(duration: string|number, opacity: number, complete?: Function): JQuery; + /** + * Adjust the opacity of the matched elements. + * + * @param duration A string or number determining how long the animation will run. + * @param opacity A number between 0 and 1 denoting the target opacity. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + */ + fadeTo(duration: string|number, opacity: number, easing?: string, complete?: Function): JQuery; + + /** + * Display or hide the matched elements by animating their opacity. + * + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + */ + fadeToggle(duration?: number|string, complete?: Function): JQuery; + /** + * Display or hide the matched elements by animating their opacity. + * + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + */ + fadeToggle(duration?: number|string, easing?: string, complete?: Function): JQuery; + /** + * Display or hide the matched elements by animating their opacity. + * + * @param options A map of additional options to pass to the method. + */ + fadeToggle(options: JQueryAnimationOptions): JQuery; + + /** + * Stop the currently-running animation, remove all queued animations, and complete all animations for the matched elements. + * + * @param queue The name of the queue in which to stop animations. + */ + finish(queue?: string): JQuery; + + /** + * Hide the matched elements. + * + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + */ + hide(duration?: number|string, complete?: Function): JQuery; + /** + * Hide the matched elements. + * + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + */ + hide(duration?: number|string, easing?: string, complete?: Function): JQuery; + /** + * Hide the matched elements. + * + * @param options A map of additional options to pass to the method. + */ + hide(options: JQueryAnimationOptions): JQuery; + + /** + * Display the matched elements. + * + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + */ + show(duration?: number|string, complete?: Function): JQuery; + /** + * Display the matched elements. + * + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + */ + show(duration?: number|string, easing?: string, complete?: Function): JQuery; + /** + * Display the matched elements. + * + * @param options A map of additional options to pass to the method. + */ + show(options: JQueryAnimationOptions): JQuery; + + /** + * Display the matched elements with a sliding motion. + * + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + */ + slideDown(duration?: number|string, complete?: Function): JQuery; + /** + * Display the matched elements with a sliding motion. + * + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + */ + slideDown(duration?: number|string, easing?: string, complete?: Function): JQuery; + /** + * Display the matched elements with a sliding motion. + * + * @param options A map of additional options to pass to the method. + */ + slideDown(options: JQueryAnimationOptions): JQuery; + + /** + * Display or hide the matched elements with a sliding motion. + * + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + */ + slideToggle(duration?: number|string, complete?: Function): JQuery; + /** + * Display or hide the matched elements with a sliding motion. + * + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + */ + slideToggle(duration?: number|string, easing?: string, complete?: Function): JQuery; + /** + * Display or hide the matched elements with a sliding motion. + * + * @param options A map of additional options to pass to the method. + */ + slideToggle(options: JQueryAnimationOptions): JQuery; + + /** + * Hide the matched elements with a sliding motion. + * + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + */ + slideUp(duration?: number|string, complete?: Function): JQuery; + /** + * Hide the matched elements with a sliding motion. + * + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + */ + slideUp(duration?: number|string, easing?: string, complete?: Function): JQuery; + /** + * Hide the matched elements with a sliding motion. + * + * @param options A map of additional options to pass to the method. + */ + slideUp(options: JQueryAnimationOptions): JQuery; + + /** + * Stop the currently-running animation on the matched elements. + * + * @param clearQueue A Boolean indicating whether to remove queued animation as well. Defaults to false. + * @param jumpToEnd A Boolean indicating whether to complete the current animation immediately. Defaults to false. + */ + stop(clearQueue?: boolean, jumpToEnd?: boolean): JQuery; + /** + * Stop the currently-running animation on the matched elements. + * + * @param queue The name of the queue in which to stop animations. + * @param clearQueue A Boolean indicating whether to remove queued animation as well. Defaults to false. + * @param jumpToEnd A Boolean indicating whether to complete the current animation immediately. Defaults to false. + */ + stop(queue?: string, clearQueue?: boolean, jumpToEnd?: boolean): JQuery; + + /** + * Display or hide the matched elements. + * + * @param duration A string or number determining how long the animation will run. + * @param complete A function to call once the animation is complete. + */ + toggle(duration?: number|string, complete?: Function): JQuery; + /** + * Display or hide the matched elements. + * + * @param duration A string or number determining how long the animation will run. + * @param easing A string indicating which easing function to use for the transition. + * @param complete A function to call once the animation is complete. + */ + toggle(duration?: number|string, easing?: string, complete?: Function): JQuery; + /** + * Display or hide the matched elements. + * + * @param options A map of additional options to pass to the method. + */ + toggle(options: JQueryAnimationOptions): JQuery; + /** + * Display or hide the matched elements. + * + * @param showOrHide A Boolean indicating whether to show or hide the elements. + */ + toggle(showOrHide: boolean): JQuery; + + /** + * Attach a handler to an event for the elements. + * + * @param eventType A string containing one or more DOM event types, such as "click" or "submit," or custom event names. + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + */ + bind(eventType: string, eventData: any, handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Attach a handler to an event for the elements. + * + * @param eventType A string containing one or more DOM event types, such as "click" or "submit," or custom event names. + * @param handler A function to execute each time the event is triggered. + */ + bind(eventType: string, handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Attach a handler to an event for the elements. + * + * @param eventType A string containing one or more DOM event types, such as "click" or "submit," or custom event names. + * @param eventData An object containing data that will be passed to the event handler. + * @param preventBubble Setting the third argument to false will attach a function that prevents the default action from occurring and stops the event from bubbling. The default is true. + */ + bind(eventType: string, eventData: any, preventBubble: boolean): JQuery; + /** + * Attach a handler to an event for the elements. + * + * @param eventType A string containing one or more DOM event types, such as "click" or "submit," or custom event names. + * @param preventBubble Setting the third argument to false will attach a function that prevents the default action from occurring and stops the event from bubbling. The default is true. + */ + bind(eventType: string, preventBubble: boolean): JQuery; + /** + * Attach a handler to an event for the elements. + * + * @param events An object containing one or more DOM event types and functions to execute for them. + */ + bind(events: any): JQuery; + + /** + * Trigger the "blur" event on an element + */ + blur(): JQuery; + /** + * Bind an event handler to the "blur" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + */ + blur(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "blur" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + */ + blur(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "change" event on an element. + */ + change(): JQuery; + /** + * Bind an event handler to the "change" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + */ + change(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "change" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + */ + change(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "click" event on an element. + */ + click(): JQuery; + /** + * Bind an event handler to the "click" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + */ + click(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "click" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + */ + click(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "dblclick" event on an element. + */ + dblclick(): JQuery; + /** + * Bind an event handler to the "dblclick" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + */ + dblclick(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "dblclick" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + */ + dblclick(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + + delegate(selector: any, eventType: string, handler: (eventObject: JQueryEventObject) => any): JQuery; + delegate(selector: any, eventType: string, eventData: any, handler: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "focus" event on an element. + */ + focus(): JQuery; + /** + * Bind an event handler to the "focus" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + */ + focus(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "focus" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + */ + focus(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "focusin" event on an element. + */ + focusin(): JQuery; + /** + * Bind an event handler to the "focusin" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + */ + focusin(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "focusin" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + */ + focusin(eventData: Object, handler: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "focusout" event on an element. + */ + focusout(): JQuery; + /** + * Bind an event handler to the "focusout" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + */ + focusout(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "focusout" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + */ + focusout(eventData: Object, handler: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Bind two handlers to the matched elements, to be executed when the mouse pointer enters and leaves the elements. + * + * @param handlerIn A function to execute when the mouse pointer enters the element. + * @param handlerOut A function to execute when the mouse pointer leaves the element. + */ + hover(handlerIn: (eventObject: JQueryEventObject) => any, handlerOut: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind a single handler to the matched elements, to be executed when the mouse pointer enters or leaves the elements. + * + * @param handlerInOut A function to execute when the mouse pointer enters or leaves the element. + */ + hover(handlerInOut: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "keydown" event on an element. + */ + keydown(): JQuery; + /** + * Bind an event handler to the "keydown" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + */ + keydown(handler: (eventObject: JQueryKeyEventObject) => any): JQuery; + /** + * Bind an event handler to the "keydown" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + */ + keydown(eventData?: any, handler?: (eventObject: JQueryKeyEventObject) => any): JQuery; + + /** + * Trigger the "keypress" event on an element. + */ + keypress(): JQuery; + /** + * Bind an event handler to the "keypress" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + */ + keypress(handler: (eventObject: JQueryKeyEventObject) => any): JQuery; + /** + * Bind an event handler to the "keypress" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + */ + keypress(eventData?: any, handler?: (eventObject: JQueryKeyEventObject) => any): JQuery; + + /** + * Trigger the "keyup" event on an element. + */ + keyup(): JQuery; + /** + * Bind an event handler to the "keyup" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + */ + keyup(handler: (eventObject: JQueryKeyEventObject) => any): JQuery; + /** + * Bind an event handler to the "keyup" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + */ + keyup(eventData?: any, handler?: (eventObject: JQueryKeyEventObject) => any): JQuery; + + /** + * Bind an event handler to the "load" JavaScript event. + * + * @param handler A function to execute when the event is triggered. + */ + load(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "load" JavaScript event. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + */ + load(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "mousedown" event on an element. + */ + mousedown(): JQuery; + /** + * Bind an event handler to the "mousedown" JavaScript event. + * + * @param handler A function to execute when the event is triggered. + */ + mousedown(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + /** + * Bind an event handler to the "mousedown" JavaScript event. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + */ + mousedown(eventData: Object, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + /** + * Trigger the "mouseenter" event on an element. + */ + mouseenter(): JQuery; + /** + * Bind an event handler to be fired when the mouse enters an element. + * + * @param handler A function to execute when the event is triggered. + */ + mouseenter(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + /** + * Bind an event handler to be fired when the mouse enters an element. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + */ + mouseenter(eventData: Object, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + /** + * Trigger the "mouseleave" event on an element. + */ + mouseleave(): JQuery; + /** + * Bind an event handler to be fired when the mouse leaves an element. + * + * @param handler A function to execute when the event is triggered. + */ + mouseleave(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + /** + * Bind an event handler to be fired when the mouse leaves an element. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + */ + mouseleave(eventData: Object, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + /** + * Trigger the "mousemove" event on an element. + */ + mousemove(): JQuery; + /** + * Bind an event handler to the "mousemove" JavaScript event. + * + * @param handler A function to execute when the event is triggered. + */ + mousemove(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + /** + * Bind an event handler to the "mousemove" JavaScript event. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + */ + mousemove(eventData: Object, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + /** + * Trigger the "mouseout" event on an element. + */ + mouseout(): JQuery; + /** + * Bind an event handler to the "mouseout" JavaScript event. + * + * @param handler A function to execute when the event is triggered. + */ + mouseout(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + /** + * Bind an event handler to the "mouseout" JavaScript event. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + */ + mouseout(eventData: Object, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + /** + * Trigger the "mouseover" event on an element. + */ + mouseover(): JQuery; + /** + * Bind an event handler to the "mouseover" JavaScript event. + * + * @param handler A function to execute when the event is triggered. + */ + mouseover(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + /** + * Bind an event handler to the "mouseover" JavaScript event. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + */ + mouseover(eventData: Object, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + /** + * Trigger the "mouseup" event on an element. + */ + mouseup(): JQuery; + /** + * Bind an event handler to the "mouseup" JavaScript event. + * + * @param handler A function to execute when the event is triggered. + */ + mouseup(handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + /** + * Bind an event handler to the "mouseup" JavaScript event. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + */ + mouseup(eventData: Object, handler: (eventObject: JQueryMouseEventObject) => any): JQuery; + + /** + * Remove an event handler. + */ + off(): JQuery; + /** + * Remove an event handler. + * + * @param events One or more space-separated event types and optional namespaces, or just namespaces, such as "click", "keydown.myPlugin", or ".myPlugin". + * @param selector A selector which should match the one originally passed to .on() when attaching event handlers. + * @param handler A handler function previously attached for the event(s), or the special value false. + */ + off(events: string, selector?: string, handler?: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Remove an event handler. + * + * @param events One or more space-separated event types and optional namespaces, or just namespaces, such as "click", "keydown.myPlugin", or ".myPlugin". + * @param handler A handler function previously attached for the event(s), or the special value false. + */ + off(events: string, handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Remove an event handler. + * + * @param events An object where the string keys represent one or more space-separated event types and optional namespaces, and the values represent handler functions previously attached for the event(s). + * @param selector A selector which should match the one originally passed to .on() when attaching event handlers. + */ + off(events: { [key: string]: any; }, selector?: string): JQuery; + + /** + * Attach an event handler function for one or more events to the selected elements. + * + * @param events One or more space-separated event types and optional namespaces, such as "click" or "keydown.myPlugin". + * @param handler A function to execute when the event is triggered. The value false is also allowed as a shorthand for a function that simply does return false. Rest parameter args is for optional parameters passed to jQuery.trigger(). Note that the actual parameters on the event handler function must be marked as optional (? syntax). + */ + on(events: string, handler: (eventObject: JQueryEventObject, ...args: any[]) => any): JQuery; + /** + * Attach an event handler function for one or more events to the selected elements. + * + * @param events One or more space-separated event types and optional namespaces, such as "click" or "keydown.myPlugin". + * @param data Data to be passed to the handler in event.data when an event is triggered. + * @param handler A function to execute when the event is triggered. The value false is also allowed as a shorthand for a function that simply does return false. + */ + on(events: string, data : any, handler: (eventObject: JQueryEventObject, ...args: any[]) => any): JQuery; + /** + * Attach an event handler function for one or more events to the selected elements. + * + * @param events One or more space-separated event types and optional namespaces, such as "click" or "keydown.myPlugin". + * @param selector A selector string to filter the descendants of the selected elements that trigger the event. If the selector is null or omitted, the event is always triggered when it reaches the selected element. + * @param handler A function to execute when the event is triggered. The value false is also allowed as a shorthand for a function that simply does return false. + */ + on(events: string, selector: string, handler: (eventObject: JQueryEventObject, ...eventData: any[]) => any): JQuery; + /** + * Attach an event handler function for one or more events to the selected elements. + * + * @param events One or more space-separated event types and optional namespaces, such as "click" or "keydown.myPlugin". + * @param selector A selector string to filter the descendants of the selected elements that trigger the event. If the selector is null or omitted, the event is always triggered when it reaches the selected element. + * @param data Data to be passed to the handler in event.data when an event is triggered. + * @param handler A function to execute when the event is triggered. The value false is also allowed as a shorthand for a function that simply does return false. + */ + on(events: string, selector: string, data: any, handler: (eventObject: JQueryEventObject, ...eventData: any[]) => any): JQuery; + /** + * Attach an event handler function for one or more events to the selected elements. + * + * @param events An object in which the string keys represent one or more space-separated event types and optional namespaces, and the values represent a handler function to be called for the event(s). + * @param selector A selector string to filter the descendants of the selected elements that will call the handler. If the selector is null or omitted, the handler is always called when it reaches the selected element. + * @param data Data to be passed to the handler in event.data when an event occurs. + */ + on(events: { [key: string]: any; }, selector?: string, data?: any): JQuery; + /** + * Attach an event handler function for one or more events to the selected elements. + * + * @param events An object in which the string keys represent one or more space-separated event types and optional namespaces, and the values represent a handler function to be called for the event(s). + * @param data Data to be passed to the handler in event.data when an event occurs. + */ + on(events: { [key: string]: any; }, data?: any): JQuery; + + /** + * Attach a handler to an event for the elements. The handler is executed at most once per element per event type. + * + * @param events A string containing one or more JavaScript event types, such as "click" or "submit," or custom event names. + * @param handler A function to execute at the time the event is triggered. + */ + one(events: string, handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Attach a handler to an event for the elements. The handler is executed at most once per element per event type. + * + * @param events A string containing one or more JavaScript event types, such as "click" or "submit," or custom event names. + * @param data An object containing data that will be passed to the event handler. + * @param handler A function to execute at the time the event is triggered. + */ + one(events: string, data: Object, handler: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Attach a handler to an event for the elements. The handler is executed at most once per element per event type. + * + * @param events One or more space-separated event types and optional namespaces, such as "click" or "keydown.myPlugin". + * @param selector A selector string to filter the descendants of the selected elements that trigger the event. If the selector is null or omitted, the event is always triggered when it reaches the selected element. + * @param handler A function to execute when the event is triggered. The value false is also allowed as a shorthand for a function that simply does return false. + */ + one(events: string, selector: string, handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Attach a handler to an event for the elements. The handler is executed at most once per element per event type. + * + * @param events One or more space-separated event types and optional namespaces, such as "click" or "keydown.myPlugin". + * @param selector A selector string to filter the descendants of the selected elements that trigger the event. If the selector is null or omitted, the event is always triggered when it reaches the selected element. + * @param data Data to be passed to the handler in event.data when an event is triggered. + * @param handler A function to execute when the event is triggered. The value false is also allowed as a shorthand for a function that simply does return false. + */ + one(events: string, selector: string, data: any, handler: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Attach a handler to an event for the elements. The handler is executed at most once per element per event type. + * + * @param events An object in which the string keys represent one or more space-separated event types and optional namespaces, and the values represent a handler function to be called for the event(s). + * @param selector A selector string to filter the descendants of the selected elements that will call the handler. If the selector is null or omitted, the handler is always called when it reaches the selected element. + * @param data Data to be passed to the handler in event.data when an event occurs. + */ + one(events: { [key: string]: any; }, selector?: string, data?: any): JQuery; + + /** + * Attach a handler to an event for the elements. The handler is executed at most once per element per event type. + * + * @param events An object in which the string keys represent one or more space-separated event types and optional namespaces, and the values represent a handler function to be called for the event(s). + * @param data Data to be passed to the handler in event.data when an event occurs. + */ + one(events: { [key: string]: any; }, data?: any): JQuery; + + + /** + * Specify a function to execute when the DOM is fully loaded. + * + * @param handler A function to execute after the DOM is ready. + */ + ready(handler: (jQueryAlias?: JQueryStatic) => any): JQuery; + + /** + * Trigger the "resize" event on an element. + */ + resize(): JQuery; + /** + * Bind an event handler to the "resize" JavaScript event. + * + * @param handler A function to execute each time the event is triggered. + */ + resize(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "resize" JavaScript event. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + */ + resize(eventData: Object, handler: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "scroll" event on an element. + */ + scroll(): JQuery; + /** + * Bind an event handler to the "scroll" JavaScript event. + * + * @param handler A function to execute each time the event is triggered. + */ + scroll(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "scroll" JavaScript event. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + */ + scroll(eventData: Object, handler: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "select" event on an element. + */ + select(): JQuery; + /** + * Bind an event handler to the "select" JavaScript event. + * + * @param handler A function to execute each time the event is triggered. + */ + select(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "select" JavaScript event. + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + */ + select(eventData: Object, handler: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Trigger the "submit" event on an element. + */ + submit(): JQuery; + /** + * Bind an event handler to the "submit" JavaScript event + * + * @param handler A function to execute each time the event is triggered. + */ + submit(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "submit" JavaScript event + * + * @param eventData An object containing data that will be passed to the event handler. + * @param handler A function to execute each time the event is triggered. + */ + submit(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Execute all handlers and behaviors attached to the matched elements for the given event type. + * + * @param eventType A string containing a JavaScript event type, such as click or submit. + * @param extraParameters Additional parameters to pass along to the event handler. + */ + trigger(eventType: string, extraParameters?: any[]|Object): JQuery; + /** + * Execute all handlers and behaviors attached to the matched elements for the given event type. + * + * @param event A jQuery.Event object. + * @param extraParameters Additional parameters to pass along to the event handler. + */ + trigger(event: JQueryEventObject, extraParameters?: any[]|Object): JQuery; + + /** + * Execute all handlers attached to an element for an event. + * + * @param eventType A string containing a JavaScript event type, such as click or submit. + * @param extraParameters An array of additional parameters to pass along to the event handler. + */ + triggerHandler(eventType: string, ...extraParameters: any[]): Object; + + /** + * Execute all handlers attached to an element for an event. + * + * @param event A jQuery.Event object. + * @param extraParameters An array of additional parameters to pass along to the event handler. + */ + triggerHandler(event: JQueryEventObject, ...extraParameters: any[]): Object; + + /** + * Remove a previously-attached event handler from the elements. + * + * @param eventType A string containing a JavaScript event type, such as click or submit. + * @param handler The function that is to be no longer executed. + */ + unbind(eventType?: string, handler?: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Remove a previously-attached event handler from the elements. + * + * @param eventType A string containing a JavaScript event type, such as click or submit. + * @param fls Unbinds the corresponding 'return false' function that was bound using .bind( eventType, false ). + */ + unbind(eventType: string, fls: boolean): JQuery; + /** + * Remove a previously-attached event handler from the elements. + * + * @param evt A JavaScript event object as passed to an event handler. + */ + unbind(evt: any): JQuery; + + /** + * Remove a handler from the event for all elements which match the current selector, based upon a specific set of root elements. + */ + undelegate(): JQuery; + /** + * Remove a handler from the event for all elements which match the current selector, based upon a specific set of root elements. + * + * @param selector A selector which will be used to filter the event results. + * @param eventType A string containing a JavaScript event type, such as "click" or "keydown" + * @param handler A function to execute at the time the event is triggered. + */ + undelegate(selector: string, eventType: string, handler?: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Remove a handler from the event for all elements which match the current selector, based upon a specific set of root elements. + * + * @param selector A selector which will be used to filter the event results. + * @param events An object of one or more event types and previously bound functions to unbind from them. + */ + undelegate(selector: string, events: Object): JQuery; + /** + * Remove a handler from the event for all elements which match the current selector, based upon a specific set of root elements. + * + * @param namespace A string containing a namespace to unbind all events from. + */ + undelegate(namespace: string): JQuery; + + /** + * Bind an event handler to the "unload" JavaScript event. (DEPRECATED from v1.8) + * + * @param handler A function to execute when the event is triggered. + */ + unload(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "unload" JavaScript event. (DEPRECATED from v1.8) + * + * @param eventData A plain object of data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + */ + unload(eventData?: any, handler?: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * The DOM node context originally passed to jQuery(); if none was passed then context will likely be the document. (DEPRECATED from v1.10) + */ + context: Element; + + jquery: string; + + /** + * Bind an event handler to the "error" JavaScript event. (DEPRECATED from v1.8) + * + * @param handler A function to execute when the event is triggered. + */ + error(handler: (eventObject: JQueryEventObject) => any): JQuery; + /** + * Bind an event handler to the "error" JavaScript event. (DEPRECATED from v1.8) + * + * @param eventData A plain object of data that will be passed to the event handler. + * @param handler A function to execute when the event is triggered. + */ + error(eventData: any, handler: (eventObject: JQueryEventObject) => any): JQuery; + + /** + * Add a collection of DOM elements onto the jQuery stack. + * + * @param elements An array of elements to push onto the stack and make into a new jQuery object. + */ + pushStack(elements: any[]): JQuery; + /** + * Add a collection of DOM elements onto the jQuery stack. + * + * @param elements An array of elements to push onto the stack and make into a new jQuery object. + * @param name The name of a jQuery method that generated the array of elements. + * @param arguments The arguments that were passed in to the jQuery method (for serialization). + */ + pushStack(elements: any[], name: string, arguments: any[]): JQuery; + + /** + * Insert content, specified by the parameter, after each element in the set of matched elements. + * + * param content1 HTML string, DOM element, array of elements, or jQuery object to insert after each element in the set of matched elements. + * param content2 One or more additional DOM elements, arrays of elements, HTML strings, or jQuery objects to insert after each element in the set of matched elements. + */ + after(content1: JQuery|any[]|Element|Text|string, ...content2: any[]): JQuery; + /** + * Insert content, specified by the parameter, after each element in the set of matched elements. + * + * param func A function that returns an HTML string, DOM element(s), or jQuery object to insert after each element in the set of matched elements. Receives the index position of the element in the set as an argument. Within the function, this refers to the current element in the set. + */ + after(func: (index: number, html: string) => string|Element|JQuery): JQuery; + + /** + * Insert content, specified by the parameter, to the end of each element in the set of matched elements. + * + * param content1 DOM element, array of elements, HTML string, or jQuery object to insert at the end of each element in the set of matched elements. + * param content2 One or more additional DOM elements, arrays of elements, HTML strings, or jQuery objects to insert at the end of each element in the set of matched elements. + */ + append(content1: JQuery|any[]|Element|Text|string, ...content2: any[]): JQuery; + /** + * Insert content, specified by the parameter, to the end of each element in the set of matched elements. + * + * param func A function that returns an HTML string, DOM element(s), or jQuery object to insert at the end of each element in the set of matched elements. Receives the index position of the element in the set and the old HTML value of the element as arguments. Within the function, this refers to the current element in the set. + */ + append(func: (index: number, html: string) => string|Element|JQuery): JQuery; + + /** + * Insert every element in the set of matched elements to the end of the target. + * + * @param target A selector, element, HTML string, array of elements, or jQuery object; the matched set of elements will be inserted at the end of the element(s) specified by this parameter. + */ + appendTo(target: JQuery|any[]|Element|string): JQuery; + + /** + * Insert content, specified by the parameter, before each element in the set of matched elements. + * + * param content1 HTML string, DOM element, array of elements, or jQuery object to insert before each element in the set of matched elements. + * param content2 One or more additional DOM elements, arrays of elements, HTML strings, or jQuery objects to insert before each element in the set of matched elements. + */ + before(content1: JQuery|any[]|Element|Text|string, ...content2: any[]): JQuery; + /** + * Insert content, specified by the parameter, before each element in the set of matched elements. + * + * param func A function that returns an HTML string, DOM element(s), or jQuery object to insert before each element in the set of matched elements. Receives the index position of the element in the set as an argument. Within the function, this refers to the current element in the set. + */ + before(func: (index: number, html: string) => string|Element|JQuery): JQuery; + + /** + * Create a deep copy of the set of matched elements. + * + * param withDataAndEvents A Boolean indicating whether event handlers and data should be copied along with the elements. The default value is false. + * param deepWithDataAndEvents A Boolean indicating whether event handlers and data for all children of the cloned element should be copied. By default its value matches the first argument's value (which defaults to false). + */ + clone(withDataAndEvents?: boolean, deepWithDataAndEvents?: boolean): JQuery; + + /** + * Remove the set of matched elements from the DOM. + * + * param selector A selector expression that filters the set of matched elements to be removed. + */ + detach(selector?: string): JQuery; + + /** + * Remove all child nodes of the set of matched elements from the DOM. + */ + empty(): JQuery; + + /** + * Insert every element in the set of matched elements after the target. + * + * param target A selector, element, array of elements, HTML string, or jQuery object; the matched set of elements will be inserted after the element(s) specified by this parameter. + */ + insertAfter(target: JQuery|any[]|Element|Text|string): JQuery; + + /** + * Insert every element in the set of matched elements before the target. + * + * param target A selector, element, array of elements, HTML string, or jQuery object; the matched set of elements will be inserted before the element(s) specified by this parameter. + */ + insertBefore(target: JQuery|any[]|Element|Text|string): JQuery; + + /** + * Insert content, specified by the parameter, to the beginning of each element in the set of matched elements. + * + * param content1 DOM element, array of elements, HTML string, or jQuery object to insert at the beginning of each element in the set of matched elements. + * param content2 One or more additional DOM elements, arrays of elements, HTML strings, or jQuery objects to insert at the beginning of each element in the set of matched elements. + */ + prepend(content1: JQuery|any[]|Element|Text|string, ...content2: any[]): JQuery; + /** + * Insert content, specified by the parameter, to the beginning of each element in the set of matched elements. + * + * param func A function that returns an HTML string, DOM element(s), or jQuery object to insert at the beginning of each element in the set of matched elements. Receives the index position of the element in the set and the old HTML value of the element as arguments. Within the function, this refers to the current element in the set. + */ + prepend(func: (index: number, html: string) => string|Element|JQuery): JQuery; + + /** + * Insert every element in the set of matched elements to the beginning of the target. + * + * @param target A selector, element, HTML string, array of elements, or jQuery object; the matched set of elements will be inserted at the beginning of the element(s) specified by this parameter. + */ + prependTo(target: JQuery|any[]|Element|string): JQuery; + + /** + * Remove the set of matched elements from the DOM. + * + * @param selector A selector expression that filters the set of matched elements to be removed. + */ + remove(selector?: string): JQuery; + + /** + * Replace each target element with the set of matched elements. + * + * @param target A selector string, jQuery object, DOM element, or array of elements indicating which element(s) to replace. + */ + replaceAll(target: JQuery|any[]|Element|string): JQuery; + + /** + * Replace each element in the set of matched elements with the provided new content and return the set of elements that was removed. + * + * param newContent The content to insert. May be an HTML string, DOM element, array of DOM elements, or jQuery object. + */ + replaceWith(newContent: JQuery|any[]|Element|Text|string): JQuery; + /** + * Replace each element in the set of matched elements with the provided new content and return the set of elements that was removed. + * + * param func A function that returns content with which to replace the set of matched elements. + */ + replaceWith(func: () => Element|JQuery): JQuery; + + /** + * Get the combined text contents of each element in the set of matched elements, including their descendants. + */ + text(): string; + /** + * Set the content of each element in the set of matched elements to the specified text. + * + * @param text The text to set as the content of each matched element. When Number or Boolean is supplied, it will be converted to a String representation. + */ + text(text: string|number|boolean): JQuery; + /** + * Set the content of each element in the set of matched elements to the specified text. + * + * @param func A function returning the text content to set. Receives the index position of the element in the set and the old text value as arguments. + */ + text(func: (index: number, text: string) => string): JQuery; + + /** + * Retrieve all the elements contained in the jQuery set, as an array. + */ + toArray(): any[]; + + /** + * Remove the parents of the set of matched elements from the DOM, leaving the matched elements in their place. + */ + unwrap(): JQuery; + + /** + * Wrap an HTML structure around each element in the set of matched elements. + * + * @param wrappingElement A selector, element, HTML string, or jQuery object specifying the structure to wrap around the matched elements. + */ + wrap(wrappingElement: JQuery|Element|string): JQuery; + /** + * Wrap an HTML structure around each element in the set of matched elements. + * + * @param func A callback function returning the HTML content or jQuery object to wrap around the matched elements. Receives the index position of the element in the set as an argument. Within the function, this refers to the current element in the set. + */ + wrap(func: (index: number) => string|JQuery): JQuery; + + /** + * Wrap an HTML structure around all elements in the set of matched elements. + * + * @param wrappingElement A selector, element, HTML string, or jQuery object specifying the structure to wrap around the matched elements. + */ + wrapAll(wrappingElement: JQuery|Element|string): JQuery; + wrapAll(func: (index: number) => string): JQuery; + + /** + * Wrap an HTML structure around the content of each element in the set of matched elements. + * + * @param wrappingElement An HTML snippet, selector expression, jQuery object, or DOM element specifying the structure to wrap around the content of the matched elements. + */ + wrapInner(wrappingElement: JQuery|Element|string): JQuery; + /** + * Wrap an HTML structure around the content of each element in the set of matched elements. + * + * @param func A callback function which generates a structure to wrap around the content of the matched elements. Receives the index position of the element in the set as an argument. Within the function, this refers to the current element in the set. + */ + wrapInner(func: (index: number) => string): JQuery; + + /** + * Iterate over a jQuery object, executing a function for each matched element. + * + * @param func A function to execute for each matched element. + */ + each(func: (index: number, elem: Element) => any): JQuery; + + /** + * Retrieve one of the elements matched by the jQuery object. + * + * @param index A zero-based integer indicating which element to retrieve. + */ + get(index: number): HTMLElement; + /** + * Retrieve the elements matched by the jQuery object. + */ + get(): any[]; + + /** + * Search for a given element from among the matched elements. + */ + index(): number; + /** + * Search for a given element from among the matched elements. + * + * @param selector A selector representing a jQuery collection in which to look for an element. + */ + index(selector: string|JQuery|Element): number; + + /** + * The number of elements in the jQuery object. + */ + length: number; + /** + * A selector representing selector passed to jQuery(), if any, when creating the original set. + * version deprecated: 1.7, removed: 1.9 + */ + selector: string; + [index: string]: any; + [index: number]: HTMLElement; + + /** + * Add elements to the set of matched elements. + * + * @param selector A string representing a selector expression to find additional elements to add to the set of matched elements. + * @param context The point in the document at which the selector should begin matching; similar to the context argument of the $(selector, context) method. + */ + add(selector: string, context?: Element): JQuery; + /** + * Add elements to the set of matched elements. + * + * @param elements One or more elements to add to the set of matched elements. + */ + add(...elements: Element[]): JQuery; + /** + * Add elements to the set of matched elements. + * + * @param html An HTML fragment to add to the set of matched elements. + */ + add(html: string): JQuery; + /** + * Add elements to the set of matched elements. + * + * @param obj An existing jQuery object to add to the set of matched elements. + */ + add(obj: JQuery): JQuery; + + /** + * Get the children of each element in the set of matched elements, optionally filtered by a selector. + * + * @param selector A string containing a selector expression to match elements against. + */ + children(selector?: string): JQuery; + + /** + * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. + * + * @param selector A string containing a selector expression to match elements against. + */ + closest(selector: string): JQuery; + /** + * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. + * + * @param selector A string containing a selector expression to match elements against. + * @param context A DOM element within which a matching element may be found. If no context is passed in then the context of the jQuery set will be used instead. + */ + closest(selector: string, context?: Element): JQuery; + /** + * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. + * + * @param obj A jQuery object to match elements against. + */ + closest(obj: JQuery): JQuery; + /** + * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. + * + * @param element An element to match elements against. + */ + closest(element: Element): JQuery; + + /** + * Get an array of all the elements and selectors matched against the current element up through the DOM tree. + * + * @param selectors An array or string containing a selector expression to match elements against (can also be a jQuery object). + * @param context A DOM element within which a matching element may be found. If no context is passed in then the context of the jQuery set will be used instead. + */ + closest(selectors: any, context?: Element): any[]; + + /** + * Get the children of each element in the set of matched elements, including text and comment nodes. + */ + contents(): JQuery; + + /** + * End the most recent filtering operation in the current chain and return the set of matched elements to its previous state. + */ + end(): JQuery; + + /** + * Reduce the set of matched elements to the one at the specified index. + * + * @param index An integer indicating the 0-based position of the element. OR An integer indicating the position of the element, counting backwards from the last element in the set. + * + */ + eq(index: number): JQuery; + + /** + * Reduce the set of matched elements to those that match the selector or pass the function's test. + * + * @param selector A string containing a selector expression to match the current set of elements against. + */ + filter(selector: string): JQuery; + /** + * Reduce the set of matched elements to those that match the selector or pass the function's test. + * + * @param func A function used as a test for each element in the set. this is the current DOM element. + */ + filter(func: (index: number, element: Element) => any): JQuery; + /** + * Reduce the set of matched elements to those that match the selector or pass the function's test. + * + * @param element An element to match the current set of elements against. + */ + filter(element: Element): JQuery; + /** + * Reduce the set of matched elements to those that match the selector or pass the function's test. + * + * @param obj An existing jQuery object to match the current set of elements against. + */ + filter(obj: JQuery): JQuery; + + /** + * Get the descendants of each element in the current set of matched elements, filtered by a selector, jQuery object, or element. + * + * @param selector A string containing a selector expression to match elements against. + */ + find(selector: string): JQuery; + /** + * Get the descendants of each element in the current set of matched elements, filtered by a selector, jQuery object, or element. + * + * @param element An element to match elements against. + */ + find(element: Element): JQuery; + /** + * Get the descendants of each element in the current set of matched elements, filtered by a selector, jQuery object, or element. + * + * @param obj A jQuery object to match elements against. + */ + find(obj: JQuery): JQuery; + + /** + * Reduce the set of matched elements to the first in the set. + */ + first(): JQuery; + + /** + * Reduce the set of matched elements to those that have a descendant that matches the selector or DOM element. + * + * @param selector A string containing a selector expression to match elements against. + */ + has(selector: string): JQuery; + /** + * Reduce the set of matched elements to those that have a descendant that matches the selector or DOM element. + * + * @param contained A DOM element to match elements against. + */ + has(contained: Element): JQuery; + + /** + * Check the current matched set of elements against a selector, element, or jQuery object and return true if at least one of these elements matches the given arguments. + * + * @param selector A string containing a selector expression to match elements against. + */ + is(selector: string): boolean; + /** + * Check the current matched set of elements against a selector, element, or jQuery object and return true if at least one of these elements matches the given arguments. + * + * @param func A function used as a test for the set of elements. It accepts one argument, index, which is the element's index in the jQuery collection.Within the function, this refers to the current DOM element. + */ + is(func: (index: number, element: Element) => boolean): boolean; + /** + * Check the current matched set of elements against a selector, element, or jQuery object and return true if at least one of these elements matches the given arguments. + * + * @param obj An existing jQuery object to match the current set of elements against. + */ + is(obj: JQuery): boolean; + /** + * Check the current matched set of elements against a selector, element, or jQuery object and return true if at least one of these elements matches the given arguments. + * + * @param elements One or more elements to match the current set of elements against. + */ + is(elements: any): boolean; + + /** + * Reduce the set of matched elements to the final one in the set. + */ + last(): JQuery; + + /** + * Pass each element in the current matched set through a function, producing a new jQuery object containing the return values. + * + * @param callback A function object that will be invoked for each element in the current set. + */ + map(callback: (index: number, domElement: Element) => any): JQuery; + + /** + * Get the immediately following sibling of each element in the set of matched elements. If a selector is provided, it retrieves the next sibling only if it matches that selector. + * + * @param selector A string containing a selector expression to match elements against. + */ + next(selector?: string): JQuery; + + /** + * Get all following siblings of each element in the set of matched elements, optionally filtered by a selector. + * + * @param selector A string containing a selector expression to match elements against. + */ + nextAll(selector?: string): JQuery; + + /** + * Get all following siblings of each element up to but not including the element matched by the selector, DOM node, or jQuery object passed. + * + * @param selector A string containing a selector expression to indicate where to stop matching following sibling elements. + * @param filter A string containing a selector expression to match elements against. + */ + nextUntil(selector?: string, filter?: string): JQuery; + /** + * Get all following siblings of each element up to but not including the element matched by the selector, DOM node, or jQuery object passed. + * + * @param element A DOM node or jQuery object indicating where to stop matching following sibling elements. + * @param filter A string containing a selector expression to match elements against. + */ + nextUntil(element?: Element, filter?: string): JQuery; + /** + * Get all following siblings of each element up to but not including the element matched by the selector, DOM node, or jQuery object passed. + * + * @param obj A DOM node or jQuery object indicating where to stop matching following sibling elements. + * @param filter A string containing a selector expression to match elements against. + */ + nextUntil(obj?: JQuery, filter?: string): JQuery; + + /** + * Remove elements from the set of matched elements. + * + * @param selector A string containing a selector expression to match elements against. + */ + not(selector: string): JQuery; + /** + * Remove elements from the set of matched elements. + * + * @param func A function used as a test for each element in the set. this is the current DOM element. + */ + not(func: (index: number, element: Element) => boolean): JQuery; + /** + * Remove elements from the set of matched elements. + * + * @param elements One or more DOM elements to remove from the matched set. + */ + not(elements: Element|Element[]): JQuery; + /** + * Remove elements from the set of matched elements. + * + * @param obj An existing jQuery object to match the current set of elements against. + */ + not(obj: JQuery): JQuery; + + /** + * Get the closest ancestor element that is positioned. + */ + offsetParent(): JQuery; + + /** + * Get the parent of each element in the current set of matched elements, optionally filtered by a selector. + * + * @param selector A string containing a selector expression to match elements against. + */ + parent(selector?: string): JQuery; + + /** + * Get the ancestors of each element in the current set of matched elements, optionally filtered by a selector. + * + * @param selector A string containing a selector expression to match elements against. + */ + parents(selector?: string): JQuery; + + /** + * Get the ancestors of each element in the current set of matched elements, up to but not including the element matched by the selector, DOM node, or jQuery object. + * + * @param selector A string containing a selector expression to indicate where to stop matching ancestor elements. + * @param filter A string containing a selector expression to match elements against. + */ + parentsUntil(selector?: string, filter?: string): JQuery; + /** + * Get the ancestors of each element in the current set of matched elements, up to but not including the element matched by the selector, DOM node, or jQuery object. + * + * @param element A DOM node or jQuery object indicating where to stop matching ancestor elements. + * @param filter A string containing a selector expression to match elements against. + */ + parentsUntil(element?: Element, filter?: string): JQuery; + /** + * Get the ancestors of each element in the current set of matched elements, up to but not including the element matched by the selector, DOM node, or jQuery object. + * + * @param obj A DOM node or jQuery object indicating where to stop matching ancestor elements. + * @param filter A string containing a selector expression to match elements against. + */ + parentsUntil(obj?: JQuery, filter?: string): JQuery; + + /** + * Get the immediately preceding sibling of each element in the set of matched elements, optionally filtered by a selector. + * + * @param selector A string containing a selector expression to match elements against. + */ + prev(selector?: string): JQuery; + + /** + * Get all preceding siblings of each element in the set of matched elements, optionally filtered by a selector. + * + * @param selector A string containing a selector expression to match elements against. + */ + prevAll(selector?: string): JQuery; + + /** + * Get all preceding siblings of each element up to but not including the element matched by the selector, DOM node, or jQuery object. + * + * @param selector A string containing a selector expression to indicate where to stop matching preceding sibling elements. + * @param filter A string containing a selector expression to match elements against. + */ + prevUntil(selector?: string, filter?: string): JQuery; + /** + * Get all preceding siblings of each element up to but not including the element matched by the selector, DOM node, or jQuery object. + * + * @param element A DOM node or jQuery object indicating where to stop matching preceding sibling elements. + * @param filter A string containing a selector expression to match elements against. + */ + prevUntil(element?: Element, filter?: string): JQuery; + /** + * Get all preceding siblings of each element up to but not including the element matched by the selector, DOM node, or jQuery object. + * + * @param obj A DOM node or jQuery object indicating where to stop matching preceding sibling elements. + * @param filter A string containing a selector expression to match elements against. + */ + prevUntil(obj?: JQuery, filter?: string): JQuery; + + /** + * Get the siblings of each element in the set of matched elements, optionally filtered by a selector. + * + * @param selector A string containing a selector expression to match elements against. + */ + siblings(selector?: string): JQuery; + + /** + * Reduce the set of matched elements to a subset specified by a range of indices. + * + * @param start An integer indicating the 0-based position at which the elements begin to be selected. If negative, it indicates an offset from the end of the set. + * @param end An integer indicating the 0-based position at which the elements stop being selected. If negative, it indicates an offset from the end of the set. If omitted, the range continues until the end of the set. + */ + slice(start: number, end?: number): JQuery; + + /** + * Show the queue of functions to be executed on the matched elements. + * + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + */ + queue(queueName?: string): any[]; + /** + * Manipulate the queue of functions to be executed, once for each matched element. + * + * @param newQueue An array of functions to replace the current queue contents. + */ + queue(newQueue: Function[]): JQuery; + /** + * Manipulate the queue of functions to be executed, once for each matched element. + * + * @param callback The new function to add to the queue, with a function to call that will dequeue the next item. + */ + queue(callback: Function): JQuery; + /** + * Manipulate the queue of functions to be executed, once for each matched element. + * + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + * @param newQueue An array of functions to replace the current queue contents. + */ + queue(queueName: string, newQueue: Function[]): JQuery; + /** + * Manipulate the queue of functions to be executed, once for each matched element. + * + * @param queueName A string containing the name of the queue. Defaults to fx, the standard effects queue. + * @param callback The new function to add to the queue, with a function to call that will dequeue the next item. + */ + queue(queueName: string, callback: Function): JQuery; +} +declare module "jquery" { + export = $; +} +declare var jQuery: JQueryStatic; +declare var $: JQueryStatic; diff --git a/js/jquery.form.d.ts b/js/jquery.form.d.ts new file mode 100644 index 0000000..2f8c47b --- /dev/null +++ b/js/jquery.form.d.ts @@ -0,0 +1,7 @@ +/// <reference path="jquery.d.ts" /> + +interface JQuery { + //These method are added by the jquery-form plugin. + ajaxForm: (options: any) => JQuery; + resetForm: () => JQuery; +} \ No newline at end of file diff --git a/js/jquery.form.js b/js/jquery.form.js new file mode 100644 index 0000000..168d4b8 --- /dev/null +++ b/js/jquery.form.js @@ -0,0 +1,1540 @@ +/*! + * jQuery Form Plugin + * version: 4.3.0 + * Requires jQuery v1.7.2 or later + * Project repository: https://github.com/jquery-form/form + + * Copyright 2017 Kevin Morris + * Copyright 2006 M. Alsup + + * Dual licensed under the LGPL-2.1+ or MIT licenses + * https://github.com/jquery-form/form#license + + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + */ +/* global ActiveXObject */ + +/* eslint-disable */ +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof module === 'object' && module.exports) { + // Node/CommonJS + module.exports = function( root, jQuery ) { + if (typeof jQuery === 'undefined') { + // require('jQuery') returns a factory that requires window to build a jQuery instance, we normalize how we use modules + // that require this pattern but the window provided is a noop if it's defined (how jquery works) + if (typeof window !== 'undefined') { + jQuery = require('jquery'); + } + else { + jQuery = require('jquery')(root); + } + } + factory(jQuery); + return jQuery; + }; + } else { + // Browser globals + factory(jQuery); + } + +}(function ($) { +/* eslint-enable */ + 'use strict'; + + /* + Usage Note: + ----------- + Do not use both ajaxSubmit and ajaxForm on the same form. These + functions are mutually exclusive. Use ajaxSubmit if you want + to bind your own submit handler to the form. For example, + + $(document).ready(function() { + $('#myForm').on('submit', function(e) { + e.preventDefault(); // <-- important + $(this).ajaxSubmit({ + target: '#output' + }); + }); + }); + + Use ajaxForm when you want the plugin to manage all the event binding + for you. For example, + + $(document).ready(function() { + $('#myForm').ajaxForm({ + target: '#output' + }); + }); + + You can also use ajaxForm with delegation (requires jQuery v1.7+), so the + form does not have to exist when you invoke ajaxForm: + + $('#myForm').ajaxForm({ + delegation: true, + target: '#output' + }); + + When using ajaxForm, the ajaxSubmit function will be invoked for you + at the appropriate time. + */ + + var rCRLF = /\r?\n/g; + + /** + * Feature detection + */ + var feature = {}; + + feature.fileapi = $('<input type="file">').get(0).files !== undefined; + feature.formdata = (typeof window.FormData !== 'undefined'); + + var hasProp = !!$.fn.prop; + + // attr2 uses prop when it can but checks the return type for + // an expected string. This accounts for the case where a form + // contains inputs with names like "action" or "method"; in those + // cases "prop" returns the element + $.fn.attr2 = function() { + if (!hasProp) { + return this.attr.apply(this, arguments); + } + + var val = this.prop.apply(this, arguments); + + if ((val && val.jquery) || typeof val === 'string') { + return val; + } + + return this.attr.apply(this, arguments); + }; + + /** + * ajaxSubmit() provides a mechanism for immediately submitting + * an HTML form using AJAX. + * + * @param {object|string} options jquery.form.js parameters or custom url for submission + * @param {object} data extraData + * @param {string} dataType ajax dataType + * @param {function} onSuccess ajax success callback function + */ + $.fn.ajaxSubmit = function(options, data, dataType, onSuccess) { + // fast fail if nothing selected (http://dev.jquery.com/ticket/2752) + if (!this.length) { + log('ajaxSubmit: skipping submit process - no element selected'); + + return this; + } + + /* eslint consistent-this: ["error", "$form"] */ + var method, action, url, isMsie, iframeSrc, $form = this; + + if (typeof options === 'function') { + options = {success: options}; + + } else if (typeof options === 'string' || (options === false && arguments.length > 0)) { + options = { + 'url' : options, + 'data' : data, + 'dataType' : dataType + }; + + if (typeof onSuccess === 'function') { + options.success = onSuccess; + } + + } else if (typeof options === 'undefined') { + options = {}; + } + + method = options.method || options.type || this.attr2('method'); + action = options.url || this.attr2('action'); + + url = (typeof action === 'string') ? $.trim(action) : ''; + url = url || window.location.href || ''; + if (url) { + // clean url (don't include hash vaue) + url = (url.match(/^([^#]+)/) || [])[1]; + } + // IE requires javascript:false in https, but this breaks chrome >83 and goes against spec. + // Instead of using javascript:false always, let's only apply it for IE. + isMsie = /(MSIE|Trident)/.test(navigator.userAgent || ''); + iframeSrc = (isMsie && /^https/i.test(window.location.href || '')) ? 'javascript:false' : 'about:blank'; // eslint-disable-line no-script-url + + options = $.extend(true, { + url : url, + success : $.ajaxSettings.success, + type : method || $.ajaxSettings.type, + iframeSrc : iframeSrc + }, options); + + // hook for manipulating the form data before it is extracted; + // convenient for use with rich editors like tinyMCE or FCKEditor + var veto = {}; + + this.trigger('form-pre-serialize', [this, options, veto]); + + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-pre-serialize trigger'); + + return this; + } + + // provide opportunity to alter form data before it is serialized + if (options.beforeSerialize && options.beforeSerialize(this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSerialize callback'); + + return this; + } + + var traditional = options.traditional; + + if (typeof traditional === 'undefined') { + traditional = $.ajaxSettings.traditional; + } + + var elements = []; + var qx, a = this.formToArray(options.semantic, elements, options.filtering); + + if (options.data) { + var optionsData = $.isFunction(options.data) ? options.data(a) : options.data; + + options.extraData = optionsData; + qx = $.param(optionsData, traditional); + } + + // give pre-submit callback an opportunity to abort the submit + if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) { + log('ajaxSubmit: submit aborted via beforeSubmit callback'); + + return this; + } + + // fire vetoable 'validate' event + this.trigger('form-submit-validate', [a, this, options, veto]); + if (veto.veto) { + log('ajaxSubmit: submit vetoed via form-submit-validate trigger'); + + return this; + } + + var q = $.param(a, traditional); + + if (qx) { + q = (q ? (q + '&' + qx) : qx); + } + + if (options.type.toUpperCase() === 'GET') { + options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q; + options.data = null; // data is null for 'get' + } else { + options.data = q; // data is the query string for 'post' + } + + var callbacks = []; + + if (options.resetForm) { + callbacks.push(function() { + $form.resetForm(); + }); + } + + if (options.clearForm) { + callbacks.push(function() { + $form.clearForm(options.includeHidden); + }); + } + + // perform a load on the target only if dataType is not provided + if (!options.dataType && options.target) { + var oldSuccess = options.success || function(){}; + + callbacks.push(function(data, textStatus, jqXHR) { + var successArguments = arguments, + fn = options.replaceTarget ? 'replaceWith' : 'html'; + + $(options.target)[fn](data).each(function(){ + oldSuccess.apply(this, successArguments); + }); + }); + + } else if (options.success) { + if ($.isArray(options.success)) { + $.merge(callbacks, options.success); + } else { + callbacks.push(options.success); + } + } + + options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg + var context = options.context || this; // jQuery 1.4+ supports scope context + + for (var i = 0, max = callbacks.length; i < max; i++) { + callbacks[i].apply(context, [data, status, xhr || $form, $form]); + } + }; + + if (options.error) { + var oldError = options.error; + + options.error = function(xhr, status, error) { + var context = options.context || this; + + oldError.apply(context, [xhr, status, error, $form]); + }; + } + + if (options.complete) { + var oldComplete = options.complete; + + options.complete = function(xhr, status) { + var context = options.context || this; + + oldComplete.apply(context, [xhr, status, $form]); + }; + } + + // are there files to upload? + + // [value] (issue #113), also see comment: + // https://github.com/malsup/form/commit/588306aedba1de01388032d5f42a60159eea9228#commitcomment-2180219 + var fileInputs = $('input[type=file]:enabled', this).filter(function() { + return $(this).val() !== ''; + }); + var hasFileInputs = fileInputs.length > 0; + var mp = 'multipart/form-data'; + var multipart = ($form.attr('enctype') === mp || $form.attr('encoding') === mp); + var fileAPI = feature.fileapi && feature.formdata; + + log('fileAPI :' + fileAPI); + + var shouldUseFrame = (hasFileInputs || multipart) && !fileAPI; + var jqxhr; + + // options.iframe allows user to force iframe mode + // 06-NOV-09: now defaulting to iframe mode if file input is detected + if (options.iframe !== false && (options.iframe || shouldUseFrame)) { + // hack to fix Safari hang (thanks to Tim Molendijk for this) + // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d + if (options.closeKeepAlive) { + $.get(options.closeKeepAlive, function() { + jqxhr = fileUploadIframe(a); + }); + + } else { + jqxhr = fileUploadIframe(a); + } + + } else if ((hasFileInputs || multipart) && fileAPI) { + jqxhr = fileUploadXhr(a); + + } else { + jqxhr = $.ajax(options); + } + + $form.removeData('jqxhr').data('jqxhr', jqxhr); + + // clear element array + for (var k = 0; k < elements.length; k++) { + elements[k] = null; + } + + // fire 'notify' event + this.trigger('form-submit-notify', [this, options]); + + return this; + + // utility fn for deep serialization + function deepSerialize(extraData) { + var serialized = $.param(extraData, options.traditional).split('&'); + var len = serialized.length; + var result = []; + var i, part; + + for (i = 0; i < len; i++) { + // #252; undo param space replacement + serialized[i] = serialized[i].replace(/\+/g, ' '); + part = serialized[i].split('='); + // #278; use array instead of object storage, favoring array serializations + result.push([decodeURIComponent(part[0]), decodeURIComponent(part[1])]); + } + + return result; + } + + // XMLHttpRequest Level 2 file uploads (big hat tip to francois2metz) + function fileUploadXhr(a) { + var formdata = new FormData(); + + for (var i = 0; i < a.length; i++) { + formdata.append(a[i].name, a[i].value); + } + + if (options.extraData) { + var serializedData = deepSerialize(options.extraData); + + for (i = 0; i < serializedData.length; i++) { + if (serializedData[i]) { + formdata.append(serializedData[i][0], serializedData[i][1]); + } + } + } + + options.data = null; + + var s = $.extend(true, {}, $.ajaxSettings, options, { + contentType : false, + processData : false, + cache : false, + type : method || 'POST' + }); + + if (options.uploadProgress) { + // workaround because jqXHR does not expose upload property + s.xhr = function() { + var xhr = $.ajaxSettings.xhr(); + + if (xhr.upload) { + xhr.upload.addEventListener('progress', function(event) { + var percent = 0; + var position = event.loaded || event.position; /* event.position is deprecated */ + var total = event.total; + + if (event.lengthComputable) { + percent = Math.ceil(position / total * 100); + } + + options.uploadProgress(event, position, total, percent); + }, false); + } + + return xhr; + }; + } + + s.data = null; + + var beforeSend = s.beforeSend; + + s.beforeSend = function(xhr, o) { + // Send FormData() provided by user + if (options.formData) { + o.data = options.formData; + } else { + o.data = formdata; + } + + if (beforeSend) { + beforeSend.call(this, xhr, o); + } + }; + + return $.ajax(s); + } + + // private function for handling file uploads (hat tip to YAHOO!) + function fileUploadIframe(a) { + var form = $form[0], el, i, s, g, id, $io, io, xhr, sub, n, timedOut, timeoutHandle; + var deferred = $.Deferred(); + + // #341 + deferred.abort = function(status) { + xhr.abort(status); + }; + + if (a) { + // ensure that every serialized input is still enabled + for (i = 0; i < elements.length; i++) { + el = $(elements[i]); + if (hasProp) { + el.prop('disabled', false); + } else { + el.removeAttr('disabled'); + } + } + } + + s = $.extend(true, {}, $.ajaxSettings, options); + s.context = s.context || s; + id = 'jqFormIO' + new Date().getTime(); + var ownerDocument = form.ownerDocument; + var $body = $form.closest('body'); + + if (s.iframeTarget) { + $io = $(s.iframeTarget, ownerDocument); + n = $io.attr2('name'); + if (!n) { + $io.attr2('name', id); + } else { + id = n; + } + + } else { + $io = $('<iframe name="' + id + '" src="' + s.iframeSrc + '" />', ownerDocument); + $io.css({position: 'absolute', top: '-1000px', left: '-1000px'}); + } + io = $io[0]; + + + xhr = { // mock object + aborted : 0, + responseText : null, + responseXML : null, + status : 0, + statusText : 'n/a', + getAllResponseHeaders : function() {}, + getResponseHeader : function() {}, + setRequestHeader : function() {}, + abort : function(status) { + var e = (status === 'timeout' ? 'timeout' : 'aborted'); + + log('aborting upload... ' + e); + this.aborted = 1; + + try { // #214, #257 + if (io.contentWindow.document.execCommand) { + io.contentWindow.document.execCommand('Stop'); + } + } catch (ignore) {} + + $io.attr('src', s.iframeSrc); // abort op in progress + xhr.error = e; + if (s.error) { + s.error.call(s.context, xhr, e, status); + } + + if (g) { + $.event.trigger('ajaxError', [xhr, s, e]); + } + + if (s.complete) { + s.complete.call(s.context, xhr, e); + } + } + }; + + g = s.global; + // trigger ajax global events so that activity/block indicators work like normal + if (g && $.active++ === 0) { + $.event.trigger('ajaxStart'); + } + if (g) { + $.event.trigger('ajaxSend', [xhr, s]); + } + + if (s.beforeSend && s.beforeSend.call(s.context, xhr, s) === false) { + if (s.global) { + $.active--; + } + deferred.reject(); + + return deferred; + } + + if (xhr.aborted) { + deferred.reject(); + + return deferred; + } + + // add submitting element to data if we know it + sub = form.clk; + if (sub) { + n = sub.name; + if (n && !sub.disabled) { + s.extraData = s.extraData || {}; + s.extraData[n] = sub.value; + if (sub.type === 'image') { + s.extraData[n + '.x'] = form.clk_x; + s.extraData[n + '.y'] = form.clk_y; + } + } + } + + var CLIENT_TIMEOUT_ABORT = 1; + var SERVER_ABORT = 2; + + function getDoc(frame) { + /* it looks like contentWindow or contentDocument do not + * carry the protocol property in ie8, when running under ssl + * frame.document is the only valid response document, since + * the protocol is know but not on the other two objects. strange? + * "Same origin policy" http://en.wikipedia.org/wiki/Same_origin_policy + */ + + var doc = null; + + // IE8 cascading access check + try { + if (frame.contentWindow) { + doc = frame.contentWindow.document; + } + } catch (err) { + // IE8 access denied under ssl & missing protocol + log('cannot get iframe.contentWindow document: ' + err); + } + + if (doc) { // successful getting content + return doc; + } + + try { // simply checking may throw in ie8 under ssl or mismatched protocol + doc = frame.contentDocument ? frame.contentDocument : frame.document; + } catch (err) { + // last attempt + log('cannot get iframe.contentDocument: ' + err); + doc = frame.document; + } + + return doc; + } + + // Rails CSRF hack (thanks to Yvan Barthelemy) + var csrf_token = $('meta[name=csrf-token]').attr('content'); + var csrf_param = $('meta[name=csrf-param]').attr('content'); + + if (csrf_param && csrf_token) { + s.extraData = s.extraData || {}; + s.extraData[csrf_param] = csrf_token; + } + + // take a breath so that pending repaints get some cpu time before the upload starts + function doSubmit() { + // make sure form attrs are set + var t = $form.attr2('target'), + a = $form.attr2('action'), + mp = 'multipart/form-data', + et = $form.attr('enctype') || $form.attr('encoding') || mp; + + // update form attrs in IE friendly way + form.setAttribute('target', id); + if (!method || /post/i.test(method)) { + form.setAttribute('method', 'POST'); + } + if (a !== s.url) { + form.setAttribute('action', s.url); + } + + // ie borks in some cases when setting encoding + if (!s.skipEncodingOverride && (!method || /post/i.test(method))) { + $form.attr({ + encoding : 'multipart/form-data', + enctype : 'multipart/form-data' + }); + } + + // support timout + if (s.timeout) { + timeoutHandle = setTimeout(function() { + timedOut = true; cb(CLIENT_TIMEOUT_ABORT); + }, s.timeout); + } + + // look for server aborts + function checkState() { + try { + var state = getDoc(io).readyState; + + log('state = ' + state); + if (state && state.toLowerCase() === 'uninitialized') { + setTimeout(checkState, 50); + } + + } catch (e) { + log('Server abort: ', e, ' (', e.name, ')'); + cb(SERVER_ABORT); // eslint-disable-line callback-return + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + timeoutHandle = undefined; + } + } + + // add "extra" data to form if provided in options + var extraInputs = []; + + try { + if (s.extraData) { + for (var n in s.extraData) { + if (s.extraData.hasOwnProperty(n)) { + // if using the $.param format that allows for multiple values with the same name + if ($.isPlainObject(s.extraData[n]) && s.extraData[n].hasOwnProperty('name') && s.extraData[n].hasOwnProperty('value')) { + extraInputs.push( + $('<input type="hidden" name="' + s.extraData[n].name + '">', ownerDocument).val(s.extraData[n].value) + .appendTo(form)[0]); + } else { + extraInputs.push( + $('<input type="hidden" name="' + n + '">', ownerDocument).val(s.extraData[n]) + .appendTo(form)[0]); + } + } + } + } + + if (!s.iframeTarget) { + // add iframe to doc and submit the form + $io.appendTo($body); + } + + if (io.attachEvent) { + io.attachEvent('onload', cb); + } else { + io.addEventListener('load', cb, false); + } + + setTimeout(checkState, 15); + + try { + form.submit(); + + } catch (err) { + // just in case form has element with name/id of 'submit' + var submitFn = document.createElement('form').submit; + + submitFn.apply(form); + } + + } finally { + // reset attrs and remove "extra" input elements + form.setAttribute('action', a); + form.setAttribute('enctype', et); // #380 + if (t) { + form.setAttribute('target', t); + } else { + $form.removeAttr('target'); + } + $(extraInputs).remove(); + } + } + + if (s.forceSync) { + doSubmit(); + } else { + setTimeout(doSubmit, 10); // this lets dom updates render + } + + var data, doc, domCheckCount = 50, callbackProcessed; + + function cb(e) { + if (xhr.aborted || callbackProcessed) { + return; + } + + doc = getDoc(io); + if (!doc) { + log('cannot access response document'); + e = SERVER_ABORT; + } + if (e === CLIENT_TIMEOUT_ABORT && xhr) { + xhr.abort('timeout'); + deferred.reject(xhr, 'timeout'); + + return; + + } + if (e === SERVER_ABORT && xhr) { + xhr.abort('server abort'); + deferred.reject(xhr, 'error', 'server abort'); + + return; + } + + if (!doc || doc.location.href === s.iframeSrc) { + // response not received yet + if (!timedOut) { + return; + } + } + + if (io.detachEvent) { + io.detachEvent('onload', cb); + } else { + io.removeEventListener('load', cb, false); + } + + var status = 'success', errMsg; + + try { + if (timedOut) { + throw 'timeout'; + } + + var isXml = s.dataType === 'xml' || doc.XMLDocument || $.isXMLDoc(doc); + + log('isXml=' + isXml); + + if (!isXml && window.opera && (doc.body === null || !doc.body.innerHTML)) { + if (--domCheckCount) { + // in some browsers (Opera) the iframe DOM is not always traversable when + // the onload callback fires, so we loop a bit to accommodate + log('requeing onLoad callback, DOM not available'); + setTimeout(cb, 250); + + return; + } + // let this fall through because server response could be an empty document + // log('Could not access iframe DOM after mutiple tries.'); + // throw 'DOMException: not available'; + } + + // log('response detected'); + var docRoot = doc.body ? doc.body : doc.documentElement; + + xhr.responseText = docRoot ? docRoot.innerHTML : null; + xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc; + if (isXml) { + s.dataType = 'xml'; + } + xhr.getResponseHeader = function(header){ + var headers = {'content-type': s.dataType}; + + return headers[header.toLowerCase()]; + }; + // support for XHR 'status' & 'statusText' emulation : + if (docRoot) { + xhr.status = Number(docRoot.getAttribute('status')) || xhr.status; + xhr.statusText = docRoot.getAttribute('statusText') || xhr.statusText; + } + + var dt = (s.dataType || '').toLowerCase(); + var scr = /(json|script|text)/.test(dt); + + if (scr || s.textarea) { + // see if user embedded response in textarea + var ta = doc.getElementsByTagName('textarea')[0]; + + if (ta) { + xhr.responseText = ta.value; + // support for XHR 'status' & 'statusText' emulation : + xhr.status = Number(ta.getAttribute('status')) || xhr.status; + xhr.statusText = ta.getAttribute('statusText') || xhr.statusText; + + } else if (scr) { + // account for browsers injecting pre around json response + var pre = doc.getElementsByTagName('pre')[0]; + var b = doc.getElementsByTagName('body')[0]; + + if (pre) { + xhr.responseText = pre.textContent ? pre.textContent : pre.innerText; + } else if (b) { + xhr.responseText = b.textContent ? b.textContent : b.innerText; + } + } + + } else if (dt === 'xml' && !xhr.responseXML && xhr.responseText) { + xhr.responseXML = toXml(xhr.responseText); // eslint-disable-line no-use-before-define + } + + try { + data = httpData(xhr, dt, s); // eslint-disable-line no-use-before-define + + } catch (err) { + status = 'parsererror'; + xhr.error = errMsg = (err || status); + } + + } catch (err) { + log('error caught: ', err); + status = 'error'; + xhr.error = errMsg = (err || status); + } + + if (xhr.aborted) { + log('upload aborted'); + status = null; + } + + if (xhr.status) { // we've set xhr.status + status = ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) ? 'success' : 'error'; + } + + // ordering of these callbacks/triggers is odd, but that's how $.ajax does it + if (status === 'success') { + if (s.success) { + s.success.call(s.context, data, 'success', xhr); + } + + deferred.resolve(xhr.responseText, 'success', xhr); + + if (g) { + $.event.trigger('ajaxSuccess', [xhr, s]); + } + + } else if (status) { + if (typeof errMsg === 'undefined') { + errMsg = xhr.statusText; + } + if (s.error) { + s.error.call(s.context, xhr, status, errMsg); + } + deferred.reject(xhr, 'error', errMsg); + if (g) { + $.event.trigger('ajaxError', [xhr, s, errMsg]); + } + } + + if (g) { + $.event.trigger('ajaxComplete', [xhr, s]); + } + + if (g && !--$.active) { + $.event.trigger('ajaxStop'); + } + + if (s.complete) { + s.complete.call(s.context, xhr, status); + } + + callbackProcessed = true; + if (s.timeout) { + clearTimeout(timeoutHandle); + } + + // clean up + setTimeout(function() { + if (!s.iframeTarget) { + $io.remove(); + } else { // adding else to clean up existing iframe response. + $io.attr('src', s.iframeSrc); + } + xhr.responseXML = null; + }, 100); + } + + var toXml = $.parseXML || function(s, doc) { // use parseXML if available (jQuery 1.5+) + if (window.ActiveXObject) { + doc = new ActiveXObject('Microsoft.XMLDOM'); + doc.async = 'false'; + doc.loadXML(s); + + } else { + doc = (new DOMParser()).parseFromString(s, 'text/xml'); + } + + return (doc && doc.documentElement && doc.documentElement.nodeName !== 'parsererror') ? doc : null; + }; + var parseJSON = $.parseJSON || function(s) { + /* jslint evil:true */ + return window['eval']('(' + s + ')'); // eslint-disable-line dot-notation + }; + + var httpData = function(xhr, type, s) { // mostly lifted from jq1.4.4 + + var ct = xhr.getResponseHeader('content-type') || '', + xml = ((type === 'xml' || !type) && ct.indexOf('xml') >= 0), + data = xml ? xhr.responseXML : xhr.responseText; + + if (xml && data.documentElement.nodeName === 'parsererror') { + if ($.error) { + $.error('parsererror'); + } + } + if (s && s.dataFilter) { + data = s.dataFilter(data, type); + } + if (typeof data === 'string') { + if ((type === 'json' || !type) && ct.indexOf('json') >= 0) { + data = parseJSON(data); + } else if ((type === 'script' || !type) && ct.indexOf('javascript') >= 0) { + $.globalEval(data); + } + } + + return data; + }; + + return deferred; + } + }; + + /** + * ajaxForm() provides a mechanism for fully automating form submission. + * + * The advantages of using this method instead of ajaxSubmit() are: + * + * 1: This method will include coordinates for <input type="image"> elements (if the element + * is used to submit the form). + * 2. This method will include the submit element's name/value data (for the element that was + * used to submit the form). + * 3. This method binds the submit() method to the form for you. + * + * The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely + * passes the options argument along after properly binding events for submit elements and + * the form itself. + */ + $.fn.ajaxForm = function(options, data, dataType, onSuccess) { + if (typeof options === 'string' || (options === false && arguments.length > 0)) { + options = { + 'url' : options, + 'data' : data, + 'dataType' : dataType + }; + + if (typeof onSuccess === 'function') { + options.success = onSuccess; + } + } + + options = options || {}; + options.delegation = options.delegation && $.isFunction($.fn.on); + + // in jQuery 1.3+ we can fix mistakes with the ready state + if (!options.delegation && this.length === 0) { + var o = {s: this.selector, c: this.context}; + + if (!$.isReady && o.s) { + log('DOM not ready, queuing ajaxForm'); + $(function() { + $(o.s, o.c).ajaxForm(options); + }); + + return this; + } + + // is your DOM ready? http://docs.jquery.com/Tutorials:Introducing_$(document).ready() + log('terminating; zero elements found by selector' + ($.isReady ? '' : ' (DOM not ready)')); + + return this; + } + + if (options.delegation) { + $(document) + .off('submit.form-plugin', this.selector, doAjaxSubmit) + .off('click.form-plugin', this.selector, captureSubmittingElement) + .on('submit.form-plugin', this.selector, options, doAjaxSubmit) + .on('click.form-plugin', this.selector, options, captureSubmittingElement); + + return this; + } + + if (options.beforeFormUnbind) { + options.beforeFormUnbind(this, options); + } + + return this.ajaxFormUnbind() + .on('submit.form-plugin', options, doAjaxSubmit) + .on('click.form-plugin', options, captureSubmittingElement); + }; + + // private event handlers + function doAjaxSubmit(e) { + /* jshint validthis:true */ + var options = e.data; + + if (!e.isDefaultPrevented()) { // if event has been canceled, don't proceed + e.preventDefault(); + $(e.target).closest('form').ajaxSubmit(options); // #365 + } + } + + function captureSubmittingElement(e) { + /* jshint validthis:true */ + var target = e.target; + var $el = $(target); + + if (!$el.is('[type=submit],[type=image]')) { + // is this a child element of the submit el? (ex: a span within a button) + var t = $el.closest('[type=submit]'); + + if (t.length === 0) { + return; + } + target = t[0]; + } + + var form = target.form; + + form.clk = target; + + if (target.type === 'image') { + if (typeof e.offsetX !== 'undefined') { + form.clk_x = e.offsetX; + form.clk_y = e.offsetY; + + } else if (typeof $.fn.offset === 'function') { + var offset = $el.offset(); + + form.clk_x = e.pageX - offset.left; + form.clk_y = e.pageY - offset.top; + + } else { + form.clk_x = e.pageX - target.offsetLeft; + form.clk_y = e.pageY - target.offsetTop; + } + } + // clear form vars + setTimeout(function() { + form.clk = form.clk_x = form.clk_y = null; + }, 100); + } + + + // ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm + $.fn.ajaxFormUnbind = function() { + return this.off('submit.form-plugin click.form-plugin'); + }; + + /** + * formToArray() gathers form element data into an array of objects that can + * be passed to any of the following ajax functions: $.get, $.post, or load. + * Each object in the array has both a 'name' and 'value' property. An example of + * an array for a simple login form might be: + * + * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ] + * + * It is this array that is passed to pre-submit callback functions provided to the + * ajaxSubmit() and ajaxForm() methods. + */ + $.fn.formToArray = function(semantic, elements, filtering) { + var a = []; + + if (this.length === 0) { + return a; + } + + var form = this[0]; + var formId = this.attr('id'); + var els = (semantic || typeof form.elements === 'undefined') ? form.getElementsByTagName('*') : form.elements; + var els2; + + if (els) { + els = $.makeArray(els); // convert to standard array + } + + // #386; account for inputs outside the form which use the 'form' attribute + // FinesseRus: in non-IE browsers outside fields are already included in form.elements. + if (formId && (semantic || /(Edge|Trident)\//.test(navigator.userAgent))) { + els2 = $(':input[form="' + formId + '"]').get(); // hat tip @thet + if (els2.length) { + els = (els || []).concat(els2); + } + } + + if (!els || !els.length) { + return a; + } + + if ($.isFunction(filtering)) { + els = $.map(els, filtering); + } + + var i, j, n, v, el, max, jmax; + + for (i = 0, max = els.length; i < max; i++) { + el = els[i]; + n = el.name; + if (!n || el.disabled) { + continue; + } + + if (semantic && form.clk && el.type === 'image') { + // handle image inputs on the fly when semantic == true + if (form.clk === el) { + a.push({name: n, value: $(el).val(), type: el.type}); + a.push({name: n + '.x', value: form.clk_x}, {name: n + '.y', value: form.clk_y}); + } + continue; + } + + v = $.fieldValue(el, true); + if (v && v.constructor === Array) { + if (elements) { + elements.push(el); + } + for (j = 0, jmax = v.length; j < jmax; j++) { + a.push({name: n, value: v[j]}); + } + + } else if (feature.fileapi && el.type === 'file') { + if (elements) { + elements.push(el); + } + + var files = el.files; + + if (files.length) { + for (j = 0; j < files.length; j++) { + a.push({name: n, value: files[j], type: el.type}); + } + } else { + // #180 + a.push({name: n, value: '', type: el.type}); + } + + } else if (v !== null && typeof v !== 'undefined') { + if (elements) { + elements.push(el); + } + a.push({name: n, value: v, type: el.type, required: el.required}); + } + } + + if (!semantic && form.clk) { + // input type=='image' are not found in elements array! handle it here + var $input = $(form.clk), input = $input[0]; + + n = input.name; + + if (n && !input.disabled && input.type === 'image') { + a.push({name: n, value: $input.val()}); + a.push({name: n + '.x', value: form.clk_x}, {name: n + '.y', value: form.clk_y}); + } + } + + return a; + }; + + /** + * Serializes form data into a 'submittable' string. This method will return a string + * in the format: name1=value1&name2=value2 + */ + $.fn.formSerialize = function(semantic) { + // hand off to jQuery.param for proper encoding + return $.param(this.formToArray(semantic)); + }; + + /** + * Serializes all field elements in the jQuery object into a query string. + * This method will return a string in the format: name1=value1&name2=value2 + */ + $.fn.fieldSerialize = function(successful) { + var a = []; + + this.each(function() { + var n = this.name; + + if (!n) { + return; + } + + var v = $.fieldValue(this, successful); + + if (v && v.constructor === Array) { + for (var i = 0, max = v.length; i < max; i++) { + a.push({name: n, value: v[i]}); + } + + } else if (v !== null && typeof v !== 'undefined') { + a.push({name: this.name, value: v}); + } + }); + + // hand off to jQuery.param for proper encoding + return $.param(a); + }; + + /** + * Returns the value(s) of the element in the matched set. For example, consider the following form: + * + * <form><fieldset> + * <input name="A" type="text"> + * <input name="A" type="text"> + * <input name="B" type="checkbox" value="B1"> + * <input name="B" type="checkbox" value="B2"> + * <input name="C" type="radio" value="C1"> + * <input name="C" type="radio" value="C2"> + * </fieldset></form> + * + * var v = $('input[type=text]').fieldValue(); + * // if no values are entered into the text inputs + * v === ['',''] + * // if values entered into the text inputs are 'foo' and 'bar' + * v === ['foo','bar'] + * + * var v = $('input[type=checkbox]').fieldValue(); + * // if neither checkbox is checked + * v === undefined + * // if both checkboxes are checked + * v === ['B1', 'B2'] + * + * var v = $('input[type=radio]').fieldValue(); + * // if neither radio is checked + * v === undefined + * // if first radio is checked + * v === ['C1'] + * + * The successful argument controls whether or not the field element must be 'successful' + * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls). + * The default value of the successful argument is true. If this value is false the value(s) + * for each element is returned. + * + * Note: This method *always* returns an array. If no valid value can be determined the + * array will be empty, otherwise it will contain one or more values. + */ + $.fn.fieldValue = function(successful) { + for (var val = [], i = 0, max = this.length; i < max; i++) { + var el = this[i]; + var v = $.fieldValue(el, successful); + + if (v === null || typeof v === 'undefined' || (v.constructor === Array && !v.length)) { + continue; + } + + if (v.constructor === Array) { + $.merge(val, v); + } else { + val.push(v); + } + } + + return val; + }; + + /** + * Returns the value of the field element. + */ + $.fieldValue = function(el, successful) { + var n = el.name, t = el.type, tag = el.tagName.toLowerCase(); + + if (typeof successful === 'undefined') { + successful = true; + } + + /* eslint-disable no-mixed-operators */ + if (successful && (!n || el.disabled || t === 'reset' || t === 'button' || + (t === 'checkbox' || t === 'radio') && !el.checked || + (t === 'submit' || t === 'image') && el.form && el.form.clk !== el || + tag === 'select' && el.selectedIndex === -1)) { + /* eslint-enable no-mixed-operators */ + return null; + } + + if (tag === 'select') { + var index = el.selectedIndex; + + if (index < 0) { + return null; + } + + var a = [], ops = el.options; + var one = (t === 'select-one'); + var max = (one ? index + 1 : ops.length); + + for (var i = (one ? index : 0); i < max; i++) { + var op = ops[i]; + + if (op.selected && !op.disabled) { + var v = op.value; + + if (!v) { // extra pain for IE... + v = (op.attributes && op.attributes.value && !(op.attributes.value.specified)) ? op.text : op.value; + } + + if (one) { + return v; + } + + a.push(v); + } + } + + return a; + } + + return $(el).val().replace(rCRLF, '\r\n'); + }; + + /** + * Clears the form data. Takes the following actions on the form's input fields: + * - input text fields will have their 'value' property set to the empty string + * - select elements will have their 'selectedIndex' property set to -1 + * - checkbox and radio inputs will have their 'checked' property set to false + * - inputs of type submit, button, reset, and hidden will *not* be effected + * - button elements will *not* be effected + */ + $.fn.clearForm = function(includeHidden) { + return this.each(function() { + $('input,select,textarea', this).clearFields(includeHidden); + }); + }; + + /** + * Clears the selected form elements. + */ + $.fn.clearFields = $.fn.clearInputs = function(includeHidden) { + var re = /^(?:color|date|datetime|email|month|number|password|range|search|tel|text|time|url|week)$/i; // 'hidden' is not in this list + + return this.each(function() { + var t = this.type, tag = this.tagName.toLowerCase(); + + if (re.test(t) || tag === 'textarea') { + this.value = ''; + + } else if (t === 'checkbox' || t === 'radio') { + this.checked = false; + + } else if (tag === 'select') { + this.selectedIndex = -1; + + } else if (t === 'file') { + if (/MSIE/.test(navigator.userAgent)) { + $(this).replaceWith($(this).clone(true)); + } else { + $(this).val(''); + } + + } else if (includeHidden) { + // includeHidden can be the value true, or it can be a selector string + // indicating a special test; for example: + // $('#myForm').clearForm('.special:hidden') + // the above would clean hidden inputs that have the class of 'special' + if ((includeHidden === true && /hidden/.test(t)) || + (typeof includeHidden === 'string' && $(this).is(includeHidden))) { + this.value = ''; + } + } + }); + }; + + + /** + * Resets the form data or individual elements. Takes the following actions + * on the selected tags: + * - all fields within form elements will be reset to their original value + * - input / textarea / select fields will be reset to their original value + * - option / optgroup fields (for multi-selects) will defaulted individually + * - non-multiple options will find the right select to default + * - label elements will be searched against its 'for' attribute + * - all others will be searched for appropriate children to default + */ + $.fn.resetForm = function() { + return this.each(function() { + var el = $(this); + var tag = this.tagName.toLowerCase(); + + switch (tag) { + case 'input': + this.checked = this.defaultChecked; + // fall through + + case 'textarea': + this.value = this.defaultValue; + + return true; + + case 'option': + case 'optgroup': + var select = el.parents('select'); + + if (select.length && select[0].multiple) { + if (tag === 'option') { + this.selected = this.defaultSelected; + } else { + el.find('option').resetForm(); + } + } else { + select.resetForm(); + } + + return true; + + case 'select': + el.find('option').each(function(i) { // eslint-disable-line consistent-return + this.selected = this.defaultSelected; + if (this.defaultSelected && !el[0].multiple) { + el[0].selectedIndex = i; + + return false; + } + }); + + return true; + + case 'label': + var forEl = $(el.attr('for')); + var list = el.find('input,select,textarea'); + + if (forEl[0]) { + list.unshift(forEl[0]); + } + + list.resetForm(); + + return true; + + case 'form': + // guard against an input with the name of 'reset' + // note that IE reports the reset function as an 'object' + if (typeof this.reset === 'function' || (typeof this.reset === 'object' && !this.reset.nodeType)) { + this.reset(); + } + + return true; + + default: + el.find('form,input,label,select,textarea').resetForm(); + + return true; + } + }); + }; + + /** + * Enables or disables any matching elements. + */ + $.fn.enable = function(b) { + if (typeof b === 'undefined') { + b = true; + } + + return this.each(function() { + this.disabled = !b; + }); + }; + + /** + * Checks/unchecks any matching checkboxes or radio buttons and + * selects/deselects and matching option elements. + */ + $.fn.selected = function(select) { + if (typeof select === 'undefined') { + select = true; + } + + return this.each(function() { + var t = this.type; + + if (t === 'checkbox' || t === 'radio') { + this.checked = select; + + } else if (this.tagName.toLowerCase() === 'option') { + var $sel = $(this).parent('select'); + + if (select && $sel[0] && $sel[0].type === 'select-one') { + // deselect all other options + $sel.find('option').selected(false); + } + + this.selected = select; + } + }); + }; + + // expose debug var + $.fn.ajaxSubmit.debug = false; + + // helper fn for console logging + function log() { + if (!$.fn.ajaxSubmit.debug) { + return; + } + + var msg = '[jquery.form] ' + Array.prototype.join.call(arguments, ''); + + if (window.console && window.console.log) { + window.console.log(msg); + + } else if (window.opera && window.opera.postError) { + window.opera.postError(msg); + } + } +})); diff --git a/js/jquery.json.js b/js/jquery.json.js new file mode 100644 index 0000000..264715c --- /dev/null +++ b/js/jquery.json.js @@ -0,0 +1,200 @@ +/** + * jQuery JSON plugin v2.5.1 + * https://github.com/Krinkle/jquery-json + * + * @author Brantley Harris, 2009-2011 + * @author Timo Tijhof, 2011-2014 + * @source This plugin is heavily influenced by MochiKit's serializeJSON, which is + * copyrighted 2005 by Bob Ippolito. + * @source Brantley Harris wrote this plugin. It is based somewhat on the JSON.org + * website's http://www.json.org/json2.js, which proclaims: + * "NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.", a sentiment that + * I uphold. + * @license MIT License <http://opensource.org/licenses/MIT> + */ +(function ($) { + 'use strict'; + + var escape = /["\\\x00-\x1f\x7f-\x9f]/g, + meta = { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"': '\\"', + '\\': '\\\\' + }, + hasOwn = Object.prototype.hasOwnProperty; + + /** + * jQuery.toJSON + * Converts the given argument into a JSON representation. + * + * @param o {Mixed} The json-serializable *thing* to be converted + * + * If an object has a toJSON prototype, that will be used to get the representation. + * Non-integer/string keys are skipped in the object, as are keys that point to a + * function. + * + */ + $.toJSON = typeof JSON === 'object' && JSON.stringify ? JSON.stringify : function (o) { + if (o === null) { + return 'null'; + } + + var pairs, k, name, val, + type = $.type(o); + + if (type === 'undefined') { + return undefined; + } + + // Also covers instantiated Number and Boolean objects, + // which are typeof 'object' but thanks to $.type, we + // catch them here. I don't know whether it is right + // or wrong that instantiated primitives are not + // exported to JSON as an {"object":..}. + // We choose this path because that's what the browsers did. + if (type === 'number' || type === 'boolean') { + return String(o); + } + if (type === 'string') { + return $.quoteString(o); + } + if (typeof o.toJSON === 'function') { + return $.toJSON(o.toJSON()); + } + if (type === 'date') { + var month = o.getUTCMonth() + 1, + day = o.getUTCDate(), + year = o.getUTCFullYear(), + hours = o.getUTCHours(), + minutes = o.getUTCMinutes(), + seconds = o.getUTCSeconds(), + milli = o.getUTCMilliseconds(); + + if (month < 10) { + month = '0' + month; + } + if (day < 10) { + day = '0' + day; + } + if (hours < 10) { + hours = '0' + hours; + } + if (minutes < 10) { + minutes = '0' + minutes; + } + if (seconds < 10) { + seconds = '0' + seconds; + } + if (milli < 100) { + milli = '0' + milli; + } + if (milli < 10) { + milli = '0' + milli; + } + return '"' + year + '-' + month + '-' + day + 'T' + + hours + ':' + minutes + ':' + seconds + + '.' + milli + 'Z"'; + } + + pairs = []; + + if ($.isArray(o)) { + for (k = 0; k < o.length; k++) { + pairs.push($.toJSON(o[k]) || 'null'); + } + return '[' + pairs.join(',') + ']'; + } + + // Any other object (plain object, RegExp, ..) + // Need to do typeof instead of $.type, because we also + // want to catch non-plain objects. + if (typeof o === 'object') { + for (k in o) { + // Only include own properties, + // Filter out inherited prototypes + if (hasOwn.call(o, k)) { + // Keys must be numerical or string. Skip others + type = typeof k; + if (type === 'number') { + name = '"' + k + '"'; + } else if (type === 'string') { + name = $.quoteString(k); + } else { + continue; + } + type = typeof o[k]; + + // Invalid values like these return undefined + // from toJSON, however those object members + // shouldn't be included in the JSON string at all. + if (type !== 'function' && type !== 'undefined') { + val = $.toJSON(o[k]); + pairs.push(name + ':' + val); + } + } + } + return '{' + pairs.join(',') + '}'; + } + }; + + /** + * jQuery.evalJSON + * Evaluates a given json string. + * + * @param str {String} + */ + $.evalJSON = typeof JSON === 'object' && JSON.parse ? JSON.parse : function (str) { + /*jshint evil: true */ + return eval('(' + str + ')'); + }; + + /** + * jQuery.secureEvalJSON + * Evals JSON in a way that is *more* secure. + * + * @param str {String} + */ + $.secureEvalJSON = typeof JSON === 'object' && JSON.parse ? JSON.parse : function (str) { + var filtered = + str + .replace(/\\["\\\/bfnrtu]/g, '@') + .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') + .replace(/(?:^|:|,)(?:\s*\[)+/g, ''); + + if (/^[\],:{}\s]*$/.test(filtered)) { + /*jshint evil: true */ + return eval('(' + str + ')'); + } + throw new SyntaxError('Error parsing JSON, source is not valid.'); + }; + + /** + * jQuery.quoteString + * Returns a string-repr of a string, escaping quotes intelligently. + * Mostly a support function for toJSON. + * Examples: + * >>> jQuery.quoteString('apple') + * "apple" + * + * >>> jQuery.quoteString('"Where are we going?", she asked.') + * "\"Where are we going?\", she asked." + */ + $.quoteString = function (str) { + if (str.match(escape)) { + return '"' + str.replace(escape, function (a) { + var c = meta[a]; + if (typeof c === 'string') { + return c; + } + c = a.charCodeAt(); + return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16); + }) + '"'; + } + return '"' + str + '"'; + }; + +}(jQuery)); diff --git a/js/jquery.qtip.js b/js/jquery.qtip.js new file mode 100644 index 0000000..cf6c682 --- /dev/null +++ b/js/jquery.qtip.js @@ -0,0 +1,3487 @@ +/* + * qTip2 - Pretty powerful tooltips - v3.0.3 + * http://qtip2.com + * + * Copyright (c) 2016 + * Released under the MIT licenses + * http://jquery.org/license + * + * Date: Wed May 11 2016 10:31 GMT+0100+0100 + * Plugins: tips modal viewport svg imagemap ie6 + * Styles: core basic css3 + */ +/*global window: false, jQuery: false, console: false, define: false */ + +/* Cache window, document, undefined */ +(function( window, document, undefined ) { + +// Uses AMD or browser globals to create a jQuery plugin. +(function( factory ) { + "use strict"; + if(typeof define === 'function' && define.amd) { + define(['jquery'], factory); + } + else if(jQuery && !jQuery.fn.qtip) { + factory(jQuery); + } +} +(function($) { + "use strict"; // Enable ECMAScript "strict" operation for this function. See more: http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/ +;// Munge the primitives - Paul Irish tip +var TRUE = true, +FALSE = false, +NULL = null, + +// Common variables +X = 'x', Y = 'y', +WIDTH = 'width', +HEIGHT = 'height', + +// Positioning sides +TOP = 'top', +LEFT = 'left', +BOTTOM = 'bottom', +RIGHT = 'right', +CENTER = 'center', + +// Position adjustment types +FLIP = 'flip', +FLIPINVERT = 'flipinvert', +SHIFT = 'shift', + +// Shortcut vars +QTIP, PROTOTYPE, CORNER, CHECKS, +PLUGINS = {}, +NAMESPACE = 'qtip', +ATTR_HAS = 'data-hasqtip', +ATTR_ID = 'data-qtip-id', +WIDGET = ['ui-widget', 'ui-tooltip'], +SELECTOR = '.'+NAMESPACE, +INACTIVE_EVENTS = 'click dblclick mousedown mouseup mousemove mouseleave mouseenter'.split(' '), + +CLASS_FIXED = NAMESPACE+'-fixed', +CLASS_DEFAULT = NAMESPACE + '-default', +CLASS_FOCUS = NAMESPACE + '-focus', +CLASS_HOVER = NAMESPACE + '-hover', +CLASS_DISABLED = NAMESPACE+'-disabled', + +replaceSuffix = '_replacedByqTip', +oldtitle = 'oldtitle', +trackingBound, + +// Browser detection +BROWSER = { + /* + * IE version detection + * + * Adapted from: http://ajaxian.com/archives/attack-of-the-ie-conditional-comment + * Credit to James Padolsey for the original implemntation! + */ + ie: (function() { + /* eslint-disable no-empty */ + var v, i; + for ( + v = 4, i = document.createElement('div'); + (i.innerHTML = '<!--[if gt IE ' + v + ']><i></i><![endif]-->') && i.getElementsByTagName('i')[0]; + v+=1 + ) {} + return v > 4 ? v : NaN; + /* eslint-enable no-empty */ + })(), + + /* + * iOS version detection + */ + iOS: parseFloat( + ('' + (/CPU.*OS ([0-9_]{1,5})|(CPU like).*AppleWebKit.*Mobile/i.exec(navigator.userAgent) || [0,''])[1]) + .replace('undefined', '3_2').replace('_', '.').replace('_', '') + ) || FALSE +}; +;function QTip(target, options, id, attr) { + // Elements and ID + this.id = id; + this.target = target; + this.tooltip = NULL; + this.elements = { target: target }; + + // Internal constructs + this._id = NAMESPACE + '-' + id; + this.timers = { img: {} }; + this.options = options; + this.plugins = {}; + + // Cache object + this.cache = { + event: {}, + target: $(), + disabled: FALSE, + attr: attr, + onTooltip: FALSE, + lastClass: '' + }; + + // Set the initial flags + this.rendered = this.destroyed = this.disabled = this.waiting = + this.hiddenDuringWait = this.positioning = this.triggering = FALSE; +} +PROTOTYPE = QTip.prototype; + +PROTOTYPE._when = function(deferreds) { + return $.when.apply($, deferreds); +}; + +PROTOTYPE.render = function(show) { + if(this.rendered || this.destroyed) { return this; } // If tooltip has already been rendered, exit + + var self = this, + options = this.options, + cache = this.cache, + elements = this.elements, + text = options.content.text, + title = options.content.title, + button = options.content.button, + posOptions = options.position, + deferreds = []; + + // Add ARIA attributes to target + $.attr(this.target[0], 'aria-describedby', this._id); + + // Create public position object that tracks current position corners + cache.posClass = this._createPosClass( + (this.position = { my: posOptions.my, at: posOptions.at }).my + ); + + // Create tooltip element + this.tooltip = elements.tooltip = $('<div/>', { + 'id': this._id, + 'class': [ NAMESPACE, CLASS_DEFAULT, options.style.classes, cache.posClass ].join(' '), + 'width': options.style.width || '', + 'height': options.style.height || '', + 'tracking': posOptions.target === 'mouse' && posOptions.adjust.mouse, + + /* ARIA specific attributes */ + 'role': 'alert', + 'aria-live': 'polite', + 'aria-atomic': FALSE, + 'aria-describedby': this._id + '-content', + 'aria-hidden': TRUE + }) + .toggleClass(CLASS_DISABLED, this.disabled) + .attr(ATTR_ID, this.id) + .data(NAMESPACE, this) + .appendTo(posOptions.container) + .append( + // Create content element + elements.content = $('<div />', { + 'class': NAMESPACE + '-content', + 'id': this._id + '-content', + 'aria-atomic': TRUE + }) + ); + + // Set rendered flag and prevent redundant reposition calls for now + this.rendered = -1; + this.positioning = TRUE; + + // Create title... + if(title) { + this._createTitle(); + + // Update title only if its not a callback (called in toggle if so) + if(!$.isFunction(title)) { + deferreds.push( this._updateTitle(title, FALSE) ); + } + } + + // Create button + if(button) { this._createButton(); } + + // Set proper rendered flag and update content if not a callback function (called in toggle) + if(!$.isFunction(text)) { + deferreds.push( this._updateContent(text, FALSE) ); + } + this.rendered = TRUE; + + // Setup widget classes + this._setWidget(); + + // Initialize 'render' plugins + $.each(PLUGINS, function(name) { + var instance; + if(this.initialize === 'render' && (instance = this(self))) { + self.plugins[name] = instance; + } + }); + + // Unassign initial events and assign proper events + this._unassignEvents(); + this._assignEvents(); + + // When deferreds have completed + this._when(deferreds).then(function() { + // tooltiprender event + self._trigger('render'); + + // Reset flags + self.positioning = FALSE; + + // Show tooltip if not hidden during wait period + if(!self.hiddenDuringWait && (options.show.ready || show)) { + self.toggle(TRUE, cache.event, FALSE); + } + self.hiddenDuringWait = FALSE; + }); + + // Expose API + QTIP.api[this.id] = this; + + return this; +}; + +PROTOTYPE.destroy = function(immediate) { + // Set flag the signify destroy is taking place to plugins + // and ensure it only gets destroyed once! + if(this.destroyed) { return this.target; } + + function process() { + if(this.destroyed) { return; } + this.destroyed = TRUE; + + var target = this.target, + title = target.attr(oldtitle), + timer; + + // Destroy tooltip if rendered + if(this.rendered) { + this.tooltip.stop(1,0).find('*').remove().end().remove(); + } + + // Destroy all plugins + $.each(this.plugins, function() { + this.destroy && this.destroy(); + }); + + // Clear timers + for (timer in this.timers) { + if (this.timers.hasOwnProperty(timer)) { + clearTimeout(this.timers[timer]); + } + } + + // Remove api object and ARIA attributes + target.removeData(NAMESPACE) + .removeAttr(ATTR_ID) + .removeAttr(ATTR_HAS) + .removeAttr('aria-describedby'); + + // Reset old title attribute if removed + if(this.options.suppress && title) { + target.attr('title', title).removeAttr(oldtitle); + } + + // Remove qTip events associated with this API + this._unassignEvents(); + + // Remove ID from used id objects, and delete object references + // for better garbage collection and leak protection + this.options = this.elements = this.cache = this.timers = + this.plugins = this.mouse = NULL; + + // Delete epoxsed API object + delete QTIP.api[this.id]; + } + + // If an immediate destroy is needed + if((immediate !== TRUE || this.triggering === 'hide') && this.rendered) { + this.tooltip.one('tooltiphidden', $.proxy(process, this)); + !this.triggering && this.hide(); + } + + // If we're not in the process of hiding... process + else { process.call(this); } + + return this.target; +}; +;function invalidOpt(a) { + return a === NULL || $.type(a) !== 'object'; +} + +function invalidContent(c) { + return !($.isFunction(c) || + c && c.attr || + c.length || + $.type(c) === 'object' && (c.jquery || c.then)); +} + +// Option object sanitizer +function sanitizeOptions(opts) { + var content, text, ajax, once; + + if(invalidOpt(opts)) { return FALSE; } + + if(invalidOpt(opts.metadata)) { + opts.metadata = { type: opts.metadata }; + } + + if('content' in opts) { + content = opts.content; + + if(invalidOpt(content) || content.jquery || content.done) { + text = invalidContent(content) ? FALSE : content; + content = opts.content = { + text: text + }; + } + else { text = content.text; } + + // DEPRECATED - Old content.ajax plugin functionality + // Converts it into the proper Deferred syntax + if('ajax' in content) { + ajax = content.ajax; + once = ajax && ajax.once !== FALSE; + delete content.ajax; + + content.text = function(event, api) { + var loading = text || $(this).attr(api.options.content.attr) || 'Loading...', + + deferred = $.ajax( + $.extend({}, ajax, { context: api }) + ) + .then(ajax.success, NULL, ajax.error) + .then(function(newContent) { + if(newContent && once) { api.set('content.text', newContent); } + return newContent; + }, + function(xhr, status, error) { + if(api.destroyed || xhr.status === 0) { return; } + api.set('content.text', status + ': ' + error); + }); + + return !once ? (api.set('content.text', loading), deferred) : loading; + }; + } + + if('title' in content) { + if($.isPlainObject(content.title)) { + content.button = content.title.button; + content.title = content.title.text; + } + + if(invalidContent(content.title || FALSE)) { + content.title = FALSE; + } + } + } + + if('position' in opts && invalidOpt(opts.position)) { + opts.position = { my: opts.position, at: opts.position }; + } + + if('show' in opts && invalidOpt(opts.show)) { + opts.show = opts.show.jquery ? { target: opts.show } : + opts.show === TRUE ? { ready: TRUE } : { event: opts.show }; + } + + if('hide' in opts && invalidOpt(opts.hide)) { + opts.hide = opts.hide.jquery ? { target: opts.hide } : { event: opts.hide }; + } + + if('style' in opts && invalidOpt(opts.style)) { + opts.style = { classes: opts.style }; + } + + // Sanitize plugin options + $.each(PLUGINS, function() { + this.sanitize && this.sanitize(opts); + }); + + return opts; +} + +// Setup builtin .set() option checks +CHECKS = PROTOTYPE.checks = { + builtin: { + // Core checks + '^id$': function(obj, o, v, prev) { + var id = v === TRUE ? QTIP.nextid : v, + newId = NAMESPACE + '-' + id; + + if(id !== FALSE && id.length > 0 && !$('#'+newId).length) { + this._id = newId; + + if(this.rendered) { + this.tooltip[0].id = this._id; + this.elements.content[0].id = this._id + '-content'; + this.elements.title[0].id = this._id + '-title'; + } + } + else { obj[o] = prev; } + }, + '^prerender': function(obj, o, v) { + v && !this.rendered && this.render(this.options.show.ready); + }, + + // Content checks + '^content.text$': function(obj, o, v) { + this._updateContent(v); + }, + '^content.attr$': function(obj, o, v, prev) { + if(this.options.content.text === this.target.attr(prev)) { + this._updateContent( this.target.attr(v) ); + } + }, + '^content.title$': function(obj, o, v) { + // Remove title if content is null + if(!v) { return this._removeTitle(); } + + // If title isn't already created, create it now and update + v && !this.elements.title && this._createTitle(); + this._updateTitle(v); + }, + '^content.button$': function(obj, o, v) { + this._updateButton(v); + }, + '^content.title.(text|button)$': function(obj, o, v) { + this.set('content.'+o, v); // Backwards title.text/button compat + }, + + // Position checks + '^position.(my|at)$': function(obj, o, v){ + if('string' === typeof v) { + this.position[o] = obj[o] = new CORNER(v, o === 'at'); + } + }, + '^position.container$': function(obj, o, v){ + this.rendered && this.tooltip.appendTo(v); + }, + + // Show checks + '^show.ready$': function(obj, o, v) { + v && (!this.rendered && this.render(TRUE) || this.toggle(TRUE)); + }, + + // Style checks + '^style.classes$': function(obj, o, v, p) { + this.rendered && this.tooltip.removeClass(p).addClass(v); + }, + '^style.(width|height)': function(obj, o, v) { + this.rendered && this.tooltip.css(o, v); + }, + '^style.widget|content.title': function() { + this.rendered && this._setWidget(); + }, + '^style.def': function(obj, o, v) { + this.rendered && this.tooltip.toggleClass(CLASS_DEFAULT, !!v); + }, + + // Events check + '^events.(render|show|move|hide|focus|blur)$': function(obj, o, v) { + this.rendered && this.tooltip[($.isFunction(v) ? '' : 'un') + 'bind']('tooltip'+o, v); + }, + + // Properties which require event reassignment + '^(show|hide|position).(event|target|fixed|inactive|leave|distance|viewport|adjust)': function() { + if(!this.rendered) { return; } + + // Set tracking flag + var posOptions = this.options.position; + this.tooltip.attr('tracking', posOptions.target === 'mouse' && posOptions.adjust.mouse); + + // Reassign events + this._unassignEvents(); + this._assignEvents(); + } + } +}; + +// Dot notation converter +function convertNotation(options, notation) { + var i = 0, obj, option = options, + + // Split notation into array + levels = notation.split('.'); + + // Loop through + while(option = option[ levels[i++] ]) { + if(i < levels.length) { obj = option; } + } + + return [obj || options, levels.pop()]; +} + +PROTOTYPE.get = function(notation) { + if(this.destroyed) { return this; } + + var o = convertNotation(this.options, notation.toLowerCase()), + result = o[0][ o[1] ]; + + return result.precedance ? result.string() : result; +}; + +function setCallback(notation, args) { + var category, rule, match; + + for(category in this.checks) { + if (!this.checks.hasOwnProperty(category)) { continue; } + + for(rule in this.checks[category]) { + if (!this.checks[category].hasOwnProperty(rule)) { continue; } + + if(match = (new RegExp(rule, 'i')).exec(notation)) { + args.push(match); + + if(category === 'builtin' || this.plugins[category]) { + this.checks[category][rule].apply( + this.plugins[category] || this, args + ); + } + } + } + } +} + +var rmove = /^position\.(my|at|adjust|target|container|viewport)|style|content|show\.ready/i, + rrender = /^prerender|show\.ready/i; + +PROTOTYPE.set = function(option, value) { + if(this.destroyed) { return this; } + + var rendered = this.rendered, + reposition = FALSE, + options = this.options, + name; + + // Convert singular option/value pair into object form + if('string' === typeof option) { + name = option; option = {}; option[name] = value; + } + else { option = $.extend({}, option); } + + // Set all of the defined options to their new values + $.each(option, function(notation, val) { + if(rendered && rrender.test(notation)) { + delete option[notation]; return; + } + + // Set new obj value + var obj = convertNotation(options, notation.toLowerCase()), previous; + previous = obj[0][ obj[1] ]; + obj[0][ obj[1] ] = val && val.nodeType ? $(val) : val; + + // Also check if we need to reposition + reposition = rmove.test(notation) || reposition; + + // Set the new params for the callback + option[notation] = [obj[0], obj[1], val, previous]; + }); + + // Re-sanitize options + sanitizeOptions(options); + + /* + * Execute any valid callbacks for the set options + * Also set positioning flag so we don't get loads of redundant repositioning calls. + */ + this.positioning = TRUE; + $.each(option, $.proxy(setCallback, this)); + this.positioning = FALSE; + + // Update position if needed + if(this.rendered && this.tooltip[0].offsetWidth > 0 && reposition) { + this.reposition( options.position.target === 'mouse' ? NULL : this.cache.event ); + } + + return this; +}; +;PROTOTYPE._update = function(content, element) { + var self = this, + cache = this.cache; + + // Make sure tooltip is rendered and content is defined. If not return + if(!this.rendered || !content) { return FALSE; } + + // Use function to parse content + if($.isFunction(content)) { + content = content.call(this.elements.target, cache.event, this) || ''; + } + + // Handle deferred content + if($.isFunction(content.then)) { + cache.waiting = TRUE; + return content.then(function(c) { + cache.waiting = FALSE; + return self._update(c, element); + }, NULL, function(e) { + return self._update(e, element); + }); + } + + // If content is null... return false + if(content === FALSE || !content && content !== '') { return FALSE; } + + // Append new content if its a DOM array and show it if hidden + if(content.jquery && content.length > 0) { + element.empty().append( + content.css({ display: 'block', visibility: 'visible' }) + ); + } + + // Content is a regular string, insert the new content + else { element.html(content); } + + // Wait for content to be loaded, and reposition + return this._waitForContent(element).then(function(images) { + if(self.rendered && self.tooltip[0].offsetWidth > 0) { + self.reposition(cache.event, !images.length); + } + }); +}; + +PROTOTYPE._waitForContent = function(element) { + var cache = this.cache; + + // Set flag + cache.waiting = TRUE; + + // If imagesLoaded is included, ensure images have loaded and return promise + return ( $.fn.imagesLoaded ? element.imagesLoaded() : new $.Deferred().resolve([]) ) + .done(function() { cache.waiting = FALSE; }) + .promise(); +}; + +PROTOTYPE._updateContent = function(content, reposition) { + this._update(content, this.elements.content, reposition); +}; + +PROTOTYPE._updateTitle = function(content, reposition) { + if(this._update(content, this.elements.title, reposition) === FALSE) { + this._removeTitle(FALSE); + } +}; + +PROTOTYPE._createTitle = function() +{ + var elements = this.elements, + id = this._id+'-title'; + + // Destroy previous title element, if present + if(elements.titlebar) { this._removeTitle(); } + + // Create title bar and title elements + elements.titlebar = $('<div />', { + 'class': NAMESPACE + '-titlebar ' + (this.options.style.widget ? createWidgetClass('header') : '') + }) + .append( + elements.title = $('<div />', { + 'id': id, + 'class': NAMESPACE + '-title', + 'aria-atomic': TRUE + }) + ) + .insertBefore(elements.content) + + // Button-specific events + .delegate('.qtip-close', 'mousedown keydown mouseup keyup mouseout', function(event) { + $(this).toggleClass('ui-state-active ui-state-focus', event.type.substr(-4) === 'down'); + }) + .delegate('.qtip-close', 'mouseover mouseout', function(event){ + $(this).toggleClass('ui-state-hover', event.type === 'mouseover'); + }); + + // Create button if enabled + if(this.options.content.button) { this._createButton(); } +}; + +PROTOTYPE._removeTitle = function(reposition) +{ + var elements = this.elements; + + if(elements.title) { + elements.titlebar.remove(); + elements.titlebar = elements.title = elements.button = NULL; + + // Reposition if enabled + if(reposition !== FALSE) { this.reposition(); } + } +}; +;PROTOTYPE._createPosClass = function(my) { + return NAMESPACE + '-pos-' + (my || this.options.position.my).abbrev(); +}; + +PROTOTYPE.reposition = function(event, effect) { + if(!this.rendered || this.positioning || this.destroyed) { return this; } + + // Set positioning flag + this.positioning = TRUE; + + var cache = this.cache, + tooltip = this.tooltip, + posOptions = this.options.position, + target = posOptions.target, + my = posOptions.my, + at = posOptions.at, + viewport = posOptions.viewport, + container = posOptions.container, + adjust = posOptions.adjust, + method = adjust.method.split(' '), + tooltipWidth = tooltip.outerWidth(FALSE), + tooltipHeight = tooltip.outerHeight(FALSE), + targetWidth = 0, + targetHeight = 0, + type = tooltip.css('position'), + position = { left: 0, top: 0 }, + visible = tooltip[0].offsetWidth > 0, + isScroll = event && event.type === 'scroll', + win = $(window), + doc = container[0].ownerDocument, + mouse = this.mouse, + pluginCalculations, offset, adjusted, newClass; + + // Check if absolute position was passed + if($.isArray(target) && target.length === 2) { + // Force left top and set position + at = { x: LEFT, y: TOP }; + position = { left: target[0], top: target[1] }; + } + + // Check if mouse was the target + else if(target === 'mouse') { + // Force left top to allow flipping + at = { x: LEFT, y: TOP }; + + // Use the mouse origin that caused the show event, if distance hiding is enabled + if((!adjust.mouse || this.options.hide.distance) && cache.origin && cache.origin.pageX) { + event = cache.origin; + } + + // Use cached event for resize/scroll events + else if(!event || event && (event.type === 'resize' || event.type === 'scroll')) { + event = cache.event; + } + + // Otherwise, use the cached mouse coordinates if available + else if(mouse && mouse.pageX) { + event = mouse; + } + + // Calculate body and container offset and take them into account below + if(type !== 'static') { position = container.offset(); } + if(doc.body.offsetWidth !== (window.innerWidth || doc.documentElement.clientWidth)) { + offset = $(document.body).offset(); + } + + // Use event coordinates for position + position = { + left: event.pageX - position.left + (offset && offset.left || 0), + top: event.pageY - position.top + (offset && offset.top || 0) + }; + + // Scroll events are a pain, some browsers + if(adjust.mouse && isScroll && mouse) { + position.left -= (mouse.scrollX || 0) - win.scrollLeft(); + position.top -= (mouse.scrollY || 0) - win.scrollTop(); + } + } + + // Target wasn't mouse or absolute... + else { + // Check if event targetting is being used + if(target === 'event') { + if(event && event.target && event.type !== 'scroll' && event.type !== 'resize') { + cache.target = $(event.target); + } + else if(!event.target) { + cache.target = this.elements.target; + } + } + else if(target !== 'event'){ + cache.target = $(target.jquery ? target : this.elements.target); + } + target = cache.target; + + // Parse the target into a jQuery object and make sure there's an element present + target = $(target).eq(0); + if(target.length === 0) { return this; } + + // Check if window or document is the target + else if(target[0] === document || target[0] === window) { + targetWidth = BROWSER.iOS ? window.innerWidth : target.width(); + targetHeight = BROWSER.iOS ? window.innerHeight : target.height(); + + if(target[0] === window) { + position = { + top: (viewport || target).scrollTop(), + left: (viewport || target).scrollLeft() + }; + } + } + + // Check if the target is an <AREA> element + else if(PLUGINS.imagemap && target.is('area')) { + pluginCalculations = PLUGINS.imagemap(this, target, at, PLUGINS.viewport ? method : FALSE); + } + + // Check if the target is an SVG element + else if(PLUGINS.svg && target && target[0].ownerSVGElement) { + pluginCalculations = PLUGINS.svg(this, target, at, PLUGINS.viewport ? method : FALSE); + } + + // Otherwise use regular jQuery methods + else { + targetWidth = target.outerWidth(FALSE); + targetHeight = target.outerHeight(FALSE); + position = target.offset(); + } + + // Parse returned plugin values into proper variables + if(pluginCalculations) { + targetWidth = pluginCalculations.width; + targetHeight = pluginCalculations.height; + offset = pluginCalculations.offset; + position = pluginCalculations.position; + } + + // Adjust position to take into account offset parents + position = this.reposition.offset(target, position, container); + + // Adjust for position.fixed tooltips (and also iOS scroll bug in v3.2-4.0 & v4.3-4.3.2) + if(BROWSER.iOS > 3.1 && BROWSER.iOS < 4.1 || + BROWSER.iOS >= 4.3 && BROWSER.iOS < 4.33 || + !BROWSER.iOS && type === 'fixed' + ){ + position.left -= win.scrollLeft(); + position.top -= win.scrollTop(); + } + + // Adjust position relative to target + if(!pluginCalculations || pluginCalculations && pluginCalculations.adjustable !== FALSE) { + position.left += at.x === RIGHT ? targetWidth : at.x === CENTER ? targetWidth / 2 : 0; + position.top += at.y === BOTTOM ? targetHeight : at.y === CENTER ? targetHeight / 2 : 0; + } + } + + // Adjust position relative to tooltip + position.left += adjust.x + (my.x === RIGHT ? -tooltipWidth : my.x === CENTER ? -tooltipWidth / 2 : 0); + position.top += adjust.y + (my.y === BOTTOM ? -tooltipHeight : my.y === CENTER ? -tooltipHeight / 2 : 0); + + // Use viewport adjustment plugin if enabled + if(PLUGINS.viewport) { + adjusted = position.adjusted = PLUGINS.viewport( + this, position, posOptions, targetWidth, targetHeight, tooltipWidth, tooltipHeight + ); + + // Apply offsets supplied by positioning plugin (if used) + if(offset && adjusted.left) { position.left += offset.left; } + if(offset && adjusted.top) { position.top += offset.top; } + + // Apply any new 'my' position + if(adjusted.my) { this.position.my = adjusted.my; } + } + + // Viewport adjustment is disabled, set values to zero + else { position.adjusted = { left: 0, top: 0 }; } + + // Set tooltip position class if it's changed + if(cache.posClass !== (newClass = this._createPosClass(this.position.my))) { + cache.posClass = newClass; + tooltip.removeClass(cache.posClass).addClass(newClass); + } + + // tooltipmove event + if(!this._trigger('move', [position, viewport.elem || viewport], event)) { return this; } + delete position.adjusted; + + // If effect is disabled, target it mouse, no animation is defined or positioning gives NaN out, set CSS directly + if(effect === FALSE || !visible || isNaN(position.left) || isNaN(position.top) || target === 'mouse' || !$.isFunction(posOptions.effect)) { + tooltip.css(position); + } + + // Use custom function if provided + else if($.isFunction(posOptions.effect)) { + posOptions.effect.call(tooltip, this, $.extend({}, position)); + tooltip.queue(function(next) { + // Reset attributes to avoid cross-browser rendering bugs + $(this).css({ opacity: '', height: '' }); + if(BROWSER.ie) { this.style.removeAttribute('filter'); } + + next(); + }); + } + + // Set positioning flag + this.positioning = FALSE; + + return this; +}; + +// Custom (more correct for qTip!) offset calculator +PROTOTYPE.reposition.offset = function(elem, pos, container) { + if(!container[0]) { return pos; } + + var ownerDocument = $(elem[0].ownerDocument), + quirks = !!BROWSER.ie && document.compatMode !== 'CSS1Compat', + parent = container[0], + scrolled, position, parentOffset, overflow; + + function scroll(e, i) { + pos.left += i * e.scrollLeft(); + pos.top += i * e.scrollTop(); + } + + // Compensate for non-static containers offset + do { + if((position = $.css(parent, 'position')) !== 'static') { + if(position === 'fixed') { + parentOffset = parent.getBoundingClientRect(); + scroll(ownerDocument, -1); + } + else { + parentOffset = $(parent).position(); + parentOffset.left += parseFloat($.css(parent, 'borderLeftWidth')) || 0; + parentOffset.top += parseFloat($.css(parent, 'borderTopWidth')) || 0; + } + + pos.left -= parentOffset.left + (parseFloat($.css(parent, 'marginLeft')) || 0); + pos.top -= parentOffset.top + (parseFloat($.css(parent, 'marginTop')) || 0); + + // If this is the first parent element with an overflow of "scroll" or "auto", store it + if(!scrolled && (overflow = $.css(parent, 'overflow')) !== 'hidden' && overflow !== 'visible') { scrolled = $(parent); } + } + } + while(parent = parent.offsetParent); + + // Compensate for containers scroll if it also has an offsetParent (or in IE quirks mode) + if(scrolled && (scrolled[0] !== ownerDocument[0] || quirks)) { + scroll(scrolled, 1); + } + + return pos; +}; + +// Corner class +var C = (CORNER = PROTOTYPE.reposition.Corner = function(corner, forceY) { + corner = ('' + corner).replace(/([A-Z])/, ' $1').replace(/middle/gi, CENTER).toLowerCase(); + this.x = (corner.match(/left|right/i) || corner.match(/center/) || ['inherit'])[0].toLowerCase(); + this.y = (corner.match(/top|bottom|center/i) || ['inherit'])[0].toLowerCase(); + this.forceY = !!forceY; + + var f = corner.charAt(0); + this.precedance = f === 't' || f === 'b' ? Y : X; +}).prototype; + +C.invert = function(z, center) { + this[z] = this[z] === LEFT ? RIGHT : this[z] === RIGHT ? LEFT : center || this[z]; +}; + +C.string = function(join) { + var x = this.x, y = this.y; + + var result = x !== y ? + x === 'center' || y !== 'center' && (this.precedance === Y || this.forceY) ? + [y,x] : + [x,y] : + [x]; + + return join !== false ? result.join(' ') : result; +}; + +C.abbrev = function() { + var result = this.string(false); + return result[0].charAt(0) + (result[1] && result[1].charAt(0) || ''); +}; + +C.clone = function() { + return new CORNER( this.string(), this.forceY ); +}; + +; +PROTOTYPE.toggle = function(state, event) { + var cache = this.cache, + options = this.options, + tooltip = this.tooltip; + + // Try to prevent flickering when tooltip overlaps show element + if(event) { + if((/over|enter/).test(event.type) && cache.event && (/out|leave/).test(cache.event.type) && + options.show.target.add(event.target).length === options.show.target.length && + tooltip.has(event.relatedTarget).length) { + return this; + } + + // Cache event + cache.event = $.event.fix(event); + } + + // If we're currently waiting and we've just hidden... stop it + this.waiting && !state && (this.hiddenDuringWait = TRUE); + + // Render the tooltip if showing and it isn't already + if(!this.rendered) { return state ? this.render(1) : this; } + else if(this.destroyed || this.disabled) { return this; } + + var type = state ? 'show' : 'hide', + opts = this.options[type], + posOptions = this.options.position, + contentOptions = this.options.content, + width = this.tooltip.css('width'), + visible = this.tooltip.is(':visible'), + animate = state || opts.target.length === 1, + sameTarget = !event || opts.target.length < 2 || cache.target[0] === event.target, + identicalState, allow, after; + + // Detect state if valid one isn't provided + if((typeof state).search('boolean|number')) { state = !visible; } + + // Check if the tooltip is in an identical state to the new would-be state + identicalState = !tooltip.is(':animated') && visible === state && sameTarget; + + // Fire tooltip(show/hide) event and check if destroyed + allow = !identicalState ? !!this._trigger(type, [90]) : NULL; + + // Check to make sure the tooltip wasn't destroyed in the callback + if(this.destroyed) { return this; } + + // If the user didn't stop the method prematurely and we're showing the tooltip, focus it + if(allow !== FALSE && state) { this.focus(event); } + + // If the state hasn't changed or the user stopped it, return early + if(!allow || identicalState) { return this; } + + // Set ARIA hidden attribute + $.attr(tooltip[0], 'aria-hidden', !!!state); + + // Execute state specific properties + if(state) { + // Store show origin coordinates + this.mouse && (cache.origin = $.event.fix(this.mouse)); + + // Update tooltip content & title if it's a dynamic function + if($.isFunction(contentOptions.text)) { this._updateContent(contentOptions.text, FALSE); } + if($.isFunction(contentOptions.title)) { this._updateTitle(contentOptions.title, FALSE); } + + // Cache mousemove events for positioning purposes (if not already tracking) + if(!trackingBound && posOptions.target === 'mouse' && posOptions.adjust.mouse) { + $(document).bind('mousemove.'+NAMESPACE, this._storeMouse); + trackingBound = TRUE; + } + + // Update the tooltip position (set width first to prevent viewport/max-width issues) + if(!width) { tooltip.css('width', tooltip.outerWidth(FALSE)); } + this.reposition(event, arguments[2]); + if(!width) { tooltip.css('width', ''); } + + // Hide other tooltips if tooltip is solo + if(!!opts.solo) { + (typeof opts.solo === 'string' ? $(opts.solo) : $(SELECTOR, opts.solo)) + .not(tooltip).not(opts.target).qtip('hide', new $.Event('tooltipsolo')); + } + } + else { + // Clear show timer if we're hiding + clearTimeout(this.timers.show); + + // Remove cached origin on hide + delete cache.origin; + + // Remove mouse tracking event if not needed (all tracking qTips are hidden) + if(trackingBound && !$(SELECTOR+'[tracking="true"]:visible', opts.solo).not(tooltip).length) { + $(document).unbind('mousemove.'+NAMESPACE); + trackingBound = FALSE; + } + + // Blur the tooltip + this.blur(event); + } + + // Define post-animation, state specific properties + after = $.proxy(function() { + if(state) { + // Prevent antialias from disappearing in IE by removing filter + if(BROWSER.ie) { tooltip[0].style.removeAttribute('filter'); } + + // Remove overflow setting to prevent tip bugs + tooltip.css('overflow', ''); + + // Autofocus elements if enabled + if('string' === typeof opts.autofocus) { + $(this.options.show.autofocus, tooltip).focus(); + } + + // If set, hide tooltip when inactive for delay period + this.options.show.target.trigger('qtip-'+this.id+'-inactive'); + } + else { + // Reset CSS states + tooltip.css({ + display: '', + visibility: '', + opacity: '', + left: '', + top: '' + }); + } + + // tooltipvisible/tooltiphidden events + this._trigger(state ? 'visible' : 'hidden'); + }, this); + + // If no effect type is supplied, use a simple toggle + if(opts.effect === FALSE || animate === FALSE) { + tooltip[ type ](); + after(); + } + + // Use custom function if provided + else if($.isFunction(opts.effect)) { + tooltip.stop(1, 1); + opts.effect.call(tooltip, this); + tooltip.queue('fx', function(n) { + after(); n(); + }); + } + + // Use basic fade function by default + else { tooltip.fadeTo(90, state ? 1 : 0, after); } + + // If inactive hide method is set, active it + if(state) { opts.target.trigger('qtip-'+this.id+'-inactive'); } + + return this; +}; + +PROTOTYPE.show = function(event) { return this.toggle(TRUE, event); }; + +PROTOTYPE.hide = function(event) { return this.toggle(FALSE, event); }; +;PROTOTYPE.focus = function(event) { + if(!this.rendered || this.destroyed) { return this; } + + var qtips = $(SELECTOR), + tooltip = this.tooltip, + curIndex = parseInt(tooltip[0].style.zIndex, 10), + newIndex = QTIP.zindex + qtips.length; + + // Only update the z-index if it has changed and tooltip is not already focused + if(!tooltip.hasClass(CLASS_FOCUS)) { + // tooltipfocus event + if(this._trigger('focus', [newIndex], event)) { + // Only update z-index's if they've changed + if(curIndex !== newIndex) { + // Reduce our z-index's and keep them properly ordered + qtips.each(function() { + if(this.style.zIndex > curIndex) { + this.style.zIndex = this.style.zIndex - 1; + } + }); + + // Fire blur event for focused tooltip + qtips.filter('.' + CLASS_FOCUS).qtip('blur', event); + } + + // Set the new z-index + tooltip.addClass(CLASS_FOCUS)[0].style.zIndex = newIndex; + } + } + + return this; +}; + +PROTOTYPE.blur = function(event) { + if(!this.rendered || this.destroyed) { return this; } + + // Set focused status to FALSE + this.tooltip.removeClass(CLASS_FOCUS); + + // tooltipblur event + this._trigger('blur', [ this.tooltip.css('zIndex') ], event); + + return this; +}; +;PROTOTYPE.disable = function(state) { + if(this.destroyed) { return this; } + + // If 'toggle' is passed, toggle the current state + if(state === 'toggle') { + state = !(this.rendered ? this.tooltip.hasClass(CLASS_DISABLED) : this.disabled); + } + + // Disable if no state passed + else if('boolean' !== typeof state) { + state = TRUE; + } + + if(this.rendered) { + this.tooltip.toggleClass(CLASS_DISABLED, state) + .attr('aria-disabled', state); + } + + this.disabled = !!state; + + return this; +}; + +PROTOTYPE.enable = function() { return this.disable(FALSE); }; +;PROTOTYPE._createButton = function() +{ + var self = this, + elements = this.elements, + tooltip = elements.tooltip, + button = this.options.content.button, + isString = typeof button === 'string', + close = isString ? button : 'Close tooltip'; + + if(elements.button) { elements.button.remove(); } + + // Use custom button if one was supplied by user, else use default + if(button.jquery) { + elements.button = button; + } + else { + elements.button = $('<a />', { + 'class': 'qtip-close ' + (this.options.style.widget ? '' : NAMESPACE+'-icon'), + 'title': close, + 'aria-label': close + }) + .prepend( + $('<span />', { + 'class': 'ui-icon ui-icon-close', + 'html': '×' + }) + ); + } + + // Create button and setup attributes + elements.button.appendTo(elements.titlebar || tooltip) + .attr('role', 'button') + .click(function(event) { + if(!tooltip.hasClass(CLASS_DISABLED)) { self.hide(event); } + return FALSE; + }); +}; + +PROTOTYPE._updateButton = function(button) +{ + // Make sure tooltip is rendered and if not, return + if(!this.rendered) { return FALSE; } + + var elem = this.elements.button; + if(button) { this._createButton(); } + else { elem.remove(); } +}; +;// Widget class creator +function createWidgetClass(cls) { + return WIDGET.concat('').join(cls ? '-'+cls+' ' : ' '); +} + +// Widget class setter method +PROTOTYPE._setWidget = function() +{ + var on = this.options.style.widget, + elements = this.elements, + tooltip = elements.tooltip, + disabled = tooltip.hasClass(CLASS_DISABLED); + + tooltip.removeClass(CLASS_DISABLED); + CLASS_DISABLED = on ? 'ui-state-disabled' : 'qtip-disabled'; + tooltip.toggleClass(CLASS_DISABLED, disabled); + + tooltip.toggleClass('ui-helper-reset '+createWidgetClass(), on).toggleClass(CLASS_DEFAULT, this.options.style.def && !on); + + if(elements.content) { + elements.content.toggleClass( createWidgetClass('content'), on); + } + if(elements.titlebar) { + elements.titlebar.toggleClass( createWidgetClass('header'), on); + } + if(elements.button) { + elements.button.toggleClass(NAMESPACE+'-icon', !on); + } +}; +;function delay(callback, duration) { + // If tooltip has displayed, start hide timer + if(duration > 0) { + return setTimeout( + $.proxy(callback, this), duration + ); + } + else{ callback.call(this); } +} + +function showMethod(event) { + if(this.tooltip.hasClass(CLASS_DISABLED)) { return; } + + // Clear hide timers + clearTimeout(this.timers.show); + clearTimeout(this.timers.hide); + + // Start show timer + this.timers.show = delay.call(this, + function() { this.toggle(TRUE, event); }, + this.options.show.delay + ); +} + +function hideMethod(event) { + if(this.tooltip.hasClass(CLASS_DISABLED) || this.destroyed) { return; } + + // Check if new target was actually the tooltip element + var relatedTarget = $(event.relatedTarget), + ontoTooltip = relatedTarget.closest(SELECTOR)[0] === this.tooltip[0], + ontoTarget = relatedTarget[0] === this.options.show.target[0]; + + // Clear timers and stop animation queue + clearTimeout(this.timers.show); + clearTimeout(this.timers.hide); + + // Prevent hiding if tooltip is fixed and event target is the tooltip. + // Or if mouse positioning is enabled and cursor momentarily overlaps + if(this !== relatedTarget[0] && + (this.options.position.target === 'mouse' && ontoTooltip) || + this.options.hide.fixed && ( + (/mouse(out|leave|move)/).test(event.type) && (ontoTooltip || ontoTarget)) + ) + { + /* eslint-disable no-empty */ + try { + event.preventDefault(); + event.stopImmediatePropagation(); + } catch(e) {} + /* eslint-enable no-empty */ + + return; + } + + // If tooltip has displayed, start hide timer + this.timers.hide = delay.call(this, + function() { this.toggle(FALSE, event); }, + this.options.hide.delay, + this + ); +} + +function inactiveMethod(event) { + if(this.tooltip.hasClass(CLASS_DISABLED) || !this.options.hide.inactive) { return; } + + // Clear timer + clearTimeout(this.timers.inactive); + + this.timers.inactive = delay.call(this, + function(){ this.hide(event); }, + this.options.hide.inactive + ); +} + +function repositionMethod(event) { + if(this.rendered && this.tooltip[0].offsetWidth > 0) { this.reposition(event); } +} + +// Store mouse coordinates +PROTOTYPE._storeMouse = function(event) { + (this.mouse = $.event.fix(event)).type = 'mousemove'; + return this; +}; + +// Bind events +PROTOTYPE._bind = function(targets, events, method, suffix, context) { + if(!targets || !method || !events.length) { return; } + var ns = '.' + this._id + (suffix ? '-'+suffix : ''); + $(targets).bind( + (events.split ? events : events.join(ns + ' ')) + ns, + $.proxy(method, context || this) + ); + return this; +}; +PROTOTYPE._unbind = function(targets, suffix) { + targets && $(targets).unbind('.' + this._id + (suffix ? '-'+suffix : '')); + return this; +}; + +// Global delegation helper +function delegate(selector, events, method) { + $(document.body).delegate(selector, + (events.split ? events : events.join('.'+NAMESPACE + ' ')) + '.'+NAMESPACE, + function() { + var api = QTIP.api[ $.attr(this, ATTR_ID) ]; + api && !api.disabled && method.apply(api, arguments); + } + ); +} +// Event trigger +PROTOTYPE._trigger = function(type, args, event) { + var callback = new $.Event('tooltip'+type); + callback.originalEvent = event && $.extend({}, event) || this.cache.event || NULL; + + this.triggering = type; + this.tooltip.trigger(callback, [this].concat(args || [])); + this.triggering = FALSE; + + return !callback.isDefaultPrevented(); +}; + +PROTOTYPE._bindEvents = function(showEvents, hideEvents, showTargets, hideTargets, showCallback, hideCallback) { + // Get tasrgets that lye within both + var similarTargets = showTargets.filter( hideTargets ).add( hideTargets.filter(showTargets) ), + toggleEvents = []; + + // If hide and show targets are the same... + if(similarTargets.length) { + + // Filter identical show/hide events + $.each(hideEvents, function(i, type) { + var showIndex = $.inArray(type, showEvents); + + // Both events are identical, remove from both hide and show events + // and append to toggleEvents + showIndex > -1 && toggleEvents.push( showEvents.splice( showIndex, 1 )[0] ); + }); + + // Toggle events are special case of identical show/hide events, which happen in sequence + if(toggleEvents.length) { + // Bind toggle events to the similar targets + this._bind(similarTargets, toggleEvents, function(event) { + var state = this.rendered ? this.tooltip[0].offsetWidth > 0 : false; + (state ? hideCallback : showCallback).call(this, event); + }); + + // Remove the similar targets from the regular show/hide bindings + showTargets = showTargets.not(similarTargets); + hideTargets = hideTargets.not(similarTargets); + } + } + + // Apply show/hide/toggle events + this._bind(showTargets, showEvents, showCallback); + this._bind(hideTargets, hideEvents, hideCallback); +}; + +PROTOTYPE._assignInitialEvents = function(event) { + var options = this.options, + showTarget = options.show.target, + hideTarget = options.hide.target, + showEvents = options.show.event ? $.trim('' + options.show.event).split(' ') : [], + hideEvents = options.hide.event ? $.trim('' + options.hide.event).split(' ') : []; + + // Catch remove/removeqtip events on target element to destroy redundant tooltips + this._bind(this.elements.target, ['remove', 'removeqtip'], function() { + this.destroy(true); + }, 'destroy'); + + /* + * Make sure hoverIntent functions properly by using mouseleave as a hide event if + * mouseenter/mouseout is used for show.event, even if it isn't in the users options. + */ + if(/mouse(over|enter)/i.test(options.show.event) && !/mouse(out|leave)/i.test(options.hide.event)) { + hideEvents.push('mouseleave'); + } + + /* + * Also make sure initial mouse targetting works correctly by caching mousemove coords + * on show targets before the tooltip has rendered. Also set onTarget when triggered to + * keep mouse tracking working. + */ + this._bind(showTarget, 'mousemove', function(moveEvent) { + this._storeMouse(moveEvent); + this.cache.onTarget = TRUE; + }); + + // Define hoverIntent function + function hoverIntent(hoverEvent) { + // Only continue if tooltip isn't disabled + if(this.disabled || this.destroyed) { return FALSE; } + + // Cache the event data + this.cache.event = hoverEvent && $.event.fix(hoverEvent); + this.cache.target = hoverEvent && $(hoverEvent.target); + + // Start the event sequence + clearTimeout(this.timers.show); + this.timers.show = delay.call(this, + function() { this.render(typeof hoverEvent === 'object' || options.show.ready); }, + options.prerender ? 0 : options.show.delay + ); + } + + // Filter and bind events + this._bindEvents(showEvents, hideEvents, showTarget, hideTarget, hoverIntent, function() { + if(!this.timers) { return FALSE; } + clearTimeout(this.timers.show); + }); + + // Prerendering is enabled, create tooltip now + if(options.show.ready || options.prerender) { hoverIntent.call(this, event); } +}; + +// Event assignment method +PROTOTYPE._assignEvents = function() { + var self = this, + options = this.options, + posOptions = options.position, + + tooltip = this.tooltip, + showTarget = options.show.target, + hideTarget = options.hide.target, + containerTarget = posOptions.container, + viewportTarget = posOptions.viewport, + documentTarget = $(document), + windowTarget = $(window), + + showEvents = options.show.event ? $.trim('' + options.show.event).split(' ') : [], + hideEvents = options.hide.event ? $.trim('' + options.hide.event).split(' ') : []; + + + // Assign passed event callbacks + $.each(options.events, function(name, callback) { + self._bind(tooltip, name === 'toggle' ? ['tooltipshow','tooltiphide'] : ['tooltip'+name], callback, null, tooltip); + }); + + // Hide tooltips when leaving current window/frame (but not select/option elements) + if(/mouse(out|leave)/i.test(options.hide.event) && options.hide.leave === 'window') { + this._bind(documentTarget, ['mouseout', 'blur'], function(event) { + if(!/select|option/.test(event.target.nodeName) && !event.relatedTarget) { + this.hide(event); + } + }); + } + + // Enable hide.fixed by adding appropriate class + if(options.hide.fixed) { + hideTarget = hideTarget.add( tooltip.addClass(CLASS_FIXED) ); + } + + /* + * Make sure hoverIntent functions properly by using mouseleave to clear show timer if + * mouseenter/mouseout is used for show.event, even if it isn't in the users options. + */ + else if(/mouse(over|enter)/i.test(options.show.event)) { + this._bind(hideTarget, 'mouseleave', function() { + clearTimeout(this.timers.show); + }); + } + + // Hide tooltip on document mousedown if unfocus events are enabled + if(('' + options.hide.event).indexOf('unfocus') > -1) { + this._bind(containerTarget.closest('html'), ['mousedown', 'touchstart'], function(event) { + var elem = $(event.target), + enabled = this.rendered && !this.tooltip.hasClass(CLASS_DISABLED) && this.tooltip[0].offsetWidth > 0, + isAncestor = elem.parents(SELECTOR).filter(this.tooltip[0]).length > 0; + + if(elem[0] !== this.target[0] && elem[0] !== this.tooltip[0] && !isAncestor && + !this.target.has(elem[0]).length && enabled + ) { + this.hide(event); + } + }); + } + + // Check if the tooltip hides when inactive + if('number' === typeof options.hide.inactive) { + // Bind inactive method to show target(s) as a custom event + this._bind(showTarget, 'qtip-'+this.id+'-inactive', inactiveMethod, 'inactive'); + + // Define events which reset the 'inactive' event handler + this._bind(hideTarget.add(tooltip), QTIP.inactiveEvents, inactiveMethod); + } + + // Filter and bind events + this._bindEvents(showEvents, hideEvents, showTarget, hideTarget, showMethod, hideMethod); + + // Mouse movement bindings + this._bind(showTarget.add(tooltip), 'mousemove', function(event) { + // Check if the tooltip hides when mouse is moved a certain distance + if('number' === typeof options.hide.distance) { + var origin = this.cache.origin || {}, + limit = this.options.hide.distance, + abs = Math.abs; + + // Check if the movement has gone beyond the limit, and hide it if so + if(abs(event.pageX - origin.pageX) >= limit || abs(event.pageY - origin.pageY) >= limit) { + this.hide(event); + } + } + + // Cache mousemove coords on show targets + this._storeMouse(event); + }); + + // Mouse positioning events + if(posOptions.target === 'mouse') { + // If mouse adjustment is on... + if(posOptions.adjust.mouse) { + // Apply a mouseleave event so we don't get problems with overlapping + if(options.hide.event) { + // Track if we're on the target or not + this._bind(showTarget, ['mouseenter', 'mouseleave'], function(event) { + if(!this.cache) {return FALSE; } + this.cache.onTarget = event.type === 'mouseenter'; + }); + } + + // Update tooltip position on mousemove + this._bind(documentTarget, 'mousemove', function(event) { + // Update the tooltip position only if the tooltip is visible and adjustment is enabled + if(this.rendered && this.cache.onTarget && !this.tooltip.hasClass(CLASS_DISABLED) && this.tooltip[0].offsetWidth > 0) { + this.reposition(event); + } + }); + } + } + + // Adjust positions of the tooltip on window resize if enabled + if(posOptions.adjust.resize || viewportTarget.length) { + this._bind( $.event.special.resize ? viewportTarget : windowTarget, 'resize', repositionMethod ); + } + + // Adjust tooltip position on scroll of the window or viewport element if present + if(posOptions.adjust.scroll) { + this._bind( windowTarget.add(posOptions.container), 'scroll', repositionMethod ); + } +}; + +// Un-assignment method +PROTOTYPE._unassignEvents = function() { + var options = this.options, + showTargets = options.show.target, + hideTargets = options.hide.target, + targets = $.grep([ + this.elements.target[0], + this.rendered && this.tooltip[0], + options.position.container[0], + options.position.viewport[0], + options.position.container.closest('html')[0], // unfocus + window, + document + ], function(i) { + return typeof i === 'object'; + }); + + // Add show and hide targets if they're valid + if(showTargets && showTargets.toArray) { + targets = targets.concat(showTargets.toArray()); + } + if(hideTargets && hideTargets.toArray) { + targets = targets.concat(hideTargets.toArray()); + } + + // Unbind the events + this._unbind(targets) + ._unbind(targets, 'destroy') + ._unbind(targets, 'inactive'); +}; + +// Apply common event handlers using delegate (avoids excessive .bind calls!) +$(function() { + delegate(SELECTOR, ['mouseenter', 'mouseleave'], function(event) { + var state = event.type === 'mouseenter', + tooltip = $(event.currentTarget), + target = $(event.relatedTarget || event.target), + options = this.options; + + // On mouseenter... + if(state) { + // Focus the tooltip on mouseenter (z-index stacking) + this.focus(event); + + // Clear hide timer on tooltip hover to prevent it from closing + tooltip.hasClass(CLASS_FIXED) && !tooltip.hasClass(CLASS_DISABLED) && clearTimeout(this.timers.hide); + } + + // On mouseleave... + else { + // When mouse tracking is enabled, hide when we leave the tooltip and not onto the show target (if a hide event is set) + if(options.position.target === 'mouse' && options.position.adjust.mouse && + options.hide.event && options.show.target && !target.closest(options.show.target[0]).length) { + this.hide(event); + } + } + + // Add hover class + tooltip.toggleClass(CLASS_HOVER, state); + }); + + // Define events which reset the 'inactive' event handler + delegate('['+ATTR_ID+']', INACTIVE_EVENTS, inactiveMethod); +}); +;// Initialization method +function init(elem, id, opts) { + var obj, posOptions, attr, config, title, + + // Setup element references + docBody = $(document.body), + + // Use document body instead of document element if needed + newTarget = elem[0] === document ? docBody : elem, + + // Grab metadata from element if plugin is present + metadata = elem.metadata ? elem.metadata(opts.metadata) : NULL, + + // If metadata type if HTML5, grab 'name' from the object instead, or use the regular data object otherwise + metadata5 = opts.metadata.type === 'html5' && metadata ? metadata[opts.metadata.name] : NULL, + + // Grab data from metadata.name (or data-qtipopts as fallback) using .data() method, + html5 = elem.data(opts.metadata.name || 'qtipopts'); + + // If we don't get an object returned attempt to parse it manualyl without parseJSON + /* eslint-disable no-empty */ + try { html5 = typeof html5 === 'string' ? $.parseJSON(html5) : html5; } + catch(e) {} + /* eslint-enable no-empty */ + + // Merge in and sanitize metadata + config = $.extend(TRUE, {}, QTIP.defaults, opts, + typeof html5 === 'object' ? sanitizeOptions(html5) : NULL, + sanitizeOptions(metadata5 || metadata)); + + // Re-grab our positioning options now we've merged our metadata and set id to passed value + posOptions = config.position; + config.id = id; + + // Setup missing content if none is detected + if('boolean' === typeof config.content.text) { + attr = elem.attr(config.content.attr); + + // Grab from supplied attribute if available + if(config.content.attr !== FALSE && attr) { config.content.text = attr; } + + // No valid content was found, abort render + else { return FALSE; } + } + + // Setup target options + if(!posOptions.container.length) { posOptions.container = docBody; } + if(posOptions.target === FALSE) { posOptions.target = newTarget; } + if(config.show.target === FALSE) { config.show.target = newTarget; } + if(config.show.solo === TRUE) { config.show.solo = posOptions.container.closest('body'); } + if(config.hide.target === FALSE) { config.hide.target = newTarget; } + if(config.position.viewport === TRUE) { config.position.viewport = posOptions.container; } + + // Ensure we only use a single container + posOptions.container = posOptions.container.eq(0); + + // Convert position corner values into x and y strings + posOptions.at = new CORNER(posOptions.at, TRUE); + posOptions.my = new CORNER(posOptions.my); + + // Destroy previous tooltip if overwrite is enabled, or skip element if not + if(elem.data(NAMESPACE)) { + if(config.overwrite) { + elem.qtip('destroy', true); + } + else if(config.overwrite === FALSE) { + return FALSE; + } + } + + // Add has-qtip attribute + elem.attr(ATTR_HAS, id); + + // Remove title attribute and store it if present + if(config.suppress && (title = elem.attr('title'))) { + // Final attr call fixes event delegatiom and IE default tooltip showing problem + elem.removeAttr('title').attr(oldtitle, title).attr('title', ''); + } + + // Initialize the tooltip and add API reference + obj = new QTip(elem, config, id, !!attr); + elem.data(NAMESPACE, obj); + + return obj; +} + +// jQuery $.fn extension method +QTIP = $.fn.qtip = function(options, notation, newValue) +{ + var command = ('' + options).toLowerCase(), // Parse command + returned = NULL, + args = $.makeArray(arguments).slice(1), + event = args[args.length - 1], + opts = this[0] ? $.data(this[0], NAMESPACE) : NULL; + + // Check for API request + if(!arguments.length && opts || command === 'api') { + return opts; + } + + // Execute API command if present + else if('string' === typeof options) { + this.each(function() { + var api = $.data(this, NAMESPACE); + if(!api) { return TRUE; } + + // Cache the event if possible + if(event && event.timeStamp) { api.cache.event = event; } + + // Check for specific API commands + if(notation && (command === 'option' || command === 'options')) { + if(newValue !== undefined || $.isPlainObject(notation)) { + api.set(notation, newValue); + } + else { + returned = api.get(notation); + return FALSE; + } + } + + // Execute API command + else if(api[command]) { + api[command].apply(api, args); + } + }); + + return returned !== NULL ? returned : this; + } + + // No API commands. validate provided options and setup qTips + else if('object' === typeof options || !arguments.length) { + // Sanitize options first + opts = sanitizeOptions($.extend(TRUE, {}, options)); + + return this.each(function(i) { + var api, id; + + // Find next available ID, or use custom ID if provided + id = $.isArray(opts.id) ? opts.id[i] : opts.id; + id = !id || id === FALSE || id.length < 1 || QTIP.api[id] ? QTIP.nextid++ : id; + + // Initialize the qTip and re-grab newly sanitized options + api = init($(this), id, opts); + if(api === FALSE) { return TRUE; } + else { QTIP.api[id] = api; } + + // Initialize plugins + $.each(PLUGINS, function() { + if(this.initialize === 'initialize') { this(api); } + }); + + // Assign initial pre-render events + api._assignInitialEvents(event); + }); + } +}; + +// Expose class +$.qtip = QTip; + +// Populated in render method +QTIP.api = {}; +;$.each({ + /* Allow other plugins to successfully retrieve the title of an element with a qTip applied */ + attr: function(attr, val) { + if(this.length) { + var self = this[0], + title = 'title', + api = $.data(self, 'qtip'); + + if(attr === title && api && api.options && 'object' === typeof api && 'object' === typeof api.options && api.options.suppress) { + if(arguments.length < 2) { + return $.attr(self, oldtitle); + } + + // If qTip is rendered and title was originally used as content, update it + if(api && api.options.content.attr === title && api.cache.attr) { + api.set('content.text', val); + } + + // Use the regular attr method to set, then cache the result + return this.attr(oldtitle, val); + } + } + + return $.fn['attr'+replaceSuffix].apply(this, arguments); + }, + + /* Allow clone to correctly retrieve cached title attributes */ + clone: function(keepData) { + // Clone our element using the real clone method + var elems = $.fn['clone'+replaceSuffix].apply(this, arguments); + + // Grab all elements with an oldtitle set, and change it to regular title attribute, if keepData is false + if(!keepData) { + elems.filter('['+oldtitle+']').attr('title', function() { + return $.attr(this, oldtitle); + }) + .removeAttr(oldtitle); + } + + return elems; + } +}, function(name, func) { + if(!func || $.fn[name+replaceSuffix]) { return TRUE; } + + var old = $.fn[name+replaceSuffix] = $.fn[name]; + $.fn[name] = function() { + return func.apply(this, arguments) || old.apply(this, arguments); + }; +}); + +/* Fire off 'removeqtip' handler in $.cleanData if jQuery UI not present (it already does similar). + * This snippet is taken directly from jQuery UI source code found here: + * http://code.jquery.com/ui/jquery-ui-git.js + */ +if(!$.ui) { + $['cleanData'+replaceSuffix] = $.cleanData; + $.cleanData = function( elems ) { + for(var i = 0, elem; (elem = $( elems[i] )).length; i++) { + if(elem.attr(ATTR_HAS)) { + /* eslint-disable no-empty */ + try { elem.triggerHandler('removeqtip'); } + catch( e ) {} + /* eslint-enable no-empty */ + } + } + $['cleanData'+replaceSuffix].apply(this, arguments); + }; +} +;// qTip version +QTIP.version = '3.0.3'; + +// Base ID for all qTips +QTIP.nextid = 0; + +// Inactive events array +QTIP.inactiveEvents = INACTIVE_EVENTS; + +// Base z-index for all qTips +QTIP.zindex = 15000; + +// Define configuration defaults +QTIP.defaults = { + prerender: FALSE, + id: FALSE, + overwrite: TRUE, + suppress: TRUE, + content: { + text: TRUE, + attr: 'title', + title: FALSE, + button: FALSE + }, + position: { + my: 'top left', + at: 'bottom right', + target: FALSE, + container: FALSE, + viewport: FALSE, + adjust: { + x: 0, y: 0, + mouse: TRUE, + scroll: TRUE, + resize: TRUE, + method: 'flipinvert flipinvert' + }, + effect: function(api, pos) { + $(this).animate(pos, { + duration: 200, + queue: FALSE + }); + } + }, + show: { + target: FALSE, + event: 'mouseenter', + effect: TRUE, + delay: 90, + solo: FALSE, + ready: FALSE, + autofocus: FALSE + }, + hide: { + target: FALSE, + event: 'mouseleave', + effect: TRUE, + delay: 0, + fixed: FALSE, + inactive: FALSE, + leave: 'window', + distance: FALSE + }, + style: { + classes: '', + widget: FALSE, + width: FALSE, + height: FALSE, + def: TRUE + }, + events: { + render: NULL, + move: NULL, + show: NULL, + hide: NULL, + toggle: NULL, + visible: NULL, + hidden: NULL, + focus: NULL, + blur: NULL + } +}; +;var TIP, +createVML, +SCALE, +PIXEL_RATIO, +BACKING_STORE_RATIO, + +// Common CSS strings +MARGIN = 'margin', +BORDER = 'border', +COLOR = 'color', +BG_COLOR = 'background-color', +TRANSPARENT = 'transparent', +IMPORTANT = ' !important', + +// Check if the browser supports <canvas/> elements +HASCANVAS = !!document.createElement('canvas').getContext, + +// Invalid colour values used in parseColours() +INVALID = /rgba?\(0, 0, 0(, 0)?\)|transparent|#123456/i; + +// Camel-case method, taken from jQuery source +// http://code.jquery.com/jquery-1.8.0.js +function camel(s) { return s.charAt(0).toUpperCase() + s.slice(1); } + +/* + * Modified from Modernizr's testPropsAll() + * http://modernizr.com/downloads/modernizr-latest.js + */ +var cssProps = {}, cssPrefixes = ['Webkit', 'O', 'Moz', 'ms']; +function vendorCss(elem, prop) { + var ucProp = prop.charAt(0).toUpperCase() + prop.slice(1), + props = (prop + ' ' + cssPrefixes.join(ucProp + ' ') + ucProp).split(' '), + cur, val, i = 0; + + // If the property has already been mapped... + if(cssProps[prop]) { return elem.css(cssProps[prop]); } + + while(cur = props[i++]) { + if((val = elem.css(cur)) !== undefined) { + cssProps[prop] = cur; + return val; + } + } +} + +// Parse a given elements CSS property into an int +function intCss(elem, prop) { + return Math.ceil(parseFloat(vendorCss(elem, prop))); +} + + +// VML creation (for IE only) +if(!HASCANVAS) { + createVML = function(tag, props, style) { + return '<qtipvml:'+tag+' xmlns="urn:schemas-microsoft.com:vml" class="qtip-vml" '+(props||'')+ + ' style="behavior: url(#default#VML); '+(style||'')+ '" />'; + }; +} + +// Canvas only definitions +else { + PIXEL_RATIO = window.devicePixelRatio || 1; + BACKING_STORE_RATIO = (function() { + var context = document.createElement('canvas').getContext('2d'); + return context.backingStorePixelRatio || context.webkitBackingStorePixelRatio || context.mozBackingStorePixelRatio || + context.msBackingStorePixelRatio || context.oBackingStorePixelRatio || 1; + })(); + SCALE = PIXEL_RATIO / BACKING_STORE_RATIO; +} + + +function Tip(qtip, options) { + this._ns = 'tip'; + this.options = options; + this.offset = options.offset; + this.size = [ options.width, options.height ]; + + // Initialize + this.qtip = qtip; + this.init(qtip); +} + +$.extend(Tip.prototype, { + init: function(qtip) { + var context, tip; + + // Create tip element and prepend to the tooltip + tip = this.element = qtip.elements.tip = $('<div />', { 'class': NAMESPACE+'-tip' }).prependTo(qtip.tooltip); + + // Create tip drawing element(s) + if(HASCANVAS) { + // save() as soon as we create the canvas element so FF2 doesn't bork on our first restore()! + context = $('<canvas />').appendTo(this.element)[0].getContext('2d'); + + // Setup constant parameters + context.lineJoin = 'miter'; + context.miterLimit = 100000; + context.save(); + } + else { + context = createVML('shape', 'coordorigin="0,0"', 'position:absolute;'); + this.element.html(context + context); + + // Prevent mousing down on the tip since it causes problems with .live() handling in IE due to VML + qtip._bind( $('*', tip).add(tip), ['click', 'mousedown'], function(event) { event.stopPropagation(); }, this._ns); + } + + // Bind update events + qtip._bind(qtip.tooltip, 'tooltipmove', this.reposition, this._ns, this); + + // Create it + this.create(); + }, + + _swapDimensions: function() { + this.size[0] = this.options.height; + this.size[1] = this.options.width; + }, + _resetDimensions: function() { + this.size[0] = this.options.width; + this.size[1] = this.options.height; + }, + + _useTitle: function(corner) { + var titlebar = this.qtip.elements.titlebar; + return titlebar && ( + corner.y === TOP || corner.y === CENTER && this.element.position().top + this.size[1] / 2 + this.options.offset < titlebar.outerHeight(TRUE) + ); + }, + + _parseCorner: function(corner) { + var my = this.qtip.options.position.my; + + // Detect corner and mimic properties + if(corner === FALSE || my === FALSE) { + corner = FALSE; + } + else if(corner === TRUE) { + corner = new CORNER( my.string() ); + } + else if(!corner.string) { + corner = new CORNER(corner); + corner.fixed = TRUE; + } + + return corner; + }, + + _parseWidth: function(corner, side, use) { + var elements = this.qtip.elements, + prop = BORDER + camel(side) + 'Width'; + + return (use ? intCss(use, prop) : + intCss(elements.content, prop) || + intCss(this._useTitle(corner) && elements.titlebar || elements.content, prop) || + intCss(elements.tooltip, prop) + ) || 0; + }, + + _parseRadius: function(corner) { + var elements = this.qtip.elements, + prop = BORDER + camel(corner.y) + camel(corner.x) + 'Radius'; + + return BROWSER.ie < 9 ? 0 : + intCss(this._useTitle(corner) && elements.titlebar || elements.content, prop) || + intCss(elements.tooltip, prop) || 0; + }, + + _invalidColour: function(elem, prop, compare) { + var val = elem.css(prop); + return !val || compare && val === elem.css(compare) || INVALID.test(val) ? FALSE : val; + }, + + _parseColours: function(corner) { + var elements = this.qtip.elements, + tip = this.element.css('cssText', ''), + borderSide = BORDER + camel(corner[ corner.precedance ]) + camel(COLOR), + colorElem = this._useTitle(corner) && elements.titlebar || elements.content, + css = this._invalidColour, color = []; + + // Attempt to detect the background colour from various elements, left-to-right precedance + color[0] = css(tip, BG_COLOR) || css(colorElem, BG_COLOR) || css(elements.content, BG_COLOR) || + css(elements.tooltip, BG_COLOR) || tip.css(BG_COLOR); + + // Attempt to detect the correct border side colour from various elements, left-to-right precedance + color[1] = css(tip, borderSide, COLOR) || css(colorElem, borderSide, COLOR) || + css(elements.content, borderSide, COLOR) || css(elements.tooltip, borderSide, COLOR) || elements.tooltip.css(borderSide); + + // Reset background and border colours + $('*', tip).add(tip).css('cssText', BG_COLOR+':'+TRANSPARENT+IMPORTANT+';'+BORDER+':0'+IMPORTANT+';'); + + return color; + }, + + _calculateSize: function(corner) { + var y = corner.precedance === Y, + width = this.options.width, + height = this.options.height, + isCenter = corner.abbrev() === 'c', + base = (y ? width: height) * (isCenter ? 0.5 : 1), + pow = Math.pow, + round = Math.round, + bigHyp, ratio, result, + + smallHyp = Math.sqrt( pow(base, 2) + pow(height, 2) ), + hyp = [ + this.border / base * smallHyp, + this.border / height * smallHyp + ]; + + hyp[2] = Math.sqrt( pow(hyp[0], 2) - pow(this.border, 2) ); + hyp[3] = Math.sqrt( pow(hyp[1], 2) - pow(this.border, 2) ); + + bigHyp = smallHyp + hyp[2] + hyp[3] + (isCenter ? 0 : hyp[0]); + ratio = bigHyp / smallHyp; + + result = [ round(ratio * width), round(ratio * height) ]; + return y ? result : result.reverse(); + }, + + // Tip coordinates calculator + _calculateTip: function(corner, size, scale) { + scale = scale || 1; + size = size || this.size; + + var width = size[0] * scale, + height = size[1] * scale, + width2 = Math.ceil(width / 2), height2 = Math.ceil(height / 2), + + // Define tip coordinates in terms of height and width values + tips = { + br: [0,0, width,height, width,0], + bl: [0,0, width,0, 0,height], + tr: [0,height, width,0, width,height], + tl: [0,0, 0,height, width,height], + tc: [0,height, width2,0, width,height], + bc: [0,0, width,0, width2,height], + rc: [0,0, width,height2, 0,height], + lc: [width,0, width,height, 0,height2] + }; + + // Set common side shapes + tips.lt = tips.br; tips.rt = tips.bl; + tips.lb = tips.tr; tips.rb = tips.tl; + + return tips[ corner.abbrev() ]; + }, + + // Tip coordinates drawer (canvas) + _drawCoords: function(context, coords) { + context.beginPath(); + context.moveTo(coords[0], coords[1]); + context.lineTo(coords[2], coords[3]); + context.lineTo(coords[4], coords[5]); + context.closePath(); + }, + + create: function() { + // Determine tip corner + var c = this.corner = (HASCANVAS || BROWSER.ie) && this._parseCorner(this.options.corner); + + // If we have a tip corner... + this.enabled = !!this.corner && this.corner.abbrev() !== 'c'; + if(this.enabled) { + // Cache it + this.qtip.cache.corner = c.clone(); + + // Create it + this.update(); + } + + // Toggle tip element + this.element.toggle(this.enabled); + + return this.corner; + }, + + update: function(corner, position) { + if(!this.enabled) { return this; } + + var elements = this.qtip.elements, + tip = this.element, + inner = tip.children(), + options = this.options, + curSize = this.size, + mimic = options.mimic, + round = Math.round, + color, precedance, context, + coords, bigCoords, translate, newSize, border; + + // Re-determine tip if not already set + if(!corner) { corner = this.qtip.cache.corner || this.corner; } + + // Use corner property if we detect an invalid mimic value + if(mimic === FALSE) { mimic = corner; } + + // Otherwise inherit mimic properties from the corner object as necessary + else { + mimic = new CORNER(mimic); + mimic.precedance = corner.precedance; + + if(mimic.x === 'inherit') { mimic.x = corner.x; } + else if(mimic.y === 'inherit') { mimic.y = corner.y; } + else if(mimic.x === mimic.y) { + mimic[ corner.precedance ] = corner[ corner.precedance ]; + } + } + precedance = mimic.precedance; + + // Ensure the tip width.height are relative to the tip position + if(corner.precedance === X) { this._swapDimensions(); } + else { this._resetDimensions(); } + + // Update our colours + color = this.color = this._parseColours(corner); + + // Detect border width, taking into account colours + if(color[1] !== TRANSPARENT) { + // Grab border width + border = this.border = this._parseWidth(corner, corner[corner.precedance]); + + // If border width isn't zero, use border color as fill if it's not invalid (1.0 style tips) + if(options.border && border < 1 && !INVALID.test(color[1])) { color[0] = color[1]; } + + // Set border width (use detected border width if options.border is true) + this.border = border = options.border !== TRUE ? options.border : border; + } + + // Border colour was invalid, set border to zero + else { this.border = border = 0; } + + // Determine tip size + newSize = this.size = this._calculateSize(corner); + tip.css({ + width: newSize[0], + height: newSize[1], + lineHeight: newSize[1]+'px' + }); + + // Calculate tip translation + if(corner.precedance === Y) { + translate = [ + round(mimic.x === LEFT ? border : mimic.x === RIGHT ? newSize[0] - curSize[0] - border : (newSize[0] - curSize[0]) / 2), + round(mimic.y === TOP ? newSize[1] - curSize[1] : 0) + ]; + } + else { + translate = [ + round(mimic.x === LEFT ? newSize[0] - curSize[0] : 0), + round(mimic.y === TOP ? border : mimic.y === BOTTOM ? newSize[1] - curSize[1] - border : (newSize[1] - curSize[1]) / 2) + ]; + } + + // Canvas drawing implementation + if(HASCANVAS) { + // Grab canvas context and clear/save it + context = inner[0].getContext('2d'); + context.restore(); context.save(); + context.clearRect(0,0,6000,6000); + + // Calculate coordinates + coords = this._calculateTip(mimic, curSize, SCALE); + bigCoords = this._calculateTip(mimic, this.size, SCALE); + + // Set the canvas size using calculated size + inner.attr(WIDTH, newSize[0] * SCALE).attr(HEIGHT, newSize[1] * SCALE); + inner.css(WIDTH, newSize[0]).css(HEIGHT, newSize[1]); + + // Draw the outer-stroke tip + this._drawCoords(context, bigCoords); + context.fillStyle = color[1]; + context.fill(); + + // Draw the actual tip + context.translate(translate[0] * SCALE, translate[1] * SCALE); + this._drawCoords(context, coords); + context.fillStyle = color[0]; + context.fill(); + } + + // VML (IE Proprietary implementation) + else { + // Calculate coordinates + coords = this._calculateTip(mimic); + + // Setup coordinates string + coords = 'm' + coords[0] + ',' + coords[1] + ' l' + coords[2] + + ',' + coords[3] + ' ' + coords[4] + ',' + coords[5] + ' xe'; + + // Setup VML-specific offset for pixel-perfection + translate[2] = border && /^(r|b)/i.test(corner.string()) ? + BROWSER.ie === 8 ? 2 : 1 : 0; + + // Set initial CSS + inner.css({ + coordsize: newSize[0]+border + ' ' + newSize[1]+border, + antialias: ''+(mimic.string().indexOf(CENTER) > -1), + left: translate[0] - translate[2] * Number(precedance === X), + top: translate[1] - translate[2] * Number(precedance === Y), + width: newSize[0] + border, + height: newSize[1] + border + }) + .each(function(i) { + var $this = $(this); + + // Set shape specific attributes + $this[ $this.prop ? 'prop' : 'attr' ]({ + coordsize: newSize[0]+border + ' ' + newSize[1]+border, + path: coords, + fillcolor: color[0], + filled: !!i, + stroked: !i + }) + .toggle(!!(border || i)); + + // Check if border is enabled and add stroke element + !i && $this.html( createVML( + 'stroke', 'weight="'+border*2+'px" color="'+color[1]+'" miterlimit="1000" joinstyle="miter"' + ) ); + }); + } + + // Opera bug #357 - Incorrect tip position + // https://github.com/Craga89/qTip2/issues/367 + window.opera && setTimeout(function() { + elements.tip.css({ + display: 'inline-block', + visibility: 'visible' + }); + }, 1); + + // Position if needed + if(position !== FALSE) { this.calculate(corner, newSize); } + }, + + calculate: function(corner, size) { + if(!this.enabled) { return FALSE; } + + var self = this, + elements = this.qtip.elements, + tip = this.element, + userOffset = this.options.offset, + position = {}, + precedance, corners; + + // Inherit corner if not provided + corner = corner || this.corner; + precedance = corner.precedance; + + // Determine which tip dimension to use for adjustment + size = size || this._calculateSize(corner); + + // Setup corners and offset array + corners = [ corner.x, corner.y ]; + if(precedance === X) { corners.reverse(); } + + // Calculate tip position + $.each(corners, function(i, side) { + var b, bc, br; + + if(side === CENTER) { + b = precedance === Y ? LEFT : TOP; + position[ b ] = '50%'; + position[MARGIN+'-' + b] = -Math.round(size[ precedance === Y ? 0 : 1 ] / 2) + userOffset; + } + else { + b = self._parseWidth(corner, side, elements.tooltip); + bc = self._parseWidth(corner, side, elements.content); + br = self._parseRadius(corner); + + position[ side ] = Math.max(-self.border, i ? bc : userOffset + (br > b ? br : -b)); + } + }); + + // Adjust for tip size + position[ corner[precedance] ] -= size[ precedance === X ? 0 : 1 ]; + + // Set and return new position + tip.css({ margin: '', top: '', bottom: '', left: '', right: '' }).css(position); + return position; + }, + + reposition: function(event, api, pos) { + if(!this.enabled) { return; } + + var cache = api.cache, + newCorner = this.corner.clone(), + adjust = pos.adjusted, + method = api.options.position.adjust.method.split(' '), + horizontal = method[0], + vertical = method[1] || method[0], + shift = { left: FALSE, top: FALSE, x: 0, y: 0 }, + offset, css = {}, props; + + function shiftflip(direction, precedance, popposite, side, opposite) { + // Horizontal - Shift or flip method + if(direction === SHIFT && newCorner.precedance === precedance && adjust[side] && newCorner[popposite] !== CENTER) { + newCorner.precedance = newCorner.precedance === X ? Y : X; + } + else if(direction !== SHIFT && adjust[side]){ + newCorner[precedance] = newCorner[precedance] === CENTER ? + adjust[side] > 0 ? side : opposite : + newCorner[precedance] === side ? opposite : side; + } + } + + function shiftonly(xy, side, opposite) { + if(newCorner[xy] === CENTER) { + css[MARGIN+'-'+side] = shift[xy] = offset[MARGIN+'-'+side] - adjust[side]; + } + else { + props = offset[opposite] !== undefined ? + [ adjust[side], -offset[side] ] : [ -adjust[side], offset[side] ]; + + if( (shift[xy] = Math.max(props[0], props[1])) > props[0] ) { + pos[side] -= adjust[side]; + shift[side] = FALSE; + } + + css[ offset[opposite] !== undefined ? opposite : side ] = shift[xy]; + } + } + + // If our tip position isn't fixed e.g. doesn't adjust with viewport... + if(this.corner.fixed !== TRUE) { + // Perform shift/flip adjustments + shiftflip(horizontal, X, Y, LEFT, RIGHT); + shiftflip(vertical, Y, X, TOP, BOTTOM); + + // Update and redraw the tip if needed (check cached details of last drawn tip) + if(newCorner.string() !== cache.corner.string() || cache.cornerTop !== adjust.top || cache.cornerLeft !== adjust.left) { + this.update(newCorner, FALSE); + } + } + + // Setup tip offset properties + offset = this.calculate(newCorner); + + // Readjust offset object to make it left/top + if(offset.right !== undefined) { offset.left = -offset.right; } + if(offset.bottom !== undefined) { offset.top = -offset.bottom; } + offset.user = this.offset; + + // Perform shift adjustments + shift.left = horizontal === SHIFT && !!adjust.left; + if(shift.left) { + shiftonly(X, LEFT, RIGHT); + } + shift.top = vertical === SHIFT && !!adjust.top; + if(shift.top) { + shiftonly(Y, TOP, BOTTOM); + } + + /* + * If the tip is adjusted in both dimensions, or in a + * direction that would cause it to be anywhere but the + * outer border, hide it! + */ + this.element.css(css).toggle( + !(shift.x && shift.y || newCorner.x === CENTER && shift.y || newCorner.y === CENTER && shift.x) + ); + + // Adjust position to accomodate tip dimensions + pos.left -= offset.left.charAt ? offset.user : + horizontal !== SHIFT || shift.top || !shift.left && !shift.top ? offset.left + this.border : 0; + pos.top -= offset.top.charAt ? offset.user : + vertical !== SHIFT || shift.left || !shift.left && !shift.top ? offset.top + this.border : 0; + + // Cache details + cache.cornerLeft = adjust.left; cache.cornerTop = adjust.top; + cache.corner = newCorner.clone(); + }, + + destroy: function() { + // Unbind events + this.qtip._unbind(this.qtip.tooltip, this._ns); + + // Remove the tip element(s) + if(this.qtip.elements.tip) { + this.qtip.elements.tip.find('*') + .remove().end().remove(); + } + } +}); + +TIP = PLUGINS.tip = function(api) { + return new Tip(api, api.options.style.tip); +}; + +// Initialize tip on render +TIP.initialize = 'render'; + +// Setup plugin sanitization options +TIP.sanitize = function(options) { + if(options.style && 'tip' in options.style) { + var opts = options.style.tip; + if(typeof opts !== 'object') { opts = options.style.tip = { corner: opts }; } + if(!(/string|boolean/i).test(typeof opts.corner)) { opts.corner = TRUE; } + } +}; + +// Add new option checks for the plugin +CHECKS.tip = { + '^position.my|style.tip.(corner|mimic|border)$': function() { + // Make sure a tip can be drawn + this.create(); + + // Reposition the tooltip + this.qtip.reposition(); + }, + '^style.tip.(height|width)$': function(obj) { + // Re-set dimensions and redraw the tip + this.size = [ obj.width, obj.height ]; + this.update(); + + // Reposition the tooltip + this.qtip.reposition(); + }, + '^content.title|style.(classes|widget)$': function() { + this.update(); + } +}; + +// Extend original qTip defaults +$.extend(TRUE, QTIP.defaults, { + style: { + tip: { + corner: TRUE, + mimic: FALSE, + width: 6, + height: 6, + border: TRUE, + offset: 0 + } + } +}); +;var MODAL, OVERLAY, + MODALCLASS = 'qtip-modal', + MODALSELECTOR = '.'+MODALCLASS; + +OVERLAY = function() +{ + var self = this, + focusableElems = {}, + current, + prevState, + elem; + + // Modified code from jQuery UI 1.10.0 source + // http://code.jquery.com/ui/1.10.0/jquery-ui.js + function focusable(element) { + // Use the defined focusable checker when possible + if($.expr[':'].focusable) { return $.expr[':'].focusable; } + + var isTabIndexNotNaN = !isNaN($.attr(element, 'tabindex')), + nodeName = element.nodeName && element.nodeName.toLowerCase(), + map, mapName, img; + + if('area' === nodeName) { + map = element.parentNode; + mapName = map.name; + if(!element.href || !mapName || map.nodeName.toLowerCase() !== 'map') { + return false; + } + img = $('img[usemap=#' + mapName + ']')[0]; + return !!img && img.is(':visible'); + } + + return /input|select|textarea|button|object/.test( nodeName ) ? + !element.disabled : + 'a' === nodeName ? + element.href || isTabIndexNotNaN : + isTabIndexNotNaN + ; + } + + // Focus inputs using cached focusable elements (see update()) + function focusInputs(blurElems) { + // Blurring body element in IE causes window.open windows to unfocus! + if(focusableElems.length < 1 && blurElems.length) { blurElems.not('body').blur(); } + + // Focus the inputs + else { focusableElems.first().focus(); } + } + + // Steal focus from elements outside tooltip + function stealFocus(event) { + if(!elem.is(':visible')) { return; } + + var target = $(event.target), + tooltip = current.tooltip, + container = target.closest(SELECTOR), + targetOnTop; + + // Determine if input container target is above this + targetOnTop = container.length < 1 ? FALSE : + parseInt(container[0].style.zIndex, 10) > parseInt(tooltip[0].style.zIndex, 10); + + // If we're showing a modal, but focus has landed on an input below + // this modal, divert focus to the first visible input in this modal + // or if we can't find one... the tooltip itself + if(!targetOnTop && target.closest(SELECTOR)[0] !== tooltip[0]) { + focusInputs(target); + } + } + + $.extend(self, { + init: function() { + // Create document overlay + elem = self.elem = $('<div />', { + id: 'qtip-overlay', + html: '<div></div>', + mousedown: function() { return FALSE; } + }) + .hide(); + + // Make sure we can't focus anything outside the tooltip + $(document.body).bind('focusin'+MODALSELECTOR, stealFocus); + + // Apply keyboard "Escape key" close handler + $(document).bind('keydown'+MODALSELECTOR, function(event) { + if(current && current.options.show.modal.escape && event.keyCode === 27) { + current.hide(event); + } + }); + + // Apply click handler for blur option + elem.bind('click'+MODALSELECTOR, function(event) { + if(current && current.options.show.modal.blur) { + current.hide(event); + } + }); + + return self; + }, + + update: function(api) { + // Update current API reference + current = api; + + // Update focusable elements if enabled + if(api.options.show.modal.stealfocus !== FALSE) { + focusableElems = api.tooltip.find('*').filter(function() { + return focusable(this); + }); + } + else { focusableElems = []; } + }, + + toggle: function(api, state, duration) { + var tooltip = api.tooltip, + options = api.options.show.modal, + effect = options.effect, + type = state ? 'show': 'hide', + visible = elem.is(':visible'), + visibleModals = $(MODALSELECTOR).filter(':visible:not(:animated)').not(tooltip); + + // Set active tooltip API reference + self.update(api); + + // If the modal can steal the focus... + // Blur the current item and focus anything in the modal we an + if(state && options.stealfocus !== FALSE) { + focusInputs( $(':focus') ); + } + + // Toggle backdrop cursor style on show + elem.toggleClass('blurs', options.blur); + + // Append to body on show + if(state) { + elem.appendTo(document.body); + } + + // Prevent modal from conflicting with show.solo, and don't hide backdrop is other modals are visible + if(elem.is(':animated') && visible === state && prevState !== FALSE || !state && visibleModals.length) { + return self; + } + + // Stop all animations + elem.stop(TRUE, FALSE); + + // Use custom function if provided + if($.isFunction(effect)) { + effect.call(elem, state); + } + + // If no effect type is supplied, use a simple toggle + else if(effect === FALSE) { + elem[ type ](); + } + + // Use basic fade function + else { + elem.fadeTo( parseInt(duration, 10) || 90, state ? 1 : 0, function() { + if(!state) { elem.hide(); } + }); + } + + // Reset position and detach from body on hide + if(!state) { + elem.queue(function(next) { + elem.css({ left: '', top: '' }); + if(!$(MODALSELECTOR).length) { elem.detach(); } + next(); + }); + } + + // Cache the state + prevState = state; + + // If the tooltip is destroyed, set reference to null + if(current.destroyed) { current = NULL; } + + return self; + } + }); + + self.init(); +}; +OVERLAY = new OVERLAY(); + +function Modal(api, options) { + this.options = options; + this._ns = '-modal'; + + this.qtip = api; + this.init(api); +} + +$.extend(Modal.prototype, { + init: function(qtip) { + var tooltip = qtip.tooltip; + + // If modal is disabled... return + if(!this.options.on) { return this; } + + // Set overlay reference + qtip.elements.overlay = OVERLAY.elem; + + // Add unique attribute so we can grab modal tooltips easily via a SELECTOR, and set z-index + tooltip.addClass(MODALCLASS).css('z-index', QTIP.modal_zindex + $(MODALSELECTOR).length); + + // Apply our show/hide/focus modal events + qtip._bind(tooltip, ['tooltipshow', 'tooltiphide'], function(event, api, duration) { + var oEvent = event.originalEvent; + + // Make sure mouseout doesn't trigger a hide when showing the modal and mousing onto backdrop + if(event.target === tooltip[0]) { + if(oEvent && event.type === 'tooltiphide' && /mouse(leave|enter)/.test(oEvent.type) && $(oEvent.relatedTarget).closest(OVERLAY.elem[0]).length) { + /* eslint-disable no-empty */ + try { event.preventDefault(); } + catch(e) {} + /* eslint-enable no-empty */ + } + else if(!oEvent || oEvent && oEvent.type !== 'tooltipsolo') { + this.toggle(event, event.type === 'tooltipshow', duration); + } + } + }, this._ns, this); + + // Adjust modal z-index on tooltip focus + qtip._bind(tooltip, 'tooltipfocus', function(event, api) { + // If focus was cancelled before it reached us, don't do anything + if(event.isDefaultPrevented() || event.target !== tooltip[0]) { return; } + + var qtips = $(MODALSELECTOR), + + // Keep the modal's lower than other, regular qtips + newIndex = QTIP.modal_zindex + qtips.length, + curIndex = parseInt(tooltip[0].style.zIndex, 10); + + // Set overlay z-index + OVERLAY.elem[0].style.zIndex = newIndex - 1; + + // Reduce modal z-index's and keep them properly ordered + qtips.each(function() { + if(this.style.zIndex > curIndex) { + this.style.zIndex -= 1; + } + }); + + // Fire blur event for focused tooltip + qtips.filter('.' + CLASS_FOCUS).qtip('blur', event.originalEvent); + + // Set the new z-index + tooltip.addClass(CLASS_FOCUS)[0].style.zIndex = newIndex; + + // Set current + OVERLAY.update(api); + + // Prevent default handling + /* eslint-disable no-empty */ + try { event.preventDefault(); } + catch(e) {} + /* eslint-enable no-empty */ + }, this._ns, this); + + // Focus any other visible modals when this one hides + qtip._bind(tooltip, 'tooltiphide', function(event) { + if(event.target === tooltip[0]) { + $(MODALSELECTOR).filter(':visible').not(tooltip).last().qtip('focus', event); + } + }, this._ns, this); + }, + + toggle: function(event, state, duration) { + // Make sure default event hasn't been prevented + if(event && event.isDefaultPrevented()) { return this; } + + // Toggle it + OVERLAY.toggle(this.qtip, !!state, duration); + }, + + destroy: function() { + // Remove modal class + this.qtip.tooltip.removeClass(MODALCLASS); + + // Remove bound events + this.qtip._unbind(this.qtip.tooltip, this._ns); + + // Delete element reference + OVERLAY.toggle(this.qtip, FALSE); + delete this.qtip.elements.overlay; + } +}); + + +MODAL = PLUGINS.modal = function(api) { + return new Modal(api, api.options.show.modal); +}; + +// Setup sanitiztion rules +MODAL.sanitize = function(opts) { + if(opts.show) { + if(typeof opts.show.modal !== 'object') { opts.show.modal = { on: !!opts.show.modal }; } + else if(typeof opts.show.modal.on === 'undefined') { opts.show.modal.on = TRUE; } + } +}; + +// Base z-index for all modal tooltips (use qTip core z-index as a base) +/* eslint-disable camelcase */ +QTIP.modal_zindex = QTIP.zindex - 200; +/* eslint-enable camelcase */ + +// Plugin needs to be initialized on render +MODAL.initialize = 'render'; + +// Setup option set checks +CHECKS.modal = { + '^show.modal.(on|blur)$': function() { + // Initialise + this.destroy(); + this.init(); + + // Show the modal if not visible already and tooltip is visible + this.qtip.elems.overlay.toggle( + this.qtip.tooltip[0].offsetWidth > 0 + ); + } +}; + +// Extend original api defaults +$.extend(TRUE, QTIP.defaults, { + show: { + modal: { + on: FALSE, + effect: TRUE, + blur: TRUE, + stealfocus: TRUE, + escape: TRUE + } + } +}); +;PLUGINS.viewport = function(api, position, posOptions, targetWidth, targetHeight, elemWidth, elemHeight) +{ + var target = posOptions.target, + tooltip = api.elements.tooltip, + my = posOptions.my, + at = posOptions.at, + adjust = posOptions.adjust, + method = adjust.method.split(' '), + methodX = method[0], + methodY = method[1] || method[0], + viewport = posOptions.viewport, + container = posOptions.container, + adjusted = { left: 0, top: 0 }, + fixed, newMy, containerOffset, containerStatic, + viewportWidth, viewportHeight, viewportScroll, viewportOffset; + + // If viewport is not a jQuery element, or it's the window/document, or no adjustment method is used... return + if(!viewport.jquery || target[0] === window || target[0] === document.body || adjust.method === 'none') { + return adjusted; + } + + // Cach container details + containerOffset = container.offset() || adjusted; + containerStatic = container.css('position') === 'static'; + + // Cache our viewport details + fixed = tooltip.css('position') === 'fixed'; + viewportWidth = viewport[0] === window ? viewport.width() : viewport.outerWidth(FALSE); + viewportHeight = viewport[0] === window ? viewport.height() : viewport.outerHeight(FALSE); + viewportScroll = { left: fixed ? 0 : viewport.scrollLeft(), top: fixed ? 0 : viewport.scrollTop() }; + viewportOffset = viewport.offset() || adjusted; + + // Generic calculation method + function calculate(side, otherSide, type, adjustment, side1, side2, lengthName, targetLength, elemLength) { + var initialPos = position[side1], + mySide = my[side], + atSide = at[side], + isShift = type === SHIFT, + myLength = mySide === side1 ? elemLength : mySide === side2 ? -elemLength : -elemLength / 2, + atLength = atSide === side1 ? targetLength : atSide === side2 ? -targetLength : -targetLength / 2, + sideOffset = viewportScroll[side1] + viewportOffset[side1] - (containerStatic ? 0 : containerOffset[side1]), + overflow1 = sideOffset - initialPos, + overflow2 = initialPos + elemLength - (lengthName === WIDTH ? viewportWidth : viewportHeight) - sideOffset, + offset = myLength - (my.precedance === side || mySide === my[otherSide] ? atLength : 0) - (atSide === CENTER ? targetLength / 2 : 0); + + // shift + if(isShift) { + offset = (mySide === side1 ? 1 : -1) * myLength; + + // Adjust position but keep it within viewport dimensions + position[side1] += overflow1 > 0 ? overflow1 : overflow2 > 0 ? -overflow2 : 0; + position[side1] = Math.max( + -containerOffset[side1] + viewportOffset[side1], + initialPos - offset, + Math.min( + Math.max( + -containerOffset[side1] + viewportOffset[side1] + (lengthName === WIDTH ? viewportWidth : viewportHeight), + initialPos + offset + ), + position[side1], + + // Make sure we don't adjust complete off the element when using 'center' + mySide === 'center' ? initialPos - myLength : 1E9 + ) + ); + + } + + // flip/flipinvert + else { + // Update adjustment amount depending on if using flipinvert or flip + adjustment *= type === FLIPINVERT ? 2 : 0; + + // Check for overflow on the left/top + if(overflow1 > 0 && (mySide !== side1 || overflow2 > 0)) { + position[side1] -= offset + adjustment; + newMy.invert(side, side1); + } + + // Check for overflow on the bottom/right + else if(overflow2 > 0 && (mySide !== side2 || overflow1 > 0) ) { + position[side1] -= (mySide === CENTER ? -offset : offset) + adjustment; + newMy.invert(side, side2); + } + + // Make sure we haven't made things worse with the adjustment and reset if so + if(position[side1] < viewportScroll[side1] && -position[side1] > overflow2) { + position[side1] = initialPos; newMy = my.clone(); + } + } + + return position[side1] - initialPos; + } + + // Set newMy if using flip or flipinvert methods + if(methodX !== 'shift' || methodY !== 'shift') { newMy = my.clone(); } + + // Adjust position based onviewport and adjustment options + adjusted = { + left: methodX !== 'none' ? calculate( X, Y, methodX, adjust.x, LEFT, RIGHT, WIDTH, targetWidth, elemWidth ) : 0, + top: methodY !== 'none' ? calculate( Y, X, methodY, adjust.y, TOP, BOTTOM, HEIGHT, targetHeight, elemHeight ) : 0, + my: newMy + }; + + return adjusted; +}; +;PLUGINS.polys = { + // POLY area coordinate calculator + // Special thanks to Ed Cradock for helping out with this. + // Uses a binary search algorithm to find suitable coordinates. + polygon: function(baseCoords, corner) { + var result = { + width: 0, height: 0, + position: { + top: 1e10, right: 0, + bottom: 0, left: 1e10 + }, + adjustable: FALSE + }, + i = 0, next, + coords = [], + compareX = 1, compareY = 1, + realX = 0, realY = 0, + newWidth, newHeight; + + // First pass, sanitize coords and determine outer edges + i = baseCoords.length; + while(i--) { + next = [ parseInt(baseCoords[--i], 10), parseInt(baseCoords[i+1], 10) ]; + + if(next[0] > result.position.right){ result.position.right = next[0]; } + if(next[0] < result.position.left){ result.position.left = next[0]; } + if(next[1] > result.position.bottom){ result.position.bottom = next[1]; } + if(next[1] < result.position.top){ result.position.top = next[1]; } + + coords.push(next); + } + + // Calculate height and width from outer edges + newWidth = result.width = Math.abs(result.position.right - result.position.left); + newHeight = result.height = Math.abs(result.position.bottom - result.position.top); + + // If it's the center corner... + if(corner.abbrev() === 'c') { + result.position = { + left: result.position.left + result.width / 2, + top: result.position.top + result.height / 2 + }; + } + else { + // Second pass, use a binary search algorithm to locate most suitable coordinate + while(newWidth > 0 && newHeight > 0 && compareX > 0 && compareY > 0) + { + newWidth = Math.floor(newWidth / 2); + newHeight = Math.floor(newHeight / 2); + + if(corner.x === LEFT){ compareX = newWidth; } + else if(corner.x === RIGHT){ compareX = result.width - newWidth; } + else{ compareX += Math.floor(newWidth / 2); } + + if(corner.y === TOP){ compareY = newHeight; } + else if(corner.y === BOTTOM){ compareY = result.height - newHeight; } + else{ compareY += Math.floor(newHeight / 2); } + + i = coords.length; + while(i--) + { + if(coords.length < 2){ break; } + + realX = coords[i][0] - result.position.left; + realY = coords[i][1] - result.position.top; + + if( + corner.x === LEFT && realX >= compareX || + corner.x === RIGHT && realX <= compareX || + corner.x === CENTER && (realX < compareX || realX > result.width - compareX) || + corner.y === TOP && realY >= compareY || + corner.y === BOTTOM && realY <= compareY || + corner.y === CENTER && (realY < compareY || realY > result.height - compareY)) { + coords.splice(i, 1); + } + } + } + result.position = { left: coords[0][0], top: coords[0][1] }; + } + + return result; + }, + + rect: function(ax, ay, bx, by) { + return { + width: Math.abs(bx - ax), + height: Math.abs(by - ay), + position: { + left: Math.min(ax, bx), + top: Math.min(ay, by) + } + }; + }, + + _angles: { + tc: 3 / 2, tr: 7 / 4, tl: 5 / 4, + bc: 1 / 2, br: 1 / 4, bl: 3 / 4, + rc: 2, lc: 1, c: 0 + }, + ellipse: function(cx, cy, rx, ry, corner) { + var c = PLUGINS.polys._angles[ corner.abbrev() ], + rxc = c === 0 ? 0 : rx * Math.cos( c * Math.PI ), + rys = ry * Math.sin( c * Math.PI ); + + return { + width: rx * 2 - Math.abs(rxc), + height: ry * 2 - Math.abs(rys), + position: { + left: cx + rxc, + top: cy + rys + }, + adjustable: FALSE + }; + }, + circle: function(cx, cy, r, corner) { + return PLUGINS.polys.ellipse(cx, cy, r, r, corner); + } +}; +;PLUGINS.svg = function(api, svg, corner) +{ + var elem = svg[0], + root = $(elem.ownerSVGElement), + ownerDocument = elem.ownerDocument, + strokeWidth2 = (parseInt(svg.css('stroke-width'), 10) || 0) / 2, + frameOffset, mtx, transformed, + len, next, i, points, + result, position; + + // Ascend the parentNode chain until we find an element with getBBox() + while(!elem.getBBox) { elem = elem.parentNode; } + if(!elem.getBBox || !elem.parentNode) { return FALSE; } + + // Determine which shape calculation to use + switch(elem.nodeName) { + case 'ellipse': + case 'circle': + result = PLUGINS.polys.ellipse( + elem.cx.baseVal.value, + elem.cy.baseVal.value, + (elem.rx || elem.r).baseVal.value + strokeWidth2, + (elem.ry || elem.r).baseVal.value + strokeWidth2, + corner + ); + break; + + case 'line': + case 'polygon': + case 'polyline': + // Determine points object (line has none, so mimic using array) + points = elem.points || [ + { x: elem.x1.baseVal.value, y: elem.y1.baseVal.value }, + { x: elem.x2.baseVal.value, y: elem.y2.baseVal.value } + ]; + + for(result = [], i = -1, len = points.numberOfItems || points.length; ++i < len;) { + next = points.getItem ? points.getItem(i) : points[i]; + result.push.apply(result, [next.x, next.y]); + } + + result = PLUGINS.polys.polygon(result, corner); + break; + + // Unknown shape or rectangle? Use bounding box + default: + result = elem.getBBox(); + result = { + width: result.width, + height: result.height, + position: { + left: result.x, + top: result.y + } + }; + break; + } + + // Shortcut assignments + position = result.position; + root = root[0]; + + // Convert position into a pixel value + if(root.createSVGPoint) { + mtx = elem.getScreenCTM(); + points = root.createSVGPoint(); + + points.x = position.left; + points.y = position.top; + transformed = points.matrixTransform( mtx ); + position.left = transformed.x; + position.top = transformed.y; + } + + // Check the element is not in a child document, and if so, adjust for frame elements offset + if(ownerDocument !== document && api.position.target !== 'mouse') { + frameOffset = $((ownerDocument.defaultView || ownerDocument.parentWindow).frameElement).offset(); + if(frameOffset) { + position.left += frameOffset.left; + position.top += frameOffset.top; + } + } + + // Adjust by scroll offset of owner document + ownerDocument = $(ownerDocument); + position.left += ownerDocument.scrollLeft(); + position.top += ownerDocument.scrollTop(); + + return result; +}; +;PLUGINS.imagemap = function(api, area, corner) +{ + if(!area.jquery) { area = $(area); } + + var shape = (area.attr('shape') || 'rect').toLowerCase().replace('poly', 'polygon'), + image = $('img[usemap="#'+area.parent('map').attr('name')+'"]'), + coordsString = $.trim(area.attr('coords')), + coordsArray = coordsString.replace(/,$/, '').split(','), + imageOffset, coords, i, result, len; + + // If we can't find the image using the map... + if(!image.length) { return FALSE; } + + // Pass coordinates string if polygon + if(shape === 'polygon') { + result = PLUGINS.polys.polygon(coordsArray, corner); + } + + // Otherwise parse the coordinates and pass them as arguments + else if(PLUGINS.polys[shape]) { + for(i = -1, len = coordsArray.length, coords = []; ++i < len;) { + coords.push( parseInt(coordsArray[i], 10) ); + } + + result = PLUGINS.polys[shape].apply( + this, coords.concat(corner) + ); + } + + // If no shapre calculation method was found, return false + else { return FALSE; } + + // Make sure we account for padding and borders on the image + imageOffset = image.offset(); + imageOffset.left += Math.ceil((image.outerWidth(FALSE) - image.width()) / 2); + imageOffset.top += Math.ceil((image.outerHeight(FALSE) - image.height()) / 2); + + // Add image position to offset coordinates + result.position.left += imageOffset.left; + result.position.top += imageOffset.top; + + return result; +}; +;var IE6, + +/* + * BGIFrame adaption (http://plugins.jquery.com/project/bgiframe) + * Special thanks to Brandon Aaron + */ +BGIFRAME = '<iframe class="qtip-bgiframe" frameborder="0" tabindex="-1" src="javascript:\'\';" ' + + ' style="display:block; position:absolute; z-index:-1; filter:alpha(opacity=0); ' + + '-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";"></iframe>'; + +function Ie6(api) { + this._ns = 'ie6'; + + this.qtip = api; + this.init(api); +} + +$.extend(Ie6.prototype, { + _scroll : function() { + var overlay = this.qtip.elements.overlay; + overlay && (overlay[0].style.top = $(window).scrollTop() + 'px'); + }, + + init: function(qtip) { + var tooltip = qtip.tooltip; + + // Create the BGIFrame element if needed + if($('select, object').length < 1) { + this.bgiframe = qtip.elements.bgiframe = $(BGIFRAME).appendTo(tooltip); + + // Update BGIFrame on tooltip move + qtip._bind(tooltip, 'tooltipmove', this.adjustBGIFrame, this._ns, this); + } + + // redraw() container for width/height calculations + this.redrawContainer = $('<div/>', { id: NAMESPACE+'-rcontainer' }) + .appendTo(document.body); + + // Fixup modal plugin if present too + if( qtip.elements.overlay && qtip.elements.overlay.addClass('qtipmodal-ie6fix') ) { + qtip._bind(window, ['scroll', 'resize'], this._scroll, this._ns, this); + qtip._bind(tooltip, ['tooltipshow'], this._scroll, this._ns, this); + } + + // Set dimensions + this.redraw(); + }, + + adjustBGIFrame: function() { + var tooltip = this.qtip.tooltip, + dimensions = { + height: tooltip.outerHeight(FALSE), + width: tooltip.outerWidth(FALSE) + }, + plugin = this.qtip.plugins.tip, + tip = this.qtip.elements.tip, + tipAdjust, offset; + + // Adjust border offset + offset = parseInt(tooltip.css('borderLeftWidth'), 10) || 0; + offset = { left: -offset, top: -offset }; + + // Adjust for tips plugin + if(plugin && tip) { + tipAdjust = plugin.corner.precedance === 'x' ? [WIDTH, LEFT] : [HEIGHT, TOP]; + offset[ tipAdjust[1] ] -= tip[ tipAdjust[0] ](); + } + + // Update bgiframe + this.bgiframe.css(offset).css(dimensions); + }, + + // Max/min width simulator function + redraw: function() { + if(this.qtip.rendered < 1 || this.drawing) { return this; } + + var tooltip = this.qtip.tooltip, + style = this.qtip.options.style, + container = this.qtip.options.position.container, + perc, width, max, min; + + // Set drawing flag + this.qtip.drawing = 1; + + // If tooltip has a set height/width, just set it... like a boss! + if(style.height) { tooltip.css(HEIGHT, style.height); } + if(style.width) { tooltip.css(WIDTH, style.width); } + + // Simulate max/min width if not set width present... + else { + // Reset width and add fluid class + tooltip.css(WIDTH, '').appendTo(this.redrawContainer); + + // Grab our tooltip width (add 1 if odd so we don't get wrapping problems.. huzzah!) + width = tooltip.width(); + if(width % 2 < 1) { width += 1; } + + // Grab our max/min properties + max = tooltip.css('maxWidth') || ''; + min = tooltip.css('minWidth') || ''; + + // Parse into proper pixel values + perc = (max + min).indexOf('%') > -1 ? container.width() / 100 : 0; + max = (max.indexOf('%') > -1 ? perc : 1 * parseInt(max, 10)) || width; + min = (min.indexOf('%') > -1 ? perc : 1 * parseInt(min, 10)) || 0; + + // Determine new dimension size based on max/min/current values + width = max + min ? Math.min(Math.max(width, min), max) : width; + + // Set the newly calculated width and remvoe fluid class + tooltip.css(WIDTH, Math.round(width)).appendTo(container); + } + + // Set drawing flag + this.drawing = 0; + + return this; + }, + + destroy: function() { + // Remove iframe + this.bgiframe && this.bgiframe.remove(); + + // Remove bound events + this.qtip._unbind([window, this.qtip.tooltip], this._ns); + } +}); + +IE6 = PLUGINS.ie6 = function(api) { + // Proceed only if the browser is IE6 + return BROWSER.ie === 6 ? new Ie6(api) : FALSE; +}; + +IE6.initialize = 'render'; + +CHECKS.ie6 = { + '^content|style$': function() { + this.redraw(); + } +}; +;})); +}( window, document )); diff --git a/js/jquery.qtip.min.js b/js/jquery.qtip.min.js new file mode 100644 index 0000000..8100d78 --- /dev/null +++ b/js/jquery.qtip.min.js @@ -0,0 +1,4 @@ +/* qtip2 v3.0.3 | Plugins: tips modal viewport svg imagemap ie6 | Styles: core basic css3 | qtip2.com | Licensed MIT | Wed May 11 2016 22:31:31 */ + +!function(a,b,c){!function(a){"use strict";"function"==typeof define&&define.amd?define(["jquery"],a):jQuery&&!jQuery.fn.qtip&&a(jQuery)}(function(d){"use strict";function e(a,b,c,e){this.id=c,this.target=a,this.tooltip=F,this.elements={target:a},this._id=S+"-"+c,this.timers={img:{}},this.options=b,this.plugins={},this.cache={event:{},target:d(),disabled:E,attr:e,onTooltip:E,lastClass:""},this.rendered=this.destroyed=this.disabled=this.waiting=this.hiddenDuringWait=this.positioning=this.triggering=E}function f(a){return a===F||"object"!==d.type(a)}function g(a){return!(d.isFunction(a)||a&&a.attr||a.length||"object"===d.type(a)&&(a.jquery||a.then))}function h(a){var b,c,e,h;return f(a)?E:(f(a.metadata)&&(a.metadata={type:a.metadata}),"content"in a&&(b=a.content,f(b)||b.jquery||b.done?(c=g(b)?E:b,b=a.content={text:c}):c=b.text,"ajax"in b&&(e=b.ajax,h=e&&e.once!==E,delete b.ajax,b.text=function(a,b){var f=c||d(this).attr(b.options.content.attr)||"Loading...",g=d.ajax(d.extend({},e,{context:b})).then(e.success,F,e.error).then(function(a){return a&&h&&b.set("content.text",a),a},function(a,c,d){b.destroyed||0===a.status||b.set("content.text",c+": "+d)});return h?f:(b.set("content.text",f),g)}),"title"in b&&(d.isPlainObject(b.title)&&(b.button=b.title.button,b.title=b.title.text),g(b.title||E)&&(b.title=E))),"position"in a&&f(a.position)&&(a.position={my:a.position,at:a.position}),"show"in a&&f(a.show)&&(a.show=a.show.jquery?{target:a.show}:a.show===D?{ready:D}:{event:a.show}),"hide"in a&&f(a.hide)&&(a.hide=a.hide.jquery?{target:a.hide}:{event:a.hide}),"style"in a&&f(a.style)&&(a.style={classes:a.style}),d.each(R,function(){this.sanitize&&this.sanitize(a)}),a)}function i(a,b){for(var c,d=0,e=a,f=b.split(".");e=e[f[d++]];)d<f.length&&(c=e);return[c||a,f.pop()]}function j(a,b){var c,d,e;for(c in this.checks)if(this.checks.hasOwnProperty(c))for(d in this.checks[c])this.checks[c].hasOwnProperty(d)&&(e=new RegExp(d,"i").exec(a))&&(b.push(e),("builtin"===c||this.plugins[c])&&this.checks[c][d].apply(this.plugins[c]||this,b))}function k(a){return V.concat("").join(a?"-"+a+" ":" ")}function l(a,b){return b>0?setTimeout(d.proxy(a,this),b):void a.call(this)}function m(a){this.tooltip.hasClass(aa)||(clearTimeout(this.timers.show),clearTimeout(this.timers.hide),this.timers.show=l.call(this,function(){this.toggle(D,a)},this.options.show.delay))}function n(a){if(!this.tooltip.hasClass(aa)&&!this.destroyed){var b=d(a.relatedTarget),c=b.closest(W)[0]===this.tooltip[0],e=b[0]===this.options.show.target[0];if(clearTimeout(this.timers.show),clearTimeout(this.timers.hide),this!==b[0]&&"mouse"===this.options.position.target&&c||this.options.hide.fixed&&/mouse(out|leave|move)/.test(a.type)&&(c||e))try{a.preventDefault(),a.stopImmediatePropagation()}catch(f){}else this.timers.hide=l.call(this,function(){this.toggle(E,a)},this.options.hide.delay,this)}}function o(a){!this.tooltip.hasClass(aa)&&this.options.hide.inactive&&(clearTimeout(this.timers.inactive),this.timers.inactive=l.call(this,function(){this.hide(a)},this.options.hide.inactive))}function p(a){this.rendered&&this.tooltip[0].offsetWidth>0&&this.reposition(a)}function q(a,c,e){d(b.body).delegate(a,(c.split?c:c.join("."+S+" "))+"."+S,function(){var a=y.api[d.attr(this,U)];a&&!a.disabled&&e.apply(a,arguments)})}function r(a,c,f){var g,i,j,k,l,m=d(b.body),n=a[0]===b?m:a,o=a.metadata?a.metadata(f.metadata):F,p="html5"===f.metadata.type&&o?o[f.metadata.name]:F,q=a.data(f.metadata.name||"qtipopts");try{q="string"==typeof q?d.parseJSON(q):q}catch(r){}if(k=d.extend(D,{},y.defaults,f,"object"==typeof q?h(q):F,h(p||o)),i=k.position,k.id=c,"boolean"==typeof k.content.text){if(j=a.attr(k.content.attr),k.content.attr===E||!j)return E;k.content.text=j}if(i.container.length||(i.container=m),i.target===E&&(i.target=n),k.show.target===E&&(k.show.target=n),k.show.solo===D&&(k.show.solo=i.container.closest("body")),k.hide.target===E&&(k.hide.target=n),k.position.viewport===D&&(k.position.viewport=i.container),i.container=i.container.eq(0),i.at=new A(i.at,D),i.my=new A(i.my),a.data(S))if(k.overwrite)a.qtip("destroy",!0);else if(k.overwrite===E)return E;return a.attr(T,c),k.suppress&&(l=a.attr("title"))&&a.removeAttr("title").attr(ca,l).attr("title",""),g=new e(a,k,c,!!j),a.data(S,g),g}function s(a){return a.charAt(0).toUpperCase()+a.slice(1)}function t(a,b){var d,e,f=b.charAt(0).toUpperCase()+b.slice(1),g=(b+" "+va.join(f+" ")+f).split(" "),h=0;if(ua[b])return a.css(ua[b]);for(;d=g[h++];)if((e=a.css(d))!==c)return ua[b]=d,e}function u(a,b){return Math.ceil(parseFloat(t(a,b)))}function v(a,b){this._ns="tip",this.options=b,this.offset=b.offset,this.size=[b.width,b.height],this.qtip=a,this.init(a)}function w(a,b){this.options=b,this._ns="-modal",this.qtip=a,this.init(a)}function x(a){this._ns="ie6",this.qtip=a,this.init(a)}var y,z,A,B,C,D=!0,E=!1,F=null,G="x",H="y",I="width",J="height",K="top",L="left",M="bottom",N="right",O="center",P="flipinvert",Q="shift",R={},S="qtip",T="data-hasqtip",U="data-qtip-id",V=["ui-widget","ui-tooltip"],W="."+S,X="click dblclick mousedown mouseup mousemove mouseleave mouseenter".split(" "),Y=S+"-fixed",Z=S+"-default",$=S+"-focus",_=S+"-hover",aa=S+"-disabled",ba="_replacedByqTip",ca="oldtitle",da={ie:function(){var a,c;for(a=4,c=b.createElement("div");(c.innerHTML="<!--[if gt IE "+a+"]><i></i><![endif]-->")&&c.getElementsByTagName("i")[0];a+=1);return a>4?a:NaN}(),iOS:parseFloat((""+(/CPU.*OS ([0-9_]{1,5})|(CPU like).*AppleWebKit.*Mobile/i.exec(navigator.userAgent)||[0,""])[1]).replace("undefined","3_2").replace("_",".").replace("_",""))||E};z=e.prototype,z._when=function(a){return d.when.apply(d,a)},z.render=function(a){if(this.rendered||this.destroyed)return this;var b=this,c=this.options,e=this.cache,f=this.elements,g=c.content.text,h=c.content.title,i=c.content.button,j=c.position,k=[];return d.attr(this.target[0],"aria-describedby",this._id),e.posClass=this._createPosClass((this.position={my:j.my,at:j.at}).my),this.tooltip=f.tooltip=d("<div/>",{id:this._id,"class":[S,Z,c.style.classes,e.posClass].join(" "),width:c.style.width||"",height:c.style.height||"",tracking:"mouse"===j.target&&j.adjust.mouse,role:"alert","aria-live":"polite","aria-atomic":E,"aria-describedby":this._id+"-content","aria-hidden":D}).toggleClass(aa,this.disabled).attr(U,this.id).data(S,this).appendTo(j.container).append(f.content=d("<div />",{"class":S+"-content",id:this._id+"-content","aria-atomic":D})),this.rendered=-1,this.positioning=D,h&&(this._createTitle(),d.isFunction(h)||k.push(this._updateTitle(h,E))),i&&this._createButton(),d.isFunction(g)||k.push(this._updateContent(g,E)),this.rendered=D,this._setWidget(),d.each(R,function(a){var c;"render"===this.initialize&&(c=this(b))&&(b.plugins[a]=c)}),this._unassignEvents(),this._assignEvents(),this._when(k).then(function(){b._trigger("render"),b.positioning=E,b.hiddenDuringWait||!c.show.ready&&!a||b.toggle(D,e.event,E),b.hiddenDuringWait=E}),y.api[this.id]=this,this},z.destroy=function(a){function b(){if(!this.destroyed){this.destroyed=D;var a,b=this.target,c=b.attr(ca);this.rendered&&this.tooltip.stop(1,0).find("*").remove().end().remove(),d.each(this.plugins,function(){this.destroy&&this.destroy()});for(a in this.timers)this.timers.hasOwnProperty(a)&&clearTimeout(this.timers[a]);b.removeData(S).removeAttr(U).removeAttr(T).removeAttr("aria-describedby"),this.options.suppress&&c&&b.attr("title",c).removeAttr(ca),this._unassignEvents(),this.options=this.elements=this.cache=this.timers=this.plugins=this.mouse=F,delete y.api[this.id]}}return this.destroyed?this.target:(a===D&&"hide"!==this.triggering||!this.rendered?b.call(this):(this.tooltip.one("tooltiphidden",d.proxy(b,this)),!this.triggering&&this.hide()),this.target)},B=z.checks={builtin:{"^id$":function(a,b,c,e){var f=c===D?y.nextid:c,g=S+"-"+f;f!==E&&f.length>0&&!d("#"+g).length?(this._id=g,this.rendered&&(this.tooltip[0].id=this._id,this.elements.content[0].id=this._id+"-content",this.elements.title[0].id=this._id+"-title")):a[b]=e},"^prerender":function(a,b,c){c&&!this.rendered&&this.render(this.options.show.ready)},"^content.text$":function(a,b,c){this._updateContent(c)},"^content.attr$":function(a,b,c,d){this.options.content.text===this.target.attr(d)&&this._updateContent(this.target.attr(c))},"^content.title$":function(a,b,c){return c?(c&&!this.elements.title&&this._createTitle(),void this._updateTitle(c)):this._removeTitle()},"^content.button$":function(a,b,c){this._updateButton(c)},"^content.title.(text|button)$":function(a,b,c){this.set("content."+b,c)},"^position.(my|at)$":function(a,b,c){"string"==typeof c&&(this.position[b]=a[b]=new A(c,"at"===b))},"^position.container$":function(a,b,c){this.rendered&&this.tooltip.appendTo(c)},"^show.ready$":function(a,b,c){c&&(!this.rendered&&this.render(D)||this.toggle(D))},"^style.classes$":function(a,b,c,d){this.rendered&&this.tooltip.removeClass(d).addClass(c)},"^style.(width|height)":function(a,b,c){this.rendered&&this.tooltip.css(b,c)},"^style.widget|content.title":function(){this.rendered&&this._setWidget()},"^style.def":function(a,b,c){this.rendered&&this.tooltip.toggleClass(Z,!!c)},"^events.(render|show|move|hide|focus|blur)$":function(a,b,c){this.rendered&&this.tooltip[(d.isFunction(c)?"":"un")+"bind"]("tooltip"+b,c)},"^(show|hide|position).(event|target|fixed|inactive|leave|distance|viewport|adjust)":function(){if(this.rendered){var a=this.options.position;this.tooltip.attr("tracking","mouse"===a.target&&a.adjust.mouse),this._unassignEvents(),this._assignEvents()}}}},z.get=function(a){if(this.destroyed)return this;var b=i(this.options,a.toLowerCase()),c=b[0][b[1]];return c.precedance?c.string():c};var ea=/^position\.(my|at|adjust|target|container|viewport)|style|content|show\.ready/i,fa=/^prerender|show\.ready/i;z.set=function(a,b){if(this.destroyed)return this;var c,e=this.rendered,f=E,g=this.options;return"string"==typeof a?(c=a,a={},a[c]=b):a=d.extend({},a),d.each(a,function(b,c){if(e&&fa.test(b))return void delete a[b];var h,j=i(g,b.toLowerCase());h=j[0][j[1]],j[0][j[1]]=c&&c.nodeType?d(c):c,f=ea.test(b)||f,a[b]=[j[0],j[1],c,h]}),h(g),this.positioning=D,d.each(a,d.proxy(j,this)),this.positioning=E,this.rendered&&this.tooltip[0].offsetWidth>0&&f&&this.reposition("mouse"===g.position.target?F:this.cache.event),this},z._update=function(a,b){var c=this,e=this.cache;return this.rendered&&a?(d.isFunction(a)&&(a=a.call(this.elements.target,e.event,this)||""),d.isFunction(a.then)?(e.waiting=D,a.then(function(a){return e.waiting=E,c._update(a,b)},F,function(a){return c._update(a,b)})):a===E||!a&&""!==a?E:(a.jquery&&a.length>0?b.empty().append(a.css({display:"block",visibility:"visible"})):b.html(a),this._waitForContent(b).then(function(a){c.rendered&&c.tooltip[0].offsetWidth>0&&c.reposition(e.event,!a.length)}))):E},z._waitForContent=function(a){var b=this.cache;return b.waiting=D,(d.fn.imagesLoaded?a.imagesLoaded():(new d.Deferred).resolve([])).done(function(){b.waiting=E}).promise()},z._updateContent=function(a,b){this._update(a,this.elements.content,b)},z._updateTitle=function(a,b){this._update(a,this.elements.title,b)===E&&this._removeTitle(E)},z._createTitle=function(){var a=this.elements,b=this._id+"-title";a.titlebar&&this._removeTitle(),a.titlebar=d("<div />",{"class":S+"-titlebar "+(this.options.style.widget?k("header"):"")}).append(a.title=d("<div />",{id:b,"class":S+"-title","aria-atomic":D})).insertBefore(a.content).delegate(".qtip-close","mousedown keydown mouseup keyup mouseout",function(a){d(this).toggleClass("ui-state-active ui-state-focus","down"===a.type.substr(-4))}).delegate(".qtip-close","mouseover mouseout",function(a){d(this).toggleClass("ui-state-hover","mouseover"===a.type)}),this.options.content.button&&this._createButton()},z._removeTitle=function(a){var b=this.elements;b.title&&(b.titlebar.remove(),b.titlebar=b.title=b.button=F,a!==E&&this.reposition())},z._createPosClass=function(a){return S+"-pos-"+(a||this.options.position.my).abbrev()},z.reposition=function(c,e){if(!this.rendered||this.positioning||this.destroyed)return this;this.positioning=D;var f,g,h,i,j=this.cache,k=this.tooltip,l=this.options.position,m=l.target,n=l.my,o=l.at,p=l.viewport,q=l.container,r=l.adjust,s=r.method.split(" "),t=k.outerWidth(E),u=k.outerHeight(E),v=0,w=0,x=k.css("position"),y={left:0,top:0},z=k[0].offsetWidth>0,A=c&&"scroll"===c.type,B=d(a),C=q[0].ownerDocument,F=this.mouse;if(d.isArray(m)&&2===m.length)o={x:L,y:K},y={left:m[0],top:m[1]};else if("mouse"===m)o={x:L,y:K},(!r.mouse||this.options.hide.distance)&&j.origin&&j.origin.pageX?c=j.origin:!c||c&&("resize"===c.type||"scroll"===c.type)?c=j.event:F&&F.pageX&&(c=F),"static"!==x&&(y=q.offset()),C.body.offsetWidth!==(a.innerWidth||C.documentElement.clientWidth)&&(g=d(b.body).offset()),y={left:c.pageX-y.left+(g&&g.left||0),top:c.pageY-y.top+(g&&g.top||0)},r.mouse&&A&&F&&(y.left-=(F.scrollX||0)-B.scrollLeft(),y.top-=(F.scrollY||0)-B.scrollTop());else{if("event"===m?c&&c.target&&"scroll"!==c.type&&"resize"!==c.type?j.target=d(c.target):c.target||(j.target=this.elements.target):"event"!==m&&(j.target=d(m.jquery?m:this.elements.target)),m=j.target,m=d(m).eq(0),0===m.length)return this;m[0]===b||m[0]===a?(v=da.iOS?a.innerWidth:m.width(),w=da.iOS?a.innerHeight:m.height(),m[0]===a&&(y={top:(p||m).scrollTop(),left:(p||m).scrollLeft()})):R.imagemap&&m.is("area")?f=R.imagemap(this,m,o,R.viewport?s:E):R.svg&&m&&m[0].ownerSVGElement?f=R.svg(this,m,o,R.viewport?s:E):(v=m.outerWidth(E),w=m.outerHeight(E),y=m.offset()),f&&(v=f.width,w=f.height,g=f.offset,y=f.position),y=this.reposition.offset(m,y,q),(da.iOS>3.1&&da.iOS<4.1||da.iOS>=4.3&&da.iOS<4.33||!da.iOS&&"fixed"===x)&&(y.left-=B.scrollLeft(),y.top-=B.scrollTop()),(!f||f&&f.adjustable!==E)&&(y.left+=o.x===N?v:o.x===O?v/2:0,y.top+=o.y===M?w:o.y===O?w/2:0)}return y.left+=r.x+(n.x===N?-t:n.x===O?-t/2:0),y.top+=r.y+(n.y===M?-u:n.y===O?-u/2:0),R.viewport?(h=y.adjusted=R.viewport(this,y,l,v,w,t,u),g&&h.left&&(y.left+=g.left),g&&h.top&&(y.top+=g.top),h.my&&(this.position.my=h.my)):y.adjusted={left:0,top:0},j.posClass!==(i=this._createPosClass(this.position.my))&&(j.posClass=i,k.removeClass(j.posClass).addClass(i)),this._trigger("move",[y,p.elem||p],c)?(delete y.adjusted,e===E||!z||isNaN(y.left)||isNaN(y.top)||"mouse"===m||!d.isFunction(l.effect)?k.css(y):d.isFunction(l.effect)&&(l.effect.call(k,this,d.extend({},y)),k.queue(function(a){d(this).css({opacity:"",height:""}),da.ie&&this.style.removeAttribute("filter"),a()})),this.positioning=E,this):this},z.reposition.offset=function(a,c,e){function f(a,b){c.left+=b*a.scrollLeft(),c.top+=b*a.scrollTop()}if(!e[0])return c;var g,h,i,j,k=d(a[0].ownerDocument),l=!!da.ie&&"CSS1Compat"!==b.compatMode,m=e[0];do"static"!==(h=d.css(m,"position"))&&("fixed"===h?(i=m.getBoundingClientRect(),f(k,-1)):(i=d(m).position(),i.left+=parseFloat(d.css(m,"borderLeftWidth"))||0,i.top+=parseFloat(d.css(m,"borderTopWidth"))||0),c.left-=i.left+(parseFloat(d.css(m,"marginLeft"))||0),c.top-=i.top+(parseFloat(d.css(m,"marginTop"))||0),g||"hidden"===(j=d.css(m,"overflow"))||"visible"===j||(g=d(m)));while(m=m.offsetParent);return g&&(g[0]!==k[0]||l)&&f(g,1),c};var ga=(A=z.reposition.Corner=function(a,b){a=(""+a).replace(/([A-Z])/," $1").replace(/middle/gi,O).toLowerCase(),this.x=(a.match(/left|right/i)||a.match(/center/)||["inherit"])[0].toLowerCase(),this.y=(a.match(/top|bottom|center/i)||["inherit"])[0].toLowerCase(),this.forceY=!!b;var c=a.charAt(0);this.precedance="t"===c||"b"===c?H:G}).prototype;ga.invert=function(a,b){this[a]=this[a]===L?N:this[a]===N?L:b||this[a]},ga.string=function(a){var b=this.x,c=this.y,d=b!==c?"center"===b||"center"!==c&&(this.precedance===H||this.forceY)?[c,b]:[b,c]:[b];return a!==!1?d.join(" "):d},ga.abbrev=function(){var a=this.string(!1);return a[0].charAt(0)+(a[1]&&a[1].charAt(0)||"")},ga.clone=function(){return new A(this.string(),this.forceY)},z.toggle=function(a,c){var e=this.cache,f=this.options,g=this.tooltip;if(c){if(/over|enter/.test(c.type)&&e.event&&/out|leave/.test(e.event.type)&&f.show.target.add(c.target).length===f.show.target.length&&g.has(c.relatedTarget).length)return this;e.event=d.event.fix(c)}if(this.waiting&&!a&&(this.hiddenDuringWait=D),!this.rendered)return a?this.render(1):this;if(this.destroyed||this.disabled)return this;var h,i,j,k=a?"show":"hide",l=this.options[k],m=this.options.position,n=this.options.content,o=this.tooltip.css("width"),p=this.tooltip.is(":visible"),q=a||1===l.target.length,r=!c||l.target.length<2||e.target[0]===c.target;return(typeof a).search("boolean|number")&&(a=!p),h=!g.is(":animated")&&p===a&&r,i=h?F:!!this._trigger(k,[90]),this.destroyed?this:(i!==E&&a&&this.focus(c),!i||h?this:(d.attr(g[0],"aria-hidden",!a),a?(this.mouse&&(e.origin=d.event.fix(this.mouse)),d.isFunction(n.text)&&this._updateContent(n.text,E),d.isFunction(n.title)&&this._updateTitle(n.title,E),!C&&"mouse"===m.target&&m.adjust.mouse&&(d(b).bind("mousemove."+S,this._storeMouse),C=D),o||g.css("width",g.outerWidth(E)),this.reposition(c,arguments[2]),o||g.css("width",""),l.solo&&("string"==typeof l.solo?d(l.solo):d(W,l.solo)).not(g).not(l.target).qtip("hide",new d.Event("tooltipsolo"))):(clearTimeout(this.timers.show),delete e.origin,C&&!d(W+'[tracking="true"]:visible',l.solo).not(g).length&&(d(b).unbind("mousemove."+S),C=E),this.blur(c)),j=d.proxy(function(){a?(da.ie&&g[0].style.removeAttribute("filter"),g.css("overflow",""),"string"==typeof l.autofocus&&d(this.options.show.autofocus,g).focus(),this.options.show.target.trigger("qtip-"+this.id+"-inactive")):g.css({display:"",visibility:"",opacity:"",left:"",top:""}),this._trigger(a?"visible":"hidden")},this),l.effect===E||q===E?(g[k](),j()):d.isFunction(l.effect)?(g.stop(1,1),l.effect.call(g,this),g.queue("fx",function(a){j(),a()})):g.fadeTo(90,a?1:0,j),a&&l.target.trigger("qtip-"+this.id+"-inactive"),this))},z.show=function(a){return this.toggle(D,a)},z.hide=function(a){return this.toggle(E,a)},z.focus=function(a){if(!this.rendered||this.destroyed)return this;var b=d(W),c=this.tooltip,e=parseInt(c[0].style.zIndex,10),f=y.zindex+b.length;return c.hasClass($)||this._trigger("focus",[f],a)&&(e!==f&&(b.each(function(){this.style.zIndex>e&&(this.style.zIndex=this.style.zIndex-1)}),b.filter("."+$).qtip("blur",a)),c.addClass($)[0].style.zIndex=f),this},z.blur=function(a){return!this.rendered||this.destroyed?this:(this.tooltip.removeClass($),this._trigger("blur",[this.tooltip.css("zIndex")],a),this)},z.disable=function(a){return this.destroyed?this:("toggle"===a?a=!(this.rendered?this.tooltip.hasClass(aa):this.disabled):"boolean"!=typeof a&&(a=D),this.rendered&&this.tooltip.toggleClass(aa,a).attr("aria-disabled",a),this.disabled=!!a,this)},z.enable=function(){return this.disable(E)},z._createButton=function(){var a=this,b=this.elements,c=b.tooltip,e=this.options.content.button,f="string"==typeof e,g=f?e:"Close tooltip";b.button&&b.button.remove(),e.jquery?b.button=e:b.button=d("<a />",{"class":"qtip-close "+(this.options.style.widget?"":S+"-icon"),title:g,"aria-label":g}).prepend(d("<span />",{"class":"ui-icon ui-icon-close",html:"×"})),b.button.appendTo(b.titlebar||c).attr("role","button").click(function(b){return c.hasClass(aa)||a.hide(b),E})},z._updateButton=function(a){if(!this.rendered)return E;var b=this.elements.button;a?this._createButton():b.remove()},z._setWidget=function(){var a=this.options.style.widget,b=this.elements,c=b.tooltip,d=c.hasClass(aa);c.removeClass(aa),aa=a?"ui-state-disabled":"qtip-disabled",c.toggleClass(aa,d),c.toggleClass("ui-helper-reset "+k(),a).toggleClass(Z,this.options.style.def&&!a),b.content&&b.content.toggleClass(k("content"),a),b.titlebar&&b.titlebar.toggleClass(k("header"),a),b.button&&b.button.toggleClass(S+"-icon",!a)},z._storeMouse=function(a){return(this.mouse=d.event.fix(a)).type="mousemove",this},z._bind=function(a,b,c,e,f){if(a&&c&&b.length){var g="."+this._id+(e?"-"+e:"");return d(a).bind((b.split?b:b.join(g+" "))+g,d.proxy(c,f||this)),this}},z._unbind=function(a,b){return a&&d(a).unbind("."+this._id+(b?"-"+b:"")),this},z._trigger=function(a,b,c){var e=new d.Event("tooltip"+a);return e.originalEvent=c&&d.extend({},c)||this.cache.event||F,this.triggering=a,this.tooltip.trigger(e,[this].concat(b||[])),this.triggering=E,!e.isDefaultPrevented()},z._bindEvents=function(a,b,c,e,f,g){var h=c.filter(e).add(e.filter(c)),i=[];h.length&&(d.each(b,function(b,c){var e=d.inArray(c,a);e>-1&&i.push(a.splice(e,1)[0])}),i.length&&(this._bind(h,i,function(a){var b=this.rendered?this.tooltip[0].offsetWidth>0:!1;(b?g:f).call(this,a)}),c=c.not(h),e=e.not(h))),this._bind(c,a,f),this._bind(e,b,g)},z._assignInitialEvents=function(a){function b(a){return this.disabled||this.destroyed?E:(this.cache.event=a&&d.event.fix(a),this.cache.target=a&&d(a.target),clearTimeout(this.timers.show),void(this.timers.show=l.call(this,function(){this.render("object"==typeof a||c.show.ready)},c.prerender?0:c.show.delay)))}var c=this.options,e=c.show.target,f=c.hide.target,g=c.show.event?d.trim(""+c.show.event).split(" "):[],h=c.hide.event?d.trim(""+c.hide.event).split(" "):[];this._bind(this.elements.target,["remove","removeqtip"],function(){this.destroy(!0)},"destroy"),/mouse(over|enter)/i.test(c.show.event)&&!/mouse(out|leave)/i.test(c.hide.event)&&h.push("mouseleave"),this._bind(e,"mousemove",function(a){this._storeMouse(a),this.cache.onTarget=D}),this._bindEvents(g,h,e,f,b,function(){return this.timers?void clearTimeout(this.timers.show):E}),(c.show.ready||c.prerender)&&b.call(this,a)},z._assignEvents=function(){var c=this,e=this.options,f=e.position,g=this.tooltip,h=e.show.target,i=e.hide.target,j=f.container,k=f.viewport,l=d(b),q=d(a),r=e.show.event?d.trim(""+e.show.event).split(" "):[],s=e.hide.event?d.trim(""+e.hide.event).split(" "):[];d.each(e.events,function(a,b){c._bind(g,"toggle"===a?["tooltipshow","tooltiphide"]:["tooltip"+a],b,null,g)}),/mouse(out|leave)/i.test(e.hide.event)&&"window"===e.hide.leave&&this._bind(l,["mouseout","blur"],function(a){/select|option/.test(a.target.nodeName)||a.relatedTarget||this.hide(a)}),e.hide.fixed?i=i.add(g.addClass(Y)):/mouse(over|enter)/i.test(e.show.event)&&this._bind(i,"mouseleave",function(){clearTimeout(this.timers.show)}),(""+e.hide.event).indexOf("unfocus")>-1&&this._bind(j.closest("html"),["mousedown","touchstart"],function(a){var b=d(a.target),c=this.rendered&&!this.tooltip.hasClass(aa)&&this.tooltip[0].offsetWidth>0,e=b.parents(W).filter(this.tooltip[0]).length>0;b[0]===this.target[0]||b[0]===this.tooltip[0]||e||this.target.has(b[0]).length||!c||this.hide(a)}),"number"==typeof e.hide.inactive&&(this._bind(h,"qtip-"+this.id+"-inactive",o,"inactive"),this._bind(i.add(g),y.inactiveEvents,o)),this._bindEvents(r,s,h,i,m,n),this._bind(h.add(g),"mousemove",function(a){if("number"==typeof e.hide.distance){var b=this.cache.origin||{},c=this.options.hide.distance,d=Math.abs;(d(a.pageX-b.pageX)>=c||d(a.pageY-b.pageY)>=c)&&this.hide(a)}this._storeMouse(a)}),"mouse"===f.target&&f.adjust.mouse&&(e.hide.event&&this._bind(h,["mouseenter","mouseleave"],function(a){return this.cache?void(this.cache.onTarget="mouseenter"===a.type):E}),this._bind(l,"mousemove",function(a){this.rendered&&this.cache.onTarget&&!this.tooltip.hasClass(aa)&&this.tooltip[0].offsetWidth>0&&this.reposition(a)})),(f.adjust.resize||k.length)&&this._bind(d.event.special.resize?k:q,"resize",p),f.adjust.scroll&&this._bind(q.add(f.container),"scroll",p)},z._unassignEvents=function(){var c=this.options,e=c.show.target,f=c.hide.target,g=d.grep([this.elements.target[0],this.rendered&&this.tooltip[0],c.position.container[0],c.position.viewport[0],c.position.container.closest("html")[0],a,b],function(a){return"object"==typeof a});e&&e.toArray&&(g=g.concat(e.toArray())),f&&f.toArray&&(g=g.concat(f.toArray())),this._unbind(g)._unbind(g,"destroy")._unbind(g,"inactive")},d(function(){q(W,["mouseenter","mouseleave"],function(a){var b="mouseenter"===a.type,c=d(a.currentTarget),e=d(a.relatedTarget||a.target),f=this.options;b?(this.focus(a),c.hasClass(Y)&&!c.hasClass(aa)&&clearTimeout(this.timers.hide)):"mouse"===f.position.target&&f.position.adjust.mouse&&f.hide.event&&f.show.target&&!e.closest(f.show.target[0]).length&&this.hide(a),c.toggleClass(_,b)}),q("["+U+"]",X,o)}),y=d.fn.qtip=function(a,b,e){var f=(""+a).toLowerCase(),g=F,i=d.makeArray(arguments).slice(1),j=i[i.length-1],k=this[0]?d.data(this[0],S):F;return!arguments.length&&k||"api"===f?k:"string"==typeof a?(this.each(function(){var a=d.data(this,S);if(!a)return D;if(j&&j.timeStamp&&(a.cache.event=j),!b||"option"!==f&&"options"!==f)a[f]&&a[f].apply(a,i);else{if(e===c&&!d.isPlainObject(b))return g=a.get(b),E;a.set(b,e)}}),g!==F?g:this):"object"!=typeof a&&arguments.length?void 0:(k=h(d.extend(D,{},a)),this.each(function(a){var b,c;return c=d.isArray(k.id)?k.id[a]:k.id,c=!c||c===E||c.length<1||y.api[c]?y.nextid++:c,b=r(d(this),c,k),b===E?D:(y.api[c]=b,d.each(R,function(){"initialize"===this.initialize&&this(b)}),void b._assignInitialEvents(j))}))},d.qtip=e,y.api={},d.each({attr:function(a,b){if(this.length){var c=this[0],e="title",f=d.data(c,"qtip");if(a===e&&f&&f.options&&"object"==typeof f&&"object"==typeof f.options&&f.options.suppress)return arguments.length<2?d.attr(c,ca):(f&&f.options.content.attr===e&&f.cache.attr&&f.set("content.text",b),this.attr(ca,b))}return d.fn["attr"+ba].apply(this,arguments)},clone:function(a){var b=d.fn["clone"+ba].apply(this,arguments);return a||b.filter("["+ca+"]").attr("title",function(){return d.attr(this,ca)}).removeAttr(ca),b}},function(a,b){if(!b||d.fn[a+ba])return D;var c=d.fn[a+ba]=d.fn[a];d.fn[a]=function(){return b.apply(this,arguments)||c.apply(this,arguments)}}),d.ui||(d["cleanData"+ba]=d.cleanData,d.cleanData=function(a){for(var b,c=0;(b=d(a[c])).length;c++)if(b.attr(T))try{b.triggerHandler("removeqtip")}catch(e){}d["cleanData"+ba].apply(this,arguments)}),y.version="3.0.3",y.nextid=0,y.inactiveEvents=X,y.zindex=15e3,y.defaults={prerender:E,id:E,overwrite:D,suppress:D,content:{text:D,attr:"title",title:E,button:E},position:{my:"top left",at:"bottom right",target:E,container:E,viewport:E,adjust:{x:0,y:0,mouse:D,scroll:D,resize:D,method:"flipinvert flipinvert"},effect:function(a,b){d(this).animate(b,{duration:200,queue:E})}},show:{target:E,event:"mouseenter",effect:D,delay:90,solo:E,ready:E,autofocus:E},hide:{target:E,event:"mouseleave",effect:D,delay:0,fixed:E,inactive:E,leave:"window",distance:E},style:{classes:"",widget:E,width:E,height:E,def:D},events:{render:F,move:F,show:F,hide:F,toggle:F,visible:F,hidden:F,focus:F,blur:F}};var ha,ia,ja,ka,la,ma="margin",na="border",oa="color",pa="background-color",qa="transparent",ra=" !important",sa=!!b.createElement("canvas").getContext,ta=/rgba?\(0, 0, 0(, 0)?\)|transparent|#123456/i,ua={},va=["Webkit","O","Moz","ms"];sa?(ka=a.devicePixelRatio||1,la=function(){var a=b.createElement("canvas").getContext("2d");return a.backingStorePixelRatio||a.webkitBackingStorePixelRatio||a.mozBackingStorePixelRatio||a.msBackingStorePixelRatio||a.oBackingStorePixelRatio||1}(),ja=ka/la):ia=function(a,b,c){return"<qtipvml:"+a+' xmlns="urn:schemas-microsoft.com:vml" class="qtip-vml" '+(b||"")+' style="behavior: url(#default#VML); '+(c||"")+'" />'},d.extend(v.prototype,{init:function(a){var b,c;c=this.element=a.elements.tip=d("<div />",{"class":S+"-tip"}).prependTo(a.tooltip),sa?(b=d("<canvas />").appendTo(this.element)[0].getContext("2d"),b.lineJoin="miter",b.miterLimit=1e5,b.save()):(b=ia("shape",'coordorigin="0,0"',"position:absolute;"),this.element.html(b+b),a._bind(d("*",c).add(c),["click","mousedown"],function(a){a.stopPropagation()},this._ns)),a._bind(a.tooltip,"tooltipmove",this.reposition,this._ns,this),this.create()},_swapDimensions:function(){this.size[0]=this.options.height,this.size[1]=this.options.width},_resetDimensions:function(){this.size[0]=this.options.width,this.size[1]=this.options.height},_useTitle:function(a){var b=this.qtip.elements.titlebar;return b&&(a.y===K||a.y===O&&this.element.position().top+this.size[1]/2+this.options.offset<b.outerHeight(D))},_parseCorner:function(a){var b=this.qtip.options.position.my;return a===E||b===E?a=E:a===D?a=new A(b.string()):a.string||(a=new A(a),a.fixed=D),a},_parseWidth:function(a,b,c){var d=this.qtip.elements,e=na+s(b)+"Width";return(c?u(c,e):u(d.content,e)||u(this._useTitle(a)&&d.titlebar||d.content,e)||u(d.tooltip,e))||0},_parseRadius:function(a){var b=this.qtip.elements,c=na+s(a.y)+s(a.x)+"Radius";return da.ie<9?0:u(this._useTitle(a)&&b.titlebar||b.content,c)||u(b.tooltip,c)||0},_invalidColour:function(a,b,c){var d=a.css(b);return!d||c&&d===a.css(c)||ta.test(d)?E:d},_parseColours:function(a){var b=this.qtip.elements,c=this.element.css("cssText",""),e=na+s(a[a.precedance])+s(oa),f=this._useTitle(a)&&b.titlebar||b.content,g=this._invalidColour,h=[];return h[0]=g(c,pa)||g(f,pa)||g(b.content,pa)||g(b.tooltip,pa)||c.css(pa),h[1]=g(c,e,oa)||g(f,e,oa)||g(b.content,e,oa)||g(b.tooltip,e,oa)||b.tooltip.css(e),d("*",c).add(c).css("cssText",pa+":"+qa+ra+";"+na+":0"+ra+";"),h},_calculateSize:function(a){var b,c,d,e=a.precedance===H,f=this.options.width,g=this.options.height,h="c"===a.abbrev(),i=(e?f:g)*(h?.5:1),j=Math.pow,k=Math.round,l=Math.sqrt(j(i,2)+j(g,2)),m=[this.border/i*l,this.border/g*l];return m[2]=Math.sqrt(j(m[0],2)-j(this.border,2)),m[3]=Math.sqrt(j(m[1],2)-j(this.border,2)),b=l+m[2]+m[3]+(h?0:m[0]),c=b/l,d=[k(c*f),k(c*g)],e?d:d.reverse()},_calculateTip:function(a,b,c){c=c||1,b=b||this.size;var d=b[0]*c,e=b[1]*c,f=Math.ceil(d/2),g=Math.ceil(e/2),h={br:[0,0,d,e,d,0],bl:[0,0,d,0,0,e],tr:[0,e,d,0,d,e],tl:[0,0,0,e,d,e],tc:[0,e,f,0,d,e],bc:[0,0,d,0,f,e],rc:[0,0,d,g,0,e],lc:[d,0,d,e,0,g]};return h.lt=h.br,h.rt=h.bl,h.lb=h.tr,h.rb=h.tl,h[a.abbrev()]},_drawCoords:function(a,b){a.beginPath(),a.moveTo(b[0],b[1]),a.lineTo(b[2],b[3]),a.lineTo(b[4],b[5]),a.closePath()},create:function(){var a=this.corner=(sa||da.ie)&&this._parseCorner(this.options.corner);return this.enabled=!!this.corner&&"c"!==this.corner.abbrev(),this.enabled&&(this.qtip.cache.corner=a.clone(),this.update()),this.element.toggle(this.enabled),this.corner},update:function(b,c){if(!this.enabled)return this;var e,f,g,h,i,j,k,l,m=this.qtip.elements,n=this.element,o=n.children(),p=this.options,q=this.size,r=p.mimic,s=Math.round;b||(b=this.qtip.cache.corner||this.corner),r===E?r=b:(r=new A(r),r.precedance=b.precedance,"inherit"===r.x?r.x=b.x:"inherit"===r.y?r.y=b.y:r.x===r.y&&(r[b.precedance]=b[b.precedance])),f=r.precedance,b.precedance===G?this._swapDimensions():this._resetDimensions(),e=this.color=this._parseColours(b),e[1]!==qa?(l=this.border=this._parseWidth(b,b[b.precedance]),p.border&&1>l&&!ta.test(e[1])&&(e[0]=e[1]),this.border=l=p.border!==D?p.border:l):this.border=l=0,k=this.size=this._calculateSize(b),n.css({width:k[0],height:k[1],lineHeight:k[1]+"px"}),j=b.precedance===H?[s(r.x===L?l:r.x===N?k[0]-q[0]-l:(k[0]-q[0])/2),s(r.y===K?k[1]-q[1]:0)]:[s(r.x===L?k[0]-q[0]:0),s(r.y===K?l:r.y===M?k[1]-q[1]-l:(k[1]-q[1])/2)],sa?(g=o[0].getContext("2d"),g.restore(),g.save(),g.clearRect(0,0,6e3,6e3),h=this._calculateTip(r,q,ja),i=this._calculateTip(r,this.size,ja),o.attr(I,k[0]*ja).attr(J,k[1]*ja),o.css(I,k[0]).css(J,k[1]),this._drawCoords(g,i),g.fillStyle=e[1],g.fill(),g.translate(j[0]*ja,j[1]*ja),this._drawCoords(g,h),g.fillStyle=e[0],g.fill()):(h=this._calculateTip(r),h="m"+h[0]+","+h[1]+" l"+h[2]+","+h[3]+" "+h[4]+","+h[5]+" xe",j[2]=l&&/^(r|b)/i.test(b.string())?8===da.ie?2:1:0,o.css({coordsize:k[0]+l+" "+k[1]+l,antialias:""+(r.string().indexOf(O)>-1),left:j[0]-j[2]*Number(f===G),top:j[1]-j[2]*Number(f===H),width:k[0]+l,height:k[1]+l}).each(function(a){var b=d(this);b[b.prop?"prop":"attr"]({coordsize:k[0]+l+" "+k[1]+l,path:h,fillcolor:e[0],filled:!!a,stroked:!a}).toggle(!(!l&&!a)),!a&&b.html(ia("stroke",'weight="'+2*l+'px" color="'+e[1]+'" miterlimit="1000" joinstyle="miter"'))})),a.opera&&setTimeout(function(){m.tip.css({display:"inline-block",visibility:"visible"})},1),c!==E&&this.calculate(b,k)},calculate:function(a,b){if(!this.enabled)return E;var c,e,f=this,g=this.qtip.elements,h=this.element,i=this.options.offset,j={}; +return a=a||this.corner,c=a.precedance,b=b||this._calculateSize(a),e=[a.x,a.y],c===G&&e.reverse(),d.each(e,function(d,e){var h,k,l;e===O?(h=c===H?L:K,j[h]="50%",j[ma+"-"+h]=-Math.round(b[c===H?0:1]/2)+i):(h=f._parseWidth(a,e,g.tooltip),k=f._parseWidth(a,e,g.content),l=f._parseRadius(a),j[e]=Math.max(-f.border,d?k:i+(l>h?l:-h)))}),j[a[c]]-=b[c===G?0:1],h.css({margin:"",top:"",bottom:"",left:"",right:""}).css(j),j},reposition:function(a,b,d){function e(a,b,c,d,e){a===Q&&j.precedance===b&&k[d]&&j[c]!==O?j.precedance=j.precedance===G?H:G:a!==Q&&k[d]&&(j[b]=j[b]===O?k[d]>0?d:e:j[b]===d?e:d)}function f(a,b,e){j[a]===O?p[ma+"-"+b]=o[a]=g[ma+"-"+b]-k[b]:(h=g[e]!==c?[k[b],-g[b]]:[-k[b],g[b]],(o[a]=Math.max(h[0],h[1]))>h[0]&&(d[b]-=k[b],o[b]=E),p[g[e]!==c?e:b]=o[a])}if(this.enabled){var g,h,i=b.cache,j=this.corner.clone(),k=d.adjusted,l=b.options.position.adjust.method.split(" "),m=l[0],n=l[1]||l[0],o={left:E,top:E,x:0,y:0},p={};this.corner.fixed!==D&&(e(m,G,H,L,N),e(n,H,G,K,M),j.string()===i.corner.string()&&i.cornerTop===k.top&&i.cornerLeft===k.left||this.update(j,E)),g=this.calculate(j),g.right!==c&&(g.left=-g.right),g.bottom!==c&&(g.top=-g.bottom),g.user=this.offset,o.left=m===Q&&!!k.left,o.left&&f(G,L,N),o.top=n===Q&&!!k.top,o.top&&f(H,K,M),this.element.css(p).toggle(!(o.x&&o.y||j.x===O&&o.y||j.y===O&&o.x)),d.left-=g.left.charAt?g.user:m!==Q||o.top||!o.left&&!o.top?g.left+this.border:0,d.top-=g.top.charAt?g.user:n!==Q||o.left||!o.left&&!o.top?g.top+this.border:0,i.cornerLeft=k.left,i.cornerTop=k.top,i.corner=j.clone()}},destroy:function(){this.qtip._unbind(this.qtip.tooltip,this._ns),this.qtip.elements.tip&&this.qtip.elements.tip.find("*").remove().end().remove()}}),ha=R.tip=function(a){return new v(a,a.options.style.tip)},ha.initialize="render",ha.sanitize=function(a){if(a.style&&"tip"in a.style){var b=a.style.tip;"object"!=typeof b&&(b=a.style.tip={corner:b}),/string|boolean/i.test(typeof b.corner)||(b.corner=D)}},B.tip={"^position.my|style.tip.(corner|mimic|border)$":function(){this.create(),this.qtip.reposition()},"^style.tip.(height|width)$":function(a){this.size=[a.width,a.height],this.update(),this.qtip.reposition()},"^content.title|style.(classes|widget)$":function(){this.update()}},d.extend(D,y.defaults,{style:{tip:{corner:D,mimic:E,width:6,height:6,border:D,offset:0}}});var wa,xa,ya="qtip-modal",za="."+ya;xa=function(){function a(a){if(d.expr[":"].focusable)return d.expr[":"].focusable;var b,c,e,f=!isNaN(d.attr(a,"tabindex")),g=a.nodeName&&a.nodeName.toLowerCase();return"area"===g?(b=a.parentNode,c=b.name,a.href&&c&&"map"===b.nodeName.toLowerCase()?(e=d("img[usemap=#"+c+"]")[0],!!e&&e.is(":visible")):!1):/input|select|textarea|button|object/.test(g)?!a.disabled:"a"===g?a.href||f:f}function c(a){j.length<1&&a.length?a.not("body").blur():j.first().focus()}function e(a){if(h.is(":visible")){var b,e=d(a.target),g=f.tooltip,i=e.closest(W);b=i.length<1?E:parseInt(i[0].style.zIndex,10)>parseInt(g[0].style.zIndex,10),b||e.closest(W)[0]===g[0]||c(e)}}var f,g,h,i=this,j={};d.extend(i,{init:function(){return h=i.elem=d("<div />",{id:"qtip-overlay",html:"<div></div>",mousedown:function(){return E}}).hide(),d(b.body).bind("focusin"+za,e),d(b).bind("keydown"+za,function(a){f&&f.options.show.modal.escape&&27===a.keyCode&&f.hide(a)}),h.bind("click"+za,function(a){f&&f.options.show.modal.blur&&f.hide(a)}),i},update:function(b){f=b,j=b.options.show.modal.stealfocus!==E?b.tooltip.find("*").filter(function(){return a(this)}):[]},toggle:function(a,e,j){var k=a.tooltip,l=a.options.show.modal,m=l.effect,n=e?"show":"hide",o=h.is(":visible"),p=d(za).filter(":visible:not(:animated)").not(k);return i.update(a),e&&l.stealfocus!==E&&c(d(":focus")),h.toggleClass("blurs",l.blur),e&&h.appendTo(b.body),h.is(":animated")&&o===e&&g!==E||!e&&p.length?i:(h.stop(D,E),d.isFunction(m)?m.call(h,e):m===E?h[n]():h.fadeTo(parseInt(j,10)||90,e?1:0,function(){e||h.hide()}),e||h.queue(function(a){h.css({left:"",top:""}),d(za).length||h.detach(),a()}),g=e,f.destroyed&&(f=F),i)}}),i.init()},xa=new xa,d.extend(w.prototype,{init:function(a){var b=a.tooltip;return this.options.on?(a.elements.overlay=xa.elem,b.addClass(ya).css("z-index",y.modal_zindex+d(za).length),a._bind(b,["tooltipshow","tooltiphide"],function(a,c,e){var f=a.originalEvent;if(a.target===b[0])if(f&&"tooltiphide"===a.type&&/mouse(leave|enter)/.test(f.type)&&d(f.relatedTarget).closest(xa.elem[0]).length)try{a.preventDefault()}catch(g){}else(!f||f&&"tooltipsolo"!==f.type)&&this.toggle(a,"tooltipshow"===a.type,e)},this._ns,this),a._bind(b,"tooltipfocus",function(a,c){if(!a.isDefaultPrevented()&&a.target===b[0]){var e=d(za),f=y.modal_zindex+e.length,g=parseInt(b[0].style.zIndex,10);xa.elem[0].style.zIndex=f-1,e.each(function(){this.style.zIndex>g&&(this.style.zIndex-=1)}),e.filter("."+$).qtip("blur",a.originalEvent),b.addClass($)[0].style.zIndex=f,xa.update(c);try{a.preventDefault()}catch(h){}}},this._ns,this),void a._bind(b,"tooltiphide",function(a){a.target===b[0]&&d(za).filter(":visible").not(b).last().qtip("focus",a)},this._ns,this)):this},toggle:function(a,b,c){return a&&a.isDefaultPrevented()?this:void xa.toggle(this.qtip,!!b,c)},destroy:function(){this.qtip.tooltip.removeClass(ya),this.qtip._unbind(this.qtip.tooltip,this._ns),xa.toggle(this.qtip,E),delete this.qtip.elements.overlay}}),wa=R.modal=function(a){return new w(a,a.options.show.modal)},wa.sanitize=function(a){a.show&&("object"!=typeof a.show.modal?a.show.modal={on:!!a.show.modal}:"undefined"==typeof a.show.modal.on&&(a.show.modal.on=D))},y.modal_zindex=y.zindex-200,wa.initialize="render",B.modal={"^show.modal.(on|blur)$":function(){this.destroy(),this.init(),this.qtip.elems.overlay.toggle(this.qtip.tooltip[0].offsetWidth>0)}},d.extend(D,y.defaults,{show:{modal:{on:E,effect:D,blur:D,stealfocus:D,escape:D}}}),R.viewport=function(c,d,e,f,g,h,i){function j(a,b,c,e,f,g,h,i,j){var k=d[f],s=u[a],t=v[a],w=c===Q,x=s===f?j:s===g?-j:-j/2,y=t===f?i:t===g?-i:-i/2,z=q[f]+r[f]-(n?0:m[f]),A=z-k,B=k+j-(h===I?o:p)-z,C=x-(u.precedance===a||s===u[b]?y:0)-(t===O?i/2:0);return w?(C=(s===f?1:-1)*x,d[f]+=A>0?A:B>0?-B:0,d[f]=Math.max(-m[f]+r[f],k-C,Math.min(Math.max(-m[f]+r[f]+(h===I?o:p),k+C),d[f],"center"===s?k-x:1e9))):(e*=c===P?2:0,A>0&&(s!==f||B>0)?(d[f]-=C+e,l.invert(a,f)):B>0&&(s!==g||A>0)&&(d[f]-=(s===O?-C:C)+e,l.invert(a,g)),d[f]<q[f]&&-d[f]>B&&(d[f]=k,l=u.clone())),d[f]-k}var k,l,m,n,o,p,q,r,s=e.target,t=c.elements.tooltip,u=e.my,v=e.at,w=e.adjust,x=w.method.split(" "),y=x[0],z=x[1]||x[0],A=e.viewport,B=e.container,C={left:0,top:0};return A.jquery&&s[0]!==a&&s[0]!==b.body&&"none"!==w.method?(m=B.offset()||C,n="static"===B.css("position"),k="fixed"===t.css("position"),o=A[0]===a?A.width():A.outerWidth(E),p=A[0]===a?A.height():A.outerHeight(E),q={left:k?0:A.scrollLeft(),top:k?0:A.scrollTop()},r=A.offset()||C,"shift"===y&&"shift"===z||(l=u.clone()),C={left:"none"!==y?j(G,H,y,w.x,L,N,I,f,h):0,top:"none"!==z?j(H,G,z,w.y,K,M,J,g,i):0,my:l}):C},R.polys={polygon:function(a,b){var c,d,e,f={width:0,height:0,position:{top:1e10,right:0,bottom:0,left:1e10},adjustable:E},g=0,h=[],i=1,j=1,k=0,l=0;for(g=a.length;g--;)c=[parseInt(a[--g],10),parseInt(a[g+1],10)],c[0]>f.position.right&&(f.position.right=c[0]),c[0]<f.position.left&&(f.position.left=c[0]),c[1]>f.position.bottom&&(f.position.bottom=c[1]),c[1]<f.position.top&&(f.position.top=c[1]),h.push(c);if(d=f.width=Math.abs(f.position.right-f.position.left),e=f.height=Math.abs(f.position.bottom-f.position.top),"c"===b.abbrev())f.position={left:f.position.left+f.width/2,top:f.position.top+f.height/2};else{for(;d>0&&e>0&&i>0&&j>0;)for(d=Math.floor(d/2),e=Math.floor(e/2),b.x===L?i=d:b.x===N?i=f.width-d:i+=Math.floor(d/2),b.y===K?j=e:b.y===M?j=f.height-e:j+=Math.floor(e/2),g=h.length;g--&&!(h.length<2);)k=h[g][0]-f.position.left,l=h[g][1]-f.position.top,(b.x===L&&k>=i||b.x===N&&i>=k||b.x===O&&(i>k||k>f.width-i)||b.y===K&&l>=j||b.y===M&&j>=l||b.y===O&&(j>l||l>f.height-j))&&h.splice(g,1);f.position={left:h[0][0],top:h[0][1]}}return f},rect:function(a,b,c,d){return{width:Math.abs(c-a),height:Math.abs(d-b),position:{left:Math.min(a,c),top:Math.min(b,d)}}},_angles:{tc:1.5,tr:7/4,tl:5/4,bc:.5,br:.25,bl:.75,rc:2,lc:1,c:0},ellipse:function(a,b,c,d,e){var f=R.polys._angles[e.abbrev()],g=0===f?0:c*Math.cos(f*Math.PI),h=d*Math.sin(f*Math.PI);return{width:2*c-Math.abs(g),height:2*d-Math.abs(h),position:{left:a+g,top:b+h},adjustable:E}},circle:function(a,b,c,d){return R.polys.ellipse(a,b,c,c,d)}},R.svg=function(a,c,e){for(var f,g,h,i,j,k,l,m,n,o=c[0],p=d(o.ownerSVGElement),q=o.ownerDocument,r=(parseInt(c.css("stroke-width"),10)||0)/2;!o.getBBox;)o=o.parentNode;if(!o.getBBox||!o.parentNode)return E;switch(o.nodeName){case"ellipse":case"circle":m=R.polys.ellipse(o.cx.baseVal.value,o.cy.baseVal.value,(o.rx||o.r).baseVal.value+r,(o.ry||o.r).baseVal.value+r,e);break;case"line":case"polygon":case"polyline":for(l=o.points||[{x:o.x1.baseVal.value,y:o.y1.baseVal.value},{x:o.x2.baseVal.value,y:o.y2.baseVal.value}],m=[],k=-1,i=l.numberOfItems||l.length;++k<i;)j=l.getItem?l.getItem(k):l[k],m.push.apply(m,[j.x,j.y]);m=R.polys.polygon(m,e);break;default:m=o.getBBox(),m={width:m.width,height:m.height,position:{left:m.x,top:m.y}}}return n=m.position,p=p[0],p.createSVGPoint&&(g=o.getScreenCTM(),l=p.createSVGPoint(),l.x=n.left,l.y=n.top,h=l.matrixTransform(g),n.left=h.x,n.top=h.y),q!==b&&"mouse"!==a.position.target&&(f=d((q.defaultView||q.parentWindow).frameElement).offset(),f&&(n.left+=f.left,n.top+=f.top)),q=d(q),n.left+=q.scrollLeft(),n.top+=q.scrollTop(),m},R.imagemap=function(a,b,c){b.jquery||(b=d(b));var e,f,g,h,i,j=(b.attr("shape")||"rect").toLowerCase().replace("poly","polygon"),k=d('img[usemap="#'+b.parent("map").attr("name")+'"]'),l=d.trim(b.attr("coords")),m=l.replace(/,$/,"").split(",");if(!k.length)return E;if("polygon"===j)h=R.polys.polygon(m,c);else{if(!R.polys[j])return E;for(g=-1,i=m.length,f=[];++g<i;)f.push(parseInt(m[g],10));h=R.polys[j].apply(this,f.concat(c))}return e=k.offset(),e.left+=Math.ceil((k.outerWidth(E)-k.width())/2),e.top+=Math.ceil((k.outerHeight(E)-k.height())/2),h.position.left+=e.left,h.position.top+=e.top,h};var Aa,Ba='<iframe class="qtip-bgiframe" frameborder="0" tabindex="-1" src="javascript:\'\';" style="display:block; position:absolute; z-index:-1; filter:alpha(opacity=0); -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";"></iframe>';d.extend(x.prototype,{_scroll:function(){var b=this.qtip.elements.overlay;b&&(b[0].style.top=d(a).scrollTop()+"px")},init:function(c){var e=c.tooltip;d("select, object").length<1&&(this.bgiframe=c.elements.bgiframe=d(Ba).appendTo(e),c._bind(e,"tooltipmove",this.adjustBGIFrame,this._ns,this)),this.redrawContainer=d("<div/>",{id:S+"-rcontainer"}).appendTo(b.body),c.elements.overlay&&c.elements.overlay.addClass("qtipmodal-ie6fix")&&(c._bind(a,["scroll","resize"],this._scroll,this._ns,this),c._bind(e,["tooltipshow"],this._scroll,this._ns,this)),this.redraw()},adjustBGIFrame:function(){var a,b,c=this.qtip.tooltip,d={height:c.outerHeight(E),width:c.outerWidth(E)},e=this.qtip.plugins.tip,f=this.qtip.elements.tip;b=parseInt(c.css("borderLeftWidth"),10)||0,b={left:-b,top:-b},e&&f&&(a="x"===e.corner.precedance?[I,L]:[J,K],b[a[1]]-=f[a[0]]()),this.bgiframe.css(b).css(d)},redraw:function(){if(this.qtip.rendered<1||this.drawing)return this;var a,b,c,d,e=this.qtip.tooltip,f=this.qtip.options.style,g=this.qtip.options.position.container;return this.qtip.drawing=1,f.height&&e.css(J,f.height),f.width?e.css(I,f.width):(e.css(I,"").appendTo(this.redrawContainer),b=e.width(),1>b%2&&(b+=1),c=e.css("maxWidth")||"",d=e.css("minWidth")||"",a=(c+d).indexOf("%")>-1?g.width()/100:0,c=(c.indexOf("%")>-1?a:1*parseInt(c,10))||b,d=(d.indexOf("%")>-1?a:1*parseInt(d,10))||0,b=c+d?Math.min(Math.max(b,d),c):b,e.css(I,Math.round(b)).appendTo(g)),this.drawing=0,this},destroy:function(){this.bgiframe&&this.bgiframe.remove(),this.qtip._unbind([a,this.qtip.tooltip],this._ns)}}),Aa=R.ie6=function(a){return 6===da.ie?new x(a):E},Aa.initialize="render",B.ie6={"^content|style$":function(){this.redraw()}}})}(window,document); diff --git a/js/jquery.sort.js b/js/jquery.sort.js new file mode 100644 index 0000000..51f1503 --- /dev/null +++ b/js/jquery.sort.js @@ -0,0 +1,65 @@ +/** + * jQuery.fn.sort + * -------------- + * @param Function comparator: + * Exactly the same behaviour as [1,2,3].sort(comparator) + * + * @param Function getSortable + * A function that should return the element that is + * to be sorted. The comparator will run on the + * current collection, but you may want the actual + * resulting sort to occur on a parent or another + * associated element. + * + * E.g. $('td').sort(comparator, function(){ + * return this.parentNode; + * }) + * + * The <td>'s parent (<tr>) will be sorted instead + * of the <td> itself. + */ +jQuery.fn.sort = (function(){ + + var sort = [].sort; + + return function(comparator, getSortable) { + + getSortable = getSortable || function(){return this;}; + + var placements = this.map(function(){ + + var sortElement = getSortable.call(this), + parentNode = sortElement.parentNode, + + // Since the element itself will change position, we have + // to have some way of storing its original position in + // the DOM. The easiest way is to have a 'flag' node: + nextSibling = parentNode.insertBefore( + document.createTextNode(''), + sortElement.nextSibling + ); + + return function() { + + if (parentNode === this) { + throw new Error( + "You can't sort elements if any one is a descendant of another." + ); + } + + // Insert before flag: + parentNode.insertBefore(this, nextSibling); + // Remove flag: + parentNode.removeChild(nextSibling); + + }; + + }); + + return sort.call(this, comparator).each(function(i){ + placements[i].call(getSortable.call(this)); + }); + + }; + +})(); \ No newline at end of file diff --git a/js/jqueryui.d.ts b/js/jqueryui.d.ts new file mode 100644 index 0000000..8c69bbe --- /dev/null +++ b/js/jqueryui.d.ts @@ -0,0 +1,1998 @@ +// Type definitions for jQueryUI 1.12 +// Project: http://jqueryui.com/ +// Definitions by: Boris Yankov <https://github.com/borisyankov>, John Reilly <https://github.com/johnnyreilly>, Dieter Oberkofler <https://github.com/doberkofler> +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.3 + +/// <reference path="jquery.d.ts" /> + +declare namespace JQueryUI { + // Accordion ////////////////////////////////////////////////// + + interface AccordionOptions extends AccordionEvents { + active?: any; // boolean or number + animate?: any; // boolean, number, string or object + collapsible?: boolean | undefined; + disabled?: boolean | undefined; + event?: string | undefined; + header?: string | undefined; + heightStyle?: string | undefined; + icons?: any; + } + + interface AccordionUIParams { + newHeader: JQuery; + oldHeader: JQuery; + newPanel: JQuery; + oldPanel: JQuery; + } + + interface AccordionEvent { + (event: JQueryEventObject, ui: AccordionUIParams): void; + } + + interface AccordionEvents { + activate?: AccordionEvent | undefined; + beforeActivate?: AccordionEvent | undefined; + create?: AccordionEvent | undefined; + } + + interface Accordion extends Widget, AccordionOptions { + } + + + // Autocomplete ////////////////////////////////////////////////// + + interface AutocompleteOptions extends AutocompleteEvents { + appendTo?: any; //Selector; + autoFocus?: boolean | undefined; + delay?: number | undefined; + disabled?: boolean | undefined; + minLength?: number | undefined; + position?: any; // object + source?: any; // [], string or () + classes?: AutocompleteClasses | undefined; + } + + interface AutocompleteClasses { + "ui-autocomplete"?: string | undefined; + "ui-autocomplete-input"?: string | undefined; + } + + interface AutocompleteUIParams { + /** + * The item selected from the menu, if any. Otherwise the property is null + */ + item?: any; + content?: any; + } + + interface AutocompleteEvent { + (event: JQueryEventObject, ui: AutocompleteUIParams): void; + } + + interface AutocompleteEvents { + change?: AutocompleteEvent | undefined; + close?: AutocompleteEvent | undefined; + create?: AutocompleteEvent | undefined; + focus?: AutocompleteEvent | undefined; + open?: AutocompleteEvent | undefined; + response?: AutocompleteEvent | undefined; + search?: AutocompleteEvent | undefined; + select?: AutocompleteEvent | undefined; + } + + interface Autocomplete extends Widget, AutocompleteOptions { + escapeRegex: (value: string) => string; + filter: (array: any, term: string) => any; + } + + + // Button ////////////////////////////////////////////////// + + interface ButtonOptions { + disabled?: boolean | undefined; + icons?: any; + label?: string | undefined; + text?: string|boolean | undefined; + click?: ((event?: Event) => void) | undefined; + } + + interface Button extends Widget, ButtonOptions { + } + + + // Datepicker ////////////////////////////////////////////////// + + interface DatepickerOptions { + /** + * An input element that is to be updated with the selected date from the datepicker. Use the altFormat option to change the format of the date within this field. Leave as blank for no alternate field. + */ + altField?: any; // Selector, jQuery or Element + /** + * The dateFormat to be used for the altField option. This allows one date format to be shown to the user for selection purposes, while a different format is actually sent behind the scenes. For a full list of the possible formats see the formatDate function + */ + altFormat?: string | undefined; + /** + * The text to display after each date field, e.g., to show the required format. + */ + appendText?: string | undefined; + /** + * Set to true to automatically resize the input field to accommodate dates in the current dateFormat. + */ + autoSize?: boolean | undefined; + /** + * A function that takes an input field and current datepicker instance and returns an options object to update the datepicker with. It is called just before the datepicker is displayed. + */ + beforeShow?: ((input: Element, inst: any) => JQueryUI.DatepickerOptions) | undefined; + /** + * A function that takes a date as a parameter and must return an array with: + * [0]: true/false indicating whether or not this date is selectable + * [1]: a CSS class name to add to the date's cell or "" for the default presentation + * [2]: an optional popup tooltip for this date + * The function is called for each day in the datepicker before it is displayed. + */ + beforeShowDay?: ((date: Date) => any[]) | undefined; + /** + * A URL of an image to use to display the datepicker when the showOn option is set to "button" or "both". If set, the buttonText option becomes the alt value and is not directly displayed. + */ + buttonImage?: string | undefined; + /** + * Whether the button image should be rendered by itself instead of inside a button element. This option is only relevant if the buttonImage option has also been set. + */ + buttonImageOnly?: boolean | undefined; + /** + * The text to display on the trigger button. Use in conjunction with the showOn option set to "button" or "both". + */ + buttonText?: string | undefined; + /** + * A function to calculate the week of the year for a given date. The default implementation uses the ISO 8601 definition: weeks start on a Monday; the first week of the year contains the first Thursday of the year. + */ + calculateWeek?: ((date: Date) => string) | undefined; + /** + * Whether the month should be rendered as a dropdown instead of text. + */ + changeMonth?: boolean | undefined; + /** + * Whether the year should be rendered as a dropdown instead of text. Use the yearRange option to control which years are made available for selection. + */ + changeYear?: boolean | undefined; + /** + * The text to display for the close link. Use the showButtonPanel option to display this button. + */ + closeText?: string | undefined; + /** + * When true, entry in the input field is constrained to those characters allowed by the current dateFormat option. + */ + constrainInput?: boolean | undefined; + /** + * The text to display for the current day link. Use the showButtonPanel option to display this button. + */ + currentText?: string | undefined; + /** + * The format for parsed and displayed dates. For a full list of the possible formats see the formatDate function. + */ + dateFormat?: string | undefined; + /** + * The list of long day names, starting from Sunday, for use as requested via the dateFormat option. + */ + dayNames?: string[] | undefined; + /** + * The list of minimised day names, starting from Sunday, for use as column headers within the datepicker. + */ + dayNamesMin?: string[] | undefined; + /** + * The list of abbreviated day names, starting from Sunday, for use as requested via the dateFormat option. + */ + dayNamesShort?: string[] | undefined; + /** + * Set the date to highlight on first opening if the field is blank. Specify either an actual date via a Date object or as a string in the current dateFormat, or a number of days from today (e.g. +7) or a string of values and periods ('y' for years, 'm' for months, 'w' for weeks, 'd' for days, e.g. '+1m +7d'), or null for today. + * Multiple types supported: + * Date: A date object containing the default date. + * Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday. + * String: A string in the format defined by the dateFormat option, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today. + */ + defaultDate?: any; // Date, number or string + /** + * Control the speed at which the datepicker appears, it may be a time in milliseconds or a string representing one of the three predefined speeds ("slow", "normal", "fast"). + */ + duration?: string | undefined; + /** + * Set the first day of the week: Sunday is 0, Monday is 1, etc. + */ + firstDay?: number | undefined; + /** + * When true, the current day link moves to the currently selected date instead of today. + */ + gotoCurrent?: boolean | undefined; + /** + * Normally the previous and next links are disabled when not applicable (see the minDate and maxDate options). You can hide them altogether by setting this attribute to true. + */ + hideIfNoPrevNext?: boolean | undefined; + /** + * Whether the current language is drawn from right to left. + */ + isRTL?: boolean | undefined; + /** + * The maximum selectable date. When set to null, there is no maximum. + * Multiple types supported: + * Date: A date object containing the maximum date. + * Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday. + * String: A string in the format defined by the dateFormat option, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today. + */ + maxDate?: any; // Date, number or string + /** + * The minimum selectable date. When set to null, there is no minimum. + * Multiple types supported: + * Date: A date object containing the minimum date. + * Number: A number of days from today. For example 2 represents two days from today and -1 represents yesterday. + * String: A string in the format defined by the dateFormat option, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today. + */ + minDate?: any; // Date, number or string + /** + * The list of full month names, for use as requested via the dateFormat option. + */ + monthNames?: string[] | undefined; + /** + * The list of abbreviated month names, as used in the month header on each datepicker and as requested via the dateFormat option. + */ + monthNamesShort?: string[] | undefined; + /** + * Whether the prevText and nextText options should be parsed as dates by the formatDate function, allowing them to display the target month names for example. + */ + navigationAsDateFormat?: boolean | undefined; + /** + * The text to display for the next month link. With the standard ThemeRoller styling, this value is replaced by an icon. + */ + nextText?: string | undefined; + /** + * The number of months to show at once. + * Multiple types supported: + * Number: The number of months to display in a single row. + * Array: An array defining the number of rows and columns to display. + */ + numberOfMonths?: any; // number or number[] + /** + * Called when the datepicker moves to a new month and/or year. The function receives the selected year, month (1-12), and the datepicker instance as parameters. this refers to the associated input field. + */ + onChangeMonthYear?: ((year: number, month: number, inst: any) => void) | undefined; + /** + * Called when the datepicker is closed, whether or not a date is selected. The function receives the selected date as text ("" if none) and the datepicker instance as parameters. this refers to the associated input field. + */ + onClose?: ((dateText: string, inst: any) => void) | undefined; + /** + * Called when the datepicker is selected. The function receives the selected date as text and the datepicker instance as parameters. this refers to the associated input field. + */ + onSelect?: ((dateText: string, inst: any) => void) | undefined; + /** + * The text to display for the previous month link. With the standard ThemeRoller styling, this value is replaced by an icon. + */ + prevText?: string | undefined; + /** + * Whether days in other months shown before or after the current month are selectable. This only applies if the showOtherMonths option is set to true. + */ + selectOtherMonths?: boolean | undefined; + /** + * The cutoff year for determining the century for a date (used in conjunction with dateFormat 'y'). Any dates entered with a year value less than or equal to the cutoff year are considered to be in the current century, while those greater than it are deemed to be in the previous century. + * Multiple types supported: + * Number: A value between 0 and 99 indicating the cutoff year. + * String: A relative number of years from the current year, e.g., "+3" or "-5". + */ + shortYearCutoff?: any; // number or string + /** + * The name of the animation used to show and hide the datepicker. Use "show" (the default), "slideDown", "fadeIn", any of the jQuery UI effects. Set to an empty string to disable animation. + */ + showAnim?: string | undefined; + /** + * Whether to display a button pane underneath the calendar. The button pane contains two buttons, a Today button that links to the current day, and a Done button that closes the datepicker. The buttons' text can be customized using the currentText and closeText options respectively. + */ + showButtonPanel?: boolean | undefined; + /** + * When displaying multiple months via the numberOfMonths option, the showCurrentAtPos option defines which position to display the current month in. + */ + showCurrentAtPos?: number | undefined; + /** + * Whether to show the month after the year in the header. + */ + showMonthAfterYear?: boolean | undefined; + /** + * When the datepicker should appear. The datepicker can appear when the field receives focus ("focus"), when a button is clicked ("button"), or when either event occurs ("both"). + */ + showOn?: string | undefined; + /** + * If using one of the jQuery UI effects for the showAnim option, you can provide additional settings for that animation via this option. + */ + showOptions?: any; // TODO + /** + * Whether to display dates in other months (non-selectable) at the start or end of the current month. To make these days selectable use the selectOtherMonths option. + */ + showOtherMonths?: boolean | undefined; + /** + * When true, a column is added to show the week of the year. The calculateWeek option determines how the week of the year is calculated. You may also want to change the firstDay option. + */ + showWeek?: boolean | undefined; + /** + * Set how many months to move when clicking the previous/next links. + */ + stepMonths?: number | undefined; + /** + * The text to display for the week of the year column heading. Use the showWeek option to display this column. + */ + weekHeader?: string | undefined; + /** + * The range of years displayed in the year drop-down: either relative to today's year ("-nn:+nn"), relative to the currently selected year ("c-nn:c+nn"), absolute ("nnnn:nnnn"), or combinations of these formats ("nnnn:-nn"). Note that this option only affects what appears in the drop-down, to restrict which dates may be selected use the minDate and/or maxDate options. + */ + yearRange?: string | undefined; + /** + * Additional text to display after the year in the month headers. + */ + yearSuffix?: string | undefined; + /** + * Set to true to automatically hide the datepicker. + */ + autohide?: boolean | undefined; + /** + * Set to date to automatically enddate the datepicker. + */ + endDate?: Date | undefined; + } + + interface DatepickerFormatDateOptions { + dayNamesShort?: string[] | undefined; + dayNames?: string[] | undefined; + monthNamesShort?: string[] | undefined; + monthNames?: string[] | undefined; + } + + interface Datepicker extends Widget, DatepickerOptions { + regional: { [languageCod3: string]: any; }; + setDefaults(defaults: DatepickerOptions): void; + formatDate(format: string, date: Date, settings?: DatepickerFormatDateOptions): string; + parseDate(format: string, date: string, settings?: DatepickerFormatDateOptions): Date; + iso8601Week(date: Date): number; + noWeekends(date: Date): any[]; + } + + + // Dialog ////////////////////////////////////////////////// + + interface DialogOptions extends DialogEvents { + autoOpen?: boolean | undefined; + buttons?: { [buttonText: string]: (event?: Event) => void } | DialogButtonOptions[] | undefined; + closeOnEscape?: boolean | undefined; + classes?: DialogClasses | undefined; + closeText?: string | undefined; + appendTo?: string | undefined; + dialogClass?: string | undefined; + disabled?: boolean | undefined; + draggable?: boolean | undefined; + height?: number | string | undefined; + hide?: boolean | number | string | DialogShowHideOptions | undefined; + maxHeight?: number | undefined; + maxWidth?: number | undefined; + minHeight?: number | undefined; + minWidth?: number | undefined; + modal?: boolean | undefined; + position?: any; // object, string or [] + resizable?: boolean | undefined; + show?: boolean | number | string | DialogShowHideOptions | undefined; + stack?: boolean | undefined; + title?: string | undefined; + width?: any; // number or string + zIndex?: number | undefined; + + open?: DialogEvent | undefined; + close?: DialogEvent | undefined; + } + + interface DialogClasses { + "ui-dialog"?: string | undefined; + "ui-dialog-content"?: string | undefined; + "ui-dialog-dragging"?: string | undefined; + "ui-dialog-resizing"?: string | undefined; + "ui-dialog-buttons"?: string | undefined; + "ui-dialog-titlebar"?: string | undefined; + "ui-dialog-title"?: string | undefined; + "ui-dialog-titlebar-close"?: string | undefined; + "ui-dialog-buttonpane"?: string | undefined; + "ui-dialog-buttonset"?: string | undefined; + "ui-widget-overlay"?: string | undefined; + } + + interface DialogButtonOptions { + icons?: any; + showText?: string | boolean | undefined; + text?: string | undefined; + click?: ((eventObject: JQueryEventObject) => any) | undefined; + [attr: string]: any; // attributes for the <button> element + } + + interface DialogShowHideOptions { + effect: string; + delay?: number | undefined; + duration?: number | undefined; + easing?: string | undefined; + } + + interface DialogUIParams { + } + + interface DialogEvent { + (event: JQueryEventObject, ui: DialogUIParams): void; + } + + interface DialogEvents { + beforeClose?: DialogEvent | undefined; + close?: DialogEvent | undefined; + create?: DialogEvent | undefined; + drag?: DialogEvent | undefined; + dragStart?: DialogEvent | undefined; + dragStop?: DialogEvent | undefined; + focus?: DialogEvent | undefined; + open?: DialogEvent | undefined; + resize?: DialogEvent | undefined; + resizeStart?: DialogEvent | undefined; + resizeStop?: DialogEvent | undefined; + } + + interface Dialog extends Widget, DialogOptions { + } + + + // Draggable ////////////////////////////////////////////////// + + interface DraggableEventUIParams { + helper: JQuery; + position: { top: number; left: number; }; + originalPosition: { top: number; left: number; }; + offset: { top: number; left: number; }; + } + + interface DraggableEvent { + (event: JQueryEventObject, ui: DraggableEventUIParams): void; + } + + interface DraggableOptions extends DraggableEvents { + disabled?: boolean | undefined; + addClasses?: boolean | undefined; + appendTo?: any; + axis?: string | undefined; + cancel?: string | undefined; + classes?: DraggableClasses | undefined; + connectToSortable?: Element | Element[] | JQuery | string | undefined; + containment?: any; + cursor?: string | undefined; + cursorAt?: any; + delay?: number | undefined; + distance?: number | undefined; + grid?: number[] | undefined; + handle?: any; + helper?: any; + iframeFix?: any; + opacity?: number | undefined; + refreshPositions?: boolean | undefined; + revert?: any; + revertDuration?: number | undefined; + scope?: string | undefined; + scroll?: boolean | undefined; + scrollSensitivity?: number | undefined; + scrollSpeed?: number | undefined; + snap?: any; + snapMode?: string | undefined; + snapTolerance?: number | undefined; + stack?: string | undefined; + zIndex?: number | undefined; + } + + interface DraggableClasses { + "ui-draggable"?: string | undefined; + "ui-draggable-disabled"?: string | undefined; + "ui-draggable-dragging"?: string | undefined; + "ui-draggable-handle"?: string | undefined; + } + + interface DraggableEvents { + create?: DraggableEvent | undefined; + start?: DraggableEvent | undefined; + drag?: DraggableEvent | undefined; + stop?: DraggableEvent | undefined; + } + + interface Draggable extends Widget, DraggableOptions, DraggableEvent { + } + + + // Droppable ////////////////////////////////////////////////// + + interface DroppableEventUIParam { + draggable: JQuery; + helper: JQuery; + position: { top: number; left: number; }; + offset: { top: number; left: number; }; + } + + interface DroppableEvent { + (event: JQueryEventObject, ui: DroppableEventUIParam): void; + } + + interface DroppableOptions extends DroppableEvents { + accept?: any; + activeClass?: string | undefined; + addClasses?: boolean | undefined; + disabled?: boolean | undefined; + greedy?: boolean | undefined; + hoverClass?: string | undefined; + scope?: string | undefined; + tolerance?: string | undefined; + } + + interface DroppableEvents { + create?: DroppableEvent | undefined; + activate?: DroppableEvent | undefined; + deactivate?: DroppableEvent | undefined; + over?: DroppableEvent | undefined; + out?: DroppableEvent | undefined; + drop?: DroppableEvent | undefined; + } + + interface Droppable extends Widget, DroppableOptions { + } + + // Menu ////////////////////////////////////////////////// + + interface MenuOptions extends MenuEvents { + disabled?: boolean | undefined; + icons?: any; + menus?: string | undefined; + position?: any; // TODO + role?: string | undefined; + } + + interface MenuUIParams { + item?: JQuery | undefined; + } + + interface MenuEvent { + (event: JQueryEventObject, ui: MenuUIParams): void; + } + + interface MenuEvents { + blur?: MenuEvent | undefined; + create?: MenuEvent | undefined; + focus?: MenuEvent | undefined; + select?: MenuEvent | undefined; + } + + interface Menu extends Widget, MenuOptions { + } + + + // Progressbar ////////////////////////////////////////////////// + + interface ProgressbarOptions extends ProgressbarEvents { + disabled?: boolean | undefined; + value?: number | boolean | undefined; + max?: number | undefined; + } + + interface ProgressbarUIParams { + } + + interface ProgressbarEvent { + (event: JQueryEventObject, ui: ProgressbarUIParams): void; + } + + interface ProgressbarEvents { + change?: ProgressbarEvent | undefined; + complete?: ProgressbarEvent | undefined; + create?: ProgressbarEvent | undefined; + } + + interface Progressbar extends Widget, ProgressbarOptions { + } + + + // Resizable ////////////////////////////////////////////////// + + interface ResizableOptions extends ResizableEvents { + alsoResize?: any; // Selector, JQuery or Element + animate?: boolean | undefined; + animateDuration?: any; // number or string + animateEasing?: string | undefined; + aspectRatio?: any; // boolean or number + autoHide?: boolean | undefined; + cancel?: string | undefined; + containment?: any; // Selector, Element or string + delay?: number | undefined; + disabled?: boolean | undefined; + distance?: number | undefined; + ghost?: boolean | undefined; + grid?: any; + handles?: any; // string or object + helper?: string | undefined; + maxHeight?: number | undefined; + maxWidth?: number | undefined; + minHeight?: number | undefined; + minWidth?: number | undefined; + } + + interface ResizableUIParams { + element: JQuery; + helper: JQuery; + originalElement: JQuery; + originalPosition: any; + originalSize: any; + position: any; + size: any; + } + + interface ResizableEvent { + (event: JQueryEventObject, ui: ResizableUIParams): void; + } + + interface ResizableEvents { + resize?: ResizableEvent | undefined; + start?: ResizableEvent | undefined; + stop?: ResizableEvent | undefined; + create?: ResizableEvent | undefined; + } + + interface Resizable extends Widget, ResizableOptions { + } + + + // Selectable ////////////////////////////////////////////////// + + interface SelectableOptions extends SelectableEvents { + autoRefresh?: boolean | undefined; + cancel?: string | undefined; + delay?: number | undefined; + disabled?: boolean | undefined; + distance?: number | undefined; + filter?: string | undefined; + tolerance?: string | undefined; + } + + interface SelectableEvents { + selected? (event: JQueryEventObject, ui: { selected?: Element | undefined; }): void; + selecting? (event: JQueryEventObject, ui: { selecting?: Element | undefined; }): void; + start? (event: JQueryEventObject, ui: any): void; + stop? (event: JQueryEventObject, ui: any): void; + unselected? (event: JQueryEventObject, ui: { unselected: Element; }): void; + unselecting? (event: JQueryEventObject, ui: { unselecting: Element; }): void; + } + + interface Selectable extends Widget, SelectableOptions { + } + + // SelectMenu ////////////////////////////////////////////////// + + interface SelectMenuOptions extends SelectMenuEvents { + appendTo?: string | undefined; + classes?: SelectMenuClasses | undefined; + disabled?: boolean | undefined; + icons?: any; + position?: JQueryPositionOptions | undefined; + width?: number | undefined; + } + + interface SelectMenuClasses { + "ui-selectmenu-button"?: string | undefined; + "ui-selectmenu-button-closed"?: string | undefined; + "ui-selectmenu-button-open"?: string | undefined; + "ui-selectmenu-text"?: string | undefined; + "ui-selectmenu-icon"?: string | undefined; + "ui-selectmenu-menu"?: string | undefined; + "ui-selectmenu-open"?: string | undefined; + "ui-selectmenu-optgroup"?: string | undefined; + } + + interface SelectMenuUIParams { + item?: JQuery | undefined; + } + + interface SelectMenuEvent { + (event: JQueryEventObject, ui: SelectMenuUIParams): void; + } + + interface SelectMenuEvents { + change?: SelectMenuEvent | undefined; + close?: SelectMenuEvent | undefined; + create?: SelectMenuEvent | undefined; + focus?: SelectMenuEvent | undefined; + open?: SelectMenuEvent | undefined; + select?: SelectMenuEvent | undefined; + } + + interface SelectMenu extends Widget, SelectMenuOptions { + } + + // Slider ////////////////////////////////////////////////// + + interface SliderOptions extends SliderEvents { + animate?: any; // boolean, string or number + disabled?: boolean | undefined; + max?: number | undefined; + min?: number | undefined; + orientation?: string | undefined; + range?: any; // boolean or string + step?: number | undefined; + value?: number | undefined; + values?: number[] | undefined; + highlight?: boolean | undefined; + classes? : SliderClasses | undefined; + } + + interface SliderClasses { + "ui-slider"?: string | undefined; + "ui-slider-horizontal"?: string | undefined; + "ui-slider-vertical"?: string | undefined; + "ui-slider-handle"?: string | undefined; + "ui-slider-range"?: string | undefined; + "ui-slider-range-min"?: string | undefined; + "ui-slider-range-max"?: string | undefined; + } + + interface SliderUIParams { + handle?: JQuery | undefined; + value?: number | undefined; + values?: number[] | undefined; + } + + interface SliderEvent { + (event: JQueryEventObject, ui: SliderUIParams): void; + } + + interface SliderEvents { + change?: SliderEvent | undefined; + create?: SliderEvent | undefined; + slide?: SliderEvent | undefined; + start?: SliderEvent | undefined; + stop?: SliderEvent | undefined; + } + + interface Slider extends Widget, SliderOptions { + } + + + // Sortable ////////////////////////////////////////////////// + + interface SortableOptions extends SortableEvents { + appendTo?: any; // jQuery, Element, Selector or string + attribute?: string | undefined; + axis?: string | undefined; + cancel?: any; // Selector + connectWith?: any; // Selector + containment?: any; // Element, Selector or string + cursor?: string | undefined; + cursorAt?: any; + delay?: number | undefined; + disabled?: boolean | undefined; + distance?: number | undefined; + dropOnEmpty?: boolean | undefined; + forceHelperSize?: boolean | undefined; + forcePlaceholderSize?: boolean | undefined; + grid?: number[] | undefined; + helper?: string | ((event: JQueryEventObject, element: Sortable) => Element) | undefined; + handle?: any; // Selector or Element + items?: any; // Selector + opacity?: number | undefined; + placeholder?: string | undefined; + revert?: any; // boolean or number + scroll?: boolean | undefined; + scrollSensitivity?: number | undefined; + scrollSpeed?: number | undefined; + tolerance?: string | undefined; + zIndex?: number | undefined; + } + + interface SortableUIParams { + helper: JQuery; + item: JQuery; + offset: any; + position: any; + originalPosition: any; + sender: JQuery; + placeholder: JQuery; + } + + interface SortableEvent { + (event: JQueryEventObject, ui: SortableUIParams): void; + } + + interface SortableEvents { + activate?: SortableEvent | undefined; + beforeStop?: SortableEvent | undefined; + change?: SortableEvent | undefined; + deactivate?: SortableEvent | undefined; + out?: SortableEvent | undefined; + over?: SortableEvent | undefined; + receive?: SortableEvent | undefined; + remove?: SortableEvent | undefined; + sort?: SortableEvent | undefined; + start?: SortableEvent | undefined; + stop?: SortableEvent | undefined; + update?: SortableEvent | undefined; + } + + interface Sortable extends Widget, SortableOptions, SortableEvents { + } + + + // Spinner ////////////////////////////////////////////////// + + interface SpinnerOptions extends SpinnerEvents { + culture?: string | undefined; + disabled?: boolean | undefined; + icons?: any; + incremental?: any; // boolean or () + max?: any; // number or string + min?: any; // number or string + numberFormat?: string | undefined; + page?: number | undefined; + step?: any; // number or string + } + + interface SpinnerUIParam { + value: number; + } + + interface SpinnerEvent<T> { + (event: JQueryEventObject, ui: T): void; + } + + interface SpinnerEvents { + change?: SpinnerEvent<{}> | undefined; + create?: SpinnerEvent<{}> | undefined; + spin?: SpinnerEvent<SpinnerUIParam> | undefined; + start?: SpinnerEvent<{}> | undefined; + stop?: SpinnerEvent<{}> | undefined; + } + + interface Spinner extends Widget, SpinnerOptions { + } + + + // Tabs ////////////////////////////////////////////////// + + interface TabsOptions extends TabsEvents { + active?: any; // boolean or number + classes?: TabClasses | undefined; + collapsible?: boolean | undefined; + disabled?: any; // boolean or [] + event?: string | undefined; + heightStyle?: string | undefined; + hide?: any; // boolean, number, string or object + show?: any; // boolean, number, string or object + } + + interface TabClasses { + "ui-tabs"?: string | undefined; + "ui-tabs-collapsible"?: string | undefined; + "ui-tabs-nav"?: string | undefined; + "ui-tabs-tab"?: string | undefined; + "ui-tabs-active"?: string | undefined; + "ui-tabs-loading"?: string | undefined; + "ui-tabs-anchor"?: string | undefined; + "ui-tabs-panel"?: string | undefined; + } + + interface TabsActivationUIParams { + newTab: JQuery; + oldTab: JQuery; + newPanel: JQuery; + oldPanel: JQuery; + } + + interface TabsBeforeLoadUIParams { + tab: JQuery; + panel: JQuery; + jqXHR: JQueryXHR; + ajaxSettings: any; + } + + interface TabsCreateOrLoadUIParams { + tab: JQuery; + panel: JQuery; + } + + interface TabsEvent<UI> { + (event: JQueryEventObject, ui: UI): void; + } + + interface TabsEvents { + activate?: TabsEvent<TabsActivationUIParams> | undefined; + beforeActivate?: TabsEvent<TabsActivationUIParams> | undefined; + beforeLoad?: TabsEvent<TabsBeforeLoadUIParams> | undefined; + load?: TabsEvent<TabsCreateOrLoadUIParams> | undefined; + create?: TabsEvent<TabsCreateOrLoadUIParams> | undefined; + } + + interface Tabs extends Widget, TabsOptions { + } + + + // Tooltip ////////////////////////////////////////////////// + + interface TooltipOptions extends TooltipEvents { + content?: any; // () or string + disabled?: boolean | undefined; + hide?: any; // boolean, number, string or object + items?: string|JQuery | undefined; + position?: any; // TODO + show?: any; // boolean, number, string or object + tooltipClass?: string | undefined; // deprecated in jQuery UI 1.12 + track?: boolean | undefined; + classes?: {[key: string]: string} | undefined; + } + + interface TooltipUIParams { + } + + interface TooltipEvent { + (event: JQueryEventObject, ui: TooltipUIParams): void; + } + + interface TooltipEvents { + close?: TooltipEvent | undefined; + open?: TooltipEvent | undefined; + } + + interface Tooltip extends Widget, TooltipOptions { + } + + + // Effects ////////////////////////////////////////////////// + + interface EffectOptions { + effect: string; + easing?: string | undefined; + duration?: number | undefined; + complete: Function; + } + + interface BlindEffect { + direction?: string | undefined; + } + + interface BounceEffect { + distance?: number | undefined; + times?: number | undefined; + } + + interface ClipEffect { + direction?: number | undefined; + } + + interface DropEffect { + direction?: number | undefined; + } + + interface ExplodeEffect { + pieces?: number | undefined; + } + + interface FadeEffect { } + + interface FoldEffect { + size?: any; + horizFirst?: boolean | undefined; + } + + interface HighlightEffect { + color?: string | undefined; + } + + interface PuffEffect { + percent?: number | undefined; + } + + interface PulsateEffect { + times?: number | undefined; + } + + interface ScaleEffect { + direction?: string | undefined; + origin?: string[] | undefined; + percent?: number | undefined; + scale?: string | undefined; + } + + interface ShakeEffect { + direction?: string | undefined; + distance?: number | undefined; + times?: number | undefined; + } + + interface SizeEffect { + to?: any; + origin?: string[] | undefined; + scale?: string | undefined; + } + + interface SlideEffect { + direction?: string | undefined; + distance?: number | undefined; + } + + interface TransferEffect { + className?: string | undefined; + to?: string | undefined; + } + + interface JQueryPositionOptions { + my?: string | undefined; + at?: string | undefined; + of?: any; + collision?: string | undefined; + using?: Function | undefined; + within?: any; + } + + + // UI ////////////////////////////////////////////////// + + interface MouseOptions { + cancel?: string | undefined; + delay?: number | undefined; + distance?: number | undefined; + } + + interface KeyCode { + BACKSPACE: number; + COMMA: number; + DELETE: number; + DOWN: number; + END: number; + ENTER: number; + ESCAPE: number; + HOME: number; + LEFT: number; + NUMPAD_ADD: number; + NUMPAD_DECIMAL: number; + NUMPAD_DIVIDE: number; + NUMPAD_ENTER: number; + NUMPAD_MULTIPLY: number; + NUMPAD_SUBTRACT: number; + PAGE_DOWN: number; + PAGE_UP: number; + PERIOD: number; + RIGHT: number; + SPACE: number; + TAB: number; + UP: number; + } + + interface UI { + mouse(method: string): JQuery; + mouse(options: MouseOptions): JQuery; + mouse(optionLiteral: string, optionName: string, optionValue: any): JQuery; + mouse(optionLiteral: string, optionValue: any): any; + + accordion: Accordion; + autocomplete: Autocomplete; + button: Button; + buttonset: Button; + datepicker: Datepicker; + dialog: Dialog; + keyCode: KeyCode; + menu: Menu; + progressbar: Progressbar; + selectmenu: SelectMenu; + slider: Slider; + spinner: Spinner; + tabs: Tabs; + tooltip: Tooltip; + version: string; + } + + + // Widget ////////////////////////////////////////////////// + + interface WidgetOptions { + disabled?: boolean | undefined; + hide?: any; + show?: any; + } + + interface WidgetCommonProperties { + element: JQuery; + defaultElement : string; + document: Document; + namespace: string; + uuid: string; + widgetEventPrefix: string; + widgetFullName: string; + window: Window; + } + + interface Widget { + (methodName: string): JQuery; + (options: WidgetOptions): JQuery; + (options: AccordionOptions): JQuery; + (optionLiteral: string, optionName: string): any; + (optionLiteral: string, options: WidgetOptions): any; + (optionLiteral: string, optionName: string, optionValue: any): JQuery; + + <T>(name: string, prototype: T & ThisType<T & WidgetCommonProperties>): JQuery; + <T>(name: string, base: Function, prototype: T & ThisType<T & WidgetCommonProperties> ): JQuery; + } + + //////////////////////////////////////////////////////////////////////////////////////////////////// + +} + +interface JQuery { + + accordion(): JQuery; + accordion(methodName: 'destroy'): void; + accordion(methodName: 'disable'): void; + accordion(methodName: 'enable'): void; + accordion(methodName: 'refresh'): void; + accordion(methodName: 'widget'): JQuery; + accordion(methodName: string): JQuery; + accordion(options: JQueryUI.AccordionOptions): JQuery; + accordion(optionLiteral: string, optionName: string): any; + accordion(optionLiteral: string, options: JQueryUI.AccordionOptions): any; + accordion(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + autocomplete(): JQuery; + autocomplete(methodName: 'close'): void; + autocomplete(methodName: 'destroy'): void; + autocomplete(methodName: 'disable'): void; + autocomplete(methodName: 'enable'): void; + autocomplete(methodName: 'search', value?: string): void; + autocomplete(methodName: 'widget'): JQuery; + autocomplete(methodName: string): JQuery; + autocomplete(options: JQueryUI.AutocompleteOptions): JQuery; + autocomplete(optionLiteral: string, optionName: string): any; + autocomplete(optionLiteral: string, options: JQueryUI.AutocompleteOptions): any; + autocomplete(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + button(): JQuery; + button(methodName: 'destroy'): void; + button(methodName: 'disable'): void; + button(methodName: 'enable'): void; + button(methodName: 'refresh'): void; + button(methodName: 'widget'): JQuery; + button(methodName: string): JQuery; + button(options: JQueryUI.ButtonOptions): JQuery; + button(optionLiteral: string, optionName: string): any; + button(optionLiteral: string, options: JQueryUI.ButtonOptions): any; + button(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + buttonset(): JQuery; + buttonset(methodName: 'destroy'): void; + buttonset(methodName: 'disable'): void; + buttonset(methodName: 'enable'): void; + buttonset(methodName: 'refresh'): void; + buttonset(methodName: 'widget'): JQuery; + buttonset(methodName: string): JQuery; + buttonset(options: JQueryUI.ButtonOptions): JQuery; + buttonset(optionLiteral: string, optionName: string): any; + buttonset(optionLiteral: string, options: JQueryUI.ButtonOptions): any; + buttonset(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + /** + * Initialize a datepicker + */ + datepicker(): JQuery; + /** + * Removes the datepicker functionality completely. This will return the element back to its pre-init state. + * + * @param methodName 'destroy' + */ + datepicker(methodName: 'destroy'): JQuery; + /** + * Opens the datepicker in a dialog box. + * + * @param methodName 'dialog' + * @param date The initial date. + * @param onSelect A callback function when a date is selected. The function receives the date text and date picker instance as parameters. + * @param settings The new settings for the date picker. + * @param pos The position of the top/left of the dialog as [x, y] or a MouseEvent that contains the coordinates. If not specified the dialog is centered on the screen. + */ + datepicker(methodName: 'dialog', date: Date, onSelect?: () => void, settings?: JQueryUI.DatepickerOptions, pos?: number[]): JQuery; + /** + * Opens the datepicker in a dialog box. + * + * @param methodName 'dialog' + * @param date The initial date. + * @param onSelect A callback function when a date is selected. The function receives the date text and date picker instance as parameters. + * @param settings The new settings for the date picker. + * @param pos The position of the top/left of the dialog as [x, y] or a MouseEvent that contains the coordinates. If not specified the dialog is centered on the screen. + */ + datepicker(methodName: 'dialog', date: Date, onSelect?: () => void, settings?: JQueryUI.DatepickerOptions, pos?: MouseEvent): JQuery; + /** + * Opens the datepicker in a dialog box. + * + * @param methodName 'dialog' + * @param date The initial date. + * @param onSelect A callback function when a date is selected. The function receives the date text and date picker instance as parameters. + * @param settings The new settings for the date picker. + * @param pos The position of the top/left of the dialog as [x, y] or a MouseEvent that contains the coordinates. If not specified the dialog is centered on the screen. + */ + datepicker(methodName: 'dialog', date: string, onSelect?: () => void, settings?: JQueryUI.DatepickerOptions, pos?: number[]): JQuery; + /** + * Opens the datepicker in a dialog box. + * + * @param methodName 'dialog' + * @param date The initial date. + * @param onSelect A callback function when a date is selected. The function receives the date text and date picker instance as parameters. + * @param settings The new settings for the date picker. + * @param pos The position of the top/left of the dialog as [x, y] or a MouseEvent that contains the coordinates. If not specified the dialog is centered on the screen. + */ + datepicker(methodName: 'dialog', date: string, onSelect?: () => void, settings?: JQueryUI.DatepickerOptions, pos?: MouseEvent): JQuery; + /** + * Returns the current date for the datepicker or null if no date has been selected. + * + * @param methodName 'getDate' + */ + datepicker(methodName: 'getDate'): Date; + /** + * Close a previously opened date picker. + * + * @param methodName 'hide' + */ + datepicker(methodName: 'hide'): JQuery; + /** + * Determine whether a date picker has been disabled. + * + * @param methodName 'isDisabled' + */ + datepicker(methodName: 'isDisabled'): boolean; + /** + * Redraw the date picker, after having made some external modifications. + * + * @param methodName 'refresh' + */ + datepicker(methodName: 'refresh'): JQuery; + /** + * Sets the date for the datepicker. The new date may be a Date object or a string in the current date format (e.g., "01/26/2009"), a number of days from today (e.g., +7) or a string of values and periods ("y" for years, "m" for months, "w" for weeks, "d" for days, e.g., "+1m +7d"), or null to clear the selected date. + * + * @param methodName 'setDate' + * @param date The new date. + */ + datepicker(methodName: 'setDate', date: Date): JQuery; + /** + * Sets the date for the datepicker. The new date may be a Date object or a string in the current date format (e.g., "01/26/2009"), a number of days from today (e.g., +7) or a string of values and periods ("y" for years, "m" for months, "w" for weeks, "d" for days, e.g., "+1m +7d"), or null to clear the selected date. + * + * @param methodName 'setDate' + * @param date The new date. + */ + datepicker(methodName: 'setDate', date: string): JQuery; + /** + * Open the date picker. If the datepicker is attached to an input, the input must be visible for the datepicker to be shown. + * + * @param methodName 'show' + */ + datepicker(methodName: 'show'): JQuery; + /** + * Returns a jQuery object containing the datepicker. + * + * @param methodName 'widget' + */ + datepicker(methodName: 'widget'): JQuery; + + /** + * Get the altField option, after initialization + * + * @param methodName 'option' + * @param optionName 'altField' + */ + datepicker(methodName: 'option', optionName: 'altField'): any; + /** + * Set the altField option, after initialization + * + * @param methodName 'option' + * @param optionName 'altField' + * @param altFieldValue An input element that is to be updated with the selected date from the datepicker. Use the altFormat option to change the format of the date within this field. Leave as blank for no alternate field. + */ + datepicker(methodName: 'option', optionName: 'altField', altFieldValue: string): JQuery; + /** + * Set the altField option, after initialization + * + * @param methodName 'option' + * @param optionName 'altField' + * @param altFieldValue An input element that is to be updated with the selected date from the datepicker. Use the altFormat option to change the format of the date within this field. Leave as blank for no alternate field. + */ + datepicker(methodName: 'option', optionName: 'altField', altFieldValue: JQuery): JQuery; + /** + * Set the altField option, after initialization + * + * @param methodName 'option' + * @param optionName 'altField' + * @param altFieldValue An input element that is to be updated with the selected date from the datepicker. Use the altFormat option to change the format of the date within this field. Leave as blank for no alternate field. + */ + datepicker(methodName: 'option', optionName: 'altField', altFieldValue: Element): JQuery; + + /** + * Get the altFormat option, after initialization + * + * @param methodName 'option' + * @param optionName 'altFormat' + */ + datepicker(methodName: 'option', optionName: 'altFormat'): string; + /** + * Set the altFormat option, after initialization + * + * @param methodName 'option' + * @param optionName 'altFormat' + * @param altFormatValue The dateFormat to be used for the altField option. This allows one date format to be shown to the user for selection purposes, while a different format is actually sent behind the scenes. For a full list of the possible formats see the formatDate function + */ + datepicker(methodName: 'option', optionName: 'altFormat', altFormatValue: string): JQuery; + + /** + * Get the appendText option, after initialization + * + * @param methodName 'option' + * @param optionName 'appendText' + */ + datepicker(methodName: 'option', optionName: 'appendText'): string; + /** + * Set the appendText option, after initialization + * + * @param methodName 'option' + * @param optionName 'appendText' + * @param appendTextValue The text to display after each date field, e.g., to show the required format. + */ + datepicker(methodName: 'option', optionName: 'appendText', appendTextValue: string): JQuery; + + /** + * Get the autoSize option, after initialization + * + * @param methodName 'option' + * @param optionName 'autoSize' + */ + datepicker(methodName: 'option', optionName: 'autoSize'): boolean; + /** + * Set the autoSize option, after initialization + * + * @param methodName 'option' + * @param optionName 'autoSize' + * @param autoSizeValue Set to true to automatically resize the input field to accommodate dates in the current dateFormat. + */ + datepicker(methodName: 'option', optionName: 'autoSize', autoSizeValue: boolean): JQuery; + + /** + * Get the beforeShow option, after initialization + * + * @param methodName 'option' + * @param optionName 'beforeShow' + */ + datepicker(methodName: 'option', optionName: 'beforeShow'): (input: Element, inst: any) => JQueryUI.DatepickerOptions; + /** + * Set the beforeShow option, after initialization + * + * @param methodName 'option' + * @param optionName 'beforeShow' + * @param beforeShowValue A function that takes an input field and current datepicker instance and returns an options object to update the datepicker with. It is called just before the datepicker is displayed. + */ + datepicker(methodName: 'option', optionName: 'beforeShow', beforeShowValue: (input: Element, inst: any) => JQueryUI.DatepickerOptions): JQuery; + + /** + * Get the beforeShow option, after initialization + * + * @param methodName 'option' + * @param optionName 'beforeShowDay' + */ + datepicker(methodName: 'option', optionName: 'beforeShowDay'): (date: Date) => any[]; + /** + * Set the beforeShow option, after initialization + * + * @param methodName 'option' + * @param optionName 'beforeShowDay' + * @param beforeShowDayValue A function that takes a date as a parameter and must return an array with: + * [0]: true/false indicating whether or not this date is selectable + * [1]: a CSS class name to add to the date's cell or "" for the default presentation + * [2]: an optional popup tooltip for this date + * The function is called for each day in the datepicker before it is displayed. + */ + datepicker(methodName: 'option', optionName: 'beforeShowDay', beforeShowDayValue: (date: Date) => any[]): JQuery; + + /** + * Get the buttonImage option, after initialization + * + * @param methodName 'option' + * @param optionName 'buttonImage' + */ + datepicker(methodName: 'option', optionName: 'buttonImage'): string; + /** + * Set the buttonImage option, after initialization + * + * @param methodName 'option' + * @param optionName 'buttonImage' + * @param buttonImageValue A URL of an image to use to display the datepicker when the showOn option is set to "button" or "both". If set, the buttonText option becomes the alt value and is not directly displayed. + */ + datepicker(methodName: 'option', optionName: 'buttonImage', buttonImageValue: string): JQuery; + + /** + * Get the buttonImageOnly option, after initialization + * + * @param methodName 'option' + * @param optionName 'buttonImageOnly' + */ + datepicker(methodName: 'option', optionName: 'buttonImageOnly'): boolean; + /** + * Set the buttonImageOnly option, after initialization + * + * @param methodName 'option' + * @param optionName 'buttonImageOnly' + * @param buttonImageOnlyValue Whether the button image should be rendered by itself instead of inside a button element. This option is only relevant if the buttonImage option has also been set. + */ + datepicker(methodName: 'option', optionName: 'buttonImageOnly', buttonImageOnlyValue: boolean): JQuery; + + /** + * Get the buttonText option, after initialization + * + * @param methodName 'option' + * @param optionName 'buttonText' + */ + datepicker(methodName: 'option', optionName: 'buttonText'): string; + + /** + * Get the autohide option, after initialization + * + * @param methodName 'option' + * @param optionName 'autohide' + */ + datepicker(methodName: 'option', optionName: 'autohide'): boolean; + + + /** + * Get the endDate after initialization + * + * @param methodName 'option' + * @param optionName 'endDate' + */ + datepicker(methodName: 'option', optionName: 'endDate'): Date; + /** + * Set the buttonText option, after initialization + * + * @param methodName 'option' + * @param optionName 'buttonText' + * @param buttonTextValue The text to display on the trigger button. Use in conjunction with the showOn option set to "button" or "both". + */ + datepicker(methodName: 'option', optionName: 'buttonText', buttonTextValue: string): JQuery; + + /** + * Get the calculateWeek option, after initialization + * + * @param methodName 'option' + * @param optionName 'calculateWeek' + */ + datepicker(methodName: 'option', optionName: 'calculateWeek'): (date: Date) => string; + /** + * Set the calculateWeek option, after initialization + * + * @param methodName 'option' + * @param optionName 'calculateWeek' + * @param calculateWeekValue A function to calculate the week of the year for a given date. The default implementation uses the ISO 8601 definition: weeks start on a Monday; the first week of the year contains the first Thursday of the year. + */ + datepicker(methodName: 'option', optionName: 'calculateWeek', calculateWeekValue: (date: Date) => string): JQuery; + + /** + * Get the changeMonth option, after initialization + * + * @param methodName 'option' + * @param optionName 'changeMonth' + */ + datepicker(methodName: 'option', optionName: 'changeMonth'): boolean; + /** + * Set the changeMonth option, after initialization + * + * @param methodName 'option' + * @param optionName 'changeMonth' + * @param changeMonthValue Whether the month should be rendered as a dropdown instead of text. + */ + datepicker(methodName: 'option', optionName: 'changeMonth', changeMonthValue: boolean): JQuery; + + /** + * Get the changeYear option, after initialization + * + * @param methodName 'option' + * @param optionName 'changeYear' + */ + datepicker(methodName: 'option', optionName: 'changeYear'): boolean; + /** + * Set the changeYear option, after initialization + * + * @param methodName 'option' + * @param optionName 'changeYear' + * @param changeYearValue Whether the year should be rendered as a dropdown instead of text. Use the yearRange option to control which years are made available for selection. + */ + datepicker(methodName: 'option', optionName: 'changeYear', changeYearValue: boolean): JQuery; + + /** + * Get the closeText option, after initialization + * + * @param methodName 'option' + * @param optionName 'closeText' + */ + datepicker(methodName: 'option', optionName: 'closeText'): string; + /** + * Set the closeText option, after initialization + * + * @param methodName 'option' + * @param optionName 'closeText' + * @param closeTextValue The text to display for the close link. Use the showButtonPanel option to display this button. + */ + datepicker(methodName: 'option', optionName: 'closeText', closeTextValue: string): JQuery; + + /** + * Get the constrainInput option, after initialization + * + * @param methodName 'option' + * @param optionName 'constrainInput' + */ + datepicker(methodName: 'option', optionName: 'constrainInput'): boolean; + /** + * Set the constrainInput option, after initialization + * + * @param methodName 'option' + * @param optionName 'constrainInput' + * @param constrainInputValue When true, entry in the input field is constrained to those characters allowed by the current dateFormat option. + */ + datepicker(methodName: 'option', optionName: 'constrainInput', constrainInputValue: boolean): JQuery; + + /** + * Get the currentText option, after initialization + * + * @param methodName 'option' + * @param optionName 'currentText' + */ + datepicker(methodName: 'option', optionName: 'currentText'): string; + /** + * Set the currentText option, after initialization + * + * @param methodName 'option' + * @param optionName 'currentText' + * @param currentTextValue The text to display for the current day link. Use the showButtonPanel option to display this button. + */ + datepicker(methodName: 'option', optionName: 'currentText', currentTextValue: string): JQuery; + + /** + * Get the dateFormat option, after initialization + * + * @param methodName 'option' + * @param optionName 'dateFormat' + */ + datepicker(methodName: 'option', optionName: 'dateFormat'): string; + /** + * Set the dateFormat option, after initialization + * + * @param methodName 'option' + * @param optionName 'dateFormat' + * @param dateFormatValue The format for parsed and displayed dates. For a full list of the possible formats see the formatDate function. + */ + datepicker(methodName: 'option', optionName: 'dateFormat', dateFormatValue: string): JQuery; + + /** + * Get the dayNames option, after initialization + * + * @param methodName 'option' + * @param optionName 'dayNames' + */ + datepicker(methodName: 'option', optionName: 'dayNames'): string[]; + /** + * Set the dayNames option, after initialization + * + * @param methodName 'option' + * @param optionName 'dayNames' + * @param dayNamesValue The list of long day names, starting from Sunday, for use as requested via the dateFormat option. + */ + datepicker(methodName: 'option', optionName: 'dayNames', dayNamesValue: string[]): JQuery; + + /** + * Get the dayNamesMin option, after initialization + * + * @param methodName 'option' + * @param optionName 'dayNamesMin' + */ + datepicker(methodName: 'option', optionName: 'dayNamesMin'): string[]; + /** + * Set the dayNamesMin option, after initialization + * + * @param methodName 'option' + * @param optionName 'dayNamesMin' + * @param dayNamesMinValue The list of minimised day names, starting from Sunday, for use as column headers within the datepicker. + */ + datepicker(methodName: 'option', optionName: 'dayNamesMin', dayNamesMinValue: string[]): JQuery; + + /** + * Get the dayNamesShort option, after initialization + * + * @param methodName 'option' + * @param optionName 'dayNamesShort' + */ + datepicker(methodName: 'option', optionName: 'dayNamesShort'): string[]; + /** + * Set the dayNamesShort option, after initialization + * + * @param methodName 'option' + * @param optionName 'dayNamesShort' + * @param dayNamesShortValue The list of abbreviated day names, starting from Sunday, for use as requested via the dateFormat option. + */ + datepicker(methodName: 'option', optionName: 'dayNamesShort', dayNamesShortValue: string[]): JQuery; + + /** + * Get the defaultDate option, after initialization + * + * @param methodName 'option' + * @param optionName 'defaultDate' + */ + datepicker(methodName: 'option', optionName: 'defaultDate'): any; + /** + * Set the defaultDate option, after initialization + * + * @param methodName 'option' + * @param optionName 'defaultDate' + * @param defaultDateValue A date object containing the default date. + */ + datepicker(methodName: 'option', optionName: 'defaultDate', defaultDateValue: Date): JQuery; + /** + * Set the defaultDate option, after initialization + * + * @param methodName 'option' + * @param optionName 'defaultDate' + * @param defaultDateValue A number of days from today. For example 2 represents two days from today and -1 represents yesterday. + */ + datepicker(methodName: 'option', optionName: 'defaultDate', defaultDateValue: number): JQuery; + /** + * Set the defaultDate option, after initialization + * + * @param methodName 'option' + * @param optionName 'defaultDate' + * @param defaultDateValue A string in the format defined by the dateFormat option, or a relative date. Relative dates must contain value and period pairs; valid periods are "y" for years, "m" for months, "w" for weeks, and "d" for days. For example, "+1m +7d" represents one month and seven days from today. + */ + datepicker(methodName: 'option', optionName: 'defaultDate', defaultDateValue: string): JQuery; + + /** + * Get the duration option, after initialization + * + * @param methodName 'option' + * @param optionName 'duration' + */ + datepicker(methodName: 'option', optionName: 'duration'): string; + /** + * Set the duration option, after initialization + * + * @param methodName 'option' + * @param optionName 'duration' + * @param durationValue Control the speed at which the datepicker appears, it may be a time in milliseconds or a string representing one of the three predefined speeds ("slow", "normal", "fast"). + */ + datepicker(methodName: 'option', optionName: 'duration', durationValue: string): JQuery; + + /** + * Get the firstDay option, after initialization + * + * @param methodName 'option' + * @param optionName 'firstDay' + */ + datepicker(methodName: 'option', optionName: 'firstDay'): number; + /** + * Set the firstDay option, after initialization + * + * @param methodName 'option' + * @param optionName 'firstDay' + * @param firstDayValue Set the first day of the week: Sunday is 0, Monday is 1, etc. + */ + datepicker(methodName: 'option', optionName: 'firstDay', firstDayValue: number): JQuery; + + /** + * Get the gotoCurrent option, after initialization + * + * @param methodName 'option' + * @param optionName 'gotoCurrent' + */ + datepicker(methodName: 'option', optionName: 'gotoCurrent'): boolean; + /** + * Set the gotoCurrent option, after initialization + * + * @param methodName 'option' + * @param optionName 'gotoCurrent' + * @param gotoCurrentValue When true, the current day link moves to the currently selected date instead of today. + */ + datepicker(methodName: 'option', optionName: 'gotoCurrent', gotoCurrentValue: boolean): JQuery; + + /** + * Gets the value currently associated with the specified optionName. + * + * @param methodName 'option' + * @param optionName The name of the option to get. + */ + datepicker(methodName: 'option', optionName: string): any; + + datepicker(methodName: 'option', optionName: string, ...otherParams: any[]): any; // Used for getting and setting options + + datepicker(methodName: string, ...otherParams: any[]): any; + + /** + * Initialize a datepicker with the given options + */ + datepicker(options: JQueryUI.DatepickerOptions): JQuery; + + dialog(): JQuery; + dialog(methodName: 'close'): JQuery; + dialog(methodName: 'destroy'): JQuery; + dialog(methodName: 'isOpen'): boolean; + dialog(methodName: 'moveToTop'): JQuery; + dialog(methodName: 'open'): JQuery; + dialog(methodName: 'widget'): JQuery; + dialog(methodName: string): JQuery; + dialog(options: JQueryUI.DialogOptions): JQuery; + dialog(optionLiteral: string, optionName: string): any; + dialog(optionLiteral: string, options: JQueryUI.DialogOptions): any; + dialog(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + draggable(): JQuery; + draggable(methodName: 'destroy'): void; + draggable(methodName: 'disable'): void; + draggable(methodName: 'enable'): void; + draggable(methodName: 'widget'): JQuery; + draggable(methodName: string): JQuery; + draggable(options: JQueryUI.DraggableOptions): JQuery; + draggable(optionLiteral: string, optionName: string): any; + draggable(optionLiteral: string, options: JQueryUI.DraggableOptions): any; + draggable(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + droppable(): JQuery; + droppable(methodName: 'destroy'): void; + droppable(methodName: 'disable'): void; + droppable(methodName: 'enable'): void; + droppable(methodName: 'widget'): JQuery; + droppable(methodName: string): JQuery; + droppable(options: JQueryUI.DroppableOptions): JQuery; + droppable(optionLiteral: string, optionName: string): any; + droppable(optionLiteral: string, options: JQueryUI.DraggableOptions): any; + droppable(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + menu: { + (): JQuery; + (methodName: 'blur'): void; + (methodName: 'collapse', event?: JQueryEventObject): void; + (methodName: 'collapseAll', event?: JQueryEventObject, all?: boolean): void; + (methodName: 'destroy'): void; + (methodName: 'disable'): void; + (methodName: 'enable'): void; + (methodName: string, event: JQueryEventObject, item: JQuery): void; + (methodName: 'focus', event: JQueryEventObject, item: JQuery): void; + (methodName: 'isFirstItem'): boolean; + (methodName: 'isLastItem'): boolean; + (methodName: 'next', event?: JQueryEventObject): void; + (methodName: 'nextPage', event?: JQueryEventObject): void; + (methodName: 'previous', event?: JQueryEventObject): void; + (methodName: 'previousPage', event?: JQueryEventObject): void; + (methodName: 'refresh'): void; + (methodName: 'select', event?: JQueryEventObject): void; + (methodName: 'widget'): JQuery; + (methodName: string): JQuery; + (options: JQueryUI.MenuOptions): JQuery; + (optionLiteral: string, optionName: string): any; + (optionLiteral: string, options: JQueryUI.MenuOptions): any; + (optionLiteral: string, optionName: string, optionValue: any): JQuery; + active: boolean; + } + + progressbar(): JQuery; + progressbar(methodName: 'destroy'): void; + progressbar(methodName: 'disable'): void; + progressbar(methodName: 'enable'): void; + progressbar(methodName: 'refresh'): void; + progressbar(methodName: 'value'): any; // number or boolean + progressbar(methodName: 'value', value: number): void; + progressbar(methodName: 'value', value: boolean): void; + progressbar(methodName: 'widget'): JQuery; + progressbar(methodName: string): JQuery; + progressbar(options: JQueryUI.ProgressbarOptions): JQuery; + progressbar(optionLiteral: string, optionName: string): any; + progressbar(optionLiteral: string, options: JQueryUI.ProgressbarOptions): any; + progressbar(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + resizable(): JQuery; + resizable(methodName: 'destroy'): void; + resizable(methodName: 'disable'): void; + resizable(methodName: 'enable'): void; + resizable(methodName: 'widget'): JQuery; + resizable(methodName: string): JQuery; + resizable(options: JQueryUI.ResizableOptions): JQuery; + resizable(optionLiteral: string, optionName: string): any; + resizable(optionLiteral: string, options: JQueryUI.ResizableOptions): any; + resizable(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + selectable(): JQuery; + selectable(methodName: 'destroy'): void; + selectable(methodName: 'disable'): void; + selectable(methodName: 'enable'): void; + selectable(methodName: 'widget'): JQuery; + selectable(methodName: string): JQuery; + selectable(options: JQueryUI.SelectableOptions): JQuery; + selectable(optionLiteral: string, optionName: string): any; + selectable(optionLiteral: string, options: JQueryUI.SelectableOptions): any; + selectable(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + selectmenu(): JQuery; + selectmenu(methodName: 'close'): JQuery; + selectmenu(methodName: 'destroy'): JQuery; + selectmenu(methodName: 'disable'): JQuery; + selectmenu(methodName: 'enable'): JQuery; + selectmenu(methodName: 'instance'): any; + selectmenu(methodName: 'menuWidget'): JQuery; + selectmenu(methodName: 'open'): JQuery; + selectmenu(methodName: 'refresh'): JQuery; + selectmenu(methodName: 'widget'): JQuery; + selectmenu(methodName: string): JQuery; + selectmenu(options: JQueryUI.SelectMenuOptions): JQuery; + selectmenu(optionLiteral: string, optionName: string): any; + selectmenu(optionLiteral: string, options: JQueryUI.SelectMenuOptions): any; + selectmenu(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + slider(): JQuery; + slider(methodName: 'destroy'): void; + slider(methodName: 'disable'): void; + slider(methodName: 'enable'): void; + slider(methodName: 'refresh'): void; + slider(methodName: 'value'): number; + slider(methodName: 'value', value: number): void; + slider(methodName: 'values'): Array<number>; + slider(methodName: 'values', index: number): number; + slider(methodName: string, index: number, value: number): void; + slider(methodName: 'values', index: number, value: number): void; + slider(methodName: string, values: Array<number>): void; + slider(methodName: 'values', values: Array<number>): void; + slider(methodName: 'widget'): JQuery; + slider(methodName: string): JQuery; + slider(options: JQueryUI.SliderOptions): JQuery; + slider(optionLiteral: string, optionName: string): any; + slider(optionLiteral: string, options: JQueryUI.SliderOptions): any; + slider(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + sortable(): JQuery; + sortable(methodName: 'destroy'): void; + sortable(methodName: 'disable'): void; + sortable(methodName: 'enable'): void; + sortable(methodName: 'widget'): JQuery; + sortable(methodName: 'toArray', options?: { attribute?: string | undefined; }): string[]; + sortable(methodName: string): JQuery; + sortable(options: JQueryUI.SortableOptions): JQuery; + sortable(optionLiteral: string, optionName: string): any; + sortable(methodName: 'serialize', options?: { key?: string | undefined; attribute?: string | undefined; expression?: RegExp | undefined }): string; + sortable(optionLiteral: string, options: JQueryUI.SortableOptions): any; + sortable(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + spinner(): JQuery; + spinner(methodName: 'destroy'): void; + spinner(methodName: 'disable'): void; + spinner(methodName: 'enable'): void; + spinner(methodName: 'pageDown', pages?: number): void; + spinner(methodName: 'pageUp', pages?: number): void; + spinner(methodName: 'stepDown', steps?: number): void; + spinner(methodName: 'stepUp', steps?: number): void; + spinner(methodName: 'value'): number; + spinner(methodName: 'value', value: number): void; + spinner(methodName: 'widget'): JQuery; + spinner(methodName: string): JQuery; + spinner(options: JQueryUI.SpinnerOptions): JQuery; + spinner(optionLiteral: string, optionName: string): any; + spinner(optionLiteral: string, options: JQueryUI.SpinnerOptions): any; + spinner(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + tabs(): JQuery; + tabs(methodName: 'destroy'): void; + tabs(methodName: 'disable'): void; + tabs(methodName: 'disable', index: number): void; + tabs(methodName: 'enable'): void; + tabs(methodName: 'enable', index: number): void; + tabs(methodName: 'load', index: number): void; + tabs(methodName: 'refresh'): void; + tabs(methodName: 'widget'): JQuery; + tabs(methodName: 'select', index: number): JQuery; + tabs(methodName: string): JQuery; + tabs(options: JQueryUI.TabsOptions): JQuery; + tabs(optionLiteral: string, optionName: string): any; + tabs(optionLiteral: string, options: JQueryUI.TabsOptions): any; + tabs(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + tooltip(): JQuery; + tooltip(methodName: 'destroy'): void; + tooltip(methodName: 'disable'): void; + tooltip(methodName: 'enable'): void; + tooltip(methodName: 'open'): void; + tooltip(methodName: 'close'): void; + tooltip(methodName: 'widget'): JQuery; + tooltip(methodName: string): JQuery; + tooltip(options: JQueryUI.TooltipOptions): JQuery; + tooltip(optionLiteral: string, optionName: string): any; + tooltip(optionLiteral: string, options: JQueryUI.TooltipOptions): any; + tooltip(optionLiteral: string, optionName: string, optionValue: any): JQuery; + + + addClass(classNames: string, speed?: number, callback?: Function): this; + addClass(classNames: string, speed?: string, callback?: Function): this; + addClass(classNames: string, speed?: number, easing?: string, callback?: Function): this; + addClass(classNames: string, speed?: string, easing?: string, callback?: Function): this; + + removeClass(classNames: string, speed?: number, callback?: Function): this; + removeClass(classNames: string, speed?: string, callback?: Function): this; + removeClass(classNames: string, speed?: number, easing?: string, callback?: Function): this; + removeClass(classNames: string, speed?: string, easing?: string, callback?: Function): this; + + switchClass(removeClassName: string, addClassName: string, duration?: number, easing?: string, complete?: Function): this; + switchClass(removeClassName: string, addClassName: string, duration?: string, easing?: string, complete?: Function): this; + + toggleClass(className: string, duration?: number, easing?: string, complete?: Function): this; + toggleClass(className: string, duration?: string, easing?: string, complete?: Function): this; + toggleClass(className: string, aswitch?: boolean, duration?: number, easing?: string, complete?: Function): this; + toggleClass(className: string, aswitch?: boolean, duration?: string, easing?: string, complete?: Function): this; + + effect(options: any): this; + effect(effect: string, options?: any, duration?: number, complete?: Function): this; + effect(effect: string, options?: any, duration?: string, complete?: Function): this; + + hide(options: any): this; + hide(effect: string, options?: any, duration?: number, complete?: Function): this; + hide(effect: string, options?: any, duration?: string, complete?: Function): this; + + show(options: any): this; + show(effect: string, options?: any, duration?: number, complete?: Function): this; + show(effect: string, options?: any, duration?: string, complete?: Function): this; + + toggle(options: any): this; + toggle(effect: string, options?: any, duration?: number, complete?: Function): this; + toggle(effect: string, options?: any, duration?: string, complete?: Function): this; + + position(options: JQueryUI.JQueryPositionOptions): JQuery; + + enableSelection(): JQuery; + disableSelection(): JQuery; + focus(delay: number, callback?: Function): JQuery; + uniqueId(): JQuery; + removeUniqueId(): JQuery; + scrollParent(): JQuery; + zIndex(): number; + zIndex(zIndex: number): JQuery; + + widget: JQueryUI.Widget; + + jQuery: JQueryStatic; +} + +interface JQueryStatic { + ui: JQueryUI.UI; + datepicker: JQueryUI.Datepicker; + widget: JQueryUI.Widget; + Widget: JQueryUI.Widget; +} + +interface JQueryEasingFunctions { + easeInQuad: JQueryEasingFunction; + easeOutQuad: JQueryEasingFunction; + easeInOutQuad: JQueryEasingFunction; + easeInCubic: JQueryEasingFunction; + easeOutCubic: JQueryEasingFunction; + easeInOutCubic: JQueryEasingFunction; + easeInQuart: JQueryEasingFunction; + easeOutQuart: JQueryEasingFunction; + easeInOutQuart: JQueryEasingFunction; + easeInQuint: JQueryEasingFunction; + easeOutQuint: JQueryEasingFunction; + easeInOutQuint: JQueryEasingFunction; + easeInExpo: JQueryEasingFunction; + easeOutExpo: JQueryEasingFunction; + easeInOutExpo: JQueryEasingFunction; + easeInSine: JQueryEasingFunction; + easeOutSine: JQueryEasingFunction; + easeInOutSine: JQueryEasingFunction; + easeInCirc: JQueryEasingFunction; + easeOutCirc: JQueryEasingFunction; + easeInOutCirc: JQueryEasingFunction; + easeInElastic: JQueryEasingFunction; + easeOutElastic: JQueryEasingFunction; + easeInOutElastic: JQueryEasingFunction; + easeInBack: JQueryEasingFunction; + easeOutBack: JQueryEasingFunction; + easeInOutBack: JQueryEasingFunction; + easeInBounce: JQueryEasingFunction; + easeOutBounce: JQueryEasingFunction; + easeInOutBounce: JQueryEasingFunction; +} diff --git a/js/knockout.d.ts b/js/knockout.d.ts new file mode 100644 index 0000000..1246062 --- /dev/null +++ b/js/knockout.d.ts @@ -0,0 +1,1064 @@ +// Type definitions for Knockout v3.4.0 +// Project: http://knockoutjs.com +// Definitions by: Boris Yankov <https://github.com/borisyankov>, +// Igor Oleinikov <https://github.com/Igorbek>, +// Clément Bourgeois <https://github.com/moonpyk>, +// Matt Brooks <https://github.com/EnableSoftware>, +// Benjamin Eckardt <https://github.com/BenjaminEckardt>, +// Mathias Lorenzen <https://github.com/ffMathy>, +// Leonardo Lombardi <https://github.com/ltlombardi> +// Retsam <https://github.com/Retsam> +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.3 + +interface KnockoutSubscribableFunctions<T> { + /** + * Notify subscribers of knockout "change" event. This doesn't actually change the observable value. + * @param eventValue A value to be sent with the event. + * @param event The knockout event. + */ + notifySubscribers(eventValue?: T, event?: "change"): void; + /** + * Notify subscribers of a knockout or user defined event. + * @param eventValue A value to be sent with the event. + * @param event The knockout or user defined event name. + */ + notifySubscribers<U>(eventValue: U, event: string): void; +} + +interface KnockoutComputedFunctions<T> { +} + +interface KnockoutObservableFunctions<T> { + /** + * Used by knockout to decide if value of observable has changed and should notify subscribers. Returns true if instances are primitives, and false if are objects. + * If your observable holds an object, this can be overwritten to return equality based on your needs. + * @param a previous value. + * @param b next value. + */ + equalityComparer(a: T, b: T): boolean; +} + +// The functions of observable arrays that don't mutate the array +interface KnockoutReadonlyObservableArrayFunctions<T> { + /** + * Returns the index of the first occurrence of a value in an array. + * @param searchElement The value to locate in the array. + * @param fromIndex The array index at which to begin the search. If fromIndex is omitted, the search starts at index 0. + */ + indexOf(searchElement: T, fromIndex?: number): number; + /** + * Returns a section of an array. + * @param start The beginning of the specified portion of the array. + * @param end The end of the specified portion of the array. + */ + slice(start: number, end?: number): T[]; +} +// The functions of observable arrays that mutate the array +interface KnockoutObservableArrayFunctions<T> extends KnockoutReadonlyObservableArrayFunctions<T> { + /** + * Removes and returns all the remaining elements starting from a given index. + * @param start The zero-based location in the array from which to start removing elements. + */ + splice(start: number): T[]; + /** + * Removes elements from an array and, if necessary, inserts new elements in their place, returning the deleted elements. + * @param start The zero-based location in the array from which to start removing elements. + * @param deleteCount The number of elements to remove. + * @param items Elements to insert into the array in place of the deleted elements. + */ + splice(start: number, deleteCount: number, ...items: T[]): T[]; + /** + * Removes the last value from the array and returns it. + */ + pop(): T; + /** + * Adds new item or items to the end of array. + * @param items Items to be added. + */ + push(...items: T[]): void; + /** + * Removes the first value from the array and returns it. + */ + shift(): T; + /** + * Inserts new item or items at the beginning of the array. + * @param items Items to be added. + */ + unshift(...items: T[]): number; + /** + * Reverses the order of the array and returns the observableArray (not the underlying array). + */ + reverse(): KnockoutObservableArray<T>; + /** + * Sorts the array contents and returns the observableArray. + */ + sort(): KnockoutObservableArray<T>; + /** + * Sorts the array contents and returns the observableArray. + * @param compareFunction A function that returns negative value if first argument is smaller, positive value if second is smaller, or zero to treat them as equal. + */ + sort(compareFunction: (left: T, right: T) => number): KnockoutObservableArray<T>; + + // Ko specific + /** + * Replaces the first value that equals oldItem with newItem. + * @param oldItem Item to be replaced. + * @param newItem Replacing item. + */ + replace(oldItem: T, newItem: T): void; + /** + * Removes all values that equal item and returns them as an array. + * @param item The item to be removed. + */ + remove(item: T): T[]; + /** + * Removes all values and returns them as an array. + * @param removeFunction A function used to determine true if item should be removed and fasle otherwise. + */ + remove(removeFunction: (item: T) => boolean): T[]; + /** + * Removes all values that equal any of the supplied items. + * @param items Items to be removed. + */ + removeAll(items: T[]): T[]; + /** + * Removes all values and returns them as an array. + */ + removeAll(): T[]; + + // Ko specific Usually relevant to Ruby on Rails developers only + /** + * Finds any objects in the array that equal someItem and gives them a special property called _destroy with value true. + * @param item Items to be marked with the property. + */ + destroy(item: T): void; + /** + * Finds any objects in the array filtered by a function and gives them a special property called _destroy with value true. + * @param destroyFunction A function used to determine which items should be marked with the property. + */ + destroy(destroyFunction: (item: T) => boolean): void; + /** + * Finds any objects in the array that equal suplied items and gives them a special property called _destroy with value true. + * @param items + */ + destroyAll(items: T[]): void; + /** + * Gives a special property called _destroy with value true to all objects in the array. + */ + destroyAll(): void; +} + +interface KnockoutSubscribableStatic { + fn: KnockoutSubscribableFunctions<any>; + + new <T>(): KnockoutSubscribable<T>; +} + +interface KnockoutSubscription { + /** + * Terminates a subscription. + */ + dispose(): void; +} + +interface KnockoutSubscribable<T> extends KnockoutSubscribableFunctions<T> { + /** + * Registers to be notified after the observable's value changes. + * @param callback Function that is called whenever the notification happens. + * @param target Defines the value of 'this' in the callback function. + * @param event The knockout event name. + */ + subscribe(callback: (newValue: T) => void, target?: any, event?: "change"): KnockoutSubscription; + /** + * Registers to be notified before the observable's value changes. + * @param callback Function that is called whenever the notification happens. + * @param target Defines the value of 'this' in the callback function. + * @param event The knockout event name. + */ + subscribe(callback: (newValue: T) => void, target: any, event: "beforeChange"): KnockoutSubscription; + /** + * Registers to be notified when a knockout or user defined event happens. + * @param callback Function that is called whenever the notification happens. eventValue can be anything. No relation to underlying observable. + * @param target Defines the value of 'this' in the callback function. + * @param event The knockout or user defined event name. + */ + subscribe<U>(callback: (eventValue: U) => void, target: any, event: string): KnockoutSubscription; + /** + * Customizes observables basic functionality. + * @param requestedExtenders Name of the extender feature and its value, e.g. { notify: 'always' }, { rateLimit: 50 } + */ + extend(requestedExtenders: { [key: string]: any; }): KnockoutSubscribable<T>; + /** + * Gets total number of subscribers. + */ + getSubscriptionsCount(): number; + /** + * Gets number of subscribers of a particular event. + * @param event Event name. + */ + getSubscriptionsCount(event: string): number; +} + +interface KnockoutComputedStatic { + fn: KnockoutComputedFunctions<any>; + + /** + * Creates computed observable. + */ + <T>(): KnockoutComputed<T>; + /** + * Creates computed observable. + * @param evaluatorFunction Function that computes the observable value. + * @param context Defines the value of 'this' when evaluating the computed observable. + * @param options An object with further properties for the computed observable. + */ + <T>(evaluatorFunction: () => T, context?: any, options?: KnockoutComputedOptions<T>): KnockoutComputed<T>; + /** + * Creates computed observable. + * @param options An object that defines the computed observable options and behavior. + * @param context Defines the value of 'this' when evaluating the computed observable. + */ + <T>(options: KnockoutComputedDefine<T>, context?: any): KnockoutComputed<T>; +} + +interface KnockoutReadonlyComputed<T> extends KnockoutReadonlyObservable<T> { + /** + * Returns whether the computed observable may be updated in the future. A computed observable is inactive if it has no dependencies. + */ + isActive(): boolean; + /** + * Returns the current number of dependencies of the computed observable. + */ + getDependenciesCount(): number; +} + +interface KnockoutComputed<T> extends KnockoutReadonlyComputed<T>, KnockoutObservable<T>, KnockoutComputedFunctions<T> { + fn: KnockoutComputedFunctions<any>; + + /** + * Manually disposes the computed observable, clearing all subscriptions to dependencies. + * This function is useful if you want to stop a computed observable from being updated or want to clean up memory for a + * computed observable that has dependencies on observables that won’t be cleaned. + */ + dispose(): void; + /** + * Customizes observables basic functionality. + * @param requestedExtenders Name of the extender feature and it's value, e.g. { notify: 'always' }, { rateLimit: 50 } + */ + extend(requestedExtenders: { [key: string]: any; }): KnockoutComputed<T>; +} + +interface KnockoutObservableArrayStatic { + fn: KnockoutObservableArrayFunctions<any>; + + <T>(value?: T[] | null): KnockoutObservableArray<T>; +} + +/** + * While all observable arrays are writable at runtime, this type is analogous to the native ReadonlyArray type: + * casting an observable array to this type expresses the intention that it shouldn't be mutated. + */ +interface KnockoutReadonlyObservableArray<T> extends KnockoutReadonlyObservable<ReadonlyArray<T>>, KnockoutReadonlyObservableArrayFunctions<T> { + // NOTE: Keep in sync with KnockoutObservableArray<T>, see note on KnockoutObservableArray<T> + subscribe(callback: (newValue: KnockoutArrayChange<T>[]) => void, target: any, event: "arrayChange"): KnockoutSubscription; + subscribe(callback: (newValue: T[]) => void, target: any, event: "beforeChange"): KnockoutSubscription; + subscribe(callback: (newValue: T[]) => void, target?: any, event?: "change"): KnockoutSubscription; + subscribe<U>(callback: (newValue: U) => void, target: any, event: string): KnockoutSubscription; +} + +/* + NOTE: In theory this should extend both KnockoutObservable<T[]> and KnockoutReadonlyObservableArray<T>, + but can't since they both provide conflicting typings of .subscribe. + So it extends KnockoutObservable<T[]> and duplicates the subscribe definitions, which should be kept in sync +*/ +interface KnockoutObservableArray<T> extends KnockoutObservable<T[]>, KnockoutObservableArrayFunctions<T> { + subscribe(callback: (newValue: KnockoutArrayChange<T>[]) => void, target: any, event: "arrayChange"): KnockoutSubscription; + subscribe(callback: (newValue: T[]) => void, target: any, event: "beforeChange"): KnockoutSubscription; + subscribe(callback: (newValue: T[]) => void, target?: any, event?: "change"): KnockoutSubscription; + subscribe<U>(callback: (newValue: U) => void, target: any, event: string): KnockoutSubscription; + + extend(requestedExtenders: { [key: string]: any; }): KnockoutObservableArray<T>; +} + +interface KnockoutObservableStatic { + fn: KnockoutObservableFunctions<any>; + + <T>(value: T): KnockoutObservable<T>; + <T = any>(value: null): KnockoutObservable<T | null> + <T = any>(): KnockoutObservable<T | undefined> +} + +/** + * While all observable are writable at runtime, this type is analogous to the native ReadonlyArray type: + * casting an observable to this type expresses the intention that this observable shouldn't be mutated. + */ +interface KnockoutReadonlyObservable<T> extends KnockoutSubscribable<T>, KnockoutObservableFunctions<T> { + (): T; + + /** + * Returns the current value of the computed observable without creating a dependency. + */ + peek(): T; + valueHasMutated?: { (): void; }; + valueWillMutate?: { (): void; }; +} + +interface KnockoutObservable<T> extends KnockoutReadonlyObservable<T> { + (value: T): void; + + // Since .extend does arbitrary thing to an observable, it's not safe to do on a readonly observable + /** + * Customizes observables basic functionality. + * @param requestedExtenders Name of the extender feature and it's value, e.g. { notify: 'always' }, { rateLimit: 50 } + */ + extend(requestedExtenders: { [key: string]: any; }): KnockoutObservable<T>; +} + +interface KnockoutComputedOptions<T> { + /** + * Makes the computed observable writable. This is a function that receives values that other code is trying to write to your computed observable. + * It’s up to you to supply custom logic to handle the incoming values, typically by writing the values to some underlying observable(s). + * @param value Value being written to the computer observable. + */ + write?(value: T): void; + /** + * Disposal of the computed observable will be triggered when the specified DOM node is removed by KO. + * This feature is used to dispose computed observables used in bindings when nodes are removed by the template and control-flow bindings. + */ + disposeWhenNodeIsRemoved?: Node; + /** + * This function is executed before each re-evaluation to determine if the computed observable should be disposed. + * A true-ish result will trigger disposal of the computed observable. + */ + disposeWhen?(): boolean; + /** + * Defines the value of 'this' whenever KO invokes your 'read' or 'write' callbacks. + */ + owner?: any; + /** + * If true, then the value of the computed observable will not be evaluated until something actually attempts to access its value or manually subscribes to it. + * By default, a computed observable has its value determined immediately during creation. + */ + deferEvaluation?: boolean; + /** + * If true, the computed observable will be set up as a purecomputed observable. This option is an alternative to the ko.pureComputed constructor. + */ + pure?: boolean; +} + +interface KnockoutComputedDefine<T> extends KnockoutComputedOptions<T> { + /** + * A function that is used to evaluate the computed observable’s current value. + */ + read(): T; +} + +interface KnockoutBindingContext { + $parent: any; + $parents: any[]; + $root: any; + $data: any; + $rawData: any | KnockoutObservable<any>; + $index?: KnockoutObservable<number>; + $parentContext?: KnockoutBindingContext; + $component: any; + $componentTemplateNodes: Node[]; + + /** + * Clones the current Binding Context, adding extra properties to it. + * @param properties object with properties to be added in the binding context. + */ + extend(properties: { [key: string]: any; } | (() => { [key: string]: any; })): KnockoutBindingContext; + /** + * This returns a new binding context whose viewmodel is the first parameter and whose $parentContext is the current bindingContext. + * @param dataItemOrAccessor The binding context of the children. + * @param dataItemAlias An alias for the data item in descendant contexts. + * @param extendCallback Function to be called. + * @param options Further options. + */ + createChildContext(dataItemOrAccessor: any, dataItemAlias?: string, extendCallback?: Function, options?: { "exportDependencies": boolean }): any; +} + +interface KnockoutAllBindingsAccessor { + (): any; + get(name: string): any; + has(name: string): boolean; +} + +interface KnockoutBindingHandler<E extends Node = any, V = any, VM = any> { + after?: Array<string>; + init?: (element: E, valueAccessor: () => V, allBindingsAccessor: KnockoutAllBindingsAccessor, viewModel: VM, bindingContext: KnockoutBindingContext) => void | { controlsDescendantBindings: boolean; }; + update?: (element: E, valueAccessor: () => V, allBindingsAccessor: KnockoutAllBindingsAccessor, viewModel: VM, bindingContext: KnockoutBindingContext) => void; + options?: any; + preprocess?: (value: string, name: string, addBindingCallback?: (name: string, value: string) => void) => string; + [s: string]: any; +} + +interface KnockoutBindingHandlers { + [bindingHandler: string]: KnockoutBindingHandler; + + // Controlling text and appearance + visible: KnockoutBindingHandler; + text: KnockoutBindingHandler; + html: KnockoutBindingHandler; + css: KnockoutBindingHandler; + style: KnockoutBindingHandler; + attr: KnockoutBindingHandler; + + // Control Flow + foreach: KnockoutBindingHandler; + if: KnockoutBindingHandler; + ifnot: KnockoutBindingHandler; + with: KnockoutBindingHandler; + + // Working with form fields + click: KnockoutBindingHandler; + event: KnockoutBindingHandler; + submit: KnockoutBindingHandler; + enable: KnockoutBindingHandler; + disable: KnockoutBindingHandler; + value: KnockoutBindingHandler; + textInput: KnockoutBindingHandler; + hasfocus: KnockoutBindingHandler; + checked: KnockoutBindingHandler; + options: KnockoutBindingHandler; + selectedOptions: KnockoutBindingHandler; + uniqueName: KnockoutBindingHandler; + + // Rendering templates + template: KnockoutBindingHandler; + + // Components (new for v3.2) + component: KnockoutBindingHandler; +} + +interface KnockoutMemoization { + memoize(callback: Function): string; + unmemoize(memoId: string, callbackParams: any[]): boolean; + unmemoizeDomNodeAndDescendants(domNode: any, extraCallbackParamsArray: any[]): boolean; + parseMemoText(memoText: string): string; +} + +interface KnockoutVirtualElement { } + +interface KnockoutVirtualElements { + allowedBindings: { [bindingName: string]: boolean; }; + emptyNode(node: KnockoutVirtualElement): void; + firstChild(node: KnockoutVirtualElement): KnockoutVirtualElement; + insertAfter(container: KnockoutVirtualElement, nodeToInsert: Node, insertAfter: Node): void; + nextSibling(node: KnockoutVirtualElement): Node; + prepend(node: KnockoutVirtualElement, toInsert: Node): void; + setDomNodeChildren(node: KnockoutVirtualElement, newChildren: { length: number;[index: number]: Node; }): void; + childNodes(node: KnockoutVirtualElement): Node[]; +} + +interface KnockoutExtenders { + throttle(target: any, timeout: number): KnockoutComputed<any>; + notify(target: any, notifyWhen: string): any; + + rateLimit(target: any, timeout: number): any; + rateLimit(target: any, options: { timeout: number; method?: string; }): any; + + trackArrayChanges(target: any): any; +} + +// +// NOTE TO MAINTAINERS AND CONTRIBUTORS : pay attention to only include symbols that are +// publicly exported in the minified version of ko, without that you can give the false +// impression that some functions will be available in production builds. +// +interface KnockoutUtils { + ////////////////////////////////// + // utils.domData.js + ////////////////////////////////// + + domData: { + get(node: Node, key: string): any; + + set(node: Node, key: string, value: any): void; + + getAll(node: Node, createIfNotFound: boolean): any; + + clear(node: Node): boolean; + }; + + ////////////////////////////////// + // utils.domNodeDisposal.js + ////////////////////////////////// + + domNodeDisposal: { + addDisposeCallback(node: Node, callback: Function): void; + + removeDisposeCallback(node: Node, callback: Function): void; + + cleanNode(node: Node): Node; + + removeNode(node: Node): void; + }; + + addOrRemoveItem<T>(array: T[] | KnockoutObservable<T>, value: T, included: T): void; + + arrayFilter<T>(array: T[], predicate: (item: T) => boolean): T[]; + + arrayFirst<T>(array: T[], predicate: (item: T) => boolean, predicateOwner?: any): T; + + arrayForEach<T>(array: T[], action: (item: T, index: number) => void): void; + + arrayGetDistinctValues<T>(array: T[]): T[]; + + arrayIndexOf<T>(array: T[], item: T): number; + + arrayMap<T, U>(array: T[], mapping: (item: T) => U): U[]; + + arrayPushAll<T>(array: T[] | KnockoutObservableArray<T>, valuesToPush: T[]): T[]; + + arrayRemoveItem(array: any[], itemToRemove: any): void; + + compareArrays<T>(a: T[], b: T[]): Array<KnockoutArrayChange<T>>; + + extend(target: Object, source: Object): Object; + + fieldsIncludedWithJsonPost: any[]; + + getFormFields(form: any, fieldName: string): any[]; + + objectForEach(obj: any, action: (key: any, value: any) => void): void; + + parseHtmlFragment(html: string): any[]; + + parseJson(jsonString: string): any; + + postJson(urlOrForm: any, data: any, options: any): void; + + peekObservable<T>(value: KnockoutObservable<T>): T; + + range(min: any, max: any): any; + + registerEventHandler(element: any, eventType: any, handler: Function): void; + + setHtml(node: Element, html: () => string): void; + + setHtml(node: Element, html: string): void; + + setTextContent(element: any, textContent: string | KnockoutObservable<string>): void; + + stringifyJson(data: any, replacer?: Function, space?: string): string; + + toggleDomNodeCssClass(node: any, className: string, shouldHaveClass: boolean): void; + + triggerEvent(element: any, eventType: any): void; + + unwrapObservable<T>(value: KnockoutObservable<T> | T): T; + unwrapObservable<T>(value: KnockoutObservableArray<T> | T[]): T[]; + + // NOT PART OF THE MINIFIED API SURFACE (ONLY IN knockout-{version}.debug.js) https://github.com/SteveSanderson/knockout/issues/670 + // forceRefresh(node: any): void; + // ieVersion: number; + // isIe6: boolean; + // isIe7: boolean; + // jQueryHtmlParse(html: string): any[]; + // makeArray(arrayLikeObject: any): any[]; + // moveCleanedNodesToContainerElement(nodes: any[]): HTMLElement; + // replaceDomNodes(nodeToReplaceOrNodeArray: any, newNodesArray: any[]): void; + // setDomNodeChildren(domNode: any, childNodes: any[]): void; + // setElementName(element: any, name: string): void; + // setOptionNodeSelectionState(optionNode: any, isSelected: boolean): void; + // simpleHtmlParse(html: string): any[]; + // stringStartsWith(str: string, startsWith: string): boolean; + // stringTokenize(str: string, delimiter: string): string[]; + // stringTrim(str: string): string; + // tagNameLower(element: any): string; +} + +interface KnockoutArrayChange<T> { + status: "added" | "deleted" | "retained"; + value: T; + index: number; + moved?: number; +} + +////////////////////////////////// +// templateSources.js +////////////////////////////////// + +interface KnockoutTemplateSourcesDomElement { + text(): any; + text(value: any): void; + + data(key: string): any; + data(key: string, value: any): any; +} + +interface KnockoutTemplateAnonymous extends KnockoutTemplateSourcesDomElement { + nodes(): any; + nodes(value: any): void; +} + +interface KnockoutTemplateSources { + + domElement: { + prototype: KnockoutTemplateSourcesDomElement + new(element: Element): KnockoutTemplateSourcesDomElement + }; + + anonymousTemplate: { + prototype: KnockoutTemplateAnonymous; + new(element: Element): KnockoutTemplateAnonymous; + }; +} + +////////////////////////////////// +// nativeTemplateEngine.js +////////////////////////////////// + +interface KnockoutNativeTemplateEngine extends KnockoutTemplateEngine { + + renderTemplateSource(templateSource: Object, bindingContext?: KnockoutBindingContext, options?: Object): any[]; +} + +////////////////////////////////// +// templateEngine.js +////////////////////////////////// + +interface KnockoutTemplateEngine { + + createJavaScriptEvaluatorBlock(script: string): string; + + makeTemplateSource(template: any, templateDocument?: Document): any; + + renderTemplate(template: any, bindingContext: KnockoutBindingContext, options: Object, templateDocument: Document): any; + + isTemplateRewritten(template: any, templateDocument: Document): boolean; + + rewriteTemplate(template: any, rewriterCallback: Function, templateDocument: Document): void; +} + +////////////////////////////////// +// tasks.js +////////////////////////////////// + +interface KnockoutTasks { + scheduler: (callback: Function) => any; + schedule(task: Function): number; + cancel(handle: number): void; + runEarly(): void; +} + +///////////////////////////////// +interface KnockoutStatic { + utils: KnockoutUtils; + memoization: KnockoutMemoization; + + bindingHandlers: KnockoutBindingHandlers; + getBindingHandler(handler: string): KnockoutBindingHandler; + + virtualElements: KnockoutVirtualElements; + extenders: KnockoutExtenders; + + applyBindings(viewModelOrBindingContext?: any, rootNode?: any): void; + applyBindingsToDescendants(viewModelOrBindingContext: any, rootNode: any): void; + applyBindingAccessorsToNode(node: Node, bindings: (bindingContext: KnockoutBindingContext, node: Node) => {}, bindingContext: KnockoutBindingContext): void; + applyBindingAccessorsToNode(node: Node, bindings: {}, bindingContext: KnockoutBindingContext): void; + applyBindingAccessorsToNode(node: Node, bindings: (bindingContext: KnockoutBindingContext, node: Node) => {}, viewModel: any): void; + applyBindingAccessorsToNode(node: Node, bindings: {}, viewModel: any): void; + applyBindingsToNode(node: Node, bindings: any, viewModelOrBindingContext?: any): any; + + subscribable: KnockoutSubscribableStatic; + observable: KnockoutObservableStatic; + + computed: KnockoutComputedStatic; + /** + * Creates a pure computed observable. + * @param evaluatorFunction Function that computes the observable value. + * @param context Defines the value of 'this' when evaluating the computed observable. + */ + pureComputed<T>(evaluatorFunction: () => T, context?: any): KnockoutComputed<T>; + /** + * Creates a pure computed observable. + * @param options An object that defines the computed observable options and behavior. + * @param context Defines the value of 'this' when evaluating the computed observable. + */ + pureComputed<T>(options: KnockoutComputedDefine<T>, context?: any): KnockoutComputed<T>; + + observableArray: KnockoutObservableArrayStatic; + + /** + * Evaluates if instance is a KnockoutSubscribable. + * @param instance Instance to be evaluated. + */ + isSubscribable(instance: any): instance is KnockoutSubscribable<any>; + /** + * Clones object substituting each observable for it's underlying value. Uses browser JSON.stringify internally to stringify the result. + * @param viewModel Object with observables to be converted. + * @param replacer A Function or array of names that alters the behavior of the stringification process. + * @param space Used to insert white space into the output JSON string for readability purposes. + */ + toJSON(viewModel: any, replacer?: Function | [string | number], space?: string | number): string; + /** + * Clones object substituting for each observable the current value of that observable. + * @param viewModel Object with observables to be converted. + */ + toJS(viewModel: any): any; + /** + * Determine if argument is an observable. Returns true for observables, observable arrays, and all computed observables. + * @param instance Object to be checked. + */ + isObservable(instance: any): instance is KnockoutObservable<any>; + /** + * Determine if argument is an observable. Returns true for observables, observable arrays, and all computed observables. + * @param instance Object to be checked. + */ + isObservable<T>(instance: KnockoutObservable<T> | T): instance is KnockoutObservable<T>; + /** + * Determine if argument is a writable observable. Returns true for observables, observable arrays, and writable computed observables. + * @param instance Object to be checked. + */ + isWriteableObservable(instance: any): instance is KnockoutObservable<any>; + /** + * Determine if argument is a writable observable. Returns true for observables, observable arrays, and writable computed observables. + * @param instance Object to be checked. + */ + isWriteableObservable<T>(instance: KnockoutObservable<T> | T): instance is KnockoutObservable<T>; + /** + * Determine if argument is a computed observable. + * @param instance Object to be checked. + */ + isComputed(instance: any): instance is KnockoutComputed<any>; + /** + * Determine if argument is a computed observable. + * @param instance Object to be checked. + */ + isComputed<T>(instance: KnockoutObservable<T> | T): instance is KnockoutComputed<T>; + + /** + * Returns the data that was available for binding against the element. + * @param node Html node that contains the binding context. + */ + dataFor(node: Node): any; + /** + * Returns the entire binding context that was available to the DOM element. + * @param node Html node that contains the binding context. + */ + contextFor(node: Node): any; + /** + * Removes a node from the DOM. + * @param node Node to be removed. + */ + removeNode(node: Node): void; + /** + * Used internally by Knockout to clean up data/computeds that it created related to the element. It does not remove any event handlers added by bindings. + * @param node Node to be cleaned. + */ + cleanNode(node: Node): Node; + renderTemplate(template: Function, viewModel: any, options?: any, target?: any, renderMode?: any): any; + renderTemplate(template: string, viewModel: any, options?: any, target?: any, renderMode?: any): any; + /** + * Returns the underlying value of the Knockout Observable or in case of plain js object, return the object. Use this to easily accept both observable and plain values. + * @param instance observable to be unwraped if it's an Observable. + */ + unwrap<T>(instance: KnockoutObservable<T> | T): T; + /** + * Gets the array inside the KnockoutObservableArray. + * @param instance observable to be unwraped. + */ + unwrap<T>(instance: KnockoutObservableArray<T> | T[]): T[]; + + /** + * Get information about the current computed property during the execution of a computed observable’s evaluator function. + */ + computedContext: KnockoutComputedContext; + + ////////////////////////////////// + // templateSources.js + ////////////////////////////////// + + templateSources: KnockoutTemplateSources; + + ////////////////////////////////// + // templateEngine.js + ////////////////////////////////// + + templateEngine: { + + prototype: KnockoutTemplateEngine; + + new(): KnockoutTemplateEngine; + }; + + ////////////////////////////////// + // templateRewriting.js + ////////////////////////////////// + + templateRewriting: { + + ensureTemplateIsRewritten(template: Node, templateEngine: KnockoutTemplateEngine, templateDocument: Document): any; + ensureTemplateIsRewritten(template: string, templateEngine: KnockoutTemplateEngine, templateDocument: Document): any; + + memoizeBindingAttributeSyntax(htmlString: string, templateEngine: KnockoutTemplateEngine): any; + + applyMemoizedBindingsToNextSibling(bindings: any, nodeName: string): string; + }; + + ////////////////////////////////// + // nativeTemplateEngine.js + ////////////////////////////////// + + nativeTemplateEngine: { + + prototype: KnockoutNativeTemplateEngine; + + new(): KnockoutNativeTemplateEngine; + + instance: KnockoutNativeTemplateEngine; + }; + + ////////////////////////////////// + // jqueryTmplTemplateEngine.js + ////////////////////////////////// + + jqueryTmplTemplateEngine: { + + prototype: KnockoutTemplateEngine; + + renderTemplateSource(templateSource: Object, bindingContext: KnockoutBindingContext, options: Object): Node[]; + + createJavaScriptEvaluatorBlock(script: string): string; + + addTemplate(templateName: string, templateMarkup: string): void; + }; + + ////////////////////////////////// + // templating.js + ////////////////////////////////// + + setTemplateEngine(templateEngine: KnockoutNativeTemplateEngine | undefined): void; + + renderTemplate(template: Function, dataOrBindingContext: KnockoutBindingContext, options: Object, targetNodeOrNodeArray: Node, renderMode: string): any; + renderTemplate(template: any, dataOrBindingContext: KnockoutBindingContext, options: Object, targetNodeOrNodeArray: Node, renderMode: string): any; + renderTemplate(template: Function, dataOrBindingContext: any, options: Object, targetNodeOrNodeArray: Node, renderMode: string): any; + renderTemplate(template: any, dataOrBindingContext: any, options: Object, targetNodeOrNodeArray: Node, renderMode: string): any; + renderTemplate(template: Function, dataOrBindingContext: KnockoutBindingContext, options: Object, targetNodeOrNodeArray: Node[], renderMode: string): any; + renderTemplate(template: any, dataOrBindingContext: KnockoutBindingContext, options: Object, targetNodeOrNodeArray: Node[], renderMode: string): any; + renderTemplate(template: Function, dataOrBindingContext: any, options: Object, targetNodeOrNodeArray: Node[], renderMode: string): any; + renderTemplate(template: any, dataOrBindingContext: any, options: Object, targetNodeOrNodeArray: Node[], renderMode: string): any; + + renderTemplateForEach(template: Function, arrayOrObservableArray: any[], options: Object, targetNode: Node, parentBindingContext: KnockoutBindingContext): any; + renderTemplateForEach(template: any, arrayOrObservableArray: any[], options: Object, targetNode: Node, parentBindingContext: KnockoutBindingContext): any; + renderTemplateForEach(template: Function, arrayOrObservableArray: KnockoutObservable<any>, options: Object, targetNode: Node, parentBindingContext: KnockoutBindingContext): any; + renderTemplateForEach(template: any, arrayOrObservableArray: KnockoutObservable<any>, options: Object, targetNode: Node, parentBindingContext: KnockoutBindingContext): any; + + /** + * Executes a callback function inside a computed observable, without creating a dependecy between it and the observables inside the function. + * @param callback Function to be called. + * @param callbackTarget Defines the value of 'this' in the callback function. + * @param callbackArgs Arguments for the callback Function. + */ + ignoreDependencies<T>(callback: () => T, callbackTarget?: any, callbackArgs?: any): T; + + expressionRewriting: { + bindingRewriteValidators: any[]; + twoWayBindings: any; + parseObjectLiteral: (objectLiteralString: string) => any[]; + + /** + Internal, private KO utility for updating model properties from within bindings + property: If the property being updated is (or might be) an observable, pass it here + If it turns out to be a writable observable, it will be written to directly + allBindings: An object with a get method to retrieve bindings in the current execution context. + This will be searched for a '_ko_property_writers' property in case you're writing to a non-observable + (See note below) + key: The key identifying the property to be written. Example: for { hasFocus: myValue }, write to 'myValue' by specifying the key 'hasFocus' + value: The value to be written + checkIfDifferent: If true, and if the property being written is a writable observable, the value will only be written if + it is !== existing value on that writable observable + + Note that if you need to write to the viewModel without an observable property, + you need to set ko.expressionRewriting.twoWayBindings[key] = true; *before* the binding evaluation. + */ + writeValueToProperty: (property: KnockoutObservable<any> | any, allBindings: KnockoutAllBindingsAccessor, key: string, value: any, checkIfDifferent?: boolean) => void; + }; + + ///////////////////////////////// + + bindingProvider: { + instance: KnockoutBindingProvider; + new(): KnockoutBindingProvider; + } + + ///////////////////////////////// + // selectExtensions.js + ///////////////////////////////// + + selectExtensions: { + + readValue(element: HTMLElement): any; + + writeValue(element: HTMLElement, value: any, allowUnset?: boolean): void; + }; + + components: KnockoutComponents; + + ///////////////////////////////// + // options.js + ///////////////////////////////// + + options: { + deferUpdates: boolean, + + useOnlyNativeEvents: boolean + }; + + ///////////////////////////////// + // tasks.js + ///////////////////////////////// + + tasks: KnockoutTasks; + + ///////////////////////////////// + // utils.js + ///////////////////////////////// + + onError?: (error: Error) => void; +} + +interface KnockoutBindingProvider { + nodeHasBindings(node: Node): boolean; + getBindings(node: Node, bindingContext: KnockoutBindingContext): {}; + getBindingAccessors?(node: Node, bindingContext: KnockoutBindingContext): { [key: string]: string; }; +} + +interface KnockoutComputedContext { + /** + * Returns the number of dependencies of the computed observable detected so far during the current evaluation. + */ + getDependenciesCount(): number; + /** + * A function that returns true if called during the first ever evaluation of the current computed observable, or false otherwise. + * For pure computed observables, isInitial() is always undefined. + */ + isInitial: () => boolean; + isSleeping: boolean; +} + +// +// refactored types into a namespace to reduce global pollution +// and used Union Types to simplify overloads (requires TypeScript 1.4) +// +declare namespace KnockoutComponentTypes { + + interface Config { + viewModel?: ViewModelFunction | ViewModelSharedInstance | ViewModelFactoryFunction | AMDModule; + template: string | Node[] | DocumentFragment | TemplateElement | AMDModule; + synchronous?: boolean; + } + + interface ComponentConfig { + viewModel?: ViewModelFunction | ViewModelSharedInstance | ViewModelFactoryFunction | AMDModule; + template: any; + createViewModel?: any; + } + + interface EmptyConfig { + } + + // common AMD type + interface AMDModule { + require: string; + } + + // viewmodel types + interface ViewModelFunction { + (params?: any): any; + } + + interface ViewModelSharedInstance { + instance: any; + } + + interface ViewModelFactoryFunction { + createViewModel: (params: any, componentInfo: ComponentInfo) => any; + } + + interface ComponentInfo { + element: Node; + templateNodes: Node[]; + } + + interface TemplateElement { + element: string | Node; + } + + interface Loader { + /** + * Define this if: you want to supply configurations programmatically based on names, e.g., to implement a naming convention. + * @see {@link https://knockoutjs.com/documentation/component-loaders.html} + */ + getConfig?(componentName: string, callback: (result: ComponentConfig | null) => void): void; + /** + * Define this if: you want to take control over how component configurations are interpreted, e.g., if you do not want to use the standard 'viewModel/template' pair format. + * @see {@link https://knockoutjs.com/documentation/component-loaders.html} + */ + loadComponent?(componentName: string, config: ComponentConfig, callback: (result: Definition | null) => void): void; + /** + * Define this if: you want to use custom logic to supply DOM nodes for a given template configuration (e.g., using an ajax request to fetch a template by URL). + * @see {@link https://knockoutjs.com/documentation/component-loaders.html} + */ + loadTemplate?(componentName: string, templateConfig: any, callback: (result: Node[] | null) => void): void; + /** + * Define this if: you want to use custom logic to supply a viewmodel factory for a given viewmodel configuration (e.g., integrating with a third-party module loader or dependency injection system). + * @see {@link https://knockoutjs.com/documentation/component-loaders.html} + */ + loadViewModel?(componentName: string, viewModelConfig: any, callback: (result: any) => void): void; + suppressLoaderExceptions?: boolean; + } + + interface Definition { + template: Node[]; + createViewModel?(params: any, options: { element: Node; }): any; + } +} + +interface KnockoutComponents { + + /** + * Registers a component, in the default component loader, to be used by name in the component binding. + * @param componentName Component name. Will be used for your custom HTML tag name. + * @param config Component configuration. + */ + register(componentName: string, config: KnockoutComponentTypes.Config | KnockoutComponentTypes.EmptyConfig): void; + /** + * Determine if a component with the specified name is already registered in the default component loader. + * @param componentName Component name. + */ + isRegistered(componentName: string): boolean; + /** + * Removes the named component from the default component loader registry. Or if no such component was registered, does nothing. + * @param componentName Component name. + */ + unregister(componentName: string): void; + /** + * Searchs each registered component loader by component name, and returns the viewmodel/template declaration via callback parameter. + * @param componentName Component name. + * @param callback Function to be called with the viewmodel/template declaration parameter. + */ + get(componentName: string, callback: (definition: KnockoutComponentTypes.Definition) => void): void; + /** + * Clears the cache knockout creates to speed up component loading, for a given component. + * @param componentName Component name. + */ + clearCachedDefinition(componentName: string): void + defaultLoader: KnockoutComponentTypes.Loader; + loaders: KnockoutComponentTypes.Loader[]; + /** + * Returns the registered component name for a HTML element. Can be overwriten to to control dynamically which HTML element map to which component name. + * @param node html element that corresponds to a custom component. + */ + getComponentNameForNode(node: Node): string; +} + +declare var ko: KnockoutStatic; + +declare module "knockout" { + export = ko; +} diff --git a/js/knockout.js b/js/knockout.js new file mode 100644 index 0000000..d7520e3 --- /dev/null +++ b/js/knockout.js @@ -0,0 +1,139 @@ +/*! + * Knockout JavaScript library v3.5.1 + * (c) The Knockout.js team - http://knockoutjs.com/ + * License: MIT (http://www.opensource.org/licenses/mit-license.php) + */ + +(function() {(function(n){var A=this||(0,eval)("this"),w=A.document,R=A.navigator,v=A.jQuery,H=A.JSON;v||"undefined"===typeof jQuery||(v=jQuery);(function(n){"function"===typeof define&&define.amd?define(["exports","require"],n):"object"===typeof exports&&"object"===typeof module?n(module.exports||exports):n(A.ko={})})(function(S,T){function K(a,c){return null===a||typeof a in W?a===c:!1}function X(b,c){var d;return function(){d||(d=a.a.setTimeout(function(){d=n;b()},c))}}function Y(b,c){var d;return function(){clearTimeout(d); +d=a.a.setTimeout(b,c)}}function Z(a,c){c&&"change"!==c?"beforeChange"===c?this.pc(a):this.gb(a,c):this.qc(a)}function aa(a,c){null!==c&&c.s&&c.s()}function ba(a,c){var d=this.qd,e=d[r];e.ra||(this.Qb&&this.mb[c]?(d.uc(c,a,this.mb[c]),this.mb[c]=null,--this.Qb):e.I[c]||d.uc(c,a,e.J?{da:a}:d.$c(a)),a.Ja&&a.gd())}var a="undefined"!==typeof S?S:{};a.b=function(b,c){for(var d=b.split("."),e=a,f=0;f<d.length-1;f++)e=e[d[f]];e[d[d.length-1]]=c};a.L=function(a,c,d){a[c]=d};a.version="3.5.1";a.b("version", +a.version);a.options={deferUpdates:!1,useOnlyNativeEvents:!1,foreachHidesDestroyed:!1};a.a=function(){function b(a,b){for(var c in a)f.call(a,c)&&b(c,a[c])}function c(a,b){if(b)for(var c in b)f.call(b,c)&&(a[c]=b[c]);return a}function d(a,b){a.__proto__=b;return a}function e(b,c,d,e){var l=b[c].match(q)||[];a.a.D(d.match(q),function(b){a.a.Na(l,b,e)});b[c]=l.join(" ")}var f=Object.prototype.hasOwnProperty,g={__proto__:[]}instanceof Array,h="function"===typeof Symbol,m={},k={};m[R&&/Firefox\/2/i.test(R.userAgent)? +"KeyboardEvent":"UIEvents"]=["keyup","keydown","keypress"];m.MouseEvents="click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave".split(" ");b(m,function(a,b){if(b.length)for(var c=0,d=b.length;c<d;c++)k[b[c]]=a});var l={propertychange:!0},p=w&&function(){for(var a=3,b=w.createElement("div"),c=b.getElementsByTagName("i");b.innerHTML="\x3c!--[if gt IE "+ ++a+"]><i></i><![endif]--\x3e",c[0];);return 4<a?a:n}(),q=/\S+/g,t;return{Jc:["authenticity_token",/^__RequestVerificationToken(_.*)?$/], +D:function(a,b,c){for(var d=0,e=a.length;d<e;d++)b.call(c,a[d],d,a)},A:"function"==typeof Array.prototype.indexOf?function(a,b){return Array.prototype.indexOf.call(a,b)}:function(a,b){for(var c=0,d=a.length;c<d;c++)if(a[c]===b)return c;return-1},Lb:function(a,b,c){for(var d=0,e=a.length;d<e;d++)if(b.call(c,a[d],d,a))return a[d];return n},Pa:function(b,c){var d=a.a.A(b,c);0<d?b.splice(d,1):0===d&&b.shift()},wc:function(b){var c=[];b&&a.a.D(b,function(b){0>a.a.A(c,b)&&c.push(b)});return c},Mb:function(a, +b,c){var d=[];if(a)for(var e=0,l=a.length;e<l;e++)d.push(b.call(c,a[e],e));return d},jb:function(a,b,c){var d=[];if(a)for(var e=0,l=a.length;e<l;e++)b.call(c,a[e],e)&&d.push(a[e]);return d},Nb:function(a,b){if(b instanceof Array)a.push.apply(a,b);else for(var c=0,d=b.length;c<d;c++)a.push(b[c]);return a},Na:function(b,c,d){var e=a.a.A(a.a.bc(b),c);0>e?d&&b.push(c):d||b.splice(e,1)},Ba:g,extend:c,setPrototypeOf:d,Ab:g?d:c,P:b,Ga:function(a,b,c){if(!a)return a;var d={},e;for(e in a)f.call(a,e)&&(d[e]= +b.call(c,a[e],e,a));return d},Tb:function(b){for(;b.firstChild;)a.removeNode(b.firstChild)},Yb:function(b){b=a.a.la(b);for(var c=(b[0]&&b[0].ownerDocument||w).createElement("div"),d=0,e=b.length;d<e;d++)c.appendChild(a.oa(b[d]));return c},Ca:function(b,c){for(var d=0,e=b.length,l=[];d<e;d++){var k=b[d].cloneNode(!0);l.push(c?a.oa(k):k)}return l},va:function(b,c){a.a.Tb(b);if(c)for(var d=0,e=c.length;d<e;d++)b.appendChild(c[d])},Xc:function(b,c){var d=b.nodeType?[b]:b;if(0<d.length){for(var e=d[0], +l=e.parentNode,k=0,f=c.length;k<f;k++)l.insertBefore(c[k],e);k=0;for(f=d.length;k<f;k++)a.removeNode(d[k])}},Ua:function(a,b){if(a.length){for(b=8===b.nodeType&&b.parentNode||b;a.length&&a[0].parentNode!==b;)a.splice(0,1);for(;1<a.length&&a[a.length-1].parentNode!==b;)a.length--;if(1<a.length){var c=a[0],d=a[a.length-1];for(a.length=0;c!==d;)a.push(c),c=c.nextSibling;a.push(d)}}return a},Zc:function(a,b){7>p?a.setAttribute("selected",b):a.selected=b},Db:function(a){return null===a||a===n?"":a.trim? +a.trim():a.toString().replace(/^[\s\xa0]+|[\s\xa0]+$/g,"")},Ud:function(a,b){a=a||"";return b.length>a.length?!1:a.substring(0,b.length)===b},vd:function(a,b){if(a===b)return!0;if(11===a.nodeType)return!1;if(b.contains)return b.contains(1!==a.nodeType?a.parentNode:a);if(b.compareDocumentPosition)return 16==(b.compareDocumentPosition(a)&16);for(;a&&a!=b;)a=a.parentNode;return!!a},Sb:function(b){return a.a.vd(b,b.ownerDocument.documentElement)},kd:function(b){return!!a.a.Lb(b,a.a.Sb)},R:function(a){return a&& +a.tagName&&a.tagName.toLowerCase()},Ac:function(b){return a.onError?function(){try{return b.apply(this,arguments)}catch(c){throw a.onError&&a.onError(c),c;}}:b},setTimeout:function(b,c){return setTimeout(a.a.Ac(b),c)},Gc:function(b){setTimeout(function(){a.onError&&a.onError(b);throw b;},0)},B:function(b,c,d){var e=a.a.Ac(d);d=l[c];if(a.options.useOnlyNativeEvents||d||!v)if(d||"function"!=typeof b.addEventListener)if("undefined"!=typeof b.attachEvent){var k=function(a){e.call(b,a)},f="on"+c;b.attachEvent(f, +k);a.a.K.za(b,function(){b.detachEvent(f,k)})}else throw Error("Browser doesn't support addEventListener or attachEvent");else b.addEventListener(c,e,!1);else t||(t="function"==typeof v(b).on?"on":"bind"),v(b)[t](c,e)},Fb:function(b,c){if(!b||!b.nodeType)throw Error("element must be a DOM node when calling triggerEvent");var d;"input"===a.a.R(b)&&b.type&&"click"==c.toLowerCase()?(d=b.type,d="checkbox"==d||"radio"==d):d=!1;if(a.options.useOnlyNativeEvents||!v||d)if("function"==typeof w.createEvent)if("function"== +typeof b.dispatchEvent)d=w.createEvent(k[c]||"HTMLEvents"),d.initEvent(c,!0,!0,A,0,0,0,0,0,!1,!1,!1,!1,0,b),b.dispatchEvent(d);else throw Error("The supplied element doesn't support dispatchEvent");else if(d&&b.click)b.click();else if("undefined"!=typeof b.fireEvent)b.fireEvent("on"+c);else throw Error("Browser doesn't support triggering events");else v(b).trigger(c)},f:function(b){return a.O(b)?b():b},bc:function(b){return a.O(b)?b.v():b},Eb:function(b,c,d){var l;c&&("object"===typeof b.classList? +(l=b.classList[d?"add":"remove"],a.a.D(c.match(q),function(a){l.call(b.classList,a)})):"string"===typeof b.className.baseVal?e(b.className,"baseVal",c,d):e(b,"className",c,d))},Bb:function(b,c){var d=a.a.f(c);if(null===d||d===n)d="";var e=a.h.firstChild(b);!e||3!=e.nodeType||a.h.nextSibling(e)?a.h.va(b,[b.ownerDocument.createTextNode(d)]):e.data=d;a.a.Ad(b)},Yc:function(a,b){a.name=b;if(7>=p)try{var c=a.name.replace(/[&<>'"]/g,function(a){return"&#"+a.charCodeAt(0)+";"});a.mergeAttributes(w.createElement("<input name='"+ +c+"'/>"),!1)}catch(d){}},Ad:function(a){9<=p&&(a=1==a.nodeType?a:a.parentNode,a.style&&(a.style.zoom=a.style.zoom))},wd:function(a){if(p){var b=a.style.width;a.style.width=0;a.style.width=b}},Pd:function(b,c){b=a.a.f(b);c=a.a.f(c);for(var d=[],e=b;e<=c;e++)d.push(e);return d},la:function(a){for(var b=[],c=0,d=a.length;c<d;c++)b.push(a[c]);return b},Da:function(a){return h?Symbol(a):a},Zd:6===p,$d:7===p,W:p,Lc:function(b,c){for(var d=a.a.la(b.getElementsByTagName("input")).concat(a.a.la(b.getElementsByTagName("textarea"))), +e="string"==typeof c?function(a){return a.name===c}:function(a){return c.test(a.name)},l=[],k=d.length-1;0<=k;k--)e(d[k])&&l.push(d[k]);return l},Nd:function(b){return"string"==typeof b&&(b=a.a.Db(b))?H&&H.parse?H.parse(b):(new Function("return "+b))():null},hc:function(b,c,d){if(!H||!H.stringify)throw Error("Cannot find JSON.stringify(). Some browsers (e.g., IE < 8) don't support it natively, but you can overcome this by adding a script reference to json2.js, downloadable from http://www.json.org/json2.js"); +return H.stringify(a.a.f(b),c,d)},Od:function(c,d,e){e=e||{};var l=e.params||{},k=e.includeFields||this.Jc,f=c;if("object"==typeof c&&"form"===a.a.R(c))for(var f=c.action,h=k.length-1;0<=h;h--)for(var g=a.a.Lc(c,k[h]),m=g.length-1;0<=m;m--)l[g[m].name]=g[m].value;d=a.a.f(d);var p=w.createElement("form");p.style.display="none";p.action=f;p.method="post";for(var q in d)c=w.createElement("input"),c.type="hidden",c.name=q,c.value=a.a.hc(a.a.f(d[q])),p.appendChild(c);b(l,function(a,b){var c=w.createElement("input"); +c.type="hidden";c.name=a;c.value=b;p.appendChild(c)});w.body.appendChild(p);e.submitter?e.submitter(p):p.submit();setTimeout(function(){p.parentNode.removeChild(p)},0)}}}();a.b("utils",a.a);a.b("utils.arrayForEach",a.a.D);a.b("utils.arrayFirst",a.a.Lb);a.b("utils.arrayFilter",a.a.jb);a.b("utils.arrayGetDistinctValues",a.a.wc);a.b("utils.arrayIndexOf",a.a.A);a.b("utils.arrayMap",a.a.Mb);a.b("utils.arrayPushAll",a.a.Nb);a.b("utils.arrayRemoveItem",a.a.Pa);a.b("utils.cloneNodes",a.a.Ca);a.b("utils.createSymbolOrString", +a.a.Da);a.b("utils.extend",a.a.extend);a.b("utils.fieldsIncludedWithJsonPost",a.a.Jc);a.b("utils.getFormFields",a.a.Lc);a.b("utils.objectMap",a.a.Ga);a.b("utils.peekObservable",a.a.bc);a.b("utils.postJson",a.a.Od);a.b("utils.parseJson",a.a.Nd);a.b("utils.registerEventHandler",a.a.B);a.b("utils.stringifyJson",a.a.hc);a.b("utils.range",a.a.Pd);a.b("utils.toggleDomNodeCssClass",a.a.Eb);a.b("utils.triggerEvent",a.a.Fb);a.b("utils.unwrapObservable",a.a.f);a.b("utils.objectForEach",a.a.P);a.b("utils.addOrRemoveItem", +a.a.Na);a.b("utils.setTextContent",a.a.Bb);a.b("unwrap",a.a.f);Function.prototype.bind||(Function.prototype.bind=function(a){var c=this;if(1===arguments.length)return function(){return c.apply(a,arguments)};var d=Array.prototype.slice.call(arguments,1);return function(){var e=d.slice(0);e.push.apply(e,arguments);return c.apply(a,e)}});a.a.g=new function(){var b=0,c="__ko__"+(new Date).getTime(),d={},e,f;a.a.W?(e=function(a,e){var f=a[c];if(!f||"null"===f||!d[f]){if(!e)return n;f=a[c]="ko"+b++;d[f]= +{}}return d[f]},f=function(a){var b=a[c];return b?(delete d[b],a[c]=null,!0):!1}):(e=function(a,b){var d=a[c];!d&&b&&(d=a[c]={});return d},f=function(a){return a[c]?(delete a[c],!0):!1});return{get:function(a,b){var c=e(a,!1);return c&&c[b]},set:function(a,b,c){(a=e(a,c!==n))&&(a[b]=c)},Ub:function(a,b,c){a=e(a,!0);return a[b]||(a[b]=c)},clear:f,Z:function(){return b++ +c}}};a.b("utils.domData",a.a.g);a.b("utils.domData.clear",a.a.g.clear);a.a.K=new function(){function b(b,c){var d=a.a.g.get(b,e); +d===n&&c&&(d=[],a.a.g.set(b,e,d));return d}function c(c){var e=b(c,!1);if(e)for(var e=e.slice(0),k=0;k<e.length;k++)e[k](c);a.a.g.clear(c);a.a.K.cleanExternalData(c);g[c.nodeType]&&d(c.childNodes,!0)}function d(b,d){for(var e=[],l,f=0;f<b.length;f++)if(!d||8===b[f].nodeType)if(c(e[e.length]=l=b[f]),b[f]!==l)for(;f--&&-1==a.a.A(e,b[f]););}var e=a.a.g.Z(),f={1:!0,8:!0,9:!0},g={1:!0,9:!0};return{za:function(a,c){if("function"!=typeof c)throw Error("Callback must be a function");b(a,!0).push(c)},yb:function(c, +d){var f=b(c,!1);f&&(a.a.Pa(f,d),0==f.length&&a.a.g.set(c,e,n))},oa:function(b){a.u.G(function(){f[b.nodeType]&&(c(b),g[b.nodeType]&&d(b.getElementsByTagName("*")))});return b},removeNode:function(b){a.oa(b);b.parentNode&&b.parentNode.removeChild(b)},cleanExternalData:function(a){v&&"function"==typeof v.cleanData&&v.cleanData([a])}}};a.oa=a.a.K.oa;a.removeNode=a.a.K.removeNode;a.b("cleanNode",a.oa);a.b("removeNode",a.removeNode);a.b("utils.domNodeDisposal",a.a.K);a.b("utils.domNodeDisposal.addDisposeCallback", +a.a.K.za);a.b("utils.domNodeDisposal.removeDisposeCallback",a.a.K.yb);(function(){var b=[0,"",""],c=[1,"<table>","</table>"],d=[3,"<table><tbody><tr>","</tr></tbody></table>"],e=[1,"<select multiple='multiple'>","</select>"],f={thead:c,tbody:c,tfoot:c,tr:[2,"<table><tbody>","</tbody></table>"],td:d,th:d,option:e,optgroup:e},g=8>=a.a.W;a.a.ua=function(c,d){var e;if(v)if(v.parseHTML)e=v.parseHTML(c,d)||[];else{if((e=v.clean([c],d))&&e[0]){for(var l=e[0];l.parentNode&&11!==l.parentNode.nodeType;)l=l.parentNode; +l.parentNode&&l.parentNode.removeChild(l)}}else{(e=d)||(e=w);var l=e.parentWindow||e.defaultView||A,p=a.a.Db(c).toLowerCase(),q=e.createElement("div"),t;t=(p=p.match(/^(?:\x3c!--.*?--\x3e\s*?)*?<([a-z]+)[\s>]/))&&f[p[1]]||b;p=t[0];t="ignored<div>"+t[1]+c+t[2]+"</div>";"function"==typeof l.innerShiv?q.appendChild(l.innerShiv(t)):(g&&e.body.appendChild(q),q.innerHTML=t,g&&q.parentNode.removeChild(q));for(;p--;)q=q.lastChild;e=a.a.la(q.lastChild.childNodes)}return e};a.a.Md=function(b,c){var d=a.a.ua(b, +c);return d.length&&d[0].parentElement||a.a.Yb(d)};a.a.fc=function(b,c){a.a.Tb(b);c=a.a.f(c);if(null!==c&&c!==n)if("string"!=typeof c&&(c=c.toString()),v)v(b).html(c);else for(var d=a.a.ua(c,b.ownerDocument),e=0;e<d.length;e++)b.appendChild(d[e])}})();a.b("utils.parseHtmlFragment",a.a.ua);a.b("utils.setHtml",a.a.fc);a.aa=function(){function b(c,e){if(c)if(8==c.nodeType){var f=a.aa.Uc(c.nodeValue);null!=f&&e.push({ud:c,Kd:f})}else if(1==c.nodeType)for(var f=0,g=c.childNodes,h=g.length;f<h;f++)b(g[f], +e)}var c={};return{Xb:function(a){if("function"!=typeof a)throw Error("You can only pass a function to ko.memoization.memoize()");var b=(4294967296*(1+Math.random())|0).toString(16).substring(1)+(4294967296*(1+Math.random())|0).toString(16).substring(1);c[b]=a;return"\x3c!--[ko_memo:"+b+"]--\x3e"},bd:function(a,b){var f=c[a];if(f===n)throw Error("Couldn't find any memo with ID "+a+". Perhaps it's already been unmemoized.");try{return f.apply(null,b||[]),!0}finally{delete c[a]}},cd:function(c,e){var f= +[];b(c,f);for(var g=0,h=f.length;g<h;g++){var m=f[g].ud,k=[m];e&&a.a.Nb(k,e);a.aa.bd(f[g].Kd,k);m.nodeValue="";m.parentNode&&m.parentNode.removeChild(m)}},Uc:function(a){return(a=a.match(/^\[ko_memo\:(.*?)\]$/))?a[1]:null}}}();a.b("memoization",a.aa);a.b("memoization.memoize",a.aa.Xb);a.b("memoization.unmemoize",a.aa.bd);a.b("memoization.parseMemoText",a.aa.Uc);a.b("memoization.unmemoizeDomNodeAndDescendants",a.aa.cd);a.na=function(){function b(){if(f)for(var b=f,c=0,d;h<f;)if(d=e[h++]){if(h>b){if(5E3<= +++c){h=f;a.a.Gc(Error("'Too much recursion' after processing "+c+" task groups."));break}b=f}try{d()}catch(p){a.a.Gc(p)}}}function c(){b();h=f=e.length=0}var d,e=[],f=0,g=1,h=0;A.MutationObserver?d=function(a){var b=w.createElement("div");(new MutationObserver(a)).observe(b,{attributes:!0});return function(){b.classList.toggle("foo")}}(c):d=w&&"onreadystatechange"in w.createElement("script")?function(a){var b=w.createElement("script");b.onreadystatechange=function(){b.onreadystatechange=null;w.documentElement.removeChild(b); +b=null;a()};w.documentElement.appendChild(b)}:function(a){setTimeout(a,0)};return{scheduler:d,zb:function(b){f||a.na.scheduler(c);e[f++]=b;return g++},cancel:function(a){a=a-(g-f);a>=h&&a<f&&(e[a]=null)},resetForTesting:function(){var a=f-h;h=f=e.length=0;return a},Sd:b}}();a.b("tasks",a.na);a.b("tasks.schedule",a.na.zb);a.b("tasks.runEarly",a.na.Sd);a.Ta={throttle:function(b,c){b.throttleEvaluation=c;var d=null;return a.$({read:b,write:function(e){clearTimeout(d);d=a.a.setTimeout(function(){b(e)}, +c)}})},rateLimit:function(a,c){var d,e,f;"number"==typeof c?d=c:(d=c.timeout,e=c.method);a.Hb=!1;f="function"==typeof e?e:"notifyWhenChangesStop"==e?Y:X;a.ub(function(a){return f(a,d,c)})},deferred:function(b,c){if(!0!==c)throw Error("The 'deferred' extender only accepts the value 'true', because it is not supported to turn deferral off once enabled.");b.Hb||(b.Hb=!0,b.ub(function(c){var e,f=!1;return function(){if(!f){a.na.cancel(e);e=a.na.zb(c);try{f=!0,b.notifySubscribers(n,"dirty")}finally{f= +!1}}}}))},notify:function(a,c){a.equalityComparer="always"==c?null:K}};var W={undefined:1,"boolean":1,number:1,string:1};a.b("extenders",a.Ta);a.ic=function(b,c,d){this.da=b;this.lc=c;this.mc=d;this.Ib=!1;this.fb=this.Jb=null;a.L(this,"dispose",this.s);a.L(this,"disposeWhenNodeIsRemoved",this.l)};a.ic.prototype.s=function(){this.Ib||(this.fb&&a.a.K.yb(this.Jb,this.fb),this.Ib=!0,this.mc(),this.da=this.lc=this.mc=this.Jb=this.fb=null)};a.ic.prototype.l=function(b){this.Jb=b;a.a.K.za(b,this.fb=this.s.bind(this))}; +a.T=function(){a.a.Ab(this,D);D.qb(this)};var D={qb:function(a){a.U={change:[]};a.sc=1},subscribe:function(b,c,d){var e=this;d=d||"change";var f=new a.ic(e,c?b.bind(c):b,function(){a.a.Pa(e.U[d],f);e.hb&&e.hb(d)});e.Qa&&e.Qa(d);e.U[d]||(e.U[d]=[]);e.U[d].push(f);return f},notifySubscribers:function(b,c){c=c||"change";"change"===c&&this.Gb();if(this.Wa(c)){var d="change"===c&&this.ed||this.U[c].slice(0);try{a.u.xc();for(var e=0,f;f=d[e];++e)f.Ib||f.lc(b)}finally{a.u.end()}}},ob:function(){return this.sc}, +Dd:function(a){return this.ob()!==a},Gb:function(){++this.sc},ub:function(b){var c=this,d=a.O(c),e,f,g,h,m;c.gb||(c.gb=c.notifySubscribers,c.notifySubscribers=Z);var k=b(function(){c.Ja=!1;d&&h===c&&(h=c.nc?c.nc():c());var a=f||m&&c.sb(g,h);m=f=e=!1;a&&c.gb(g=h)});c.qc=function(a,b){b&&c.Ja||(m=!b);c.ed=c.U.change.slice(0);c.Ja=e=!0;h=a;k()};c.pc=function(a){e||(g=a,c.gb(a,"beforeChange"))};c.rc=function(){m=!0};c.gd=function(){c.sb(g,c.v(!0))&&(f=!0)}},Wa:function(a){return this.U[a]&&this.U[a].length}, +Bd:function(b){if(b)return this.U[b]&&this.U[b].length||0;var c=0;a.a.P(this.U,function(a,b){"dirty"!==a&&(c+=b.length)});return c},sb:function(a,c){return!this.equalityComparer||!this.equalityComparer(a,c)},toString:function(){return"[object Object]"},extend:function(b){var c=this;b&&a.a.P(b,function(b,e){var f=a.Ta[b];"function"==typeof f&&(c=f(c,e)||c)});return c}};a.L(D,"init",D.qb);a.L(D,"subscribe",D.subscribe);a.L(D,"extend",D.extend);a.L(D,"getSubscriptionsCount",D.Bd);a.a.Ba&&a.a.setPrototypeOf(D, +Function.prototype);a.T.fn=D;a.Qc=function(a){return null!=a&&"function"==typeof a.subscribe&&"function"==typeof a.notifySubscribers};a.b("subscribable",a.T);a.b("isSubscribable",a.Qc);a.S=a.u=function(){function b(a){d.push(e);e=a}function c(){e=d.pop()}var d=[],e,f=0;return{xc:b,end:c,cc:function(b){if(e){if(!a.Qc(b))throw Error("Only subscribable things can act as dependencies");e.od.call(e.pd,b,b.fd||(b.fd=++f))}},G:function(a,d,e){try{return b(),a.apply(d,e||[])}finally{c()}},qa:function(){if(e)return e.o.qa()}, +Va:function(){if(e)return e.o.Va()},Ya:function(){if(e)return e.Ya},o:function(){if(e)return e.o}}}();a.b("computedContext",a.S);a.b("computedContext.getDependenciesCount",a.S.qa);a.b("computedContext.getDependencies",a.S.Va);a.b("computedContext.isInitial",a.S.Ya);a.b("computedContext.registerDependency",a.S.cc);a.b("ignoreDependencies",a.Yd=a.u.G);var I=a.a.Da("_latestValue");a.ta=function(b){function c(){if(0<arguments.length)return c.sb(c[I],arguments[0])&&(c.ya(),c[I]=arguments[0],c.xa()),this; +a.u.cc(c);return c[I]}c[I]=b;a.a.Ba||a.a.extend(c,a.T.fn);a.T.fn.qb(c);a.a.Ab(c,F);a.options.deferUpdates&&a.Ta.deferred(c,!0);return c};var F={equalityComparer:K,v:function(){return this[I]},xa:function(){this.notifySubscribers(this[I],"spectate");this.notifySubscribers(this[I])},ya:function(){this.notifySubscribers(this[I],"beforeChange")}};a.a.Ba&&a.a.setPrototypeOf(F,a.T.fn);var G=a.ta.Ma="__ko_proto__";F[G]=a.ta;a.O=function(b){if((b="function"==typeof b&&b[G])&&b!==F[G]&&b!==a.o.fn[G])throw Error("Invalid object that looks like an observable; possibly from another Knockout instance"); +return!!b};a.Za=function(b){return"function"==typeof b&&(b[G]===F[G]||b[G]===a.o.fn[G]&&b.Nc)};a.b("observable",a.ta);a.b("isObservable",a.O);a.b("isWriteableObservable",a.Za);a.b("isWritableObservable",a.Za);a.b("observable.fn",F);a.L(F,"peek",F.v);a.L(F,"valueHasMutated",F.xa);a.L(F,"valueWillMutate",F.ya);a.Ha=function(b){b=b||[];if("object"!=typeof b||!("length"in b))throw Error("The argument passed when initializing an observable array must be an array, or null, or undefined.");b=a.ta(b);a.a.Ab(b, +a.Ha.fn);return b.extend({trackArrayChanges:!0})};a.Ha.fn={remove:function(b){for(var c=this.v(),d=[],e="function"!=typeof b||a.O(b)?function(a){return a===b}:b,f=0;f<c.length;f++){var g=c[f];if(e(g)){0===d.length&&this.ya();if(c[f]!==g)throw Error("Array modified during remove; cannot remove item");d.push(g);c.splice(f,1);f--}}d.length&&this.xa();return d},removeAll:function(b){if(b===n){var c=this.v(),d=c.slice(0);this.ya();c.splice(0,c.length);this.xa();return d}return b?this.remove(function(c){return 0<= +a.a.A(b,c)}):[]},destroy:function(b){var c=this.v(),d="function"!=typeof b||a.O(b)?function(a){return a===b}:b;this.ya();for(var e=c.length-1;0<=e;e--){var f=c[e];d(f)&&(f._destroy=!0)}this.xa()},destroyAll:function(b){return b===n?this.destroy(function(){return!0}):b?this.destroy(function(c){return 0<=a.a.A(b,c)}):[]},indexOf:function(b){var c=this();return a.a.A(c,b)},replace:function(a,c){var d=this.indexOf(a);0<=d&&(this.ya(),this.v()[d]=c,this.xa())},sorted:function(a){var c=this().slice(0); +return a?c.sort(a):c.sort()},reversed:function(){return this().slice(0).reverse()}};a.a.Ba&&a.a.setPrototypeOf(a.Ha.fn,a.ta.fn);a.a.D("pop push reverse shift sort splice unshift".split(" "),function(b){a.Ha.fn[b]=function(){var a=this.v();this.ya();this.zc(a,b,arguments);var d=a[b].apply(a,arguments);this.xa();return d===a?this:d}});a.a.D(["slice"],function(b){a.Ha.fn[b]=function(){var a=this();return a[b].apply(a,arguments)}});a.Pc=function(b){return a.O(b)&&"function"==typeof b.remove&&"function"== +typeof b.push};a.b("observableArray",a.Ha);a.b("isObservableArray",a.Pc);a.Ta.trackArrayChanges=function(b,c){function d(){function c(){if(m){var d=[].concat(b.v()||[]),e;if(b.Wa("arrayChange")){if(!f||1<m)f=a.a.Pb(k,d,b.Ob);e=f}k=d;f=null;m=0;e&&e.length&&b.notifySubscribers(e,"arrayChange")}}e?c():(e=!0,h=b.subscribe(function(){++m},null,"spectate"),k=[].concat(b.v()||[]),f=null,g=b.subscribe(c))}b.Ob={};c&&"object"==typeof c&&a.a.extend(b.Ob,c);b.Ob.sparse=!0;if(!b.zc){var e=!1,f=null,g,h,m=0, +k,l=b.Qa,p=b.hb;b.Qa=function(a){l&&l.call(b,a);"arrayChange"===a&&d()};b.hb=function(a){p&&p.call(b,a);"arrayChange"!==a||b.Wa("arrayChange")||(g&&g.s(),h&&h.s(),h=g=null,e=!1,k=n)};b.zc=function(b,c,d){function l(a,b,c){return k[k.length]={status:a,value:b,index:c}}if(e&&!m){var k=[],p=b.length,g=d.length,h=0;switch(c){case "push":h=p;case "unshift":for(c=0;c<g;c++)l("added",d[c],h+c);break;case "pop":h=p-1;case "shift":p&&l("deleted",b[h],h);break;case "splice":c=Math.min(Math.max(0,0>d[0]?p+d[0]: +d[0]),p);for(var p=1===g?p:Math.min(c+(d[1]||0),p),g=c+g-2,h=Math.max(p,g),U=[],L=[],n=2;c<h;++c,++n)c<p&&L.push(l("deleted",b[c],c)),c<g&&U.push(l("added",d[n],c));a.a.Kc(L,U);break;default:return}f=k}}}};var r=a.a.Da("_state");a.o=a.$=function(b,c,d){function e(){if(0<arguments.length){if("function"===typeof f)f.apply(g.nb,arguments);else throw Error("Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.");return this}g.ra|| +a.u.cc(e);(g.ka||g.J&&e.Xa())&&e.ha();return g.X}"object"===typeof b?d=b:(d=d||{},b&&(d.read=b));if("function"!=typeof d.read)throw Error("Pass a function that returns the value of the ko.computed");var f=d.write,g={X:n,sa:!0,ka:!0,rb:!1,jc:!1,ra:!1,wb:!1,J:!1,Wc:d.read,nb:c||d.owner,l:d.disposeWhenNodeIsRemoved||d.l||null,Sa:d.disposeWhen||d.Sa,Rb:null,I:{},V:0,Ic:null};e[r]=g;e.Nc="function"===typeof f;a.a.Ba||a.a.extend(e,a.T.fn);a.T.fn.qb(e);a.a.Ab(e,C);d.pure?(g.wb=!0,g.J=!0,a.a.extend(e,da)): +d.deferEvaluation&&a.a.extend(e,ea);a.options.deferUpdates&&a.Ta.deferred(e,!0);g.l&&(g.jc=!0,g.l.nodeType||(g.l=null));g.J||d.deferEvaluation||e.ha();g.l&&e.ja()&&a.a.K.za(g.l,g.Rb=function(){e.s()});return e};var C={equalityComparer:K,qa:function(){return this[r].V},Va:function(){var b=[];a.a.P(this[r].I,function(a,d){b[d.Ka]=d.da});return b},Vb:function(b){if(!this[r].V)return!1;var c=this.Va();return-1!==a.a.A(c,b)?!0:!!a.a.Lb(c,function(a){return a.Vb&&a.Vb(b)})},uc:function(a,c,d){if(this[r].wb&& +c===this)throw Error("A 'pure' computed must not be called recursively");this[r].I[a]=d;d.Ka=this[r].V++;d.La=c.ob()},Xa:function(){var a,c,d=this[r].I;for(a in d)if(Object.prototype.hasOwnProperty.call(d,a)&&(c=d[a],this.Ia&&c.da.Ja||c.da.Dd(c.La)))return!0},Jd:function(){this.Ia&&!this[r].rb&&this.Ia(!1)},ja:function(){var a=this[r];return a.ka||0<a.V},Rd:function(){this.Ja?this[r].ka&&(this[r].sa=!0):this.Hc()},$c:function(a){if(a.Hb){var c=a.subscribe(this.Jd,this,"dirty"),d=a.subscribe(this.Rd, +this);return{da:a,s:function(){c.s();d.s()}}}return a.subscribe(this.Hc,this)},Hc:function(){var b=this,c=b.throttleEvaluation;c&&0<=c?(clearTimeout(this[r].Ic),this[r].Ic=a.a.setTimeout(function(){b.ha(!0)},c)):b.Ia?b.Ia(!0):b.ha(!0)},ha:function(b){var c=this[r],d=c.Sa,e=!1;if(!c.rb&&!c.ra){if(c.l&&!a.a.Sb(c.l)||d&&d()){if(!c.jc){this.s();return}}else c.jc=!1;c.rb=!0;try{e=this.zd(b)}finally{c.rb=!1}return e}},zd:function(b){var c=this[r],d=!1,e=c.wb?n:!c.V,d={qd:this,mb:c.I,Qb:c.V};a.u.xc({pd:d, +od:ba,o:this,Ya:e});c.I={};c.V=0;var f=this.yd(c,d);c.V?d=this.sb(c.X,f):(this.s(),d=!0);d&&(c.J?this.Gb():this.notifySubscribers(c.X,"beforeChange"),c.X=f,this.notifySubscribers(c.X,"spectate"),!c.J&&b&&this.notifySubscribers(c.X),this.rc&&this.rc());e&&this.notifySubscribers(c.X,"awake");return d},yd:function(b,c){try{var d=b.Wc;return b.nb?d.call(b.nb):d()}finally{a.u.end(),c.Qb&&!b.J&&a.a.P(c.mb,aa),b.sa=b.ka=!1}},v:function(a){var c=this[r];(c.ka&&(a||!c.V)||c.J&&this.Xa())&&this.ha();return c.X}, +ub:function(b){a.T.fn.ub.call(this,b);this.nc=function(){this[r].J||(this[r].sa?this.ha():this[r].ka=!1);return this[r].X};this.Ia=function(a){this.pc(this[r].X);this[r].ka=!0;a&&(this[r].sa=!0);this.qc(this,!a)}},s:function(){var b=this[r];!b.J&&b.I&&a.a.P(b.I,function(a,b){b.s&&b.s()});b.l&&b.Rb&&a.a.K.yb(b.l,b.Rb);b.I=n;b.V=0;b.ra=!0;b.sa=!1;b.ka=!1;b.J=!1;b.l=n;b.Sa=n;b.Wc=n;this.Nc||(b.nb=n)}},da={Qa:function(b){var c=this,d=c[r];if(!d.ra&&d.J&&"change"==b){d.J=!1;if(d.sa||c.Xa())d.I=null,d.V= +0,c.ha()&&c.Gb();else{var e=[];a.a.P(d.I,function(a,b){e[b.Ka]=a});a.a.D(e,function(a,b){var e=d.I[a],m=c.$c(e.da);m.Ka=b;m.La=e.La;d.I[a]=m});c.Xa()&&c.ha()&&c.Gb()}d.ra||c.notifySubscribers(d.X,"awake")}},hb:function(b){var c=this[r];c.ra||"change"!=b||this.Wa("change")||(a.a.P(c.I,function(a,b){b.s&&(c.I[a]={da:b.da,Ka:b.Ka,La:b.La},b.s())}),c.J=!0,this.notifySubscribers(n,"asleep"))},ob:function(){var b=this[r];b.J&&(b.sa||this.Xa())&&this.ha();return a.T.fn.ob.call(this)}},ea={Qa:function(a){"change"!= +a&&"beforeChange"!=a||this.v()}};a.a.Ba&&a.a.setPrototypeOf(C,a.T.fn);var N=a.ta.Ma;C[N]=a.o;a.Oc=function(a){return"function"==typeof a&&a[N]===C[N]};a.Fd=function(b){return a.Oc(b)&&b[r]&&b[r].wb};a.b("computed",a.o);a.b("dependentObservable",a.o);a.b("isComputed",a.Oc);a.b("isPureComputed",a.Fd);a.b("computed.fn",C);a.L(C,"peek",C.v);a.L(C,"dispose",C.s);a.L(C,"isActive",C.ja);a.L(C,"getDependenciesCount",C.qa);a.L(C,"getDependencies",C.Va);a.xb=function(b,c){if("function"===typeof b)return a.o(b, +c,{pure:!0});b=a.a.extend({},b);b.pure=!0;return a.o(b,c)};a.b("pureComputed",a.xb);(function(){function b(a,f,g){g=g||new d;a=f(a);if("object"!=typeof a||null===a||a===n||a instanceof RegExp||a instanceof Date||a instanceof String||a instanceof Number||a instanceof Boolean)return a;var h=a instanceof Array?[]:{};g.save(a,h);c(a,function(c){var d=f(a[c]);switch(typeof d){case "boolean":case "number":case "string":case "function":h[c]=d;break;case "object":case "undefined":var l=g.get(d);h[c]=l!== +n?l:b(d,f,g)}});return h}function c(a,b){if(a instanceof Array){for(var c=0;c<a.length;c++)b(c);"function"==typeof a.toJSON&&b("toJSON")}else for(c in a)b(c)}function d(){this.keys=[];this.values=[]}a.ad=function(c){if(0==arguments.length)throw Error("When calling ko.toJS, pass the object you want to convert.");return b(c,function(b){for(var c=0;a.O(b)&&10>c;c++)b=b();return b})};a.toJSON=function(b,c,d){b=a.ad(b);return a.a.hc(b,c,d)};d.prototype={constructor:d,save:function(b,c){var d=a.a.A(this.keys, +b);0<=d?this.values[d]=c:(this.keys.push(b),this.values.push(c))},get:function(b){b=a.a.A(this.keys,b);return 0<=b?this.values[b]:n}}})();a.b("toJS",a.ad);a.b("toJSON",a.toJSON);a.Wd=function(b,c,d){function e(c){var e=a.xb(b,d).extend({ma:"always"}),h=e.subscribe(function(a){a&&(h.s(),c(a))});e.notifySubscribers(e.v());return h}return"function"!==typeof Promise||c?e(c.bind(d)):new Promise(e)};a.b("when",a.Wd);(function(){a.w={M:function(b){switch(a.a.R(b)){case "option":return!0===b.__ko__hasDomDataOptionValue__? +a.a.g.get(b,a.c.options.$b):7>=a.a.W?b.getAttributeNode("value")&&b.getAttributeNode("value").specified?b.value:b.text:b.value;case "select":return 0<=b.selectedIndex?a.w.M(b.options[b.selectedIndex]):n;default:return b.value}},cb:function(b,c,d){switch(a.a.R(b)){case "option":"string"===typeof c?(a.a.g.set(b,a.c.options.$b,n),"__ko__hasDomDataOptionValue__"in b&&delete b.__ko__hasDomDataOptionValue__,b.value=c):(a.a.g.set(b,a.c.options.$b,c),b.__ko__hasDomDataOptionValue__=!0,b.value="number"=== +typeof c?c:"");break;case "select":if(""===c||null===c)c=n;for(var e=-1,f=0,g=b.options.length,h;f<g;++f)if(h=a.w.M(b.options[f]),h==c||""===h&&c===n){e=f;break}if(d||0<=e||c===n&&1<b.size)b.selectedIndex=e,6===a.a.W&&a.a.setTimeout(function(){b.selectedIndex=e},0);break;default:if(null===c||c===n)c="";b.value=c}}}})();a.b("selectExtensions",a.w);a.b("selectExtensions.readValue",a.w.M);a.b("selectExtensions.writeValue",a.w.cb);a.m=function(){function b(b){b=a.a.Db(b);123===b.charCodeAt(0)&&(b=b.slice(1, +-1));b+="\n,";var c=[],d=b.match(e),p,q=[],h=0;if(1<d.length){for(var x=0,B;B=d[x];++x){var u=B.charCodeAt(0);if(44===u){if(0>=h){c.push(p&&q.length?{key:p,value:q.join("")}:{unknown:p||q.join("")});p=h=0;q=[];continue}}else if(58===u){if(!h&&!p&&1===q.length){p=q.pop();continue}}else if(47===u&&1<B.length&&(47===B.charCodeAt(1)||42===B.charCodeAt(1)))continue;else 47===u&&x&&1<B.length?(u=d[x-1].match(f))&&!g[u[0]]&&(b=b.substr(b.indexOf(B)+1),d=b.match(e),x=-1,B="/"):40===u||123===u||91===u?++h: +41===u||125===u||93===u?--h:p||q.length||34!==u&&39!==u||(B=B.slice(1,-1));q.push(B)}if(0<h)throw Error("Unbalanced parentheses, braces, or brackets");}return c}var c=["true","false","null","undefined"],d=/^(?:[$_a-z][$\w]*|(.+)(\.\s*[$_a-z][$\w]*|\[.+\]))$/i,e=RegExp("\"(?:\\\\.|[^\"])*\"|'(?:\\\\.|[^'])*'|`(?:\\\\.|[^`])*`|/\\*(?:[^*]|\\*+[^*/])*\\*+/|//.*\n|/(?:\\\\.|[^/])+/w*|[^\\s:,/][^,\"'`{}()/:[\\]]*[^\\s,\"'`{}()/:[\\]]|[^\\s]","g"),f=/[\])"'A-Za-z0-9_$]+$/,g={"in":1,"return":1,"typeof":1}, +h={};return{Ra:[],wa:h,ac:b,vb:function(e,f){function l(b,e){var f;if(!x){var k=a.getBindingHandler(b);if(k&&k.preprocess&&!(e=k.preprocess(e,b,l)))return;if(k=h[b])f=e,0<=a.a.A(c,f)?f=!1:(k=f.match(d),f=null===k?!1:k[1]?"Object("+k[1]+")"+k[2]:f),k=f;k&&q.push("'"+("string"==typeof h[b]?h[b]:b)+"':function(_z){"+f+"=_z}")}g&&(e="function(){return "+e+" }");p.push("'"+b+"':"+e)}f=f||{};var p=[],q=[],g=f.valueAccessors,x=f.bindingParams,B="string"===typeof e?b(e):e;a.a.D(B,function(a){l(a.key||a.unknown, +a.value)});q.length&&l("_ko_property_writers","{"+q.join(",")+" }");return p.join(",")},Id:function(a,b){for(var c=0;c<a.length;c++)if(a[c].key==b)return!0;return!1},eb:function(b,c,d,e,f){if(b&&a.O(b))!a.Za(b)||f&&b.v()===e||b(e);else if((b=c.get("_ko_property_writers"))&&b[d])b[d](e)}}}();a.b("expressionRewriting",a.m);a.b("expressionRewriting.bindingRewriteValidators",a.m.Ra);a.b("expressionRewriting.parseObjectLiteral",a.m.ac);a.b("expressionRewriting.preProcessBindings",a.m.vb);a.b("expressionRewriting._twoWayBindings", +a.m.wa);a.b("jsonExpressionRewriting",a.m);a.b("jsonExpressionRewriting.insertPropertyAccessorsIntoJson",a.m.vb);(function(){function b(a){return 8==a.nodeType&&g.test(f?a.text:a.nodeValue)}function c(a){return 8==a.nodeType&&h.test(f?a.text:a.nodeValue)}function d(d,e){for(var f=d,h=1,g=[];f=f.nextSibling;){if(c(f)&&(a.a.g.set(f,k,!0),h--,0===h))return g;g.push(f);b(f)&&h++}if(!e)throw Error("Cannot find closing comment tag to match: "+d.nodeValue);return null}function e(a,b){var c=d(a,b);return c? +0<c.length?c[c.length-1].nextSibling:a.nextSibling:null}var f=w&&"\x3c!--test--\x3e"===w.createComment("test").text,g=f?/^\x3c!--\s*ko(?:\s+([\s\S]+))?\s*--\x3e$/:/^\s*ko(?:\s+([\s\S]+))?\s*$/,h=f?/^\x3c!--\s*\/ko\s*--\x3e$/:/^\s*\/ko\s*$/,m={ul:!0,ol:!0},k="__ko_matchedEndComment__";a.h={ea:{},childNodes:function(a){return b(a)?d(a):a.childNodes},Ea:function(c){if(b(c)){c=a.h.childNodes(c);for(var d=0,e=c.length;d<e;d++)a.removeNode(c[d])}else a.a.Tb(c)},va:function(c,d){if(b(c)){a.h.Ea(c);for(var e= +c.nextSibling,f=0,k=d.length;f<k;f++)e.parentNode.insertBefore(d[f],e)}else a.a.va(c,d)},Vc:function(a,c){var d;b(a)?(d=a.nextSibling,a=a.parentNode):d=a.firstChild;d?c!==d&&a.insertBefore(c,d):a.appendChild(c)},Wb:function(c,d,e){e?(e=e.nextSibling,b(c)&&(c=c.parentNode),e?d!==e&&c.insertBefore(d,e):c.appendChild(d)):a.h.Vc(c,d)},firstChild:function(a){if(b(a))return!a.nextSibling||c(a.nextSibling)?null:a.nextSibling;if(a.firstChild&&c(a.firstChild))throw Error("Found invalid end comment, as the first child of "+ +a);return a.firstChild},nextSibling:function(d){b(d)&&(d=e(d));if(d.nextSibling&&c(d.nextSibling)){var f=d.nextSibling;if(c(f)&&!a.a.g.get(f,k))throw Error("Found end comment without a matching opening comment, as child of "+d);return null}return d.nextSibling},Cd:b,Vd:function(a){return(a=(f?a.text:a.nodeValue).match(g))?a[1]:null},Sc:function(d){if(m[a.a.R(d)]){var f=d.firstChild;if(f){do if(1===f.nodeType){var k;k=f.firstChild;var h=null;if(k){do if(h)h.push(k);else if(b(k)){var g=e(k,!0);g?k= +g:h=[k]}else c(k)&&(h=[k]);while(k=k.nextSibling)}if(k=h)for(h=f.nextSibling,g=0;g<k.length;g++)h?d.insertBefore(k[g],h):d.appendChild(k[g])}while(f=f.nextSibling)}}}}})();a.b("virtualElements",a.h);a.b("virtualElements.allowedBindings",a.h.ea);a.b("virtualElements.emptyNode",a.h.Ea);a.b("virtualElements.insertAfter",a.h.Wb);a.b("virtualElements.prepend",a.h.Vc);a.b("virtualElements.setDomNodeChildren",a.h.va);(function(){a.ga=function(){this.nd={}};a.a.extend(a.ga.prototype,{nodeHasBindings:function(b){switch(b.nodeType){case 1:return null!= +b.getAttribute("data-bind")||a.j.getComponentNameForNode(b);case 8:return a.h.Cd(b);default:return!1}},getBindings:function(b,c){var d=this.getBindingsString(b,c),d=d?this.parseBindingsString(d,c,b):null;return a.j.tc(d,b,c,!1)},getBindingAccessors:function(b,c){var d=this.getBindingsString(b,c),d=d?this.parseBindingsString(d,c,b,{valueAccessors:!0}):null;return a.j.tc(d,b,c,!0)},getBindingsString:function(b){switch(b.nodeType){case 1:return b.getAttribute("data-bind");case 8:return a.h.Vd(b);default:return null}}, +parseBindingsString:function(b,c,d,e){try{var f=this.nd,g=b+(e&&e.valueAccessors||""),h;if(!(h=f[g])){var m,k="with($context){with($data||{}){return{"+a.m.vb(b,e)+"}}}";m=new Function("$context","$element",k);h=f[g]=m}return h(c,d)}catch(l){throw l.message="Unable to parse bindings.\nBindings value: "+b+"\nMessage: "+l.message,l;}}});a.ga.instance=new a.ga})();a.b("bindingProvider",a.ga);(function(){function b(b){var c=(b=a.a.g.get(b,z))&&b.N;c&&(b.N=null,c.Tc())}function c(c,d,e){this.node=c;this.yc= +d;this.kb=[];this.H=!1;d.N||a.a.K.za(c,b);e&&e.N&&(e.N.kb.push(c),this.Kb=e)}function d(a){return function(){return a}}function e(a){return a()}function f(b){return a.a.Ga(a.u.G(b),function(a,c){return function(){return b()[c]}})}function g(b,c,e){return"function"===typeof b?f(b.bind(null,c,e)):a.a.Ga(b,d)}function h(a,b){return f(this.getBindings.bind(this,a,b))}function m(b,c){var d=a.h.firstChild(c);if(d){var e,f=a.ga.instance,l=f.preprocessNode;if(l){for(;e=d;)d=a.h.nextSibling(e),l.call(f,e); +d=a.h.firstChild(c)}for(;e=d;)d=a.h.nextSibling(e),k(b,e)}a.i.ma(c,a.i.H)}function k(b,c){var d=b,e=1===c.nodeType;e&&a.h.Sc(c);if(e||a.ga.instance.nodeHasBindings(c))d=p(c,null,b).bindingContextForDescendants;d&&!u[a.a.R(c)]&&m(d,c)}function l(b){var c=[],d={},e=[];a.a.P(b,function ca(f){if(!d[f]){var k=a.getBindingHandler(f);k&&(k.after&&(e.push(f),a.a.D(k.after,function(c){if(b[c]){if(-1!==a.a.A(e,c))throw Error("Cannot combine the following bindings, because they have a cyclic dependency: "+e.join(", ")); +ca(c)}}),e.length--),c.push({key:f,Mc:k}));d[f]=!0}});return c}function p(b,c,d){var f=a.a.g.Ub(b,z,{}),k=f.hd;if(!c){if(k)throw Error("You cannot apply bindings multiple times to the same element.");f.hd=!0}k||(f.context=d);f.Zb||(f.Zb={});var g;if(c&&"function"!==typeof c)g=c;else{var p=a.ga.instance,q=p.getBindingAccessors||h,m=a.$(function(){if(g=c?c(d,b):q.call(p,b,d)){if(d[t])d[t]();if(d[B])d[B]()}return g},null,{l:b});g&&m.ja()||(m=null)}var x=d,u;if(g){var J=function(){return a.a.Ga(m?m(): +g,e)},r=m?function(a){return function(){return e(m()[a])}}:function(a){return g[a]};J.get=function(a){return g[a]&&e(r(a))};J.has=function(a){return a in g};a.i.H in g&&a.i.subscribe(b,a.i.H,function(){var c=(0,g[a.i.H])();if(c){var d=a.h.childNodes(b);d.length&&c(d,a.Ec(d[0]))}});a.i.pa in g&&(x=a.i.Cb(b,d),a.i.subscribe(b,a.i.pa,function(){var c=(0,g[a.i.pa])();c&&a.h.firstChild(b)&&c(b)}));f=l(g);a.a.D(f,function(c){var d=c.Mc.init,e=c.Mc.update,f=c.key;if(8===b.nodeType&&!a.h.ea[f])throw Error("The binding '"+ +f+"' cannot be used with virtual elements");try{"function"==typeof d&&a.u.G(function(){var a=d(b,r(f),J,x.$data,x);if(a&&a.controlsDescendantBindings){if(u!==n)throw Error("Multiple bindings ("+u+" and "+f+") are trying to control descendant bindings of the same element. You cannot use these bindings together on the same element.");u=f}}),"function"==typeof e&&a.$(function(){e(b,r(f),J,x.$data,x)},null,{l:b})}catch(k){throw k.message='Unable to process binding "'+f+": "+g[f]+'"\nMessage: '+k.message, +k;}})}f=u===n;return{shouldBindDescendants:f,bindingContextForDescendants:f&&x}}function q(b,c){return b&&b instanceof a.fa?b:new a.fa(b,n,n,c)}var t=a.a.Da("_subscribable"),x=a.a.Da("_ancestorBindingInfo"),B=a.a.Da("_dataDependency");a.c={};var u={script:!0,textarea:!0,template:!0};a.getBindingHandler=function(b){return a.c[b]};var J={};a.fa=function(b,c,d,e,f){function k(){var b=p?h():h,f=a.a.f(b);c?(a.a.extend(l,c),x in c&&(l[x]=c[x])):(l.$parents=[],l.$root=f,l.ko=a);l[t]=q;g?f=l.$data:(l.$rawData= +b,l.$data=f);d&&(l[d]=f);e&&e(l,c,f);if(c&&c[t]&&!a.S.o().Vb(c[t]))c[t]();m&&(l[B]=m);return l.$data}var l=this,g=b===J,h=g?n:b,p="function"==typeof h&&!a.O(h),q,m=f&&f.dataDependency;f&&f.exportDependencies?k():(q=a.xb(k),q.v(),q.ja()?q.equalityComparer=null:l[t]=n)};a.fa.prototype.createChildContext=function(b,c,d,e){!e&&c&&"object"==typeof c&&(e=c,c=e.as,d=e.extend);if(c&&e&&e.noChildContext){var f="function"==typeof b&&!a.O(b);return new a.fa(J,this,null,function(a){d&&d(a);a[c]=f?b():b},e)}return new a.fa(b, +this,c,function(a,b){a.$parentContext=b;a.$parent=b.$data;a.$parents=(b.$parents||[]).slice(0);a.$parents.unshift(a.$parent);d&&d(a)},e)};a.fa.prototype.extend=function(b,c){return new a.fa(J,this,null,function(c){a.a.extend(c,"function"==typeof b?b(c):b)},c)};var z=a.a.g.Z();c.prototype.Tc=function(){this.Kb&&this.Kb.N&&this.Kb.N.sd(this.node)};c.prototype.sd=function(b){a.a.Pa(this.kb,b);!this.kb.length&&this.H&&this.Cc()};c.prototype.Cc=function(){this.H=!0;this.yc.N&&!this.kb.length&&(this.yc.N= +null,a.a.K.yb(this.node,b),a.i.ma(this.node,a.i.pa),this.Tc())};a.i={H:"childrenComplete",pa:"descendantsComplete",subscribe:function(b,c,d,e,f){var k=a.a.g.Ub(b,z,{});k.Fa||(k.Fa=new a.T);f&&f.notifyImmediately&&k.Zb[c]&&a.u.G(d,e,[b]);return k.Fa.subscribe(d,e,c)},ma:function(b,c){var d=a.a.g.get(b,z);if(d&&(d.Zb[c]=!0,d.Fa&&d.Fa.notifySubscribers(b,c),c==a.i.H))if(d.N)d.N.Cc();else if(d.N===n&&d.Fa&&d.Fa.Wa(a.i.pa))throw Error("descendantsComplete event not supported for bindings on this node"); +},Cb:function(b,d){var e=a.a.g.Ub(b,z,{});e.N||(e.N=new c(b,e,d[x]));return d[x]==e?d:d.extend(function(a){a[x]=e})}};a.Td=function(b){return(b=a.a.g.get(b,z))&&b.context};a.ib=function(b,c,d){1===b.nodeType&&a.h.Sc(b);return p(b,c,q(d))};a.ld=function(b,c,d){d=q(d);return a.ib(b,g(c,d,b),d)};a.Oa=function(a,b){1!==b.nodeType&&8!==b.nodeType||m(q(a),b)};a.vc=function(a,b,c){!v&&A.jQuery&&(v=A.jQuery);if(2>arguments.length){if(b=w.body,!b)throw Error("ko.applyBindings: could not find document.body; has the document been loaded?"); +}else if(!b||1!==b.nodeType&&8!==b.nodeType)throw Error("ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node");k(q(a,c),b)};a.Dc=function(b){return!b||1!==b.nodeType&&8!==b.nodeType?n:a.Td(b)};a.Ec=function(b){return(b=a.Dc(b))?b.$data:n};a.b("bindingHandlers",a.c);a.b("bindingEvent",a.i);a.b("bindingEvent.subscribe",a.i.subscribe);a.b("bindingEvent.startPossiblyAsyncContentBinding",a.i.Cb);a.b("applyBindings",a.vc);a.b("applyBindingsToDescendants",a.Oa); +a.b("applyBindingAccessorsToNode",a.ib);a.b("applyBindingsToNode",a.ld);a.b("contextFor",a.Dc);a.b("dataFor",a.Ec)})();(function(b){function c(c,e){var k=Object.prototype.hasOwnProperty.call(f,c)?f[c]:b,l;k?k.subscribe(e):(k=f[c]=new a.T,k.subscribe(e),d(c,function(b,d){var e=!(!d||!d.synchronous);g[c]={definition:b,Gd:e};delete f[c];l||e?k.notifySubscribers(b):a.na.zb(function(){k.notifySubscribers(b)})}),l=!0)}function d(a,b){e("getConfig",[a],function(c){c?e("loadComponent",[a,c],function(a){b(a, +c)}):b(null,null)})}function e(c,d,f,l){l||(l=a.j.loaders.slice(0));var g=l.shift();if(g){var q=g[c];if(q){var t=!1;if(q.apply(g,d.concat(function(a){t?f(null):null!==a?f(a):e(c,d,f,l)}))!==b&&(t=!0,!g.suppressLoaderExceptions))throw Error("Component loaders must supply values by invoking the callback, not by returning values synchronously.");}else e(c,d,f,l)}else f(null)}var f={},g={};a.j={get:function(d,e){var f=Object.prototype.hasOwnProperty.call(g,d)?g[d]:b;f?f.Gd?a.u.G(function(){e(f.definition)}): +a.na.zb(function(){e(f.definition)}):c(d,e)},Bc:function(a){delete g[a]},oc:e};a.j.loaders=[];a.b("components",a.j);a.b("components.get",a.j.get);a.b("components.clearCachedDefinition",a.j.Bc)})();(function(){function b(b,c,d,e){function g(){0===--B&&e(h)}var h={},B=2,u=d.template;d=d.viewModel;u?f(c,u,function(c){a.j.oc("loadTemplate",[b,c],function(a){h.template=a;g()})}):g();d?f(c,d,function(c){a.j.oc("loadViewModel",[b,c],function(a){h[m]=a;g()})}):g()}function c(a,b,d){if("function"===typeof b)d(function(a){return new b(a)}); +else if("function"===typeof b[m])d(b[m]);else if("instance"in b){var e=b.instance;d(function(){return e})}else"viewModel"in b?c(a,b.viewModel,d):a("Unknown viewModel value: "+b)}function d(b){switch(a.a.R(b)){case "script":return a.a.ua(b.text);case "textarea":return a.a.ua(b.value);case "template":if(e(b.content))return a.a.Ca(b.content.childNodes)}return a.a.Ca(b.childNodes)}function e(a){return A.DocumentFragment?a instanceof DocumentFragment:a&&11===a.nodeType}function f(a,b,c){"string"===typeof b.require? +T||A.require?(T||A.require)([b.require],function(a){a&&"object"===typeof a&&a.Xd&&a["default"]&&(a=a["default"]);c(a)}):a("Uses require, but no AMD loader is present"):c(b)}function g(a){return function(b){throw Error("Component '"+a+"': "+b);}}var h={};a.j.register=function(b,c){if(!c)throw Error("Invalid configuration for "+b);if(a.j.tb(b))throw Error("Component "+b+" is already registered");h[b]=c};a.j.tb=function(a){return Object.prototype.hasOwnProperty.call(h,a)};a.j.unregister=function(b){delete h[b]; +a.j.Bc(b)};a.j.Fc={getConfig:function(b,c){c(a.j.tb(b)?h[b]:null)},loadComponent:function(a,c,d){var e=g(a);f(e,c,function(c){b(a,e,c,d)})},loadTemplate:function(b,c,f){b=g(b);if("string"===typeof c)f(a.a.ua(c));else if(c instanceof Array)f(c);else if(e(c))f(a.a.la(c.childNodes));else if(c.element)if(c=c.element,A.HTMLElement?c instanceof HTMLElement:c&&c.tagName&&1===c.nodeType)f(d(c));else if("string"===typeof c){var h=w.getElementById(c);h?f(d(h)):b("Cannot find element with ID "+c)}else b("Unknown element type: "+ +c);else b("Unknown template value: "+c)},loadViewModel:function(a,b,d){c(g(a),b,d)}};var m="createViewModel";a.b("components.register",a.j.register);a.b("components.isRegistered",a.j.tb);a.b("components.unregister",a.j.unregister);a.b("components.defaultLoader",a.j.Fc);a.j.loaders.push(a.j.Fc);a.j.dd=h})();(function(){function b(b,e){var f=b.getAttribute("params");if(f){var f=c.parseBindingsString(f,e,b,{valueAccessors:!0,bindingParams:!0}),f=a.a.Ga(f,function(c){return a.o(c,null,{l:b})}),g=a.a.Ga(f, +function(c){var e=c.v();return c.ja()?a.o({read:function(){return a.a.f(c())},write:a.Za(e)&&function(a){c()(a)},l:b}):e});Object.prototype.hasOwnProperty.call(g,"$raw")||(g.$raw=f);return g}return{$raw:{}}}a.j.getComponentNameForNode=function(b){var c=a.a.R(b);if(a.j.tb(c)&&(-1!=c.indexOf("-")||"[object HTMLUnknownElement]"==""+b||8>=a.a.W&&b.tagName===c))return c};a.j.tc=function(c,e,f,g){if(1===e.nodeType){var h=a.j.getComponentNameForNode(e);if(h){c=c||{};if(c.component)throw Error('Cannot use the "component" binding on a custom element matching a component'); +var m={name:h,params:b(e,f)};c.component=g?function(){return m}:m}}return c};var c=new a.ga;9>a.a.W&&(a.j.register=function(a){return function(b){return a.apply(this,arguments)}}(a.j.register),w.createDocumentFragment=function(b){return function(){var c=b(),f=a.j.dd,g;for(g in f);return c}}(w.createDocumentFragment))})();(function(){function b(b,c,d){c=c.template;if(!c)throw Error("Component '"+b+"' has no template");b=a.a.Ca(c);a.h.va(d,b)}function c(a,b,c){var d=a.createViewModel;return d?d.call(a, +b,c):b}var d=0;a.c.component={init:function(e,f,g,h,m){function k(){var a=l&&l.dispose;"function"===typeof a&&a.call(l);q&&q.s();p=l=q=null}var l,p,q,t=a.a.la(a.h.childNodes(e));a.h.Ea(e);a.a.K.za(e,k);a.o(function(){var g=a.a.f(f()),h,u;"string"===typeof g?h=g:(h=a.a.f(g.name),u=a.a.f(g.params));if(!h)throw Error("No component name specified");var n=a.i.Cb(e,m),z=p=++d;a.j.get(h,function(d){if(p===z){k();if(!d)throw Error("Unknown component '"+h+"'");b(h,d,e);var f=c(d,u,{element:e,templateNodes:t}); +d=n.createChildContext(f,{extend:function(a){a.$component=f;a.$componentTemplateNodes=t}});f&&f.koDescendantsComplete&&(q=a.i.subscribe(e,a.i.pa,f.koDescendantsComplete,f));l=f;a.Oa(d,e)}})},null,{l:e});return{controlsDescendantBindings:!0}}};a.h.ea.component=!0})();var V={"class":"className","for":"htmlFor"};a.c.attr={update:function(b,c){var d=a.a.f(c())||{};a.a.P(d,function(c,d){d=a.a.f(d);var g=c.indexOf(":"),g="lookupNamespaceURI"in b&&0<g&&b.lookupNamespaceURI(c.substr(0,g)),h=!1===d||null=== +d||d===n;h?g?b.removeAttributeNS(g,c):b.removeAttribute(c):d=d.toString();8>=a.a.W&&c in V?(c=V[c],h?b.removeAttribute(c):b[c]=d):h||(g?b.setAttributeNS(g,c,d):b.setAttribute(c,d));"name"===c&&a.a.Yc(b,h?"":d)})}};(function(){a.c.checked={after:["value","attr"],init:function(b,c,d){function e(){var e=b.checked,f=g();if(!a.S.Ya()&&(e||!m&&!a.S.qa())){var k=a.u.G(c);if(l){var q=p?k.v():k,z=t;t=f;z!==f?e&&(a.a.Na(q,f,!0),a.a.Na(q,z,!1)):a.a.Na(q,f,e);p&&a.Za(k)&&k(q)}else h&&(f===n?f=e:e||(f=n)),a.m.eb(k, +d,"checked",f,!0)}}function f(){var d=a.a.f(c()),e=g();l?(b.checked=0<=a.a.A(d,e),t=e):b.checked=h&&e===n?!!d:g()===d}var g=a.xb(function(){if(d.has("checkedValue"))return a.a.f(d.get("checkedValue"));if(q)return d.has("value")?a.a.f(d.get("value")):b.value}),h="checkbox"==b.type,m="radio"==b.type;if(h||m){var k=c(),l=h&&a.a.f(k)instanceof Array,p=!(l&&k.push&&k.splice),q=m||l,t=l?g():n;m&&!b.name&&a.c.uniqueName.init(b,function(){return!0});a.o(e,null,{l:b});a.a.B(b,"click",e);a.o(f,null,{l:b}); +k=n}}};a.m.wa.checked=!0;a.c.checkedValue={update:function(b,c){b.value=a.a.f(c())}}})();a.c["class"]={update:function(b,c){var d=a.a.Db(a.a.f(c()));a.a.Eb(b,b.__ko__cssValue,!1);b.__ko__cssValue=d;a.a.Eb(b,d,!0)}};a.c.css={update:function(b,c){var d=a.a.f(c());null!==d&&"object"==typeof d?a.a.P(d,function(c,d){d=a.a.f(d);a.a.Eb(b,c,d)}):a.c["class"].update(b,c)}};a.c.enable={update:function(b,c){var d=a.a.f(c());d&&b.disabled?b.removeAttribute("disabled"):d||b.disabled||(b.disabled=!0)}};a.c.disable= +{update:function(b,c){a.c.enable.update(b,function(){return!a.a.f(c())})}};a.c.event={init:function(b,c,d,e,f){var g=c()||{};a.a.P(g,function(g){"string"==typeof g&&a.a.B(b,g,function(b){var k,l=c()[g];if(l){try{var p=a.a.la(arguments);e=f.$data;p.unshift(e);k=l.apply(e,p)}finally{!0!==k&&(b.preventDefault?b.preventDefault():b.returnValue=!1)}!1===d.get(g+"Bubble")&&(b.cancelBubble=!0,b.stopPropagation&&b.stopPropagation())}})})}};a.c.foreach={Rc:function(b){return function(){var c=b(),d=a.a.bc(c); +if(!d||"number"==typeof d.length)return{foreach:c,templateEngine:a.ba.Ma};a.a.f(c);return{foreach:d.data,as:d.as,noChildContext:d.noChildContext,includeDestroyed:d.includeDestroyed,afterAdd:d.afterAdd,beforeRemove:d.beforeRemove,afterRender:d.afterRender,beforeMove:d.beforeMove,afterMove:d.afterMove,templateEngine:a.ba.Ma}}},init:function(b,c){return a.c.template.init(b,a.c.foreach.Rc(c))},update:function(b,c,d,e,f){return a.c.template.update(b,a.c.foreach.Rc(c),d,e,f)}};a.m.Ra.foreach=!1;a.h.ea.foreach= +!0;a.c.hasfocus={init:function(b,c,d){function e(e){b.__ko_hasfocusUpdating=!0;var f=b.ownerDocument;if("activeElement"in f){var g;try{g=f.activeElement}catch(l){g=f.body}e=g===b}f=c();a.m.eb(f,d,"hasfocus",e,!0);b.__ko_hasfocusLastValue=e;b.__ko_hasfocusUpdating=!1}var f=e.bind(null,!0),g=e.bind(null,!1);a.a.B(b,"focus",f);a.a.B(b,"focusin",f);a.a.B(b,"blur",g);a.a.B(b,"focusout",g);b.__ko_hasfocusLastValue=!1},update:function(b,c){var d=!!a.a.f(c());b.__ko_hasfocusUpdating||b.__ko_hasfocusLastValue=== +d||(d?b.focus():b.blur(),!d&&b.__ko_hasfocusLastValue&&b.ownerDocument.body.focus(),a.u.G(a.a.Fb,null,[b,d?"focusin":"focusout"]))}};a.m.wa.hasfocus=!0;a.c.hasFocus=a.c.hasfocus;a.m.wa.hasFocus="hasfocus";a.c.html={init:function(){return{controlsDescendantBindings:!0}},update:function(b,c){a.a.fc(b,c())}};(function(){function b(b,d,e){a.c[b]={init:function(b,c,h,m,k){var l,p,q={},t,x,n;if(d){m=h.get("as");var u=h.get("noChildContext");n=!(m&&u);q={as:m,noChildContext:u,exportDependencies:n}}x=(t= +"render"==h.get("completeOn"))||h.has(a.i.pa);a.o(function(){var h=a.a.f(c()),m=!e!==!h,u=!p,r;if(n||m!==l){x&&(k=a.i.Cb(b,k));if(m){if(!d||n)q.dataDependency=a.S.o();r=d?k.createChildContext("function"==typeof h?h:c,q):a.S.qa()?k.extend(null,q):k}u&&a.S.qa()&&(p=a.a.Ca(a.h.childNodes(b),!0));m?(u||a.h.va(b,a.a.Ca(p)),a.Oa(r,b)):(a.h.Ea(b),t||a.i.ma(b,a.i.H));l=m}},null,{l:b});return{controlsDescendantBindings:!0}}};a.m.Ra[b]=!1;a.h.ea[b]=!0}b("if");b("ifnot",!1,!0);b("with",!0)})();a.c.let={init:function(b, +c,d,e,f){c=f.extend(c);a.Oa(c,b);return{controlsDescendantBindings:!0}}};a.h.ea.let=!0;var Q={};a.c.options={init:function(b){if("select"!==a.a.R(b))throw Error("options binding applies only to SELECT elements");for(;0<b.length;)b.remove(0);return{controlsDescendantBindings:!0}},update:function(b,c,d){function e(){return a.a.jb(b.options,function(a){return a.selected})}function f(a,b,c){var d=typeof b;return"function"==d?b(a):"string"==d?a[b]:c}function g(c,d){if(x&&l)a.i.ma(b,a.i.H);else if(t.length){var e= +0<=a.a.A(t,a.w.M(d[0]));a.a.Zc(d[0],e);x&&!e&&a.u.G(a.a.Fb,null,[b,"change"])}}var h=b.multiple,m=0!=b.length&&h?b.scrollTop:null,k=a.a.f(c()),l=d.get("valueAllowUnset")&&d.has("value"),p=d.get("optionsIncludeDestroyed");c={};var q,t=[];l||(h?t=a.a.Mb(e(),a.w.M):0<=b.selectedIndex&&t.push(a.w.M(b.options[b.selectedIndex])));k&&("undefined"==typeof k.length&&(k=[k]),q=a.a.jb(k,function(b){return p||b===n||null===b||!a.a.f(b._destroy)}),d.has("optionsCaption")&&(k=a.a.f(d.get("optionsCaption")),null!== +k&&k!==n&&q.unshift(Q)));var x=!1;c.beforeRemove=function(a){b.removeChild(a)};k=g;d.has("optionsAfterRender")&&"function"==typeof d.get("optionsAfterRender")&&(k=function(b,c){g(0,c);a.u.G(d.get("optionsAfterRender"),null,[c[0],b!==Q?b:n])});a.a.ec(b,q,function(c,e,g){g.length&&(t=!l&&g[0].selected?[a.w.M(g[0])]:[],x=!0);e=b.ownerDocument.createElement("option");c===Q?(a.a.Bb(e,d.get("optionsCaption")),a.w.cb(e,n)):(g=f(c,d.get("optionsValue"),c),a.w.cb(e,a.a.f(g)),c=f(c,d.get("optionsText"),g), +a.a.Bb(e,c));return[e]},c,k);if(!l){var B;h?B=t.length&&e().length<t.length:B=t.length&&0<=b.selectedIndex?a.w.M(b.options[b.selectedIndex])!==t[0]:t.length||0<=b.selectedIndex;B&&a.u.G(a.a.Fb,null,[b,"change"])}(l||a.S.Ya())&&a.i.ma(b,a.i.H);a.a.wd(b);m&&20<Math.abs(m-b.scrollTop)&&(b.scrollTop=m)}};a.c.options.$b=a.a.g.Z();a.c.selectedOptions={init:function(b,c,d){function e(){var e=c(),f=[];a.a.D(b.getElementsByTagName("option"),function(b){b.selected&&f.push(a.w.M(b))});a.m.eb(e,d,"selectedOptions", +f)}function f(){var d=a.a.f(c()),e=b.scrollTop;d&&"number"==typeof d.length&&a.a.D(b.getElementsByTagName("option"),function(b){var c=0<=a.a.A(d,a.w.M(b));b.selected!=c&&a.a.Zc(b,c)});b.scrollTop=e}if("select"!=a.a.R(b))throw Error("selectedOptions binding applies only to SELECT elements");var g;a.i.subscribe(b,a.i.H,function(){g?e():(a.a.B(b,"change",e),g=a.o(f,null,{l:b}))},null,{notifyImmediately:!0})},update:function(){}};a.m.wa.selectedOptions=!0;a.c.style={update:function(b,c){var d=a.a.f(c()|| +{});a.a.P(d,function(c,d){d=a.a.f(d);if(null===d||d===n||!1===d)d="";if(v)v(b).css(c,d);else if(/^--/.test(c))b.style.setProperty(c,d);else{c=c.replace(/-(\w)/g,function(a,b){return b.toUpperCase()});var g=b.style[c];b.style[c]=d;d===g||b.style[c]!=g||isNaN(d)||(b.style[c]=d+"px")}})}};a.c.submit={init:function(b,c,d,e,f){if("function"!=typeof c())throw Error("The value for a submit binding must be a function");a.a.B(b,"submit",function(a){var d,e=c();try{d=e.call(f.$data,b)}finally{!0!==d&&(a.preventDefault? +a.preventDefault():a.returnValue=!1)}})}};a.c.text={init:function(){return{controlsDescendantBindings:!0}},update:function(b,c){a.a.Bb(b,c())}};a.h.ea.text=!0;(function(){if(A&&A.navigator){var b=function(a){if(a)return parseFloat(a[1])},c=A.navigator.userAgent,d,e,f,g,h;(d=A.opera&&A.opera.version&&parseInt(A.opera.version()))||(h=b(c.match(/Edge\/([^ ]+)$/)))||b(c.match(/Chrome\/([^ ]+)/))||(e=b(c.match(/Version\/([^ ]+) Safari/)))||(f=b(c.match(/Firefox\/([^ ]+)/)))||(g=a.a.W||b(c.match(/MSIE ([^ ]+)/)))|| +(g=b(c.match(/rv:([^ )]+)/)))}if(8<=g&&10>g)var m=a.a.g.Z(),k=a.a.g.Z(),l=function(b){var c=this.activeElement;(c=c&&a.a.g.get(c,k))&&c(b)},p=function(b,c){var d=b.ownerDocument;a.a.g.get(d,m)||(a.a.g.set(d,m,!0),a.a.B(d,"selectionchange",l));a.a.g.set(b,k,c)};a.c.textInput={init:function(b,c,k){function l(c,d){a.a.B(b,c,d)}function m(){var d=a.a.f(c());if(null===d||d===n)d="";L!==n&&d===L?a.a.setTimeout(m,4):b.value!==d&&(y=!0,b.value=d,y=!1,v=b.value)}function r(){w||(L=b.value,w=a.a.setTimeout(z, +4))}function z(){clearTimeout(w);L=w=n;var d=b.value;v!==d&&(v=d,a.m.eb(c(),k,"textInput",d))}var v=b.value,w,L,A=9==a.a.W?r:z,y=!1;g&&l("keypress",z);11>g&&l("propertychange",function(a){y||"value"!==a.propertyName||A(a)});8==g&&(l("keyup",z),l("keydown",z));p&&(p(b,A),l("dragend",r));(!g||9<=g)&&l("input",A);5>e&&"textarea"===a.a.R(b)?(l("keydown",r),l("paste",r),l("cut",r)):11>d?l("keydown",r):4>f?(l("DOMAutoComplete",z),l("dragdrop",z),l("drop",z)):h&&"number"===b.type&&l("keydown",r);l("change", +z);l("blur",z);a.o(m,null,{l:b})}};a.m.wa.textInput=!0;a.c.textinput={preprocess:function(a,b,c){c("textInput",a)}}})();a.c.uniqueName={init:function(b,c){if(c()){var d="ko_unique_"+ ++a.c.uniqueName.rd;a.a.Yc(b,d)}}};a.c.uniqueName.rd=0;a.c.using={init:function(b,c,d,e,f){var g;d.has("as")&&(g={as:d.get("as"),noChildContext:d.get("noChildContext")});c=f.createChildContext(c,g);a.Oa(c,b);return{controlsDescendantBindings:!0}}};a.h.ea.using=!0;a.c.value={init:function(b,c,d){var e=a.a.R(b),f="input"== +e;if(!f||"checkbox"!=b.type&&"radio"!=b.type){var g=[],h=d.get("valueUpdate"),m=!1,k=null;h&&("string"==typeof h?g=[h]:g=a.a.wc(h),a.a.Pa(g,"change"));var l=function(){k=null;m=!1;var e=c(),f=a.w.M(b);a.m.eb(e,d,"value",f)};!a.a.W||!f||"text"!=b.type||"off"==b.autocomplete||b.form&&"off"==b.form.autocomplete||-1!=a.a.A(g,"propertychange")||(a.a.B(b,"propertychange",function(){m=!0}),a.a.B(b,"focus",function(){m=!1}),a.a.B(b,"blur",function(){m&&l()}));a.a.D(g,function(c){var d=l;a.a.Ud(c,"after")&& +(d=function(){k=a.w.M(b);a.a.setTimeout(l,0)},c=c.substring(5));a.a.B(b,c,d)});var p;p=f&&"file"==b.type?function(){var d=a.a.f(c());null===d||d===n||""===d?b.value="":a.u.G(l)}:function(){var f=a.a.f(c()),g=a.w.M(b);if(null!==k&&f===k)a.a.setTimeout(p,0);else if(f!==g||g===n)"select"===e?(g=d.get("valueAllowUnset"),a.w.cb(b,f,g),g||f===a.w.M(b)||a.u.G(l)):a.w.cb(b,f)};if("select"===e){var q;a.i.subscribe(b,a.i.H,function(){q?d.get("valueAllowUnset")?p():l():(a.a.B(b,"change",l),q=a.o(p,null,{l:b}))}, +null,{notifyImmediately:!0})}else a.a.B(b,"change",l),a.o(p,null,{l:b})}else a.ib(b,{checkedValue:c})},update:function(){}};a.m.wa.value=!0;a.c.visible={update:function(b,c){var d=a.a.f(c()),e="none"!=b.style.display;d&&!e?b.style.display="":!d&&e&&(b.style.display="none")}};a.c.hidden={update:function(b,c){a.c.visible.update(b,function(){return!a.a.f(c())})}};(function(b){a.c[b]={init:function(c,d,e,f,g){return a.c.event.init.call(this,c,function(){var a={};a[b]=d();return a},e,f,g)}}})("click"); +a.ca=function(){};a.ca.prototype.renderTemplateSource=function(){throw Error("Override renderTemplateSource");};a.ca.prototype.createJavaScriptEvaluatorBlock=function(){throw Error("Override createJavaScriptEvaluatorBlock");};a.ca.prototype.makeTemplateSource=function(b,c){if("string"==typeof b){c=c||w;var d=c.getElementById(b);if(!d)throw Error("Cannot find template with ID "+b);return new a.C.F(d)}if(1==b.nodeType||8==b.nodeType)return new a.C.ia(b);throw Error("Unknown template type: "+b);};a.ca.prototype.renderTemplate= +function(a,c,d,e){a=this.makeTemplateSource(a,e);return this.renderTemplateSource(a,c,d,e)};a.ca.prototype.isTemplateRewritten=function(a,c){return!1===this.allowTemplateRewriting?!0:this.makeTemplateSource(a,c).data("isRewritten")};a.ca.prototype.rewriteTemplate=function(a,c,d){a=this.makeTemplateSource(a,d);c=c(a.text());a.text(c);a.data("isRewritten",!0)};a.b("templateEngine",a.ca);a.kc=function(){function b(b,c,d,h){b=a.m.ac(b);for(var m=a.m.Ra,k=0;k<b.length;k++){var l=b[k].key;if(Object.prototype.hasOwnProperty.call(m, +l)){var p=m[l];if("function"===typeof p){if(l=p(b[k].value))throw Error(l);}else if(!p)throw Error("This template engine does not support the '"+l+"' binding within its templates");}}d="ko.__tr_ambtns(function($context,$element){return(function(){return{ "+a.m.vb(b,{valueAccessors:!0})+" } })()},'"+d.toLowerCase()+"')";return h.createJavaScriptEvaluatorBlock(d)+c}var c=/(<([a-z]+\d*)(?:\s+(?!data-bind\s*=\s*)[a-z0-9\-]+(?:=(?:\"[^\"]*\"|\'[^\']*\'|[^>]*))?)*\s+)data-bind\s*=\s*(["'])([\s\S]*?)\3/gi, +d=/\x3c!--\s*ko\b\s*([\s\S]*?)\s*--\x3e/g;return{xd:function(b,c,d){c.isTemplateRewritten(b,d)||c.rewriteTemplate(b,function(b){return a.kc.Ld(b,c)},d)},Ld:function(a,f){return a.replace(c,function(a,c,d,e,l){return b(l,c,d,f)}).replace(d,function(a,c){return b(c,"\x3c!-- ko --\x3e","#comment",f)})},md:function(b,c){return a.aa.Xb(function(d,h){var m=d.nextSibling;m&&m.nodeName.toLowerCase()===c&&a.ib(m,b,h)})}}}();a.b("__tr_ambtns",a.kc.md);(function(){a.C={};a.C.F=function(b){if(this.F=b){var c= +a.a.R(b);this.ab="script"===c?1:"textarea"===c?2:"template"==c&&b.content&&11===b.content.nodeType?3:4}};a.C.F.prototype.text=function(){var b=1===this.ab?"text":2===this.ab?"value":"innerHTML";if(0==arguments.length)return this.F[b];var c=arguments[0];"innerHTML"===b?a.a.fc(this.F,c):this.F[b]=c};var b=a.a.g.Z()+"_";a.C.F.prototype.data=function(c){if(1===arguments.length)return a.a.g.get(this.F,b+c);a.a.g.set(this.F,b+c,arguments[1])};var c=a.a.g.Z();a.C.F.prototype.nodes=function(){var b=this.F; +if(0==arguments.length){var e=a.a.g.get(b,c)||{},f=e.lb||(3===this.ab?b.content:4===this.ab?b:n);if(!f||e.jd){var g=this.text();g&&g!==e.bb&&(f=a.a.Md(g,b.ownerDocument),a.a.g.set(b,c,{lb:f,bb:g,jd:!0}))}return f}e=arguments[0];this.ab!==n&&this.text("");a.a.g.set(b,c,{lb:e})};a.C.ia=function(a){this.F=a};a.C.ia.prototype=new a.C.F;a.C.ia.prototype.constructor=a.C.ia;a.C.ia.prototype.text=function(){if(0==arguments.length){var b=a.a.g.get(this.F,c)||{};b.bb===n&&b.lb&&(b.bb=b.lb.innerHTML);return b.bb}a.a.g.set(this.F, +c,{bb:arguments[0]})};a.b("templateSources",a.C);a.b("templateSources.domElement",a.C.F);a.b("templateSources.anonymousTemplate",a.C.ia)})();(function(){function b(b,c,d){var e;for(c=a.h.nextSibling(c);b&&(e=b)!==c;)b=a.h.nextSibling(e),d(e,b)}function c(c,d){if(c.length){var e=c[0],f=c[c.length-1],g=e.parentNode,h=a.ga.instance,m=h.preprocessNode;if(m){b(e,f,function(a,b){var c=a.previousSibling,d=m.call(h,a);d&&(a===e&&(e=d[0]||b),a===f&&(f=d[d.length-1]||c))});c.length=0;if(!e)return;e===f?c.push(e): +(c.push(e,f),a.a.Ua(c,g))}b(e,f,function(b){1!==b.nodeType&&8!==b.nodeType||a.vc(d,b)});b(e,f,function(b){1!==b.nodeType&&8!==b.nodeType||a.aa.cd(b,[d])});a.a.Ua(c,g)}}function d(a){return a.nodeType?a:0<a.length?a[0]:null}function e(b,e,f,h,m){m=m||{};var n=(b&&d(b)||f||{}).ownerDocument,B=m.templateEngine||g;a.kc.xd(f,B,n);f=B.renderTemplate(f,h,m,n);if("number"!=typeof f.length||0<f.length&&"number"!=typeof f[0].nodeType)throw Error("Template engine must return an array of DOM nodes");n=!1;switch(e){case "replaceChildren":a.h.va(b, +f);n=!0;break;case "replaceNode":a.a.Xc(b,f);n=!0;break;case "ignoreTargetNode":break;default:throw Error("Unknown renderMode: "+e);}n&&(c(f,h),m.afterRender&&a.u.G(m.afterRender,null,[f,h[m.as||"$data"]]),"replaceChildren"==e&&a.i.ma(b,a.i.H));return f}function f(b,c,d){return a.O(b)?b():"function"===typeof b?b(c,d):b}var g;a.gc=function(b){if(b!=n&&!(b instanceof a.ca))throw Error("templateEngine must inherit from ko.templateEngine");g=b};a.dc=function(b,c,h,m,t){h=h||{};if((h.templateEngine||g)== +n)throw Error("Set a template engine before calling renderTemplate");t=t||"replaceChildren";if(m){var x=d(m);return a.$(function(){var g=c&&c instanceof a.fa?c:new a.fa(c,null,null,null,{exportDependencies:!0}),n=f(b,g.$data,g),g=e(m,t,n,g,h);"replaceNode"==t&&(m=g,x=d(m))},null,{Sa:function(){return!x||!a.a.Sb(x)},l:x&&"replaceNode"==t?x.parentNode:x})}return a.aa.Xb(function(d){a.dc(b,c,h,d,"replaceNode")})};a.Qd=function(b,d,g,h,m){function x(b,c){a.u.G(a.a.ec,null,[h,b,u,g,r,c]);a.i.ma(h,a.i.H)} +function r(a,b){c(b,v);g.afterRender&&g.afterRender(b,a);v=null}function u(a,c){v=m.createChildContext(a,{as:z,noChildContext:g.noChildContext,extend:function(a){a.$index=c;z&&(a[z+"Index"]=c)}});var d=f(b,a,v);return e(h,"ignoreTargetNode",d,v,g)}var v,z=g.as,w=!1===g.includeDestroyed||a.options.foreachHidesDestroyed&&!g.includeDestroyed;if(w||g.beforeRemove||!a.Pc(d))return a.$(function(){var b=a.a.f(d)||[];"undefined"==typeof b.length&&(b=[b]);w&&(b=a.a.jb(b,function(b){return b===n||null===b|| +!a.a.f(b._destroy)}));x(b)},null,{l:h});x(d.v());var A=d.subscribe(function(a){x(d(),a)},null,"arrayChange");A.l(h);return A};var h=a.a.g.Z(),m=a.a.g.Z();a.c.template={init:function(b,c){var d=a.a.f(c());if("string"==typeof d||"name"in d)a.h.Ea(b);else if("nodes"in d){d=d.nodes||[];if(a.O(d))throw Error('The "nodes" option must be a plain, non-observable array.');var e=d[0]&&d[0].parentNode;e&&a.a.g.get(e,m)||(e=a.a.Yb(d),a.a.g.set(e,m,!0));(new a.C.ia(b)).nodes(e)}else if(d=a.h.childNodes(b),0<d.length)e= +a.a.Yb(d),(new a.C.ia(b)).nodes(e);else throw Error("Anonymous template defined, but no template content was provided");return{controlsDescendantBindings:!0}},update:function(b,c,d,e,f){var g=c();c=a.a.f(g);d=!0;e=null;"string"==typeof c?c={}:(g="name"in c?c.name:b,"if"in c&&(d=a.a.f(c["if"])),d&&"ifnot"in c&&(d=!a.a.f(c.ifnot)),d&&!g&&(d=!1));"foreach"in c?e=a.Qd(g,d&&c.foreach||[],c,b,f):d?(d=f,"data"in c&&(d=f.createChildContext(c.data,{as:c.as,noChildContext:c.noChildContext,exportDependencies:!0})), +e=a.dc(g,d,c,b)):a.h.Ea(b);f=e;(c=a.a.g.get(b,h))&&"function"==typeof c.s&&c.s();a.a.g.set(b,h,!f||f.ja&&!f.ja()?n:f)}};a.m.Ra.template=function(b){b=a.m.ac(b);return 1==b.length&&b[0].unknown||a.m.Id(b,"name")?null:"This template engine does not support anonymous templates nested within its templates"};a.h.ea.template=!0})();a.b("setTemplateEngine",a.gc);a.b("renderTemplate",a.dc);a.a.Kc=function(a,c,d){if(a.length&&c.length){var e,f,g,h,m;for(e=f=0;(!d||e<d)&&(h=a[f]);++f){for(g=0;m=c[g];++g)if(h.value=== +m.value){h.moved=m.index;m.moved=h.index;c.splice(g,1);e=g=0;break}e+=g}}};a.a.Pb=function(){function b(b,d,e,f,g){var h=Math.min,m=Math.max,k=[],l,p=b.length,q,n=d.length,r=n-p||1,v=p+n+1,u,w,z;for(l=0;l<=p;l++)for(w=u,k.push(u=[]),z=h(n,l+r),q=m(0,l-1);q<=z;q++)u[q]=q?l?b[l-1]===d[q-1]?w[q-1]:h(w[q]||v,u[q-1]||v)+1:q+1:l+1;h=[];m=[];r=[];l=p;for(q=n;l||q;)n=k[l][q]-1,q&&n===k[l][q-1]?m.push(h[h.length]={status:e,value:d[--q],index:q}):l&&n===k[l-1][q]?r.push(h[h.length]={status:f,value:b[--l],index:l}): +(--q,--l,g.sparse||h.push({status:"retained",value:d[q]}));a.a.Kc(r,m,!g.dontLimitMoves&&10*p);return h.reverse()}return function(a,d,e){e="boolean"===typeof e?{dontLimitMoves:e}:e||{};a=a||[];d=d||[];return a.length<d.length?b(a,d,"added","deleted",e):b(d,a,"deleted","added",e)}}();a.b("utils.compareArrays",a.a.Pb);(function(){function b(b,c,d,h,m){var k=[],l=a.$(function(){var l=c(d,m,a.a.Ua(k,b))||[];0<k.length&&(a.a.Xc(k,l),h&&a.u.G(h,null,[d,l,m]));k.length=0;a.a.Nb(k,l)},null,{l:b,Sa:function(){return!a.a.kd(k)}}); +return{Y:k,$:l.ja()?l:n}}var c=a.a.g.Z(),d=a.a.g.Z();a.a.ec=function(e,f,g,h,m,k){function l(b){y={Aa:b,pb:a.ta(w++)};v.push(y);r||F.push(y)}function p(b){y=t[b];w!==y.pb.v()&&D.push(y);y.pb(w++);a.a.Ua(y.Y,e);v.push(y)}function q(b,c){if(b)for(var d=0,e=c.length;d<e;d++)a.a.D(c[d].Y,function(a){b(a,d,c[d].Aa)})}f=f||[];"undefined"==typeof f.length&&(f=[f]);h=h||{};var t=a.a.g.get(e,c),r=!t,v=[],u=0,w=0,z=[],A=[],C=[],D=[],F=[],y,I=0;if(r)a.a.D(f,l);else{if(!k||t&&t._countWaitingForRemove){var E= +a.a.Mb(t,function(a){return a.Aa});k=a.a.Pb(E,f,{dontLimitMoves:h.dontLimitMoves,sparse:!0})}for(var E=0,G,H,K;G=k[E];E++)switch(H=G.moved,K=G.index,G.status){case "deleted":for(;u<K;)p(u++);H===n&&(y=t[u],y.$&&(y.$.s(),y.$=n),a.a.Ua(y.Y,e).length&&(h.beforeRemove&&(v.push(y),I++,y.Aa===d?y=null:C.push(y)),y&&z.push.apply(z,y.Y)));u++;break;case "added":for(;w<K;)p(u++);H!==n?(A.push(v.length),p(H)):l(G.value)}for(;w<f.length;)p(u++);v._countWaitingForRemove=I}a.a.g.set(e,c,v);q(h.beforeMove,D);a.a.D(z, +h.beforeRemove?a.oa:a.removeNode);var M,O,P;try{P=e.ownerDocument.activeElement}catch(N){}if(A.length)for(;(E=A.shift())!=n;){y=v[E];for(M=n;E;)if((O=v[--E].Y)&&O.length){M=O[O.length-1];break}for(f=0;u=y.Y[f];M=u,f++)a.h.Wb(e,u,M)}for(E=0;y=v[E];E++){y.Y||a.a.extend(y,b(e,g,y.Aa,m,y.pb));for(f=0;u=y.Y[f];M=u,f++)a.h.Wb(e,u,M);!y.Ed&&m&&(m(y.Aa,y.Y,y.pb),y.Ed=!0,M=y.Y[y.Y.length-1])}P&&e.ownerDocument.activeElement!=P&&P.focus();q(h.beforeRemove,C);for(E=0;E<C.length;++E)C[E].Aa=d;q(h.afterMove,D); +q(h.afterAdd,F)}})();a.b("utils.setDomNodeChildrenFromArrayMapping",a.a.ec);a.ba=function(){this.allowTemplateRewriting=!1};a.ba.prototype=new a.ca;a.ba.prototype.constructor=a.ba;a.ba.prototype.renderTemplateSource=function(b,c,d,e){if(c=(9>a.a.W?0:b.nodes)?b.nodes():null)return a.a.la(c.cloneNode(!0).childNodes);b=b.text();return a.a.ua(b,e)};a.ba.Ma=new a.ba;a.gc(a.ba.Ma);a.b("nativeTemplateEngine",a.ba);(function(){a.$a=function(){var a=this.Hd=function(){if(!v||!v.tmpl)return 0;try{if(0<=v.tmpl.tag.tmpl.open.toString().indexOf("__"))return 2}catch(a){}return 1}(); +this.renderTemplateSource=function(b,e,f,g){g=g||w;f=f||{};if(2>a)throw Error("Your version of jQuery.tmpl is too old. Please upgrade to jQuery.tmpl 1.0.0pre or later.");var h=b.data("precompiled");h||(h=b.text()||"",h=v.template(null,"{{ko_with $item.koBindingContext}}"+h+"{{/ko_with}}"),b.data("precompiled",h));b=[e.$data];e=v.extend({koBindingContext:e},f.templateOptions);e=v.tmpl(h,b,e);e.appendTo(g.createElement("div"));v.fragments={};return e};this.createJavaScriptEvaluatorBlock=function(a){return"{{ko_code ((function() { return "+ +a+" })()) }}"};this.addTemplate=function(a,b){w.write("<script type='text/html' id='"+a+"'>"+b+"\x3c/script>")};0<a&&(v.tmpl.tag.ko_code={open:"__.push($1 || '');"},v.tmpl.tag.ko_with={open:"with($1) {",close:"} "})};a.$a.prototype=new a.ca;a.$a.prototype.constructor=a.$a;var b=new a.$a;0<b.Hd&&a.gc(b);a.b("jqueryTmplTemplateEngine",a.$a)})()})})();})(); diff --git a/js/lazyload.d.ts b/js/lazyload.d.ts new file mode 100644 index 0000000..27fb708 --- /dev/null +++ b/js/lazyload.d.ts @@ -0,0 +1,326 @@ +interface ILazyLoadOptions { + /** + * The CSS selector of the elements to load lazily, + * which will be selected as descendants of the container object. + * @default ".lazy" + */ + elements_selector?: string; + /** + * The scrolling container of the elements in the `elements_selector` option. + * + * @default document + */ + container?: HTMLElement; + /** + * A number of pixels representing the outer distance off the scrolling area + * from which to start loading the elements. + * + * @default "300 0" + */ + threshold?: number; + /** + * + * Similar to threshold, but accepting multiple values and both px and % units. + * It maps directly to the rootMargin property of IntersectionObserver (read more), + * so it must be a string with a syntax similar to the CSS margin property. + * You can use it when you need to have different thresholds for the scrolling area. + * It overrides threshold when passed. + * + * @default null + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver/rootMargin + */ + thresholds?: string; + /** + * The name of the data attribute containing the element URL to load, + * excluding the `"data-"` part. + * E.g. if your data attribute is named `"data-src"`, + * just pass `"src"` + * + * @default "src" + */ + data_src?: string; + /** + * The name of the data attribute containing the image URL set to load, + * in either img and source tags, + * excluding the "data-" part. + * E.g. if your data attribute is named `"data-srcset"`, + * just pass `"srcset"` + * + * @default "srcset" + */ + data_srcset?: string; + /** + * The name of the data attribute containing the sizes attribute to use, excluding the `"data-"` part. + * E.g. if your data attribute is named `"data-sizes"`, just pass `"sizes"` + * + * @default sizes + */ + data_sizes?: string; + /** + * The name of the data attribute containing the URL of `background-image` to load lazily, + * excluding the `"data-"` part. + * E.g. if your data attribute is named `"data-bg"`, just pass `"bg"`. + * The attribute value must be a valid value for `background-image`, + * including the `url()` part of the CSS instruction. + * + * + * @default "bg" + */ + data_bg?: string; + /** + * The name of the data attribute containing the URL of `background-image` + * to load lazily on HiDPI screens, excluding the `"data-"` part. + * E.g. if your data attribute is named `"data-bg-hidpi"`, just pass `"bg-hidpi"`. + * The attribute value must be a valid value for `background-image`, + * including the `url()` part of the CSS instruction. + * + * @default "bg-hidpi" + */ + data_bg_hidpi?: string; + /** + * The name of the data attribute containing the value of multiple `background-image` + * to load lazily, excluding the `"data-"` part. + * E.g. if your data attribute is named `"data-bg-multi"`, just pass `"bg-multi"`. + * The attribute value must be a valid value for `background-image`, + * including the `url()` part of the CSS instruction. + * + * @default "bg-multi" + */ + data_bg_multi?: string; + /** + * The name of the data attribute containing the value of multiple `background-image` + * to load lazily on HiDPI screens, excluding the `"data-"` part. + * E.g. if your data attribute is named `"data-bg-multi-hidpi"`, just pass `"bg-multi-hidpi"`. + * The attribute value must be a valid value for `background-image`, + * including the `url()` part of the CSS instruction. + * + * @default "bg-multi-hidpi" + */ + data_bg_multi_hidpi?: string; + /** + * The name of the data attribute containing the value of poster to load lazily, + * excluding the `"data-"` part. + * E.g. if your data attribute is named `"data-poster"`, just pass `"poster"`. + * + * @default "poster" + */ + data_poster?: string; + + /** + * The class applied to the multiple background elements after the multiple background was applied + * + * @default "applied" + */ + class_applied?: string; + /** + * The class applied to the elements while the loading is in progress. + * + * @default "loading" + */ + class_loading?: string; + /** + * The class applied to the elements when the loading is complete. + * + * @default "loaded" + */ + class_loaded?: string; + /** + * The class applied to the elements when the element causes an error. + * + * @default "error" + */ + class_error?: string; + /** + * DEPRECATED + * + * You should change `load_delay: ___` with `cancel_on_exit: true`. + * + * @deprecated + */ + load_delay?: number; + + /** + * A boolean that defines whether or not to cancel the download of the images + * that exit the viewport while they are still loading, + * eventually restoring the original attributes. + * It applies only to images so to the `img` (and `picture`) tags, + * so it doesn't apply to background images, `iframes` nor `videos`. + * + * @default true + */ + cancel_on_exit?: boolean; + /** + * A boolean that defines whether or not to automatically unobserve elements once they entered the viewport + * + * @default false + */ + unobserve_entered?: boolean; + /** + * A boolean that defines whether or not to automatically unobserve elements once they've loaded or throwed an error + * + * @default true + */ + unobserve_completed?: boolean; + /** + * DEPRECATED + * + * You should replace `auto_unobserve` with `unobserve_completed` + * + * @deprecated + */ + auto_unobserve?: boolean; + /** + * A callback function which is called whenever a multiple background element starts loading. + * Arguments: `DOM element`, `lazyload instance`. + */ + callback_applied?: (elt: HTMLElement, instance: ILazyLoadInstance) => void; + /** + * A callback function which is called whenever an element loading is + * canceled while loading, as for `cancel_on_exit: true` + */ + callback_cancel?: ( + elt: HTMLElement, + entry: IntersectionObserverEntry, + instance: ILazyLoadInstance + ) => void; + /** + * A callback function which is called whenever an element enters the viewport. + * Arguments: DOM element, intersection observer entry, lazyload instance. + */ + callback_enter?: ( + elt: HTMLElement, + entry: IntersectionObserverEntry, + instance: ILazyLoadInstance + ) => void; + /** + * A callback function which is called whenever an element exits the viewport. + * Arguments: `DOM element`, `intersection observer entry`, `lazyload instance`. + */ + callback_exit?: ( + elt: HTMLElement, + entry: IntersectionObserverEntry, + instance: ILazyLoadInstance + ) => void; + /** + * A callback function which is called whenever an element starts loading. + * Arguments: `DOM element`, `lazyload instance`. + */ + callback_loading?: (elt: HTMLElement, instance: ILazyLoadInstance) => void; + /** + * A callback function which is called whenever an element finishes loading. + * Note that, in version older than 11.0.0, this option went under the name `callback_load`. + * Arguments: `DOM element`, `lazyload instance`. + */ + callback_loaded?: (elt: HTMLElement, instance: ILazyLoadInstance) => void; + /** + * A callback function which is called whenever an element triggers an error. + * Arguments: `DOM element`, `lazyload instance`. + */ + callback_error?: (elt: HTMLElement, instance: ILazyLoadInstance) => void; + /** + * + */ + + /** + * A callback function which is called when there are no more elements to load and all elements have been downloaded. + * Arguments: `lazyload instance`. + */ + callback_finish?: () => void; + /** + * This boolean sets whether or not to use [native lazy loading](https://addyosmani.com/blog/lazy-loading/) + * to do [hybrid lazy loading](https://www.smashingmagazine.com/2019/05/hybrid-lazy-loading-progressive-migration-native/). + * On browsers that support it, LazyLoad will set the `loading="lazy"` attribute on `images` and `iframes`, + * and delegate their loading to the browser. + * + * @default false + */ + use_native?: boolean; + /** + * DEPRECATED, WILL BE REMOVED IN V. 15 + * + * @deprecated + */ + callback_reveal?: (elt: HTMLElement) => void; +} + +interface ILazyLoadInstance { + /** + * Make LazyLoad to re-check the DOM for `elements_selector` elements inside its `container`. + * + * ### Use case + * + * Update LazyLoad after you added or removed DOM elements to the page. + */ + update: (elements?: NodeListOf<HTMLElement>) => void; + + /** + * Destroys the instance, unsetting instance variables and removing listeners. + * + * ### Use case + * + * Free up some memory. Especially useful for Single Page Applications. + */ + destroy: () => void; + + load: (element: HTMLElement, force?: boolean) => void; + + /** + * Loads all the lazy elements right away and stop observing them, + * no matter if they are inside or outside the viewport, + * no matter if they are hidden or visible. + * + * ### Use case + * + * To load all the remaining elements in advance + */ + loadAll: () => void; + + /** + * The number of elements that are currently downloading from the network + * (limitedly to the ones managed by the instance of LazyLoad). + * This is particularly useful to understand whether + * or not is safe to destroy this instance of LazyLoad. + */ + loadingCount: number; + + /** + * The number of elements that haven't been lazyloaded yet + * (limitedly to the ones managed by the instance of LazyLoad) + */ + toLoadCount: number; +} + +interface ILazyLoad { + new ( + options?: ILazyLoadOptions, + elements?: NodeListOf<HTMLElement> + ): ILazyLoadInstance; + + /** + * Immediately loads the lazy `element`. + * You can pass your custom options in the settings parameter. + * Note that the `elements_selector` option has no effect, + * since you are passing the element as a parameter. + * Also note that this method has effect only once on a specific `element`. + * + * ### Use case + * + * To load an `element` at mouseover or at any other event different than "entering the viewport" + */ + load(element: HTMLElement, settings: ILazyLoadOptions): void; + + /** + * Resets the internal status of the given element. + * + * ### Use case + * + * To tell LazyLoad to consider this `element` again, for example if you changed + * the `data-src` attribute after the previous `data-src` was loaded, + * call this method, then call `update()`. + */ + resetStatus(element: HTMLElement): void; +} + +declare var LazyLoad: ILazyLoad; + diff --git a/js/lazyload.min.js b/js/lazyload.min.js new file mode 100644 index 0000000..2b1b196 --- /dev/null +++ b/js/lazyload.min.js @@ -0,0 +1 @@ +!function(n,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(n="undefined"!=typeof globalThis?globalThis:n||self).LazyLoad=t()}(this,(function(){"use strict";function n(){return n=Object.assign||function(n){for(var t=1;t<arguments.length;t++){var e=arguments[t];for(var i in e)Object.prototype.hasOwnProperty.call(e,i)&&(n[i]=e[i])}return n},n.apply(this,arguments)}var t="undefined"!=typeof window,e=t&&!("onscroll"in window)||"undefined"!=typeof navigator&&/(gle|ing|ro)bot|crawl|spider/i.test(navigator.userAgent),i=t&&"IntersectionObserver"in window,o=t&&"classList"in document.createElement("p"),a=t&&window.devicePixelRatio>1,r={elements_selector:".lazy",container:e||t?document:null,threshold:300,thresholds:null,data_src:"src",data_srcset:"srcset",data_sizes:"sizes",data_bg:"bg",data_bg_hidpi:"bg-hidpi",data_bg_multi:"bg-multi",data_bg_multi_hidpi:"bg-multi-hidpi",data_poster:"poster",class_applied:"applied",class_loading:"loading",class_loaded:"loaded",class_error:"error",class_entered:"entered",class_exited:"exited",unobserve_completed:!0,unobserve_entered:!1,cancel_on_exit:!0,callback_enter:null,callback_exit:null,callback_applied:null,callback_loading:null,callback_loaded:null,callback_error:null,callback_finish:null,callback_cancel:null,use_native:!1},c=function(t){return n({},r,t)},u=function(n,t){var e,i="LazyLoad::Initialized",o=new n(t);try{e=new CustomEvent(i,{detail:{instance:o}})}catch(n){(e=document.createEvent("CustomEvent")).initCustomEvent(i,!1,!1,{instance:o})}window.dispatchEvent(e)},l="src",s="srcset",f="sizes",d="poster",_="llOriginalAttrs",g="loading",v="loaded",b="applied",p="error",h="native",m="data-",E="ll-status",I=function(n,t){return n.getAttribute(m+t)},y=function(n){return I(n,E)},A=function(n,t){return function(n,t,e){var i="data-ll-status";null!==e?n.setAttribute(i,e):n.removeAttribute(i)}(n,0,t)},k=function(n){return A(n,null)},L=function(n){return null===y(n)},w=function(n){return y(n)===h},x=[g,v,b,p],O=function(n,t,e,i){n&&(void 0===i?void 0===e?n(t):n(t,e):n(t,e,i))},N=function(n,t){o?n.classList.add(t):n.className+=(n.className?" ":"")+t},C=function(n,t){o?n.classList.remove(t):n.className=n.className.replace(new RegExp("(^|\\s+)"+t+"(\\s+|$)")," ").replace(/^\s+/,"").replace(/\s+$/,"")},M=function(n){return n.llTempImage},z=function(n,t){if(t){var e=t._observer;e&&e.unobserve(n)}},R=function(n,t){n&&(n.loadingCount+=t)},T=function(n,t){n&&(n.toLoadCount=t)},G=function(n){for(var t,e=[],i=0;t=n.children[i];i+=1)"SOURCE"===t.tagName&&e.push(t);return e},D=function(n,t){var e=n.parentNode;e&&"PICTURE"===e.tagName&&G(e).forEach(t)},V=function(n,t){G(n).forEach(t)},F=[l],j=[l,d],P=[l,s,f],S=function(n){return!!n[_]},U=function(n){return n[_]},$=function(n){return delete n[_]},q=function(n,t){if(!S(n)){var e={};t.forEach((function(t){e[t]=n.getAttribute(t)})),n[_]=e}},H=function(n,t){if(S(n)){var e=U(n);t.forEach((function(t){!function(n,t,e){e?n.setAttribute(t,e):n.removeAttribute(t)}(n,t,e[t])}))}},B=function(n,t,e){N(n,t.class_loading),A(n,g),e&&(R(e,1),O(t.callback_loading,n,e))},J=function(n,t,e){e&&n.setAttribute(t,e)},K=function(n,t){J(n,f,I(n,t.data_sizes)),J(n,s,I(n,t.data_srcset)),J(n,l,I(n,t.data_src))},Q={IMG:function(n,t){D(n,(function(n){q(n,P),K(n,t)})),q(n,P),K(n,t)},IFRAME:function(n,t){q(n,F),J(n,l,I(n,t.data_src))},VIDEO:function(n,t){V(n,(function(n){q(n,F),J(n,l,I(n,t.data_src))})),q(n,j),J(n,d,I(n,t.data_poster)),J(n,l,I(n,t.data_src)),n.load()}},W=["IMG","IFRAME","VIDEO"],X=function(n,t){!t||function(n){return n.loadingCount>0}(t)||function(n){return n.toLoadCount>0}(t)||O(n.callback_finish,t)},Y=function(n,t,e){n.addEventListener(t,e),n.llEvLisnrs[t]=e},Z=function(n,t,e){n.removeEventListener(t,e)},nn=function(n){return!!n.llEvLisnrs},tn=function(n){if(nn(n)){var t=n.llEvLisnrs;for(var e in t){var i=t[e];Z(n,e,i)}delete n.llEvLisnrs}},en=function(n,t,e){!function(n){delete n.llTempImage}(n),R(e,-1),function(n){n&&(n.toLoadCount-=1)}(e),C(n,t.class_loading),t.unobserve_completed&&z(n,e)},on=function(n,t,e){var i=M(n)||n;nn(i)||function(n,t,e){nn(n)||(n.llEvLisnrs={});var i="VIDEO"===n.tagName?"loadeddata":"load";Y(n,i,t),Y(n,"error",e)}(i,(function(o){!function(n,t,e,i){var o=w(t);en(t,e,i),N(t,e.class_loaded),A(t,v),O(e.callback_loaded,t,i),o||X(e,i)}(0,n,t,e),tn(i)}),(function(o){!function(n,t,e,i){var o=w(t);en(t,e,i),N(t,e.class_error),A(t,p),O(e.callback_error,t,i),o||X(e,i)}(0,n,t,e),tn(i)}))},an=function(n,t,e){!function(n){n.llTempImage=document.createElement("IMG")}(n),on(n,t,e),function(n){S(n)||(n[_]={backgroundImage:n.style.backgroundImage})}(n),function(n,t,e){var i=I(n,t.data_bg),o=I(n,t.data_bg_hidpi),r=a&&o?o:i;r&&(n.style.backgroundImage='url("'.concat(r,'")'),M(n).setAttribute(l,r),B(n,t,e))}(n,t,e),function(n,t,e){var i=I(n,t.data_bg_multi),o=I(n,t.data_bg_multi_hidpi),r=a&&o?o:i;r&&(n.style.backgroundImage=r,function(n,t,e){N(n,t.class_applied),A(n,b),e&&(t.unobserve_completed&&z(n,t),O(t.callback_applied,n,e))}(n,t,e))}(n,t,e)},rn=function(n,t,e){!function(n){return W.indexOf(n.tagName)>-1}(n)?an(n,t,e):function(n,t,e){on(n,t,e),function(n,t,e){var i=Q[n.tagName];i&&(i(n,t),B(n,t,e))}(n,t,e)}(n,t,e)},cn=function(n){n.removeAttribute(l),n.removeAttribute(s),n.removeAttribute(f)},un=function(n){D(n,(function(n){H(n,P)})),H(n,P)},ln={IMG:un,IFRAME:function(n){H(n,F)},VIDEO:function(n){V(n,(function(n){H(n,F)})),H(n,j),n.load()}},sn=function(n,t){(function(n){var t=ln[n.tagName];t?t(n):function(n){if(S(n)){var t=U(n);n.style.backgroundImage=t.backgroundImage}}(n)})(n),function(n,t){L(n)||w(n)||(C(n,t.class_entered),C(n,t.class_exited),C(n,t.class_applied),C(n,t.class_loading),C(n,t.class_loaded),C(n,t.class_error))}(n,t),k(n),$(n)},fn=["IMG","IFRAME","VIDEO"],dn=function(n){return n.use_native&&"loading"in HTMLImageElement.prototype},_n=function(n,t,e){n.forEach((function(n){return function(n){return n.isIntersecting||n.intersectionRatio>0}(n)?function(n,t,e,i){var o=function(n){return x.indexOf(y(n))>=0}(n);A(n,"entered"),N(n,e.class_entered),C(n,e.class_exited),function(n,t,e){t.unobserve_entered&&z(n,e)}(n,e,i),O(e.callback_enter,n,t,i),o||rn(n,e,i)}(n.target,n,t,e):function(n,t,e,i){L(n)||(N(n,e.class_exited),function(n,t,e,i){e.cancel_on_exit&&function(n){return y(n)===g}(n)&&"IMG"===n.tagName&&(tn(n),function(n){D(n,(function(n){cn(n)})),cn(n)}(n),un(n),C(n,e.class_loading),R(i,-1),k(n),O(e.callback_cancel,n,t,i))}(n,t,e,i),O(e.callback_exit,n,t,i))}(n.target,n,t,e)}))},gn=function(n){return Array.prototype.slice.call(n)},vn=function(n){return n.container.querySelectorAll(n.elements_selector)},bn=function(n){return function(n){return y(n)===p}(n)},pn=function(n,t){return function(n){return gn(n).filter(L)}(n||vn(t))},hn=function(n,e){var o=c(n);this._settings=o,this.loadingCount=0,function(n,t){i&&!dn(n)&&(t._observer=new IntersectionObserver((function(e){_n(e,n,t)}),function(n){return{root:n.container===document?null:n.container,rootMargin:n.thresholds||n.threshold+"px"}}(n)))}(o,this),function(n,e){t&&window.addEventListener("online",(function(){!function(n,t){var e;(e=vn(n),gn(e).filter(bn)).forEach((function(t){C(t,n.class_error),k(t)})),t.update()}(n,e)}))}(o,this),this.update(e)};return hn.prototype={update:function(n){var t,o,a=this._settings,r=pn(n,a);T(this,r.length),!e&&i?dn(a)?function(n,t,e){n.forEach((function(n){-1!==fn.indexOf(n.tagName)&&function(n,t,e){n.setAttribute("loading","lazy"),on(n,t,e),function(n,t){var e=Q[n.tagName];e&&e(n,t)}(n,t),A(n,h)}(n,t,e)})),T(e,0)}(r,a,this):(o=r,function(n){n.disconnect()}(t=this._observer),function(n,t){t.forEach((function(t){n.observe(t)}))}(t,o)):this.loadAll(r)},destroy:function(){this._observer&&this._observer.disconnect(),vn(this._settings).forEach((function(n){$(n)})),delete this._observer,delete this._settings,delete this.loadingCount,delete this.toLoadCount},loadAll:function(n){var t=this,e=this._settings;pn(n,e).forEach((function(n){z(n,t),rn(n,e,t)}))},restoreAll:function(){var n=this._settings;vn(n).forEach((function(t){sn(t,n)}))}},hn.load=function(n,t){var e=c(t);rn(n,e)},hn.resetStatus=function(n){k(n)},t&&function(n,t){if(t)if(t.length)for(var e,i=0;e=t[i];i+=1)u(n,e);else u(n,t)}(hn,window.lazyLoadOptions),hn})); diff --git a/js/lodash-3.10.d.ts b/js/lodash-3.10.d.ts new file mode 100644 index 0000000..43cc3df --- /dev/null +++ b/js/lodash-3.10.d.ts @@ -0,0 +1,15041 @@ +// Type definitions for Lo-Dash +// Project: http://lodash.com/ +// Definitions by: Brian Zengel <https://github.com/bczengel>, Ilya Mochalov <https://github.com/chrootsu> +// Definitions: https://github.com/borisyankov/DefinitelyTyped + +declare var _: _.LoDashStatic; + +declare module _ { + interface LoDashStatic { + /** + * Creates a lodash object which wraps the given value to enable intuitive method chaining. + * + * In addition to Lo-Dash methods, wrappers also have the following Array methods: + * concat, join, pop, push, reverse, shift, slice, sort, splice, and unshift + * + * Chaining is supported in custom builds as long as the value method is implicitly or + * explicitly included in the build. + * + * The chainable wrapper functions are: + * after, assign, bind, bindAll, bindKey, chain, chunk, compact, compose, concat, countBy, + * createCallback, curry, debounce, defaults, defer, delay, difference, filter, flatten, + * forEach, forEachRight, forIn, forInRight, forOwn, forOwnRight, functions, groupBy, + * indexBy, initial, intersection, invert, invoke, keys, map, max, memoize, merge, min, + * object, omit, once, pairs, partial, partialRight, pick, pluck, pull, push, range, reject, + * remove, rest, reverse, sample, shuffle, slice, sort, sortBy, splice, tap, throttle, times, + * toArray, transform, union, uniq, unshift, unzip, values, where, without, wrap, and zip + * + * The non-chainable wrapper functions are: + * clone, cloneDeep, contains, escape, every, find, findIndex, findKey, findLast, + * findLastIndex, findLastKey, has, identity, indexOf, isArguments, isArray, isBoolean, + * isDate, isElement, isEmpty, isEqual, isFinite, isFunction, isNaN, isNull, isNumber, + * isObject, isPlainObject, isRegExp, isString, isUndefined, join, lastIndexOf, mixin, + * noConflict, parseInt, pop, random, reduce, reduceRight, result, shift, size, some, + * sortedIndex, runInContext, template, unescape, uniqueId, and value + * + * The wrapper functions first and last return wrapped values when n is provided, otherwise + * they return unwrapped values. + * + * Explicit chaining can be enabled by using the _.chain method. + **/ + (value: number): LoDashImplicitWrapper<number>; + (value: string): LoDashImplicitStringWrapper; + (value: boolean): LoDashImplicitWrapper<boolean>; + (value: Array<number>): LoDashImplicitNumberArrayWrapper; + <T>(value: Array<T>): LoDashImplicitArrayWrapper<T>; + <T extends {}>(value: T): LoDashImplicitObjectWrapper<T>; + (value: any): LoDashImplicitWrapper<any>; + + /** + * The semantic version number. + **/ + VERSION: string; + + /** + * An object used to flag environments features. + **/ + support: Support; + + /** + * By default, the template delimiters used by Lo-Dash are similar to those in embedded Ruby + * (ERB). Change the following template settings to use alternative delimiters. + **/ + templateSettings: TemplateSettings; + } + + /** + * By default, the template delimiters used by Lo-Dash are similar to those in embedded Ruby + * (ERB). Change the following template settings to use alternative delimiters. + **/ + interface TemplateSettings { + /** + * The "escape" delimiter. + **/ + escape?: RegExp; + + /** + * The "evaluate" delimiter. + **/ + evaluate?: RegExp; + + /** + * An object to import into the template as local variables. + **/ + imports?: Dictionary<any>; + + /** + * The "interpolate" delimiter. + **/ + interpolate?: RegExp; + + /** + * Used to reference the data object in the template text. + **/ + variable?: string; + } + + /** + * Creates a cache object to store key/value pairs. + */ + interface MapCache { + /** + * Removes `key` and its value from the cache. + * @param key The key of the value to remove. + * @return Returns `true` if the entry was removed successfully, else `false`. + */ + delete(key: string): boolean; + + /** + * Gets the cached value for `key`. + * @param key The key of the value to get. + * @return Returns the cached value. + */ + get(key: string): any; + + /** + * Checks if a cached value for `key` exists. + * @param key The key of the entry to check. + * @return Returns `true` if an entry for `key` exists, else `false`. + */ + has(key: string): boolean; + + /** + * Sets `value` to `key` of the cache. + * @param key The key of the value to cache. + * @param value The value to cache. + * @return Returns the cache object. + */ + set(key: string, value: any): _.Dictionary<any>; + } + + /** + * An object used to flag environments features. + **/ + interface Support { + /** + * Detect if an arguments object's [[Class]] is resolvable (all but Firefox < 4, IE < 9). + **/ + argsClass: boolean; + + /** + * Detect if arguments objects are Object objects (all but Narwhal and Opera < 10.5). + **/ + argsObject: boolean; + + /** + * Detect if name or message properties of Error.prototype are enumerable by default. + * (IE < 9, Safari < 5.1) + **/ + enumErrorProps: boolean; + + /** + * Detect if prototype properties are enumerable by default. + * + * Firefox < 3.6, Opera > 9.50 - Opera < 11.60, and Safari < 5.1 (if the prototype or a property on the + * prototype has been set) incorrectly set the [[Enumerable]] value of a function’s prototype property to true. + **/ + enumPrototypes: boolean; + + /** + * Detect if Function#bind exists and is inferred to be fast (all but V8). + **/ + fastBind: boolean; + + /** + * Detect if functions can be decompiled by Function#toString (all but PS3 and older Opera + * mobile browsers & avoided in Windows 8 apps). + **/ + funcDecomp: boolean; + + /** + * Detect if Function#name is supported (all but IE). + **/ + funcNames: boolean; + + /** + * Detect if arguments object indexes are non-enumerable (Firefox < 4, IE < 9, PhantomJS, + * Safari < 5.1). + **/ + nonEnumArgs: boolean; + + /** + * Detect if properties shadowing those on Object.prototype are non-enumerable. + * + * In IE < 9 an objects own properties, shadowing non-enumerable ones, are made + * non-enumerable as well (a.k.a the JScript [[DontEnum]] bug). + **/ + nonEnumShadows: boolean; + + /** + * Detect if own properties are iterated after inherited properties (all but IE < 9). + **/ + ownLast: boolean; + + /** + * Detect if Array#shift and Array#splice augment array-like objects correctly. + * + * Firefox < 10, IE compatibility mode, and IE < 9 have buggy Array shift() and splice() + * functions that fail to remove the last element, value[0], of array-like objects even + * though the length property is set to 0. The shift() method is buggy in IE 8 compatibility + * mode, while splice() is buggy regardless of mode in IE < 9 and buggy in compatibility mode + * in IE 9. + **/ + spliceObjects: boolean; + + /** + * Detect lack of support for accessing string characters by index. + * + * IE < 8 can't access characters by index and IE 8 can only access characters by index on + * string literals. + **/ + unindexedChars: boolean; + } + + interface LoDashWrapperBase<T, TWrapper> { } + + interface LoDashImplicitWrapperBase<T, TWrapper> extends LoDashWrapperBase<T, TWrapper> { } + + interface LoDashExplicitWrapperBase<T, TWrapper> extends LoDashWrapperBase<T, TWrapper> { } + + interface LoDashImplicitWrapper<T> extends LoDashImplicitWrapperBase<T, LoDashImplicitWrapper<T>> { } + + interface LoDashExplicitWrapper<T> extends LoDashExplicitWrapperBase<T, LoDashExplicitWrapper<T>> { } + + interface LoDashImplicitStringWrapper extends LoDashImplicitWrapper<string> { } + + interface LoDashExplicitStringWrapper extends LoDashExplicitWrapper<string> { } + + interface LoDashImplicitObjectWrapper<T> extends LoDashImplicitWrapperBase<T, LoDashImplicitObjectWrapper<T>> { } + + interface LoDashExplicitObjectWrapper<T> extends LoDashExplicitWrapperBase<T, LoDashExplicitObjectWrapper<T>> { } + + interface LoDashImplicitArrayWrapper<T> extends LoDashImplicitWrapperBase<T[], LoDashImplicitArrayWrapper<T>> { + pop(): T; + push(...items: T[]): LoDashImplicitArrayWrapper<T>; + shift(): T; + sort(compareFn?: (a: T, b: T) => number): LoDashImplicitArrayWrapper<T>; + splice(start: number): LoDashImplicitArrayWrapper<T>; + splice(start: number, deleteCount: number, ...items: any[]): LoDashImplicitArrayWrapper<T>; + unshift(...items: T[]): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitArrayWrapper<T> extends LoDashExplicitWrapperBase<T[], LoDashExplicitArrayWrapper<T>> { } + + interface LoDashImplicitNumberArrayWrapper extends LoDashImplicitArrayWrapper<number> { } + + interface LoDashExplicitNumberArrayWrapper extends LoDashExplicitArrayWrapper<number> { } + + // join (exists only in wrappers) + interface LoDashImplicitWrapper<T> { + /** + * @see _.join + */ + join(separator?: string): string; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.join + */ + join(separator?: string): string; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.join + */ + join(separator?: string): string; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.join + */ + join(separator?: string): LoDashExplicitWrapper<string>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.join + */ + join(separator?: string): LoDashExplicitWrapper<string>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.join + */ + join(separator?: string): LoDashExplicitWrapper<string>; + } + + /********* + * Array * + *********/ + + //_.chunk + interface LoDashStatic { + /** + * Creates an array of elements split into groups the length of size. If collection can’t be split evenly, the + * final chunk will be the remaining elements. + * + * @param array The array to process. + * @param size The length of each chunk. + * @return Returns the new array containing chunks. + */ + chunk<T>( + array: List<T>, + size?: number + ): T[][]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.chunk + */ + chunk(size?: number): LoDashImplicitArrayWrapper<T[]>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.chunk + */ + chunk<TResult>(size?: number): LoDashImplicitArrayWrapper<TResult[]>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.chunk + */ + chunk(size?: number): LoDashExplicitArrayWrapper<T[]>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.chunk + */ + chunk<TResult>(size?: number): LoDashExplicitArrayWrapper<TResult[]>; + } + + //_.compact + interface LoDashStatic { + /** + * Creates an array with all falsey values removed. The values false, null, 0, "", undefined, and NaN are + * falsey. + * + * @param array The array to compact. + * @return (Array) Returns the new array of filtered values. + */ + compact<T>(array?: List<T>): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.compact + */ + compact(): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.compact + */ + compact<TResult>(): LoDashImplicitArrayWrapper<TResult>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.compact + */ + compact(): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.compact + */ + compact<TResult>(): LoDashExplicitArrayWrapper<TResult>; + } + + //_.difference + interface LoDashStatic { + /** + * Creates an array of unique array values not included in the other provided arrays using SameValueZero for + * equality comparisons. + * + * @param array The array to inspect. + * @param values The arrays of values to exclude. + * @return Returns the new array of filtered values. + */ + difference<T>( + array: T[]|List<T>, + ...values: (T[]|List<T>)[] + ): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.difference + */ + difference(...values: (T[]|List<T>)[]): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.difference + */ + difference<TValue>(...values: (TValue[]|List<TValue>)[]): LoDashImplicitArrayWrapper<TValue>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.difference + */ + difference(...values: (T[]|List<T>)[]): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.difference + */ + difference<TValue>(...values: (TValue[]|List<TValue>)[]): LoDashExplicitArrayWrapper<TValue>; + } + + //_.drop + interface LoDashStatic { + /** + * Creates a slice of array with n elements dropped from the beginning. + * + * @param array The array to query. + * @param n The number of elements to drop. + * @return Returns the slice of array. + */ + drop<T>(array: T[]|List<T>, n?: number): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.drop + */ + drop(n?: number): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.drop + */ + drop<T>(n?: number): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.drop + */ + drop(n?: number): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.drop + */ + drop<T>(n?: number): LoDashExplicitArrayWrapper<T>; + } + + //_.dropRight + interface LoDashStatic { + /** + * Creates a slice of array with n elements dropped from the end. + * + * @param array The array to query. + * @param n The number of elements to drop. + * @return Returns the slice of array. + */ + dropRight<T>( + array: List<T>, + n?: number + ): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.dropRight + */ + dropRight(n?: number): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.dropRight + */ + dropRight<TResult>(n?: number): LoDashImplicitArrayWrapper<TResult>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.dropRight + */ + dropRight(n?: number): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.dropRight + */ + dropRight<TResult>(n?: number): LoDashExplicitArrayWrapper<TResult>; + } + + //_.dropRightWhile + interface LoDashStatic { + /** + * Creates a slice of array excluding elements dropped from the end. Elements are dropped until predicate + * returns falsey. The predicate is bound to thisArg and invoked with three arguments: (value, index, array). + * + * If a property name is provided for predicate the created _.property style callback returns the property + * value of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for predicate the created _.matches style callback returns true for elements that + * match the properties of the given object, else false. + * + * @param array The array to query. + * @param predicate The function invoked per iteration. + * @param thisArg The this binding of predicate. + * @return Returns the slice of array. + */ + dropRightWhile<TValue>( + array: List<TValue>, + predicate?: ListIterator<TValue, boolean>, + thisArg?: any + ): TValue[]; + + /** + * @see _.dropRightWhile + */ + dropRightWhile<TValue>( + array: List<TValue>, + predicate?: string, + thisArg?: any + ): TValue[]; + + /** + * @see _.dropRightWhile + */ + dropRightWhile<TWhere, TValue>( + array: List<TValue>, + predicate?: TWhere + ): TValue[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.dropRightWhile + */ + dropRightWhile( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.dropRightWhile + */ + dropRightWhile( + predicate?: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.dropRightWhile + */ + dropRightWhile<TWhere>( + predicate?: TWhere + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.dropRightWhile + */ + dropRightWhile<TValue>( + predicate?: ListIterator<TValue, boolean>, + thisArg?: any + ): LoDashImplicitArrayWrapper<TValue>; + + /** + * @see _.dropRightWhile + */ + dropRightWhile<TValue>( + predicate?: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<TValue>; + + /** + * @see _.dropRightWhile + */ + dropRightWhile<TWhere, TValue>( + predicate?: TWhere + ): LoDashImplicitArrayWrapper<TValue>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.dropRightWhile + */ + dropRightWhile( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.dropRightWhile + */ + dropRightWhile( + predicate?: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.dropRightWhile + */ + dropRightWhile<TWhere>( + predicate?: TWhere + ): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.dropRightWhile + */ + dropRightWhile<TValue>( + predicate?: ListIterator<TValue, boolean>, + thisArg?: any + ): LoDashExplicitArrayWrapper<TValue>; + + /** + * @see _.dropRightWhile + */ + dropRightWhile<TValue>( + predicate?: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<TValue>; + + /** + * @see _.dropRightWhile + */ + dropRightWhile<TWhere, TValue>( + predicate?: TWhere + ): LoDashExplicitArrayWrapper<TValue>; + } + + //_.dropWhile + interface LoDashStatic { + /** + * Creates a slice of array excluding elements dropped from the beginning. Elements are dropped until predicate + * returns falsey. The predicate is bound to thisArg and invoked with three arguments: (value, index, array). + * + * If a property name is provided for predicate the created _.property style callback returns the property + * value of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for predicate the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * @param array The array to query. + * @param predicate The function invoked per iteration. + * @param thisArg The this binding of predicate. + * @return Returns the slice of array. + */ + dropWhile<TValue>( + array: List<TValue>, + predicate?: ListIterator<TValue, boolean>, + thisArg?: any + ): TValue[]; + + /** + * @see _.dropWhile + */ + dropWhile<TValue>( + array: List<TValue>, + predicate?: string, + thisArg?: any + ): TValue[]; + + /** + * @see _.dropWhile + */ + dropWhile<TWhere, TValue>( + array: List<TValue>, + predicate?: TWhere + ): TValue[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.dropWhile + */ + dropWhile( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.dropWhile + */ + dropWhile( + predicate?: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.dropWhile + */ + dropWhile<TWhere>( + predicate?: TWhere + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.dropWhile + */ + dropWhile<TValue>( + predicate?: ListIterator<TValue, boolean>, + thisArg?: any + ): LoDashImplicitArrayWrapper<TValue>; + + /** + * @see _.dropWhile + */ + dropWhile<TValue>( + predicate?: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<TValue>; + + /** + * @see _.dropWhile + */ + dropWhile<TWhere, TValue>( + predicate?: TWhere + ): LoDashImplicitArrayWrapper<TValue>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.dropWhile + */ + dropWhile( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.dropWhile + */ + dropWhile( + predicate?: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.dropWhile + */ + dropWhile<TWhere>( + predicate?: TWhere + ): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.dropWhile + */ + dropWhile<TValue>( + predicate?: ListIterator<TValue, boolean>, + thisArg?: any + ): LoDashExplicitArrayWrapper<TValue>; + + /** + * @see _.dropWhile + */ + dropWhile<TValue>( + predicate?: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<TValue>; + + /** + * @see _.dropWhile + */ + dropWhile<TWhere, TValue>( + predicate?: TWhere + ): LoDashExplicitArrayWrapper<TValue>; + } + + //_.fill + interface LoDashStatic { + /** + * Fills elements of array with value from start up to, but not including, end. + * + * Note: This method mutates array. + * + * @param array The array to fill. + * @param value The value to fill array with. + * @param start The start position. + * @param end The end position. + * @return Returns array. + */ + fill<T>( + array: any[], + value: T, + start?: number, + end?: number + ): T[]; + + /** + * @see _.fill + */ + fill<T>( + array: List<any>, + value: T, + start?: number, + end?: number + ): List<T>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.fill + */ + fill<T>( + value: T, + start?: number, + end?: number + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.fill + */ + fill<T>( + value: T, + start?: number, + end?: number + ): LoDashImplicitObjectWrapper<List<T>>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.fill + */ + fill<T>( + value: T, + start?: number, + end?: number + ): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.fill + */ + fill<T>( + value: T, + start?: number, + end?: number + ): LoDashExplicitObjectWrapper<List<T>>; + } + + //_.findIndex + interface LoDashStatic { + /** + * This method is like _.find except that it returns the index of the first element predicate returns truthy + * for instead of the element itself. + * + * If a property name is provided for predicate the created _.property style callback returns the property + * value of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for predicate the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * @param array The array to search. + * @param predicate The function invoked per iteration. + * @param thisArg The this binding of predicate. + * @return Returns the index of the found element, else -1. + */ + findIndex<T>( + array: List<T>, + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): number; + + /** + * @see _.findIndex + */ + findIndex<T>( + array: List<T>, + predicate?: string, + thisArg?: any + ): number; + + /** + * @see _.findIndex + */ + findIndex<W, T>( + array: List<T>, + predicate?: W + ): number; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.findIndex + */ + findIndex( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): number; + + /** + * @see _.findIndex + */ + findIndex( + predicate?: string, + thisArg?: any + ): number; + + /** + * @see _.findIndex + */ + findIndex<W>( + predicate?: W + ): number; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.findIndex + */ + findIndex<TResult>( + predicate?: ListIterator<TResult, boolean>, + thisArg?: any + ): number; + + /** + * @see _.findIndex + */ + findIndex( + predicate?: string, + thisArg?: any + ): number; + + /** + * @see _.findIndex + */ + findIndex<W>( + predicate?: W + ): number; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.findIndex + */ + findIndex( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): LoDashExplicitWrapper<number>; + + /** + * @see _.findIndex + */ + findIndex( + predicate?: string, + thisArg?: any + ): LoDashExplicitWrapper<number>; + + /** + * @see _.findIndex + */ + findIndex<W>( + predicate?: W + ): LoDashExplicitWrapper<number>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.findIndex + */ + findIndex<TResult>( + predicate?: ListIterator<TResult, boolean>, + thisArg?: any + ): LoDashExplicitWrapper<number>; + + /** + * @see _.findIndex + */ + findIndex( + predicate?: string, + thisArg?: any + ): LoDashExplicitWrapper<number>; + + /** + * @see _.findIndex + */ + findIndex<W>( + predicate?: W + ): LoDashExplicitWrapper<number>; + } + + //_.findLastIndex + interface LoDashStatic { + /** + * This method is like _.findIndex except that it iterates over elements of collection from right to left. + * + * If a property name is provided for predicate the created _.property style callback returns the property + * value of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for predicate the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * @param array The array to search. + * @param predicate The function invoked per iteration. + * @param thisArg The function invoked per iteration. + * @return Returns the index of the found element, else -1. + */ + findLastIndex<T>( + array: List<T>, + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): number; + + /** + * @see _.findLastIndex + */ + findLastIndex<T>( + array: List<T>, + predicate?: string, + thisArg?: any + ): number; + + /** + * @see _.findLastIndex + */ + findLastIndex<W, T>( + array: List<T>, + predicate?: W + ): number; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.findLastIndex + */ + findLastIndex( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): number; + + /** + * @see _.findLastIndex + */ + findLastIndex( + predicate?: string, + thisArg?: any + ): number; + + /** + * @see _.findLastIndex + */ + findLastIndex<W>( + predicate?: W + ): number; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.findLastIndex + */ + findLastIndex<TResult>( + predicate?: ListIterator<TResult, boolean>, + thisArg?: any + ): number; + + /** + * @see _.findLastIndex + */ + findLastIndex( + predicate?: string, + thisArg?: any + ): number; + + /** + * @see _.findLastIndex + */ + findLastIndex<W>( + predicate?: W + ): number; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.findLastIndex + */ + findLastIndex( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): LoDashExplicitWrapper<number>; + + /** + * @see _.findLastIndex + */ + findLastIndex( + predicate?: string, + thisArg?: any + ): LoDashExplicitWrapper<number>; + + /** + * @see _.findLastIndex + */ + findLastIndex<W>( + predicate?: W + ): LoDashExplicitWrapper<number>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.findLastIndex + */ + findLastIndex<TResult>( + predicate?: ListIterator<TResult, boolean>, + thisArg?: any + ): LoDashExplicitWrapper<number>; + + /** + * @see _.findLastIndex + */ + findLastIndex( + predicate?: string, + thisArg?: any + ): LoDashExplicitWrapper<number>; + + /** + * @see _.findLastIndex + */ + findLastIndex<W>( + predicate?: W + ): LoDashExplicitWrapper<number>; + } + + //_.first + interface LoDashStatic { + /** + * Gets the first element of array. + * + * @alias _.head + * + * @param array The array to query. + * @return Returns the first element of array. + */ + first<T>(array: List<T>): T; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.first + */ + first(): T; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.first + */ + first<TResult>(): TResult; + } + + interface RecursiveArray<T> extends Array<T|RecursiveArray<T>> {} + interface ListOfRecursiveArraysOrValues<T> extends List<T|RecursiveArray<T>> {} + + //_.flatten + interface LoDashStatic { + /** + * Flattens a nested array. If isDeep is true the array is recursively flattened, otherwise it’s only + * flattened a single level. + * + * @param array The array to flatten. + * @param isDeep Specify a deep flatten. + * @return Returns the new flattened array. + */ + flatten<T>(array: ListOfRecursiveArraysOrValues<T>, isDeep: boolean): T[]; + + /** + * @see _.flatten + */ + flatten<T>(array: List<T|T[]>): T[]; + + /** + * @see _.flatten + */ + flatten<T>(array: ListOfRecursiveArraysOrValues<T>): RecursiveArray<T>; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.flatten + */ + flatten(): LoDashImplicitArrayWrapper<string>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.flatten + */ + flatten<TResult>(isDeep?: boolean): LoDashImplicitArrayWrapper<TResult>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.flatten + */ + flatten<TResult>(isDeep?: boolean): LoDashImplicitArrayWrapper<TResult>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.flatten + */ + flatten(): LoDashExplicitArrayWrapper<string>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.flatten + */ + flatten<TResult>(isDeep?: boolean): LoDashExplicitArrayWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.flatten + */ + flatten<TResult>(isDeep?: boolean): LoDashExplicitArrayWrapper<TResult>; + } + + //_.flattenDeep + interface LoDashStatic { + /** + * Recursively flattens a nested array. + * + * @param array The array to recursively flatten. + * @return Returns the new flattened array. + */ + flattenDeep<T>(array: ListOfRecursiveArraysOrValues<T>): T[]; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.flattenDeep + */ + flattenDeep(): LoDashImplicitArrayWrapper<string>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.flattenDeep + */ + flattenDeep<T>(): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.flattenDeep + */ + flattenDeep<T>(): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.flattenDeep + */ + flattenDeep(): LoDashExplicitArrayWrapper<string>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.flattenDeep + */ + flattenDeep<T>(): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.flattenDeep + */ + flattenDeep<T>(): LoDashExplicitArrayWrapper<T>; + } + + //_.head + interface LoDashStatic { + /** + * @see _.first + */ + head<T>(array: List<T>): T; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.first + */ + head(): T; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.first + */ + head<TResult>(): TResult; + } + + //_.indexOf + interface LoDashStatic { + /** + * Gets the index at which the first occurrence of value is found in array using SameValueZero for equality + * comparisons. If fromIndex is negative, it’s used as the offset from the end of array. If array is sorted + * providing true for fromIndex performs a faster binary search. + * + * @param array The array to search. + * @param value The value to search for. + * @param fromIndex The index to search from or true to perform a binary search on a sorted array. + * @return The index to search from or true to perform a binary search on a sorted array. + */ + indexOf<T>( + array: List<T>, + value: T, + fromIndex?: boolean|number + ): number; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.indexOf + */ + indexOf( + value: T, + fromIndex?: boolean|number + ): number; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.indexOf + */ + indexOf<TValue>( + value: TValue, + fromIndex?: boolean|number + ): number; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.indexOf + */ + indexOf( + value: T, + fromIndex?: boolean|number + ): LoDashExplicitWrapper<number>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.indexOf + */ + indexOf<TValue>( + value: TValue, + fromIndex?: boolean|number + ): LoDashExplicitWrapper<number>; + } + + //_.initial + interface LoDashStatic { + /** + * Gets all but the last element of array. + * + * @param array The array to query. + * @return Returns the slice of array. + */ + initial<T>(array: List<T>): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.initial + */ + initial(): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.initial + */ + initial<T>(): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.initial + */ + initial(): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.initial + */ + initial<T>(): LoDashExplicitArrayWrapper<T>; + } + + //_.intersection + interface LoDashStatic { + /** + * Creates an array of unique values that are included in all of the provided arrays using SameValueZero for + * equality comparisons. + * + * @param arrays The arrays to inspect. + * @return Returns the new array of shared values. + */ + intersection<T>(...arrays: (T[]|List<T>)[]): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.intersection + */ + intersection<TResult>(...arrays: (TResult[]|List<TResult>)[]): LoDashImplicitArrayWrapper<TResult>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.intersection + */ + intersection<TResult>(...arrays: (TResult[]|List<TResult>)[]): LoDashImplicitArrayWrapper<TResult>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.intersection + */ + intersection<TResult>(...arrays: (TResult[]|List<TResult>)[]): LoDashExplicitArrayWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.intersection + */ + intersection<TResult>(...arrays: (TResult[]|List<TResult>)[]): LoDashExplicitArrayWrapper<TResult>; + } + + //_.last + interface LoDashStatic { + /** + * Gets the last element of array. + * + * @param array The array to query. + * @return Returns the last element of array. + */ + last<T>(array: List<T>): T; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.last + */ + last(): T; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.last + */ + last<T>(): T; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.last + */ + last(): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.last + */ + last<T>(): LoDashExplicitObjectWrapper<T>; + } + + //_.lastIndexOf + interface LoDashStatic { + /** + * This method is like _.indexOf except that it iterates over elements of array from right to left. + * + * @param array The array to search. + * @param value The value to search for. + * @param fromIndex The index to search from or true to perform a binary search on a sorted array. + * @return Returns the index of the matched value, else -1. + */ + lastIndexOf<T>( + array: List<T>, + value: T, + fromIndex?: boolean|number + ): number; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.lastIndexOf + */ + lastIndexOf( + value: T, + fromIndex?: boolean|number + ): number; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.lastIndexOf + */ + lastIndexOf<TResult>( + value: TResult, + fromIndex?: boolean|number + ): number; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.lastIndexOf + */ + lastIndexOf( + value: T, + fromIndex?: boolean|number + ): LoDashExplicitWrapper<number>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.lastIndexOf + */ + lastIndexOf<TResult>( + value: TResult, + fromIndex?: boolean|number + ): LoDashExplicitWrapper<number>; + } + + //_.object + interface LoDashStatic { + /** + * @see _.zipObject + */ + object<TValues, TResult extends {}>( + props: List<StringRepresentable>|List<List<any>>, + values?: List<TValues> + ): TResult; + + /** + * @see _.zipObject + */ + object<TResult extends {}>( + props: List<StringRepresentable>|List<List<any>>, + values?: List<any> + ): TResult; + + /** + * @see _.zipObject + */ + object( + props: List<StringRepresentable>|List<List<any>>, + values?: List<any> + ): _.Dictionary<any>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.zipObject + */ + object<TValues, TResult extends {}>( + values?: List<TValues> + ): _.LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.zipObject + */ + object<TResult extends {}>( + values?: List<any> + ): _.LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.zipObject + */ + object( + values?: List<any> + ): _.LoDashImplicitObjectWrapper<_.Dictionary<any>>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.zipObject + */ + object<TValues, TResult extends {}>( + values?: List<TValues> + ): _.LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.zipObject + */ + object<TResult extends {}>( + values?: List<any> + ): _.LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.zipObject + */ + object( + values?: List<any> + ): _.LoDashImplicitObjectWrapper<_.Dictionary<any>>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.zipObject + */ + object<TValues, TResult extends {}>( + values?: List<TValues> + ): _.LoDashExplicitObjectWrapper<TResult>; + + /** + * @see _.zipObject + */ + object<TResult extends {}>( + values?: List<any> + ): _.LoDashExplicitObjectWrapper<TResult>; + + /** + * @see _.zipObject + */ + object( + values?: List<any> + ): _.LoDashExplicitObjectWrapper<_.Dictionary<any>>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.zipObject + */ + object<TValues, TResult extends {}>( + values?: List<TValues> + ): _.LoDashExplicitObjectWrapper<TResult>; + + /** + * @see _.zipObject + */ + object<TResult extends {}>( + values?: List<any> + ): _.LoDashExplicitObjectWrapper<TResult>; + + /** + * @see _.zipObject + */ + object( + values?: List<any> + ): _.LoDashExplicitObjectWrapper<_.Dictionary<any>>; + } + + //_.pull + interface LoDashStatic { + /** + * Removes all provided values from array using SameValueZero for equality comparisons. + * + * Note: Unlike _.without, this method mutates array. + * + * @param array The array to modify. + * @param values The values to remove. + * @return Returns array. + */ + pull<T>( + array: T[], + ...values: T[] + ): T[]; + + /** + * @see _.pull + */ + pull<T>( + array: List<T>, + ...values: T[] + ): List<T>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.pull + */ + pull(...values: T[]): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.pull + */ + pull<TValue>(...values: TValue[]): LoDashImplicitObjectWrapper<List<TValue>>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.pull + */ + pull(...values: T[]): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.pull + */ + pull<TValue>(...values: TValue[]): LoDashExplicitObjectWrapper<List<TValue>>; + } + + //_.pullAt + interface LoDashStatic { + /** + * Removes elements from array corresponding to the given indexes and returns an array of the removed elements. + * Indexes may be specified as an array of indexes or as individual arguments. + * + * Note: Unlike _.at, this method mutates array. + * + * @param array The array to modify. + * @param indexes The indexes of elements to remove, specified as individual indexes or arrays of indexes. + * @return Returns the new array of removed elements. + */ + pullAt<T>( + array: List<T>, + ...indexes: (number|number[])[] + ): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.pullAt + */ + pullAt(...indexes: (number|number[])[]): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.pullAt + */ + pullAt<T>(...indexes: (number|number[])[]): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.pullAt + */ + pullAt(...indexes: (number|number[])[]): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.pullAt + */ + pullAt<T>(...indexes: (number|number[])[]): LoDashExplicitArrayWrapper<T>; + } + + //_.remove + interface LoDashStatic { + /** + * Removes all elements from array that predicate returns truthy for and returns an array of the removed + * elements. The predicate is bound to thisArg and invoked with three arguments: (value, index, array). + * + * If a property name is provided for predicate the created _.property style callback returns the property + * value of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for predicate the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * Note: Unlike _.filter, this method mutates array. + * + * @param array The array to modify. + * @param predicate The function invoked per iteration. + * @param thisArg The this binding of predicate. + * @return Returns the new array of removed elements. + */ + remove<T>( + array: List<T>, + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): T[]; + + /** + * @see _.remove + */ + remove<T>( + array: List<T>, + predicate?: string, + thisArg?: any + ): T[]; + + /** + * @see _.remove + */ + remove<W, T>( + array: List<T>, + predicate?: W + ): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.remove + */ + remove( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.remove + */ + remove( + predicate?: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.remove + */ + remove<W>( + predicate?: W + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.remove + */ + remove<TResult>( + predicate?: ListIterator<TResult, boolean>, + thisArg?: any + ): LoDashImplicitArrayWrapper<TResult>; + + /** + * @see _.remove + */ + remove<TResult>( + predicate?: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<TResult>; + + /** + * @see _.remove + */ + remove<W, TResult>( + predicate?: W + ): LoDashImplicitArrayWrapper<TResult>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.remove + */ + remove( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.remove + */ + remove( + predicate?: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.remove + */ + remove<W>( + predicate?: W + ): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.remove + */ + remove<TResult>( + predicate?: ListIterator<TResult, boolean>, + thisArg?: any + ): LoDashExplicitArrayWrapper<TResult>; + + /** + * @see _.remove + */ + remove<TResult>( + predicate?: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<TResult>; + + /** + * @see _.remove + */ + remove<W, TResult>( + predicate?: W + ): LoDashExplicitArrayWrapper<TResult>; + } + + //_.rest + interface LoDashStatic { + /** + * Gets all but the first element of array. + * + * @alias _.tail + * + * @param array The array to query. + * @return Returns the slice of array. + */ + rest<T>(array: List<T>): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.rest + */ + rest(): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.rest + */ + rest<T>(): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.rest + */ + rest(): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.rest + */ + rest<T>(): LoDashExplicitArrayWrapper<T>; + } + + //_.slice + interface LoDashStatic { + /** + * Creates a slice of array from start up to, but not including, end. + * + * @param array The array to slice. + * @param start The start position. + * @param end The end position. + * @return Returns the slice of array. + */ + slice<T>( + array: T[], + start?: number, + end?: number + ): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.slice + */ + slice( + start?: number, + end?: number + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.slice + */ + slice( + start?: number, + end?: number + ): LoDashExplicitArrayWrapper<T>; + } + + //_.sortedIndex + interface LoDashStatic { + /** + * Uses a binary search to determine the lowest index at which value should be inserted into array in order to maintain its sort order. If an iteratee function is provided it’s invoked for value and each element of array to compute their sort ranking. The iteratee is bound to thisArg and invoked with one argument; (value). + * + * If a property name is provided for iteratee the created _.property style callback returns the property value of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for elements that have a matching property value, else false. + * + * If an object is provided for iteratee the created _.matches style callback returns true for elements that have the properties of the given object, else false. + * + * @param array The sorted array to inspect. + * @param value The value to evaluate. + * @param iteratee The function invoked per iteration. + * @return The this binding of iteratee. + */ + sortedIndex<T, TSort>( + array: List<T>, + value: T, + iteratee?: (x: T) => TSort, + thisArg?: any + ): number; + + /** + * @see _.sortedIndex + */ + sortedIndex<T>( + array: List<T>, + value: T, + iteratee?: (x: T) => any, + thisArg?: any + ): number; + + /** + * @see _.sortedIndex + */ + sortedIndex<T>( + array: List<T>, + value: T, + iteratee: string + ): number; + + /** + * @see _.sortedIndex + */ + sortedIndex<W, T>( + array: List<T>, + value: T, + iteratee: W + ): number; + + /** + * @see _.sortedIndex + */ + sortedIndex<T>( + array: List<T>, + value: T, + iteratee: Object + ): number; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.sortedIndex + */ + sortedIndex<TSort>( + value: string, + iteratee?: (x: string) => TSort, + thisArg?: any + ): number; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.sortedIndex + */ + sortedIndex<TSort>( + value: T, + iteratee?: (x: T) => TSort, + thisArg?: any + ): number; + + /** + * @see _.sortedIndex + */ + sortedIndex( + value: T, + iteratee: string + ): number; + + /** + * @see _.sortedIndex + */ + sortedIndex<W>( + value: T, + iteratee: W + ): number; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.sortedIndex + */ + sortedIndex<T, TSort>( + value: T, + iteratee?: (x: T) => TSort, + thisArg?: any + ): number; + + /** + * @see _.sortedIndex + */ + sortedIndex<T>( + value: T, + iteratee?: (x: T) => any, + thisArg?: any + ): number; + + /** + * @see _.sortedIndex + */ + sortedIndex<T>( + value: T, + iteratee: string + ): number; + + /** + * @see _.sortedIndex + */ + sortedIndex<W, T>( + value: T, + iteratee: W + ): number; + + /** + * @see _.sortedIndex + */ + sortedIndex<T>( + value: T, + iteratee: Object + ): number; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.sortedIndex + */ + sortedIndex<TSort>( + value: string, + iteratee?: (x: string) => TSort, + thisArg?: any + ): LoDashExplicitWrapper<number>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.sortedIndex + */ + sortedIndex<TSort>( + value: T, + iteratee?: (x: T) => TSort, + thisArg?: any + ): LoDashExplicitWrapper<number>; + + /** + * @see _.sortedIndex + */ + sortedIndex( + value: T, + iteratee: string + ): LoDashExplicitWrapper<number>; + + /** + * @see _.sortedIndex + */ + sortedIndex<W>( + value: T, + iteratee: W + ): LoDashExplicitWrapper<number>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.sortedIndex + */ + sortedIndex<T, TSort>( + value: T, + iteratee?: (x: T) => TSort, + thisArg?: any + ): LoDashExplicitWrapper<number>; + + /** + * @see _.sortedIndex + */ + sortedIndex<T>( + value: T, + iteratee?: (x: T) => any, + thisArg?: any + ): LoDashExplicitWrapper<number>; + + /** + * @see _.sortedIndex + */ + sortedIndex<T>( + value: T, + iteratee: string + ): LoDashExplicitWrapper<number>; + + /** + * @see _.sortedIndex + */ + sortedIndex<W, T>( + value: T, + iteratee: W + ): LoDashExplicitWrapper<number>; + + /** + * @see _.sortedIndex + */ + sortedIndex<T>( + value: T, + iteratee: Object + ): LoDashExplicitWrapper<number>; + } + + //_.sortedLastIndex + interface LoDashStatic { + /** + * This method is like _.sortedIndex except that it returns the highest index at which value should be + * inserted into array in order to maintain its sort order. + * + * @param array The sorted array to inspect. + * @param value The value to evaluate. + * @param iteratee The function invoked per iteration. + * @param thisArg The this binding of iteratee. + * @return Returns the index at which value should be inserted into array. + */ + sortedLastIndex<T, TSort>( + array: List<T>, + value: T, + iteratee?: (x: T) => TSort, + thisArg?: any + ): number; + + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<T>( + array: List<T>, + value: T, + iteratee?: (x: T) => any, + thisArg?: any + ): number; + + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<T>( + array: List<T>, + value: T, + iteratee: string + ): number; + + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<W, T>( + array: List<T>, + value: T, + iteratee: W + ): number; + + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<T>( + array: List<T>, + value: T, + iteratee: Object + ): number; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<TSort>( + value: string, + iteratee?: (x: string) => TSort, + thisArg?: any + ): number; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<TSort>( + value: T, + iteratee?: (x: T) => TSort, + thisArg?: any + ): number; + + /** + * @see _.sortedLastIndex + */ + sortedLastIndex( + value: T, + iteratee: string + ): number; + + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<W>( + value: T, + iteratee: W + ): number; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<T, TSort>( + value: T, + iteratee?: (x: T) => TSort, + thisArg?: any + ): number; + + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<T>( + value: T, + iteratee?: (x: T) => any, + thisArg?: any + ): number; + + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<T>( + value: T, + iteratee: string + ): number; + + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<W, T>( + value: T, + iteratee: W + ): number; + + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<T>( + value: T, + iteratee: Object + ): number; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<TSort>( + value: string, + iteratee?: (x: string) => TSort, + thisArg?: any + ): LoDashExplicitWrapper<number>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<TSort>( + value: T, + iteratee?: (x: T) => TSort, + thisArg?: any + ): LoDashExplicitWrapper<number>; + + /** + * @see _.sortedLastIndex + */ + sortedLastIndex( + value: T, + iteratee: string + ): LoDashExplicitWrapper<number>; + + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<W>( + value: T, + iteratee: W + ): LoDashExplicitWrapper<number>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<T, TSort>( + value: T, + iteratee?: (x: T) => TSort, + thisArg?: any + ): LoDashExplicitWrapper<number>; + + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<T>( + value: T, + iteratee?: (x: T) => any, + thisArg?: any + ): LoDashExplicitWrapper<number>; + + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<T>( + value: T, + iteratee: string + ): LoDashExplicitWrapper<number>; + + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<W, T>( + value: T, + iteratee: W + ): LoDashExplicitWrapper<number>; + + /** + * @see _.sortedLastIndex + */ + sortedLastIndex<T>( + value: T, + iteratee: Object + ): LoDashExplicitWrapper<number>; + } + + //_.tail + interface LoDashStatic { + /** + * @see _.rest + */ + tail<T>(array: List<T>): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.rest + */ + tail(): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.rest + */ + tail<T>(): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.rest + */ + tail(): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.rest + */ + tail<T>(): LoDashExplicitArrayWrapper<T>; + } + + //_.take + interface LoDashStatic { + /** + * Creates a slice of array with n elements taken from the beginning. + * + * @param array The array to query. + * @param n The number of elements to take. + * @return Returns the slice of array. + */ + take<T>( + array: List<T>, + n?: number + ): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.take + */ + take(n?: number): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.take + */ + take<TResult>(n?: number): LoDashImplicitArrayWrapper<TResult>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.take + */ + take(n?: number): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.take + */ + take<TResult>(n?: number): LoDashExplicitArrayWrapper<TResult>; + } + + //_.takeRight + interface LoDashStatic { + /** + * Creates a slice of array with n elements taken from the end. + * + * @param array The array to query. + * @param n The number of elements to take. + * @return Returns the slice of array. + */ + takeRight<T>( + array: List<T>, + n?: number + ): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.takeRight + */ + takeRight(n?: number): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.takeRight + */ + takeRight<TResult>(n?: number): LoDashImplicitArrayWrapper<TResult>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.takeRight + */ + takeRight(n?: number): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.takeRight + */ + takeRight<TResult>(n?: number): LoDashExplicitArrayWrapper<TResult>; + } + + //_.takeRightWhile + interface LoDashStatic { + /** + * Creates a slice of array with elements taken from the end. Elements are taken until predicate returns + * falsey. The predicate is bound to thisArg and invoked with three arguments: (value, index, array). + * + * If a property name is provided for predicate the created _.property style callback returns the property + * value of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for predicate the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * @param array The array to query. + * @param predicate The function invoked per iteration. + * @param thisArg The this binding of predicate. + * @return Returns the slice of array. + */ + takeRightWhile<TValue>( + array: List<TValue>, + predicate?: ListIterator<TValue, boolean>, + thisArg?: any + ): TValue[]; + + /** + * @see _.takeRightWhile + */ + takeRightWhile<TValue>( + array: List<TValue>, + predicate?: string, + thisArg?: any + ): TValue[]; + + /** + * @see _.takeRightWhile + */ + takeRightWhile<TWhere, TValue>( + array: List<TValue>, + predicate?: TWhere + ): TValue[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.takeRightWhile + */ + takeRightWhile( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.takeRightWhile + */ + takeRightWhile( + predicate?: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.takeRightWhile + */ + takeRightWhile<TWhere>( + predicate?: TWhere + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.takeRightWhile + */ + takeRightWhile<TValue>( + predicate?: ListIterator<TValue, boolean>, + thisArg?: any + ): LoDashImplicitArrayWrapper<TValue>; + + /** + * @see _.takeRightWhile + */ + takeRightWhile<TValue>( + predicate?: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<TValue>; + + /** + * @see _.takeRightWhile + */ + takeRightWhile<TWhere, TValue>( + predicate?: TWhere + ): LoDashImplicitArrayWrapper<TValue>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.takeRightWhile + */ + takeRightWhile( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.takeRightWhile + */ + takeRightWhile( + predicate?: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.takeRightWhile + */ + takeRightWhile<TWhere>( + predicate?: TWhere + ): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.takeRightWhile + */ + takeRightWhile<TValue>( + predicate?: ListIterator<TValue, boolean>, + thisArg?: any + ): LoDashExplicitArrayWrapper<TValue>; + + /** + * @see _.takeRightWhile + */ + takeRightWhile<TValue>( + predicate?: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<TValue>; + + /** + * @see _.takeRightWhile + */ + takeRightWhile<TWhere, TValue>( + predicate?: TWhere + ): LoDashExplicitArrayWrapper<TValue>; + } + + //_.takeWhile + interface LoDashStatic { + /** + * Creates a slice of array with elements taken from the beginning. Elements are taken until predicate returns + * falsey. The predicate is bound to thisArg and invoked with three arguments: (value, index, array). + * + * If a property name is provided for predicate the created _.property style callback returns the property + * value of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for predicate the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * @param array The array to query. + * @param predicate The function invoked per iteration. + * @param thisArg The this binding of predicate. + * @return Returns the slice of array. + */ + takeWhile<TValue>( + array: List<TValue>, + predicate?: ListIterator<TValue, boolean>, + thisArg?: any + ): TValue[]; + + /** + * @see _.takeWhile + */ + takeWhile<TValue>( + array: List<TValue>, + predicate?: string, + thisArg?: any + ): TValue[]; + + /** + * @see _.takeWhile + */ + takeWhile<TWhere, TValue>( + array: List<TValue>, + predicate?: TWhere + ): TValue[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.takeWhile + */ + takeWhile( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.takeWhile + */ + takeWhile( + predicate?: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.takeWhile + */ + takeWhile<TWhere>( + predicate?: TWhere + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.takeWhile + */ + takeWhile<TValue>( + predicate?: ListIterator<TValue, boolean>, + thisArg?: any + ): LoDashImplicitArrayWrapper<TValue>; + + /** + * @see _.takeWhile + */ + takeWhile<TValue>( + predicate?: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<TValue>; + + /** + * @see _.takeWhile + */ + takeWhile<TWhere, TValue>( + predicate?: TWhere + ): LoDashImplicitArrayWrapper<TValue>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.takeWhile + */ + takeWhile( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.takeWhile + */ + takeWhile( + predicate?: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.takeWhile + */ + takeWhile<TWhere>( + predicate?: TWhere + ): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.takeWhile + */ + takeWhile<TValue>( + predicate?: ListIterator<TValue, boolean>, + thisArg?: any + ): LoDashExplicitArrayWrapper<TValue>; + + /** + * @see _.takeWhile + */ + takeWhile<TValue>( + predicate?: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<TValue>; + + /** + * @see _.takeWhile + */ + takeWhile<TWhere, TValue>( + predicate?: TWhere + ): LoDashExplicitArrayWrapper<TValue>; + } + + //_.union + interface LoDashStatic { + /** + * Creates an array of unique values, in order, from all of the provided arrays using SameValueZero for + * equality comparisons. + * + * @param arrays The arrays to inspect. + * @return Returns the new array of combined values. + */ + union<T>(...arrays: List<T>[]): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.union + */ + union(...arrays: List<T>[]): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.union + */ + union<T>(...arrays: List<T>[]): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.union + */ + union<T>(...arrays: List<T>[]): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.union + */ + union(...arrays: List<T>[]): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.union + */ + union<T>(...arrays: List<T>[]): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.union + */ + union<T>(...arrays: List<T>[]): LoDashExplicitArrayWrapper<T>; + } + + //_.uniq + interface LoDashStatic { + /** + * Creates a duplicate-free version of an array, using SameValueZero for equality comparisons, in which only + * the first occurrence of each element is kept. Providing true for isSorted performs a faster search + * algorithm for sorted arrays. If an iteratee function is provided it’s invoked for each element in the + * array to generate the criterion by which uniqueness is computed. The iteratee is bound to thisArg and + * invoked with three arguments: (value, index, array). + * + * If a property name is provided for iteratee the created _.property style callback returns the property + * value of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for iteratee the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * @alias _.unique + * + * @param array The array to inspect. + * @param isSorted Specify the array is sorted. + * @param iteratee The function invoked per iteration. + * @param thisArg iteratee + * @return Returns the new duplicate-value-free array. + */ + uniq<T>( + array: List<T>, + isSorted?: boolean, + iteratee?: ListIterator<T, any>, + thisArg?: any + ): T[]; + + /** + * @see _.uniq + */ + uniq<T, TSort>( + array: List<T>, + isSorted?: boolean, + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): T[]; + + /** + * @see _.uniq + */ + uniq<T>( + array: List<T>, + iteratee?: ListIterator<T, any>, + thisArg?: any + ): T[]; + + /** + * @see _.uniq + */ + uniq<T, TSort>( + array: List<T>, + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): T[]; + + /** + * @see _.uniq + */ + uniq<T>( + array: List<T>, + isSorted?: boolean, + iteratee?: string, + thisArg?: any + ): T[]; + + /** + * @see _.uniq + */ + uniq<T>( + array: List<T>, + iteratee?: string, + thisArg?: any + ): T[]; + + /** + * @see _.uniq + */ + uniq<T>( + array: List<T>, + isSorted?: boolean, + iteratee?: Object + ): T[]; + + /** + * @see _.uniq + */ + uniq<TWhere extends {}, T>( + array: List<T>, + isSorted?: boolean, + iteratee?: TWhere + ): T[]; + + /** + * @see _.uniq + */ + uniq<T>( + array: List<T>, + iteratee?: Object + ): T[]; + + /** + * @see _.uniq + */ + uniq<TWhere extends {}, T>( + array: List<T>, + iteratee?: TWhere + ): T[]; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.uniq + */ + uniq<TSort>( + isSorted?: boolean, + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<TSort>( + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.uniq + */ + uniq<TSort>( + isSorted?: boolean, + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<TSort>( + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq( + isSorted?: boolean, + iteratee?: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq( + iteratee?: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<TWhere extends {}>( + isSorted?: boolean, + iteratee?: TWhere + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<TWhere extends {}>( + iteratee?: TWhere + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + uniq<T>( + isSorted?: boolean, + iteratee?: ListIterator<T, any>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<T, TSort>( + isSorted?: boolean, + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<T>( + iteratee?: ListIterator<T, any>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<T, TSort>( + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<T>( + isSorted?: boolean, + iteratee?: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<T>( + iteratee?: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<T>( + isSorted?: boolean, + iteratee?: Object + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<TWhere extends {}, T>( + isSorted?: boolean, + iteratee?: TWhere + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<T>( + iteratee?: Object + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<TWhere extends {}, T>( + iteratee?: TWhere + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.uniq + */ + uniq<TSort>( + isSorted?: boolean, + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<TSort>( + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.uniq + */ + uniq<TSort>( + isSorted?: boolean, + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<TSort>( + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq( + isSorted?: boolean, + iteratee?: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq( + iteratee?: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<TWhere extends {}>( + isSorted?: boolean, + iteratee?: TWhere + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<TWhere extends {}>( + iteratee?: TWhere + ): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + uniq<T>( + isSorted?: boolean, + iteratee?: ListIterator<T, any>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<T, TSort>( + isSorted?: boolean, + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<T>( + iteratee?: ListIterator<T, any>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<T, TSort>( + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<T>( + isSorted?: boolean, + iteratee?: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<T>( + iteratee?: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<T>( + isSorted?: boolean, + iteratee?: Object + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<TWhere extends {}, T>( + isSorted?: boolean, + iteratee?: TWhere + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<T>( + iteratee?: Object + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + uniq<TWhere extends {}, T>( + iteratee?: TWhere + ): LoDashExplicitArrayWrapper<T>; + } + + //_.unique + interface LoDashStatic { + /** + * @see _.uniq + */ + unique<T>( + array: List<T>, + isSorted?: boolean, + iteratee?: ListIterator<T, any>, + thisArg?: any + ): T[]; + + /** + * @see _.uniq + */ + unique<T, TSort>( + array: List<T>, + isSorted?: boolean, + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): T[]; + + /** + * @see _.uniq + */ + unique<T>( + array: List<T>, + iteratee?: ListIterator<T, any>, + thisArg?: any + ): T[]; + + /** + * @see _.uniq + */ + unique<T, TSort>( + array: List<T>, + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): T[]; + + /** + * @see _.uniq + */ + unique<T>( + array: List<T>, + isSorted?: boolean, + iteratee?: string, + thisArg?: any + ): T[]; + + /** + * @see _.uniq + */ + unique<T>( + array: List<T>, + iteratee?: string, + thisArg?: any + ): T[]; + + /** + * @see _.uniq + */ + unique<T>( + array: List<T>, + isSorted?: boolean, + iteratee?: Object + ): T[]; + + /** + * @see _.uniq + */ + unique<TWhere extends {}, T>( + array: List<T>, + isSorted?: boolean, + iteratee?: TWhere + ): T[]; + + /** + * @see _.uniq + */ + unique<T>( + array: List<T>, + iteratee?: Object + ): T[]; + + /** + * @see _.uniq + */ + unique<TWhere extends {}, T>( + array: List<T>, + iteratee?: TWhere + ): T[]; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.uniq + */ + unique<TSort>( + isSorted?: boolean, + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<TSort>( + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.uniq + */ + unique<TSort>( + isSorted?: boolean, + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<TSort>( + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique( + isSorted?: boolean, + iteratee?: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique( + iteratee?: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<TWhere extends {}>( + isSorted?: boolean, + iteratee?: TWhere + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<TWhere extends {}>( + iteratee?: TWhere + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + unique<T>( + isSorted?: boolean, + iteratee?: ListIterator<T, any>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<T, TSort>( + isSorted?: boolean, + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<T>( + iteratee?: ListIterator<T, any>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<T, TSort>( + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<T>( + isSorted?: boolean, + iteratee?: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<T>( + iteratee?: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<T>( + isSorted?: boolean, + iteratee?: Object + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<TWhere extends {}, T>( + isSorted?: boolean, + iteratee?: TWhere + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<T>( + iteratee?: Object + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<TWhere extends {}, T>( + iteratee?: TWhere + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.uniq + */ + unique<TSort>( + isSorted?: boolean, + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<TSort>( + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.uniq + */ + unique<TSort>( + isSorted?: boolean, + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<TSort>( + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique( + isSorted?: boolean, + iteratee?: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique( + iteratee?: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<TWhere extends {}>( + isSorted?: boolean, + iteratee?: TWhere + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<TWhere extends {}>( + iteratee?: TWhere + ): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + unique<T>( + isSorted?: boolean, + iteratee?: ListIterator<T, any>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<T, TSort>( + isSorted?: boolean, + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<T>( + iteratee?: ListIterator<T, any>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<T, TSort>( + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<T>( + isSorted?: boolean, + iteratee?: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<T>( + iteratee?: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<T>( + isSorted?: boolean, + iteratee?: Object + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<TWhere extends {}, T>( + isSorted?: boolean, + iteratee?: TWhere + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<T>( + iteratee?: Object + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.uniq + */ + unique<TWhere extends {}, T>( + iteratee?: TWhere + ): LoDashExplicitArrayWrapper<T>; + } + + //_.unzip + interface LoDashStatic { + /** + * This method is like _.zip except that it accepts an array of grouped elements and creates an array + * regrouping the elements to their pre-zip configuration. + * + * @param array The array of grouped elements to process. + * @return Returns the new array of regrouped elements. + */ + unzip<T>(array: List<List<T>>): T[][]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.unzip + */ + unzip<T>(): LoDashImplicitArrayWrapper<T[]>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.unzip + */ + unzip<T>(): LoDashImplicitArrayWrapper<T[]>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.unzip + */ + unzip<T>(): LoDashExplicitArrayWrapper<T[]>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.unzip + */ + unzip<T>(): LoDashExplicitArrayWrapper<T[]>; + } + + //_.unzipWith + interface LoDashStatic { + /** + * This method is like _.unzip except that it accepts an iteratee to specify how regrouped values should be + * combined. The iteratee is bound to thisArg and invoked with four arguments: (accumulator, value, index, + * group). + * + * @param array The array of grouped elements to process. + * @param iteratee The function to combine regrouped values. + * @param thisArg The this binding of iteratee. + * @return Returns the new array of regrouped elements. + */ + unzipWith<TArray, TResult>( + array: List<List<TArray>>, + iteratee?: MemoIterator<TArray, TResult>, + thisArg?: any + ): TResult[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.unzipWith + */ + unzipWith<TArr, TResult>( + iteratee?: MemoIterator<TArr, TResult>, + thisArg?: any + ): LoDashImplicitArrayWrapper<TResult>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.unzipWith + */ + unzipWith<TArr, TResult>( + iteratee?: MemoIterator<TArr, TResult>, + thisArg?: any + ): LoDashImplicitArrayWrapper<TResult>; + } + + //_.without + interface LoDashStatic { + /** + * Creates an array excluding all provided values using SameValueZero for equality comparisons. + * + * @param array The array to filter. + * @param values The values to exclude. + * @return Returns the new array of filtered values. + */ + without<T>( + array: List<T>, + ...values: T[] + ): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.without + */ + without(...values: T[]): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.without + */ + without<T>(...values: T[]): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.without + */ + without(...values: T[]): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.without + */ + without<T>(...values: T[]): LoDashExplicitArrayWrapper<T>; + } + + //_.xor + interface LoDashStatic { + /** + * Creates an array of unique values that is the symmetric difference of the provided arrays. + * + * @param arrays The arrays to inspect. + * @return Returns the new array of values. + */ + xor<T>(...arrays: List<T>[]): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.xor + */ + xor(...arrays: List<T>[]): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.xor + */ + xor<T>(...arrays: List<T>[]): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.xor + */ + xor(...arrays: List<T>[]): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.xor + */ + xor<T>(...arrays: List<T>[]): LoDashExplicitArrayWrapper<T>; + } + + //_.zip + interface LoDashStatic { + /** + * Creates an array of grouped elements, the first of which contains the first elements of the given arrays, + * the second of which contains the second elements of the given arrays, and so on. + * + * @param arrays The arrays to process. + * @return Returns the new array of grouped elements. + */ + zip<T>(...arrays: List<T>[]): T[][]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.zip + */ + zip<T>(...arrays: List<T>[]): _.LoDashImplicitArrayWrapper<T[]>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.zip + */ + zip<T>(...arrays: List<T>[]): _.LoDashImplicitArrayWrapper<T[]>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.zip + */ + zip<T>(...arrays: List<T>[]): _.LoDashExplicitArrayWrapper<T[]>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.zip + */ + zip<T>(...arrays: List<T>[]): _.LoDashExplicitArrayWrapper<T[]>; + } + + //_.zipObject + interface LoDashStatic { + /** + * The inverse of _.pairs; this method returns an object composed from arrays of property names and values. + * Provide either a single two dimensional array, e.g. [[key1, value1], [key2, value2]] or two arrays, one of + * property names and one of corresponding values. + * + * @alias _.object + * + * @param props The property names. + * @param values The property values. + * @return Returns the new object. + */ + zipObject<TValues, TResult extends {}>( + props: List<StringRepresentable>|List<List<any>>, + values?: List<TValues> + ): TResult; + + /** + * @see _.zipObject + */ + zipObject<TResult extends {}>( + props: List<StringRepresentable>|List<List<any>>, + values?: List<any> + ): TResult; + + /** + * @see _.zipObject + */ + zipObject( + props: List<StringRepresentable>|List<List<any>>, + values?: List<any> + ): _.Dictionary<any>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.zipObject + */ + zipObject<TValues, TResult extends {}>( + values?: List<TValues> + ): _.LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.zipObject + */ + zipObject<TResult extends {}>( + values?: List<any> + ): _.LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.zipObject + */ + zipObject( + values?: List<any> + ): _.LoDashImplicitObjectWrapper<_.Dictionary<any>>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.zipObject + */ + zipObject<TValues, TResult extends {}>( + values?: List<TValues> + ): _.LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.zipObject + */ + zipObject<TResult extends {}>( + values?: List<any> + ): _.LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.zipObject + */ + zipObject( + values?: List<any> + ): _.LoDashImplicitObjectWrapper<_.Dictionary<any>>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.zipObject + */ + zipObject<TValues, TResult extends {}>( + values?: List<TValues> + ): _.LoDashExplicitObjectWrapper<TResult>; + + /** + * @see _.zipObject + */ + zipObject<TResult extends {}>( + values?: List<any> + ): _.LoDashExplicitObjectWrapper<TResult>; + + /** + * @see _.zipObject + */ + zipObject( + values?: List<any> + ): _.LoDashExplicitObjectWrapper<_.Dictionary<any>>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.zipObject + */ + zipObject<TValues, TResult extends {}>( + values?: List<TValues> + ): _.LoDashExplicitObjectWrapper<TResult>; + + /** + * @see _.zipObject + */ + zipObject<TResult extends {}>( + values?: List<any> + ): _.LoDashExplicitObjectWrapper<TResult>; + + /** + * @see _.zipObject + */ + zipObject( + values?: List<any> + ): _.LoDashExplicitObjectWrapper<_.Dictionary<any>>; + } + + //_.zipWith + interface LoDashStatic { + /** + * This method is like _.zip except that it accepts an iteratee to specify how grouped values should be + * combined. The iteratee is bound to thisArg and invoked with four arguments: (accumulator, value, index, + * group). + * @param {...Array} [arrays] The arrays to process. + * @param {Function} [iteratee] The function to combine grouped values. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @return Returns the new array of grouped elements. + */ + zipWith<TResult>(...args: any[]): TResult[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.zipWith + */ + zipWith<TResult>(...args: any[]): LoDashImplicitArrayWrapper<TResult>; + } + + /********* + * Chain * + *********/ + + //_.chain + interface LoDashStatic { + /** + * Creates a lodash object that wraps value with explicit method chaining enabled. + * + * @param value The value to wrap. + * @return Returns the new lodash wrapper instance. + */ + chain(value: number): LoDashExplicitWrapper<number>; + chain(value: string): LoDashExplicitWrapper<string>; + chain(value: boolean): LoDashExplicitWrapper<boolean>; + chain<T>(value: T[]): LoDashExplicitArrayWrapper<T>; + chain<T extends {}>(value: T): LoDashExplicitObjectWrapper<T>; + chain(value: any): LoDashExplicitWrapper<any>; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.chain + */ + chain(): LoDashExplicitWrapper<T>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.chain + */ + chain(): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.chain + */ + chain(): LoDashExplicitObjectWrapper<T>; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.chain + */ + chain(): TWrapper; + } + + //_.tap + interface LoDashStatic { + /** + * This method invokes interceptor and returns value. The interceptor is bound to thisArg and invoked with one + * argument; (value). The purpose of this method is to "tap into" a method chain in order to perform operations + * on intermediate results within the chain. + * + * @param value The value to provide to interceptor. + * @param interceptor The function to invoke. + * @parem thisArg The this binding of interceptor. + * @return Returns value. + **/ + tap<T>( + value: T, + interceptor: (value: T) => void, + thisArg?: any + ): T; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.tap + */ + tap( + interceptor: (value: T) => void, + thisArg?: any + ): TWrapper; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.tap + */ + tap( + interceptor: (value: T) => void, + thisArg?: any + ): TWrapper; + } + + //_.thru + interface LoDashStatic { + /** + * This method is like _.tap except that it returns the result of interceptor. + * + * @param value The value to provide to interceptor. + * @param interceptor The function to invoke. + * @param thisArg The this binding of interceptor. + * @return Returns the result of interceptor. + */ + thru<T, TResult>( + value: T, + interceptor: (value: T) => TResult, + thisArg?: any + ): TResult; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.thru + */ + thru<TResult extends number>( + interceptor: (value: T) => TResult, + thisArg?: any): LoDashImplicitWrapper<TResult>; + + /** + * @see _.thru + */ + thru<TResult extends string>( + interceptor: (value: T) => TResult, + thisArg?: any): LoDashImplicitWrapper<TResult>; + + /** + * @see _.thru + */ + thru<TResult extends boolean>( + interceptor: (value: T) => TResult, + thisArg?: any): LoDashImplicitWrapper<TResult>; + + /** + * @see _.thru + */ + thru<TResult extends {}>( + interceptor: (value: T) => TResult, + thisArg?: any): LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.thru + */ + thru<TResult>( + interceptor: (value: T) => TResult[], + thisArg?: any): LoDashImplicitArrayWrapper<TResult>; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.thru + */ + thru<TResult extends number>( + interceptor: (value: T) => TResult, + thisArg?: any + ): LoDashExplicitWrapper<TResult>; + + /** + * @see _.thru + */ + thru<TResult extends string>( + interceptor: (value: T) => TResult, + thisArg?: any + ): LoDashExplicitWrapper<TResult>; + + /** + * @see _.thru + */ + thru<TResult extends boolean>( + interceptor: (value: T) => TResult, + thisArg?: any + ): LoDashExplicitWrapper<TResult>; + + /** + * @see _.thru + */ + thru<TResult extends {}>( + interceptor: (value: T) => TResult, + thisArg?: any + ): LoDashExplicitObjectWrapper<TResult>; + + /** + * @see _.thru + */ + thru<TResult>( + interceptor: (value: T) => TResult[], + thisArg?: any + ): LoDashExplicitArrayWrapper<TResult>; + } + + //_.prototype.commit + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * Executes the chained sequence and returns the wrapped result. + * + * @return Returns the new lodash wrapper instance. + */ + commit(): TWrapper; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.commit + */ + commit(): TWrapper; + } + + //_.prototype.concat + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * Creates a new array joining a wrapped array with any additional arrays and/or values. + * + * @param items + * @return Returns the new concatenated array. + */ + concat<TItem>(...items: Array<TItem|Array<TItem>>): LoDashImplicitArrayWrapper<TItem>; + + /** + * @see _.concat + */ + concat(...items: Array<T|Array<T>>): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.concat + */ + concat<TItem>(...items: Array<TItem|Array<TItem>>): LoDashExplicitArrayWrapper<TItem>; + + /** + * @see _.concat + */ + concat(...items: Array<T|Array<T>>): LoDashExplicitArrayWrapper<T>; + } + + //_.prototype.plant + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * Creates a clone of the chained sequence planting value as the wrapped value. + * @param value The value to plant as the wrapped value. + * @return Returns the new lodash wrapper instance. + */ + plant(value: number): LoDashImplicitWrapper<number>; + + /** + * @see _.plant + */ + plant(value: string): LoDashImplicitStringWrapper; + + /** + * @see _.plant + */ + plant(value: boolean): LoDashImplicitWrapper<boolean>; + + /** + * @see _.plant + */ + plant(value: number[]): LoDashImplicitNumberArrayWrapper; + + /** + * @see _.plant + */ + plant<T>(value: T[]): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.plant + */ + plant<T extends {}>(value: T): LoDashImplicitObjectWrapper<T>; + + /** + * @see _.plant + */ + plant(value: any): LoDashImplicitWrapper<any>; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.plant + */ + plant(value: number): LoDashExplicitWrapper<number>; + + /** + * @see _.plant + */ + plant(value: string): LoDashExplicitStringWrapper; + + /** + * @see _.plant + */ + plant(value: boolean): LoDashExplicitWrapper<boolean>; + + /** + * @see _.plant + */ + plant(value: number[]): LoDashExplicitNumberArrayWrapper; + + /** + * @see _.plant + */ + plant<T>(value: T[]): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.plant + */ + plant<T extends {}>(value: T): LoDashExplicitObjectWrapper<T>; + + /** + * @see _.plant + */ + plant(value: any): LoDashExplicitWrapper<any>; + } + + //_.prototype.reverse + interface LoDashImplicitArrayWrapper<T> { + /** + * Reverses the wrapped array so the first element becomes the last, the second element becomes the second to + * last, and so on. + * + * Note: This method mutates the wrapped array. + * + * @return Returns the new reversed lodash wrapper instance. + */ + reverse(): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.reverse + */ + reverse(): LoDashExplicitArrayWrapper<T>; + } + + //_.prototype.run + interface LoDashWrapperBase<T, TWrapper> { + /** + * @see _.value + */ + run(): T; + } + + //_.prototype.toJSON + interface LoDashWrapperBase<T, TWrapper> { + /** + * @see _.value + */ + toJSON(): T; + } + + //_.prototype.toString + interface LoDashWrapperBase<T, TWrapper> { + /** + * Produces the result of coercing the unwrapped value to a string. + * + * @return Returns the coerced string value. + */ + toString(): string; + } + + //_.prototype.value + interface LoDashWrapperBase<T, TWrapper> { + /** + * Executes the chained sequence to extract the unwrapped value. + * + * @alias _.run, _.toJSON, _.valueOf + * + * @return Returns the resolved unwrapped value. + */ + value(): T; + } + + //_.valueOf + interface LoDashWrapperBase<T, TWrapper> { + /** + * @see _.value + */ + valueOf(): T; + } + + /************** + * Collection * + **************/ + + //_.all + interface LoDashStatic { + /** + * @see _.every + */ + all<T>( + collection: List<T>, + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): boolean; + + /** + * @see _.every + */ + all<T>( + collection: Dictionary<T>, + predicate?: DictionaryIterator<T, boolean>, + thisArg?: any + ): boolean; + + /** + * @see _.every + */ + all<T>( + collection: List<T>|Dictionary<T>, + predicate?: string, + thisArg?: any + ): boolean; + + /** + * @see _.every + */ + all<TObject extends {}, T>( + collection: List<T>|Dictionary<T>, + predicate?: TObject + ): boolean; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.every + */ + all( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): boolean; + + /** + * @see _.every + */ + all( + predicate?: string, + thisArg?: any + ): boolean; + + /** + * @see _.every + */ + all<TObject extends {}>( + predicate?: TObject + ): boolean; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.every + */ + all<TResult>( + predicate?: ListIterator<TResult, boolean>|DictionaryIterator<TResult, boolean>, + thisArg?: any + ): boolean; + + /** + * @see _.every + */ + all( + predicate?: string, + thisArg?: any + ): boolean; + + /** + * @see _.every + */ + all<TObject extends {}>( + predicate?: TObject + ): boolean; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.every + */ + all( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): LoDashExplicitWrapper<boolean>; + + /** + * @see _.every + */ + all( + predicate?: string, + thisArg?: any + ): LoDashExplicitWrapper<boolean>; + + /** + * @see _.every + */ + all<TObject extends {}>( + predicate?: TObject + ): LoDashExplicitWrapper<boolean>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.every + */ + all<TResult>( + predicate?: ListIterator<TResult, boolean>|DictionaryIterator<TResult, boolean>, + thisArg?: any + ): LoDashExplicitWrapper<boolean>; + + /** + * @see _.every + */ + all( + predicate?: string, + thisArg?: any + ): LoDashExplicitWrapper<boolean>; + + /** + * @see _.every + */ + all<TObject extends {}>( + predicate?: TObject + ): LoDashExplicitWrapper<boolean>; + } + + //_.any + interface LoDashStatic { + /** + * @see _.some + */ + any<T>( + collection: List<T>, + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): boolean; + + /** + * @see _.some + */ + any<T>( + collection: Dictionary<T>, + predicate?: DictionaryIterator<T, boolean>, + thisArg?: any + ): boolean; + + /** + * @see _.some + */ + any<T>( + collection: NumericDictionary<T>, + predicate?: NumericDictionaryIterator<T, boolean>, + thisArg?: any + ): boolean; + + /** + * @see _.some + */ + any<T>( + collection: List<T>|Dictionary<T>|NumericDictionary<T>, + predicate?: string, + thisArg?: any + ): boolean; + + /** + * @see _.some + */ + any<TObject extends {}, T>( + collection: List<T>|Dictionary<T>|NumericDictionary<T>, + predicate?: TObject + ): boolean; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.some + */ + any( + predicate?: ListIterator<T, boolean>|NumericDictionaryIterator<T, boolean>, + thisArg?: any + ): boolean; + + /** + * @see _.some + */ + any( + predicate?: string, + thisArg?: any + ): boolean; + + /** + * @see _.some + */ + any<TObject extends {}>( + predicate?: TObject + ): boolean; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.some + */ + any<TResult>( + predicate?: ListIterator<TResult, boolean>|DictionaryIterator<TResult, boolean>|NumericDictionaryIterator<T, boolean>, + thisArg?: any + ): boolean; + + /** + * @see _.some + */ + any( + predicate?: string, + thisArg?: any + ): boolean; + + /** + * @see _.some + */ + any<TObject extends {}>( + predicate?: TObject + ): boolean; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.some + */ + any( + predicate?: ListIterator<T, boolean>|NumericDictionaryIterator<T, boolean>, + thisArg?: any + ): LoDashExplicitWrapper<boolean>; + + /** + * @see _.some + */ + any( + predicate?: string, + thisArg?: any + ): LoDashExplicitWrapper<boolean>; + + /** + * @see _.some + */ + any<TObject extends {}>( + predicate?: TObject + ): LoDashExplicitWrapper<boolean>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.some + */ + any<TResult>( + predicate?: ListIterator<TResult, boolean>|DictionaryIterator<TResult, boolean>|NumericDictionaryIterator<T, boolean>, + thisArg?: any + ): LoDashExplicitWrapper<boolean>; + + /** + * @see _.some + */ + any( + predicate?: string, + thisArg?: any + ): LoDashExplicitWrapper<boolean>; + + /** + * @see _.some + */ + any<TObject extends {}>( + predicate?: TObject + ): LoDashExplicitWrapper<boolean>; + } + + //_.at + interface LoDashStatic { + /** + * Creates an array of elements corresponding to the given keys, or indexes, of collection. Keys may be + * specified as individual arguments or as arrays of keys. + * + * @param collection The collection to iterate over. + * @param props The property names or indexes of elements to pick, specified individually or in arrays. + * @return Returns the new array of picked elements. + */ + at<T>( + collection: List<T>|Dictionary<T>, + ...props: (number|string|(number|string)[])[] + ): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.at + */ + at(...props: (number|string|(number|string)[])[]): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.at + */ + at<T>(...props: (number|string|(number|string)[])[]): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.at + */ + at(...props: (number|string|(number|string)[])[]): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.at + */ + at<T>(...props: (number|string|(number|string)[])[]): LoDashExplicitArrayWrapper<T>; + } + + //_.collect + interface LoDashStatic { + /** + * @see _.map + */ + collect<T, TResult>( + collection: List<T>, + iteratee?: ListIterator<T, TResult>, + thisArg?: any + ): TResult[]; + + /** + * @see _.map + */ + collect<T extends {}, TResult>( + collection: Dictionary<T>, + iteratee?: DictionaryIterator<T, TResult>, + thisArg?: any + ): TResult[]; + + /** + * @see _.map + */ + collect<T, TResult>( + collection: List<T>|Dictionary<T>, + iteratee?: string + ): TResult[]; + + /** + * @see _.map + */ + collect<T, TObject extends {}>( + collection: List<T>|Dictionary<T>, + iteratee?: TObject + ): boolean[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.map + */ + collect<TResult>( + iteratee?: ListIterator<T, TResult>, + thisArg?: any + ): LoDashImplicitArrayWrapper<TResult>; + + /** + * @see _.map + */ + collect<TResult>( + iteratee?: string + ): LoDashImplicitArrayWrapper<TResult>; + + /** + * @see _.map + */ + collect<TObject extends {}>( + iteratee?: TObject + ): LoDashImplicitArrayWrapper<boolean>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.map + */ + collect<TValue, TResult>( + iteratee?: ListIterator<TValue, TResult>|DictionaryIterator<TValue, TResult>, + thisArg?: any + ): LoDashImplicitArrayWrapper<TResult>; + + /** + * @see _.map + */ + collect<TValue, TResult>( + iteratee?: string + ): LoDashImplicitArrayWrapper<TResult>; + + /** + * @see _.map + */ + collect<TObject extends {}>( + iteratee?: TObject + ): LoDashImplicitArrayWrapper<boolean>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.map + */ + collect<TResult>( + iteratee?: ListIterator<T, TResult>, + thisArg?: any + ): LoDashExplicitArrayWrapper<TResult>; + + /** + * @see _.map + */ + collect<TResult>( + iteratee?: string + ): LoDashExplicitArrayWrapper<TResult>; + + /** + * @see _.map + */ + collect<TObject extends {}>( + iteratee?: TObject + ): LoDashExplicitArrayWrapper<boolean>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.map + */ + collect<TValue, TResult>( + iteratee?: ListIterator<TValue, TResult>|DictionaryIterator<TValue, TResult>, + thisArg?: any + ): LoDashExplicitArrayWrapper<TResult>; + + /** + * @see _.map + */ + collect<TValue, TResult>( + iteratee?: string + ): LoDashExplicitArrayWrapper<TResult>; + + /** + * @see _.map + */ + collect<TObject extends {}>( + iteratee?: TObject + ): LoDashExplicitArrayWrapper<boolean>; + } + + //_.contains + interface LoDashStatic { + /** + * @see _.includes + */ + contains<T>( + collection: List<T>|Dictionary<T>, + target: T, + fromIndex?: number + ): boolean; + + /** + * @see _.includes + */ + contains( + collection: string, + target: string, + fromIndex?: number + ): boolean; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.includes + */ + contains( + target: T, + fromIndex?: number + ): boolean; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.includes + */ + contains<TValue>( + target: TValue, + fromIndex?: number + ): boolean; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.includes + */ + contains( + target: string, + fromIndex?: number + ): boolean; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.includes + */ + contains( + target: T, + fromIndex?: number + ): LoDashExplicitWrapper<boolean>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.includes + */ + contains<TValue>( + target: TValue, + fromIndex?: number + ): LoDashExplicitWrapper<boolean>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.includes + */ + contains( + target: string, + fromIndex?: number + ): LoDashExplicitWrapper<boolean>; + } + + //_.countBy + interface LoDashStatic { + /** + * Creates an object composed of keys generated from the results of running each element of collection through + * iteratee. The corresponding value of each key is the number of times the key was returned by iteratee. The + * iteratee is bound to thisArg and invoked with three arguments: + * (value, index|key, collection). + * + * If a property name is provided for iteratee the created _.property style callback returns the property + * value of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for iteratee the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * @param collection The collection to iterate over. + * @param iteratee The function invoked per iteration. + * @param thisArg The this binding of iteratee. + * @return Returns the composed aggregate object. + */ + countBy<T>( + collection: List<T>, + iteratee?: ListIterator<T, any>, + thisArg?: any + ): Dictionary<number>; + + /** + * @see _.countBy + */ + countBy<T>( + collection: Dictionary<T>, + iteratee?: DictionaryIterator<T, any>, + thisArg?: any + ): Dictionary<number>; + + /** + * @see _.countBy + */ + countBy<T>( + collection: NumericDictionary<T>, + iteratee?: NumericDictionaryIterator<T, any>, + thisArg?: any + ): Dictionary<number>; + + /** + * @see _.countBy + */ + countBy<T>( + collection: List<T>|Dictionary<T>|NumericDictionary<T>, + iteratee?: string, + thisArg?: any + ): Dictionary<number>; + + /** + * @see _.countBy + */ + countBy<W, T>( + collection: List<T>|Dictionary<T>|NumericDictionary<T>, + iteratee?: W + ): Dictionary<number>; + + /** + * @see _.countBy + */ + countBy<T>( + collection: List<T>|Dictionary<T>|NumericDictionary<T>, + iteratee?: Object + ): Dictionary<number>; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.countBy + */ + countBy( + iteratee?: ListIterator<T, any>, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<number>>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.countBy + */ + countBy( + iteratee?: ListIterator<T, any>, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<number>>; + + /** + * @see _.countBy + */ + countBy( + iteratee?: string, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<number>>; + + /** + * @see _.countBy + */ + countBy<W>( + iteratee?: W + ): LoDashImplicitObjectWrapper<Dictionary<number>>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.countBy + */ + countBy<T>( + iteratee?: ListIterator<T, any>|DictionaryIterator<T, any>|NumericDictionaryIterator<T, any>, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<number>>; + + /** + * @see _.countBy + */ + countBy( + iteratee?: string, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<number>>; + + /** + * @see _.countBy + */ + countBy<W>( + iteratee?: W + ): LoDashImplicitObjectWrapper<Dictionary<number>>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.countBy + */ + countBy( + iteratee?: ListIterator<T, any>, + thisArg?: any + ): LoDashExplicitObjectWrapper<Dictionary<number>>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.countBy + */ + countBy( + iteratee?: ListIterator<T, any>, + thisArg?: any + ): LoDashExplicitObjectWrapper<Dictionary<number>>; + + /** + * @see _.countBy + */ + countBy( + iteratee?: string, + thisArg?: any + ): LoDashExplicitObjectWrapper<Dictionary<number>>; + + /** + * @see _.countBy + */ + countBy<W>( + iteratee?: W + ): LoDashExplicitObjectWrapper<Dictionary<number>>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.countBy + */ + countBy<T>( + iteratee?: ListIterator<T, any>|DictionaryIterator<T, any>|NumericDictionaryIterator<T, any>, + thisArg?: any + ): LoDashExplicitObjectWrapper<Dictionary<number>>; + + /** + * @see _.countBy + */ + countBy( + iteratee?: string, + thisArg?: any + ): LoDashExplicitObjectWrapper<Dictionary<number>>; + + /** + * @see _.countBy + */ + countBy<W>( + iteratee?: W + ): LoDashExplicitObjectWrapper<Dictionary<number>>; + } + + //_.detect + interface LoDashStatic { + /** + * @see _.find + */ + detect<T>( + collection: List<T>, + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): T; + + /** + * @see _.find + */ + detect<T>( + collection: Dictionary<T>, + predicate?: DictionaryIterator<T, boolean>, + thisArg?: any + ): T; + + /** + * @see _.find + */ + detect<T>( + collection: List<T>|Dictionary<T>, + predicate?: string, + thisArg?: any + ): T; + + /** + * @see _.find + */ + detect<TObject extends {}, T>( + collection: List<T>|Dictionary<T>, + predicate?: TObject + ): T; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.find + */ + detect( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): T; + + /** + * @see _.find + */ + detect( + predicate?: string, + thisArg?: any + ): T; + + /** + * @see _.find + */ + detect<TObject extends {}>( + predicate?: TObject + ): T; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.find + */ + detect<TResult>( + predicate?: ListIterator<TResult, boolean>|DictionaryIterator<TResult, boolean>, + thisArg?: any + ): TResult; + + /** + * @see _.find + */ + detect<TResult>( + predicate?: string, + thisArg?: any + ): TResult; + + /** + * @see _.find + */ + detect<TObject extends {}, TResult>( + predicate?: TObject + ): TResult; + } + + //_.each + interface LoDashStatic { + /** + * @see _.forEach + */ + each<T>( + collection: T[], + iteratee?: ListIterator<T, any>, + thisArg?: any + ): T[]; + + /** + * @see _.forEach + */ + each<T>( + collection: List<T>, + iteratee?: ListIterator<T, any>, + thisArg?: any + ): List<T>; + + /** + * @see _.forEach + */ + each<T>( + collection: Dictionary<T>, + iteratee?: DictionaryIterator<T, any>, + thisArg?: any + ): Dictionary<T>; + + /** + * @see _.forEach + */ + each<T extends {}>( + collection: T, + iteratee?: ObjectIterator<any, any>, + thisArgs?: any + ): T; + + /** + * @see _.forEach + */ + each<T extends {}, TValue>( + collection: T, + iteratee?: ObjectIterator<TValue, any>, + thisArgs?: any + ): T; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.forEach + */ + each( + iteratee: ListIterator<string, any>, + thisArg?: any + ): LoDashImplicitWrapper<string>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.forEach + */ + each( + iteratee: ListIterator<T, any>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.forEach + */ + each<TValue>( + iteratee?: ListIterator<TValue, any>|DictionaryIterator<TValue, any>, + thisArg?: any + ): LoDashImplicitObjectWrapper<T>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.forEach + */ + each( + iteratee: ListIterator<string, any>, + thisArg?: any + ): LoDashExplicitWrapper<string>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.forEach + */ + each( + iteratee: ListIterator<T, any>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.forEach + */ + each<TValue>( + iteratee?: ListIterator<TValue, any>|DictionaryIterator<TValue, any>, + thisArg?: any + ): LoDashExplicitObjectWrapper<T>; + } + + //_.eachRight + interface LoDashStatic { + /** + * @see _.forEachRight + */ + eachRight<T>( + collection: T[], + iteratee?: ListIterator<T, any>, + thisArg?: any + ): T[]; + + /** + * @see _.forEachRight + */ + eachRight<T>( + collection: List<T>, + iteratee?: ListIterator<T, any>, + thisArg?: any + ): List<T>; + + /** + * @see _.forEachRight + */ + eachRight<T>( + collection: Dictionary<T>, + iteratee?: DictionaryIterator<T, any>, + thisArg?: any + ): Dictionary<T>; + + /** + * @see _.forEachRight + */ + eachRight<T extends {}>( + collection: T, + iteratee?: ObjectIterator<any, any>, + thisArgs?: any + ): T; + + /** + * @see _.forEachRight + */ + eachRight<T extends {}, TValue>( + collection: T, + iteratee?: ObjectIterator<TValue, any>, + thisArgs?: any + ): T; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.forEachRight + */ + eachRight( + iteratee: ListIterator<string, any>, + thisArg?: any + ): LoDashImplicitWrapper<string>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.forEachRight + */ + eachRight( + iteratee: ListIterator<T, any>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.forEachRight + */ + eachRight<TValue>( + iteratee?: ListIterator<TValue, any>|DictionaryIterator<TValue, any>, + thisArg?: any + ): LoDashImplicitObjectWrapper<T>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.forEachRight + */ + eachRight( + iteratee: ListIterator<string, any>, + thisArg?: any + ): LoDashExplicitWrapper<string>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.forEachRight + */ + eachRight( + iteratee: ListIterator<T, any>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.forEachRight + */ + eachRight<TValue>( + iteratee?: ListIterator<TValue, any>|DictionaryIterator<TValue, any>, + thisArg?: any + ): LoDashExplicitObjectWrapper<T>; + } + + //_.every + interface LoDashStatic { + /** + * Checks if predicate returns truthy for all elements of collection. The predicate is bound to thisArg and + * invoked with three arguments: (value, index|key, collection). + * + * If a property name is provided for predicate the created _.property style callback returns the property + * value of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for predicate the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * @alias _.all + * + * @param collection The collection to iterate over. + * @param predicate The function invoked per iteration. + * @param thisArg The this binding of predicate. + * @return Returns true if all elements pass the predicate check, else false. + */ + every<T>( + collection: List<T>, + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): boolean; + + /** + * @see _.every + */ + every<T>( + collection: Dictionary<T>, + predicate?: DictionaryIterator<T, boolean>, + thisArg?: any + ): boolean; + + /** + * @see _.every + */ + every<T>( + collection: List<T>|Dictionary<T>, + predicate?: string, + thisArg?: any + ): boolean; + + /** + * @see _.every + */ + every<TObject extends {}, T>( + collection: List<T>|Dictionary<T>, + predicate?: TObject + ): boolean; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.every + */ + every( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): boolean; + + /** + * @see _.every + */ + every( + predicate?: string, + thisArg?: any + ): boolean; + + /** + * @see _.every + */ + every<TObject extends {}>( + predicate?: TObject + ): boolean; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.every + */ + every<TResult>( + predicate?: ListIterator<TResult, boolean>|DictionaryIterator<TResult, boolean>, + thisArg?: any + ): boolean; + + /** + * @see _.every + */ + every( + predicate?: string, + thisArg?: any + ): boolean; + + /** + * @see _.every + */ + every<TObject extends {}>( + predicate?: TObject + ): boolean; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.every + */ + every( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): LoDashExplicitWrapper<boolean>; + + /** + * @see _.every + */ + every( + predicate?: string, + thisArg?: any + ): LoDashExplicitWrapper<boolean>; + + /** + * @see _.every + */ + every<TObject extends {}>( + predicate?: TObject + ): LoDashExplicitWrapper<boolean>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.every + */ + every<TResult>( + predicate?: ListIterator<TResult, boolean>|DictionaryIterator<TResult, boolean>, + thisArg?: any + ): LoDashExplicitWrapper<boolean>; + + /** + * @see _.every + */ + every( + predicate?: string, + thisArg?: any + ): LoDashExplicitWrapper<boolean>; + + /** + * @see _.every + */ + every<TObject extends {}>( + predicate?: TObject + ): LoDashExplicitWrapper<boolean>; + } + + //_.filter + interface LoDashStatic { + /** + * Iterates over elements of collection, returning an array of all elements predicate returns truthy for. The + * predicate is bound to thisArg and invoked with three arguments: (value, index|key, collection). + * + * If a property name is provided for predicate the created _.property style callback returns the property + * value of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for predicate the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * @alias _.select + * + * @param collection The collection to iterate over. + * @param predicate The function invoked per iteration. + * @param thisArg The this binding of predicate. + * @return Returns the new filtered array. + */ + filter<T>( + collection: List<T>, + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): T[]; + + /** + * @see _.filter + */ + filter<T>( + collection: Dictionary<T>, + predicate?: DictionaryIterator<T, boolean>, + thisArg?: any + ): T[]; + + /** + * @see _.filter + */ + filter( + collection: string, + predicate?: StringIterator<boolean>, + thisArg?: any + ): string[]; + + /** + * @see _.filter + */ + filter<T>( + collection: List<T>|Dictionary<T>, + predicate: string, + thisArg?: any + ): T[]; + + /** + * @see _.filter + */ + filter<W extends {}, T>( + collection: List<T>|Dictionary<T>, + predicate: W + ): T[]; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.filter + */ + filter( + predicate?: StringIterator<boolean>, + thisArg?: any + ): LoDashImplicitArrayWrapper<string>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.filter + */ + filter( + predicate: ListIterator<T, boolean>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.filter + */ + filter( + predicate: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.filter + */ + filter<W>(predicate: W): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.filter + */ + filter<T>( + predicate: ListIterator<T, boolean>|DictionaryIterator<T, boolean>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.filter + */ + filter<T>( + predicate: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.filter + */ + filter<W, T>(predicate: W): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.filter + */ + filter( + predicate?: StringIterator<boolean>, + thisArg?: any + ): LoDashExplicitArrayWrapper<string>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.filter + */ + filter( + predicate: ListIterator<T, boolean>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.filter + */ + filter( + predicate: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.filter + */ + filter<W>(predicate: W): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.filter + */ + filter<T>( + predicate: ListIterator<T, boolean>|DictionaryIterator<T, boolean>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.filter + */ + filter<T>( + predicate: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.filter + */ + filter<W, T>(predicate: W): LoDashExplicitArrayWrapper<T>; + } + + //_.find + interface LoDashStatic { + /** + * Iterates over elements of collection, returning the first element predicate returns truthy for. + * The predicate is bound to thisArg and invoked with three arguments: (value, index|key, collection). + * + * If a property name is provided for predicate the created _.property style callback returns the property + * value of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for predicate the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * @alias _.detect + * + * @param collection The collection to search. + * @param predicate The function invoked per iteration. + * @param thisArg The this binding of predicate. + * @return Returns the matched element, else undefined. + */ + find<T>( + collection: List<T>, + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): T; + + /** + * @see _.find + */ + find<T>( + collection: Dictionary<T>, + predicate?: DictionaryIterator<T, boolean>, + thisArg?: any + ): T; + + /** + * @see _.find + */ + find<T>( + collection: List<T>|Dictionary<T>, + predicate?: string, + thisArg?: any + ): T; + + /** + * @see _.find + */ + find<TObject extends {}, T>( + collection: List<T>|Dictionary<T>, + predicate?: TObject + ): T; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.find + */ + find( + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): T; + + /** + * @see _.find + */ + find( + predicate?: string, + thisArg?: any + ): T; + + /** + * @see _.find + */ + find<TObject extends {}>( + predicate?: TObject + ): T; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.find + */ + find<TResult>( + predicate?: ListIterator<TResult, boolean>|DictionaryIterator<TResult, boolean>, + thisArg?: any + ): TResult; + + /** + * @see _.find + */ + find<TResult>( + predicate?: string, + thisArg?: any + ): TResult; + + /** + * @see _.find + */ + find<TObject extends {}, TResult>( + predicate?: TObject + ): TResult; + } + + //_.findWhere + interface LoDashStatic { + /** + * @see _.find + **/ + findWhere<T>( + collection: Array<T>, + callback: ListIterator<T, boolean>, + thisArg?: any): T; + + /** + * @see _.find + **/ + findWhere<T>( + collection: List<T>, + callback: ListIterator<T, boolean>, + thisArg?: any): T; + + /** + * @see _.find + **/ + findWhere<T>( + collection: Dictionary<T>, + callback: DictionaryIterator<T, boolean>, + thisArg?: any): T; + + /** + * @see _.find + * @param _.matches style callback + **/ + findWhere<W, T>( + collection: Array<T>, + whereValue: W): T; + + /** + * @see _.find + * @param _.matches style callback + **/ + findWhere<W, T>( + collection: List<T>, + whereValue: W): T; + + /** + * @see _.find + * @param _.matches style callback + **/ + findWhere<W, T>( + collection: Dictionary<T>, + whereValue: W): T; + + /** + * @see _.find + * @param _.property style callback + **/ + findWhere<T>( + collection: Array<T>, + pluckValue: string): T; + + /** + * @see _.find + * @param _.property style callback + **/ + findWhere<T>( + collection: List<T>, + pluckValue: string): T; + + /** + * @see _.find + * @param _.property style callback + **/ + findWhere<T>( + collection: Dictionary<T>, + pluckValue: string): T; + } + + //_.findLast + interface LoDashStatic { + /** + * This method is like _.find except that it iterates over elements of a collection from + * right to left. + * @param collection Searches for a value in this list. + * @param callback The function called per iteration. + * @param thisArg The this binding of callback. + * @return The found element, else undefined. + **/ + findLast<T>( + collection: Array<T>, + callback: ListIterator<T, boolean>, + thisArg?: any): T; + + /** + * @see _.find + **/ + findLast<T>( + collection: List<T>, + callback: ListIterator<T, boolean>, + thisArg?: any): T; + + /** + * @see _.find + **/ + findLast<T>( + collection: Dictionary<T>, + callback: DictionaryIterator<T, boolean>, + thisArg?: any): T; + + /** + * @see _.find + * @param _.pluck style callback + **/ + findLast<W, T>( + collection: Array<T>, + whereValue: W): T; + + /** + * @see _.find + * @param _.pluck style callback + **/ + findLast<W, T>( + collection: List<T>, + whereValue: W): T; + + /** + * @see _.find + * @param _.pluck style callback + **/ + findLast<W, T>( + collection: Dictionary<T>, + whereValue: W): T; + + /** + * @see _.find + * @param _.where style callback + **/ + findLast<T>( + collection: Array<T>, + pluckValue: string): T; + + /** + * @see _.find + * @param _.where style callback + **/ + findLast<T>( + collection: List<T>, + pluckValue: string): T; + + /** + * @see _.find + * @param _.where style callback + **/ + findLast<T>( + collection: Dictionary<T>, + pluckValue: string): T; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.findLast + */ + findLast( + callback: ListIterator<T, boolean>, + thisArg?: any): T; + /** + * @see _.findLast + * @param _.where style callback + */ + findLast<W>( + whereValue: W): T; + + /** + * @see _.findLast + * @param _.where style callback + */ + findLast( + pluckValue: string): T; + } + + //_.forEach + interface LoDashStatic { + /** + * Iterates over elements of collection invoking iteratee for each element. The iteratee is bound to thisArg + * and invoked with three arguments: + * (value, index|key, collection). Iteratee functions may exit iteration early by explicitly returning false. + * + * Note: As with other "Collections" methods, objects with a "length" property are iterated like arrays. To + * avoid this behavior _.forIn or _.forOwn may be used for object iteration. + * + * @alias _.each + * + * @param collection The collection to iterate over. + * @param iteratee The function invoked per iteration. + * @param thisArg The this binding of iteratee. + */ + forEach<T>( + collection: T[], + iteratee?: ListIterator<T, any>, + thisArg?: any + ): T[]; + + /** + * @see _.forEach + */ + forEach<T>( + collection: List<T>, + iteratee?: ListIterator<T, any>, + thisArg?: any + ): List<T>; + + /** + * @see _.forEach + */ + forEach<T>( + collection: Dictionary<T>, + iteratee?: DictionaryIterator<T, any>, + thisArg?: any + ): Dictionary<T>; + + /** + * @see _.forEach + */ + forEach<T extends {}>( + collection: T, + iteratee?: ObjectIterator<any, any>, + thisArgs?: any + ): T; + + /** + * @see _.forEach + */ + forEach<T extends {}, TValue>( + collection: T, + iteratee?: ObjectIterator<TValue, any>, + thisArgs?: any + ): T; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.forEach + */ + forEach( + iteratee: ListIterator<string, any>, + thisArg?: any + ): LoDashImplicitWrapper<string>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.forEach + */ + forEach( + iteratee: ListIterator<T, any>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.forEach + */ + forEach<TValue>( + iteratee?: ListIterator<TValue, any>|DictionaryIterator<TValue, any>, + thisArg?: any + ): LoDashImplicitObjectWrapper<T>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.forEach + */ + forEach( + iteratee: ListIterator<string, any>, + thisArg?: any + ): LoDashExplicitWrapper<string>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.forEach + */ + forEach( + iteratee: ListIterator<T, any>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.forEach + */ + forEach<TValue>( + iteratee?: ListIterator<TValue, any>|DictionaryIterator<TValue, any>, + thisArg?: any + ): LoDashExplicitObjectWrapper<T>; + } + + //_.forEachRight + interface LoDashStatic { + /** + * This method is like _.forEach except that it iterates over elements of collection from right to left. + * + * @alias _.eachRight + * + * @param collection The collection to iterate over. + * @param iteratee The function called per iteration. + * @param thisArg The this binding of callback. + */ + forEachRight<T>( + collection: T[], + iteratee?: ListIterator<T, any>, + thisArg?: any + ): T[]; + + /** + * @see _.forEachRight + */ + forEachRight<T>( + collection: List<T>, + iteratee?: ListIterator<T, any>, + thisArg?: any + ): List<T>; + + /** + * @see _.forEachRight + */ + forEachRight<T>( + collection: Dictionary<T>, + iteratee?: DictionaryIterator<T, any>, + thisArg?: any + ): Dictionary<T>; + + /** + * @see _.forEachRight + */ + forEachRight<T extends {}>( + collection: T, + iteratee?: ObjectIterator<any, any>, + thisArgs?: any + ): T; + + /** + * @see _.forEachRight + */ + forEachRight<T extends {}, TValue>( + collection: T, + iteratee?: ObjectIterator<TValue, any>, + thisArgs?: any + ): T; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.forEachRight + */ + forEachRight( + iteratee: ListIterator<string, any>, + thisArg?: any + ): LoDashImplicitWrapper<string>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.forEachRight + */ + forEachRight( + iteratee: ListIterator<T, any>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.forEachRight + */ + forEachRight<TValue>( + iteratee?: ListIterator<TValue, any>|DictionaryIterator<TValue, any>, + thisArg?: any + ): LoDashImplicitObjectWrapper<T>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.forEachRight + */ + forEachRight( + iteratee: ListIterator<string, any>, + thisArg?: any + ): LoDashExplicitWrapper<string>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.forEachRight + */ + forEachRight( + iteratee: ListIterator<T, any>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.forEachRight + */ + forEachRight<TValue>( + iteratee?: ListIterator<TValue, any>|DictionaryIterator<TValue, any>, + thisArg?: any + ): LoDashExplicitObjectWrapper<T>; + } + + //_.groupBy + interface LoDashStatic { + /** + * Creates an object composed of keys generated from the results of running each element of collection through + * iteratee. The corresponding value of each key is an array of the elements responsible for generating the + * key. The iteratee is bound to thisArg and invoked with three arguments: + * (value, index|key, collection). + * + * If a property name is provided for iteratee the created _.property style callback returns the property + * value of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for iteratee the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * @param collection The collection to iterate over. + * @param iteratee The function invoked per iteration. + * @param thisArg The this binding of iteratee. + * @return Returns the composed aggregate object. + */ + groupBy<T, TKey>( + collection: List<T>, + iteratee?: ListIterator<T, TKey>, + thisArg?: any + ): Dictionary<T[]>; + + /** + * @see _.groupBy + */ + groupBy<T>( + collection: List<any>, + iteratee?: ListIterator<T, any>, + thisArg?: any + ): Dictionary<T[]>; + + /** + * @see _.groupBy + */ + groupBy<T, TKey>( + collection: Dictionary<T>, + iteratee?: DictionaryIterator<T, TKey>, + thisArg?: any + ): Dictionary<T[]>; + + /** + * @see _.groupBy + */ + groupBy<T>( + collection: Dictionary<any>, + iteratee?: DictionaryIterator<T, any>, + thisArg?: any + ): Dictionary<T[]>; + + /** + * @see _.groupBy + */ + groupBy<T, TValue>( + collection: List<T>|Dictionary<T>, + iteratee?: string, + thisArg?: TValue + ): Dictionary<T[]>; + + /** + * @see _.groupBy + */ + groupBy<T>( + collection: List<T>|Dictionary<T>, + iteratee?: string, + thisArg?: any + ): Dictionary<T[]>; + + /** + * @see _.groupBy + */ + groupBy<TWhere, T>( + collection: List<T>|Dictionary<T>, + iteratee?: TWhere + ): Dictionary<T[]>; + + /** + * @see _.groupBy + */ + groupBy<T>( + collection: List<T>|Dictionary<T>, + iteratee?: Object + ): Dictionary<T[]>; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.groupBy + */ + groupBy<TKey>( + iteratee?: ListIterator<T, TKey>, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<T[]>>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.groupBy + */ + groupBy<TKey>( + iteratee?: ListIterator<T, TKey>, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<T[]>>; + + /** + * @see _.groupBy + */ + groupBy<TValue>( + iteratee?: string, + thisArg?: TValue + ): LoDashImplicitObjectWrapper<Dictionary<T[]>>; + + /** + * @see _.groupBy + */ + groupBy<TWhere>( + iteratee?: TWhere + ): LoDashImplicitObjectWrapper<Dictionary<T[]>>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.groupBy + */ + groupBy<T, TKey>( + iteratee?: ListIterator<T, TKey>|DictionaryIterator<T, TKey>, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<T[]>>; + + /** + * @see _.groupBy + */ + groupBy<T>( + iteratee?: ListIterator<T, any>|DictionaryIterator<T, any>, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<T[]>>; + + /** + * @see _.groupBy + */ + groupBy<T, TValue>( + iteratee?: string, + thisArg?: TValue + ): LoDashImplicitObjectWrapper<Dictionary<T[]>>; + + /** + * @see _.groupBy + */ + groupBy<T>( + iteratee?: string, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<T[]>>; + + /** + * @see _.groupBy + */ + groupBy<TWhere, T>( + iteratee?: TWhere + ): LoDashImplicitObjectWrapper<Dictionary<T[]>>; + + /** + * @see _.groupBy + */ + groupBy<T>( + iteratee?: Object + ): LoDashImplicitObjectWrapper<Dictionary<T[]>>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.groupBy + */ + groupBy<TKey>( + iteratee?: ListIterator<T, TKey>, + thisArg?: any + ): LoDashExplicitObjectWrapper<Dictionary<T[]>>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.groupBy + */ + groupBy<TKey>( + iteratee?: ListIterator<T, TKey>, + thisArg?: any + ): LoDashExplicitObjectWrapper<Dictionary<T[]>>; + + /** + * @see _.groupBy + */ + groupBy<TValue>( + iteratee?: string, + thisArg?: TValue + ): LoDashExplicitObjectWrapper<Dictionary<T[]>>; + + /** + * @see _.groupBy + */ + groupBy<TWhere>( + iteratee?: TWhere + ): LoDashExplicitObjectWrapper<Dictionary<T[]>>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.groupBy + */ + groupBy<T, TKey>( + iteratee?: ListIterator<T, TKey>|DictionaryIterator<T, TKey>, + thisArg?: any + ): LoDashExplicitObjectWrapper<Dictionary<T[]>>; + + /** + * @see _.groupBy + */ + groupBy<T>( + iteratee?: ListIterator<T, any>|DictionaryIterator<T, any>, + thisArg?: any + ): LoDashExplicitObjectWrapper<Dictionary<T[]>>; + + /** + * @see _.groupBy + */ + groupBy<T, TValue>( + iteratee?: string, + thisArg?: TValue + ): LoDashExplicitObjectWrapper<Dictionary<T[]>>; + + /** + * @see _.groupBy + */ + groupBy<T>( + iteratee?: string, + thisArg?: any + ): LoDashExplicitObjectWrapper<Dictionary<T[]>>; + + /** + * @see _.groupBy + */ + groupBy<TWhere, T>( + iteratee?: TWhere + ): LoDashExplicitObjectWrapper<Dictionary<T[]>>; + + /** + * @see _.groupBy + */ + groupBy<T>( + iteratee?: Object + ): LoDashExplicitObjectWrapper<Dictionary<T[]>>; + } + + //_.include + interface LoDashStatic { + /** + * @see _.includes + */ + include<T>( + collection: List<T>|Dictionary<T>, + target: T, + fromIndex?: number + ): boolean; + + /** + * @see _.includes + */ + include( + collection: string, + target: string, + fromIndex?: number + ): boolean; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.includes + */ + include( + target: T, + fromIndex?: number + ): boolean; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.includes + */ + include<TValue>( + target: TValue, + fromIndex?: number + ): boolean; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.includes + */ + include( + target: string, + fromIndex?: number + ): boolean; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.includes + */ + include( + target: T, + fromIndex?: number + ): LoDashExplicitWrapper<boolean>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.includes + */ + include<TValue>( + target: TValue, + fromIndex?: number + ): LoDashExplicitWrapper<boolean>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.includes + */ + include( + target: string, + fromIndex?: number + ): LoDashExplicitWrapper<boolean>; + } + + //_.includes + interface LoDashStatic { + /** + * Checks if target is in collection using SameValueZero for equality comparisons. If fromIndex is negative, + * it’s used as the offset from the end of collection. + * + * @alias _.contains, _.include + * + * @param collection The collection to search. + * @param target The value to search for. + * @param fromIndex The index to search from. + * @return True if the target element is found, else false. + */ + includes<T>( + collection: List<T>|Dictionary<T>, + target: T, + fromIndex?: number + ): boolean; + + /** + * @see _.includes + */ + includes( + collection: string, + target: string, + fromIndex?: number + ): boolean; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.includes + */ + includes( + target: T, + fromIndex?: number + ): boolean; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.includes + */ + includes<TValue>( + target: TValue, + fromIndex?: number + ): boolean; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.includes + */ + includes( + target: string, + fromIndex?: number + ): boolean; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.includes + */ + includes( + target: T, + fromIndex?: number + ): LoDashExplicitWrapper<boolean>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.includes + */ + includes<TValue>( + target: TValue, + fromIndex?: number + ): LoDashExplicitWrapper<boolean>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.includes + */ + includes( + target: string, + fromIndex?: number + ): LoDashExplicitWrapper<boolean>; + } + + //_.indexBy + interface LoDashStatic { + /** + * Creates an object composed of keys generated from the results of running each element of collection through + * iteratee. The corresponding value of each key is the last element responsible for generating the key. The + * iteratee function is bound to thisArg and invoked with three arguments: + * (value, index|key, collection). + * + * If a property name is provided for iteratee the created _.property style callback returns the property + * value of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for iteratee the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * @param collection The collection to iterate over. + * @param iteratee The function invoked per iteration. + * @param thisArg The this binding of iteratee. + * @return Returns the composed aggregate object. + */ + indexBy<T>( + collection: List<T>, + iteratee?: ListIterator<T, any>, + thisArg?: any + ): Dictionary<T>; + + /** + * @see _.indexBy + */ + indexBy<T>( + collection: NumericDictionary<T>, + iteratee?: NumericDictionaryIterator<T, any>, + thisArg?: any + ): Dictionary<T>; + + /** + * @see _.indexBy + */ + indexBy<T>( + collection: Dictionary<T>, + iteratee?: DictionaryIterator<T, any>, + thisArg?: any + ): Dictionary<T>; + + /** + * @see _.indexBy + */ + indexBy<T>( + collection: List<T>|NumericDictionary<T>|Dictionary<T>, + iteratee?: string, + thisArg?: any + ): Dictionary<T>; + + /** + * @see _.indexBy + */ + indexBy<W extends Object, T>( + collection: List<T>|NumericDictionary<T>|Dictionary<T>, + iteratee?: W + ): Dictionary<T>; + + /** + * @see _.indexBy + */ + indexBy<T>( + collection: List<T>|NumericDictionary<T>|Dictionary<T>, + iteratee?: Object + ): Dictionary<T>; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.indexBy + */ + indexBy( + iteratee?: ListIterator<T, any>, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<T>>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.indexBy + */ + indexBy( + iteratee?: ListIterator<T, any>, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<T>>; + + /** + * @see _.indexBy + */ + indexBy( + iteratee?: string, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<T>>; + + /** + * @see _.indexBy + */ + indexBy<W extends Object>( + iteratee?: W + ): LoDashImplicitObjectWrapper<Dictionary<T>>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.indexBy + */ + indexBy<T>( + iteratee?: ListIterator<T, any>|NumericDictionaryIterator<T, any>|DictionaryIterator<T, any>, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<T>>; + + /** + * @see _.indexBy + */ + indexBy<T>( + iteratee?: string, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<T>>; + + /** + * @see _.indexBy + */ + indexBy<W extends Object, T>( + iteratee?: W + ): LoDashImplicitObjectWrapper<Dictionary<T>>; + + /** + * @see _.indexBy + */ + indexBy<T>( + iteratee?: Object + ): LoDashImplicitObjectWrapper<Dictionary<T>>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.indexBy + */ + indexBy( + iteratee?: ListIterator<T, any>, + thisArg?: any + ): LoDashExplicitObjectWrapper<Dictionary<T>>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.indexBy + */ + indexBy( + iteratee?: ListIterator<T, any>, + thisArg?: any + ): LoDashExplicitObjectWrapper<Dictionary<T>>; + + /** + * @see _.indexBy + */ + indexBy( + iteratee?: string, + thisArg?: any + ): LoDashExplicitObjectWrapper<Dictionary<T>>; + + /** + * @see _.indexBy + */ + indexBy<W extends Object>( + iteratee?: W + ): LoDashExplicitObjectWrapper<Dictionary<T>>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.indexBy + */ + indexBy<T>( + iteratee?: ListIterator<T, any>|NumericDictionaryIterator<T, any>|DictionaryIterator<T, any>, + thisArg?: any + ): LoDashExplicitObjectWrapper<Dictionary<T>>; + + /** + * @see _.indexBy + */ + indexBy<T>( + iteratee?: string, + thisArg?: any + ): LoDashExplicitObjectWrapper<Dictionary<T>>; + + /** + * @see _.indexBy + */ + indexBy<W extends Object, T>( + iteratee?: W + ): LoDashExplicitObjectWrapper<Dictionary<T>>; + + /** + * @see _.indexBy + */ + indexBy<T>( + iteratee?: Object + ): LoDashExplicitObjectWrapper<Dictionary<T>>; + } + + //_.invoke + interface LoDashStatic { + /** + * Invokes the method named by methodName on each element in the collection returning + * an array of the results of each invoked method. Additional arguments will be provided + * to each invoked method. If methodName is a function it will be invoked for, and this + * bound to, each element in the collection. + * @param collection The collection to iterate over. + * @param methodName The name of the method to invoke. + * @param args Arguments to invoke the method with. + **/ + invoke<T extends {}>( + collection: Array<T>, + methodName: string, + ...args: any[]): any; + + /** + * @see _.invoke + **/ + invoke<T extends {}>( + collection: List<T>, + methodName: string, + ...args: any[]): any; + + /** + * @see _.invoke + **/ + invoke<T extends {}>( + collection: Dictionary<T>, + methodName: string, + ...args: any[]): any; + + /** + * @see _.invoke + **/ + invoke<T extends {}>( + collection: Array<T>, + method: Function, + ...args: any[]): any; + + /** + * @see _.invoke + **/ + invoke<T extends {}>( + collection: List<T>, + method: Function, + ...args: any[]): any; + + /** + * @see _.invoke + **/ + invoke<T extends {}>( + collection: Dictionary<T>, + method: Function, + ...args: any[]): any; + } + + //_.map + interface LoDashStatic { + /** + * Creates an array of values by running each element in collection through iteratee. The iteratee is bound to + * thisArg and invoked with three arguments: (value, index|key, collection). + * + * If a property name is provided for iteratee the created _.property style callback returns the property value + * of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for iteratee the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * Many lodash methods are guarded to work as iteratees for methods like _.every, _.filter, _.map, _.mapValues, + * _.reject, and _.some. + * + * The guarded methods are: + * ary, callback, chunk, clone, create, curry, curryRight, drop, dropRight, every, fill, flatten, invert, max, + * min, parseInt, slice, sortBy, take, takeRight, template, trim, trimLeft, trimRight, trunc, random, range, + * sample, some, sum, uniq, and words + * + * @alias _.collect + * + * @param collection The collection to iterate over. + * @param iteratee The function invoked per iteration. + * @param thisArg The this binding of iteratee. + * @return Returns the new mapped array. + */ + map<T, TResult>( + collection: List<T>, + iteratee?: ListIterator<T, TResult>, + thisArg?: any + ): TResult[]; + + /** + * @see _.map + */ + map<T extends {}, TResult>( + collection: Dictionary<T>, + iteratee?: DictionaryIterator<T, TResult>, + thisArg?: any + ): TResult[]; + + /** + * @see _.map + */ + map<T, TResult>( + collection: List<T>|Dictionary<T>, + iteratee?: string + ): TResult[]; + + /** + * @see _.map + */ + map<T, TObject extends {}>( + collection: List<T>|Dictionary<T>, + iteratee?: TObject + ): boolean[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.map + */ + map<TResult>( + iteratee?: ListIterator<T, TResult>, + thisArg?: any + ): LoDashImplicitArrayWrapper<TResult>; + + /** + * @see _.map + */ + map<TResult>( + iteratee?: string + ): LoDashImplicitArrayWrapper<TResult>; + + /** + * @see _.map + */ + map<TObject extends {}>( + iteratee?: TObject + ): LoDashImplicitArrayWrapper<boolean>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.map + */ + map<TValue, TResult>( + iteratee?: ListIterator<TValue, TResult>|DictionaryIterator<TValue, TResult>, + thisArg?: any + ): LoDashImplicitArrayWrapper<TResult>; + + /** + * @see _.map + */ + map<TValue, TResult>( + iteratee?: string + ): LoDashImplicitArrayWrapper<TResult>; + + /** + * @see _.map + */ + map<TObject extends {}>( + iteratee?: TObject + ): LoDashImplicitArrayWrapper<boolean>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.map + */ + map<TResult>( + iteratee?: ListIterator<T, TResult>, + thisArg?: any + ): LoDashExplicitArrayWrapper<TResult>; + + /** + * @see _.map + */ + map<TResult>( + iteratee?: string + ): LoDashExplicitArrayWrapper<TResult>; + + /** + * @see _.map + */ + map<TObject extends {}>( + iteratee?: TObject + ): LoDashExplicitArrayWrapper<boolean>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.map + */ + map<TValue, TResult>( + iteratee?: ListIterator<TValue, TResult>|DictionaryIterator<TValue, TResult>, + thisArg?: any + ): LoDashExplicitArrayWrapper<TResult>; + + /** + * @see _.map + */ + map<TValue, TResult>( + iteratee?: string + ): LoDashExplicitArrayWrapper<TResult>; + + /** + * @see _.map + */ + map<TObject extends {}>( + iteratee?: TObject + ): LoDashExplicitArrayWrapper<boolean>; + } + + //_.partition + interface LoDashStatic { + /** + * Creates an array of elements split into two groups, the first of which contains elements predicate returns truthy for, + * while the second of which contains elements predicate returns falsey for. + * The predicate is bound to thisArg and invoked with three arguments: (value, index|key, collection). + * + * If a property name is provided for predicate the created _.property style callback + * returns the property value of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback + * returns true for elements that have a matching property value, else false. + * + * If an object is provided for predicate the created _.matches style callback returns + * true for elements that have the properties of the given object, else false. + * + * @param collection The collection to iterate over. + * @param callback The function called per iteration. + * @param thisArg The this binding of predicate. + * @return Returns the array of grouped elements. + **/ + partition<T>( + collection: List<T>, + callback: ListIterator<T, boolean>, + thisArg?: any): T[][]; + + /** + * @see _.partition + **/ + partition<T>( + collection: Dictionary<T>, + callback: DictionaryIterator<T, boolean>, + thisArg?: any): T[][]; + + /** + * @see _.partition + **/ + partition<W, T>( + collection: List<T>, + whereValue: W): T[][]; + + /** + * @see _.partition + **/ + partition<W, T>( + collection: Dictionary<T>, + whereValue: W): T[][]; + + /** + * @see _.partition + **/ + partition<T>( + collection: List<T>, + path: string, + srcValue: any): T[][]; + + /** + * @see _.partition + **/ + partition<T>( + collection: Dictionary<T>, + path: string, + srcValue: any): T[][]; + + /** + * @see _.partition + **/ + partition<T>( + collection: List<T>, + pluckValue: string): T[][]; + + /** + * @see _.partition + **/ + partition<T>( + collection: Dictionary<T>, + pluckValue: string): T[][]; + } + + interface LoDashImplicitStringWrapper { + /** + * @see _.partition + */ + partition( + callback: ListIterator<string, boolean>, + thisArg?: any): LoDashImplicitArrayWrapper<string[]>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.partition + */ + partition( + callback: ListIterator<T, boolean>, + thisArg?: any): LoDashImplicitArrayWrapper<T[]>; + /** + * @see _.partition + */ + partition<W>( + whereValue: W): LoDashImplicitArrayWrapper<T[]>; + /** + * @see _.partition + */ + partition( + path: string, + srcValue: any): LoDashImplicitArrayWrapper<T[]>; + /** + * @see _.partition + */ + partition( + pluckValue: string): LoDashImplicitArrayWrapper<T[]>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.partition + */ + partition<TResult>( + callback: ListIterator<TResult, boolean>, + thisArg?: any): LoDashImplicitArrayWrapper<TResult[]>; + + /** + * @see _.partition + */ + partition<TResult>( + callback: DictionaryIterator<TResult, boolean>, + thisArg?: any): LoDashImplicitArrayWrapper<TResult[]>; + + /** + * @see _.partition + */ + partition<W, TResult>( + whereValue: W): LoDashImplicitArrayWrapper<TResult[]>; + + /** + * @see _.partition + */ + partition<TResult>( + path: string, + srcValue: any): LoDashImplicitArrayWrapper<TResult[]>; + + /** + * @see _.partition + */ + partition<TResult>( + pluckValue: string): LoDashImplicitArrayWrapper<TResult[]>; + } + + //_.pluck + interface LoDashStatic { + /** + * Gets the property value of path from all elements in collection. + * + * @param collection The collection to iterate over. + * @param path The path of the property to pluck. + * @return A new array of property values. + */ + pluck<T extends {}>( + collection: List<T>|Dictionary<T>, + path: string|StringRepresentable|StringRepresentable[] + ): any[]; + + /** + * @see _.pluck + */ + pluck<T extends {}, TResult>( + collection: List<T>|Dictionary<T>, + path: string|StringRepresentable|StringRepresentable[] + ): TResult[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.pluck + */ + pluck<TResult>(path: string|StringRepresentable|StringRepresentable[]): LoDashImplicitArrayWrapper<TResult>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.pluck + */ + pluck<TResult>(path: string|StringRepresentable|StringRepresentable[]): LoDashImplicitArrayWrapper<TResult>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.pluck + */ + pluck<TResult>(path: string|StringRepresentable|StringRepresentable[]): LoDashExplicitArrayWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.pluck + */ + pluck<TResult>(path: string|StringRepresentable|StringRepresentable[]): LoDashExplicitArrayWrapper<TResult>; + } + + //_.reduce + interface LoDashStatic { + /** + * Reduces a collection to a value which is the accumulated result of running each + * element in the collection through the callback, where each successive callback execution + * consumes the return value of the previous execution. If accumulator is not provided the + * first element of the collection will be used as the initial accumulator value. The callback + * is bound to thisArg and invoked with four arguments; (accumulator, value, index|key, collection). + * @param collection The collection to iterate over. + * @param callback The function called per iteration. + * @param accumulator Initial value of the accumulator. + * @param thisArg The this binding of callback. + * @return Returns the accumulated value. + **/ + reduce<T, TResult>( + collection: Array<T>, + callback: MemoIterator<T, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + reduce<T, TResult>( + collection: List<T>, + callback: MemoIterator<T, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + reduce<T, TResult>( + collection: Dictionary<T>, + callback: MemoIterator<T, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + reduce<T, TResult>( + collection: Array<T>, + callback: MemoIterator<T, TResult>, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + reduce<T, TResult>( + collection: List<T>, + callback: MemoIterator<T, TResult>, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + reduce<T, TResult>( + collection: Dictionary<T>, + callback: MemoIterator<T, TResult>, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + inject<T, TResult>( + collection: Array<T>, + callback: MemoIterator<T, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + inject<T, TResult>( + collection: List<T>, + callback: MemoIterator<T, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + inject<T, TResult>( + collection: Dictionary<T>, + callback: MemoIterator<T, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + inject<T, TResult>( + collection: Array<T>, + callback: MemoIterator<T, TResult>, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + inject<T, TResult>( + collection: List<T>, + callback: MemoIterator<T, TResult>, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + inject<T, TResult>( + collection: Dictionary<T>, + callback: MemoIterator<T, TResult>, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + foldl<T, TResult>( + collection: Array<T>, + callback: MemoIterator<T, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + foldl<T, TResult>( + collection: List<T>, + callback: MemoIterator<T, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + foldl<T, TResult>( + collection: Dictionary<T>, + callback: MemoIterator<T, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + foldl<T, TResult>( + collection: Array<T>, + callback: MemoIterator<T, TResult>, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + foldl<T, TResult>( + collection: List<T>, + callback: MemoIterator<T, TResult>, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + foldl<T, TResult>( + collection: Dictionary<T>, + callback: MemoIterator<T, TResult>, + thisArg?: any): TResult; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.reduce + **/ + reduce<TResult>( + callback: MemoIterator<T, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + reduce<TResult>( + callback: MemoIterator<T, TResult>, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + inject<TResult>( + callback: MemoIterator<T, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + inject<TResult>( + callback: MemoIterator<T, TResult>, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + foldl<TResult>( + callback: MemoIterator<T, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + foldl<TResult>( + callback: MemoIterator<T, TResult>, + thisArg?: any): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.reduce + **/ + reduce<TValue, TResult>( + callback: MemoIterator<TValue, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + reduce<TValue, TResult>( + callback: MemoIterator<TValue, TResult>, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + inject<TValue, TResult>( + callback: MemoIterator<TValue, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + inject<TValue, TResult>( + callback: MemoIterator<TValue, TResult>, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + foldl<TValue, TResult>( + callback: MemoIterator<TValue, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduce + **/ + foldl<TValue, TResult>( + callback: MemoIterator<TValue, TResult>, + thisArg?: any): TResult; + } + + //_.reduceRight + interface LoDashStatic { + /** + * This method is like _.reduce except that it iterates over elements of a collection from + * right to left. + * @param collection The collection to iterate over. + * @param callback The function called per iteration. + * @param accumulator Initial value of the accumulator. + * @param thisArg The this binding of callback. + * @return The accumulated value. + **/ + reduceRight<T, TResult>( + collection: Array<T>, + callback: MemoIterator<T, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduceRight + **/ + reduceRight<T, TResult>( + collection: List<T>, + callback: MemoIterator<T, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduceRight + **/ + reduceRight<T, TResult>( + collection: Dictionary<T>, + callback: MemoIterator<T, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduceRight + **/ + reduceRight<T, TResult>( + collection: Array<T>, + callback: MemoIterator<T, TResult>, + thisArg?: any): TResult; + + /** + * @see _.reduceRight + **/ + reduceRight<T, TResult>( + collection: List<T>, + callback: MemoIterator<T, TResult>, + thisArg?: any): TResult; + + /** + * @see _.reduceRight + **/ + reduceRight<T, TResult>( + collection: Dictionary<T>, + callback: MemoIterator<T, TResult>, + thisArg?: any): TResult; + + /** + * @see _.reduceRight + **/ + foldr<T, TResult>( + collection: Array<T>, + callback: MemoIterator<T, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduceRight + **/ + foldr<T, TResult>( + collection: List<T>, + callback: MemoIterator<T, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduceRight + **/ + foldr<T, TResult>( + collection: Dictionary<T>, + callback: MemoIterator<T, TResult>, + accumulator: TResult, + thisArg?: any): TResult; + + /** + * @see _.reduceRight + **/ + foldr<T, TResult>( + collection: Array<T>, + callback: MemoIterator<T, TResult>, + thisArg?: any): TResult; + + /** + * @see _.reduceRight + **/ + foldr<T, TResult>( + collection: List<T>, + callback: MemoIterator<T, TResult>, + thisArg?: any): TResult; + + /** + * @see _.reduceRight + **/ + foldr<T, TResult>( + collection: Dictionary<T>, + callback: MemoIterator<T, TResult>, + thisArg?: any): TResult; + } + + //_.reject + interface LoDashStatic { + /** + * The opposite of _.filter; this method returns the elements of collection that predicate does not return + * truthy for. + * + * @param collection The collection to iterate over. + * @param predicate The function invoked per iteration. + * @param thisArg The this binding of predicate. + * @return Returns the new filtered array. + */ + reject<T>( + collection: List<T>, + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): T[]; + + /** + * @see _.reject + */ + reject<T>( + collection: Dictionary<T>, + predicate?: DictionaryIterator<T, boolean>, + thisArg?: any + ): T[]; + + /** + * @see _.reject + */ + reject( + collection: string, + predicate?: StringIterator<boolean>, + thisArg?: any + ): string[]; + + /** + * @see _.reject + */ + reject<T>( + collection: List<T>|Dictionary<T>, + predicate: string, + thisArg?: any + ): T[]; + + /** + * @see _.reject + */ + reject<W extends {}, T>( + collection: List<T>|Dictionary<T>, + predicate: W + ): T[]; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.reject + */ + reject( + predicate?: StringIterator<boolean>, + thisArg?: any + ): LoDashImplicitArrayWrapper<string>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.reject + */ + reject( + predicate: ListIterator<T, boolean>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.reject + */ + reject( + predicate: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.reject + */ + reject<W>(predicate: W): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.reject + */ + reject<T>( + predicate: ListIterator<T, boolean>|DictionaryIterator<T, boolean>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.reject + */ + reject<T>( + predicate: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.reject + */ + reject<W, T>(predicate: W): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.reject + */ + reject( + predicate?: StringIterator<boolean>, + thisArg?: any + ): LoDashExplicitArrayWrapper<string>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.reject + */ + reject( + predicate: ListIterator<T, boolean>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.reject + */ + reject( + predicate: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.reject + */ + reject<W>(predicate: W): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.reject + */ + reject<T>( + predicate: ListIterator<T, boolean>|DictionaryIterator<T, boolean>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.reject + */ + reject<T>( + predicate: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.reject + */ + reject<W, T>(predicate: W): LoDashExplicitArrayWrapper<T>; + } + + //_.sample + interface LoDashStatic { + /** + * Retrieves a random element or n random elements from a collection. + * @param collection The collection to sample. + * @return Returns the random sample(s) of collection. + **/ + sample<T>(collection: Array<T>): T; + + /** + * @see _.sample + **/ + sample<T>(collection: List<T>): T; + + /** + * @see _.sample + **/ + sample<T>(collection: Dictionary<T>): T; + + /** + * @see _.sample + * @param n The number of elements to sample. + **/ + sample<T>(collection: Array<T>, n: number): T[]; + + /** + * @see _.sample + * @param n The number of elements to sample. + **/ + sample<T>(collection: List<T>, n: number): T[]; + + /** + * @see _.sample + * @param n The number of elements to sample. + **/ + sample<T>(collection: Dictionary<T>, n: number): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.sample + **/ + sample(n: number): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.sample + **/ + sample(): LoDashImplicitWrapper<T>; + } + + //_.select + interface LoDashStatic { + /** + * @see _.filter + */ + select<T>( + collection: List<T>, + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): T[]; + + /** + * @see _.filter + */ + select<T>( + collection: Dictionary<T>, + predicate?: DictionaryIterator<T, boolean>, + thisArg?: any + ): T[]; + + /** + * @see _.filter + */ + select( + collection: string, + predicate?: StringIterator<boolean>, + thisArg?: any + ): string[]; + + /** + * @see _.filter + */ + select<T>( + collection: List<T>|Dictionary<T>, + predicate: string, + thisArg?: any + ): T[]; + + /** + * @see _.filter + */ + select<W extends {}, T>( + collection: List<T>|Dictionary<T>, + predicate: W + ): T[]; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.filter + */ + select( + predicate?: StringIterator<boolean>, + thisArg?: any + ): LoDashImplicitArrayWrapper<string>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.filter + */ + select( + predicate: ListIterator<T, boolean>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.filter + */ + select( + predicate: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.filter + */ + select<W>(predicate: W): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.filter + */ + select<T>( + predicate: ListIterator<T, boolean>|DictionaryIterator<T, boolean>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.filter + */ + select<T>( + predicate: string, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.filter + */ + select<W, T>(predicate: W): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.filter + */ + select( + predicate?: StringIterator<boolean>, + thisArg?: any + ): LoDashExplicitArrayWrapper<string>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.filter + */ + select( + predicate: ListIterator<T, boolean>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.filter + */ + select( + predicate: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.filter + */ + select<W>(predicate: W): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.filter + */ + select<T>( + predicate: ListIterator<T, boolean>|DictionaryIterator<T, boolean>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.filter + */ + select<T>( + predicate: string, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.filter + */ + select<W, T>(predicate: W): LoDashExplicitArrayWrapper<T>; + } + + //_.shuffle + interface LoDashStatic { + /** + * Creates an array of shuffled values, using a version of the Fisher-Yates shuffle. + * + * @param collection The collection to shuffle. + * @return Returns the new shuffled array. + */ + shuffle<T>(collection: List<T>|Dictionary<T>): T[]; + + /** + * @see _.shuffle + */ + shuffle(collection: string): string[]; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.shuffle + */ + shuffle(): LoDashImplicitArrayWrapper<string>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.shuffle + */ + shuffle(): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.shuffle + */ + shuffle<T>(): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.shuffle + */ + shuffle(): LoDashExplicitArrayWrapper<string>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.shuffle + */ + shuffle(): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.shuffle + */ + shuffle<T>(): LoDashExplicitArrayWrapper<T>; + } + + //_.size + interface LoDashStatic { + /** + * Gets the size of collection by returning its length for array-like values or the number of own enumerable + * properties for objects. + * + * @param collection The collection to inspect. + * @return Returns the size of collection. + */ + size<T>(collection: List<T>|Dictionary<T>): number; + + /** + * @see _.size + */ + size(collection: string): number; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.size + */ + size(): number; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.size + */ + size(): number; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.size + */ + size(): number; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.size + */ + size(): LoDashExplicitWrapper<number>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.size + */ + size(): LoDashExplicitWrapper<number>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.size + */ + size(): LoDashExplicitWrapper<number>; + } + + //_.some + interface LoDashStatic { + /** + * Checks if predicate returns truthy for any element of collection. The function returns as soon as it finds + * a passing value and does not iterate over the entire collection. The predicate is bound to thisArg and + * invoked with three arguments: (value, index|key, collection). + * + * If a property name is provided for predicate the created _.property style callback returns the property + * value of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for predicate the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * @alias _.any + * + * @param collection The collection to iterate over. + * @param predicate The function invoked per iteration. + * @param thisArg The this binding of predicate. + * @return Returns true if any element passes the predicate check, else false. + */ + some<T>( + collection: List<T>, + predicate?: ListIterator<T, boolean>, + thisArg?: any + ): boolean; + + /** + * @see _.some + */ + some<T>( + collection: Dictionary<T>, + predicate?: DictionaryIterator<T, boolean>, + thisArg?: any + ): boolean; + + /** + * @see _.some + */ + some<T>( + collection: NumericDictionary<T>, + predicate?: NumericDictionaryIterator<T, boolean>, + thisArg?: any + ): boolean; + + /** + * @see _.some + */ + some<T>( + collection: List<T>|Dictionary<T>|NumericDictionary<T>, + predicate?: string, + thisArg?: any + ): boolean; + + /** + * @see _.some + */ + some<TObject extends {}, T>( + collection: List<T>|Dictionary<T>|NumericDictionary<T>, + predicate?: TObject + ): boolean; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.some + */ + some( + predicate?: ListIterator<T, boolean>|NumericDictionaryIterator<T, boolean>, + thisArg?: any + ): boolean; + + /** + * @see _.some + */ + some( + predicate?: string, + thisArg?: any + ): boolean; + + /** + * @see _.some + */ + some<TObject extends {}>( + predicate?: TObject + ): boolean; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.some + */ + some<TResult>( + predicate?: ListIterator<TResult, boolean>|DictionaryIterator<TResult, boolean>|NumericDictionaryIterator<T, boolean>, + thisArg?: any + ): boolean; + + /** + * @see _.some + */ + some( + predicate?: string, + thisArg?: any + ): boolean; + + /** + * @see _.some + */ + some<TObject extends {}>( + predicate?: TObject + ): boolean; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.some + */ + some( + predicate?: ListIterator<T, boolean>|NumericDictionaryIterator<T, boolean>, + thisArg?: any + ): LoDashExplicitWrapper<boolean>; + + /** + * @see _.some + */ + some( + predicate?: string, + thisArg?: any + ): LoDashExplicitWrapper<boolean>; + + /** + * @see _.some + */ + some<TObject extends {}>( + predicate?: TObject + ): LoDashExplicitWrapper<boolean>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.some + */ + some<TResult>( + predicate?: ListIterator<TResult, boolean>|DictionaryIterator<TResult, boolean>|NumericDictionaryIterator<T, boolean>, + thisArg?: any + ): LoDashExplicitWrapper<boolean>; + + /** + * @see _.some + */ + some( + predicate?: string, + thisArg?: any + ): LoDashExplicitWrapper<boolean>; + + /** + * @see _.some + */ + some<TObject extends {}>( + predicate?: TObject + ): LoDashExplicitWrapper<boolean>; + } + + //_.sortBy + interface LoDashStatic { + /** + * Creates an array of elements, sorted in ascending order by the results of running each element in a + * collection through iteratee. This method performs a stable sort, that is, it preserves the original sort + * order of equal elements. The iteratee is bound to thisArg and invoked with three arguments: + * (value, index|key, collection). + * + * If a property name is provided for iteratee the created _.property style callback returns the property + * valueof the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for iteratee the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * @param collection The collection to iterate over. + * @param iteratee The function invoked per iteration. + * @param thisArg The this binding of iteratee. + * @return Returns the new sorted array. + */ + sortBy<T, TSort>( + collection: List<T>, + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): T[]; + + /** + * @see _.sortBy + */ + sortBy<T, TSort>( + collection: Dictionary<T>, + iteratee?: DictionaryIterator<T, TSort>, + thisArg?: any + ): T[]; + + /** + * @see _.sortBy + */ + sortBy<T>( + collection: List<T>|Dictionary<T>, + iteratee: string + ): T[]; + + /** + * @see _.sortBy + */ + sortBy<W extends {}, T>( + collection: List<T>|Dictionary<T>, + whereValue: W + ): T[]; + + /** + * @see _.sortBy + */ + sortBy<T>( + collection: List<T>|Dictionary<T> + ): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.sortBy + */ + sortBy<TSort>( + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.sortBy + */ + sortBy(iteratee: string): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.sortBy + */ + sortBy<W extends {}>(whereValue: W): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.sortBy + */ + sortBy(): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.sortBy + */ + sortBy<T, TSort>( + iteratee?: ListIterator<T, TSort>|DictionaryIterator<T, TSort>, + thisArg?: any + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.sortBy + */ + sortBy<T>(iteratee: string): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.sortBy + */ + sortBy<W extends {}, T>(whereValue: W): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.sortBy + */ + sortBy<T>(): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.sortBy + */ + sortBy<TSort>( + iteratee?: ListIterator<T, TSort>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.sortBy + */ + sortBy(iteratee: string): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.sortBy + */ + sortBy<W extends {}>(whereValue: W): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.sortBy + */ + sortBy(): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.sortBy + */ + sortBy<T, TSort>( + iteratee?: ListIterator<T, TSort>|DictionaryIterator<T, TSort>, + thisArg?: any + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.sortBy + */ + sortBy<T>(iteratee: string): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.sortBy + */ + sortBy<W extends {}, T>(whereValue: W): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.sortBy + */ + sortBy<T>(): LoDashExplicitArrayWrapper<T>; + } + + //_.sortByAll + interface LoDashStatic { + /** + * This method is like "_.sortBy" except that it can sort by multiple iteratees or + * property names. + * + * If a property name is provided for an iteratee the created "_.property" style callback + * returns the property value of the given element. + * + * If a value is also provided for thisArg the created "_.matchesProperty" style callback + * returns true for elements that have a matching property value, else false. + * + * If an object is provided for an iteratee the created "_.matches" style callback returns + * true for elements that have the properties of the given object, else false. + * + * @param collection The collection to iterate over. + * @param callback The function called per iteration. + * @param thisArg The this binding of callback. + * @return A new array of sorted elements. + **/ + sortByAll<T>( + collection: Array<T>, + iteratees: (ListIterator<T, any>|string|Object)[]): T[]; + + /** + * @see _.sortByAll + **/ + sortByAll<T>( + collection: List<T>, + iteratees: (ListIterator<T, any>|string|Object)[]): T[]; + + /** + * @see _.sortByAll + **/ + sortByAll<T>( + collection: Array<T>, + ...iteratees: (ListIterator<T, any>|string|Object)[]): T[]; + + /** + * @see _.sortByAll + **/ + sortByAll<T>( + collection: List<T>, + ...iteratees: (ListIterator<T, any>|string|Object)[]): T[]; + + /** + * Sorts by all the given arguments, using either ListIterator, pluckValue, or whereValue foramts + * @param args The rules by which to sort + */ + sortByAll<T>( + collection: (Array<T>|List<T>), + ...args: (ListIterator<T, boolean>|Object|string)[] + ): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * Sorts by all the given arguments, using either ListIterator, pluckValue, or whereValue foramts + * @param args The rules by which to sort + */ + sortByAll(...args: (ListIterator<T, boolean>|Object|string)[]): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.sortByAll + **/ + sortByAll( + iteratees: (ListIterator<T, any>|string|Object)[]): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.sortByAll + **/ + sortByAll( + ...iteratees: (ListIterator<T, any>|string|Object)[]): LoDashImplicitArrayWrapper<T>; + } + + //_.sortByOrder + interface LoDashStatic { + /** + * This method is like _.sortByAll except that it allows specifying the sort orders of the iteratees to sort + * by. If orders is unspecified, all values are sorted in ascending order. Otherwise, a value is sorted in + * ascending order if its corresponding order is "asc", and descending if "desc". + * + * If a property name is provided for an iteratee the created _.property style callback returns the property + * value of the given element. + * + * If an object is provided for an iteratee the created _.matches style callback returns true for elements + * that have the properties of the given object, else false. + * + * @param collection The collection to iterate over. + * @param iteratees The iteratees to sort by. + * @param orders The sort orders of iteratees. + * @return Returns the new sorted array. + */ + sortByOrder<W extends Object, T>( + collection: List<T>, + iteratees: ListIterator<T, any>|string|W|(ListIterator<T, any>|string|W)[], + orders?: boolean|string|(boolean|string)[] + ): T[]; + + /** + * @see _.sortByOrder + */ + sortByOrder<T>( + collection: List<T>, + iteratees: ListIterator<T, any>|string|Object|(ListIterator<T, any>|string|Object)[], + orders?: boolean|string|(boolean|string)[] + ): T[]; + + /** + * @see _.sortByOrder + */ + sortByOrder<W extends Object, T>( + collection: NumericDictionary<T>, + iteratees: NumericDictionaryIterator<T, any>|string|W|(NumericDictionaryIterator<T, any>|string|W)[], + orders?: boolean|string|(boolean|string)[] + ): T[]; + + /** + * @see _.sortByOrder + */ + sortByOrder<T>( + collection: NumericDictionary<T>, + iteratees: NumericDictionaryIterator<T, any>|string|Object|(NumericDictionaryIterator<T, any>|string|Object)[], + orders?: boolean|string|(boolean|string)[] + ): T[]; + + /** + * @see _.sortByOrder + */ + sortByOrder<W extends Object, T>( + collection: Dictionary<T>, + iteratees: DictionaryIterator<T, any>|string|W|(DictionaryIterator<T, any>|string|W)[], + orders?: boolean|string|(boolean|string)[] + ): T[]; + + /** + * @see _.sortByOrder + */ + sortByOrder<T>( + collection: Dictionary<T>, + iteratees: DictionaryIterator<T, any>|string|Object|(DictionaryIterator<T, any>|string|Object)[], + orders?: boolean|string|(boolean|string)[] + ): T[]; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.sortByOrder + */ + sortByOrder( + iteratees: ListIterator<T, any>|string|(ListIterator<T, any>|string)[], + orders?: boolean|string|(boolean|string)[] + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.sortByOrder + */ + sortByOrder<W extends Object>( + iteratees: ListIterator<T, any>|string|W|(ListIterator<T, any>|string|W)[], + orders?: boolean|string|(boolean|string)[] + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.sortByOrder + */ + sortByOrder<W extends Object, T>( + iteratees: ListIterator<T, any>|string|W|(ListIterator<T, any>|string|W)[], + orders?: boolean|string|(boolean|string)[] + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.sortByOrder + */ + sortByOrder<T>( + iteratees: ListIterator<T, any>|string|Object|(ListIterator<T, any>|string|Object)[], + orders?: boolean|string|(boolean|string)[] + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.sortByOrder + */ + sortByOrder<W extends Object, T>( + iteratees: NumericDictionaryIterator<T, any>|string|W|(NumericDictionaryIterator<T, any>|string|W)[], + orders?: boolean|string|(boolean|string)[] + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.sortByOrder + */ + sortByOrder<T>( + iteratees: NumericDictionaryIterator<T, any>|string|Object|(NumericDictionaryIterator<T, any>|string|Object)[], + orders?: boolean|string|(boolean|string)[] + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.sortByOrder + */ + sortByOrder<W extends Object, T>( + iteratees: DictionaryIterator<T, any>|string|W|(DictionaryIterator<T, any>|string|W)[], + orders?: boolean|string|(boolean|string)[] + ): LoDashImplicitArrayWrapper<T>; + + /** + * @see _.sortByOrder + */ + sortByOrder<T>( + iteratees: DictionaryIterator<T, any>|string|Object|(DictionaryIterator<T, any>|string|Object)[], + orders?: boolean|string|(boolean|string)[] + ): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.sortByOrder + */ + sortByOrder( + iteratees: ListIterator<T, any>|string|(ListIterator<T, any>|string)[], + orders?: boolean|string|(boolean|string)[] + ): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.sortByOrder + */ + sortByOrder<W extends Object>( + iteratees: ListIterator<T, any>|string|W|(ListIterator<T, any>|string|W)[], + orders?: boolean|string|(boolean|string)[] + ): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.sortByOrder + */ + sortByOrder<W extends Object, T>( + iteratees: ListIterator<T, any>|string|W|(ListIterator<T, any>|string|W)[], + orders?: boolean|string|(boolean|string)[] + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.sortByOrder + */ + sortByOrder<T>( + iteratees: ListIterator<T, any>|string|Object|(ListIterator<T, any>|string|Object)[], + orders?: boolean|string|(boolean|string)[] + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.sortByOrder + */ + sortByOrder<W extends Object, T>( + iteratees: NumericDictionaryIterator<T, any>|string|W|(NumericDictionaryIterator<T, any>|string|W)[], + orders?: boolean|string|(boolean|string)[] + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.sortByOrder + */ + sortByOrder<T>( + iteratees: NumericDictionaryIterator<T, any>|string|Object|(NumericDictionaryIterator<T, any>|string|Object)[], + orders?: boolean|string|(boolean|string)[] + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.sortByOrder + */ + sortByOrder<W extends Object, T>( + iteratees: DictionaryIterator<T, any>|string|W|(DictionaryIterator<T, any>|string|W)[], + orders?: boolean|string|(boolean|string)[] + ): LoDashExplicitArrayWrapper<T>; + + /** + * @see _.sortByOrder + */ + sortByOrder<T>( + iteratees: DictionaryIterator<T, any>|string|Object|(DictionaryIterator<T, any>|string|Object)[], + orders?: boolean|string|(boolean|string)[] + ): LoDashExplicitArrayWrapper<T>; + } + + //_.where + interface LoDashStatic { + /** + * Performs a deep comparison of each element in a collection to the given properties + * object, returning an array of all elements that have equivalent property values. + * @param collection The collection to iterate over. + * @param properties The object of property values to filter by. + * @return A new array of elements that have the given properties. + **/ + where<T, U extends {}>( + list: Array<T>, + properties: U): T[]; + + /** + * @see _.where + **/ + where<T, U extends {}>( + list: List<T>, + properties: U): T[]; + + /** + * @see _.where + **/ + where<T, U extends {}>( + list: Dictionary<T>, + properties: U): T[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.where + **/ + where<U extends {}>(properties: U): LoDashImplicitArrayWrapper<T>; + } + + /******** + * Date * + ********/ + + //_.now + interface LoDashStatic { + /** + * Gets the number of milliseconds that have elapsed since the Unix epoch (1 January 1970 00:00:00 UTC). + * + * @return The number of milliseconds. + */ + now(): number; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.now + */ + now(): number; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.now + */ + now(): LoDashExplicitWrapper<number>; + } + + /************* + * Functions * + *************/ + + //_.after + interface LoDashStatic { + /** + * The opposite of _.before; this method creates a function that invokes func once it’s called n or more times. + * + * @param n The number of calls before func is invoked. + * @param func The function to restrict. + * @return Returns the new restricted function. + */ + after<TFunc extends Function>( + n: number, + func: TFunc + ): TFunc; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.after + **/ + after<TFunc extends Function>(func: TFunc): LoDashImplicitObjectWrapper<TFunc>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.after + **/ + after<TFunc extends Function>(func: TFunc): LoDashExplicitObjectWrapper<TFunc>; + } + + //_.ary + interface LoDashStatic { + /** + * Creates a function that accepts up to n arguments ignoring any additional arguments. + * + * @param func The function to cap arguments for. + * @param n The arity cap. + * @returns Returns the new function. + */ + ary<TResult extends Function>( + func: Function, + n?: number + ): TResult; + + ary<T extends Function, TResult extends Function>( + func: T, + n?: number + ): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.ary + */ + ary<TResult extends Function>(n?: number): LoDashImplicitObjectWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.ary + */ + ary<TResult extends Function>(n?: number): LoDashExplicitObjectWrapper<TResult>; + } + + //_.backflow + interface LoDashStatic { + /** + * @see _.flowRight + */ + backflow<TResult extends Function>(...funcs: Function[]): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.flowRight + */ + backflow<TResult extends Function>(...funcs: Function[]): LoDashImplicitObjectWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.flowRight + */ + backflow<TResult extends Function>(...funcs: Function[]): LoDashExplicitObjectWrapper<TResult>; + } + + //_.before + interface LoDashStatic { + /** + * Creates a function that invokes func, with the this binding and arguments of the created function, while + * it’s called less than n times. Subsequent calls to the created function return the result of the last func + * invocation. + * + * @param n The number of calls at which func is no longer invoked. + * @param func The function to restrict. + * @return Returns the new restricted function. + */ + before<TFunc extends Function>( + n: number, + func: TFunc + ): TFunc; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.before + **/ + before<TFunc extends Function>(func: TFunc): LoDashImplicitObjectWrapper<TFunc>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.before + **/ + before<TFunc extends Function>(func: TFunc): LoDashExplicitObjectWrapper<TFunc>; + } + + //_.bind + interface FunctionBind { + placeholder: any; + + <T extends Function, TResult extends Function>( + func: T, + thisArg: any, + ...partials: any[] + ): TResult; + + <TResult extends Function>( + func: Function, + thisArg: any, + ...partials: any[] + ): TResult; + } + + interface LoDashStatic { + /** + * Creates a function that invokes func with the this binding of thisArg and prepends any additional _.bind + * arguments to those provided to the bound function. + * + * The _.bind.placeholder value, which defaults to _ in monolithic builds, may be used as a placeholder for + * partially applied arguments. + * + * Note: Unlike native Function#bind this method does not set the "length" property of bound functions. + * + * @param func The function to bind. + * @param thisArg The this binding of func. + * @param partials The arguments to be partially applied. + * @return Returns the new bound function. + */ + bind: FunctionBind; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.bind + */ + bind<TResult extends Function>( + thisArg: any, + ...partials: any[] + ): LoDashImplicitObjectWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.bind + */ + bind<TResult extends Function>( + thisArg: any, + ...partials: any[] + ): LoDashExplicitObjectWrapper<TResult>; + } + + //_.bindAll + interface LoDashStatic { + /** + * Binds methods of an object to the object itself, overwriting the existing method. Method names may be + * specified as individual arguments or as arrays of method names. If no method names are provided all + * enumerable function properties, own and inherited, of object are bound. + * + * Note: This method does not set the "length" property of bound functions. + * + * @param object The object to bind and assign the bound methods to. + * @param methodNames The object method names to bind, specified as individual method names or arrays of + * method names. + * @return Returns object. + */ + bindAll<T>( + object: T, + ...methodNames: (string|string[])[] + ): T; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.bindAll + */ + bindAll(...methodNames: (string|string[])[]): LoDashImplicitObjectWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.bindAll + */ + bindAll(...methodNames: (string|string[])[]): LoDashExplicitObjectWrapper<T>; + } + + //_.bindKey + interface FunctionBindKey { + placeholder: any; + + <T extends Object, TResult extends Function>( + object: T, + key: any, + ...partials: any[] + ): TResult; + + <TResult extends Function>( + object: Object, + key: any, + ...partials: any[] + ): TResult; + } + + interface LoDashStatic { + /** + * Creates a function that invokes the method at object[key] and prepends any additional _.bindKey arguments + * to those provided to the bound function. + * + * This method differs from _.bind by allowing bound functions to reference methods that may be redefined + * or don’t yet exist. See Peter Michaux’s article for more details. + * + * The _.bindKey.placeholder value, which defaults to _ in monolithic builds, may be used as a placeholder + * for partially applied arguments. + * + * @param object The object the method belongs to. + * @param key The key of the method. + * @param partials The arguments to be partially applied. + * @return Returns the new bound function. + */ + bindKey: FunctionBindKey; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.bindKey + */ + bindKey<TResult extends Function>( + key: any, + ...partials: any[] + ): LoDashImplicitObjectWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.bindKey + */ + bindKey<TResult extends Function>( + key: any, + ...partials: any[] + ): LoDashExplicitObjectWrapper<TResult>; + } + + //_.compose + interface LoDashStatic { + /** + * @see _.flowRight + */ + compose<TResult extends Function>(...funcs: Function[]): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.flowRight + */ + compose<TResult extends Function>(...funcs: Function[]): LoDashImplicitObjectWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.flowRight + */ + compose<TResult extends Function>(...funcs: Function[]): LoDashExplicitObjectWrapper<TResult>; + } + + //_.createCallback + interface LoDashStatic { + /** + * Produces a callback bound to an optional thisArg. If func is a property name the created + * callback will return the property value for a given element. If func is an object the created + * callback will return true for elements that contain the equivalent object properties, + * otherwise it will return false. + * @param func The value to convert to a callback. + * @param thisArg The this binding of the created callback. + * @param argCount The number of arguments the callback accepts. + * @return A callback function. + **/ + createCallback( + func: string, + thisArg?: any, + argCount?: number): () => any; + + /** + * @see _.createCallback + **/ + createCallback( + func: Dictionary<any>, + thisArg?: any, + argCount?: number): () => boolean; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.createCallback + **/ + createCallback( + thisArg?: any, + argCount?: number): LoDashImplicitObjectWrapper<() => any>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.createCallback + **/ + createCallback( + thisArg?: any, + argCount?: number): LoDashImplicitObjectWrapper<() => any>; + } + + //_.curry + interface LoDashStatic { + /** + * Creates a function that accepts one or more arguments of func that when called either invokes func returning + * its result, if all func arguments have been provided, or returns a function that accepts one or more of the + * remaining func arguments, and so on. The arity of func may be specified if func.length is not sufficient. + * @param func The function to curry. + * @return Returns the new curried function. + */ + curry<T1, R>(func: (t1: T1) => R): + CurriedFunction1<T1, R>; + /** + * Creates a function that accepts one or more arguments of func that when called either invokes func returning + * its result, if all func arguments have been provided, or returns a function that accepts one or more of the + * remaining func arguments, and so on. The arity of func may be specified if func.length is not sufficient. + * @param func The function to curry. + * @return Returns the new curried function. + */ + curry<T1, T2, R>(func: (t1: T1, t2: T2) => R): + CurriedFunction2<T1, T2, R>; + /** + * Creates a function that accepts one or more arguments of func that when called either invokes func returning + * its result, if all func arguments have been provided, or returns a function that accepts one or more of the + * remaining func arguments, and so on. The arity of func may be specified if func.length is not sufficient. + * @param func The function to curry. + * @return Returns the new curried function. + */ + curry<T1, T2, T3, R>(func: (t1: T1, t2: T2, t3: T3) => R): + CurriedFunction3<T1, T2, T3, R>; + /** + * Creates a function that accepts one or more arguments of func that when called either invokes func returning + * its result, if all func arguments have been provided, or returns a function that accepts one or more of the + * remaining func arguments, and so on. The arity of func may be specified if func.length is not sufficient. + * @param func The function to curry. + * @return Returns the new curried function. + */ + curry<T1, T2, T3, T4, R>(func: (t1: T1, t2: T2, t3: T3, t4: T4) => R): + CurriedFunction4<T1, T2, T3, T4, R>; + /** + * Creates a function that accepts one or more arguments of func that when called either invokes func returning + * its result, if all func arguments have been provided, or returns a function that accepts one or more of the + * remaining func arguments, and so on. The arity of func may be specified if func.length is not sufficient. + * @param func The function to curry. + * @return Returns the new curried function. + */ + curry<T1, T2, T3, T4, T5, R>(func: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => R): + CurriedFunction5<T1, T2, T3, T4, T5, R>; + /** + * Creates a function that accepts one or more arguments of func that when called either invokes func returning + * its result, if all func arguments have been provided, or returns a function that accepts one or more of the + * remaining func arguments, and so on. The arity of func may be specified if func.length is not sufficient. + * @param func The function to curry. + * @param arity The arity of func. + * @return Returns the new curried function. + */ + curry<TResult extends Function>( + func: Function, + arity?: number): TResult; + } + + interface CurriedFunction1<T1, R> { + (): CurriedFunction1<T1, R>; + (t1: T1): R; + } + + interface CurriedFunction2<T1, T2, R> { + (): CurriedFunction2<T1, T2, R>; + (t1: T1): CurriedFunction1<T2, R>; + (t1: T1, t2: T2): R; + } + + interface CurriedFunction3<T1, T2, T3, R> { + (): CurriedFunction3<T1, T2, T3, R>; + (t1: T1): CurriedFunction2<T2, T3, R>; + (t1: T1, t2: T2): CurriedFunction1<T3, R>; + (t1: T1, t2: T2, t3: T3): R; + } + + interface CurriedFunction4<T1, T2, T3, T4, R> { + (): CurriedFunction4<T1, T2, T3, T4, R>; + (t1: T1): CurriedFunction3<T2, T3, T4, R>; + (t1: T1, t2: T2): CurriedFunction2<T3, T4, R>; + (t1: T1, t2: T2, t3: T3): CurriedFunction1<T4, R>; + (t1: T1, t2: T2, t3: T3, t4: T4): R; + } + + interface CurriedFunction5<T1, T2, T3, T4, T5, R> { + (): CurriedFunction5<T1, T2, T3, T4, T5, R>; + (t1: T1): CurriedFunction4<T2, T3, T4, T5, R>; + (t1: T1, t2: T2): CurriedFunction3<T3, T4, T5, R>; + (t1: T1, t2: T2, t3: T3): CurriedFunction2<T4, T5, R>; + (t1: T1, t2: T2, t3: T3, t4: T4): CurriedFunction1<T5, R>; + (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5): R; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.curry + **/ + curry<TResult extends Function>(arity?: number): LoDashImplicitObjectWrapper<TResult>; + } + + //_.curryRight + interface LoDashStatic { + /** + * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight + * instead of _.partial. + * @param func The function to curry. + * @return Returns the new curried function. + */ + curryRight<T1, R>(func: (t1: T1) => R): + CurriedFunction1<T1, R>; + /** + * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight + * instead of _.partial. + * @param func The function to curry. + * @return Returns the new curried function. + */ + curryRight<T1, T2, R>(func: (t1: T1, t2: T2) => R): + CurriedFunction2<T2, T1, R>; + /** + * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight + * instead of _.partial. + * @param func The function to curry. + * @return Returns the new curried function. + */ + curryRight<T1, T2, T3, R>(func: (t1: T1, t2: T2, t3: T3) => R): + CurriedFunction3<T3, T2, T1, R>; + /** + * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight + * instead of _.partial. + * @param func The function to curry. + * @return Returns the new curried function. + */ + curryRight<T1, T2, T3, T4, R>(func: (t1: T1, t2: T2, t3: T3, t4: T4) => R): + CurriedFunction4<T4, T3, T2, T1, R>; + /** + * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight + * instead of _.partial. + * @param func The function to curry. + * @return Returns the new curried function. + */ + curryRight<T1, T2, T3, T4, T5, R>(func: (t1: T1, t2: T2, t3: T3, t4: T4, t5: T5) => R): + CurriedFunction5<T5, T4, T3, T2, T1, R>; + /** + * This method is like _.curry except that arguments are applied to func in the manner of _.partialRight + * instead of _.partial. + * @param func The function to curry. + * @param arity The arity of func. + * @return Returns the new curried function. + */ + curryRight<TResult extends Function>( + func: Function, + arity?: number): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.curryRight + **/ + curryRight<TResult extends Function>(arity?: number): LoDashImplicitObjectWrapper<TResult>; + } + + //_.debounce + interface DebounceSettings { + /** + * Specify invoking on the leading edge of the timeout. + */ + leading?: boolean; + + /** + * The maximum time func is allowed to be delayed before it’s invoked. + */ + maxWait?: number; + + /** + * Specify invoking on the trailing edge of the timeout. + */ + trailing?: boolean; + } + + interface LoDashStatic { + /** + * Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since + * the last time the debounced function was invoked. The debounced function comes with a cancel method to + * cancel delayed invocations. Provide an options object to indicate that func should be invoked on the + * leading and/or trailing edge of the wait timeout. Subsequent calls to the debounced function return the + * result of the last func invocation. + * + * Note: If leading and trailing options are true, func is invoked on the trailing edge of the timeout only + * if the the debounced function is invoked more than once during the wait timeout. + * + * See David Corbacho’s article for details over the differences between _.debounce and _.throttle. + * + * @param func The function to debounce. + * @param wait The number of milliseconds to delay. + * @param options The options object. + * @param options.leading Specify invoking on the leading edge of the timeout. + * @param options.maxWait The maximum time func is allowed to be delayed before it’s invoked. + * @param options.trailing Specify invoking on the trailing edge of the timeout. + * @return Returns the new debounced function. + */ + debounce<T extends Function>( + func: T, + wait?: number, + options?: DebounceSettings + ): T & Cancelable; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.debounce + */ + debounce( + wait?: number, + options?: DebounceSettings + ): LoDashImplicitObjectWrapper<T & Cancelable>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.debounce + */ + debounce( + wait?: number, + options?: DebounceSettings + ): LoDashExplicitObjectWrapper<T & Cancelable>; + } + + //_.defer + interface LoDashStatic { + /** + * Defers invoking the func until the current call stack has cleared. Any additional arguments are provided to + * func when it’s invoked. + * + * @param func The function to defer. + * @param args The arguments to invoke the function with. + * @return Returns the timer id. + */ + defer<T extends Function>( + func: T, + ...args: any[] + ): number; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.defer + */ + defer(...args: any[]): LoDashImplicitWrapper<number>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.defer + */ + defer(...args: any[]): LoDashExplicitWrapper<number>; + } + + //_.delay + interface LoDashStatic { + /** + * Invokes func after wait milliseconds. Any additional arguments are provided to func when it’s invoked. + * + * @param func The function to delay. + * @param wait The number of milliseconds to delay invocation. + * @param args The arguments to invoke the function with. + * @return Returns the timer id. + */ + delay<T extends Function>( + func: T, + wait: number, + ...args: any[] + ): number; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.delay + */ + delay( + wait: number, + ...args: any[] + ): LoDashImplicitWrapper<number>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.delay + */ + delay( + wait: number, + ...args: any[] + ): LoDashExplicitWrapper<number>; + } + + //_.flow + interface LoDashStatic { + /** + * Creates a function that returns the result of invoking the provided functions with the this binding of the + * created function, where each successive invocation is supplied the return value of the previous. + * + * @param funcs Functions to invoke. + * @return Returns the new function. + */ + flow<TResult extends Function>(...funcs: Function[]): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.flow + */ + flow<TResult extends Function>(...funcs: Function[]): LoDashImplicitObjectWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.flow + */ + flow<TResult extends Function>(...funcs: Function[]): LoDashExplicitObjectWrapper<TResult>; + } + + //_.flowRight + interface LoDashStatic { + /** + * This method is like _.flow except that it creates a function that invokes the provided functions from right + * to left. + * + * @alias _.backflow, _.compose + * + * @param funcs Functions to invoke. + * @return Returns the new function. + */ + flowRight<TResult extends Function>(...funcs: Function[]): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.flowRight + */ + flowRight<TResult extends Function>(...funcs: Function[]): LoDashImplicitObjectWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.flowRight + */ + flowRight<TResult extends Function>(...funcs: Function[]): LoDashExplicitObjectWrapper<TResult>; + } + + + //_.memoize + interface MemoizedFunction extends Function { + cache: MapCache; + } + + interface LoDashStatic { + /** + * Creates a function that memoizes the result of func. If resolver is provided it determines the cache key for + * storing the result based on the arguments provided to the memoized function. By default, the first argument + * provided to the memoized function is coerced to a string and used as the cache key. The func is invoked with + * the this binding of the memoized function. + * @param func The function to have its output memoized. + * @param resolver The function to resolve the cache key. + * @return Returns the new memoizing function. + */ + memoize<TResult extends MemoizedFunction>( + func: Function, + resolver?: Function): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.memoize + */ + memoize<TResult extends MemoizedFunction>(resolver?: Function): LoDashImplicitObjectWrapper<TResult>; + } + + //_.modArgs + interface LoDashStatic { + /** + * Creates a function that runs each argument through a corresponding transform function. + * + * @param func The function to wrap. + * @param transforms The functions to transform arguments, specified as individual functions or arrays + * of functions. + * @return Returns the new function. + */ + modArgs<T extends Function, TResult extends Function>( + func: T, + ...transforms: Function[] + ): TResult; + + /** + * @see _.modArgs + */ + modArgs<T extends Function, TResult extends Function>( + func: T, + transforms: Function[] + ): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.modArgs + */ + modArgs<TResult extends Function>(...transforms: Function[]): LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.modArgs + */ + modArgs<TResult extends Function>(transforms: Function[]): LoDashImplicitObjectWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.modArgs + */ + modArgs<TResult extends Function>(...transforms: Function[]): LoDashExplicitObjectWrapper<TResult>; + + /** + * @see _.modArgs + */ + modArgs<TResult extends Function>(transforms: Function[]): LoDashExplicitObjectWrapper<TResult>; + } + + //_.negate + interface LoDashStatic { + /** + * Creates a function that negates the result of the predicate func. The func predicate is invoked with + * the this binding and arguments of the created function. + * + * @param predicate The predicate to negate. + * @return Returns the new function. + */ + negate<T extends Function>(predicate: T): (...args: any[]) => boolean; + + /** + * @see _.negate + */ + negate<T extends Function, TResult extends Function>(predicate: T): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.negate + */ + negate(): LoDashImplicitObjectWrapper<(...args: any[]) => boolean>; + + /** + * @see _.negate + */ + negate<TResult extends Function>(): LoDashImplicitObjectWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.negate + */ + negate(): LoDashExplicitObjectWrapper<(...args: any[]) => boolean>; + + /** + * @see _.negate + */ + negate<TResult extends Function>(): LoDashExplicitObjectWrapper<TResult>; + } + + //_.once + interface LoDashStatic { + /** + * Creates a function that is restricted to invoking func once. Repeat calls to the function return the value + * of the first call. The func is invoked with the this binding and arguments of the created function. + * + * @param func The function to restrict. + * @return Returns the new restricted function. + */ + once<T extends Function>(func: T): T; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.once + */ + once(): LoDashImplicitObjectWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.once + */ + once(): LoDashExplicitObjectWrapper<T>; + } + + //_.partial + interface LoDashStatic { + /** + * Creates a function that, when called, invokes func with any additional partial arguments + * prepended to those provided to the new function. This method is similar to _.bind except + * it does not alter the this binding. + * @param func The function to partially apply arguments to. + * @param args Arguments to be partially applied. + * @return The new partially applied function. + **/ + partial: Partial; + } + + type PH = LoDashStatic; + + interface Function0<R> { + (): R; + } + interface Function1<T1, R> { + (t1: T1): R; + } + interface Function2<T1, T2, R> { + (t1: T1, t2: T2): R; + } + interface Function3<T1, T2, T3, R> { + (t1: T1, t2: T2, t3: T3): R; + } + interface Function4<T1, T2, T3, T4, R> { + (t1: T1, t2: T2, t3: T3, t4: T4): R; + } + + interface Partial { + // arity 0 + <R>(func: Function0<R>): Function0<R>; + // arity 1 + <T1, R>(func: Function1<T1, R>): Function1<T1, R>; + <T1, R>(func: Function1<T1, R>, arg1: T1): Function0<R>; + // arity 2 + <T1, T2, R>(func: Function2<T1, T2, R>): Function2<T1, T2, R>; + <T1, T2, R>(func: Function2<T1, T2, R>, arg1: T1): Function1< T2, R>; + <T1, T2, R>(func: Function2<T1, T2, R>, plc1: PH, arg2: T2): Function1<T1, R>; + <T1, T2, R>(func: Function2<T1, T2, R>, arg1: T1, arg2: T2): Function0< R>; + // arity 3 + <T1, T2, T3, R>(func: Function3<T1, T2, T3, R>): Function3<T1, T2, T3, R>; + <T1, T2, T3, R>(func: Function3<T1, T2, T3, R>, arg1: T1): Function2< T2, T3, R>; + <T1, T2, T3, R>(func: Function3<T1, T2, T3, R>, plc1: PH, arg2: T2): Function2<T1, T3, R>; + <T1, T2, T3, R>(func: Function3<T1, T2, T3, R>, arg1: T1, arg2: T2): Function1< T3, R>; + <T1, T2, T3, R>(func: Function3<T1, T2, T3, R>, plc1: PH, plc2: PH, arg3: T3): Function2<T1, T2, R>; + <T1, T2, T3, R>(func: Function3<T1, T2, T3, R>, arg1: T1, plc2: PH, arg3: T3): Function1< T2, R>; + <T1, T2, T3, R>(func: Function3<T1, T2, T3, R>, plc1: PH, arg2: T2, arg3: T3): Function1<T1, R>; + <T1, T2, T3, R>(func: Function3<T1, T2, T3, R>, arg1: T1, arg2: T2, arg3: T3): Function0< R>; + // arity 4 + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>): Function4<T1, T2, T3, T4, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg1: T1): Function3< T2, T3, T4, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, plc1: PH, arg2: T2): Function3<T1, T3, T4, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg1: T1, arg2: T2): Function2< T3, T4, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, plc1: PH, plc2: PH, arg3: T3): Function3<T1, T2, T4, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg1: T1, plc2: PH, arg3: T3): Function2< T2, T4, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, plc1: PH, arg2: T2, arg3: T3): Function2<T1, T4, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg1: T1, arg2: T2, arg3: T3): Function1< T4, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, plc1: PH, plc2: PH, plc3: PH, arg4: T4): Function3<T1, T2, T3, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg1: T1, plc2: PH, plc3: PH, arg4: T4): Function2< T2, T3, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, plc1: PH, arg2: T2, plc3: PH, arg4: T4): Function2<T1, T3, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg1: T1, arg2: T2, plc3: PH, arg4: T4): Function1< T3, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, plc1: PH, plc2: PH, arg3: T3, arg4: T4): Function2<T1, T2, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg1: T1, plc2: PH, arg3: T3, arg4: T4): Function1< T2, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, plc1: PH, arg2: T2, arg3: T3, arg4: T4): Function1<T1, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg1: T1, arg2: T2, arg3: T3, arg4: T4): Function0< R>; + // catch-all + (func: Function, ...args: any[]): Function; + } + + //_.partialRight + interface LoDashStatic { + /** + * This method is like _.partial except that partial arguments are appended to those provided + * to the new function. + * @param func The function to partially apply arguments to. + * @param args Arguments to be partially applied. + * @return The new partially applied function. + **/ + partialRight: PartialRight + } + + interface PartialRight { + // arity 0 + <R>(func: Function0<R>): Function0<R>; + // arity 1 + <T1, R>(func: Function1<T1, R>): Function1<T1, R>; + <T1, R>(func: Function1<T1, R>, arg1: T1): Function0<R>; + // arity 2 + <T1, T2, R>(func: Function2<T1, T2, R>): Function2<T1, T2, R>; + <T1, T2, R>(func: Function2<T1, T2, R>, arg1: T1, plc2: PH): Function1< T2, R>; + <T1, T2, R>(func: Function2<T1, T2, R>, arg2: T2): Function1<T1, R>; + <T1, T2, R>(func: Function2<T1, T2, R>, arg1: T1, arg2: T2): Function0< R>; + // arity 3 + <T1, T2, T3, R>(func: Function3<T1, T2, T3, R>): Function3<T1, T2, T3, R>; + <T1, T2, T3, R>(func: Function3<T1, T2, T3, R>, arg1: T1, plc2: PH, plc3: PH): Function2< T2, T3, R>; + <T1, T2, T3, R>(func: Function3<T1, T2, T3, R>, arg2: T2, plc3: PH): Function2<T1, T3, R>; + <T1, T2, T3, R>(func: Function3<T1, T2, T3, R>, arg1: T1, arg2: T2, plc3: PH): Function1< T3, R>; + <T1, T2, T3, R>(func: Function3<T1, T2, T3, R>, arg3: T3): Function2<T1, T2, R>; + <T1, T2, T3, R>(func: Function3<T1, T2, T3, R>, arg1: T1, plc2: PH, arg3: T3): Function1< T2, R>; + <T1, T2, T3, R>(func: Function3<T1, T2, T3, R>, arg2: T2, arg3: T3): Function1<T1, R>; + <T1, T2, T3, R>(func: Function3<T1, T2, T3, R>, arg1: T1, arg2: T2, arg3: T3): Function0< R>; + // arity 4 + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>): Function4<T1, T2, T3, T4, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg1: T1, plc2: PH, plc3: PH, plc4: PH): Function3< T2, T3, T4, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg2: T2, plc3: PH, plc4: PH): Function3<T1, T3, T4, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg1: T1, arg2: T2, plc3: PH, plc4: PH): Function2< T3, T4, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg3: T3, plc4: PH): Function3<T1, T2, T4, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg1: T1, plc2: PH, arg3: T3, plc4: PH): Function2< T2, T4, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg2: T2, arg3: T3, plc4: PH): Function2<T1, T4, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg1: T1, arg2: T2, arg3: T3, plc4: PH): Function1< T4, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg4: T4): Function3<T1, T2, T3, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg1: T1, plc2: PH, plc3: PH, arg4: T4): Function2< T2, T3, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg2: T2, plc3: PH, arg4: T4): Function2<T1, T3, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg1: T1, arg2: T2, plc3: PH, arg4: T4): Function1< T3, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg3: T3, arg4: T4): Function2<T1, T2, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg1: T1, plc2: PH, arg3: T3, arg4: T4): Function1< T2, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg2: T2, arg3: T3, arg4: T4): Function1<T1, R>; + <T1, T2, T3, T4, R>(func: Function4<T1, T2, T3, T4, R>, arg1: T1, arg2: T2, arg3: T3, arg4: T4): Function0< R>; + // catch-all + (func: Function, ...args: any[]): Function; + } + + //_.rearg + interface LoDashStatic { + /** + * Creates a function that invokes func with arguments arranged according to the specified indexes where the + * argument value at the first index is provided as the first argument, the argument value at the second index + * is provided as the second argument, and so on. + * @param func The function to rearrange arguments for. + * @param indexes The arranged argument indexes, specified as individual indexes or arrays of indexes. + * @return Returns the new function. + */ + rearg<TResult extends Function>(func: Function, indexes: number[]): TResult; + + /** + * @see _.rearg + */ + rearg<TResult extends Function>(func: Function, ...indexes: number[]): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.rearg + */ + rearg<TResult extends Function>(indexes: number[]): LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.rearg + */ + rearg<TResult extends Function>(...indexes: number[]): LoDashImplicitObjectWrapper<TResult>; + } + + //_.restParam + interface LoDashStatic { + /** + * Creates a function that invokes func with the this binding of the created function and arguments from start + * and beyond provided as an array. + * + * Note: This method is based on the rest parameter. + * + * @param func The function to apply a rest parameter to. + * @param start The start position of the rest parameter. + * @return Returns the new function. + */ + restParam<TResult extends Function>( + func: Function, + start?: number + ): TResult; + + /** + * @see _.restParam + */ + restParam<TResult extends Function, TFunc extends Function>( + func: TFunc, + start?: number + ): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.restParam + */ + restParam<TResult extends Function>(start?: number): LoDashImplicitObjectWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.restParam + */ + restParam<TResult extends Function>(start?: number): LoDashExplicitObjectWrapper<TResult>; + } + + //_.spread + interface LoDashStatic { + /** + * Creates a function that invokes func with the this binding of the created function and an array of arguments + * much like Function#apply. + * + * Note: This method is based on the spread operator. + * + * @param func The function to spread arguments over. + * @return Returns the new function. + */ + spread<F extends Function, T extends Function>(func: F): T; + + /** + * @see _.spread + */ + spread<T extends Function>(func: Function): T; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.spread + */ + spread<T extends Function>(): LoDashImplicitObjectWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.spread + */ + spread<T extends Function>(): LoDashExplicitObjectWrapper<T>; + } + + //_.throttle + interface ThrottleSettings { + /** + * If you'd like to disable the leading-edge call, pass this as false. + */ + leading?: boolean; + + /** + * If you'd like to disable the execution on the trailing-edge, pass false. + */ + trailing?: boolean; + } + + interface LoDashStatic { + /** + * Creates a throttled function that only invokes func at most once per every wait milliseconds. The throttled + * function comes with a cancel method to cancel delayed invocations. Provide an options object to indicate + * that func should be invoked on the leading and/or trailing edge of the wait timeout. Subsequent calls to + * the throttled function return the result of the last func call. + * + * Note: If leading and trailing options are true, func is invoked on the trailing edge of the timeout only if + * the the throttled function is invoked more than once during the wait timeout. + * + * @param func The function to throttle. + * @param wait The number of milliseconds to throttle invocations to. + * @param options The options object. + * @param options.leading Specify invoking on the leading edge of the timeout. + * @param options.trailing Specify invoking on the trailing edge of the timeout. + * @return Returns the new throttled function. + */ + throttle<T extends Function>( + func: T, + wait?: number, + options?: ThrottleSettings + ): T & Cancelable; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.throttle + */ + throttle( + wait?: number, + options?: ThrottleSettings + ): LoDashImplicitObjectWrapper<T & Cancelable>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.throttle + */ + throttle( + wait?: number, + options?: ThrottleSettings + ): LoDashExplicitObjectWrapper<T & Cancelable>; + } + + //_.wrap + interface LoDashStatic { + /** + * Creates a function that provides value to the wrapper function as its first argument. Any additional + * arguments provided to the function are appended to those provided to the wrapper function. The wrapper is + * invoked with the this binding of the created function. + * + * @param value The value to wrap. + * @param wrapper The wrapper function. + * @return Returns the new function. + */ + wrap<V, W extends Function, R extends Function>( + value: V, + wrapper: W + ): R; + + /** + * @see _.wrap + */ + wrap<V, R extends Function>( + value: V, + wrapper: Function + ): R; + + /** + * @see _.wrap + */ + wrap<R extends Function>( + value: any, + wrapper: Function + ): R; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.wrap + */ + wrap<W extends Function, R extends Function>(wrapper: W): LoDashImplicitObjectWrapper<R>; + + /** + * @see _.wrap + */ + wrap<R extends Function>(wrapper: Function): LoDashImplicitObjectWrapper<R>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.wrap + */ + wrap<W extends Function, R extends Function>(wrapper: W): LoDashImplicitObjectWrapper<R>; + + /** + * @see _.wrap + */ + wrap<R extends Function>(wrapper: Function): LoDashImplicitObjectWrapper<R>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.wrap + */ + wrap<W extends Function, R extends Function>(wrapper: W): LoDashImplicitObjectWrapper<R>; + + /** + * @see _.wrap + */ + wrap<R extends Function>(wrapper: Function): LoDashImplicitObjectWrapper<R>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.wrap + */ + wrap<W extends Function, R extends Function>(wrapper: W): LoDashExplicitObjectWrapper<R>; + + /** + * @see _.wrap + */ + wrap<R extends Function>(wrapper: Function): LoDashExplicitObjectWrapper<R>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.wrap + */ + wrap<W extends Function, R extends Function>(wrapper: W): LoDashExplicitObjectWrapper<R>; + + /** + * @see _.wrap + */ + wrap<R extends Function>(wrapper: Function): LoDashExplicitObjectWrapper<R>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.wrap + */ + wrap<W extends Function, R extends Function>(wrapper: W): LoDashExplicitObjectWrapper<R>; + + /** + * @see _.wrap + */ + wrap<R extends Function>(wrapper: Function): LoDashExplicitObjectWrapper<R>; + } + + /******** + * Lang * + ********/ + + //_.clone + interface LoDashStatic { + /** + * Creates a clone of value. If isDeep is true nested objects are cloned, otherwise they are assigned by + * reference. If customizer is provided it’s invoked to produce the cloned values. If customizer returns + * undefined cloning is handled by the method instead. The customizer is bound to thisArg and invoked with up + * to three argument; (value [, index|key, object]). + * Note: This method is loosely based on the structured clone algorithm. The enumerable properties of arguments + * objects and objects created by constructors other than Object are cloned to plain Object objects. An empty + * object is returned for uncloneable values such as functions, DOM nodes, Maps, Sets, and WeakMaps. + * @param value The value to clone. + * @param isDeep Specify a deep clone. + * @param customizer The function to customize cloning values. + * @param thisArg The this binding of customizer. + * @return Returns the cloned value. + */ + clone<T>( + value: T, + isDeep?: boolean, + customizer?: (value: any) => any, + thisArg?: any): T; + + /** + * @see _.clone + */ + clone<T>( + value: T, + customizer?: (value: any) => any, + thisArg?: any): T; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.clone + */ + clone( + isDeep?: boolean, + customizer?: (value: any) => any, + thisArg?: any): T; + + /** + * @see _.clone + */ + clone( + customizer?: (value: any) => any, + thisArg?: any): T; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.clone + */ + clone( + isDeep?: boolean, + customizer?: (value: any) => any, + thisArg?: any): T[]; + + /** + * @see _.clone + */ + clone( + customizer?: (value: any) => any, + thisArg?: any): T[]; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.clone + */ + clone( + isDeep?: boolean, + customizer?: (value: any) => any, + thisArg?: any): T; + + /** + * @see _.clone + */ + clone( + customizer?: (value: any) => any, + thisArg?: any): T; + } + + //_.cloneDeep + interface LoDashStatic { + /** + * Creates a deep clone of value. If customizer is provided it’s invoked to produce the cloned values. If + * customizer returns undefined cloning is handled by the method instead. The customizer is bound to thisArg + * and invoked with up to three argument; (value [, index|key, object]). + * Note: This method is loosely based on the structured clone algorithm. The enumerable properties of arguments + * objects and objects created by constructors other than Object are cloned to plain Object objects. An empty + * object is returned for uncloneable values such as functions, DOM nodes, Maps, Sets, and WeakMaps. + * @param value The value to deep clone. + * @param customizer The function to customize cloning values. + * @param thisArg The this binding of customizer. + * @return Returns the deep cloned value. + */ + cloneDeep<T>( + value: T, + customizer?: (value: any) => any, + thisArg?: any): T; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.cloneDeep + */ + cloneDeep( + customizer?: (value: any) => any, + thisArg?: any): T; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.cloneDeep + */ + cloneDeep( + customizer?: (value: any) => any, + thisArg?: any): T[]; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.cloneDeep + */ + cloneDeep( + customizer?: (value: any) => any, + thisArg?: any): T; + } + + //_.eq + interface LoDashStatic { + /** + * @see _.isEqual + */ + eq( + value: any, + other: any, + customizer?: IsEqualCustomizer, + thisArg?: any + ): boolean; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.isEqual + */ + eq( + other: any, + customizer?: IsEqualCustomizer, + thisArg?: any + ): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.isEqual + */ + eq( + other: any, + customizer?: IsEqualCustomizer, + thisArg?: any + ): LoDashExplicitWrapper<boolean>; + } + + //_.gt + interface LoDashStatic { + /** + * Checks if value is greater than other. + * + * @param value The value to compare. + * @param other The other value to compare. + * @return Returns true if value is greater than other, else false. + */ + gt( + value: any, + other: any + ): boolean; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.gt + */ + gt(other: any): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.gt + */ + gt(other: any): LoDashExplicitWrapper<boolean>; + } + + //_.gte + interface LoDashStatic { + /** + * Checks if value is greater than or equal to other. + * + * @param value The value to compare. + * @param other The other value to compare. + * @return Returns true if value is greater than or equal to other, else false. + */ + gte( + value: any, + other: any + ): boolean; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.gte + */ + gte(other: any): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.gte + */ + gte(other: any): LoDashExplicitWrapper<boolean>; + } + + //_.isArguments + interface LoDashStatic { + /** + * Checks if value is classified as an arguments object. + * + * @param value The value to check. + * @return Returns true if value is correctly classified, else false. + */ + isArguments(value?: any): value is IArguments; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.isArguments + */ + isArguments(): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.isArguments + */ + isArguments(): LoDashExplicitWrapper<boolean>; + } + + //_.isArray + interface LoDashStatic { + /** + * Checks if value is classified as an Array object. + * @param value The value to check. + * + * @return Returns true if value is correctly classified, else false. + */ + isArray<T>(value?: any): value is T[]; + } + + interface LoDashImplicitWrapperBase<T,TWrapper> { + /** + * @see _.isArray + */ + isArray(): boolean; + } + + interface LoDashExplicitWrapperBase<T,TWrapper> { + /** + * @see _.isArray + */ + isArray(): LoDashExplicitWrapper<boolean>; + } + + //_.isBoolean + interface LoDashStatic { + /** + * Checks if value is classified as a boolean primitive or object. + * + * @param value The value to check. + * @return Returns true if value is correctly classified, else false. + */ + isBoolean(value?: any): value is boolean; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.isBoolean + */ + isBoolean(): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.isBoolean + */ + isBoolean(): LoDashExplicitWrapper<boolean>; + } + + //_.isDate + interface LoDashStatic { + /** + * Checks if value is classified as a Date object. + * @param value The value to check. + * + * @return Returns true if value is correctly classified, else false. + */ + isDate(value?: any): value is Date; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.isDate + */ + isDate(): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.isDate + */ + isDate(): LoDashExplicitWrapper<boolean>; + } + + //_.isElement + interface LoDashStatic { + /** + * Checks if value is a DOM element. + * + * @param value The value to check. + * @return Returns true if value is a DOM element, else false. + */ + isElement(value?: any): boolean; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.isElement + */ + isElement(): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.isElement + */ + isElement(): LoDashExplicitWrapper<boolean>; + } + + //_.isEmpty + interface LoDashStatic { + /** + * Checks if value is empty. A value is considered empty unless it’s an arguments object, array, string, or + * jQuery-like collection with a length greater than 0 or an object with own enumerable properties. + * + * @param value The value to inspect. + * @return Returns true if value is empty, else false. + */ + isEmpty(value?: any): boolean; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.isEmpty + */ + isEmpty(): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.isEmpty + */ + isEmpty(): LoDashExplicitWrapper<boolean>; + } + + //_.isEqual + interface IsEqualCustomizer { + (value: any, other: any, indexOrKey?: number|string): boolean; + } + + interface LoDashStatic { + /** + * Performs a deep comparison between two values to determine if they are equivalent. If customizer is + * provided it’s invoked to compare values. If customizer returns undefined comparisons are handled by the + * method instead. The customizer is bound to thisArg and invoked with up to three arguments: (value, other + * [, index|key]). + * + * Note: This method supports comparing arrays, booleans, Date objects, numbers, Object objects, regexes, + * and strings. Objects are compared by their own, not inherited, enumerable properties. Functions and DOM + * nodes are not supported. Provide a customizer function to extend support for comparing other values. + * + * @alias _.eq + * + * @param value The value to compare. + * @param other The other value to compare. + * @param customizer The function to customize value comparisons. + * @param thisArg The this binding of customizer. + * @return Returns true if the values are equivalent, else false. + */ + isEqual( + value: any, + other: any, + customizer?: IsEqualCustomizer, + thisArg?: any + ): boolean; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.isEqual + */ + isEqual( + other: any, + customizer?: IsEqualCustomizer, + thisArg?: any + ): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.isEqual + */ + isEqual( + other: any, + customizer?: IsEqualCustomizer, + thisArg?: any + ): LoDashExplicitWrapper<boolean>; + } + + //_.isError + interface LoDashStatic { + /** + * Checks if value is an Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, or URIError + * object. + * + * @param value The value to check. + * @return Returns true if value is an error object, else false. + */ + isError(value: any): value is Error; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.isError + */ + isError(): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.isError + */ + isError(): LoDashExplicitWrapper<boolean>; + } + + //_.isFinite + interface LoDashStatic { + /** + * Checks if value is a finite primitive number. + * + * Note: This method is based on Number.isFinite. + * + * @param value The value to check. + * @return Returns true if value is a finite number, else false. + */ + isFinite(value?: any): boolean; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.isFinite + */ + isFinite(): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.isFinite + */ + isFinite(): LoDashExplicitWrapper<boolean>; + } + + //_.isFunction + interface LoDashStatic { + /** + * Checks if value is classified as a Function object. + * + * @param value The value to check. + * @return Returns true if value is correctly classified, else false. + */ + isFunction(value?: any): value is Function; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.isFunction + */ + isFunction(): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.isFunction + */ + isFunction(): LoDashExplicitWrapper<boolean>; + } + + //_.isMatch + interface isMatchCustomizer { + (value: any, other: any, indexOrKey?: number|string): boolean; + } + + interface LoDashStatic { + /** + * Performs a deep comparison between object and source to determine if object contains equivalent property + * values. If customizer is provided it’s invoked to compare values. If customizer returns undefined + * comparisons are handled by the method instead. The customizer is bound to thisArg and invoked with three + * arguments: (value, other, index|key). + * @param object The object to inspect. + * @param source The object of property values to match. + * @param customizer The function to customize value comparisons. + * @param thisArg The this binding of customizer. + * @return Returns true if object is a match, else false. + */ + isMatch(object: Object, source: Object, customizer?: isMatchCustomizer, thisArg?: any): boolean; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.isMatch + */ + isMatch(source: Object, customizer?: isMatchCustomizer, thisArg?: any): boolean; + } + + //_.isNaN + interface LoDashStatic { + /** + * Checks if value is NaN. + * + * Note: This method is not the same as isNaN which returns true for undefined and other non-numeric values. + * + * @param value The value to check. + * @return Returns true if value is NaN, else false. + */ + isNaN(value?: any): boolean; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.isNaN + */ + isNaN(): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.isNaN + */ + isNaN(): LoDashExplicitWrapper<boolean>; + } + + //_.isNative + interface LoDashStatic { + /** + * Checks if value is a native function. + * @param value The value to check. + * + * @retrun Returns true if value is a native function, else false. + */ + isNative(value: any): value is Function; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * see _.isNative + */ + isNative(): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * see _.isNative + */ + isNative(): LoDashExplicitWrapper<boolean>; + } + + //_.isNull + interface LoDashStatic { + /** + * Checks if value is null. + * + * @param value The value to check. + * @return Returns true if value is null, else false. + */ + isNull(value?: any): boolean; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * see _.isNull + */ + isNull(): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * see _.isNull + */ + isNull(): LoDashExplicitWrapper<boolean>; + } + + //_.isNumber + interface LoDashStatic { + /** + * Checks if value is classified as a Number primitive or object. + * + * Note: To exclude Infinity, -Infinity, and NaN, which are classified as numbers, use the _.isFinite method. + * + * @param value The value to check. + * @return Returns true if value is correctly classified, else false. + */ + isNumber(value?: any): value is number; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * see _.isNumber + */ + isNumber(): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * see _.isNumber + */ + isNumber(): LoDashExplicitWrapper<boolean>; + } + + //_.isObject + interface LoDashStatic { + /** + * Checks if value is the language type of Object. (e.g. arrays, functions, objects, regexes, new Number(0), + * and new String('')) + * + * @param value The value to check. + * @return Returns true if value is an object, else false. + */ + isObject(value?: any): boolean; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * see _.isObject + */ + isObject(): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * see _.isObject + */ + isObject(): LoDashExplicitWrapper<boolean>; + } + + //_.isPlainObject + interface LoDashStatic { + /** + * Checks if value is a plain object, that is, an object created by the Object constructor or one with a + * [[Prototype]] of null. + * + * Note: This method assumes objects created by the Object constructor have no inherited enumerable properties. + * + * @param value The value to check. + * @return Returns true if value is a plain object, else false. + */ + isPlainObject(value?: any): boolean; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * see _.isPlainObject + */ + isPlainObject(): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * see _.isPlainObject + */ + isPlainObject(): LoDashExplicitWrapper<boolean>; + } + + //_.isRegExp + interface LoDashStatic { + /** + * Checks if value is classified as a RegExp object. + * @param value The value to check. + * + * @return Returns true if value is correctly classified, else false. + */ + isRegExp(value?: any): value is RegExp; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * see _.isRegExp + */ + isRegExp(): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * see _.isRegExp + */ + isRegExp(): LoDashExplicitWrapper<boolean>; + } + + //_.isString + interface LoDashStatic { + /** + * Checks if value is classified as a String primitive or object. + * + * @param value The value to check. + * @return Returns true if value is correctly classified, else false. + */ + isString(value?: any): value is string; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * see _.isString + */ + isString(): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * see _.isString + */ + isString(): LoDashExplicitWrapper<boolean>; + } + + //_.isTypedArray + interface LoDashStatic { + /** + * Checks if value is classified as a typed array. + * + * @param value The value to check. + * @return Returns true if value is correctly classified, else false. + */ + isTypedArray(value: any): boolean; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * see _.isTypedArray + */ + isTypedArray(): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * see _.isTypedArray + */ + isTypedArray(): LoDashExplicitWrapper<boolean>; + } + + //_.isUndefined + interface LoDashStatic { + /** + * Checks if value is undefined. + * + * @param value The value to check. + * @return Returns true if value is undefined, else false. + */ + isUndefined(value: any): boolean; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * see _.isUndefined + */ + isUndefined(): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * see _.isUndefined + */ + isUndefined(): LoDashExplicitWrapper<boolean>; + } + + //_.lt + interface LoDashStatic { + /** + * Checks if value is less than other. + * + * @param value The value to compare. + * @param other The other value to compare. + * @return Returns true if value is less than other, else false. + */ + lt( + value: any, + other: any + ): boolean; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.lt + */ + lt(other: any): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.lt + */ + lt(other: any): LoDashExplicitWrapper<boolean>; + } + + //_.lte + interface LoDashStatic { + /** + * Checks if value is less than or equal to other. + * + * @param value The value to compare. + * @param other The other value to compare. + * @return Returns true if value is less than or equal to other, else false. + */ + lte( + value: any, + other: any + ): boolean; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.lte + */ + lte(other: any): boolean; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.lte + */ + lte(other: any): LoDashExplicitWrapper<boolean>; + } + + //_.toArray + interface LoDashStatic { + /** + * Converts value to an array. + * + * @param value The value to convert. + * @return Returns the converted array. + */ + toArray<T>(value: List<T>|Dictionary<T>|NumericDictionary<T>): T[]; + + /** + * @see _.toArray + */ + toArray<TValue, TResult>(value: TValue): TResult[]; + + /** + * @see _.toArray + */ + toArray<TResult>(value?: any): TResult[]; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.toArray + */ + toArray<TResult>(): LoDashImplicitArrayWrapper<TResult>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.toArray + */ + toArray(): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.toArray + */ + toArray<TResult>(): LoDashImplicitArrayWrapper<TResult>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.toArray + */ + toArray<TResult>(): LoDashExplicitArrayWrapper<TResult>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.toArray + */ + toArray(): LoDashExplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.toArray + */ + toArray<TResult>(): LoDashExplicitArrayWrapper<TResult>; + } + + //_.toPlainObject + interface LoDashStatic { + /** + * Converts value to a plain object flattening inherited enumerable properties of value to own properties + * of the plain object. + * + * @param value The value to convert. + * @return Returns the converted plain object. + */ + toPlainObject<TResult extends {}>(value?: any): TResult; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.toPlainObject + */ + toPlainObject<TResult extends {}>(): LoDashImplicitObjectWrapper<TResult>; + } + + /******** + * Math * + ********/ + + //_.add + interface LoDashStatic { + /** + * Adds two numbers. + * + * @param augend The first number to add. + * @param addend The second number to add. + * @return Returns the sum. + */ + add( + augend: number, + addend: number + ): number; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.add + */ + add(addend: number): number; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.add + */ + add(addend: number): LoDashExplicitWrapper<number>; + } + + //_.ceil + interface LoDashStatic { + /** + * Calculates n rounded up to precision. + * + * @param n The number to round up. + * @param precision The precision to round up to. + * @return Returns the rounded up number. + */ + ceil( + n: number, + precision?: number + ): number; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.ceil + */ + ceil(precision?: number): number; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.ceil + */ + ceil(precision?: number): LoDashExplicitWrapper<number>; + } + + //_.floor + interface LoDashStatic { + /** + * Calculates n rounded down to precision. + * + * @param n The number to round down. + * @param precision The precision to round down to. + * @return Returns the rounded down number. + */ + floor( + n: number, + precision?: number + ): number; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.floor + */ + floor(precision?: number): number; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.floor + */ + floor(precision?: number): LoDashExplicitWrapper<number>; + } + + //_.max + interface LoDashStatic { + /** + * Gets the maximum value of collection. If collection is empty or falsey -Infinity is returned. If an iteratee + * function is provided it’s invoked for each value in collection to generate the criterion by which the value + * is ranked. The iteratee is bound to thisArg and invoked with three arguments: (value, index, collection). + * + * If a property name is provided for iteratee the created _.property style callback returns the property value + * of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for iteratee the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * @param collection The collection to iterate over. + * @param iteratee The function invoked per iteration. + * @param thisArg The this binding of iteratee. + * @return Returns the maximum value. + */ + max<T>( + collection: List<T>, + iteratee?: ListIterator<T, any>, + thisArg?: any + ): T; + + /** + * @see _.max + */ + max<T>( + collection: Dictionary<T>, + iteratee?: DictionaryIterator<T, any>, + thisArg?: any + ): T; + + /** + * @see _.max + */ + max<T>( + collection: List<T>|Dictionary<T>, + iteratee?: string, + thisArg?: any + ): T; + + /** + * @see _.max + */ + max<TObject extends {}, T>( + collection: List<T>|Dictionary<T>, + whereValue?: TObject + ): T; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.max + */ + max( + iteratee?: ListIterator<T, any>, + thisArg?: any + ): T; + + /** + * @see _.max + */ + max( + iteratee?: string, + thisArg?: any + ): T; + + /** + * @see _.max + */ + max<TObject extends {}>( + whereValue?: TObject + ): T; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.max + */ + max<T>( + iteratee?: ListIterator<T, any>|DictionaryIterator<T, any>, + thisArg?: any + ): T; + + /** + * @see _.max + */ + max<T>( + iteratee?: string, + thisArg?: any + ): T; + + /** + * @see _.max + */ + max<TObject extends {}, T>( + whereValue?: TObject + ): T; + } + + //_.min + interface LoDashStatic { + /** + * Gets the minimum value of collection. If collection is empty or falsey Infinity is returned. If an iteratee + * function is provided it’s invoked for each value in collection to generate the criterion by which the value + * is ranked. The iteratee is bound to thisArg and invoked with three arguments: (value, index, collection). + * + * If a property name is provided for iteratee the created _.property style callback returns the property value + * of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for iteratee the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * @param collection The collection to iterate over. + * @param iteratee The function invoked per iteration. + * @param thisArg The this binding of iteratee. + * @return Returns the minimum value. + */ + min<T>( + collection: List<T>, + iteratee?: ListIterator<T, any>, + thisArg?: any + ): T; + + /** + * @see _.min + */ + min<T>( + collection: Dictionary<T>, + iteratee?: DictionaryIterator<T, any>, + thisArg?: any + ): T; + + /** + * @see _.min + */ + min<T>( + collection: List<T>|Dictionary<T>, + iteratee?: string, + thisArg?: any + ): T; + + /** + * @see _.min + */ + min<TObject extends {}, T>( + collection: List<T>|Dictionary<T>, + whereValue?: TObject + ): T; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.min + */ + min( + iteratee?: ListIterator<T, any>, + thisArg?: any + ): T; + + /** + * @see _.min + */ + min( + iteratee?: string, + thisArg?: any + ): T; + + /** + * @see _.min + */ + min<TObject extends {}>( + whereValue?: TObject + ): T; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.min + */ + min<T>( + iteratee?: ListIterator<T, any>|DictionaryIterator<T, any>, + thisArg?: any + ): T; + + /** + * @see _.min + */ + min<T>( + iteratee?: string, + thisArg?: any + ): T; + + /** + * @see _.min + */ + min<TObject extends {}, T>( + whereValue?: TObject + ): T; + } + + //_.round + interface LoDashStatic { + /** + * Calculates n rounded to precision. + * + * @param n The number to round. + * @param precision The precision to round to. + * @return Returns the rounded number. + */ + round( + n: number, + precision?: number + ): number; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.round + */ + round(precision?: number): number; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.round + */ + round(precision?: number): LoDashExplicitWrapper<number>; + } + + //_.sum + interface LoDashStatic { + /** + * Gets the sum of the values in collection. + * + * @param collection The collection to iterate over. + * @param iteratee The function invoked per iteration. + * @param thisArg The this binding of iteratee. + * @return Returns the sum. + */ + sum<T>( + collection: List<T>, + iteratee: ListIterator<T, number>, + thisArg?: any + ): number; + + /** + * @see _.sum + **/ + sum<T>( + collection: Dictionary<T>, + iteratee: DictionaryIterator<T, number>, + thisArg?: any + ): number; + + /** + * @see _.sum + */ + sum<T>( + collection: List<number>|Dictionary<number>, + iteratee: string + ): number; + + /** + * @see _.sum + */ + sum<T>(collection: List<T>|Dictionary<T>): number; + + /** + * @see _.sum + */ + sum(collection: List<number>|Dictionary<number>): number; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.sum + */ + sum( + iteratee: ListIterator<T, number>, + thisArg?: any + ): number; + + /** + * @see _.sum + */ + sum(iteratee: string): number; + + /** + * @see _.sum + */ + sum(): number; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.sum + **/ + sum<TValue>( + iteratee: ListIterator<TValue, number>|DictionaryIterator<TValue, number>, + thisArg?: any + ): number; + + /** + * @see _.sum + */ + sum(iteratee: string): number; + + /** + * @see _.sum + */ + sum(): number; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.sum + */ + sum( + iteratee: ListIterator<T, number>, + thisArg?: any + ): LoDashExplicitWrapper<number>; + + /** + * @see _.sum + */ + sum(iteratee: string): LoDashExplicitWrapper<number>; + + /** + * @see _.sum + */ + sum(): LoDashExplicitWrapper<number>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.sum + */ + sum<TValue>( + iteratee: ListIterator<TValue, number>|DictionaryIterator<TValue, number>, + thisArg?: any + ): LoDashExplicitWrapper<number>; + + /** + * @see _.sum + */ + sum(iteratee: string): LoDashExplicitWrapper<number>; + + /** + * @see _.sum + */ + sum(): LoDashExplicitWrapper<number>; + } + + /********** + * Number * + **********/ + + //_.inRange + interface LoDashStatic { + /** + * Checks if n is between start and up to but not including, end. If end is not specified it’s set to start + * with start then set to 0. + * + * @param n The number to check. + * @param start The start of the range. + * @param end The end of the range. + * @return Returns true if n is in the range, else false. + */ + inRange( + n: number, + start: number, + end: number + ): boolean; + + + /** + * @see _.inRange + */ + inRange( + n: number, + end: number + ): boolean; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.inRange + */ + inRange( + start: number, + end: number + ): boolean; + + /** + * @see _.inRange + */ + inRange(end: number): boolean; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.inRange + */ + inRange( + start: number, + end: number + ): LoDashExplicitWrapper<boolean>; + + /** + * @see _.inRange + */ + inRange(end: number): LoDashExplicitWrapper<boolean>; + } + + //_.random + interface LoDashStatic { + /** + * Produces a random number between min and max (inclusive). If only one argument is provided a number between + * 0 and the given number is returned. If floating is true, or either min or max are floats, a floating-point + * number is returned instead of an integer. + * + * @param min The minimum possible value. + * @param max The maximum possible value. + * @param floating Specify returning a floating-point number. + * @return Returns the random number. + */ + random( + min?: number, + max?: number, + floating?: boolean + ): number; + + /** + * @see _.random + */ + random( + min?: number, + floating?: boolean + ): number; + + /** + * @see _.random + */ + random(floating?: boolean): number; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.random + */ + random( + max?: number, + floating?: boolean + ): number; + + /** + * @see _.random + */ + random(floating?: boolean): number; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.random + */ + random( + max?: number, + floating?: boolean + ): LoDashExplicitWrapper<number>; + + /** + * @see _.random + */ + random(floating?: boolean): LoDashExplicitWrapper<number>; + } + + /********** + * Object * + **********/ + + //_.assign + interface AssignCustomizer { + (objectValue: any, sourceValue: any, key?: string, object?: {}, source?: {}): any; + } + + interface LoDashStatic { + /** + * Assigns own enumerable properties of source object(s) to the destination object. Subsequent sources + * overwrite property assignments of previous sources. If customizer is provided it’s invoked to produce the + * assigned values. The customizer is bound to thisArg and invoked with five arguments: + * (objectValue, sourceValue, key, object, source). + * + * Note: This method mutates object and is based on Object.assign. + * + * @alias _.extend + * + * @param object The destination object. + * @param source The source objects. + * @param customizer The function to customize assigned values. + * @param thisArg The this binding of callback. + * @return The destination object. + */ + assign<TObject extends {}, TSource extends {}, TResult extends {}>( + object: TObject, + source: TSource, + customizer?: AssignCustomizer, + thisArg?: any + ): TResult; + + /** + * @see assign + */ + assign<TObject extends {}, TSource1 extends {}, TSource2 extends {}, TResult extends {}>( + object: TObject, + source1: TSource1, + source2: TSource2, + customizer?: AssignCustomizer, + thisArg?: any + ): TResult; + + /** + * @see assign + */ + assign<TObject extends {}, TSource1 extends {}, TSource2 extends {}, TSource3 extends {}, TResult extends {}>( + object: TObject, + source1: TSource1, + source2: TSource2, + source3: TSource3, + customizer?: AssignCustomizer, + thisArg?: any + ): TResult; + + /** + * @see assign + */ + assign<TObject extends {}, TSource1 extends {}, TSource2 extends {}, TSource3 extends {}, TSource4 extends {}, + TResult extends {}> + ( + object: TObject, + source1: TSource1, + source2: TSource2, + source3: TSource3, + source4: TSource4, + customizer?: AssignCustomizer, + thisArg?: any + ): TResult; + + /** + * @see _.assign + */ + assign<TObject extends {}>(object: TObject): TObject; + + /** + * @see _.assign + */ + assign<TObject extends {}, TResult extends {}>( + object: TObject, ...otherArgs: any[] + ): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.assign + */ + assign<TSource extends {}, TResult extends {}>( + source: TSource, + customizer?: AssignCustomizer, + thisArg?: any + ): LoDashImplicitObjectWrapper<TResult>; + + /** + * @see assign + */ + assign<TSource1 extends {}, TSource2 extends {}, TResult extends {}>( + source1: TSource1, + source2: TSource2, + customizer?: AssignCustomizer, + thisArg?: any + ): LoDashImplicitObjectWrapper<TResult>; + + /** + * @see assign + */ + assign<TSource1 extends {}, TSource2 extends {}, TSource3 extends {}, TResult extends {}>( + source1: TSource1, + source2: TSource2, + source3: TSource3, + customizer?: AssignCustomizer, + thisArg?: any + ): LoDashImplicitObjectWrapper<TResult>; + + /** + * @see assign + */ + assign<TSource1 extends {}, TSource2 extends {}, TSource3 extends {}, TSource4 extends {}, TResult extends {}>( + source1: TSource1, + source2: TSource2, + source3: TSource3, + source4: TSource4, + customizer?: AssignCustomizer, + thisArg?: any + ): LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.assign + */ + assign(): LoDashImplicitObjectWrapper<T>; + + /** + * @see _.assign + */ + assign<TResult extends {}>(...otherArgs: any[]): LoDashImplicitObjectWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.assign + */ + assign<TSource extends {}, TResult extends {}>( + source: TSource, + customizer?: AssignCustomizer, + thisArg?: any + ): LoDashExplicitObjectWrapper<TResult>; + + /** + * @see assign + */ + assign<TSource1 extends {}, TSource2 extends {}, TResult extends {}>( + source1: TSource1, + source2: TSource2, + customizer?: AssignCustomizer, + thisArg?: any + ): LoDashExplicitObjectWrapper<TResult>; + + /** + * @see assign + */ + assign<TSource1 extends {}, TSource2 extends {}, TSource3 extends {}, TResult extends {}>( + source1: TSource1, + source2: TSource2, + source3: TSource3, + customizer?: AssignCustomizer, + thisArg?: any + ): LoDashExplicitObjectWrapper<TResult>; + + /** + * @see assign + */ + assign<TSource1 extends {}, TSource2 extends {}, TSource3 extends {}, TSource4 extends {}, TResult extends {}>( + source1: TSource1, + source2: TSource2, + source3: TSource3, + source4: TSource4, + customizer?: AssignCustomizer, + thisArg?: any + ): LoDashExplicitObjectWrapper<TResult>; + + /** + * @see _.assign + */ + assign(): LoDashExplicitObjectWrapper<T>; + + /** + * @see _.assign + */ + assign<TResult extends {}>(...otherArgs: any[]): LoDashExplicitObjectWrapper<TResult>; + } + + //_.create + interface LoDashStatic { + /** + * Creates an object that inherits from the given prototype object. If a properties object is provided its own + * enumerable properties are assigned to the created object. + * + * @param prototype The object to inherit from. + * @param properties The properties to assign to the object. + * @return Returns the new object. + */ + create<T extends Object, U extends Object>( + prototype: T, + properties?: U + ): T & U; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.create + */ + create<U extends Object>(properties?: U): LoDashImplicitObjectWrapper<T & U>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.create + */ + create<U extends Object>(properties?: U): LoDashExplicitObjectWrapper<T & U>; + } + + //_.defaults + interface LoDashStatic { + /** + * Assigns own enumerable properties of source object(s) to the destination object for all destination + * properties that resolve to undefined. Once a property is set, additional values of the same property are + * ignored. + * + * Note: This method mutates object. + * + * @param object The destination object. + * @param sources The source objects. + * @return The destination object. + */ + defaults<Obj extends {}, TResult extends {}>( + object: Obj, + ...sources: {}[] + ): TResult; + + /** + * @see _.defaults + */ + defaults<Obj extends {}, S1 extends {}, TResult extends {}>( + object: Obj, + source1: S1, + ...sources: {}[] + ): TResult; + + /** + * @see _.defaults + */ + defaults<Obj extends {}, S1 extends {}, S2 extends {}, TResult extends {}>( + object: Obj, + source1: S1, + source2: S2, + ...sources: {}[] + ): TResult; + + /** + * @see _.defaults + */ + defaults<Obj extends {}, S1 extends {}, S2 extends {}, S3 extends {}, TResult extends {}>( + object: Obj, + source1: S1, + source2: S2, + source3: S3, + ...sources: {}[] + ): TResult; + + /** + * @see _.defaults + */ + defaults<Obj extends {}, S1 extends {}, S2 extends {}, S3 extends {}, S4 extends {}, TResult extends {}>( + object: Obj, + source1: S1, + source2: S2, + source3: S3, + source4: S4, + ...sources: {}[] + ): TResult; + + /** + * @see _.defaults + */ + defaults<TResult extends {}>( + object: {}, + ...sources: {}[] + ): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.defaults + */ + defaults<S1 extends {}, TResult extends {}>( + source1: S1, + ...sources: {}[] + ): LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.defaults + */ + defaults<S1 extends {}, S2 extends {}, TResult extends {}>( + source1: S1, + source2: S2, + ...sources: {}[] + ): LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.defaults + */ + defaults<S1 extends {}, S2 extends {}, S3 extends {}, TResult extends {}>( + source1: S1, + source2: S2, + source3: S3, + ...sources: {}[] + ): LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.defaults + */ + defaults<S1 extends {}, S2 extends {}, S3 extends {}, S4 extends {}, TResult extends {}>( + source1: S1, + source2: S2, + source3: S3, + source4: S4, + ...sources: {}[] + ): LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.defaults + */ + defaults(): LoDashImplicitObjectWrapper<T>; + + /** + * @see _.defaults + */ + defaults<TResult>(...sources: {}[]): LoDashImplicitObjectWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.defaults + */ + defaults<S1 extends {}, TResult extends {}>( + source1: S1, + ...sources: {}[] + ): LoDashExplicitObjectWrapper<TResult>; + + /** + * @see _.defaults + */ + defaults<S1 extends {}, S2 extends {}, TResult extends {}>( + source1: S1, + source2: S2, + ...sources: {}[] + ): LoDashExplicitObjectWrapper<TResult>; + + /** + * @see _.defaults + */ + defaults<S1 extends {}, S2 extends {}, S3 extends {}, TResult extends {}>( + source1: S1, + source2: S2, + source3: S3, + ...sources: {}[] + ): LoDashExplicitObjectWrapper<TResult>; + + /** + * @see _.defaults + */ + defaults<S1 extends {}, S2 extends {}, S3 extends {}, S4 extends {}, TResult extends {}>( + source1: S1, + source2: S2, + source3: S3, + source4: S4, + ...sources: {}[] + ): LoDashExplicitObjectWrapper<TResult>; + + /** + * @see _.defaults + */ + defaults(): LoDashExplicitObjectWrapper<T>; + + /** + * @see _.defaults + */ + defaults<TResult>(...sources: {}[]): LoDashExplicitObjectWrapper<TResult>; + } + + //_.defaultsDeep + interface LoDashStatic { + /** + * This method is like _.defaults except that it recursively assigns default properties. + * @param object The destination object. + * @param sources The source objects. + * @return Returns object. + **/ + defaultsDeep<T, TResult>( + object: T, + ...sources: any[]): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.defaultsDeep + **/ + defaultsDeep<TResult>(...sources: any[]): LoDashImplicitObjectWrapper<TResult> + } + + //_.extend + interface LoDashStatic { + /** + * @see assign + */ + extend<TObject extends {}, TSource extends {}, TResult extends {}>( + object: TObject, + source: TSource, + customizer?: AssignCustomizer, + thisArg?: any + ): TResult; + + /** + * @see assign + */ + extend<TObject extends {}, TSource1 extends {}, TSource2 extends {}, TResult extends {}>( + object: TObject, + source1: TSource1, + source2: TSource2, + customizer?: AssignCustomizer, + thisArg?: any + ): TResult; + + /** + * @see assign + */ + extend<TObject extends {}, TSource1 extends {}, TSource2 extends {}, TSource3 extends {}, TResult extends {}>( + object: TObject, + source1: TSource1, + source2: TSource2, + source3: TSource3, + customizer?: AssignCustomizer, + thisArg?: any + ): TResult; + + /** + * @see assign + */ + extend<TObject extends {}, TSource1 extends {}, TSource2 extends {}, TSource3 extends {}, TSource4 extends {}, + TResult extends {}> + ( + object: TObject, + source1: TSource1, + source2: TSource2, + source3: TSource3, + source4: TSource4, + customizer?: AssignCustomizer, + thisArg?: any + ): TResult; + + /** + * @see _.assign + */ + extend<TObject extends {}>(object: TObject): TObject; + + /** + * @see _.assign + */ + extend<TObject extends {}, TResult extends {}>( + object: TObject, ...otherArgs: any[] + ): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.assign + */ + extend<TSource extends {}, TResult extends {}>( + source: TSource, + customizer?: AssignCustomizer, + thisArg?: any + ): LoDashImplicitObjectWrapper<TResult>; + + /** + * @see assign + */ + extend<TSource1 extends {}, TSource2 extends {}, TResult extends {}>( + source1: TSource1, + source2: TSource2, + customizer?: AssignCustomizer, + thisArg?: any + ): LoDashImplicitObjectWrapper<TResult>; + + /** + * @see assign + */ + extend<TSource1 extends {}, TSource2 extends {}, TSource3 extends {}, TResult extends {}>( + source1: TSource1, + source2: TSource2, + source3: TSource3, + customizer?: AssignCustomizer, + thisArg?: any + ): LoDashImplicitObjectWrapper<TResult>; + + /** + * @see assign + */ + extend<TSource1 extends {}, TSource2 extends {}, TSource3 extends {}, TSource4 extends {}, TResult extends {}>( + source1: TSource1, + source2: TSource2, + source3: TSource3, + source4: TSource4, + customizer?: AssignCustomizer, + thisArg?: any + ): LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.assign + */ + extend(): LoDashImplicitObjectWrapper<T>; + + /** + * @see _.assign + */ + extend<TResult extends {}>(...otherArgs: any[]): LoDashImplicitObjectWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.assign + */ + extend<TSource extends {}, TResult extends {}>( + source: TSource, + customizer?: AssignCustomizer, + thisArg?: any + ): LoDashExplicitObjectWrapper<TResult>; + + /** + * @see assign + */ + extend<TSource1 extends {}, TSource2 extends {}, TResult extends {}>( + source1: TSource1, + source2: TSource2, + customizer?: AssignCustomizer, + thisArg?: any + ): LoDashExplicitObjectWrapper<TResult>; + + /** + * @see assign + */ + extend<TSource1 extends {}, TSource2 extends {}, TSource3 extends {}, TResult extends {}>( + source1: TSource1, + source2: TSource2, + source3: TSource3, + customizer?: AssignCustomizer, + thisArg?: any + ): LoDashExplicitObjectWrapper<TResult>; + + /** + * @see assign + */ + extend<TSource1 extends {}, TSource2 extends {}, TSource3 extends {}, TSource4 extends {}, TResult extends {}>( + source1: TSource1, + source2: TSource2, + source3: TSource3, + source4: TSource4, + customizer?: AssignCustomizer, + thisArg?: any + ): LoDashExplicitObjectWrapper<TResult>; + + /** + * @see _.assign + */ + extend(): LoDashExplicitObjectWrapper<T>; + + /** + * @see _.assign + */ + extend<TResult extends {}>(...otherArgs: any[]): LoDashExplicitObjectWrapper<TResult>; + } + + //_.findKey + interface LoDashStatic { + /** + * This method is like _.find except that it returns the key of the first element predicate returns truthy for + * instead of the element itself. + * + * If a property name is provided for predicate the created _.property style callback returns the property + * value of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for predicate the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * @param object The object to search. + * @param predicate The function invoked per iteration. + * @param thisArg The this binding of predicate. + * @return Returns the key of the matched element, else undefined. + */ + findKey<TValues, TObject>( + object: TObject, + predicate?: DictionaryIterator<TValues, boolean>, + thisArg?: any + ): string; + + /** + * @see _.findKey + */ + findKey<TObject>( + object: TObject, + predicate?: ObjectIterator<any, boolean>, + thisArg?: any + ): string; + + /** + * @see _.findKey + */ + findKey<TObject>( + object: TObject, + predicate?: string, + thisArg?: any + ): string; + + /** + * @see _.findKey + */ + findKey<TWhere extends Dictionary<any>, TObject>( + object: TObject, + predicate?: TWhere + ): string; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.findKey + */ + findKey<TValues>( + predicate?: DictionaryIterator<TValues, boolean>, + thisArg?: any + ): string; + + /** + * @see _.findKey + */ + findKey( + predicate?: ObjectIterator<any, boolean>, + thisArg?: any + ): string; + + /** + * @see _.findKey + */ + findKey( + predicate?: string, + thisArg?: any + ): string; + + /** + * @see _.findKey + */ + findKey<TWhere extends Dictionary<any>>( + predicate?: TWhere + ): string; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.findKey + */ + findKey<TValues>( + predicate?: DictionaryIterator<TValues, boolean>, + thisArg?: any + ): LoDashExplicitWrapper<string>; + + /** + * @see _.findKey + */ + findKey( + predicate?: ObjectIterator<any, boolean>, + thisArg?: any + ): LoDashExplicitWrapper<string>; + + /** + * @see _.findKey + */ + findKey( + predicate?: string, + thisArg?: any + ): LoDashExplicitWrapper<string>; + + /** + * @see _.findKey + */ + findKey<TWhere extends Dictionary<any>>( + predicate?: TWhere + ): LoDashExplicitWrapper<string>; + } + + //_.findLastKey + interface LoDashStatic { + /** + * This method is like _.findKey except that it iterates over elements of a collection in the opposite order. + * + * If a property name is provided for predicate the created _.property style callback returns the property + * value of the given element. + * + * If a value is also provided for thisArg the created _.matchesProperty style callback returns true for + * elements that have a matching property value, else false. + * + * If an object is provided for predicate the created _.matches style callback returns true for elements that + * have the properties of the given object, else false. + * + * @param object The object to search. + * @param predicate The function invoked per iteration. + * @param thisArg The this binding of predicate. + * @return Returns the key of the matched element, else undefined. + */ + findLastKey<TValues, TObject>( + object: TObject, + predicate?: DictionaryIterator<TValues, boolean>, + thisArg?: any + ): string; + + /** + * @see _.findLastKey + */ + findLastKey<TObject>( + object: TObject, + predicate?: ObjectIterator<any, boolean>, + thisArg?: any + ): string; + + /** + * @see _.findLastKey + */ + findLastKey<TObject>( + object: TObject, + predicate?: string, + thisArg?: any + ): string; + + /** + * @see _.findLastKey + */ + findLastKey<TWhere extends Dictionary<any>, TObject>( + object: TObject, + predicate?: TWhere + ): string; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.findLastKey + */ + findLastKey<TValues>( + predicate?: DictionaryIterator<TValues, boolean>, + thisArg?: any + ): string; + + /** + * @see _.findLastKey + */ + findLastKey( + predicate?: ObjectIterator<any, boolean>, + thisArg?: any + ): string; + + /** + * @see _.findLastKey + */ + findLastKey( + predicate?: string, + thisArg?: any + ): string; + + /** + * @see _.findLastKey + */ + findLastKey<TWhere extends Dictionary<any>>( + predicate?: TWhere + ): string; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.findLastKey + */ + findLastKey<TValues>( + predicate?: DictionaryIterator<TValues, boolean>, + thisArg?: any + ): LoDashExplicitWrapper<string>; + + /** + * @see _.findLastKey + */ + findLastKey( + predicate?: ObjectIterator<any, boolean>, + thisArg?: any + ): LoDashExplicitWrapper<string>; + + /** + * @see _.findLastKey + */ + findLastKey( + predicate?: string, + thisArg?: any + ): LoDashExplicitWrapper<string>; + + /** + * @see _.findLastKey + */ + findLastKey<TWhere extends Dictionary<any>>( + predicate?: TWhere + ): LoDashExplicitWrapper<string>; + } + + //_.forIn + interface LoDashStatic { + /** + * Iterates over own and inherited enumerable properties of an object invoking iteratee for each property. The + * iteratee is bound to thisArg and invoked with three arguments: (value, key, object). Iteratee functions may + * exit iteration early by explicitly returning false. + * + * @param object The object to iterate over. + * @param iteratee The function invoked per iteration. + * @param thisArg The this binding of iteratee. + * @return Returns object. + */ + forIn<T>( + object: Dictionary<T>, + iteratee?: DictionaryIterator<T, any>, + thisArg?: any + ): Dictionary<T>; + + /** + * @see _.forIn + */ + forIn<T extends {}>( + object: T, + iteratee?: ObjectIterator<any, any>, + thisArg?: any + ): T; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.forIn + */ + forIn<TValue>( + iteratee?: DictionaryIterator<TValue, any>, + thisArg?: any + ): _.LoDashImplicitObjectWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.forIn + */ + forIn<TValue>( + iteratee?: DictionaryIterator<TValue, any>, + thisArg?: any + ): _.LoDashExplicitObjectWrapper<T>; + } + + //_.forInRight + interface LoDashStatic { + /** + * This method is like _.forIn except that it iterates over properties of object in the opposite order. + * + * @param object The object to iterate over. + * @param iteratee The function invoked per iteration. + * @param thisArg The this binding of iteratee. + * @return Returns object. + */ + forInRight<T>( + object: Dictionary<T>, + iteratee?: DictionaryIterator<T, any>, + thisArg?: any + ): Dictionary<T>; + + /** + * @see _.forInRight + */ + forInRight<T extends {}>( + object: T, + iteratee?: ObjectIterator<any, any>, + thisArg?: any + ): T; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.forInRight + */ + forInRight<TValue>( + iteratee?: DictionaryIterator<TValue, any>, + thisArg?: any + ): _.LoDashImplicitObjectWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.forInRight + */ + forInRight<TValue>( + iteratee?: DictionaryIterator<TValue, any>, + thisArg?: any + ): _.LoDashExplicitObjectWrapper<T>; + } + + //_.forOwn + interface LoDashStatic { + /** + * Iterates over own enumerable properties of an object invoking iteratee for each property. The iteratee is + * bound to thisArg and invoked with three arguments: (value, key, object). Iteratee functions may exit + * iteration early by explicitly returning false. + * + * @param object The object to iterate over. + * @param iteratee The function invoked per iteration. + * @param thisArg The this binding of iteratee. + * @return Returns object. + */ + forOwn<T>( + object: Dictionary<T>, + iteratee?: DictionaryIterator<T, any>, + thisArg?: any + ): Dictionary<T>; + + /** + * @see _.forOwn + */ + forOwn<T extends {}>( + object: T, + iteratee?: ObjectIterator<any, any>, + thisArg?: any + ): T; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.forOwn + */ + forOwn<TValue>( + iteratee?: DictionaryIterator<TValue, any>, + thisArg?: any + ): _.LoDashImplicitObjectWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.forOwn + */ + forOwn<TValue>( + iteratee?: DictionaryIterator<TValue, any>, + thisArg?: any + ): _.LoDashExplicitObjectWrapper<T>; + } + + //_.forOwnRight + interface LoDashStatic { + /** + * This method is like _.forOwn except that it iterates over properties of object in the opposite order. + * + * @param object The object to iterate over. + * @param iteratee The function invoked per iteration. + * @param thisArg The this binding of iteratee. + * @return Returns object. + */ + forOwnRight<T>( + object: Dictionary<T>, + iteratee?: DictionaryIterator<T, any>, + thisArg?: any + ): Dictionary<T>; + + /** + * @see _.forOwnRight + */ + forOwnRight<T extends {}>( + object: T, + iteratee?: ObjectIterator<any, any>, + thisArg?: any + ): T; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.forOwnRight + */ + forOwnRight<TValue>( + iteratee?: DictionaryIterator<TValue, any>, + thisArg?: any + ): _.LoDashImplicitObjectWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.forOwnRight + */ + forOwnRight<TValue>( + iteratee?: DictionaryIterator<TValue, any>, + thisArg?: any + ): _.LoDashExplicitObjectWrapper<T>; + } + + //_.functions + interface LoDashStatic { + /** + * Creates an array of function property names from all enumerable properties, own and inherited, of object. + * + * @alias _.methods + * + * @param object The object to inspect. + * @return Returns the new array of property names. + */ + functions<T extends {}>(object: any): string[]; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.functions + */ + functions(): _.LoDashImplicitArrayWrapper<string>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.functions + */ + functions(): _.LoDashExplicitArrayWrapper<string>; + } + + //_.get + interface LoDashStatic { + /** + * Gets the property value at path of object. If the resolved + * value is undefined the defaultValue is used in its place. + * @param object The object to query. + * @param path The path of the property to get. + * @param defaultValue The value returned if the resolved value is undefined. + * @return Returns the resolved value. + **/ + get<TResult>(object: Object, + path: string|number|boolean|Array<string|number|boolean>, + defaultValue?:TResult + ): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.get + **/ + get<TResult>(path: string|number|boolean|Array<string|number|boolean>, + defaultValue?: TResult + ): TResult; + } + + //_.has + interface LoDashStatic { + /** + * Checks if path is a direct property. + * + * @param object The object to query. + * @param path The path to check. + * @return Returns true if path is a direct property, else false. + */ + has<T extends {}>( + object: T, + path: string|StringRepresentable|StringRepresentable[] + ): boolean; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.has + */ + has(path: string|StringRepresentable|StringRepresentable[]): boolean; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.has + */ + has(path: string|StringRepresentable|StringRepresentable[]): LoDashExplicitWrapper<boolean>; + } + + //_.invert + interface LoDashStatic { + /** + * Creates an object composed of the inverted keys and values of object. If object contains duplicate values, + * subsequent values overwrite property assignments of previous values unless multiValue is true. + * + * @param object The object to invert. + * @param multiValue Allow multiple values per key. + * @return Returns the new inverted object. + */ + invert<T extends {}, TResult extends {}>( + object: T, + multiValue?: boolean + ): TResult; + + /** + * @see _.invert + */ + invert<TResult extends {}>( + object: Object, + multiValue?: boolean + ): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.invert + */ + invert<TResult extends {}>(multiValue?: boolean): LoDashImplicitObjectWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.invert + */ + invert<TResult extends {}>(multiValue?: boolean): LoDashExplicitObjectWrapper<TResult>; + } + + //_.keys + interface LoDashStatic { + /** + * Creates an array of the own enumerable property names of object. + * + * Note: Non-object values are coerced to objects. See the ES spec for more details. + * + * @param object The object to query. + * @return Returns the array of property names. + */ + keys(object?: any): string[]; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.keys + */ + keys(): LoDashImplicitArrayWrapper<string>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.keys + */ + keys(): LoDashExplicitArrayWrapper<string>; + } + + //_.keysIn + interface LoDashStatic { + /** + * Creates an array of the own and inherited enumerable property names of object. + * + * Note: Non-object values are coerced to objects. + * + * @param object The object to query. + * @return An array of property names. + */ + keysIn(object?: any): string[]; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.keysIn + */ + keysIn(): LoDashImplicitArrayWrapper<string>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.keysIn + */ + keysIn(): LoDashExplicitArrayWrapper<string>; + } + + //_.mapKeys + interface LoDashStatic { + /** + * The opposite of _.mapValues; this method creates an object with the same values as object and keys generated + * by running each own enumerable property of object through iteratee. + * + * @param object The object to iterate over. + * @param iteratee The function invoked per iteration. + * @param thisArg The this binding of iteratee. + * @return Returns the new mapped object. + */ + mapKeys<T, TKey>( + object: List<T>, + iteratee?: ListIterator<T, TKey>, + thisArg?: any + ): Dictionary<T>; + + /** + * @see _.mapKeys + */ + mapKeys<T, TKey>( + object: Dictionary<T>, + iteratee?: DictionaryIterator<T, TKey>, + thisArg?: any + ): Dictionary<T>; + + /** + * @see _.mapKeys + */ + mapKeys<T, TObject extends {}>( + object: List<T>|Dictionary<T>, + iteratee?: TObject + ): Dictionary<T>; + + /** + * @see _.mapKeys + */ + mapKeys<T>( + object: List<T>|Dictionary<T>, + iteratee?: string, + thisArg?: any + ): Dictionary<T>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.mapKeys + */ + mapKeys<TKey>( + iteratee?: ListIterator<T, TKey>, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<T>>; + + /** + * @see _.mapKeys + */ + mapKeys<TObject extends {}>( + iteratee?: TObject + ): LoDashImplicitObjectWrapper<Dictionary<T>>; + + /** + * @see _.mapKeys + */ + mapKeys( + iteratee?: string, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<T>>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.mapKeys + */ + mapKeys<TResult, TKey>( + iteratee?: ListIterator<TResult, TKey>|DictionaryIterator<TResult, TKey>, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<TResult>>; + + /** + * @see _.mapKeys + */ + mapKeys<TResult, TObject extends {}>( + iteratee?: TObject + ): LoDashImplicitObjectWrapper<Dictionary<TResult>>; + + /** + * @see _.mapKeys + */ + mapKeys<TResult>( + iteratee?: string, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<TResult>>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.mapKeys + */ + mapKeys<TKey>( + iteratee?: ListIterator<T, TKey>, + thisArg?: any + ): LoDashExplicitObjectWrapper<Dictionary<T>>; + + /** + * @see _.mapKeys + */ + mapKeys<TObject extends {}>( + iteratee?: TObject + ): LoDashExplicitObjectWrapper<Dictionary<T>>; + + /** + * @see _.mapKeys + */ + mapKeys( + iteratee?: string, + thisArg?: any + ): LoDashExplicitObjectWrapper<Dictionary<T>>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.mapKeys + */ + mapKeys<TResult, TKey>( + iteratee?: ListIterator<TResult, TKey>|DictionaryIterator<TResult, TKey>, + thisArg?: any + ): LoDashExplicitObjectWrapper<Dictionary<TResult>>; + + /** + * @see _.mapKeys + */ + mapKeys<TResult, TObject extends {}>( + iteratee?: TObject + ): LoDashExplicitObjectWrapper<Dictionary<TResult>>; + + /** + * @see _.mapKeys + */ + mapKeys<TResult>( + iteratee?: string, + thisArg?: any + ): LoDashExplicitObjectWrapper<Dictionary<TResult>>; + } + + //_.mapValues + interface LoDashStatic { + /** + * Creates an object with the same keys as object and values generated by running each own + * enumerable property of object through iteratee. The iteratee function is bound to thisArg + * and invoked with three arguments: (value, key, object). + * + * If a property name is provided iteratee the created "_.property" style callback returns + * the property value of the given element. + * + * If a value is also provided for thisArg the creted "_.matchesProperty" style callback returns + * true for elements that have a matching property value, else false;. + * + * If an object is provided for iteratee the created "_.matches" style callback returns true + * for elements that have the properties of the given object, else false. + * + * @param {Object} object The object to iterate over. + * @param {Function|Object|string} [iteratee=_.identity] The function invoked per iteration. + * @param {Object} [thisArg] The `this` binding of `iteratee`. + * @return {Object} Returns the new mapped object. + */ + mapValues<T, TResult>(obj: Dictionary<T>, callback: ObjectIterator<T, TResult>, thisArg?: any): Dictionary<TResult>; + mapValues<T>(obj: Dictionary<T>, where: Dictionary<T>): Dictionary<boolean>; + mapValues<T, TMapped>(obj: T, pluck: string): TMapped; + mapValues<T>(obj: T, callback: ObjectIterator<any, any>, thisArg?: any): T; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.mapValues + * TValue is the type of the property values of T. + * TResult is the type output by the ObjectIterator function + */ + mapValues<TValue, TResult>(callback: ObjectIterator<TValue, TResult>, thisArg?: any): LoDashImplicitObjectWrapper<Dictionary<TResult>>; + + /** + * @see _.mapValues + * TResult is the type of the property specified by pluck. + * T should be a Dictionary<Dictionary<TResult>> + */ + mapValues<TResult>(pluck: string): LoDashImplicitObjectWrapper<Dictionary<TResult>>; + + /** + * @see _.mapValues + * TResult is the type of the properties on the object specified by pluck. + * T should be a Dictionary<Dictionary<Dictionary<TResult>>> + */ + mapValues<TResult>(pluck: string, where: Dictionary<TResult>): LoDashImplicitArrayWrapper<Dictionary<boolean>>; + + /** + * @see _.mapValues + * TResult is the type of the properties of each object in the values of T + * T should be a Dictionary<Dictionary<TResult>> + */ + mapValues<TResult>(where: Dictionary<TResult>): LoDashImplicitArrayWrapper<boolean>; + } + + //_.merge + interface MergeCustomizer { + (value: any, srcValue: any, key?: string, object?: Object, source?: Object): any; + } + + interface LoDashStatic { + /** + * Recursively merges own enumerable properties of the source object(s), that don’t resolve to undefined into + * the destination object. Subsequent sources overwrite property assignments of previous sources. If customizer + * is provided it’s invoked to produce the merged values of the destination and source properties. If + * customizer returns undefined merging is handled by the method instead. The customizer is bound to thisArg + * and invoked with five arguments: (objectValue, sourceValue, key, object, source). + * + * @param object The destination object. + * @param source The source objects. + * @param customizer The function to customize assigned values. + * @param thisArg The this binding of customizer. + * @return Returns object. + */ + merge<TObject, TSource>( + object: TObject, + source: TSource, + customizer?: MergeCustomizer, + thisArg?: any + ): TObject & TSource; + + /** + * @see _.merge + */ + merge<TObject, TSource1, TSource2>( + object: TObject, + source1: TSource1, + source2: TSource2, + customizer?: MergeCustomizer, + thisArg?: any + ): TObject & TSource1 & TSource2; + + /** + * @see _.merge + */ + merge<TObject, TSource1, TSource2, TSource3>( + object: TObject, + source1: TSource1, + source2: TSource2, + source3: TSource3, + customizer?: MergeCustomizer, + thisArg?: any + ): TObject & TSource1 & TSource2 & TSource3; + + /** + * @see _.merge + */ + merge<TObject, TSource1, TSource2, TSource3, TSource4>( + object: TObject, + source1: TSource1, + source2: TSource2, + source3: TSource3, + source4: TSource4, + customizer?: MergeCustomizer, + thisArg?: any + ): TObject & TSource1 & TSource2 & TSource3 & TSource4; + + /** + * @see _.merge + */ + merge<TResult>( + object: any, + ...otherArgs: any[] + ): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.merge + */ + merge<TSource>( + source: TSource, + customizer?: MergeCustomizer, + thisArg?: any + ): LoDashImplicitObjectWrapper<T & TSource>; + + /** + * @see _.merge + */ + merge<TSource1, TSource2>( + source1: TSource1, + source2: TSource2, + customizer?: MergeCustomizer, + thisArg?: any + ): LoDashImplicitObjectWrapper<T & TSource1 & TSource2>; + + /** + * @see _.merge + */ + merge<TSource1, TSource2, TSource3>( + source1: TSource1, + source2: TSource2, + source3: TSource3, + customizer?: MergeCustomizer, + thisArg?: any + ): LoDashImplicitObjectWrapper<T & TSource1 & TSource2 & TSource3>; + + /** + * @see _.merge + */ + merge<TSource1, TSource2, TSource3, TSource4>( + source1: TSource1, + source2: TSource2, + source3: TSource3, + source4: TSource4, + customizer?: MergeCustomizer, + thisArg?: any + ): LoDashImplicitObjectWrapper<T & TSource1 & TSource2 & TSource3 & TSource4>; + + /** + * @see _.merge + */ + merge<TResult>( + ...otherArgs: any[] + ): LoDashImplicitObjectWrapper<TResult>; + } + + //_.methods + interface LoDashStatic { + /** + * @see _.functions + */ + methods<T extends {}>(object: any): string[]; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.functions + */ + methods(): _.LoDashImplicitArrayWrapper<string>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.functions + */ + methods(): _.LoDashExplicitArrayWrapper<string>; + } + + //_.omit + interface LoDashStatic { + /** + * The opposite of _.pick; this method creates an object composed of the own and inherited enumerable + * properties of object that are not omitted. + * + * @param object The source object. + * @param predicate The function invoked per iteration or property names to omit, specified as individual + * property names or arrays of property names. + * @param thisArg The this binding of predicate. + * @return Returns the new object. + */ + omit<TResult extends {}, T extends {}>( + object: T, + predicate: ObjectIterator<any, boolean>, + thisArg?: any + ): TResult; + + /** + * @see _.omit + */ + omit<TResult extends {}, T extends {}>( + object: T, + ...predicate: (StringRepresentable|StringRepresentable[])[] + ): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.omit + */ + omit<TResult extends {}>( + predicate: ObjectIterator<any, boolean>, + thisArg?: any + ): LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.omit + */ + omit<TResult extends {}>( + ...predicate: (string|StringRepresentable|StringRepresentable[])[] + ): LoDashImplicitObjectWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.omit + */ + omit<TResult extends {}>( + predicate: ObjectIterator<any, boolean>, + thisArg?: any + ): LoDashExplicitObjectWrapper<TResult>; + + /** + * @see _.omit + */ + omit<TResult extends {}>( + ...predicate: (string|StringRepresentable|StringRepresentable[])[] + ): LoDashExplicitObjectWrapper<TResult>; + } + + //_.pairs + interface LoDashStatic { + /** + * Creates a two dimensional array of the key-value pairs for object, e.g. [[key1, value1], [key2, value2]]. + * + * @param object The object to query. + * @return Returns the new array of key-value pairs. + */ + pairs<T extends {}>(object?: T): any[][]; + + pairs<T extends {}, TResult>(object?: T): TResult[][]; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.pairs + */ + pairs<TResult>(): LoDashImplicitArrayWrapper<TResult[]>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.pairs + */ + pairs<TResult>(): LoDashExplicitArrayWrapper<TResult[]>; + } + + //_.pick + interface LoDashStatic { + /** + * Creates an object composed of the picked object properties. Property names may be specified as individual + * arguments or as arrays of property names. If predicate is provided it’s invoked for each property of object + * picking the properties predicate returns truthy for. The predicate is bound to thisArg and invoked with + * three arguments: (value, key, object). + * + * @param object The source object. + * @param predicate The function invoked per iteration or property names to pick, specified as individual + * property names or arrays of property names. + * @param thisArg The this binding of predicate. + * @return Returns the new object. + */ + pick<TResult extends {}, T extends {}>( + object: T, + predicate: ObjectIterator<any, boolean>, + thisArg?: any + ): TResult; + + /** + * @see _.pick + */ + pick<TResult extends {}, T extends {}>( + object: T, + ...predicate: (StringRepresentable|StringRepresentable[])[] + ): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.pick + */ + pick<TResult extends {}>( + predicate: ObjectIterator<any, boolean>, + thisArg?: any + ): LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.pick + */ + pick<TResult extends {}>( + ...predicate: (string|StringRepresentable|StringRepresentable[])[] + ): LoDashImplicitObjectWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.pick + */ + pick<TResult extends {}>( + predicate: ObjectIterator<any, boolean>, + thisArg?: any + ): LoDashExplicitObjectWrapper<TResult>; + + /** + * @see _.pick + */ + pick<TResult extends {}>( + ...predicate: (string|StringRepresentable|StringRepresentable[])[] + ): LoDashExplicitObjectWrapper<TResult>; + } + + //_.result + interface LoDashStatic { + /** + * This method is like _.get except that if the resolved value is a function it’s invoked with the this binding + * of its parent object and its result is returned. + * + * @param object The object to query. + * @param path The path of the property to resolve. + * @param defaultValue The value returned if the resolved value is undefined. + * @return Returns the resolved value. + */ + result<TObject, TResult>( + object: TObject, + path: number|string|boolean|Array<number|string|boolean>, + defaultValue?: TResult + ): TResult; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.result + */ + result<TResult>( + path: number|string|boolean|Array<number|string|boolean>, + defaultValue?: TResult + ): TResult; + } + + //_.set + interface LoDashStatic { + /** + * Sets the property value of path on object. If a portion of path does not exist it’s created. + * + * @param object The object to augment. + * @param path The path of the property to set. + * @param value The value to set. + * @return Returns object. + */ + set<T>( + object: T, + path: string|StringRepresentable|StringRepresentable[], + value: any + ): T; + + /** + * @see _.set + */ + set<V, T>( + object: T, + path: string|StringRepresentable|StringRepresentable[], + value: V + ): T; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.set + */ + set<V>( + path: string|StringRepresentable|StringRepresentable[], + value: V + ): LoDashImplicitObjectWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.set + */ + set<V>( + path: string|StringRepresentable|StringRepresentable[], + value: V + ): LoDashExplicitObjectWrapper<T>; + } + + //_.transform + interface LoDashStatic { + /** + * An alternative to _.reduce; this method transforms object to a new accumulator object which is the result of + * running each of its own enumerable properties through iteratee, with each invocation potentially mutating + * the accumulator object. The iteratee is bound to thisArg and invoked with four arguments: (accumulator, + * value, key, object). Iteratee functions may exit iteration early by explicitly returning false. + * + * @param object The object to iterate over. + * @param iteratee The function invoked per iteration. + * @param accumulator The custom accumulator value. + * @param thisArg The this binding of iteratee. + * @return Returns the accumulated value. + */ + transform<T, TResult>( + object: T[], + iteratee?: MemoVoidArrayIterator<T, TResult[]>, + accumulator?: TResult[], + thisArg?: any + ): TResult[]; + + /** + * @see _.transform + */ + transform<T, TResult>( + object: T[], + iteratee?: MemoVoidArrayIterator<T, Dictionary<TResult>>, + accumulator?: Dictionary<TResult>, + thisArg?: any + ): Dictionary<TResult>; + + /** + * @see _.transform + */ + transform<T, TResult>( + object: Dictionary<T>, + iteratee?: MemoVoidDictionaryIterator<T, Dictionary<TResult>>, + accumulator?: Dictionary<TResult>, + thisArg?: any + ): Dictionary<TResult>; + + /** + * @see _.transform + */ + transform<T, TResult>( + object: Dictionary<T>, + iteratee?: MemoVoidDictionaryIterator<T, TResult[]>, + accumulator?: TResult[], + thisArg?: any + ): TResult[]; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.transform + */ + transform<TResult>( + iteratee?: MemoVoidArrayIterator<T, TResult[]>, + accumulator?: TResult[], + thisArg?: any + ): LoDashImplicitArrayWrapper<TResult>; + + /** + * @see _.transform + */ + transform<TResult>( + iteratee?: MemoVoidArrayIterator<T, Dictionary<TResult>>, + accumulator?: Dictionary<TResult>, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<TResult>>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.transform + */ + transform<T, TResult>( + iteratee?: MemoVoidDictionaryIterator<T, Dictionary<TResult>>, + accumulator?: Dictionary<TResult>, + thisArg?: any + ): LoDashImplicitObjectWrapper<Dictionary<TResult>>; + + /** + * @see _.transform + */ + transform<T, TResult>( + iteratee?: MemoVoidDictionaryIterator<T, TResult[]>, + accumulator?: TResult[], + thisArg?: any + ): LoDashImplicitArrayWrapper<TResult>; + } + + //_.values + interface LoDashStatic { + /** + * Creates an array of the own enumerable property values of object. + * + * @param object The object to query. + * @return Returns an array of property values. + */ + values<T>(object?: any): T[]; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.values + */ + values<T>(): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.values + */ + values<T>(): LoDashExplicitArrayWrapper<T>; + } + + //_.valuesIn + interface LoDashStatic { + /** + * Creates an array of the own and inherited enumerable property values of object. + * + * @param object The object to query. + * @return Returns the array of property values. + */ + valuesIn<T>(object?: any): T[]; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.valuesIn + */ + valuesIn<T>(): LoDashImplicitArrayWrapper<T>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.valuesIn + */ + valuesIn<T>(): LoDashExplicitArrayWrapper<T>; + } + + /********** + * String * + **********/ + + //_.camelCase + interface LoDashStatic { + /** + * Converts string to camel case. + * + * @param string The string to convert. + * @return Returns the camel cased string. + */ + camelCase(string?: string): string; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.camelCase + */ + camelCase(): string; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.camelCase + */ + camelCase(): LoDashExplicitWrapper<string>; + } + + //_.capitalize + interface LoDashStatic { + capitalize(string?: string): string; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.capitalize + */ + capitalize(): string; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.capitalize + */ + capitalize(): LoDashExplicitWrapper<string>; + } + + //_.deburr + interface LoDashStatic { + /** + * Deburrs string by converting latin-1 supplementary letters to basic latin letters and removing combining + * diacritical marks. + * + * @param string The string to deburr. + * @return Returns the deburred string. + */ + deburr(string?: string): string; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.deburr + */ + deburr(): string; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.deburr + */ + deburr(): LoDashExplicitWrapper<string>; + } + + //_.endsWith + interface LoDashStatic { + /** + * Checks if string ends with the given target string. + * + * @param string The string to search. + * @param target The string to search for. + * @param position The position to search from. + * @return Returns true if string ends with target, else false. + */ + endsWith( + string?: string, + target?: string, + position?: number + ): boolean; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.endsWith + */ + endsWith( + target?: string, + position?: number + ): boolean; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.endsWith + */ + endsWith( + target?: string, + position?: number + ): LoDashExplicitWrapper<boolean>; + } + + // _.escape + interface LoDashStatic { + /** + * Converts the characters "&", "<", ">", '"', "'", and "`", in string to their corresponding HTML entities. + * + * Note: No other characters are escaped. To escape additional characters use a third-party library like he. + * + * Though the ">" character is escaped for symmetry, characters like ">" and "/" don’t need escaping in HTML + * and have no special meaning unless they're part of a tag or unquoted attribute value. See Mathias Bynens’s + * article (under "semi-related fun fact") for more details. + * + * Backticks are escaped because in Internet Explorer < 9, they can break out of attribute values or HTML + * comments. See #59, #102, #108, and #133 of the HTML5 Security Cheatsheet for more details. + * + * When working with HTML you should always quote attribute values to reduce XSS vectors. + * + * @param string The string to escape. + * @return Returns the escaped string. + */ + escape(string?: string): string; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.escape + */ + escape(): string; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.escape + */ + escape(): LoDashExplicitWrapper<string>; + } + + // _.escapeRegExp + interface LoDashStatic { + /** + * Escapes the RegExp special characters "\", "/", "^", "$", ".", "|", "?", "*", "+", "(", ")", "[", "]", + * "{" and "}" in string. + * + * @param string The string to escape. + * @return Returns the escaped string. + */ + escapeRegExp(string?: string): string; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.escapeRegExp + */ + escapeRegExp(): string; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.escapeRegExp + */ + escapeRegExp(): LoDashExplicitWrapper<string>; + } + + //_.kebabCase + interface LoDashStatic { + /** + * Converts string to kebab case. + * + * @param string The string to convert. + * @return Returns the kebab cased string. + */ + kebabCase(string?: string): string; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.kebabCase + */ + kebabCase(): string; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.kebabCase + */ + kebabCase(): LoDashExplicitWrapper<string>; + } + + //_.pad + interface LoDashStatic { + /** + * Pads string on the left and right sides if it’s shorter than length. Padding characters are truncated if + * they can’t be evenly divided by length. + * + * @param string The string to pad. + * @param length The padding length. + * @param chars The string used as padding. + * @return Returns the padded string. + */ + pad( + string?: string, + length?: number, + chars?: string + ): string; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.pad + */ + pad( + length?: number, + chars?: string + ): string; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.pad + */ + pad( + length?: number, + chars?: string + ): LoDashExplicitWrapper<string>; + } + + //_.padLeft + interface LoDashStatic { + /** + * Pads string on the left side if it’s shorter than length. Padding characters are truncated if they exceed + * length. + * + * @param string The string to pad. + * @param length The padding length. + * @param chars The string used as padding. + * @return Returns the padded string. + */ + padLeft( + string?: string, + length?: number, + chars?: string + ): string; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.padLeft + */ + padLeft( + length?: number, + chars?: string + ): string; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.padLeft + */ + padLeft( + length?: number, + chars?: string + ): LoDashExplicitWrapper<string>; + } + + //_.padRight + interface LoDashStatic { + /** + * Pads string on the right side if it’s shorter than length. Padding characters are truncated if they exceed + * length. + * + * @param string The string to pad. + * @param length The padding length. + * @param chars The string used as padding. + * @return Returns the padded string. + */ + padRight( + string?: string, + length?: number, + chars?: string + ): string; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.padRight + */ + padRight( + length?: number, + chars?: string + ): string; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.padRight + */ + padRight( + length?: number, + chars?: string + ): LoDashExplicitWrapper<string>; + } + + //_.parseInt + interface LoDashStatic { + /** + * Converts string to an integer of the specified radix. If radix is undefined or 0, a radix of 10 is used + * unless value is a hexadecimal, in which case a radix of 16 is used. + * + * Note: This method aligns with the ES5 implementation of parseInt. + * + * @param string The string to convert. + * @param radix The radix to interpret value by. + * @return Returns the converted integer. + */ + parseInt( + string: string, + radix?: number + ): number; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.parseInt + */ + parseInt(radix?: number): number; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.parseInt + */ + parseInt(radix?: number): LoDashExplicitWrapper<number>; + } + + //_.repeat + interface LoDashStatic { + /** + * Repeats the given string n times. + * + * @param string The string to repeat. + * @param n The number of times to repeat the string. + * @return Returns the repeated string. + */ + repeat( + string?: string, + n?: number + ): string; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.repeat + */ + repeat(n?: number): string; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.repeat + */ + repeat(n?: number): LoDashExplicitWrapper<string>; + } + + //_.snakeCase + interface LoDashStatic { + /** + * Converts string to snake case. + * + * @param string The string to convert. + * @return Returns the snake cased string. + */ + snakeCase(string?: string): string; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.snakeCase + */ + snakeCase(): string; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.snakeCase + */ + snakeCase(): LoDashExplicitWrapper<string>; + } + + //_.startCase + interface LoDashStatic { + /** + * Converts string to start case. + * + * @param string The string to convert. + * @return Returns the start cased string. + */ + startCase(string?: string): string; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.startCase + */ + startCase(): string; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.startCase + */ + startCase(): LoDashExplicitWrapper<string>; + } + + //_.startsWith + interface LoDashStatic { + /** + * Checks if string starts with the given target string. + * + * @param string The string to search. + * @param target The string to search for. + * @param position The position to search from. + * @return Returns true if string starts with target, else false. + */ + startsWith( + string?: string, + target?: string, + position?: number + ): boolean; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.startsWith + */ + startsWith( + target?: string, + position?: number + ): boolean; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.startsWith + */ + startsWith( + target?: string, + position?: number + ): LoDashExplicitWrapper<boolean>; + } + + //_.template + interface TemplateOptions extends TemplateSettings { + /** + * The sourceURL of the template's compiled source. + */ + sourceURL?: string; + } + + interface TemplateExecutor { + (data?: Object): string; + source: string; + } + + interface LoDashStatic { + /** + * Creates a compiled template function that can interpolate data properties in "interpolate" delimiters, + * HTML-escape interpolated data properties in "escape" delimiters, and execute JavaScript in "evaluate" + * delimiters. Data properties may be accessed as free variables in the template. If a setting object is + * provided it takes precedence over _.templateSettings values. + * + * Note: In the development build _.template utilizes + * [sourceURLs](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl) for easier + * debugging. + * + * For more information on precompiling templates see + * [lodash's custom builds documentation](https://lodash.com/custom-builds). + * + * For more information on Chrome extension sandboxes see + * [Chrome's extensions documentation](https://developer.chrome.com/extensions/sandboxingEval). + * + * @param string The template string. + * @param options The options object. + * @param options.escape The HTML "escape" delimiter. + * @param options.evaluate The "evaluate" delimiter. + * @param options.imports An object to import into the template as free variables. + * @param options.interpolate The "interpolate" delimiter. + * @param options.sourceURL The sourceURL of the template's compiled source. + * @param options.variable The data object variable name. + * @return Returns the compiled template function. + */ + template( + string: string, + options?: TemplateOptions + ): TemplateExecutor; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.template + */ + template(options?: TemplateOptions): TemplateExecutor; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.template + */ + template(options?: TemplateOptions): LoDashExplicitObjectWrapper<TemplateExecutor>; + } + + //_.trim + interface LoDashStatic { + /** + * Removes leading and trailing whitespace or specified characters from string. + * + * @param string The string to trim. + * @param chars The characters to trim. + * @return Returns the trimmed string. + */ + trim( + string?: string, + chars?: string + ): string; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.trim + */ + trim(chars?: string): string; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.trim + */ + trim(chars?: string): LoDashExplicitWrapper<string>; + } + + //_.trimLeft + interface LoDashStatic { + /** + * Removes leading whitespace or specified characters from string. + * + * @param string The string to trim. + * @param chars The characters to trim. + * @return Returns the trimmed string. + */ + trimLeft( + string?: string, + chars?: string + ): string; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.trimLeft + */ + trimLeft(chars?: string): string; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.trimLeft + */ + trimLeft(chars?: string): LoDashExplicitWrapper<string>; + } + + //_.trimRight + interface LoDashStatic { + /** + * Removes trailing whitespace or specified characters from string. + * + * @param string The string to trim. + * @param chars The characters to trim. + * @return Returns the trimmed string. + */ + trimRight( + string?: string, + chars?: string + ): string; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.trimRight + */ + trimRight(chars?: string): string; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.trimRight + */ + trimRight(chars?: string): LoDashExplicitWrapper<string>; + } + + //_.trunc + interface TruncOptions { + /** The maximum string length. */ + length?: number; + /** The string to indicate text is omitted. */ + omission?: string; + /** The separator pattern to truncate to. */ + separator?: string|RegExp; + } + + interface LoDashStatic { + /** + * Truncates string if it’s longer than the given maximum string length. The last characters of the truncated + * string are replaced with the omission string which defaults to "…". + * + * @param string The string to truncate. + * @param options The options object or maximum string length. + * @return Returns the truncated string. + */ + trunc( + string?: string, + options?: TruncOptions|number + ): string; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.trunc + */ + trunc(options?: TruncOptions|number): string; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.trunc + */ + trunc(options?: TruncOptions|number): LoDashExplicitWrapper<string>; + } + + //_.unescape + interface LoDashStatic { + /** + * The inverse of _.escape; this method converts the HTML entities &, <, >, ", ', and ` + * in string to their corresponding characters. + * + * @param string The string to unescape. + * @return Returns the unescaped string. + */ + unescape(string?: string): string; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.unescape + */ + unescape(): string; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.unescape + */ + unescape(): LoDashExplicitWrapper<string>; + } + + //_.words + interface LoDashStatic { + /** + * Splits string into an array of its words. + * + * @param string The string to inspect. + * @param pattern The pattern to match words. + * @return Returns the words of string. + */ + words( + string?: string, + pattern?: string|RegExp + ): string[]; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.words + */ + words(pattern?: string|RegExp): string[]; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.words + */ + words(pattern?: string|RegExp): LoDashExplicitArrayWrapper<string>; + } + + /*********** + * Utility * + ***********/ + + //_.attempt + interface LoDashStatic { + /** + * Attempts to invoke func, returning either the result or the caught error object. Any additional arguments + * are provided to func when it’s invoked. + * + * @param func The function to attempt. + * @return Returns the func result or error object. + */ + attempt<TResult>(func: (...args: any[]) => TResult, ...args: any[]): TResult|Error; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.attempt + */ + attempt<TResult>(...args: any[]): TResult|Error; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.attempt + */ + attempt<TResult>(...args: any[]): LoDashExplicitObjectWrapper<TResult|Error>; + } + + //_.callback + interface LoDashStatic { + /** + * Creates a function that invokes func with the this binding of thisArg and arguments of the created function. + * If func is a property name the created callback returns the property value for a given element. If func is + * an object the created callback returns true for elements that contain the equivalent object properties, + * otherwise it returns false. + * + * @param func The value to convert to a callback. + * @param thisArg The this binding of func. + * @result Returns the callback. + */ + callback<TResult>( + func: Function, + thisArg?: any + ): (...args: any[]) => TResult; + + /** + * @see _.callback + */ + callback<TResult>( + func: string, + thisArg?: any + ): (object: any) => TResult; + + /** + * @see _.callback + */ + callback( + func: Object, + thisArg?: any + ): (object: any) => boolean; + + /** + * @see _.callback + */ + callback<TResult>(): (value: TResult) => TResult; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.callback + */ + callback<TResult>(thisArg?: any): LoDashImplicitObjectWrapper<(object: any) => TResult>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.callback + */ + callback(thisArg?: any): LoDashImplicitObjectWrapper<(object: any) => boolean>; + + /** + * @see _.callback + */ + callback<TResult>(thisArg?: any): LoDashImplicitObjectWrapper<(...args: any[]) => TResult>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.callback + */ + callback<TResult>(thisArg?: any): LoDashExplicitObjectWrapper<(object: any) => TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.callback + */ + callback(thisArg?: any): LoDashExplicitObjectWrapper<(object: any) => boolean>; + + /** + * @see _.callback + */ + callback<TResult>(thisArg?: any): LoDashExplicitObjectWrapper<(...args: any[]) => TResult>; + } + + //_.constant + interface LoDashStatic { + /** + * Creates a function that returns value. + * + * @param value The value to return from the new function. + * @return Returns the new function. + */ + constant<T>(value: T): () => T; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.constant + */ + constant<TResult>(): LoDashImplicitObjectWrapper<() => TResult>; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.constant + */ + constant<TResult>(): LoDashExplicitObjectWrapper<() => TResult>; + } + + //_.identity + interface LoDashStatic { + /** + * This method returns the first argument provided to it. + * @param value Any value. + * @return Returns value. + */ + identity<T>(value?: T): T; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.identity + */ + identity(): T; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.identity + */ + identity(): T[]; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.identity + */ + identity(): T; + } + + //_.iteratee + interface LoDashStatic { + /** + * @see _.callback + */ + iteratee<TResult>( + func: Function, + thisArg?: any + ): (...args: any[]) => TResult; + + /** + * @see _.callback + */ + iteratee<TResult>( + func: string, + thisArg?: any + ): (object: any) => TResult; + + /** + * @see _.callback + */ + iteratee( + func: Object, + thisArg?: any + ): (object: any) => boolean; + + /** + * @see _.callback + */ + iteratee<TResult>(): (value: TResult) => TResult; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.callback + */ + iteratee<TResult>(thisArg?: any): LoDashImplicitObjectWrapper<(object: any) => TResult>; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.callback + */ + iteratee(thisArg?: any): LoDashImplicitObjectWrapper<(object: any) => boolean>; + + /** + * @see _.callback + */ + iteratee<TResult>(thisArg?: any): LoDashImplicitObjectWrapper<(...args: any[]) => TResult>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.callback + */ + iteratee<TResult>(thisArg?: any): LoDashExplicitObjectWrapper<(object: any) => TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.callback + */ + iteratee(thisArg?: any): LoDashExplicitObjectWrapper<(object: any) => boolean>; + + /** + * @see _.callback + */ + iteratee<TResult>(thisArg?: any): LoDashExplicitObjectWrapper<(...args: any[]) => TResult>; + } + + //_.matches + interface LoDashStatic { + /** + * Creates a function that performs a deep comparison between a given object and source, returning true if the + * given object has equivalent property values, else false. + * + * Note: This method supports comparing arrays, booleans, Date objects, numbers, Object objects, regexes, and + * strings. Objects are compared by their own, not inherited, enumerable properties. For comparing a single own + * or inherited property value see _.matchesProperty. + * + * @param source The object of property values to match. + * @return Returns the new function. + */ + matches<T>(source: T): (value: any) => boolean; + + /** + * @see _.matches + */ + matches<T, V>(source: T): (value: V) => boolean; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.matches + */ + matches<V>(): LoDashImplicitObjectWrapper<(value: V) => boolean>; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.matches + */ + matches<V>(): LoDashExplicitObjectWrapper<(value: V) => boolean>; + } + + //_.matchesProperty + interface LoDashStatic { + /** + * Creates a function that compares the property value of path on a given object to value. + * + * Note: This method supports comparing arrays, booleans, Date objects, numbers, Object objects, regexes, and + * strings. Objects are compared by their own, not inherited, enumerable properties. + * + * @param path The path of the property to get. + * @param srcValue The value to match. + * @return Returns the new function. + */ + matchesProperty<T>( + path: StringRepresentable|StringRepresentable[], + srcValue: T + ): (value: any) => boolean; + + /** + * @see _.matchesProperty + */ + matchesProperty<T, V>( + path: StringRepresentable|StringRepresentable[], + srcValue: T + ): (value: V) => boolean; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.matchesProperty + */ + matchesProperty<SrcValue>( + srcValue: SrcValue + ): LoDashImplicitObjectWrapper<(value: any) => boolean>; + + /** + * @see _.matchesProperty + */ + matchesProperty<SrcValue, Value>( + srcValue: SrcValue + ): LoDashImplicitObjectWrapper<(value: Value) => boolean>; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.matchesProperty + */ + matchesProperty<SrcValue>( + srcValue: SrcValue + ): LoDashExplicitObjectWrapper<(value: any) => boolean>; + + /** + * @see _.matchesProperty + */ + matchesProperty<SrcValue, Value>( + srcValue: SrcValue + ): LoDashExplicitObjectWrapper<(value: Value) => boolean>; + } + + //_.method + interface LoDashStatic { + /** + * Creates a function that invokes the method at path on a given object. Any additional arguments are provided + * to the invoked method. + * + * @param path The path of the method to invoke. + * @param args The arguments to invoke the method with. + * @return Returns the new function. + */ + method<TObject, TResult>( + path: string|StringRepresentable[], + ...args: any[] + ): (object: TObject) => TResult; + + /** + * @see _.method + */ + method<TResult>( + path: string|StringRepresentable[], + ...args: any[] + ): (object: any) => TResult; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.method + */ + method<TObject, TResult>(...args: any[]): LoDashImplicitObjectWrapper<(object: TObject) => TResult>; + + /** + * @see _.method + */ + method<TResult>(...args: any[]): LoDashImplicitObjectWrapper<(object: any) => TResult>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.method + */ + method<TObject, TResult>(...args: any[]): LoDashImplicitObjectWrapper<(object: TObject) => TResult>; + + /** + * @see _.method + */ + method<TResult>(...args: any[]): LoDashImplicitObjectWrapper<(object: any) => TResult>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.method + */ + method<TObject, TResult>(...args: any[]): LoDashExplicitObjectWrapper<(object: TObject) => TResult>; + + /** + * @see _.method + */ + method<TResult>(...args: any[]): LoDashExplicitObjectWrapper<(object: any) => TResult>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.method + */ + method<TObject, TResult>(...args: any[]): LoDashExplicitObjectWrapper<(object: TObject) => TResult>; + + /** + * @see _.method + */ + method<TResult>(...args: any[]): LoDashExplicitObjectWrapper<(object: any) => TResult>; + } + + //_.methodOf + interface LoDashStatic { + /** + * The opposite of _.method; this method creates a function that invokes the method at a given path on object. + * Any additional arguments are provided to the invoked method. + * + * @param object The object to query. + * @param args The arguments to invoke the method with. + * @return Returns the new function. + */ + methodOf<TObject extends {}, TResult>( + object: TObject, + ...args: any[] + ): (path: StringRepresentable|StringRepresentable[]) => TResult; + + /** + * @see _.methodOf + */ + methodOf<TResult>( + object: {}, + ...args: any[] + ): (path: StringRepresentable|StringRepresentable[]) => TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.methodOf + */ + methodOf<TResult>( + ...args: any[] + ): LoDashImplicitObjectWrapper<(path: StringRepresentable|StringRepresentable[]) => TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.methodOf + */ + methodOf<TResult>( + ...args: any[] + ): LoDashExplicitObjectWrapper<(path: StringRepresentable|StringRepresentable[]) => TResult>; + } + + //_.mixin + interface MixinOptions { + chain?: boolean; + } + + interface LoDashStatic { + /** + * Adds all own enumerable function properties of a source object to the destination object. If object is a + * function then methods are added to its prototype as well. + * + * Note: Use _.runInContext to create a pristine lodash function to avoid conflicts caused by modifying + * the original. + * + * @param object The destination object. + * @param source The object of functions to add. + * @param options The options object. + * @param options.chain Specify whether the functions added are chainable. + * @return Returns object. + */ + mixin<TResult, TObject>( + object: TObject, + source: Dictionary<Function>, + options?: MixinOptions + ): TResult; + + /** + * @see _.mixin + */ + mixin<TResult>( + source: Dictionary<Function>, + options?: MixinOptions + ): TResult; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.mixin + */ + mixin<TResult>( + source: Dictionary<Function>, + options?: MixinOptions + ): LoDashImplicitObjectWrapper<TResult>; + + /** + * @see _.mixin + */ + mixin<TResult>( + options?: MixinOptions + ): LoDashImplicitObjectWrapper<TResult>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.mixin + */ + mixin<TResult>( + source: Dictionary<Function>, + options?: MixinOptions + ): LoDashExplicitObjectWrapper<TResult>; + + /** + * @see _.mixin + */ + mixin<TResult>( + options?: MixinOptions + ): LoDashExplicitObjectWrapper<TResult>; + } + + //_.noConflict + interface LoDashStatic { + /** + * Reverts the _ variable to its previous value and returns a reference to the lodash function. + * + * @return Returns the lodash function. + */ + noConflict(): typeof _; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.noConflict + */ + noConflict(): typeof _; + } + + //_.noop + interface LoDashStatic { + /** + * A no-operation function that returns undefined regardless of the arguments it receives. + * + * @return undefined + */ + noop(...args: any[]): void; + } + + interface LoDashImplicitWrapperBase<T, TWrapper> { + /** + * @see _.noop + */ + noop(...args: any[]): void; + } + + interface LoDashExplicitWrapperBase<T, TWrapper> { + /** + * @see _.noop + */ + noop(...args: any[]): _.LoDashExplicitWrapper<void>; + } + + //_.property + interface LoDashStatic { + /** + * Creates a function that returns the property value at path on a given object. + * + * @param path The path of the property to get. + * @return Returns the new function. + */ + property<TObj, TResult>(path: StringRepresentable|StringRepresentable[]): (obj: TObj) => TResult; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.property + */ + property<TObj, TResult>(): LoDashImplicitObjectWrapper<(obj: TObj) => TResult>; + } + + interface LoDashImplicitArrayWrapper<T> { + /** + * @see _.property + */ + property<TObj, TResult>(): LoDashImplicitObjectWrapper<(obj: TObj) => TResult>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.property + */ + property<TObj, TResult>(): LoDashExplicitObjectWrapper<(obj: TObj) => TResult>; + } + + interface LoDashExplicitArrayWrapper<T> { + /** + * @see _.property + */ + property<TObj, TResult>(): LoDashExplicitObjectWrapper<(obj: TObj) => TResult>; + } + + //_.propertyOf + interface LoDashStatic { + /** + * The opposite of _.property; this method creates a function that returns the property value at a given path + * on object. + * + * @param object The object to query. + * @return Returns the new function. + */ + propertyOf<T extends {}>(object: T): (path: string|string[]) => any; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.propertyOf + */ + propertyOf(): LoDashImplicitObjectWrapper<(path: string|string[]) => any>; + } + + interface LoDashExplicitObjectWrapper<T> { + /** + * @see _.propertyOf + */ + propertyOf(): LoDashExplicitObjectWrapper<(path: string|string[]) => any>; + } + + //_.range + interface LoDashStatic { + /** + * Creates an array of numbers (positive and/or negative) progressing from start up to, but not including, end. + * If end is not specified it’s set to start with start then set to 0. If end is less than start a zero-length + * range is created unless a negative step is specified. + * + * @param start The start of the range. + * @param end The end of the range. + * @param step The value to increment or decrement by. + * @return Returns a new range array. + */ + range( + start: number, + end: number, + step?: number + ): number[]; + + /** + * @see _.range + */ + range( + end: number, + step?: number + ): number[]; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.range + */ + range( + end?: number, + step?: number + ): LoDashImplicitArrayWrapper<number>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.range + */ + range( + end?: number, + step?: number + ): LoDashExplicitArrayWrapper<number>; + } + + //_.runInContext + interface LoDashStatic { + /** + * Create a new pristine lodash function using the given context object. + * + * @param context The context object. + * @return Returns a new lodash function. + */ + runInContext(context?: Object): typeof _; + } + + interface LoDashImplicitObjectWrapper<T> { + /** + * @see _.runInContext + */ + runInContext(): typeof _; + } + + //_.times + interface LoDashStatic { + /** + * Invokes the iteratee function n times, returning an array of the results of each invocation. The iteratee is + * bound to thisArg and invoked with one argument; (index). + * + * @param n The number of times to invoke iteratee. + * @param iteratee The function invoked per iteration. + * @param thisArg The this binding of iteratee. + * @return Returns the array of results. + */ + times<TResult>( + n: number, + iteratee: (num: number) => TResult, + thisArg?: any + ): TResult[]; + + /** + * @see _.times + */ + times(n: number): number[]; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.times + */ + times<TResult>( + iteratee: (num: number) => TResult, + thisArgs?: any + ): LoDashImplicitArrayWrapper<TResult>; + + /** + * @see _.times + */ + times(): LoDashImplicitArrayWrapper<number>; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.times + */ + times<TResult>( + iteratee: (num: number) => TResult, + thisArgs?: any + ): LoDashExplicitArrayWrapper<TResult>; + + /** + * @see _.times + */ + times(): LoDashExplicitArrayWrapper<number>; + } + + //_.uniqueId + interface LoDashStatic { + /** + * Generates a unique ID. If prefix is provided the ID is appended to it. + * + * @param prefix The value to prefix the ID with. + * @return Returns the unique ID. + */ + uniqueId(prefix?: string): string; + } + + interface LoDashImplicitWrapper<T> { + /** + * @see _.uniqueId + */ + uniqueId(): string; + } + + interface LoDashExplicitWrapper<T> { + /** + * @see _.uniqueId + */ + uniqueId(): LoDashExplicitWrapper<string>; + } + + interface ListIterator<T, TResult> { + (value: T, index: number, collection: List<T>): TResult; + } + + interface DictionaryIterator<T, TResult> { + (value: T, key?: string, collection?: Dictionary<T>): TResult; + } + + interface NumericDictionaryIterator<T, TResult> { + (value: T, key?: number, collection?: Dictionary<T>): TResult; + } + + interface ObjectIterator<T, TResult> { + (element: T, key?: string, collection?: any): TResult; + } + + interface StringIterator<TResult> { + (char: string, index?: number, string?: string): TResult; + } + + interface MemoVoidIterator<T, TResult> { + (prev: TResult, curr: T, indexOrKey?: any, list?: T[]): void; + } + interface MemoIterator<T, TResult> { + (prev: TResult, curr: T, indexOrKey?: any, list?: T[]): TResult; + } + + interface MemoVoidArrayIterator<T, TResult> { + (acc: TResult, curr: T, index?: number, arr?: T[]): void; + } + interface MemoVoidDictionaryIterator<T, TResult> { + (acc: TResult, curr: T, key?: string, dict?: Dictionary<T>): void; + } + + //interface Collection<T> {} + + // Common interface between Arrays and jQuery objects + interface List<T> { + [index: number]: T; + length: number; + } + + interface Dictionary<T> { + [index: string]: T; + } + + interface NumericDictionary<T> { + [index: number]: T; + } + + interface StringRepresentable { + toString(): string; + } + + interface Cancelable { + cancel(): void; + } +} + +declare module "lodash" { + export = _; +} diff --git a/js/lodash.js b/js/lodash.js new file mode 100644 index 0000000..12f0d1a --- /dev/null +++ b/js/lodash.js @@ -0,0 +1,12557 @@ +/** + * @license + * lodash 3.10.1 (Custom Build) <https://lodash.com/> + * Build: `lodash compat -o ./lodash.js` + * Copyright 2012-2015 The Dojo Foundation <http://dojofoundation.org/> + * Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE> + * Copyright 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Available under MIT license <https://lodash.com/license> + */ +;(function() { + + /** Used as a safe reference for `undefined` in pre-ES5 environments. */ + var undefined; + + /** Used as the semantic version number. */ + var VERSION = '3.10.1'; + + /** Used to compose bitmasks for wrapper metadata. */ + var BIND_FLAG = 1, + BIND_KEY_FLAG = 2, + CURRY_BOUND_FLAG = 4, + CURRY_FLAG = 8, + CURRY_RIGHT_FLAG = 16, + PARTIAL_FLAG = 32, + PARTIAL_RIGHT_FLAG = 64, + ARY_FLAG = 128, + REARG_FLAG = 256; + + /** Used as default options for `_.trunc`. */ + var DEFAULT_TRUNC_LENGTH = 30, + DEFAULT_TRUNC_OMISSION = '...'; + + /** Used to detect when a function becomes hot. */ + var HOT_COUNT = 150, + HOT_SPAN = 16; + + /** Used as the size to enable large array optimizations. */ + var LARGE_ARRAY_SIZE = 200; + + /** Used to indicate the type of lazy iteratees. */ + var LAZY_FILTER_FLAG = 1, + LAZY_MAP_FLAG = 2; + + /** Used as the `TypeError` message for "Functions" methods. */ + var FUNC_ERROR_TEXT = 'Expected a function'; + + /** Used as the internal argument placeholder. */ + var PLACEHOLDER = '__lodash_placeholder__'; + + /** `Object#toString` result references. */ + var argsTag = '[object Arguments]', + arrayTag = '[object Array]', + boolTag = '[object Boolean]', + dateTag = '[object Date]', + errorTag = '[object Error]', + funcTag = '[object Function]', + mapTag = '[object Map]', + numberTag = '[object Number]', + objectTag = '[object Object]', + regexpTag = '[object RegExp]', + setTag = '[object Set]', + stringTag = '[object String]', + weakMapTag = '[object WeakMap]'; + + var arrayBufferTag = '[object ArrayBuffer]', + float32Tag = '[object Float32Array]', + float64Tag = '[object Float64Array]', + int8Tag = '[object Int8Array]', + int16Tag = '[object Int16Array]', + int32Tag = '[object Int32Array]', + uint8Tag = '[object Uint8Array]', + uint8ClampedTag = '[object Uint8ClampedArray]', + uint16Tag = '[object Uint16Array]', + uint32Tag = '[object Uint32Array]'; + + /** Used to match empty string literals in compiled template source. */ + var reEmptyStringLeading = /\b__p \+= '';/g, + reEmptyStringMiddle = /\b(__p \+=) '' \+/g, + reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g; + + /** Used to match HTML entities and HTML characters. */ + var reEscapedHtml = /&(?:amp|lt|gt|quot|#39|#96);/g, + reUnescapedHtml = /[&<>"'`]/g, + reHasEscapedHtml = RegExp(reEscapedHtml.source), + reHasUnescapedHtml = RegExp(reUnescapedHtml.source); + + /** Used to match template delimiters. */ + var reEscape = /<%-([\s\S]+?)%>/g, + reEvaluate = /<%([\s\S]+?)%>/g, + reInterpolate = /<%=([\s\S]+?)%>/g; + + /** Used to match property names within property paths. */ + var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\n\\]|\\.)*?\1)\]/, + reIsPlainProp = /^\w*$/, + rePropName = /[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\n\\]|\\.)*?)\2)\]/g; + + /** + * Used to match `RegExp` [syntax characters](http://ecma-international.org/ecma-262/6.0/#sec-patterns) + * and those outlined by [`EscapeRegExpPattern`](http://ecma-international.org/ecma-262/6.0/#sec-escaperegexppattern). + */ + var reRegExpChars = /^[:!,]|[\\^$.*+?()[\]{}|\/]|(^[0-9a-fA-Fnrtuvx])|([\n\r\u2028\u2029])/g, + reHasRegExpChars = RegExp(reRegExpChars.source); + + /** Used to match [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks). */ + var reComboMark = /[\u0300-\u036f\ufe20-\ufe23]/g; + + /** Used to match backslashes in property paths. */ + var reEscapeChar = /\\(\\)?/g; + + /** Used to match [ES template delimiters](http://ecma-international.org/ecma-262/6.0/#sec-template-literal-lexical-components). */ + var reEsTemplate = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g; + + /** Used to match `RegExp` flags from their coerced string values. */ + var reFlags = /\w*$/; + + /** Used to detect hexadecimal string values. */ + var reHasHexPrefix = /^0[xX]/; + + /** Used to detect host constructors (Safari > 5). */ + var reIsHostCtor = /^\[object .+?Constructor\]$/; + + /** Used to detect unsigned integer values. */ + var reIsUint = /^\d+$/; + + /** Used to match latin-1 supplementary letters (excluding mathematical operators). */ + var reLatin1 = /[\xc0-\xd6\xd8-\xde\xdf-\xf6\xf8-\xff]/g; + + /** Used to ensure capturing order of template delimiters. */ + var reNoMatch = /($^)/; + + /** Used to match unescaped characters in compiled string literals. */ + var reUnescapedString = /['\n\r\u2028\u2029\\]/g; + + /** Used to match words to create compound words. */ + var reWords = (function() { + var upper = '[A-Z\\xc0-\\xd6\\xd8-\\xde]', + lower = '[a-z\\xdf-\\xf6\\xf8-\\xff]+'; + + return RegExp(upper + '+(?=' + upper + lower + ')|' + upper + '?' + lower + '|' + upper + '+|[0-9]+', 'g'); + }()); + + /** Used to assign default `context` object properties. */ + var contextProps = [ + 'Array', 'ArrayBuffer', 'Date', 'Error', 'Float32Array', 'Float64Array', + 'Function', 'Int8Array', 'Int16Array', 'Int32Array', 'Math', 'Number', + 'Object', 'RegExp', 'Set', 'String', '_', 'clearTimeout', 'isFinite', + 'parseFloat', 'parseInt', 'setTimeout', 'TypeError', 'Uint8Array', + 'Uint8ClampedArray', 'Uint16Array', 'Uint32Array', 'WeakMap' + ]; + + /** Used to fix the JScript `[[DontEnum]]` bug. */ + var shadowProps = [ + 'constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', + 'toLocaleString', 'toString', 'valueOf' + ]; + + /** Used to make template sourceURLs easier to identify. */ + var templateCounter = -1; + + /** Used to identify `toStringTag` values of typed arrays. */ + var typedArrayTags = {}; + typedArrayTags[float32Tag] = typedArrayTags[float64Tag] = + typedArrayTags[int8Tag] = typedArrayTags[int16Tag] = + typedArrayTags[int32Tag] = typedArrayTags[uint8Tag] = + typedArrayTags[uint8ClampedTag] = typedArrayTags[uint16Tag] = + typedArrayTags[uint32Tag] = true; + typedArrayTags[argsTag] = typedArrayTags[arrayTag] = + typedArrayTags[arrayBufferTag] = typedArrayTags[boolTag] = + typedArrayTags[dateTag] = typedArrayTags[errorTag] = + typedArrayTags[funcTag] = typedArrayTags[mapTag] = + typedArrayTags[numberTag] = typedArrayTags[objectTag] = + typedArrayTags[regexpTag] = typedArrayTags[setTag] = + typedArrayTags[stringTag] = typedArrayTags[weakMapTag] = false; + + /** Used to identify `toStringTag` values supported by `_.clone`. */ + var cloneableTags = {}; + cloneableTags[argsTag] = cloneableTags[arrayTag] = + cloneableTags[arrayBufferTag] = cloneableTags[boolTag] = + cloneableTags[dateTag] = cloneableTags[float32Tag] = + cloneableTags[float64Tag] = cloneableTags[int8Tag] = + cloneableTags[int16Tag] = cloneableTags[int32Tag] = + cloneableTags[numberTag] = cloneableTags[objectTag] = + cloneableTags[regexpTag] = cloneableTags[stringTag] = + cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] = + cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true; + cloneableTags[errorTag] = cloneableTags[funcTag] = + cloneableTags[mapTag] = cloneableTags[setTag] = + cloneableTags[weakMapTag] = false; + + /** Used to map latin-1 supplementary letters to basic latin letters. */ + var deburredLetters = { + '\xc0': 'A', '\xc1': 'A', '\xc2': 'A', '\xc3': 'A', '\xc4': 'A', '\xc5': 'A', + '\xe0': 'a', '\xe1': 'a', '\xe2': 'a', '\xe3': 'a', '\xe4': 'a', '\xe5': 'a', + '\xc7': 'C', '\xe7': 'c', + '\xd0': 'D', '\xf0': 'd', + '\xc8': 'E', '\xc9': 'E', '\xca': 'E', '\xcb': 'E', + '\xe8': 'e', '\xe9': 'e', '\xea': 'e', '\xeb': 'e', + '\xcC': 'I', '\xcd': 'I', '\xce': 'I', '\xcf': 'I', + '\xeC': 'i', '\xed': 'i', '\xee': 'i', '\xef': 'i', + '\xd1': 'N', '\xf1': 'n', + '\xd2': 'O', '\xd3': 'O', '\xd4': 'O', '\xd5': 'O', '\xd6': 'O', '\xd8': 'O', + '\xf2': 'o', '\xf3': 'o', '\xf4': 'o', '\xf5': 'o', '\xf6': 'o', '\xf8': 'o', + '\xd9': 'U', '\xda': 'U', '\xdb': 'U', '\xdc': 'U', + '\xf9': 'u', '\xfa': 'u', '\xfb': 'u', '\xfc': 'u', + '\xdd': 'Y', '\xfd': 'y', '\xff': 'y', + '\xc6': 'Ae', '\xe6': 'ae', + '\xde': 'Th', '\xfe': 'th', + '\xdf': 'ss' + }; + + /** Used to map characters to HTML entities. */ + var htmlEscapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }; + + /** Used to map HTML entities to characters. */ + var htmlUnescapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'", + '`': '`' + }; + + /** Used to determine if values are of the language type `Object`. */ + var objectTypes = { + 'function': true, + 'object': true + }; + + /** Used to escape characters for inclusion in compiled regexes. */ + var regexpEscapes = { + '0': 'x30', '1': 'x31', '2': 'x32', '3': 'x33', '4': 'x34', + '5': 'x35', '6': 'x36', '7': 'x37', '8': 'x38', '9': 'x39', + 'A': 'x41', 'B': 'x42', 'C': 'x43', 'D': 'x44', 'E': 'x45', 'F': 'x46', + 'a': 'x61', 'b': 'x62', 'c': 'x63', 'd': 'x64', 'e': 'x65', 'f': 'x66', + 'n': 'x6e', 'r': 'x72', 't': 'x74', 'u': 'x75', 'v': 'x76', 'x': 'x78' + }; + + /** Used to escape characters for inclusion in compiled string literals. */ + var stringEscapes = { + '\\': '\\', + "'": "'", + '\n': 'n', + '\r': 'r', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + /** Detect free variable `exports`. */ + var freeExports = objectTypes[typeof exports] && exports && !exports.nodeType && exports; + + /** Detect free variable `module`. */ + var freeModule = objectTypes[typeof module] && module && !module.nodeType && module; + + /** Detect free variable `global` from Node.js. */ + var freeGlobal = freeExports && freeModule && typeof global == 'object' && global && global.Object && global; + + /** Detect free variable `self`. */ + var freeSelf = objectTypes[typeof self] && self && self.Object && self; + + /** Detect free variable `window`. */ + var freeWindow = objectTypes[typeof window] && window && window.Object && window; + + /** Detect the popular CommonJS extension `module.exports`. */ + var moduleExports = freeModule && freeModule.exports === freeExports && freeExports; + + /** + * Used as a reference to the global object. + * + * The `this` value is used if it's the global object to avoid Greasemonkey's + * restricted `window` object, otherwise the `window` object is used. + */ + var root = freeGlobal || ((freeWindow !== (this && this.window)) && freeWindow) || freeSelf || this; + + /*--------------------------------------------------------------------------*/ + + /** + * The base implementation of `compareAscending` which compares values and + * sorts them in ascending order without guaranteeing a stable sort. + * + * @private + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {number} Returns the sort order indicator for `value`. + */ + function baseCompareAscending(value, other) { + if (value !== other) { + var valIsNull = value === null, + valIsUndef = value === undefined, + valIsReflexive = value === value; + + var othIsNull = other === null, + othIsUndef = other === undefined, + othIsReflexive = other === other; + + if ((value > other && !othIsNull) || !valIsReflexive || + (valIsNull && !othIsUndef && othIsReflexive) || + (valIsUndef && othIsReflexive)) { + return 1; + } + if ((value < other && !valIsNull) || !othIsReflexive || + (othIsNull && !valIsUndef && valIsReflexive) || + (othIsUndef && valIsReflexive)) { + return -1; + } + } + return 0; + } + + /** + * The base implementation of `_.findIndex` and `_.findLastIndex` without + * support for callback shorthands and `this` binding. + * + * @private + * @param {Array} array The array to search. + * @param {Function} predicate The function invoked per iteration. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {number} Returns the index of the matched value, else `-1`. + */ + function baseFindIndex(array, predicate, fromRight) { + var length = array.length, + index = fromRight ? length : -1; + + while ((fromRight ? index-- : ++index < length)) { + if (predicate(array[index], index, array)) { + return index; + } + } + return -1; + } + + /** + * The base implementation of `_.indexOf` without support for binary searches. + * + * @private + * @param {Array} array The array to search. + * @param {*} value The value to search for. + * @param {number} fromIndex The index to search from. + * @returns {number} Returns the index of the matched value, else `-1`. + */ + function baseIndexOf(array, value, fromIndex) { + if (value !== value) { + return indexOfNaN(array, fromIndex); + } + var index = fromIndex - 1, + length = array.length; + + while (++index < length) { + if (array[index] === value) { + return index; + } + } + return -1; + } + + /** + * The base implementation of `_.isFunction` without support for environments + * with incorrect `typeof` results. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. + */ + function baseIsFunction(value) { + // Avoid a Chakra JIT bug in compatibility modes of IE 11. + // See https://github.com/jashkenas/underscore/issues/1621 for more details. + return typeof value == 'function' || false; + } + + /** + * Converts `value` to a string if it's not one. An empty string is returned + * for `null` or `undefined` values. + * + * @private + * @param {*} value The value to process. + * @returns {string} Returns the string. + */ + function baseToString(value) { + return value == null ? '' : (value + ''); + } + + /** + * Used by `_.trim` and `_.trimLeft` to get the index of the first character + * of `string` that is not found in `chars`. + * + * @private + * @param {string} string The string to inspect. + * @param {string} chars The characters to find. + * @returns {number} Returns the index of the first character not found in `chars`. + */ + function charsLeftIndex(string, chars) { + var index = -1, + length = string.length; + + while (++index < length && chars.indexOf(string.charAt(index)) > -1) {} + return index; + } + + /** + * Used by `_.trim` and `_.trimRight` to get the index of the last character + * of `string` that is not found in `chars`. + * + * @private + * @param {string} string The string to inspect. + * @param {string} chars The characters to find. + * @returns {number} Returns the index of the last character not found in `chars`. + */ + function charsRightIndex(string, chars) { + var index = string.length; + + while (index-- && chars.indexOf(string.charAt(index)) > -1) {} + return index; + } + + /** + * Used by `_.sortBy` to compare transformed elements of a collection and stable + * sort them in ascending order. + * + * @private + * @param {Object} object The object to compare. + * @param {Object} other The other object to compare. + * @returns {number} Returns the sort order indicator for `object`. + */ + function compareAscending(object, other) { + return baseCompareAscending(object.criteria, other.criteria) || (object.index - other.index); + } + + /** + * Used by `_.sortByOrder` to compare multiple properties of a value to another + * and stable sort them. + * + * If `orders` is unspecified, all valuess are sorted in ascending order. Otherwise, + * a value is sorted in ascending order if its corresponding order is "asc", and + * descending if "desc". + * + * @private + * @param {Object} object The object to compare. + * @param {Object} other The other object to compare. + * @param {boolean[]} orders The order to sort by for each property. + * @returns {number} Returns the sort order indicator for `object`. + */ + function compareMultiple(object, other, orders) { + var index = -1, + objCriteria = object.criteria, + othCriteria = other.criteria, + length = objCriteria.length, + ordersLength = orders.length; + + while (++index < length) { + var result = baseCompareAscending(objCriteria[index], othCriteria[index]); + if (result) { + if (index >= ordersLength) { + return result; + } + var order = orders[index]; + return result * ((order === 'asc' || order === true) ? 1 : -1); + } + } + // Fixes an `Array#sort` bug in the JS engine embedded in Adobe applications + // that causes it, under certain circumstances, to provide the same value for + // `object` and `other`. See https://github.com/jashkenas/underscore/pull/1247 + // for more details. + // + // This also ensures a stable sort in V8 and other engines. + // See https://code.google.com/p/v8/issues/detail?id=90 for more details. + return object.index - other.index; + } + + /** + * Used by `_.deburr` to convert latin-1 supplementary letters to basic latin letters. + * + * @private + * @param {string} letter The matched letter to deburr. + * @returns {string} Returns the deburred letter. + */ + function deburrLetter(letter) { + return deburredLetters[letter]; + } + + /** + * Used by `_.escape` to convert characters to HTML entities. + * + * @private + * @param {string} chr The matched character to escape. + * @returns {string} Returns the escaped character. + */ + function escapeHtmlChar(chr) { + return htmlEscapes[chr]; + } + + /** + * Used by `_.escapeRegExp` to escape characters for inclusion in compiled regexes. + * + * @private + * @param {string} chr The matched character to escape. + * @param {string} leadingChar The capture group for a leading character. + * @param {string} whitespaceChar The capture group for a whitespace character. + * @returns {string} Returns the escaped character. + */ + function escapeRegExpChar(chr, leadingChar, whitespaceChar) { + if (leadingChar) { + chr = regexpEscapes[chr]; + } else if (whitespaceChar) { + chr = stringEscapes[chr]; + } + return '\\' + chr; + } + + /** + * Used by `_.template` to escape characters for inclusion in compiled string literals. + * + * @private + * @param {string} chr The matched character to escape. + * @returns {string} Returns the escaped character. + */ + function escapeStringChar(chr) { + return '\\' + stringEscapes[chr]; + } + + /** + * Gets the index at which the first occurrence of `NaN` is found in `array`. + * + * @private + * @param {Array} array The array to search. + * @param {number} fromIndex The index to search from. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {number} Returns the index of the matched `NaN`, else `-1`. + */ + function indexOfNaN(array, fromIndex, fromRight) { + var length = array.length, + index = fromIndex + (fromRight ? 0 : -1); + + while ((fromRight ? index-- : ++index < length)) { + var other = array[index]; + if (other !== other) { + return index; + } + } + return -1; + } + + /** + * Checks if `value` is a host object in IE < 9. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a host object, else `false`. + */ + var isHostObject = (function() { + try { + Object({ 'toString': 0 } + ''); + } catch(e) { + return function() { return false; }; + } + return function(value) { + // IE < 9 presents many host objects as `Object` objects that can coerce + // to strings despite having improperly defined `toString` methods. + return typeof value.toString != 'function' && typeof (value + '') == 'string'; + }; + }()); + + /** + * Checks if `value` is object-like. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is object-like, else `false`. + */ + function isObjectLike(value) { + return !!value && typeof value == 'object'; + } + + /** + * Used by `trimmedLeftIndex` and `trimmedRightIndex` to determine if a + * character code is whitespace. + * + * @private + * @param {number} charCode The character code to inspect. + * @returns {boolean} Returns `true` if `charCode` is whitespace, else `false`. + */ + function isSpace(charCode) { + return ((charCode <= 160 && (charCode >= 9 && charCode <= 13) || charCode == 32 || charCode == 160) || charCode == 5760 || charCode == 6158 || + (charCode >= 8192 && (charCode <= 8202 || charCode == 8232 || charCode == 8233 || charCode == 8239 || charCode == 8287 || charCode == 12288 || charCode == 65279))); + } + + /** + * Replaces all `placeholder` elements in `array` with an internal placeholder + * and returns an array of their indexes. + * + * @private + * @param {Array} array The array to modify. + * @param {*} placeholder The placeholder to replace. + * @returns {Array} Returns the new array of placeholder indexes. + */ + function replaceHolders(array, placeholder) { + var index = -1, + length = array.length, + resIndex = -1, + result = []; + + while (++index < length) { + if (array[index] === placeholder) { + array[index] = PLACEHOLDER; + result[++resIndex] = index; + } + } + return result; + } + + /** + * An implementation of `_.uniq` optimized for sorted arrays without support + * for callback shorthands and `this` binding. + * + * @private + * @param {Array} array The array to inspect. + * @param {Function} [iteratee] The function invoked per iteration. + * @returns {Array} Returns the new duplicate free array. + */ + function sortedUniq(array, iteratee) { + var seen, + index = -1, + length = array.length, + resIndex = -1, + result = []; + + while (++index < length) { + var value = array[index], + computed = iteratee ? iteratee(value, index, array) : value; + + if (!index || seen !== computed) { + seen = computed; + result[++resIndex] = value; + } + } + return result; + } + + /** + * Used by `_.trim` and `_.trimLeft` to get the index of the first non-whitespace + * character of `string`. + * + * @private + * @param {string} string The string to inspect. + * @returns {number} Returns the index of the first non-whitespace character. + */ + function trimmedLeftIndex(string) { + var index = -1, + length = string.length; + + while (++index < length && isSpace(string.charCodeAt(index))) {} + return index; + } + + /** + * Used by `_.trim` and `_.trimRight` to get the index of the last non-whitespace + * character of `string`. + * + * @private + * @param {string} string The string to inspect. + * @returns {number} Returns the index of the last non-whitespace character. + */ + function trimmedRightIndex(string) { + var index = string.length; + + while (index-- && isSpace(string.charCodeAt(index))) {} + return index; + } + + /** + * Used by `_.unescape` to convert HTML entities to characters. + * + * @private + * @param {string} chr The matched character to unescape. + * @returns {string} Returns the unescaped character. + */ + function unescapeHtmlChar(chr) { + return htmlUnescapes[chr]; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Create a new pristine `lodash` function using the given `context` object. + * + * @static + * @memberOf _ + * @category Utility + * @param {Object} [context=root] The context object. + * @returns {Function} Returns a new `lodash` function. + * @example + * + * _.mixin({ 'foo': _.constant('foo') }); + * + * var lodash = _.runInContext(); + * lodash.mixin({ 'bar': lodash.constant('bar') }); + * + * _.isFunction(_.foo); + * // => true + * _.isFunction(_.bar); + * // => false + * + * lodash.isFunction(lodash.foo); + * // => false + * lodash.isFunction(lodash.bar); + * // => true + * + * // using `context` to mock `Date#getTime` use in `_.now` + * var mock = _.runInContext({ + * 'Date': function() { + * return { 'getTime': getTimeMock }; + * } + * }); + * + * // or creating a suped-up `defer` in Node.js + * var defer = _.runInContext({ 'setTimeout': setImmediate }).defer; + */ + function runInContext(context) { + // Avoid issues with some ES3 environments that attempt to use values, named + // after built-in constructors like `Object`, for the creation of literals. + // ES5 clears this up by stating that literals must use built-in constructors. + // See https://es5.github.io/#x11.1.5 for more details. + context = context ? _.defaults(root.Object(), context, _.pick(root, contextProps)) : root; + + /** Native constructor references. */ + var Array = context.Array, + Date = context.Date, + Error = context.Error, + Function = context.Function, + Math = context.Math, + Number = context.Number, + Object = context.Object, + RegExp = context.RegExp, + String = context.String, + TypeError = context.TypeError; + + /** Used for native method references. */ + var arrayProto = Array.prototype, + errorProto = Error.prototype, + objectProto = Object.prototype, + stringProto = String.prototype; + + /** Used to resolve the decompiled source of functions. */ + var fnToString = Function.prototype.toString; + + /** Used to check objects for own properties. */ + var hasOwnProperty = objectProto.hasOwnProperty; + + /** Used to generate unique IDs. */ + var idCounter = 0; + + /** + * Used to resolve the [`toStringTag`](http://ecma-international.org/ecma-262/6.0/#sec-object.prototype.tostring) + * of values. + */ + var objToString = objectProto.toString; + + /** Used to restore the original `_` reference in `_.noConflict`. */ + var oldDash = root._; + + /** Used to detect if a method is native. */ + var reIsNative = RegExp('^' + + fnToString.call(hasOwnProperty).replace(/[\\^$.*+?()[\]{}|]/g, '\\$&') + .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$' + ); + + /** Native method references. */ + var ArrayBuffer = context.ArrayBuffer, + clearTimeout = context.clearTimeout, + parseFloat = context.parseFloat, + pow = Math.pow, + propertyIsEnumerable = objectProto.propertyIsEnumerable, + Set = getNative(context, 'Set'), + setTimeout = context.setTimeout, + splice = arrayProto.splice, + Uint8Array = context.Uint8Array, + WeakMap = getNative(context, 'WeakMap'); + + /* Native method references for those with the same name as other `lodash` methods. */ + var nativeCeil = Math.ceil, + nativeCreate = getNative(Object, 'create'), + nativeFloor = Math.floor, + nativeIsArray = getNative(Array, 'isArray'), + nativeIsFinite = context.isFinite, + nativeKeys = getNative(Object, 'keys'), + nativeMax = Math.max, + nativeMin = Math.min, + nativeNow = getNative(Date, 'now'), + nativeParseInt = context.parseInt, + nativeRandom = Math.random; + + /** Used as references for `-Infinity` and `Infinity`. */ + var NEGATIVE_INFINITY = Number.NEGATIVE_INFINITY, + POSITIVE_INFINITY = Number.POSITIVE_INFINITY; + + /** Used as references for the maximum length and index of an array. */ + var MAX_ARRAY_LENGTH = 4294967295, + MAX_ARRAY_INDEX = MAX_ARRAY_LENGTH - 1, + HALF_MAX_ARRAY_LENGTH = MAX_ARRAY_LENGTH >>> 1; + + /** + * Used as the [maximum length](http://ecma-international.org/ecma-262/6.0/#sec-number.max_safe_integer) + * of an array-like value. + */ + var MAX_SAFE_INTEGER = 9007199254740991; + + /** Used to store function metadata. */ + var metaMap = WeakMap && new WeakMap; + + /** Used to lookup unminified function names. */ + var realNames = {}; + + /** Used to lookup a type array constructors by `toStringTag`. */ + var ctorByTag = {}; + ctorByTag[float32Tag] = context.Float32Array; + ctorByTag[float64Tag] = context.Float64Array; + ctorByTag[int8Tag] = context.Int8Array; + ctorByTag[int16Tag] = context.Int16Array; + ctorByTag[int32Tag] = context.Int32Array; + ctorByTag[uint8Tag] = Uint8Array; + ctorByTag[uint8ClampedTag] = context.Uint8ClampedArray; + ctorByTag[uint16Tag] = context.Uint16Array; + ctorByTag[uint32Tag] = context.Uint32Array; + + /** Used to avoid iterating over non-enumerable properties in IE < 9. */ + var nonEnumProps = {}; + nonEnumProps[arrayTag] = nonEnumProps[dateTag] = nonEnumProps[numberTag] = { 'constructor': true, 'toLocaleString': true, 'toString': true, 'valueOf': true }; + nonEnumProps[boolTag] = nonEnumProps[stringTag] = { 'constructor': true, 'toString': true, 'valueOf': true }; + nonEnumProps[errorTag] = nonEnumProps[funcTag] = nonEnumProps[regexpTag] = { 'constructor': true, 'toString': true }; + nonEnumProps[objectTag] = { 'constructor': true }; + + arrayEach(shadowProps, function(key) { + for (var tag in nonEnumProps) { + if (hasOwnProperty.call(nonEnumProps, tag)) { + var props = nonEnumProps[tag]; + props[key] = hasOwnProperty.call(props, key); + } + } + }); + + /*------------------------------------------------------------------------*/ + + /** + * Creates a `lodash` object which wraps `value` to enable implicit chaining. + * Methods that operate on and return arrays, collections, and functions can + * be chained together. Methods that retrieve a single value or may return a + * primitive value will automatically end the chain returning the unwrapped + * value. Explicit chaining may be enabled using `_.chain`. The execution of + * chained methods is lazy, that is, execution is deferred until `_#value` + * is implicitly or explicitly called. + * + * Lazy evaluation allows several methods to support shortcut fusion. Shortcut + * fusion is an optimization strategy which merge iteratee calls; this can help + * to avoid the creation of intermediate data structures and greatly reduce the + * number of iteratee executions. + * + * Chaining is supported in custom builds as long as the `_#value` method is + * directly or indirectly included in the build. + * + * In addition to lodash methods, wrappers have `Array` and `String` methods. + * + * The wrapper `Array` methods are: + * `concat`, `join`, `pop`, `push`, `reverse`, `shift`, `slice`, `sort`, + * `splice`, and `unshift` + * + * The wrapper `String` methods are: + * `replace` and `split` + * + * The wrapper methods that support shortcut fusion are: + * `compact`, `drop`, `dropRight`, `dropRightWhile`, `dropWhile`, `filter`, + * `first`, `initial`, `last`, `map`, `pluck`, `reject`, `rest`, `reverse`, + * `slice`, `take`, `takeRight`, `takeRightWhile`, `takeWhile`, `toArray`, + * and `where` + * + * The chainable wrapper methods are: + * `after`, `ary`, `assign`, `at`, `before`, `bind`, `bindAll`, `bindKey`, + * `callback`, `chain`, `chunk`, `commit`, `compact`, `concat`, `constant`, + * `countBy`, `create`, `curry`, `debounce`, `defaults`, `defaultsDeep`, + * `defer`, `delay`, `difference`, `drop`, `dropRight`, `dropRightWhile`, + * `dropWhile`, `fill`, `filter`, `flatten`, `flattenDeep`, `flow`, `flowRight`, + * `forEach`, `forEachRight`, `forIn`, `forInRight`, `forOwn`, `forOwnRight`, + * `functions`, `groupBy`, `indexBy`, `initial`, `intersection`, `invert`, + * `invoke`, `keys`, `keysIn`, `map`, `mapKeys`, `mapValues`, `matches`, + * `matchesProperty`, `memoize`, `merge`, `method`, `methodOf`, `mixin`, + * `modArgs`, `negate`, `omit`, `once`, `pairs`, `partial`, `partialRight`, + * `partition`, `pick`, `plant`, `pluck`, `property`, `propertyOf`, `pull`, + * `pullAt`, `push`, `range`, `rearg`, `reject`, `remove`, `rest`, `restParam`, + * `reverse`, `set`, `shuffle`, `slice`, `sort`, `sortBy`, `sortByAll`, + * `sortByOrder`, `splice`, `spread`, `take`, `takeRight`, `takeRightWhile`, + * `takeWhile`, `tap`, `throttle`, `thru`, `times`, `toArray`, `toPlainObject`, + * `transform`, `union`, `uniq`, `unshift`, `unzip`, `unzipWith`, `values`, + * `valuesIn`, `where`, `without`, `wrap`, `xor`, `zip`, `zipObject`, `zipWith` + * + * The wrapper methods that are **not** chainable by default are: + * `add`, `attempt`, `camelCase`, `capitalize`, `ceil`, `clone`, `cloneDeep`, + * `deburr`, `endsWith`, `escape`, `escapeRegExp`, `every`, `find`, `findIndex`, + * `findKey`, `findLast`, `findLastIndex`, `findLastKey`, `findWhere`, `first`, + * `floor`, `get`, `gt`, `gte`, `has`, `identity`, `includes`, `indexOf`, + * `inRange`, `isArguments`, `isArray`, `isBoolean`, `isDate`, `isElement`, + * `isEmpty`, `isEqual`, `isError`, `isFinite` `isFunction`, `isMatch`, + * `isNative`, `isNaN`, `isNull`, `isNumber`, `isObject`, `isPlainObject`, + * `isRegExp`, `isString`, `isUndefined`, `isTypedArray`, `join`, `kebabCase`, + * `last`, `lastIndexOf`, `lt`, `lte`, `max`, `min`, `noConflict`, `noop`, + * `now`, `pad`, `padLeft`, `padRight`, `parseInt`, `pop`, `random`, `reduce`, + * `reduceRight`, `repeat`, `result`, `round`, `runInContext`, `shift`, `size`, + * `snakeCase`, `some`, `sortedIndex`, `sortedLastIndex`, `startCase`, + * `startsWith`, `sum`, `template`, `trim`, `trimLeft`, `trimRight`, `trunc`, + * `unescape`, `uniqueId`, `value`, and `words` + * + * The wrapper method `sample` will return a wrapped value when `n` is provided, + * otherwise an unwrapped value is returned. + * + * @name _ + * @constructor + * @category Chain + * @param {*} value The value to wrap in a `lodash` instance. + * @returns {Object} Returns the new `lodash` wrapper instance. + * @example + * + * var wrapped = _([1, 2, 3]); + * + * // returns an unwrapped value + * wrapped.reduce(function(total, n) { + * return total + n; + * }); + * // => 6 + * + * // returns a wrapped value + * var squares = wrapped.map(function(n) { + * return n * n; + * }); + * + * _.isArray(squares); + * // => false + * + * _.isArray(squares.value()); + * // => true + */ + function lodash(value) { + if (isObjectLike(value) && !isArray(value) && !(value instanceof LazyWrapper)) { + if (value instanceof LodashWrapper) { + return value; + } + if (hasOwnProperty.call(value, '__chain__') && hasOwnProperty.call(value, '__wrapped__')) { + return wrapperClone(value); + } + } + return new LodashWrapper(value); + } + + /** + * The function whose prototype all chaining wrappers inherit from. + * + * @private + */ + function baseLodash() { + // No operation performed. + } + + /** + * The base constructor for creating `lodash` wrapper objects. + * + * @private + * @param {*} value The value to wrap. + * @param {boolean} [chainAll] Enable chaining for all wrapper methods. + * @param {Array} [actions=[]] Actions to peform to resolve the unwrapped value. + */ + function LodashWrapper(value, chainAll, actions) { + this.__wrapped__ = value; + this.__actions__ = actions || []; + this.__chain__ = !!chainAll; + } + + /** + * An object environment feature flags. + * + * @static + * @memberOf _ + * @type Object + */ + var support = lodash.support = {}; + + (function(x) { + var Ctor = function() { this.x = x; }, + object = { '0': x, 'length': x }, + props = []; + + Ctor.prototype = { 'valueOf': x, 'y': x }; + for (var key in new Ctor) { props.push(key); } + + /** + * Detect if `name` or `message` properties of `Error.prototype` are + * enumerable by default (IE < 9, Safari < 5.1). + * + * @memberOf _.support + * @type boolean + */ + support.enumErrorProps = propertyIsEnumerable.call(errorProto, 'message') || + propertyIsEnumerable.call(errorProto, 'name'); + + /** + * Detect if `prototype` properties are enumerable by default. + * + * Firefox < 3.6, Opera > 9.50 - Opera < 11.60, and Safari < 5.1 + * (if the prototype or a property on the prototype has been set) + * incorrectly set the `[[Enumerable]]` value of a function's `prototype` + * property to `true`. + * + * @memberOf _.support + * @type boolean + */ + support.enumPrototypes = propertyIsEnumerable.call(Ctor, 'prototype'); + + /** + * Detect if properties shadowing those on `Object.prototype` are non-enumerable. + * + * In IE < 9 an object's own properties, shadowing non-enumerable ones, + * are made non-enumerable as well (a.k.a the JScript `[[DontEnum]]` bug). + * + * @memberOf _.support + * @type boolean + */ + support.nonEnumShadows = !/valueOf/.test(props); + + /** + * Detect if own properties are iterated after inherited properties (IE < 9). + * + * @memberOf _.support + * @type boolean + */ + support.ownLast = props[0] != 'x'; + + /** + * Detect if `Array#shift` and `Array#splice` augment array-like objects + * correctly. + * + * Firefox < 10, compatibility modes of IE 8, and IE < 9 have buggy Array + * `shift()` and `splice()` functions that fail to remove the last element, + * `value[0]`, of array-like objects even though the "length" property is + * set to `0`. The `shift()` method is buggy in compatibility modes of IE 8, + * while `splice()` is buggy regardless of mode in IE < 9. + * + * @memberOf _.support + * @type boolean + */ + support.spliceObjects = (splice.call(object, 0, 1), !object[0]); + + /** + * Detect lack of support for accessing string characters by index. + * + * IE < 8 can't access characters by index. IE 8 can only access characters + * by index on string literals, not string objects. + * + * @memberOf _.support + * @type boolean + */ + support.unindexedChars = ('x'[0] + Object('x')[0]) != 'xx'; + }(1, 0)); + + /** + * By default, the template delimiters used by lodash are like those in + * embedded Ruby (ERB). Change the following template settings to use + * alternative delimiters. + * + * @static + * @memberOf _ + * @type Object + */ + lodash.templateSettings = { + + /** + * Used to detect `data` property values to be HTML-escaped. + * + * @memberOf _.templateSettings + * @type RegExp + */ + 'escape': reEscape, + + /** + * Used to detect code to be evaluated. + * + * @memberOf _.templateSettings + * @type RegExp + */ + 'evaluate': reEvaluate, + + /** + * Used to detect `data` property values to inject. + * + * @memberOf _.templateSettings + * @type RegExp + */ + 'interpolate': reInterpolate, + + /** + * Used to reference the data object in the template text. + * + * @memberOf _.templateSettings + * @type string + */ + 'variable': '', + + /** + * Used to import variables into the compiled template. + * + * @memberOf _.templateSettings + * @type Object + */ + 'imports': { + + /** + * A reference to the `lodash` function. + * + * @memberOf _.templateSettings.imports + * @type Function + */ + '_': lodash + } + }; + + /*------------------------------------------------------------------------*/ + + /** + * Creates a lazy wrapper object which wraps `value` to enable lazy evaluation. + * + * @private + * @param {*} value The value to wrap. + */ + function LazyWrapper(value) { + this.__wrapped__ = value; + this.__actions__ = []; + this.__dir__ = 1; + this.__filtered__ = false; + this.__iteratees__ = []; + this.__takeCount__ = POSITIVE_INFINITY; + this.__views__ = []; + } + + /** + * Creates a clone of the lazy wrapper object. + * + * @private + * @name clone + * @memberOf LazyWrapper + * @returns {Object} Returns the cloned `LazyWrapper` object. + */ + function lazyClone() { + var result = new LazyWrapper(this.__wrapped__); + result.__actions__ = arrayCopy(this.__actions__); + result.__dir__ = this.__dir__; + result.__filtered__ = this.__filtered__; + result.__iteratees__ = arrayCopy(this.__iteratees__); + result.__takeCount__ = this.__takeCount__; + result.__views__ = arrayCopy(this.__views__); + return result; + } + + /** + * Reverses the direction of lazy iteration. + * + * @private + * @name reverse + * @memberOf LazyWrapper + * @returns {Object} Returns the new reversed `LazyWrapper` object. + */ + function lazyReverse() { + if (this.__filtered__) { + var result = new LazyWrapper(this); + result.__dir__ = -1; + result.__filtered__ = true; + } else { + result = this.clone(); + result.__dir__ *= -1; + } + return result; + } + + /** + * Extracts the unwrapped value from its lazy wrapper. + * + * @private + * @name value + * @memberOf LazyWrapper + * @returns {*} Returns the unwrapped value. + */ + function lazyValue() { + var array = this.__wrapped__.value(), + dir = this.__dir__, + isArr = isArray(array), + isRight = dir < 0, + arrLength = isArr ? array.length : 0, + view = getView(0, arrLength, this.__views__), + start = view.start, + end = view.end, + length = end - start, + index = isRight ? end : (start - 1), + iteratees = this.__iteratees__, + iterLength = iteratees.length, + resIndex = 0, + takeCount = nativeMin(length, this.__takeCount__); + + if (!isArr || arrLength < LARGE_ARRAY_SIZE || (arrLength == length && takeCount == length)) { + return baseWrapperValue(array, this.__actions__); + } + var result = []; + + outer: + while (length-- && resIndex < takeCount) { + index += dir; + + var iterIndex = -1, + value = array[index]; + + while (++iterIndex < iterLength) { + var data = iteratees[iterIndex], + iteratee = data.iteratee, + type = data.type, + computed = iteratee(value); + + if (type == LAZY_MAP_FLAG) { + value = computed; + } else if (!computed) { + if (type == LAZY_FILTER_FLAG) { + continue outer; + } else { + break outer; + } + } + } + result[resIndex++] = value; + } + return result; + } + + /*------------------------------------------------------------------------*/ + + /** + * Creates a cache object to store key/value pairs. + * + * @private + * @static + * @name Cache + * @memberOf _.memoize + */ + function MapCache() { + this.__data__ = {}; + } + + /** + * Removes `key` and its value from the cache. + * + * @private + * @name delete + * @memberOf _.memoize.Cache + * @param {string} key The key of the value to remove. + * @returns {boolean} Returns `true` if the entry was removed successfully, else `false`. + */ + function mapDelete(key) { + return this.has(key) && delete this.__data__[key]; + } + + /** + * Gets the cached value for `key`. + * + * @private + * @name get + * @memberOf _.memoize.Cache + * @param {string} key The key of the value to get. + * @returns {*} Returns the cached value. + */ + function mapGet(key) { + return key == '__proto__' ? undefined : this.__data__[key]; + } + + /** + * Checks if a cached value for `key` exists. + * + * @private + * @name has + * @memberOf _.memoize.Cache + * @param {string} key The key of the entry to check. + * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`. + */ + function mapHas(key) { + return key != '__proto__' && hasOwnProperty.call(this.__data__, key); + } + + /** + * Sets `value` to `key` of the cache. + * + * @private + * @name set + * @memberOf _.memoize.Cache + * @param {string} key The key of the value to cache. + * @param {*} value The value to cache. + * @returns {Object} Returns the cache object. + */ + function mapSet(key, value) { + if (key != '__proto__') { + this.__data__[key] = value; + } + return this; + } + + /*------------------------------------------------------------------------*/ + + /** + * + * Creates a cache object to store unique values. + * + * @private + * @param {Array} [values] The values to cache. + */ + function SetCache(values) { + var length = values ? values.length : 0; + + this.data = { 'hash': nativeCreate(null), 'set': new Set }; + while (length--) { + this.push(values[length]); + } + } + + /** + * Checks if `value` is in `cache` mimicking the return signature of + * `_.indexOf` by returning `0` if the value is found, else `-1`. + * + * @private + * @param {Object} cache The cache to search. + * @param {*} value The value to search for. + * @returns {number} Returns `0` if `value` is found, else `-1`. + */ + function cacheIndexOf(cache, value) { + var data = cache.data, + result = (typeof value == 'string' || isObject(value)) ? data.set.has(value) : data.hash[value]; + + return result ? 0 : -1; + } + + /** + * Adds `value` to the cache. + * + * @private + * @name push + * @memberOf SetCache + * @param {*} value The value to cache. + */ + function cachePush(value) { + var data = this.data; + if (typeof value == 'string' || isObject(value)) { + data.set.add(value); + } else { + data.hash[value] = true; + } + } + + /*------------------------------------------------------------------------*/ + + /** + * Creates a new array joining `array` with `other`. + * + * @private + * @param {Array} array The array to join. + * @param {Array} other The other array to join. + * @returns {Array} Returns the new concatenated array. + */ + function arrayConcat(array, other) { + var index = -1, + length = array.length, + othIndex = -1, + othLength = other.length, + result = Array(length + othLength); + + while (++index < length) { + result[index] = array[index]; + } + while (++othIndex < othLength) { + result[index++] = other[othIndex]; + } + return result; + } + + /** + * Copies the values of `source` to `array`. + * + * @private + * @param {Array} source The array to copy values from. + * @param {Array} [array=[]] The array to copy values to. + * @returns {Array} Returns `array`. + */ + function arrayCopy(source, array) { + var index = -1, + length = source.length; + + array || (array = Array(length)); + while (++index < length) { + array[index] = source[index]; + } + return array; + } + + /** + * A specialized version of `_.forEach` for arrays without support for callback + * shorthands and `this` binding. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns `array`. + */ + function arrayEach(array, iteratee) { + var index = -1, + length = array.length; + + while (++index < length) { + if (iteratee(array[index], index, array) === false) { + break; + } + } + return array; + } + + /** + * A specialized version of `_.forEachRight` for arrays without support for + * callback shorthands and `this` binding. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns `array`. + */ + function arrayEachRight(array, iteratee) { + var length = array.length; + + while (length--) { + if (iteratee(array[length], length, array) === false) { + break; + } + } + return array; + } + + /** + * A specialized version of `_.every` for arrays without support for callback + * shorthands and `this` binding. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {boolean} Returns `true` if all elements pass the predicate check, + * else `false`. + */ + function arrayEvery(array, predicate) { + var index = -1, + length = array.length; + + while (++index < length) { + if (!predicate(array[index], index, array)) { + return false; + } + } + return true; + } + + /** + * A specialized version of `baseExtremum` for arrays which invokes `iteratee` + * with one argument: (value). + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {Function} comparator The function used to compare values. + * @param {*} exValue The initial extremum value. + * @returns {*} Returns the extremum value. + */ + function arrayExtremum(array, iteratee, comparator, exValue) { + var index = -1, + length = array.length, + computed = exValue, + result = computed; + + while (++index < length) { + var value = array[index], + current = +iteratee(value); + + if (comparator(current, computed)) { + computed = current; + result = value; + } + } + return result; + } + + /** + * A specialized version of `_.filter` for arrays without support for callback + * shorthands and `this` binding. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {Array} Returns the new filtered array. + */ + function arrayFilter(array, predicate) { + var index = -1, + length = array.length, + resIndex = -1, + result = []; + + while (++index < length) { + var value = array[index]; + if (predicate(value, index, array)) { + result[++resIndex] = value; + } + } + return result; + } + + /** + * A specialized version of `_.map` for arrays without support for callback + * shorthands and `this` binding. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the new mapped array. + */ + function arrayMap(array, iteratee) { + var index = -1, + length = array.length, + result = Array(length); + + while (++index < length) { + result[index] = iteratee(array[index], index, array); + } + return result; + } + + /** + * Appends the elements of `values` to `array`. + * + * @private + * @param {Array} array The array to modify. + * @param {Array} values The values to append. + * @returns {Array} Returns `array`. + */ + function arrayPush(array, values) { + var index = -1, + length = values.length, + offset = array.length; + + while (++index < length) { + array[offset + index] = values[index]; + } + return array; + } + + /** + * A specialized version of `_.reduce` for arrays without support for callback + * shorthands and `this` binding. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @param {boolean} [initFromArray] Specify using the first element of `array` + * as the initial value. + * @returns {*} Returns the accumulated value. + */ + function arrayReduce(array, iteratee, accumulator, initFromArray) { + var index = -1, + length = array.length; + + if (initFromArray && length) { + accumulator = array[++index]; + } + while (++index < length) { + accumulator = iteratee(accumulator, array[index], index, array); + } + return accumulator; + } + + /** + * A specialized version of `_.reduceRight` for arrays without support for + * callback shorthands and `this` binding. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @param {boolean} [initFromArray] Specify using the last element of `array` + * as the initial value. + * @returns {*} Returns the accumulated value. + */ + function arrayReduceRight(array, iteratee, accumulator, initFromArray) { + var length = array.length; + if (initFromArray && length) { + accumulator = array[--length]; + } + while (length--) { + accumulator = iteratee(accumulator, array[length], length, array); + } + return accumulator; + } + + /** + * A specialized version of `_.some` for arrays without support for callback + * shorthands and `this` binding. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {boolean} Returns `true` if any element passes the predicate check, + * else `false`. + */ + function arraySome(array, predicate) { + var index = -1, + length = array.length; + + while (++index < length) { + if (predicate(array[index], index, array)) { + return true; + } + } + return false; + } + + /** + * A specialized version of `_.sum` for arrays without support for callback + * shorthands and `this` binding.. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {number} Returns the sum. + */ + function arraySum(array, iteratee) { + var length = array.length, + result = 0; + + while (length--) { + result += +iteratee(array[length]) || 0; + } + return result; + } + + /** + * Used by `_.defaults` to customize its `_.assign` use. + * + * @private + * @param {*} objectValue The destination object property value. + * @param {*} sourceValue The source object property value. + * @returns {*} Returns the value to assign to the destination object. + */ + function assignDefaults(objectValue, sourceValue) { + return objectValue === undefined ? sourceValue : objectValue; + } + + /** + * Used by `_.template` to customize its `_.assign` use. + * + * **Note:** This function is like `assignDefaults` except that it ignores + * inherited property values when checking if a property is `undefined`. + * + * @private + * @param {*} objectValue The destination object property value. + * @param {*} sourceValue The source object property value. + * @param {string} key The key associated with the object and source values. + * @param {Object} object The destination object. + * @returns {*} Returns the value to assign to the destination object. + */ + function assignOwnDefaults(objectValue, sourceValue, key, object) { + return (objectValue === undefined || !hasOwnProperty.call(object, key)) + ? sourceValue + : objectValue; + } + + /** + * A specialized version of `_.assign` for customizing assigned values without + * support for argument juggling, multiple sources, and `this` binding `customizer` + * functions. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @param {Function} customizer The function to customize assigned values. + * @returns {Object} Returns `object`. + */ + function assignWith(object, source, customizer) { + var index = -1, + props = keys(source), + length = props.length; + + while (++index < length) { + var key = props[index], + value = object[key], + result = customizer(value, source[key], key, object, source); + + if ((result === result ? (result !== value) : (value === value)) || + (value === undefined && !(key in object))) { + object[key] = result; + } + } + return object; + } + + /** + * The base implementation of `_.assign` without support for argument juggling, + * multiple sources, and `customizer` functions. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @returns {Object} Returns `object`. + */ + function baseAssign(object, source) { + return source == null + ? object + : baseCopy(source, keys(source), object); + } + + /** + * The base implementation of `_.at` without support for string collections + * and individual key arguments. + * + * @private + * @param {Array|Object} collection The collection to iterate over. + * @param {number[]|string[]} props The property names or indexes of elements to pick. + * @returns {Array} Returns the new array of picked elements. + */ + function baseAt(collection, props) { + var index = -1, + isNil = collection == null, + isArr = !isNil && isArrayLike(collection), + length = isArr ? collection.length : 0, + propsLength = props.length, + result = Array(propsLength); + + while(++index < propsLength) { + var key = props[index]; + if (isArr) { + result[index] = isIndex(key, length) ? collection[key] : undefined; + } else { + result[index] = isNil ? undefined : collection[key]; + } + } + return result; + } + + /** + * Copies properties of `source` to `object`. + * + * @private + * @param {Object} source The object to copy properties from. + * @param {Array} props The property names to copy. + * @param {Object} [object={}] The object to copy properties to. + * @returns {Object} Returns `object`. + */ + function baseCopy(source, props, object) { + object || (object = {}); + + var index = -1, + length = props.length; + + while (++index < length) { + var key = props[index]; + object[key] = source[key]; + } + return object; + } + + /** + * The base implementation of `_.callback` which supports specifying the + * number of arguments to provide to `func`. + * + * @private + * @param {*} [func=_.identity] The value to convert to a callback. + * @param {*} [thisArg] The `this` binding of `func`. + * @param {number} [argCount] The number of arguments to provide to `func`. + * @returns {Function} Returns the callback. + */ + function baseCallback(func, thisArg, argCount) { + var type = typeof func; + if (type == 'function') { + return thisArg === undefined + ? func + : bindCallback(func, thisArg, argCount); + } + if (func == null) { + return identity; + } + if (type == 'object') { + return baseMatches(func); + } + return thisArg === undefined + ? property(func) + : baseMatchesProperty(func, thisArg); + } + + /** + * The base implementation of `_.clone` without support for argument juggling + * and `this` binding `customizer` functions. + * + * @private + * @param {*} value The value to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @param {Function} [customizer] The function to customize cloning values. + * @param {string} [key] The key of `value`. + * @param {Object} [object] The object `value` belongs to. + * @param {Array} [stackA=[]] Tracks traversed source objects. + * @param {Array} [stackB=[]] Associates clones with source counterparts. + * @returns {*} Returns the cloned value. + */ + function baseClone(value, isDeep, customizer, key, object, stackA, stackB) { + var result; + if (customizer) { + result = object ? customizer(value, key, object) : customizer(value); + } + if (result !== undefined) { + return result; + } + if (!isObject(value)) { + return value; + } + var isArr = isArray(value); + if (isArr) { + result = initCloneArray(value); + if (!isDeep) { + return arrayCopy(value, result); + } + } else { + var tag = objToString.call(value), + isFunc = tag == funcTag; + + if (tag == objectTag || tag == argsTag || (isFunc && !object)) { + if (isHostObject(value)) { + return object ? value : {}; + } + result = initCloneObject(isFunc ? {} : value); + if (!isDeep) { + return baseAssign(result, value); + } + } else { + return cloneableTags[tag] + ? initCloneByTag(value, tag, isDeep) + : (object ? value : {}); + } + } + // Check for circular references and return its corresponding clone. + stackA || (stackA = []); + stackB || (stackB = []); + + var length = stackA.length; + while (length--) { + if (stackA[length] == value) { + return stackB[length]; + } + } + // Add the source value to the stack of traversed objects and associate it with its clone. + stackA.push(value); + stackB.push(result); + + // Recursively populate clone (susceptible to call stack limits). + (isArr ? arrayEach : baseForOwn)(value, function(subValue, key) { + result[key] = baseClone(subValue, isDeep, customizer, key, value, stackA, stackB); + }); + return result; + } + + /** + * The base implementation of `_.create` without support for assigning + * properties to the created object. + * + * @private + * @param {Object} prototype The object to inherit from. + * @returns {Object} Returns the new object. + */ + var baseCreate = (function() { + function object() {} + return function(prototype) { + if (isObject(prototype)) { + object.prototype = prototype; + var result = new object; + object.prototype = undefined; + } + return result || {}; + }; + }()); + + /** + * The base implementation of `_.delay` and `_.defer` which accepts an index + * of where to slice the arguments to provide to `func`. + * + * @private + * @param {Function} func The function to delay. + * @param {number} wait The number of milliseconds to delay invocation. + * @param {Object} args The arguments provide to `func`. + * @returns {number} Returns the timer id. + */ + function baseDelay(func, wait, args) { + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + return setTimeout(function() { func.apply(undefined, args); }, wait); + } + + /** + * The base implementation of `_.difference` which accepts a single array + * of values to exclude. + * + * @private + * @param {Array} array The array to inspect. + * @param {Array} values The values to exclude. + * @returns {Array} Returns the new array of filtered values. + */ + function baseDifference(array, values) { + var length = array ? array.length : 0, + result = []; + + if (!length) { + return result; + } + var index = -1, + indexOf = getIndexOf(), + isCommon = indexOf === baseIndexOf, + cache = (isCommon && values.length >= LARGE_ARRAY_SIZE) ? createCache(values) : null, + valuesLength = values.length; + + if (cache) { + indexOf = cacheIndexOf; + isCommon = false; + values = cache; + } + outer: + while (++index < length) { + var value = array[index]; + + if (isCommon && value === value) { + var valuesIndex = valuesLength; + while (valuesIndex--) { + if (values[valuesIndex] === value) { + continue outer; + } + } + result.push(value); + } + else if (indexOf(values, value, 0) < 0) { + result.push(value); + } + } + return result; + } + + /** + * The base implementation of `_.forEach` without support for callback + * shorthands and `this` binding. + * + * @private + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array|Object|string} Returns `collection`. + */ + var baseEach = createBaseEach(baseForOwn); + + /** + * The base implementation of `_.forEachRight` without support for callback + * shorthands and `this` binding. + * + * @private + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array|Object|string} Returns `collection`. + */ + var baseEachRight = createBaseEach(baseForOwnRight, true); + + /** + * The base implementation of `_.every` without support for callback + * shorthands and `this` binding. + * + * @private + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {boolean} Returns `true` if all elements pass the predicate check, + * else `false` + */ + function baseEvery(collection, predicate) { + var result = true; + baseEach(collection, function(value, index, collection) { + result = !!predicate(value, index, collection); + return result; + }); + return result; + } + + /** + * Gets the extremum value of `collection` invoking `iteratee` for each value + * in `collection` to generate the criterion by which the value is ranked. + * The `iteratee` is invoked with three arguments: (value, index|key, collection). + * + * @private + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {Function} comparator The function used to compare values. + * @param {*} exValue The initial extremum value. + * @returns {*} Returns the extremum value. + */ + function baseExtremum(collection, iteratee, comparator, exValue) { + var computed = exValue, + result = computed; + + baseEach(collection, function(value, index, collection) { + var current = +iteratee(value, index, collection); + if (comparator(current, computed) || (current === exValue && current === result)) { + computed = current; + result = value; + } + }); + return result; + } + + /** + * The base implementation of `_.fill` without an iteratee call guard. + * + * @private + * @param {Array} array The array to fill. + * @param {*} value The value to fill `array` with. + * @param {number} [start=0] The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns `array`. + */ + function baseFill(array, value, start, end) { + var length = array.length; + + start = start == null ? 0 : (+start || 0); + if (start < 0) { + start = -start > length ? 0 : (length + start); + } + end = (end === undefined || end > length) ? length : (+end || 0); + if (end < 0) { + end += length; + } + length = start > end ? 0 : (end >>> 0); + start >>>= 0; + + while (start < length) { + array[start++] = value; + } + return array; + } + + /** + * The base implementation of `_.filter` without support for callback + * shorthands and `this` binding. + * + * @private + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {Array} Returns the new filtered array. + */ + function baseFilter(collection, predicate) { + var result = []; + baseEach(collection, function(value, index, collection) { + if (predicate(value, index, collection)) { + result.push(value); + } + }); + return result; + } + + /** + * The base implementation of `_.find`, `_.findLast`, `_.findKey`, and `_.findLastKey`, + * without support for callback shorthands and `this` binding, which iterates + * over `collection` using the provided `eachFunc`. + * + * @private + * @param {Array|Object|string} collection The collection to search. + * @param {Function} predicate The function invoked per iteration. + * @param {Function} eachFunc The function to iterate over `collection`. + * @param {boolean} [retKey] Specify returning the key of the found element + * instead of the element itself. + * @returns {*} Returns the found element or its key, else `undefined`. + */ + function baseFind(collection, predicate, eachFunc, retKey) { + var result; + eachFunc(collection, function(value, key, collection) { + if (predicate(value, key, collection)) { + result = retKey ? key : value; + return false; + } + }); + return result; + } + + /** + * The base implementation of `_.flatten` with added support for restricting + * flattening and specifying the start index. + * + * @private + * @param {Array} array The array to flatten. + * @param {boolean} [isDeep] Specify a deep flatten. + * @param {boolean} [isStrict] Restrict flattening to arrays-like objects. + * @param {Array} [result=[]] The initial result value. + * @returns {Array} Returns the new flattened array. + */ + function baseFlatten(array, isDeep, isStrict, result) { + result || (result = []); + + var index = -1, + length = array.length; + + while (++index < length) { + var value = array[index]; + if (isObjectLike(value) && isArrayLike(value) && + (isStrict || isArray(value) || isArguments(value))) { + if (isDeep) { + // Recursively flatten arrays (susceptible to call stack limits). + baseFlatten(value, isDeep, isStrict, result); + } else { + arrayPush(result, value); + } + } else if (!isStrict) { + result[result.length] = value; + } + } + return result; + } + + /** + * The base implementation of `baseForIn` and `baseForOwn` which iterates + * over `object` properties returned by `keysFunc` invoking `iteratee` for + * each property. Iteratee functions may exit iteration early by explicitly + * returning `false`. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {Function} keysFunc The function to get the keys of `object`. + * @returns {Object} Returns `object`. + */ + var baseFor = createBaseFor(); + + /** + * This function is like `baseFor` except that it iterates over properties + * in the opposite order. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {Function} keysFunc The function to get the keys of `object`. + * @returns {Object} Returns `object`. + */ + var baseForRight = createBaseFor(true); + + /** + * The base implementation of `_.forIn` without support for callback + * shorthands and `this` binding. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Object} Returns `object`. + */ + function baseForIn(object, iteratee) { + return baseFor(object, iteratee, keysIn); + } + + /** + * The base implementation of `_.forOwn` without support for callback + * shorthands and `this` binding. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Object} Returns `object`. + */ + function baseForOwn(object, iteratee) { + return baseFor(object, iteratee, keys); + } + + /** + * The base implementation of `_.forOwnRight` without support for callback + * shorthands and `this` binding. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Object} Returns `object`. + */ + function baseForOwnRight(object, iteratee) { + return baseForRight(object, iteratee, keys); + } + + /** + * The base implementation of `_.functions` which creates an array of + * `object` function property names filtered from those provided. + * + * @private + * @param {Object} object The object to inspect. + * @param {Array} props The property names to filter. + * @returns {Array} Returns the new array of filtered property names. + */ + function baseFunctions(object, props) { + var index = -1, + length = props.length, + resIndex = -1, + result = []; + + while (++index < length) { + var key = props[index]; + if (isFunction(object[key])) { + result[++resIndex] = key; + } + } + return result; + } + + /** + * The base implementation of `get` without support for string paths + * and default values. + * + * @private + * @param {Object} object The object to query. + * @param {Array} path The path of the property to get. + * @param {string} [pathKey] The key representation of path. + * @returns {*} Returns the resolved value. + */ + function baseGet(object, path, pathKey) { + if (object == null) { + return; + } + object = toObject(object); + if (pathKey !== undefined && pathKey in object) { + path = [pathKey]; + } + var index = 0, + length = path.length; + + while (object != null && index < length) { + object = toObject(object)[path[index++]]; + } + return (index && index == length) ? object : undefined; + } + + /** + * The base implementation of `_.isEqual` without support for `this` binding + * `customizer` functions. + * + * @private + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @param {Function} [customizer] The function to customize comparing values. + * @param {boolean} [isLoose] Specify performing partial comparisons. + * @param {Array} [stackA] Tracks traversed `value` objects. + * @param {Array} [stackB] Tracks traversed `other` objects. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + */ + function baseIsEqual(value, other, customizer, isLoose, stackA, stackB) { + if (value === other) { + return true; + } + if (value == null || other == null || (!isObject(value) && !isObjectLike(other))) { + return value !== value && other !== other; + } + return baseIsEqualDeep(value, other, baseIsEqual, customizer, isLoose, stackA, stackB); + } + + /** + * A specialized version of `baseIsEqual` for arrays and objects which performs + * deep comparisons and tracks traversed objects enabling objects with circular + * references to be compared. + * + * @private + * @param {Object} object The object to compare. + * @param {Object} other The other object to compare. + * @param {Function} equalFunc The function to determine equivalents of values. + * @param {Function} [customizer] The function to customize comparing objects. + * @param {boolean} [isLoose] Specify performing partial comparisons. + * @param {Array} [stackA=[]] Tracks traversed `value` objects. + * @param {Array} [stackB=[]] Tracks traversed `other` objects. + * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. + */ + function baseIsEqualDeep(object, other, equalFunc, customizer, isLoose, stackA, stackB) { + var objIsArr = isArray(object), + othIsArr = isArray(other), + objTag = arrayTag, + othTag = arrayTag; + + if (!objIsArr) { + objTag = objToString.call(object); + if (objTag == argsTag) { + objTag = objectTag; + } else if (objTag != objectTag) { + objIsArr = isTypedArray(object); + } + } + if (!othIsArr) { + othTag = objToString.call(other); + if (othTag == argsTag) { + othTag = objectTag; + } else if (othTag != objectTag) { + othIsArr = isTypedArray(other); + } + } + var objIsObj = objTag == objectTag && !isHostObject(object), + othIsObj = othTag == objectTag && !isHostObject(other), + isSameTag = objTag == othTag; + + if (isSameTag && !(objIsArr || objIsObj)) { + return equalByTag(object, other, objTag); + } + if (!isLoose) { + var objIsWrapped = objIsObj && hasOwnProperty.call(object, '__wrapped__'), + othIsWrapped = othIsObj && hasOwnProperty.call(other, '__wrapped__'); + + if (objIsWrapped || othIsWrapped) { + return equalFunc(objIsWrapped ? object.value() : object, othIsWrapped ? other.value() : other, customizer, isLoose, stackA, stackB); + } + } + if (!isSameTag) { + return false; + } + // Assume cyclic values are equal. + // For more information on detecting circular references see https://es5.github.io/#JO. + stackA || (stackA = []); + stackB || (stackB = []); + + var length = stackA.length; + while (length--) { + if (stackA[length] == object) { + return stackB[length] == other; + } + } + // Add `object` and `other` to the stack of traversed objects. + stackA.push(object); + stackB.push(other); + + var result = (objIsArr ? equalArrays : equalObjects)(object, other, equalFunc, customizer, isLoose, stackA, stackB); + + stackA.pop(); + stackB.pop(); + + return result; + } + + /** + * The base implementation of `_.isMatch` without support for callback + * shorthands and `this` binding. + * + * @private + * @param {Object} object The object to inspect. + * @param {Array} matchData The propery names, values, and compare flags to match. + * @param {Function} [customizer] The function to customize comparing objects. + * @returns {boolean} Returns `true` if `object` is a match, else `false`. + */ + function baseIsMatch(object, matchData, customizer) { + var index = matchData.length, + length = index, + noCustomizer = !customizer; + + if (object == null) { + return !length; + } + object = toObject(object); + while (index--) { + var data = matchData[index]; + if ((noCustomizer && data[2]) + ? data[1] !== object[data[0]] + : !(data[0] in object) + ) { + return false; + } + } + while (++index < length) { + data = matchData[index]; + var key = data[0], + objValue = object[key], + srcValue = data[1]; + + if (noCustomizer && data[2]) { + if (objValue === undefined && !(key in object)) { + return false; + } + } else { + var result = customizer ? customizer(objValue, srcValue, key) : undefined; + if (!(result === undefined ? baseIsEqual(srcValue, objValue, customizer, true) : result)) { + return false; + } + } + } + return true; + } + + /** + * The base implementation of `_.map` without support for callback shorthands + * and `this` binding. + * + * @private + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {Array} Returns the new mapped array. + */ + function baseMap(collection, iteratee) { + var index = -1, + result = isArrayLike(collection) ? Array(collection.length) : []; + + baseEach(collection, function(value, key, collection) { + result[++index] = iteratee(value, key, collection); + }); + return result; + } + + /** + * The base implementation of `_.matches` which does not clone `source`. + * + * @private + * @param {Object} source The object of property values to match. + * @returns {Function} Returns the new function. + */ + function baseMatches(source) { + var matchData = getMatchData(source); + if (matchData.length == 1 && matchData[0][2]) { + var key = matchData[0][0], + value = matchData[0][1]; + + return function(object) { + if (object == null) { + return false; + } + object = toObject(object); + return object[key] === value && (value !== undefined || (key in object)); + }; + } + return function(object) { + return baseIsMatch(object, matchData); + }; + } + + /** + * The base implementation of `_.matchesProperty` which does not clone `srcValue`. + * + * @private + * @param {string} path The path of the property to get. + * @param {*} srcValue The value to compare. + * @returns {Function} Returns the new function. + */ + function baseMatchesProperty(path, srcValue) { + var isArr = isArray(path), + isCommon = isKey(path) && isStrictComparable(srcValue), + pathKey = (path + ''); + + path = toPath(path); + return function(object) { + if (object == null) { + return false; + } + var key = pathKey; + object = toObject(object); + if ((isArr || !isCommon) && !(key in object)) { + object = path.length == 1 ? object : baseGet(object, baseSlice(path, 0, -1)); + if (object == null) { + return false; + } + key = last(path); + object = toObject(object); + } + return object[key] === srcValue + ? (srcValue !== undefined || (key in object)) + : baseIsEqual(srcValue, object[key], undefined, true); + }; + } + + /** + * The base implementation of `_.merge` without support for argument juggling, + * multiple sources, and `this` binding `customizer` functions. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @param {Function} [customizer] The function to customize merged values. + * @param {Array} [stackA=[]] Tracks traversed source objects. + * @param {Array} [stackB=[]] Associates values with source counterparts. + * @returns {Object} Returns `object`. + */ + function baseMerge(object, source, customizer, stackA, stackB) { + if (!isObject(object)) { + return object; + } + var isSrcArr = isArrayLike(source) && (isArray(source) || isTypedArray(source)), + props = isSrcArr ? undefined : keys(source); + + arrayEach(props || source, function(srcValue, key) { + if (props) { + key = srcValue; + srcValue = source[key]; + } + if (isObjectLike(srcValue)) { + stackA || (stackA = []); + stackB || (stackB = []); + baseMergeDeep(object, source, key, baseMerge, customizer, stackA, stackB); + } + else { + var value = object[key], + result = customizer ? customizer(value, srcValue, key, object, source) : undefined, + isCommon = result === undefined; + + if (isCommon) { + result = srcValue; + } + if ((result !== undefined || (isSrcArr && !(key in object))) && + (isCommon || (result === result ? (result !== value) : (value === value)))) { + object[key] = result; + } + } + }); + return object; + } + + /** + * A specialized version of `baseMerge` for arrays and objects which performs + * deep merges and tracks traversed objects enabling objects with circular + * references to be merged. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @param {string} key The key of the value to merge. + * @param {Function} mergeFunc The function to merge values. + * @param {Function} [customizer] The function to customize merged values. + * @param {Array} [stackA=[]] Tracks traversed source objects. + * @param {Array} [stackB=[]] Associates values with source counterparts. + * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. + */ + function baseMergeDeep(object, source, key, mergeFunc, customizer, stackA, stackB) { + var length = stackA.length, + srcValue = source[key]; + + while (length--) { + if (stackA[length] == srcValue) { + object[key] = stackB[length]; + return; + } + } + var value = object[key], + result = customizer ? customizer(value, srcValue, key, object, source) : undefined, + isCommon = result === undefined; + + if (isCommon) { + result = srcValue; + if (isArrayLike(srcValue) && (isArray(srcValue) || isTypedArray(srcValue))) { + result = isArray(value) + ? value + : (isArrayLike(value) ? arrayCopy(value) : []); + } + else if (isPlainObject(srcValue) || isArguments(srcValue)) { + result = isArguments(value) + ? toPlainObject(value) + : (isPlainObject(value) ? value : {}); + } + else { + isCommon = false; + } + } + // Add the source value to the stack of traversed objects and associate + // it with its merged value. + stackA.push(srcValue); + stackB.push(result); + + if (isCommon) { + // Recursively merge objects and arrays (susceptible to call stack limits). + object[key] = mergeFunc(result, srcValue, customizer, stackA, stackB); + } else if (result === result ? (result !== value) : (value === value)) { + object[key] = result; + } + } + + /** + * The base implementation of `_.property` without support for deep paths. + * + * @private + * @param {string} key The key of the property to get. + * @returns {Function} Returns the new function. + */ + function baseProperty(key) { + return function(object) { + return object == null ? undefined : toObject(object)[key]; + }; + } + + /** + * A specialized version of `baseProperty` which supports deep paths. + * + * @private + * @param {Array|string} path The path of the property to get. + * @returns {Function} Returns the new function. + */ + function basePropertyDeep(path) { + var pathKey = (path + ''); + path = toPath(path); + return function(object) { + return baseGet(object, path, pathKey); + }; + } + + /** + * The base implementation of `_.pullAt` without support for individual + * index arguments and capturing the removed elements. + * + * @private + * @param {Array} array The array to modify. + * @param {number[]} indexes The indexes of elements to remove. + * @returns {Array} Returns `array`. + */ + function basePullAt(array, indexes) { + var length = array ? indexes.length : 0; + while (length--) { + var index = indexes[length]; + if (index != previous && isIndex(index)) { + var previous = index; + splice.call(array, index, 1); + } + } + return array; + } + + /** + * The base implementation of `_.random` without support for argument juggling + * and returning floating-point numbers. + * + * @private + * @param {number} min The minimum possible value. + * @param {number} max The maximum possible value. + * @returns {number} Returns the random number. + */ + function baseRandom(min, max) { + return min + nativeFloor(nativeRandom() * (max - min + 1)); + } + + /** + * The base implementation of `_.reduce` and `_.reduceRight` without support + * for callback shorthands and `this` binding, which iterates over `collection` + * using the provided `eachFunc`. + * + * @private + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @param {*} accumulator The initial value. + * @param {boolean} initFromCollection Specify using the first or last element + * of `collection` as the initial value. + * @param {Function} eachFunc The function to iterate over `collection`. + * @returns {*} Returns the accumulated value. + */ + function baseReduce(collection, iteratee, accumulator, initFromCollection, eachFunc) { + eachFunc(collection, function(value, index, collection) { + accumulator = initFromCollection + ? (initFromCollection = false, value) + : iteratee(accumulator, value, index, collection); + }); + return accumulator; + } + + /** + * The base implementation of `setData` without support for hot loop detection. + * + * @private + * @param {Function} func The function to associate metadata with. + * @param {*} data The metadata. + * @returns {Function} Returns `func`. + */ + var baseSetData = !metaMap ? identity : function(func, data) { + metaMap.set(func, data); + return func; + }; + + /** + * The base implementation of `_.slice` without an iteratee call guard. + * + * @private + * @param {Array} array The array to slice. + * @param {number} [start=0] The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns the slice of `array`. + */ + function baseSlice(array, start, end) { + var index = -1, + length = array.length; + + start = start == null ? 0 : (+start || 0); + if (start < 0) { + start = -start > length ? 0 : (length + start); + } + end = (end === undefined || end > length) ? length : (+end || 0); + if (end < 0) { + end += length; + } + length = start > end ? 0 : ((end - start) >>> 0); + start >>>= 0; + + var result = Array(length); + while (++index < length) { + result[index] = array[index + start]; + } + return result; + } + + /** + * The base implementation of `_.some` without support for callback shorthands + * and `this` binding. + * + * @private + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} predicate The function invoked per iteration. + * @returns {boolean} Returns `true` if any element passes the predicate check, + * else `false`. + */ + function baseSome(collection, predicate) { + var result; + + baseEach(collection, function(value, index, collection) { + result = predicate(value, index, collection); + return !result; + }); + return !!result; + } + + /** + * The base implementation of `_.sortBy` which uses `comparer` to define + * the sort order of `array` and replaces criteria objects with their + * corresponding values. + * + * @private + * @param {Array} array The array to sort. + * @param {Function} comparer The function to define sort order. + * @returns {Array} Returns `array`. + */ + function baseSortBy(array, comparer) { + var length = array.length; + + array.sort(comparer); + while (length--) { + array[length] = array[length].value; + } + return array; + } + + /** + * The base implementation of `_.sortByOrder` without param guards. + * + * @private + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function[]|Object[]|string[]} iteratees The iteratees to sort by. + * @param {boolean[]} orders The sort orders of `iteratees`. + * @returns {Array} Returns the new sorted array. + */ + function baseSortByOrder(collection, iteratees, orders) { + var callback = getCallback(), + index = -1; + + iteratees = arrayMap(iteratees, function(iteratee) { return callback(iteratee); }); + + var result = baseMap(collection, function(value) { + var criteria = arrayMap(iteratees, function(iteratee) { return iteratee(value); }); + return { 'criteria': criteria, 'index': ++index, 'value': value }; + }); + + return baseSortBy(result, function(object, other) { + return compareMultiple(object, other, orders); + }); + } + + /** + * The base implementation of `_.sum` without support for callback shorthands + * and `this` binding. + * + * @private + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} iteratee The function invoked per iteration. + * @returns {number} Returns the sum. + */ + function baseSum(collection, iteratee) { + var result = 0; + baseEach(collection, function(value, index, collection) { + result += +iteratee(value, index, collection) || 0; + }); + return result; + } + + /** + * The base implementation of `_.uniq` without support for callback shorthands + * and `this` binding. + * + * @private + * @param {Array} array The array to inspect. + * @param {Function} [iteratee] The function invoked per iteration. + * @returns {Array} Returns the new duplicate free array. + */ + function baseUniq(array, iteratee) { + var index = -1, + indexOf = getIndexOf(), + length = array.length, + isCommon = indexOf === baseIndexOf, + isLarge = isCommon && length >= LARGE_ARRAY_SIZE, + seen = isLarge ? createCache() : null, + result = []; + + if (seen) { + indexOf = cacheIndexOf; + isCommon = false; + } else { + isLarge = false; + seen = iteratee ? [] : result; + } + outer: + while (++index < length) { + var value = array[index], + computed = iteratee ? iteratee(value, index, array) : value; + + if (isCommon && value === value) { + var seenIndex = seen.length; + while (seenIndex--) { + if (seen[seenIndex] === computed) { + continue outer; + } + } + if (iteratee) { + seen.push(computed); + } + result.push(value); + } + else if (indexOf(seen, computed, 0) < 0) { + if (iteratee || isLarge) { + seen.push(computed); + } + result.push(value); + } + } + return result; + } + + /** + * The base implementation of `_.values` and `_.valuesIn` which creates an + * array of `object` property values corresponding to the property names + * of `props`. + * + * @private + * @param {Object} object The object to query. + * @param {Array} props The property names to get values for. + * @returns {Object} Returns the array of property values. + */ + function baseValues(object, props) { + var index = -1, + length = props.length, + result = Array(length); + + while (++index < length) { + result[index] = object[props[index]]; + } + return result; + } + + /** + * The base implementation of `_.dropRightWhile`, `_.dropWhile`, `_.takeRightWhile`, + * and `_.takeWhile` without support for callback shorthands and `this` binding. + * + * @private + * @param {Array} array The array to query. + * @param {Function} predicate The function invoked per iteration. + * @param {boolean} [isDrop] Specify dropping elements instead of taking them. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Array} Returns the slice of `array`. + */ + function baseWhile(array, predicate, isDrop, fromRight) { + var length = array.length, + index = fromRight ? length : -1; + + while ((fromRight ? index-- : ++index < length) && predicate(array[index], index, array)) {} + return isDrop + ? baseSlice(array, (fromRight ? 0 : index), (fromRight ? index + 1 : length)) + : baseSlice(array, (fromRight ? index + 1 : 0), (fromRight ? length : index)); + } + + /** + * The base implementation of `wrapperValue` which returns the result of + * performing a sequence of actions on the unwrapped `value`, where each + * successive action is supplied the return value of the previous. + * + * @private + * @param {*} value The unwrapped value. + * @param {Array} actions Actions to peform to resolve the unwrapped value. + * @returns {*} Returns the resolved value. + */ + function baseWrapperValue(value, actions) { + var result = value; + if (result instanceof LazyWrapper) { + result = result.value(); + } + var index = -1, + length = actions.length; + + while (++index < length) { + var action = actions[index]; + result = action.func.apply(action.thisArg, arrayPush([result], action.args)); + } + return result; + } + + /** + * Performs a binary search of `array` to determine the index at which `value` + * should be inserted into `array` in order to maintain its sort order. + * + * @private + * @param {Array} array The sorted array to inspect. + * @param {*} value The value to evaluate. + * @param {boolean} [retHighest] Specify returning the highest qualified index. + * @returns {number} Returns the index at which `value` should be inserted + * into `array`. + */ + function binaryIndex(array, value, retHighest) { + var low = 0, + high = array ? array.length : low; + + if (typeof value == 'number' && value === value && high <= HALF_MAX_ARRAY_LENGTH) { + while (low < high) { + var mid = (low + high) >>> 1, + computed = array[mid]; + + if ((retHighest ? (computed <= value) : (computed < value)) && computed !== null) { + low = mid + 1; + } else { + high = mid; + } + } + return high; + } + return binaryIndexBy(array, value, identity, retHighest); + } + + /** + * This function is like `binaryIndex` except that it invokes `iteratee` for + * `value` and each element of `array` to compute their sort ranking. The + * iteratee is invoked with one argument; (value). + * + * @private + * @param {Array} array The sorted array to inspect. + * @param {*} value The value to evaluate. + * @param {Function} iteratee The function invoked per iteration. + * @param {boolean} [retHighest] Specify returning the highest qualified index. + * @returns {number} Returns the index at which `value` should be inserted + * into `array`. + */ + function binaryIndexBy(array, value, iteratee, retHighest) { + value = iteratee(value); + + var low = 0, + high = array ? array.length : 0, + valIsNaN = value !== value, + valIsNull = value === null, + valIsUndef = value === undefined; + + while (low < high) { + var mid = nativeFloor((low + high) / 2), + computed = iteratee(array[mid]), + isDef = computed !== undefined, + isReflexive = computed === computed; + + if (valIsNaN) { + var setLow = isReflexive || retHighest; + } else if (valIsNull) { + setLow = isReflexive && isDef && (retHighest || computed != null); + } else if (valIsUndef) { + setLow = isReflexive && (retHighest || isDef); + } else if (computed == null) { + setLow = false; + } else { + setLow = retHighest ? (computed <= value) : (computed < value); + } + if (setLow) { + low = mid + 1; + } else { + high = mid; + } + } + return nativeMin(high, MAX_ARRAY_INDEX); + } + + /** + * A specialized version of `baseCallback` which only supports `this` binding + * and specifying the number of arguments to provide to `func`. + * + * @private + * @param {Function} func The function to bind. + * @param {*} thisArg The `this` binding of `func`. + * @param {number} [argCount] The number of arguments to provide to `func`. + * @returns {Function} Returns the callback. + */ + function bindCallback(func, thisArg, argCount) { + if (typeof func != 'function') { + return identity; + } + if (thisArg === undefined) { + return func; + } + switch (argCount) { + case 1: return function(value) { + return func.call(thisArg, value); + }; + case 3: return function(value, index, collection) { + return func.call(thisArg, value, index, collection); + }; + case 4: return function(accumulator, value, index, collection) { + return func.call(thisArg, accumulator, value, index, collection); + }; + case 5: return function(value, other, key, object, source) { + return func.call(thisArg, value, other, key, object, source); + }; + } + return function() { + return func.apply(thisArg, arguments); + }; + } + + /** + * Creates a clone of the given array buffer. + * + * @private + * @param {ArrayBuffer} buffer The array buffer to clone. + * @returns {ArrayBuffer} Returns the cloned array buffer. + */ + function bufferClone(buffer) { + var result = new ArrayBuffer(buffer.byteLength), + view = new Uint8Array(result); + + view.set(new Uint8Array(buffer)); + return result; + } + + /** + * Creates an array that is the composition of partially applied arguments, + * placeholders, and provided arguments into a single array of arguments. + * + * @private + * @param {Array|Object} args The provided arguments. + * @param {Array} partials The arguments to prepend to those provided. + * @param {Array} holders The `partials` placeholder indexes. + * @returns {Array} Returns the new array of composed arguments. + */ + function composeArgs(args, partials, holders) { + var holdersLength = holders.length, + argsIndex = -1, + argsLength = nativeMax(args.length - holdersLength, 0), + leftIndex = -1, + leftLength = partials.length, + result = Array(leftLength + argsLength); + + while (++leftIndex < leftLength) { + result[leftIndex] = partials[leftIndex]; + } + while (++argsIndex < holdersLength) { + result[holders[argsIndex]] = args[argsIndex]; + } + while (argsLength--) { + result[leftIndex++] = args[argsIndex++]; + } + return result; + } + + /** + * This function is like `composeArgs` except that the arguments composition + * is tailored for `_.partialRight`. + * + * @private + * @param {Array|Object} args The provided arguments. + * @param {Array} partials The arguments to append to those provided. + * @param {Array} holders The `partials` placeholder indexes. + * @returns {Array} Returns the new array of composed arguments. + */ + function composeArgsRight(args, partials, holders) { + var holdersIndex = -1, + holdersLength = holders.length, + argsIndex = -1, + argsLength = nativeMax(args.length - holdersLength, 0), + rightIndex = -1, + rightLength = partials.length, + result = Array(argsLength + rightLength); + + while (++argsIndex < argsLength) { + result[argsIndex] = args[argsIndex]; + } + var offset = argsIndex; + while (++rightIndex < rightLength) { + result[offset + rightIndex] = partials[rightIndex]; + } + while (++holdersIndex < holdersLength) { + result[offset + holders[holdersIndex]] = args[argsIndex++]; + } + return result; + } + + /** + * Creates a `_.countBy`, `_.groupBy`, `_.indexBy`, or `_.partition` function. + * + * @private + * @param {Function} setter The function to set keys and values of the accumulator object. + * @param {Function} [initializer] The function to initialize the accumulator object. + * @returns {Function} Returns the new aggregator function. + */ + function createAggregator(setter, initializer) { + return function(collection, iteratee, thisArg) { + var result = initializer ? initializer() : {}; + iteratee = getCallback(iteratee, thisArg, 3); + + if (isArray(collection)) { + var index = -1, + length = collection.length; + + while (++index < length) { + var value = collection[index]; + setter(result, value, iteratee(value, index, collection), collection); + } + } else { + baseEach(collection, function(value, key, collection) { + setter(result, value, iteratee(value, key, collection), collection); + }); + } + return result; + }; + } + + /** + * Creates a `_.assign`, `_.defaults`, or `_.merge` function. + * + * @private + * @param {Function} assigner The function to assign values. + * @returns {Function} Returns the new assigner function. + */ + function createAssigner(assigner) { + return restParam(function(object, sources) { + var index = -1, + length = object == null ? 0 : sources.length, + customizer = length > 2 ? sources[length - 2] : undefined, + guard = length > 2 ? sources[2] : undefined, + thisArg = length > 1 ? sources[length - 1] : undefined; + + if (typeof customizer == 'function') { + customizer = bindCallback(customizer, thisArg, 5); + length -= 2; + } else { + customizer = typeof thisArg == 'function' ? thisArg : undefined; + length -= (customizer ? 1 : 0); + } + if (guard && isIterateeCall(sources[0], sources[1], guard)) { + customizer = length < 3 ? undefined : customizer; + length = 1; + } + while (++index < length) { + var source = sources[index]; + if (source) { + assigner(object, source, customizer); + } + } + return object; + }); + } + + /** + * Creates a `baseEach` or `baseEachRight` function. + * + * @private + * @param {Function} eachFunc The function to iterate over a collection. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new base function. + */ + function createBaseEach(eachFunc, fromRight) { + return function(collection, iteratee) { + var length = collection ? getLength(collection) : 0; + if (!isLength(length)) { + return eachFunc(collection, iteratee); + } + var index = fromRight ? length : -1, + iterable = toObject(collection); + + while ((fromRight ? index-- : ++index < length)) { + if (iteratee(iterable[index], index, iterable) === false) { + break; + } + } + return collection; + }; + } + + /** + * Creates a base function for `_.forIn` or `_.forInRight`. + * + * @private + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new base function. + */ + function createBaseFor(fromRight) { + return function(object, iteratee, keysFunc) { + var iterable = toObject(object), + props = keysFunc(object), + length = props.length, + index = fromRight ? length : -1; + + while ((fromRight ? index-- : ++index < length)) { + var key = props[index]; + if (iteratee(iterable[key], key, iterable) === false) { + break; + } + } + return object; + }; + } + + /** + * Creates a function that wraps `func` and invokes it with the `this` + * binding of `thisArg`. + * + * @private + * @param {Function} func The function to bind. + * @param {*} [thisArg] The `this` binding of `func`. + * @returns {Function} Returns the new bound function. + */ + function createBindWrapper(func, thisArg) { + var Ctor = createCtorWrapper(func); + + function wrapper() { + var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func; + return fn.apply(thisArg, arguments); + } + return wrapper; + } + + /** + * Creates a `Set` cache object to optimize linear searches of large arrays. + * + * @private + * @param {Array} [values] The values to cache. + * @returns {null|Object} Returns the new cache object if `Set` is supported, else `null`. + */ + function createCache(values) { + return (nativeCreate && Set) ? new SetCache(values) : null; + } + + /** + * Creates a function that produces compound words out of the words in a + * given string. + * + * @private + * @param {Function} callback The function to combine each word. + * @returns {Function} Returns the new compounder function. + */ + function createCompounder(callback) { + return function(string) { + var index = -1, + array = words(deburr(string)), + length = array.length, + result = ''; + + while (++index < length) { + result = callback(result, array[index], index); + } + return result; + }; + } + + /** + * Creates a function that produces an instance of `Ctor` regardless of + * whether it was invoked as part of a `new` expression or by `call` or `apply`. + * + * @private + * @param {Function} Ctor The constructor to wrap. + * @returns {Function} Returns the new wrapped function. + */ + function createCtorWrapper(Ctor) { + return function() { + // Use a `switch` statement to work with class constructors. + // See http://ecma-international.org/ecma-262/6.0/#sec-ecmascript-function-objects-call-thisargument-argumentslist + // for more details. + var args = arguments; + switch (args.length) { + case 0: return new Ctor; + case 1: return new Ctor(args[0]); + case 2: return new Ctor(args[0], args[1]); + case 3: return new Ctor(args[0], args[1], args[2]); + case 4: return new Ctor(args[0], args[1], args[2], args[3]); + case 5: return new Ctor(args[0], args[1], args[2], args[3], args[4]); + case 6: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5]); + case 7: return new Ctor(args[0], args[1], args[2], args[3], args[4], args[5], args[6]); + } + var thisBinding = baseCreate(Ctor.prototype), + result = Ctor.apply(thisBinding, args); + + // Mimic the constructor's `return` behavior. + // See https://es5.github.io/#x13.2.2 for more details. + return isObject(result) ? result : thisBinding; + }; + } + + /** + * Creates a `_.curry` or `_.curryRight` function. + * + * @private + * @param {boolean} flag The curry bit flag. + * @returns {Function} Returns the new curry function. + */ + function createCurry(flag) { + function curryFunc(func, arity, guard) { + if (guard && isIterateeCall(func, arity, guard)) { + arity = undefined; + } + var result = createWrapper(func, flag, undefined, undefined, undefined, undefined, undefined, arity); + result.placeholder = curryFunc.placeholder; + return result; + } + return curryFunc; + } + + /** + * Creates a `_.defaults` or `_.defaultsDeep` function. + * + * @private + * @param {Function} assigner The function to assign values. + * @param {Function} customizer The function to customize assigned values. + * @returns {Function} Returns the new defaults function. + */ + function createDefaults(assigner, customizer) { + return restParam(function(args) { + var object = args[0]; + if (object == null) { + return object; + } + args.push(customizer); + return assigner.apply(undefined, args); + }); + } + + /** + * Creates a `_.max` or `_.min` function. + * + * @private + * @param {Function} comparator The function used to compare values. + * @param {*} exValue The initial extremum value. + * @returns {Function} Returns the new extremum function. + */ + function createExtremum(comparator, exValue) { + return function(collection, iteratee, thisArg) { + if (thisArg && isIterateeCall(collection, iteratee, thisArg)) { + iteratee = undefined; + } + iteratee = getCallback(iteratee, thisArg, 3); + if (iteratee.length == 1) { + collection = isArray(collection) ? collection : toIterable(collection); + var result = arrayExtremum(collection, iteratee, comparator, exValue); + if (!(collection.length && result === exValue)) { + return result; + } + } + return baseExtremum(collection, iteratee, comparator, exValue); + }; + } + + /** + * Creates a `_.find` or `_.findLast` function. + * + * @private + * @param {Function} eachFunc The function to iterate over a collection. + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new find function. + */ + function createFind(eachFunc, fromRight) { + return function(collection, predicate, thisArg) { + predicate = getCallback(predicate, thisArg, 3); + if (isArray(collection)) { + var index = baseFindIndex(collection, predicate, fromRight); + return index > -1 ? collection[index] : undefined; + } + return baseFind(collection, predicate, eachFunc); + }; + } + + /** + * Creates a `_.findIndex` or `_.findLastIndex` function. + * + * @private + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new find function. + */ + function createFindIndex(fromRight) { + return function(array, predicate, thisArg) { + if (!(array && array.length)) { + return -1; + } + predicate = getCallback(predicate, thisArg, 3); + return baseFindIndex(array, predicate, fromRight); + }; + } + + /** + * Creates a `_.findKey` or `_.findLastKey` function. + * + * @private + * @param {Function} objectFunc The function to iterate over an object. + * @returns {Function} Returns the new find function. + */ + function createFindKey(objectFunc) { + return function(object, predicate, thisArg) { + predicate = getCallback(predicate, thisArg, 3); + return baseFind(object, predicate, objectFunc, true); + }; + } + + /** + * Creates a `_.flow` or `_.flowRight` function. + * + * @private + * @param {boolean} [fromRight] Specify iterating from right to left. + * @returns {Function} Returns the new flow function. + */ + function createFlow(fromRight) { + return function() { + var wrapper, + length = arguments.length, + index = fromRight ? length : -1, + leftIndex = 0, + funcs = Array(length); + + while ((fromRight ? index-- : ++index < length)) { + var func = funcs[leftIndex++] = arguments[index]; + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + if (!wrapper && LodashWrapper.prototype.thru && getFuncName(func) == 'wrapper') { + wrapper = new LodashWrapper([], true); + } + } + index = wrapper ? -1 : length; + while (++index < length) { + func = funcs[index]; + + var funcName = getFuncName(func), + data = funcName == 'wrapper' ? getData(func) : undefined; + + if (data && isLaziable(data[0]) && data[1] == (ARY_FLAG | CURRY_FLAG | PARTIAL_FLAG | REARG_FLAG) && !data[4].length && data[9] == 1) { + wrapper = wrapper[getFuncName(data[0])].apply(wrapper, data[3]); + } else { + wrapper = (func.length == 1 && isLaziable(func)) ? wrapper[funcName]() : wrapper.thru(func); + } + } + return function() { + var args = arguments, + value = args[0]; + + if (wrapper && args.length == 1 && isArray(value) && value.length >= LARGE_ARRAY_SIZE) { + return wrapper.plant(value).value(); + } + var index = 0, + result = length ? funcs[index].apply(this, args) : value; + + while (++index < length) { + result = funcs[index].call(this, result); + } + return result; + }; + }; + } + + /** + * Creates a function for `_.forEach` or `_.forEachRight`. + * + * @private + * @param {Function} arrayFunc The function to iterate over an array. + * @param {Function} eachFunc The function to iterate over a collection. + * @returns {Function} Returns the new each function. + */ + function createForEach(arrayFunc, eachFunc) { + return function(collection, iteratee, thisArg) { + return (typeof iteratee == 'function' && thisArg === undefined && isArray(collection)) + ? arrayFunc(collection, iteratee) + : eachFunc(collection, bindCallback(iteratee, thisArg, 3)); + }; + } + + /** + * Creates a function for `_.forIn` or `_.forInRight`. + * + * @private + * @param {Function} objectFunc The function to iterate over an object. + * @returns {Function} Returns the new each function. + */ + function createForIn(objectFunc) { + return function(object, iteratee, thisArg) { + if (typeof iteratee != 'function' || thisArg !== undefined) { + iteratee = bindCallback(iteratee, thisArg, 3); + } + return objectFunc(object, iteratee, keysIn); + }; + } + + /** + * Creates a function for `_.forOwn` or `_.forOwnRight`. + * + * @private + * @param {Function} objectFunc The function to iterate over an object. + * @returns {Function} Returns the new each function. + */ + function createForOwn(objectFunc) { + return function(object, iteratee, thisArg) { + if (typeof iteratee != 'function' || thisArg !== undefined) { + iteratee = bindCallback(iteratee, thisArg, 3); + } + return objectFunc(object, iteratee); + }; + } + + /** + * Creates a function for `_.mapKeys` or `_.mapValues`. + * + * @private + * @param {boolean} [isMapKeys] Specify mapping keys instead of values. + * @returns {Function} Returns the new map function. + */ + function createObjectMapper(isMapKeys) { + return function(object, iteratee, thisArg) { + var result = {}; + iteratee = getCallback(iteratee, thisArg, 3); + + baseForOwn(object, function(value, key, object) { + var mapped = iteratee(value, key, object); + key = isMapKeys ? mapped : key; + value = isMapKeys ? value : mapped; + result[key] = value; + }); + return result; + }; + } + + /** + * Creates a function for `_.padLeft` or `_.padRight`. + * + * @private + * @param {boolean} [fromRight] Specify padding from the right. + * @returns {Function} Returns the new pad function. + */ + function createPadDir(fromRight) { + return function(string, length, chars) { + string = baseToString(string); + return (fromRight ? string : '') + createPadding(string, length, chars) + (fromRight ? '' : string); + }; + } + + /** + * Creates a `_.partial` or `_.partialRight` function. + * + * @private + * @param {boolean} flag The partial bit flag. + * @returns {Function} Returns the new partial function. + */ + function createPartial(flag) { + var partialFunc = restParam(function(func, partials) { + var holders = replaceHolders(partials, partialFunc.placeholder); + return createWrapper(func, flag, undefined, partials, holders); + }); + return partialFunc; + } + + /** + * Creates a function for `_.reduce` or `_.reduceRight`. + * + * @private + * @param {Function} arrayFunc The function to iterate over an array. + * @param {Function} eachFunc The function to iterate over a collection. + * @returns {Function} Returns the new each function. + */ + function createReduce(arrayFunc, eachFunc) { + return function(collection, iteratee, accumulator, thisArg) { + var initFromArray = arguments.length < 3; + return (typeof iteratee == 'function' && thisArg === undefined && isArray(collection)) + ? arrayFunc(collection, iteratee, accumulator, initFromArray) + : baseReduce(collection, getCallback(iteratee, thisArg, 4), accumulator, initFromArray, eachFunc); + }; + } + + /** + * Creates a function that wraps `func` and invokes it with optional `this` + * binding of, partial application, and currying. + * + * @private + * @param {Function|string} func The function or method name to reference. + * @param {number} bitmask The bitmask of flags. See `createWrapper` for more details. + * @param {*} [thisArg] The `this` binding of `func`. + * @param {Array} [partials] The arguments to prepend to those provided to the new function. + * @param {Array} [holders] The `partials` placeholder indexes. + * @param {Array} [partialsRight] The arguments to append to those provided to the new function. + * @param {Array} [holdersRight] The `partialsRight` placeholder indexes. + * @param {Array} [argPos] The argument positions of the new function. + * @param {number} [ary] The arity cap of `func`. + * @param {number} [arity] The arity of `func`. + * @returns {Function} Returns the new wrapped function. + */ + function createHybridWrapper(func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity) { + var isAry = bitmask & ARY_FLAG, + isBind = bitmask & BIND_FLAG, + isBindKey = bitmask & BIND_KEY_FLAG, + isCurry = bitmask & CURRY_FLAG, + isCurryBound = bitmask & CURRY_BOUND_FLAG, + isCurryRight = bitmask & CURRY_RIGHT_FLAG, + Ctor = isBindKey ? undefined : createCtorWrapper(func); + + function wrapper() { + // Avoid `arguments` object use disqualifying optimizations by + // converting it to an array before providing it to other functions. + var length = arguments.length, + index = length, + args = Array(length); + + while (index--) { + args[index] = arguments[index]; + } + if (partials) { + args = composeArgs(args, partials, holders); + } + if (partialsRight) { + args = composeArgsRight(args, partialsRight, holdersRight); + } + if (isCurry || isCurryRight) { + var placeholder = wrapper.placeholder, + argsHolders = replaceHolders(args, placeholder); + + length -= argsHolders.length; + if (length < arity) { + var newArgPos = argPos ? arrayCopy(argPos) : undefined, + newArity = nativeMax(arity - length, 0), + newsHolders = isCurry ? argsHolders : undefined, + newHoldersRight = isCurry ? undefined : argsHolders, + newPartials = isCurry ? args : undefined, + newPartialsRight = isCurry ? undefined : args; + + bitmask |= (isCurry ? PARTIAL_FLAG : PARTIAL_RIGHT_FLAG); + bitmask &= ~(isCurry ? PARTIAL_RIGHT_FLAG : PARTIAL_FLAG); + + if (!isCurryBound) { + bitmask &= ~(BIND_FLAG | BIND_KEY_FLAG); + } + var newData = [func, bitmask, thisArg, newPartials, newsHolders, newPartialsRight, newHoldersRight, newArgPos, ary, newArity], + result = createHybridWrapper.apply(undefined, newData); + + if (isLaziable(func)) { + setData(result, newData); + } + result.placeholder = placeholder; + return result; + } + } + var thisBinding = isBind ? thisArg : this, + fn = isBindKey ? thisBinding[func] : func; + + if (argPos) { + args = reorder(args, argPos); + } + if (isAry && ary < args.length) { + args.length = ary; + } + if (this && this !== root && this instanceof wrapper) { + fn = Ctor || createCtorWrapper(func); + } + return fn.apply(thisBinding, args); + } + return wrapper; + } + + /** + * Creates the padding required for `string` based on the given `length`. + * The `chars` string is truncated if the number of characters exceeds `length`. + * + * @private + * @param {string} string The string to create padding for. + * @param {number} [length=0] The padding length. + * @param {string} [chars=' '] The string used as padding. + * @returns {string} Returns the pad for `string`. + */ + function createPadding(string, length, chars) { + var strLength = string.length; + length = +length; + + if (strLength >= length || !nativeIsFinite(length)) { + return ''; + } + var padLength = length - strLength; + chars = chars == null ? ' ' : (chars + ''); + return repeat(chars, nativeCeil(padLength / chars.length)).slice(0, padLength); + } + + /** + * Creates a function that wraps `func` and invokes it with the optional `this` + * binding of `thisArg` and the `partials` prepended to those provided to + * the wrapper. + * + * @private + * @param {Function} func The function to partially apply arguments to. + * @param {number} bitmask The bitmask of flags. See `createWrapper` for more details. + * @param {*} thisArg The `this` binding of `func`. + * @param {Array} partials The arguments to prepend to those provided to the new function. + * @returns {Function} Returns the new bound function. + */ + function createPartialWrapper(func, bitmask, thisArg, partials) { + var isBind = bitmask & BIND_FLAG, + Ctor = createCtorWrapper(func); + + function wrapper() { + // Avoid `arguments` object use disqualifying optimizations by + // converting it to an array before providing it `func`. + var argsIndex = -1, + argsLength = arguments.length, + leftIndex = -1, + leftLength = partials.length, + args = Array(leftLength + argsLength); + + while (++leftIndex < leftLength) { + args[leftIndex] = partials[leftIndex]; + } + while (argsLength--) { + args[leftIndex++] = arguments[++argsIndex]; + } + var fn = (this && this !== root && this instanceof wrapper) ? Ctor : func; + return fn.apply(isBind ? thisArg : this, args); + } + return wrapper; + } + + /** + * Creates a `_.ceil`, `_.floor`, or `_.round` function. + * + * @private + * @param {string} methodName The name of the `Math` method to use when rounding. + * @returns {Function} Returns the new round function. + */ + function createRound(methodName) { + var func = Math[methodName]; + return function(number, precision) { + precision = precision === undefined ? 0 : (+precision || 0); + if (precision) { + precision = pow(10, precision); + return func(number * precision) / precision; + } + return func(number); + }; + } + + /** + * Creates a `_.sortedIndex` or `_.sortedLastIndex` function. + * + * @private + * @param {boolean} [retHighest] Specify returning the highest qualified index. + * @returns {Function} Returns the new index function. + */ + function createSortedIndex(retHighest) { + return function(array, value, iteratee, thisArg) { + var callback = getCallback(iteratee); + return (iteratee == null && callback === baseCallback) + ? binaryIndex(array, value, retHighest) + : binaryIndexBy(array, value, callback(iteratee, thisArg, 1), retHighest); + }; + } + + /** + * Creates a function that either curries or invokes `func` with optional + * `this` binding and partially applied arguments. + * + * @private + * @param {Function|string} func The function or method name to reference. + * @param {number} bitmask The bitmask of flags. + * The bitmask may be composed of the following flags: + * 1 - `_.bind` + * 2 - `_.bindKey` + * 4 - `_.curry` or `_.curryRight` of a bound function + * 8 - `_.curry` + * 16 - `_.curryRight` + * 32 - `_.partial` + * 64 - `_.partialRight` + * 128 - `_.rearg` + * 256 - `_.ary` + * @param {*} [thisArg] The `this` binding of `func`. + * @param {Array} [partials] The arguments to be partially applied. + * @param {Array} [holders] The `partials` placeholder indexes. + * @param {Array} [argPos] The argument positions of the new function. + * @param {number} [ary] The arity cap of `func`. + * @param {number} [arity] The arity of `func`. + * @returns {Function} Returns the new wrapped function. + */ + function createWrapper(func, bitmask, thisArg, partials, holders, argPos, ary, arity) { + var isBindKey = bitmask & BIND_KEY_FLAG; + if (!isBindKey && typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + var length = partials ? partials.length : 0; + if (!length) { + bitmask &= ~(PARTIAL_FLAG | PARTIAL_RIGHT_FLAG); + partials = holders = undefined; + } + length -= (holders ? holders.length : 0); + if (bitmask & PARTIAL_RIGHT_FLAG) { + var partialsRight = partials, + holdersRight = holders; + + partials = holders = undefined; + } + var data = isBindKey ? undefined : getData(func), + newData = [func, bitmask, thisArg, partials, holders, partialsRight, holdersRight, argPos, ary, arity]; + + if (data) { + mergeData(newData, data); + bitmask = newData[1]; + arity = newData[9]; + } + newData[9] = arity == null + ? (isBindKey ? 0 : func.length) + : (nativeMax(arity - length, 0) || 0); + + if (bitmask == BIND_FLAG) { + var result = createBindWrapper(newData[0], newData[2]); + } else if ((bitmask == PARTIAL_FLAG || bitmask == (BIND_FLAG | PARTIAL_FLAG)) && !newData[4].length) { + result = createPartialWrapper.apply(undefined, newData); + } else { + result = createHybridWrapper.apply(undefined, newData); + } + var setter = data ? baseSetData : setData; + return setter(result, newData); + } + + /** + * A specialized version of `baseIsEqualDeep` for arrays with support for + * partial deep comparisons. + * + * @private + * @param {Array} array The array to compare. + * @param {Array} other The other array to compare. + * @param {Function} equalFunc The function to determine equivalents of values. + * @param {Function} [customizer] The function to customize comparing arrays. + * @param {boolean} [isLoose] Specify performing partial comparisons. + * @param {Array} [stackA] Tracks traversed `value` objects. + * @param {Array} [stackB] Tracks traversed `other` objects. + * @returns {boolean} Returns `true` if the arrays are equivalent, else `false`. + */ + function equalArrays(array, other, equalFunc, customizer, isLoose, stackA, stackB) { + var index = -1, + arrLength = array.length, + othLength = other.length; + + if (arrLength != othLength && !(isLoose && othLength > arrLength)) { + return false; + } + // Ignore non-index properties. + while (++index < arrLength) { + var arrValue = array[index], + othValue = other[index], + result = customizer ? customizer(isLoose ? othValue : arrValue, isLoose ? arrValue : othValue, index) : undefined; + + if (result !== undefined) { + if (result) { + continue; + } + return false; + } + // Recursively compare arrays (susceptible to call stack limits). + if (isLoose) { + if (!arraySome(other, function(othValue) { + return arrValue === othValue || equalFunc(arrValue, othValue, customizer, isLoose, stackA, stackB); + })) { + return false; + } + } else if (!(arrValue === othValue || equalFunc(arrValue, othValue, customizer, isLoose, stackA, stackB))) { + return false; + } + } + return true; + } + + /** + * A specialized version of `baseIsEqualDeep` for comparing objects of + * the same `toStringTag`. + * + * **Note:** This function only supports comparing values with tags of + * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`. + * + * @private + * @param {Object} object The object to compare. + * @param {Object} other The other object to compare. + * @param {string} tag The `toStringTag` of the objects to compare. + * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. + */ + function equalByTag(object, other, tag) { + switch (tag) { + case boolTag: + case dateTag: + // Coerce dates and booleans to numbers, dates to milliseconds and booleans + // to `1` or `0` treating invalid dates coerced to `NaN` as not equal. + return +object == +other; + + case errorTag: + return object.name == other.name && object.message == other.message; + + case numberTag: + // Treat `NaN` vs. `NaN` as equal. + return (object != +object) + ? other != +other + : object == +other; + + case regexpTag: + case stringTag: + // Coerce regexes to strings and treat strings primitives and string + // objects as equal. See https://es5.github.io/#x15.10.6.4 for more details. + return object == (other + ''); + } + return false; + } + + /** + * A specialized version of `baseIsEqualDeep` for objects with support for + * partial deep comparisons. + * + * @private + * @param {Object} object The object to compare. + * @param {Object} other The other object to compare. + * @param {Function} equalFunc The function to determine equivalents of values. + * @param {Function} [customizer] The function to customize comparing values. + * @param {boolean} [isLoose] Specify performing partial comparisons. + * @param {Array} [stackA] Tracks traversed `value` objects. + * @param {Array} [stackB] Tracks traversed `other` objects. + * @returns {boolean} Returns `true` if the objects are equivalent, else `false`. + */ + function equalObjects(object, other, equalFunc, customizer, isLoose, stackA, stackB) { + var objProps = keys(object), + objLength = objProps.length, + othProps = keys(other), + othLength = othProps.length; + + if (objLength != othLength && !isLoose) { + return false; + } + var index = objLength; + while (index--) { + var key = objProps[index]; + if (!(isLoose ? key in other : hasOwnProperty.call(other, key))) { + return false; + } + } + var skipCtor = isLoose; + while (++index < objLength) { + key = objProps[index]; + var objValue = object[key], + othValue = other[key], + result = customizer ? customizer(isLoose ? othValue : objValue, isLoose? objValue : othValue, key) : undefined; + + // Recursively compare objects (susceptible to call stack limits). + if (!(result === undefined ? equalFunc(objValue, othValue, customizer, isLoose, stackA, stackB) : result)) { + return false; + } + skipCtor || (skipCtor = key == 'constructor'); + } + if (!skipCtor) { + var objCtor = object.constructor, + othCtor = other.constructor; + + // Non `Object` object instances with different constructors are not equal. + if (objCtor != othCtor && + ('constructor' in object && 'constructor' in other) && + !(typeof objCtor == 'function' && objCtor instanceof objCtor && + typeof othCtor == 'function' && othCtor instanceof othCtor)) { + return false; + } + } + return true; + } + + /** + * Gets the appropriate "callback" function. If the `_.callback` method is + * customized this function returns the custom method, otherwise it returns + * the `baseCallback` function. If arguments are provided the chosen function + * is invoked with them and its result is returned. + * + * @private + * @returns {Function} Returns the chosen function or its result. + */ + function getCallback(func, thisArg, argCount) { + var result = lodash.callback || callback; + result = result === callback ? baseCallback : result; + return argCount ? result(func, thisArg, argCount) : result; + } + + /** + * Gets metadata for `func`. + * + * @private + * @param {Function} func The function to query. + * @returns {*} Returns the metadata for `func`. + */ + var getData = !metaMap ? noop : function(func) { + return metaMap.get(func); + }; + + /** + * Gets the name of `func`. + * + * @private + * @param {Function} func The function to query. + * @returns {string} Returns the function name. + */ + function getFuncName(func) { + var result = (func.name + ''), + array = realNames[result], + length = array ? array.length : 0; + + while (length--) { + var data = array[length], + otherFunc = data.func; + if (otherFunc == null || otherFunc == func) { + return data.name; + } + } + return result; + } + + /** + * Gets the appropriate "indexOf" function. If the `_.indexOf` method is + * customized this function returns the custom method, otherwise it returns + * the `baseIndexOf` function. If arguments are provided the chosen function + * is invoked with them and its result is returned. + * + * @private + * @returns {Function|number} Returns the chosen function or its result. + */ + function getIndexOf(collection, target, fromIndex) { + var result = lodash.indexOf || indexOf; + result = result === indexOf ? baseIndexOf : result; + return collection ? result(collection, target, fromIndex) : result; + } + + /** + * Gets the "length" property value of `object`. + * + * **Note:** This function is used to avoid a [JIT bug](https://bugs.webkit.org/show_bug.cgi?id=142792) + * that affects Safari on at least iOS 8.1-8.3 ARM64. + * + * @private + * @param {Object} object The object to query. + * @returns {*} Returns the "length" value. + */ + var getLength = baseProperty('length'); + + /** + * Gets the propery names, values, and compare flags of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the match data of `object`. + */ + function getMatchData(object) { + var result = pairs(object), + length = result.length; + + while (length--) { + result[length][2] = isStrictComparable(result[length][1]); + } + return result; + } + + /** + * Gets the native function at `key` of `object`. + * + * @private + * @param {Object} object The object to query. + * @param {string} key The key of the method to get. + * @returns {*} Returns the function if it's native, else `undefined`. + */ + function getNative(object, key) { + var value = object == null ? undefined : object[key]; + return isNative(value) ? value : undefined; + } + + /** + * Gets the view, applying any `transforms` to the `start` and `end` positions. + * + * @private + * @param {number} start The start of the view. + * @param {number} end The end of the view. + * @param {Array} transforms The transformations to apply to the view. + * @returns {Object} Returns an object containing the `start` and `end` + * positions of the view. + */ + function getView(start, end, transforms) { + var index = -1, + length = transforms.length; + + while (++index < length) { + var data = transforms[index], + size = data.size; + + switch (data.type) { + case 'drop': start += size; break; + case 'dropRight': end -= size; break; + case 'take': end = nativeMin(end, start + size); break; + case 'takeRight': start = nativeMax(start, end - size); break; + } + } + return { 'start': start, 'end': end }; + } + + /** + * Initializes an array clone. + * + * @private + * @param {Array} array The array to clone. + * @returns {Array} Returns the initialized clone. + */ + function initCloneArray(array) { + var length = array.length, + result = new array.constructor(length); + + // Add array properties assigned by `RegExp#exec`. + if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) { + result.index = array.index; + result.input = array.input; + } + return result; + } + + /** + * Initializes an object clone. + * + * @private + * @param {Object} object The object to clone. + * @returns {Object} Returns the initialized clone. + */ + function initCloneObject(object) { + var Ctor = object.constructor; + if (!(typeof Ctor == 'function' && Ctor instanceof Ctor)) { + Ctor = Object; + } + return new Ctor; + } + + /** + * Initializes an object clone based on its `toStringTag`. + * + * **Note:** This function only supports cloning values with tags of + * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`. + * + * @private + * @param {Object} object The object to clone. + * @param {string} tag The `toStringTag` of the object to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @returns {Object} Returns the initialized clone. + */ + function initCloneByTag(object, tag, isDeep) { + var Ctor = object.constructor; + switch (tag) { + case arrayBufferTag: + return bufferClone(object); + + case boolTag: + case dateTag: + return new Ctor(+object); + + case float32Tag: case float64Tag: + case int8Tag: case int16Tag: case int32Tag: + case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag: + // Safari 5 mobile incorrectly has `Object` as the constructor of typed arrays. + if (Ctor instanceof Ctor) { + Ctor = ctorByTag[tag]; + } + var buffer = object.buffer; + return new Ctor(isDeep ? bufferClone(buffer) : buffer, object.byteOffset, object.length); + + case numberTag: + case stringTag: + return new Ctor(object); + + case regexpTag: + var result = new Ctor(object.source, reFlags.exec(object)); + result.lastIndex = object.lastIndex; + } + return result; + } + + /** + * Invokes the method at `path` on `object`. + * + * @private + * @param {Object} object The object to query. + * @param {Array|string} path The path of the method to invoke. + * @param {Array} args The arguments to invoke the method with. + * @returns {*} Returns the result of the invoked method. + */ + function invokePath(object, path, args) { + if (object != null && !isKey(path, object)) { + path = toPath(path); + object = path.length == 1 ? object : baseGet(object, baseSlice(path, 0, -1)); + path = last(path); + } + var func = object == null ? object : object[path]; + return func == null ? undefined : func.apply(object, args); + } + + /** + * Checks if `value` is array-like. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is array-like, else `false`. + */ + function isArrayLike(value) { + return value != null && isLength(getLength(value)); + } + + /** + * Checks if `value` is a valid array-like index. + * + * @private + * @param {*} value The value to check. + * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index. + * @returns {boolean} Returns `true` if `value` is a valid index, else `false`. + */ + function isIndex(value, length) { + value = (typeof value == 'number' || reIsUint.test(value)) ? +value : -1; + length = length == null ? MAX_SAFE_INTEGER : length; + return value > -1 && value % 1 == 0 && value < length; + } + + /** + * Checks if the provided arguments are from an iteratee call. + * + * @private + * @param {*} value The potential iteratee value argument. + * @param {*} index The potential iteratee index or key argument. + * @param {*} object The potential iteratee object argument. + * @returns {boolean} Returns `true` if the arguments are from an iteratee call, else `false`. + */ + function isIterateeCall(value, index, object) { + if (!isObject(object)) { + return false; + } + var type = typeof index; + if (type == 'number' + ? (isArrayLike(object) && isIndex(index, object.length)) + : (type == 'string' && index in object)) { + var other = object[index]; + return value === value ? (value === other) : (other !== other); + } + return false; + } + + /** + * Checks if `value` is a property name and not a property path. + * + * @private + * @param {*} value The value to check. + * @param {Object} [object] The object to query keys on. + * @returns {boolean} Returns `true` if `value` is a property name, else `false`. + */ + function isKey(value, object) { + var type = typeof value; + if ((type == 'string' && reIsPlainProp.test(value)) || type == 'number') { + return true; + } + if (isArray(value)) { + return false; + } + var result = !reIsDeepProp.test(value); + return result || (object != null && value in toObject(object)); + } + + /** + * Checks if `func` has a lazy counterpart. + * + * @private + * @param {Function} func The function to check. + * @returns {boolean} Returns `true` if `func` has a lazy counterpart, else `false`. + */ + function isLaziable(func) { + var funcName = getFuncName(func), + other = lodash[funcName]; + + if (typeof other != 'function' || !(funcName in LazyWrapper.prototype)) { + return false; + } + if (func === other) { + return true; + } + var data = getData(other); + return !!data && func === data[0]; + } + + /** + * Checks if `value` is a valid array-like length. + * + * **Note:** This function is based on [`ToLength`](http://ecma-international.org/ecma-262/6.0/#sec-tolength). + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a valid length, else `false`. + */ + function isLength(value) { + return typeof value == 'number' && value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER; + } + + /** + * Checks if `value` is suitable for strict equality comparisons, i.e. `===`. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` if suitable for strict + * equality comparisons, else `false`. + */ + function isStrictComparable(value) { + return value === value && !isObject(value); + } + + /** + * Merges the function metadata of `source` into `data`. + * + * Merging metadata reduces the number of wrappers required to invoke a function. + * This is possible because methods like `_.bind`, `_.curry`, and `_.partial` + * may be applied regardless of execution order. Methods like `_.ary` and `_.rearg` + * augment function arguments, making the order in which they are executed important, + * preventing the merging of metadata. However, we make an exception for a safe + * common case where curried functions have `_.ary` and or `_.rearg` applied. + * + * @private + * @param {Array} data The destination metadata. + * @param {Array} source The source metadata. + * @returns {Array} Returns `data`. + */ + function mergeData(data, source) { + var bitmask = data[1], + srcBitmask = source[1], + newBitmask = bitmask | srcBitmask, + isCommon = newBitmask < ARY_FLAG; + + var isCombo = + (srcBitmask == ARY_FLAG && bitmask == CURRY_FLAG) || + (srcBitmask == ARY_FLAG && bitmask == REARG_FLAG && data[7].length <= source[8]) || + (srcBitmask == (ARY_FLAG | REARG_FLAG) && bitmask == CURRY_FLAG); + + // Exit early if metadata can't be merged. + if (!(isCommon || isCombo)) { + return data; + } + // Use source `thisArg` if available. + if (srcBitmask & BIND_FLAG) { + data[2] = source[2]; + // Set when currying a bound function. + newBitmask |= (bitmask & BIND_FLAG) ? 0 : CURRY_BOUND_FLAG; + } + // Compose partial arguments. + var value = source[3]; + if (value) { + var partials = data[3]; + data[3] = partials ? composeArgs(partials, value, source[4]) : arrayCopy(value); + data[4] = partials ? replaceHolders(data[3], PLACEHOLDER) : arrayCopy(source[4]); + } + // Compose partial right arguments. + value = source[5]; + if (value) { + partials = data[5]; + data[5] = partials ? composeArgsRight(partials, value, source[6]) : arrayCopy(value); + data[6] = partials ? replaceHolders(data[5], PLACEHOLDER) : arrayCopy(source[6]); + } + // Use source `argPos` if available. + value = source[7]; + if (value) { + data[7] = arrayCopy(value); + } + // Use source `ary` if it's smaller. + if (srcBitmask & ARY_FLAG) { + data[8] = data[8] == null ? source[8] : nativeMin(data[8], source[8]); + } + // Use source `arity` if one is not provided. + if (data[9] == null) { + data[9] = source[9]; + } + // Use source `func` and merge bitmasks. + data[0] = source[0]; + data[1] = newBitmask; + + return data; + } + + /** + * Used by `_.defaultsDeep` to customize its `_.merge` use. + * + * @private + * @param {*} objectValue The destination object property value. + * @param {*} sourceValue The source object property value. + * @returns {*} Returns the value to assign to the destination object. + */ + function mergeDefaults(objectValue, sourceValue) { + return objectValue === undefined ? sourceValue : merge(objectValue, sourceValue, mergeDefaults); + } + + /** + * A specialized version of `_.pick` which picks `object` properties specified + * by `props`. + * + * @private + * @param {Object} object The source object. + * @param {string[]} props The property names to pick. + * @returns {Object} Returns the new object. + */ + function pickByArray(object, props) { + object = toObject(object); + + var index = -1, + length = props.length, + result = {}; + + while (++index < length) { + var key = props[index]; + if (key in object) { + result[key] = object[key]; + } + } + return result; + } + + /** + * A specialized version of `_.pick` which picks `object` properties `predicate` + * returns truthy for. + * + * @private + * @param {Object} object The source object. + * @param {Function} predicate The function invoked per iteration. + * @returns {Object} Returns the new object. + */ + function pickByCallback(object, predicate) { + var result = {}; + baseForIn(object, function(value, key, object) { + if (predicate(value, key, object)) { + result[key] = value; + } + }); + return result; + } + + /** + * Reorder `array` according to the specified indexes where the element at + * the first index is assigned as the first element, the element at + * the second index is assigned as the second element, and so on. + * + * @private + * @param {Array} array The array to reorder. + * @param {Array} indexes The arranged array indexes. + * @returns {Array} Returns `array`. + */ + function reorder(array, indexes) { + var arrLength = array.length, + length = nativeMin(indexes.length, arrLength), + oldArray = arrayCopy(array); + + while (length--) { + var index = indexes[length]; + array[length] = isIndex(index, arrLength) ? oldArray[index] : undefined; + } + return array; + } + + /** + * Sets metadata for `func`. + * + * **Note:** If this function becomes hot, i.e. is invoked a lot in a short + * period of time, it will trip its breaker and transition to an identity function + * to avoid garbage collection pauses in V8. See [V8 issue 2070](https://code.google.com/p/v8/issues/detail?id=2070) + * for more details. + * + * @private + * @param {Function} func The function to associate metadata with. + * @param {*} data The metadata. + * @returns {Function} Returns `func`. + */ + var setData = (function() { + var count = 0, + lastCalled = 0; + + return function(key, value) { + var stamp = now(), + remaining = HOT_SPAN - (stamp - lastCalled); + + lastCalled = stamp; + if (remaining > 0) { + if (++count >= HOT_COUNT) { + return key; + } + } else { + count = 0; + } + return baseSetData(key, value); + }; + }()); + + /** + * A fallback implementation of `Object.keys` which creates an array of the + * own enumerable property names of `object`. + * + * @private + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + */ + function shimKeys(object) { + var props = keysIn(object), + propsLength = props.length, + length = propsLength && object.length; + + var allowIndexes = !!length && isLength(length) && + (isArray(object) || isArguments(object) || isString(object)); + + var index = -1, + result = []; + + while (++index < propsLength) { + var key = props[index]; + if ((allowIndexes && isIndex(key, length)) || hasOwnProperty.call(object, key)) { + result.push(key); + } + } + return result; + } + + /** + * Converts `value` to an array-like object if it's not one. + * + * @private + * @param {*} value The value to process. + * @returns {Array|Object} Returns the array-like object. + */ + function toIterable(value) { + if (value == null) { + return []; + } + if (!isArrayLike(value)) { + return values(value); + } + if (lodash.support.unindexedChars && isString(value)) { + return value.split(''); + } + return isObject(value) ? value : Object(value); + } + + /** + * Converts `value` to an object if it's not one. + * + * @private + * @param {*} value The value to process. + * @returns {Object} Returns the object. + */ + function toObject(value) { + if (lodash.support.unindexedChars && isString(value)) { + var index = -1, + length = value.length, + result = Object(value); + + while (++index < length) { + result[index] = value.charAt(index); + } + return result; + } + return isObject(value) ? value : Object(value); + } + + /** + * Converts `value` to property path array if it's not one. + * + * @private + * @param {*} value The value to process. + * @returns {Array} Returns the property path array. + */ + function toPath(value) { + if (isArray(value)) { + return value; + } + var result = []; + baseToString(value).replace(rePropName, function(match, number, quote, string) { + result.push(quote ? string.replace(reEscapeChar, '$1') : (number || match)); + }); + return result; + } + + /** + * Creates a clone of `wrapper`. + * + * @private + * @param {Object} wrapper The wrapper to clone. + * @returns {Object} Returns the cloned wrapper. + */ + function wrapperClone(wrapper) { + return wrapper instanceof LazyWrapper + ? wrapper.clone() + : new LodashWrapper(wrapper.__wrapped__, wrapper.__chain__, arrayCopy(wrapper.__actions__)); + } + + /*------------------------------------------------------------------------*/ + + /** + * Creates an array of elements split into groups the length of `size`. + * If `collection` can't be split evenly, the final chunk will be the remaining + * elements. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to process. + * @param {number} [size=1] The length of each chunk. + * @param- {Object} [guard] Enables use as a callback for functions like `_.map`. + * @returns {Array} Returns the new array containing chunks. + * @example + * + * _.chunk(['a', 'b', 'c', 'd'], 2); + * // => [['a', 'b'], ['c', 'd']] + * + * _.chunk(['a', 'b', 'c', 'd'], 3); + * // => [['a', 'b', 'c'], ['d']] + */ + function chunk(array, size, guard) { + if (guard ? isIterateeCall(array, size, guard) : size == null) { + size = 1; + } else { + size = nativeMax(nativeFloor(size) || 1, 1); + } + var index = 0, + length = array ? array.length : 0, + resIndex = -1, + result = Array(nativeCeil(length / size)); + + while (index < length) { + result[++resIndex] = baseSlice(array, index, (index += size)); + } + return result; + } + + /** + * Creates an array with all falsey values removed. The values `false`, `null`, + * `0`, `""`, `undefined`, and `NaN` are falsey. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to compact. + * @returns {Array} Returns the new array of filtered values. + * @example + * + * _.compact([0, 1, false, 2, '', 3]); + * // => [1, 2, 3] + */ + function compact(array) { + var index = -1, + length = array ? array.length : 0, + resIndex = -1, + result = []; + + while (++index < length) { + var value = array[index]; + if (value) { + result[++resIndex] = value; + } + } + return result; + } + + /** + * Creates an array of unique `array` values not included in the other + * provided arrays using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) + * for equality comparisons. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to inspect. + * @param {...Array} [values] The arrays of values to exclude. + * @returns {Array} Returns the new array of filtered values. + * @example + * + * _.difference([1, 2, 3], [4, 2]); + * // => [1, 3] + */ + var difference = restParam(function(array, values) { + return (isObjectLike(array) && isArrayLike(array)) + ? baseDifference(array, baseFlatten(values, false, true)) + : []; + }); + + /** + * Creates a slice of `array` with `n` elements dropped from the beginning. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to query. + * @param {number} [n=1] The number of elements to drop. + * @param- {Object} [guard] Enables use as a callback for functions like `_.map`. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.drop([1, 2, 3]); + * // => [2, 3] + * + * _.drop([1, 2, 3], 2); + * // => [3] + * + * _.drop([1, 2, 3], 5); + * // => [] + * + * _.drop([1, 2, 3], 0); + * // => [1, 2, 3] + */ + function drop(array, n, guard) { + var length = array ? array.length : 0; + if (!length) { + return []; + } + if (guard ? isIterateeCall(array, n, guard) : n == null) { + n = 1; + } + return baseSlice(array, n < 0 ? 0 : n); + } + + /** + * Creates a slice of `array` with `n` elements dropped from the end. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to query. + * @param {number} [n=1] The number of elements to drop. + * @param- {Object} [guard] Enables use as a callback for functions like `_.map`. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.dropRight([1, 2, 3]); + * // => [1, 2] + * + * _.dropRight([1, 2, 3], 2); + * // => [1] + * + * _.dropRight([1, 2, 3], 5); + * // => [] + * + * _.dropRight([1, 2, 3], 0); + * // => [1, 2, 3] + */ + function dropRight(array, n, guard) { + var length = array ? array.length : 0; + if (!length) { + return []; + } + if (guard ? isIterateeCall(array, n, guard) : n == null) { + n = 1; + } + n = length - (+n || 0); + return baseSlice(array, 0, n < 0 ? 0 : n); + } + + /** + * Creates a slice of `array` excluding elements dropped from the end. + * Elements are dropped until `predicate` returns falsey. The predicate is + * bound to `thisArg` and invoked with three arguments: (value, index, array). + * + * If a property name is provided for `predicate` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `predicate` the created `_.matches` style + * callback returns `true` for elements that match the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to query. + * @param {Function|Object|string} [predicate=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `predicate`. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.dropRightWhile([1, 2, 3], function(n) { + * return n > 1; + * }); + * // => [1] + * + * var users = [ + * { 'user': 'barney', 'active': true }, + * { 'user': 'fred', 'active': false }, + * { 'user': 'pebbles', 'active': false } + * ]; + * + * // using the `_.matches` callback shorthand + * _.pluck(_.dropRightWhile(users, { 'user': 'pebbles', 'active': false }), 'user'); + * // => ['barney', 'fred'] + * + * // using the `_.matchesProperty` callback shorthand + * _.pluck(_.dropRightWhile(users, 'active', false), 'user'); + * // => ['barney'] + * + * // using the `_.property` callback shorthand + * _.pluck(_.dropRightWhile(users, 'active'), 'user'); + * // => ['barney', 'fred', 'pebbles'] + */ + function dropRightWhile(array, predicate, thisArg) { + return (array && array.length) + ? baseWhile(array, getCallback(predicate, thisArg, 3), true, true) + : []; + } + + /** + * Creates a slice of `array` excluding elements dropped from the beginning. + * Elements are dropped until `predicate` returns falsey. The predicate is + * bound to `thisArg` and invoked with three arguments: (value, index, array). + * + * If a property name is provided for `predicate` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `predicate` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to query. + * @param {Function|Object|string} [predicate=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `predicate`. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.dropWhile([1, 2, 3], function(n) { + * return n < 3; + * }); + * // => [3] + * + * var users = [ + * { 'user': 'barney', 'active': false }, + * { 'user': 'fred', 'active': false }, + * { 'user': 'pebbles', 'active': true } + * ]; + * + * // using the `_.matches` callback shorthand + * _.pluck(_.dropWhile(users, { 'user': 'barney', 'active': false }), 'user'); + * // => ['fred', 'pebbles'] + * + * // using the `_.matchesProperty` callback shorthand + * _.pluck(_.dropWhile(users, 'active', false), 'user'); + * // => ['pebbles'] + * + * // using the `_.property` callback shorthand + * _.pluck(_.dropWhile(users, 'active'), 'user'); + * // => ['barney', 'fred', 'pebbles'] + */ + function dropWhile(array, predicate, thisArg) { + return (array && array.length) + ? baseWhile(array, getCallback(predicate, thisArg, 3), true) + : []; + } + + /** + * Fills elements of `array` with `value` from `start` up to, but not + * including, `end`. + * + * **Note:** This method mutates `array`. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to fill. + * @param {*} value The value to fill `array` with. + * @param {number} [start=0] The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns `array`. + * @example + * + * var array = [1, 2, 3]; + * + * _.fill(array, 'a'); + * console.log(array); + * // => ['a', 'a', 'a'] + * + * _.fill(Array(3), 2); + * // => [2, 2, 2] + * + * _.fill([4, 6, 8], '*', 1, 2); + * // => [4, '*', 8] + */ + function fill(array, value, start, end) { + var length = array ? array.length : 0; + if (!length) { + return []; + } + if (start && typeof start != 'number' && isIterateeCall(array, value, start)) { + start = 0; + end = length; + } + return baseFill(array, value, start, end); + } + + /** + * This method is like `_.find` except that it returns the index of the first + * element `predicate` returns truthy for instead of the element itself. + * + * If a property name is provided for `predicate` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `predicate` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to search. + * @param {Function|Object|string} [predicate=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `predicate`. + * @returns {number} Returns the index of the found element, else `-1`. + * @example + * + * var users = [ + * { 'user': 'barney', 'active': false }, + * { 'user': 'fred', 'active': false }, + * { 'user': 'pebbles', 'active': true } + * ]; + * + * _.findIndex(users, function(chr) { + * return chr.user == 'barney'; + * }); + * // => 0 + * + * // using the `_.matches` callback shorthand + * _.findIndex(users, { 'user': 'fred', 'active': false }); + * // => 1 + * + * // using the `_.matchesProperty` callback shorthand + * _.findIndex(users, 'active', false); + * // => 0 + * + * // using the `_.property` callback shorthand + * _.findIndex(users, 'active'); + * // => 2 + */ + var findIndex = createFindIndex(); + + /** + * This method is like `_.findIndex` except that it iterates over elements + * of `collection` from right to left. + * + * If a property name is provided for `predicate` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `predicate` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to search. + * @param {Function|Object|string} [predicate=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `predicate`. + * @returns {number} Returns the index of the found element, else `-1`. + * @example + * + * var users = [ + * { 'user': 'barney', 'active': true }, + * { 'user': 'fred', 'active': false }, + * { 'user': 'pebbles', 'active': false } + * ]; + * + * _.findLastIndex(users, function(chr) { + * return chr.user == 'pebbles'; + * }); + * // => 2 + * + * // using the `_.matches` callback shorthand + * _.findLastIndex(users, { 'user': 'barney', 'active': true }); + * // => 0 + * + * // using the `_.matchesProperty` callback shorthand + * _.findLastIndex(users, 'active', false); + * // => 2 + * + * // using the `_.property` callback shorthand + * _.findLastIndex(users, 'active'); + * // => 0 + */ + var findLastIndex = createFindIndex(true); + + /** + * Gets the first element of `array`. + * + * @static + * @memberOf _ + * @alias head + * @category Array + * @param {Array} array The array to query. + * @returns {*} Returns the first element of `array`. + * @example + * + * _.first([1, 2, 3]); + * // => 1 + * + * _.first([]); + * // => undefined + */ + function first(array) { + return array ? array[0] : undefined; + } + + /** + * Flattens a nested array. If `isDeep` is `true` the array is recursively + * flattened, otherwise it's only flattened a single level. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to flatten. + * @param {boolean} [isDeep] Specify a deep flatten. + * @param- {Object} [guard] Enables use as a callback for functions like `_.map`. + * @returns {Array} Returns the new flattened array. + * @example + * + * _.flatten([1, [2, 3, [4]]]); + * // => [1, 2, 3, [4]] + * + * // using `isDeep` + * _.flatten([1, [2, 3, [4]]], true); + * // => [1, 2, 3, 4] + */ + function flatten(array, isDeep, guard) { + var length = array ? array.length : 0; + if (guard && isIterateeCall(array, isDeep, guard)) { + isDeep = false; + } + return length ? baseFlatten(array, isDeep) : []; + } + + /** + * Recursively flattens a nested array. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to recursively flatten. + * @returns {Array} Returns the new flattened array. + * @example + * + * _.flattenDeep([1, [2, 3, [4]]]); + * // => [1, 2, 3, 4] + */ + function flattenDeep(array) { + var length = array ? array.length : 0; + return length ? baseFlatten(array, true) : []; + } + + /** + * Gets the index at which the first occurrence of `value` is found in `array` + * using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) + * for equality comparisons. If `fromIndex` is negative, it's used as the offset + * from the end of `array`. If `array` is sorted providing `true` for `fromIndex` + * performs a faster binary search. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to search. + * @param {*} value The value to search for. + * @param {boolean|number} [fromIndex=0] The index to search from or `true` + * to perform a binary search on a sorted array. + * @returns {number} Returns the index of the matched value, else `-1`. + * @example + * + * _.indexOf([1, 2, 1, 2], 2); + * // => 1 + * + * // using `fromIndex` + * _.indexOf([1, 2, 1, 2], 2, 2); + * // => 3 + * + * // performing a binary search + * _.indexOf([1, 1, 2, 2], 2, true); + * // => 2 + */ + function indexOf(array, value, fromIndex) { + var length = array ? array.length : 0; + if (!length) { + return -1; + } + if (typeof fromIndex == 'number') { + fromIndex = fromIndex < 0 ? nativeMax(length + fromIndex, 0) : fromIndex; + } else if (fromIndex) { + var index = binaryIndex(array, value); + if (index < length && + (value === value ? (value === array[index]) : (array[index] !== array[index]))) { + return index; + } + return -1; + } + return baseIndexOf(array, value, fromIndex || 0); + } + + /** + * Gets all but the last element of `array`. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to query. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.initial([1, 2, 3]); + * // => [1, 2] + */ + function initial(array) { + return dropRight(array, 1); + } + + /** + * Creates an array of unique values that are included in all of the provided + * arrays using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) + * for equality comparisons. + * + * @static + * @memberOf _ + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @returns {Array} Returns the new array of shared values. + * @example + * _.intersection([1, 2], [4, 2], [2, 1]); + * // => [2] + */ + var intersection = restParam(function(arrays) { + var othLength = arrays.length, + othIndex = othLength, + caches = Array(length), + indexOf = getIndexOf(), + isCommon = indexOf === baseIndexOf, + result = []; + + while (othIndex--) { + var value = arrays[othIndex] = isArrayLike(value = arrays[othIndex]) ? value : []; + caches[othIndex] = (isCommon && value.length >= 120) ? createCache(othIndex && value) : null; + } + var array = arrays[0], + index = -1, + length = array ? array.length : 0, + seen = caches[0]; + + outer: + while (++index < length) { + value = array[index]; + if ((seen ? cacheIndexOf(seen, value) : indexOf(result, value, 0)) < 0) { + var othIndex = othLength; + while (--othIndex) { + var cache = caches[othIndex]; + if ((cache ? cacheIndexOf(cache, value) : indexOf(arrays[othIndex], value, 0)) < 0) { + continue outer; + } + } + if (seen) { + seen.push(value); + } + result.push(value); + } + } + return result; + }); + + /** + * Gets the last element of `array`. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to query. + * @returns {*} Returns the last element of `array`. + * @example + * + * _.last([1, 2, 3]); + * // => 3 + */ + function last(array) { + var length = array ? array.length : 0; + return length ? array[length - 1] : undefined; + } + + /** + * This method is like `_.indexOf` except that it iterates over elements of + * `array` from right to left. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to search. + * @param {*} value The value to search for. + * @param {boolean|number} [fromIndex=array.length-1] The index to search from + * or `true` to perform a binary search on a sorted array. + * @returns {number} Returns the index of the matched value, else `-1`. + * @example + * + * _.lastIndexOf([1, 2, 1, 2], 2); + * // => 3 + * + * // using `fromIndex` + * _.lastIndexOf([1, 2, 1, 2], 2, 2); + * // => 1 + * + * // performing a binary search + * _.lastIndexOf([1, 1, 2, 2], 2, true); + * // => 3 + */ + function lastIndexOf(array, value, fromIndex) { + var length = array ? array.length : 0; + if (!length) { + return -1; + } + var index = length; + if (typeof fromIndex == 'number') { + index = (fromIndex < 0 ? nativeMax(length + fromIndex, 0) : nativeMin(fromIndex || 0, length - 1)) + 1; + } else if (fromIndex) { + index = binaryIndex(array, value, true) - 1; + var other = array[index]; + if (value === value ? (value === other) : (other !== other)) { + return index; + } + return -1; + } + if (value !== value) { + return indexOfNaN(array, index, true); + } + while (index--) { + if (array[index] === value) { + return index; + } + } + return -1; + } + + /** + * Removes all provided values from `array` using + * [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) + * for equality comparisons. + * + * **Note:** Unlike `_.without`, this method mutates `array`. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to modify. + * @param {...*} [values] The values to remove. + * @returns {Array} Returns `array`. + * @example + * + * var array = [1, 2, 3, 1, 2, 3]; + * + * _.pull(array, 2, 3); + * console.log(array); + * // => [1, 1] + */ + function pull() { + var args = arguments, + array = args[0]; + + if (!(array && array.length)) { + return array; + } + var index = 0, + indexOf = getIndexOf(), + length = args.length; + + while (++index < length) { + var fromIndex = 0, + value = args[index]; + + while ((fromIndex = indexOf(array, value, fromIndex)) > -1) { + splice.call(array, fromIndex, 1); + } + } + return array; + } + + /** + * Removes elements from `array` corresponding to the given indexes and returns + * an array of the removed elements. Indexes may be specified as an array of + * indexes or as individual arguments. + * + * **Note:** Unlike `_.at`, this method mutates `array`. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to modify. + * @param {...(number|number[])} [indexes] The indexes of elements to remove, + * specified as individual indexes or arrays of indexes. + * @returns {Array} Returns the new array of removed elements. + * @example + * + * var array = [5, 10, 15, 20]; + * var evens = _.pullAt(array, 1, 3); + * + * console.log(array); + * // => [5, 15] + * + * console.log(evens); + * // => [10, 20] + */ + var pullAt = restParam(function(array, indexes) { + indexes = baseFlatten(indexes); + + var result = baseAt(array, indexes); + basePullAt(array, indexes.sort(baseCompareAscending)); + return result; + }); + + /** + * Removes all elements from `array` that `predicate` returns truthy for + * and returns an array of the removed elements. The predicate is bound to + * `thisArg` and invoked with three arguments: (value, index, array). + * + * If a property name is provided for `predicate` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `predicate` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * **Note:** Unlike `_.filter`, this method mutates `array`. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to modify. + * @param {Function|Object|string} [predicate=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `predicate`. + * @returns {Array} Returns the new array of removed elements. + * @example + * + * var array = [1, 2, 3, 4]; + * var evens = _.remove(array, function(n) { + * return n % 2 == 0; + * }); + * + * console.log(array); + * // => [1, 3] + * + * console.log(evens); + * // => [2, 4] + */ + function remove(array, predicate, thisArg) { + var result = []; + if (!(array && array.length)) { + return result; + } + var index = -1, + indexes = [], + length = array.length; + + predicate = getCallback(predicate, thisArg, 3); + while (++index < length) { + var value = array[index]; + if (predicate(value, index, array)) { + result.push(value); + indexes.push(index); + } + } + basePullAt(array, indexes); + return result; + } + + /** + * Gets all but the first element of `array`. + * + * @static + * @memberOf _ + * @alias tail + * @category Array + * @param {Array} array The array to query. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.rest([1, 2, 3]); + * // => [2, 3] + */ + function rest(array) { + return drop(array, 1); + } + + /** + * Creates a slice of `array` from `start` up to, but not including, `end`. + * + * **Note:** This method is used instead of `Array#slice` to support node + * lists in IE < 9 and to ensure dense arrays are returned. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to slice. + * @param {number} [start=0] The start position. + * @param {number} [end=array.length] The end position. + * @returns {Array} Returns the slice of `array`. + */ + function slice(array, start, end) { + var length = array ? array.length : 0; + if (!length) { + return []; + } + if (end && typeof end != 'number' && isIterateeCall(array, start, end)) { + start = 0; + end = length; + } + return baseSlice(array, start, end); + } + + /** + * Uses a binary search to determine the lowest index at which `value` should + * be inserted into `array` in order to maintain its sort order. If an iteratee + * function is provided it's invoked for `value` and each element of `array` + * to compute their sort ranking. The iteratee is bound to `thisArg` and + * invoked with one argument; (value). + * + * If a property name is provided for `iteratee` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `iteratee` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The sorted array to inspect. + * @param {*} value The value to evaluate. + * @param {Function|Object|string} [iteratee=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {number} Returns the index at which `value` should be inserted + * into `array`. + * @example + * + * _.sortedIndex([30, 50], 40); + * // => 1 + * + * _.sortedIndex([4, 4, 5, 5], 5); + * // => 2 + * + * var dict = { 'data': { 'thirty': 30, 'forty': 40, 'fifty': 50 } }; + * + * // using an iteratee function + * _.sortedIndex(['thirty', 'fifty'], 'forty', function(word) { + * return this.data[word]; + * }, dict); + * // => 1 + * + * // using the `_.property` callback shorthand + * _.sortedIndex([{ 'x': 30 }, { 'x': 50 }], { 'x': 40 }, 'x'); + * // => 1 + */ + var sortedIndex = createSortedIndex(); + + /** + * This method is like `_.sortedIndex` except that it returns the highest + * index at which `value` should be inserted into `array` in order to + * maintain its sort order. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The sorted array to inspect. + * @param {*} value The value to evaluate. + * @param {Function|Object|string} [iteratee=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {number} Returns the index at which `value` should be inserted + * into `array`. + * @example + * + * _.sortedLastIndex([4, 4, 5, 5], 5); + * // => 4 + */ + var sortedLastIndex = createSortedIndex(true); + + /** + * Creates a slice of `array` with `n` elements taken from the beginning. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to query. + * @param {number} [n=1] The number of elements to take. + * @param- {Object} [guard] Enables use as a callback for functions like `_.map`. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.take([1, 2, 3]); + * // => [1] + * + * _.take([1, 2, 3], 2); + * // => [1, 2] + * + * _.take([1, 2, 3], 5); + * // => [1, 2, 3] + * + * _.take([1, 2, 3], 0); + * // => [] + */ + function take(array, n, guard) { + var length = array ? array.length : 0; + if (!length) { + return []; + } + if (guard ? isIterateeCall(array, n, guard) : n == null) { + n = 1; + } + return baseSlice(array, 0, n < 0 ? 0 : n); + } + + /** + * Creates a slice of `array` with `n` elements taken from the end. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to query. + * @param {number} [n=1] The number of elements to take. + * @param- {Object} [guard] Enables use as a callback for functions like `_.map`. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.takeRight([1, 2, 3]); + * // => [3] + * + * _.takeRight([1, 2, 3], 2); + * // => [2, 3] + * + * _.takeRight([1, 2, 3], 5); + * // => [1, 2, 3] + * + * _.takeRight([1, 2, 3], 0); + * // => [] + */ + function takeRight(array, n, guard) { + var length = array ? array.length : 0; + if (!length) { + return []; + } + if (guard ? isIterateeCall(array, n, guard) : n == null) { + n = 1; + } + n = length - (+n || 0); + return baseSlice(array, n < 0 ? 0 : n); + } + + /** + * Creates a slice of `array` with elements taken from the end. Elements are + * taken until `predicate` returns falsey. The predicate is bound to `thisArg` + * and invoked with three arguments: (value, index, array). + * + * If a property name is provided for `predicate` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `predicate` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to query. + * @param {Function|Object|string} [predicate=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `predicate`. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.takeRightWhile([1, 2, 3], function(n) { + * return n > 1; + * }); + * // => [2, 3] + * + * var users = [ + * { 'user': 'barney', 'active': true }, + * { 'user': 'fred', 'active': false }, + * { 'user': 'pebbles', 'active': false } + * ]; + * + * // using the `_.matches` callback shorthand + * _.pluck(_.takeRightWhile(users, { 'user': 'pebbles', 'active': false }), 'user'); + * // => ['pebbles'] + * + * // using the `_.matchesProperty` callback shorthand + * _.pluck(_.takeRightWhile(users, 'active', false), 'user'); + * // => ['fred', 'pebbles'] + * + * // using the `_.property` callback shorthand + * _.pluck(_.takeRightWhile(users, 'active'), 'user'); + * // => [] + */ + function takeRightWhile(array, predicate, thisArg) { + return (array && array.length) + ? baseWhile(array, getCallback(predicate, thisArg, 3), false, true) + : []; + } + + /** + * Creates a slice of `array` with elements taken from the beginning. Elements + * are taken until `predicate` returns falsey. The predicate is bound to + * `thisArg` and invoked with three arguments: (value, index, array). + * + * If a property name is provided for `predicate` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `predicate` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to query. + * @param {Function|Object|string} [predicate=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `predicate`. + * @returns {Array} Returns the slice of `array`. + * @example + * + * _.takeWhile([1, 2, 3], function(n) { + * return n < 3; + * }); + * // => [1, 2] + * + * var users = [ + * { 'user': 'barney', 'active': false }, + * { 'user': 'fred', 'active': false}, + * { 'user': 'pebbles', 'active': true } + * ]; + * + * // using the `_.matches` callback shorthand + * _.pluck(_.takeWhile(users, { 'user': 'barney', 'active': false }), 'user'); + * // => ['barney'] + * + * // using the `_.matchesProperty` callback shorthand + * _.pluck(_.takeWhile(users, 'active', false), 'user'); + * // => ['barney', 'fred'] + * + * // using the `_.property` callback shorthand + * _.pluck(_.takeWhile(users, 'active'), 'user'); + * // => [] + */ + function takeWhile(array, predicate, thisArg) { + return (array && array.length) + ? baseWhile(array, getCallback(predicate, thisArg, 3)) + : []; + } + + /** + * Creates an array of unique values, in order, from all of the provided arrays + * using [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) + * for equality comparisons. + * + * @static + * @memberOf _ + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @returns {Array} Returns the new array of combined values. + * @example + * + * _.union([1, 2], [4, 2], [2, 1]); + * // => [1, 2, 4] + */ + var union = restParam(function(arrays) { + return baseUniq(baseFlatten(arrays, false, true)); + }); + + /** + * Creates a duplicate-free version of an array, using + * [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) + * for equality comparisons, in which only the first occurence of each element + * is kept. Providing `true` for `isSorted` performs a faster search algorithm + * for sorted arrays. If an iteratee function is provided it's invoked for + * each element in the array to generate the criterion by which uniqueness + * is computed. The `iteratee` is bound to `thisArg` and invoked with three + * arguments: (value, index, array). + * + * If a property name is provided for `iteratee` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `iteratee` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @alias unique + * @category Array + * @param {Array} array The array to inspect. + * @param {boolean} [isSorted] Specify the array is sorted. + * @param {Function|Object|string} [iteratee] The function invoked per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {Array} Returns the new duplicate-value-free array. + * @example + * + * _.uniq([2, 1, 2]); + * // => [2, 1] + * + * // using `isSorted` + * _.uniq([1, 1, 2], true); + * // => [1, 2] + * + * // using an iteratee function + * _.uniq([1, 2.5, 1.5, 2], function(n) { + * return this.floor(n); + * }, Math); + * // => [1, 2.5] + * + * // using the `_.property` callback shorthand + * _.uniq([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x'); + * // => [{ 'x': 1 }, { 'x': 2 }] + */ + function uniq(array, isSorted, iteratee, thisArg) { + var length = array ? array.length : 0; + if (!length) { + return []; + } + if (isSorted != null && typeof isSorted != 'boolean') { + thisArg = iteratee; + iteratee = isIterateeCall(array, isSorted, thisArg) ? undefined : isSorted; + isSorted = false; + } + var callback = getCallback(); + if (!(iteratee == null && callback === baseCallback)) { + iteratee = callback(iteratee, thisArg, 3); + } + return (isSorted && getIndexOf() === baseIndexOf) + ? sortedUniq(array, iteratee) + : baseUniq(array, iteratee); + } + + /** + * This method is like `_.zip` except that it accepts an array of grouped + * elements and creates an array regrouping the elements to their pre-zip + * configuration. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array of grouped elements to process. + * @returns {Array} Returns the new array of regrouped elements. + * @example + * + * var zipped = _.zip(['fred', 'barney'], [30, 40], [true, false]); + * // => [['fred', 30, true], ['barney', 40, false]] + * + * _.unzip(zipped); + * // => [['fred', 'barney'], [30, 40], [true, false]] + */ + function unzip(array) { + if (!(array && array.length)) { + return []; + } + var index = -1, + length = 0; + + array = arrayFilter(array, function(group) { + if (isArrayLike(group)) { + length = nativeMax(group.length, length); + return true; + } + }); + var result = Array(length); + while (++index < length) { + result[index] = arrayMap(array, baseProperty(index)); + } + return result; + } + + /** + * This method is like `_.unzip` except that it accepts an iteratee to specify + * how regrouped values should be combined. The `iteratee` is bound to `thisArg` + * and invoked with four arguments: (accumulator, value, index, group). + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array of grouped elements to process. + * @param {Function} [iteratee] The function to combine regrouped values. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {Array} Returns the new array of regrouped elements. + * @example + * + * var zipped = _.zip([1, 2], [10, 20], [100, 200]); + * // => [[1, 10, 100], [2, 20, 200]] + * + * _.unzipWith(zipped, _.add); + * // => [3, 30, 300] + */ + function unzipWith(array, iteratee, thisArg) { + var length = array ? array.length : 0; + if (!length) { + return []; + } + var result = unzip(array); + if (iteratee == null) { + return result; + } + iteratee = bindCallback(iteratee, thisArg, 4); + return arrayMap(result, function(group) { + return arrayReduce(group, iteratee, undefined, true); + }); + } + + /** + * Creates an array excluding all provided values using + * [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) + * for equality comparisons. + * + * @static + * @memberOf _ + * @category Array + * @param {Array} array The array to filter. + * @param {...*} [values] The values to exclude. + * @returns {Array} Returns the new array of filtered values. + * @example + * + * _.without([1, 2, 1, 3], 1, 2); + * // => [3] + */ + var without = restParam(function(array, values) { + return isArrayLike(array) + ? baseDifference(array, values) + : []; + }); + + /** + * Creates an array of unique values that is the [symmetric difference](https://en.wikipedia.org/wiki/Symmetric_difference) + * of the provided arrays. + * + * @static + * @memberOf _ + * @category Array + * @param {...Array} [arrays] The arrays to inspect. + * @returns {Array} Returns the new array of values. + * @example + * + * _.xor([1, 2], [4, 2]); + * // => [1, 4] + */ + function xor() { + var index = -1, + length = arguments.length; + + while (++index < length) { + var array = arguments[index]; + if (isArrayLike(array)) { + var result = result + ? arrayPush(baseDifference(result, array), baseDifference(array, result)) + : array; + } + } + return result ? baseUniq(result) : []; + } + + /** + * Creates an array of grouped elements, the first of which contains the first + * elements of the given arrays, the second of which contains the second elements + * of the given arrays, and so on. + * + * @static + * @memberOf _ + * @category Array + * @param {...Array} [arrays] The arrays to process. + * @returns {Array} Returns the new array of grouped elements. + * @example + * + * _.zip(['fred', 'barney'], [30, 40], [true, false]); + * // => [['fred', 30, true], ['barney', 40, false]] + */ + var zip = restParam(unzip); + + /** + * The inverse of `_.pairs`; this method returns an object composed from arrays + * of property names and values. Provide either a single two dimensional array, + * e.g. `[[key1, value1], [key2, value2]]` or two arrays, one of property names + * and one of corresponding values. + * + * @static + * @memberOf _ + * @alias object + * @category Array + * @param {Array} props The property names. + * @param {Array} [values=[]] The property values. + * @returns {Object} Returns the new object. + * @example + * + * _.zipObject([['fred', 30], ['barney', 40]]); + * // => { 'fred': 30, 'barney': 40 } + * + * _.zipObject(['fred', 'barney'], [30, 40]); + * // => { 'fred': 30, 'barney': 40 } + */ + function zipObject(props, values) { + var index = -1, + length = props ? props.length : 0, + result = {}; + + if (length && !values && !isArray(props[0])) { + values = []; + } + while (++index < length) { + var key = props[index]; + if (values) { + result[key] = values[index]; + } else if (key) { + result[key[0]] = key[1]; + } + } + return result; + } + + /** + * This method is like `_.zip` except that it accepts an iteratee to specify + * how grouped values should be combined. The `iteratee` is bound to `thisArg` + * and invoked with four arguments: (accumulator, value, index, group). + * + * @static + * @memberOf _ + * @category Array + * @param {...Array} [arrays] The arrays to process. + * @param {Function} [iteratee] The function to combine grouped values. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {Array} Returns the new array of grouped elements. + * @example + * + * _.zipWith([1, 2], [10, 20], [100, 200], _.add); + * // => [111, 222] + */ + var zipWith = restParam(function(arrays) { + var length = arrays.length, + iteratee = length > 2 ? arrays[length - 2] : undefined, + thisArg = length > 1 ? arrays[length - 1] : undefined; + + if (length > 2 && typeof iteratee == 'function') { + length -= 2; + } else { + iteratee = (length > 1 && typeof thisArg == 'function') ? (--length, thisArg) : undefined; + thisArg = undefined; + } + arrays.length = length; + return unzipWith(arrays, iteratee, thisArg); + }); + + /*------------------------------------------------------------------------*/ + + /** + * Creates a `lodash` object that wraps `value` with explicit method + * chaining enabled. + * + * @static + * @memberOf _ + * @category Chain + * @param {*} value The value to wrap. + * @returns {Object} Returns the new `lodash` wrapper instance. + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36 }, + * { 'user': 'fred', 'age': 40 }, + * { 'user': 'pebbles', 'age': 1 } + * ]; + * + * var youngest = _.chain(users) + * .sortBy('age') + * .map(function(chr) { + * return chr.user + ' is ' + chr.age; + * }) + * .first() + * .value(); + * // => 'pebbles is 1' + */ + function chain(value) { + var result = lodash(value); + result.__chain__ = true; + return result; + } + + /** + * This method invokes `interceptor` and returns `value`. The interceptor is + * bound to `thisArg` and invoked with one argument; (value). The purpose of + * this method is to "tap into" a method chain in order to perform operations + * on intermediate results within the chain. + * + * @static + * @memberOf _ + * @category Chain + * @param {*} value The value to provide to `interceptor`. + * @param {Function} interceptor The function to invoke. + * @param {*} [thisArg] The `this` binding of `interceptor`. + * @returns {*} Returns `value`. + * @example + * + * _([1, 2, 3]) + * .tap(function(array) { + * array.pop(); + * }) + * .reverse() + * .value(); + * // => [2, 1] + */ + function tap(value, interceptor, thisArg) { + interceptor.call(thisArg, value); + return value; + } + + /** + * This method is like `_.tap` except that it returns the result of `interceptor`. + * + * @static + * @memberOf _ + * @category Chain + * @param {*} value The value to provide to `interceptor`. + * @param {Function} interceptor The function to invoke. + * @param {*} [thisArg] The `this` binding of `interceptor`. + * @returns {*} Returns the result of `interceptor`. + * @example + * + * _(' abc ') + * .chain() + * .trim() + * .thru(function(value) { + * return [value]; + * }) + * .value(); + * // => ['abc'] + */ + function thru(value, interceptor, thisArg) { + return interceptor.call(thisArg, value); + } + + /** + * Enables explicit method chaining on the wrapper object. + * + * @name chain + * @memberOf _ + * @category Chain + * @returns {Object} Returns the new `lodash` wrapper instance. + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36 }, + * { 'user': 'fred', 'age': 40 } + * ]; + * + * // without explicit chaining + * _(users).first(); + * // => { 'user': 'barney', 'age': 36 } + * + * // with explicit chaining + * _(users).chain() + * .first() + * .pick('user') + * .value(); + * // => { 'user': 'barney' } + */ + function wrapperChain() { + return chain(this); + } + + /** + * Executes the chained sequence and returns the wrapped result. + * + * @name commit + * @memberOf _ + * @category Chain + * @returns {Object} Returns the new `lodash` wrapper instance. + * @example + * + * var array = [1, 2]; + * var wrapped = _(array).push(3); + * + * console.log(array); + * // => [1, 2] + * + * wrapped = wrapped.commit(); + * console.log(array); + * // => [1, 2, 3] + * + * wrapped.last(); + * // => 3 + * + * console.log(array); + * // => [1, 2, 3] + */ + function wrapperCommit() { + return new LodashWrapper(this.value(), this.__chain__); + } + + /** + * Creates a new array joining a wrapped array with any additional arrays + * and/or values. + * + * @name concat + * @memberOf _ + * @category Chain + * @param {...*} [values] The values to concatenate. + * @returns {Array} Returns the new concatenated array. + * @example + * + * var array = [1]; + * var wrapped = _(array).concat(2, [3], [[4]]); + * + * console.log(wrapped.value()); + * // => [1, 2, 3, [4]] + * + * console.log(array); + * // => [1] + */ + var wrapperConcat = restParam(function(values) { + values = baseFlatten(values); + return this.thru(function(array) { + return arrayConcat(isArray(array) ? array : [toObject(array)], values); + }); + }); + + /** + * Creates a clone of the chained sequence planting `value` as the wrapped value. + * + * @name plant + * @memberOf _ + * @category Chain + * @returns {Object} Returns the new `lodash` wrapper instance. + * @example + * + * var array = [1, 2]; + * var wrapped = _(array).map(function(value) { + * return Math.pow(value, 2); + * }); + * + * var other = [3, 4]; + * var otherWrapped = wrapped.plant(other); + * + * otherWrapped.value(); + * // => [9, 16] + * + * wrapped.value(); + * // => [1, 4] + */ + function wrapperPlant(value) { + var result, + parent = this; + + while (parent instanceof baseLodash) { + var clone = wrapperClone(parent); + if (result) { + previous.__wrapped__ = clone; + } else { + result = clone; + } + var previous = clone; + parent = parent.__wrapped__; + } + previous.__wrapped__ = value; + return result; + } + + /** + * Reverses the wrapped array so the first element becomes the last, the + * second element becomes the second to last, and so on. + * + * **Note:** This method mutates the wrapped array. + * + * @name reverse + * @memberOf _ + * @category Chain + * @returns {Object} Returns the new reversed `lodash` wrapper instance. + * @example + * + * var array = [1, 2, 3]; + * + * _(array).reverse().value() + * // => [3, 2, 1] + * + * console.log(array); + * // => [3, 2, 1] + */ + function wrapperReverse() { + var value = this.__wrapped__; + + var interceptor = function(value) { + return value.reverse(); + }; + if (value instanceof LazyWrapper) { + var wrapped = value; + if (this.__actions__.length) { + wrapped = new LazyWrapper(this); + } + wrapped = wrapped.reverse(); + wrapped.__actions__.push({ 'func': thru, 'args': [interceptor], 'thisArg': undefined }); + return new LodashWrapper(wrapped, this.__chain__); + } + return this.thru(interceptor); + } + + /** + * Produces the result of coercing the unwrapped value to a string. + * + * @name toString + * @memberOf _ + * @category Chain + * @returns {string} Returns the coerced string value. + * @example + * + * _([1, 2, 3]).toString(); + * // => '1,2,3' + */ + function wrapperToString() { + return (this.value() + ''); + } + + /** + * Executes the chained sequence to extract the unwrapped value. + * + * @name value + * @memberOf _ + * @alias run, toJSON, valueOf + * @category Chain + * @returns {*} Returns the resolved unwrapped value. + * @example + * + * _([1, 2, 3]).value(); + * // => [1, 2, 3] + */ + function wrapperValue() { + return baseWrapperValue(this.__wrapped__, this.__actions__); + } + + /*------------------------------------------------------------------------*/ + + /** + * Creates an array of elements corresponding to the given keys, or indexes, + * of `collection`. Keys may be specified as individual arguments or as arrays + * of keys. + * + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {...(number|number[]|string|string[])} [props] The property names + * or indexes of elements to pick, specified individually or in arrays. + * @returns {Array} Returns the new array of picked elements. + * @example + * + * _.at(['a', 'b', 'c'], [0, 2]); + * // => ['a', 'c'] + * + * _.at(['barney', 'fred', 'pebbles'], 0, 2); + * // => ['barney', 'pebbles'] + */ + var at = restParam(function(collection, props) { + if (isArrayLike(collection)) { + collection = toIterable(collection); + } + return baseAt(collection, baseFlatten(props)); + }); + + /** + * Creates an object composed of keys generated from the results of running + * each element of `collection` through `iteratee`. The corresponding value + * of each key is the number of times the key was returned by `iteratee`. + * The `iteratee` is bound to `thisArg` and invoked with three arguments: + * (value, index|key, collection). + * + * If a property name is provided for `iteratee` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `iteratee` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [iteratee=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {Object} Returns the composed aggregate object. + * @example + * + * _.countBy([4.3, 6.1, 6.4], function(n) { + * return Math.floor(n); + * }); + * // => { '4': 1, '6': 2 } + * + * _.countBy([4.3, 6.1, 6.4], function(n) { + * return this.floor(n); + * }, Math); + * // => { '4': 1, '6': 2 } + * + * _.countBy(['one', 'two', 'three'], 'length'); + * // => { '3': 2, '5': 1 } + */ + var countBy = createAggregator(function(result, value, key) { + hasOwnProperty.call(result, key) ? ++result[key] : (result[key] = 1); + }); + + /** + * Checks if `predicate` returns truthy for **all** elements of `collection`. + * The predicate is bound to `thisArg` and invoked with three arguments: + * (value, index|key, collection). + * + * If a property name is provided for `predicate` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `predicate` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @alias all + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [predicate=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `predicate`. + * @returns {boolean} Returns `true` if all elements pass the predicate check, + * else `false`. + * @example + * + * _.every([true, 1, null, 'yes'], Boolean); + * // => false + * + * var users = [ + * { 'user': 'barney', 'active': false }, + * { 'user': 'fred', 'active': false } + * ]; + * + * // using the `_.matches` callback shorthand + * _.every(users, { 'user': 'barney', 'active': false }); + * // => false + * + * // using the `_.matchesProperty` callback shorthand + * _.every(users, 'active', false); + * // => true + * + * // using the `_.property` callback shorthand + * _.every(users, 'active'); + * // => false + */ + function every(collection, predicate, thisArg) { + var func = isArray(collection) ? arrayEvery : baseEvery; + if (thisArg && isIterateeCall(collection, predicate, thisArg)) { + predicate = undefined; + } + if (typeof predicate != 'function' || thisArg !== undefined) { + predicate = getCallback(predicate, thisArg, 3); + } + return func(collection, predicate); + } + + /** + * Iterates over elements of `collection`, returning an array of all elements + * `predicate` returns truthy for. The predicate is bound to `thisArg` and + * invoked with three arguments: (value, index|key, collection). + * + * If a property name is provided for `predicate` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `predicate` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @alias select + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [predicate=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `predicate`. + * @returns {Array} Returns the new filtered array. + * @example + * + * _.filter([4, 5, 6], function(n) { + * return n % 2 == 0; + * }); + * // => [4, 6] + * + * var users = [ + * { 'user': 'barney', 'age': 36, 'active': true }, + * { 'user': 'fred', 'age': 40, 'active': false } + * ]; + * + * // using the `_.matches` callback shorthand + * _.pluck(_.filter(users, { 'age': 36, 'active': true }), 'user'); + * // => ['barney'] + * + * // using the `_.matchesProperty` callback shorthand + * _.pluck(_.filter(users, 'active', false), 'user'); + * // => ['fred'] + * + * // using the `_.property` callback shorthand + * _.pluck(_.filter(users, 'active'), 'user'); + * // => ['barney'] + */ + function filter(collection, predicate, thisArg) { + var func = isArray(collection) ? arrayFilter : baseFilter; + predicate = getCallback(predicate, thisArg, 3); + return func(collection, predicate); + } + + /** + * Iterates over elements of `collection`, returning the first element + * `predicate` returns truthy for. The predicate is bound to `thisArg` and + * invoked with three arguments: (value, index|key, collection). + * + * If a property name is provided for `predicate` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `predicate` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @alias detect + * @category Collection + * @param {Array|Object|string} collection The collection to search. + * @param {Function|Object|string} [predicate=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `predicate`. + * @returns {*} Returns the matched element, else `undefined`. + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36, 'active': true }, + * { 'user': 'fred', 'age': 40, 'active': false }, + * { 'user': 'pebbles', 'age': 1, 'active': true } + * ]; + * + * _.result(_.find(users, function(chr) { + * return chr.age < 40; + * }), 'user'); + * // => 'barney' + * + * // using the `_.matches` callback shorthand + * _.result(_.find(users, { 'age': 1, 'active': true }), 'user'); + * // => 'pebbles' + * + * // using the `_.matchesProperty` callback shorthand + * _.result(_.find(users, 'active', false), 'user'); + * // => 'fred' + * + * // using the `_.property` callback shorthand + * _.result(_.find(users, 'active'), 'user'); + * // => 'barney' + */ + var find = createFind(baseEach); + + /** + * This method is like `_.find` except that it iterates over elements of + * `collection` from right to left. + * + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object|string} collection The collection to search. + * @param {Function|Object|string} [predicate=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `predicate`. + * @returns {*} Returns the matched element, else `undefined`. + * @example + * + * _.findLast([1, 2, 3, 4], function(n) { + * return n % 2 == 1; + * }); + * // => 3 + */ + var findLast = createFind(baseEachRight, true); + + /** + * Performs a deep comparison between each element in `collection` and the + * source object, returning the first element that has equivalent property + * values. + * + * **Note:** This method supports comparing arrays, booleans, `Date` objects, + * numbers, `Object` objects, regexes, and strings. Objects are compared by + * their own, not inherited, enumerable properties. For comparing a single + * own or inherited property value see `_.matchesProperty`. + * + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object|string} collection The collection to search. + * @param {Object} source The object of property values to match. + * @returns {*} Returns the matched element, else `undefined`. + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36, 'active': true }, + * { 'user': 'fred', 'age': 40, 'active': false } + * ]; + * + * _.result(_.findWhere(users, { 'age': 36, 'active': true }), 'user'); + * // => 'barney' + * + * _.result(_.findWhere(users, { 'age': 40, 'active': false }), 'user'); + * // => 'fred' + */ + function findWhere(collection, source) { + return find(collection, baseMatches(source)); + } + + /** + * Iterates over elements of `collection` invoking `iteratee` for each element. + * The `iteratee` is bound to `thisArg` and invoked with three arguments: + * (value, index|key, collection). Iteratee functions may exit iteration early + * by explicitly returning `false`. + * + * **Note:** As with other "Collections" methods, objects with a "length" property + * are iterated like arrays. To avoid this behavior `_.forIn` or `_.forOwn` + * may be used for object iteration. + * + * @static + * @memberOf _ + * @alias each + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {Array|Object|string} Returns `collection`. + * @example + * + * _([1, 2]).forEach(function(n) { + * console.log(n); + * }).value(); + * // => logs each value from left to right and returns the array + * + * _.forEach({ 'a': 1, 'b': 2 }, function(n, key) { + * console.log(n, key); + * }); + * // => logs each value-key pair and returns the object (iteration order is not guaranteed) + */ + var forEach = createForEach(arrayEach, baseEach); + + /** + * This method is like `_.forEach` except that it iterates over elements of + * `collection` from right to left. + * + * @static + * @memberOf _ + * @alias eachRight + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {Array|Object|string} Returns `collection`. + * @example + * + * _([1, 2]).forEachRight(function(n) { + * console.log(n); + * }).value(); + * // => logs each value from right to left and returns the array + */ + var forEachRight = createForEach(arrayEachRight, baseEachRight); + + /** + * Creates an object composed of keys generated from the results of running + * each element of `collection` through `iteratee`. The corresponding value + * of each key is an array of the elements responsible for generating the key. + * The `iteratee` is bound to `thisArg` and invoked with three arguments: + * (value, index|key, collection). + * + * If a property name is provided for `iteratee` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `iteratee` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [iteratee=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {Object} Returns the composed aggregate object. + * @example + * + * _.groupBy([4.2, 6.1, 6.4], function(n) { + * return Math.floor(n); + * }); + * // => { '4': [4.2], '6': [6.1, 6.4] } + * + * _.groupBy([4.2, 6.1, 6.4], function(n) { + * return this.floor(n); + * }, Math); + * // => { '4': [4.2], '6': [6.1, 6.4] } + * + * // using the `_.property` callback shorthand + * _.groupBy(['one', 'two', 'three'], 'length'); + * // => { '3': ['one', 'two'], '5': ['three'] } + */ + var groupBy = createAggregator(function(result, value, key) { + if (hasOwnProperty.call(result, key)) { + result[key].push(value); + } else { + result[key] = [value]; + } + }); + + /** + * Checks if `target` is in `collection` using + * [`SameValueZero`](http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero) + * for equality comparisons. If `fromIndex` is negative, it's used as the offset + * from the end of `collection`. + * + * @static + * @memberOf _ + * @alias contains, include + * @category Collection + * @param {Array|Object|string} collection The collection to search. + * @param {*} target The value to search for. + * @param {number} [fromIndex=0] The index to search from. + * @param- {Object} [guard] Enables use as a callback for functions like `_.reduce`. + * @returns {boolean} Returns `true` if a matching element is found, else `false`. + * @example + * + * _.includes([1, 2, 3], 1); + * // => true + * + * _.includes([1, 2, 3], 1, 2); + * // => false + * + * _.includes({ 'user': 'fred', 'age': 40 }, 'fred'); + * // => true + * + * _.includes('pebbles', 'eb'); + * // => true + */ + function includes(collection, target, fromIndex, guard) { + var length = collection ? getLength(collection) : 0; + if (!isLength(length)) { + collection = values(collection); + length = collection.length; + } + if (typeof fromIndex != 'number' || (guard && isIterateeCall(target, fromIndex, guard))) { + fromIndex = 0; + } else { + fromIndex = fromIndex < 0 ? nativeMax(length + fromIndex, 0) : (fromIndex || 0); + } + return (typeof collection == 'string' || !isArray(collection) && isString(collection)) + ? (fromIndex <= length && collection.indexOf(target, fromIndex) > -1) + : (!!length && getIndexOf(collection, target, fromIndex) > -1); + } + + /** + * Creates an object composed of keys generated from the results of running + * each element of `collection` through `iteratee`. The corresponding value + * of each key is the last element responsible for generating the key. The + * iteratee function is bound to `thisArg` and invoked with three arguments: + * (value, index|key, collection). + * + * If a property name is provided for `iteratee` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `iteratee` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [iteratee=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {Object} Returns the composed aggregate object. + * @example + * + * var keyData = [ + * { 'dir': 'left', 'code': 97 }, + * { 'dir': 'right', 'code': 100 } + * ]; + * + * _.indexBy(keyData, 'dir'); + * // => { 'left': { 'dir': 'left', 'code': 97 }, 'right': { 'dir': 'right', 'code': 100 } } + * + * _.indexBy(keyData, function(object) { + * return String.fromCharCode(object.code); + * }); + * // => { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } } + * + * _.indexBy(keyData, function(object) { + * return this.fromCharCode(object.code); + * }, String); + * // => { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } } + */ + var indexBy = createAggregator(function(result, value, key) { + result[key] = value; + }); + + /** + * Invokes the method at `path` of each element in `collection`, returning + * an array of the results of each invoked method. Any additional arguments + * are provided to each invoked method. If `methodName` is a function it's + * invoked for, and `this` bound to, each element in `collection`. + * + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Array|Function|string} path The path of the method to invoke or + * the function invoked per iteration. + * @param {...*} [args] The arguments to invoke the method with. + * @returns {Array} Returns the array of results. + * @example + * + * _.invoke([[5, 1, 7], [3, 2, 1]], 'sort'); + * // => [[1, 5, 7], [1, 2, 3]] + * + * _.invoke([123, 456], String.prototype.split, ''); + * // => [['1', '2', '3'], ['4', '5', '6']] + */ + var invoke = restParam(function(collection, path, args) { + var index = -1, + isFunc = typeof path == 'function', + isProp = isKey(path), + result = isArrayLike(collection) ? Array(collection.length) : []; + + baseEach(collection, function(value) { + var func = isFunc ? path : ((isProp && value != null) ? value[path] : undefined); + result[++index] = func ? func.apply(value, args) : invokePath(value, path, args); + }); + return result; + }); + + /** + * Creates an array of values by running each element in `collection` through + * `iteratee`. The `iteratee` is bound to `thisArg` and invoked with three + * arguments: (value, index|key, collection). + * + * If a property name is provided for `iteratee` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `iteratee` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * Many lodash methods are guarded to work as iteratees for methods like + * `_.every`, `_.filter`, `_.map`, `_.mapValues`, `_.reject`, and `_.some`. + * + * The guarded methods are: + * `ary`, `callback`, `chunk`, `clone`, `create`, `curry`, `curryRight`, + * `drop`, `dropRight`, `every`, `fill`, `flatten`, `invert`, `max`, `min`, + * `parseInt`, `slice`, `sortBy`, `take`, `takeRight`, `template`, `trim`, + * `trimLeft`, `trimRight`, `trunc`, `random`, `range`, `sample`, `some`, + * `sum`, `uniq`, and `words` + * + * @static + * @memberOf _ + * @alias collect + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [iteratee=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {Array} Returns the new mapped array. + * @example + * + * function timesThree(n) { + * return n * 3; + * } + * + * _.map([1, 2], timesThree); + * // => [3, 6] + * + * _.map({ 'a': 1, 'b': 2 }, timesThree); + * // => [3, 6] (iteration order is not guaranteed) + * + * var users = [ + * { 'user': 'barney' }, + * { 'user': 'fred' } + * ]; + * + * // using the `_.property` callback shorthand + * _.map(users, 'user'); + * // => ['barney', 'fred'] + */ + function map(collection, iteratee, thisArg) { + var func = isArray(collection) ? arrayMap : baseMap; + iteratee = getCallback(iteratee, thisArg, 3); + return func(collection, iteratee); + } + + /** + * Creates an array of elements split into two groups, the first of which + * contains elements `predicate` returns truthy for, while the second of which + * contains elements `predicate` returns falsey for. The predicate is bound + * to `thisArg` and invoked with three arguments: (value, index|key, collection). + * + * If a property name is provided for `predicate` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `predicate` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [predicate=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `predicate`. + * @returns {Array} Returns the array of grouped elements. + * @example + * + * _.partition([1, 2, 3], function(n) { + * return n % 2; + * }); + * // => [[1, 3], [2]] + * + * _.partition([1.2, 2.3, 3.4], function(n) { + * return this.floor(n) % 2; + * }, Math); + * // => [[1.2, 3.4], [2.3]] + * + * var users = [ + * { 'user': 'barney', 'age': 36, 'active': false }, + * { 'user': 'fred', 'age': 40, 'active': true }, + * { 'user': 'pebbles', 'age': 1, 'active': false } + * ]; + * + * var mapper = function(array) { + * return _.pluck(array, 'user'); + * }; + * + * // using the `_.matches` callback shorthand + * _.map(_.partition(users, { 'age': 1, 'active': false }), mapper); + * // => [['pebbles'], ['barney', 'fred']] + * + * // using the `_.matchesProperty` callback shorthand + * _.map(_.partition(users, 'active', false), mapper); + * // => [['barney', 'pebbles'], ['fred']] + * + * // using the `_.property` callback shorthand + * _.map(_.partition(users, 'active'), mapper); + * // => [['fred'], ['barney', 'pebbles']] + */ + var partition = createAggregator(function(result, value, key) { + result[key ? 0 : 1].push(value); + }, function() { return [[], []]; }); + + /** + * Gets the property value of `path` from all elements in `collection`. + * + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Array|string} path The path of the property to pluck. + * @returns {Array} Returns the property values. + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36 }, + * { 'user': 'fred', 'age': 40 } + * ]; + * + * _.pluck(users, 'user'); + * // => ['barney', 'fred'] + * + * var userIndex = _.indexBy(users, 'user'); + * _.pluck(userIndex, 'age'); + * // => [36, 40] (iteration order is not guaranteed) + */ + function pluck(collection, path) { + return map(collection, property(path)); + } + + /** + * Reduces `collection` to a value which is the accumulated result of running + * each element in `collection` through `iteratee`, where each successive + * invocation is supplied the return value of the previous. If `accumulator` + * is not provided the first element of `collection` is used as the initial + * value. The `iteratee` is bound to `thisArg` and invoked with four arguments: + * (accumulator, value, index|key, collection). + * + * Many lodash methods are guarded to work as iteratees for methods like + * `_.reduce`, `_.reduceRight`, and `_.transform`. + * + * The guarded methods are: + * `assign`, `defaults`, `defaultsDeep`, `includes`, `merge`, `sortByAll`, + * and `sortByOrder` + * + * @static + * @memberOf _ + * @alias foldl, inject + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {*} Returns the accumulated value. + * @example + * + * _.reduce([1, 2], function(total, n) { + * return total + n; + * }); + * // => 3 + * + * _.reduce({ 'a': 1, 'b': 2 }, function(result, n, key) { + * result[key] = n * 3; + * return result; + * }, {}); + * // => { 'a': 3, 'b': 6 } (iteration order is not guaranteed) + */ + var reduce = createReduce(arrayReduce, baseEach); + + /** + * This method is like `_.reduce` except that it iterates over elements of + * `collection` from right to left. + * + * @static + * @memberOf _ + * @alias foldr + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @param {*} [accumulator] The initial value. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {*} Returns the accumulated value. + * @example + * + * var array = [[0, 1], [2, 3], [4, 5]]; + * + * _.reduceRight(array, function(flattened, other) { + * return flattened.concat(other); + * }, []); + * // => [4, 5, 2, 3, 0, 1] + */ + var reduceRight = createReduce(arrayReduceRight, baseEachRight); + + /** + * The opposite of `_.filter`; this method returns the elements of `collection` + * that `predicate` does **not** return truthy for. + * + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [predicate=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `predicate`. + * @returns {Array} Returns the new filtered array. + * @example + * + * _.reject([1, 2, 3, 4], function(n) { + * return n % 2 == 0; + * }); + * // => [1, 3] + * + * var users = [ + * { 'user': 'barney', 'age': 36, 'active': false }, + * { 'user': 'fred', 'age': 40, 'active': true } + * ]; + * + * // using the `_.matches` callback shorthand + * _.pluck(_.reject(users, { 'age': 40, 'active': true }), 'user'); + * // => ['barney'] + * + * // using the `_.matchesProperty` callback shorthand + * _.pluck(_.reject(users, 'active', false), 'user'); + * // => ['fred'] + * + * // using the `_.property` callback shorthand + * _.pluck(_.reject(users, 'active'), 'user'); + * // => ['barney'] + */ + function reject(collection, predicate, thisArg) { + var func = isArray(collection) ? arrayFilter : baseFilter; + predicate = getCallback(predicate, thisArg, 3); + return func(collection, function(value, index, collection) { + return !predicate(value, index, collection); + }); + } + + /** + * Gets a random element or `n` random elements from a collection. + * + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object|string} collection The collection to sample. + * @param {number} [n] The number of elements to sample. + * @param- {Object} [guard] Enables use as a callback for functions like `_.map`. + * @returns {*} Returns the random sample(s). + * @example + * + * _.sample([1, 2, 3, 4]); + * // => 2 + * + * _.sample([1, 2, 3, 4], 2); + * // => [3, 1] + */ + function sample(collection, n, guard) { + if (guard ? isIterateeCall(collection, n, guard) : n == null) { + collection = toIterable(collection); + var length = collection.length; + return length > 0 ? collection[baseRandom(0, length - 1)] : undefined; + } + var index = -1, + result = toArray(collection), + length = result.length, + lastIndex = length - 1; + + n = nativeMin(n < 0 ? 0 : (+n || 0), length); + while (++index < n) { + var rand = baseRandom(index, lastIndex), + value = result[rand]; + + result[rand] = result[index]; + result[index] = value; + } + result.length = n; + return result; + } + + /** + * Creates an array of shuffled values, using a version of the + * [Fisher-Yates shuffle](https://en.wikipedia.org/wiki/Fisher-Yates_shuffle). + * + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object|string} collection The collection to shuffle. + * @returns {Array} Returns the new shuffled array. + * @example + * + * _.shuffle([1, 2, 3, 4]); + * // => [4, 1, 3, 2] + */ + function shuffle(collection) { + return sample(collection, POSITIVE_INFINITY); + } + + /** + * Gets the size of `collection` by returning its length for array-like + * values or the number of own enumerable properties for objects. + * + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object|string} collection The collection to inspect. + * @returns {number} Returns the size of `collection`. + * @example + * + * _.size([1, 2, 3]); + * // => 3 + * + * _.size({ 'a': 1, 'b': 2 }); + * // => 2 + * + * _.size('pebbles'); + * // => 7 + */ + function size(collection) { + var length = collection ? getLength(collection) : 0; + return isLength(length) ? length : keys(collection).length; + } + + /** + * Checks if `predicate` returns truthy for **any** element of `collection`. + * The function returns as soon as it finds a passing value and does not iterate + * over the entire collection. The predicate is bound to `thisArg` and invoked + * with three arguments: (value, index|key, collection). + * + * If a property name is provided for `predicate` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `predicate` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @alias any + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [predicate=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `predicate`. + * @returns {boolean} Returns `true` if any element passes the predicate check, + * else `false`. + * @example + * + * _.some([null, 0, 'yes', false], Boolean); + * // => true + * + * var users = [ + * { 'user': 'barney', 'active': true }, + * { 'user': 'fred', 'active': false } + * ]; + * + * // using the `_.matches` callback shorthand + * _.some(users, { 'user': 'barney', 'active': false }); + * // => false + * + * // using the `_.matchesProperty` callback shorthand + * _.some(users, 'active', false); + * // => true + * + * // using the `_.property` callback shorthand + * _.some(users, 'active'); + * // => true + */ + function some(collection, predicate, thisArg) { + var func = isArray(collection) ? arraySome : baseSome; + if (thisArg && isIterateeCall(collection, predicate, thisArg)) { + predicate = undefined; + } + if (typeof predicate != 'function' || thisArg !== undefined) { + predicate = getCallback(predicate, thisArg, 3); + } + return func(collection, predicate); + } + + /** + * Creates an array of elements, sorted in ascending order by the results of + * running each element in a collection through `iteratee`. This method performs + * a stable sort, that is, it preserves the original sort order of equal elements. + * The `iteratee` is bound to `thisArg` and invoked with three arguments: + * (value, index|key, collection). + * + * If a property name is provided for `iteratee` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `iteratee` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [iteratee=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {Array} Returns the new sorted array. + * @example + * + * _.sortBy([1, 2, 3], function(n) { + * return Math.sin(n); + * }); + * // => [3, 1, 2] + * + * _.sortBy([1, 2, 3], function(n) { + * return this.sin(n); + * }, Math); + * // => [3, 1, 2] + * + * var users = [ + * { 'user': 'fred' }, + * { 'user': 'pebbles' }, + * { 'user': 'barney' } + * ]; + * + * // using the `_.property` callback shorthand + * _.pluck(_.sortBy(users, 'user'), 'user'); + * // => ['barney', 'fred', 'pebbles'] + */ + function sortBy(collection, iteratee, thisArg) { + if (collection == null) { + return []; + } + if (thisArg && isIterateeCall(collection, iteratee, thisArg)) { + iteratee = undefined; + } + var index = -1; + iteratee = getCallback(iteratee, thisArg, 3); + + var result = baseMap(collection, function(value, key, collection) { + return { 'criteria': iteratee(value, key, collection), 'index': ++index, 'value': value }; + }); + return baseSortBy(result, compareAscending); + } + + /** + * This method is like `_.sortBy` except that it can sort by multiple iteratees + * or property names. + * + * If a property name is provided for an iteratee the created `_.property` + * style callback returns the property value of the given element. + * + * If an object is provided for an iteratee the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {...(Function|Function[]|Object|Object[]|string|string[])} iteratees + * The iteratees to sort by, specified as individual values or arrays of values. + * @returns {Array} Returns the new sorted array. + * @example + * + * var users = [ + * { 'user': 'fred', 'age': 48 }, + * { 'user': 'barney', 'age': 36 }, + * { 'user': 'fred', 'age': 42 }, + * { 'user': 'barney', 'age': 34 } + * ]; + * + * _.map(_.sortByAll(users, ['user', 'age']), _.values); + * // => [['barney', 34], ['barney', 36], ['fred', 42], ['fred', 48]] + * + * _.map(_.sortByAll(users, 'user', function(chr) { + * return Math.floor(chr.age / 10); + * }), _.values); + * // => [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 42]] + */ + var sortByAll = restParam(function(collection, iteratees) { + if (collection == null) { + return []; + } + var guard = iteratees[2]; + if (guard && isIterateeCall(iteratees[0], iteratees[1], guard)) { + iteratees.length = 1; + } + return baseSortByOrder(collection, baseFlatten(iteratees), []); + }); + + /** + * This method is like `_.sortByAll` except that it allows specifying the + * sort orders of the iteratees to sort by. If `orders` is unspecified, all + * values are sorted in ascending order. Otherwise, a value is sorted in + * ascending order if its corresponding order is "asc", and descending if "desc". + * + * If a property name is provided for an iteratee the created `_.property` + * style callback returns the property value of the given element. + * + * If an object is provided for an iteratee the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function[]|Object[]|string[]} iteratees The iteratees to sort by. + * @param {boolean[]} [orders] The sort orders of `iteratees`. + * @param- {Object} [guard] Enables use as a callback for functions like `_.reduce`. + * @returns {Array} Returns the new sorted array. + * @example + * + * var users = [ + * { 'user': 'fred', 'age': 48 }, + * { 'user': 'barney', 'age': 34 }, + * { 'user': 'fred', 'age': 42 }, + * { 'user': 'barney', 'age': 36 } + * ]; + * + * // sort by `user` in ascending order and by `age` in descending order + * _.map(_.sortByOrder(users, ['user', 'age'], ['asc', 'desc']), _.values); + * // => [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 42]] + */ + function sortByOrder(collection, iteratees, orders, guard) { + if (collection == null) { + return []; + } + if (guard && isIterateeCall(iteratees, orders, guard)) { + orders = undefined; + } + if (!isArray(iteratees)) { + iteratees = iteratees == null ? [] : [iteratees]; + } + if (!isArray(orders)) { + orders = orders == null ? [] : [orders]; + } + return baseSortByOrder(collection, iteratees, orders); + } + + /** + * Performs a deep comparison between each element in `collection` and the + * source object, returning an array of all elements that have equivalent + * property values. + * + * **Note:** This method supports comparing arrays, booleans, `Date` objects, + * numbers, `Object` objects, regexes, and strings. Objects are compared by + * their own, not inherited, enumerable properties. For comparing a single + * own or inherited property value see `_.matchesProperty`. + * + * @static + * @memberOf _ + * @category Collection + * @param {Array|Object|string} collection The collection to search. + * @param {Object} source The object of property values to match. + * @returns {Array} Returns the new filtered array. + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36, 'active': false, 'pets': ['hoppy'] }, + * { 'user': 'fred', 'age': 40, 'active': true, 'pets': ['baby puss', 'dino'] } + * ]; + * + * _.pluck(_.where(users, { 'age': 36, 'active': false }), 'user'); + * // => ['barney'] + * + * _.pluck(_.where(users, { 'pets': ['dino'] }), 'user'); + * // => ['fred'] + */ + function where(collection, source) { + return filter(collection, baseMatches(source)); + } + + /*------------------------------------------------------------------------*/ + + /** + * Gets the number of milliseconds that have elapsed since the Unix epoch + * (1 January 1970 00:00:00 UTC). + * + * @static + * @memberOf _ + * @category Date + * @example + * + * _.defer(function(stamp) { + * console.log(_.now() - stamp); + * }, _.now()); + * // => logs the number of milliseconds it took for the deferred function to be invoked + */ + var now = nativeNow || function() { + return new Date().getTime(); + }; + + /*------------------------------------------------------------------------*/ + + /** + * The opposite of `_.before`; this method creates a function that invokes + * `func` once it's called `n` or more times. + * + * @static + * @memberOf _ + * @category Function + * @param {number} n The number of calls before `func` is invoked. + * @param {Function} func The function to restrict. + * @returns {Function} Returns the new restricted function. + * @example + * + * var saves = ['profile', 'settings']; + * + * var done = _.after(saves.length, function() { + * console.log('done saving!'); + * }); + * + * _.forEach(saves, function(type) { + * asyncSave({ 'type': type, 'complete': done }); + * }); + * // => logs 'done saving!' after the two async saves have completed + */ + function after(n, func) { + if (typeof func != 'function') { + if (typeof n == 'function') { + var temp = n; + n = func; + func = temp; + } else { + throw new TypeError(FUNC_ERROR_TEXT); + } + } + n = nativeIsFinite(n = +n) ? n : 0; + return function() { + if (--n < 1) { + return func.apply(this, arguments); + } + }; + } + + /** + * Creates a function that accepts up to `n` arguments ignoring any + * additional arguments. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to cap arguments for. + * @param {number} [n=func.length] The arity cap. + * @param- {Object} [guard] Enables use as a callback for functions like `_.map`. + * @returns {Function} Returns the new function. + * @example + * + * _.map(['6', '8', '10'], _.ary(parseInt, 1)); + * // => [6, 8, 10] + */ + function ary(func, n, guard) { + if (guard && isIterateeCall(func, n, guard)) { + n = undefined; + } + n = (func && n == null) ? func.length : nativeMax(+n || 0, 0); + return createWrapper(func, ARY_FLAG, undefined, undefined, undefined, undefined, n); + } + + /** + * Creates a function that invokes `func`, with the `this` binding and arguments + * of the created function, while it's called less than `n` times. Subsequent + * calls to the created function return the result of the last `func` invocation. + * + * @static + * @memberOf _ + * @category Function + * @param {number} n The number of calls at which `func` is no longer invoked. + * @param {Function} func The function to restrict. + * @returns {Function} Returns the new restricted function. + * @example + * + * jQuery('#add').on('click', _.before(5, addContactToList)); + * // => allows adding up to 4 contacts to the list + */ + function before(n, func) { + var result; + if (typeof func != 'function') { + if (typeof n == 'function') { + var temp = n; + n = func; + func = temp; + } else { + throw new TypeError(FUNC_ERROR_TEXT); + } + } + return function() { + if (--n > 0) { + result = func.apply(this, arguments); + } + if (n <= 1) { + func = undefined; + } + return result; + }; + } + + /** + * Creates a function that invokes `func` with the `this` binding of `thisArg` + * and prepends any additional `_.bind` arguments to those provided to the + * bound function. + * + * The `_.bind.placeholder` value, which defaults to `_` in monolithic builds, + * may be used as a placeholder for partially applied arguments. + * + * **Note:** Unlike native `Function#bind` this method does not set the "length" + * property of bound functions. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to bind. + * @param {*} thisArg The `this` binding of `func`. + * @param {...*} [partials] The arguments to be partially applied. + * @returns {Function} Returns the new bound function. + * @example + * + * var greet = function(greeting, punctuation) { + * return greeting + ' ' + this.user + punctuation; + * }; + * + * var object = { 'user': 'fred' }; + * + * var bound = _.bind(greet, object, 'hi'); + * bound('!'); + * // => 'hi fred!' + * + * // using placeholders + * var bound = _.bind(greet, object, _, '!'); + * bound('hi'); + * // => 'hi fred!' + */ + var bind = restParam(function(func, thisArg, partials) { + var bitmask = BIND_FLAG; + if (partials.length) { + var holders = replaceHolders(partials, bind.placeholder); + bitmask |= PARTIAL_FLAG; + } + return createWrapper(func, bitmask, thisArg, partials, holders); + }); + + /** + * Binds methods of an object to the object itself, overwriting the existing + * method. Method names may be specified as individual arguments or as arrays + * of method names. If no method names are provided all enumerable function + * properties, own and inherited, of `object` are bound. + * + * **Note:** This method does not set the "length" property of bound functions. + * + * @static + * @memberOf _ + * @category Function + * @param {Object} object The object to bind and assign the bound methods to. + * @param {...(string|string[])} [methodNames] The object method names to bind, + * specified as individual method names or arrays of method names. + * @returns {Object} Returns `object`. + * @example + * + * var view = { + * 'label': 'docs', + * 'onClick': function() { + * console.log('clicked ' + this.label); + * } + * }; + * + * _.bindAll(view); + * jQuery('#docs').on('click', view.onClick); + * // => logs 'clicked docs' when the element is clicked + */ + var bindAll = restParam(function(object, methodNames) { + methodNames = methodNames.length ? baseFlatten(methodNames) : functions(object); + + var index = -1, + length = methodNames.length; + + while (++index < length) { + var key = methodNames[index]; + object[key] = createWrapper(object[key], BIND_FLAG, object); + } + return object; + }); + + /** + * Creates a function that invokes the method at `object[key]` and prepends + * any additional `_.bindKey` arguments to those provided to the bound function. + * + * This method differs from `_.bind` by allowing bound functions to reference + * methods that may be redefined or don't yet exist. + * See [Peter Michaux's article](http://peter.michaux.ca/articles/lazy-function-definition-pattern) + * for more details. + * + * The `_.bindKey.placeholder` value, which defaults to `_` in monolithic + * builds, may be used as a placeholder for partially applied arguments. + * + * @static + * @memberOf _ + * @category Function + * @param {Object} object The object the method belongs to. + * @param {string} key The key of the method. + * @param {...*} [partials] The arguments to be partially applied. + * @returns {Function} Returns the new bound function. + * @example + * + * var object = { + * 'user': 'fred', + * 'greet': function(greeting, punctuation) { + * return greeting + ' ' + this.user + punctuation; + * } + * }; + * + * var bound = _.bindKey(object, 'greet', 'hi'); + * bound('!'); + * // => 'hi fred!' + * + * object.greet = function(greeting, punctuation) { + * return greeting + 'ya ' + this.user + punctuation; + * }; + * + * bound('!'); + * // => 'hiya fred!' + * + * // using placeholders + * var bound = _.bindKey(object, 'greet', _, '!'); + * bound('hi'); + * // => 'hiya fred!' + */ + var bindKey = restParam(function(object, key, partials) { + var bitmask = BIND_FLAG | BIND_KEY_FLAG; + if (partials.length) { + var holders = replaceHolders(partials, bindKey.placeholder); + bitmask |= PARTIAL_FLAG; + } + return createWrapper(key, bitmask, object, partials, holders); + }); + + /** + * Creates a function that accepts one or more arguments of `func` that when + * called either invokes `func` returning its result, if all `func` arguments + * have been provided, or returns a function that accepts one or more of the + * remaining `func` arguments, and so on. The arity of `func` may be specified + * if `func.length` is not sufficient. + * + * The `_.curry.placeholder` value, which defaults to `_` in monolithic builds, + * may be used as a placeholder for provided arguments. + * + * **Note:** This method does not set the "length" property of curried functions. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to curry. + * @param {number} [arity=func.length] The arity of `func`. + * @param- {Object} [guard] Enables use as a callback for functions like `_.map`. + * @returns {Function} Returns the new curried function. + * @example + * + * var abc = function(a, b, c) { + * return [a, b, c]; + * }; + * + * var curried = _.curry(abc); + * + * curried(1)(2)(3); + * // => [1, 2, 3] + * + * curried(1, 2)(3); + * // => [1, 2, 3] + * + * curried(1, 2, 3); + * // => [1, 2, 3] + * + * // using placeholders + * curried(1)(_, 3)(2); + * // => [1, 2, 3] + */ + var curry = createCurry(CURRY_FLAG); + + /** + * This method is like `_.curry` except that arguments are applied to `func` + * in the manner of `_.partialRight` instead of `_.partial`. + * + * The `_.curryRight.placeholder` value, which defaults to `_` in monolithic + * builds, may be used as a placeholder for provided arguments. + * + * **Note:** This method does not set the "length" property of curried functions. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to curry. + * @param {number} [arity=func.length] The arity of `func`. + * @param- {Object} [guard] Enables use as a callback for functions like `_.map`. + * @returns {Function} Returns the new curried function. + * @example + * + * var abc = function(a, b, c) { + * return [a, b, c]; + * }; + * + * var curried = _.curryRight(abc); + * + * curried(3)(2)(1); + * // => [1, 2, 3] + * + * curried(2, 3)(1); + * // => [1, 2, 3] + * + * curried(1, 2, 3); + * // => [1, 2, 3] + * + * // using placeholders + * curried(3)(1, _)(2); + * // => [1, 2, 3] + */ + var curryRight = createCurry(CURRY_RIGHT_FLAG); + + /** + * Creates a debounced function that delays invoking `func` until after `wait` + * milliseconds have elapsed since the last time the debounced function was + * invoked. The debounced function comes with a `cancel` method to cancel + * delayed invocations. Provide an options object to indicate that `func` + * should be invoked on the leading and/or trailing edge of the `wait` timeout. + * Subsequent calls to the debounced function return the result of the last + * `func` invocation. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is invoked + * on the trailing edge of the timeout only if the the debounced function is + * invoked more than once during the `wait` timeout. + * + * See [David Corbacho's article](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation) + * for details over the differences between `_.debounce` and `_.throttle`. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to debounce. + * @param {number} [wait=0] The number of milliseconds to delay. + * @param {Object} [options] The options object. + * @param {boolean} [options.leading=false] Specify invoking on the leading + * edge of the timeout. + * @param {number} [options.maxWait] The maximum time `func` is allowed to be + * delayed before it's invoked. + * @param {boolean} [options.trailing=true] Specify invoking on the trailing + * edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // avoid costly calculations while the window size is in flux + * jQuery(window).on('resize', _.debounce(calculateLayout, 150)); + * + * // invoke `sendMail` when the click event is fired, debouncing subsequent calls + * jQuery('#postbox').on('click', _.debounce(sendMail, 300, { + * 'leading': true, + * 'trailing': false + * })); + * + * // ensure `batchLog` is invoked once after 1 second of debounced calls + * var source = new EventSource('/stream'); + * jQuery(source).on('message', _.debounce(batchLog, 250, { + * 'maxWait': 1000 + * })); + * + * // cancel a debounced call + * var todoChanges = _.debounce(batchLog, 1000); + * Object.observe(models.todo, todoChanges); + * + * Object.observe(models, function(changes) { + * if (_.find(changes, { 'user': 'todo', 'type': 'delete'})) { + * todoChanges.cancel(); + * } + * }, ['delete']); + * + * // ...at some point `models.todo` is changed + * models.todo.completed = true; + * + * // ...before 1 second has passed `models.todo` is deleted + * // which cancels the debounced `todoChanges` call + * delete models.todo; + */ + function debounce(func, wait, options) { + var args, + maxTimeoutId, + result, + stamp, + thisArg, + timeoutId, + trailingCall, + lastCalled = 0, + maxWait = false, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + wait = wait < 0 ? 0 : (+wait || 0); + if (options === true) { + var leading = true; + trailing = false; + } else if (isObject(options)) { + leading = !!options.leading; + maxWait = 'maxWait' in options && nativeMax(+options.maxWait || 0, wait); + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + + function cancel() { + if (timeoutId) { + clearTimeout(timeoutId); + } + if (maxTimeoutId) { + clearTimeout(maxTimeoutId); + } + lastCalled = 0; + maxTimeoutId = timeoutId = trailingCall = undefined; + } + + function complete(isCalled, id) { + if (id) { + clearTimeout(id); + } + maxTimeoutId = timeoutId = trailingCall = undefined; + if (isCalled) { + lastCalled = now(); + result = func.apply(thisArg, args); + if (!timeoutId && !maxTimeoutId) { + args = thisArg = undefined; + } + } + } + + function delayed() { + var remaining = wait - (now() - stamp); + if (remaining <= 0 || remaining > wait) { + complete(trailingCall, maxTimeoutId); + } else { + timeoutId = setTimeout(delayed, remaining); + } + } + + function maxDelayed() { + complete(trailing, timeoutId); + } + + function debounced() { + args = arguments; + stamp = now(); + thisArg = this; + trailingCall = trailing && (timeoutId || !leading); + + if (maxWait === false) { + var leadingCall = leading && !timeoutId; + } else { + if (!maxTimeoutId && !leading) { + lastCalled = stamp; + } + var remaining = maxWait - (stamp - lastCalled), + isCalled = remaining <= 0 || remaining > maxWait; + + if (isCalled) { + if (maxTimeoutId) { + maxTimeoutId = clearTimeout(maxTimeoutId); + } + lastCalled = stamp; + result = func.apply(thisArg, args); + } + else if (!maxTimeoutId) { + maxTimeoutId = setTimeout(maxDelayed, remaining); + } + } + if (isCalled && timeoutId) { + timeoutId = clearTimeout(timeoutId); + } + else if (!timeoutId && wait !== maxWait) { + timeoutId = setTimeout(delayed, wait); + } + if (leadingCall) { + isCalled = true; + result = func.apply(thisArg, args); + } + if (isCalled && !timeoutId && !maxTimeoutId) { + args = thisArg = undefined; + } + return result; + } + debounced.cancel = cancel; + return debounced; + } + + /** + * Defers invoking the `func` until the current call stack has cleared. Any + * additional arguments are provided to `func` when it's invoked. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to defer. + * @param {...*} [args] The arguments to invoke the function with. + * @returns {number} Returns the timer id. + * @example + * + * _.defer(function(text) { + * console.log(text); + * }, 'deferred'); + * // logs 'deferred' after one or more milliseconds + */ + var defer = restParam(function(func, args) { + return baseDelay(func, 1, args); + }); + + /** + * Invokes `func` after `wait` milliseconds. Any additional arguments are + * provided to `func` when it's invoked. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to delay. + * @param {number} wait The number of milliseconds to delay invocation. + * @param {...*} [args] The arguments to invoke the function with. + * @returns {number} Returns the timer id. + * @example + * + * _.delay(function(text) { + * console.log(text); + * }, 1000, 'later'); + * // => logs 'later' after one second + */ + var delay = restParam(function(func, wait, args) { + return baseDelay(func, wait, args); + }); + + /** + * Creates a function that returns the result of invoking the provided + * functions with the `this` binding of the created function, where each + * successive invocation is supplied the return value of the previous. + * + * @static + * @memberOf _ + * @category Function + * @param {...Function} [funcs] Functions to invoke. + * @returns {Function} Returns the new function. + * @example + * + * function square(n) { + * return n * n; + * } + * + * var addSquare = _.flow(_.add, square); + * addSquare(1, 2); + * // => 9 + */ + var flow = createFlow(); + + /** + * This method is like `_.flow` except that it creates a function that + * invokes the provided functions from right to left. + * + * @static + * @memberOf _ + * @alias backflow, compose + * @category Function + * @param {...Function} [funcs] Functions to invoke. + * @returns {Function} Returns the new function. + * @example + * + * function square(n) { + * return n * n; + * } + * + * var addSquare = _.flowRight(square, _.add); + * addSquare(1, 2); + * // => 9 + */ + var flowRight = createFlow(true); + + /** + * Creates a function that memoizes the result of `func`. If `resolver` is + * provided it determines the cache key for storing the result based on the + * arguments provided to the memoized function. By default, the first argument + * provided to the memoized function is coerced to a string and used as the + * cache key. The `func` is invoked with the `this` binding of the memoized + * function. + * + * **Note:** The cache is exposed as the `cache` property on the memoized + * function. Its creation may be customized by replacing the `_.memoize.Cache` + * constructor with one whose instances implement the [`Map`](http://ecma-international.org/ecma-262/6.0/#sec-properties-of-the-map-prototype-object) + * method interface of `get`, `has`, and `set`. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to have its output memoized. + * @param {Function} [resolver] The function to resolve the cache key. + * @returns {Function} Returns the new memoizing function. + * @example + * + * var upperCase = _.memoize(function(string) { + * return string.toUpperCase(); + * }); + * + * upperCase('fred'); + * // => 'FRED' + * + * // modifying the result cache + * upperCase.cache.set('fred', 'BARNEY'); + * upperCase('fred'); + * // => 'BARNEY' + * + * // replacing `_.memoize.Cache` + * var object = { 'user': 'fred' }; + * var other = { 'user': 'barney' }; + * var identity = _.memoize(_.identity); + * + * identity(object); + * // => { 'user': 'fred' } + * identity(other); + * // => { 'user': 'fred' } + * + * _.memoize.Cache = WeakMap; + * var identity = _.memoize(_.identity); + * + * identity(object); + * // => { 'user': 'fred' } + * identity(other); + * // => { 'user': 'barney' } + */ + function memoize(func, resolver) { + if (typeof func != 'function' || (resolver && typeof resolver != 'function')) { + throw new TypeError(FUNC_ERROR_TEXT); + } + var memoized = function() { + var args = arguments, + key = resolver ? resolver.apply(this, args) : args[0], + cache = memoized.cache; + + if (cache.has(key)) { + return cache.get(key); + } + var result = func.apply(this, args); + memoized.cache = cache.set(key, result); + return result; + }; + memoized.cache = new memoize.Cache; + return memoized; + } + + /** + * Creates a function that runs each argument through a corresponding + * transform function. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to wrap. + * @param {...(Function|Function[])} [transforms] The functions to transform + * arguments, specified as individual functions or arrays of functions. + * @returns {Function} Returns the new function. + * @example + * + * function doubled(n) { + * return n * 2; + * } + * + * function square(n) { + * return n * n; + * } + * + * var modded = _.modArgs(function(x, y) { + * return [x, y]; + * }, square, doubled); + * + * modded(1, 2); + * // => [1, 4] + * + * modded(5, 10); + * // => [25, 20] + */ + var modArgs = restParam(function(func, transforms) { + transforms = baseFlatten(transforms); + if (typeof func != 'function' || !arrayEvery(transforms, baseIsFunction)) { + throw new TypeError(FUNC_ERROR_TEXT); + } + var length = transforms.length; + return restParam(function(args) { + var index = nativeMin(args.length, length); + while (index--) { + args[index] = transforms[index](args[index]); + } + return func.apply(this, args); + }); + }); + + /** + * Creates a function that negates the result of the predicate `func`. The + * `func` predicate is invoked with the `this` binding and arguments of the + * created function. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} predicate The predicate to negate. + * @returns {Function} Returns the new function. + * @example + * + * function isEven(n) { + * return n % 2 == 0; + * } + * + * _.filter([1, 2, 3, 4, 5, 6], _.negate(isEven)); + * // => [1, 3, 5] + */ + function negate(predicate) { + if (typeof predicate != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + return function() { + return !predicate.apply(this, arguments); + }; + } + + /** + * Creates a function that is restricted to invoking `func` once. Repeat calls + * to the function return the value of the first call. The `func` is invoked + * with the `this` binding and arguments of the created function. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to restrict. + * @returns {Function} Returns the new restricted function. + * @example + * + * var initialize = _.once(createApplication); + * initialize(); + * initialize(); + * // `initialize` invokes `createApplication` once + */ + function once(func) { + return before(2, func); + } + + /** + * Creates a function that invokes `func` with `partial` arguments prepended + * to those provided to the new function. This method is like `_.bind` except + * it does **not** alter the `this` binding. + * + * The `_.partial.placeholder` value, which defaults to `_` in monolithic + * builds, may be used as a placeholder for partially applied arguments. + * + * **Note:** This method does not set the "length" property of partially + * applied functions. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to partially apply arguments to. + * @param {...*} [partials] The arguments to be partially applied. + * @returns {Function} Returns the new partially applied function. + * @example + * + * var greet = function(greeting, name) { + * return greeting + ' ' + name; + * }; + * + * var sayHelloTo = _.partial(greet, 'hello'); + * sayHelloTo('fred'); + * // => 'hello fred' + * + * // using placeholders + * var greetFred = _.partial(greet, _, 'fred'); + * greetFred('hi'); + * // => 'hi fred' + */ + var partial = createPartial(PARTIAL_FLAG); + + /** + * This method is like `_.partial` except that partially applied arguments + * are appended to those provided to the new function. + * + * The `_.partialRight.placeholder` value, which defaults to `_` in monolithic + * builds, may be used as a placeholder for partially applied arguments. + * + * **Note:** This method does not set the "length" property of partially + * applied functions. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to partially apply arguments to. + * @param {...*} [partials] The arguments to be partially applied. + * @returns {Function} Returns the new partially applied function. + * @example + * + * var greet = function(greeting, name) { + * return greeting + ' ' + name; + * }; + * + * var greetFred = _.partialRight(greet, 'fred'); + * greetFred('hi'); + * // => 'hi fred' + * + * // using placeholders + * var sayHelloTo = _.partialRight(greet, 'hello', _); + * sayHelloTo('fred'); + * // => 'hello fred' + */ + var partialRight = createPartial(PARTIAL_RIGHT_FLAG); + + /** + * Creates a function that invokes `func` with arguments arranged according + * to the specified indexes where the argument value at the first index is + * provided as the first argument, the argument value at the second index is + * provided as the second argument, and so on. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to rearrange arguments for. + * @param {...(number|number[])} indexes The arranged argument indexes, + * specified as individual indexes or arrays of indexes. + * @returns {Function} Returns the new function. + * @example + * + * var rearged = _.rearg(function(a, b, c) { + * return [a, b, c]; + * }, 2, 0, 1); + * + * rearged('b', 'c', 'a') + * // => ['a', 'b', 'c'] + * + * var map = _.rearg(_.map, [1, 0]); + * map(function(n) { + * return n * 3; + * }, [1, 2, 3]); + * // => [3, 6, 9] + */ + var rearg = restParam(function(func, indexes) { + return createWrapper(func, REARG_FLAG, undefined, undefined, undefined, baseFlatten(indexes)); + }); + + /** + * Creates a function that invokes `func` with the `this` binding of the + * created function and arguments from `start` and beyond provided as an array. + * + * **Note:** This method is based on the [rest parameter](https://developer.mozilla.org/Web/JavaScript/Reference/Functions/rest_parameters). + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to apply a rest parameter to. + * @param {number} [start=func.length-1] The start position of the rest parameter. + * @returns {Function} Returns the new function. + * @example + * + * var say = _.restParam(function(what, names) { + * return what + ' ' + _.initial(names).join(', ') + + * (_.size(names) > 1 ? ', & ' : '') + _.last(names); + * }); + * + * say('hello', 'fred', 'barney', 'pebbles'); + * // => 'hello fred, barney, & pebbles' + */ + function restParam(func, start) { + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + start = nativeMax(start === undefined ? (func.length - 1) : (+start || 0), 0); + return function() { + var args = arguments, + index = -1, + length = nativeMax(args.length - start, 0), + rest = Array(length); + + while (++index < length) { + rest[index] = args[start + index]; + } + switch (start) { + case 0: return func.call(this, rest); + case 1: return func.call(this, args[0], rest); + case 2: return func.call(this, args[0], args[1], rest); + } + var otherArgs = Array(start + 1); + index = -1; + while (++index < start) { + otherArgs[index] = args[index]; + } + otherArgs[start] = rest; + return func.apply(this, otherArgs); + }; + } + + /** + * Creates a function that invokes `func` with the `this` binding of the created + * function and an array of arguments much like [`Function#apply`](https://es5.github.io/#x15.3.4.3). + * + * **Note:** This method is based on the [spread operator](https://developer.mozilla.org/Web/JavaScript/Reference/Operators/Spread_operator). + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to spread arguments over. + * @returns {Function} Returns the new function. + * @example + * + * var say = _.spread(function(who, what) { + * return who + ' says ' + what; + * }); + * + * say(['fred', 'hello']); + * // => 'fred says hello' + * + * // with a Promise + * var numbers = Promise.all([ + * Promise.resolve(40), + * Promise.resolve(36) + * ]); + * + * numbers.then(_.spread(function(x, y) { + * return x + y; + * })); + * // => a Promise of 76 + */ + function spread(func) { + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + return function(array) { + return func.apply(this, array); + }; + } + + /** + * Creates a throttled function that only invokes `func` at most once per + * every `wait` milliseconds. The throttled function comes with a `cancel` + * method to cancel delayed invocations. Provide an options object to indicate + * that `func` should be invoked on the leading and/or trailing edge of the + * `wait` timeout. Subsequent calls to the throttled function return the + * result of the last `func` call. + * + * **Note:** If `leading` and `trailing` options are `true`, `func` is invoked + * on the trailing edge of the timeout only if the the throttled function is + * invoked more than once during the `wait` timeout. + * + * See [David Corbacho's article](http://drupalmotion.com/article/debounce-and-throttle-visual-explanation) + * for details over the differences between `_.throttle` and `_.debounce`. + * + * @static + * @memberOf _ + * @category Function + * @param {Function} func The function to throttle. + * @param {number} [wait=0] The number of milliseconds to throttle invocations to. + * @param {Object} [options] The options object. + * @param {boolean} [options.leading=true] Specify invoking on the leading + * edge of the timeout. + * @param {boolean} [options.trailing=true] Specify invoking on the trailing + * edge of the timeout. + * @returns {Function} Returns the new throttled function. + * @example + * + * // avoid excessively updating the position while scrolling + * jQuery(window).on('scroll', _.throttle(updatePosition, 100)); + * + * // invoke `renewToken` when the click event is fired, but not more than once every 5 minutes + * jQuery('.interactive').on('click', _.throttle(renewToken, 300000, { + * 'trailing': false + * })); + * + * // cancel a trailing throttled call + * jQuery(window).on('popstate', throttled.cancel); + */ + function throttle(func, wait, options) { + var leading = true, + trailing = true; + + if (typeof func != 'function') { + throw new TypeError(FUNC_ERROR_TEXT); + } + if (options === false) { + leading = false; + } else if (isObject(options)) { + leading = 'leading' in options ? !!options.leading : leading; + trailing = 'trailing' in options ? !!options.trailing : trailing; + } + return debounce(func, wait, { 'leading': leading, 'maxWait': +wait, 'trailing': trailing }); + } + + /** + * Creates a function that provides `value` to the wrapper function as its + * first argument. Any additional arguments provided to the function are + * appended to those provided to the wrapper function. The wrapper is invoked + * with the `this` binding of the created function. + * + * @static + * @memberOf _ + * @category Function + * @param {*} value The value to wrap. + * @param {Function} wrapper The wrapper function. + * @returns {Function} Returns the new function. + * @example + * + * var p = _.wrap(_.escape, function(func, text) { + * return '<p>' + func(text) + '</p>'; + * }); + * + * p('fred, barney, & pebbles'); + * // => '<p>fred, barney, & pebbles</p>' + */ + function wrap(value, wrapper) { + wrapper = wrapper == null ? identity : wrapper; + return createWrapper(wrapper, PARTIAL_FLAG, undefined, [value], []); + } + + /*------------------------------------------------------------------------*/ + + /** + * Creates a clone of `value`. If `isDeep` is `true` nested objects are cloned, + * otherwise they are assigned by reference. If `customizer` is provided it's + * invoked to produce the cloned values. If `customizer` returns `undefined` + * cloning is handled by the method instead. The `customizer` is bound to + * `thisArg` and invoked with up to three argument; (value [, index|key, object]). + * + * **Note:** This method is loosely based on the + * [structured clone algorithm](http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm). + * The enumerable properties of `arguments` objects and objects created by + * constructors other than `Object` are cloned to plain `Object` objects. An + * empty object is returned for uncloneable values such as functions, DOM nodes, + * Maps, Sets, and WeakMaps. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to clone. + * @param {boolean} [isDeep] Specify a deep clone. + * @param {Function} [customizer] The function to customize cloning values. + * @param {*} [thisArg] The `this` binding of `customizer`. + * @returns {*} Returns the cloned value. + * @example + * + * var users = [ + * { 'user': 'barney' }, + * { 'user': 'fred' } + * ]; + * + * var shallow = _.clone(users); + * shallow[0] === users[0]; + * // => true + * + * var deep = _.clone(users, true); + * deep[0] === users[0]; + * // => false + * + * // using a customizer callback + * var el = _.clone(document.body, function(value) { + * if (_.isElement(value)) { + * return value.cloneNode(false); + * } + * }); + * + * el === document.body + * // => false + * el.nodeName + * // => BODY + * el.childNodes.length; + * // => 0 + */ + function clone(value, isDeep, customizer, thisArg) { + if (isDeep && typeof isDeep != 'boolean' && isIterateeCall(value, isDeep, customizer)) { + isDeep = false; + } + else if (typeof isDeep == 'function') { + thisArg = customizer; + customizer = isDeep; + isDeep = false; + } + return typeof customizer == 'function' + ? baseClone(value, isDeep, bindCallback(customizer, thisArg, 3)) + : baseClone(value, isDeep); + } + + /** + * Creates a deep clone of `value`. If `customizer` is provided it's invoked + * to produce the cloned values. If `customizer` returns `undefined` cloning + * is handled by the method instead. The `customizer` is bound to `thisArg` + * and invoked with up to three argument; (value [, index|key, object]). + * + * **Note:** This method is loosely based on the + * [structured clone algorithm](http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm). + * The enumerable properties of `arguments` objects and objects created by + * constructors other than `Object` are cloned to plain `Object` objects. An + * empty object is returned for uncloneable values such as functions, DOM nodes, + * Maps, Sets, and WeakMaps. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to deep clone. + * @param {Function} [customizer] The function to customize cloning values. + * @param {*} [thisArg] The `this` binding of `customizer`. + * @returns {*} Returns the deep cloned value. + * @example + * + * var users = [ + * { 'user': 'barney' }, + * { 'user': 'fred' } + * ]; + * + * var deep = _.cloneDeep(users); + * deep[0] === users[0]; + * // => false + * + * // using a customizer callback + * var el = _.cloneDeep(document.body, function(value) { + * if (_.isElement(value)) { + * return value.cloneNode(true); + * } + * }); + * + * el === document.body + * // => false + * el.nodeName + * // => BODY + * el.childNodes.length; + * // => 20 + */ + function cloneDeep(value, customizer, thisArg) { + return typeof customizer == 'function' + ? baseClone(value, true, bindCallback(customizer, thisArg, 3)) + : baseClone(value, true); + } + + /** + * Checks if `value` is greater than `other`. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is greater than `other`, else `false`. + * @example + * + * _.gt(3, 1); + * // => true + * + * _.gt(3, 3); + * // => false + * + * _.gt(1, 3); + * // => false + */ + function gt(value, other) { + return value > other; + } + + /** + * Checks if `value` is greater than or equal to `other`. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is greater than or equal to `other`, else `false`. + * @example + * + * _.gte(3, 1); + * // => true + * + * _.gte(3, 3); + * // => true + * + * _.gte(1, 3); + * // => false + */ + function gte(value, other) { + return value >= other; + } + + /** + * Checks if `value` is classified as an `arguments` object. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. + * @example + * + * _.isArguments(function() { return arguments; }()); + * // => true + * + * _.isArguments([1, 2, 3]); + * // => false + */ + function isArguments(value) { + return isObjectLike(value) && isArrayLike(value) && + hasOwnProperty.call(value, 'callee') && !propertyIsEnumerable.call(value, 'callee'); + } + + /** + * Checks if `value` is classified as an `Array` object. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. + * @example + * + * _.isArray([1, 2, 3]); + * // => true + * + * _.isArray(function() { return arguments; }()); + * // => false + */ + var isArray = nativeIsArray || function(value) { + return isObjectLike(value) && isLength(value.length) && objToString.call(value) == arrayTag; + }; + + /** + * Checks if `value` is classified as a boolean primitive or object. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. + * @example + * + * _.isBoolean(false); + * // => true + * + * _.isBoolean(null); + * // => false + */ + function isBoolean(value) { + return value === true || value === false || (isObjectLike(value) && objToString.call(value) == boolTag); + } + + /** + * Checks if `value` is classified as a `Date` object. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. + * @example + * + * _.isDate(new Date); + * // => true + * + * _.isDate('Mon April 23 2012'); + * // => false + */ + function isDate(value) { + return isObjectLike(value) && objToString.call(value) == dateTag; + } + + /** + * Checks if `value` is a DOM element. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a DOM element, else `false`. + * @example + * + * _.isElement(document.body); + * // => true + * + * _.isElement('<body>'); + * // => false + */ + function isElement(value) { + return !!value && value.nodeType === 1 && isObjectLike(value) && !isPlainObject(value); + } + + /** + * Checks if `value` is empty. A value is considered empty unless it's an + * `arguments` object, array, string, or jQuery-like collection with a length + * greater than `0` or an object with own enumerable properties. + * + * @static + * @memberOf _ + * @category Lang + * @param {Array|Object|string} value The value to inspect. + * @returns {boolean} Returns `true` if `value` is empty, else `false`. + * @example + * + * _.isEmpty(null); + * // => true + * + * _.isEmpty(true); + * // => true + * + * _.isEmpty(1); + * // => true + * + * _.isEmpty([1, 2, 3]); + * // => false + * + * _.isEmpty({ 'a': 1 }); + * // => false + */ + function isEmpty(value) { + if (value == null) { + return true; + } + if (isArrayLike(value) && (isArray(value) || isString(value) || isArguments(value) || + (isObjectLike(value) && isFunction(value.splice)))) { + return !value.length; + } + return !keys(value).length; + } + + /** + * Performs a deep comparison between two values to determine if they are + * equivalent. If `customizer` is provided it's invoked to compare values. + * If `customizer` returns `undefined` comparisons are handled by the method + * instead. The `customizer` is bound to `thisArg` and invoked with up to + * three arguments: (value, other [, index|key]). + * + * **Note:** This method supports comparing arrays, booleans, `Date` objects, + * numbers, `Object` objects, regexes, and strings. Objects are compared by + * their own, not inherited, enumerable properties. Functions and DOM nodes + * are **not** supported. Provide a customizer function to extend support + * for comparing other values. + * + * @static + * @memberOf _ + * @alias eq + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @param {Function} [customizer] The function to customize value comparisons. + * @param {*} [thisArg] The `this` binding of `customizer`. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + * @example + * + * var object = { 'user': 'fred' }; + * var other = { 'user': 'fred' }; + * + * object == other; + * // => false + * + * _.isEqual(object, other); + * // => true + * + * // using a customizer callback + * var array = ['hello', 'goodbye']; + * var other = ['hi', 'goodbye']; + * + * _.isEqual(array, other, function(value, other) { + * if (_.every([value, other], RegExp.prototype.test, /^h(?:i|ello)$/)) { + * return true; + * } + * }); + * // => true + */ + function isEqual(value, other, customizer, thisArg) { + customizer = typeof customizer == 'function' ? bindCallback(customizer, thisArg, 3) : undefined; + var result = customizer ? customizer(value, other) : undefined; + return result === undefined ? baseIsEqual(value, other, customizer) : !!result; + } + + /** + * Checks if `value` is an `Error`, `EvalError`, `RangeError`, `ReferenceError`, + * `SyntaxError`, `TypeError`, or `URIError` object. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an error object, else `false`. + * @example + * + * _.isError(new Error); + * // => true + * + * _.isError(Error); + * // => false + */ + function isError(value) { + return isObjectLike(value) && typeof value.message == 'string' && objToString.call(value) == errorTag; + } + + /** + * Checks if `value` is a finite primitive number. + * + * **Note:** This method is based on [`Number.isFinite`](http://ecma-international.org/ecma-262/6.0/#sec-number.isfinite). + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a finite number, else `false`. + * @example + * + * _.isFinite(10); + * // => true + * + * _.isFinite('10'); + * // => false + * + * _.isFinite(true); + * // => false + * + * _.isFinite(Object(10)); + * // => false + * + * _.isFinite(Infinity); + * // => false + */ + function isFinite(value) { + return typeof value == 'number' && nativeIsFinite(value); + } + + /** + * Checks if `value` is classified as a `Function` object. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. + * @example + * + * _.isFunction(_); + * // => true + * + * _.isFunction(/abc/); + * // => false + */ + function isFunction(value) { + // The use of `Object#toString` avoids issues with the `typeof` operator + // in older versions of Chrome and Safari which return 'function' for regexes + // and Safari 8 which returns 'object' for typed array constructors. + return isObject(value) && objToString.call(value) == funcTag; + } + + /** + * Checks if `value` is the [language type](https://es5.github.io/#x8) of `Object`. + * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(1); + * // => false + */ + function isObject(value) { + // Avoid a V8 JIT bug in Chrome 19-20. + // See https://code.google.com/p/v8/issues/detail?id=2291 for more details. + var type = typeof value; + return !!value && (type == 'object' || type == 'function'); + } + + /** + * Performs a deep comparison between `object` and `source` to determine if + * `object` contains equivalent property values. If `customizer` is provided + * it's invoked to compare values. If `customizer` returns `undefined` + * comparisons are handled by the method instead. The `customizer` is bound + * to `thisArg` and invoked with three arguments: (value, other, index|key). + * + * **Note:** This method supports comparing properties of arrays, booleans, + * `Date` objects, numbers, `Object` objects, regexes, and strings. Functions + * and DOM nodes are **not** supported. Provide a customizer function to extend + * support for comparing other values. + * + * @static + * @memberOf _ + * @category Lang + * @param {Object} object The object to inspect. + * @param {Object} source The object of property values to match. + * @param {Function} [customizer] The function to customize value comparisons. + * @param {*} [thisArg] The `this` binding of `customizer`. + * @returns {boolean} Returns `true` if `object` is a match, else `false`. + * @example + * + * var object = { 'user': 'fred', 'age': 40 }; + * + * _.isMatch(object, { 'age': 40 }); + * // => true + * + * _.isMatch(object, { 'age': 36 }); + * // => false + * + * // using a customizer callback + * var object = { 'greeting': 'hello' }; + * var source = { 'greeting': 'hi' }; + * + * _.isMatch(object, source, function(value, other) { + * return _.every([value, other], RegExp.prototype.test, /^h(?:i|ello)$/) || undefined; + * }); + * // => true + */ + function isMatch(object, source, customizer, thisArg) { + customizer = typeof customizer == 'function' ? bindCallback(customizer, thisArg, 3) : undefined; + return baseIsMatch(object, getMatchData(source), customizer); + } + + /** + * Checks if `value` is `NaN`. + * + * **Note:** This method is not the same as [`isNaN`](https://es5.github.io/#x15.1.2.4) + * which returns `true` for `undefined` and other non-numeric values. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is `NaN`, else `false`. + * @example + * + * _.isNaN(NaN); + * // => true + * + * _.isNaN(new Number(NaN)); + * // => true + * + * isNaN(undefined); + * // => true + * + * _.isNaN(undefined); + * // => false + */ + function isNaN(value) { + // An `NaN` primitive is the only value that is not equal to itself. + // Perform the `toStringTag` check first to avoid errors with some host objects in IE. + return isNumber(value) && value != +value; + } + + /** + * Checks if `value` is a native function. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a native function, else `false`. + * @example + * + * _.isNative(Array.prototype.push); + * // => true + * + * _.isNative(_); + * // => false + */ + function isNative(value) { + if (value == null) { + return false; + } + if (isFunction(value)) { + return reIsNative.test(fnToString.call(value)); + } + return isObjectLike(value) && (isHostObject(value) ? reIsNative : reIsHostCtor).test(value); + } + + /** + * Checks if `value` is `null`. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is `null`, else `false`. + * @example + * + * _.isNull(null); + * // => true + * + * _.isNull(void 0); + * // => false + */ + function isNull(value) { + return value === null; + } + + /** + * Checks if `value` is classified as a `Number` primitive or object. + * + * **Note:** To exclude `Infinity`, `-Infinity`, and `NaN`, which are classified + * as numbers, use the `_.isFinite` method. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. + * @example + * + * _.isNumber(8.4); + * // => true + * + * _.isNumber(NaN); + * // => true + * + * _.isNumber('8.4'); + * // => false + */ + function isNumber(value) { + return typeof value == 'number' || (isObjectLike(value) && objToString.call(value) == numberTag); + } + + /** + * Checks if `value` is a plain object, that is, an object created by the + * `Object` constructor or one with a `[[Prototype]]` of `null`. + * + * **Note:** This method assumes objects created by the `Object` constructor + * have no inherited enumerable properties. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a plain object, else `false`. + * @example + * + * function Foo() { + * this.a = 1; + * } + * + * _.isPlainObject(new Foo); + * // => false + * + * _.isPlainObject([1, 2, 3]); + * // => false + * + * _.isPlainObject({ 'x': 0, 'y': 0 }); + * // => true + * + * _.isPlainObject(Object.create(null)); + * // => true + */ + function isPlainObject(value) { + var Ctor; + + // Exit early for non `Object` objects. + if (!(isObjectLike(value) && objToString.call(value) == objectTag && !isHostObject(value) && !isArguments(value)) || + (!hasOwnProperty.call(value, 'constructor') && (Ctor = value.constructor, typeof Ctor == 'function' && !(Ctor instanceof Ctor)))) { + return false; + } + // IE < 9 iterates inherited properties before own properties. If the first + // iterated property is an object's own property then there are no inherited + // enumerable properties. + var result; + if (lodash.support.ownLast) { + baseForIn(value, function(subValue, key, object) { + result = hasOwnProperty.call(object, key); + return false; + }); + return result !== false; + } + // In most environments an object's own properties are iterated before + // its inherited properties. If the last iterated property is an object's + // own property then there are no inherited enumerable properties. + baseForIn(value, function(subValue, key) { + result = key; + }); + return result === undefined || hasOwnProperty.call(value, result); + } + + /** + * Checks if `value` is classified as a `RegExp` object. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. + * @example + * + * _.isRegExp(/abc/); + * // => true + * + * _.isRegExp('/abc/'); + * // => false + */ + function isRegExp(value) { + return isObject(value) && objToString.call(value) == regexpTag; + } + + /** + * Checks if `value` is classified as a `String` primitive or object. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. + * @example + * + * _.isString('abc'); + * // => true + * + * _.isString(1); + * // => false + */ + function isString(value) { + return typeof value == 'string' || (isObjectLike(value) && objToString.call(value) == stringTag); + } + + /** + * Checks if `value` is classified as a typed array. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is correctly classified, else `false`. + * @example + * + * _.isTypedArray(new Uint8Array); + * // => true + * + * _.isTypedArray([]); + * // => false + */ + function isTypedArray(value) { + return isObjectLike(value) && isLength(value.length) && !!typedArrayTags[objToString.call(value)]; + } + + /** + * Checks if `value` is `undefined`. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is `undefined`, else `false`. + * @example + * + * _.isUndefined(void 0); + * // => true + * + * _.isUndefined(null); + * // => false + */ + function isUndefined(value) { + return value === undefined; + } + + /** + * Checks if `value` is less than `other`. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is less than `other`, else `false`. + * @example + * + * _.lt(1, 3); + * // => true + * + * _.lt(3, 3); + * // => false + * + * _.lt(3, 1); + * // => false + */ + function lt(value, other) { + return value < other; + } + + /** + * Checks if `value` is less than or equal to `other`. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to compare. + * @param {*} other The other value to compare. + * @returns {boolean} Returns `true` if `value` is less than or equal to `other`, else `false`. + * @example + * + * _.lte(1, 3); + * // => true + * + * _.lte(3, 3); + * // => true + * + * _.lte(3, 1); + * // => false + */ + function lte(value, other) { + return value <= other; + } + + /** + * Converts `value` to an array. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to convert. + * @returns {Array} Returns the converted array. + * @example + * + * (function() { + * return _.toArray(arguments).slice(1); + * }(1, 2, 3)); + * // => [2, 3] + */ + function toArray(value) { + var length = value ? getLength(value) : 0; + if (!isLength(length)) { + return values(value); + } + if (!length) { + return []; + } + return (lodash.support.unindexedChars && isString(value)) + ? value.split('') + : arrayCopy(value); + } + + /** + * Converts `value` to a plain object flattening inherited enumerable + * properties of `value` to own properties of the plain object. + * + * @static + * @memberOf _ + * @category Lang + * @param {*} value The value to convert. + * @returns {Object} Returns the converted plain object. + * @example + * + * function Foo() { + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.assign({ 'a': 1 }, new Foo); + * // => { 'a': 1, 'b': 2 } + * + * _.assign({ 'a': 1 }, _.toPlainObject(new Foo)); + * // => { 'a': 1, 'b': 2, 'c': 3 } + */ + function toPlainObject(value) { + return baseCopy(value, keysIn(value)); + } + + /*------------------------------------------------------------------------*/ + + /** + * Recursively merges own enumerable properties of the source object(s), that + * don't resolve to `undefined` into the destination object. Subsequent sources + * overwrite property assignments of previous sources. If `customizer` is + * provided it's invoked to produce the merged values of the destination and + * source properties. If `customizer` returns `undefined` merging is handled + * by the method instead. The `customizer` is bound to `thisArg` and invoked + * with five arguments: (objectValue, sourceValue, key, object, source). + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The destination object. + * @param {...Object} [sources] The source objects. + * @param {Function} [customizer] The function to customize assigned values. + * @param {*} [thisArg] The `this` binding of `customizer`. + * @returns {Object} Returns `object`. + * @example + * + * var users = { + * 'data': [{ 'user': 'barney' }, { 'user': 'fred' }] + * }; + * + * var ages = { + * 'data': [{ 'age': 36 }, { 'age': 40 }] + * }; + * + * _.merge(users, ages); + * // => { 'data': [{ 'user': 'barney', 'age': 36 }, { 'user': 'fred', 'age': 40 }] } + * + * // using a customizer callback + * var object = { + * 'fruits': ['apple'], + * 'vegetables': ['beet'] + * }; + * + * var other = { + * 'fruits': ['banana'], + * 'vegetables': ['carrot'] + * }; + * + * _.merge(object, other, function(a, b) { + * if (_.isArray(a)) { + * return a.concat(b); + * } + * }); + * // => { 'fruits': ['apple', 'banana'], 'vegetables': ['beet', 'carrot'] } + */ + var merge = createAssigner(baseMerge); + + /** + * Assigns own enumerable properties of source object(s) to the destination + * object. Subsequent sources overwrite property assignments of previous sources. + * If `customizer` is provided it's invoked to produce the assigned values. + * The `customizer` is bound to `thisArg` and invoked with five arguments: + * (objectValue, sourceValue, key, object, source). + * + * **Note:** This method mutates `object` and is based on + * [`Object.assign`](http://ecma-international.org/ecma-262/6.0/#sec-object.assign). + * + * @static + * @memberOf _ + * @alias extend + * @category Object + * @param {Object} object The destination object. + * @param {...Object} [sources] The source objects. + * @param {Function} [customizer] The function to customize assigned values. + * @param {*} [thisArg] The `this` binding of `customizer`. + * @returns {Object} Returns `object`. + * @example + * + * _.assign({ 'user': 'barney' }, { 'age': 40 }, { 'user': 'fred' }); + * // => { 'user': 'fred', 'age': 40 } + * + * // using a customizer callback + * var defaults = _.partialRight(_.assign, function(value, other) { + * return _.isUndefined(value) ? other : value; + * }); + * + * defaults({ 'user': 'barney' }, { 'age': 36 }, { 'user': 'fred' }); + * // => { 'user': 'barney', 'age': 36 } + */ + var assign = createAssigner(function(object, source, customizer) { + return customizer + ? assignWith(object, source, customizer) + : baseAssign(object, source); + }); + + /** + * Creates an object that inherits from the given `prototype` object. If a + * `properties` object is provided its own enumerable properties are assigned + * to the created object. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} prototype The object to inherit from. + * @param {Object} [properties] The properties to assign to the object. + * @param- {Object} [guard] Enables use as a callback for functions like `_.map`. + * @returns {Object} Returns the new object. + * @example + * + * function Shape() { + * this.x = 0; + * this.y = 0; + * } + * + * function Circle() { + * Shape.call(this); + * } + * + * Circle.prototype = _.create(Shape.prototype, { + * 'constructor': Circle + * }); + * + * var circle = new Circle; + * circle instanceof Circle; + * // => true + * + * circle instanceof Shape; + * // => true + */ + function create(prototype, properties, guard) { + var result = baseCreate(prototype); + if (guard && isIterateeCall(prototype, properties, guard)) { + properties = undefined; + } + return properties ? baseAssign(result, properties) : result; + } + + /** + * Assigns own enumerable properties of source object(s) to the destination + * object for all destination properties that resolve to `undefined`. Once a + * property is set, additional values of the same property are ignored. + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The destination object. + * @param {...Object} [sources] The source objects. + * @returns {Object} Returns `object`. + * @example + * + * _.defaults({ 'user': 'barney' }, { 'age': 36 }, { 'user': 'fred' }); + * // => { 'user': 'barney', 'age': 36 } + */ + var defaults = createDefaults(assign, assignDefaults); + + /** + * This method is like `_.defaults` except that it recursively assigns + * default properties. + * + * **Note:** This method mutates `object`. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The destination object. + * @param {...Object} [sources] The source objects. + * @returns {Object} Returns `object`. + * @example + * + * _.defaultsDeep({ 'user': { 'name': 'barney' } }, { 'user': { 'name': 'fred', 'age': 36 } }); + * // => { 'user': { 'name': 'barney', 'age': 36 } } + * + */ + var defaultsDeep = createDefaults(merge, mergeDefaults); + + /** + * This method is like `_.find` except that it returns the key of the first + * element `predicate` returns truthy for instead of the element itself. + * + * If a property name is provided for `predicate` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `predicate` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to search. + * @param {Function|Object|string} [predicate=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `predicate`. + * @returns {string|undefined} Returns the key of the matched element, else `undefined`. + * @example + * + * var users = { + * 'barney': { 'age': 36, 'active': true }, + * 'fred': { 'age': 40, 'active': false }, + * 'pebbles': { 'age': 1, 'active': true } + * }; + * + * _.findKey(users, function(chr) { + * return chr.age < 40; + * }); + * // => 'barney' (iteration order is not guaranteed) + * + * // using the `_.matches` callback shorthand + * _.findKey(users, { 'age': 1, 'active': true }); + * // => 'pebbles' + * + * // using the `_.matchesProperty` callback shorthand + * _.findKey(users, 'active', false); + * // => 'fred' + * + * // using the `_.property` callback shorthand + * _.findKey(users, 'active'); + * // => 'barney' + */ + var findKey = createFindKey(baseForOwn); + + /** + * This method is like `_.findKey` except that it iterates over elements of + * a collection in the opposite order. + * + * If a property name is provided for `predicate` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `predicate` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to search. + * @param {Function|Object|string} [predicate=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `predicate`. + * @returns {string|undefined} Returns the key of the matched element, else `undefined`. + * @example + * + * var users = { + * 'barney': { 'age': 36, 'active': true }, + * 'fred': { 'age': 40, 'active': false }, + * 'pebbles': { 'age': 1, 'active': true } + * }; + * + * _.findLastKey(users, function(chr) { + * return chr.age < 40; + * }); + * // => returns `pebbles` assuming `_.findKey` returns `barney` + * + * // using the `_.matches` callback shorthand + * _.findLastKey(users, { 'age': 36, 'active': true }); + * // => 'barney' + * + * // using the `_.matchesProperty` callback shorthand + * _.findLastKey(users, 'active', false); + * // => 'fred' + * + * // using the `_.property` callback shorthand + * _.findLastKey(users, 'active'); + * // => 'pebbles' + */ + var findLastKey = createFindKey(baseForOwnRight); + + /** + * Iterates over own and inherited enumerable properties of an object invoking + * `iteratee` for each property. The `iteratee` is bound to `thisArg` and invoked + * with three arguments: (value, key, object). Iteratee functions may exit + * iteration early by explicitly returning `false`. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {Object} Returns `object`. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.forIn(new Foo, function(value, key) { + * console.log(key); + * }); + * // => logs 'a', 'b', and 'c' (iteration order is not guaranteed) + */ + var forIn = createForIn(baseFor); + + /** + * This method is like `_.forIn` except that it iterates over properties of + * `object` in the opposite order. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {Object} Returns `object`. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.forInRight(new Foo, function(value, key) { + * console.log(key); + * }); + * // => logs 'c', 'b', and 'a' assuming `_.forIn ` logs 'a', 'b', and 'c' + */ + var forInRight = createForIn(baseForRight); + + /** + * Iterates over own enumerable properties of an object invoking `iteratee` + * for each property. The `iteratee` is bound to `thisArg` and invoked with + * three arguments: (value, key, object). Iteratee functions may exit iteration + * early by explicitly returning `false`. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {Object} Returns `object`. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.forOwn(new Foo, function(value, key) { + * console.log(key); + * }); + * // => logs 'a' and 'b' (iteration order is not guaranteed) + */ + var forOwn = createForOwn(baseForOwn); + + /** + * This method is like `_.forOwn` except that it iterates over properties of + * `object` in the opposite order. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {Object} Returns `object`. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.forOwnRight(new Foo, function(value, key) { + * console.log(key); + * }); + * // => logs 'b' and 'a' assuming `_.forOwn` logs 'a' and 'b' + */ + var forOwnRight = createForOwn(baseForOwnRight); + + /** + * Creates an array of function property names from all enumerable properties, + * own and inherited, of `object`. + * + * @static + * @memberOf _ + * @alias methods + * @category Object + * @param {Object} object The object to inspect. + * @returns {Array} Returns the new array of property names. + * @example + * + * _.functions(_); + * // => ['after', 'ary', 'assign', ...] + */ + function functions(object) { + return baseFunctions(object, keysIn(object)); + } + + /** + * Gets the property value at `path` of `object`. If the resolved value is + * `undefined` the `defaultValue` is used in its place. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @param {Array|string} path The path of the property to get. + * @param {*} [defaultValue] The value returned if the resolved value is `undefined`. + * @returns {*} Returns the resolved value. + * @example + * + * var object = { 'a': [{ 'b': { 'c': 3 } }] }; + * + * _.get(object, 'a[0].b.c'); + * // => 3 + * + * _.get(object, ['a', '0', 'b', 'c']); + * // => 3 + * + * _.get(object, 'a.b.c', 'default'); + * // => 'default' + */ + function get(object, path, defaultValue) { + var result = object == null ? undefined : baseGet(object, toPath(path), (path + '')); + return result === undefined ? defaultValue : result; + } + + /** + * Checks if `path` is a direct property. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @param {Array|string} path The path to check. + * @returns {boolean} Returns `true` if `path` is a direct property, else `false`. + * @example + * + * var object = { 'a': { 'b': { 'c': 3 } } }; + * + * _.has(object, 'a'); + * // => true + * + * _.has(object, 'a.b.c'); + * // => true + * + * _.has(object, ['a', 'b', 'c']); + * // => true + */ + function has(object, path) { + if (object == null) { + return false; + } + var result = hasOwnProperty.call(object, path); + if (!result && !isKey(path)) { + path = toPath(path); + object = path.length == 1 ? object : baseGet(object, baseSlice(path, 0, -1)); + if (object == null) { + return false; + } + path = last(path); + result = hasOwnProperty.call(object, path); + } + return result || (isLength(object.length) && isIndex(path, object.length) && + (isArray(object) || isArguments(object) || isString(object))); + } + + /** + * Creates an object composed of the inverted keys and values of `object`. + * If `object` contains duplicate values, subsequent values overwrite property + * assignments of previous values unless `multiValue` is `true`. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to invert. + * @param {boolean} [multiValue] Allow multiple values per key. + * @param- {Object} [guard] Enables use as a callback for functions like `_.map`. + * @returns {Object} Returns the new inverted object. + * @example + * + * var object = { 'a': 1, 'b': 2, 'c': 1 }; + * + * _.invert(object); + * // => { '1': 'c', '2': 'b' } + * + * // with `multiValue` + * _.invert(object, true); + * // => { '1': ['a', 'c'], '2': ['b'] } + */ + function invert(object, multiValue, guard) { + if (guard && isIterateeCall(object, multiValue, guard)) { + multiValue = undefined; + } + var index = -1, + props = keys(object), + length = props.length, + result = {}; + + while (++index < length) { + var key = props[index], + value = object[key]; + + if (multiValue) { + if (hasOwnProperty.call(result, value)) { + result[value].push(key); + } else { + result[value] = [key]; + } + } + else { + result[value] = key; + } + } + return result; + } + + /** + * Creates an array of the own enumerable property names of `object`. + * + * **Note:** Non-object values are coerced to objects. See the + * [ES spec](http://ecma-international.org/ecma-262/6.0/#sec-object.keys) + * for more details. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.keys(new Foo); + * // => ['a', 'b'] (iteration order is not guaranteed) + * + * _.keys('hi'); + * // => ['0', '1'] + */ + var keys = !nativeKeys ? shimKeys : function(object) { + var Ctor = object == null ? undefined : object.constructor; + if ((typeof Ctor == 'function' && Ctor.prototype === object) || + (typeof object == 'function' ? lodash.support.enumPrototypes : isArrayLike(object))) { + return shimKeys(object); + } + return isObject(object) ? nativeKeys(object) : []; + }; + + /** + * Creates an array of the own and inherited enumerable property names of `object`. + * + * **Note:** Non-object values are coerced to objects. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property names. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.keysIn(new Foo); + * // => ['a', 'b', 'c'] (iteration order is not guaranteed) + */ + function keysIn(object) { + if (object == null) { + return []; + } + if (!isObject(object)) { + object = Object(object); + } + var length = object.length, + support = lodash.support; + + length = (length && isLength(length) && + (isArray(object) || isArguments(object) || isString(object)) && length) || 0; + + var Ctor = object.constructor, + index = -1, + proto = (isFunction(Ctor) && Ctor.prototype) || objectProto, + isProto = proto === object, + result = Array(length), + skipIndexes = length > 0, + skipErrorProps = support.enumErrorProps && (object === errorProto || object instanceof Error), + skipProto = support.enumPrototypes && isFunction(object); + + while (++index < length) { + result[index] = (index + ''); + } + // lodash skips the `constructor` property when it infers it's iterating + // over a `prototype` object because IE < 9 can't set the `[[Enumerable]]` + // attribute of an existing property and the `constructor` property of a + // prototype defaults to non-enumerable. + for (var key in object) { + if (!(skipProto && key == 'prototype') && + !(skipErrorProps && (key == 'message' || key == 'name')) && + !(skipIndexes && isIndex(key, length)) && + !(key == 'constructor' && (isProto || !hasOwnProperty.call(object, key)))) { + result.push(key); + } + } + if (support.nonEnumShadows && object !== objectProto) { + var tag = object === stringProto ? stringTag : (object === errorProto ? errorTag : objToString.call(object)), + nonEnums = nonEnumProps[tag] || nonEnumProps[objectTag]; + + if (tag == objectTag) { + proto = objectProto; + } + length = shadowProps.length; + while (length--) { + key = shadowProps[length]; + var nonEnum = nonEnums[key]; + if (!(isProto && nonEnum) && + (nonEnum ? hasOwnProperty.call(object, key) : object[key] !== proto[key])) { + result.push(key); + } + } + } + return result; + } + + /** + * The opposite of `_.mapValues`; this method creates an object with the + * same values as `object` and keys generated by running each own enumerable + * property of `object` through `iteratee`. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function|Object|string} [iteratee=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {Object} Returns the new mapped object. + * @example + * + * _.mapKeys({ 'a': 1, 'b': 2 }, function(value, key) { + * return key + value; + * }); + * // => { 'a1': 1, 'b2': 2 } + */ + var mapKeys = createObjectMapper(true); + + /** + * Creates an object with the same keys as `object` and values generated by + * running each own enumerable property of `object` through `iteratee`. The + * iteratee function is bound to `thisArg` and invoked with three arguments: + * (value, key, object). + * + * If a property name is provided for `iteratee` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `iteratee` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to iterate over. + * @param {Function|Object|string} [iteratee=_.identity] The function invoked + * per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {Object} Returns the new mapped object. + * @example + * + * _.mapValues({ 'a': 1, 'b': 2 }, function(n) { + * return n * 3; + * }); + * // => { 'a': 3, 'b': 6 } + * + * var users = { + * 'fred': { 'user': 'fred', 'age': 40 }, + * 'pebbles': { 'user': 'pebbles', 'age': 1 } + * }; + * + * // using the `_.property` callback shorthand + * _.mapValues(users, 'age'); + * // => { 'fred': 40, 'pebbles': 1 } (iteration order is not guaranteed) + */ + var mapValues = createObjectMapper(); + + /** + * The opposite of `_.pick`; this method creates an object composed of the + * own and inherited enumerable properties of `object` that are not omitted. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The source object. + * @param {Function|...(string|string[])} [predicate] The function invoked per + * iteration or property names to omit, specified as individual property + * names or arrays of property names. + * @param {*} [thisArg] The `this` binding of `predicate`. + * @returns {Object} Returns the new object. + * @example + * + * var object = { 'user': 'fred', 'age': 40 }; + * + * _.omit(object, 'age'); + * // => { 'user': 'fred' } + * + * _.omit(object, _.isNumber); + * // => { 'user': 'fred' } + */ + var omit = restParam(function(object, props) { + if (object == null) { + return {}; + } + if (typeof props[0] != 'function') { + var props = arrayMap(baseFlatten(props), String); + return pickByArray(object, baseDifference(keysIn(object), props)); + } + var predicate = bindCallback(props[0], props[1], 3); + return pickByCallback(object, function(value, key, object) { + return !predicate(value, key, object); + }); + }); + + /** + * Creates a two dimensional array of the key-value pairs for `object`, + * e.g. `[[key1, value1], [key2, value2]]`. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the new array of key-value pairs. + * @example + * + * _.pairs({ 'barney': 36, 'fred': 40 }); + * // => [['barney', 36], ['fred', 40]] (iteration order is not guaranteed) + */ + function pairs(object) { + object = toObject(object); + + var index = -1, + props = keys(object), + length = props.length, + result = Array(length); + + while (++index < length) { + var key = props[index]; + result[index] = [key, object[key]]; + } + return result; + } + + /** + * Creates an object composed of the picked `object` properties. Property + * names may be specified as individual arguments or as arrays of property + * names. If `predicate` is provided it's invoked for each property of `object` + * picking the properties `predicate` returns truthy for. The predicate is + * bound to `thisArg` and invoked with three arguments: (value, key, object). + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The source object. + * @param {Function|...(string|string[])} [predicate] The function invoked per + * iteration or property names to pick, specified as individual property + * names or arrays of property names. + * @param {*} [thisArg] The `this` binding of `predicate`. + * @returns {Object} Returns the new object. + * @example + * + * var object = { 'user': 'fred', 'age': 40 }; + * + * _.pick(object, 'user'); + * // => { 'user': 'fred' } + * + * _.pick(object, _.isString); + * // => { 'user': 'fred' } + */ + var pick = restParam(function(object, props) { + if (object == null) { + return {}; + } + return typeof props[0] == 'function' + ? pickByCallback(object, bindCallback(props[0], props[1], 3)) + : pickByArray(object, baseFlatten(props)); + }); + + /** + * This method is like `_.get` except that if the resolved value is a function + * it's invoked with the `this` binding of its parent object and its result + * is returned. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @param {Array|string} path The path of the property to resolve. + * @param {*} [defaultValue] The value returned if the resolved value is `undefined`. + * @returns {*} Returns the resolved value. + * @example + * + * var object = { 'a': [{ 'b': { 'c1': 3, 'c2': _.constant(4) } }] }; + * + * _.result(object, 'a[0].b.c1'); + * // => 3 + * + * _.result(object, 'a[0].b.c2'); + * // => 4 + * + * _.result(object, 'a.b.c', 'default'); + * // => 'default' + * + * _.result(object, 'a.b.c', _.constant('default')); + * // => 'default' + */ + function result(object, path, defaultValue) { + var result = object == null ? undefined : toObject(object)[path]; + if (result === undefined) { + if (object != null && !isKey(path, object)) { + path = toPath(path); + object = path.length == 1 ? object : baseGet(object, baseSlice(path, 0, -1)); + result = object == null ? undefined : toObject(object)[last(path)]; + } + result = result === undefined ? defaultValue : result; + } + return isFunction(result) ? result.call(object) : result; + } + + /** + * Sets the property value of `path` on `object`. If a portion of `path` + * does not exist it's created. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to augment. + * @param {Array|string} path The path of the property to set. + * @param {*} value The value to set. + * @returns {Object} Returns `object`. + * @example + * + * var object = { 'a': [{ 'b': { 'c': 3 } }] }; + * + * _.set(object, 'a[0].b.c', 4); + * console.log(object.a[0].b.c); + * // => 4 + * + * _.set(object, 'x[0].y.z', 5); + * console.log(object.x[0].y.z); + * // => 5 + */ + function set(object, path, value) { + if (object == null) { + return object; + } + var pathKey = (path + ''); + path = (object[pathKey] != null || isKey(path, object)) ? [pathKey] : toPath(path); + + var index = -1, + length = path.length, + lastIndex = length - 1, + nested = object; + + while (nested != null && ++index < length) { + var key = path[index]; + if (isObject(nested)) { + if (index == lastIndex) { + nested[key] = value; + } else if (nested[key] == null) { + nested[key] = isIndex(path[index + 1]) ? [] : {}; + } + } + nested = nested[key]; + } + return object; + } + + /** + * An alternative to `_.reduce`; this method transforms `object` to a new + * `accumulator` object which is the result of running each of its own enumerable + * properties through `iteratee`, with each invocation potentially mutating + * the `accumulator` object. The `iteratee` is bound to `thisArg` and invoked + * with four arguments: (accumulator, value, key, object). Iteratee functions + * may exit iteration early by explicitly returning `false`. + * + * @static + * @memberOf _ + * @category Object + * @param {Array|Object} object The object to iterate over. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @param {*} [accumulator] The custom accumulator value. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {*} Returns the accumulated value. + * @example + * + * _.transform([2, 3, 4], function(result, n) { + * result.push(n *= n); + * return n % 2 == 0; + * }); + * // => [4, 9] + * + * _.transform({ 'a': 1, 'b': 2 }, function(result, n, key) { + * result[key] = n * 3; + * }); + * // => { 'a': 3, 'b': 6 } + */ + function transform(object, iteratee, accumulator, thisArg) { + var isArr = isArray(object) || isTypedArray(object); + iteratee = getCallback(iteratee, thisArg, 4); + + if (accumulator == null) { + if (isArr || isObject(object)) { + var Ctor = object.constructor; + if (isArr) { + accumulator = isArray(object) ? new Ctor : []; + } else { + accumulator = baseCreate(isFunction(Ctor) ? Ctor.prototype : undefined); + } + } else { + accumulator = {}; + } + } + (isArr ? arrayEach : baseForOwn)(object, function(value, index, object) { + return iteratee(accumulator, value, index, object); + }); + return accumulator; + } + + /** + * Creates an array of the own enumerable property values of `object`. + * + * **Note:** Non-object values are coerced to objects. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property values. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.values(new Foo); + * // => [1, 2] (iteration order is not guaranteed) + * + * _.values('hi'); + * // => ['h', 'i'] + */ + function values(object) { + return baseValues(object, keys(object)); + } + + /** + * Creates an array of the own and inherited enumerable property values + * of `object`. + * + * **Note:** Non-object values are coerced to objects. + * + * @static + * @memberOf _ + * @category Object + * @param {Object} object The object to query. + * @returns {Array} Returns the array of property values. + * @example + * + * function Foo() { + * this.a = 1; + * this.b = 2; + * } + * + * Foo.prototype.c = 3; + * + * _.valuesIn(new Foo); + * // => [1, 2, 3] (iteration order is not guaranteed) + */ + function valuesIn(object) { + return baseValues(object, keysIn(object)); + } + + /*------------------------------------------------------------------------*/ + + /** + * Checks if `n` is between `start` and up to but not including, `end`. If + * `end` is not specified it's set to `start` with `start` then set to `0`. + * + * @static + * @memberOf _ + * @category Number + * @param {number} n The number to check. + * @param {number} [start=0] The start of the range. + * @param {number} end The end of the range. + * @returns {boolean} Returns `true` if `n` is in the range, else `false`. + * @example + * + * _.inRange(3, 2, 4); + * // => true + * + * _.inRange(4, 8); + * // => true + * + * _.inRange(4, 2); + * // => false + * + * _.inRange(2, 2); + * // => false + * + * _.inRange(1.2, 2); + * // => true + * + * _.inRange(5.2, 4); + * // => false + */ + function inRange(value, start, end) { + start = +start || 0; + if (end === undefined) { + end = start; + start = 0; + } else { + end = +end || 0; + } + return value >= nativeMin(start, end) && value < nativeMax(start, end); + } + + /** + * Produces a random number between `min` and `max` (inclusive). If only one + * argument is provided a number between `0` and the given number is returned. + * If `floating` is `true`, or either `min` or `max` are floats, a floating-point + * number is returned instead of an integer. + * + * @static + * @memberOf _ + * @category Number + * @param {number} [min=0] The minimum possible value. + * @param {number} [max=1] The maximum possible value. + * @param {boolean} [floating] Specify returning a floating-point number. + * @returns {number} Returns the random number. + * @example + * + * _.random(0, 5); + * // => an integer between 0 and 5 + * + * _.random(5); + * // => also an integer between 0 and 5 + * + * _.random(5, true); + * // => a floating-point number between 0 and 5 + * + * _.random(1.2, 5.2); + * // => a floating-point number between 1.2 and 5.2 + */ + function random(min, max, floating) { + if (floating && isIterateeCall(min, max, floating)) { + max = floating = undefined; + } + var noMin = min == null, + noMax = max == null; + + if (floating == null) { + if (noMax && typeof min == 'boolean') { + floating = min; + min = 1; + } + else if (typeof max == 'boolean') { + floating = max; + noMax = true; + } + } + if (noMin && noMax) { + max = 1; + noMax = false; + } + min = +min || 0; + if (noMax) { + max = min; + min = 0; + } else { + max = +max || 0; + } + if (floating || min % 1 || max % 1) { + var rand = nativeRandom(); + return nativeMin(min + (rand * (max - min + parseFloat('1e-' + ((rand + '').length - 1)))), max); + } + return baseRandom(min, max); + } + + /*------------------------------------------------------------------------*/ + + /** + * Converts `string` to [camel case](https://en.wikipedia.org/wiki/CamelCase). + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the camel cased string. + * @example + * + * _.camelCase('Foo Bar'); + * // => 'fooBar' + * + * _.camelCase('--foo-bar'); + * // => 'fooBar' + * + * _.camelCase('__foo_bar__'); + * // => 'fooBar' + */ + var camelCase = createCompounder(function(result, word, index) { + word = word.toLowerCase(); + return result + (index ? (word.charAt(0).toUpperCase() + word.slice(1)) : word); + }); + + /** + * Capitalizes the first character of `string`. + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to capitalize. + * @returns {string} Returns the capitalized string. + * @example + * + * _.capitalize('fred'); + * // => 'Fred' + */ + function capitalize(string) { + string = baseToString(string); + return string && (string.charAt(0).toUpperCase() + string.slice(1)); + } + + /** + * Deburrs `string` by converting [latin-1 supplementary letters](https://en.wikipedia.org/wiki/Latin-1_Supplement_(Unicode_block)#Character_table) + * to basic latin letters and removing [combining diacritical marks](https://en.wikipedia.org/wiki/Combining_Diacritical_Marks). + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to deburr. + * @returns {string} Returns the deburred string. + * @example + * + * _.deburr('déjà vu'); + * // => 'deja vu' + */ + function deburr(string) { + string = baseToString(string); + return string && string.replace(reLatin1, deburrLetter).replace(reComboMark, ''); + } + + /** + * Checks if `string` ends with the given target string. + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to search. + * @param {string} [target] The string to search for. + * @param {number} [position=string.length] The position to search from. + * @returns {boolean} Returns `true` if `string` ends with `target`, else `false`. + * @example + * + * _.endsWith('abc', 'c'); + * // => true + * + * _.endsWith('abc', 'b'); + * // => false + * + * _.endsWith('abc', 'b', 2); + * // => true + */ + function endsWith(string, target, position) { + string = baseToString(string); + target = (target + ''); + + var length = string.length; + position = position === undefined + ? length + : nativeMin(position < 0 ? 0 : (+position || 0), length); + + position -= target.length; + return position >= 0 && string.indexOf(target, position) == position; + } + + /** + * Converts the characters "&", "<", ">", '"', "'", and "\`", in `string` to + * their corresponding HTML entities. + * + * **Note:** No other characters are escaped. To escape additional characters + * use a third-party library like [_he_](https://mths.be/he). + * + * Though the ">" character is escaped for symmetry, characters like + * ">" and "/" don't need escaping in HTML and have no special meaning + * unless they're part of a tag or unquoted attribute value. + * See [Mathias Bynens's article](https://mathiasbynens.be/notes/ambiguous-ampersands) + * (under "semi-related fun fact") for more details. + * + * Backticks are escaped because in Internet Explorer < 9, they can break out + * of attribute values or HTML comments. See [#59](https://html5sec.org/#59), + * [#102](https://html5sec.org/#102), [#108](https://html5sec.org/#108), and + * [#133](https://html5sec.org/#133) of the [HTML5 Security Cheatsheet](https://html5sec.org/) + * for more details. + * + * When working with HTML you should always [quote attribute values](http://wonko.com/post/html-escaping) + * to reduce XSS vectors. + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to escape. + * @returns {string} Returns the escaped string. + * @example + * + * _.escape('fred, barney, & pebbles'); + * // => 'fred, barney, & pebbles' + */ + function escape(string) { + // Reset `lastIndex` because in IE < 9 `String#replace` does not. + string = baseToString(string); + return (string && reHasUnescapedHtml.test(string)) + ? string.replace(reUnescapedHtml, escapeHtmlChar) + : string; + } + + /** + * Escapes the `RegExp` special characters "\", "/", "^", "$", ".", "|", "?", + * "*", "+", "(", ")", "[", "]", "{" and "}" in `string`. + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to escape. + * @returns {string} Returns the escaped string. + * @example + * + * _.escapeRegExp('[lodash](https://lodash.com/)'); + * // => '\[lodash\]\(https:\/\/lodash\.com\/\)' + */ + function escapeRegExp(string) { + string = baseToString(string); + return (string && reHasRegExpChars.test(string)) + ? string.replace(reRegExpChars, escapeRegExpChar) + : (string || '(?:)'); + } + + /** + * Converts `string` to [kebab case](https://en.wikipedia.org/wiki/Letter_case#Special_case_styles). + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the kebab cased string. + * @example + * + * _.kebabCase('Foo Bar'); + * // => 'foo-bar' + * + * _.kebabCase('fooBar'); + * // => 'foo-bar' + * + * _.kebabCase('__foo_bar__'); + * // => 'foo-bar' + */ + var kebabCase = createCompounder(function(result, word, index) { + return result + (index ? '-' : '') + word.toLowerCase(); + }); + + /** + * Pads `string` on the left and right sides if it's shorter than `length`. + * Padding characters are truncated if they can't be evenly divided by `length`. + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to pad. + * @param {number} [length=0] The padding length. + * @param {string} [chars=' '] The string used as padding. + * @returns {string} Returns the padded string. + * @example + * + * _.pad('abc', 8); + * // => ' abc ' + * + * _.pad('abc', 8, '_-'); + * // => '_-abc_-_' + * + * _.pad('abc', 3); + * // => 'abc' + */ + function pad(string, length, chars) { + string = baseToString(string); + length = +length; + + var strLength = string.length; + if (strLength >= length || !nativeIsFinite(length)) { + return string; + } + var mid = (length - strLength) / 2, + leftLength = nativeFloor(mid), + rightLength = nativeCeil(mid); + + chars = createPadding('', rightLength, chars); + return chars.slice(0, leftLength) + string + chars; + } + + /** + * Pads `string` on the left side if it's shorter than `length`. Padding + * characters are truncated if they exceed `length`. + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to pad. + * @param {number} [length=0] The padding length. + * @param {string} [chars=' '] The string used as padding. + * @returns {string} Returns the padded string. + * @example + * + * _.padLeft('abc', 6); + * // => ' abc' + * + * _.padLeft('abc', 6, '_-'); + * // => '_-_abc' + * + * _.padLeft('abc', 3); + * // => 'abc' + */ + var padLeft = createPadDir(); + + /** + * Pads `string` on the right side if it's shorter than `length`. Padding + * characters are truncated if they exceed `length`. + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to pad. + * @param {number} [length=0] The padding length. + * @param {string} [chars=' '] The string used as padding. + * @returns {string} Returns the padded string. + * @example + * + * _.padRight('abc', 6); + * // => 'abc ' + * + * _.padRight('abc', 6, '_-'); + * // => 'abc_-_' + * + * _.padRight('abc', 3); + * // => 'abc' + */ + var padRight = createPadDir(true); + + /** + * Converts `string` to an integer of the specified radix. If `radix` is + * `undefined` or `0`, a `radix` of `10` is used unless `value` is a hexadecimal, + * in which case a `radix` of `16` is used. + * + * **Note:** This method aligns with the [ES5 implementation](https://es5.github.io/#E) + * of `parseInt`. + * + * @static + * @memberOf _ + * @category String + * @param {string} string The string to convert. + * @param {number} [radix] The radix to interpret `value` by. + * @param- {Object} [guard] Enables use as a callback for functions like `_.map`. + * @returns {number} Returns the converted integer. + * @example + * + * _.parseInt('08'); + * // => 8 + * + * _.map(['6', '08', '10'], _.parseInt); + * // => [6, 8, 10] + */ + function parseInt(string, radix, guard) { + // Firefox < 21 and Opera < 15 follow ES3 for `parseInt`. + // Chrome fails to trim leading <BOM> whitespace characters. + // See https://code.google.com/p/v8/issues/detail?id=3109 for more details. + if (guard ? isIterateeCall(string, radix, guard) : radix == null) { + radix = 0; + } else if (radix) { + radix = +radix; + } + string = trim(string); + return nativeParseInt(string, radix || (reHasHexPrefix.test(string) ? 16 : 10)); + } + + /** + * Repeats the given string `n` times. + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to repeat. + * @param {number} [n=0] The number of times to repeat the string. + * @returns {string} Returns the repeated string. + * @example + * + * _.repeat('*', 3); + * // => '***' + * + * _.repeat('abc', 2); + * // => 'abcabc' + * + * _.repeat('abc', 0); + * // => '' + */ + function repeat(string, n) { + var result = ''; + string = baseToString(string); + n = +n; + if (n < 1 || !string || !nativeIsFinite(n)) { + return result; + } + // Leverage the exponentiation by squaring algorithm for a faster repeat. + // See https://en.wikipedia.org/wiki/Exponentiation_by_squaring for more details. + do { + if (n % 2) { + result += string; + } + n = nativeFloor(n / 2); + string += string; + } while (n); + + return result; + } + + /** + * Converts `string` to [snake case](https://en.wikipedia.org/wiki/Snake_case). + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the snake cased string. + * @example + * + * _.snakeCase('Foo Bar'); + * // => 'foo_bar' + * + * _.snakeCase('fooBar'); + * // => 'foo_bar' + * + * _.snakeCase('--foo-bar'); + * // => 'foo_bar' + */ + var snakeCase = createCompounder(function(result, word, index) { + return result + (index ? '_' : '') + word.toLowerCase(); + }); + + /** + * Converts `string` to [start case](https://en.wikipedia.org/wiki/Letter_case#Stylistic_or_specialised_usage). + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to convert. + * @returns {string} Returns the start cased string. + * @example + * + * _.startCase('--foo-bar'); + * // => 'Foo Bar' + * + * _.startCase('fooBar'); + * // => 'Foo Bar' + * + * _.startCase('__foo_bar__'); + * // => 'Foo Bar' + */ + var startCase = createCompounder(function(result, word, index) { + return result + (index ? ' ' : '') + (word.charAt(0).toUpperCase() + word.slice(1)); + }); + + /** + * Checks if `string` starts with the given target string. + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to search. + * @param {string} [target] The string to search for. + * @param {number} [position=0] The position to search from. + * @returns {boolean} Returns `true` if `string` starts with `target`, else `false`. + * @example + * + * _.startsWith('abc', 'a'); + * // => true + * + * _.startsWith('abc', 'b'); + * // => false + * + * _.startsWith('abc', 'b', 1); + * // => true + */ + function startsWith(string, target, position) { + string = baseToString(string); + position = position == null + ? 0 + : nativeMin(position < 0 ? 0 : (+position || 0), string.length); + + return string.lastIndexOf(target, position) == position; + } + + /** + * Creates a compiled template function that can interpolate data properties + * in "interpolate" delimiters, HTML-escape interpolated data properties in + * "escape" delimiters, and execute JavaScript in "evaluate" delimiters. Data + * properties may be accessed as free variables in the template. If a setting + * object is provided it takes precedence over `_.templateSettings` values. + * + * **Note:** In the development build `_.template` utilizes + * [sourceURLs](http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl) + * for easier debugging. + * + * For more information on precompiling templates see + * [lodash's custom builds documentation](https://lodash.com/custom-builds). + * + * For more information on Chrome extension sandboxes see + * [Chrome's extensions documentation](https://developer.chrome.com/extensions/sandboxingEval). + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The template string. + * @param {Object} [options] The options object. + * @param {RegExp} [options.escape] The HTML "escape" delimiter. + * @param {RegExp} [options.evaluate] The "evaluate" delimiter. + * @param {Object} [options.imports] An object to import into the template as free variables. + * @param {RegExp} [options.interpolate] The "interpolate" delimiter. + * @param {string} [options.sourceURL] The sourceURL of the template's compiled source. + * @param {string} [options.variable] The data object variable name. + * @param- {Object} [otherOptions] Enables the legacy `options` param signature. + * @returns {Function} Returns the compiled template function. + * @example + * + * // using the "interpolate" delimiter to create a compiled template + * var compiled = _.template('hello <%= user %>!'); + * compiled({ 'user': 'fred' }); + * // => 'hello fred!' + * + * // using the HTML "escape" delimiter to escape data property values + * var compiled = _.template('<b><%- value %></b>'); + * compiled({ 'value': '<script>' }); + * // => '<b><script></b>' + * + * // using the "evaluate" delimiter to execute JavaScript and generate HTML + * var compiled = _.template('<% _.forEach(users, function(user) { %><li><%- user %></li><% }); %>'); + * compiled({ 'users': ['fred', 'barney'] }); + * // => '<li>fred</li><li>barney</li>' + * + * // using the internal `print` function in "evaluate" delimiters + * var compiled = _.template('<% print("hello " + user); %>!'); + * compiled({ 'user': 'barney' }); + * // => 'hello barney!' + * + * // using the ES delimiter as an alternative to the default "interpolate" delimiter + * var compiled = _.template('hello ${ user }!'); + * compiled({ 'user': 'pebbles' }); + * // => 'hello pebbles!' + * + * // using custom template delimiters + * _.templateSettings.interpolate = /{{([\s\S]+?)}}/g; + * var compiled = _.template('hello {{ user }}!'); + * compiled({ 'user': 'mustache' }); + * // => 'hello mustache!' + * + * // using backslashes to treat delimiters as plain text + * var compiled = _.template('<%= "\\<%- value %\\>" %>'); + * compiled({ 'value': 'ignored' }); + * // => '<%- value %>' + * + * // using the `imports` option to import `jQuery` as `jq` + * var text = '<% jq.each(users, function(user) { %><li><%- user %></li><% }); %>'; + * var compiled = _.template(text, { 'imports': { 'jq': jQuery } }); + * compiled({ 'users': ['fred', 'barney'] }); + * // => '<li>fred</li><li>barney</li>' + * + * // using the `sourceURL` option to specify a custom sourceURL for the template + * var compiled = _.template('hello <%= user %>!', { 'sourceURL': '/basic/greeting.jst' }); + * compiled(data); + * // => find the source of "greeting.jst" under the Sources tab or Resources panel of the web inspector + * + * // using the `variable` option to ensure a with-statement isn't used in the compiled template + * var compiled = _.template('hi <%= data.user %>!', { 'variable': 'data' }); + * compiled.source; + * // => function(data) { + * // var __t, __p = ''; + * // __p += 'hi ' + ((__t = ( data.user )) == null ? '' : __t) + '!'; + * // return __p; + * // } + * + * // using the `source` property to inline compiled templates for meaningful + * // line numbers in error messages and a stack trace + * fs.writeFileSync(path.join(cwd, 'jst.js'), '\ + * var JST = {\ + * "main": ' + _.template(mainText).source + '\ + * };\ + * '); + */ + function template(string, options, otherOptions) { + // Based on John Resig's `tmpl` implementation (http://ejohn.org/blog/javascript-micro-templating/) + // and Laura Doktorova's doT.js (https://github.com/olado/doT). + var settings = lodash.templateSettings; + + if (otherOptions && isIterateeCall(string, options, otherOptions)) { + options = otherOptions = undefined; + } + string = baseToString(string); + options = assignWith(baseAssign({}, otherOptions || options), settings, assignOwnDefaults); + + var imports = assignWith(baseAssign({}, options.imports), settings.imports, assignOwnDefaults), + importsKeys = keys(imports), + importsValues = baseValues(imports, importsKeys); + + var isEscaping, + isEvaluating, + index = 0, + interpolate = options.interpolate || reNoMatch, + source = "__p += '"; + + // Compile the regexp to match each delimiter. + var reDelimiters = RegExp( + (options.escape || reNoMatch).source + '|' + + interpolate.source + '|' + + (interpolate === reInterpolate ? reEsTemplate : reNoMatch).source + '|' + + (options.evaluate || reNoMatch).source + '|$' + , 'g'); + + // Use a sourceURL for easier debugging. + var sourceURL = '//# sourceURL=' + + ('sourceURL' in options + ? options.sourceURL + : ('lodash.templateSources[' + (++templateCounter) + ']') + ) + '\n'; + + string.replace(reDelimiters, function(match, escapeValue, interpolateValue, esTemplateValue, evaluateValue, offset) { + interpolateValue || (interpolateValue = esTemplateValue); + + // Escape characters that can't be included in string literals. + source += string.slice(index, offset).replace(reUnescapedString, escapeStringChar); + + // Replace delimiters with snippets. + if (escapeValue) { + isEscaping = true; + source += "' +\n__e(" + escapeValue + ") +\n'"; + } + if (evaluateValue) { + isEvaluating = true; + source += "';\n" + evaluateValue + ";\n__p += '"; + } + if (interpolateValue) { + source += "' +\n((__t = (" + interpolateValue + ")) == null ? '' : __t) +\n'"; + } + index = offset + match.length; + + // The JS engine embedded in Adobe products requires returning the `match` + // string in order to produce the correct `offset` value. + return match; + }); + + source += "';\n"; + + // If `variable` is not specified wrap a with-statement around the generated + // code to add the data object to the top of the scope chain. + var variable = options.variable; + if (!variable) { + source = 'with (obj) {\n' + source + '\n}\n'; + } + // Cleanup code by stripping empty strings. + source = (isEvaluating ? source.replace(reEmptyStringLeading, '') : source) + .replace(reEmptyStringMiddle, '$1') + .replace(reEmptyStringTrailing, '$1;'); + + // Frame code as the function body. + source = 'function(' + (variable || 'obj') + ') {\n' + + (variable + ? '' + : 'obj || (obj = {});\n' + ) + + "var __t, __p = ''" + + (isEscaping + ? ', __e = _.escape' + : '' + ) + + (isEvaluating + ? ', __j = Array.prototype.join;\n' + + "function print() { __p += __j.call(arguments, '') }\n" + : ';\n' + ) + + source + + 'return __p\n}'; + + var result = attempt(function() { + return Function(importsKeys, sourceURL + 'return ' + source).apply(undefined, importsValues); + }); + + // Provide the compiled function's source by its `toString` method or + // the `source` property as a convenience for inlining compiled templates. + result.source = source; + if (isError(result)) { + throw result; + } + return result; + } + + /** + * Removes leading and trailing whitespace or specified characters from `string`. + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to trim. + * @param {string} [chars=whitespace] The characters to trim. + * @param- {Object} [guard] Enables use as a callback for functions like `_.map`. + * @returns {string} Returns the trimmed string. + * @example + * + * _.trim(' abc '); + * // => 'abc' + * + * _.trim('-_-abc-_-', '_-'); + * // => 'abc' + * + * _.map([' foo ', ' bar '], _.trim); + * // => ['foo', 'bar'] + */ + function trim(string, chars, guard) { + var value = string; + string = baseToString(string); + if (!string) { + return string; + } + if (guard ? isIterateeCall(value, chars, guard) : chars == null) { + return string.slice(trimmedLeftIndex(string), trimmedRightIndex(string) + 1); + } + chars = (chars + ''); + return string.slice(charsLeftIndex(string, chars), charsRightIndex(string, chars) + 1); + } + + /** + * Removes leading whitespace or specified characters from `string`. + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to trim. + * @param {string} [chars=whitespace] The characters to trim. + * @param- {Object} [guard] Enables use as a callback for functions like `_.map`. + * @returns {string} Returns the trimmed string. + * @example + * + * _.trimLeft(' abc '); + * // => 'abc ' + * + * _.trimLeft('-_-abc-_-', '_-'); + * // => 'abc-_-' + */ + function trimLeft(string, chars, guard) { + var value = string; + string = baseToString(string); + if (!string) { + return string; + } + if (guard ? isIterateeCall(value, chars, guard) : chars == null) { + return string.slice(trimmedLeftIndex(string)); + } + return string.slice(charsLeftIndex(string, (chars + ''))); + } + + /** + * Removes trailing whitespace or specified characters from `string`. + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to trim. + * @param {string} [chars=whitespace] The characters to trim. + * @param- {Object} [guard] Enables use as a callback for functions like `_.map`. + * @returns {string} Returns the trimmed string. + * @example + * + * _.trimRight(' abc '); + * // => ' abc' + * + * _.trimRight('-_-abc-_-', '_-'); + * // => '-_-abc' + */ + function trimRight(string, chars, guard) { + var value = string; + string = baseToString(string); + if (!string) { + return string; + } + if (guard ? isIterateeCall(value, chars, guard) : chars == null) { + return string.slice(0, trimmedRightIndex(string) + 1); + } + return string.slice(0, charsRightIndex(string, (chars + '')) + 1); + } + + /** + * Truncates `string` if it's longer than the given maximum string length. + * The last characters of the truncated string are replaced with the omission + * string which defaults to "...". + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to truncate. + * @param {Object|number} [options] The options object or maximum string length. + * @param {number} [options.length=30] The maximum string length. + * @param {string} [options.omission='...'] The string to indicate text is omitted. + * @param {RegExp|string} [options.separator] The separator pattern to truncate to. + * @param- {Object} [guard] Enables use as a callback for functions like `_.map`. + * @returns {string} Returns the truncated string. + * @example + * + * _.trunc('hi-diddly-ho there, neighborino'); + * // => 'hi-diddly-ho there, neighbo...' + * + * _.trunc('hi-diddly-ho there, neighborino', 24); + * // => 'hi-diddly-ho there, n...' + * + * _.trunc('hi-diddly-ho there, neighborino', { + * 'length': 24, + * 'separator': ' ' + * }); + * // => 'hi-diddly-ho there,...' + * + * _.trunc('hi-diddly-ho there, neighborino', { + * 'length': 24, + * 'separator': /,? +/ + * }); + * // => 'hi-diddly-ho there...' + * + * _.trunc('hi-diddly-ho there, neighborino', { + * 'omission': ' [...]' + * }); + * // => 'hi-diddly-ho there, neig [...]' + */ + function trunc(string, options, guard) { + if (guard && isIterateeCall(string, options, guard)) { + options = undefined; + } + var length = DEFAULT_TRUNC_LENGTH, + omission = DEFAULT_TRUNC_OMISSION; + + if (options != null) { + if (isObject(options)) { + var separator = 'separator' in options ? options.separator : separator; + length = 'length' in options ? (+options.length || 0) : length; + omission = 'omission' in options ? baseToString(options.omission) : omission; + } else { + length = +options || 0; + } + } + string = baseToString(string); + if (length >= string.length) { + return string; + } + var end = length - omission.length; + if (end < 1) { + return omission; + } + var result = string.slice(0, end); + if (separator == null) { + return result + omission; + } + if (isRegExp(separator)) { + if (string.slice(end).search(separator)) { + var match, + newEnd, + substring = string.slice(0, end); + + if (!separator.global) { + separator = RegExp(separator.source, (reFlags.exec(separator) || '') + 'g'); + } + separator.lastIndex = 0; + while ((match = separator.exec(substring))) { + newEnd = match.index; + } + result = result.slice(0, newEnd == null ? end : newEnd); + } + } else if (string.indexOf(separator, end) != end) { + var index = result.lastIndexOf(separator); + if (index > -1) { + result = result.slice(0, index); + } + } + return result + omission; + } + + /** + * The inverse of `_.escape`; this method converts the HTML entities + * `&`, `<`, `>`, `"`, `'`, and ``` in `string` to their + * corresponding characters. + * + * **Note:** No other HTML entities are unescaped. To unescape additional HTML + * entities use a third-party library like [_he_](https://mths.be/he). + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to unescape. + * @returns {string} Returns the unescaped string. + * @example + * + * _.unescape('fred, barney, & pebbles'); + * // => 'fred, barney, & pebbles' + */ + function unescape(string) { + string = baseToString(string); + return (string && reHasEscapedHtml.test(string)) + ? string.replace(reEscapedHtml, unescapeHtmlChar) + : string; + } + + /** + * Splits `string` into an array of its words. + * + * @static + * @memberOf _ + * @category String + * @param {string} [string=''] The string to inspect. + * @param {RegExp|string} [pattern] The pattern to match words. + * @param- {Object} [guard] Enables use as a callback for functions like `_.map`. + * @returns {Array} Returns the words of `string`. + * @example + * + * _.words('fred, barney, & pebbles'); + * // => ['fred', 'barney', 'pebbles'] + * + * _.words('fred, barney, & pebbles', /[^, ]+/g); + * // => ['fred', 'barney', '&', 'pebbles'] + */ + function words(string, pattern, guard) { + if (guard && isIterateeCall(string, pattern, guard)) { + pattern = undefined; + } + string = baseToString(string); + return string.match(pattern || reWords) || []; + } + + /*------------------------------------------------------------------------*/ + + /** + * Attempts to invoke `func`, returning either the result or the caught error + * object. Any additional arguments are provided to `func` when it's invoked. + * + * @static + * @memberOf _ + * @category Utility + * @param {Function} func The function to attempt. + * @returns {*} Returns the `func` result or error object. + * @example + * + * // avoid throwing errors for invalid selectors + * var elements = _.attempt(function(selector) { + * return document.querySelectorAll(selector); + * }, '>_>'); + * + * if (_.isError(elements)) { + * elements = []; + * } + */ + var attempt = restParam(function(func, args) { + try { + return func.apply(undefined, args); + } catch(e) { + return isError(e) ? e : new Error(e); + } + }); + + /** + * Creates a function that invokes `func` with the `this` binding of `thisArg` + * and arguments of the created function. If `func` is a property name the + * created callback returns the property value for a given element. If `func` + * is an object the created callback returns `true` for elements that contain + * the equivalent object properties, otherwise it returns `false`. + * + * @static + * @memberOf _ + * @alias iteratee + * @category Utility + * @param {*} [func=_.identity] The value to convert to a callback. + * @param {*} [thisArg] The `this` binding of `func`. + * @param- {Object} [guard] Enables use as a callback for functions like `_.map`. + * @returns {Function} Returns the callback. + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36 }, + * { 'user': 'fred', 'age': 40 } + * ]; + * + * // wrap to create custom callback shorthands + * _.callback = _.wrap(_.callback, function(callback, func, thisArg) { + * var match = /^(.+?)__([gl]t)(.+)$/.exec(func); + * if (!match) { + * return callback(func, thisArg); + * } + * return function(object) { + * return match[2] == 'gt' + * ? object[match[1]] > match[3] + * : object[match[1]] < match[3]; + * }; + * }); + * + * _.filter(users, 'age__gt36'); + * // => [{ 'user': 'fred', 'age': 40 }] + */ + function callback(func, thisArg, guard) { + if (guard && isIterateeCall(func, thisArg, guard)) { + thisArg = undefined; + } + return isObjectLike(func) + ? matches(func) + : baseCallback(func, thisArg); + } + + /** + * Creates a function that returns `value`. + * + * @static + * @memberOf _ + * @category Utility + * @param {*} value The value to return from the new function. + * @returns {Function} Returns the new function. + * @example + * + * var object = { 'user': 'fred' }; + * var getter = _.constant(object); + * + * getter() === object; + * // => true + */ + function constant(value) { + return function() { + return value; + }; + } + + /** + * This method returns the first argument provided to it. + * + * @static + * @memberOf _ + * @category Utility + * @param {*} value Any value. + * @returns {*} Returns `value`. + * @example + * + * var object = { 'user': 'fred' }; + * + * _.identity(object) === object; + * // => true + */ + function identity(value) { + return value; + } + + /** + * Creates a function that performs a deep comparison between a given object + * and `source`, returning `true` if the given object has equivalent property + * values, else `false`. + * + * **Note:** This method supports comparing arrays, booleans, `Date` objects, + * numbers, `Object` objects, regexes, and strings. Objects are compared by + * their own, not inherited, enumerable properties. For comparing a single + * own or inherited property value see `_.matchesProperty`. + * + * @static + * @memberOf _ + * @category Utility + * @param {Object} source The object of property values to match. + * @returns {Function} Returns the new function. + * @example + * + * var users = [ + * { 'user': 'barney', 'age': 36, 'active': true }, + * { 'user': 'fred', 'age': 40, 'active': false } + * ]; + * + * _.filter(users, _.matches({ 'age': 40, 'active': false })); + * // => [{ 'user': 'fred', 'age': 40, 'active': false }] + */ + function matches(source) { + return baseMatches(baseClone(source, true)); + } + + /** + * Creates a function that compares the property value of `path` on a given + * object to `value`. + * + * **Note:** This method supports comparing arrays, booleans, `Date` objects, + * numbers, `Object` objects, regexes, and strings. Objects are compared by + * their own, not inherited, enumerable properties. + * + * @static + * @memberOf _ + * @category Utility + * @param {Array|string} path The path of the property to get. + * @param {*} srcValue The value to match. + * @returns {Function} Returns the new function. + * @example + * + * var users = [ + * { 'user': 'barney' }, + * { 'user': 'fred' } + * ]; + * + * _.find(users, _.matchesProperty('user', 'fred')); + * // => { 'user': 'fred' } + */ + function matchesProperty(path, srcValue) { + return baseMatchesProperty(path, baseClone(srcValue, true)); + } + + /** + * Creates a function that invokes the method at `path` on a given object. + * Any additional arguments are provided to the invoked method. + * + * @static + * @memberOf _ + * @category Utility + * @param {Array|string} path The path of the method to invoke. + * @param {...*} [args] The arguments to invoke the method with. + * @returns {Function} Returns the new function. + * @example + * + * var objects = [ + * { 'a': { 'b': { 'c': _.constant(2) } } }, + * { 'a': { 'b': { 'c': _.constant(1) } } } + * ]; + * + * _.map(objects, _.method('a.b.c')); + * // => [2, 1] + * + * _.invoke(_.sortBy(objects, _.method(['a', 'b', 'c'])), 'a.b.c'); + * // => [1, 2] + */ + var method = restParam(function(path, args) { + return function(object) { + return invokePath(object, path, args); + }; + }); + + /** + * The opposite of `_.method`; this method creates a function that invokes + * the method at a given path on `object`. Any additional arguments are + * provided to the invoked method. + * + * @static + * @memberOf _ + * @category Utility + * @param {Object} object The object to query. + * @param {...*} [args] The arguments to invoke the method with. + * @returns {Function} Returns the new function. + * @example + * + * var array = _.times(3, _.constant), + * object = { 'a': array, 'b': array, 'c': array }; + * + * _.map(['a[2]', 'c[0]'], _.methodOf(object)); + * // => [2, 0] + * + * _.map([['a', '2'], ['c', '0']], _.methodOf(object)); + * // => [2, 0] + */ + var methodOf = restParam(function(object, args) { + return function(path) { + return invokePath(object, path, args); + }; + }); + + /** + * Adds all own enumerable function properties of a source object to the + * destination object. If `object` is a function then methods are added to + * its prototype as well. + * + * **Note:** Use `_.runInContext` to create a pristine `lodash` function to + * avoid conflicts caused by modifying the original. + * + * @static + * @memberOf _ + * @category Utility + * @param {Function|Object} [object=lodash] The destination object. + * @param {Object} source The object of functions to add. + * @param {Object} [options] The options object. + * @param {boolean} [options.chain=true] Specify whether the functions added + * are chainable. + * @returns {Function|Object} Returns `object`. + * @example + * + * function vowels(string) { + * return _.filter(string, function(v) { + * return /[aeiou]/i.test(v); + * }); + * } + * + * _.mixin({ 'vowels': vowels }); + * _.vowels('fred'); + * // => ['e'] + * + * _('fred').vowels().value(); + * // => ['e'] + * + * _.mixin({ 'vowels': vowels }, { 'chain': false }); + * _('fred').vowels(); + * // => ['e'] + */ + function mixin(object, source, options) { + if (options == null) { + var isObj = isObject(source), + props = isObj ? keys(source) : undefined, + methodNames = (props && props.length) ? baseFunctions(source, props) : undefined; + + if (!(methodNames ? methodNames.length : isObj)) { + methodNames = false; + options = source; + source = object; + object = this; + } + } + if (!methodNames) { + methodNames = baseFunctions(source, keys(source)); + } + var chain = true, + index = -1, + isFunc = isFunction(object), + length = methodNames.length; + + if (options === false) { + chain = false; + } else if (isObject(options) && 'chain' in options) { + chain = options.chain; + } + while (++index < length) { + var methodName = methodNames[index], + func = source[methodName]; + + object[methodName] = func; + if (isFunc) { + object.prototype[methodName] = (function(func) { + return function() { + var chainAll = this.__chain__; + if (chain || chainAll) { + var result = object(this.__wrapped__), + actions = result.__actions__ = arrayCopy(this.__actions__); + + actions.push({ 'func': func, 'args': arguments, 'thisArg': object }); + result.__chain__ = chainAll; + return result; + } + return func.apply(object, arrayPush([this.value()], arguments)); + }; + }(func)); + } + } + return object; + } + + /** + * Reverts the `_` variable to its previous value and returns a reference to + * the `lodash` function. + * + * @static + * @memberOf _ + * @category Utility + * @returns {Function} Returns the `lodash` function. + * @example + * + * var lodash = _.noConflict(); + */ + function noConflict() { + root._ = oldDash; + return this; + } + + /** + * A no-operation function that returns `undefined` regardless of the + * arguments it receives. + * + * @static + * @memberOf _ + * @category Utility + * @example + * + * var object = { 'user': 'fred' }; + * + * _.noop(object) === undefined; + * // => true + */ + function noop() { + // No operation performed. + } + + /** + * Creates a function that returns the property value at `path` on a + * given object. + * + * @static + * @memberOf _ + * @category Utility + * @param {Array|string} path The path of the property to get. + * @returns {Function} Returns the new function. + * @example + * + * var objects = [ + * { 'a': { 'b': { 'c': 2 } } }, + * { 'a': { 'b': { 'c': 1 } } } + * ]; + * + * _.map(objects, _.property('a.b.c')); + * // => [2, 1] + * + * _.pluck(_.sortBy(objects, _.property(['a', 'b', 'c'])), 'a.b.c'); + * // => [1, 2] + */ + function property(path) { + return isKey(path) ? baseProperty(path) : basePropertyDeep(path); + } + + /** + * The opposite of `_.property`; this method creates a function that returns + * the property value at a given path on `object`. + * + * @static + * @memberOf _ + * @category Utility + * @param {Object} object The object to query. + * @returns {Function} Returns the new function. + * @example + * + * var array = [0, 1, 2], + * object = { 'a': array, 'b': array, 'c': array }; + * + * _.map(['a[2]', 'c[0]'], _.propertyOf(object)); + * // => [2, 0] + * + * _.map([['a', '2'], ['c', '0']], _.propertyOf(object)); + * // => [2, 0] + */ + function propertyOf(object) { + return function(path) { + return baseGet(object, toPath(path), (path + '')); + }; + } + + /** + * Creates an array of numbers (positive and/or negative) progressing from + * `start` up to, but not including, `end`. If `end` is not specified it's + * set to `start` with `start` then set to `0`. If `end` is less than `start` + * a zero-length range is created unless a negative `step` is specified. + * + * @static + * @memberOf _ + * @category Utility + * @param {number} [start=0] The start of the range. + * @param {number} end The end of the range. + * @param {number} [step=1] The value to increment or decrement by. + * @returns {Array} Returns the new array of numbers. + * @example + * + * _.range(4); + * // => [0, 1, 2, 3] + * + * _.range(1, 5); + * // => [1, 2, 3, 4] + * + * _.range(0, 20, 5); + * // => [0, 5, 10, 15] + * + * _.range(0, -4, -1); + * // => [0, -1, -2, -3] + * + * _.range(1, 4, 0); + * // => [1, 1, 1] + * + * _.range(0); + * // => [] + */ + function range(start, end, step) { + if (step && isIterateeCall(start, end, step)) { + end = step = undefined; + } + start = +start || 0; + step = step == null ? 1 : (+step || 0); + + if (end == null) { + end = start; + start = 0; + } else { + end = +end || 0; + } + // Use `Array(length)` so engines like Chakra and V8 avoid slower modes. + // See https://youtu.be/XAqIpGU8ZZk#t=17m25s for more details. + var index = -1, + length = nativeMax(nativeCeil((end - start) / (step || 1)), 0), + result = Array(length); + + while (++index < length) { + result[index] = start; + start += step; + } + return result; + } + + /** + * Invokes the iteratee function `n` times, returning an array of the results + * of each invocation. The `iteratee` is bound to `thisArg` and invoked with + * one argument; (index). + * + * @static + * @memberOf _ + * @category Utility + * @param {number} n The number of times to invoke `iteratee`. + * @param {Function} [iteratee=_.identity] The function invoked per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {Array} Returns the array of results. + * @example + * + * var diceRolls = _.times(3, _.partial(_.random, 1, 6, false)); + * // => [3, 6, 4] + * + * _.times(3, function(n) { + * mage.castSpell(n); + * }); + * // => invokes `mage.castSpell(n)` three times with `n` of `0`, `1`, and `2` + * + * _.times(3, function(n) { + * this.cast(n); + * }, mage); + * // => also invokes `mage.castSpell(n)` three times + */ + function times(n, iteratee, thisArg) { + n = nativeFloor(n); + + // Exit early to avoid a JSC JIT bug in Safari 8 + // where `Array(0)` is treated as `Array(1)`. + if (n < 1 || !nativeIsFinite(n)) { + return []; + } + var index = -1, + result = Array(nativeMin(n, MAX_ARRAY_LENGTH)); + + iteratee = bindCallback(iteratee, thisArg, 1); + while (++index < n) { + if (index < MAX_ARRAY_LENGTH) { + result[index] = iteratee(index); + } else { + iteratee(index); + } + } + return result; + } + + /** + * Generates a unique ID. If `prefix` is provided the ID is appended to it. + * + * @static + * @memberOf _ + * @category Utility + * @param {string} [prefix] The value to prefix the ID with. + * @returns {string} Returns the unique ID. + * @example + * + * _.uniqueId('contact_'); + * // => 'contact_104' + * + * _.uniqueId(); + * // => '105' + */ + function uniqueId(prefix) { + var id = ++idCounter; + return baseToString(prefix) + id; + } + + /*------------------------------------------------------------------------*/ + + /** + * Adds two numbers. + * + * @static + * @memberOf _ + * @category Math + * @param {number} augend The first number to add. + * @param {number} addend The second number to add. + * @returns {number} Returns the sum. + * @example + * + * _.add(6, 4); + * // => 10 + */ + function add(augend, addend) { + return (+augend || 0) + (+addend || 0); + } + + /** + * Calculates `n` rounded up to `precision`. + * + * @static + * @memberOf _ + * @category Math + * @param {number} n The number to round up. + * @param {number} [precision=0] The precision to round up to. + * @returns {number} Returns the rounded up number. + * @example + * + * _.ceil(4.006); + * // => 5 + * + * _.ceil(6.004, 2); + * // => 6.01 + * + * _.ceil(6040, -2); + * // => 6100 + */ + var ceil = createRound('ceil'); + + /** + * Calculates `n` rounded down to `precision`. + * + * @static + * @memberOf _ + * @category Math + * @param {number} n The number to round down. + * @param {number} [precision=0] The precision to round down to. + * @returns {number} Returns the rounded down number. + * @example + * + * _.floor(4.006); + * // => 4 + * + * _.floor(0.046, 2); + * // => 0.04 + * + * _.floor(4060, -2); + * // => 4000 + */ + var floor = createRound('floor'); + + /** + * Gets the maximum value of `collection`. If `collection` is empty or falsey + * `-Infinity` is returned. If an iteratee function is provided it's invoked + * for each value in `collection` to generate the criterion by which the value + * is ranked. The `iteratee` is bound to `thisArg` and invoked with three + * arguments: (value, index, collection). + * + * If a property name is provided for `iteratee` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `iteratee` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Math + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [iteratee] The function invoked per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {*} Returns the maximum value. + * @example + * + * _.max([4, 2, 8, 6]); + * // => 8 + * + * _.max([]); + * // => -Infinity + * + * var users = [ + * { 'user': 'barney', 'age': 36 }, + * { 'user': 'fred', 'age': 40 } + * ]; + * + * _.max(users, function(chr) { + * return chr.age; + * }); + * // => { 'user': 'fred', 'age': 40 } + * + * // using the `_.property` callback shorthand + * _.max(users, 'age'); + * // => { 'user': 'fred', 'age': 40 } + */ + var max = createExtremum(gt, NEGATIVE_INFINITY); + + /** + * Gets the minimum value of `collection`. If `collection` is empty or falsey + * `Infinity` is returned. If an iteratee function is provided it's invoked + * for each value in `collection` to generate the criterion by which the value + * is ranked. The `iteratee` is bound to `thisArg` and invoked with three + * arguments: (value, index, collection). + * + * If a property name is provided for `iteratee` the created `_.property` + * style callback returns the property value of the given element. + * + * If a value is also provided for `thisArg` the created `_.matchesProperty` + * style callback returns `true` for elements that have a matching property + * value, else `false`. + * + * If an object is provided for `iteratee` the created `_.matches` style + * callback returns `true` for elements that have the properties of the given + * object, else `false`. + * + * @static + * @memberOf _ + * @category Math + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [iteratee] The function invoked per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {*} Returns the minimum value. + * @example + * + * _.min([4, 2, 8, 6]); + * // => 2 + * + * _.min([]); + * // => Infinity + * + * var users = [ + * { 'user': 'barney', 'age': 36 }, + * { 'user': 'fred', 'age': 40 } + * ]; + * + * _.min(users, function(chr) { + * return chr.age; + * }); + * // => { 'user': 'barney', 'age': 36 } + * + * // using the `_.property` callback shorthand + * _.min(users, 'age'); + * // => { 'user': 'barney', 'age': 36 } + */ + var min = createExtremum(lt, POSITIVE_INFINITY); + + /** + * Calculates `n` rounded to `precision`. + * + * @static + * @memberOf _ + * @category Math + * @param {number} n The number to round. + * @param {number} [precision=0] The precision to round to. + * @returns {number} Returns the rounded number. + * @example + * + * _.round(4.006); + * // => 4 + * + * _.round(4.006, 2); + * // => 4.01 + * + * _.round(4060, -2); + * // => 4100 + */ + var round = createRound('round'); + + /** + * Gets the sum of the values in `collection`. + * + * @static + * @memberOf _ + * @category Math + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [iteratee] The function invoked per iteration. + * @param {*} [thisArg] The `this` binding of `iteratee`. + * @returns {number} Returns the sum. + * @example + * + * _.sum([4, 6]); + * // => 10 + * + * _.sum({ 'a': 4, 'b': 6 }); + * // => 10 + * + * var objects = [ + * { 'n': 4 }, + * { 'n': 6 } + * ]; + * + * _.sum(objects, function(object) { + * return object.n; + * }); + * // => 10 + * + * // using the `_.property` callback shorthand + * _.sum(objects, 'n'); + * // => 10 + */ + function sum(collection, iteratee, thisArg) { + if (thisArg && isIterateeCall(collection, iteratee, thisArg)) { + iteratee = undefined; + } + iteratee = getCallback(iteratee, thisArg, 3); + return iteratee.length == 1 + ? arraySum(isArray(collection) ? collection : toIterable(collection), iteratee) + : baseSum(collection, iteratee); + } + + /*------------------------------------------------------------------------*/ + + // Ensure wrappers are instances of `baseLodash`. + lodash.prototype = baseLodash.prototype; + + LodashWrapper.prototype = baseCreate(baseLodash.prototype); + LodashWrapper.prototype.constructor = LodashWrapper; + + LazyWrapper.prototype = baseCreate(baseLodash.prototype); + LazyWrapper.prototype.constructor = LazyWrapper; + + // Add functions to the `Map` cache. + MapCache.prototype['delete'] = mapDelete; + MapCache.prototype.get = mapGet; + MapCache.prototype.has = mapHas; + MapCache.prototype.set = mapSet; + + // Add functions to the `Set` cache. + SetCache.prototype.push = cachePush; + + // Assign cache to `_.memoize`. + memoize.Cache = MapCache; + + // Add functions that return wrapped values when chaining. + lodash.after = after; + lodash.ary = ary; + lodash.assign = assign; + lodash.at = at; + lodash.before = before; + lodash.bind = bind; + lodash.bindAll = bindAll; + lodash.bindKey = bindKey; + lodash.callback = callback; + lodash.chain = chain; + lodash.chunk = chunk; + lodash.compact = compact; + lodash.constant = constant; + lodash.countBy = countBy; + lodash.create = create; + lodash.curry = curry; + lodash.curryRight = curryRight; + lodash.debounce = debounce; + lodash.defaults = defaults; + lodash.defaultsDeep = defaultsDeep; + lodash.defer = defer; + lodash.delay = delay; + lodash.difference = difference; + lodash.drop = drop; + lodash.dropRight = dropRight; + lodash.dropRightWhile = dropRightWhile; + lodash.dropWhile = dropWhile; + lodash.fill = fill; + lodash.filter = filter; + lodash.flatten = flatten; + lodash.flattenDeep = flattenDeep; + lodash.flow = flow; + lodash.flowRight = flowRight; + lodash.forEach = forEach; + lodash.forEachRight = forEachRight; + lodash.forIn = forIn; + lodash.forInRight = forInRight; + lodash.forOwn = forOwn; + lodash.forOwnRight = forOwnRight; + lodash.functions = functions; + lodash.groupBy = groupBy; + lodash.indexBy = indexBy; + lodash.initial = initial; + lodash.intersection = intersection; + lodash.invert = invert; + lodash.invoke = invoke; + lodash.keys = keys; + lodash.keysIn = keysIn; + lodash.map = map; + lodash.mapKeys = mapKeys; + lodash.mapValues = mapValues; + lodash.matches = matches; + lodash.matchesProperty = matchesProperty; + lodash.memoize = memoize; + lodash.merge = merge; + lodash.method = method; + lodash.methodOf = methodOf; + lodash.mixin = mixin; + lodash.modArgs = modArgs; + lodash.negate = negate; + lodash.omit = omit; + lodash.once = once; + lodash.pairs = pairs; + lodash.partial = partial; + lodash.partialRight = partialRight; + lodash.partition = partition; + lodash.pick = pick; + lodash.pluck = pluck; + lodash.property = property; + lodash.propertyOf = propertyOf; + lodash.pull = pull; + lodash.pullAt = pullAt; + lodash.range = range; + lodash.rearg = rearg; + lodash.reject = reject; + lodash.remove = remove; + lodash.rest = rest; + lodash.restParam = restParam; + lodash.set = set; + lodash.shuffle = shuffle; + lodash.slice = slice; + lodash.sortBy = sortBy; + lodash.sortByAll = sortByAll; + lodash.sortByOrder = sortByOrder; + lodash.spread = spread; + lodash.take = take; + lodash.takeRight = takeRight; + lodash.takeRightWhile = takeRightWhile; + lodash.takeWhile = takeWhile; + lodash.tap = tap; + lodash.throttle = throttle; + lodash.thru = thru; + lodash.times = times; + lodash.toArray = toArray; + lodash.toPlainObject = toPlainObject; + lodash.transform = transform; + lodash.union = union; + lodash.uniq = uniq; + lodash.unzip = unzip; + lodash.unzipWith = unzipWith; + lodash.values = values; + lodash.valuesIn = valuesIn; + lodash.where = where; + lodash.without = without; + lodash.wrap = wrap; + lodash.xor = xor; + lodash.zip = zip; + lodash.zipObject = zipObject; + lodash.zipWith = zipWith; + + // Add aliases. + lodash.backflow = flowRight; + lodash.collect = map; + lodash.compose = flowRight; + lodash.each = forEach; + lodash.eachRight = forEachRight; + lodash.extend = assign; + lodash.iteratee = callback; + lodash.methods = functions; + lodash.object = zipObject; + lodash.select = filter; + lodash.tail = rest; + lodash.unique = uniq; + + // Add functions to `lodash.prototype`. + mixin(lodash, lodash); + + /*------------------------------------------------------------------------*/ + + // Add functions that return unwrapped values when chaining. + lodash.add = add; + lodash.attempt = attempt; + lodash.camelCase = camelCase; + lodash.capitalize = capitalize; + lodash.ceil = ceil; + lodash.clone = clone; + lodash.cloneDeep = cloneDeep; + lodash.deburr = deburr; + lodash.endsWith = endsWith; + lodash.escape = escape; + lodash.escapeRegExp = escapeRegExp; + lodash.every = every; + lodash.find = find; + lodash.findIndex = findIndex; + lodash.findKey = findKey; + lodash.findLast = findLast; + lodash.findLastIndex = findLastIndex; + lodash.findLastKey = findLastKey; + lodash.findWhere = findWhere; + lodash.first = first; + lodash.floor = floor; + lodash.get = get; + lodash.gt = gt; + lodash.gte = gte; + lodash.has = has; + lodash.identity = identity; + lodash.includes = includes; + lodash.indexOf = indexOf; + lodash.inRange = inRange; + lodash.isArguments = isArguments; + lodash.isArray = isArray; + lodash.isBoolean = isBoolean; + lodash.isDate = isDate; + lodash.isElement = isElement; + lodash.isEmpty = isEmpty; + lodash.isEqual = isEqual; + lodash.isError = isError; + lodash.isFinite = isFinite; + lodash.isFunction = isFunction; + lodash.isMatch = isMatch; + lodash.isNaN = isNaN; + lodash.isNative = isNative; + lodash.isNull = isNull; + lodash.isNumber = isNumber; + lodash.isObject = isObject; + lodash.isPlainObject = isPlainObject; + lodash.isRegExp = isRegExp; + lodash.isString = isString; + lodash.isTypedArray = isTypedArray; + lodash.isUndefined = isUndefined; + lodash.kebabCase = kebabCase; + lodash.last = last; + lodash.lastIndexOf = lastIndexOf; + lodash.lt = lt; + lodash.lte = lte; + lodash.max = max; + lodash.min = min; + lodash.noConflict = noConflict; + lodash.noop = noop; + lodash.now = now; + lodash.pad = pad; + lodash.padLeft = padLeft; + lodash.padRight = padRight; + lodash.parseInt = parseInt; + lodash.random = random; + lodash.reduce = reduce; + lodash.reduceRight = reduceRight; + lodash.repeat = repeat; + lodash.result = result; + lodash.round = round; + lodash.runInContext = runInContext; + lodash.size = size; + lodash.snakeCase = snakeCase; + lodash.some = some; + lodash.sortedIndex = sortedIndex; + lodash.sortedLastIndex = sortedLastIndex; + lodash.startCase = startCase; + lodash.startsWith = startsWith; + lodash.sum = sum; + lodash.template = template; + lodash.trim = trim; + lodash.trimLeft = trimLeft; + lodash.trimRight = trimRight; + lodash.trunc = trunc; + lodash.unescape = unescape; + lodash.uniqueId = uniqueId; + lodash.words = words; + + // Add aliases. + lodash.all = every; + lodash.any = some; + lodash.contains = includes; + lodash.eq = isEqual; + lodash.detect = find; + lodash.foldl = reduce; + lodash.foldr = reduceRight; + lodash.head = first; + lodash.include = includes; + lodash.inject = reduce; + + mixin(lodash, (function() { + var source = {}; + baseForOwn(lodash, function(func, methodName) { + if (!lodash.prototype[methodName]) { + source[methodName] = func; + } + }); + return source; + }()), false); + + /*------------------------------------------------------------------------*/ + + // Add functions capable of returning wrapped and unwrapped values when chaining. + lodash.sample = sample; + + lodash.prototype.sample = function(n) { + if (!this.__chain__ && n == null) { + return sample(this.value()); + } + return this.thru(function(value) { + return sample(value, n); + }); + }; + + /*------------------------------------------------------------------------*/ + + /** + * The semantic version number. + * + * @static + * @memberOf _ + * @type string + */ + lodash.VERSION = VERSION; + + // Assign default placeholders. + arrayEach(['bind', 'bindKey', 'curry', 'curryRight', 'partial', 'partialRight'], function(methodName) { + lodash[methodName].placeholder = lodash; + }); + + // Add `LazyWrapper` methods for `_.drop` and `_.take` variants. + arrayEach(['drop', 'take'], function(methodName, index) { + LazyWrapper.prototype[methodName] = function(n) { + var filtered = this.__filtered__; + if (filtered && !index) { + return new LazyWrapper(this); + } + n = n == null ? 1 : nativeMax(nativeFloor(n) || 0, 0); + + var result = this.clone(); + if (filtered) { + result.__takeCount__ = nativeMin(result.__takeCount__, n); + } else { + result.__views__.push({ 'size': n, 'type': methodName + (result.__dir__ < 0 ? 'Right' : '') }); + } + return result; + }; + + LazyWrapper.prototype[methodName + 'Right'] = function(n) { + return this.reverse()[methodName](n).reverse(); + }; + }); + + // Add `LazyWrapper` methods that accept an `iteratee` value. + arrayEach(['filter', 'map', 'takeWhile'], function(methodName, index) { + var type = index + 1, + isFilter = type != LAZY_MAP_FLAG; + + LazyWrapper.prototype[methodName] = function(iteratee, thisArg) { + var result = this.clone(); + result.__iteratees__.push({ 'iteratee': getCallback(iteratee, thisArg, 1), 'type': type }); + result.__filtered__ = result.__filtered__ || isFilter; + return result; + }; + }); + + // Add `LazyWrapper` methods for `_.first` and `_.last`. + arrayEach(['first', 'last'], function(methodName, index) { + var takeName = 'take' + (index ? 'Right' : ''); + + LazyWrapper.prototype[methodName] = function() { + return this[takeName](1).value()[0]; + }; + }); + + // Add `LazyWrapper` methods for `_.initial` and `_.rest`. + arrayEach(['initial', 'rest'], function(methodName, index) { + var dropName = 'drop' + (index ? '' : 'Right'); + + LazyWrapper.prototype[methodName] = function() { + return this.__filtered__ ? new LazyWrapper(this) : this[dropName](1); + }; + }); + + // Add `LazyWrapper` methods for `_.pluck` and `_.where`. + arrayEach(['pluck', 'where'], function(methodName, index) { + var operationName = index ? 'filter' : 'map', + createCallback = index ? baseMatches : property; + + LazyWrapper.prototype[methodName] = function(value) { + return this[operationName](createCallback(value)); + }; + }); + + LazyWrapper.prototype.compact = function() { + return this.filter(identity); + }; + + LazyWrapper.prototype.reject = function(predicate, thisArg) { + predicate = getCallback(predicate, thisArg, 1); + return this.filter(function(value) { + return !predicate(value); + }); + }; + + LazyWrapper.prototype.slice = function(start, end) { + start = start == null ? 0 : (+start || 0); + + var result = this; + if (result.__filtered__ && (start > 0 || end < 0)) { + return new LazyWrapper(result); + } + if (start < 0) { + result = result.takeRight(-start); + } else if (start) { + result = result.drop(start); + } + if (end !== undefined) { + end = (+end || 0); + result = end < 0 ? result.dropRight(-end) : result.take(end - start); + } + return result; + }; + + LazyWrapper.prototype.takeRightWhile = function(predicate, thisArg) { + return this.reverse().takeWhile(predicate, thisArg).reverse(); + }; + + LazyWrapper.prototype.toArray = function() { + return this.take(POSITIVE_INFINITY); + }; + + // Add `LazyWrapper` methods to `lodash.prototype`. + baseForOwn(LazyWrapper.prototype, function(func, methodName) { + var checkIteratee = /^(?:filter|map|reject)|While$/.test(methodName), + retUnwrapped = /^(?:first|last)$/.test(methodName), + lodashFunc = lodash[retUnwrapped ? ('take' + (methodName == 'last' ? 'Right' : '')) : methodName]; + + if (!lodashFunc) { + return; + } + lodash.prototype[methodName] = function() { + var args = retUnwrapped ? [1] : arguments, + chainAll = this.__chain__, + value = this.__wrapped__, + isHybrid = !!this.__actions__.length, + isLazy = value instanceof LazyWrapper, + iteratee = args[0], + useLazy = isLazy || isArray(value); + + if (useLazy && checkIteratee && typeof iteratee == 'function' && iteratee.length != 1) { + // Avoid lazy use if the iteratee has a "length" value other than `1`. + isLazy = useLazy = false; + } + var interceptor = function(value) { + return (retUnwrapped && chainAll) + ? lodashFunc(value, 1)[0] + : lodashFunc.apply(undefined, arrayPush([value], args)); + }; + + var action = { 'func': thru, 'args': [interceptor], 'thisArg': undefined }, + onlyLazy = isLazy && !isHybrid; + + if (retUnwrapped && !chainAll) { + if (onlyLazy) { + value = value.clone(); + value.__actions__.push(action); + return func.call(value); + } + return lodashFunc.call(undefined, this.value())[0]; + } + if (!retUnwrapped && useLazy) { + value = onlyLazy ? value : new LazyWrapper(this); + var result = func.apply(value, args); + result.__actions__.push(action); + return new LodashWrapper(result, chainAll); + } + return this.thru(interceptor); + }; + }); + + // Add `Array` and `String` methods to `lodash.prototype`. + arrayEach(['join', 'pop', 'push', 'replace', 'shift', 'sort', 'splice', 'split', 'unshift'], function(methodName) { + var protoFunc = (/^(?:replace|split)$/.test(methodName) ? stringProto : arrayProto)[methodName], + chainName = /^(?:push|sort|unshift)$/.test(methodName) ? 'tap' : 'thru', + fixObjects = !support.spliceObjects && /^(?:pop|shift|splice)$/.test(methodName), + retUnwrapped = /^(?:join|pop|replace|shift)$/.test(methodName); + + // Avoid array-like object bugs with `Array#shift` and `Array#splice` in + // IE < 9, Firefox < 10, and RingoJS. + var func = !fixObjects ? protoFunc : function() { + var result = protoFunc.apply(this, arguments); + if (this.length === 0) { + delete this[0]; + } + return result; + }; + + lodash.prototype[methodName] = function() { + var args = arguments; + if (retUnwrapped && !this.__chain__) { + return func.apply(this.value(), args); + } + return this[chainName](function(value) { + return func.apply(value, args); + }); + }; + }); + + // Map minified function names to their real names. + baseForOwn(LazyWrapper.prototype, function(func, methodName) { + var lodashFunc = lodash[methodName]; + if (lodashFunc) { + var key = (lodashFunc.name + ''), + names = realNames[key] || (realNames[key] = []); + + names.push({ 'name': methodName, 'func': lodashFunc }); + } + }); + + realNames[createHybridWrapper(undefined, BIND_KEY_FLAG).name] = [{ 'name': 'wrapper', 'func': undefined }]; + + // Add functions to the lazy wrapper. + LazyWrapper.prototype.clone = lazyClone; + LazyWrapper.prototype.reverse = lazyReverse; + LazyWrapper.prototype.value = lazyValue; + + // Add chaining functions to the `lodash` wrapper. + lodash.prototype.chain = wrapperChain; + lodash.prototype.commit = wrapperCommit; + lodash.prototype.concat = wrapperConcat; + lodash.prototype.plant = wrapperPlant; + lodash.prototype.reverse = wrapperReverse; + lodash.prototype.toString = wrapperToString; + lodash.prototype.run = lodash.prototype.toJSON = lodash.prototype.valueOf = lodash.prototype.value = wrapperValue; + + // Add function aliases to the `lodash` wrapper. + lodash.prototype.collect = lodash.prototype.map; + lodash.prototype.head = lodash.prototype.first; + lodash.prototype.select = lodash.prototype.filter; + lodash.prototype.tail = lodash.prototype.rest; + + return lodash; + } + + /*--------------------------------------------------------------------------*/ + + // Export lodash. + var _ = runInContext(); + + // Some AMD build optimizers like r.js check for condition patterns like the following: + if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) { + // Expose lodash to the global object when an AMD loader is present to avoid + // errors in cases where lodash is loaded by a script tag and not intended + // as an AMD module. See http://requirejs.org/docs/errors.html#mismatch for + // more details. + root._ = _; + + // Define as an anonymous module so, through path mapping, it can be + // referenced as the "underscore" module. + define(function() { + return _; + }); + } + // Check for `exports` after `define` in case a build optimizer adds an `exports` object. + else if (freeExports && freeModule) { + // Export for Node.js or RingoJS. + if (moduleExports) { + (freeModule.exports = _)._ = _; + } + // Export for Rhino with CommonJS support. + else { + freeExports._ = _; + } + } + else { + // Export for a browser or Rhino. + root._ = _; + } +}.call(this)); diff --git a/js/lodash.min.js b/js/lodash.min.js new file mode 100644 index 0000000..05870d1 --- /dev/null +++ b/js/lodash.min.js @@ -0,0 +1,102 @@ +/** + * @license + * lodash 3.10.1 (Custom Build) lodash.com/license | Underscore.js 1.8.3 underscorejs.org/LICENSE + * Build: `lodash compat -o ./lodash.js` + */ +;(function(){function n(n,t){if(n!==t){var r=null===n,e=n===w,u=n===n,o=null===t,i=t===w,f=t===t;if(n>t&&!o||!u||r&&!i&&f||e&&f)return 1;if(n<t&&!r||!f||o&&!e&&u||i&&u)return-1}return 0}function t(n,t,r){for(var e=n.length,u=r?e:-1;r?u--:++u<e;)if(t(n[u],u,n))return u;return-1}function r(n,t,r){if(t!==t)return p(n,r);r-=1;for(var e=n.length;++r<e;)if(n[r]===t)return r;return-1}function e(n){return typeof n=="function"||false}function u(n){return null==n?"":n+""}function o(n,t){for(var r=-1,e=n.length;++r<e&&-1<t.indexOf(n.charAt(r));); +return r}function i(n,t){for(var r=n.length;r--&&-1<t.indexOf(n.charAt(r)););return r}function f(t,r){return n(t.a,r.a)||t.b-r.b}function a(n){return Nn[n]}function c(n){return Tn[n]}function l(n,t,r){return t?n=Bn[n]:r&&(n=Dn[n]),"\\"+n}function s(n){return"\\"+Dn[n]}function p(n,t,r){var e=n.length;for(t+=r?0:-1;r?t--:++t<e;){var u=n[t];if(u!==u)return t}return-1}function h(n){return!!n&&typeof n=="object"}function _(n){return 160>=n&&9<=n&&13>=n||32==n||160==n||5760==n||6158==n||8192<=n&&(8202>=n||8232==n||8233==n||8239==n||8287==n||12288==n||65279==n); +}function v(n,t){for(var r=-1,e=n.length,u=-1,o=[];++r<e;)n[r]===t&&(n[r]=P,o[++u]=r);return o}function g(n){for(var t=-1,r=n.length;++t<r&&_(n.charCodeAt(t)););return t}function y(n){for(var t=n.length;t--&&_(n.charCodeAt(t)););return t}function d(n){return Pn[n]}function m(_){function Nn(n){if(h(n)&&!(Wo(n)||n instanceof zn)){if(n instanceof Pn)return n;if(eu.call(n,"__chain__")&&eu.call(n,"__wrapped__"))return qr(n)}return new Pn(n)}function Tn(){}function Pn(n,t,r){this.__wrapped__=n,this.__actions__=r||[], +this.__chain__=!!t}function zn(n){this.__wrapped__=n,this.__actions__=[],this.__dir__=1,this.__filtered__=false,this.__iteratees__=[],this.__takeCount__=Cu,this.__views__=[]}function Bn(){this.__data__={}}function Dn(n){var t=n?n.length:0;for(this.data={hash:mu(null),set:new hu};t--;)this.push(n[t])}function Mn(n,t){var r=n.data;return(typeof t=="string"||de(t)?r.set.has(t):r.hash[t])?0:-1}function qn(n,t){var r=-1,e=n.length;for(t||(t=De(e));++r<e;)t[r]=n[r];return t}function Kn(n,t){for(var r=-1,e=n.length;++r<e&&false!==t(n[r],r,n);); +return n}function Vn(n,t){for(var r=-1,e=n.length;++r<e;)if(!t(n[r],r,n))return false;return true}function Zn(n,t){for(var r=-1,e=n.length,u=-1,o=[];++r<e;){var i=n[r];t(i,r,n)&&(o[++u]=i)}return o}function Xn(n,t){for(var r=-1,e=n.length,u=De(e);++r<e;)u[r]=t(n[r],r,n);return u}function Hn(n,t){for(var r=-1,e=t.length,u=n.length;++r<e;)n[u+r]=t[r];return n}function Qn(n,t,r,e){var u=-1,o=n.length;for(e&&o&&(r=n[++u]);++u<o;)r=t(r,n[u],u,n);return r}function nt(n,t){for(var r=-1,e=n.length;++r<e;)if(t(n[r],r,n))return true; +return false}function tt(n,t,r,e){return n!==w&&eu.call(e,r)?n:t}function rt(n,t,r){for(var e=-1,u=Ko(t),o=u.length;++e<o;){var i=u[e],f=n[i],a=r(f,t[i],i,n,t);(a===a?a===f:f!==f)&&(f!==w||i in n)||(n[i]=a)}return n}function et(n,t){return null==t?n:ot(t,Ko(t),n)}function ut(n,t){for(var r=-1,e=null==n,u=!e&&Sr(n),o=u?n.length:0,i=t.length,f=De(i);++r<i;){var a=t[r];f[r]=u?Ur(a,o)?n[a]:w:e?w:n[a]}return f}function ot(n,t,r){r||(r={});for(var e=-1,u=t.length;++e<u;){var o=t[e];r[o]=n[o]}return r}function it(n,t,r){ +var e=typeof n;return"function"==e?t===w?n:Dt(n,t,r):null==n?Ne:"object"==e?At(n):t===w?Be(n):jt(n,t)}function ft(n,t,r,e,u,o,i){var f;if(r&&(f=u?r(n,e,u):r(n)),f!==w)return f;if(!de(n))return n;if(e=Wo(n)){if(f=Ir(n),!t)return qn(n,f)}else{var a=ou.call(n),c=a==K;if(a!=Z&&a!=z&&(!c||u))return Ln[a]?Er(n,a,t):u?n:{};if(Gn(n))return u?n:{};if(f=Rr(c?{}:n),!t)return et(f,n)}for(o||(o=[]),i||(i=[]),u=o.length;u--;)if(o[u]==n)return i[u];return o.push(n),i.push(f),(e?Kn:gt)(n,function(e,u){f[u]=ft(e,t,r,u,n,o,i); +}),f}function at(n,t,r){if(typeof n!="function")throw new Xe(T);return _u(function(){n.apply(w,r)},t)}function ct(n,t){var e=n?n.length:0,u=[];if(!e)return u;var o=-1,i=jr(),f=i===r,a=f&&t.length>=F&&mu&&hu?new Dn(t):null,c=t.length;a&&(i=Mn,f=false,t=a);n:for(;++o<e;)if(a=n[o],f&&a===a){for(var l=c;l--;)if(t[l]===a)continue n;u.push(a)}else 0>i(t,a,0)&&u.push(a);return u}function lt(n,t){var r=true;return zu(n,function(n,e,u){return r=!!t(n,e,u)}),r}function st(n,t,r,e){var u=e,o=u;return zu(n,function(n,i,f){ +i=+t(n,i,f),(r(i,u)||i===e&&i===o)&&(u=i,o=n)}),o}function pt(n,t){var r=[];return zu(n,function(n,e,u){t(n,e,u)&&r.push(n)}),r}function ht(n,t,r,e){var u;return r(n,function(n,r,o){return t(n,r,o)?(u=e?r:n,false):void 0}),u}function _t(n,t,r,e){e||(e=[]);for(var u=-1,o=n.length;++u<o;){var i=n[u];h(i)&&Sr(i)&&(r||Wo(i)||_e(i))?t?_t(i,t,r,e):Hn(e,i):r||(e[e.length]=i)}return e}function vt(n,t){return Du(n,t,Ee)}function gt(n,t){return Du(n,t,Ko)}function yt(n,t){return Mu(n,t,Ko)}function dt(n,t){for(var r=-1,e=t.length,u=-1,o=[];++r<e;){ +var i=t[r];ye(n[i])&&(o[++u]=i)}return o}function mt(n,t,r){if(null!=n){n=Dr(n),r!==w&&r in n&&(t=[r]),r=0;for(var e=t.length;null!=n&&r<e;)n=Dr(n)[t[r++]];return r&&r==e?n:w}}function wt(n,t,r,e,u,o){if(n===t)return true;if(null==n||null==t||!de(n)&&!h(t))return n!==n&&t!==t;n:{var i=wt,f=Wo(n),a=Wo(t),c=B,l=B;f||(c=ou.call(n),c==z?c=Z:c!=Z&&(f=je(n))),a||(l=ou.call(t),l==z?l=Z:l!=Z&&je(t));var s=c==Z&&!Gn(n),a=l==Z&&!Gn(t),l=c==l;if(!l||f||s){if(!e&&(c=s&&eu.call(n,"__wrapped__"),a=a&&eu.call(t,"__wrapped__"), +c||a)){n=i(c?n.value():n,a?t.value():t,r,e,u,o);break n}if(l){for(u||(u=[]),o||(o=[]),c=u.length;c--;)if(u[c]==n){n=o[c]==t;break n}u.push(n),o.push(t),n=(f?mr:xr)(n,t,i,r,e,u,o),u.pop(),o.pop()}else n=false}else n=wr(n,t,c)}return n}function xt(n,t,r){var e=t.length,u=e,o=!r;if(null==n)return!u;for(n=Dr(n);e--;){var i=t[e];if(o&&i[2]?i[1]!==n[i[0]]:!(i[0]in n))return false}for(;++e<u;){var i=t[e],f=i[0],a=n[f],c=i[1];if(o&&i[2]){if(a===w&&!(f in n))return false}else if(i=r?r(a,c,f):w,i===w?!wt(c,a,r,true):!i)return false; +}return true}function bt(n,t){var r=-1,e=Sr(n)?De(n.length):[];return zu(n,function(n,u,o){e[++r]=t(n,u,o)}),e}function At(n){var t=kr(n);if(1==t.length&&t[0][2]){var r=t[0][0],e=t[0][1];return function(n){return null==n?false:(n=Dr(n),n[r]===e&&(e!==w||r in n))}}return function(n){return xt(n,t)}}function jt(n,t){var r=Wo(n),e=Wr(n)&&t===t&&!de(t),u=n+"";return n=Mr(n),function(o){if(null==o)return false;var i=u;if(o=Dr(o),!(!r&&e||i in o)){if(o=1==n.length?o:mt(o,St(n,0,-1)),null==o)return false;i=Gr(n),o=Dr(o); +}return o[i]===t?t!==w||i in o:wt(t,o[i],w,true)}}function kt(n,t,r,e,u){if(!de(n))return n;var o=Sr(t)&&(Wo(t)||je(t)),i=o?w:Ko(t);return Kn(i||t,function(f,a){if(i&&(a=f,f=t[a]),h(f)){e||(e=[]),u||(u=[]);n:{for(var c=a,l=e,s=u,p=l.length,_=t[c];p--;)if(l[p]==_){n[c]=s[p];break n}var p=n[c],v=r?r(p,_,c,n,t):w,g=v===w;g&&(v=_,Sr(_)&&(Wo(_)||je(_))?v=Wo(p)?p:Sr(p)?qn(p):[]:xe(_)||_e(_)?v=_e(p)?Ie(p):xe(p)?p:{}:g=false),l.push(_),s.push(v),g?n[c]=kt(v,_,r,l,s):(v===v?v!==p:p===p)&&(n[c]=v)}}else c=n[a], +l=r?r(c,f,a,n,t):w,(s=l===w)&&(l=f),l===w&&(!o||a in n)||!s&&(l===l?l===c:c!==c)||(n[a]=l)}),n}function Ot(n){return function(t){return null==t?w:Dr(t)[n]}}function It(n){var t=n+"";return n=Mr(n),function(r){return mt(r,n,t)}}function Rt(n,t){for(var r=n?t.length:0;r--;){var e=t[r];if(e!=u&&Ur(e)){var u=e;vu.call(n,e,1)}}return n}function Et(n,t){return n+wu(Ru()*(t-n+1))}function Ct(n,t,r,e,u){return u(n,function(n,u,o){r=e?(e=false,n):t(r,n,u,o)}),r}function St(n,t,r){var e=-1,u=n.length;for(t=null==t?0:+t||0, +0>t&&(t=-t>u?0:u+t),r=r===w||r>u?u:+r||0,0>r&&(r+=u),u=t>r?0:r-t>>>0,t>>>=0,r=De(u);++e<u;)r[e]=n[e+t];return r}function Ut(n,t){var r;return zu(n,function(n,e,u){return r=t(n,e,u),!r}),!!r}function $t(n,t){var r=n.length;for(n.sort(t);r--;)n[r]=n[r].c;return n}function Wt(t,r,e){var u=br(),o=-1;return r=Xn(r,function(n){return u(n)}),t=bt(t,function(n){return{a:Xn(r,function(t){return t(n)}),b:++o,c:n}}),$t(t,function(t,r){var u;n:{for(var o=-1,i=t.a,f=r.a,a=i.length,c=e.length;++o<a;)if(u=n(i[o],f[o])){ +if(o>=c)break n;o=e[o],u*="asc"===o||true===o?1:-1;break n}u=t.b-r.b}return u})}function Ft(n,t){var r=0;return zu(n,function(n,e,u){r+=+t(n,e,u)||0}),r}function Lt(n,t){var e=-1,u=jr(),o=n.length,i=u===r,f=i&&o>=F,a=f&&mu&&hu?new Dn(void 0):null,c=[];a?(u=Mn,i=false):(f=false,a=t?[]:c);n:for(;++e<o;){var l=n[e],s=t?t(l,e,n):l;if(i&&l===l){for(var p=a.length;p--;)if(a[p]===s)continue n;t&&a.push(s),c.push(l)}else 0>u(a,s,0)&&((t||f)&&a.push(s),c.push(l))}return c}function Nt(n,t){for(var r=-1,e=t.length,u=De(e);++r<e;)u[r]=n[t[r]]; +return u}function Tt(n,t,r,e){for(var u=n.length,o=e?u:-1;(e?o--:++o<u)&&t(n[o],o,n););return r?St(n,e?0:o,e?o+1:u):St(n,e?o+1:0,e?u:o)}function Pt(n,t){var r=n;r instanceof zn&&(r=r.value());for(var e=-1,u=t.length;++e<u;)var o=t[e],r=o.func.apply(o.thisArg,Hn([r],o.args));return r}function zt(n,t,r){var e=0,u=n?n.length:e;if(typeof t=="number"&&t===t&&u<=Uu){for(;e<u;){var o=e+u>>>1,i=n[o];(r?i<=t:i<t)&&null!==i?e=o+1:u=o}return u}return Bt(n,t,Ne,r)}function Bt(n,t,r,e){t=r(t);for(var u=0,o=n?n.length:0,i=t!==t,f=null===t,a=t===w;u<o;){ +var c=wu((u+o)/2),l=r(n[c]),s=l!==w,p=l===l;(i?p||e:f?p&&s&&(e||null!=l):a?p&&(e||s):null==l?0:e?l<=t:l<t)?u=c+1:o=c}return ku(o,Su)}function Dt(n,t,r){if(typeof n!="function")return Ne;if(t===w)return n;switch(r){case 1:return function(r){return n.call(t,r)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,o){return n.call(t,r,e,u,o)};case 5:return function(r,e,u,o,i){return n.call(t,r,e,u,o,i)}}return function(){return n.apply(t,arguments)}}function Mt(n){var t=new au(n.byteLength); +return new gu(t).set(new gu(n)),t}function qt(n,t,r){for(var e=r.length,u=-1,o=ju(n.length-e,0),i=-1,f=t.length,a=De(f+o);++i<f;)a[i]=t[i];for(;++u<e;)a[r[u]]=n[u];for(;o--;)a[i++]=n[u++];return a}function Kt(n,t,r){for(var e=-1,u=r.length,o=-1,i=ju(n.length-u,0),f=-1,a=t.length,c=De(i+a);++o<i;)c[o]=n[o];for(i=o;++f<a;)c[i+f]=t[f];for(;++e<u;)c[i+r[e]]=n[o++];return c}function Vt(n,t){return function(r,e,u){var o=t?t():{};if(e=br(e,u,3),Wo(r)){u=-1;for(var i=r.length;++u<i;){var f=r[u];n(o,f,e(f,u,r),r); +}}else zu(r,function(t,r,u){n(o,t,e(t,r,u),u)});return o}}function Zt(n){return pe(function(t,r){var e=-1,u=null==t?0:r.length,o=2<u?r[u-2]:w,i=2<u?r[2]:w,f=1<u?r[u-1]:w;for(typeof o=="function"?(o=Dt(o,f,5),u-=2):(o=typeof f=="function"?f:w,u-=o?1:0),i&&$r(r[0],r[1],i)&&(o=3>u?w:o,u=1);++e<u;)(i=r[e])&&n(t,i,o);return t})}function Yt(n,t){return function(r,e){var u=r?Vu(r):0;if(!Lr(u))return n(r,e);for(var o=t?u:-1,i=Dr(r);(t?o--:++o<u)&&false!==e(i[o],o,i););return r}}function Gt(n){return function(t,r,e){ +var u=Dr(t);e=e(t);for(var o=e.length,i=n?o:-1;n?i--:++i<o;){var f=e[i];if(false===r(u[f],f,u))break}return t}}function Jt(n,t){function r(){return(this&&this!==Yn&&this instanceof r?e:n).apply(t,arguments)}var e=Ht(n);return r}function Xt(n){return function(t){var r=-1;t=Fe(Ue(t));for(var e=t.length,u="";++r<e;)u=n(u,t[r],r);return u}}function Ht(n){return function(){var t=arguments;switch(t.length){case 0:return new n;case 1:return new n(t[0]);case 2:return new n(t[0],t[1]);case 3:return new n(t[0],t[1],t[2]); +case 4:return new n(t[0],t[1],t[2],t[3]);case 5:return new n(t[0],t[1],t[2],t[3],t[4]);case 6:return new n(t[0],t[1],t[2],t[3],t[4],t[5]);case 7:return new n(t[0],t[1],t[2],t[3],t[4],t[5],t[6])}var r=Pu(n.prototype),t=n.apply(r,t);return de(t)?t:r}}function Qt(n){function t(r,e,u){return u&&$r(r,e,u)&&(e=w),r=dr(r,n,w,w,w,w,w,e),r.placeholder=t.placeholder,r}return t}function nr(n,t){return pe(function(r){var e=r[0];return null==e?e:(r.push(t),n.apply(w,r))})}function tr(n,t){return function(r,e,u){ +if(u&&$r(r,e,u)&&(e=w),e=br(e,u,3),1==e.length){u=r=Wo(r)?r:Br(r);for(var o=e,i=-1,f=u.length,a=t,c=a;++i<f;){var l=u[i],s=+o(l);n(s,a)&&(a=s,c=l)}if(u=c,!r.length||u!==t)return u}return st(r,e,n,t)}}function rr(n,r){return function(e,u,o){return u=br(u,o,3),Wo(e)?(u=t(e,u,r),-1<u?e[u]:w):ht(e,u,n)}}function er(n){return function(r,e,u){return r&&r.length?(e=br(e,u,3),t(r,e,n)):-1}}function ur(n){return function(t,r,e){return r=br(r,e,3),ht(t,r,n,true)}}function or(n){return function(){for(var t,r=arguments.length,e=n?r:-1,u=0,o=De(r);n?e--:++e<r;){ +var i=o[u++]=arguments[e];if(typeof i!="function")throw new Xe(T);!t&&Pn.prototype.thru&&"wrapper"==Ar(i)&&(t=new Pn([],true))}for(e=t?-1:r;++e<r;){var i=o[e],u=Ar(i),f="wrapper"==u?Ku(i):w;t=f&&Fr(f[0])&&f[1]==(E|k|I|C)&&!f[4].length&&1==f[9]?t[Ar(f[0])].apply(t,f[3]):1==i.length&&Fr(i)?t[u]():t.thru(i)}return function(){var n=arguments,e=n[0];if(t&&1==n.length&&Wo(e)&&e.length>=F)return t.plant(e).value();for(var u=0,n=r?o[u].apply(this,n):e;++u<r;)n=o[u].call(this,n);return n}}}function ir(n,t){ +return function(r,e,u){return typeof e=="function"&&u===w&&Wo(r)?n(r,e):t(r,Dt(e,u,3))}}function fr(n){return function(t,r,e){return(typeof r!="function"||e!==w)&&(r=Dt(r,e,3)),n(t,r,Ee)}}function ar(n){return function(t,r,e){return(typeof r!="function"||e!==w)&&(r=Dt(r,e,3)),n(t,r)}}function cr(n){return function(t,r,e){var u={};return r=br(r,e,3),gt(t,function(t,e,o){o=r(t,e,o),e=n?o:e,t=n?t:o,u[e]=t}),u}}function lr(n){return function(t,r,e){return t=u(t),(n?t:"")+_r(t,r,e)+(n?"":t)}}function sr(n){ +var t=pe(function(r,e){var u=v(e,t.placeholder);return dr(r,n,w,e,u)});return t}function pr(n,t){return function(r,e,u,o){var i=3>arguments.length;return typeof e=="function"&&o===w&&Wo(r)?n(r,e,u,i):Ct(r,br(e,o,4),u,i,t)}}function hr(n,t,r,e,u,o,i,f,a,c){function l(){for(var m=arguments.length,x=m,j=De(m);x--;)j[x]=arguments[x];if(e&&(j=qt(j,e,u)),o&&(j=Kt(j,o,i)),_||y){var x=l.placeholder,k=v(j,x),m=m-k.length;if(m<c){var O=f?qn(f):w,m=ju(c-m,0),E=_?k:w,k=_?w:k,C=_?j:w,j=_?w:j;return t|=_?I:R,t&=~(_?R:I), +g||(t&=~(b|A)),j=[n,t,r,C,E,j,k,O,a,m],O=hr.apply(w,j),Fr(n)&&Zu(O,j),O.placeholder=x,O}}if(x=p?r:this,O=h?x[n]:n,f)for(m=j.length,E=ku(f.length,m),k=qn(j);E--;)C=f[E],j[E]=Ur(C,m)?k[C]:w;return s&&a<j.length&&(j.length=a),this&&this!==Yn&&this instanceof l&&(O=d||Ht(n)),O.apply(x,j)}var s=t&E,p=t&b,h=t&A,_=t&k,g=t&j,y=t&O,d=h?w:Ht(n);return l}function _r(n,t,r){return n=n.length,t=+t,n<t&&bu(t)?(t-=n,r=null==r?" ":r+"",$e(r,du(t/r.length)).slice(0,t)):""}function vr(n,t,r,e){function u(){for(var t=-1,f=arguments.length,a=-1,c=e.length,l=De(c+f);++a<c;)l[a]=e[a]; +for(;f--;)l[a++]=arguments[++t];return(this&&this!==Yn&&this instanceof u?i:n).apply(o?r:this,l)}var o=t&b,i=Ht(n);return u}function gr(n){var t=Ve[n];return function(n,r){return(r=r===w?0:+r||0)?(r=su(10,r),t(n*r)/r):t(n)}}function yr(n){return function(t,r,e,u){var o=br(e);return null==e&&o===it?zt(t,r,n):Bt(t,r,o(e,u,1),n)}}function dr(n,t,r,e,u,o,i,f){var a=t&A;if(!a&&typeof n!="function")throw new Xe(T);var c=e?e.length:0;if(c||(t&=~(I|R),e=u=w),c-=u?u.length:0,t&R){var l=e,s=u;e=u=w}var p=a?w:Ku(n); +return r=[n,t,r,e,u,l,s,o,i,f],p&&(e=r[1],t=p[1],f=e|t,u=t==E&&e==k||t==E&&e==C&&r[7].length<=p[8]||t==(E|C)&&e==k,(f<E||u)&&(t&b&&(r[2]=p[2],f|=e&b?0:j),(e=p[3])&&(u=r[3],r[3]=u?qt(u,e,p[4]):qn(e),r[4]=u?v(r[3],P):qn(p[4])),(e=p[5])&&(u=r[5],r[5]=u?Kt(u,e,p[6]):qn(e),r[6]=u?v(r[5],P):qn(p[6])),(e=p[7])&&(r[7]=qn(e)),t&E&&(r[8]=null==r[8]?p[8]:ku(r[8],p[8])),null==r[9]&&(r[9]=p[9]),r[0]=p[0],r[1]=f),t=r[1],f=r[9]),r[9]=null==f?a?0:n.length:ju(f-c,0)||0,n=t==b?Jt(r[0],r[2]):t!=I&&t!=(b|I)||r[4].length?hr.apply(w,r):vr.apply(w,r), +(p?qu:Zu)(n,r)}function mr(n,t,r,e,u,o,i){var f=-1,a=n.length,c=t.length;if(a!=c&&(!u||c<=a))return false;for(;++f<a;){var l=n[f],c=t[f],s=e?e(u?c:l,u?l:c,f):w;if(s!==w){if(s)continue;return false}if(u){if(!nt(t,function(n){return l===n||r(l,n,e,u,o,i)}))return false}else if(l!==c&&!r(l,c,e,u,o,i))return false}return true}function wr(n,t,r){switch(r){case D:case M:return+n==+t;case q:return n.name==t.name&&n.message==t.message;case V:return n!=+n?t!=+t:n==+t;case Y:case G:return n==t+""}return false}function xr(n,t,r,e,u,o,i){ +var f=Ko(n),a=f.length,c=Ko(t).length;if(a!=c&&!u)return false;for(c=a;c--;){var l=f[c];if(!(u?l in t:eu.call(t,l)))return false}for(var s=u;++c<a;){var l=f[c],p=n[l],h=t[l],_=e?e(u?h:p,u?p:h,l):w;if(_===w?!r(p,h,e,u,o,i):!_)return false;s||(s="constructor"==l)}return s||(r=n.constructor,e=t.constructor,!(r!=e&&"constructor"in n&&"constructor"in t)||typeof r=="function"&&r instanceof r&&typeof e=="function"&&e instanceof e)?true:false}function br(n,t,r){var e=Nn.callback||Le,e=e===Le?it:e;return r?e(n,t,r):e}function Ar(n){ +for(var t=n.name+"",r=Fu[t],e=r?r.length:0;e--;){var u=r[e],o=u.func;if(null==o||o==n)return u.name}return t}function jr(n,t,e){var u=Nn.indexOf||Yr,u=u===Yr?r:u;return n?u(n,t,e):u}function kr(n){n=Ce(n);for(var t=n.length;t--;){var r,e=n[t];r=n[t][1],r=r===r&&!de(r),e[2]=r}return n}function Or(n,t){var r=null==n?w:n[t];return me(r)?r:w}function Ir(n){var t=n.length,r=new n.constructor(t);return t&&"string"==typeof n[0]&&eu.call(n,"index")&&(r.index=n.index,r.input=n.input),r}function Rr(n){return n=n.constructor, +typeof n=="function"&&n instanceof n||(n=Ye),new n}function Er(n,t,r){var e=n.constructor;switch(t){case J:return Mt(n);case D:case M:return new e(+n);case X:case H:case Q:case nn:case tn:case rn:case en:case un:case on:return e instanceof e&&(e=Lu[t]),t=n.buffer,new e(r?Mt(t):t,n.byteOffset,n.length);case V:case G:return new e(n);case Y:var u=new e(n.source,kn.exec(n));u.lastIndex=n.lastIndex}return u}function Cr(n,t,r){return null==n||Wr(t,n)||(t=Mr(t),n=1==t.length?n:mt(n,St(t,0,-1)),t=Gr(t)), +t=null==n?n:n[t],null==t?w:t.apply(n,r)}function Sr(n){return null!=n&&Lr(Vu(n))}function Ur(n,t){return n=typeof n=="number"||Rn.test(n)?+n:-1,t=null==t?$u:t,-1<n&&0==n%1&&n<t}function $r(n,t,r){if(!de(r))return false;var e=typeof t;return("number"==e?Sr(r)&&Ur(t,r.length):"string"==e&&t in r)?(t=r[t],n===n?n===t:t!==t):false}function Wr(n,t){var r=typeof n;return"string"==r&&dn.test(n)||"number"==r?true:Wo(n)?false:!yn.test(n)||null!=t&&n in Dr(t)}function Fr(n){var t=Ar(n),r=Nn[t];return typeof r=="function"&&t in zn.prototype?n===r?true:(t=Ku(r), +!!t&&n===t[0]):false}function Lr(n){return typeof n=="number"&&-1<n&&0==n%1&&n<=$u}function Nr(n,t){return n===w?t:Fo(n,t,Nr)}function Tr(n,t){n=Dr(n);for(var r=-1,e=t.length,u={};++r<e;){var o=t[r];o in n&&(u[o]=n[o])}return u}function Pr(n,t){var r={};return vt(n,function(n,e,u){t(n,e,u)&&(r[e]=n)}),r}function zr(n){for(var t=Ee(n),r=t.length,e=r&&n.length,u=!!e&&Lr(e)&&(Wo(n)||_e(n)||Ae(n)),o=-1,i=[];++o<r;){var f=t[o];(u&&Ur(f,e)||eu.call(n,f))&&i.push(f)}return i}function Br(n){return null==n?[]:Sr(n)?Nn.support.unindexedChars&&Ae(n)?n.split(""):de(n)?n:Ye(n):Se(n); +}function Dr(n){if(Nn.support.unindexedChars&&Ae(n)){for(var t=-1,r=n.length,e=Ye(n);++t<r;)e[t]=n.charAt(t);return e}return de(n)?n:Ye(n)}function Mr(n){if(Wo(n))return n;var t=[];return u(n).replace(mn,function(n,r,e,u){t.push(e?u.replace(An,"$1"):r||n)}),t}function qr(n){return n instanceof zn?n.clone():new Pn(n.__wrapped__,n.__chain__,qn(n.__actions__))}function Kr(n,t,r){return n&&n.length?((r?$r(n,t,r):null==t)&&(t=1),St(n,0>t?0:t)):[]}function Vr(n,t,r){var e=n?n.length:0;return e?((r?$r(n,t,r):null==t)&&(t=1), +t=e-(+t||0),St(n,0,0>t?0:t)):[]}function Zr(n){return n?n[0]:w}function Yr(n,t,e){var u=n?n.length:0;if(!u)return-1;if(typeof e=="number")e=0>e?ju(u+e,0):e;else if(e)return e=zt(n,t),e<u&&(t===t?t===n[e]:n[e]!==n[e])?e:-1;return r(n,t,e||0)}function Gr(n){var t=n?n.length:0;return t?n[t-1]:w}function Jr(n){return Kr(n,1)}function Xr(n,t,e,u){if(!n||!n.length)return[];null!=t&&typeof t!="boolean"&&(u=e,e=$r(n,t,u)?w:t,t=false);var o=br();if((null!=e||o!==it)&&(e=o(e,u,3)),t&&jr()===r){t=e;var i;e=-1, +u=n.length;for(var o=-1,f=[];++e<u;){var a=n[e],c=t?t(a,e,n):a;e&&i===c||(i=c,f[++o]=a)}n=f}else n=Lt(n,e);return n}function Hr(n){if(!n||!n.length)return[];var t=-1,r=0;n=Zn(n,function(n){return Sr(n)?(r=ju(n.length,r),true):void 0});for(var e=De(r);++t<r;)e[t]=Xn(n,Ot(t));return e}function Qr(n,t,r){return n&&n.length?(n=Hr(n),null==t?n:(t=Dt(t,r,4),Xn(n,function(n){return Qn(n,t,w,true)}))):[]}function ne(n,t){var r=-1,e=n?n.length:0,u={};for(!e||t||Wo(n[0])||(t=[]);++r<e;){var o=n[r];t?u[o]=t[r]:o&&(u[o[0]]=o[1]); +}return u}function te(n){return n=Nn(n),n.__chain__=true,n}function re(n,t,r){return t.call(r,n)}function ee(n,t,r){var e=Wo(n)?Vn:lt;return r&&$r(n,t,r)&&(t=w),(typeof t!="function"||r!==w)&&(t=br(t,r,3)),e(n,t)}function ue(n,t,r){var e=Wo(n)?Zn:pt;return t=br(t,r,3),e(n,t)}function oe(n,t,r,e){var u=n?Vu(n):0;return Lr(u)||(n=Se(n),u=n.length),r=typeof r!="number"||e&&$r(t,r,e)?0:0>r?ju(u+r,0):r||0,typeof n=="string"||!Wo(n)&&Ae(n)?r<=u&&-1<n.indexOf(t,r):!!u&&-1<jr(n,t,r)}function ie(n,t,r){var e=Wo(n)?Xn:bt; +return t=br(t,r,3),e(n,t)}function fe(n,t,r){if(r?$r(n,t,r):null==t){n=Br(n);var e=n.length;return 0<e?n[Et(0,e-1)]:w}r=-1,n=Oe(n);var e=n.length,u=e-1;for(t=ku(0>t?0:+t||0,e);++r<t;){var e=Et(r,u),o=n[e];n[e]=n[r],n[r]=o}return n.length=t,n}function ae(n,t,r){var e=Wo(n)?nt:Ut;return r&&$r(n,t,r)&&(t=w),(typeof t!="function"||r!==w)&&(t=br(t,r,3)),e(n,t)}function ce(n,t){var r;if(typeof t!="function"){if(typeof n!="function")throw new Xe(T);var e=n;n=t,t=e}return function(){return 0<--n&&(r=t.apply(this,arguments)), +1>=n&&(t=w),r}}function le(n,t,r){function e(t,r){r&&cu(r),a=p=h=w,t&&(_=wo(),c=n.apply(s,f),p||a||(f=s=w))}function u(){var n=t-(wo()-l);0>=n||n>t?e(h,a):p=_u(u,n)}function o(){e(g,p)}function i(){if(f=arguments,l=wo(),s=this,h=g&&(p||!y),false===v)var r=y&&!p;else{a||y||(_=l);var e=v-(l-_),i=0>=e||e>v;i?(a&&(a=cu(a)),_=l,c=n.apply(s,f)):a||(a=_u(o,e))}return i&&p?p=cu(p):p||t===v||(p=_u(u,t)),r&&(i=true,c=n.apply(s,f)),!i||p||a||(f=s=w),c}var f,a,c,l,s,p,h,_=0,v=false,g=true;if(typeof n!="function")throw new Xe(T); +if(t=0>t?0:+t||0,true===r)var y=true,g=false;else de(r)&&(y=!!r.leading,v="maxWait"in r&&ju(+r.maxWait||0,t),g="trailing"in r?!!r.trailing:g);return i.cancel=function(){p&&cu(p),a&&cu(a),_=0,a=p=h=w},i}function se(n,t){if(typeof n!="function"||t&&typeof t!="function")throw new Xe(T);var r=function(){var e=arguments,u=t?t.apply(this,e):e[0],o=r.cache;return o.has(u)?o.get(u):(e=n.apply(this,e),r.cache=o.set(u,e),e)};return r.cache=new se.Cache,r}function pe(n,t){if(typeof n!="function")throw new Xe(T);return t=ju(t===w?n.length-1:+t||0,0), +function(){for(var r=arguments,e=-1,u=ju(r.length-t,0),o=De(u);++e<u;)o[e]=r[t+e];switch(t){case 0:return n.call(this,o);case 1:return n.call(this,r[0],o);case 2:return n.call(this,r[0],r[1],o)}for(u=De(t+1),e=-1;++e<t;)u[e]=r[e];return u[t]=o,n.apply(this,u)}}function he(n,t){return n>t}function _e(n){return h(n)&&Sr(n)&&eu.call(n,"callee")&&!pu.call(n,"callee")}function ve(n,t,r,e){return e=(r=typeof r=="function"?Dt(r,e,3):w)?r(n,t):w,e===w?wt(n,t,r):!!e}function ge(n){return h(n)&&typeof n.message=="string"&&ou.call(n)==q; +}function ye(n){return de(n)&&ou.call(n)==K}function de(n){var t=typeof n;return!!n&&("object"==t||"function"==t)}function me(n){return null==n?false:ye(n)?fu.test(ru.call(n)):h(n)&&(Gn(n)?fu:In).test(n)}function we(n){return typeof n=="number"||h(n)&&ou.call(n)==V}function xe(n){var t;if(!h(n)||ou.call(n)!=Z||Gn(n)||_e(n)||!(eu.call(n,"constructor")||(t=n.constructor,typeof t!="function"||t instanceof t)))return false;var r;return Nn.support.ownLast?(vt(n,function(n,t,e){return r=eu.call(e,t),false}),false!==r):(vt(n,function(n,t){ +r=t}),r===w||eu.call(n,r))}function be(n){return de(n)&&ou.call(n)==Y}function Ae(n){return typeof n=="string"||h(n)&&ou.call(n)==G}function je(n){return h(n)&&Lr(n.length)&&!!Fn[ou.call(n)]}function ke(n,t){return n<t}function Oe(n){var t=n?Vu(n):0;return Lr(t)?t?Nn.support.unindexedChars&&Ae(n)?n.split(""):qn(n):[]:Se(n)}function Ie(n){return ot(n,Ee(n))}function Re(n){return dt(n,Ee(n))}function Ee(n){if(null==n)return[];de(n)||(n=Ye(n));for(var t=n.length,r=Nn.support,t=t&&Lr(t)&&(Wo(n)||_e(n)||Ae(n))&&t||0,e=n.constructor,u=-1,e=ye(e)&&e.prototype||nu,o=e===n,i=De(t),f=0<t,a=r.enumErrorProps&&(n===Qe||n instanceof qe),c=r.enumPrototypes&&ye(n);++u<t;)i[u]=u+""; +for(var l in n)c&&"prototype"==l||a&&("message"==l||"name"==l)||f&&Ur(l,t)||"constructor"==l&&(o||!eu.call(n,l))||i.push(l);if(r.nonEnumShadows&&n!==nu)for(t=n===tu?G:n===Qe?q:ou.call(n),r=Nu[t]||Nu[Z],t==Z&&(e=nu),t=Wn.length;t--;)l=Wn[t],u=r[l],o&&u||(u?!eu.call(n,l):n[l]===e[l])||i.push(l);return i}function Ce(n){n=Dr(n);for(var t=-1,r=Ko(n),e=r.length,u=De(e);++t<e;){var o=r[t];u[t]=[o,n[o]]}return u}function Se(n){return Nt(n,Ko(n))}function Ue(n){return(n=u(n))&&n.replace(En,a).replace(bn,""); +}function $e(n,t){var r="";if(n=u(n),t=+t,1>t||!n||!bu(t))return r;do t%2&&(r+=n),t=wu(t/2),n+=n;while(t);return r}function We(n,t,r){var e=n;return(n=u(n))?(r?$r(e,t,r):null==t)?n.slice(g(n),y(n)+1):(t+="",n.slice(o(n,t),i(n,t)+1)):n}function Fe(n,t,r){return r&&$r(n,t,r)&&(t=w),n=u(n),n.match(t||Un)||[]}function Le(n,t,r){return r&&$r(n,t,r)&&(t=w),h(n)?Te(n):it(n,t)}function Ne(n){return n}function Te(n){return At(ft(n,true))}function Pe(n,t,r){if(null==r){var e=de(t),u=e?Ko(t):w;((u=u&&u.length?dt(t,u):w)?u.length:e)||(u=false, +r=t,t=n,n=this)}u||(u=dt(t,Ko(t)));var o=true,e=-1,i=ye(n),f=u.length;false===r?o=false:de(r)&&"chain"in r&&(o=r.chain);for(;++e<f;){r=u[e];var a=t[r];n[r]=a,i&&(n.prototype[r]=function(t){return function(){var r=this.__chain__;if(o||r){var e=n(this.__wrapped__);return(e.__actions__=qn(this.__actions__)).push({func:t,args:arguments,thisArg:n}),e.__chain__=r,e}return t.apply(n,Hn([this.value()],arguments))}}(a))}return n}function ze(){}function Be(n){return Wr(n)?Ot(n):It(n)}_=_?Jn.defaults(Yn.Object(),_,Jn.pick(Yn,$n)):Yn; +var De=_.Array,Me=_.Date,qe=_.Error,Ke=_.Function,Ve=_.Math,Ze=_.Number,Ye=_.Object,Ge=_.RegExp,Je=_.String,Xe=_.TypeError,He=De.prototype,Qe=qe.prototype,nu=Ye.prototype,tu=Je.prototype,ru=Ke.prototype.toString,eu=nu.hasOwnProperty,uu=0,ou=nu.toString,iu=Yn._,fu=Ge("^"+ru.call(eu).replace(/[\\^$.*+?()[\]{}|]/g,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),au=_.ArrayBuffer,cu=_.clearTimeout,lu=_.parseFloat,su=Ve.pow,pu=nu.propertyIsEnumerable,hu=Or(_,"Set"),_u=_.setTimeout,vu=He.splice,gu=_.Uint8Array,yu=Or(_,"WeakMap"),du=Ve.ceil,mu=Or(Ye,"create"),wu=Ve.floor,xu=Or(De,"isArray"),bu=_.isFinite,Au=Or(Ye,"keys"),ju=Ve.max,ku=Ve.min,Ou=Or(Me,"now"),Iu=_.parseInt,Ru=Ve.random,Eu=Ze.NEGATIVE_INFINITY,Cu=Ze.POSITIVE_INFINITY,Su=4294967294,Uu=2147483647,$u=9007199254740991,Wu=yu&&new yu,Fu={},Lu={}; +Lu[X]=_.Float32Array,Lu[H]=_.Float64Array,Lu[Q]=_.Int8Array,Lu[nn]=_.Int16Array,Lu[tn]=_.Int32Array,Lu[rn]=gu,Lu[en]=_.Uint8ClampedArray,Lu[un]=_.Uint16Array,Lu[on]=_.Uint32Array;var Nu={};Nu[B]=Nu[M]=Nu[V]={constructor:true,toLocaleString:true,toString:true,valueOf:true},Nu[D]=Nu[G]={constructor:true,toString:true,valueOf:true},Nu[q]=Nu[K]=Nu[Y]={constructor:true,toString:true},Nu[Z]={constructor:true},Kn(Wn,function(n){for(var t in Nu)if(eu.call(Nu,t)){var r=Nu[t];r[n]=eu.call(r,n)}});var Tu=Nn.support={};!function(n){ +var t=function(){this.x=n},r={0:n,length:n},e=[];t.prototype={valueOf:n,y:n};for(var u in new t)e.push(u);Tu.enumErrorProps=pu.call(Qe,"message")||pu.call(Qe,"name"),Tu.enumPrototypes=pu.call(t,"prototype"),Tu.nonEnumShadows=!/valueOf/.test(e),Tu.ownLast="x"!=e[0],Tu.spliceObjects=(vu.call(r,0,1),!r[0]),Tu.unindexedChars="xx"!="x"[0]+Ye("x")[0]}(1,0),Nn.templateSettings={escape:_n,evaluate:vn,interpolate:gn,variable:"",imports:{_:Nn}};var Pu=function(){function n(){}return function(t){if(de(t)){n.prototype=t; +var r=new n;n.prototype=w}return r||{}}}(),zu=Yt(gt),Bu=Yt(yt,true),Du=Gt(),Mu=Gt(true),qu=Wu?function(n,t){return Wu.set(n,t),n}:Ne,Ku=Wu?function(n){return Wu.get(n)}:ze,Vu=Ot("length"),Zu=function(){var n=0,t=0;return function(r,e){var u=wo(),o=W-(u-t);if(t=u,0<o){if(++n>=$)return r}else n=0;return qu(r,e)}}(),Yu=pe(function(n,t){return h(n)&&Sr(n)?ct(n,_t(t,false,true)):[]}),Gu=er(),Ju=er(true),Xu=pe(function(n){for(var t=n.length,e=t,u=De(l),o=jr(),i=o===r,f=[];e--;){var a=n[e]=Sr(a=n[e])?a:[];u[e]=i&&120<=a.length&&mu&&hu?new Dn(e&&a):null; +}var i=n[0],c=-1,l=i?i.length:0,s=u[0];n:for(;++c<l;)if(a=i[c],0>(s?Mn(s,a):o(f,a,0))){for(e=t;--e;){var p=u[e];if(0>(p?Mn(p,a):o(n[e],a,0)))continue n}s&&s.push(a),f.push(a)}return f}),Hu=pe(function(t,r){r=_t(r);var e=ut(t,r);return Rt(t,r.sort(n)),e}),Qu=yr(),no=yr(true),to=pe(function(n){return Lt(_t(n,false,true))}),ro=pe(function(n,t){return Sr(n)?ct(n,t):[]}),eo=pe(Hr),uo=pe(function(n){var t=n.length,r=2<t?n[t-2]:w,e=1<t?n[t-1]:w;return 2<t&&typeof r=="function"?t-=2:(r=1<t&&typeof e=="function"?(--t, +e):w,e=w),n.length=t,Qr(n,r,e)}),oo=pe(function(n){return n=_t(n),this.thru(function(t){t=Wo(t)?t:[Dr(t)];for(var r=n,e=-1,u=t.length,o=-1,i=r.length,f=De(u+i);++e<u;)f[e]=t[e];for(;++o<i;)f[e++]=r[o];return f})}),io=pe(function(n,t){return Sr(n)&&(n=Br(n)),ut(n,_t(t))}),fo=Vt(function(n,t,r){eu.call(n,r)?++n[r]:n[r]=1}),ao=rr(zu),co=rr(Bu,true),lo=ir(Kn,zu),so=ir(function(n,t){for(var r=n.length;r--&&false!==t(n[r],r,n););return n},Bu),po=Vt(function(n,t,r){eu.call(n,r)?n[r].push(t):n[r]=[t]}),ho=Vt(function(n,t,r){ +n[r]=t}),_o=pe(function(n,t,r){var e=-1,u=typeof t=="function",o=Wr(t),i=Sr(n)?De(n.length):[];return zu(n,function(n){var f=u?t:o&&null!=n?n[t]:w;i[++e]=f?f.apply(n,r):Cr(n,t,r)}),i}),vo=Vt(function(n,t,r){n[r?0:1].push(t)},function(){return[[],[]]}),go=pr(Qn,zu),yo=pr(function(n,t,r,e){var u=n.length;for(e&&u&&(r=n[--u]);u--;)r=t(r,n[u],u,n);return r},Bu),mo=pe(function(n,t){if(null==n)return[];var r=t[2];return r&&$r(t[0],t[1],r)&&(t.length=1),Wt(n,_t(t),[])}),wo=Ou||function(){return(new Me).getTime(); +},xo=pe(function(n,t,r){var e=b;if(r.length)var u=v(r,xo.placeholder),e=e|I;return dr(n,e,t,r,u)}),bo=pe(function(n,t){t=t.length?_t(t):Re(n);for(var r=-1,e=t.length;++r<e;){var u=t[r];n[u]=dr(n[u],b,n)}return n}),Ao=pe(function(n,t,r){var e=b|A;if(r.length)var u=v(r,Ao.placeholder),e=e|I;return dr(t,e,n,r,u)}),jo=Qt(k),ko=Qt(O),Oo=pe(function(n,t){return at(n,1,t)}),Io=pe(function(n,t,r){return at(n,t,r)}),Ro=or(),Eo=or(true),Co=pe(function(n,t){if(t=_t(t),typeof n!="function"||!Vn(t,e))throw new Xe(T); +var r=t.length;return pe(function(e){for(var u=ku(e.length,r);u--;)e[u]=t[u](e[u]);return n.apply(this,e)})}),So=sr(I),Uo=sr(R),$o=pe(function(n,t){return dr(n,C,w,w,w,_t(t))}),Wo=xu||function(n){return h(n)&&Lr(n.length)&&ou.call(n)==B},Fo=Zt(kt),Lo=Zt(function(n,t,r){return r?rt(n,t,r):et(n,t)}),No=nr(Lo,function(n,t){return n===w?t:n}),To=nr(Fo,Nr),Po=ur(gt),zo=ur(yt),Bo=fr(Du),Do=fr(Mu),Mo=ar(gt),qo=ar(yt),Ko=Au?function(n){var t=null==n?w:n.constructor;return typeof t=="function"&&t.prototype===n||(typeof n=="function"?Nn.support.enumPrototypes:Sr(n))?zr(n):de(n)?Au(n):[]; +}:zr,Vo=cr(true),Zo=cr(),Yo=pe(function(n,t){if(null==n)return{};if("function"!=typeof t[0])return t=Xn(_t(t),Je),Tr(n,ct(Ee(n),t));var r=Dt(t[0],t[1],3);return Pr(n,function(n,t,e){return!r(n,t,e)})}),Go=pe(function(n,t){return null==n?{}:"function"==typeof t[0]?Pr(n,Dt(t[0],t[1],3)):Tr(n,_t(t))}),Jo=Xt(function(n,t,r){return t=t.toLowerCase(),n+(r?t.charAt(0).toUpperCase()+t.slice(1):t)}),Xo=Xt(function(n,t,r){return n+(r?"-":"")+t.toLowerCase()}),Ho=lr(),Qo=lr(true),ni=Xt(function(n,t,r){return n+(r?"_":"")+t.toLowerCase(); +}),ti=Xt(function(n,t,r){return n+(r?" ":"")+(t.charAt(0).toUpperCase()+t.slice(1))}),ri=pe(function(n,t){try{return n.apply(w,t)}catch(r){return ge(r)?r:new qe(r)}}),ei=pe(function(n,t){return function(r){return Cr(r,n,t)}}),ui=pe(function(n,t){return function(r){return Cr(n,r,t)}}),oi=gr("ceil"),ii=gr("floor"),fi=tr(he,Eu),ai=tr(ke,Cu),ci=gr("round");return Nn.prototype=Tn.prototype,Pn.prototype=Pu(Tn.prototype),Pn.prototype.constructor=Pn,zn.prototype=Pu(Tn.prototype),zn.prototype.constructor=zn, +Bn.prototype["delete"]=function(n){return this.has(n)&&delete this.__data__[n]},Bn.prototype.get=function(n){return"__proto__"==n?w:this.__data__[n]},Bn.prototype.has=function(n){return"__proto__"!=n&&eu.call(this.__data__,n)},Bn.prototype.set=function(n,t){return"__proto__"!=n&&(this.__data__[n]=t),this},Dn.prototype.push=function(n){var t=this.data;typeof n=="string"||de(n)?t.set.add(n):t.hash[n]=true},se.Cache=Bn,Nn.after=function(n,t){if(typeof t!="function"){if(typeof n!="function")throw new Xe(T); +var r=n;n=t,t=r}return n=bu(n=+n)?n:0,function(){return 1>--n?t.apply(this,arguments):void 0}},Nn.ary=function(n,t,r){return r&&$r(n,t,r)&&(t=w),t=n&&null==t?n.length:ju(+t||0,0),dr(n,E,w,w,w,w,t)},Nn.assign=Lo,Nn.at=io,Nn.before=ce,Nn.bind=xo,Nn.bindAll=bo,Nn.bindKey=Ao,Nn.callback=Le,Nn.chain=te,Nn.chunk=function(n,t,r){t=(r?$r(n,t,r):null==t)?1:ju(wu(t)||1,1),r=0;for(var e=n?n.length:0,u=-1,o=De(du(e/t));r<e;)o[++u]=St(n,r,r+=t);return o},Nn.compact=function(n){for(var t=-1,r=n?n.length:0,e=-1,u=[];++t<r;){ +var o=n[t];o&&(u[++e]=o)}return u},Nn.constant=function(n){return function(){return n}},Nn.countBy=fo,Nn.create=function(n,t,r){var e=Pu(n);return r&&$r(n,t,r)&&(t=w),t?et(e,t):e},Nn.curry=jo,Nn.curryRight=ko,Nn.debounce=le,Nn.defaults=No,Nn.defaultsDeep=To,Nn.defer=Oo,Nn.delay=Io,Nn.difference=Yu,Nn.drop=Kr,Nn.dropRight=Vr,Nn.dropRightWhile=function(n,t,r){return n&&n.length?Tt(n,br(t,r,3),true,true):[]},Nn.dropWhile=function(n,t,r){return n&&n.length?Tt(n,br(t,r,3),true):[]},Nn.fill=function(n,t,r,e){ +var u=n?n.length:0;if(!u)return[];for(r&&typeof r!="number"&&$r(n,t,r)&&(r=0,e=u),u=n.length,r=null==r?0:+r||0,0>r&&(r=-r>u?0:u+r),e=e===w||e>u?u:+e||0,0>e&&(e+=u),u=r>e?0:e>>>0,r>>>=0;r<u;)n[r++]=t;return n},Nn.filter=ue,Nn.flatten=function(n,t,r){var e=n?n.length:0;return r&&$r(n,t,r)&&(t=false),e?_t(n,t):[]},Nn.flattenDeep=function(n){return n&&n.length?_t(n,true):[]},Nn.flow=Ro,Nn.flowRight=Eo,Nn.forEach=lo,Nn.forEachRight=so,Nn.forIn=Bo,Nn.forInRight=Do,Nn.forOwn=Mo,Nn.forOwnRight=qo,Nn.functions=Re, +Nn.groupBy=po,Nn.indexBy=ho,Nn.initial=function(n){return Vr(n,1)},Nn.intersection=Xu,Nn.invert=function(n,t,r){r&&$r(n,t,r)&&(t=w),r=-1;for(var e=Ko(n),u=e.length,o={};++r<u;){var i=e[r],f=n[i];t?eu.call(o,f)?o[f].push(i):o[f]=[i]:o[f]=i}return o},Nn.invoke=_o,Nn.keys=Ko,Nn.keysIn=Ee,Nn.map=ie,Nn.mapKeys=Vo,Nn.mapValues=Zo,Nn.matches=Te,Nn.matchesProperty=function(n,t){return jt(n,ft(t,true))},Nn.memoize=se,Nn.merge=Fo,Nn.method=ei,Nn.methodOf=ui,Nn.mixin=Pe,Nn.modArgs=Co,Nn.negate=function(n){if(typeof n!="function")throw new Xe(T); +return function(){return!n.apply(this,arguments)}},Nn.omit=Yo,Nn.once=function(n){return ce(2,n)},Nn.pairs=Ce,Nn.partial=So,Nn.partialRight=Uo,Nn.partition=vo,Nn.pick=Go,Nn.pluck=function(n,t){return ie(n,Be(t))},Nn.property=Be,Nn.propertyOf=function(n){return function(t){return mt(n,Mr(t),t+"")}},Nn.pull=function(){var n=arguments,t=n[0];if(!t||!t.length)return t;for(var r=0,e=jr(),u=n.length;++r<u;)for(var o=0,i=n[r];-1<(o=e(t,i,o));)vu.call(t,o,1);return t},Nn.pullAt=Hu,Nn.range=function(n,t,r){ +r&&$r(n,t,r)&&(t=r=w),n=+n||0,r=null==r?1:+r||0,null==t?(t=n,n=0):t=+t||0;var e=-1;t=ju(du((t-n)/(r||1)),0);for(var u=De(t);++e<t;)u[e]=n,n+=r;return u},Nn.rearg=$o,Nn.reject=function(n,t,r){var e=Wo(n)?Zn:pt;return t=br(t,r,3),e(n,function(n,r,e){return!t(n,r,e)})},Nn.remove=function(n,t,r){var e=[];if(!n||!n.length)return e;var u=-1,o=[],i=n.length;for(t=br(t,r,3);++u<i;)r=n[u],t(r,u,n)&&(e.push(r),o.push(u));return Rt(n,o),e},Nn.rest=Jr,Nn.restParam=pe,Nn.set=function(n,t,r){if(null==n)return n; +var e=t+"";t=null!=n[e]||Wr(t,n)?[e]:Mr(t);for(var e=-1,u=t.length,o=u-1,i=n;null!=i&&++e<u;){var f=t[e];de(i)&&(e==o?i[f]=r:null==i[f]&&(i[f]=Ur(t[e+1])?[]:{})),i=i[f]}return n},Nn.shuffle=function(n){return fe(n,Cu)},Nn.slice=function(n,t,r){var e=n?n.length:0;return e?(r&&typeof r!="number"&&$r(n,t,r)&&(t=0,r=e),St(n,t,r)):[]},Nn.sortBy=function(n,t,r){if(null==n)return[];r&&$r(n,t,r)&&(t=w);var e=-1;return t=br(t,r,3),n=bt(n,function(n,r,u){return{a:t(n,r,u),b:++e,c:n}}),$t(n,f)},Nn.sortByAll=mo, +Nn.sortByOrder=function(n,t,r,e){return null==n?[]:(e&&$r(t,r,e)&&(r=w),Wo(t)||(t=null==t?[]:[t]),Wo(r)||(r=null==r?[]:[r]),Wt(n,t,r))},Nn.spread=function(n){if(typeof n!="function")throw new Xe(T);return function(t){return n.apply(this,t)}},Nn.take=function(n,t,r){return n&&n.length?((r?$r(n,t,r):null==t)&&(t=1),St(n,0,0>t?0:t)):[]},Nn.takeRight=function(n,t,r){var e=n?n.length:0;return e?((r?$r(n,t,r):null==t)&&(t=1),t=e-(+t||0),St(n,0>t?0:t)):[]},Nn.takeRightWhile=function(n,t,r){return n&&n.length?Tt(n,br(t,r,3),false,true):[]; +},Nn.takeWhile=function(n,t,r){return n&&n.length?Tt(n,br(t,r,3)):[]},Nn.tap=function(n,t,r){return t.call(r,n),n},Nn.throttle=function(n,t,r){var e=true,u=true;if(typeof n!="function")throw new Xe(T);return false===r?e=false:de(r)&&(e="leading"in r?!!r.leading:e,u="trailing"in r?!!r.trailing:u),le(n,t,{leading:e,maxWait:+t,trailing:u})},Nn.thru=re,Nn.times=function(n,t,r){if(n=wu(n),1>n||!bu(n))return[];var e=-1,u=De(ku(n,4294967295));for(t=Dt(t,r,1);++e<n;)4294967295>e?u[e]=t(e):t(e);return u},Nn.toArray=Oe, +Nn.toPlainObject=Ie,Nn.transform=function(n,t,r,e){var u=Wo(n)||je(n);return t=br(t,e,4),null==r&&(u||de(n)?(e=n.constructor,r=u?Wo(n)?new e:[]:Pu(ye(e)?e.prototype:w)):r={}),(u?Kn:gt)(n,function(n,e,u){return t(r,n,e,u)}),r},Nn.union=to,Nn.uniq=Xr,Nn.unzip=Hr,Nn.unzipWith=Qr,Nn.values=Se,Nn.valuesIn=function(n){return Nt(n,Ee(n))},Nn.where=function(n,t){return ue(n,At(t))},Nn.without=ro,Nn.wrap=function(n,t){return t=null==t?Ne:t,dr(t,I,w,[n],[])},Nn.xor=function(){for(var n=-1,t=arguments.length;++n<t;){ +var r=arguments[n];if(Sr(r))var e=e?Hn(ct(e,r),ct(r,e)):r}return e?Lt(e):[]},Nn.zip=eo,Nn.zipObject=ne,Nn.zipWith=uo,Nn.backflow=Eo,Nn.collect=ie,Nn.compose=Eo,Nn.each=lo,Nn.eachRight=so,Nn.extend=Lo,Nn.iteratee=Le,Nn.methods=Re,Nn.object=ne,Nn.select=ue,Nn.tail=Jr,Nn.unique=Xr,Pe(Nn,Nn),Nn.add=function(n,t){return(+n||0)+(+t||0)},Nn.attempt=ri,Nn.camelCase=Jo,Nn.capitalize=function(n){return(n=u(n))&&n.charAt(0).toUpperCase()+n.slice(1)},Nn.ceil=oi,Nn.clone=function(n,t,r,e){return t&&typeof t!="boolean"&&$r(n,t,r)?t=false:typeof t=="function"&&(e=r, +r=t,t=false),typeof r=="function"?ft(n,t,Dt(r,e,3)):ft(n,t)},Nn.cloneDeep=function(n,t,r){return typeof t=="function"?ft(n,true,Dt(t,r,3)):ft(n,true)},Nn.deburr=Ue,Nn.endsWith=function(n,t,r){n=u(n),t+="";var e=n.length;return r=r===w?e:ku(0>r?0:+r||0,e),r-=t.length,0<=r&&n.indexOf(t,r)==r},Nn.escape=function(n){return(n=u(n))&&hn.test(n)?n.replace(sn,c):n},Nn.escapeRegExp=function(n){return(n=u(n))&&xn.test(n)?n.replace(wn,l):n||"(?:)"},Nn.every=ee,Nn.find=ao,Nn.findIndex=Gu,Nn.findKey=Po,Nn.findLast=co, +Nn.findLastIndex=Ju,Nn.findLastKey=zo,Nn.findWhere=function(n,t){return ao(n,At(t))},Nn.first=Zr,Nn.floor=ii,Nn.get=function(n,t,r){return n=null==n?w:mt(n,Mr(t),t+""),n===w?r:n},Nn.gt=he,Nn.gte=function(n,t){return n>=t},Nn.has=function(n,t){if(null==n)return false;var r=eu.call(n,t);if(!r&&!Wr(t)){if(t=Mr(t),n=1==t.length?n:mt(n,St(t,0,-1)),null==n)return false;t=Gr(t),r=eu.call(n,t)}return r||Lr(n.length)&&Ur(t,n.length)&&(Wo(n)||_e(n)||Ae(n))},Nn.identity=Ne,Nn.includes=oe,Nn.indexOf=Yr,Nn.inRange=function(n,t,r){ +return t=+t||0,r===w?(r=t,t=0):r=+r||0,n>=ku(t,r)&&n<ju(t,r)},Nn.isArguments=_e,Nn.isArray=Wo,Nn.isBoolean=function(n){return true===n||false===n||h(n)&&ou.call(n)==D},Nn.isDate=function(n){return h(n)&&ou.call(n)==M},Nn.isElement=function(n){return!!n&&1===n.nodeType&&h(n)&&!xe(n)},Nn.isEmpty=function(n){return null==n?true:Sr(n)&&(Wo(n)||Ae(n)||_e(n)||h(n)&&ye(n.splice))?!n.length:!Ko(n).length},Nn.isEqual=ve,Nn.isError=ge,Nn.isFinite=function(n){return typeof n=="number"&&bu(n)},Nn.isFunction=ye,Nn.isMatch=function(n,t,r,e){ +return r=typeof r=="function"?Dt(r,e,3):w,xt(n,kr(t),r)},Nn.isNaN=function(n){return we(n)&&n!=+n},Nn.isNative=me,Nn.isNull=function(n){return null===n},Nn.isNumber=we,Nn.isObject=de,Nn.isPlainObject=xe,Nn.isRegExp=be,Nn.isString=Ae,Nn.isTypedArray=je,Nn.isUndefined=function(n){return n===w},Nn.kebabCase=Xo,Nn.last=Gr,Nn.lastIndexOf=function(n,t,r){var e=n?n.length:0;if(!e)return-1;var u=e;if(typeof r=="number")u=(0>r?ju(e+r,0):ku(r||0,e-1))+1;else if(r)return u=zt(n,t,true)-1,n=n[u],(t===t?t===n:n!==n)?u:-1; +if(t!==t)return p(n,u,true);for(;u--;)if(n[u]===t)return u;return-1},Nn.lt=ke,Nn.lte=function(n,t){return n<=t},Nn.max=fi,Nn.min=ai,Nn.noConflict=function(){return Yn._=iu,this},Nn.noop=ze,Nn.now=wo,Nn.pad=function(n,t,r){n=u(n),t=+t;var e=n.length;return e<t&&bu(t)?(e=(t-e)/2,t=wu(e),e=du(e),r=_r("",e,r),r.slice(0,t)+n+r):n},Nn.padLeft=Ho,Nn.padRight=Qo,Nn.parseInt=function(n,t,r){return(r?$r(n,t,r):null==t)?t=0:t&&(t=+t),n=We(n),Iu(n,t||(On.test(n)?16:10))},Nn.random=function(n,t,r){r&&$r(n,t,r)&&(t=r=w); +var e=null==n,u=null==t;return null==r&&(u&&typeof n=="boolean"?(r=n,n=1):typeof t=="boolean"&&(r=t,u=true)),e&&u&&(t=1,u=false),n=+n||0,u?(t=n,n=0):t=+t||0,r||n%1||t%1?(r=Ru(),ku(n+r*(t-n+lu("1e-"+((r+"").length-1))),t)):Et(n,t)},Nn.reduce=go,Nn.reduceRight=yo,Nn.repeat=$e,Nn.result=function(n,t,r){var e=null==n?w:Dr(n)[t];return e===w&&(null==n||Wr(t,n)||(t=Mr(t),n=1==t.length?n:mt(n,St(t,0,-1)),e=null==n?w:Dr(n)[Gr(t)]),e=e===w?r:e),ye(e)?e.call(n):e},Nn.round=ci,Nn.runInContext=m,Nn.size=function(n){ +var t=n?Vu(n):0;return Lr(t)?t:Ko(n).length},Nn.snakeCase=ni,Nn.some=ae,Nn.sortedIndex=Qu,Nn.sortedLastIndex=no,Nn.startCase=ti,Nn.startsWith=function(n,t,r){return n=u(n),r=null==r?0:ku(0>r?0:+r||0,n.length),n.lastIndexOf(t,r)==r},Nn.sum=function(n,t,r){if(r&&$r(n,t,r)&&(t=w),t=br(t,r,3),1==t.length){n=Wo(n)?n:Br(n),r=n.length;for(var e=0;r--;)e+=+t(n[r])||0;n=e}else n=Ft(n,t);return n},Nn.template=function(n,t,r){var e=Nn.templateSettings;r&&$r(n,t,r)&&(t=r=w),n=u(n),t=rt(et({},r||t),e,tt),r=rt(et({},t.imports),e.imports,tt); +var o,i,f=Ko(r),a=Nt(r,f),c=0;r=t.interpolate||Cn;var l="__p+='";r=Ge((t.escape||Cn).source+"|"+r.source+"|"+(r===gn?jn:Cn).source+"|"+(t.evaluate||Cn).source+"|$","g");var p="sourceURL"in t?"//# sourceURL="+t.sourceURL+"\n":"";if(n.replace(r,function(t,r,e,u,f,a){return e||(e=u),l+=n.slice(c,a).replace(Sn,s),r&&(o=true,l+="'+__e("+r+")+'"),f&&(i=true,l+="';"+f+";\n__p+='"),e&&(l+="'+((__t=("+e+"))==null?'':__t)+'"),c=a+t.length,t}),l+="';",(t=t.variable)||(l="with(obj){"+l+"}"),l=(i?l.replace(fn,""):l).replace(an,"$1").replace(cn,"$1;"), +l="function("+(t||"obj")+"){"+(t?"":"obj||(obj={});")+"var __t,__p=''"+(o?",__e=_.escape":"")+(i?",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,'')}":";")+l+"return __p}",t=ri(function(){return Ke(f,p+"return "+l).apply(w,a)}),t.source=l,ge(t))throw t;return t},Nn.trim=We,Nn.trimLeft=function(n,t,r){var e=n;return(n=u(n))?n.slice((r?$r(e,t,r):null==t)?g(n):o(n,t+"")):n},Nn.trimRight=function(n,t,r){var e=n;return(n=u(n))?(r?$r(e,t,r):null==t)?n.slice(0,y(n)+1):n.slice(0,i(n,t+"")+1):n; +},Nn.trunc=function(n,t,r){r&&$r(n,t,r)&&(t=w);var e=S;if(r=U,null!=t)if(de(t)){var o="separator"in t?t.separator:o,e="length"in t?+t.length||0:e;r="omission"in t?u(t.omission):r}else e=+t||0;if(n=u(n),e>=n.length)return n;if(e-=r.length,1>e)return r;if(t=n.slice(0,e),null==o)return t+r;if(be(o)){if(n.slice(e).search(o)){var i,f=n.slice(0,e);for(o.global||(o=Ge(o.source,(kn.exec(o)||"")+"g")),o.lastIndex=0;n=o.exec(f);)i=n.index;t=t.slice(0,null==i?e:i)}}else n.indexOf(o,e)!=e&&(o=t.lastIndexOf(o), +-1<o&&(t=t.slice(0,o)));return t+r},Nn.unescape=function(n){return(n=u(n))&&pn.test(n)?n.replace(ln,d):n},Nn.uniqueId=function(n){var t=++uu;return u(n)+t},Nn.words=Fe,Nn.all=ee,Nn.any=ae,Nn.contains=oe,Nn.eq=ve,Nn.detect=ao,Nn.foldl=go,Nn.foldr=yo,Nn.head=Zr,Nn.include=oe,Nn.inject=go,Pe(Nn,function(){var n={};return gt(Nn,function(t,r){Nn.prototype[r]||(n[r]=t)}),n}(),false),Nn.sample=fe,Nn.prototype.sample=function(n){return this.__chain__||null!=n?this.thru(function(t){return fe(t,n)}):fe(this.value()); +},Nn.VERSION=x,Kn("bind bindKey curry curryRight partial partialRight".split(" "),function(n){Nn[n].placeholder=Nn}),Kn(["drop","take"],function(n,t){zn.prototype[n]=function(r){var e=this.__filtered__;if(e&&!t)return new zn(this);r=null==r?1:ju(wu(r)||0,0);var u=this.clone();return e?u.__takeCount__=ku(u.__takeCount__,r):u.__views__.push({size:r,type:n+(0>u.__dir__?"Right":"")}),u},zn.prototype[n+"Right"]=function(t){return this.reverse()[n](t).reverse()}}),Kn(["filter","map","takeWhile"],function(n,t){ +var r=t+1,e=r!=N;zn.prototype[n]=function(n,t){var u=this.clone();return u.__iteratees__.push({iteratee:br(n,t,1),type:r}),u.__filtered__=u.__filtered__||e,u}}),Kn(["first","last"],function(n,t){var r="take"+(t?"Right":"");zn.prototype[n]=function(){return this[r](1).value()[0]}}),Kn(["initial","rest"],function(n,t){var r="drop"+(t?"":"Right");zn.prototype[n]=function(){return this.__filtered__?new zn(this):this[r](1)}}),Kn(["pluck","where"],function(n,t){var r=t?"filter":"map",e=t?At:Be;zn.prototype[n]=function(n){ +return this[r](e(n))}}),zn.prototype.compact=function(){return this.filter(Ne)},zn.prototype.reject=function(n,t){return n=br(n,t,1),this.filter(function(t){return!n(t)})},zn.prototype.slice=function(n,t){n=null==n?0:+n||0;var r=this;return r.__filtered__&&(0<n||0>t)?new zn(r):(0>n?r=r.takeRight(-n):n&&(r=r.drop(n)),t!==w&&(t=+t||0,r=0>t?r.dropRight(-t):r.take(t-n)),r)},zn.prototype.takeRightWhile=function(n,t){return this.reverse().takeWhile(n,t).reverse()},zn.prototype.toArray=function(){return this.take(Cu); +},gt(zn.prototype,function(n,t){var r=/^(?:filter|map|reject)|While$/.test(t),e=/^(?:first|last)$/.test(t),u=Nn[e?"take"+("last"==t?"Right":""):t];u&&(Nn.prototype[t]=function(){var t=e?[1]:arguments,o=this.__chain__,i=this.__wrapped__,f=!!this.__actions__.length,a=i instanceof zn,c=t[0],l=a||Wo(i);l&&r&&typeof c=="function"&&1!=c.length&&(a=l=false);var s=function(n){return e&&o?u(n,1)[0]:u.apply(w,Hn([n],t))},c={func:re,args:[s],thisArg:w},f=a&&!f;return e&&!o?f?(i=i.clone(),i.__actions__.push(c), +n.call(i)):u.call(w,this.value())[0]:!e&&l?(i=f?i:new zn(this),i=n.apply(i,t),i.__actions__.push(c),new Pn(i,o)):this.thru(s)})}),Kn("join pop push replace shift sort splice split unshift".split(" "),function(n){var t=(/^(?:replace|split)$/.test(n)?tu:He)[n],r=/^(?:push|sort|unshift)$/.test(n)?"tap":"thru",e=!Tu.spliceObjects&&/^(?:pop|shift|splice)$/.test(n),u=/^(?:join|pop|replace|shift)$/.test(n),o=e?function(){var n=t.apply(this,arguments);return 0===this.length&&delete this[0],n}:t;Nn.prototype[n]=function(){ +var n=arguments;return u&&!this.__chain__?o.apply(this.value(),n):this[r](function(t){return o.apply(t,n)})}}),gt(zn.prototype,function(n,t){var r=Nn[t];if(r){var e=r.name+"";(Fu[e]||(Fu[e]=[])).push({name:t,func:r})}}),Fu[hr(w,A).name]=[{name:"wrapper",func:w}],zn.prototype.clone=function(){var n=new zn(this.__wrapped__);return n.__actions__=qn(this.__actions__),n.__dir__=this.__dir__,n.__filtered__=this.__filtered__,n.__iteratees__=qn(this.__iteratees__),n.__takeCount__=this.__takeCount__,n.__views__=qn(this.__views__), +n},zn.prototype.reverse=function(){if(this.__filtered__){var n=new zn(this);n.__dir__=-1,n.__filtered__=true}else n=this.clone(),n.__dir__*=-1;return n},zn.prototype.value=function(){var n,t=this.__wrapped__.value(),r=this.__dir__,e=Wo(t),u=0>r,o=e?t.length:0;n=0;for(var i=o,f=this.__views__,a=-1,c=f.length;++a<c;){var l=f[a],s=l.size;switch(l.type){case"drop":n+=s;break;case"dropRight":i-=s;break;case"take":i=ku(i,n+s);break;case"takeRight":n=ju(n,i-s)}}if(n={start:n,end:i},i=n.start,f=n.end,n=f-i, +u=u?f:i-1,i=this.__iteratees__,f=i.length,a=0,c=ku(n,this.__takeCount__),!e||o<F||o==n&&c==n)return Pt(t,this.__actions__);e=[];n:for(;n--&&a<c;){for(u+=r,o=-1,l=t[u];++o<f;){var p=i[o],s=p.type,p=p.iteratee(l);if(s==N)l=p;else if(!p){if(s==L)continue n;break n}}e[a++]=l}return e},Nn.prototype.chain=function(){return te(this)},Nn.prototype.commit=function(){return new Pn(this.value(),this.__chain__)},Nn.prototype.concat=oo,Nn.prototype.plant=function(n){for(var t,r=this;r instanceof Tn;){var e=qr(r); +t?u.__wrapped__=e:t=e;var u=e,r=r.__wrapped__}return u.__wrapped__=n,t},Nn.prototype.reverse=function(){var n=this.__wrapped__,t=function(n){return n.reverse()};return n instanceof zn?(this.__actions__.length&&(n=new zn(this)),n=n.reverse(),n.__actions__.push({func:re,args:[t],thisArg:w}),new Pn(n,this.__chain__)):this.thru(t)},Nn.prototype.toString=function(){return this.value()+""},Nn.prototype.run=Nn.prototype.toJSON=Nn.prototype.valueOf=Nn.prototype.value=function(){return Pt(this.__wrapped__,this.__actions__); +},Nn.prototype.collect=Nn.prototype.map,Nn.prototype.head=Nn.prototype.first,Nn.prototype.select=Nn.prototype.filter,Nn.prototype.tail=Nn.prototype.rest,Nn}var w,x="3.10.1",b=1,A=2,j=4,k=8,O=16,I=32,R=64,E=128,C=256,S=30,U="...",$=150,W=16,F=200,L=1,N=2,T="Expected a function",P="__lodash_placeholder__",z="[object Arguments]",B="[object Array]",D="[object Boolean]",M="[object Date]",q="[object Error]",K="[object Function]",V="[object Number]",Z="[object Object]",Y="[object RegExp]",G="[object String]",J="[object ArrayBuffer]",X="[object Float32Array]",H="[object Float64Array]",Q="[object Int8Array]",nn="[object Int16Array]",tn="[object Int32Array]",rn="[object Uint8Array]",en="[object Uint8ClampedArray]",un="[object Uint16Array]",on="[object Uint32Array]",fn=/\b__p\+='';/g,an=/\b(__p\+=)''\+/g,cn=/(__e\(.*?\)|\b__t\))\+'';/g,ln=/&(?:amp|lt|gt|quot|#39|#96);/g,sn=/[&<>"'`]/g,pn=RegExp(ln.source),hn=RegExp(sn.source),_n=/<%-([\s\S]+?)%>/g,vn=/<%([\s\S]+?)%>/g,gn=/<%=([\s\S]+?)%>/g,yn=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\n\\]|\\.)*?\1)\]/,dn=/^\w*$/,mn=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\n\\]|\\.)*?)\2)\]/g,wn=/^[:!,]|[\\^$.*+?()[\]{}|\/]|(^[0-9a-fA-Fnrtuvx])|([\n\r\u2028\u2029])/g,xn=RegExp(wn.source),bn=/[\u0300-\u036f\ufe20-\ufe23]/g,An=/\\(\\)?/g,jn=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,kn=/\w*$/,On=/^0[xX]/,In=/^\[object .+?Constructor\]$/,Rn=/^\d+$/,En=/[\xc0-\xd6\xd8-\xde\xdf-\xf6\xf8-\xff]/g,Cn=/($^)/,Sn=/['\n\r\u2028\u2029\\]/g,Un=RegExp("[A-Z\\xc0-\\xd6\\xd8-\\xde]+(?=[A-Z\\xc0-\\xd6\\xd8-\\xde][a-z\\xdf-\\xf6\\xf8-\\xff]+)|[A-Z\\xc0-\\xd6\\xd8-\\xde]?[a-z\\xdf-\\xf6\\xf8-\\xff]+|[A-Z\\xc0-\\xd6\\xd8-\\xde]+|[0-9]+","g"),$n="Array ArrayBuffer Date Error Float32Array Float64Array Function Int8Array Int16Array Int32Array Math Number Object RegExp Set String _ clearTimeout isFinite parseFloat parseInt setTimeout TypeError Uint8Array Uint8ClampedArray Uint16Array Uint32Array WeakMap".split(" "),Wn="constructor hasOwnProperty isPrototypeOf propertyIsEnumerable toLocaleString toString valueOf".split(" "),Fn={}; +Fn[X]=Fn[H]=Fn[Q]=Fn[nn]=Fn[tn]=Fn[rn]=Fn[en]=Fn[un]=Fn[on]=true,Fn[z]=Fn[B]=Fn[J]=Fn[D]=Fn[M]=Fn[q]=Fn[K]=Fn["[object Map]"]=Fn[V]=Fn[Z]=Fn[Y]=Fn["[object Set]"]=Fn[G]=Fn["[object WeakMap]"]=false;var Ln={};Ln[z]=Ln[B]=Ln[J]=Ln[D]=Ln[M]=Ln[X]=Ln[H]=Ln[Q]=Ln[nn]=Ln[tn]=Ln[V]=Ln[Z]=Ln[Y]=Ln[G]=Ln[rn]=Ln[en]=Ln[un]=Ln[on]=true,Ln[q]=Ln[K]=Ln["[object Map]"]=Ln["[object Set]"]=Ln["[object WeakMap]"]=false;var Nn={"\xc0":"A","\xc1":"A","\xc2":"A","\xc3":"A","\xc4":"A","\xc5":"A","\xe0":"a","\xe1":"a","\xe2":"a", +"\xe3":"a","\xe4":"a","\xe5":"a","\xc7":"C","\xe7":"c","\xd0":"D","\xf0":"d","\xc8":"E","\xc9":"E","\xca":"E","\xcb":"E","\xe8":"e","\xe9":"e","\xea":"e","\xeb":"e","\xcc":"I","\xcd":"I","\xce":"I","\xcf":"I","\xec":"i","\xed":"i","\xee":"i","\xef":"i","\xd1":"N","\xf1":"n","\xd2":"O","\xd3":"O","\xd4":"O","\xd5":"O","\xd6":"O","\xd8":"O","\xf2":"o","\xf3":"o","\xf4":"o","\xf5":"o","\xf6":"o","\xf8":"o","\xd9":"U","\xda":"U","\xdb":"U","\xdc":"U","\xf9":"u","\xfa":"u","\xfb":"u","\xfc":"u","\xdd":"Y", +"\xfd":"y","\xff":"y","\xc6":"Ae","\xe6":"ae","\xde":"Th","\xfe":"th","\xdf":"ss"},Tn={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},Pn={"&":"&","<":"<",">":">",""":'"',"'":"'","`":"`"},zn={"function":true,object:true},Bn={0:"x30",1:"x31",2:"x32",3:"x33",4:"x34",5:"x35",6:"x36",7:"x37",8:"x38",9:"x39",A:"x41",B:"x42",C:"x43",D:"x44",E:"x45",F:"x46",a:"x61",b:"x62",c:"x63",d:"x64",e:"x65",f:"x66",n:"x6e",r:"x72",t:"x74",u:"x75",v:"x76",x:"x78"},Dn={"\\":"\\", +"'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},Mn=zn[typeof exports]&&exports&&!exports.nodeType&&exports,qn=zn[typeof module]&&module&&!module.nodeType&&module,Kn=zn[typeof self]&&self&&self.Object&&self,Vn=zn[typeof window]&&window&&window.Object&&window,Zn=qn&&qn.exports===Mn&&Mn,Yn=Mn&&qn&&typeof global=="object"&&global&&global.Object&&global||Vn!==(this&&this.window)&&Vn||Kn||this,Gn=function(){try{Object({toString:0}+"")}catch(n){return function(){return false}}return function(n){ +return typeof n.toString!="function"&&typeof(n+"")=="string"}}(),Jn=m();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(Yn._=Jn, define(function(){return Jn})):Mn&&qn?Zn?(qn.exports=Jn)._=Jn:Mn._=Jn:Yn._=Jn}).call(this); \ No newline at end of file diff --git a/js/menu-editor.js b/js/menu-editor.js new file mode 100644 index 0000000..cb037a8 --- /dev/null +++ b/js/menu-editor.js @@ -0,0 +1,5764 @@ +//(c) W-Shadow + +/*global wsEditorData, defaultMenu, customMenu, _:false */ + +/** + * @property wsEditorData + * @property {boolean} wsEditorData.wsMenuEditorPro + * + * @property {object} wsEditorData.blankMenuItem + * @property {object} wsEditorData.itemTemplates + * @property {object} wsEditorData.customItemTemplate + * + * @property {string} wsEditorData.adminAjaxUrl + * @property {string} wsEditorData.imagesUrl + * + * @property {string} wsEditorData.menuFormatName + * @property {string} wsEditorData.menuFormatVersion + * + * @property {boolean} wsEditorData.hideAdvancedSettings + * @property {boolean} wsEditorData.showExtraIcons + * @property {boolean} wsEditorData.dashiconsAvailable + * @property {string} wsEditorData.submenuIconsEnabled + * @property {Object} wsEditorData.showHints + * + * @property {string} wsEditorData.hideAdvancedSettingsNonce + * @property {string} wsEditorData.getPagesNonce + * @property {string} wsEditorData.getPageDetailsNonce + * @property {string} wsEditorData.disableDashboardConfirmationNonce + * + * @property {string} wsEditorData.captionShowAdvanced + * @property {string} wsEditorData.captionHideAdvanced + * + * @property {string} wsEditorData.unclickableTemplateId + * @property {string} wsEditorData.unclickableTemplateClass + * @property {string} wsEditorData.embeddedPageTemplateId + * + * @property {string} wsEditorData.currentUserLogin + * @property {string|null} wsEditorData.selectedActor + * + * @property {object} wsEditorData.actors + * @property {string[]} wsEditorData.visibleUsers + * + * @property {object} wsEditorData.postTypes + * @property {object} wsEditorData.taxonomies + * + * @property {string|null} wsEditorData.selectedMenu + * @property {string|null} wsEditorData.selectedSubmenu + * + * @property {string} wsEditorData.setTestConfigurationNonce + * @property {string} wsEditorData.testAccessNonce + * + * @property {string|null} wsEditorData.deepNestingEnabled + * + * @property {boolean} wsEditorData.isDemoMode + * @property {boolean} wsEditorData.isMasterMode + */ + +wsEditorData.wsMenuEditorPro = !!wsEditorData.wsMenuEditorPro; //Cast to boolean. +var wsIdCounter = 0; + +//A bit of black magic/hack to convince my IDE that wsAmeLodash is an alias for lodash. +window.wsAmeLodash = (function() { + 'use strict'; + if (typeof wsAmeLodash !== 'undefined') { + return wsAmeLodash; + } + return _.noConflict(); +})(); + +//These two properties must be objects, not arrays. +jQuery.each(['grant_access', 'hidden_from_actor'], function(unused, key) { + 'use strict'; + if (wsEditorData.blankMenuItem.hasOwnProperty(key) && !jQuery.isPlainObject(wsEditorData.blankMenuItem[key])) { + wsEditorData.blankMenuItem[key] = {}; + } +}); + +AmeCapabilityManager = AmeActors; + +/** + * A utility for retrieving post and page titles. + */ +var AmePageTitles = (function($) { + 'use strict'; + + var me = {}, cache = {}; + + function getCacheKey(pageId, blogId) { + return blogId + '_' + pageId; + } + + /** + * Add a page title to the cache. + * + * @param {Number} pageId Post or page ID. + * @param {Number} blogId Blog ID. + * @param {String} title The title of the post or page. + */ + me.add = function(pageId, blogId, title) { + cache[getCacheKey(pageId, blogId)] = title; + }; + + /** + * Get page title. + * + * Note: This method does not return the title. Instead, it calls the provided callback with the title + * as the first argument. The callback will be executed asynchronously if the title hasn't been cached yet. + * + * @param {Number} pageId + * @param {Number} blogId + * @param {Function} callback + */ + me.get = function(pageId, blogId, callback) { + var key = getCacheKey(pageId, blogId); + if (typeof cache[key] !== 'undefined') { + callback(cache[key], pageId, blogId); + return; + } + + $.getJSON( + wsEditorData.adminAjaxUrl, + { + 'action' : 'ws_ame_get_page_details', + '_ajax_nonce' : wsEditorData.getPageDetailsNonce, + 'post_id' : pageId, + 'blog_id' : blogId + }, + function(details) { + var title; + if (typeof details.error !== 'undefined'){ + title = details.error; + } else if ((typeof details !== 'object') || (typeof details.post_title === 'undefined')) { + title = '< Server error >'; + } else { + title = details.post_title; + } + cache[key] = title; + + callback(cache[key], pageId, blogId); + } + ); + }; + + return me; +})(jQuery); + +var AmeEditorApi = {}; +window.AmeEditorApi = AmeEditorApi; + + +(function ($, _){ +'use strict'; + +var actorSelectorWidget = new AmeActorSelector(AmeActors, wsEditorData.wsMenuEditorPro); + +AmeEditorApi.actorSelectorWidget = actorSelectorWidget; + +var itemTemplates = { + templates: wsEditorData.itemTemplates, + + getTemplateById: function(templateId) { + if (wsEditorData.itemTemplates.hasOwnProperty(templateId)) { + return wsEditorData.itemTemplates[templateId]; + } else if ((templateId === '') || (templateId === 'custom')) { + return wsEditorData.customItemTemplate; + } + return null; + }, + + getDefaults: function (templateId) { + var template = this.getTemplateById(templateId); + if (template) { + return template.defaults; + } else { + return null; + } + }, + + getDefaultValue: function (templateId, fieldName) { + if (fieldName === 'template_id') { + return null; + } + + var defaults = this.getDefaults(templateId); + if (defaults && (typeof defaults[fieldName] !== 'undefined')) { + return defaults[fieldName]; + } + return null; + }, + + hasDefaultValue: function(templateId, fieldName) { + return (this.getDefaultValue(templateId, fieldName) !== null); + } +}; + + /** + * @type {AmeMenuPresenter} + */ + let menuPresenter; + +/** + * Set an input field to a value. The only difference from jQuery.val() is that + * setting a checkbox to true/false will check/clear it. + * + * @param input + * @param value + */ +function setInputValue(input, value) { + if (input.attr('type') === 'checkbox'){ + input.prop('checked', value); + } else { + input.val(value); + } +} + +/** + * Get the value of an input field. The only difference from jQuery.val() is that + * checked/unchecked checkboxes will return true/false. + * + * @param input + * @return {*} + */ +function getInputValue(input) { + if (input.attr('type') === 'checkbox'){ + return input.is(':checked'); + } + return input.val(); +} + + +/* + * Utility function for generating pseudo-random alphanumeric menu IDs. + * Rationale: Simpler than atomically auto-incrementing or globally unique IDs. + */ +function randomMenuId(prefix, size){ + prefix = (typeof prefix === 'undefined') ? 'custom_item_' : prefix; + size = (typeof size === 'undefined') ? 5 : size; + + var suffix = ""; + var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + for( var i=0; i < size; i++ ) { + suffix += possible.charAt(Math.floor(Math.random() * possible.length)); + } + + return prefix + suffix; +} +AmeEditorApi.randomMenuId = randomMenuId; + +function outputWpMenu(menu){ + const menuCopy = $.extend(true, {}, menu); + + //Remove the current menu data + menuPresenter.clear(); + + //Display the new menu + const firstColumn = menuPresenter.getColumnImmediate(1); + const itemList = firstColumn.getVisibleItemList(); + for (let filename in menuCopy){ + if (!menuCopy.hasOwnProperty(filename)){ + continue; + } + firstColumn.outputItem(menuCopy[filename], null, itemList); + } + + //Automatically select the first top-level menu + if (itemList) { + itemList.find('.ws_menu:first').trigger('click'); + } +} + +/** + * Load a menu configuration in the editor. + * Note: All previous settings will be discarded without warning. Unsaved changes will be lost. + * + * @param {Object} adminMenu The menu structure to load. + */ +function loadMenuConfiguration(adminMenu) { + //There are some menu properties that need to be objects, but PHP JSON-encodes empty associative + //arrays as numeric arrays. We want them to be empty objects instead. + if (adminMenu.hasOwnProperty('color_presets') && !$.isPlainObject(adminMenu.color_presets)) { + adminMenu.color_presets = {}; + } + + var objectProperties = ['grant_access', 'hidden_from_actor']; + //noinspection JSUnusedLocalSymbols + function fixEmptyObjects(unused, menuItem) { + for (var i = 0; i < objectProperties.length; i++) { + var key = objectProperties[i]; + if (menuItem.hasOwnProperty(key) && !$.isPlainObject(menuItem[key])) { + menuItem[key] = {}; + } + } + if (menuItem.hasOwnProperty('items')) { + $.each(menuItem.items, fixEmptyObjects); + } + } + $.each(adminMenu.tree, fixEmptyObjects); + + //Load color presets from the new configuration. + if (typeof adminMenu.color_presets === 'object') { + colorPresets = $.extend(true, {}, adminMenu.color_presets); + } else { + colorPresets = {}; + } + wasPresetDropdownPopulated = false; + + //Load capabilities. + AmeCapabilityManager.setGrantedCapabilities(_.get(adminMenu, 'granted_capabilities', {})); + + //Load general menu visibility. + generalComponentVisibility = _.get(adminMenu, 'component_visibility', {}); + AmeEditorApi.refreshComponentVisibility(); + + //Display the new admin menu. + outputWpMenu(adminMenu.tree); + + $(document).trigger('menuConfigurationLoaded.adminMenuEditor', adminMenu); +} + + /** + * Check if it's possible to delete a menu item. + * + * @param {JQuery} containerNode + * @returns {boolean} + */ + function canDeleteItem(containerNode) { + if (!containerNode || (containerNode.length < 1)) { + return false; + } + + var menuItem = containerNode.data('menu_item'); + var isDefaultItem = + ( menuItem.template_id !== '') + && ( menuItem.template_id !== wsEditorData.unclickableTemplateId) + && ( menuItem.template_id !== wsEditorData.embeddedPageTemplateId) + && (!menuItem.separator); + + var otherCopiesExist = false; + if (isDefaultItem) { + //Check if there are any other menus with the same template ID. + $('#ws_menu_editor').find('.ws_container').each(function() { + var otherItem = $(this).data('menu_item'); + if ((menuItem !== otherItem) && (menuItem.template_id === otherItem.template_id)) { + otherCopiesExist = true; + return false; + } + return true; + }); + } + + return (!isDefaultItem || otherCopiesExist); + } + + /** + * Get or create the submenu container of a menu item. + * + * @param {JQuery|null} container + * @param {AmeEditorColumn} [nextColumn] + * @return {JQuery|null} + */ + function getSubmenuOf(container, nextColumn) { + if (!container || (container.length < 1)) { + return null; + } + + const submenuId = container.data('submenu_id'); + if (submenuId) { + let $submenu = $('#' + submenuId).first(); + if ($submenu.length > 0) { + return $submenu; + } + } + + //If a submenu doesn't exist yet, create it in the next column. + if (nextColumn) { + return createSubmenuFor(container, nextColumn); + } else { + return null; + } + } + + /** + * Create a submenu container for a menu item. + * @param {JQuery} container + * @param {AmeEditorColumn} nextColumn + * @return {JQuery} + */ + function createSubmenuFor(container, nextColumn) { + const $submenu = nextColumn.buildSubmenuContainer(container.attr('id')); + nextColumn.appendSubmenuContainer($submenu); + container.data('submenu_id', $submenu.attr('id')) + return $submenu; + } + + /** + * @param {Number} level + * @param {JQuery|null} predecessor + * @param {JQuery|null} [container] + * @param {Function} [getNextColumn] + * @constructor + */ + function AmeEditorColumn(level, predecessor, container, getNextColumn) { + const self = this; + + this.level = level; + this.usesSubmenuContainers = (this.level > 1); + + let isNewContainer = false; + if ((typeof container === 'undefined') || (container === null)) { + isNewContainer = true; + container = $('#ame-submenu-column-template').first().clone(); + container.attr('id', ''); + container.find('.ws_box').first().attr('id', ''); + container.show().insertAfter(predecessor); + } + container.data('ame-menu-level', level); + container.addClass('ame-editor-column-' + level); + + this.container = container; + this.menuBox = container.find('.ws_box').first(); + this.dropZone = container.children('.ws_dropzone').first(); + this.visibleItemList = null; + + if (!this.usesSubmenuContainers) { + this.menuBox.addClass('ame-visible-item-list'); + } + + if (typeof getNextColumn !== 'undefined') { + this.getNextColumn = getNextColumn; + } else { + this.getNextColumn = function(callback) { + callback(null); + }; + } + + this.container.children('.ws_toolbar').on('click', '.ws_button', function() { + const $button = $(this); + let buttonAction = $button.data('ame-button-action') || 'unknown'; + let selectedItem = self.getSelectedItem(); + self.container.trigger( + 'adminMenuEditor:action-' + buttonAction, + [(selectedItem.length > 0) ? selectedItem : null, self, $button] + ); + return false; + }); + + if (isNewContainer && (this.dropZone.length > 0)) { + this.container.closest('.ws_main_container').droppable({ + 'hoverClass' : 'ws_top_to_submenu_drop_hover', + + 'accept' : (function(thing) { + const visibleSubmenu = self.getVisibleItemList(); + if (!visibleSubmenu || (visibleSubmenu.length < 1)) { + return false; //Can't drop anything on a non-existent submenu. + } + + function isParentOf(menuItem, something) { + const parent = getParentMenuNode(something) + if (menuItem.is(parent)) { + return true; + } else if (parent.length > 0) { + return isParentOf(menuItem, parent); + } + return false; + } + + const thingContainer = thing.closest('.ws_main_container'); + return ( + //Accept only menus from other columns. + !self.container.is(thingContainer) && + + //Prevent users from dropping a parent menu on one of its own sub-menus. + !isParentOf(thing, visibleSubmenu) + ); + }), + + 'drop' : (function(event, ui){ + const droppedItemData = readItemState(ui.draggable); + self.pasteItem(droppedItemData, null); + if ( !event.ctrlKey ) { + self.destroyItem(ui.draggable); + } + }) + }); + } + } + + /** + * Create editor widgets for a menu item and its submenus. + * + * @param {Object} itemData An object containing menu data. + * @param {JQuery|null} [afterNode] Insert the widget after this node. If it's NULL, the widget + * will be added to the end fo the list. + * @param {JQuery} [itemList] The container where to insert the widget. Defaults to the currently + * visible item list. For columns that don't use submenu containers, it's always the menuBox. + * @return {Object} Object with two fields - 'menu' and 'submenu' - containing the jQuery objects + * of the created widgets. + */ + AmeEditorColumn.prototype.outputItem = function(itemData, afterNode, itemList) { + if (!itemList) { + itemList = this.getVisibleItemList(); + } + const self = this; + + //Create the menu widget + const isTopLevel = this.level <= 1; + const $item = buildMenuItem(itemData, isTopLevel); + + if ((typeof afterNode !== 'undefined') && (afterNode !== null)) { + //phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions -- buildMenuItem() should be safe. + $(afterNode).after($item); + } else { + $item.appendTo(itemList); + } + + const children = (typeof itemData.items !== 'undefined') ? itemData.items : []; + const hasChildren = !_.isEmpty(children); + let $submenu = null; + + this.getNextColumn( + /** + * @param {AmeEditorColumn|null} nextColumn + */ + function (nextColumn) { + if (nextColumn) { + //Create a submenu container even if this item doesn't have children. + //The user could add submenu items later. + $submenu = createSubmenuFor($item, nextColumn); + + //Output children. + if (hasChildren) { + $.each(children, function (index, item) { + nextColumn.outputItem(item, null, $submenu); + }); + } + } else { + //TODO: This branch could be optimized by letting the recursive outputItem call know that there is no next column. + //There is no next column, so any submenu items that belong to this item will be + //displayed in the same column, below the item. + if (hasChildren) { + let $previousItem = $item; + $.each(children, function (index, child) { + const result = self.outputItem(child, $previousItem, itemList); + if (result && result.menu) { + $previousItem = result.menu; + } + }); + } + } + + //Note: Update the menu only after its children are ready. It needs the submenu items to decide + //whether to display the access checkbox as checked or indeterminate. + updateItemEditor($item); + }, + hasChildren + ); + + //Note that $submenu could still be NULL at this point if the "get next column" callback + //is called asynchronously. + return { + 'menu': $item, + 'submenu': $submenu + }; + }; + + /** + * Paste a menu item in this column. + * + * @param {Object} item + * @param {JQuery|null} [afterItem] Defaults to the current selection. Set to NULL to paste at the end of the list. + * @param {JQuery} [itemList] + */ + AmeEditorColumn.prototype.pasteItem = function(item, afterItem, itemList) { + if (typeof afterItem === 'undefined') { + afterItem = this.getSelectedItem(); + if (afterItem.length < 1) { + afterItem = null; + } + } + + if (!itemList) { + itemList = this.getVisibleItemList(); + } + + //The user shouldn't need to worry about giving separators a unique filename. + if (item.separator) { + item.defaults.file = randomMenuId('separator_'); + } + + //If we're pasting from a sub-menu into the top level, we may need to fix some properties + //that are blank for sub-menu items but required for top level menus. + const isTopLevel = this.level <= 1; + if (isTopLevel) { + function isNonEmptyString(value) { + return (typeof value === 'string') && (value !== ''); + } + + if (!isNonEmptyString(getFieldValue(item, 'css_class', ''))) { + item.css_class = 'menu-top'; + } + if (!isNonEmptyString(getFieldValue(item, 'icon_url', ''))) { + item.icon_url = 'dashicons-admin-generic'; + } + if (!isNonEmptyString(getFieldValue(item, 'hookname', ''))) { + item.hookname = randomMenuId(); + } + } + + const result = this.outputItem(item, afterItem, itemList); + + if (this.level > 1) { + updateParentAccessUi(itemList); + } + + return result; + }; + + /** + * @return {JQuery|null} + */ + AmeEditorColumn.prototype.getVisibleItemList = function() { + if (this.usesSubmenuContainers) { + if (this.visibleItemList) { + return this.visibleItemList; + } + + const $list = this.menuBox.children('.ws_submenu:visible').first().addClass('ame-visible-item-list'); + if ($list && ($list.length > 0)) { + this.visibleItemList = $list; + } + return $list; + } else { + return this.menuBox; + } + }; + + /** + * @param {JQuery|null} $submenu + */ + AmeEditorColumn.prototype.setVisibleItemList = function($submenu) { + //Do nothing if the new list is the same as the old one. + if (($submenu === this.visibleItemList) || ($submenu && ($submenu.is(this.visibleItemList)))) { + return; + } + + if (this.visibleItemList) { + this.visibleItemList.hide().removeClass('ame-visible-item-list'); + } + this.visibleItemList = $submenu; + + if (this.visibleItemList) { + this.visibleItemList.show().addClass('ame-visible-item-list'); + } + + //Each item list/submenu has its own own selected item, so switching to a different item list + //also effectively changes the selected item. + this.selectionHasChanged(); + }; + + /** + * @return {JQuery} + */ + AmeEditorColumn.prototype.getAllItemLists = function() { + if (this.usesSubmenuContainers) { + return this.menuBox.children('.ws_submenu'); + } + return this.menuBox; + }; + + /** + * @return {JQuery} + */ + AmeEditorColumn.prototype.getSelectedItem = function() { + const list = this.getVisibleItemList(); + if (list && (list.length > 0)) { + return list.children('.ws_active').first(); + } + return $([]); + }; + + /** + * @param {JQuery} container + */ + AmeEditorColumn.prototype.selectItem = function(container) { + if (container.hasClass('ws_active')) { + //The menu item is already selected. + return; + } + + //Highlight the active item and un-highlight the previous one + container.addClass('ws_active'); + container.siblings('.ws_active').removeClass('ws_active'); + + this.selectionHasChanged(container); + }; + + /** + * @param {JQuery|null} [$item] + */ + AmeEditorColumn.prototype.selectionHasChanged = function($item) { + if (typeof $item === 'undefined') { + $item = this.getSelectedItem(); + } + if (!$item || ($item.length < 1)) { + $item = null; + } + + //Make the "delete" button appear disabled if you can't delete this item. + this.container.find('.ws_toolbar .ws_delete_menu_button') + .toggleClass('ws_button_disabled', !canDeleteItem($item)) + + const self = this; + this.getNextColumn(function(nextColumn) { + if (nextColumn) { + nextColumn.setVisibleItemList(getSubmenuOf($item, nextColumn)); + if ($item) { + self.updateSubmenuBoxHeight($item, nextColumn); + } + } + }, false); + }; + + /** + * @param {JQuery} selectedMenu + * @param {AmeEditorColumn} nextColumn + */ + AmeEditorColumn.prototype.updateSubmenuBoxHeight = function updateSubmenuBoxHeight(selectedMenu, nextColumn) { + if (!nextColumn || (nextColumn === this)) { + return; + } + let mainMenuBox = this.menuBox, + submenuBox = nextColumn.menuBox, + submenuDropZone = nextColumn.dropZone; + + //Make the submenu box tall enough to reach the selected item. + //This prevents the menu tip (if any) from floating in empty space. + if (selectedMenu.hasClass('ws_menu_separator')) { + submenuBox.css('min-height', ''); + } else { + var menuTipHeight = 30, + empiricalExtraHeight = 4, + verticalBoxOffset = (submenuBox.offset().top - mainMenuBox.offset().top), + minSubmenuHeight = (selectedMenu.offset().top - mainMenuBox.offset().top) + - verticalBoxOffset + + menuTipHeight - (submenuDropZone.outerHeight() || 0) + empiricalExtraHeight; + minSubmenuHeight = Math.max(minSubmenuHeight, 0); + submenuBox.css('min-height', minSubmenuHeight); + } + } + + AmeEditorColumn.prototype.buildSubmenuContainer = function(parentMenuId) { + //Create a container for menu items. + const submenu = $('<div class="ws_submenu" style="display:none;"></div>'); + submenu.attr('id', 'ws-submenu-'+(wsIdCounter++)); + + if (parentMenuId) { + submenu.data('parent_menu_id', parentMenuId); + } + + //Make the submenu sortable + makeBoxSortable(submenu); + + return submenu; + }; + + AmeEditorColumn.prototype.appendSubmenuContainer = function($submenu) { + this.usesSubmenuContainers = true; + $submenu.appendTo(this.menuBox); + }; + + /** + * Delete a menu item and all of its children. + * + * @param {JQuery} container + */ + AmeEditorColumn.prototype.destroyItem = function(container) { + const wasSelected = container.is('.ws_active'); + + //Recursively destroy any submenu items. + const submenuId = container.data('submenu_id'); + if (submenuId) { + const self = this; + const $submenu = $('#' + submenuId); + $submenu.children('.ws_container').each(function() { + self.destroyItem($(this)); + }); + $submenu.remove(); + } + + //Destroy the item itself. + container.remove(); + + if (wasSelected) { + this.selectionHasChanged(); + } + }; + + /** + * Remove all items and item lists from this column. + * + * Note: Does not remove item submenus that are in other columns. + */ + AmeEditorColumn.prototype.reset = function() { + this.menuBox.empty(); + this.visibleItemList = null; + this.selectionHasChanged(null); + }; + + /** + * + * @param {JQuery} editorNode + * @param {Boolean|null|string} [deepNestingEnabled] + * @param {Number} [maxLevels] + * @param {Number} [initialLevels] + * @constructor + */ + function AmeMenuPresenter(editorNode, deepNestingEnabled, maxLevels, initialLevels ) { + const self = this; + this.editorNode = editorNode; + + if (typeof deepNestingEnabled === 'string') { + deepNestingEnabled = (deepNestingEnabled === '1'); + } + this.isDeepNestingEnabled = (typeof deepNestingEnabled !== 'undefined') ? deepNestingEnabled : null; + this.nestingQueryPromise = null; + + if (typeof maxLevels === 'undefined') { + maxLevels = 3; + } + if (typeof initialLevels === 'undefined') { + if (this.isDeepNestingEnabled) { + //If additional levels are enabled, show the maximum number of levels. + initialLevels = maxLevels; + } else { + //WordPress only supports up to two levels by default. + initialLevels = Math.min(maxLevels, 2); + } + } + if (initialLevels > this.maxLevels) { + initialLevels = this.maxLevels; + } + + this.maxLevels = maxLevels; + + const $topLevelContainer = this.editorNode.find('#ws_menu_box').first().closest('.ws_main_container'); + this.columns = [ + //Empty zeroth column. + new AmeEditorColumn(0, null, $()), + //The first column contains top level menus. + new AmeEditorColumn(1, null, $topLevelContainer, makeNextColumnGetter(1)) + ]; + this.currentLevels = this.columns.length - 1; + + function makeNextColumnGetter(ownLevel) { + if (ownLevel >= self.maxLevels) { + //This column will never have a next column, so we can just use NULL. + return function(callback) { + callback(null); + }; + } + return function(callback, createIfNotExists) { + self.getColumn(ownLevel + 1, callback, createIfNotExists); + }; + } + + /** + * @param {Number} level + * @return {AmeEditorColumn} + */ + function createColumn(level) { + if (level > self.maxLevels) { + throw new Error('Cannot exceed maximum nesting level: ' + self.maxLevels); + } + if (typeof self.columns[level] !== 'undefined') { + throw new Error('Cannot overwrite an existing column ' + level); + } + + let predecessor; + if (typeof self.columns[level - 1] !== 'undefined') { + predecessor = self.columns[level - 1].container; + } else { + predecessor = self.columns[self.currentLevels].container; + } + + let newColumn = new AmeEditorColumn(level, predecessor, null, makeNextColumnGetter(level)); + self.columns.push(newColumn); + + if (level > self.currentLevels) { + self.currentLevels = level; + } + + return newColumn; + } + + /** + * Can we create another column? + * + * @param {Number} level + * @param {Function} callback + */ + function queryCanCreateColumn(level, callback) { + if ( + (level > self.maxLevels) //Do not exceed the maximum depth. + || (typeof self.columns[level] !== 'undefined') //Do not overwrite existing columns. + ) { + callback(false); + return; + } + + //WordPress core only supports two admin menu levels. We call anything beyond that "deep". + const isDeep = (level > 2); + if (!isDeep) { + callback(true); + return; + } + //Do we already know if we can create deeply nested menus? + if (self.isDeepNestingEnabled !== null) { + callback(self.isDeepNestingEnabled); + return; + } + + //If we're already waiting for a decision, just add another callback to the queue. + if (self.nestingQueryPromise !== null) { + self.nestingQueryPromise.always(function() { + callback(self.isDeepNestingEnabled); + }); + return; + } + + //Let's allow other code/plugins to decide this. Scripts can add deferred objects or promises + //to an array. All deferred objects must resolve successfully to enable deep nesting. + let deferreds = []; + self.editorNode.trigger('adminMenuEditor:queryDeepNesting', [deferreds]); + + if (deferreds.length > 0) { + self.nestingQueryPromise = $.when.apply($, deferreds) + .done(function() { + self.isDeepNestingEnabled = true; + }) + .fail(function() { + self.isDeepNestingEnabled = false; + }) + .always(function() { + callback(self.isDeepNestingEnabled); + }); + } else { + //Deep nesting is disabled by default. + self.isDeepNestingEnabled = false; + callback(self.isDeepNestingEnabled); + } + } + + /** + * Get or create a column. The callback will be called with one argument: either the column object, + * or NULL if the column does not exist and could not be created. + * + * @param {Number} level + * @param {Function} callback + * @param {Boolean} [createIfNotExists] Defaults to true. + */ + this.getColumn = function(level, callback, createIfNotExists) { + if (typeof this.columns[level] !== 'undefined') { + callback(this.columns[level]); + return; + } + + if (typeof createIfNotExists === 'undefined') { + createIfNotExists = true; + } + + if (createIfNotExists) { + queryCanCreateColumn(level, function (isAllowed) { + //It could be that another callback has already created the next column, + //so we need to check again if it exists. + if (typeof self.columns[level] !== 'undefined') { + callback(self.columns[level]); + } else if (isAllowed) { + callback(createColumn(level)); + } else { + callback(null); + } + }); + } else { + callback(null); + } + }; + + /** + * Get or create a column. Like getColumn(), but it will default to not creating deeply nested + * menu levels unless that feature is already enabled. + * + * @param {Number} level + * @return {AmeEditorColumn|null} + */ + this.getColumnImmediate = function(level) { + if (typeof this.columns[level] !== 'undefined') { + return this.columns[level]; + } + if (level > this.maxLevels) { + return null; + } + + if ((level <= 2) || (this.isDeepNestingEnabled === true)) { + return createColumn(level); + } + return null; + }; + + /** + * Get the column that contains a specific item. + * + * @param {JQuery} container Menu item container. + * @return {AmeEditorColumn|null} + */ + this.getItemColumn = function(container) { + if (!container) { + return null; + } + const level = container.closest('.ws_main_container').data('ame-menu-level'); + if (typeof level === 'undefined') { + return null; + } + return this.getColumnImmediate(level); + }; + + /** + * Create editor widgets for a menu item and its submenus and append them all to the DOM. + * + * @param {Number} level + * @param {Object} itemData + * @param {JQuery} [afterNode] Insert the widget after this node. + */ + this.outputMenuItem = function(level, itemData, afterNode) { + const column = this.getColumnImmediate(level); + return column.outputItem(itemData, afterNode); + } + + /** + * Select a menu item and show its submenu. + * + * @param {JQuery} container + */ + this.selectItem = function(container) { + const thisColumn = this.getColumnImmediate(container.closest('.ws_main_container').data('ame-menu-level')); + if (thisColumn) { + thisColumn.selectItem(container); + } + }; + + /** + * Delete a menu item and all of its children. + * + * @param {JQuery} container + */ + this.destroyItem = function(container) { + const column = this.getItemColumn(container); + if (column) { + column.destroyItem(container); + } + }; + + /** + * Delete all items and reset all columns. + */ + this.clear = function() { + for (let level = 0; level < this.columns.length; level++) { + if (typeof this.columns[level] !== 'undefined') { + this.columns[level].reset(); + } + } + }; + + //Initialisation. + for (let level = this.currentLevels + 1; level <= initialLevels; level++) { + createColumn(level); + } + } + +/* + * Create edit widgets for a top-level menu and its submenus and append them all to the DOM. + * + * Inputs : + * menu - an object containing menu data + * afterNode - if specified, the new menu widget will be inserted after this node. Otherwise, + * it will be added to the end of the list. + * Outputs : + * Object with two fields - 'menu' and 'submenu' - containing the DOM nodes of the created widgets. + */ +function outputTopMenu(menu, afterNode){ + if (!menuPresenter) { + throw new Error('outputTopMenu cannot be called before the menu presenter has been initialised.'); + } + return menuPresenter.outputMenuItem(1, menu, afterNode); +} + +/** + * Create an edit widget for a menu item. + * + * @param {Object} itemData + * @param {Boolean} [isTopLevel] Specify if this is a top-level menu or a sub-menu item. Defaults to false (= sub-item). + * @return {*} The created widget as a jQuery object. + */ +function buildMenuItem(itemData, isTopLevel) { + isTopLevel = (typeof isTopLevel === 'undefined') ? false : isTopLevel; + + //Create the menu HTML + var item = $('<div></div>') + .attr('class', "ws_container") + .attr('id', 'ws-menu-item-' + (wsIdCounter++)) + .data('menu_item', itemData) + .data('field_editors_created', false); + + item.addClass(isTopLevel ? 'ws_menu' : 'ws_item'); + if ( itemData.separator ) { + item.addClass('ws_menu_separator'); + } + + //Add a header and a container for property editors (to improve performance + //the editors themselves are created later, when the user tries to access them + //for the first time). + var contents = []; + var menuTitle = getFieldValue(itemData, 'menu_title', ''); + if (menuTitle === '') { + menuTitle = ' '; + } + + contents.push( + '<div class="ws_item_head">', + itemData.separator ? '' : '<a class="ws_edit_link"> </a><div class="ws_flag_container"> </div>', + '<input type="checkbox" class="ws_actor_access_checkbox">', + '<span class="ws_item_title">', + formatMenuTitle(menuTitle), + ' </span>', + + '</div>', + '<div class="ws_editbox" style="display: none;"></div>' + ); + item.append(contents.join('')); + + //Apply flags based on the item's state + var flags = ['hidden', 'unused', 'custom']; + for (var i = 0; i < flags.length; i++) { + setMenuFlag(item, flags[i], getFieldValue(itemData, flags[i], false)); + } + + if ( isTopLevel && !itemData.separator ){ + //Allow the user to drag menu items to top-level menus + item.droppable({ + 'hoverClass' : 'ws_menu_drop_hover', + + 'accept' : (function(thing){ + return thing.hasClass('ws_item'); + }), + + 'drop' : (function(event, ui){ + const column = menuPresenter.getItemColumn(item); + if (!column) { + return; + } + const nextColumn = menuPresenter.getColumnImmediate(column.level + 1); + const submenu = getSubmenuOf(item, nextColumn); + if (!submenu || !nextColumn) { + return; + } + + const droppedItemData = readItemState(ui.draggable); + const sourceSubmenu = ui.draggable.parent(); + + let result = nextColumn.outputItem(droppedItemData, null, submenu); + + if ( !event.ctrlKey ) { + menuPresenter.destroyItem(ui.draggable); + } + + updateItemEditor(result.menu); + + //Moving an item can change aggregate menu permissions. Update the UI accordingly. + updateParentAccessUi(submenu); + if (sourceSubmenu) { + updateParentAccessUi(sourceSubmenu); + } + }) + }); + } + + return item; +} + +function jsTrim(str){ + return str.replace(/^\s+|\s+$/g, ""); +} + +//Expose this handy tool to our other scripts. +AmeEditorApi.jsTrim = jsTrim; + +function stripAllTags(input) { + //Based on: http://phpjs.org/functions/strip_tags/ + var tags = /<\/?([a-z][a-z0-9]*)\b[^>]*>/gi, + commentsAndPhpTags = /<!--[\s\S]*?-->|<\?(?:php)?[\s\S]*?\?>/gi; + return input.replace(commentsAndPhpTags, '').replace(tags, ''); +} + +function truncateString(input, maxLength, padding) { + if (typeof padding === 'undefined') { + padding = ''; + } + + if (input.length > maxLength) { + input = input.substring(0, maxLength - 1) + padding; + } + + return input; +} + +/** + * Format menu title for display in HTML. + * Strips tags and truncates long titles. + * + * @param {String} title + * @returns {String} + */ +function formatMenuTitle(title) { + title = stripAllTags(title); + + //Compact whitespace. + title = title.replace(/[\s\t\r\n]+/g, ' '); + title = jsTrim(title); + + //The max. length was chosen empirically. + title = truncateString(title, 34, '\u2026'); + return title; +} + +//Editor field spec template. +var baseField = { + caption : '[No caption]', + standardCaption : true, + advanced : false, + type : 'text', + defaultValue: '', + onlyForTopMenus: false, + addDropdown : false, + visible: true, + + write: null, + display: null, + + tooltip: null +}; + +/* + * List of all menu fields that have an associated editor + */ +var knownMenuFields = { + 'menu_title' : $.extend({}, baseField, { + caption : 'Menu title', + display: function(menuItem, displayValue, input, containerNode) { + //Update the header as well. + containerNode.find('.ws_item_title').text(formatMenuTitle(displayValue) + '\xa0'); + return displayValue; + }, + write: function(menuItem, value, input, containerNode) { + menuItem.menu_title = value; + containerNode.find('.ws_item_title').text(stripAllTags(input.val()) + '\xa0'); + } + }), + + 'template_id' : $.extend({}, baseField, { + caption : 'Target page', + type : 'select', + options : (function(){ + //Generate name => id mappings for all item templates + the special "Custom" template. + var itemTemplateIds = []; + itemTemplateIds.push([wsEditorData.customItemTemplate.name, '']); + + for (var template_id in wsEditorData.itemTemplates) { + if (wsEditorData.itemTemplates.hasOwnProperty(template_id)) { + itemTemplateIds.push([wsEditorData.itemTemplates[template_id].name, template_id]); + } + } + + itemTemplateIds.sort(function(a, b) { + if (a[1] === b[1]) { + return 0; + } + + //The "Custom" item is always first. + if (a[1] === '') { + return -1; + } else if (b[1] === '') { + return 1; + } + + //Top-level items go before submenus. + var aIsTop = (a[1].charAt(0) === '>') ? 1 : 0; + var bIsTop = (b[1].charAt(0) === '>') ? 1 : 0; + if (aIsTop !== bIsTop) { + return bIsTop - aIsTop; + } + + //Everything else is sorted by name, in alphabetical order. + if (a[0] > b[0]) { + return 1; + } else if (a[0] < b[0]) { + return -1; + } + return 0; + }); + + return itemTemplateIds; + })(), + + write: function(menuItem, value, input, containerNode) { + var oldTemplateId = menuItem.template_id; + + menuItem.template_id = value; + menuItem.defaults = itemTemplates.getDefaults(menuItem.template_id); + menuItem.custom = (menuItem.template_id === ''); + + // The file/URL of non-custom items is read-only and equal to the default + // value. Rationale: simplifies menu generation, prevents some user mistakes. + if (menuItem.template_id !== '') { + menuItem.file = null; + } + + // The new template might not have default values for some of the fields + // currently set to null (= "default"). In those cases, we need to make + // the current values explicit. + containerNode.find('.ws_edit_field').each(function(index, field){ + field = $(field); + var fieldName = field.data('field_name'); + var isSetToDefault = (menuItem[fieldName] === null); + var hasDefaultValue = itemTemplates.hasDefaultValue(menuItem.template_id, fieldName); + + if (isSetToDefault && !hasDefaultValue) { + var oldDefaultValue = itemTemplates.getDefaultValue(oldTemplateId, fieldName); + if (oldDefaultValue !== null) { + menuItem[fieldName] = oldDefaultValue; + } + } + }); + } + }), + + 'embedded_page_id' : $.extend({}, baseField, { + caption: 'Embedded page ID', + defaultValue: 'Select page to display', + type: 'text', + addDropdown: 'ws_embedded_page_selector', + + display: function(menuItem, displayValue, input) { + input.prop('readonly', true); + var pageId = parseInt(getFieldValue(menuItem, 'embedded_page_id', 0), 10), + blogId = parseInt(getFieldValue(menuItem, 'embedded_page_blog_id', 1), 10), + formattedId = 'ID: ' + pageId; + + if (pageId <= 0) { + return 'Select page =>'; + } + + if (blogId !== 1) { + formattedId = formattedId + ', blog ID: ' + blogId; + } + displayValue = formattedId; + + AmePageTitles.get(pageId, blogId, function(title) { + //If we retrieved the title via AJAX, the user might have selected a different page in the meantime. + //Make sure it's still the same page before displaying the title. + var currentPageId = parseInt(getFieldValue(menuItem, 'embedded_page_id', 0), 10), + currentBlogId = parseInt(getFieldValue(menuItem, 'embedded_page_blog_id', 1), 10); + if ((currentPageId !== pageId) || (currentBlogId !== blogId)) { + return; + } + + displayValue = title + ' (' + formattedId + ')'; + input.val(displayValue); + }); + + return displayValue; + }, + + write: function() { + //The user cannot directly edit this field. We deliberately ignore writes. + }, + + visible: function(menuItem) { + //Only show this field if the "Embed WP page" template is selected. + return (menuItem.template_id === wsEditorData.embeddedPageTemplateId); + } + }), + + 'file' : $.extend({}, baseField, { + caption: 'URL', + display: function(menuItem, displayValue, input) { + // The URL/file field is read-only for default menus. Also, since the "file" + // field is usually set to a page slug or plugin filename for plugin/hook pages, + // we display the dynamically generated "url" field here (i.e. the actual URL) instead. + if (menuItem.template_id !== '') { + input.prop('readonly', true); + displayValue = itemTemplates.getDefaultValue(menuItem.template_id, 'url'); + } else { + input.prop('readonly', false); + } + return displayValue; + }, + + write: function(menuItem, value) { + // A menu must always have a non-empty URL. If the user deletes the current value, + // reset it to the old value. + if (value === '') { + value = menuItem.file; + } + // Default menus always point to the default file/URL. + if (menuItem.template_id !== '') { + value = null; + } + menuItem.file = value; + } + }), + + 'access_level' : $.extend({}, baseField, { + caption: 'Permissions', + defaultValue: 'read', + type: 'access_editor', + visible: false, //Will be set to visible only in Pro version. + + display: function(menuItem) { + //Permissions display is a little complicated and could use improvement. + var requiredCap = getFieldValue(menuItem, 'access_level', ''); + var extraCap = getFieldValue(menuItem, 'extra_capability', ''); + + var displayValue = (menuItem.template_id === '') ? '< Custom >' : requiredCap; + if (extraCap !== '') { + if (menuItem.template_id === '') { + displayValue = extraCap; + } else { + displayValue = displayValue + '+' + extraCap; + } + } + + return displayValue; + }, + + write: function(menuItem) { + //The required capability can't be directly edited and always equals the default. + menuItem.access_level = null; + } + }), + + //TODO: Never save this field. It just wastes database space. + 'required_capability_read_only' : $.extend({}, baseField, { + caption: 'Required capability', + defaultValue: 'none', + type: 'text', + tooltip: "Only users who have this capability can see the menu. "+ + "The capability can't be changed because it's usually hard-coded in WordPress or the plugin that created the menu."+ + "<br><br>Use the \"Extra capability\" field to restrict access to this menu.", + + visible: function(menuItem) { + //Show only in the free version, on non-custom menus. + return !wsEditorData.wsMenuEditorPro && (menuItem.template_id !== ''); + }, + + display: function(menuItem, displayValue, input) { + input.prop('readonly', true); + return getFieldValue(menuItem, 'access_level', ''); + }, + + write: function(menuItem, value) { + //The required capability is read-only. Ignore writes. + } + }), + + 'extra_capability' : $.extend({}, baseField, { + caption: 'Extra capability', + defaultValue: 'read', + type: 'text', + addDropdown: 'ws_cap_selector', + tooltip: function(menuItem) { + if (menuItem.template_id === '') { + return 'Only users who have this capability can see the menu.'; + } + return 'An additional capability check that is applied on top of the required capability.'; + }, + + display: function(menuItem) { + var requiredCap = getFieldValue(menuItem, 'access_level', ''); + var extraCap = getFieldValue(menuItem, 'extra_capability', ''); + + //On custom menus, show the default required cap when no extra cap is selected. + //Otherwise there would be no visible capability requirements at all. + var displayValue = extraCap; + if ((menuItem.template_id === '') && (extraCap === '')) { + displayValue = requiredCap; + } + + return displayValue; + }, + + write: function(menuItem, value) { + value = jsTrim(value); + + //Reset to default if the user clears the input. + if (value === '') { + menuItem.extra_capability = null; + return; + } + + menuItem.extra_capability = value; + } + }), + + 'appearance_heading' : $.extend({}, baseField, { + caption: 'Appearance', + advanced : true, + onlyForTopMenus: false, + type: 'heading', + standardCaption: false, + visible: false //Only visible in the Pro version. + }), + + 'icon_url' : $.extend({}, baseField, { + caption: 'Icon URL', + type : 'icon_selector', + advanced : true, + defaultValue: 'div', + onlyForTopMenus: true, + + display: function(menuItem, displayValue, input, containerNode) { + //Display the current icon in the selector. + var cssClass = getFieldValue(menuItem, 'css_class', ''); + var iconUrl = getFieldValue(menuItem, 'icon_url', '', containerNode); + displayValue = iconUrl; + + //When submenu icon visibility is set to "only if manually selected", + //don't show the default submenu icons. + var isDefault = (typeof menuItem.icon_url === 'undefined') || (menuItem.icon_url === null); + if (isDefault && (wsEditorData.submenuIconsEnabled === 'if_custom') && containerNode.hasClass('ws_item')) { + iconUrl = 'none'; + cssClass = ''; + } + + var selectButton = input.closest('.ws_edit_field').find('.ws_select_icon'); + var cssIcon = selectButton.find('.ws_icon_image'); + var imageIcon = selectButton.find('img'); + + var matches = cssClass.match(/\b(ame-)?menu-icon-([^\s]+)\b/); + var iconFontMatches = iconUrl && iconUrl.match(/^\s*((dashicons|ame-fa)-[a-z0-9\-]+)/); + + //Icon URL takes precedence over icon class. + if ( iconUrl && iconUrl !== 'none' && iconUrl !== 'div' && !iconFontMatches ) { + //Regular image icon. + cssIcon.hide(); + imageIcon.prop('src', iconUrl).show(); + } else if ( iconFontMatches ) { + cssIcon.removeClass().addClass('ws_icon_image'); + if ( iconFontMatches[2] === 'dashicons' ) { + //Dashicon. + cssIcon.addClass('dashicons ' + iconFontMatches[1]); + } else if ( iconFontMatches[2] === 'ame-fa' ) { + //FontAwesome icon. + cssIcon.addClass('ame-fa ' + iconFontMatches[1]); + } + imageIcon.hide(); + cssIcon.show(); + } else if ( matches ) { + //Other CSS-based icon. + imageIcon.hide(); + var iconClass = (matches[1] ? matches[1] : '') + 'icon-' + matches[2]; + cssIcon.removeClass().addClass('ws_icon_image ' + iconClass).show(); + } else { + //This menu has no icon at all. This is actually a valid state + //and WordPress will display a menu like that correctly. + imageIcon.hide(); + cssIcon.removeClass().addClass('ws_icon_image').show(); + } + + return displayValue; + } + }), + + 'colors' : $.extend({}, baseField, { + caption: 'Color scheme', + defaultValue: 'Default', + type: 'color_scheme_editor', + onlyForTopMenus: true, + visible: false, + advanced : true, + + display: function(menuItem, displayValue, input, containerNode) { + var colors = getFieldValue(menuItem, 'colors', {}) || {}; + var colorList = containerNode.find('.ws_color_scheme_display'); + + colorList.empty(); + var count = 0, maxColorsToShow = 7; + + $.each(colors, function(name, value) { + if ( !value || (count >= maxColorsToShow) ) { + return; + } + + colorList.append( + $('<span></span>').addClass('ws_color_display_item').css('background-color', value) + ); + count++; + }); + + if (count === 0) { + colorList.append('Default'); + } + + return 'Placeholder. You should never see this.'; + }, + + write: function(menuItem) { + //Menu colors can't be directly edited. + } + }), + + 'html_heading' : $.extend({}, baseField, { + caption: 'HTML', + advanced : true, + onlyForTopMenus: true, + type: 'heading', + standardCaption: false + }), + + 'open_in' : $.extend({}, baseField, { + caption: 'Open in', + advanced : true, + type : 'select', + options : [ + ['Same window or tab', 'same_window'], + ['New window', 'new_window'], + ['Frame', 'iframe'] + ], + defaultValue: 'same_window', + visible: false + }), + + 'iframe_height' : $.extend({}, baseField, { + caption: 'Frame height (pixels)', + advanced : true, + visible: function(menuItem) { + return wsEditorData.wsMenuEditorPro && (getFieldValue(menuItem, 'open_in') === 'iframe'); + }, + + display: function(menuItem, displayValue, input) { + input.prop('placeholder', 'Auto'); + if (displayValue === 0 || displayValue === '0') { + displayValue = ''; + } + return displayValue; + }, + + write: function(menuItem, value) { + value = parseInt(value, 10); + if (isNaN(value) || (value < 0)) { + value = 0; + } + value = Math.round(value); + + if (value > 10000) { + value = 10000; + } + + if (value === 0) { + menuItem.iframe_height = null; + } else { + menuItem.iframe_height = value; + } + + } + }), + + 'css_class' : $.extend({}, baseField, { + caption: 'CSS classes', + advanced : true, + onlyForTopMenus: true + }), + + 'hookname' : $.extend({}, baseField, { + caption: 'ID attribute', + advanced : true, + onlyForTopMenus: true + }), + + 'page_properties_heading' : $.extend({}, baseField, { + caption: 'Page', + advanced : true, + onlyForTopMenus: true, + type: 'heading', + standardCaption: false + }), + + 'page_heading' : $.extend({}, baseField, { + caption: 'Page heading', + advanced : true, + onlyForTopMenus: false, + visible: false + }), + + 'page_title' : $.extend({}, baseField, { + caption: "Window title", + standardCaption : true, + advanced : true + }), + + 'is_always_open' : $.extend({}, baseField, { + caption: 'Keep this menu expanded', + advanced : true, + onlyForTopMenus: true, + type: 'checkbox', + standardCaption: false + }) +}; + +var visibleMenuFieldsByType = {}; + +AmeEditorApi.getItemDisplayUrl = function(menuItem) { + var url = getFieldValue(menuItem, 'file', ''); + if (menuItem.template_id !== '') { + //Use the template URL. It's a preset that can't be overridden. + var defaultUrl = itemTemplates.getDefaultValue(menuItem.template_id, 'url'); + if (defaultUrl) { + url = defaultUrl; + } + } + return url; +}; + +/* + * Create editors for the visible fields of a menu entry and append them to the specified node. + */ +function buildEditboxFields(fieldContainer, entry, isTopLevel){ + isTopLevel = (typeof isTopLevel === 'undefined') ? false : isTopLevel; + + var basicFields = $('<div class="ws_edit_panel ws_basic"></div>').appendTo(fieldContainer); + var advancedFields = $('<div class="ws_edit_panel ws_advanced"></div>').appendTo(fieldContainer); + + if ( wsEditorData.hideAdvancedSettings ){ + advancedFields.css('display', 'none'); + } + + for (var field_name in knownMenuFields){ + if (!knownMenuFields.hasOwnProperty(field_name)) { + continue; + } + + var fieldSpec = knownMenuFields[field_name]; + if (fieldSpec.onlyForTopMenus && !isTopLevel) { + continue; + } + + var field = buildEditboxField(entry, field_name, fieldSpec); + if (field){ + if (fieldSpec.advanced){ + advancedFields.append(field); + } else { + basicFields.append(field); + } + } + } + + //Add a link that shows/hides advanced fields + fieldContainer.append( + $('<div>').addClass('ws_toggle_container').append( + $('<a></a>', {href: '#'}) + .addClass('ws_toggle_advanced_fields') + .text( + wsEditorData.hideAdvancedSettings + ? wsEditorData.captionShowAdvanced + : wsEditorData.captionHideAdvanced + ) + .toggle(!!wsEditorData.hideAdvancedSettings) //Conver to boolean because it could be a string ("1" or "0"). + ) + ); +} + +/* + * Create an editor for a specified field. + */ +//noinspection JSUnusedLocalSymbols +function buildEditboxField(entry, field_name, field_settings){ + //Build a form field of the appropriate type + var inputBox; + var basicTextField = '<input type="text" class="ws_field_value">'; + //noinspection FallthroughInSwitchStatementJS + switch(field_settings.type){ + case 'select': + inputBox = $('<select class="ws_field_value">'); + var option = null; + for( var index = 0; index < field_settings.options.length; index++ ){ + var optionTitle = field_settings.options[index][0]; + var optionValue = field_settings.options[index][1]; + + option = $('<option>') + .val(optionValue) + .text(optionTitle); + option.appendTo(inputBox); + } + break; + + case 'checkbox': + inputBox = $('<label></label>') + .append($('<input>', {type: 'checkbox', "class": 'ws_field_value'})) + .append(' ') + .append($('<span></span>', {"class": 'ws_field_label_text'}).text(field_settings.caption)) + break; + + case 'access_editor': + inputBox = $('<input type="text" class="ws_field_value" readonly="readonly">') + .add('<input type="button" class="button ws_launch_access_editor" value="Edit...">'); + break; + + case 'icon_selector': + //noinspection HtmlUnknownTag + inputBox = $(basicTextField) + .add('<button class="button ws_select_icon" title="Select icon"><div class="ws_icon_image dashicons dashicons-admin-generic"></div><img src="" style="display:none;" alt="Icon"></button>'); + break; + + case 'color_scheme_editor': + inputBox = $('<span class="ws_color_scheme_display">Placeholder</span>') + .add('<input type="button" class="button ws_open_color_editor" value="Edit...">'); + break; + + case 'heading': + inputBox = $('<span></span>').text(field_settings.caption); + break; + + case 'text': + /* falls through */ + default: + inputBox = $(basicTextField); + } + + + var className = "ws_edit_field ws_edit_field-"+field_name; + if (field_settings.addDropdown){ + className += ' ws_has_dropdown'; + } + if (!field_settings.standardCaption) { + className += ' ws_no_field_caption'; + } + if (field_settings.type === 'heading') { + className += ' ws_field_group_heading'; + } + + var caption = $(); //Empty set by default. + if (field_settings.standardCaption) { + var $labelText = $('<span></span>') + .addClass('ws_field_label_text') + .text(field_settings.caption + ' '); + + if (field_settings.tooltip !== null) { + $labelText.append( + '<a class="ws_field_tooltip_trigger"><div class="dashicons dashicons-info"></div></a>' + ); + } + + caption = caption.add($labelText).add('<br>'); //Note: add(), not append(). + } + var editField = $('<div></div>') + .attr('class', className) + .append(caption) + .append(inputBox); + + if (field_settings.addDropdown) { + //Add a dropdown button + var dropdownId = field_settings.addDropdown; + editField.append( + $('<input type="button" value="">') + .addClass('button ws_dropdown_button ' + dropdownId + '_trigger') + .attr('tabindex', '-1') + .data('dropdownId', dropdownId) + ); + } + + editField + .append( + $('<img class="ws_reset_button" title="Reset to default value" src="" alt="Reset">') + .attr('src', wsEditorData.imagesUrl + '/transparent16.png') + ).data('field_name', field_name); + + var visible; + if (typeof field_settings.visible === 'function') { + visible = field_settings.visible(entry, field_name); + } else { + visible = field_settings.visible; + } + if (!visible) { + editField.css('display', 'none'); + } + + return editField; +} + +/** + * Get the parent menu of a menu item. + * + * @param containerNode A DOM element as a jQuery object. + * @return {JQuery} Parent container node, or an empty jQuery set. + */ +function getParentMenuNode(containerNode) { + var submenu = containerNode.closest('.ws_submenu', '#ws_menu_editor'), + parentId = submenu.data('parent_menu_id'); + if (parentId) { + return $('#' + parentId); + } else { + return $([]); + } +} + +/** + * Get all submenu items of a menu item. + * + * @param {JQuery} containerNode + * @return {JQuery} A list of submenu item container nodes, or an empty set. + */ +function getSubmenuItemNodes(containerNode) { + var subMenuId = containerNode.data('submenu_id'); + if (subMenuId) { + return $('#' + subMenuId).find('.ws_container'); + } else { + return $([]); + } +} + +/** + * Apply a callback recursively to a menu item and all of its children, in depth-first order. + * The callback will be invoked with two arguments: (containerNode, menuItem). + * + * @param containerNode + * @param {Function} callback + */ +function walkMenuTree(containerNode, callback) { + getSubmenuItemNodes(containerNode).each(function() { + walkMenuTree($(this), callback); + }); + callback(containerNode, containerNode.data('menu_item')); +} + +/** + * Update the UI elements that that indicate whether the currently selected + * actor can access a menu item. + * + * @param containerNode + */ +function updateActorAccessUi(containerNode) { + //Update the permissions checkbox & UI + var menuItem = containerNode.data('menu_item'); + if (actorSelectorWidget.selectedActor !== null) { + var hasAccess = actorCanAccessMenu(menuItem, actorSelectorWidget.selectedActor); + var hasCustomPermissions = actorHasCustomPermissions(menuItem, actorSelectorWidget.selectedActor); + + var isOverrideActive = !hasAccess && getFieldValue(menuItem, 'restrict_access_to_items', false); + + //Check if the parent menu has the "hide all submenus if this is hidden" override in effect. + var currentChild = containerNode, parentNode, parentItem; + do { + parentNode = getParentMenuNode(currentChild); + parentItem = parentNode.data('menu_item'); + if ( + parentItem + && getFieldValue(parentItem, 'restrict_access_to_items', false) + && !actorCanAccessMenu(parentItem, actorSelectorWidget.selectedActor) + ) { + hasAccess = false; + isOverrideActive = true; + break; + } + currentChild = parentNode; + } while (parentNode.length > 0); + + var checkbox = containerNode.find('.ws_actor_access_checkbox'); + checkbox.prop('checked', hasAccess); + + //Display the checkbox in an indeterminate state if the actual menu permissions are unknown + //because it uses meta capabilities. + var isIndeterminate = (hasAccess === null); + //Also show it as indeterminate if some items of this menu are hidden and some are visible, + //or if their permissions don't match this menu's permissions. + var submenuItems = getSubmenuItemNodes(containerNode); + if ((submenuItems.length > 0) && !isOverrideActive) { + var differentPermissions = false; + submenuItems.each(function() { + var item = $(this).data('menu_item'); + if ( !item ) { //Skip placeholder items created by drag & drop operations. + return true; + } + var hasSubmenuAccess = actorCanAccessMenu(item, actorSelectorWidget.selectedActor); + if (hasSubmenuAccess !== hasAccess) { + differentPermissions = true; + return false; + } + return true; + }); + + if (differentPermissions) { + isIndeterminate = true; + } + } + checkbox.prop('indeterminate', isIndeterminate); + + if (isIndeterminate && (hasAccess === null)) { + setMenuFlag( + containerNode, + 'uncertain_meta_cap', + true, + "This item might be visible.\n" + + "The plugin cannot reliably detect if \"" + actorSelectorWidget.selectedDisplayName + + "\" has the \"" + getFieldValue(menuItem, 'access_level', '[No capability]') + + "\" capability. If you need to hide the item, try checking and then unchecking it." + ); + } else { + setMenuFlag(containerNode, 'uncertain_meta_cap', false); + } + + containerNode.toggleClass('ws_is_hidden_for_actor', !hasAccess); + containerNode.toggleClass('ws_has_custom_permissions_for_actor', hasCustomPermissions); + setMenuFlag(containerNode, 'custom_actor_permissions', hasCustomPermissions); + setMenuFlag(containerNode, 'hidden_from_others', false); + } else { + containerNode.removeClass('ws_is_hidden_for_actor ws_has_custom_permissions_for_actor'); + setMenuFlag(containerNode, 'custom_actor_permissions', false); + setMenuFlag(containerNode, 'uncertain_meta_cap', false); + + var currentUserActor = 'user:' + wsEditorData.currentUserLogin; + var otherActors = _(wsEditorData.actors).keys().without(currentUserActor, 'special:super_admin').value(), + hiddenFromCurrentUser = ! actorCanAccessMenu(menuItem, currentUserActor), + hasAccessToThisItem = _.curry(actorCanAccessMenu, 2)(menuItem), + hiddenFromOthers = _.every(otherActors, function(actorId) { + return (hasAccessToThisItem(actorId) === false); + }), + visibleForSuperAdmin = AmeActors.isMultisite && actorCanAccessMenu(menuItem, 'special:super_admin'); + + setMenuFlag( + containerNode, + 'hidden_from_others', + hiddenFromOthers, + hiddenFromCurrentUser + ? 'Hidden from everyone' + : ('Hidden from everyone except you' + (visibleForSuperAdmin ? ' and Super Admins' : '')) + ); + } + + //Update the "hidden" flag. + setMenuFlag(containerNode, 'hidden', itemHasHiddenFlag(menuItem, actorSelectorWidget.selectedActor)); +} + +/** + * Like updateActorAccessUi() except it updates the specified menu's parent, not the menu itself. + * If the menu has no parent (i.e. it's a top-level menu), this function does nothing. + * + * @param containerNode Either a menu item or a submenu container. + */ +function updateParentAccessUi(containerNode) { + var submenu; + if ( containerNode.is('.ws_submenu') ) { + submenu = containerNode; + } else { + submenu = containerNode.parent(); + } + + var parentId = submenu.data('parent_menu_id'); + if (parentId) { + updateActorAccessUi($('#' + parentId)); + } +} + +/** + * Update an edit widget with the current menu item settings. + * + * @param {JQuery} containerNode + */ +function updateItemEditor(containerNode) { + var menuItem = containerNode.data('menu_item'); + var itemSubType = (menuItem.hasOwnProperty('sub_type') ? menuItem['sub_type'] : ''); + + //Apply flags based on the item's state. + var flags = ['hidden', 'unused', 'custom']; + for (var i = 0; i < flags.length; i++) { + setMenuFlag(containerNode, flags[i], getFieldValue(menuItem, flags[i], false)); + } + + if (itemSubType) { + var typeTitle = itemSubType.charAt(0).toUpperCase() + itemSubType.slice(1); + setMenuFlag(containerNode, 'subtype_' + itemSubType, true, typeTitle); + } + + //Update the permissions checkbox & other actor-specific UI + updateActorAccessUi(containerNode); + + //Update all input fields with the current values. + containerNode.find('.ws_edit_field').each(function(index, field) { + field = $(field); + var fieldName = field.data('field_name'); + var input = field.find('.ws_field_value').first(); + + var hasADefaultValue = itemTemplates.hasDefaultValue(menuItem.template_id, fieldName); + var defaultValue = getDefaultValue(menuItem, fieldName, null, containerNode); + var isDefault = hasADefaultValue && ((typeof menuItem[fieldName] === 'undefined') || (menuItem[fieldName] === null)); + + if (fieldName === 'access_level') { + isDefault = (getFieldValue(menuItem, 'extra_capability', '') === '') + && isEmptyObject(menuItem.grant_access) + && (!getFieldValue(menuItem, 'restrict_access_to_items', false)); + } else if (fieldName === 'required_capability_read_only') { + isDefault = true; + hasADefaultValue = true; + } + + field.toggleClass('ws_has_no_default', !hasADefaultValue); + field.toggleClass('ws_input_default', isDefault); + + var displayValue = isDefault ? defaultValue : menuItem[fieldName]; + if (knownMenuFields[fieldName].display !== null) { + displayValue = knownMenuFields[fieldName].display(menuItem, displayValue, input, containerNode); + } + + setInputValue(input, displayValue); + + //Store the value to help with change detection. + if (input.length > 0) { + $.data(input.get(0), 'ame_last_display_value', displayValue); + } + + var isFieldVisible = _.get(visibleMenuFieldsByType, [itemSubType, fieldName], true); + if (typeof (knownMenuFields[fieldName].visible) === 'function') { + isFieldVisible = isFieldVisible && knownMenuFields[fieldName].visible(menuItem, fieldName); + } else { + isFieldVisible = isFieldVisible && knownMenuFields[fieldName].visible; + } + if (isFieldVisible) { + field.css('display', ''); + } else { + field.css('display', 'none'); + } + }); +} + +AmeEditorApi.updateParentAccessUi = updateParentAccessUi; +AmeEditorApi.updateItemEditor = updateItemEditor; + +function isEmptyObject(obj) { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + return false; + } + } + return true; +} + +/** + * Get the current value of a single menu field. + * + * If the specified field is not set, this function will attempt to retrieve it + * from the "defaults" property of the menu object. If *that* fails, it will return + * the value of the optional third argument defaultValue. + * + * @param {Object} entry + * @param {string} fieldName + * @param {*} [defaultValue] + * @param {JQuery} [containerNode] + * @return {*} + */ +function getFieldValue(entry, fieldName, defaultValue, containerNode){ + if ( (typeof entry[fieldName] === 'undefined') || (entry[fieldName] === null) ) { + return getDefaultValue(entry, fieldName, defaultValue, containerNode); + } else { + return entry[fieldName]; + } +} + +AmeEditorApi.getFieldValue = getFieldValue; + +/** + * Get the default value of a menu field. + * + * @param {Object} entry + * @param {String} fieldName + * @param {*} [defaultValue] + * @param {JQuery} [containerNode] + * @returns {*} + */ +function getDefaultValue(entry, fieldName, defaultValue, containerNode) { + //By default, a submenu item has the same icon as its parent. + if ((fieldName === 'icon_url') && containerNode && (wsEditorData.submenuIconsEnabled !== 'never')) { + var parentContainerNode = getParentMenuNode(containerNode), + parentMenuItem = parentContainerNode.data('menu_item'); + if (parentMenuItem) { + return getFieldValue(parentMenuItem, fieldName, defaultValue, parentContainerNode); + } + } + + //Use the custom menu title as the page title if the default page title matches the default menu title. + //Note that if the page title is an empty string (''), WP automatically uses the menu title. So we do the same. + if ((fieldName === 'page_title') && (entry.template_id !== '')) { + var defaultPageTitle = itemTemplates.getDefaultValue(entry.template_id, 'page_title'), + defaultMenuTitle = itemTemplates.getDefaultValue(entry.template_id, 'menu_title'), + customMenuTitle = entry['menu_title']; + + if ( + (customMenuTitle !== null) + && (customMenuTitle !== '') + && ((defaultPageTitle === '') || (defaultMenuTitle === defaultPageTitle)) + ) { + return customMenuTitle; + } + } + + if (typeof defaultValue === 'undefined') { + defaultValue = null; + } + + //Known templates take precedence. + if ((entry.template_id === '') || (typeof itemTemplates.templates[entry.template_id] !== 'undefined')) { + var templateDefault = itemTemplates.getDefaultValue(entry.template_id, fieldName); + return (templateDefault !== null) ? templateDefault : defaultValue; + } + + if (fieldName === 'template_id') { + return null; + } + + //Separators can have their own defaults, independent of templates. + var hasDefault = (typeof entry.defaults !== 'undefined') && (typeof entry.defaults[fieldName] !== 'undefined'); + if (hasDefault){ + return entry.defaults[fieldName]; + } + + return defaultValue; +} + +/* + * Make a menu container sortable + */ +function makeBoxSortable(menuBox){ + //Make the submenu sortable + menuBox.sortable({ + items: '> .ws_container', + cursor: 'move', + dropOnEmpty: true, + cancel : '.ws_editbox, .ws_edit_link', + + placeholder: 'ws_container ws_sortable_placeholder', + forcePlaceholderSize: true, + + stop: function(even, ui) { + //Fix incorrect item overlap caused by jQuery.sortable applying the initial z-index as an inline style. + ui.item.css('z-index', ''); + + //Fix submenu container height. It should be tall enough to reach the selected parent menu. + if (ui.item.hasClass('ws_menu') && ui.item.hasClass('ws_active')) { + AmeEditorApi.updateSubmenuBoxHeight(ui.item); + } + } + }); +} + +/** + * Iterates over all menu items invoking a callback for each item. + * + * The callback will be passed two arguments: the menu item and its UI container node (a jQuery object). + * You can stop iteration by returning false from the callback. + * + * @param {Function} callback + * @param {boolean} [skipSeparators] Defaults to true. Set to false to include separators in the iteration. + */ +AmeEditorApi.forEachMenuItem = function(callback, skipSeparators) { + if (typeof skipSeparators === 'undefined') { + skipSeparators = true; + } + + $('#ws_menu_editor').find('.ws_container').each(function() { + var containerNode = $(this); + if ( !(skipSeparators && containerNode.hasClass('ws_menu_separator')) ) { + return callback(containerNode.data('menu_item'), containerNode); + } + }); +}; + +/** + * Select the first menu item that has the specified URL. + * + * @param {number|string} selectorOrLevel + * @param {string} url + * @param {null|Boolean} [expandProperties] + * @returns {JQuery} + */ +AmeEditorApi.selectMenuItemByUrl = function(selectorOrLevel, url, expandProperties) { + if (typeof expandProperties === 'undefined') { + expandProperties = null; + } + + let level; + if (selectorOrLevel === '#ws_menu_box') { + level = 1; + } else if (selectorOrLevel === '#ws_submenu_box') { + level = 2; + } else { + level = selectorOrLevel; + } + + const column = menuPresenter.getColumnImmediate(level); + if (!column) { + return $([]); + } + + const box = column.getVisibleItemList(); + + const containerNode = + box.find('.ws_container') + .filter(function() { + const itemUrl = AmeEditorApi.getItemDisplayUrl($(this).data('menu_item')); + return (itemUrl === url); + }) + .first(); + + if (containerNode.length > 0) { + AmeEditorApi.selectItem(containerNode); + + if (expandProperties !== null) { + const expandLink = containerNode.find('.ws_edit_link').first(); + if (expandLink.hasClass('ws_edit_link_expanded') !== expandProperties) { + expandLink.trigger('click'); + } + } + } + return containerNode; +}; + +/*************************************************************************** + Parsing & encoding menu inputs + ***************************************************************************/ + +/** + * Encode the current menu structure as JSON + * + * @return {String} A JSON-encoded string representing the current menu tree loaded in the editor. + */ +function encodeMenuAsJSON(tree){ + if (typeof tree === 'undefined' || !tree) { + tree = readMenuTreeState(); + } + tree.format = { + name: wsEditorData.menuFormatName, + version: wsEditorData.menuFormatVersion + }; + + //Compress the admin menu. + tree = compressMenu(tree); + + return JSON.stringify(tree); +} + +function readMenuTreeState(){ + var tree = {}; + var menuPosition = 0; + var itemsByFilename = {}; + + //Gather all menus and their items + $('#ws_menu_box').find('.ws_menu').each(function() { + var containerNode = this; + var menu = readItemState(containerNode, menuPosition++); + + //Attach the current menu to the main structure. + var filename = getFieldValue(menu, 'file'); + + //Give unclickable items unique keys. + if (menu.template_id === wsEditorData.unclickableTemplateId) { + ws_paste_count++; + filename = '#' + wsEditorData.unclickableTemplateClass + '-' + ws_paste_count; + } else if (menu.template_id === wsEditorData.embeddedPageTemplateId) { + ws_paste_count++; + filename = '#embedded-page-' + ws_paste_count; + } + + //Prevent the user from saving top level items with duplicate URLs. + //WordPress indexes the submenu array by parent URL and AME uses a {url : menu_data} hashtable internally. + //Duplicate URLs would cause problems for both. + if (itemsByFilename.hasOwnProperty(filename)) { + throw { + code: 'duplicate_top_level_url', + message: 'Error: Found a duplicate URL! All top level menus must have unique URLs.', + duplicates: [itemsByFilename[filename], containerNode] + }; + } + + tree[filename] = menu; + itemsByFilename[filename] = containerNode; + }); + + AmeCapabilityManager.pruneGrantedUserCapabilities(); + + var result = { + tree: tree, + color_presets: $.extend(true, {}, colorPresets), + granted_capabilities: AmeCapabilityManager.getGrantedCapabilities(), + component_visibility: $.extend(true, {}, generalComponentVisibility) + }; + + $(document).trigger('getMenuConfiguration.adminMenuEditor', result); + return result; +} + +/** + * Losslessly compress the admin menu configuration. + * + * This is a JS port of the ameMenu::compress() function defined in /includes/menu.php. + * + * @param {Object} adminMenu + * @returns {Object} + */ +function compressMenu(adminMenu) { + var common = { + properties: _.omit(wsEditorData.blankMenuItem, ['defaults']), + basic_defaults: _.clone(_.get(wsEditorData.blankMenuItem, 'defaults', {})), + custom_item_defaults: _.clone(itemTemplates.getTemplateById('').defaults) + }; + + adminMenu.format.compressed = true; + adminMenu.format.common = common; + + function compressItem(item) { + //These empty arrays can be dropped. + if ( _.isEmpty(item['grant_access']) ) { + delete item['grant_access']; + } + if ( _.isEmpty(item['items']) ) { + delete item['items']; + } + + //Normal and custom menu items have different defaults. + //Remove defaults that are the same for all items of that type. + var defaults = _.get(item, 'custom', false) ? common['custom_item_defaults'] : common['basic_defaults']; + if ( _.has(item, 'defaults') ) { + _.forEach(defaults, function(value, key) { + if (_.has(item['defaults'], key) && (item['defaults'][key] === value)) { + delete item['defaults'][key]; + } + }); + } + + //Remove properties that match the common values. + _.forEach(common['properties'], function(value, key) { + if (_.has(item, key) && (item[key] === value)) { + delete item[key]; + } + }); + + return item; + } + + adminMenu.tree = _.mapValues(adminMenu.tree, function(topMenu) { + topMenu = compressItem(topMenu); + if (typeof topMenu.items !== 'undefined') { + topMenu.items = _.map(topMenu.items, compressItem); + } + return topMenu; + }); + + return adminMenu; +} + +AmeEditorApi.readMenuTreeState = readMenuTreeState; +AmeEditorApi.encodeMenuAsJson = encodeMenuAsJSON; + +/** + * Extract the current menu item settings from its editor widget. + * + * @param itemDiv DOM node containing the editor widget, usually with the .ws_item or .ws_menu class. + * @param {Number} [position] Menu item position among its sibling menu items. Defaults to zero. + * @return {Object} A menu object in the tree format. + */ +function readItemState(itemDiv, position){ + position = (typeof position === 'undefined') ? 0 : position; + + itemDiv = $(itemDiv); + var item = $.extend(true, {}, wsEditorData.blankMenuItem, itemDiv.data('menu_item'), readAllFields(itemDiv)); + + item.defaults = itemDiv.data('menu_item').defaults; + + //Save the position data + item.position = position; + item.defaults.position = position; //The real default value will later overwrite this + + item.separator = itemDiv.hasClass('ws_menu_separator'); + item.custom = menuHasFlag(itemDiv, 'custom'); + + //Gather the menu's sub-items, if any + item.items = []; + var subMenuId = itemDiv.data('submenu_id'); + if (subMenuId) { + var itemPosition = 0; + $('#' + subMenuId).find('.ws_item').each(function () { + var sub_item = readItemState(this, itemPosition++); + item.items.push(sub_item); + }); + } + + return item; +} + +/* + * Extract the values of all menu/item fields present in a container node + * + * Inputs: + * container - a jQuery collection representing the node to read. + */ +function readAllFields(container){ + if ( !container.hasClass('ws_container') ){ + container = container.closest('.ws_container'); + } + + if ( !container.data('field_editors_created') ){ + return container.data('menu_item'); + } + + var state = {}; + + //Iterate over all fields of the item + container.find('.ws_edit_field').each(function() { + var field = $(this); + + //Get the name of this field + var field_name = field.data('field_name'); + //Skip if unnamed + if (!field_name) { + return true; + } + + //Hackety-hack. The "Page" input is for display purposes and contains more than just the ID. Skip it. + //Eventually we'll need a better way to handle this. + if (field_name === 'embedded_page_id') { + return true; + } + //Headings contain no useful data. + if (field.hasClass('ws_field_group_heading')) { + return true; + } + + //Find the field (usually an input or select element). + var input_box = field.find('.ws_field_value'); + + //Save null if default used, custom value otherwise + if (field.hasClass('ws_input_default')){ + state[field_name] = null; + } else { + state[field_name] = getInputValue(input_box); + } + return true; + }); + + //Permission settings are not stored in the visible access_level field (that's just for show), + //so do not attempt to read them from there. + state.access_level = null; + + return state; +} + + +/*************************************************************************** + Flag manipulation + ***************************************************************************/ + +var item_flags = { + 'custom': 'This is a custom menu item', + 'unused': 'This item was added since the last time you saved menu settings.', + 'hidden': 'Cosmetically hidden', + 'custom_actor_permissions': "The selected role has custom permissions for this item.", + 'hidden_from_others': 'Hidden from everyone except you.', + 'uncertain_meta_cap': 'The plugin cannot detect if this item is visible by default.' +}; + +function setMenuFlag(item, flag, state, title) { + title = title || item_flags[flag]; + item = $(item); + + var item_class = 'ws_' + flag; + var img_class = 'ws_' + flag + '_flag'; + + item.toggleClass(item_class, state); + if (state) { + //Add the flag image. + var flag_container = item.find('.ws_flag_container'); + var image = flag_container.find('.' + img_class); + if (image.length === 0) { + image = $('<div></div>').addClass('ws_flag').addClass(img_class); + flag_container.append(image); + } + image.attr('title', title); + } else { + //Remove the flag image. + item.find('.' + img_class).remove(); + } +} + +function menuHasFlag(item, flag){ + return $(item).hasClass('ws_'+flag); +} + +//The "hidden" flag is special. There's both a global version and one that's actor-specific. + +/** + * Check if a menu item is hidden from an actor. + * This function only checks the "hidden" and "hidden_from_actor" flags, not permissions. + * + * @param {Object} menuItem + * @param {string|null} actor + * @returns {boolean} + */ +function itemHasHiddenFlag(menuItem, actor) { + var isHidden = false, + userActors, + userPrefix = 'user:', + userLogin; + + //(Only) A globally hidden item is hidden from everyone. + if ((actor === null) || menuItem.hidden) { + return menuItem.hidden; + } + + if (actor.substr(0, userPrefix.length) === userPrefix) { + //You can set an exception for a specific user. It takes precedence. + if (menuItem.hidden_from_actor.hasOwnProperty(actor)) { + isHidden = menuItem.hidden_from_actor[actor]; + } else { + //Otherwise the item is hidden only if it is hidden from all of the user's roles. + userLogin = actorSelectorWidget.selectedActor.substr(userPrefix.length); + userActors = AmeCapabilityManager.getGroupActorsFor(userLogin); + for (var i = 0; i < userActors.length; i++) { + if (menuItem.hidden_from_actor.hasOwnProperty(userActors[i]) && menuItem.hidden_from_actor[userActors[i]]) { + isHidden = true; + } else { + isHidden = false; + break; + } + } + } + } else { + //Roles and the super admin are straightforward. + isHidden = menuItem.hidden_from_actor.hasOwnProperty(actor) && menuItem.hidden_from_actor[actor]; + } + + return isHidden; +} + +/** + * Toggle menu visibility without changing its permissions. + * + * Applies to the selected actor, or all actors if no actor is selected. + * + * @param {JQuery} selection A menu container node. + * @param {boolean} [isHidden] Optional. True = hide the menu, false = show the menu. + */ +function toggleItemHiddenFlag(selection, isHidden) { + var menuItem = selection.data('menu_item'); + + //By default, invert the current state. + if (typeof isHidden === 'undefined') { + isHidden = !itemHasHiddenFlag(menuItem, actorSelectorWidget.selectedActor); + } + + //Mark the menu as hidden/visible + if (actorSelectorWidget.selectedActor === null) { + //For ALL roles and users. + menuItem.hidden = isHidden; + menuItem.hidden_from_actor = {}; + } else { + //Just for the current role. + if (isHidden) { + menuItem.hidden_from_actor[actorSelectorWidget.selectedActor] = true; + } else { + if (actorSelectorWidget.selectedActor.indexOf('user:') === 0) { + //User-specific exception. Lets you can hide a menu from all admins but leave it visible to yourself. + menuItem.hidden_from_actor[actorSelectorWidget.selectedActor] = false; + } else { + delete menuItem.hidden_from_actor[actorSelectorWidget.selectedActor]; + } + } + + //When the user un-hides a menu that was globally hidden via the "hidden" flag, we must remove + //that flag but also make sure the menu stays hidden from other roles. + if (!isHidden && menuItem.hidden) { + menuItem.hidden = false; + $.each(wsEditorData.actors, function(otherActor) { + if (otherActor !== actorSelectorWidget.selectedActor) { + menuItem.hidden_from_actor[otherActor] = true; + } + }); + } + } + setMenuFlag(selection, 'hidden', isHidden); + + //Also mark all of it's submenus as hidden/visible + var submenuId = selection.data('submenu_id'); + if (submenuId) { + $('#' + submenuId + ' .ws_item').each(function(){ + toggleItemHiddenFlag($(this), isHidden); + }); + } +} + +/*********************************************************** + Capability manipulation + ************************************************************/ + +function actorCanAccessMenu(menuItem, actor) { + if (!$.isPlainObject(menuItem.grant_access)) { + menuItem.grant_access = {}; + } + + //By default, any actor that has the required cap has access to the menu. + //Users can override this on a per-menu basis. + var requiredCap = getFieldValue(menuItem, 'access_level', '< Error: access_level is missing! >'); + var actorHasAccess; + if (menuItem.grant_access.hasOwnProperty(actor)) { + actorHasAccess = menuItem.grant_access[actor]; + } else { + actorHasAccess = AmeCapabilityManager.hasCap(actor, requiredCap, menuItem.grant_access); + } + return actorHasAccess; +} + +AmeEditorApi.actorCanAccessMenu = actorCanAccessMenu; + +function actorHasCustomPermissions(menuItem, actor) { + if (menuItem.grant_access && menuItem.grant_access.hasOwnProperty && menuItem.grant_access.hasOwnProperty(actor)) { + return (menuItem.grant_access[actor] !== null); + } + return false; +} + +/** + * @param containerNode + * @param {string|Object.<string, boolean>} actor + * @param {boolean} [allowAccess] + */ +function setActorAccess(containerNode, actor, allowAccess) { + var menuItem = containerNode.data('menu_item'); + + //grant_access comes from PHP, which JSON-encodes empty assoc. arrays as arrays. + //However, we want it to be a dictionary. + if (!$.isPlainObject(menuItem.grant_access)) { + menuItem.grant_access = {}; + } + + if (typeof actor === 'string') { + menuItem.grant_access[actor] = Boolean(allowAccess); + } else { + _.assign(menuItem.grant_access, actor); + } +} + +/** + * Make a menu item inaccessible to everyone except a particular actor. + * + * Will not change access settings for actors that are more specific than the input actor. + * For example, if the input actor is a "role:", this function will only disable other roles, + * but will leave "user:" actors untouched. + * + * @param {Object} menuItem + * @param {String} actor + * @return {Object} + */ +function denyAccessForAllExcept(menuItem, actor) { + //grant_access comes from PHP, which JSON-encodes empty assoc. arrays as arrays. + //However, we want it to be a dictionary. + if (!$.isPlainObject(menuItem.grant_access)) { + menuItem.grant_access = {}; + } + + $.each(wsEditorData.actors, function(otherActor) { + //If the input actor is more or equally specific... + if ((actor === null) || (AmeActorManager.compareActorSpecificity(actor, otherActor) >= 0)) { + menuItem.grant_access[otherActor] = false; + } + }); + + if (actor !== null) { + menuItem.grant_access[actor] = true; + } + return menuItem; +} + +/*************************************************************************** + Event handlers + ***************************************************************************/ + +//Cut & paste stuff +var menu_in_clipboard = null; +var ws_paste_count = 0; + +//Color preset stuff. +var colorPresets = {}, + wasPresetDropdownPopulated = false; + +//General admin menu visibility. +var generalComponentVisibility = {}; + +//Combined DOM-ready event handler. +var isDomReadyDone = false; + +function ameOnDomReady() { + if (isDomReadyDone) { + return; + } + isDomReadyDone = true; + + //Some editor elements are only available in the Pro version. + if (wsEditorData.wsMenuEditorPro) { + knownMenuFields.open_in.visible = true; + knownMenuFields.access_level.visible = true; + knownMenuFields.page_heading.visible = true; + knownMenuFields.colors.visible = true; + knownMenuFields.appearance_heading.visible = true; + knownMenuFields.appearance_heading.onlyForTopMenus = false; + knownMenuFields.extra_capability.visible = false; //Superseded by the "access_level" field. + + //The Pro version supports submenu icons, but they can be disabled by the user. + knownMenuFields.icon_url.onlyForTopMenus = (wsEditorData.submenuIconsEnabled === 'never'); + + $('.ws_hide_if_pro').hide(); + } + + //Let other plugins filter knownMenuFields and menu fields by type. + $(document).trigger('filterMenuFields.adminMenuEditor', [knownMenuFields, baseField]); + $(document).trigger('filterVisibleMenuFields.adminMenuEditor', [visibleMenuFieldsByType]); + + //Make the top menu box sortable (we only need to do this once) + var mainMenuBox = $('#ws_menu_box'); + makeBoxSortable(mainMenuBox); + + /*************************************************************************** + Event handlers for editor widgets + ***************************************************************************/ + const menuEditorNode = $('#ws_menu_editor'); + + menuPresenter = new AmeMenuPresenter(menuEditorNode, wsEditorData.deepNestingEnabled); + + /** + * Select a menu item and show its submenu. + * + * @param {JQuery|HTMLElement} container Menu container node. + */ + function selectItem(container) { + menuPresenter.selectItem(container); + } + AmeEditorApi.selectItem = selectItem; + + //Select the clicked menu item and show its submenu + menuEditorNode.on('click', '.ws_container', (function () { + selectItem($(this)); + })); + + function updateSubmenuBoxHeight(selectedMenu) { + //TODO: Eliminate this duplication. Maybe we could just call the corresponding column method. + const myColumn = menuPresenter.getColumnImmediate(selectedMenu.closest('.ws_main_container').data('ame-menu-level') || 1); + const nextColumn = menuPresenter.getColumnImmediate(myColumn.level + 1); + if (!nextColumn || (nextColumn === myColumn)) { + return; + } + let mainMenuBox = myColumn.menuBox, + submenuBox = nextColumn.menuBox, + submenuDropZone = nextColumn.container.find('.ws_dropzone').first(); + + //Make the submenu box tall enough to reach the selected item. + //This prevents the menu tip (if any) from floating in empty space. + if (selectedMenu.hasClass('ws_menu_separator')) { + submenuBox.css('min-height', ''); + } else { + var menuTipHeight = 30, + empiricalExtraHeight = 4, + verticalBoxOffset = (submenuBox.offset().top - mainMenuBox.offset().top), + minSubmenuHeight = (selectedMenu.offset().top - mainMenuBox.offset().top) + - verticalBoxOffset + + menuTipHeight - (submenuDropZone.outerHeight() || 0) + empiricalExtraHeight; + minSubmenuHeight = Math.max(minSubmenuHeight, 0); + submenuBox.css('min-height', minSubmenuHeight); + } + } + + AmeEditorApi.updateSubmenuBoxHeight = updateSubmenuBoxHeight; + + //Show a notification icon next to the "Permissions" field when the menu item supports extended permissions. + function updateExtPermissionsIndicator(container, menuItem) { + var extPermissions = AmeItemAccessEditor.detectExtPermissions(AmeEditorApi.getItemDisplayUrl(menuItem)), + fieldTitle = container.find('.ws_edit_field-access_level .ws_field_label_text'), + indicator = fieldTitle.find('.ws_ext_permissions_indicator'); + + if (wsEditorData.wsMenuEditorPro && (extPermissions !== null)) { + if (indicator.length < 1) { + indicator = $('<div class="dashicons dashicons-info ws_ext_permissions_indicator"></div>'); + fieldTitle.append(" ").append(indicator); + } + //Idea: Change the icon based on the kind of permissions available (post type, tags, etc). + indicator.show().data('ext_permissions', extPermissions); + } else { + indicator.hide(); + } + } + + menuEditorNode.on('adminMenuEditor:fieldChange', function(event, menuItem, fieldName) { + if ((fieldName === 'template_id') || (fieldName === 'file')) { + updateExtPermissionsIndicator($(event.target), menuItem); + } + }); + + //Show/hide a menu's properties + menuEditorNode.on('click', '.ws_edit_link', (function (event) { + event.preventDefault(); + + var container = $(this).parents('.ws_container').first(); + var box = container.find('.ws_editbox'); + + //For performance, the property editors for each menu are only created + //when the user tries to access access them for the first time. + if ( !container.data('field_editors_created') ){ + var menuItem = container.data('menu_item'); + buildEditboxFields(box, menuItem, container.hasClass('ws_menu')); + container.data('field_editors_created', true); + updateItemEditor(container); + updateExtPermissionsIndicator(container, menuItem); + } + + $(this).toggleClass('ws_edit_link_expanded'); + //show/hide the editbox + if ($(this).hasClass('ws_edit_link_expanded')){ + box.show(); + } else { + //Make sure changes are applied before the menu is collapsed + box.find('input').change(); + box.hide(); + } + })); + + //The "Default" button : Reset to default value when clicked + menuEditorNode.on('click', '.ws_reset_button', (function () { + //Find the field div (it holds the field name) + var field = $(this).parents('.ws_edit_field'); + var fieldName = field.data('field_name'); + + if ( (field.length > 0) && fieldName ) { + //Extract the default value from the menu item. + var containerNode = field.closest('.ws_container'); + var menuItem = containerNode.data('menu_item'); + + if (fieldName === 'access_level') { + //This is a pretty nasty hack. + menuItem.grant_access = {}; + menuItem.extra_capability = null; + menuItem.restrict_access_to_items = false; + delete menuItem.had_access_before_hiding; + } + + if (itemTemplates.hasDefaultValue(menuItem.template_id, fieldName)) { + menuItem[fieldName] = null; + updateItemEditor(containerNode); + updateParentAccessUi(containerNode); + } + } + })); + + //When a field is edited, change it's appearance if it's contents don't match the default value. + function fieldValueChange(){ + /* jshint validthis:true */ + var input = $(this); + var field = input.parents('.ws_edit_field').first(); + var fieldName = field.data('field_name'); + + if ((fieldName === 'access_level') || (fieldName === 'embedded_page_id')) { + //These fields are read-only and can never be directly edited by the user. + //Ignore spurious change events. + return; + } + + var containerNode = field.parents('.ws_container').first(); + var menuItem = containerNode.data('menu_item'); + + var oldValue = menuItem[fieldName]; + var oldDisplayValue = $.data(this, 'ame_last_display_value'); + var value = getInputValue(input); + var defaultValue = getDefaultValue(menuItem, fieldName, null, containerNode); + var hasADefaultValue = (defaultValue !== null); + + //Some fields/templates have no default values. + field.toggleClass('ws_has_no_default', !hasADefaultValue); + if (!hasADefaultValue) { + field.removeClass('ws_input_default'); + } + + // noinspection EqualityComparisonWithCoercionJS It's been like this so long that I'm afraid to change it. + if (field.hasClass('ws_input_default') && (value == defaultValue)) { + value = null; //null = use default. + } + + //Ignore changes where the new value is the same as the old one. + if ((value === oldValue) || (value === oldDisplayValue)) { + return; + } + + //Update the item. + if (knownMenuFields[fieldName].write !== null) { + // phpcs:ignore WordPressVIPMinimum.JS.HTMLExecutingFunctions.write -- Misdetected. Not document.write(). + knownMenuFields[fieldName].write(menuItem, value, input, containerNode); + } else { + menuItem[fieldName] = value; + } + + updateItemEditor(containerNode); + updateParentAccessUi(containerNode); + + containerNode.trigger('adminMenuEditor:fieldChange', [menuItem, fieldName]); + } + menuEditorNode.on('click change', '.ws_field_value', fieldValueChange); + + //Show/hide advanced fields + menuEditorNode.on('click', '.ws_toggle_advanced_fields', function(){ + var self = $(this); + var advancedFields = self.parents('.ws_container').first().find('.ws_advanced'); + + if ( advancedFields.is(':visible') ){ + advancedFields.hide(); + self.text(wsEditorData.captionShowAdvanced); + } else { + advancedFields.show(); + self.text(wsEditorData.captionHideAdvanced); + } + + return false; + }); + + //Allow/forbid items in actor-specific views + menuEditorNode.on('click', 'input.ws_actor_access_checkbox', function() { + if (actorSelectorWidget.selectedActor === null) { + return; + } + + var checked = $(this).is(':checked'); + var containerNode = $(this).closest('.ws_container'); + + var menu = containerNode.data('menu_item'); + //Ask for confirmation if the user tries to hide Dashboard -> Home. + if ( !checked && ((menu.template_id === 'index.php>index.php') || (menu.template_id === '>index.php')) ) { + updateItemEditor(containerNode); //Resets the checkbox back to the old value. + confirmDashboardHiding(function(ok) { + if (ok) { + setActorAccessForTreeAndUpdateUi(containerNode, actorSelectorWidget.selectedActor, checked); + } + }); + } else { + setActorAccessForTreeAndUpdateUi(containerNode, actorSelectorWidget.selectedActor, checked); + } + }); + + /** + * This confusingly named function sets actor access for the specified menu item + * and all of its children (if any). It also updates the UI with the new settings. + * + * (And it violates SRP in a particularly egregious manner.) + * + * @param containerNode + * @param {String|Object.<String, Boolean>} actor + * @param {Boolean} [allowAccess] + * @param {Boolean} [skipParentUiRefresh] Whether to skip updating the parent access UI. Defaults to false. + */ + function setActorAccessForTreeAndUpdateUi(containerNode, actor, allowAccess, skipParentUiRefresh) { + setActorAccess(containerNode, actor, allowAccess); + + //Apply the same permissions to sub-menus. + const subMenuId = containerNode.data('submenu_id'); + if (subMenuId) { + $('.ws_item', '#' + subMenuId).each(function() { + const node = $(this); + setActorAccessForTreeAndUpdateUi(node, actor, allowAccess, true); + }); + } + + updateItemEditor(containerNode); + updateActorAccessUi(containerNode); + + if ( !skipParentUiRefresh ) { + updateParentAccessUi(containerNode); + } + } + + /** + * Insert a new top level menu after the selected menu or at the end of the list. + * + * @param {Object} menu + */ + function insertMenu(menu) { + const selection = (typeof getSelectedMenu !== 'undefined') ? getSelectedMenu() : null; + if (selection && (selection.length > 0) ) { + outputTopMenu(menu, selection); + } else { + outputTopMenu(menu); + } + } + AmeEditorApi.insertMenu = insertMenu; + + /** + * Confirm with the user that they want to hide "Dashboard -> Home". + * + * This particular menu is important because hiding it can cause an "insufficient permissions" error + * to be displayed right when someone logs in, making it look like login failed. + */ + var permissionConfirmationDialog = $('#ws-ame-dashboard-hide-confirmation').dialog({ + autoOpen: false, + modal: true, + closeText: ' ', + width: 380, + title: 'Warning' + }); + var currentConfirmationCallback = function(ok) {}; + + /** + * Confirm hiding "Dashboard -> Home". + * + * @param callback Called when the user selects an option. True = confirmed. + */ + function confirmDashboardHiding(callback) { + //The user can disable the confirmation dialog. + if (!wsEditorData.dashboardHidingConfirmationEnabled) { + callback(true); + return; + } + + currentConfirmationCallback = callback; + permissionConfirmationDialog.dialog('open'); + } + + $('#ws_confirm_menu_hiding, #ws_cancel_menu_hiding').on('click', function() { + var confirmed = $(this).is('#ws_confirm_menu_hiding'); + var dontShowAgain = permissionConfirmationDialog.find('.ws_dont_show_again input[type="checkbox"]').is(':checked'); + + currentConfirmationCallback(confirmed); + permissionConfirmationDialog.dialog('close'); + + if (dontShowAgain) { + wsEditorData.dashboardHidingConfirmationEnabled = false; + //Run an AJAX request to disable the dialog for this user. + $.post( + wsEditorData.adminAjaxUrl, + { + 'action' : 'ws_ame_disable_dashboard_hiding_confirmation', + '_ajax_nonce' : wsEditorData.disableDashboardConfirmationNonce + } + ); + } + }); + + + /************************************************************************* + Access editor dialog + *************************************************************************/ + + AmeItemAccessEditor.setup({ + api: AmeEditorApi, + actorSelector: actorSelectorWidget, + postTypes: wsEditorData.postTypes, + taxonomies: wsEditorData.taxonomies, + lodash: _, + isPro: wsEditorData.wsMenuEditorPro, + + save: function(menuItem, containerNode, settings) { + //Save the new settings. + menuItem.extra_capability = settings.extraCapability; + menuItem.grant_access = settings.grantAccess; + menuItem.restrict_access_to_items = settings.restrictAccessToItems; + + //Save granted capabilities. + var newlyDisabledCaps = {}; + _.forEach(settings.grantedCapabilities, function(capabilities, actor) { + _.forEach(capabilities, function(grant, capability) { + if (!_.isArray(grant)) { + grant = [grant, null, null]; + } + + AmeCapabilityManager.setCap(actor, capability, grant[0], grant[1], grant[2]); + + if (!grant[0]) { + if (!newlyDisabledCaps.hasOwnProperty(capability)) { + newlyDisabledCaps[capability] = []; + } + newlyDisabledCaps[capability].push(actor); + } + }); + }); + + AmeEditorApi.forEachMenuItem(function(menuItem, containerNode) { + //When the user unchecks a capability, uncheck ALL menu items associated with that capability. + //Anything less won't actually get rid of the capability as enabled menus auto-grant req. caps. + var requiredCap = getFieldValue(menuItem, 'access_level'); + if (newlyDisabledCaps.hasOwnProperty(requiredCap)) { + //It's enough to remove custom "allow" settings. The rest happens automatically - items that + //have no custom per-role settings use capability checks. + _.forEach(newlyDisabledCaps[requiredCap], function(actor) { + if (_.get(menuItem.grant_access, actor) === true) { + delete menuItem.grant_access[actor]; + } + }); + } + + //Due to changed caps and cascading submenu overrides, changes to one item's permissions + //can affect other items. Lets just update all items. + updateActorAccessUi(containerNode); + }); + + //Refresh the UI. + updateItemEditor(containerNode); + } + }); + + menuEditorNode.on('click', '.ws_launch_access_editor', function() { + var containerNode = $(this).parents('.ws_container').first(); + var menuItem = containerNode.data('menu_item'); + + AmeItemAccessEditor.open({ + menuItem: menuItem, + containerNode: containerNode, + selectedActor: actorSelectorWidget.selectedActor, + itemHasSubmenus: (!!(containerNode.data('submenu_id')) && + $('#' + containerNode.data('submenu_id')).find('.ws_item').length > 0) + }); + }); + + /*************************************************************************** + General dialog handlers + ***************************************************************************/ + + $(document).on('click', '.ws_close_dialog', function() { + $(this).parents('.ui-dialog-content').dialog('close'); + }); + + + /*************************************************************************** + Drop-down list for combo-box fields + ***************************************************************************/ + + var capSelectorDropdown = $('#ws_cap_selector'); + var currentDropdownOwner = null; //The input element that the dropdown is currently associated with. + var currentDropdownOwnerMenu = null; //The menu item that the above input belongs to. + + var isDropdownBeingHidden = false, isSuggestionClick = false; + + const $extraCapInAccessEditor = $('#ws_extra_capability'); + + //Show/hide the capability drop-down list when the trigger button is clicked + $('#ws_trigger_capability_dropdown').on('mousedown click', onDropdownTriggerClicked); + menuEditorNode.on('mousedown click', '.ws_cap_selector_trigger', onDropdownTriggerClicked); + + function onDropdownTriggerClicked(event){ + /* jshint validthis:true */ + var inputBox; + var button = $(this); + + var isInAccessEditor = false; + isSuggestionClick = false; + + //Find the input associated with the button that was clicked. + if ( button.attr('id') === 'ws_trigger_capability_dropdown' ) { + inputBox = $extraCapInAccessEditor; + isInAccessEditor = true; + } else { + inputBox = button.closest('.ws_edit_field').find('.ws_field_value').first(); + } + + //If the user clicks the same button again while the dropdown is already visible, + //ignore the click. The dropdown will be hidden by its "blur" handler. + if (event.type === 'mousedown') { + if ( capSelectorDropdown.is(':visible') && inputBox.is(currentDropdownOwner) ) { + isDropdownBeingHidden = true; + } + return; + } else if (isDropdownBeingHidden) { + isDropdownBeingHidden = false; //Ignore the click event. + return; + } + + //A jQuery UI dialog widget will prevent focus from leaving the dialog. So if we want + //the dropdown to be properly focused when displaying it in a dialog, we must make it + //a child of the dialog's DOM node (and vice versa when it's not in a dialog). + var parentContainer = $(this).closest('.ui-dialog, #ws_menu_editor'); + if ((parentContainer.length > 0) && (capSelectorDropdown.closest(parentContainer).length === 0)) { + var oldHeight = capSelectorDropdown.height(); //Height seems to reset when moving to a new parent. + capSelectorDropdown.detach().appendTo(parentContainer).height(oldHeight); + } + + //Pre-select the current capability (will clear selection if there's no match). + capSelectorDropdown.val(inputBox.val()).show(); + + //Move the drop-down near the input box. + var inputPos = inputBox.offset(); + capSelectorDropdown + .css({ + position: 'absolute', + zIndex: 1010 //Must be higher than the permissions dialog overlay. + }) + .offset({ + left: inputPos.left, + top : inputPos.top + inputBox.outerHeight() + }). + width(inputBox.outerWidth()); + + currentDropdownOwner = inputBox; + + currentDropdownOwnerMenu = null; + if (isInAccessEditor) { + currentDropdownOwnerMenu = AmeItemAccessEditor.getCurrentMenuItem(); + } else { + currentDropdownOwnerMenu = currentDropdownOwner.closest('.ws_container').data('menu_item'); + } + + capSelectorDropdown.focus(); + + capSuggestionFeature.show(); + } + + //Also show it when the user presses the down arrow in the input field (doesn't work in Opera). + $extraCapInAccessEditor.bind('keyup', function(event){ + if ( event.which === 40 ){ + $('#ws_trigger_capability_dropdown').trigger('click'); + } + }); + + function hideCapSelector() { + capSelectorDropdown.hide(); + capSuggestionFeature.hide(); + isSuggestionClick = false; + } + + //Event handlers for the drop-down lists themselves + var dropdownNodes = $('.ws_dropdown'); + + // Hide capability drop-down when it loses focus. + dropdownNodes.on('blur', function(){ + if (!isSuggestionClick) { + hideCapSelector(); + } + }); + + dropdownNodes.on('keydown', function(event){ + + //Hide it when the user presses Esc + if ( event.which === 27 ){ + hideCapSelector(); + if (currentDropdownOwner) { + currentDropdownOwner.focus(); + } + + //Select an item & hide the list when the user presses Enter or Tab + } else if ( (event.which === 13) || (event.which === 9) ){ + hideCapSelector(); + + if (currentDropdownOwner) { + if ( capSelectorDropdown.val() ){ + currentDropdownOwner.val(capSelectorDropdown.val()).change(); + } + currentDropdownOwner.focus(); + } + + event.preventDefault(); + } + }); + + //Eat Tab keys to prevent focus theft. Required to make the "select item on Tab" thing work. + dropdownNodes.on('keyup', function(event){ + if ( event.which === 9 ){ + event.preventDefault(); + } + }); + + + //Update the input & hide the list when an option is clicked + dropdownNodes.on('click', function(){ + if (capSelectorDropdown.val()){ + hideCapSelector(); + if (currentDropdownOwner) { + currentDropdownOwner.val(capSelectorDropdown.val()).change().focus(); + } + } + }); + + //Highlight an option when the user mouses over it (doesn't work in IE) + dropdownNodes.on('mousemove', function(event){ + if ( !event.target ){ + return; + } + + var option = event.target; + if ( (typeof option.selected !== 'undefined') && !option.selected && option.value ){ + option.selected = true; + + //Preview which roles have this capability and the required cap. + capSuggestionFeature.previewAccessForItem(currentDropdownOwnerMenu, option.value); + } + }); + + /************************************************************************ + * Capability suggestions + *************************************************************************/ + + var capSuggestionFeature = (function() { + //This feature is not used in the Pro version because it has a different permission UI. + if (wsEditorData.wsMenuEditorPro) { + return { + previewAccessForItem: function () {}, + show: function () {}, + hide: function () {} + } + } + + var capabilitySuggestions = $('#ws_capability_suggestions'), + suggestionBody = capabilitySuggestions.find('table tbody').first().empty(), + suggestedCapabilities = AmeActors.getSuggestedCapabilities(); + + for (var i = 0; i < suggestedCapabilities.length; i++) { + var role = suggestedCapabilities[i].role, capability = suggestedCapabilities[i].capability; + $('<tr>') + .data('role', role) + .data('capability', capability) + .append( + $('<th>', {text: role.displayName, scope: 'row'}).addClass('ws_ame_role_name') + ) + .append( + $('<td>', {text: capability}).addClass('ws_ame_suggested_capability') + ) + .appendTo(suggestionBody); + } + + var currentPreviewedCaps = null; + + /** + * Update the access preview. + * @param {string|string[]|null} capabilities + */ + function previewAccess(capabilities) { + if (typeof capabilities === 'string') { + capabilities = [capabilities]; + } + + if (_.isEqual(capabilities, currentPreviewedCaps)) { + return; + } + currentPreviewedCaps = capabilities; + capabilitySuggestions.find('#ws_previewed_caps').text(currentPreviewedCaps.join(' + ')); + + //Short-circuit the no-caps case. + if (capabilities === null || capabilities.length === 0) { + suggestionBody.find('tr').removeClass('ws_preview_has_access'); + return; + } + + suggestionBody.find('tr').each(function() { + var $row = $(this), + role = $row.data('role'); + + var hasCaps = true; + for (var i = 0; i < capabilities.length; i++) { + hasCaps = hasCaps && AmeActors.hasCap(role.id, capabilities[i]); + } + $row.toggleClass('ws_preview_has_access', hasCaps); + }); + } + + function previewAccessForItem(menuItem, selectedExtraCap) { + var requiredCap = '', extraCap = ''; + + if (menuItem) { + requiredCap = getFieldValue(menuItem, 'access_level', ''); + extraCap = getFieldValue(menuItem, 'extra_capability', ''); + } + if (typeof selectedExtraCap !== 'undefined') { + extraCap = selectedExtraCap; + } + + var caps = []; + if (menuItem && (menuItem.template_id !== '') || (extraCap === '')) { + caps.push(requiredCap); + } + if (extraCap !== '') { + caps.push(extraCap); + } + + previewAccess(caps); + } + + suggestionBody.on('mouseenter', 'td.ws_ame_suggested_capability', function() { + var row = $(this).closest('tr'); + previewAccessForItem(currentDropdownOwnerMenu, row.data('capability')); + }); + + capSelectorDropdown.on('keydown keyup', function() { + previewAccessForItem(currentDropdownOwnerMenu, capSelectorDropdown.val()); + }); + + suggestionBody.on('mousedown', 'td.ws_ame_suggested_capability', function() { + //Don't immediately hide the list when the user tries to click a suggestion. + //It would prevent the click from registering. + isSuggestionClick = true; + }); + + suggestionBody.on('click', 'td.ws_ame_suggested_capability', function() { + var capability = $(this).closest('tr').data('capability'); + + //Change the input to the selected capability. + if (currentDropdownOwner) { + currentDropdownOwner.val(capability).change(); + } + + hideCapSelector(); + }); + + //Workaround for pressing LMB on a suggestion, then moving the mouse outside the suggestion box and releasing the button. + $(document).on('click', function(event) { + if ( + isSuggestionClick + && capabilitySuggestions.is(':visible') + && ( $(event.target).closest(capabilitySuggestions).length < 1 ) + ) { + hideCapSelector(); + } + }); + + return { + previewAccessForItem: previewAccessForItem, + show: function() { + //Position the capability suggestion table next to the selector and match heights. + capabilitySuggestions + .css({ + position: 'absolute', + zIndex: 1009 + }) + .show() + .position({ + my: 'left top', + at: 'right top', + of: capSelectorDropdown, + collision: 'none' + }); + + var selectorHeight = capSelectorDropdown.height(), + suggestionsHeight = capabilitySuggestions.height(), + desiredHeight = Math.max(selectorHeight, suggestionsHeight); + if (selectorHeight < desiredHeight) { + capSelectorDropdown.height(desiredHeight); + } + if (suggestionsHeight < desiredHeight) { + capabilitySuggestions.height(desiredHeight); + } + + if (currentDropdownOwnerMenu) { + previewAccessForItem(currentDropdownOwnerMenu); + } + }, + hide: function() { + capabilitySuggestions.hide(); + } + }; + })(); + + + /************************************************************************* + Icon selector + *************************************************************************/ + var iconSelector = $('#ws_icon_selector'); + var currentIconButton = null; //Keep track of the last clicked icon button. + + var iconSelectorTabs = iconSelector.find('#ws_icon_source_tabs'); + iconSelectorTabs.tabs(); + + //When the user clicks one of the available icons, update the menu item. + iconSelector.on('click', '.ws_icon_option', function() { + var selectedIcon = $(this).addClass('ws_selected_icon'); + iconSelector.hide(); + + //Assign the selected icon to the menu. + if (currentIconButton) { + var container = currentIconButton.closest('.ws_container'); + var item = container.data('menu_item'); + + //Remove the existing icon class, if any. + var cssClass = getFieldValue(item, 'css_class', ''); + cssClass = jsTrim( cssClass.replace(/\b(ame-)?menu-icon-[^\s]+\b/, '') ); + + if (selectedIcon.data('icon-class')) { + //Add the new class. + cssClass = selectedIcon.data('icon-class') + ' ' + cssClass; + //Can't have both a class and an image or we'll get two overlapping icons. + item.icon_url = ''; + } else if (selectedIcon.data('icon-url')) { + item.icon_url = selectedIcon.data('icon-url'); + } + item.css_class = cssClass; + + updateItemEditor(container); + } + + currentIconButton = null; + }); + + //Show/hide the icon selector when the user clicks the icon button. + menuEditorNode.on('click', '.ws_select_icon', function() { + var button = $(this); + //Clicking the same button a second time hides the icon list. + if ( currentIconButton && button.is(currentIconButton) ) { + iconSelector.hide(); + //noinspection JSUnusedAssignment + currentIconButton = null; + return; + } + + currentIconButton = button; + + var containerNode = currentIconButton.closest('.ws_container'); + var menuItem = containerNode.data('menu_item'); + var cssClass = getFieldValue(menuItem, 'css_class', ''); + var iconUrl = getFieldValue(menuItem, 'icon_url', '', containerNode); + + var customImageOption = iconSelector.find('.ws_custom_image_icon').hide(); + + //Highlight the currently selected icon. + iconSelector.find('.ws_selected_icon').removeClass('ws_selected_icon'); + + var selectedIcon = null; + var classMatches = cssClass.match(/\b(ame-)?menu-icon-([^\s]+)\b/); + //Dashicons and FontAwesome icons are set via the icon URL field, but they are actually CSS-based. + var iconFontMatches = iconUrl && iconUrl.match('^\s*((?:dashicons|ame-fa)-[a-z0-9\-]+)\s*$'); + + if ( iconUrl && iconUrl !== 'none' && iconUrl !== 'div' && !iconFontMatches ) { + var currentIcon = iconSelector.find('.ws_icon_option img[src="' + iconUrl + '"]').first().closest('.ws_icon_option'); + if ( currentIcon.length > 0 ) { + selectedIcon = currentIcon.addClass('ws_selected_icon').show(); + } else { + //Display and highlight the custom image. + customImageOption.find('img').prop('src', iconUrl); + customImageOption.addClass('ws_selected_icon').show().data('icon-url', iconUrl); + selectedIcon = customImageOption; + } + } else if ( classMatches || iconFontMatches ) { + //Highlight the icon that corresponds to the current CSS class or Dashicon/FontAwesome icon. + var iconClass = iconFontMatches ? iconFontMatches[1] : ((classMatches[1] ? classMatches[1] : '') + 'icon-' + classMatches[2]); + selectedIcon = iconSelector.find('.' + iconClass).closest('.ws_icon_option').addClass('ws_selected_icon'); + } + + //Activate the tab that contains the icon. + var activeTabId = ((selectedIcon !== null) + ? selectedIcon.closest('.ws_tool_tab').prop('id') + : 'ws_core_icons_tab'), + activeTabItem = iconSelectorTabs.find('a[href="#' + activeTabId + '"]').closest('li'); + if (activeTabItem.length > 0) { + iconSelectorTabs.tabs('option', 'active', activeTabItem.index()); + } + + iconSelector.show(); + iconSelector.position({ //Requires jQuery UI. + my: 'left top', + at: 'left bottom', + of: button + }); + }); + + //Alternatively, use the WordPress media uploader to select a custom icon. + //This code is based on the header selection script in /wp-admin/js/custom-header.js. + var mediaFrame = null; + $('#ws_choose_icon_from_media').on('click', function(event) { + event.preventDefault(); + + //This option is not usable on the demo site since the filesystem is usually read-only. + if (wsEditorData.isDemoMode) { + alert('Sorry, image upload is disabled in demo mode!'); + return; + } + + //If the media frame already exists, reopen it. + if ( mediaFrame !== null ) { + mediaFrame.open(); + return; + } + + //Create a custom media frame. + mediaFrame = wp.media.frames.customAdminMenuIcon = wp.media({ + //Set the title of the modal. + title: 'Choose a Custom Icon (20x20)', + + //Tell it to show only images. + library: { + type: 'image' + }, + + //Customize the submit button. + button: { + text: 'Set as icon', //Button text. + close: true //Clicking the button closes the frame. + } + }); + + //When an image is selected, set it as the menu icon. + mediaFrame.on( 'select', function() { + //Grab the selected attachment. + var attachment = mediaFrame.state().get('selection').first(); + //TODO: Warn the user if the image exceeds 20x20 pixels. + + //Set the menu icon to the attachment URL. + if (currentIconButton) { + var container = currentIconButton.closest('.ws_container'); + var item = container.data('menu_item'); + + //Remove the existing icon class, if any. + var cssClass = getFieldValue(item, 'css_class', ''); + item.css_class = jsTrim( cssClass.replace(/\b(ame-)?menu-icon-[^\s]+\b/, '') ); + + //Set the new icon URL. + item.icon_url = attachment.attributes.url; + + updateItemEditor(container); + } + + currentIconButton = null; + }); + + //If the user closes the frame by via Esc or the "X" button, clear up state. + mediaFrame.on('escape', function(){ + currentIconButton = null; + }); + + mediaFrame.open(); + iconSelector.hide(); + }); + + //Hide the icon selector if the user clicks outside of it. + //Exception: Clicks on "Select icon" buttons are handled above. + $(document).on('mouseup', function(event) { + if ( !iconSelector.is(':visible') ) { + return; + } + + if ( + !iconSelector.is(event.target) + && iconSelector.has(event.target).length === 0 + && $(event.target).closest('.ws_select_icon').length === 0 + ) { + iconSelector.hide(); + currentIconButton = null; + } + }); + + + /************************************************************************* + Embedded page selector + *************************************************************************/ + + var pageSelector = $('#ws_embedded_page_selector'), + pageListBox = pageSelector.find('#ws_current_site_pages'), + currentPageSelectorButton = null, //The last page dropdown button that was clicked. + isPageListPopulated = false, + isPageRequestInProgress = false; + + pageSelector.tabs({ + heightStyle: 'auto', + hide: false, + show: false + }); + //Hack. The selector needs to be hidden by default, but it can't start out as "display: none" because that makes + //jQuery miscalculate tab heights. So we put it in a hidden container, then hide it on load and move it elsewhere. + pageSelector.hide().appendTo(menuEditorNode); + + /** + * Update the page selector with the current menu item's settings. + */ + function updatePageSelector() { + var menuItem, selectedPageId = 0, selectedBlogId = 1; + if ( currentPageSelectorButton ) { + menuItem = currentPageSelectorButton.closest('.ws_container').data('menu_item'); + selectedPageId = parseInt(getFieldValue(menuItem, 'embedded_page_id', 0), 10); + selectedBlogId = parseInt(getFieldValue(menuItem, 'embedded_page_blog_id', 1), 10); + } + + if (selectedPageId === 0) { + pageListBox.val(null); + } else { + var optionValue = selectedBlogId + '_' + selectedPageId; + pageListBox.val(optionValue); + if ( pageListBox.val() !== optionValue ) { + pageListBox.val('custom'); + } + } + + pageSelector.find('#ws_embedded_page_id').val(selectedPageId); + pageSelector.find('#ws_embedded_page_blog_id').val(selectedBlogId); + } + + menuEditorNode.on('click', '.ws_embedded_page_selector_trigger', function(event) { + var thisButton = $(this), + thisInput = thisButton.closest('.ws_edit_field').find('input.ws_field_value:first'); + + //Clicking the same button a second time hides the page selector. + if (thisButton.is(currentPageSelectorButton) && pageSelector.is(':visible')) { + pageSelector.hide(); + //noinspection JSUnusedAssignment + currentPageSelectorButton = null; + return; + } + + currentPageSelectorButton = thisButton; + pageSelector.show(); + pageSelector.position({ + my: 'left top', + at: 'left bottom', + of: thisInput + }); + + event.stopPropagation(); + + if (!isPageListPopulated && !isPageRequestInProgress) { + isPageRequestInProgress = true; + + var pageList = pageSelector.find('#ws_current_site_pages'); + pageList.prop('readonly', true); + + $.getJSON( + wsEditorData.adminAjaxUrl, + { + 'action' : 'ws_ame_get_pages', + '_ajax_nonce' : wsEditorData.getPagesNonce + }, + function(data){ + isPageRequestInProgress = false; + pageList.prop('readonly', false); + + if (typeof data.error !== 'undefined'){ + alert(data.error); + return; + } else if ((typeof data !== 'object') || (typeof data.length === 'undefined')) { + alert('Error: Could not retrieve a list of pages. Unexpected response from the server.'); + return; + } + + //An alphabetised list is easier to scan visually. + var pages = data.sort(function(a, b) { + return a.post_title.localeCompare(b.post_title); + }); + + //Populate the select box. + pageList.empty(); + $.each(pages, function(index, page) { + pageList.append($('<option>', { + val: page.blog_id + '_' + page.post_id, + text: page.post_title + })); + }); + + //Add a "custom" option. Select it when the current setting doesn't match any of the listed pages. + pageList.prepend($('<option>', { + val: 'custom', + text: '< Custom >' + })); + + updatePageSelector(); + isPageListPopulated = true; + }, + 'json' + ); + + } + + updatePageSelector(); + + //Open the "Pages" tab by default, or the "Custom" tab if that's what's selected in the list box. + //The updatePageSelector call above sets the pageListBox value. + pageSelector.tabs('option', 'active', (pageListBox.val() === 'custom') ? 1 : 0); + }); + + //Hide the page selector if the user clicks outside of it and outside the current button. + $(document).on('mouseup', function(event) { + if ( !pageSelector.is(':visible') ) { + return; + } + + var target = $(event.target); + var isOutsideSelector = target.closest(pageSelector).length === 0; + var isOutsideButton = currentPageSelectorButton && (target.closest(currentPageSelectorButton).length === 0); + + if (isOutsideSelector && isOutsideButton) { + pageSelector.hide(); + currentPageSelectorButton = null; + } + }); + + function setEmbeddedPageForCurrentItem(newPageId, newBlogId, title) { + if ( currentPageSelectorButton ) { + var containerNode = currentPageSelectorButton.closest('.ws_container'), + menuItem = containerNode.data('menu_item'); + + menuItem.embedded_page_id = newPageId; + menuItem.embedded_page_blog_id = newBlogId; + + if (typeof title === 'string') { + //Store the page title for later. It will be displayed in the text box. + AmePageTitles.add(newPageId, newBlogId, title); + } + + updateItemEditor(containerNode); + } + } + + //When the user chooses a page from the list, update the menu item and hide the dropdown. + pageListBox.on('change', function() { + var selection = pageListBox.val(); + if (selection === 'custom') { // jshint ignore:line + //Do nothing. Presumably, the user will now switch to the "Custom" tab and enter new settings. + //If they don't do that and just close the dropdown, we keep the previous settings. + } else if ( currentPageSelectorButton ) { + //Set the new page and blog IDs. The expected value format is "blogid_postid". + var parts = selection.split('_'), + newBlogId = parseInt(parts[0], 10), + newPageId = parseInt(parts[1], 10); + + pageSelector.hide(); + setEmbeddedPageForCurrentItem(newPageId, newBlogId, pageListBox.children(':selected').text()); + } + }); + + pageSelector.find('#ws_custom_embedded_page_tab form').on('submit', function(event) { + event.preventDefault(); + + var newPageId = parseInt(pageSelector.find('#ws_embedded_page_id').val(), 10), + newBlogId = parseInt(pageSelector.find('#ws_embedded_page_blog_id').val(), 10); + + if (isNaN(newPageId) || (newPageId < 0)) { + alert('Error: Invalid post ID'); + } else if (isNaN(newBlogId) || (newBlogId < 0)) { + alert('Error: Invalid blog ID'); + } else if ( currentPageSelectorButton ) { + pageSelector.hide(); + setEmbeddedPageForCurrentItem(newPageId, newBlogId); + } + }); + + + /************************************************************************* + Color picker + *************************************************************************/ + + var menuColorDialog = $('#ws-ame-menu-color-settings'); + if (menuColorDialog.length > 0) { + menuColorDialog.dialog({ + autoOpen: false, + closeText: ' ', + draggable: false, + modal: true, + minHeight: 400, + minWidth: 520 + }); + } + + var colorDialogState = { + menuItem: null, + editingGlobalColors: false + }; + + var menuColorVariables = [ + 'base-color', + 'text-color', + 'highlight-color', + 'icon-color', + + 'menu-highlight-text', + 'menu-highlight-icon', + 'menu-highlight-background', + + 'menu-current-text', + 'menu-current-icon', + 'menu-current-background', + + 'menu-submenu-text', + 'menu-submenu-background', + 'menu-submenu-focus-text', + 'menu-submenu-current-text', + + 'menu-bubble-text', + 'menu-bubble-background', + 'menu-bubble-current-text', + 'menu-bubble-current-background' + ]; + + var colorPresetDropdown = $('#ame-menu-color-presets'), + colorPresetDeleteButton = $("#ws-ame-delete-color-preset"), + areColorChangesIgnored = false; + + //Show only the primary color settings by default. + var showAdvancedColors = false; + $('#ws-ame-show-advanced-colors').on('click', function() { + showAdvancedColors = !showAdvancedColors; + $('#ws-ame-menu-color-settings').find('.ame-advanced-menu-color').toggle(showAdvancedColors); + $(this).text(showAdvancedColors ? 'Hide advanced options' : 'Show advanced options'); + }); + + var colorPickersInitialized = false; + function setUpColorDialog(dialogTitle) { + //Initializing the color pickers takes a while, so we only do it when needed instead of on document ready. + if ( !colorPickersInitialized ) { + menuColorDialog.find('.ame-color-picker').wpColorPicker({ + //Deselect the current preset when the user changes any of the color options. + change: deselectPresetOnColorChange, + clear: deselectPresetOnColorChange + }); + colorPickersInitialized = true; + } + + //Populate presets and deselect the previously selected option. + colorPresetDropdown.val(''); + if (!wasPresetDropdownPopulated) { + populatePresetDropdown(); + wasPresetDropdownPopulated = true; + } + + //Update the dialog title. + menuColorDialog.dialog('option', 'title', dialogTitle); + } + + //"Edit.." color schemes. + menuEditorNode.on('click', '.ws_open_color_editor, .ws_color_scheme_display', function() { + var containerNode = $(this).parents('.ws_container').first(); + var menuItem = containerNode.data('menu_item'); + + colorDialogState.containerNode = containerNode; + colorDialogState.menuItem = menuItem; + colorDialogState.editingGlobalColors = false; + + //Add menu title to the dialog caption. + var title = getFieldValue(menuItem, 'menu_title', null); + setUpColorDialog(title ? ('Colors: ' + formatMenuTitle(title)) : 'Colors'); + + //Show the [global] preset only if the user has set it up. + var globalPresetExists = colorPresets.hasOwnProperty('[global]'); + menuColorDialog.find('#ame-global-colors-preset').toggle(globalPresetExists); + + var colors = getFieldValue(menuItem, 'colors', {}), + colorsToDisplay = colors || {}; + if (_.isEmpty(colors)) { + //Normalization. No custom colors = use default colors, and null is used to indicate default settings. + menuItem.colors = null; + //If no custom colors, select and display the global preset. + if (globalPresetExists) { + colorsToDisplay = colorPresets['[global]']; + colorPresetDropdown.val('[global]'); + colorPresetDeleteButton.hide(); + } + } + displayColorSettingsInDialog(colorsToDisplay); + + menuColorDialog.dialog('open'); + }); + + //The "Colors" button in the main sidebar. + $('#ws_edit_global_colors').on('click', function() { + colorDialogState.editingGlobalColors = true; + colorDialogState.menuItem = null; + colorDialogState.containerNode = null; + + setUpColorDialog('Default menu colors'); + displayColorSettingsInDialog(_.get(colorPresets, '[global]', {})); + + //Hide the [global] preset. We'll be editing it. + menuColorDialog.find('#ame-global-colors-preset').hide(); + + menuColorDialog.dialog('open'); + }); + + function getColorSettingsFromDialog() { + var colors = {}, colorCount = 0; + + for (var i = 0; i < menuColorVariables.length; i++) { + var name = menuColorVariables[i]; + var value = $('#ame-color-' + name).val(); + if (value) { + colors[name] = value; + colorCount++; + } + } + + if (colorCount > 0) { + return colors; + } else { + return null; + } + } + + function displayColorSettingsInDialog(colors) { + //noinspection JSUnusedAssignment + areColorChangesIgnored = true; + var customColorCount = 0; + + for (var i = 0; i < menuColorVariables.length; i++) { + var name = menuColorVariables[i]; + var value = colors.hasOwnProperty(name) ? colors[name] : false; + + if ( value ) { + $('#ame-color-' + name).wpColorPicker('color', value); + customColorCount++; + } else { + $('#ame-color-' + name).closest('.wp-picker-container').find('.wp-picker-clear').trigger('click'); + } + } + + areColorChangesIgnored = false; + return customColorCount; + } + + //The "Save Changes" button in the color dialog. + $('#ws-ame-save-menu-colors').on('click', function() { + menuColorDialog.dialog('close'); + var colors = getColorSettingsFromDialog(); + + if ( colorDialogState.editingGlobalColors ) { + if (colors === null) { + delete colorPresets['[global]']; + } else { + colorPresets['[global]'] = colors; + } + } else if ( colorDialogState.menuItem ) { + var menuItem = colorDialogState.menuItem; + //If colors match the global settings, reset them to null. Using the [global] preset is the default. + if (_.has(colorPresets, '[global]') && _.isEqual(colors, colorPresets['[global]'])) { + menuItem.colors = null; + } else { + menuItem.colors = colors; + } + updateItemEditor(colorDialogState.containerNode); + } + + colorDialogState.containerNode = null; + colorDialogState.menuItem = null; + colorDialogState.editingGlobalColors = false; + }); + + //The "Apply to All" button in the same dialog. + $('#ws-ame-apply-colors-to-all').on('click', function() { + if (!confirm('Apply these color settings to ALL top level menus?')) { + return; + } + + //Set this as the global preset and remove custom settings from all items. + var newColors = getColorSettingsFromDialog(); + if (newColors === null) { + delete colorPresets['[global]']; + } else { + colorPresets['[global]'] = newColors; + } + $('#ws_menu_box').find('.ws_menu').each(function() { + var containerNode = $(this), + menuItem = containerNode.data('menu_item'); + if (!menuItem.separator) { + menuItem.colors = null; + updateItemEditor(containerNode); + } + }); + + menuColorDialog.dialog('close'); + colorDialogState.containerNode = null; + colorDialogState.menuItem = null; + }); + + function addColorPreset(name, colors) { + colorPresets[name] = colors; + populatePresetDropdown(); + colorPresetDropdown.val(name); + colorPresetDeleteButton.removeClass('hidden'); + } + + function deleteColorPreset(name) { + delete colorPresets[name]; + populatePresetDropdown(); + colorPresetDropdown.val(''); + colorPresetDeleteButton.addClass('hidden'); + } + + function populatePresetDropdown() { + var separator = colorPresetDropdown.find('#ame-color-preset-separator'); + + //Delete the old options, but keep the "save preset" option and so on. + colorPresetDropdown.find('option').not('.ame-meta-option').remove(); + + //Sort presets alphabetically. + var presetNames = $.map(colorPresets, function(unused, name) { + return name; + }).sort(function(a, b) { + return a.localeCompare(b); + }); + + //Add them all to the dropdown. + var newOptions = jQuery([]); + $.each(presetNames, function(unused, name) { + if (name === '[global]') { + return; + } + + newOptions = newOptions.add($('<option>', { + val: name, + text: name + })); + }); + newOptions.insertBefore(separator); + } + + function deselectPresetOnColorChange() { + //Most jQuery widgets don't trigger change events when you update them via JavaScript, + //but apparently wpColorPicker does. We want to ignore those superfluous events. + if (!areColorChangesIgnored && (colorPresetDropdown.val() !== '')) { + colorPresetDropdown.val(''); + } + } + + colorPresetDropdown.on('change', function() { + var dropdown = $(this), + presetName = dropdown.val(); + + colorPresetDeleteButton.toggleClass('hidden', _.includes(['[save_preset]', '[global]', '', null], presetName)); + + if ((presetName === '[save_preset]') && menuColorDialog.dialog('isOpen')) { + //Create a new preset. + var colors = getColorSettingsFromDialog(); + if (colors === null) { + dropdown.val(''); + alert('Error: No colors selected'); + return; + } + + var newPresetName = window.prompt('New preset name:', ''); + if ((newPresetName === null) || (jsTrim(newPresetName) === '')) { + dropdown.val(''); + return; + } + + addColorPreset(newPresetName, colors); + } else if (presetName !== '') { + //Apply the selected preset. + var preset = colorPresets[presetName]; + displayColorSettingsInDialog(preset); + } + }); + + colorPresetDeleteButton.on('click', function() { + var presetName = $('#ame-menu-color-presets').val(); + if ( _.includes(['[save_preset]', '[global]', '', null], presetName) ) { + return false; + } + if (!confirm('Are you sure you want to delete the preset "' + presetName + '"?')) { + return false; + } + + deleteColorPreset(presetName); + return false; + }); + + //region Toolbar buttons + + /************************************************************************* + Menu toolbar buttons + *************************************************************************/ + function getSelectedMenu() { + return menuPresenter.getColumnImmediate(1).getSelectedItem(); + } + AmeEditorApi.getSelectedMenu = getSelectedMenu; + + //Show/Hide menu + menuEditorNode.on( + 'adminMenuEditor:action-hide', + /** + * @param event + * @param {JQuery|null} selectedItem + * @param {AmeEditorColumn} column + */ + function(event, selectedItem, column) { + const selection = column.getSelectedItem(); + if (selection.length < 1) { + return; + } + + toggleItemHiddenFlag(selection); + } + ); + + //Hide a menu and deny access. + menuEditorNode.on( + 'adminMenuEditor:action-deny', + /** + * @param event + * @param {JQuery|null} selectedItem + * @param {AmeEditorColumn} column + */ + function(event, selectedItem, column) { + const selection = column.getSelectedItem(); + if (selection.length < 1) { + return; + } + + function objectFillKeys(keys, value) { + let result = {}; + _.forEach(keys, function(key) { + result[key] = value; + }); + return result; + } + + if (actorSelectorWidget.selectedActor === null) { + //Hide from everyone except Super Admin and the current user. + let menuItem = selection.data('menu_item'), + validActors = _.keys(wsEditorData.actors), + alwaysAllowedActors = _.intersection( + ['special:super_admin', 'user:' + wsEditorData.currentUserLogin], + validActors + ), + victims = _.difference(validActors, alwaysAllowedActors), + shouldHide; + + //First, lets check who has access. Maybe this item is already hidden from the victims. + shouldHide = _.some(victims, _.curry(actorCanAccessMenu, 2)(menuItem)); + + let keepEnabled = objectFillKeys(alwaysAllowedActors, true), + hideAllExceptAllowed = _.assign(objectFillKeys(victims, false), keepEnabled); + + walkMenuTree(selection, function(container, item) { + let newAccess; + if (shouldHide) { + //Yay, hide it now! + newAccess = hideAllExceptAllowed; + //Only update had_access_before_hiding if this item isn't hidden yet or the field is missing. + //We don't want to double-hide an item. + let actorsWithAccess = _.filter(victims, function(actor) { + return actorCanAccessMenu(item, actor); + }); + if ((actorsWithAccess.length) > 0 || _.isEmpty(_.get(item, 'had_access_before_hiding', null))) { + item.had_access_before_hiding = actorsWithAccess; + } + } else { + //Give back access to the roles and users who previously had access. + //Careful, don't give access to roles that no longer exist. + let actorsWhoHadAccess = _.get(item, 'had_access_before_hiding', []) || []; + actorsWhoHadAccess = _.intersection(actorsWhoHadAccess, validActors); + + newAccess = _.assign(objectFillKeys(actorsWhoHadAccess, true), keepEnabled); + delete item.had_access_before_hiding; + } + + setActorAccess(container, newAccess); + updateItemEditor(container); + }); + + } else { + //Just toggle the checkbox. + selection.find('input.ws_actor_access_checkbox').trigger('click'); + } + } + ); + + //Delete error dialog. It shows up when the user tries to delete one of the default menus. + var menuDeletionDialog = $('#ws-ame-menu-deletion-error').dialog({ + autoOpen: false, + modal: true, + closeText: ' ', + title: 'Error', + draggable: false + }); + var menuDeletionCallback = function(hide) { + menuDeletionDialog.dialog('close'); + var selection = menuDeletionDialog.data('selected_menu'); + + function applyCallbackRecursively(containerNode, callback) { + callback(containerNode.data('menu_item')); + + var subMenuId = containerNode.data('submenu_id'); + if (subMenuId && containerNode.hasClass('ws_menu')) { + $('.ws_item', '#' + subMenuId).each(function() { + var node = $(this); + callback(node.data('menu_item')); + updateItemEditor(node); + }); + } + + updateItemEditor(containerNode); + } + + function hideRecursively(containerNode, exceptActor) { + var otherActors = _(actorSelectorWidget.getVisibleActors()) + .pluck('id') + .without(exceptActor) + .value(); + + applyCallbackRecursively(containerNode, function(menuItem) { + //Remember which actors had access to this item so that it + //can be un-hidden by the toolbar button. + var actorsWithAccess = _.filter(otherActors, function(actor) { + return actorCanAccessMenu(menuItem, actor); + }); + if ((actorsWithAccess.length) > 0) { + menuItem.had_access_before_hiding = actorsWithAccess; + } + + denyAccessForAllExcept(menuItem, exceptActor); + }); + updateParentAccessUi(containerNode); + } + + //TODO: Write had_access_before_hiding so that it can be un-hidden using the toolbar button. + if (hide === 'all') { + if (wsEditorData.wsMenuEditorPro) { + hideRecursively(selection, null); + } else { + //The free version doesn't have role permissions, so use the global "hidden" flag. + applyCallbackRecursively(selection, function(menuItem) { + menuItem.hidden = true; + }); + } + } else if (hide === 'except_current_user') { + hideRecursively(selection, 'user:' + wsEditorData.currentUserLogin); + } else if (hide === 'except_administrator' && !wsEditorData.wsMenuEditorPro) { + //Set "required capability" to something only the Administrator role would have. + var adminOnlyCap = 'manage_options'; + applyCallbackRecursively(selection, function(menuItem) { + menuItem.extra_capability = adminOnlyCap; + }); + alert('The "required capability" field was set to "' + adminOnlyCap + '".'); + } + }; + + //Callbacks for each of the dialog buttons. + $('#ws_cancel_menu_deletion').on('click', function() { + menuDeletionCallback(false); + }); + $('#ws_hide_menu_from_everyone').on('click', function() { + menuDeletionCallback('all'); + }); + const $hideExceptCurrentUser = $('#ws_hide_menu_except_current_user').on('click', function() { + menuDeletionCallback('except_current_user'); + }); + const $hideExceptAdmin = $('#ws_hide_menu_except_administrator').on('click', function() { + menuDeletionCallback('except_administrator'); + }); + + /** + * Attempt to delete a menu item. Will check if the item can actually be deleted and ask the user for confirmation. + * UI callback. + * + * @param {JQuery} selection The selected menu item (DOM node). + */ + function tryDeleteItem(selection) { + var menuItem = selection.data('menu_item'); + var shouldDelete = false; + + if (canDeleteItem(selection)) { + //Custom and duplicate items can be deleted normally. + shouldDelete = confirm('Delete this menu?'); + } else { + //Non-custom items can not be deleted, but they can be hidden. Ask the user if they want to do that. + menuDeletionDialog.find('#ws-ame-menu-type-desc').text( + getDefaultValue(menuItem, 'is_plugin_page') ? 'an item added by another plugin' : 'a built-in menu item' + ); + menuDeletionDialog.data('selected_menu', selection); + + //Different versions get slightly different options because only the Pro version has + //role-specific permissions. + $hideExceptCurrentUser.toggleClass('hidden', !wsEditorData.wsMenuEditorPro); + $hideExceptAdmin.toggleClass('hidden', wsEditorData.wsMenuEditorPro); + + menuDeletionDialog.dialog('open'); + + //Select "Cancel" as the default button. + menuDeletionDialog.find('#ws_cancel_menu_deletion').focus(); + } + + if (shouldDelete) { + const parentSubmenu = selection.closest('.ws_submenu'); + + //Delete the menu. + menuPresenter.destroyItem(selection); + + if (parentSubmenu && (parentSubmenu.length > 0)) { + //Refresh permissions UI for this menu's parent (if any). + updateParentAccessUi(parentSubmenu); + } + } + } + + //Delete menu + menuEditorNode.on( + 'adminMenuEditor:action-delete', + /** + * @param event + * @param {JQuery|null} selectedItem + * @param {AmeEditorColumn} column + */ + function(event, selectedItem, column) { + const selection = column.getSelectedItem(); + if (selection.length < 1) { + return; + } + + tryDeleteItem(selection); + } + ); + + //Copy menu + menuEditorNode.on( + 'adminMenuEditor:action-copy', + + /** + * @param event + * @param {JQuery|null} selectedItem + */ + function (event, selectedItem) { + //Get the selected menu + if (!selectedItem || (selectedItem.lengt < 1)) { + return; + } + + //Store a copy of the current menu state in clipboard + menu_in_clipboard = readItemState(selectedItem); + } + ); + + //Cut menu + menuEditorNode.on( + 'adminMenuEditor:action-cut', + + /** + * @param event + * @param {JQuery|null} selectedItem + * @param {AmeEditorColumn} column + */ + function (event, selectedItem, column) { + if (selectedItem === null) { + alert('Please select a menu item first.'); + return; + } + const submenu = selectedItem.closest('.ws_submenu'); + + //Store a copy of the current menu state in clipboard + menu_in_clipboard = readItemState(selectedItem); + + //Remove the original menu and submenu + column.destroyItem(selectedItem); + + //If this submenu had mixed permissions, that might have changed now that the item is gone. + updateParentAccessUi(submenu); + } + ); + + menuEditorNode.on( + 'adminMenuEditor:action-paste', + /** + * @param event + * @param {JQuery|null} selectedItem + * @param {AmeEditorColumn} column + */ + function(event, selectedItem, column) { + //Check if anything has been copied/cut + if (!menu_in_clipboard) { + return; + } + + //You can only add separators to submenus in the Pro version. + if ( menu_in_clipboard.separator && !wsEditorData.wsMenuEditorPro ) { + return; + } + + const copyOfItem = $.extend(true, {}, menu_in_clipboard); + + //Paste the menu after the selection. + column.pasteItem(copyOfItem, selectedItem); + } + ); + + //New menu + menuEditorNode.on( + 'adminMenuEditor:action-new-menu', + /** + * @param event + * @param {JQuery|null} selectedItem + * @param {AmeEditorColumn} column + */ + function(event, selectedItem, column) { + const visibleList = column.getVisibleItemList(); + if (!visibleList || (visibleList.length < 1)) { + //Abort if there's no item list in this column. This can happen if nothing is selected + //in the previous column. + return; + } + + ws_paste_count++; + + //The new menu starts out rather bare. + let item = $.extend(true, {}, wsEditorData.blankMenuItem, { + custom: true, //Important : flag the new menu as custom, or it won't show up after saving. + template_id: '', + menu_title: 'Custom Menu ' + ws_paste_count, + file: randomMenuId(), + items: [] + }); + item.defaults = $.extend(true, {}, itemTemplates.getDefaults('')); + + //Make it accessible only to the current actor if one is selected. + if (actorSelectorWidget.selectedActor !== null) { + denyAccessForAllExcept(item, actorSelectorWidget.selectedActor); + } + + //Insert the new menu item. + let selection = column.getSelectedItem(); + if (!selection || (selection.length < 1)) { + selection = null; + } + let result = column.outputItem(item, selection); + + if (result && result.menu) { + //The menu's editbox is always open + result.menu.find('.ws_edit_link').trigger('click'); + + updateParentAccessUi(result.menu); + } + } + ); + + //New separator + menuEditorNode.on( + 'adminMenuEditor:action-new-separator', + /** + * @param event + * @param {JQuery|null} selectedItem + * @param {AmeEditorColumn} column + */ + function(event, selectedItem, column) { + const visibleList = column.getVisibleItemList(); + if (!visibleList || (visibleList.length < 1)) { + //Abort if there's no item list in this column. This can happen if nothing is selected + //in the previous column. + return; + } + + ws_paste_count++; + + const randomId = randomMenuId('separator_'); + let item = $.extend(true, {}, wsEditorData.blankMenuItem, { + separator: true, //Flag as a separator + custom: false, //Separators don't need to flagged as custom to be retained. + items: [], + defaults: { + separator: true, + css_class : 'wp-menu-separator', + access_level : 'read', + file : randomId, + hookname : randomId + } + }); + + const selection = column.getSelectedItem(); + column.outputItem(item, (selection.length > 0) ? selection : null); + } + ); + + //Toggle all menus for the currently selected actor + menuEditorNode.on( + 'adminMenuEditor:action-toggle-all', + /** + * @param event + */ + function(event) { + if ( actorSelectorWidget.selectedActor === null ) { + alert("This button enables/disables all menus for the selected role. To use it, click a role and then click this button again."); + return; + } + + //Look at the first menu's permissions and set everything to the opposite. + const firstColumn = menuPresenter.getColumnImmediate(1); + const topMenuNodes = $('.ws_menu', firstColumn.getVisibleItemList()); + + const allow = ! actorCanAccessMenu(topMenuNodes.eq(0).data('menu_item'), actorSelectorWidget.selectedActor); + + topMenuNodes.each(function() { + let containerNode = $(this); + setActorAccessForTreeAndUpdateUi(containerNode, actorSelectorWidget.selectedActor, allow); + }); + } + ); + + //Copy all menu permissions from one role to another. + var copyPermissionsDialog = $('#ws-ame-copy-permissions-dialog').dialog({ + autoOpen: false, + modal: true, + closeText: ' ', + draggable: false + }); + + var sourceActorList = $('#ame-copy-source-actor'), destinationActorList = $('#ame-copy-destination-actor'); + + //The "Copy permissions" toolbar button. + menuEditorNode.on( + 'adminMenuEditor:action-copy-permissions', + /** + * @param event + * @param {JQuery|null} selectedItem + * @param {AmeEditorColumn} column + */ + function(event, selectedItem, column) { + const previousSource = sourceActorList.val(); + + //Populate source/destination lists. + sourceActorList.find('option').not('[disabled]').remove(); + destinationActorList.find('option').not('[disabled]').remove(); + $.each(actorSelectorWidget.getVisibleActors(), function(index, actor) { + let option = $('<option>', { + val: actor.id, + text: actorSelectorWidget.getNiceName(actor) + }); + sourceActorList.append(option); + destinationActorList.append(option.clone()); + }); + + //Pre-select the current actor as the destination. + if (actorSelectorWidget.selectedActor !== null) { + destinationActorList.val(actorSelectorWidget.selectedActor); + } + + //Restore the previous source selection. + if (previousSource) { + sourceActorList.val(previousSource); + } + if (!sourceActorList.val()) { + sourceActorList.find('option').first().prop('selected', true); //Fallback. + } + + copyPermissionsDialog.dialog('open'); + } + ); + + //Actually copy the permissions when the user click the confirmation button. + var copyConfirmationButton = $('#ws-ame-confirm-copy-permissions'); + copyConfirmationButton.on('click', function() { + var sourceActor = sourceActorList.val(); + var destinationActor = destinationActorList.val(); + + if (sourceActor === null || destinationActor === null) { + alert('Select a source and a destination first.'); + return; + } + + //Iterate over all menu items and copy the permissions from one actor to the other. + AmeEditorApi.forEachMenuItem(function (menuItem, node) { + //Only change permissions when they don't match. This ensures we won't unnecessarily overwrite default + //permissions and bloat the configuration with extra grant_access entries. + const sourceAccess = actorCanAccessMenu(menuItem, sourceActor); + const destinationAccess = actorCanAccessMenu(menuItem, destinationActor); + if (sourceAccess !== destinationAccess) { + setActorAccess(node, destinationActor, sourceAccess); + //Note: In theory, we could also look at the default permissions for destinationActor and + //revert to default instead of overwriting if that would make the two actors' permissions match. + } + }); + + //todo: copy granted permissions like CPTs. + + //If the user is currently looking at the destination actor, force the UI to refresh + //so that they can see the new permissions. + if (actorSelectorWidget.selectedActor === destinationActor) { + //This is a bit of a hack, but right now there's no better way to refresh all items at once. + actorSelectorWidget.setSelectedActor(null); + actorSelectorWidget.setSelectedActor(destinationActor); + } + + //All done. + copyPermissionsDialog.dialog('close'); + }); + + //Only enable the copy button when the user selects a valid source and destination. + copyConfirmationButton.prop('disabled', true); + sourceActorList.add(destinationActorList).on('click', function() { + var sourceActor = sourceActorList.val(); + var destinationActor = destinationActorList.val(); + + var validInputs = (sourceActor !== null) && (destinationActor !== null) && (sourceActor !== destinationActor); + copyConfirmationButton.prop('disabled', !validInputs); + }); + + //Sort menus in ascending or descending order. + menuEditorNode.on( + 'adminMenuEditor:action-sort', + /** + * @param event + * @param {JQuery|null} selectedItem + * @param {AmeEditorColumn} column + * @param {JQuery} button + */ + function(event, selectedItem, column, button) { + let direction = button.data('sort-direction') || 'asc', + menuBox = column.getVisibleItemList(); + + if (!menuBox || (menuBox.length < 1)) { + return; + } + + function sortRecursively($box, currentColumn) { + //When indirectly sorting the second menu level (regular submenus), leave the first item unmoved. + //Moving the first item would change the parent menu URL (WP always links it to the first item), + //which can be unexpected and confusing. The user can always move the first item manually. + let leaveFirstItem = ((currentColumn !== column) && (currentColumn.level === 2)); + sortMenuItems($box, direction, leaveFirstItem); + + //Also sort child items in the next columns. + const nextColumn = menuPresenter.getColumnImmediate(currentColumn.level + 1); + if (nextColumn) { + $box.find('.ws_container').each(function () { + const $submenu = getSubmenuOf($(this), null); + if ($submenu) { + sortRecursively($submenu, nextColumn); + } + }); + } + } + + sortRecursively(menuBox, column); + } + ); + + /** + * Sort menu items by title. + * + * @param $menuBox A DOM node that contains multiple menu items. + * @param {string} direction 'asc' or 'desc' + * @param {boolean} [leaveFirstItem] Leave the first item in its original position. Defaults to false. + */ + function sortMenuItems($menuBox, direction, leaveFirstItem) { + var multiplier = (direction === 'desc') ? -1 : 1, + items = $menuBox.find('.ws_container'), + firstItem = items.first(); + + //Separators don't have a title, but we don't want them to end up at the top of the list. + //Instead, lets keep their position the same relative to the previous item. + var prevItemTitle = ''; + items.each((function(){ + var item = $(this), sortValue; + if (item.is('.ws_menu_separator')) { + sortValue = prevItemTitle; + } else { + sortValue = jsTrim(item.find('.ws_item_title').text()); + prevItemTitle = sortValue; + } + item.data('ame-sort-value', sortValue); + })); + + function compareMenus(a, b){ + var aTitle = $(a).data('ame-sort-value'), + bTitle = $(b).data('ame-sort-value'); + + aTitle = aTitle.toLowerCase(); + bTitle = bTitle.toLowerCase(); + + if (aTitle > bTitle) { + return multiplier; + } else if (aTitle < bTitle) { + return -multiplier; + } + return 0; + } + + items.sort(compareMenus); + + if (leaveFirstItem) { + //Move the first item back to the top. + firstItem.prependTo($menuBox); + } + } + + //Toggle the second row of toolbar buttons. + menuEditorNode.on( + 'adminMenuEditor:action-toggle-toolbar', + /** + * @param event + */ + function(event) { + let visible = menuEditorNode.find('.ws_second_toolbar_row').toggle().is(':visible'); + if (typeof $['cookie'] !== 'undefined') { + $.cookie('ame-show-second-toolbar', visible ? '1' : '0', {expires: 90}); + } + } + ); + + + /************************************************************************* + Item toolbar buttons + *************************************************************************/ + function getSelectedSubmenuItem() { + return menuPresenter.getColumnImmediate(2).getSelectedItem(); + } + + //endregion + + //============================================== + // Main buttons + //============================================== + + //Save Changes - encode the current menu as JSON and save + $('#ws_save_menu').on('click', function () { + try { + var tree = readMenuTreeState(); + } catch (error) { + //Right now the only known error condition is duplicate top level URLs. + if (error.hasOwnProperty('code') && (error.code === 'duplicate_top_level_url')) { + var message = 'Error: Duplicate menu URLs. The following top level menus have the same URL:\n\n' ; + for (var i = 0; i < error.duplicates.length; i++) { + var containerNode = $(error.duplicates[i]); + message += (i + 1) + '. ' + containerNode.find('.ws_item_title').first().text() + '\n'; + } + message += '\nPlease change the URLs to be unique or delete the duplicates.'; + alert(message); + } else { + alert(error.message); + } + return; + } + + function findItemByTemplateId(items, templateId) { + var foundItem = null; + + $.each(items, function(index, item) { + if (item.template_id === templateId) { + foundItem = item; + return false; + } + if (item.hasOwnProperty('items') && (item.items.length > 0)) { + foundItem = findItemByTemplateId(item.items, templateId); + if (foundItem !== null) { + return false; + } + } + return true; + }); + + return foundItem; + } + + //Abort the save if it would make the editor inaccessible. + if (wsEditorData.wsMenuEditorPro) { + var myMenuItem = findItemByTemplateId(tree.tree, 'options-general.php>menu_editor'); + if (myMenuItem === null) { // jshint ignore:line + //This is OK - the missing menu item will be re-inserted automatically. + } else if (!actorCanAccessMenu(myMenuItem, 'user:' + wsEditorData.currentUserLogin)) { + alert( + "Error: This configuration would make you unable to access the menu editor!\n\n" + + "Please click either your role name or \"Current user (" + wsEditorData.currentUserLogin + ")\" "+ + "and enable the \"Menu Editor Pro\" menu item." + ); + return; + } + } + + var data = encodeMenuAsJSON(tree); + $('#ws_data').val(data); + $('#ws_data_length').val(data.length); + $('#ws_selected_actor').val(actorSelectorWidget.selectedActor === null ? '' : actorSelectorWidget.selectedActor); + + $('#ws_is_deep_nesting_enabled').val(JSON.stringify(menuPresenter.isDeepNestingEnabled)); + + var selectedMenu = getSelectedMenu(); + if (selectedMenu.length > 0) { + $('#ws_selected_menu_url').val(AmeEditorApi.getItemDisplayUrl(selectedMenu.data('menu_item'))); + $('#ws_expand_selected_menu').val(selectedMenu.find('.ws_editbox').is(':visible') ? '1' : ''); + + var selectedSubmenu = getSelectedSubmenuItem(); + if (selectedSubmenu.length > 0) { + $('#ws_selected_submenu_url').val(AmeEditorApi.getItemDisplayUrl(selectedSubmenu.data('menu_item'))); + $('#ws_expand_selected_submenu').val(selectedSubmenu.find('.ws_editbox').is(':visible') ? '1' : ''); + } + } + + $('#ws_main_form').trigger('submit'); + }); + + //Load default menu - load the default WordPress menu + $('#ws_load_menu').on('click', function () { + if (confirm('Are you sure you want to load the default WordPress menu?')){ + loadMenuConfiguration(defaultMenu); + } + }); + + //Reset menu - re-load the custom menu. Discards any changes made by user. + $('#ws_reset_menu').on('click', function () { + if (confirm('Undo all changes made in the current editing session?')){ + loadMenuConfiguration(customMenu); + } + }); + + //Enable the "load default menu" and "undo changes" buttons only when "All" is selected. + //Otherwise some users incorrectly assume these buttons only affect the currently selected role or user. + actorSelectorWidget.onChange(function (newSelectedActor) { + $('#ws_load_menu, #ws_reset_menu').prop('disabled', newSelectedActor !== null); + }); + $('#ws_load_menu, #ws_reset_menu').prop('disabled', actorSelectorWidget.selectedActor !== null); + + $('#ws_toggle_editor_layout').on('click', function () { + var isCompactLayoutEnabled = menuEditorNode.toggleClass('ws_compact_layout').hasClass('ws_compact_layout'); + if (typeof $['cookie'] !== 'undefined') { + $.cookie('ame-compact-layout', isCompactLayoutEnabled ? '1' : '0', {expires: 90}); + } + + var button = $(this); + if (button.is('input')) { + var checkMark = '\u2713'; + button.val(button.val().replace(checkMark, '')); + if (isCompactLayoutEnabled) { + button.val(checkMark + ' ' + button.val()); + } + } + }); + + //Export menu - download the current menu as a file + $('#export_dialog').dialog({ + autoOpen: false, + closeText: ' ', + modal: true, + minHeight: 100 + }); + + $('#ws_export_menu').on('click', function(){ + var button = $(this); + button.prop('disabled', true); + button.val('Exporting...'); + + $('#export_complete_notice, #download_menu_button').hide(); + $('#export_progress_notice').show(); + var exportDialog = $('#export_dialog'); + exportDialog.dialog('open'); + + //Encode the menu. + try { + var exportData = encodeMenuAsJSON(); + } catch (error) { + exportDialog.dialog('close'); + alert(error.message); + + button.val('Export'); + button.prop('disabled', false); + return; + } + + //Store the menu for download. + $.post( + wsEditorData.adminAjaxUrl, + { + 'data' : exportData, + 'action' : 'export_custom_menu', + '_ajax_nonce' : wsEditorData.exportMenuNonce + }, + /** + * @param {Object} data + */ + function(data){ + button.val('Export'); + button.prop('disabled', false); + + if ( typeof data.error !== 'undefined' ){ + exportDialog.dialog('close'); + alert(data.error); + } + + if ( _.has(data, 'download_url') ){ + //window.location = data.download_url; + $('#download_menu_button').attr('href', _.get(data, 'download_url')).data('filesize', _.get(data, 'filesize')); + $('#export_progress_notice').hide(); + $('#export_complete_notice, #download_menu_button').show(); + } + }, + 'json' + ); + }); + + $('#ws_cancel_export').on('click', function(){ + $('#export_dialog').dialog('close'); + }); + + $('#download_menu_button').on('click', function(){ + $('#export_dialog').dialog('close'); + }); + + //Import menu - upload an exported menu and show it in the editor + $('#import_dialog').dialog({ + autoOpen: false, + closeText: ' ', + modal: true + }); + const $importMenuForm = $('#import_menu_form'); + + $('#ws_cancel_import').on('click', function(){ + $('#import_dialog').dialog('close'); + }); + + $('#ws_import_menu').on('click', function(){ + $('#import_progress_notice, #import_progress_notice2, #import_complete_notice, #ws_import_error').hide(); + $('#ws_import_panel').show(); + $importMenuForm.resetForm(); + //The "Upload" button is disabled until the user selects a file + $('#ws_start_import').attr('disabled', 'disabled'); + + var importDialog = $('#import_dialog'); + importDialog.find('.hide-when-uploading').show(); + importDialog.dialog('open'); + }); + + $('#import_file_selector').on('change', function(){ + $('#ws_start_import').prop('disabled', ! $(this).val() ); + }); + + //This function displays unhandled server side errors. In theory, our upload handler always returns a well-formed + //response even if there's an error. In practice, stuff can go wrong in unexpected ways (e.g. plugin conflicts). + function handleUnexpectedImportError(xhr, errorMessage) { + //The server-side code didn't catch this error, so it's probably something serious + //and retrying won't work. + $importMenuForm.resetForm(); + $('#ws_import_panel').hide(); + + //Display error information. + $('#ws_import_error_message').text(errorMessage); + $('#ws_import_error_http_code').text(xhr.status); + $('#ws_import_error_response').text((xhr.responseText !== '') ? xhr.responseText : '[Empty response]'); + $('#ws_import_error').show(); + } + + //AJAXify the upload form + $importMenuForm.ajaxForm({ + dataType : 'json', + beforeSubmit: function(formData) { + + //Check if the user has selected a file + for(var i = 0; i < formData.length; i++){ + if ( formData[i].name === 'menu' ){ + if ( (typeof formData[i].value === 'undefined') || !formData[i].value){ + alert('Select a file first!'); + return false; + } + } + } + + $('#import_dialog').find('.hide-when-uploading').hide(); + $('#import_progress_notice').show(); + + $('#ws_start_import').attr('disabled', 'disabled'); + return true; + }, + success: function(data, status, xhr) { + $('#import_progress_notice').hide(); + + var importDialog = $('#import_dialog'); + if ( !importDialog.dialog('isOpen') ){ + //Whoops, the user closed the dialog while the upload was in progress. + //Discard the response silently. + return; + } + + if ( data === null ) { + handleUnexpectedImportError(xhr, 'Invalid response from server. Please check your PHP error log.'); + return; + } + + if ( typeof data.error !== 'undefined' ){ + alert(data.error); + //Let the user try again + $importMenuForm.resetForm(); + importDialog.find('.hide-when-uploading').show(); + } + + if ( (typeof data.tree !== 'undefined') && data.tree ){ + //Whee, we got back a (seemingly) valid menu. A veritable miracle! + //Lets load it into the editor. + var progressNotice = $('#import_progress_notice2').show(); + loadMenuConfiguration(data); + progressNotice.hide(); + //Display a success notice, then automatically close the window after a few moments + $('#import_complete_notice').show(); + setTimeout((function(){ + //Close the import dialog + $('#import_dialog').dialog('close'); + }), 500); + } + + }, + error: function(xhr, status, errorMessage) { + handleUnexpectedImportError(xhr, errorMessage); + } + }); + + /************************************************************************* + Drag & drop items between menu levels + *************************************************************************/ + + if (wsEditorData.wsMenuEditorPro) { + //Allow the user to drag sub-menu items to the top level. + $('#ws_top_menu_dropzone').droppable({ + 'hoverClass' : 'ws_dropzone_hover', + 'activeClass' : 'ws_dropzone_active', + + 'accept' : (function(thing){ + return thing.hasClass('ws_item'); + }), + + 'drop' : (function(event, ui){ + const firstColumn = menuPresenter.getColumnImmediate(1); + if (!firstColumn) { + return; + } + const nextColumn = menuPresenter.getColumnImmediate(firstColumn.level + 1); + + const droppedItemData = readItemState(ui.draggable); + const newItemNodes = firstColumn.pasteItem(droppedItemData, null); + + //If the item was originally a top level menu, also move its original submenu items. + if ((getFieldValue(droppedItemData, 'parent') === null) && (newItemNodes.submenu)) { + const droppedItemFile = getFieldValue(droppedItemData, 'file'); + const nearbyItems = $(ui.draggable).siblings('.ws_item'); + nearbyItems.each(function() { + const containerNode = $(this), + submenuItem = containerNode.data('menu_item'); + + //Was this item originally a child of the dragged menu? + if (getFieldValue(submenuItem, 'parent') === droppedItemFile) { + nextColumn.pasteItem(submenuItem, null, newItemNodes.submenu); + if ( !event.ctrlKey ) { + menuPresenter.destroyItem(containerNode); + } + } + }); + } + + if ( !event.ctrlKey ) { + menuPresenter.destroyItem(ui.draggable); + } + }) + }); + } + + /****************************************************************** + Component visibility settings + ******************************************************************/ + + var $generalVisBox = $('#ws_ame_general_vis_box'), + $showAdminMenu = $('#ws_ame_show_admin_menu'), + $showWpToolbar = $('#ws_ame_show_toolbar'); + + AmeEditorApi.actorCanSeeComponent = function(component, actorId) { + if (actorId === null) { + return _.some(actorSelectorWidget.getVisibleActors(), function(actor) { + return AmeEditorApi.actorCanSeeComponent(component, actor.id); + }); + } + + var actorSpecificSetting = _.get(generalComponentVisibility, [component, actorId], null); + if (actorSpecificSetting !== null) { + return actorSpecificSetting; + } + + //Super Admin can see everything by default. + if (actorId === AmeSuperAdmin.permanentActorId) { + return _.get(generalComponentVisibility, [component, AmeSuperAdmin.permanentActorId], true); + } + + var actor = AmeActors.getActor(actorId); + if (actor instanceof AmeUser) { + var grants = _.get(generalComponentVisibility, component, {}); + + //Super Admin has priority. + if (actor.isSuperAdmin) { + return AmeEditorApi.actorCanSeeComponent(component, AmeSuperAdmin.permanentActorId); + } + + //The user can see the admin menu/Toolbar if at least one of their roles can see it. + var result = null; + _.forEach(actor.roles, function(roleName) { + var allow = _.get(grants, 'role:' + roleName, true); + if (result === null) { + result = allow; + } else { + result = result || allow; + } + }); + + if (result !== null) { + return result; + } + } + + //Everyone can see the admin menu and the Toolbar by default. + return true; + }; + + AmeEditorApi.refreshComponentVisibility = function() { + if ($generalVisBox.length < 1) { + return; + } + + var actorId = actorSelectorWidget.selectedActor; + $showAdminMenu.prop('checked', AmeEditorApi.actorCanSeeComponent('adminMenu', actorId)); + $showWpToolbar.prop('checked', AmeEditorApi.actorCanSeeComponent('toolbar', actorId)); + }; + + AmeEditorApi.setComponentVisibility = function(section, actorId, enabled) { + if (actorId === null) { + _.forEach(actorSelectorWidget.getVisibleActors(), function(actor) { + _.set(generalComponentVisibility, [section, actor.id], enabled); + }); + } else { + _.set(generalComponentVisibility, [section, actorId], enabled); + } + }; + + if ($generalVisBox.length > 0) { + $showAdminMenu.on('click', function() { + AmeEditorApi.setComponentVisibility( + 'adminMenu', + actorSelectorWidget.selectedActor, + $(this).is(':checked') + ); + }); + $showWpToolbar.on('click', function () { + AmeEditorApi.setComponentVisibility( + 'toolbar', + actorSelectorWidget.selectedActor, + $(this).is(':checked') + ); + }); + + $generalVisBox.find('.handlediv').on('click', function() { + $generalVisBox.toggleClass('closed'); + if (typeof $['cookie'] !== 'undefined') { + $.cookie( + 'ame_vis_box_open', + ($generalVisBox.hasClass('closed') ? '0' : '1'), + { expires: 90 } + ); + } + }); + + actorSelectorWidget.onChange(function() { + AmeEditorApi.refreshComponentVisibility(); + }); + } + + /****************************************************************** + Tooltips and hints + ******************************************************************/ + + + //Set up tooltips + $('.ws_tooltip_trigger').qtip({ + style: { + classes: 'qtip qtip-rounded ws_tooltip_node' + }, + hide: { + fixed: true, + delay: 300 + } + }); + + //Set up menu field tooltips. + menuEditorNode.on('mouseenter click', '.ws_edit_field .ws_field_tooltip_trigger', function(event) { + var $trigger = $(this), + fieldName = $trigger.closest('.ws_edit_field').data('field_name'); + + if (knownMenuFields[fieldName].tooltip === null) { + return; + } + + var tooltipText = 'Invalid tooltip'; + if (typeof knownMenuFields[fieldName].tooltip === 'string') { + tooltipText = knownMenuFields[fieldName].tooltip; + } else if (typeof knownMenuFields[fieldName].tooltip === 'function') { + tooltipText = function() { + var $theTrigger = $(this), + menuItem = $theTrigger.closest('.ws_container').data('menu_item'); + return knownMenuFields[fieldName].tooltip(menuItem); + } + } + + $trigger.qtip({ + overwrite: false, + content: { + text: tooltipText + }, + show: { + event: event.type, + ready: true //Show immediately. + }, + style: { + classes: 'qtip qtip-rounded ws_tooltip_node' + }, + hide: { + fixed: true, + delay: 300 + }, + position: { + my: 'bottom center', + at: 'top center' + } + }, event); + }); + + //Set up the "additional permissions are available" tooltips. + menuEditorNode.on('mouseenter click', '.ws_ext_permissions_indicator', function() { + var $indicator = $(this); + $indicator.qtip({ + overwrite: false, + content: { + text: function() { + var indicator = $(this), + extPermissions = indicator.data('ext_permissions'), + text = 'Additional permission settings are available. Click "Edit..." to change them.', + heading = '', + $content = $('<span></span>'); + + if (extPermissions && extPermissions.hasOwnProperty('title')) { + heading = extPermissions.title; + if (extPermissions.hasOwnProperty('type')) { + heading = _.capitalize(_.startCase(extPermissions.type).toLowerCase()) + ': ' + heading; + } + $content.append($('<strong></strong>').text(heading)).append('<br>'); + } + + $content.append($(document.createTextNode(text))); + return $content; + } + }, + show: { + ready: true //Show immediately. + }, + style: { + classes: 'qtip qtip-rounded ws_tooltip_node' + }, + hide: { + fixed: true, + delay: 300 + }, + position: { + my: 'bottom center', + at: 'top center' + } + }); + }); + + //Flag closed hints as hidden by sending the appropriate AJAX request to the backend. + $('.ws_hint_close').on('click', function() { + var hint = $(this).parents('.ws_hint').first(); + hint.hide(); + wsEditorData.showHints[hint.attr('id')] = false; + $.post( + wsEditorData.adminAjaxUrl, + { + 'action' : 'ws_ame_hide_hint', + 'hint' : hint.attr('id') + } + ); + }); + + //Expand/collapse the "How To" box. + var $howToBox = $("#ws_ame_how_to_box"); + $howToBox.find(".handlediv").on('click', function() { + $howToBox.toggleClass('closed'); + if (typeof $['cookie'] !== 'undefined') { + $.cookie( + 'ame_how_to_box_open', + ($howToBox.hasClass('closed') ? '0' : '1'), + { expires: 180 } + ); + } + }); + + + /****************************************************************** + Actor views + ******************************************************************/ + + if (wsEditorData.wsMenuEditorPro) { + actorSelectorWidget.onChange(function() { + //There are some UI elements that can be visible or hidden depending on whether an actor is selected. + var editorNode = $('#ws_menu_editor'); + editorNode.toggleClass('ws_is_actor_view', (actorSelectorWidget.selectedActor !== null)); + + //Update the menu item states to indicate whether they're accessible. + editorNode.find('.ws_container').each(function() { + updateActorAccessUi($(this)); + }); + }); + + if (wsEditorData.hasOwnProperty('selectedActor') && wsEditorData.selectedActor) { + actorSelectorWidget.setSelectedActor(wsEditorData.selectedActor); + } else { + actorSelectorWidget.setSelectedActor(null); + } + } + + /****************************************************************** + "Test Access" feature + ******************************************************************/ + var testAccessDialog = $('#ws_ame_test_access_screen').dialog({ + autoOpen: false, + modal: true, + closeText: ' ', + title: 'Test access', + width: 900 + //draggable: false + }), + testMenuItemList = $('#ws_ame_test_menu_item'), + testActorList = $('#ws_ame_test_relevant_actor'), + testAccessButton = $('#ws_ame_start_access_test'), + testAccessFrame = $('#ws_ame_test_access_frame'), + testConfig = null, + + testProgress = $('#ws_ame_test_progress'), + testProgressText = $('#ws_ame_test_progress_text'); + + $('#ws_test_access').on('click', function () { + testConfig = readMenuTreeState(); + + var selectedMenuContainer = getSelectedMenu(), + selectedItemContainer = getSelectedSubmenuItem(), + selectedMenu = null, + selectedItem = null, + selectedUrl = null; + if (selectedMenuContainer.length > 0) { + selectedMenu = selectedMenuContainer.data('menu_item'); + selectedUrl = getFieldValue(selectedMenu, 'url'); + } + if (selectedItemContainer.length > 0) { + selectedItem = selectedItemContainer.data('menu_item'); + selectedUrl = getFieldValue(selectedItem, 'url'); + } + + function addMenuItems(collection, parentTitle, parentFile) { + _.each(collection, function (menuItem) { + if (menuItem.separator) { + return; + } + + var title = formatMenuTitle(getFieldValue(menuItem, 'menu_title', '[Untitled menu]')); + if (parentTitle) { + title = parentTitle + ' -> ' + title; + } + var url = getFieldValue(menuItem, 'url', '[no-url]'); + + var option = $( + '<option>', { + val: url, + text: title + } + ); + option.data('menu_item', menuItem); + option.data('parent_file', parentFile || ''); + option.prop('selected', (url === selectedUrl)); + + testMenuItemList.append(option); + + if (menuItem.items) { + addMenuItems(menuItem.items, title, getFieldValue(menuItem, 'file', '')); + } + }); + } + + //Populate the list of menu items. + testMenuItemList.empty(); + addMenuItems(testConfig.tree); + + //Populate the actor list. + testActorList.empty(); + testActorList.append($('<option>', {text: 'Not selected', val: ''})); + _.each(actorSelectorWidget.getVisibleActors(), function (actor) { + //TODO: Skip anything that isn't a role + var option = $('<option>', { + val: actor.id, + text: actorSelectorWidget.getNiceName(actor) + }); + testActorList.append(option); + }); + + //Pre-select the current actor. + if (actorSelectorWidget.selectedActor !== null) { + testActorList.val(actorSelectorWidget.selectedActor); + } + + testAccessDialog.dialog('open'); + }); + + testAccessButton.on('click', function () { + testAccessButton.prop('disabled', true); + testProgress.show(); + testProgressText.text('Sending menu settings...'); + + var selectedOption = testMenuItemList.find('option:selected').first(), + selectedMenu = selectedOption.data('menu_item'), + menuUrl = selectedOption.val(); + + $.ajax( + wsEditorData.adminAjaxUrl, + { + data: { + 'action': 'ws_ame_set_test_configuration', + 'data': encodeMenuAsJSON(testConfig), + '_ajax_nonce': wsEditorData.setTestConfigurationNonce + }, + method: 'post', + dataType: 'json', + success: function(response) { + if (!response) { + alert('Error: Could not parse the server response.'); + testAccessButton.prop('disabled', false); + return; + } + if (response.error) { + alert(response.error); + testAccessButton.prop('disabled', false); + return; + } + if (!response.success) { + alert('Error: The request failed, but there is no error information available.'); + testAccessButton.prop('disabled', false); + return; + } + + throw new Error('Not fully implemented yet!'); + //Caution: Won't work in IE. Needs compat checks. + //var testPageUrl = new URL(menuUrl, window.location.href); + var testPageUrl = 'fixme'; + testPageUrl.searchParams.append('ame-test-menu-access-as', $('#ws_ame_test_access_username').val()); + testPageUrl.searchParams.append('_wpnonce', wsEditorData.testAccessNonce); + testPageUrl.searchParams.append('ame-test-relevant-role', testActorList.val()); + + testPageUrl.searchParams.append('ame-test-target-item', getFieldValue(selectedMenu, 'file', '')); + testPageUrl.searchParams.append('ame-test-target-parent', selectedOption.data('parent_file')); + + testProgressText.text('Loading the test page....'); + $('#ws_ame_test_frame_placeholder').hide(); + + $(window).on('message', receiveTestAccessResults); + testAccessFrame + .show() + .on('load', onAccessTestLoaded) + .prop('src', testPageUrl.href); + }, + error: function(jqXHR, textStatus) { + alert('HTTP Error: ' + textStatus); + testAccessButton.prop('disabled', false); + } + } + ); + }); + + function onAccessTestLoaded() { + testAccessFrame.off('load', onAccessTestLoaded); + testProgress.hide(); + + testAccessButton.prop('disabled', false); + } + + function receiveTestAccessResults(event) { + if (event.originalEvent.source !== testAccessFrame.get(0).contentWindow) { + if (console && console.warn) { + console.warn('AME: Received a message from an unexpected source. Message ignored.'); + } + return; + } + var message = event.originalEvent.data || event.originalEvent.message; + console.log('message received', message); + + $(window).off('message', receiveTestAccessResults); + } + + + //Finally, show the menu + loadMenuConfiguration(customMenu); + + //Select the previous selected menu, if any. + if (wsEditorData.selectedMenu) { + AmeEditorApi.selectMenuItemByUrl( + '#ws_menu_box', + wsEditorData.selectedMenu, + _.get(wsEditorData, 'expandSelectedMenu') === '1' + ); + + if (wsEditorData.selectedSubmenu) { + AmeEditorApi.selectMenuItemByUrl( + '#ws_submenu_box', + wsEditorData.selectedSubmenu, + _.get(wsEditorData, 'expandSelectedSubmenu') === '1' + ); + } + } + + //... and make the UI visible now that it's fully rendered. + menuEditorNode.css('visibility', 'visible'); +} + +$(document).ready(ameOnDomReady); + +//Compatibility workaround: If another plugin or theme throws an exception in its jQuery.ready() handler, +//our callback might never get run. As a backup, set a timer and manually check if the DOM is ready. +var domCheckAttempts = 0, + maxDomCheckAttempts = 30; +var domCheckIntervalId = window.setInterval(function () { + if (isDomReadyDone || (domCheckAttempts >= maxDomCheckAttempts)) { + window.clearInterval(domCheckIntervalId); + return; + } + domCheckAttempts++; + + if ($ && $.isReady) { + window.clearInterval(domCheckIntervalId); + ameOnDomReady(); + } +}, 1000); + +})(jQuery, wsAmeLodash); + +//============================================== +// Screen options +//============================================== + +jQuery(function($){ + 'use strict'; + + var screenOptions = $('#ws-ame-screen-meta-contents'); + var hideSettingsCheckbox = screenOptions.find('#ws-hide-advanced-settings'); + hideSettingsCheckbox.prop('checked', wsEditorData.hideAdvancedSettings); + + //Update editor state when settings change + $('#ws-hide-advanced-settings').on('click', function(){ + wsEditorData.hideAdvancedSettings = hideSettingsCheckbox.prop('checked'); + + //Show/hide advanced settings dynamically as the user changes the setting. + if ($(this).is(hideSettingsCheckbox)) { + var menuEditorNode = $('#ws_menu_editor'); + if ( wsEditorData.hideAdvancedSettings ){ + menuEditorNode.find('div.ws_advanced').hide(); + menuEditorNode.find('a.ws_toggle_advanced_fields').text(wsEditorData.captionShowAdvanced).show(); + } else { + menuEditorNode.find('div.ws_advanced').show(); + menuEditorNode.find('a.ws_toggle_advanced_fields').text(wsEditorData.captionHideAdvanced).hide(); + } + } + + $.post( + wsEditorData.adminAjaxUrl, + { + 'action' : 'ws_ame_save_screen_options', + 'hide_advanced_settings' : wsEditorData.hideAdvancedSettings ? 1 : 0, + 'show_extra_icons' : wsEditorData.showExtraIcons ? 1 : 0, + '_ajax_nonce' : wsEditorData.hideAdvancedSettingsNonce + } + ); + }); + + //Move our options into the screen meta panel + var advSettings = $('#adv-settings'); + if (advSettings.length > 0) { + advSettings.empty().append(screenOptions.show()); + } +}); \ No newline at end of file diff --git a/js/menu-highlight-fix.js b/js/menu-highlight-fix.js new file mode 100644 index 0000000..ac90248 --- /dev/null +++ b/js/menu-highlight-fix.js @@ -0,0 +1,309 @@ +(function ($) { + // parseUri 1.2.2 + // (c) Steven Levithan <stevenlevithan.com> + // MIT License + // Modified: Added partial URL-decoding support. + + function parseUri (str) { + var o = parseUri.options, + m = o.parser[o.strictMode ? "strict" : "loose"].exec(str), + uri = {}, + i = 14; + + while (i--) uri[o.key[i]] = m[i] || ""; + + uri[o.q.name] = {}; + uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) { + if ($1) { + //Decode percent-encoded query parameters. + if (o.q.name === 'queryKey') { + //A space can be encoded either as "%20" or "+". decodeUriComponent doesn't decode plus signs, + //so we need to do that first. + $2 = $2.replace('+', ' '); + + $1 = decodeURIComponent($1); + $2 = decodeURIComponent($2); + } + uri[o.q.name][$1] = $2; + } + }); + + return uri; + } + + parseUri.options = { + strictMode: false, + key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], + q: { + name: "queryKey", + parser: /(?:^|&)([^&=]*)=?([^&]*)/g + }, + parser: { + strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, + loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ + } + }; + + // --- parseUri ends --- + + var hasRunAtLeastOnce = false; + + function highlightCurrentMenu() { + hasRunAtLeastOnce = true; + + //Find the menu item whose URL best matches the currently open page. + var currentUri = parseUri(location.href); + var bestMatch = { + uri: null, + /** + * @type {JQuery|null} + */ + link: null, + matchingParams: -1, + differentParams: 10000, + isAnchorMatch: false, + isTopMenu: false, + level: 0, + isHighlighted: false + }; + + //Special case: ".../wp-admin/" should match ".../wp-admin/index.php". + if (currentUri.path.match(/\/wp-admin\/$/)) { + currentUri.path = currentUri.path + 'index.php'; + } + + //Special case: if post_type is not specified for edit.php and post-new.php, + //WordPress assumes it is "post". Here we make this explicit. + if ((currentUri.file === 'edit.php') || (currentUri.file === 'post-new.php')) { + if (!currentUri.queryKey.hasOwnProperty('post_type')) { + currentUri.queryKey['post_type'] = 'post'; + } + } + + var adminMenu = $('#adminmenu'); + adminMenu.find('li > a').each(function (index, link) { + var $link = $(link); + + //Skip links that have no href or contain nothing but an "#anchor". Both AME and some + //other plugins (e.g. S2Member 120703) use them as separators. + if (!$link.is('[href]') || ($link.attr('href').substring(0, 1) === '#')) { + return; + } + + var uri = parseUri(link.href); + + //Same as above - use "post" as the default post type. + if ((uri.file === 'edit.php') || (uri.file === 'post-new.php')) { + if (!uri.queryKey.hasOwnProperty('post_type')) { + uri.queryKey['post_type'] = 'post'; + } + } + //TODO: Consider using get_current_screen and the current_screen filter to get post types and taxonomies. + + //Check for a close match - everything but query and #anchor. + var components = ['protocol', 'host', 'port', 'user', 'password', 'path']; + var isCloseMatch = true; + for (var i = 0; (i < components.length) && isCloseMatch; i++) { + isCloseMatch = isCloseMatch && (uri[components[i]] === currentUri[components[i]]); + } + + if (!isCloseMatch) { + return; //Skip to the next link. + } + + //Calculate the number of matching and different query parameters. + var matchingParams = 0, differentParams = 0, param; + for (param in uri.queryKey) { + if (uri.queryKey.hasOwnProperty(param)) { + if (currentUri.queryKey.hasOwnProperty(param)) { + //All parameters that are present in *both* URLs must have the same exact values. + if (uri.queryKey[param] === currentUri.queryKey[param]) { + matchingParams++; + } else { + return; //Skip to the next link. + } + } else { + differentParams++; + } + } + } + for (param in currentUri.queryKey) { + if (currentUri.queryKey.hasOwnProperty(param) && !uri.queryKey.hasOwnProperty(param)) { + differentParams++; + } + } + + var isAnchorMatch = uri.anchor === currentUri.anchor; + var level = $link.parentsUntil(adminMenu, 'li').length; + var isHighlighted = $link.is('.current, .wp-has-current-submenu'); + + //Figure out if the current link is better than the best found so far. + //To do that, we compare them by several criteria (in order of priority): + var comparisons = [ + { + better: (matchingParams > bestMatch.matchingParams), + equal: (matchingParams === bestMatch.matchingParams) + }, + { + better: (differentParams < bestMatch.differentParams), + equal: (differentParams === bestMatch.differentParams) + }, + { + better: (isAnchorMatch && (!bestMatch.isAnchorMatch)), + equal: (isAnchorMatch === bestMatch.isAnchorMatch) + }, + + //When a menu has multiple submenus, the first submenu usually has the same URL + //as the parent menu. We want to highlight this item and not just the parent. + { + better: ((level > bestMatch.level) + //Is this link a child of the current best match? + && (!bestMatch.link || ($link.closest(bestMatch.link.closest('li')).length > 0)) + ), + equal: (level === bestMatch.level) + }, + + //All else being equal, the item highlighted by WP is probably a better match. + { + better: (isHighlighted && !bestMatch.isHighlighted), + equal: (isHighlighted === bestMatch.isHighlighted) + } + ]; + + var isBetterMatch = false, + isEquallyGood = true, + j = 0; + + while (isEquallyGood && !isBetterMatch && (j < comparisons.length)) { + isBetterMatch = comparisons[j].better; + isEquallyGood = comparisons[j].equal; + j++; + } + + if (isBetterMatch) { + bestMatch = { + uri: uri, + link: $link, + matchingParams: matchingParams, + differentParams: differentParams, + isAnchorMatch: isAnchorMatch, + level: level, + isHighlighted: isHighlighted + } + } + }); + + var shouldUpdateStickiness = false; + + function isInViewport($thing) { + if (!$thing || ($thing.length < 1)) { + return false; //A non-existent element is never visible. + } + + var element = $thing.get(0); + if (!element.getBoundingClientRect || !window.innerHeight || !window.innerWidth) { + return true; //The browser doesn't support the necessary APIs, default to true. + } + + var rect = element.getBoundingClientRect(); + return ( + (rect.top < window.innerHeight) + && (rect.bottom > 0) + && (rect.left < window.innerWidth) + && (rect.right > 0) + ); + } + + //Highlight and/or expand the best matching menu. + if (bestMatch.link !== null) { + var bestMatchLink = bestMatch.link; + var linkAndItem = bestMatchLink.add(bestMatchLink.closest('li')); + var allParentMenus = bestMatchLink.parentsUntil(adminMenu, 'li'); + var topLevelParent = allParentMenus.filter('li.menu-top').last(); + //console.log('Best match is: ', bestMatchLink); + + shouldUpdateStickiness = !isInViewport(bestMatchLink); + + var otherHighlightedMenus = $('li.wp-has-current-submenu, li.menu-top.current', '#adminmenu') + .not(allParentMenus) + .not('.ws-ame-has-always-open-submenu'); + + var otherHighlightedSubmenus = adminMenu.find('li.current,a.current').not(linkAndItem); + + var isWrongItemHighlighted = !bestMatchLink.hasClass('current') || (otherHighlightedSubmenus.length > 0); + var isWrongMenuHighlighted = !topLevelParent.is('.wp-has-current-submenu, .current') || + (otherHighlightedMenus.length > 0); + + if (isWrongMenuHighlighted) { + //Account for users who use the Expanded Admin Menus plugin to keep all menus expanded. + var shouldCloseOtherMenus = !$('div.expand-arrow', '#adminmenu').get(0); + if (shouldCloseOtherMenus) { + otherHighlightedMenus + .add('> a', otherHighlightedMenus) + .removeClass('wp-menu-open wp-has-current-submenu current') + .addClass('wp-not-current-submenu'); + } + + var parentMenuAndLink = topLevelParent.add('> a.menu-top', topLevelParent.get(0)); + parentMenuAndLink.removeClass('wp-not-current-submenu'); + if (topLevelParent.hasClass('wp-has-submenu')) { + parentMenuAndLink.addClass('wp-has-current-submenu wp-menu-open'); + } + + shouldUpdateStickiness = true; + + //Workaround: Prevent the current submenu from "jumping around" when you click an item. This glitch is + //caused by a `focusin` event handler in common.js. WP adds this handler to all top level menus that + //are not the current menu. Since we're changing the current menu, we need to also remove this handler. + if (typeof topLevelParent['off'] === 'function') { + topLevelParent.off('focusin.adminmenu'); + } + } + + if (isWrongItemHighlighted) { + adminMenu.find('.current').removeClass('current'); + linkAndItem.addClass('current'); + shouldUpdateStickiness = true; + } + + //WordPress adds the "current" class only to the <li> that is the direct parent of the highlighted link. + //If the highlighted item is a submenu, its top-level parent shouldn't have this class. + bestMatchLink.closest('li').parentsUntil(adminMenu, 'li').children('a').addBack().removeClass('current'); + + bestMatchLink.closest('ul').parentsUntil(adminMenu, 'li').addClass('ame-has-highlighted-item'); + } + + //If a submenu is highlighted, so must be its parent. + //In some cases, if we decide to stick with the WP-selected highlighted menu, + //this might not be the case and we'll need to fix it. + var parentOfHighlightedMenu = $('.wp-submenu a.current', '#adminmenu').closest('.menu-top').first(); + parentOfHighlightedMenu + .add('> a.menu-top', parentOfHighlightedMenu) + .removeClass('wp-not-current-submenu') + .addClass('wp-has-current-submenu wp-menu-open'); + + if (shouldUpdateStickiness) { + //Note: WordPress switches the admin menu between `position: fixed` and `position: relative` depending on + //how tall it is compared to the browser window. Opening a different submenu can change the menu's height, + //so we must trigger the position update to avoid bugs. If we don't, we can end up with a very tall menu + //that's not scrollable (due to being stuck with `position: fixed`). + if ((typeof window['stickyMenu'] === 'object') && (typeof window['stickyMenu']['update'] === 'function')) { + window.stickyMenu.update(); + } else { + //As of WP core revision 29599 (2014-10-05) the `stickyMenu` object no longer exists + //and the replacement (`setPinMenu` in common.js) is not accessible from an outside scope. + //We'll resort to faking a resize event to make WP update the menu height and state. + $(document).trigger('wp-window-resized'); + } + } + } + + //Other scripts can trigger this feature by using a custom event. + $(document).on('adminMenuEditor:highlightCurrentMenu', highlightCurrentMenu); + + $(function () { + if (!hasRunAtLeastOnce) { + highlightCurrentMenu(); + } + }); +})(jQuery); \ No newline at end of file diff --git a/js/tab-utils.js b/js/tab-utils.js new file mode 100644 index 0000000..d050386 --- /dev/null +++ b/js/tab-utils.js @@ -0,0 +1,64 @@ +jQuery(function ($) { + var menuEditorHeading = $('#ws_ame_editor_heading').first(); + var pageWrapper = menuEditorHeading.closest('.wrap'); + var tabList = pageWrapper.find('.nav-tab-wrapper').first(); + + //On AME pages, move settings tabs after the heading. This is necessary to make them appear on the right side, + //and WordPress breaks that by moving notices like "Settings saved" after the first H1 (see common.js). + var menuEditorTabs = tabList.add(tabList.next('.clear')); + if ((menuEditorHeading.length > 0) && (menuEditorTabs.length > 0)) { + menuEditorTabs.insertAfter(menuEditorHeading); + } + + //Switch tab styles when there are too many tabs and they don't fit on one row. + var $firstTab = null, + $lastTab = null, + knownTabWrapThreshold = -1; + + function updateTabStyles() { + if (($firstTab === null) || ($lastTab === null)) { + var $tabItems = tabList.children('.nav-tab'); + $firstTab = $tabItems.first(); + $lastTab = $tabItems.last(); + } + + //To detect if any tabs are wrapped to the next row, check if the top of the last tab + //is below the bottom of the first tab. + var firstPosition = $firstTab.position(); + var lastPosition = $lastTab.position(); + var windowWidth = $(window).width(); + //Sanity check. + if ( + !firstPosition || !lastPosition || !windowWidth + || (typeof firstPosition['top'] === 'undefined') + || (typeof lastPosition['top'] === 'undefined') + ) { + return; + } + var firstTabBottom = firstPosition.top + $firstTab.outerHeight(); + var areTabsWrapped = (lastPosition.top >= firstTabBottom); + + //Tab positions may change when we apply different styles, which could lead to the tab bar + //rapidly cycling between one and two two rows when the browser width is just right. + //To prevent that, remember what the width was when we detected wrapping, and always apply + //the alternative styles if the width is lower than that. + var wouldWrapByDefault = (windowWidth <= knownTabWrapThreshold); + + var tooManyTabs = areTabsWrapped || wouldWrapByDefault; + if (tooManyTabs && (windowWidth > knownTabWrapThreshold)) { + knownTabWrapThreshold = windowWidth; + } + + pageWrapper.toggleClass('ws-ame-too-many-tabs', tooManyTabs); + } + + updateTabStyles(); + + $(window).on('resize', wsAmeLodash.debounce( + function () { + updateTabStyles(); + }, + 300 + )); +}); + diff --git a/license-manager/BasicPluginLicensingUi.php b/license-manager/BasicPluginLicensingUi.php new file mode 100644 index 0000000..a41e26c --- /dev/null +++ b/license-manager/BasicPluginLicensingUi.php @@ -0,0 +1,774 @@ +<?php + +class Wslm_BasicPluginLicensingUI { + const AUTO_LICENSE_CRON_ACTION = 'wslm_auto_activate_license-'; + const ACTIVATION_FAILURE_FLAG = 'wslm_auto_activation_failed-'; + + private $licenseManager; + /** + * @var Puc_v4p10_Plugin_UpdateChecker + */ + private $updateChecker; + private $pluginFile; + private $slug; + private $requiredCapability = 'update_plugins'; + + private $triedLicenseKey = null; + /** + * @var Wslm_ProductLicense + */ + private $triedLicense = null; + private $currentTab = 'current-license'; + private $tabs = array(); + + private $keyConstant = null; + private $maxAutoActivationAttempts = 8; + + /** @var bool Whether to display the site token (if any) in the licensing window. */ + protected $tokenDisplayEnabled = false; + + public function __construct(Wslm_LicenseManagerClient $licenseManager, $pluginFile, $updateChecker = null, $keyConstant = null) { + $this->licenseManager = $licenseManager; + $this->pluginFile = $pluginFile; + $this->slug = $this->licenseManager->getProductSlug(); + $this->keyConstant = $keyConstant; + $this->updateChecker = $updateChecker; + + $this->tabs = array( + 'current-license' => array( + 'caption' => 'Current License', + 'callback' => array($this, 'tabCurrentLicense'), + ), + 'manage-sites' => array( + 'caption' => 'Manage Sites', + 'callback' => array($this, 'tabManageSites'), + ), + ); + + //Turning on the DISALLOW_FILE_MODS constant disables the "update_plugins" capability, + //so we need to use something else in that case. + if ( defined('DISALLOW_FILE_MODS') && constant('DISALLOW_FILE_MODS') ) { + if ( is_multisite() ) { + $this->requiredCapability = 'manage_network_plugins'; + } else { + $this->requiredCapability = 'activate_plugins'; + } + } + + $basename = plugin_basename($this->pluginFile); + add_filter( + 'plugin_action_links_' . $basename, + array($this, 'addLicenseActionLink') + ); + add_filter( + 'network_admin_plugin_action_links_' . $basename, + array($this, 'addLicenseActionLink') + ); + + add_action('wp_ajax_' . $this->getAjaxActionName(), array($this, 'printUi')); + + add_action('after_plugin_row_' . $basename, array($this, 'printPluginRowNotice'), 10, 0); + + if ( isset($this->updateChecker) ) { + add_filter('upgrader_pre_download', array($this, 'authorizePluginUpdate'), 10, 3); + } + + add_action('all_admin_notices', array($this, 'autoActivateLicense')); + add_action(self::AUTO_LICENSE_CRON_ACTION . $this->slug, array($this, 'autoActivateLicense')); + add_action('wslm_license_activated-' . $this->slug, array($this, 'clearActivationFailureFlag'), 10, 0); + } + + public function addLicenseActionLink($links) { + if ( $this->currentUserCanManageLicense() ) { + $links['licenses'] = $this->makeLicenseLink(); + } + return $links; + } + + private function currentUserCanManageLicense() { + return apply_filters( + 'wslm_current_user_can_manage_license-' . $this->slug, + current_user_can($this->requiredCapability) + ); + } + + private function makeLicenseLink($linkText = 'License') { + return sprintf( + '<a href="%s" class="thickbox" title="%s">%s</a>', + esc_attr(add_query_arg( + array( 'TB_iframe' => true, ), + $this->getLicensingPageUrl() + )), + esc_attr($this->getPageTitle()), + apply_filters('wslm_action_link_text-' . $this->slug, $linkText) + ); + } + + private function getLicensingPageUrl() { + $url = add_query_arg( + array( + 'action' => $this->getAjaxActionName(), + '_wpnonce' => wp_create_nonce('show_license'), //Assumes the default license action = "show_license". + ), + admin_url('admin-ajax.php') + ); + return $url; + } + + private function getAjaxActionName() { + return 'show_license_ui-' . $this->slug; + } + + private function getPageTitle() { + return apply_filters('wslm_license_ui_title-' . $this->slug, 'Manage Licenses'); + } + + public function printUi() { + if ( !$this->currentUserCanManageLicense() ) { + wp_die("You don't have sufficient permissions to manage licenses for this product."); + } + + $action = isset($_REQUEST['license_action']) ? strval($_REQUEST['license_action']) : ''; + if ( empty($action) ) { + $action = 'show_license'; + } + check_admin_referer($action); + + $this->triedLicenseKey = isset($_POST['license_key']) ? trim(strval($_POST['license_key'])) : $this->licenseManager->getLicenseKey(); + + if ( isset($_REQUEST['tab']) && is_string($_REQUEST['tab']) && array_key_exists($_REQUEST['tab'], $this->tabs) ) { + $this->currentTab = $_REQUEST['tab']; + } + + //We run some core hooks later to load admin CSS and other dependencies. Some plugins that + //use those hooks will crash if they encounter a "fake" admin page without a screen object, + //causing the license page to be blank. Lets set up a screen to avoid that. + set_current_screen('wslm-' . $this->slug . '-licensing_ui'); + + $this->printHeader(); + $this->dispatchAction($action); + $this->printLogo(); + $this->printTabList(); + ?> + <div class="wrap" id="wslm-section-holder"> + <?php + foreach($this->tabs as $id => $tab) { + printf( + '<div id="section-%1$s" class="wslm-section%2$s">', + esc_attr($id), + ($this->currentTab !== $id) ? ' hidden' : '' + ); + call_user_func($tab['callback']); + echo '</div>'; + } + ?> + </div> <!-- #wslm-section-holder --> + <?php + + exit(); + } + + private function dispatchAction($action) { + do_action('wslm_ui_action-' . $action . '-' . $this->slug); + $method = 'action' . str_replace(' ', '', ucwords(str_replace('_', ' ', $action))); + if ( method_exists($this, $method) ) { + $this->$method(); + } else { + $this->printNotice( + sprintf('Unknown action "%s"', htmlentities($action)), + 'error' + ); + } + } + + /** @noinspection PhpUnusedPrivateMethodInspection Used by dispatchAction(). */ + private function actionShowLicense() { + //Don't need to do anything special in this case, I think. + //Maybe request the site list if we have a license key. + $this->licenseManager->checkForLicenseUpdates(); + $this->triedLicenseKey = $this->licenseManager->getLicenseKey(); + $this->triedLicense = $this->licenseManager->getLicense(); + } + + /** @noinspection PhpUnusedPrivateMethodInspection Used by dispatchAction(). */ + private function actionLicenseThisSite() { + if ( empty($this->triedLicenseKey) ) { + $this->printNotice('The license key must not be empty.', 'error'); + return; + } + $result = $this->licenseManager->licenseThisSite($this->triedLicenseKey); + if ( is_wp_error($result) ) { + $this->printError($result); + } else { + $this->printNotice('Success! This site is now licensed.'); + + //Print any notices or warnings, like "you can't receive updates, please renew". + if ( isset($result['notice']) ) { + $this->printNotice($result['notice']['message'], $result['notice']['class']); + } + } + } + + /** @noinspection PhpUnusedPrivateMethodInspection Used by dispatchAction(). */ + private function actionUnlicenseThisSite() { + $result = $this->licenseManager->unlicenseThisSite(); + if ( is_wp_error($result) ) { + $this->printError($result); + } else { + $this->printNotice('Success! The existing license has been removed from this site.'); + } + } + + /** @noinspection PhpUnusedPrivateMethodInspection Used by dispatchAction(). */ + private function actionUnlicenseOtherSite() { + $this->currentTab = 'manage-sites'; + + $siteUrl = isset($_POST['site_url']) ? strval($_POST['site_url']) : ''; + if ( empty($siteUrl) || empty($this->triedLicenseKey) ) { + $this->printNotice('Please specify both the site URL and license key.', 'error'); + return; + } + + $result = $this->licenseManager->unlicenseSite($siteUrl, $this->triedLicenseKey); + if ( is_wp_error($result) ) { + $this->printError($result); + $this->triedLicense = $result->get_error_data('license'); + } else { + $this->printNotice( + 'Success! This license key is no longer associated with ' . htmlentities($siteUrl) + ); + $this->triedLicense = $result; + } + } + + /** @noinspection PhpUnusedPrivateMethodInspection Used by dispatchAction(). */ + private function actionShowLicensedSites() { + $this->currentTab = 'manage-sites'; + if ( empty($this->triedLicenseKey) ) { + $this->printNotice('License key must not be empty.', 'error'); + return; + } + + $result = $this->licenseManager->requestLicenseDetails($this->triedLicenseKey); + if ( is_wp_error($result) ) { + $this->printError($result); + } else { + $this->triedLicense = $result; + } + } + + /** @noinspection PhpUnusedPrivateMethodInspection */ + private function tabCurrentLicense() { + //Display license information + $currentLicense = $this->licenseManager->getLicense(); + echo '<h3>Current License</h3>'; + + if ( $currentLicense->isValid() ) { + if ( !$currentLicense->canReceiveProductUpdates() ) { + $this->printLicenseDetails( + 'Valid License (updates disabled)', + 'This site is currently licensed. However, your access to updates and support has expired. + Please consider renewing your license.', + $currentLicense + ); + } else { + $this->printLicenseDetails( + 'Valid License', + 'This site is currently licensed and qualifies for automatic upgrades & support for this product. + If you no longer wish to use this product on this site you can remove the license.', + $currentLicense + ); + } + + ?> + <form method="post" action="<?php echo esc_attr($this->getLicensingPageUrl()); ?>"> + <input type="hidden" name="license_action" value="unlicense_this_site" /> + <?php wp_nonce_field('unlicense_this_site'); ?> + <?php submit_button('Remove License', 'secondary', 'submit', false); ?> + </form> + <?php + $this->printLicenseKeyForm( + 'Change License Key', + 'Want to use a different license key? Enter it below.', + 'Change Key', + 'secondary' + ); + } else { + if ( $currentLicense->getStatus() === 'no_license_yet' ) { + $this->printLicenseDetails( + 'No License Yet', + 'This site is currently not licensed. Please enter your license key below.' + ); + $this->printLicenseKeyForm(); + } else { + $this->printLicenseDetails( + 'Invalid license (' . htmlentities($currentLicense->getStatus()) . ')', + 'The current license is not valid. Please enter a valid license key below.', + $currentLicense + ); + $this->printLicenseKeyForm(); + } + } + } + + /** + * @param string $status + * @param string $message + * @param Wslm_ProductLicense $currentLicense + */ + private function printLicenseDetails($status, $message = '', $currentLicense = null) { + $currentKey = $this->licenseManager->getLicenseKey(); + $currentToken = $this->licenseManager->getSiteToken(); + ?> + <p> + <span class="license-status"> + <label>Status:</label> <?php echo $status; ?> + </span> + </p> + + <?php + if ( !empty($currentKey) ) { + ?><p><label>License key:</label> <?php echo htmlentities($currentKey); ?></p><?php + } + if ( !empty($currentToken) && $this->tokenDisplayEnabled ) { + ?><p><label>Site token:</label> <?php echo htmlentities($currentToken); ?></p><?php + } + + $expiresOn = isset($currentLicense) ? $currentLicense->get('expires_on') : null; + if ( $expiresOn ) { + $formattedDate = date_i18n(get_option('date_format'), strtotime($expiresOn)); + ?><p> + <label>Expires:</label> + <span title="<?php echo esc_attr($expiresOn); ?>"><?php echo $formattedDate ?></span> + </p> + <?php + } + + do_action('wslm_license_ui_details-' . $this->slug, $currentKey, $currentToken, $currentLicense); + + if ( !empty($message) ) { + echo '<p>', $message, '</p>'; + } + } + + /** @noinspection PhpUnusedPrivateMethodInspection */ + private function tabManageSites() { + if ( isset($this->triedLicense, $this->triedLicense->sites) ) { + + ?> + <h3>Sites Associated With License Key "<?php echo htmlentities($this->triedLicenseKey); ?>"</h3> + <?php + if ( !empty($this->triedLicense->sites) ): + ?> + <table class="widefat"> + <?php foreach($this->triedLicense->sites as $site): ?> + <tr> + <td> + <?php echo htmlentities($site->site_url); ?><br> + Token: <?php echo htmlentities($site->token); ?> + </td> + <td style="vertical-align: middle; width: 11em;"> + <form method="post" action="<?php echo esc_attr($this->getLicensingPageUrl()); ?>"> + <input type="hidden" name="site_url" value="<?php echo esc_attr($site->site_url); ?>" /> + <input type="hidden" name="license_key" value="<?php echo esc_attr($this->triedLicenseKey); ?>" /> + <input type="hidden" name="license_action" value="unlicense_other_site" /> + <?php wp_nonce_field('unlicense_other_site'); ?> + <?php submit_button('Remove License', 'secondary', 'submit', false); ?> + </form> + </td> + </tr> + <?php endforeach; ?> + </table> + <?php + else: + ?> + There are currently no sites using this license key. + <?php + endif; + + } else { + $this->printLicenseKeyForm( + '', + 'To view sites currently associated with a license, enter your license key below.', + 'Show Licensed Sites', + 'primary', + 'show_licensed_sites' + ); + } + } + + private function printLicenseKeyForm( + $formCaption = 'Enter a License Key', + $formDescription = '', + $buttonTitle = 'Activate Key', + $buttonType = 'primary', + $licenseAction = 'license_this_site' + ) { + ?> + <h3><?php echo $formCaption; ?></h3> + <?php + if ( !empty($formDescription) ) { + echo '<p>', $formDescription, '</p>'; + } + ?> + <form method="post" action="<?php echo esc_attr($this->getLicensingPageUrl()); ?>"> + <input type="hidden" name="license_action" value="<?php echo esc_attr($licenseAction); ?>" /> + <?php wp_nonce_field($licenseAction); ?> + <!--suppress HtmlFormInputWithoutLabel --> + <input type="text" name="license_key" size="36" /> + <?php submit_button($buttonTitle, $buttonType, 'submit', false); ?> + </form> + <?php + } + + private function printError(WP_Error $error) { + foreach ($error->get_error_codes() as $code) { + foreach ($error->get_error_messages($code) as $message) { + if ( !empty($message) ) { + $this->printNotice( + $message . "\n<br>Error code: <code>" . htmlentities($code) . '</code>', + 'error' + ); + } + } + } + } + + private function printNotice($message, $class = 'updated') { + printf('<div class="notice %s"><p>%s</p></div>', esc_attr($class), $message); + } + + private function printHeader() { + ?> + <!DOCTYPE html> + <!--[if IE 8]> + <html xmlns="http://www.w3.org/1999/xhtml" class="ie8" <?php do_action('admin_xml_ns'); ?> <?php language_attributes(); ?>> + <![endif]--> + <!--[if !(IE 8) ]><!--> + <html xmlns="http://www.w3.org/1999/xhtml" <?php do_action('admin_xml_ns'); ?> <?php language_attributes(); ?>> + <!--<![endif]--> + <head> + <meta http-equiv="Content-Type" content="<?php + bloginfo('html_type'); + echo '; charset', '=' , get_option('blog_charset'); + ?>" /> + <title><?php echo esc_html($this->getPageTitle()); ?> + + + + + '; + do_action('wslm_license_ui_logo-' . $this->slug); + echo ''; + } + + private function printTabList() { + ?> +
+
    + tabs as $name => $tab) { + printf( + '
  • %s
  • ', + esc_attr($name), + esc_attr(add_query_arg('tab', $name, $baseTabUrl)), + ($name === $this->currentTab) ? ' class="current"' : '', + $tab['caption'] + ); + } + ?> +
+
+ + + licenseManager->getLicense(); + if ( !$this->currentUserCanManageLicense() || ($license->getStatus() === 'valid') ) { + return; + } + + $renewalUrl = $license->get('renewal_url'); + return trrue; + + $messages = array( + 'no_license_yet' => "License is not set yet. Please enter your license key to enable automatic updates.", + 'expired' => sprintf( + 'Your access to updates has expired. You can continue using the plugin, but you\'ll need to %1$srenew your license%2$s to receive updates and bug fixes.', + $renewalUrl ? '' : '', + $renewalUrl ? '' : '' + ), + 'not_found' => 'The current license key or site token is invalid.', + 'wrong_site' => 'Please re-enter your license key. This is necessary because the site URL has changed.', + ); + $status = $license->getStatus(); + $notice = isset($messages[$status]) ? $messages[$status] : 'The current license is invalid.'; + + $licenseLink = $this->makeLicenseLink(apply_filters( + 'wslm_plugin_row_link_text-' . $this->slug, + 'Enter License Key' + )); + $showLicenseLink = ($status !== 'expired'); + + //WP 4.6+ uses different styles for the update row. We use an inverted condition here because some buggy + //plugins overwrite $wp_version. This way the default is to assume it's WP 4.6 or higher. + $isWP46orHigher = !(isset($GLOBALS['wp_version']) && version_compare($GLOBALS['wp_version'], '4.5.9', '<=')); + + $messageClasses = array('update-message'); + if ( $isWP46orHigher ) { + $messageClasses = array_merge($messageClasses, array('notice', 'inline', 'notice-warning', 'notice-alt')); + } + + ?> + + +
+ '; + } + + if ( $showLicenseLink ) { + echo $licenseLink, ' | '; + } + echo $notice; + + if ( $isWP46orHigher ) { + echo '

'; + } + ?> +
+ + + skin) ) { + return $result; + } + + $license = $this->licenseManager->getLicense(); + if ( $license->canReceiveProductUpdates() || !$this->updateChecker->isPluginBeingUpgraded($upgrader) ) { + return $result; + } + + if ( $license->getStatus() === 'expired' ) { + //Reload the license in case the user just renewed and is retrying the update. + $this->licenseManager->checkForLicenseUpdates(); + $license = $this->licenseManager->getLicense(); + if ( $license->canReceiveProductUpdates() ) { + return $result; + } + } + + $status = $license->getStatus(); + $messages = array( + 'no_license_yet' => "Please enter your license key to enable plugin updates.", + 'expired' => sprintf( + 'Your access to %s updates has expired. Please renew your license.', + apply_filters('wslm_product_name-' . $this->slug, $this->slug) + ) + ); + + $result = new WP_Error( + 'wslm_update_not_available', + isset($messages[$status]) ? $messages[$status] : 'Update not available. Please (re)enter your license key.', + '[' . $status . ']' + ); + + //This bit is important. At least in WP 4.3, the return value will be lost or replaced with a generic + //"download failed" error unless you also set it on the upgrader skin. + $upgrader->skin->set_result($result); + + return $result; + } + + public function autoActivateLicense() { + $doingCron = defined('DOING_CRON') && DOING_CRON; + if ( !$this->currentUserCanManageLicense() && !$doingCron ) { + return; + } + + $license = $this->licenseManager->getLicense(); + if ( $license->isValid() ) { + return; + } + + $failureFlag = self::ACTIVATION_FAILURE_FLAG . $this->slug; + $state = get_site_option($failureFlag, null); + if ( !is_array($state) || !isset($state['failures']) ) { + $state = array( + 'failures' => $state ? 1 : 0, + 'lastAttemptTime' => 0, + ); + } + + if ( ($state['failures'] > $this->maxAutoActivationAttempts) ) { + return; + } + + $elapsedTime = time() - $state['lastAttemptTime']; + $desiredDelay = $this->calculateLicenseActivationDelay($state['failures']); + if ( $elapsedTime < $desiredDelay ) { + return; + } + + $state['failures']++; + $state['lastAttemptTime'] = time(); + update_site_option($failureFlag, $state); + + $result = null; + $tokenHistory = $this->licenseManager->getTokenHistory(); + if ( !empty($this->keyConstant) && defined($this->keyConstant) ) { + //Attempt to activate the license key that's defined in wp-config.php. + $result = $this->licenseManager->licenseThisSite(constant($this->keyConstant)); + } else if ( !empty($tokenHistory) ) { + //Check if there's a known token that matches the current site URL. Try to activate that token. + $possibleToken = array_search($this->licenseManager->getSiteUrl(), array_reverse($tokenHistory, true)); + if ( !empty($possibleToken) ) { + $result = $this->licenseManager->licenseThisSiteByToken($possibleToken); + } + } + + if ( is_wp_error($result) ) { + $productName = apply_filters('wslm_product_name-' . $this->slug, $this->slug); + if ( is_admin() && !$doingCron ) { + printf( + '
+

+ %1$s tried to automatically activate your license, but it didn\'t work.
+ Error: %2$s [%3$s] +

+

Please go to the Plugins page and enter your license key.

+
', + $productName, + $result->get_error_message(), + $result->get_error_code(), + is_multisite() ? network_admin_url('plugins.php') : admin_url('plugins.php') + ); + } + + $plainError = sprintf( + '%1$s failed to automatically activate a license. Error: %2$s [%3$s]', + $productName, + $result->get_error_message(), + $result->get_error_code() + ); + error_log($plainError); + + //Try again later, but only if we have a license key. + if ( + ($state['failures'] < $this->maxAutoActivationAttempts) + && !empty($this->keyConstant) + && defined($this->keyConstant) + ) { + wp_schedule_single_event( + time() + $this->calculateLicenseActivationDelay($state['failures']) + 1, + self::AUTO_LICENSE_CRON_ACTION . $this->slug, + array($state['failures'] + 1) + ); + } + } else if ( $result instanceof Wslm_ProductLicense ) { + //Success! Don't output anything, just proceed as normal. + $this->clearActivationFailureFlag(); + } + } + + /** + * Calculate the minimum delay after the Nth automatic license + * activation attempt. + * + * @param int $attempt + * @return float|int Delay in seconds. + */ + private function calculateLicenseActivationDelay($attempt) { + if ( $attempt < 1 ) { + return 0; + } + + $minDelay = 20; + $maxDelay = 600; + $growthLimit = 4; //Stop increasing the delay after this many attempts. + + $fraction = (min($attempt - 1, $growthLimit) / ($growthLimit)); + + $desiredDelay = $minDelay + round(($maxDelay - $minDelay) * $fraction); + if ( $desiredDelay < $minDelay ) { + $desiredDelay = $minDelay; + } + return $desiredDelay; + } + + public function clearActivationFailureFlag() { + delete_site_option(self::ACTIVATION_FAILURE_FLAG . $this->slug); + } +} diff --git a/license-manager/Database.php b/license-manager/Database.php new file mode 100644 index 0000000..77a915f --- /dev/null +++ b/license-manager/Database.php @@ -0,0 +1,32 @@ +getResults($query, $parameters); + if ( !empty($results) ) { + return reset($results); + } else { + return null; + } + } +} diff --git a/license-manager/LicenseManager.php b/license-manager/LicenseManager.php new file mode 100644 index 0000000..56cbeb8 --- /dev/null +++ b/license-manager/LicenseManager.php @@ -0,0 +1,657 @@ + null, + 'api_provider' => null, + 'product_slug' => null, + 'option_name' => null, + 'license_scope' => self::LICENSE_SCOPE_SITE, + 'check_period' => null, + 'store_license_key' => false, + 'update_checker' => null, + 'token_history_size' => 0, + ); + $args = array_merge($defaults, $args); + + if ( isset($args['api_provider']) ) { + $this->api = $args['api_provider']; + } else { + $this->api = new Wslm_LicenseManagerApi($args['api_url']); + } + + $this->productSlug = $args['product_slug']; + $this->optionName = $args['option_name']; + $this->licenseScope = $args['license_scope']; + $this->tokenHistorySize = $args['token_history_size']; + + if ($args['update_checker'] !== null) { + $this->updateChecker = $args['update_checker']; + } + + if ( $this->updateChecker !== null ) { + if ( $this->productSlug === null ) { + $this->productSlug = $this->updateChecker->slug; + } + + $this->updateChecker->addResultFilter(array($this, 'refreshLicenseFromPluginInfo')); + $this->addUpdateFiltersTo($this->updateChecker); + } + + if ( empty($this->optionName) ) { + $this->optionName = 'wsh_license_manager-' . $this->productSlug; + } + + //Set up the periodic update checks + $this->cronHook = 'check_license_updates-' . $this->getProductSlug(); + $this->checkPeriod = $args['check_period']; + if ( $this->checkPeriod > 0 && $this->shouldCheckForUpdates() ){ + //Trigger the check via Cron + add_filter('cron_schedules', array($this, 'addCustomSchedule')); + if ( !wp_next_scheduled($this->cronHook) && !defined('WP_INSTALLING') ) { + $scheduleName = 'every' . $this->checkPeriod . 'hours'; + wp_schedule_event(time(), $scheduleName, $this->cronHook); + } + add_action($this->cronHook, array($this, 'checkForLicenseUpdates')); + } else { + //Periodic checks are disabled. + wp_clear_scheduled_hook($this->cronHook); + } + } + + protected function load() { + if ( $this->licenseScope === self::LICENSE_SCOPE_NETWORK ) { + $options = get_site_option($this->optionName, array()); + } else { + $options = get_option($this->optionName, array()); + } + + $this->licenseKey = isset($options['license_key']) ? $options['license_key'] : null; + $this->siteToken = isset($options['site_token']) ? $options['site_token'] : null; + $this->license = null; + if ( isset($options['license']) ) { + $this->license = $this->createLicenseObject($options['license']); + } + + if ( $this->tokenHistorySize > 0 ) { + $this->tokenHistory = isset($options['token_history']) ? $options['token_history'] : array(); + } else { + $this->tokenHistory = null; + } + } + + protected function lazyLoad() { + static $isLoaded = false; + if ( !$isLoaded ) { + $this->load(); + $isLoaded = true; + } + } + + protected function save() { + $licenseData = null; + if ( !empty($this->license) ) { + $licenseData = $this->license->getData(); + unset($licenseData['sites']); + unset($licenseData['notice']); + if ( !$this->storeLicenseKey ) { + unset($licenseData['license_key']); + } + } + + $options = array( + 'license_key' => $this->storeLicenseKey ? $this->licenseKey : null, + 'site_token' => $this->siteToken, + 'license' => $licenseData, + ); + + if ( ($this->tokenHistorySize > 0) && !empty($this->tokenHistory) ) { + $options['token_history'] = array_slice($this->tokenHistory, -$this->tokenHistorySize); + } + + if ( $this->licenseScope === self::LICENSE_SCOPE_NETWORK ) { + update_site_option($this->optionName, $options); + } else { + update_option($this->optionName, $options); + } + } + + protected function resetState() { + $this->license = null; + $this->licenseKey = null; + $this->siteToken = null; + $this->tokenHistory = null; + wp_clear_scheduled_hook($this->cronHook); + + if ( $this->licenseScope === self::LICENSE_SCOPE_NETWORK ) { + delete_site_option($this->optionName); + } else { + delete_option($this->optionName); + } + + //We no longer have a license, so maybe we no longer have access to updates. + //Calling resetUpdateState() will ensure any cached updates are discarded. + if ( $this->updateChecker !== null ) { + $this->updateChecker->resetUpdateState(); + } + } + + /** + * @return Wslm_ProductLicense + */ + public function getLicense() { + $this->lazyLoad(); + if ( $this->license === null ) { + return $this->createLicenseObject(array( + 'status' => 'no_license_yet', + 'is_virtual' => true, + )); + } + return $this->license; + } + + public function hasExistingLicense() { + $this->lazyLoad(); + return ($this->getSiteToken() !== null) + && ($this->license !== null) + && $this->license->isExisting(); + } + + public function checkForLicenseUpdates() { + $this->lazyLoad(); + if ( $this->shouldCheckForUpdates() ) { + $result = $this->requestLicenseDetails(); + + if ( !is_wp_error($result) ) { + $this->license = $result; + } else if ( in_array($result->get_error_code(), array('not_found', 'wrong_site')) ) { + //If the key or token is definitely not valid, we have an invalid license. + //If some other, unexpected error occurs, we can't say with confidence whether + //the license is OK or not, so then we just return that error. + //Note: We could use $apiResponse->httpCode == 404 here. More consistent than error code strings. + $this->license = $this->createLicenseObject(array( + 'status' => $result->get_error_code(), + 'error' => array( + 'code' => $result->get_error_code(), + 'message' => $result->get_error_message(), + ), + 'is_virtual' => true, + )); + } else { + return $result; + } + + $this->save(); + return $this->license; + } else { + return new WP_Error( + 'no_license_set', + "Can't check for license updates because this site doesn't have a license yet." + ); + } + } + + protected function shouldCheckForUpdates() { + return $this->getLicenseKey() !== null || $this->getSiteToken() !== null; + } + + /** + * @param string $licenseKey + * @return Wslm_ProductLicense|WP_Error + */ + public function requestLicenseDetails($licenseKey = null) { + //Try to download license details. + if ( isset($licenseKey) ) { + $result = $this->api->getLicense($this->productSlug, $licenseKey, $this->getSiteUrl()); + } else { + if ( $this->getLicenseKey() !== null ) { + $result = $this->api->getLicense($this->productSlug, $this->getLicenseKey(), $this->getSiteUrl()); + } else { + $result = $this->api->getLicenseByToken($this->productSlug, $this->getSiteToken(), $this->getSiteUrl()); + } + } + + if ( $result->success() ) { + return $this->createLicenseObject($result->response->license); + } else { + return $result->asWpError(); + } + } + + public function createLicenseObject($licenseData = null) { + $license = apply_filters('wslm_create_license_object-' . $this->productSlug, null, $licenseData); + if ( $license === null ) { + $license = new Wslm_ProductLicense($licenseData); + } + return $license; + } + + /** + * Activate a license on the current site. + * + * @param string $licenseKey + * @return Wslm_ProductLicense|WP_Error + */ + public function licenseThisSite($licenseKey) { + $result = $this->api->licenseSite($this->productSlug, $licenseKey, $this->getSiteUrl()); + return $this->processActivationResponse($result, $licenseKey); + } + + /** + * Associate an already-activated, site-specific license with the current site. + * + * This is for situations where you have already obtained a site token somehow and just need + * the license manager to verify it and refresh license details. + * + * @param string $siteToken + * @return WP_Error|Wslm_ProductLicense + */ + public function licenseThisSiteByToken($siteToken) { + $result = $this->api->getLicenseByToken($this->productSlug, $siteToken, $this->getSiteUrl()); + return $this->processActivationResponse($result, null, $siteToken); + } + + /** + * @param Wslm_LicenseManagerApiResponse $result + * @param string|null $licenseKey + * @param string|null $siteToken + * @return Wslm_ProductLicense|WP_Error + */ + private function processActivationResponse($result, $licenseKey = null, $siteToken = null) { + $this->lazyLoad(); + if ( $result->success() ) { + //Success! Lets save our license data. + $this->license = $this->createLicenseObject($result->response->license); + $this->siteToken = isset($result->response->site_token) ? $result->response->site_token : $siteToken; + $this->licenseKey = $this->storeLicenseKey ? $licenseKey : null; + + if ( !isset($this->license['site_url']) && !empty($this->siteToken) ) { + $this->license['site_url'] = $this->getSiteUrl(); + } + + if ( ($this->tokenHistorySize > 0) && !empty($this->siteToken) ) { + //Add this token+site combination to the bottom of the list. + $this->tokenHistory = isset($this->tokenHistory) ? $this->tokenHistory : array(); + unset($this->tokenHistory[$this->siteToken]); + $this->tokenHistory[$this->siteToken] = $this->getSiteUrl(); + } + + if ( isset($result->response->notice) ) { + //Stick the notice in the license object for lack of a better place. + $this->license['notice'] = array( + 'message' => $result->response->notice->message, + 'class' => isset($result->response->notice->class) ? $result->response->notice->class : 'notice-info' + ); + } + + do_action('wslm_license_activated-' . $this->productSlug, $this->license); + + $this->save(); + + //Now that we have a valid license, an update might be available. Clear the cache. + if ( $this->updateChecker !== null ) { + $this->updateChecker->resetUpdateState(); + } + + return $this->license; + } else { + $error = $result->asWpError(); + if ( isset($result->response->license) ) { + $error->add_data($this->createLicenseObject($result->response->license), 'license'); + } + return $error; + } + } + + public function unlicenseThisSite() { + if ( $this->hasExistingLicense() ) { + $result = $this->unlicenseSite($this->getSiteUrl(), $this->getLicenseKey(), $this->getSiteToken()); + if ( is_wp_error($result) && ($result->get_error_code() === 'api_request_failed') ) { + return $result; + } + } + + $this->resetState(); + return true; + } + + /** + * Unlicense a site. + * + * @param string $siteUrl + * @param string $licenseKey + * @param string $token + * @return Wslm_ProductLicense|WP_Error|null + */ + public function unlicenseSite($siteUrl, $licenseKey, $token = null) { + if ( !empty($licenseKey) ) { + $apiResponse = $this->api->unlicenseSite($this->productSlug, $licenseKey, $siteUrl); + } else if ( !empty($token) ) { + $apiResponse = $this->api->unlicenseSiteByToken($this->getProductSlug(), $token, $siteUrl); + } else { + return new WP_Error( + 'invalid_argument', + 'To unlicense a site, you must specify either a license key or a site token.' + ); + } + + $responseLicense = null; + if ( isset($apiResponse->response->license) ) { + $responseLicense = $this->createLicenseObject($apiResponse->response->license); + } + + if ( $apiResponse->success() ) { + $result = $responseLicense; + } else { + $error = $apiResponse->asWpError(); + if ( isset($responseLicense) ) { + $error->add_data('license', $responseLicense); + } + $result = $error; + } + + //Did we just remove the license from the current site? + if ( $siteUrl === $this->getSiteUrl() ) { + if ( !is_wp_error($result) || ($result->get_error_code() !== 'api_request_failed') ) { + //Success. Or, if the request fails for any reason other than API problems, + //chances are the stored license was invalid. So we'll remove the local copy anyway. + $this->resetState(); + } + } + + return $result; + } + + /** + * @return string|null + */ + public function getLicenseKey() { + $this->lazyLoad(); + if (is_string($this->licenseKey) && $this->licenseKey !== '') { + return $this->licenseKey; + } + return null; + } + + /** + * @return string|null + */ + public function getSiteToken() { + $this->lazyLoad(); + if (is_string($this->siteToken) && $this->siteToken !== '') { + return $this->siteToken; + } + return null; + } + + /** + * @return array + */ + public function getTokenHistory() { + $this->lazyLoad(); + return isset($this->tokenHistory) ? $this->tokenHistory : array(); + } + + public function getProductSlug() { + return $this->productSlug; + } + + public function getApi() { + return $this->api; + } + + public function getSiteUrl() { + if ( $this->licenseScope === self::LICENSE_SCOPE_NETWORK ) { + $url = network_site_url(); + } else { + $url = site_url(); + } + return str_replace('https://', 'http://', $url); + } + + /** + * Register filters that will add license details to update requests and download URLs. + * Add-ons can use this method to easily re-use the same license key as the main plugin. + * + * @param Puc_v4p11_Plugin_UpdateChecker $updateChecker + */ + public function addUpdateFiltersTo($updateChecker) { + $updateChecker->addQueryArgFilter(array($this, 'filterUpdateChecks')); + + //Add license data to update download URL, or remove the URL if we don't have a license. + $downloadFilter = array($this, 'filterUpdateDownloadUrl'); + $updateChecker->addFilter('request_info_result', $downloadFilter, 20); + $updateChecker->addFilter('pre_inject_update', $downloadFilter); + $updateChecker->addFilter('pre_inject_info', $downloadFilter); + } + + public function filterUpdateChecks($queryArgs) { + if ( $this->getSiteToken() !== null ) { + $queryArgs['license_token'] = $this->getSiteToken(); + } + if ( $this->getLicenseKey() !== null ) { + $queryArgs['license_key'] = $this->getLicenseKey(); + } + $queryArgs['license_site_url'] = $this->getSiteUrl(); + return $queryArgs; + } + + /** + * @param Puc_v4p11_Plugin_Info|null $pluginInfo + * @param array $result + * @return Puc_v4p11_Plugin_Info|null + */ + public function refreshLicenseFromPluginInfo($pluginInfo, $result) { + $this->lazyLoad(); + if ( !is_wp_error($result) && isset($result['response']['code']) && ($result['response']['code'] == 200) && !empty($result['body']) ){ + $apiResponse = json_decode($result['body']); + if ( is_object($apiResponse) && isset($apiResponse->license) ) { + $this->license = $this->createLicenseObject($apiResponse->license); + $this->save(); + } + } + return $pluginInfo; + } + + /** + * Add license data to the update download URL if we have a valid license, + * or remove the URL (thus disabling one-click updates) if we don't. + * + * @param Puc_v4p11_Plugin_Update|Puc_v4p11_Plugin_Info $pluginInfo + * @return Puc_v4p11_Plugin_Update|Puc_v4p11_Plugin_Info + */ + public function filterUpdateDownloadUrl($pluginInfo) { + if ( isset($pluginInfo, $pluginInfo->download_url) && !empty($pluginInfo->download_url) ) { + $license = $this->getLicense(); + if ( $license->canReceiveProductUpdates() ) { + //Append license data to the download URL so that the server can verify it. + $args = array_filter(array( + 'license_key' => $this->getLicenseKey(), + 'license_token' => $this->getSiteToken(), + 'license_site_url' => $this->getSiteUrl(), + )); + $pluginInfo->download_url = add_query_arg($args, $pluginInfo->download_url); + } else { + //No downloads without a license! + $pluginInfo->download_url = null; + } + } + return $pluginInfo; + } + + public function addCustomSchedule($schedules){ + if ( $this->checkPeriod && ($this->checkPeriod > 0) ){ + $scheduleName = 'every' . $this->checkPeriod . 'hours'; + $schedules[$scheduleName] = array( + 'interval' => $this->checkPeriod * 3600, + 'display' => sprintf('Every %d hours', $this->checkPeriod), + ); + } + return $schedules; + } +} + +class Wslm_LicenseManagerApi { + private $apiUrl; + + public function __construct($apiUrl) { + $this->apiUrl = $apiUrl; + } + + public function getLicense($product, $key, $siteUrl = null) { + $params = ($siteUrl !== null) ? array('site_url' => $siteUrl) : array(); + return $this->get($this->endpoint($product, $key), $params); + } + + public function licenseSite($product, $key, $siteUrl) { + $params = array('site_url' => $siteUrl); + return $this->post($this->endpoint($product, $key, null, 'license_site'), $params); + } + + public function unlicenseSite($product, $key, $siteUrl) { + $params = array('site_url' => $siteUrl); + return $this->post($this->endpoint($product, $key, null, 'unlicense_site'), $params); + } + + public function getLicenseByToken($product, $token, $siteUrl = null) { + $params = ($siteUrl !== null) ? array('site_url' => $siteUrl) : array(); + return $this->get($this->endpoint($product, null, $token), $params); + } + + public function unlicenseSiteByToken($product, $token, $siteUrl) { + $params = array('site_url' => $siteUrl); + return $this->post($this->endpoint($product, null, $token, 'unlicense_site'), $params); + } + + public function get($endpoint, $params = array()) { + return $this->request('get', $endpoint, $params); + } + + public function post($endpoint, $params = array()) { + return $this->request('post', $endpoint, $params); + } + + private function endpoint($product, $license = null, $token = null, $action = null) { + $endpoint = '/products/' . urlencode($product) . '/licenses/'; + $endpoint .= ($token !== null) ? 'bytoken/' . urlencode($token) : urlencode($license); + if ( $action !== null ) { + $endpoint .= '/' . $action; + } + return $endpoint; + } + + /** + * Send an API request. + * + * @param string $method + * @param string $endpoint + * @param array $params + * @return Wslm_LicenseManagerApiResponse + */ + public function request($method, $endpoint, $params = array()) { + $url = $this->getApiUrl($endpoint); + $method = strtoupper($method); + $args = array('method' => $method, 'timeout' => 30); + + if ( !empty($params) ) { + if ( ($method === 'POST') || ($method === 'PUT') ) { + $args['body'] = $params; + } else { + $url .= '?' . http_build_query($params, '', '&'); + } + } + + $response = wp_remote_request($url, $args); + return new Wslm_LicenseManagerApiResponse($response); + } + + private function getApiUrl($endpoint) { + return rtrim($this->apiUrl, '/') . '/' . ltrim($endpoint, '/'); + } +} + +class Wslm_LicenseManagerApiResponse { + public $response = null; + public $httpCode; + public $httpResponse; + + private $error = null; + + /** + * @param array|WP_Error $httpResponse + */ + public function __construct($httpResponse) { + $this->httpResponse = $httpResponse; + $this->httpCode = intval(wp_remote_retrieve_response_code($httpResponse)); + + if ( is_wp_error($httpResponse) ) { + $this->error = new WP_Error('api_request_failed', $httpResponse->get_error_message(), $this); + $this->response = null; + } else { + //Attempt to parse the API response. Expect a JSON document. + $data = json_decode(wp_remote_retrieve_body($httpResponse)); + if ( $data === null ) { + $this->error = new WP_Error( + 'api_request_failed', + 'Failed to parse the response returned by the licensing API (expected JSON).', + $this + ); + } + $this->response = $data; + } + } + + public function success() { + return empty($this->error) && ($this->httpCode >= 200 && $this->httpCode < 400); + } + + public function asWpError() { + if ( $this->success() ) { + return null; + } + + if ( !empty($this->error) ) { + return $this->error; + } else if ( isset($this->response->error) ) { + return new WP_Error($this->response->error->code, $this->response->error->message, $this); + } else { + return new WP_Error('http_' . $this->httpCode, 'HTTP error ' . $this->httpCode, $this); + } + } +} \ No newline at end of file diff --git a/license-manager/LicenseServer.php b/license-manager/LicenseServer.php new file mode 100644 index 0000000..d9f5455 --- /dev/null +++ b/license-manager/LicenseServer.php @@ -0,0 +1,750 @@ +db = $database; + + $this->tablePrefix = $tablePrefix; + if ( isset($GLOBALS['wpdb']) ) { + $this->wpdb = $GLOBALS['wpdb']; + } + if ( !isset($this->tablePrefix) ) { + $this->tablePrefix = isset($this->wpdb) ? $this->wpdb->prefix : 'wp_'; + } + $this->get = $_GET; + $this->post = $_POST; + + if ( function_exists('add_action') ) { + add_action('init', array($this, 'addRewriteRules')); + add_filter('query_vars', array($this, 'addQueryVars')); + + add_action('template_redirect', array($this, 'dispatchRequest'), 5); + + $cronHook = 'wslm_delete_unused_tokens'; + if ( function_exists('wp_next_scheduled') && is_admin() ) { + if ( !wp_next_scheduled($cronHook) && !defined('WP_INSTALLING') ) { + wp_schedule_event(time(), 'daily', $cronHook); + } + } + add_action($cronHook, array($this, 'deleteUnusedTokens')); + } + } + + /** + * Generate a new license. + * + * The $licenseData array determines the properties of the new license. + * The supported fields are: + * 'product_slug' - The product associated with the license. Required. + * 'product_id' - A numeric product ID. + * 'customer_id' - A numeric customer ID. + * 'max_sites' - On how many sites can the license be activated. Null = no limit. + * 'expires_on' - When the license expires (e.g. "2015-01-31 12:00"). Null = never expire. + * + * The 'product_id' and 'customer_id' fields are primarily intended for your own use, + * i.e. to help integrate this licensing library with your existing store. The license + * server doesn't actually use them for anything. + * + * @param array $licenseData + * @return Wslm_ProductLicense + * @throws InvalidArgumentException + */ + public function generateLicense($licenseData) { + $licenseData = array_merge(array( + 'product_id' => 0, + 'customer_id' => 0, + 'max_sites' => null, + 'expires_on' => null, + 'issued_on' => date('Y-m-d H:i:s'), + 'license_key' => $this->generateRandomString(32), + ), $licenseData); + + if ( empty($licenseData['product_slug']) ) { + throw new InvalidArgumentException("Product slug must not be empty"); + } + + return $this->saveLicense($licenseData); + } + + /** + * Delete a license and associated tokens. + * + * @param Wslm_ProductLicense $license + */ + public function deleteLicense($license) { + $this->wpdb->delete( + $this->tablePrefix . 'licenses', + array('license_id' => $license['license_id']) + ); + //Delete associated tokens. + $this->wpdb->delete( + $this->tablePrefix . 'tokens', + array('license_id' => $license['license_id']) + ); + } + + /** + * Update or insert a license. + * + * @param Wslm_ProductLicense|array $license + * @return Wslm_ProductLicense + */ + public function saveLicense($license) { + if ( is_array($license) ) { + $license = new Wslm_ProductLicense($license); + } + $data = $license->getData(); + + //The license object might have some virtual or computed fields that don't exist in the DB. + //If we try to update/insert those, we'll get an SQL error. So lets filter the data array + //to ensure only valid fields are included in the query. + $licenseDbFields = array( + 'license_id', 'license_key', 'product_id', 'product_slug', 'customer_id', + 'status', 'issued_on', 'expires_on', 'max_sites', 'base_price', 'renewal_price', + ); + $licenseDbFields = apply_filters('wslm_license_db_fields', $licenseDbFields); + $data = array_intersect_key($data, array_flip($licenseDbFields)); + + if ( is_numeric($data['expires_on']) ) { + $data['expires_on'] = date('Y-m-d H:i:s', $data['expires_on']); + } + + if ( $license->get('license_id') === null ) { + //wpdb converts null values to "0" which is not what we want. + //When inserting, we can simply strip them and let the DB fill in the blanks with NULLs. + $data = array_filter($data, __CLASS__ . '::isNotNull'); + $this->wpdb->insert($this->tablePrefix . 'licenses', $data); + $license['license_id'] = $this->wpdb->insert_id; + } else { + //When updating, we need to check for nulls and treat them differently, + //so we can't use $wpdb->update here. + $query = "UPDATE {$this->tablePrefix}licenses SET "; + $expressions = array(); + foreach($data as $field => $value) { + $expressions[] = $field . ' = ' . (($value === null) ? 'NULL' : $this->wpdb->prepare('%s', $value)); + } + $query .= implode(', ', $expressions); + $query .= ' WHERE license_id = ' . absint($license['license_id']); + $this->wpdb->query($query); + } + + //Save add-ons. + $this->wpdb->query('START TRANSACTION'); + //Delete old add-on records first. + $query = $this->wpdb->prepare( + "DELETE FROM `{$this->tablePrefix}license_addons` WHERE license_id = %d", + $license['license_id'] + ); + $this->wpdb->query($query); + + //Insert new license-to-add-on relations. + if ( !empty($license['addons']) ) { + $query = + "INSERT INTO `{$this->tablePrefix}license_addons`(license_id, addon_id) + SELECT %d AS license_id, addon_id + FROM `{$this->tablePrefix}addons`"; + $query = $this->wpdb->prepare($query, $license['license_id']); + + $preparedSlugs = array(); + foreach($license['addons'] as $slug => $ignored) { + $preparedSlugs[] = $this->wpdb->prepare('%s', $slug); + } + $query .= ' WHERE slug IN (' . implode(', ', $preparedSlugs) . ')'; + + $this->wpdb->query($query); + } + $this->wpdb->query('COMMIT'); + + return $license; + } + + public static function isNotNull($value) { + return $value !== null; + } + + public function dispatchRequest() { + if ( get_query_var('licensing_api') != '1' ) { + return; + } + + $action = get_query_var('license_action'); + if ( empty($action) ) { + $action = 'get_license'; + } + $productSlug = get_query_var('license_product'); + $licenseKey = get_query_var('license_key'); + $token = get_query_var('license_token'); + + switch ($action) { + case 'get_license': + $this->actionGetLicense($productSlug, $licenseKey, $token); + break; + case 'license_site': + $this->actionLicenseSite($productSlug, $licenseKey); + break; + case 'unlicense_site': + $this->actionUnlicenseSite($productSlug, $licenseKey, $token); + break; + default: + $this->outputError('invalid_action', 'Unsupported API action "' . $action . '"', 400); + break; + } + + exit; + } + + protected function actionGetLicense($productSlug, $licenseKey = null, $token = null) { + $this->requireRequestMethod('GET'); + $license = $this->validateLicenseRequest($productSlug, $licenseKey, $token, $this->get); + $this->outputResponse(array( + 'license' => $this->prepareLicenseForOutput($license, !empty($token)), + )); + + if ( !empty($token) ) { + $this->logUpdateCheck($token); + } + } + + /** + * Check if the specified license exists, and quit with an API error if not. + * Returns the requested license on success. + * + * @param string $productSlug + * @param string|null $licenseKey + * @param string|null $token + * @param array $params + * @return Wslm_ProductLicense + */ + protected function validateLicenseRequest($productSlug, $licenseKey, $token = null, $params = array()) { + $usingToken = !empty($token); + if ( $usingToken ) { + $siteUrl = $this->sanitizeSiteUrl(isset($params['site_url']) ? strval($params['site_url']) : ''); + } else { + $siteUrl = null; + } + + $license = $this->verifyLicenseExists($productSlug, $licenseKey, $token, $siteUrl); + if ( is_wp_error($license) ) { + $this->outputError( + $license->get_error_code(), + $license->get_error_message(), + $license->get_error_data() + ); + exit; + } else { + return $license; + } + + } + + /** + * Check if the specified license key or token exists and return the corresponding license. + * + * If you specify both a token and a site URL this method will also verify that the token + * matches the site URL. + * + * @param string $productSlug + * @param string $licenseKey + * @param string|null $token Takes precedence over the license key. + * @param string|null $siteUrl + * @return Wslm_ProductLicense|WP_Error A license object, or WP_Error if the license doesn't exist or doesn't match the URL. + */ + public function verifyLicenseExists($productSlug, $licenseKey, $token = null, $siteUrl = null) { + if ( empty($licenseKey) && empty($token) ) { + return new WP_Error('not_found', 'You must specify a license key or a site token.', 400); + } + + $license = $this->loadLicense($licenseKey, $token); + if ( empty($license) ) { + if ( !empty($token) ) { + return new WP_Error('not_found', 'Invalid site token.', 404); + } else { + return new WP_Error('not_found', 'Invalid license key. Please verify the key or contact the developer for assistance.', 404); + } + } + + if ( $license['product_slug'] !== $productSlug ) { + if ( $license->hasAddOn($productSlug) ) { + //This request is for an add-on, not the main product. That's fine. + } else { + return new WP_Error('not_found', 'This license key is for a different product.', 404); + } + } + + //Make sure the site token was actually issued to that site and not another one. + if ( $siteUrl !== null && $token !== null ) { + $siteUrl = $this->sanitizeSiteUrl($siteUrl); + if ( !$this->isValidUrl($siteUrl) ) { + return new WP_Error('invalid_site_url', 'You must specify a valid site URL when using a site token.', 400); + } + if ( $siteUrl != $this->sanitizeSiteUrl($license['site_url']) ) { + return new WP_Error('wrong_site', 'This token is associated with a different site.', 400); + } + } + + return $license; + } + + /** + * Retrieve a license by license key or token. + * + * If you specify a token, this method will ignore $licenseKey and just + * look for the token. The returned license object will also include + * the URL of the site associated with that token in a 'site_url' field. + * + * @param string|int|null $licenseKeyOrId + * @param string|null $token + * @throws InvalidArgumentException + * @return Wslm_ProductLicense|null A license object, or null if the license doesn't exist. + */ + public function loadLicense($licenseKeyOrId, $token = null) { + if ( !empty($token) ) { + $query = "SELECT licenses.*, tokens.site_url + FROM + `{$this->tablePrefix}licenses` AS licenses + JOIN `{$this->tablePrefix}tokens` AS tokens + ON licenses.license_id = tokens.license_id + WHERE tokens.token = ?"; + $params = array($token); + } else if ( is_numeric($licenseKeyOrId) && (!is_string($licenseKeyOrId) || (strlen($licenseKeyOrId) < 13)) ) { + $query = + "SELECT licenses.* FROM `{$this->tablePrefix}licenses` AS licenses + WHERE license_id = ?"; + $params = array($licenseKeyOrId); + } else if ( !empty($licenseKeyOrId) ) { + $query = + "SELECT licenses.* FROM `{$this->tablePrefix}licenses` AS licenses + WHERE license_key = ?"; + $params = array($licenseKeyOrId); + } else { + throw new InvalidArgumentException('You must specify a license key or a site token.'); + } + + $license = $this->db->getRow($query, $params); + if ( !empty($license) ) { + //Also include the list of sites and add-ons associated with this license. + $license['sites'] = $this->loadLicenseSites($license['license_id']); + $license['addons'] = $this->loadLicenseAddOns($license['license_id']); + $license = new Wslm_ProductLicense($license); + + $license['renewal_url'] = 'http://adminmenueditor.com/renew-license/'; //TODO: Put this in a config of some sort instead. + $license['upgrade_url'] = 'http://adminmenueditor.com/upgrade-license/'; + } else { + $license = null; + } + return $license; + } + + protected function loadLicenseSites($licenseId) { + $licensedSites = $this->db->getResults( + "SELECT site_url, token, issued_on + FROM {$this->tablePrefix}tokens + WHERE license_id = ?", + array($licenseId) + ); + return $licensedSites; + } + + protected function loadLicenseAddOns($licenseId) { + $rows = $this->db->getResults( + "SELECT addons.slug, addons.name + FROM + {$this->tablePrefix}license_addons AS license_addons + JOIN {$this->tablePrefix}addons AS addons + ON (license_addons.addon_id = addons.addon_id) + WHERE license_addons.license_id = ?", + array($licenseId) + ); + + $addOns = array(); + foreach($rows as $row) { + $addOns[$row['slug']] = $row['name']; + } + return $addOns; + } + + /** + * @param Wslm_ProductLicense $license + * @param bool $usingToken + * @return array + */ + public function prepareLicenseForOutput($license, $usingToken = false) { + $data = $license->getData(); + $data['status'] = $license->getStatus(); + + //Ensure timestamps are formatted consistently. + foreach(array('issued_on', 'expires_on') as $datetimeField) { + if ( isset($data[$datetimeField]) ) { + $data[$datetimeField] = gmdate('c', strtotime($data[$datetimeField])); + } + } + + $visibleFields = array_fill_keys(array( + 'license_key', 'product_slug', 'status', 'issued_on', 'max_sites', + 'expires_on', 'sites', 'site_url', 'error', 'renewal_url', 'addons', + ), true); + if ( $usingToken ) { + $visibleFields = array_merge($visibleFields, array( + 'license_key' => false, + 'sites' => false, + )); + } + if ( function_exists('apply_filters') ) { + $visibleFields = apply_filters('wslm_api_visible_license_fields', $visibleFields); + } + $data = array_intersect_key($data, array_filter($visibleFields)); + return $data; + } + + /** + * Record that a specific site just checked for updates. + * + * @param string $token Unique site token. + */ + public function logUpdateCheck($token) { + $query = "UPDATE {$this->tablePrefix}tokens SET last_update_check = NOW() WHERE token = ?"; + $this->db->query($query, array($token)); + } + + /** + * Delete tokens associated with sites that haven't checked for updates in the last X days. + */ + public function deleteUnusedTokens() { + $query = + "DELETE FROM {$this->tablePrefix}tokens + WHERE + last_update_check IS NOT NULL + AND last_update_check < DATE_SUB(NOW(), INTERVAL ? DAY)"; + $this->db->query($query, array($this->tokenDeletionThreshold)); + + //TODO: Also delete sites that were licensed a long time ago and that have never checked for updates. + } + + protected function actionLicenseSite($productSlug, $licenseKey) { + $this->requireRequestMethod('POST'); + $license = $this->validateLicenseRequest($productSlug, $licenseKey); + + //Is the license still valid? + if ( !$license->isValid() ) { + if ( $license->getStatus() == 'expired' ) { + $renewalUrl = $license->get('renewal_url'); + $this->outputError( + 'expired_license', + sprintf( + 'This license key has expired. You can still use the plugin (without activating the key), but you will need to %1$srenew the license%2$s to receive updates.', + $renewalUrl ? '' : '', + $renewalUrl ? '' : '' + ), + 400 + ); + } else { + $this->outputError('invalid_license', 'This license key is invalid or has expired.', 400); + } + return; + } + + $siteUrl = isset($this->post['site_url']) ? strval($this->post['site_url']) : ''; + if ( !$this->isValidUrl($siteUrl) ) { + $this->outputError('site_url_expected', "Missing or invalid site URL.", 400); + return; + } + $siteUrl = $this->sanitizeSiteUrl($siteUrl); + + //Maybe the site is already licensed? + $token = $this->wpdb->get_var($this->wpdb->prepare( + "SELECT token FROM {$this->tablePrefix}tokens WHERE site_url = %s AND license_id = %d", + $siteUrl, $license['license_id'] + )); + if ( !empty($token) ) { + $this->outputResponse(array( + 'site_token' => $token, + 'license' => $this->prepareLicenseForOutput($license), + )); + return; + } + + //Check the number of sites already licensed and see if we can add another one. + if ( $license['max_sites'] !== null ) { + $licensedSiteCount = $this->wpdb->get_var($this->wpdb->prepare( + "SELECT COUNT(*) FROM {$this->tablePrefix}tokens WHERE license_id = %d", + $license['license_id'] + )); + if ( intval($licensedSiteCount) >= intval($license['max_sites']) ) { + $upgradeUrl = $license->get('upgrade_url'); + $this->outputError( + 'max_sites_reached', + sprintf( + 'You have reached the maximum number of sites allowed by your license. ' + . 'To activate it on another site, you need to either %1$supgrade the license%2$s ' + . 'or remove it from one of your existing sites in the "Manage Sites" tab.', + $upgradeUrl ? '' : '', + $upgradeUrl ? '' : '', + 'ame-open-tab-manage-sites' + ), + 400, + $this->prepareLicenseForOutput($license, false) + ); + return; + } + } + + //If the site was already associated with another key, remove that association. Only one key per site. + //Local sites are an exception because they're not unique. Many developers use http://localhost/. + if ( !$this->isLocalHost($siteUrl) ) { + $otherToken = $this->wpdb->get_var($this->wpdb->prepare( + "SELECT tokens.token + FROM {$this->tablePrefix}tokens AS tokens + JOIN {$this->tablePrefix}licenses AS licenses + ON tokens.license_id = licenses.license_id + WHERE + tokens.site_url = %s + AND licenses.product_slug = %s + AND licenses.license_id <> %d", + $siteUrl, $productSlug, $license['license_id'] + )); + if ( !empty($otherToken) ) { + $this->wpdb->delete($this->tablePrefix . 'tokens', array('token' => $otherToken)); + } + } + + //Everything checks out, lets create a new token. + $token = $this->generateRandomString(32); + $this->wpdb->insert( + $this->tablePrefix . 'tokens', + array( + 'license_id' => $license['license_id'], + 'token' => $token, + 'site_url' => $siteUrl, + 'issued_on' => date('Y-m-d H:i:s'), + ) + ); + + //Reload the license to ensure it includes the changes we just made. + $license = $this->loadLicense($licenseKey); + + $response = array( + 'site_token' => $token, + 'license' => $this->prepareLicenseForOutput($license), + ); + + //Add a notice to expired licenses. + if ( $license->getStatus() === 'expired' ) { + $renewalUrl = $license->get('renewal_url'); + $response['notice'] = array( + 'message' => sprintf( + 'Your access to updates and support has expired. To receive updates, please %1$srenew your license%2$s.', + $renewalUrl ? '' : '', + $renewalUrl ? '' : '' + ), + 'class' => 'notice-warning' + ); + } + + $this->outputResponse($response); + } + + public function generateRandomString($length, $alphabet = null) { + if ( $alphabet === null ) { + $alphabet = 'ABDEFGHJKLMNOPQRSTVWXYZ0123456789'; + //U and C intentionally left out to lessen the chances of generating an obscene string. + //"I" was left out because it's visually similar to 1. + } + $maxIndex = strlen($alphabet) - 1; + $str = ''; + for ($i = 0; $i < $length; $i++) { + $str .= substr($alphabet, rand(0, $maxIndex), 1); + } + return $str; + } + + protected function isLocalHost($siteUrl) { + $host = @parse_url($siteUrl, PHP_URL_HOST); + if ( empty($host) ) { + return false; + } + return (preg_match('/\.?(localhost|local|dev)$/', $host) == 1); + } + + protected function actionUnlicenseSite($productSlug, $licenseKey = null, $token = null) { + $this->requireRequestMethod('POST'); + $license = $this->validateLicenseRequest($productSlug, $licenseKey, $token, $this->post); + + $siteUrl = $this->sanitizeSiteUrl(isset($this->post['site_url']) ? strval($this->post['site_url']) : ''); + $usingToken = !empty($token); + + $response = array( 'license' => $this->prepareLicenseForOutput($license, $usingToken), ); + + if ( !$usingToken ) { + $token = $this->wpdb->get_var($this->wpdb->prepare( + "SELECT token FROM `{$this->tablePrefix}tokens` WHERE site_url = %s AND license_id = %d", + $siteUrl, $license['license_id'] + )); + } + + if ( empty($token) ) { + //The user tried to un-license a site that wasn't licensed in the first place. Still, + //the desired end state - site not licensed - has ben achieved, so treat it as a success. + $response['notice'] = "The specified site wasn't licensed in the first place."; + } else { + $this->wpdb->delete( + $this->tablePrefix . 'tokens', + array( + 'token' => $token, + 'license_id' => $license['license_id'], + ) + ); + + //Reload the license to ensure the site list is correct. + $license = $this->loadLicense($license['license_key']); + $response['license'] = $this->prepareLicenseForOutput($license, $usingToken); + + $response = array_merge($response, array( + 'site_token_removed' => $token, + 'site_url' => $siteUrl + )); + } + $this->outputResponse($response); + } + + protected function requireRequestMethod($httpVerbs) { + $httpVerbs = array_map('strtoupper', (array)$httpVerbs); + if ( !in_array(strtoupper($_SERVER['REQUEST_METHOD']), $httpVerbs) ) { + header('Allow: '. implode(', ', $httpVerbs)); + $this->outputError( + 'invalid_method', + 'This resource does not support the ' . $_SERVER['REQUEST_METHOD'] . ' method.', + 405 + ); + exit; + } + } + + protected function outputError($code, $message, $httpStatus = null, $licenseData = null) { + $httpStatus = (isset($httpStatus) && is_numeric($httpStatus)) ? $httpStatus : 500; + $response = array('error' => array('code' => $code, 'message' => $message),); + if ( isset($licenseData) ) { + $response['license'] = $licenseData; + } + $this->outputResponse($response, $httpStatus); + } + + private function outputResponse($body, $httpStatus = 200) { + status_header($httpStatus); + header('Content-Type: application/json'); + echo wsh_pretty_json(json_encode($body)); + } + + private function isValidUrl($url) { + $parts = @parse_url($url); + return !empty($url) && !empty($parts) && isset($parts['host']); + } + + private function sanitizeSiteUrl($url) { + //Convert Punycode domain names to UTF-8. + $domain = @parse_url($url, PHP_URL_HOST); + if ( $domain && preg_match('/xn--./', $domain) && is_callable('idn_to_utf8') ) { + /** @noinspection PhpComposerExtensionStubsInspection */ + $converted = idn_to_utf8($domain); + if ($converted && ($converted !== $domain)) { + $url = preg_replace_callback( + '/' . preg_quote($domain, '/') . '/', + function() use ($converted) { + return $converted; + }, + $url, + 1 + ); + } + } + return rtrim($url, '/'); + } + + /** + * Retrieve a customer's licenses. You can optionally specify a slug to retrieve only + * licenses for that product. + * + * @param int $customerId + * @param string|null $productSlug + * @return Wslm_ProductLicense[] An array of licenses ordered by status and expiry (newest valid licenses first). + */ + public function getCustomerLicenses($customerId, $productSlug = null) { + //This UNION hack is due to the fact that, when dealing with businesses, different people + //can renew or upgrade the same license. People have different emails, so they count as different + //customers. We need all of them to be able to access the license. + $query = $this->wpdb->prepare( + "SELECT customerLicenses.* + FROM ( + SELECT licenses.* + FROM {$this->tablePrefix}licenses AS licenses + WHERE (licenses.customer_id = %d) + + UNION DISTINCT + + SELECT order_licenses.* + FROM {$this->tablePrefix}orders AS orders JOIN {$this->tablePrefix}licenses AS order_licenses + ON (orders.license_id = order_licenses.license_id) + WHERE (orders.customer_id = %d) + ) AS customerLicenses + WHERE 1", + array($customerId, $customerId) + ); + if ( $productSlug !== null ) { + $query .= $this->wpdb->prepare(' AND product_slug=%s', $productSlug); + } + $query .= ' ORDER BY status ASC, expires_on IS NULL DESC, expires_on DESC'; //Valid licenses first. + + $rows = $this->wpdb->get_results($query, ARRAY_A); + $licenses = array(); + if ( is_array($rows) ) { + foreach($rows as $row) { + $row['addons'] = $this->loadLicenseAddOns($row['license_id']); + $licenses[] = new Wslm_ProductLicense($row); + } + } + return $licenses; + } + + public function addRewriteRules() { + $apiRewriteRules = array( + 'licensing_api/products/([^/\?]+)/licenses/bytoken/([^/\?]+)(?:/([a-z0-9_]+))?' => + 'index.php?licensing_api=1&license_product=$matches[1]&license_token=$matches[2]&license_action=$matches[3]', + + 'licensing_api/products/([^/\?]+)/licenses/([^/\?]+)(?:/([a-z0-9_]+))?' => + 'index.php?licensing_api=1&license_product=$matches[1]&license_key=$matches[2]&license_action=$matches[3]', + ); + + foreach ($apiRewriteRules as $pattern => $redirect) { + add_rewrite_rule($pattern, $redirect, 'top'); + } + + //Flush the rules only if they didn't exist before. + $wp_rewrite = $GLOBALS['wp_rewrite']; /** @var WP_Rewrite $wp_rewrite */ + $missingRules = array_diff_assoc($apiRewriteRules, $wp_rewrite->wp_rewrite_rules()); + if ( !empty($missingRules) ) { + flush_rewrite_rules(false); + } + } + + public function addQueryVars($queryVariables) { + $licensingVariables = array( + 'licensing_api', 'license_product', 'license_key', + 'license_token', 'license_action' + ); + $queryVariables = array_merge($queryVariables, $licensingVariables); + return $queryVariables; + } +} diff --git a/license-manager/PdoDatabase.php b/license-manager/PdoDatabase.php new file mode 100644 index 0000000..ae9792b --- /dev/null +++ b/license-manager/PdoDatabase.php @@ -0,0 +1,24 @@ +pdo = new PDO($dsn, $username, $password); + } + + public function getResults($query, $parameters = array()) { + $statement = $this->pdo->prepare($query); + $statement->execute($parameters); + return $statement->fetchAll(PDO::FETCH_ASSOC); + } + + public function query($query, $parameters = array()) { + $statement = $this->pdo->prepare($query); + $statement->execute($parameters); + return $statement->rowCount(); + } +} diff --git a/license-manager/ProductLicense.php b/license-manager/ProductLicense.php new file mode 100644 index 0000000..646374e --- /dev/null +++ b/license-manager/ProductLicense.php @@ -0,0 +1,116 @@ +data = (array)$data; + } + if ( !isset($this->data['addons']) ) { + $this->data['addons'] = array(); + } + + $integerFields = array('license_id', 'max_sites', 'customer_id'); + foreach($integerFields as $field) { + if ( isset($this->data[$field]) && is_string($this->data[$field]) && is_numeric($this->data[$field]) ) { + $this->data[$field] = intval($this->data[$field]); + } + } + } + + public function getData() { + return $this->data; + } + + public function isValid() { + //A license that's "expired" is still valid, it just doesn't qualify for updates. + $status = $this->getStatus(); + return ($status === 'valid') || ($status === 'expired'); + } + + public function getStatus() { + $status = $this->get('status'); + if ( $status === null ) { + $status = 'valid'; + } + + if ( $status === 'valid' ) { + $expires = $this->get('expires_on'); + if ( isset($expires) && strtotime($expires) < time() ) { + $status = 'expired'; + } + } + return $status; + } + + public function canReceiveProductUpdates() { + return ($this->getStatus() === 'valid'); + } + + public function canDownloadCurrentVersion() { + //Just an alias, but it's a separate method because there's a subtle + //semantic difference between being able to download the plugin and + //having access to updates in general. It might matter one day. + return $this->canReceiveProductUpdates(); + } + + public function addAddOn($slug, $name = null) { + $this->data['addons'][$slug] = isset($name) ? $name : $slug; + } + + public function removeAddOn($slug) { + unset($this->data['addons'][$slug]); + } + + public function hasAddOn($slug) { + return (isset($this->data['addons'], $this->data['addons'][$slug])) && $this->data['addons'][$slug]; + } + + public function get($name, $default = null) { + if ( array_key_exists($name, $this->data) ) { + return $this->data[$name]; + } else { + return $default; + } + } + + public function isExisting() { + return ( $this->getStatus() !== 'no_license_yet' ) && ( ! $this->get('is_virtual', false) ); + } + + public function __get($key) { + if ( array_key_exists($key, $this->data) ) { + return $this->data[$key]; + } else { + throw new RuntimeException('Unknown property '. $key); + } + } + + public function __isset($key) { + return isset($this->data[$key]); + } + + + #[\ReturnTypeWillChange] + public function offsetExists($offset) { + return isset($this->data[$offset]); + } + + #[\ReturnTypeWillChange] + public function offsetGet($offset) { + return $this->data[$offset]; + } + + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) { + $this->data[$offset] = $value; + } + + #[\ReturnTypeWillChange] + public function offsetUnset($offset) { + unset($this->data[$offset]); + } +} \ No newline at end of file diff --git a/license-manager/WpDatabase.php b/license-manager/WpDatabase.php new file mode 100644 index 0000000..f6e6d4a --- /dev/null +++ b/license-manager/WpDatabase.php @@ -0,0 +1,28 @@ +wpdb = $GLOBALS['wpdb']; + } + + public function getResults($query, $parameters = array()) { + if ( !empty($parameters) ) { + $query = str_replace('?', '%s', $query); + $query = $this->wpdb->prepare($query, $parameters); + } + return $this->wpdb->get_results($query, ARRAY_A); + } + + public function query($query, $parameters = array()) { + if ( !empty($parameters) ) { + $query = str_replace('?', '%s', $query); + $query = $this->wpdb->prepare($query, $parameters); + } + return $this->wpdb->query($query); + } +} diff --git a/license-manager/pretty-json.php b/license-manager/pretty-json.php new file mode 100644 index 0000000..7c4639f --- /dev/null +++ b/license-manager/pretty-json.php @@ -0,0 +1,73 @@ +=') ) { + $ameDialogClasses[] = 'ame-is-wp53-plus'; +} +?> + \ No newline at end of file diff --git a/modules/access-editor/access-editor.js b/modules/access-editor/access-editor.js new file mode 100644 index 0000000..f638f7a --- /dev/null +++ b/modules/access-editor/access-editor.js @@ -0,0 +1,501 @@ +/* globals AmeCapabilityManager, jQuery, AmeActors */ + +window.AmeItemAccessEditor = (function ($) { + 'use strict'; + + /** + * A group of related permissions that can be displayed in the extended permissions panel. + * + * @typedef {Object} ExtPermissionsGroup + * @property {string} title A descriptive title, e.g. the name of the post type. + * @property {Object} capabilities A dictionary of ["readable name" => "capability"]. + * @property {string} type What kind of group this is. Examples: "post_type", "taxonomy". + * @property {string|null} objectKey Post type or taxonomy key. Examples: "page", "category". + */ + + var _, + api, + isProVersion = false, + actorSelector, + postTypes, + taxonomies, + + $editor, + $actorTableRows = null, + wasDialogCreated = false, + saveCallback, + + menuItem, + itemRequiredCap = '', + containerNode = null, + selectedActor = null, + readableNamesEnabled = true, + + /** + * @type {ExtPermissionsGroup} + */ + extPermissions = null, + /** + * @type {jQuery} + */ + $currentExtTable = null, + + hasExtendedPermissions = false, + unsavedCapabilities = {}; + + var defaultDialogWidth = 390, + extendedDialogWidth = 755; + + function createEditorDialog() { + $editor = $editor || $('#ws_menu_access_editor'); + $editor.dialog({ + autoOpen: false, + closeText: ' ', + modal: true, + minHeight: 100, + width: defaultDialogWidth, + draggable: false + }); + + $editor.find('label.ws_ext_action_name') + .wrapInner('') + .append(''); + + $editor.find('#ws_ext_permissions_container') + .toggleClass('ws_ext_readable_names_enabled', readableNamesEnabled); + + wasDialogCreated = true; + } + + function setSelectedActor(actor) { + selectedActor = actor || null; + + //Deselect the previously selected actor. + $actorTableRows.removeClass('ws_cpt_selected_role'); + + //Select the new one. + if (selectedActor) { + $actorTableRows.filter(function() { + return $(this).data('actor') === selectedActor; + }).addClass('ws_cpt_selected_role'); + + $editor.find('.ws_aed_selected_actor_name').text( + actorSelector.getNiceName(AmeActors.getActor(selectedActor)) + ); + } + + if (hasExtendedPermissions) { + refreshExtPermissionsTable(); + } + } + + /** + * Get a row from the role/actor table by actor ID. + * + * @param {string} actor + * @returns {jQuery} + */ + function getActorRow(actor) { + return $actorTableRows.filter(function() { + return $(this).data('actor') === actor; + }); + } + + function refreshExtPermissionsTable() { + //Show what permissions the actor has for this CPT or taxonomy. + if (!hasExtendedPermissions) { + return; + } + + var actions = $currentExtTable.find('tr td.ws_ext_action_check_column input[type="checkbox"]'); + actions.each(function() { + var actionCheckbox = $(this), + requiredCapability = extPermissions.capabilities[actionCheckbox.data('ext_action')], + hasCap = AmeCapabilityManager.hasCap(selectedActor, requiredCapability, unsavedCapabilities), + hasCapByDefault = AmeCapabilityManager.hasCapByDefault(selectedActor, requiredCapability); + actionCheckbox.prop('checked', hasCap); + + //Flag settings that don't match the default. This can help find problems. + actionCheckbox.closest('tr').toggleClass('ws_ext_has_custom_setting', hasCap !== hasCapByDefault); + }); + } + + /** + * Get the available permission settings for the post type or taxonomy that a URL refers to. + * + * Taxonomy has precedence over post type because it's less common in admin menus, and thus more notable. + * If a URL mentions both, this function only returns the taxonomy. Returns null if the URL isn't related to + * any CPTs or taxonomies. + * + * @param {string} url + * @returns {ExtPermissionsGroup|null} + */ + function detectExtPermissions(url) { + url = url || ''; + //To ease parsing, convert "something.php" to "/wp-admin/something.php". Otherwise the parser will think + //"something.php" is a domain name. + if (/^[\w\-]+?\.php/.test(url)) { + url = '/wp-admin/' + url; + } + + var parsed = parseUri(url); + if (_.includes(['edit.php', 'post-new.php', 'edit-tags.php'], parsed.file)) { + var taxonomy = _.get(parsed, 'queryKey.taxonomy', null), + postType = _.get(parsed, 'queryKey.post_type', null); + + if (taxonomy && taxonomies.hasOwnProperty(taxonomy)) { + return _.assign({}, taxonomies[taxonomy], {type: 'taxonomy', objectKey: taxonomy}); + } else if (postType && postTypes.hasOwnProperty(postType)) { + return _.assign({}, postTypes[postType], {type: 'post_type', objectKey: postType}); + } else if ((parsed.file === 'edit-tags.php') && (taxonomies.hasOwnProperty('category'))) { + return _.assign({}, _.get(taxonomies, 'category'), {type: 'taxonomy', objectKey: 'category'}); + } else if (postTypes.hasOwnProperty('post')) { + return _.assign({}, _.get(postTypes, 'post'), {type: 'post_type', objectKey: 'post'}); + } + } + return null; + } + + // parseUri 1.2.2 + // (c) Steven Levithan [http://stevenlevithan.com] + // MIT License + // Modified: Added partial URL-decoding support. + + function parseUri(str) { + var o = parseUri.options, + m = o.parser[o.strictMode ? "strict" : "loose"].exec(str), + uri = {}, + i = 14; + + while (i--) { uri[o.key[i]] = m[i] || ""; } + + uri[o.q.name] = {}; + uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) { + if ($1) { + //Decode percent-encoded query parameters. + if (o.q.name === 'queryKey') { + $1 = decodeURIComponent($1); + $2 = decodeURIComponent($2); + } + uri[o.q.name][$1] = $2; + } + }); + + return uri; + } + + parseUri.options = { + strictMode: false, + key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"], + q: { + name: "queryKey", + parser: /(?:^|&)([^&=]*)=?([^&]*)/g + }, + parser: { + strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/, + loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ + } + }; + + // --- parseUri ends --- + + //Set up dialog event handlers. + $(document).ready(function() { + $editor = $('#ws_menu_access_editor'); + + //Select a role or user on click. + $editor.on('click', '.ws_role_table_body tr', function() { + if (hasExtendedPermissions) { + setSelectedActor($(this).closest('tr').data('actor')); + } + }); + + //Toggle readable names vs capabilities. + $editor.on('click', '#ws_ext_toggle_capability_names', function() { + readableNamesEnabled = !readableNamesEnabled; + $('#ws_ext_permissions_container').toggleClass('ws_ext_readable_names_enabled', readableNamesEnabled); + + //Remember the user's choice. + if (typeof $['cookie'] !== 'undefined') { + $.cookie('ame-readable-capability-names', readableNamesEnabled ? '1' : '0', {expires: 90}); + } + }); + + //Prevent the user from accidentally changing menu permissions when selecting a role. + $editor.on('click', '.ws_column_role label', function(event) { + if (hasExtendedPermissions) { + //Usually, clicking the role label would toggle the access checkbox. This prevents that. + event.preventDefault(); + } + }); + + //Store changes made by the user in a temporary location. + $editor.find('.ws_ext_permissions_table').on( + 'change', + 'input.ws_ext_action_allowed', + function() { + if (!hasExtendedPermissions) { + return; + } + + var checkbox = $(this), + isAllowed = checkbox.prop('checked'), + capability = extPermissions.capabilities[checkbox.data('ext_action')], + hasCapWhenReset; + + //Don't create custom settings unless necessary. + AmeCapabilityManager.resetCapInContext(unsavedCapabilities, selectedActor, capability); + hasCapWhenReset = AmeCapabilityManager.hasCap(selectedActor, capability, unsavedCapabilities); + if (isAllowed !== hasCapWhenReset) { + AmeCapabilityManager.setCapInContext( + unsavedCapabilities, + selectedActor, + capability, + isAllowed, + extPermissions.type, + extPermissions.objectKey + ); + } + + //If this is also the cap that's required to access the menu item, update the actor checkbox. + if (capability === itemRequiredCap) { + getActorRow(selectedActor).find('input.ws_role_access').prop('checked', isAllowed); + } + + refreshExtPermissionsTable(); + } + ); + + //Checking a role also gives it the required capability. However, that happens later, on the server side, + //and we don't want to give the role access to other menus associated with that capability. That means we only + //grant them that capability HERE if they already had it. Yep, that's not confusing at all. + $editor.find('.ws_role_table_body').on('change', 'input.ws_role_access', function() { + if (!hasExtendedPermissions || !itemRequiredCap) { + return; + } + + var isAllowed = $(this).prop('checked'), + hasCap = AmeCapabilityManager.hasCap(selectedActor, itemRequiredCap, unsavedCapabilities), + hasCapByDefault = AmeCapabilityManager.hasCapByDefault(selectedActor, itemRequiredCap); + + if (isAllowed && hasCapByDefault && !hasCap) { + AmeCapabilityManager.setCapInContext( + unsavedCapabilities, + selectedActor, + itemRequiredCap, + true, + extPermissions.type, + extPermissions.objectKey + ); + refreshExtPermissionsTable(); + } + }); + + //The "Save Changes" button. + $editor.find('#ws_save_access_settings').on('click', function() { + //Read the new settings from the form. + var extraCapability, restrictAccessToItems, grantAccess; + + extraCapability = api.jsTrim($('#ws_extra_capability').val()) || null; + restrictAccessToItems = $('#ws_restrict_access_to_items').prop('checked'); + + grantAccess = $.extend({}, menuItem.grant_access); + $actorTableRows.each(function() { + var row = $(this); + grantAccess[row.data('actor')] = row.find('input.ws_role_access').prop('checked'); + }); + + //Notify the editor. It will then update the menu item with the new values and refresh the UI. + if (saveCallback) { + saveCallback( + menuItem, + containerNode, + { + extraCapability : extraCapability, + grantAccess : grantAccess, + restrictAccessToItems : restrictAccessToItems, + grantedCapabilities : unsavedCapabilities + } + ); + } + + $editor.dialog('close'); + }); + }); + + return { + /** + * @param {AmeEditorApi} config.api + * @param {Object} config.actors + * @param {AmeActorSelector} config.actorSelector + * @param {Object} config.postTypes + * @param {Object} config.taxonomies + * @param {lodash} config.lodash + * @param {Function} config.save + * @param {boolean} [config.isPro] + * + * @param config + */ + setup: function(config) { + _ = config.lodash; + api = config.api; + actorSelector = config.actorSelector; + + postTypes = config.postTypes; + taxonomies = config.taxonomies; + + saveCallback = config.save || null; + isProVersion = _.get(config, 'isPro', false); + + //Read settings from cookies. + if (typeof $['cookie'] !== 'undefined') { + readableNamesEnabled = $.cookie('ame-readable-capability-names'); + } + if (typeof readableNamesEnabled === 'undefined') { + readableNamesEnabled = true; + } else { + readableNamesEnabled = (readableNamesEnabled === '1'); //Expected: "1" or "0". + } + }, + + open: function(state) { + menuItem = state.menuItem; + containerNode = state.containerNode; + unsavedCapabilities = {}; + if (!wasDialogCreated) { + createEditorDialog(); + } + + //Write the values of this item to the editor fields. + itemRequiredCap = api.getFieldValue(menuItem, 'access_level', 'Error: access_level is missing!'); + var requiredCapField = $editor.find('#ws_required_capability').empty(); + if (menuItem.template_id === '') { + //Custom items have no required caps, only what users set. + requiredCapField.append('None'); + } else { + requiredCapField.text(itemRequiredCap); + } + + $editor.find('#ws_extra_capability').val(api.getFieldValue(menuItem, 'extra_capability', '')); + $editor.find('#ws_restrict_access_to_items').prop( + 'checked', + api.getFieldValue(menuItem, 'restrict_access_to_items', false) + ); + + //Generate the actor list. + var table = $editor.find('.ws_role_table_body tbody').empty(), + alternate = '', + visibleActors = actorSelector.getVisibleActors(); + for(var index = 0; index < visibleActors.length; index++) { + var actor = visibleActors[index]; + + var checkboxId = 'allow_' + actor.id.replace(/[^a-zA-Z0-9_]/g, '_'); + var checkbox = $('').addClass('ws_role_access').attr('id', checkboxId); + + var actorHasAccess = api.actorCanAccessMenu(menuItem, actor.id); + checkbox.prop('checked', actorHasAccess); + + alternate = (alternate === '') ? 'alternate' : ''; + + var cell = ''; + var row = $('').data('actor', actor.id).attr('class', alternate).append( + $(cell).addClass('ws_column_access').append(checkbox), + $(cell).addClass('ws_column_role post-title').append( + $('