Commit 1dcb63ca by resurtm

Merge branch 'master' of github.com:yiisoft/yii2

parents 2f360e53 f9a92b82
......@@ -232,6 +232,13 @@ for Russian:
In the above it worth mentioning that `=1` matches exactly `n = 1` while `one` matches `21` or `101`.
Note that if you are using placeholder twice and one time it's used as plural another one should be used as number else
you'll get "Inconsistent types declared for an argument: U_ARGUMENT_TYPE_MISMATCH" error:
```
Total {count, number} {count, plural, one{item} other{items}}.
```
To learn which inflection forms you should specify for your language you can referer to
[rules reference at unicode.org](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html).
......
......@@ -106,6 +106,15 @@ Yii::$app->on($eventName, $handler);
Yii::$app->trigger($eventName);
```
If you need to handle all instances of a class instead of the object you can attach a handler like the following:
```php
Event::on([ActiveRecord::className(), ActiveRecord::EVENT_AFTER_INSERT], function ($event) {
Yii::trace(get_class($event->sender) . ' is inserted.');
});
```
The code above defines a handler that will be triggered for every Active Record object's `EVENT_AFTER_INSERT` event.
Path Alias
----------
......
......@@ -358,13 +358,13 @@ class Component extends Object
public function hasEventHandlers($name)
{
$this->ensureBehaviors();
return !empty($this->_events[$name]);
return !empty($this->_events[$name]) || Event::hasHandlers($this, $name);
}
/**
* Attaches an event handler to an event.
*
* An event handler must be a valid PHP callback. The followings are
* The event handler must be a valid PHP callback. The followings are
* some examples:
*
* ~~~
......@@ -374,7 +374,7 @@ class Component extends Object
* 'handleClick' // global function handleClick()
* ~~~
*
* An event handler must be defined with the following signature,
* The event handler must be defined with the following signature,
*
* ~~~
* function ($event)
......@@ -455,6 +455,7 @@ class Component extends Object
}
}
}
Event::trigger($this, $name, $event);
}
/**
......
......@@ -45,4 +45,136 @@ class Event extends Object
* Note that this varies according to which event handler is currently executing.
*/
public $data;
private static $_events = [];
/**
* Attaches an event handler to a class-level event.
*
* When a class-level event is triggered, event handlers attached
* to that class and all parent classes will be invoked.
*
* For example, the following code attaches an event handler to `ActiveRecord`'s
* `afterInsert` event:
*
* ~~~
* Event::on([ActiveRecord::className(), ActiveRecord::EVENT_AFTER_INSERT], function ($event) {
* Yii::trace(get_class($event->sender) . ' is inserted.');
* });
* ~~~
*
* The handler will be invoked for EVERY successful ActiveRecord insertion.
*
* For more details about how to declare an event handler, please refer to [[Component::on()]].
*
* @param string $class the fully qualified class name to which the event handler needs to attach
* @param string $name the event name
* @param callback $handler the event handler
* @param mixed $data the data to be passed to the event handler when the event is triggered.
* When the event handler is invoked, this data can be accessed via [[Event::data]].
* @see off()
*/
public static function on($class, $name, $handler, $data = null)
{
self::$_events[$name][ltrim($class, '\\')][] = [$handler, $data];
}
/**
* Detaches an event handler from a class-level event.
*
* This method is the opposite of [[on()]].
*
* @param string $class the fully qualified class name from which the event handler needs to be detached
* @param string $name the event name
* @param callback $handler the event handler to be removed.
* If it is null, all handlers attached to the named event will be removed.
* @return boolean if a handler is found and detached
* @see on()
*/
public static function off($class, $name, $handler = null)
{
$class = ltrim($class, '\\');
if (empty(self::$_events[$name][$class])) {
return false;
}
if ($handler === null) {
unset(self::$_events[$name][$class]);
return true;
} else {
$removed = false;
foreach (self::$_events[$name][$class] as $i => $event) {
if ($event[0] === $handler) {
unset(self::$_events[$name][$class][$i]);
$removed = true;
}
}
if ($removed) {
self::$_events[$name][$class] = array_values(self::$_events[$name][$class]);
}
return $removed;
}
}
/**
* Returns a value indicating whether there is any handler attached to the specified class-level event.
* Note that this method will also check all parent classes to see if there is any handler attached
* to the named event.
* @param string|object $class the object or the fully qualified class name specifying the class-level event
* @param string $name the event name
* @return boolean whether there is any handler attached to the event.
*/
public static function hasHandlers($class, $name)
{
if (empty(self::$_events[$name])) {
return false;
}
if (is_object($class)) {
$class = get_class($class);
} else {
$class = ltrim($class, '\\');
}
do {
if (!empty(self::$_events[$name][$class])) {
return true;
}
} while (($class = get_parent_class($class)) !== false);
return false;
}
/**
* Triggers a class-level event.
* This method will cause invocation of event handlers that are attached to the named event
* for the specified class and all its parent classes.
* @param string|object $class the object or the fully qualified class name specifying the class-level event
* @param string $name the event name
* @param Event $event the event parameter. If not set, a default [[Event]] object will be created.
*/
public static function trigger($class, $name, $event = null)
{
if (empty(self::$_events[$name])) {
return;
}
if ($event === null) {
$event = new self;
}
$event->handled = false;
$event->name = $name;
if (is_object($class)) {
$class = get_class($class);
} else {
$class = ltrim($class, '\\');
}
do {
if (!empty(self::$_events[$name][$class])) {
foreach (self::$_events[$name][$class] as $handler) {
$event->data = $handler[1];
call_user_func($handler[0], $event);
if ($event instanceof Event && $event->handled) {
return;
}
}
}
} while (($class = get_parent_class($class)) !== false);
}
}
......@@ -111,6 +111,17 @@ abstract class Generator extends Model
}
/**
* Returns the list of auto complete values.
* The array keys are the attribute names, and the array values are the corresponding auto complete values.
* Auto complete values can also be callable typed in order one want to make postponed data generation.
* @return array the list of auto complete values
*/
public function autoCompleteData()
{
return [];
}
/**
* Returns the message to be displayed when the newly generated code is saved successfully.
* Child classes may override this method to customize the message.
* @return string the message to be displayed when the newly generated code is saved successfully.
......
......@@ -26,12 +26,14 @@ class GiiAsset extends AssetBundle
*/
public $css = [
'main.css',
'typeahead.js-bootstrap.css',
];
/**
* @inheritdoc
*/
public $js = [
'gii.js',
'typeahead.js',
];
/**
* @inheritdoc
......
......@@ -201,3 +201,11 @@ body {
.DifferencesInline .ChangeReplace del {
background: #e99;
}
/* additional styles for typeahead.js-bootstrap.css */
.twitter-typeahead {
display: block !important;
}
.twitter-typeahead .tt-hint {
padding: 6px 12px !important;
}
/*!
* typeahead.js 0.9.3
* https://github.com/twitter/typeahead
* Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT
*/
(function($) {
var VERSION = "0.9.3";
var utils = {
isMsie: function() {
var match = /(msie) ([\w.]+)/i.exec(navigator.userAgent);
return match ? parseInt(match[2], 10) : false;
},
isBlankString: function(str) {
return !str || /^\s*$/.test(str);
},
escapeRegExChars: function(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
},
isString: function(obj) {
return typeof obj === "string";
},
isNumber: function(obj) {
return typeof obj === "number";
},
isArray: $.isArray,
isFunction: $.isFunction,
isObject: $.isPlainObject,
isUndefined: function(obj) {
return typeof obj === "undefined";
},
bind: $.proxy,
bindAll: function(obj) {
var val;
for (var key in obj) {
$.isFunction(val = obj[key]) && (obj[key] = $.proxy(val, obj));
}
},
indexOf: function(haystack, needle) {
for (var i = 0; i < haystack.length; i++) {
if (haystack[i] === needle) {
return i;
}
}
return -1;
},
each: $.each,
map: $.map,
filter: $.grep,
every: function(obj, test) {
var result = true;
if (!obj) {
return result;
}
$.each(obj, function(key, val) {
if (!(result = test.call(null, val, key, obj))) {
return false;
}
});
return !!result;
},
some: function(obj, test) {
var result = false;
if (!obj) {
return result;
}
$.each(obj, function(key, val) {
if (result = test.call(null, val, key, obj)) {
return false;
}
});
return !!result;
},
mixin: $.extend,
getUniqueId: function() {
var counter = 0;
return function() {
return counter++;
};
}(),
defer: function(fn) {
setTimeout(fn, 0);
},
debounce: function(func, wait, immediate) {
var timeout, result;
return function() {
var context = this, args = arguments, later, callNow;
later = function() {
timeout = null;
if (!immediate) {
result = func.apply(context, args);
}
};
callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
result = func.apply(context, args);
}
return result;
};
},
throttle: function(func, wait) {
var context, args, timeout, result, previous, later;
previous = 0;
later = function() {
previous = new Date();
timeout = null;
result = func.apply(context, args);
};
return function() {
var now = new Date(), remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0) {
clearTimeout(timeout);
timeout = null;
previous = now;
result = func.apply(context, args);
} else if (!timeout) {
timeout = setTimeout(later, remaining);
}
return result;
};
},
tokenizeQuery: function(str) {
return $.trim(str).toLowerCase().split(/[\s]+/);
},
tokenizeText: function(str) {
return $.trim(str).toLowerCase().split(/[\s\-_]+/);
},
getProtocol: function() {
return location.protocol;
},
noop: function() {}
};
var EventTarget = function() {
var eventSplitter = /\s+/;
return {
on: function(events, callback) {
var event;
if (!callback) {
return this;
}
this._callbacks = this._callbacks || {};
events = events.split(eventSplitter);
while (event = events.shift()) {
this._callbacks[event] = this._callbacks[event] || [];
this._callbacks[event].push(callback);
}
return this;
},
trigger: function(events, data) {
var event, callbacks;
if (!this._callbacks) {
return this;
}
events = events.split(eventSplitter);
while (event = events.shift()) {
if (callbacks = this._callbacks[event]) {
for (var i = 0; i < callbacks.length; i += 1) {
callbacks[i].call(this, {
type: event,
data: data
});
}
}
}
return this;
}
};
}();
var EventBus = function() {
var namespace = "typeahead:";
function EventBus(o) {
if (!o || !o.el) {
$.error("EventBus initialized without el");
}
this.$el = $(o.el);
}
utils.mixin(EventBus.prototype, {
trigger: function(type) {
var args = [].slice.call(arguments, 1);
this.$el.trigger(namespace + type, args);
}
});
return EventBus;
}();
var PersistentStorage = function() {
var ls, methods;
try {
ls = window.localStorage;
ls.setItem("~~~", "!");
ls.removeItem("~~~");
} catch (err) {
ls = null;
}
function PersistentStorage(namespace) {
this.prefix = [ "__", namespace, "__" ].join("");
this.ttlKey = "__ttl__";
this.keyMatcher = new RegExp("^" + this.prefix);
}
if (ls && window.JSON) {
methods = {
_prefix: function(key) {
return this.prefix + key;
},
_ttlKey: function(key) {
return this._prefix(key) + this.ttlKey;
},
get: function(key) {
if (this.isExpired(key)) {
this.remove(key);
}
return decode(ls.getItem(this._prefix(key)));
},
set: function(key, val, ttl) {
if (utils.isNumber(ttl)) {
ls.setItem(this._ttlKey(key), encode(now() + ttl));
} else {
ls.removeItem(this._ttlKey(key));
}
return ls.setItem(this._prefix(key), encode(val));
},
remove: function(key) {
ls.removeItem(this._ttlKey(key));
ls.removeItem(this._prefix(key));
return this;
},
clear: function() {
var i, key, keys = [], len = ls.length;
for (i = 0; i < len; i++) {
if ((key = ls.key(i)).match(this.keyMatcher)) {
keys.push(key.replace(this.keyMatcher, ""));
}
}
for (i = keys.length; i--; ) {
this.remove(keys[i]);
}
return this;
},
isExpired: function(key) {
var ttl = decode(ls.getItem(this._ttlKey(key)));
return utils.isNumber(ttl) && now() > ttl ? true : false;
}
};
} else {
methods = {
get: utils.noop,
set: utils.noop,
remove: utils.noop,
clear: utils.noop,
isExpired: utils.noop
};
}
utils.mixin(PersistentStorage.prototype, methods);
return PersistentStorage;
function now() {
return new Date().getTime();
}
function encode(val) {
return JSON.stringify(utils.isUndefined(val) ? null : val);
}
function decode(val) {
return JSON.parse(val);
}
}();
var RequestCache = function() {
function RequestCache(o) {
utils.bindAll(this);
o = o || {};
this.sizeLimit = o.sizeLimit || 10;
this.cache = {};
this.cachedKeysByAge = [];
}
utils.mixin(RequestCache.prototype, {
get: function(url) {
return this.cache[url];
},
set: function(url, resp) {
var requestToEvict;
if (this.cachedKeysByAge.length === this.sizeLimit) {
requestToEvict = this.cachedKeysByAge.shift();
delete this.cache[requestToEvict];
}
this.cache[url] = resp;
this.cachedKeysByAge.push(url);
}
});
return RequestCache;
}();
var Transport = function() {
var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests, requestCache;
function Transport(o) {
utils.bindAll(this);
o = utils.isString(o) ? {
url: o
} : o;
requestCache = requestCache || new RequestCache();
maxPendingRequests = utils.isNumber(o.maxParallelRequests) ? o.maxParallelRequests : maxPendingRequests || 6;
this.url = o.url;
this.wildcard = o.wildcard || "%QUERY";
this.filter = o.filter;
this.replace = o.replace;
this.ajaxSettings = {
type: "get",
cache: o.cache,
timeout: o.timeout,
dataType: o.dataType || "json",
beforeSend: o.beforeSend
};
this._get = (/^throttle$/i.test(o.rateLimitFn) ? utils.throttle : utils.debounce)(this._get, o.rateLimitWait || 300);
}
utils.mixin(Transport.prototype, {
_get: function(url, cb) {
var that = this;
if (belowPendingRequestsThreshold()) {
this._sendRequest(url).done(done);
} else {
this.onDeckRequestArgs = [].slice.call(arguments, 0);
}
function done(resp) {
var data = that.filter ? that.filter(resp) : resp;
cb && cb(data);
requestCache.set(url, resp);
}
},
_sendRequest: function(url) {
var that = this, jqXhr = pendingRequests[url];
if (!jqXhr) {
incrementPendingRequests();
jqXhr = pendingRequests[url] = $.ajax(url, this.ajaxSettings).always(always);
}
return jqXhr;
function always() {
decrementPendingRequests();
pendingRequests[url] = null;
if (that.onDeckRequestArgs) {
that._get.apply(that, that.onDeckRequestArgs);
that.onDeckRequestArgs = null;
}
}
},
get: function(query, cb) {
var that = this, encodedQuery = encodeURIComponent(query || ""), url, resp;
cb = cb || utils.noop;
url = this.replace ? this.replace(this.url, encodedQuery) : this.url.replace(this.wildcard, encodedQuery);
if (resp = requestCache.get(url)) {
utils.defer(function() {
cb(that.filter ? that.filter(resp) : resp);
});
} else {
this._get(url, cb);
}
return !!resp;
}
});
return Transport;
function incrementPendingRequests() {
pendingRequestsCount++;
}
function decrementPendingRequests() {
pendingRequestsCount--;
}
function belowPendingRequestsThreshold() {
return pendingRequestsCount < maxPendingRequests;
}
}();
var Dataset = function() {
var keys = {
thumbprint: "thumbprint",
protocol: "protocol",
itemHash: "itemHash",
adjacencyList: "adjacencyList"
};
function Dataset(o) {
utils.bindAll(this);
if (utils.isString(o.template) && !o.engine) {
$.error("no template engine specified");
}
if (!o.local && !o.prefetch && !o.remote) {
$.error("one of local, prefetch, or remote is required");
}
this.name = o.name || utils.getUniqueId();
this.limit = o.limit || 5;
this.minLength = o.minLength || 1;
this.header = o.header;
this.footer = o.footer;
this.valueKey = o.valueKey || "value";
this.template = compileTemplate(o.template, o.engine, this.valueKey);
this.local = o.local;
this.prefetch = o.prefetch;
this.remote = o.remote;
this.itemHash = {};
this.adjacencyList = {};
this.storage = o.name ? new PersistentStorage(o.name) : null;
}
utils.mixin(Dataset.prototype, {
_processLocalData: function(data) {
this._mergeProcessedData(this._processData(data));
},
_loadPrefetchData: function(o) {
var that = this, thumbprint = VERSION + (o.thumbprint || ""), storedThumbprint, storedProtocol, storedItemHash, storedAdjacencyList, isExpired, deferred;
if (this.storage) {
storedThumbprint = this.storage.get(keys.thumbprint);
storedProtocol = this.storage.get(keys.protocol);
storedItemHash = this.storage.get(keys.itemHash);
storedAdjacencyList = this.storage.get(keys.adjacencyList);
}
isExpired = storedThumbprint !== thumbprint || storedProtocol !== utils.getProtocol();
o = utils.isString(o) ? {
url: o
} : o;
o.ttl = utils.isNumber(o.ttl) ? o.ttl : 24 * 60 * 60 * 1e3;
if (storedItemHash && storedAdjacencyList && !isExpired) {
this._mergeProcessedData({
itemHash: storedItemHash,
adjacencyList: storedAdjacencyList
});
deferred = $.Deferred().resolve();
} else {
deferred = $.getJSON(o.url).done(processPrefetchData);
}
return deferred;
function processPrefetchData(data) {
var filteredData = o.filter ? o.filter(data) : data, processedData = that._processData(filteredData), itemHash = processedData.itemHash, adjacencyList = processedData.adjacencyList;
if (that.storage) {
that.storage.set(keys.itemHash, itemHash, o.ttl);
that.storage.set(keys.adjacencyList, adjacencyList, o.ttl);
that.storage.set(keys.thumbprint, thumbprint, o.ttl);
that.storage.set(keys.protocol, utils.getProtocol(), o.ttl);
}
that._mergeProcessedData(processedData);
}
},
_transformDatum: function(datum) {
var value = utils.isString(datum) ? datum : datum[this.valueKey], tokens = datum.tokens || utils.tokenizeText(value), item = {
value: value,
tokens: tokens
};
if (utils.isString(datum)) {
item.datum = {};
item.datum[this.valueKey] = datum;
} else {
item.datum = datum;
}
item.tokens = utils.filter(item.tokens, function(token) {
return !utils.isBlankString(token);
});
item.tokens = utils.map(item.tokens, function(token) {
return token.toLowerCase();
});
return item;
},
_processData: function(data) {
var that = this, itemHash = {}, adjacencyList = {};
utils.each(data, function(i, datum) {
var item = that._transformDatum(datum), id = utils.getUniqueId(item.value);
itemHash[id] = item;
utils.each(item.tokens, function(i, token) {
var character = token.charAt(0), adjacency = adjacencyList[character] || (adjacencyList[character] = [ id ]);
!~utils.indexOf(adjacency, id) && adjacency.push(id);
});
});
return {
itemHash: itemHash,
adjacencyList: adjacencyList
};
},
_mergeProcessedData: function(processedData) {
var that = this;
utils.mixin(this.itemHash, processedData.itemHash);
utils.each(processedData.adjacencyList, function(character, adjacency) {
var masterAdjacency = that.adjacencyList[character];
that.adjacencyList[character] = masterAdjacency ? masterAdjacency.concat(adjacency) : adjacency;
});
},
_getLocalSuggestions: function(terms) {
var that = this, firstChars = [], lists = [], shortestList, suggestions = [];
utils.each(terms, function(i, term) {
var firstChar = term.charAt(0);
!~utils.indexOf(firstChars, firstChar) && firstChars.push(firstChar);
});
utils.each(firstChars, function(i, firstChar) {
var list = that.adjacencyList[firstChar];
if (!list) {
return false;
}
lists.push(list);
if (!shortestList || list.length < shortestList.length) {
shortestList = list;
}
});
if (lists.length < firstChars.length) {
return [];
}
utils.each(shortestList, function(i, id) {
var item = that.itemHash[id], isCandidate, isMatch;
isCandidate = utils.every(lists, function(list) {
return ~utils.indexOf(list, id);
});
isMatch = isCandidate && utils.every(terms, function(term) {
return utils.some(item.tokens, function(token) {
return token.indexOf(term) === 0;
});
});
isMatch && suggestions.push(item);
});
return suggestions;
},
initialize: function() {
var deferred;
this.local && this._processLocalData(this.local);
this.transport = this.remote ? new Transport(this.remote) : null;
deferred = this.prefetch ? this._loadPrefetchData(this.prefetch) : $.Deferred().resolve();
this.local = this.prefetch = this.remote = null;
this.initialize = function() {
return deferred;
};
return deferred;
},
getSuggestions: function(query, cb) {
var that = this, terms, suggestions, cacheHit = false;
if (query.length < this.minLength) {
return;
}
terms = utils.tokenizeQuery(query);
suggestions = this._getLocalSuggestions(terms).slice(0, this.limit);
if (suggestions.length < this.limit && this.transport) {
cacheHit = this.transport.get(query, processRemoteData);
}
!cacheHit && cb && cb(suggestions);
function processRemoteData(data) {
suggestions = suggestions.slice(0);
utils.each(data, function(i, datum) {
var item = that._transformDatum(datum), isDuplicate;
isDuplicate = utils.some(suggestions, function(suggestion) {
return item.value === suggestion.value;
});
!isDuplicate && suggestions.push(item);
return suggestions.length < that.limit;
});
cb && cb(suggestions);
}
}
});
return Dataset;
function compileTemplate(template, engine, valueKey) {
var renderFn, compiledTemplate;
if (utils.isFunction(template)) {
renderFn = template;
} else if (utils.isString(template)) {
compiledTemplate = engine.compile(template);
renderFn = utils.bind(compiledTemplate.render, compiledTemplate);
} else {
renderFn = function(context) {
return "<p>" + context[valueKey] + "</p>";
};
}
return renderFn;
}
}();
var InputView = function() {
function InputView(o) {
var that = this;
utils.bindAll(this);
this.specialKeyCodeMap = {
9: "tab",
27: "esc",
37: "left",
39: "right",
13: "enter",
38: "up",
40: "down"
};
this.$hint = $(o.hint);
this.$input = $(o.input).on("blur.tt", this._handleBlur).on("focus.tt", this._handleFocus).on("keydown.tt", this._handleSpecialKeyEvent);
if (!utils.isMsie()) {
this.$input.on("input.tt", this._compareQueryToInputValue);
} else {
this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) {
if (that.specialKeyCodeMap[$e.which || $e.keyCode]) {
return;
}
utils.defer(that._compareQueryToInputValue);
});
}
this.query = this.$input.val();
this.$overflowHelper = buildOverflowHelper(this.$input);
}
utils.mixin(InputView.prototype, EventTarget, {
_handleFocus: function() {
this.trigger("focused");
},
_handleBlur: function() {
this.trigger("blured");
},
_handleSpecialKeyEvent: function($e) {
var keyName = this.specialKeyCodeMap[$e.which || $e.keyCode];
keyName && this.trigger(keyName + "Keyed", $e);
},
_compareQueryToInputValue: function() {
var inputValue = this.getInputValue(), isSameQuery = compareQueries(this.query, inputValue), isSameQueryExceptWhitespace = isSameQuery ? this.query.length !== inputValue.length : false;
if (isSameQueryExceptWhitespace) {
this.trigger("whitespaceChanged", {
value: this.query
});
} else if (!isSameQuery) {
this.trigger("queryChanged", {
value: this.query = inputValue
});
}
},
destroy: function() {
this.$hint.off(".tt");
this.$input.off(".tt");
this.$hint = this.$input = this.$overflowHelper = null;
},
focus: function() {
this.$input.focus();
},
blur: function() {
this.$input.blur();
},
getQuery: function() {
return this.query;
},
setQuery: function(query) {
this.query = query;
},
getInputValue: function() {
return this.$input.val();
},
setInputValue: function(value, silent) {
this.$input.val(value);
!silent && this._compareQueryToInputValue();
},
getHintValue: function() {
return this.$hint.val();
},
setHintValue: function(value) {
this.$hint.val(value);
},
getLanguageDirection: function() {
return (this.$input.css("direction") || "ltr").toLowerCase();
},
isOverflow: function() {
this.$overflowHelper.text(this.getInputValue());
return this.$overflowHelper.width() > this.$input.width();
},
isCursorAtEnd: function() {
var valueLength = this.$input.val().length, selectionStart = this.$input[0].selectionStart, range;
if (utils.isNumber(selectionStart)) {
return selectionStart === valueLength;
} else if (document.selection) {
range = document.selection.createRange();
range.moveStart("character", -valueLength);
return valueLength === range.text.length;
}
return true;
}
});
return InputView;
function buildOverflowHelper($input) {
return $("<span></span>").css({
position: "absolute",
left: "-9999px",
visibility: "hidden",
whiteSpace: "nowrap",
fontFamily: $input.css("font-family"),
fontSize: $input.css("font-size"),
fontStyle: $input.css("font-style"),
fontVariant: $input.css("font-variant"),
fontWeight: $input.css("font-weight"),
wordSpacing: $input.css("word-spacing"),
letterSpacing: $input.css("letter-spacing"),
textIndent: $input.css("text-indent"),
textRendering: $input.css("text-rendering"),
textTransform: $input.css("text-transform")
}).insertAfter($input);
}
function compareQueries(a, b) {
a = (a || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
b = (b || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
return a === b;
}
}();
var DropdownView = function() {
var html = {
suggestionsList: '<span class="tt-suggestions"></span>'
}, css = {
suggestionsList: {
display: "block"
},
suggestion: {
whiteSpace: "nowrap",
cursor: "pointer"
},
suggestionChild: {
whiteSpace: "normal"
}
};
function DropdownView(o) {
utils.bindAll(this);
this.isOpen = false;
this.isEmpty = true;
this.isMouseOverDropdown = false;
this.$menu = $(o.menu).on("mouseenter.tt", this._handleMouseenter).on("mouseleave.tt", this._handleMouseleave).on("click.tt", ".tt-suggestion", this._handleSelection).on("mouseover.tt", ".tt-suggestion", this._handleMouseover);
}
utils.mixin(DropdownView.prototype, EventTarget, {
_handleMouseenter: function() {
this.isMouseOverDropdown = true;
},
_handleMouseleave: function() {
this.isMouseOverDropdown = false;
},
_handleMouseover: function($e) {
var $suggestion = $($e.currentTarget);
this._getSuggestions().removeClass("tt-is-under-cursor");
$suggestion.addClass("tt-is-under-cursor");
},
_handleSelection: function($e) {
var $suggestion = $($e.currentTarget);
this.trigger("suggestionSelected", extractSuggestion($suggestion));
},
_show: function() {
this.$menu.css("display", "block");
},
_hide: function() {
this.$menu.hide();
},
_moveCursor: function(increment) {
var $suggestions, $cur, nextIndex, $underCursor;
if (!this.isVisible()) {
return;
}
$suggestions = this._getSuggestions();
$cur = $suggestions.filter(".tt-is-under-cursor");
$cur.removeClass("tt-is-under-cursor");
nextIndex = $suggestions.index($cur) + increment;
nextIndex = (nextIndex + 1) % ($suggestions.length + 1) - 1;
if (nextIndex === -1) {
this.trigger("cursorRemoved");
return;
} else if (nextIndex < -1) {
nextIndex = $suggestions.length - 1;
}
$underCursor = $suggestions.eq(nextIndex).addClass("tt-is-under-cursor");
this._ensureVisibility($underCursor);
this.trigger("cursorMoved", extractSuggestion($underCursor));
},
_getSuggestions: function() {
return this.$menu.find(".tt-suggestions > .tt-suggestion");
},
_ensureVisibility: function($el) {
var menuHeight = this.$menu.height() + parseInt(this.$menu.css("paddingTop"), 10) + parseInt(this.$menu.css("paddingBottom"), 10), menuScrollTop = this.$menu.scrollTop(), elTop = $el.position().top, elBottom = elTop + $el.outerHeight(true);
if (elTop < 0) {
this.$menu.scrollTop(menuScrollTop + elTop);
} else if (menuHeight < elBottom) {
this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight));
}
},
destroy: function() {
this.$menu.off(".tt");
this.$menu = null;
},
isVisible: function() {
return this.isOpen && !this.isEmpty;
},
closeUnlessMouseIsOverDropdown: function() {
if (!this.isMouseOverDropdown) {
this.close();
}
},
close: function() {
if (this.isOpen) {
this.isOpen = false;
this.isMouseOverDropdown = false;
this._hide();
this.$menu.find(".tt-suggestions > .tt-suggestion").removeClass("tt-is-under-cursor");
this.trigger("closed");
}
},
open: function() {
if (!this.isOpen) {
this.isOpen = true;
!this.isEmpty && this._show();
this.trigger("opened");
}
},
setLanguageDirection: function(dir) {
var ltrCss = {
left: "0",
right: "auto"
}, rtlCss = {
left: "auto",
right: " 0"
};
dir === "ltr" ? this.$menu.css(ltrCss) : this.$menu.css(rtlCss);
},
moveCursorUp: function() {
this._moveCursor(-1);
},
moveCursorDown: function() {
this._moveCursor(+1);
},
getSuggestionUnderCursor: function() {
var $suggestion = this._getSuggestions().filter(".tt-is-under-cursor").first();
return $suggestion.length > 0 ? extractSuggestion($suggestion) : null;
},
getFirstSuggestion: function() {
var $suggestion = this._getSuggestions().first();
return $suggestion.length > 0 ? extractSuggestion($suggestion) : null;
},
renderSuggestions: function(dataset, suggestions) {
var datasetClassName = "tt-dataset-" + dataset.name, wrapper = '<div class="tt-suggestion">%body</div>', compiledHtml, $suggestionsList, $dataset = this.$menu.find("." + datasetClassName), elBuilder, fragment, $el;
if ($dataset.length === 0) {
$suggestionsList = $(html.suggestionsList).css(css.suggestionsList);
$dataset = $("<div></div>").addClass(datasetClassName).append(dataset.header).append($suggestionsList).append(dataset.footer).appendTo(this.$menu);
}
if (suggestions.length > 0) {
this.isEmpty = false;
this.isOpen && this._show();
elBuilder = document.createElement("div");
fragment = document.createDocumentFragment();
utils.each(suggestions, function(i, suggestion) {
suggestion.dataset = dataset.name;
compiledHtml = dataset.template(suggestion.datum);
elBuilder.innerHTML = wrapper.replace("%body", compiledHtml);
$el = $(elBuilder.firstChild).css(css.suggestion).data("suggestion", suggestion);
$el.children().each(function() {
$(this).css(css.suggestionChild);
});
fragment.appendChild($el[0]);
});
$dataset.show().find(".tt-suggestions").html(fragment);
} else {
this.clearSuggestions(dataset.name);
}
this.trigger("suggestionsRendered");
},
clearSuggestions: function(datasetName) {
var $datasets = datasetName ? this.$menu.find(".tt-dataset-" + datasetName) : this.$menu.find('[class^="tt-dataset-"]'), $suggestions = $datasets.find(".tt-suggestions");
$datasets.hide();
$suggestions.empty();
if (this._getSuggestions().length === 0) {
this.isEmpty = true;
this._hide();
}
}
});
return DropdownView;
function extractSuggestion($el) {
return $el.data("suggestion");
}
}();
var TypeaheadView = function() {
var html = {
wrapper: '<span class="twitter-typeahead"></span>',
hint: '<input class="tt-hint" type="text" autocomplete="off" spellcheck="off" disabled>',
dropdown: '<span class="tt-dropdown-menu"></span>'
}, css = {
wrapper: {
position: "relative",
display: "inline-block"
},
hint: {
position: "absolute",
top: "0",
left: "0",
borderColor: "transparent",
boxShadow: "none"
},
query: {
position: "relative",
verticalAlign: "top",
backgroundColor: "transparent"
},
dropdown: {
position: "absolute",
top: "100%",
left: "0",
zIndex: "100",
display: "none"
}
};
if (utils.isMsie()) {
utils.mixin(css.query, {
backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"
});
}
if (utils.isMsie() && utils.isMsie() <= 7) {
utils.mixin(css.wrapper, {
display: "inline",
zoom: "1"
});
utils.mixin(css.query, {
marginTop: "-1px"
});
}
function TypeaheadView(o) {
var $menu, $input, $hint;
utils.bindAll(this);
this.$node = buildDomStructure(o.input);
this.datasets = o.datasets;
this.dir = null;
this.eventBus = o.eventBus;
$menu = this.$node.find(".tt-dropdown-menu");
$input = this.$node.find(".tt-query");
$hint = this.$node.find(".tt-hint");
this.dropdownView = new DropdownView({
menu: $menu
}).on("suggestionSelected", this._handleSelection).on("cursorMoved", this._clearHint).on("cursorMoved", this._setInputValueToSuggestionUnderCursor).on("cursorRemoved", this._setInputValueToQuery).on("cursorRemoved", this._updateHint).on("suggestionsRendered", this._updateHint).on("opened", this._updateHint).on("closed", this._clearHint).on("opened closed", this._propagateEvent);
this.inputView = new InputView({
input: $input,
hint: $hint
}).on("focused", this._openDropdown).on("blured", this._closeDropdown).on("blured", this._setInputValueToQuery).on("enterKeyed tabKeyed", this._handleSelection).on("queryChanged", this._clearHint).on("queryChanged", this._clearSuggestions).on("queryChanged", this._getSuggestions).on("whitespaceChanged", this._updateHint).on("queryChanged whitespaceChanged", this._openDropdown).on("queryChanged whitespaceChanged", this._setLanguageDirection).on("escKeyed", this._closeDropdown).on("escKeyed", this._setInputValueToQuery).on("tabKeyed upKeyed downKeyed", this._managePreventDefault).on("upKeyed downKeyed", this._moveDropdownCursor).on("upKeyed downKeyed", this._openDropdown).on("tabKeyed leftKeyed rightKeyed", this._autocomplete);
}
utils.mixin(TypeaheadView.prototype, EventTarget, {
_managePreventDefault: function(e) {
var $e = e.data, hint, inputValue, preventDefault = false;
switch (e.type) {
case "tabKeyed":
hint = this.inputView.getHintValue();
inputValue = this.inputView.getInputValue();
preventDefault = hint && hint !== inputValue;
break;
case "upKeyed":
case "downKeyed":
preventDefault = !$e.shiftKey && !$e.ctrlKey && !$e.metaKey;
break;
}
preventDefault && $e.preventDefault();
},
_setLanguageDirection: function() {
var dir = this.inputView.getLanguageDirection();
if (dir !== this.dir) {
this.dir = dir;
this.$node.css("direction", dir);
this.dropdownView.setLanguageDirection(dir);
}
},
_updateHint: function() {
var suggestion = this.dropdownView.getFirstSuggestion(), hint = suggestion ? suggestion.value : null, dropdownIsVisible = this.dropdownView.isVisible(), inputHasOverflow = this.inputView.isOverflow(), inputValue, query, escapedQuery, beginsWithQuery, match;
if (hint && dropdownIsVisible && !inputHasOverflow) {
inputValue = this.inputView.getInputValue();
query = inputValue.replace(/\s{2,}/g, " ").replace(/^\s+/g, "");
escapedQuery = utils.escapeRegExChars(query);
beginsWithQuery = new RegExp("^(?:" + escapedQuery + ")(.*$)", "i");
match = beginsWithQuery.exec(hint);
this.inputView.setHintValue(inputValue + (match ? match[1] : ""));
}
},
_clearHint: function() {
this.inputView.setHintValue("");
},
_clearSuggestions: function() {
this.dropdownView.clearSuggestions();
},
_setInputValueToQuery: function() {
this.inputView.setInputValue(this.inputView.getQuery());
},
_setInputValueToSuggestionUnderCursor: function(e) {
var suggestion = e.data;
this.inputView.setInputValue(suggestion.value, true);
},
_openDropdown: function() {
this.dropdownView.open();
},
_closeDropdown: function(e) {
this.dropdownView[e.type === "blured" ? "closeUnlessMouseIsOverDropdown" : "close"]();
},
_moveDropdownCursor: function(e) {
var $e = e.data;
if (!$e.shiftKey && !$e.ctrlKey && !$e.metaKey) {
this.dropdownView[e.type === "upKeyed" ? "moveCursorUp" : "moveCursorDown"]();
}
},
_handleSelection: function(e) {
var byClick = e.type === "suggestionSelected", suggestion = byClick ? e.data : this.dropdownView.getSuggestionUnderCursor();
if (suggestion) {
this.inputView.setInputValue(suggestion.value);
byClick ? this.inputView.focus() : e.data.preventDefault();
byClick && utils.isMsie() ? utils.defer(this.dropdownView.close) : this.dropdownView.close();
this.eventBus.trigger("selected", suggestion.datum, suggestion.dataset);
}
},
_getSuggestions: function() {
var that = this, query = this.inputView.getQuery();
if (utils.isBlankString(query)) {
return;
}
utils.each(this.datasets, function(i, dataset) {
dataset.getSuggestions(query, function(suggestions) {
if (query === that.inputView.getQuery()) {
that.dropdownView.renderSuggestions(dataset, suggestions);
}
});
});
},
_autocomplete: function(e) {
var isCursorAtEnd, ignoreEvent, query, hint, suggestion;
if (e.type === "rightKeyed" || e.type === "leftKeyed") {
isCursorAtEnd = this.inputView.isCursorAtEnd();
ignoreEvent = this.inputView.getLanguageDirection() === "ltr" ? e.type === "leftKeyed" : e.type === "rightKeyed";
if (!isCursorAtEnd || ignoreEvent) {
return;
}
}
query = this.inputView.getQuery();
hint = this.inputView.getHintValue();
if (hint !== "" && query !== hint) {
suggestion = this.dropdownView.getFirstSuggestion();
this.inputView.setInputValue(suggestion.value);
this.eventBus.trigger("autocompleted", suggestion.datum, suggestion.dataset);
}
},
_propagateEvent: function(e) {
this.eventBus.trigger(e.type);
},
destroy: function() {
this.inputView.destroy();
this.dropdownView.destroy();
destroyDomStructure(this.$node);
this.$node = null;
},
setQuery: function(query) {
this.inputView.setQuery(query);
this.inputView.setInputValue(query);
this._clearHint();
this._clearSuggestions();
this._getSuggestions();
}
});
return TypeaheadView;
function buildDomStructure(input) {
var $wrapper = $(html.wrapper), $dropdown = $(html.dropdown), $input = $(input), $hint = $(html.hint);
$wrapper = $wrapper.css(css.wrapper);
$dropdown = $dropdown.css(css.dropdown);
$hint.css(css.hint).css({
backgroundAttachment: $input.css("background-attachment"),
backgroundClip: $input.css("background-clip"),
backgroundColor: $input.css("background-color"),
backgroundImage: $input.css("background-image"),
backgroundOrigin: $input.css("background-origin"),
backgroundPosition: $input.css("background-position"),
backgroundRepeat: $input.css("background-repeat"),
backgroundSize: $input.css("background-size")
});
$input.data("ttAttrs", {
dir: $input.attr("dir"),
autocomplete: $input.attr("autocomplete"),
spellcheck: $input.attr("spellcheck"),
style: $input.attr("style")
});
$input.addClass("tt-query").attr({
autocomplete: "off",
spellcheck: false
}).css(css.query);
try {
!$input.attr("dir") && $input.attr("dir", "auto");
} catch (e) {}
return $input.wrap($wrapper).parent().prepend($hint).append($dropdown);
}
function destroyDomStructure($node) {
var $input = $node.find(".tt-query");
utils.each($input.data("ttAttrs"), function(key, val) {
utils.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val);
});
$input.detach().removeData("ttAttrs").removeClass("tt-query").insertAfter($node);
$node.remove();
}
}();
(function() {
var cache = {}, viewKey = "ttView", methods;
methods = {
initialize: function(datasetDefs) {
var datasets;
datasetDefs = utils.isArray(datasetDefs) ? datasetDefs : [ datasetDefs ];
if (datasetDefs.length === 0) {
$.error("no datasets provided");
}
datasets = utils.map(datasetDefs, function(o) {
var dataset = cache[o.name] ? cache[o.name] : new Dataset(o);
if (o.name) {
cache[o.name] = dataset;
}
return dataset;
});
return this.each(initialize);
function initialize() {
var $input = $(this), deferreds, eventBus = new EventBus({
el: $input
});
deferreds = utils.map(datasets, function(dataset) {
return dataset.initialize();
});
$input.data(viewKey, new TypeaheadView({
input: $input,
eventBus: eventBus = new EventBus({
el: $input
}),
datasets: datasets
}));
$.when.apply($, deferreds).always(function() {
utils.defer(function() {
eventBus.trigger("initialized");
});
});
}
},
destroy: function() {
return this.each(destroy);
function destroy() {
var $this = $(this), view = $this.data(viewKey);
if (view) {
view.destroy();
$this.removeData(viewKey);
}
}
},
setQuery: function(query) {
return this.each(setQuery);
function setQuery() {
var view = $(this).data(viewKey);
view && view.setQuery(query);
}
}
};
jQuery.fn.typeahead = function(method) {
if (methods[method]) {
return methods[method].apply(this, [].slice.call(arguments, 1));
} else {
return methods.initialize.apply(this, arguments);
}
};
})();
})(window.jQuery);
\ No newline at end of file
/* always keep this link here when updating this file: https://github.com/jharding/typeahead.js-bootstrap.css */
.twitter-typeahead .tt-query,
.twitter-typeahead .tt-hint {
margin-bottom: 0;
}
.tt-dropdown-menu {
min-width: 160px;
margin-top: 2px;
padding: 5px 0;
background-color: #fff;
border: 1px solid #ccc;
border: 1px solid rgba(0,0,0,.2);
*border-right-width: 2px;
*border-bottom-width: 2px;
-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;
}
.tt-suggestion {
display: block;
padding: 3px 20px;
}
.tt-suggestion.tt-is-under-cursor {
color: #fff;
background-color: #0081c2;
background-image: -moz-linear-gradient(top, #0088cc, #0077b3);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3));
background-image: -webkit-linear-gradient(top, #0088cc, #0077b3);
background-image: -o-linear-gradient(top, #0088cc, #0077b3);
background-image: linear-gradient(to bottom, #0088cc, #0077b3);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0)
}
.tt-suggestion.tt-is-under-cursor a {
color: #fff;
}
.tt-suggestion p {
margin: 0;
}
......@@ -8,6 +8,7 @@
namespace yii\gii\components;
use yii\gii\Generator;
use yii\helpers\Json;
/**
* @author Qiang Xue <qiang.xue@gmail.com>
......@@ -30,10 +31,18 @@ class ActiveField extends \yii\widgets\ActiveField
if (isset($hints[$this->attribute])) {
$this->hint($hints[$this->attribute]);
}
$autoCompleteData = $this->model->autoCompleteData();
if (isset($autoCompleteData[$this->attribute])) {
if (is_callable($autoCompleteData[$this->attribute])) {
$this->autoComplete(call_user_func($autoCompleteData[$this->attribute]));
} else {
$this->autoComplete($autoCompleteData[$this->attribute]);
}
}
}
/**
* Makes filed remember its value between page reloads
* Makes field remember its value between page reloads
* @return static the field object itself
*/
public function sticky()
......@@ -41,4 +50,17 @@ class ActiveField extends \yii\widgets\ActiveField
$this->options['class'] .= ' sticky';
return $this;
}
/**
* Makes field auto completable
* @param array $data auto complete data (array of callables or scalars)
* @return static the field object itself
*/
public function autoComplete($data)
{
static $counter = 0;
$this->inputOptions['class'] .= ' typeahead-' . (++$counter);
$this->form->getView()->registerJs("jQuery('.typeahead-{$counter}').typeahead({local: " . Json::encode($data) . "});");
return $this;
}
}
......@@ -34,7 +34,7 @@ foreach ($generator->getTableSchema()->getColumnNames() as $attribute) {
if (++$count < 6) {
echo "\t\t<?= " . $generator->generateActiveSearchField($attribute) . " ?>\n\n";
} else {
echo "\t\t<?= // " . $generator->generateActiveSearchField($attribute) . " ?>\n\n";
echo "\t\t<?php // echo " . $generator->generateActiveSearchField($attribute) . " ?>\n\n";
}
}
?>
......
......@@ -113,6 +113,18 @@ class Generator extends \yii\gii\Generator
/**
* @inheritdoc
*/
public function autoCompleteData()
{
return [
'tableName' => function () {
return $this->getDbConnection()->getSchema()->getTableNames();
},
];
}
/**
* @inheritdoc
*/
public function requiredTemplates()
{
return ['model.php'];
......
......@@ -92,7 +92,7 @@ class MessageFormatter extends Component
return $this->fallbackFormat($pattern, $params, $language);
}
if (version_compare(PHP_VERSION, '5.5.0', '<')) {
if (version_compare(PHP_VERSION, '5.5.0', '<') || version_compare(INTL_ICU_VERSION, '4.8', '<')) {
$pattern = $this->replaceNamedArguments($pattern, $params);
$params = array_values($params);
}
......
......@@ -135,18 +135,21 @@ abstract class BaseListView extends Widget
$totalCount = $this->dataProvider->getTotalCount();
$begin = $pagination->getPage() * $pagination->pageSize + 1;
$end = $begin + $count - 1;
if ($begin > $end) {
$begin = $end;
}
$page = $pagination->getPage() + 1;
$pageCount = $pagination->pageCount;
if (($summaryContent = $this->summary) === null) {
$summaryContent = '<div class="summary">'
. Yii::t('yii', 'Showing <b>{totalCount, plural, zero{0} other{{begin, number, integer}-{end, number, integer}}}</b> of <b>{totalCount, number, integer}</b> {totalCount, plural, one{item} other{items}}.')
. Yii::t('yii', 'Showing <b>{begin, number}-{end, number}</b> of <b>{totalCount, number}</b> {totalCount, plural, one{item} other{items}}.')
. '</div>';
}
} else {
$begin = $page = $pageCount = 1;
$end = $totalCount = $count;
if (($summaryContent = $this->summary) === null) {
$summaryContent = '<div class="summary">' . Yii::t('yii', 'Total <b>{count}</b> {count, plural, one{item} other{items}}.') . '</div>';
$summaryContent = '<div class="summary">' . Yii::t('yii', 'Total <b>{count, number}</b> {count, plural, one{item} other{items}}.') . '</div>';
}
}
return Yii::$app->getI18n()->format($summaryContent, [
......
......@@ -302,7 +302,6 @@ class ComponentTest extends TestCase
$component->detachBehaviors();
$this->assertNull($component->getBehavior('a'));
$this->assertNull($component->getBehavior('b'));
}
}
......
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yiiunit\framework\base;
use yii\base\Component;
use yii\base\Event;
use yiiunit\TestCase;
/**
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class EventTest extends TestCase
{
public $counter;
public function setUp()
{
$this->counter = 0;
Event::off(ActiveRecord::className(), 'save');
Event::off(Post::className(), 'save');
Event::off(User::className(), 'save');
}
public function testOn()
{
Event::on(Post::className(), 'save', function ($event) {
$this->counter += 1;
});
Event::on(ActiveRecord::className(), 'save', function ($event) {
$this->counter += 3;
});
$this->assertEquals(0, $this->counter);
$post = new Post;
$post->save();
$this->assertEquals(4, $this->counter);
$user = new User;
$user->save();
$this->assertEquals(7, $this->counter);
}
public function testOff()
{
$handler = function ($event) {
$this->counter ++;
};
$this->assertFalse(Event::hasHandlers(Post::className(), 'save'));
Event::on(Post::className(), 'save', $handler);
$this->assertTrue(Event::hasHandlers(Post::className(), 'save'));
Event::off(Post::className(), 'save', $handler);
$this->assertFalse(Event::hasHandlers(Post::className(), 'save'));
}
public function testHasHandlers()
{
$this->assertFalse(Event::hasHandlers(Post::className(), 'save'));
$this->assertFalse(Event::hasHandlers(ActiveRecord::className(), 'save'));
Event::on(Post::className(), 'save', function ($event) {
$this->counter += 1;
});
$this->assertTrue(Event::hasHandlers(Post::className(), 'save'));
$this->assertFalse(Event::hasHandlers(ActiveRecord::className(), 'save'));
$this->assertFalse(Event::hasHandlers(User::className(), 'save'));
Event::on(ActiveRecord::className(), 'save', function ($event) {
$this->counter += 1;
});
$this->assertTrue(Event::hasHandlers(User::className(), 'save'));
$this->assertTrue(Event::hasHandlers(ActiveRecord::className(), 'save'));
}
}
class ActiveRecord extends Component
{
public function save()
{
$this->trigger('save');
}
}
class Post extends ActiveRecord
{
}
class User extends ActiveRecord
{
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment