Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
Y
yii2
Project
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
PSDI Army
yii2
Commits
1dcb63ca
Commit
1dcb63ca
authored
Oct 31, 2013
by
resurtm
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' of github.com:yiisoft/yii2
parents
2f360e53
f9a92b82
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
1499 additions
and
9 deletions
+1499
-9
i18n.md
docs/guide/i18n.md
+7
-0
upgrade-from-v1.md
docs/guide/upgrade-from-v1.md
+9
-0
Component.php
framework/yii/base/Component.php
+4
-3
Event.php
framework/yii/base/Event.php
+132
-0
Generator.php
framework/yii/gii/Generator.php
+11
-0
GiiAsset.php
framework/yii/gii/GiiAsset.php
+2
-0
main.css
framework/yii/gii/assets/main.css
+8
-0
typeahead.js
framework/yii/gii/assets/typeahead.js
+1140
-0
typeahead.js-bootstrap.css
framework/yii/gii/assets/typeahead.js-bootstrap.css
+51
-0
ActiveField.php
framework/yii/gii/components/ActiveField.php
+23
-1
_search.php
...ework/yii/gii/generators/crud/templates/views/_search.php
+1
-1
Generator.php
framework/yii/gii/generators/model/Generator.php
+12
-0
MessageFormatter.php
framework/yii/i18n/MessageFormatter.php
+1
-1
BaseListView.php
framework/yii/widgets/BaseListView.php
+5
-2
ComponentTest.php
tests/unit/framework/base/ComponentTest.php
+0
-1
EventTest.php
tests/unit/framework/base/EventTest.php
+93
-0
No files found.
docs/guide/i18n.md
View file @
1dcb63ca
...
...
@@ -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
)
.
...
...
docs/guide/upgrade-from-v1.md
View file @
1dcb63ca
...
...
@@ -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
----------
...
...
framework/yii/base/Component.php
View file @
1dcb63ca
...
...
@@ -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
);
}
/**
...
...
framework/yii/base/Event.php
View file @
1dcb63ca
...
...
@@ -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
);
}
}
framework/yii/gii/Generator.php
View file @
1dcb63ca
...
...
@@ -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.
...
...
framework/yii/gii/GiiAsset.php
View file @
1dcb63ca
...
...
@@ -26,12 +26,14 @@ class GiiAsset extends AssetBundle
*/
public
$css
=
[
'main.css'
,
'typeahead.js-bootstrap.css'
,
];
/**
* @inheritdoc
*/
public
$js
=
[
'gii.js'
,
'typeahead.js'
,
];
/**
* @inheritdoc
...
...
framework/yii/gii/assets/main.css
View file @
1dcb63ca
...
...
@@ -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
;
}
framework/yii/gii/assets/typeahead.js
0 → 100644
View file @
1dcb63ca
/*!
* 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
*
1
e3
;
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
framework/yii/gii/assets/typeahead.js-bootstrap.css
0 → 100644
View file @
1dcb63ca
/* 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
;
}
framework/yii/gii/components/ActiveField.php
View file @
1dcb63ca
...
...
@@ -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 fi
le
d remember its value between page reloads
* Makes fi
el
d 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
;
}
}
framework/yii/gii/generators/crud/templates/views/_search.php
View file @
1dcb63ca
...
...
@@ -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
"
;
}
}
?>
...
...
framework/yii/gii/generators/model/Generator.php
View file @
1dcb63ca
...
...
@@ -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'
];
...
...
framework/yii/i18n/MessageFormatter.php
View file @
1dcb63ca
...
...
@@ -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
);
}
...
...
framework/yii/widgets/BaseListView.php
View file @
1dcb63ca
...
...
@@ -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, integ
er}</b> {totalCount, plural, one{item} other{items}}.'
)
.
Yii
::
t
(
'yii'
,
'Showing <b>{
begin, number}-{end, number}</b> of <b>{totalCount, numb
er}</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
,
[
...
...
tests/unit/framework/base/ComponentTest.php
View file @
1dcb63ca
...
...
@@ -302,7 +302,6 @@ class ComponentTest extends TestCase
$component
->
detachBehaviors
();
$this
->
assertNull
(
$component
->
getBehavior
(
'a'
));
$this
->
assertNull
(
$component
->
getBehavior
(
'b'
));
}
}
...
...
tests/unit/framework/base/EventTest.php
0 → 100644
View file @
1dcb63ca
<?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
{
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment