Skip to content

Instantly share code, notes, and snippets.

@icfantv
Last active June 10, 2016 20:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save icfantv/caddaf02abc9a7d2ce8724c85ca78821 to your computer and use it in GitHub Desktop.
Save icfantv/caddaf02abc9a7d2ce8724c85ca78821 to your computer and use it in GitHub Desktop.
Bootstrap-ish Chips from Stripped-down Material
[tabindex='-1']:focus {
outline: none;
}
input[type="search"] {
-webkit-appearance: textfield;
box-sizing: content-box;
-webkit-box-sizing: content-box;
}
input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
}
input:-webkit-autofill {
text-shadow: none;
}
._md-visually-hidden {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
text-transform: none;
width: 1px;
}
.md-scroll-mask {
position: absolute;
background-color: transparent;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 50;
}
.md-scroll-mask > .md-scroll-mask-bar {
display: block;
position: absolute;
background-color: #fafafa;
right: 0;
top: 0;
bottom: 0;
z-index: 65;
box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.3);
}
@-webkit-keyframes md-autocomplete-list-out {
0% {
-webkit-animation-timing-function: linear;
animation-timing-function: linear;
}
50% {
opacity: 0;
height: 40px;
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
100% {
height: 0;
opacity: 0;
}
}
@keyframes md-autocomplete-list-out {
0% {
-webkit-animation-timing-function: linear;
animation-timing-function: linear;
}
50% {
opacity: 0;
height: 40px;
-webkit-animation-timing-function: ease-in;
animation-timing-function: ease-in;
}
100% {
height: 0;
opacity: 0;
}
}
@-webkit-keyframes md-autocomplete-list-in {
0% {
opacity: 0;
height: 0;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
50% {
opacity: 0;
height: 40px;
}
100% {
opacity: 1;
height: 40px;
}
}
@keyframes md-autocomplete-list-in {
0% {
opacity: 0;
height: 0;
-webkit-animation-timing-function: ease-out;
animation-timing-function: ease-out;
}
50% {
opacity: 0;
height: 40px;
}
100% {
opacity: 1;
height: 40px;
}
}
md-autocomplete {
border-radius: 2px;
display: block;
height: 40px;
position: relative;
overflow: visible;
min-width: 190px;
}
md-autocomplete[disabled] input {
cursor: default;
}
md-autocomplete[md-floating-label] {
border-radius: 0;
background: transparent;
height: auto;
}
md-autocomplete[md-floating-label] md-input-container {
padding-bottom: 0;
}
md-autocomplete[md-floating-label] md-autocomplete-wrap {
height: auto;
}
md-autocomplete[md-floating-label] button {
position: absolute;
top: auto;
bottom: 0;
right: 0;
width: 30px;
height: 30px;
}
md-autocomplete md-autocomplete-wrap {
display: block;
position: relative;
overflow: visible;
height: 40px;
}
md-autocomplete md-autocomplete-wrap.md-menu-showing {
z-index: 51;
}
md-autocomplete md-autocomplete-wrap md-progress-linear {
position: absolute;
bottom: -2px;
left: 0;
}
md-autocomplete md-autocomplete-wrap md-progress-linear.md-inline {
bottom: 40px;
right: 2px;
left: 2px;
width: auto;
}
md-autocomplete md-autocomplete-wrap md-progress-linear ._md-mode-indeterminate {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 3px;
transition: none;
}
md-autocomplete md-autocomplete-wrap md-progress-linear ._md-mode-indeterminate .md-container {
transition: none;
height: 3px;
}
md-autocomplete md-autocomplete-wrap md-progress-linear ._md-mode-indeterminate.ng-enter {
transition: opacity 0.15s linear;
}
md-autocomplete md-autocomplete-wrap md-progress-linear ._md-mode-indeterminate.ng-enter.ng-enter-active {
opacity: 1;
}
md-autocomplete md-autocomplete-wrap md-progress-linear ._md-mode-indeterminate.ng-leave {
transition: opacity 0.15s linear;
}
md-autocomplete md-autocomplete-wrap md-progress-linear ._md-mode-indeterminate.ng-leave.ng-leave-active {
opacity: 0;
}
md-autocomplete input:not(.md-input) {
font-size: 14px;
box-sizing: border-box;
border: none;
box-shadow: none;
outline: none;
background: transparent;
width: 100%;
padding: 0 15px;
line-height: 40px;
height: 40px;
}
md-autocomplete input:not(.md-input)::-ms-clear {
display: none;
}
.md-virtual-repeat-container.md-autocomplete-suggestions-container {
position: absolute;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25);
height: 225.5px;
max-height: 225.5px;
z-index: 100;
}
.md-virtual-repeat-container.md-not-found {
height: 48px;
}
.md-autocomplete-suggestions {
margin: 0;
list-style: none;
padding: 0;
}
.md-autocomplete-suggestions li {
font-size: 14px;
overflow: hidden;
padding: 0 15px;
line-height: 48px;
height: 48px;
transition: background 0.15s linear;
margin: 0;
white-space: nowrap;
text-overflow: ellipsis;
}
.md-autocomplete-suggestions li:focus {
outline: none;
}
.md-autocomplete-suggestions li:not(.md-not-found-wrapper) {
cursor: pointer;
}
.md-chips {
display: block;
font-family: Roboto, "Helvetica Neue", sans-serif;
font-size: 16px;
padding: 0 0 8px 3px;
vertical-align: middle;
}
.md-chips:after {
content: '';
display: table;
clear: both;
}
[dir=rtl] .md-chips {
padding: 0 3px 8px 0;
}
.md-chips.md-readonly ._md-chip-input-container {
min-height: 32px;
}
.md-chips:not(.md-readonly) {
cursor: text;
}
.md-chips:not(.md-readonly) md-chip:not(.md-readonly) {
padding-right: 22px;
}
[dir=rtl] .md-chips:not(.md-readonly) md-chip:not(.md-readonly) {
padding-right: 0;
padding-right: auto;
padding-right: initial;
padding-left: 22px;
}
.md-chips:not(.md-readonly) md-chip:not(.md-readonly) ._md-chip-content {
padding-right: 4px;
}
[dir=rtl] .md-chips:not(.md-readonly) md-chip:not(.md-readonly) ._md-chip-content {
padding-right: 0;
padding-right: auto;
padding-right: initial;
padding-left: 4px;
}
.md-chips md-chip {
cursor: default;
border-radius: 16px;
display: block;
height: 32px;
line-height: 32px;
margin: 8px 8px 0 0;
padding: 0 12px 0 12px;
float: left;
box-sizing: border-box;
max-width: 100%;
position: relative;
}
[dir=rtl] .md-chips md-chip {
margin: 8px 0 0 8px;
}
[dir=rtl] .md-chips md-chip {
float: right;
}
.md-chips md-chip ._md-chip-content {
display: block;
float: left;
white-space: nowrap;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
[dir=rtl] .md-chips md-chip ._md-chip-content {
float: right;
}
.md-chips md-chip ._md-chip-content:focus {
outline: none;
}
.md-chips md-chip._md-chip-content-edit-is-enabled {
-webkit-user-select: none;
/* webkit (safari, chrome) browsers */
-moz-user-select: none;
/* mozilla browsers */
-khtml-user-select: none;
/* webkit (konqueror) browsers */
-ms-user-select: none;
/* IE10+ */
}
.md-chips md-chip ._md-chip-remove-container {
position: absolute;
right: 0;
line-height: 22px;
}
[dir=rtl] .md-chips md-chip ._md-chip-remove-container {
right: 0;
right: auto;
right: initial;
left: 0;
}
.md-chips md-chip ._md-chip-remove {
text-align: center;
width: 32px;
height: 32px;
min-width: 0;
padding: 0;
background: transparent;
border: none;
box-shadow: none;
margin: 0;
position: relative;
}
.md-chips ._md-chip-input-container {
display: block;
line-height: 32px;
margin: 8px 8px 0 0;
padding: 0;
float: left;
}
[dir=rtl] .md-chips ._md-chip-input-container {
margin: 8px 0 0 8px;
}
[dir=rtl] .md-chips ._md-chip-input-container {
float: right;
}
.md-chips ._md-chip-input-container input:not([type]), .md-chips ._md-chip-input-container input[type="email"], .md-chips ._md-chip-input-container input[type="number"], .md-chips ._md-chip-input-container input[type="tel"], .md-chips ._md-chip-input-container input[type="url"], .md-chips ._md-chip-input-container input[type="text"] {
border: 0;
height: 32px;
line-height: 32px;
padding: 0;
}
.md-chips ._md-chip-input-container input:not([type]):focus, .md-chips ._md-chip-input-container input[type="email"]:focus, .md-chips ._md-chip-input-container input[type="number"]:focus, .md-chips ._md-chip-input-container input[type="tel"]:focus, .md-chips ._md-chip-input-container input[type="url"]:focus, .md-chips ._md-chip-input-container input[type="text"]:focus {
outline: none;
}
.md-chips ._md-chip-input-container md-autocomplete, .md-chips ._md-chip-input-container md-autocomplete-wrap {
background: transparent;
height: 32px;
}
.md-chips ._md-chip-input-container md-autocomplete md-autocomplete-wrap {
box-shadow: none;
}
.md-chips ._md-chip-input-container md-autocomplete input {
position: relative;
}
.md-chips ._md-chip-input-container input {
border: 0;
height: 32px;
line-height: 32px;
padding: 0;
}
.md-chips ._md-chip-input-container input:focus {
outline: none;
}
.md-chips ._md-chip-input-container md-autocomplete, .md-chips ._md-chip-input-container md-autocomplete-wrap {
height: 32px;
}
.md-chips ._md-chip-input-container md-autocomplete {
box-shadow: none;
}
.md-chips ._md-chip-input-container md-autocomplete input {
position: relative;
}
.md-chips ._md-chip-input-container:not(:first-child) {
margin: 8px 8px 0 0;
}
[dir=rtl] .md-chips ._md-chip-input-container:not(:first-child) {
margin: 8px 0 0 8px;
}
.md-chips ._md-chip-input-container input {
background: transparent;
border-width: 0;
}
.md-chips md-autocomplete button {
display: none;
}
@media screen and (-ms-high-contrast: active) {
._md-chip-input-container,
md-chip {
border: 1px solid #fff;
}
._md-chip-input-container md-autocomplete {
border: none;
}
}
@-webkit-keyframes indeterminate-rotate {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes indeterminate-rotate {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
md-progress-linear {
display: block;
position: relative;
width: 100%;
height: 5px;
padding-top: 0 !important;
margin-bottom: 0 !important;
}
md-progress-linear._md-progress-linear-disabled {
visibility: hidden;
}
md-progress-linear ._md-container {
display: block;
position: relative;
overflow: hidden;
width: 100%;
height: 5px;
-webkit-transform: translate(0, 0) scale(1, 1);
transform: translate(0, 0) scale(1, 1);
}
md-progress-linear ._md-container ._md-bar {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 100%;
height: 5px;
}
md-progress-linear ._md-container ._md-dashed:before {
content: "";
display: none;
position: absolute;
margin-top: 0;
height: 5px;
width: 100%;
background-size: 10px 10px !important;
background: transparent 0 -23px;
}
md-progress-linear ._md-container ._md-bar1, md-progress-linear ._md-container ._md-bar2 {
transition: -webkit-transform 0.2s linear;
transition: transform 0.2s linear;
}
md-progress-linear ._md-container._md-mode-query ._md-bar1 {
display: none;
}
md-progress-linear ._md-container._md-mode-query ._md-bar2 {
transition: all 0.2s linear;
-webkit-animation: query 0.8s infinite cubic-bezier(0.39, 0.575, 0.565, 1);
animation: query 0.8s infinite cubic-bezier(0.39, 0.575, 0.565, 1);
}
md-progress-linear ._md-container._md-mode-determinate ._md-bar1 {
display: none;
}
md-progress-linear ._md-container._md-mode-indeterminate ._md-bar1 {
-webkit-animation: md-progress-linear-indeterminate-scale-1 4s infinite, md-progress-linear-indeterminate-1 4s infinite;
animation: md-progress-linear-indeterminate-scale-1 4s infinite, md-progress-linear-indeterminate-1 4s infinite;
}
md-progress-linear ._md-container._md-mode-indeterminate ._md-bar2 {
-webkit-animation: md-progress-linear-indeterminate-scale-2 4s infinite, md-progress-linear-indeterminate-2 4s infinite;
animation: md-progress-linear-indeterminate-scale-2 4s infinite, md-progress-linear-indeterminate-2 4s infinite;
}
md-progress-linear ._md-container.ng-hide ._md-progress-linear-disabled md-progress-linear ._md-container {
-webkit-animation: none;
animation: none;
}
md-progress-linear ._md-container.ng-hide ._md-progress-linear-disabled md-progress-linear ._md-container ._md-bar1 {
-webkit-animation-name: none;
animation-name: none;
}
md-progress-linear ._md-container.ng-hide ._md-progress-linear-disabled md-progress-linear ._md-container ._md-bar2 {
-webkit-animation-name: none;
animation-name: none;
}
md-progress-linear ._md-container._md-mode-buffer {
background-color: transparent !important;
transition: all 0.2s linear;
}
md-progress-linear ._md-container._md-mode-buffer ._md-dashed:before {
display: block;
-webkit-animation: buffer 3s infinite linear;
animation: buffer 3s infinite linear;
}
@-webkit-keyframes query {
0% {
opacity: 1;
-webkit-transform: translateX(35%) scale(0.3, 1);
transform: translateX(35%) scale(0.3, 1);
}
100% {
opacity: 0;
-webkit-transform: translateX(-50%) scale(0, 1);
transform: translateX(-50%) scale(0, 1);
}
}
@keyframes query {
0% {
opacity: 1;
-webkit-transform: translateX(35%) scale(0.3, 1);
transform: translateX(35%) scale(0.3, 1);
}
100% {
opacity: 0;
-webkit-transform: translateX(-50%) scale(0, 1);
transform: translateX(-50%) scale(0, 1);
}
}
@-webkit-keyframes buffer {
0% {
opacity: 1;
background-position: 0 -23px;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
background-position: -200px -23px;
}
}
@keyframes buffer {
0% {
opacity: 1;
background-position: 0 -23px;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
background-position: -200px -23px;
}
}
@-webkit-keyframes md-progress-linear-indeterminate-scale-1 {
0% {
-webkit-transform: scaleX(0.1);
transform: scaleX(0.1);
-webkit-animation-timing-function: linear;
animation-timing-function: linear;
}
36.6% {
-webkit-transform: scaleX(0.1);
transform: scaleX(0.1);
-webkit-animation-timing-function: cubic-bezier(0.33473, 0.12482, 0.78584, 1);
animation-timing-function: cubic-bezier(0.33473, 0.12482, 0.78584, 1);
}
69.15% {
-webkit-transform: scaleX(0.83);
transform: scaleX(0.83);
-webkit-animation-timing-function: cubic-bezier(0.22573, 0, 0.23365, 1.37098);
animation-timing-function: cubic-bezier(0.22573, 0, 0.23365, 1.37098);
}
100% {
-webkit-transform: scaleX(0.1);
transform: scaleX(0.1);
}
}
@keyframes md-progress-linear-indeterminate-scale-1 {
0% {
-webkit-transform: scaleX(0.1);
transform: scaleX(0.1);
-webkit-animation-timing-function: linear;
animation-timing-function: linear;
}
36.6% {
-webkit-transform: scaleX(0.1);
transform: scaleX(0.1);
-webkit-animation-timing-function: cubic-bezier(0.33473, 0.12482, 0.78584, 1);
animation-timing-function: cubic-bezier(0.33473, 0.12482, 0.78584, 1);
}
69.15% {
-webkit-transform: scaleX(0.83);
transform: scaleX(0.83);
-webkit-animation-timing-function: cubic-bezier(0.22573, 0, 0.23365, 1.37098);
animation-timing-function: cubic-bezier(0.22573, 0, 0.23365, 1.37098);
}
100% {
-webkit-transform: scaleX(0.1);
transform: scaleX(0.1);
}
}
@-webkit-keyframes md-progress-linear-indeterminate-1 {
0% {
left: -105.16667%;
-webkit-animation-timing-function: linear;
animation-timing-function: linear;
}
20% {
left: -105.16667%;
-webkit-animation-timing-function: cubic-bezier(0.5, 0, 0.70173, 0.49582);
animation-timing-function: cubic-bezier(0.5, 0, 0.70173, 0.49582);
}
69.15% {
left: 21.5%;
-webkit-animation-timing-function: cubic-bezier(0.30244, 0.38135, 0.55, 0.95635);
animation-timing-function: cubic-bezier(0.30244, 0.38135, 0.55, 0.95635);
}
100% {
left: 95.44444%;
}
}
@keyframes md-progress-linear-indeterminate-1 {
0% {
left: -105.16667%;
-webkit-animation-timing-function: linear;
animation-timing-function: linear;
}
20% {
left: -105.16667%;
-webkit-animation-timing-function: cubic-bezier(0.5, 0, 0.70173, 0.49582);
animation-timing-function: cubic-bezier(0.5, 0, 0.70173, 0.49582);
}
69.15% {
left: 21.5%;
-webkit-animation-timing-function: cubic-bezier(0.30244, 0.38135, 0.55, 0.95635);
animation-timing-function: cubic-bezier(0.30244, 0.38135, 0.55, 0.95635);
}
100% {
left: 95.44444%;
}
}
@-webkit-keyframes md-progress-linear-indeterminate-scale-2 {
0% {
-webkit-transform: scaleX(0.1);
transform: scaleX(0.1);
-webkit-animation-timing-function: cubic-bezier(0.20503, 0.05705, 0.57661, 0.45397);
animation-timing-function: cubic-bezier(0.20503, 0.05705, 0.57661, 0.45397);
}
19.15% {
-webkit-transform: scaleX(0.57);
transform: scaleX(0.57);
-webkit-animation-timing-function: cubic-bezier(0.15231, 0.19643, 0.64837, 1.00432);
animation-timing-function: cubic-bezier(0.15231, 0.19643, 0.64837, 1.00432);
}
44.15% {
-webkit-transform: scaleX(0.91);
transform: scaleX(0.91);
-webkit-animation-timing-function: cubic-bezier(0.25776, -0.00316, 0.21176, 1.38179);
animation-timing-function: cubic-bezier(0.25776, -0.00316, 0.21176, 1.38179);
}
100% {
-webkit-transform: scaleX(0.1);
transform: scaleX(0.1);
}
}
@keyframes md-progress-linear-indeterminate-scale-2 {
0% {
-webkit-transform: scaleX(0.1);
transform: scaleX(0.1);
-webkit-animation-timing-function: cubic-bezier(0.20503, 0.05705, 0.57661, 0.45397);
animation-timing-function: cubic-bezier(0.20503, 0.05705, 0.57661, 0.45397);
}
19.15% {
-webkit-transform: scaleX(0.57);
transform: scaleX(0.57);
-webkit-animation-timing-function: cubic-bezier(0.15231, 0.19643, 0.64837, 1.00432);
animation-timing-function: cubic-bezier(0.15231, 0.19643, 0.64837, 1.00432);
}
44.15% {
-webkit-transform: scaleX(0.91);
transform: scaleX(0.91);
-webkit-animation-timing-function: cubic-bezier(0.25776, -0.00316, 0.21176, 1.38179);
animation-timing-function: cubic-bezier(0.25776, -0.00316, 0.21176, 1.38179);
}
100% {
-webkit-transform: scaleX(0.1);
transform: scaleX(0.1);
}
}
@-webkit-keyframes md-progress-linear-indeterminate-2 {
0% {
left: -54.88889%;
-webkit-animation-timing-function: cubic-bezier(0.15, 0, 0.51506, 0.40968);
animation-timing-function: cubic-bezier(0.15, 0, 0.51506, 0.40968);
}
25% {
left: -17.25%;
-webkit-animation-timing-function: cubic-bezier(0.31033, 0.28406, 0.8, 0.73372);
animation-timing-function: cubic-bezier(0.31033, 0.28406, 0.8, 0.73372);
}
48.35% {
left: 29.5%;
-webkit-animation-timing-function: cubic-bezier(0.4, 0.62703, 0.6, 0.90203);
animation-timing-function: cubic-bezier(0.4, 0.62703, 0.6, 0.90203);
}
100% {
left: 117.38889%;
}
}
@keyframes md-progress-linear-indeterminate-2 {
0% {
left: -54.88889%;
-webkit-animation-timing-function: cubic-bezier(0.15, 0, 0.51506, 0.40968);
animation-timing-function: cubic-bezier(0.15, 0, 0.51506, 0.40968);
}
25% {
left: -17.25%;
-webkit-animation-timing-function: cubic-bezier(0.31033, 0.28406, 0.8, 0.73372);
animation-timing-function: cubic-bezier(0.31033, 0.28406, 0.8, 0.73372);
}
48.35% {
left: 29.5%;
-webkit-animation-timing-function: cubic-bezier(0.4, 0.62703, 0.6, 0.90203);
animation-timing-function: cubic-bezier(0.4, 0.62703, 0.6, 0.90203);
}
100% {
left: 117.38889%;
}
}
.md-virtual-repeat-container {
box-sizing: border-box;
display: block;
margin: 0;
overflow: hidden;
padding: 0;
position: relative;
}
.md-virtual-repeat-container .md-virtual-repeat-scroller {
bottom: 0;
box-sizing: border-box;
left: 0;
margin: 0;
overflow-x: hidden;
padding: 0;
position: absolute;
right: 0;
top: 0;
}
.md-virtual-repeat-container .md-virtual-repeat-sizer {
box-sizing: border-box;
height: 1px;
display: block;
margin: 0;
padding: 0;
width: 1px;
}
.md-virtual-repeat-container .md-virtual-repeat-offsetter {
box-sizing: border-box;
left: 0;
margin: 0;
padding: 0;
position: absolute;
right: 0;
top: 0;
}
.md-virtual-repeat-container.md-orient-horizontal .md-virtual-repeat-scroller {
overflow-x: auto;
overflow-y: hidden;
}
.md-virtual-repeat-container.md-orient-horizontal .md-virtual-repeat-offsetter {
bottom: 16px;
right: auto;
white-space: nowrap;
}
[dir=rtl] .md-virtual-repeat-container.md-orient-horizontal .md-virtual-repeat-offsetter {
right: 0;
right: auto;
right: initial;
left: auto;
}
.md-whiteframe-z1 {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2), 0 1px 1px 0 rgba(0, 0, 0, 0.14), 0 2px 1px -1px rgba(0, 0, 0, 0.12);
}
<html lang="en">
<head>
<link rel="stylesheet" href="angular-material.css">
<!--<link rel="stylesheet" href="node_modules/angular-material/angular-material.css">-->
<link rel="stylesheet" href="node_modules/font-awesome/css/font-awesome.css">
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.css">
<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap-theme.css">
<script src="node_modules/lodash/lodash.js"></script>
<script src="node_modules/angular/angular.min.js"></script>
<script src="node_modules/angular-animate/angular-animate.min.js"></script>
<script src="node_modules/angular-aria/angular-aria.min.js"></script>
<!--<script src="node_modules/angular-material/angular-material.js"></script>-->
<script src="new-angular-material.js"></script>
<style>
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
display: none !important;
}
md-chips-wrap.md-chips {
border: 1px solid #ccc;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
-webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
padding: 0 12px;
}
md-chips md-chips-wrap.md-focused {
border-color: #66afe9 !important;
outline: 0 !important;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6) !important;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6) !important;
}
.md-chips {
font-size: 12px;
padding: 0 0 0 3px;
}
.md-chips ._md-chip-input-container {
margin: 0 8px 0 0;
}
.md-chips:not(.md-readonly) md-chip:not(.md-readonly) {
padding-right: 13px;
}
.md-chips md-chip {
height: 24px;
margin: 4px 5px 0 0;
color: #333;
background-color: #fff;
border-radius: 3px;
line-height: 1.5;
padding: 1px 5px;
text-shadow: 0 1px 0 #fff;
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0));
background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border: 1px solid #ccc;
}
.md-chips md-chip:hover {
text-decoration: none;
color: #333;
border-color: #adadad;
background-color: #e0e0e0;
background-position: 0 -15px;
}
md-chips md-chip.md-focused {
background-color: #e0e0e0;
color: inherit;
}
.md-chips md-chip ._md-chip-remove {
color: #ccc;
height: 21px;
width: 16px;
}
.md-chips md-chip ._md-chip-remove:hover {
color: #666;
}
md-chip ._md-chip-content {
margin-top: 2px;
}
.md-chips ._md-chip-input-container,
.md-chips ._md-chip-input-container input {
height: 34px;
line-height: 34px;
}
.md-chips ._md-chip-input-container:not(:first-child) {
margin: 0 8px 0 0;
}
[dir=rtl] .md-chips ._md-chip-input-container:not(:first-child) {
margin: 0 0 0 8px;
}
.md-chips ._md-chip-input-container md-autocomplete,
.md-chips ._md-chip-input-container md-autocomplete-wrap {
height: 34px;
}
md-chips md-autocomplete md-autocomplete-wrap > input::-webkit-input-placeholder {
color: #999 !important;
font-size: 14px !important;
}
.md-autocomplete-suggestions li {
line-height: 30px;
height: 30px;
}
.md-virtual-repeat-container.md-autocomplete-suggestions-container {
border-radius: 4px;
}
.md-autocomplete-suggestions-container.md-default-theme li:hover,
.md-autocomplete-suggestions-container li:hover {
background-color: #eee;
}
.md-autocomplete-suggestions-container.md-default-theme li.selected,
.md-autocomplete-suggestions-container li.selected {
background-color: #ccc;
}
</style>
<script language="javascript">
var app = angular.module('myApp', ['ngMaterialChips']);
app.controller('TestController', function() {
var vm = this;
vm.list = [
"$100k+",
"$150k+",
"$75k+",
"1+ Adults in Household",
"100k LA",
"100k+",
"2+ Adults in Household",
"3+ Adults in Household",
"4+ Adults in Household",
"5+ Adults in Household",
"75k LA",
"AUTO 4 Wheel Drive",
"AUTO All-Terrain Vehicle",
"AUTO Audi Owners",
"AUTO Chevy DMA Rochester",
"AUTO Compressed Natural Gas Fuel Vehicle",
"AUTO Crossover Utility Vehicle",
"AUTO Domestic Buying Pattern",
"AUTO Electric Vehicle",
"AUTO Exotic Car",
"AUTO Import Buying Pattern",
"AUTO Leased Car",
"AUTO Leased Truck",
"AUTO Leased Vehicle",
"AUTO Luxury 1/2 Ton Full Size Pickup",
"AUTO Luxury Category All Makes and Vehicle Value DMA NYC",
"AUTO Luxury Compact Car",
"AUTO Luxury Compact Crossover Utility Vehicle",
"AUTO Luxury Full Size Car",
"AUTO Luxury Full Size SUV",
"AUTO Luxury Midsize Car",
"AUTO Luxury Midsize Crossover Utility Vehicle",
"AUTO Luxury Midsize SUV",
"AUTO Luxury Own All Makes DMA Los Angeles",
"AUTO Luxury Prestige Full Size Car",
"AUTO Luxury Sporty Car",
"AUTO Luxury Sub-Compact Car",
"AUTO Motorcycle Purchased 0-6 Months",
"AUTO Motorcycle Purchased 13-24 Months",
"AUTO Motorcycle Purchased 25-36 Months",
"AUTO Motorcycle Purchased 37-48 Months Ago",
"AUTO Motorcycle Purchased 7-12 Months",
"AUTO Motorhome",
"AUTO New Car",
"AUTO New Truck",
"AUTO New Vehicle",
"AUTO No Vehicle",
"AUTO Non-Luxury 1/2 Ton Full Size Pickup",
"AUTO Non-Luxury 1/2 Ton Full Size Van",
"AUTO Non-Luxury 3/4 Or 1 Ton Full Size Pickup",
"AUTO Non-Luxury 3/4 Or 1 Ton Full Size Van",
"AUTO Non-Luxury Compact CUV",
"AUTO Non-Luxury Compact Car",
"AUTO Non-Luxury Compact Pickup",
"AUTO Non-Luxury Compact SUV",
"AUTO Non-Luxury Compact Van",
"AUTO Non-Luxury Full Size Car",
"AUTO Non-Luxury Full Size SUV",
"AUTO Non-Luxury Midsize Car",
"AUTO Non-Luxury Midsize Crossover Utility Vehicle",
"AUTO Non-Luxury Midsize Pickup",
"AUTO Non-Luxury Midsize SUV",
"AUTO Non-Luxury Midsize Sporty Car",
"AUTO Non-Luxury Midsize Van",
"AUTO Non-Luxury Sporty Car",
"AUTO Non-Luxury Sub-Compact Car",
"AUTO NonLuxury Traditional Own All Makes DMA Los Angeles",
"AUTO One Vehicle",
"AUTO Own Domestic Vehicle 1 Year Old",
"AUTO Own Domestic Vehicle 10+ Year Old",
"AUTO Own Domestic Vehicle 2 Year Old",
"AUTO Own Domestic Vehicle 3 Year Old",
"AUTO Own Domestic Vehicle 4 Year Old",
"AUTO Own Domestic Vehicle 5 year old",
"AUTO Own Domestic Vehicle 6 Year Old",
"AUTO Own Domestic Vehicle 7 Year Old",
"AUTO Own Domestic Vehicle 8 Year Old",
"AUTO Own Domestic Vehicle 9 Year Old",
"AUTO Own Imported Vehicle 0 Year Old",
"AUTO Own Imported Vehicle 1 Year Old",
"AUTO Own Imported Vehicle 10+ Year Old",
"AUTO Own Imported Vehicle 2 Year Old",
"AUTO Own Imported Vehicle 3 Year Old",
"AUTO Own Imported Vehicle 4 Year Old",
"AUTO Own Imported Vehicle 5 Year Old",
"AUTO Own Imported Vehicle 6 Year Old",
"AUTO Own Imported Vehicle 7 Year Old",
"AUTO Own Imported Vehicle 8 Year Old",
"AUTO Own Imported Vehicle 9 Year Old",
"AUTO Own Vehicle 0-1 Year Old",
"AUTO Own Vehicle 11-15 Year Old",
"AUTO Own Vehicle 16 to 20 Year Old",
"AUTO Own Vehicle 2 Year Old",
"AUTO Own Vehicle 21+ Year Old",
"AUTO Own Vehicle 3 Year Old",
"AUTO Own Vehicle 4-5 Year Old",
"AUTO Own Vehicle 6-10 Year Old",
"AUTO Own Vehicle CMV 100K",
"AUTO Own Vehicle CMV 20K",
"AUTO Own Vehicle CMV 30k",
"AUTO Own Vehicle CMV 40K",
"AUTO Own Vehicle CMV 50K",
"AUTO Own Vehicle CMV 75K",
"AUTO Own Vehicle Current Market Value $4K Or Less",
"AUTO Own Vehicle Current Market Value 10 - 14K",
"AUTO Own Vehicle Current Market Value 15 - 19K",
"AUTO Own Vehicle Current Market Value 20 - 29K",
"AUTO Own Vehicle Current Market Value 30K",
"AUTO Own Vehicle Current Market Value 5 - 9K",
"AUTO Recent Purchase 0-6 Months",
"AUTO Recent Purchase 10-14k",
"AUTO Recent Purchase 13-24 Months",
"AUTO Recent Purchase 15-19k",
"AUTO Recent Purchase 20-29k",
"AUTO Recent Purchase 25-36 Months",
"AUTO Recent Purchase 30-39K",
"AUTO Recent Purchase 37-48 Months",
"AUTO Recent Purchase 49+ Months",
"AUTO Recent Purchase 7-12 Months",
"AUTO Recent Purchase <4K",
"AUTO Recent Purchase >40K",
"AUTO Recent Purchase New",
"AUTO Recent Purchase New Vehicle 0-6 Months",
"AUTO Recent Purchase New Vehicle 13-24 Months",
"AUTO Recent Purchase New Vehicle 25-36",
"AUTO Recent Purchase New Vehicle 37-48 Months",
"AUTO Recent Purchase New Vehicle 49+",
"AUTO Recent Purchase New Vehicle 7-12 Months",
"AUTO Recent Purchase Used",
"AUTO Recent Purchase Used Vehicle 0-6 Months",
"AUTO Recent Purchase Used Vehicle 13-24 Months",
"AUTO Recent Purchase Used Vehicle 25-36 Months",
"AUTO Recent Purchase Used Vehicle 37-48 Months",
"AUTO Recent Purchase Used Vehicle 49+ Months",
"AUTO Recent Purchase Used Vehicle 7-12 Months",
"AUTO Two or More Vehicles",
"AUTO Used Car",
"AUTO Used Truck",
"AUTO Used Vehicle",
"AUTO Vehicle Purchased 0-6 Months",
"AUTO Vehicle Purchased 13-24 Months",
"AUTO Vehicle Purchased 25-36 Months",
"AUTO Vehicle Purchased 37-48 Months",
"AUTO Vehicle Purchased 49+ Months",
"AUTO Vehicle Purchased 7-12 Months",
"AUTO recent purchase 5-9K",
"AUTOAcura",
"AUTOAsianImport",
"AUTOAudi",
"AUTOBmw",
"AUTOBmwMotorcycle",
"AUTOBuick",
"AUTOCadillac",
"AUTOChevy",
"AUTOChevyTruck",
"AUTOChrylser",
"AUTOChryslerDivision",
"AUTOConvertible",
"AUTOCoupe",
"AUTOCrossoversMidsizesuv",
"AUTODaewoo",
"AUTODiesel",
"AUTODodgeCar",
"AUTODodgeTruck",
"AUTODomesticMotorcycle",
"AUTOEconomycars",
"AUTOEuropeanImport",
"AUTOFiat",
"AUTOFlexFuel",
"AUTOFord",
"AUTOFordCar",
"AUTOFordLA",
"AUTOFordTruck",
"AUTOGm",
"AUTOGmc",
"AUTOHarleyMotorcycle",
"AUTOHonda",
"AUTOHummer",
"AUTOHybrid",
"AUTOHybridFuelType",
"AUTOHyundai",
"AUTOImportMotorcycle",
"AUTOInfiniti",
"AUTOIsuzu",
"AUTOJaguar",
"AUTOJeep",
"AUTOKawasakiMotorcycle",
"AUTOKia",
"AUTOLandrover",
"AUTOLargeMotorcycle",
"AUTOLargesuvMinivans",
"AUTOLexus",
"AUTOLincoln",
"AUTOLuxurysedanSportsvehicle",
"AUTOMazda",
"AUTOMercedes",
"AUTOMercury",
"AUTOMini",
"AUTOMiniBike",
"AUTOMitsubishi",
"AUTONissan",
"AUTONonluxuryfullsizesedan",
"AUTONonluxurymidsizesedan",
"AUTOOldsmobile",
"AUTOPlymouth",
"AUTOPolarisMotorcycle",
"AUTOPontiac",
"AUTOPorsche",
"AUTOSaturn",
"AUTOSedan",
"AUTOSmallMotorcycle",
"AUTOStreetBike",
"AUTOSubaru",
"AUTOSuzuki",
"AUTOSuzukiMotorcycle",
"AUTOToyota",
"AUTOTrucks",
"AUTOTrucksPremium",
"AUTOUsedMotorcycle",
"AUTOVolkswagen",
"AUTOVolvo",
"AUTOYamahaMotorcycle",
"Adam Test 1",
"Auto Motorcycle Purchased 49+ Months Ago Rank",
"Auto Own Domestic Vehicle 0 Year Old",
"AutoDirtBike",
"AutoHondaMotorcycle",
"AutoNewMotorcycle",
"AutoSaab",
"Charlotte Cable Modems",
"Charlotte geo",
"Children Aged 0-3",
"Children Aged 10-12",
"Children Aged 13-18",
"Children Aged 4-6",
"Children Aged 7-9",
"College Graduate Plus",
"DMAAlbany",
"DMAAustin",
"DMABuffalo",
"DMACincinnati",
"DMACleveland",
"DMAColumbus-Oh",
"DMADallas",
"DMADayton",
"DMAFlorence",
"DMAGreenBay",
"DMAGreensboro",
"DMAKansas City",
"DMALA",
"DMALima",
"DMAMilwaukee",
"DMANYC",
"DMASan Antonio",
"DMASyracuse",
"DMAToledo",
"DMAWaco",
"Democrats vote any last 4 Primary Election",
"Education Level: Bachelors Degree",
"Education Level: Graduate Degree",
"Education Level: HS Diploma",
"Education Level: Less than HS Diploma",
"Education Level: No College Degree",
"Education Level: Some College",
"Encore Subscription",
"Epix Subscription",
"Extremely Likely Married",
"Extremely Likely Single",
"Freewheel Test",
"Greater LA",
"Greater LA 1",
"Greater LA 2",
"HBO Subscribers",
"HH $150k-$200k",
"HH Income $1k-$25k",
"HHI $100k-$150k",
"HHI $200k-$250k",
"HHI $250k+",
"HHI $25k-$50k",
"HHI $50k-75k",
"HHI $75k-$100k",
"HHI$150K+",
"HHI$50K+",
"HHI$50K+ subscribes to HBO",
"HHI$50K+ with No Premium Network Subscription",
"HHI$50K+ with Premium Network Subscription Excluding HBO",
"HHI$50K+ with any Premium Network Subscription",
"Household Presence of Children",
"Internet Subscribe with no cable subscription",
"Internet Subscribers with no cable subscription in LA-NY-Dallas DMA",
"LA, Age 25-49",
"LA, Age 25-54",
"LAX-01",
"LOB Video and HSD",
"LOB- High Speed Digital- No",
"LOB- High Speed Digital- Yes",
"LOB- Mobile- No",
"LOB- Mobile- Yes",
"LOB- Phone- No",
"LOB- Phone- Yes",
"LOB- Security- No",
"LOB- Security- Yes",
"LOB- Video- No",
"LOB- Video- Yes",
"LOB- Wireless- No",
"LOB- Wireless- Yes",
"Less Than HHI$50K HBO subscribers",
"Lexus LA",
"NYC W25-54 Kids HHI$100K+",
"Non Cinemax Subscription",
"Non Disney Subscription",
"Non Encore Subscription",
"Non Epix Subscription",
"Non HBO Subscribers",
"Non MLB Network Subscription",
"Non NBA Subscription",
"Non NHL Subscription",
"Non Premium Movie Network Subscription",
"Non Premium Network Subscription (no HBO)",
"Non STARZ subscribers",
"Non Showtime Subscription",
"Occupation- Blue Collar",
"Occupation- Farm Related",
"Occupation- Other",
"Occupation- Professional/Technical",
"Occupation- Retired",
"Occupation- Sales/Service",
"Occupation- Unknown",
"Own homes",
"PH Volvo Luxury POLK Custom Profile",
"PH-HHI$100K with likely to buy luxury cars with Used Cars-Dec2015",
"PH-HHI$150K with likely to buy luxury cars with Used Cars-Dec2015",
"PH-HHI$75K+ with Likely to buy All luxury cars by types-Dec2015",
"PH-Luxury Car Ownership-In Garage-Dec2015",
"PH-Luxury Cars By Make-Dec2015",
"PH-Upgraders-All Luxury Car By Type-Dec2015",
"PH-Upgraders-Luxury Car By Type with Used Car-Dec 2015",
"PH-Upgraders-Luxury Car By Type-Dec2015",
"Political Democrat Vote at Any Past 4 General Election",
"Political Democrats Vote at Any Past 4 Primary Election",
"Political Independent Vote at Any Past 4 General Election",
"Political Independent Vote at Any Past 4 Primary Election",
"Political Party - Democrat",
"Political Party - Independent",
"Political Party - Republican",
"Political Republican Vote at Any Past 4 General Election",
"Political Republican Vote at Any Past 4 Primary Election",
"Political Vote at ALL Past 4 General Election",
"Political Vote at Any Past 4 General Election",
"Political Vote at Any Past 4 Primary Election",
"Premium Network Subscription",
"Premium Network Subscription without HBO",
"Presence of Children- No",
"Presence of Children- Yes",
"Product Usage: Interest in Travel",
"Product Usage: Interested in Domestic Travel",
"Product Usage: Interested in Personal Care, Beauty",
"Product Usage: Multiple Mail Order Buys",
"Rochester",
"STARZ Subscription",
"Showtime Subscription",
"Subscriber Info- Does Not Subscribe to IPTV Capable Device",
"Subscriber Info- Does not Subscribe to Digital STB",
"Subscriber Info- Does not Subscribe to Display Capable Device",
"Subscriber Info- Does not Subscribe to Interactive Capable Device",
"Subscriber Info- HH Subscribes to Cable Modem",
"Subscriber Info- HH does not Subscribe to Cable Modem",
"Subscriber Info- No Online Internet Subscription",
"Subscriber Info- Online Internet Subscription",
"Subscriber Info- Subscribes to Digital STB",
"Subscriber Info- Subscribes to Display Capable Device",
"Subscriber Info- Subscribes to IPTV Capable Device",
"Subscriber Info- Subscribes to Interactive Capable Device",
"T1",
"Test Geographic Profile",
"Test Geographic Profile 2",
"Test-POLK-SAAB",
"Time Warner/Antelope Valley, CA",
"Time Warner/Barstow, CA",
"Time Warner/Bergen, NJ-Mt. Vernon, NY",
"Time Warner/Brooklyn, NY",
"Time Warner/Camarillo, CA",
"Time Warner/Central Los Angeles, CA",
"Time Warner/Central Orange County, CA",
"Time Warner/Chino-Corona-Diamond Bar, CA",
"Time Warner/Downey-MidCities, CA",
"Time Warner/East San Fernando Valley, CA",
"Time Warner/East San Gabriel Valley, CA",
"Time Warner/East Ventura, CA",
"Time Warner/Hacienda Heights, CA",
"Time Warner/Hollywood, CA",
"Time Warner/Huntington Beach, CA",
"Time Warner/Kingston, NY",
"Time Warner/Los Feliz, CA",
"Time Warner/Moreno Valley, CA",
"Time Warner/Newburgh Zone, NY",
"Time Warner/North Orange County, CA",
"Time Warner/Northern Manhattan, NY",
"Time Warner/Ontario, CA",
"Time Warner/Orange Zone, NY",
"Time Warner/Poughkeepsie, NY",
"Time Warner/Queens, NY",
"Time Warner/Redlands, CA",
"Time Warner/San Bernardino County, CA",
"Time Warner/San Pedro, CA",
"Time Warner/Santa Clarita, CA",
"Time Warner/Simi Valley, CA",
"Time Warner/So. San Fernando Valley, CA",
"Time Warner/South Bay, CA",
"Time Warner/South Orange County, CA",
"Time Warner/Southern Manhattan, NY",
"Time Warner/Staten Island Cable, NY",
"Time Warner/Sullivan Zone, NY",
"Time Warner/Temecula, CA",
"Time Warner/Thousand Oaks, CA",
"Time Warner/Torrance, CA",
"Time Warner/West Los Angeles, CA",
"Time Warner/West San Fernando Valley, CA",
"Time Warner/Westside-Beverly Hills, CA",
"Z_LKN",
"test"
];
// ["AliceBlue", "AntiqueWhite", "Aqua", "Aquamarine", "Azure", "Beige", "Bisque", "Black", "BlanchedAlmond", "Blue", "BlueViolet", "Brown", "BurlyWood", "CadetBlue", "Chartreuse", "Chocolate", "Coral", "CornflowerBlue", "Cornsilk", "Crimson", "Cyan", "DarkBlue", "DarkCyan", "DarkGoldenRod", "DarkGray", "DarkGrey", "DarkGreen", "DarkKhaki", "DarkMagenta", "DarkOliveGreen", "Darkorange", "DarkOrchid", "DarkRed", "DarkSalmon", "DarkSeaGreen", "DarkSlateBlue", "DarkSlateGray", "DarkSlateGrey", "DarkTurquoise", "DarkViolet", "DeepPink", "DeepSkyBlue", "DimGray", "DimGrey", "DodgerBlue", "FireBrick", "FloralWhite", "ForestGreen", "Fuchsia", "Gainsboro", "GhostWhite", "Gold", "GoldenRod", "Gray", "Grey", "Green", "GreenYellow", "HoneyDew", "HotPink", "IndianRed", "Indigo", "Ivory", "Khaki", "Lavender", "LavenderBlush", "LawnGreen", "LemonChiffon", "LightBlue", "LightCoral", "LightCyan", "LightGoldenRodYellow", "LightGray", "LightGrey", "LightGreen", "LightPink", "LightSalmon", "LightSeaGreen", "LightSkyBlue", "LightSlateGray", "LightSlateGrey", "LightSteelBlue", "LightYellow", "Lime", "LimeGreen", "Linen", "Magenta", "Maroon", "MediumAquaMarine", "MediumBlue", "MediumOrchid", "MediumPurple", "MediumSeaGreen", "MediumSlateBlue", "MediumSpringGreen", "MediumTurquoise", "MediumVioletRed", "MidnightBlue", "MintCream", "MistyRose", "Moccasin", "NavajoWhite", "Navy", "OldLace", "Olive", "OliveDrab", "Orange", "OrangeRed", "Orchid", "PaleGoldenRod", "PaleGreen", "PaleTurquoise", "PaleVioletRed", "PapayaWhip", "PeachPuff", "Peru", "Pink", "Plum", "PowderBlue", "Purple", "Red", "RosyBrown", "RoyalBlue", "SaddleBrown", "Salmon", "SandyBrown", "SeaGreen", "SeaShell", "Sienna", "Silver", "SkyBlue", "SlateBlue", "SlateGray", "SlateGrey", "Snow", "SpringGreen", "SteelBlue", "Tan", "Teal", "Thistle", "Tomato", "Turquoise", "Violet", "Wheat", "White", "WhiteSmoke", "Yellow", "YellowGreen"];
vm.content = {
list: []
};
});
app.filter('difference', function() {
return function(list, list2) {
// Change order here to show most revelant match first?
var result = _.difference(list, list2).sort();
return result;
}
});
</script>
</head>
<body ng-app="myApp">
<div ng-controller="TestController as vm">
<div class="container">
<div class="jumbotron" style="background-color: darkblue; color: white;">
<h1>Hello, world!</h1>
<p>
This is a simple hero unit, a simple jumbotron-style component for calling extra attention to featured content or information.
</p>
<p><a class="btn btn-default btn-lg" href="#" role="button">Learn more</a></p>
</div>
<p><a class="btn btn-default btn-xs" href="#" role="button">Learn more</a></p>
</div>
<div class="container">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Angular Material Reduction</h3>
</div>
<div class="panel-body">
<div class="form-group"
ng-if="!formState.readOnly || (formState.readOnly && (model[options.key]))"
ng-class="{'has-feedback': to.loading , 'has-error': options.validation.errorExistsAndShouldBeVisible}">
<label for="snoopy"
class="control-label"
ng-class="::formState.horizontalLabelClass">
Test Label
<span ng-if="::to.required" class="text-red">*</span>
</label>
<div ng-class="::formState.horizontalFieldClass">
<md-chips ng-model="vm.content.list" md-autocomplete-snap md-require-match="true">
<md-autocomplete md-autoselect="true"
md-selected-item="vm.selectedItem"
md-search-text="vm.searchText"
md-items="item in vm.list | filter: vm.searchText | difference:vm.content.list"
md-item-text="item"
md-no-cache="true"
md-min-length="0"
placeholder="Insert Tags">
<span md-highlight-flags="^i" md-highlight-text="vm.searchText">{{item}}</span>
</md-autocomplete>
<md-chip-template><span>{{$chip}}</span></md-chip-template>
</md-chips>
</div>
</div>
<div class="form-group">
<label class="control-label">
Foo
</label>
<input type="text" class="form-control" placeholder="Insert Tags"/>
</div>
</div>
</div>
<hr/>
<pre>{{ vm.content.list | json }}</pre>
</div>
</div>
</body>
</html>
(function( window, angular, undefined ){
"use strict";
(function(){
"use strict";
angular.module('ngMaterial', ["ng","material.core","material.core.theming.palette","material.core.theming","material.core.animate","material.components.autocomplete","material.components.chips","material.components.showHide","material.components.virtualRepeat"]);
})();
(function(){
"use strict";
angular
.module('material.core', [
'ngAnimate',
'material.core.animate',
'material.core.theming'
])
.config(MdCoreConfigure);
function MdCoreConfigure($provide, $mdThemingProvider) {
$provide.decorator('$$rAF', ["$delegate", rAFDecorator]);
$mdThemingProvider.theme('default')
.primaryPalette('indigo')
.accentPalette('pink')
.warnPalette('deep-orange')
.backgroundPalette('grey');
}
MdCoreConfigure.$inject = ["$provide", "$mdThemingProvider"];
function rAFDecorator($delegate) {
/**
* Use this to throttle events that come in often.
* The throttled function will always use the *last* invocation before the
* coming frame.
*
* For example, window resize events that fire many times a second:
* If we set to use an raf-throttled callback on window resize, then
* our callback will only be fired once per frame, with the last resize
* event that happened before that frame.
*
* @param {function} callback function to debounce
*/
$delegate.throttle = function(cb) {
var queuedArgs, alreadyQueued, queueCb, context;
return function debounced() {
queuedArgs = arguments;
context = this;
queueCb = cb;
if (!alreadyQueued) {
alreadyQueued = true;
$delegate(function() {
queueCb.apply(context, Array.prototype.slice.call(queuedArgs));
alreadyQueued = false;
});
}
};
};
return $delegate;
}
rAFDecorator.$inject = ["$delegate"];
})(); // material.core: .config(MdCoreConfigure);
(function(){
"use strict";
angular.module('material.core')
// Support the deprecated md-auto-focus and md-sidenav-focus as well
.directive('mdAutoFocus', MdAutofocusDirective);
function MdAutofocusDirective() {
return {
restrict: 'A',
link: postLink
}
}
function postLink(scope, element, attrs) {
var attr = attrs.mdAutoFocus || attrs.mdAutofocus;
// Setup a watcher on the proper attribute to update a class we can check for in $mdUtil
scope.$watch(attr, function(canAutofocus) {
element.toggleClass('_md-autofocus', canAutofocus);
});
}
})(); // material.core: .directive('mdAutoFocus', MdAutofocusDirective)
(function(){
"use strict";
angular.module('material.core')
.factory('$mdConstant', MdConstantFactory);
function MdConstantFactory($sniffer) {
var vendorPrefix = $sniffer.vendorPrefix;
var isWebkit = /webkit/i.test(vendorPrefix);
var SPECIAL_CHARS_REGEXP = /([:\-_]+(.))/g;
var prefixTestEl = document.createElement('div');
function vendorProperty(name) {
// Add a dash between the prefix and name, to be able to transform the string into camelcase.
var prefixedName = vendorPrefix + '-' + name;
var ucPrefix = camelCase(prefixedName);
var lcPrefix = ucPrefix.charAt(0).toLowerCase() + ucPrefix.substring(1);
return hasStyleProperty(name) ? name : // The current browser supports the un-prefixed property
hasStyleProperty(ucPrefix) ? ucPrefix : // The current browser only supports the prefixed property.
hasStyleProperty(lcPrefix) ? lcPrefix : name; // Some browsers are only supporting the prefix in lowercase.
}
function hasStyleProperty(property) {
return angular.isDefined(prefixTestEl.style[property]);
}
function camelCase(input) {
return input.replace(SPECIAL_CHARS_REGEXP, function(matches, separator, letter, offset) {
return offset ? letter.toUpperCase() : letter;
});
}
return {
KEY_CODE: {
COMMA: 188,
SEMICOLON : 186,
ENTER: 13,
ESCAPE: 27,
SPACE: 32,
PAGE_UP: 33,
PAGE_DOWN: 34,
END: 35,
HOME: 36,
LEFT_ARROW : 37,
UP_ARROW : 38,
RIGHT_ARROW : 39,
DOWN_ARROW : 40,
TAB : 9,
BACKSPACE: 8,
DELETE: 46
},
CSS: {
/* Constants */
TRANSITIONEND: 'transitionend' + (isWebkit ? ' webkitTransitionEnd' : ''),
ANIMATIONEND: 'animationend' + (isWebkit ? ' webkitAnimationEnd' : ''),
TRANSFORM: vendorProperty('transform'),
TRANSFORM_ORIGIN: vendorProperty('transformOrigin'),
TRANSITION: vendorProperty('transition'),
TRANSITION_DURATION: vendorProperty('transitionDuration'),
ANIMATION_PLAY_STATE: vendorProperty('animationPlayState'),
ANIMATION_DURATION: vendorProperty('animationDuration'),
ANIMATION_NAME: vendorProperty('animationName'),
ANIMATION_TIMING: vendorProperty('animationTimingFunction'),
ANIMATION_DIRECTION: vendorProperty('animationDirection')
},
/**
* As defined in core/style/variables.scss
*
* $layout-breakpoint-xs: 600px !default;
* $layout-breakpoint-sm: 960px !default;
* $layout-breakpoint-md: 1280px !default;
* $layout-breakpoint-lg: 1920px !default;
*
*/
MEDIA: {
'xs' : '(max-width: 599px)' ,
'gt-xs' : '(min-width: 600px)' ,
'sm' : '(min-width: 600px) and (max-width: 959px)' ,
'gt-sm' : '(min-width: 960px)' ,
'md' : '(min-width: 960px) and (max-width: 1279px)' ,
'gt-md' : '(min-width: 1280px)' ,
'lg' : '(min-width: 1280px) and (max-width: 1919px)',
'gt-lg' : '(min-width: 1920px)' ,
'xl' : '(min-width: 1920px)' ,
'landscape' : '(orientation: landscape)' ,
'portrait' : '(orientation: portrait)' ,
'print' : 'print'
},
MEDIA_PRIORITY: [
'xl',
'gt-lg',
'lg',
'gt-md',
'md',
'gt-sm',
'sm',
'gt-xs',
'xs',
'landscape',
'portrait',
'print'
]
};
}
MdConstantFactory.$inject = ["$sniffer"];
})(); // material.core: .factory('$mdConstant', MdConstantFactory);
(function(){
"use strict";
angular
.module('material.core')
.config( ["$provide", function($provide){
$provide.decorator('$mdUtil', ['$delegate', function ($delegate){
$delegate.iterator = MdIterator;
return $delegate;
}
]);
}]);
function MdIterator(items, reloop) {
var trueFn = function() { return true; };
if (items && !angular.isArray(items)) {
items = Array.prototype.slice.call(items);
}
reloop = !!reloop;
var _items = items || [ ];
// Published API
return {
items: getItems,
count: count,
inRange: inRange,
contains: contains,
indexOf: indexOf,
itemAt: itemAt,
findBy: findBy,
add: add,
remove: remove,
first: first,
last: last,
next: angular.bind(null, findSubsequentItem, false),
previous: angular.bind(null, findSubsequentItem, true),
hasPrevious: hasPrevious,
hasNext: hasNext
};
/**
* Publish copy of the enumerable set
* @returns {Array|*}
*/
function getItems() {
return [].concat(_items);
}
/**
* Determine length of the list
* @returns {Array.length|*|number}
*/
function count() {
return _items.length;
}
/**
* Is the index specified valid
* @param index
* @returns {Array.length|*|number|boolean}
*/
function inRange(index) {
return _items.length && ( index > -1 ) && (index < _items.length );
}
/**
* Can the iterator proceed to the next item in the list; relative to
* the specified item.
*
* @param item
* @returns {Array.length|*|number|boolean}
*/
function hasNext(item) {
return item ? inRange(indexOf(item) + 1) : false;
}
/**
* Can the iterator proceed to the previous item in the list; relative to
* the specified item.
*
* @param item
* @returns {Array.length|*|number|boolean}
*/
function hasPrevious(item) {
return item ? inRange(indexOf(item) - 1) : false;
}
/**
* Get item at specified index/position
* @param index
* @returns {*}
*/
function itemAt(index) {
return inRange(index) ? _items[index] : null;
}
/**
* Find all elements matching the key/value pair
* otherwise return null
*
* @param val
* @param key
*
* @return array
*/
function findBy(key, val) {
return _items.filter(function(item) {
return item[key] === val;
});
}
/**
* Add item to list
* @param item
* @param index
* @returns {*}
*/
function add(item, index) {
if ( !item ) return -1;
if (!angular.isNumber(index)) {
index = _items.length;
}
_items.splice(index, 0, item);
return indexOf(item);
}
/**
* Remove item from list...
* @param item
*/
function remove(item) {
if ( contains(item) ){
_items.splice(indexOf(item), 1);
}
}
/**
* Get the zero-based index of the target item
* @param item
* @returns {*}
*/
function indexOf(item) {
return _items.indexOf(item);
}
/**
* Boolean existence check
* @param item
* @returns {boolean}
*/
function contains(item) {
return item && (indexOf(item) > -1);
}
/**
* Return first item in the list
* @returns {*}
*/
function first() {
return _items.length ? _items[0] : null;
}
/**
* Return last item in the list...
* @returns {*}
*/
function last() {
return _items.length ? _items[_items.length - 1] : null;
}
/**
* Find the next item. If reloop is true and at the end of the list, it will go back to the
* first item. If given, the `validate` callback will be used to determine whether the next item
* is valid. If not valid, it will try to find the next item again.
*
* @param {boolean} backwards Specifies the direction of searching (forwards/backwards)
* @param {*} item The item whose subsequent item we are looking for
* @param {Function=} validate The `validate` function
* @param {integer=} limit The recursion limit
*
* @returns {*} The subsequent item or null
*/
function findSubsequentItem(backwards, item, validate, limit) {
validate = validate || trueFn;
var curIndex = indexOf(item);
while (true) {
if (!inRange(curIndex)) return null;
var nextIndex = curIndex + (backwards ? -1 : 1);
var foundItem = null;
if (inRange(nextIndex)) {
foundItem = _items[nextIndex];
} else if (reloop) {
foundItem = backwards ? last() : first();
nextIndex = indexOf(foundItem);
}
if ((foundItem === null) || (nextIndex === limit)) return null;
if (validate(foundItem)) return foundItem;
if (angular.isUndefined(limit)) limit = nextIndex;
curIndex = nextIndex;
}
}
}
})(); // material.core: .config( ["$provide", function($provide){
(function(){
"use strict";
angular
.module('material.core')
.config( ["$provide", function($provide) {
$provide.decorator('$mdUtil', ['$delegate', function ($delegate) {
// Inject the prefixer into our original $mdUtil service.
$delegate.prefixer = MdPrefixer;
return $delegate;
}]);
}]);
function MdPrefixer(initialAttributes, buildSelector) {
var PREFIXES = ['data', 'x'];
if (initialAttributes) {
// The prefixer also accepts attributes as a parameter, and immediately builds a list or selector for
// the specified attributes.
return buildSelector ? _buildSelector(initialAttributes) : _buildList(initialAttributes);
}
return {
buildList: _buildList,
buildSelector: _buildSelector,
hasAttribute: _hasAttribute
};
function _buildList(attributes) {
attributes = angular.isArray(attributes) ? attributes : [attributes];
attributes.forEach(function(item) {
PREFIXES.forEach(function(prefix) {
attributes.push(prefix + '-' + item);
});
});
return attributes;
}
function _buildSelector(attributes) {
attributes = angular.isArray(attributes) ? attributes : [attributes];
return _buildList(attributes)
.map(function (item) {
return '[' + item + ']'
})
.join(',');
}
function _hasAttribute(element, attribute) {
element = element[0] || element;
var prefixedAttrs = _buildList(attribute);
for (var i = 0; i < prefixedAttrs.length; i++) {
if (element.hasAttribute(prefixedAttrs[i])) {
return true;
}
}
return false;
}
}
})(); // material.core: .config( ["$provide", function($provide) {
(function(){
"use strict";
/*
* This var has to be outside the angular factory, otherwise when
* there are multiple material apps on the same page, each app
* will create its own instance of this array and the app's IDs
* will not be unique.
*/
var nextUniqueId = 0;
angular
.module('material.core')
.factory('$mdUtil', UtilFactory);
function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $interpolate, $log, $rootElement, $window) {
// Setup some core variables for the processTemplate method
var startSymbol = $interpolate.startSymbol(),
endSymbol = $interpolate.endSymbol(),
usesStandardSymbols = ((startSymbol === '{{') && (endSymbol === '}}'));
var hasComputedStyle = function (target, key, expectedVal) {
var hasValue = false;
if ( target && target.length ) {
var computedStyles = $window.getComputedStyle(target[0]);
hasValue = angular.isDefined(computedStyles[key]) && (expectedVal ? computedStyles[key] == expectedVal : true);
}
return hasValue;
};
var $mdUtil = {
dom: {},
now: window.performance ?
angular.bind(window.performance, window.performance.now) : Date.now || function() {
return new Date().getTime();
},
bidi : function(element, property, lValue, rValue) {
var ltr = !($document[0].dir == 'rtl' || $document[0].body.dir == 'rtl');
// If accessor
if ( arguments.length == 0 ) return ltr ? 'ltr' : 'rtl';
// If mutator
if ( ltr && angular.isDefined(lValue)) {
angular.element(element).css(property, validate(lValue));
}
else if ( !ltr && angular.isDefined(rValue)) {
angular.element(element).css(property, validate(rValue) );
}
// Internal utils
function validate(value) {
return !value ? '0' :
hasPx(value) ? value : value + 'px';
}
function hasPx(value) {
return String(value).indexOf('px') > -1;
}
},
clientRect: function(element, offsetParent, isOffsetRect) {
var node = getNode(element);
offsetParent = getNode(offsetParent || node.offsetParent || document.body);
var nodeRect = node.getBoundingClientRect();
// The user can ask for an offsetRect: a rect relative to the offsetParent,
// or a clientRect: a rect relative to the page
var offsetRect = isOffsetRect ?
offsetParent.getBoundingClientRect() :
{left: 0, top: 0, width: 0, height: 0};
return {
left: nodeRect.left - offsetRect.left,
top: nodeRect.top - offsetRect.top,
width: nodeRect.width,
height: nodeRect.height
};
},
offsetRect: function(element, offsetParent) {
return $mdUtil.clientRect(element, offsetParent, true);
},
// Annoying method to copy nodes to an array, thanks to IE
nodesToArray: function(nodes) {
nodes = nodes || [];
var results = [];
for (var i = 0; i < nodes.length; ++i) {
results.push(nodes.item(i));
}
return results;
},
scrollTop: function(element) {
element = angular.element(element || $document[0].body);
var body = (element[0] == $document[0].body) ? $document[0].body : undefined;
var scrollTop = body ? body.scrollTop + body.parentElement.scrollTop : 0;
// Calculate the positive scroll offset
return scrollTop || Math.abs(element[0].getBoundingClientRect().top);
},
findFocusTarget: function(containerEl, attributeVal) {
var AUTO_FOCUS = this.prefixer('md-autofocus', true);
var elToFocus;
elToFocus = scanForFocusable(containerEl, attributeVal || AUTO_FOCUS);
if ( !elToFocus && attributeVal != AUTO_FOCUS) {
// Scan for deprecated attribute
elToFocus = scanForFocusable(containerEl, this.prefixer('md-auto-focus', true));
if ( !elToFocus ) {
// Scan for fallback to 'universal' API
elToFocus = scanForFocusable(containerEl, AUTO_FOCUS);
}
}
return elToFocus;
function scanForFocusable(target, selector) {
var elFound, items = target[0].querySelectorAll(selector);
// Find the last child element with the focus attribute
if ( items && items.length ){
items.length && angular.forEach(items, function(it) {
it = angular.element(it);
// Check the element for the _md-autofocus class to ensure any associated expression
// evaluated to true.
var isFocusable = it.hasClass('_md-autofocus');
if (isFocusable) elFound = it;
});
}
return elFound;
}
},
disableScrollAround: function(element, parent) {
$mdUtil.disableScrollAround._count = $mdUtil.disableScrollAround._count || 0;
++$mdUtil.disableScrollAround._count;
if ($mdUtil.disableScrollAround._enableScrolling) return $mdUtil.disableScrollAround._enableScrolling;
var body = $document[0].body,
restoreBody = disableBodyScroll(),
restoreElement = disableElementScroll(parent);
return $mdUtil.disableScrollAround._enableScrolling = function() {
if (!--$mdUtil.disableScrollAround._count) {
restoreBody();
restoreElement();
delete $mdUtil.disableScrollAround._enableScrolling;
}
};
// Creates a virtual scrolling mask to absorb touchmove, keyboard, scrollbar clicking, and wheel events
function disableElementScroll(element) {
element = angular.element(element || body)[0];
var scrollMask = angular.element(
'<div class="md-scroll-mask">' +
' <div class="md-scroll-mask-bar"></div>' +
'</div>');
element.appendChild(scrollMask[0]);
scrollMask.on('wheel', preventDefault);
scrollMask.on('touchmove', preventDefault);
return function restoreScroll() {
scrollMask.off('wheel');
scrollMask.off('touchmove');
scrollMask[0].parentNode.removeChild(scrollMask[0]);
delete $mdUtil.disableScrollAround._enableScrolling;
};
function preventDefault(e) {
e.preventDefault();
}
}
// Converts the body to a position fixed block and translate it to the proper scroll
// position
function disableBodyScroll() {
var htmlNode = body.parentNode;
var restoreHtmlStyle = htmlNode.style.cssText || '';
var restoreBodyStyle = body.style.cssText || '';
var scrollOffset = $mdUtil.scrollTop(body);
var clientWidth = body.clientWidth;
if (body.scrollHeight > body.clientHeight + 1) {
applyStyles(body, {
position: 'fixed',
width: '100%',
top: -scrollOffset + 'px'
});
htmlNode.style.overflowY = 'scroll';
}
if (body.clientWidth < clientWidth) applyStyles(body, {overflow: 'hidden'});
return function restoreScroll() {
body.style.cssText = restoreBodyStyle;
htmlNode.style.cssText = restoreHtmlStyle;
body.scrollTop = scrollOffset;
htmlNode.scrollTop = scrollOffset;
};
}
function applyStyles(el, styles) {
for (var key in styles) {
el.style[key] = styles[key];
}
}
},
enableScrolling: function() {
var method = this.disableScrollAround._enableScrolling;
method && method();
},
floatingScrollbars: function() {
if (this.floatingScrollbars.cached === undefined) {
var tempNode = angular.element('<div><div></div></div>').css({
width: '100%',
'z-index': -1,
position: 'absolute',
height: '35px',
'overflow-y': 'scroll'
});
tempNode.children().css('height', '60px');
$document[0].body.appendChild(tempNode[0]);
this.floatingScrollbars.cached = (tempNode[0].offsetWidth == tempNode[0].childNodes[0].offsetWidth);
tempNode.remove();
}
return this.floatingScrollbars.cached;
},
// Mobile safari only allows you to set focus in click event listeners...
forceFocus: function(element) {
var node = element[0] || element;
document.addEventListener('click', function focusOnClick(ev) {
if (ev.target === node && ev.$focus) {
node.focus();
ev.stopImmediatePropagation();
ev.preventDefault();
node.removeEventListener('click', focusOnClick);
}
}, true);
var newEvent = document.createEvent('MouseEvents');
newEvent.initMouseEvent('click', false, true, window, {}, 0, 0, 0, 0,
false, false, false, false, 0, null);
newEvent.$material = true;
newEvent.$focus = true;
node.dispatchEvent(newEvent);
},
createBackdrop: function(scope, addClass) {
return $compile($mdUtil.supplant('<md-backdrop class="{0}">', [addClass]))(scope);
},
supplant: function(template, values, pattern) {
pattern = pattern || /\{([^\{\}]*)\}/g;
return template.replace(pattern, function(a, b) {
var p = b.split('.'),
r = values;
try {
for (var s in p) {
if (p.hasOwnProperty(s) ) {
r = r[p[s]];
}
}
} catch (e) {
r = a;
}
return (typeof r === 'string' || typeof r === 'number') ? r : a;
});
},
fakeNgModel: function() {
return {
$fake: true,
$setTouched: angular.noop,
$setViewValue: function(value) {
this.$viewValue = value;
this.$render(value);
this.$viewChangeListeners.forEach(function(cb) {
cb();
});
},
$isEmpty: function(value) {
return ('' + value).length === 0;
},
$parsers: [],
$formatters: [],
$viewChangeListeners: [],
$render: angular.noop
};
},
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds.
// @param wait Integer value of msecs to delay (since last debounce reset); default value 10 msecs
// @param invokeApply should the $timeout trigger $digest() dirty checking
debounce: function(func, wait, scope, invokeApply) {
var timer;
return function debounced() {
var context = scope,
args = Array.prototype.slice.call(arguments);
$timeout.cancel(timer);
timer = $timeout(function() {
timer = undefined;
func.apply(context, args);
}, wait || 10, invokeApply);
};
},
// Returns a function that can only be triggered every `delay` milliseconds.
// In other words, the function will not be called unless it has been more
// than `delay` milliseconds since the last call.
throttle: function throttle(func, delay) {
var recent;
return function throttled() {
var context = this;
var args = arguments;
var now = $mdUtil.now();
if (!recent || (now - recent > delay)) {
func.apply(context, args);
recent = now;
}
};
},
time: function time(cb) {
var start = $mdUtil.now();
cb();
return $mdUtil.now() - start;
},
valueOnUse : function (scope, key, getter) {
var value = null, args = Array.prototype.slice.call(arguments);
var params = (args.length > 3) ? args.slice(3) : [ ];
Object.defineProperty(scope, key, {
get: function () {
if (value === null) value = getter.apply(scope, params);
return value;
}
});
},
nextUid: function() {
return '' + nextUniqueId++;
},
// Stop watchers and events from firing on a scope without destroying it,
// by disconnecting it from its parent and its siblings' linked lists.
disconnectScope: function disconnectScope(scope) {
if (!scope) return;
// we can't destroy the root scope or a scope that has been already destroyed
if (scope.$root === scope) return;
if (scope.$$destroyed) return;
var parent = scope.$parent;
scope.$$disconnected = true;
// See Scope.$destroy
if (parent.$$childHead === scope) parent.$$childHead = scope.$$nextSibling;
if (parent.$$childTail === scope) parent.$$childTail = scope.$$prevSibling;
if (scope.$$prevSibling) scope.$$prevSibling.$$nextSibling = scope.$$nextSibling;
if (scope.$$nextSibling) scope.$$nextSibling.$$prevSibling = scope.$$prevSibling;
scope.$$nextSibling = scope.$$prevSibling = null;
},
// Undo the effects of disconnectScope above.
reconnectScope: function reconnectScope(scope) {
if (!scope) return;
// we can't disconnect the root node or scope already disconnected
if (scope.$root === scope) return;
if (!scope.$$disconnected) return;
var child = scope;
var parent = child.$parent;
child.$$disconnected = false;
// See Scope.$new for this logic...
child.$$prevSibling = parent.$$childTail;
if (parent.$$childHead) {
parent.$$childTail.$$nextSibling = child;
parent.$$childTail = child;
} else {
parent.$$childHead = parent.$$childTail = child;
}
},
/*
* getClosest replicates jQuery.closest() to walk up the DOM tree until it finds a matching nodeName
*
* @param el Element to start walking the DOM from
* @param tagName Tag name to find closest to el, such as 'form'
* @param onlyParent Only start checking from the parent element, not `el`.
*/
getClosest: function getClosest(el, tagName, onlyParent) {
if (el instanceof angular.element) el = el[0];
tagName = tagName.toUpperCase();
if (onlyParent) el = el.parentNode;
if (!el) return null;
do {
if (el.nodeName === tagName) {
return el;
}
} while (el = el.parentNode);
return null;
},
elementContains: function(node, child) {
var hasContains = (window.Node && window.Node.prototype && Node.prototype.contains);
var findFn = hasContains ? angular.bind(node, node.contains) : angular.bind(node, function(arg) {
// compares the positions of two nodes and returns a bitmask
return (node === child) || !!(this.compareDocumentPosition(arg) & 16)
});
return findFn(child);
},
extractElementByName: function(element, nodeName, scanDeep, warnNotFound) {
var found = scanTree(element);
if (!found && !!warnNotFound) {
$log.warn( $mdUtil.supplant("Unable to find node '{0}' in element '{1}'.",[nodeName, element[0].outerHTML]) );
}
return angular.element(found || element);
/**
* Breadth-First tree scan for element with matching `nodeName`
*/
function scanTree(element) {
return scanLevel(element) || (!!scanDeep ? scanChildren(element) : null);
}
/**
* Case-insensitive scan of current elements only (do not descend).
*/
function scanLevel(element) {
if ( element ) {
for (var i = 0, len = element.length; i < len; i++) {
if (element[i].nodeName.toLowerCase() === nodeName) {
return element[i];
}
}
}
return null;
}
/**
* Scan children of specified node
*/
function scanChildren(element) {
var found;
if ( element ) {
for (var i = 0, len = element.length; i < len; i++) {
var target = element[i];
if ( !found ) {
for (var j = 0, numChild = target.childNodes.length; j < numChild; j++) {
found = found || scanTree([target.childNodes[j]]);
}
}
}
}
return found;
}
},
/**
* Give optional properties with no value a boolean true if attr provided or false otherwise
*/
initOptionalProperties: function(scope, attr, defaults) {
defaults = defaults || {};
angular.forEach(scope.$$isolateBindings, function(binding, key) {
if (binding.optional && angular.isUndefined(scope[key])) {
var attrIsDefined = angular.isDefined(attr[binding.attrName]);
scope[key] = angular.isDefined(defaults[key]) ? defaults[key] : attrIsDefined;
}
});
},
/**
* Alternative to $timeout calls with 0 delay.
* nextTick() coalesces all calls within a single frame
* to minimize $digest thrashing
*
* @param callback
* @param digest
* @returns {*}
*/
nextTick: function(callback, digest, scope) {
//-- grab function reference for storing state details
var nextTick = $mdUtil.nextTick;
var timeout = nextTick.timeout;
var queue = nextTick.queue || [];
//-- add callback to the queue
queue.push({scope: scope, callback: callback});
//-- set default value for digest
if (digest == null) digest = true;
//-- store updated digest/queue values
nextTick.digest = nextTick.digest || digest;
nextTick.queue = queue;
//-- either return existing timeout or create a new one
return timeout || (nextTick.timeout = $timeout(processQueue, 0, false));
/**
* Grab a copy of the current queue
* Clear the queue for future use
* Process the existing queue
* Trigger digest if necessary
*/
function processQueue() {
var queue = nextTick.queue;
var digest = nextTick.digest;
nextTick.queue = [];
nextTick.timeout = null;
nextTick.digest = false;
queue.forEach(function(queueItem) {
var skip = queueItem.scope && queueItem.scope.$$destroyed;
if (!skip) {
queueItem.callback();
}
});
if (digest) $rootScope.$digest();
}
},
/**
* Processes a template and replaces the start/end symbols if the application has
* overriden them.
*
* @param template The template to process whose start/end tags may be replaced.
* @returns {*}
*/
processTemplate: function(template) {
if (usesStandardSymbols) {
return template;
} else {
if (!template || !angular.isString(template)) return template;
return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
}
},
/**
* Scan up dom hierarchy for enabled parent;
*/
getParentWithPointerEvents: function (element) {
var parent = element.parent();
// jqLite might return a non-null, but still empty, parent; so check for parent and length
while (hasComputedStyle(parent, 'pointer-events', 'none')) {
parent = parent.parent();
}
return parent;
},
getNearestContentElement: function (element) {
var current = element.parent()[0];
// Look for the nearest parent md-content, stopping at the rootElement.
while (current && current !== $rootElement[0] && current !== document.body && current.nodeName.toUpperCase() !== 'MD-CONTENT') {
current = current.parentNode;
}
return current;
},
/**
* Parses an attribute value, mostly a string.
* By default checks for negated values and returns `false´ if present.
* Negated values are: (native falsy) and negative strings like:
* `false` or `0`.
* @param value Attribute value which should be parsed.
* @param negatedCheck When set to false, won't check for negated values.
* @returns {boolean}
*/
parseAttributeBoolean: function(value, negatedCheck) {
return value === '' || !!value && (negatedCheck === false || value !== 'false' && value !== '0');
},
hasComputedStyle: hasComputedStyle
};
// Instantiate other namespace utility methods
$mdUtil.dom.animator = $$mdAnimate($mdUtil);
return $mdUtil;
function getNode(el) {
return el[0] || el;
}
}
UtilFactory.$inject = ["$document", "$timeout", "$compile", "$rootScope", "$$mdAnimate", "$interpolate", "$log", "$rootElement", "$window"];
/*
* Since removing jQuery from the demos, some code that uses `element.focus()` is broken.
* We need to add `element.focus()`, because it's testable unlike `element[0].focus`.
*/
angular.element.prototype.focus = angular.element.prototype.focus || function() {
if (this.length) {
this[0].focus();
}
return this;
};
angular.element.prototype.blur = angular.element.prototype.blur || function() {
if (this.length) {
this[0].blur();
}
return this;
};
})(); // material.core: .factory('$mdUtil', UtilFactory);
(function(){
"use strict";
angular.module('material.core.theming.palette', [])
.constant('$mdColorPalette', {
'red': {
'50': '#ffebee',
'100': '#ffcdd2',
'200': '#ef9a9a',
'300': '#e57373',
'400': '#ef5350',
'500': '#f44336',
'600': '#e53935',
'700': '#d32f2f',
'800': '#c62828',
'900': '#b71c1c',
'A100': '#ff8a80',
'A200': '#ff5252',
'A400': '#ff1744',
'A700': '#d50000',
'contrastDefaultColor': 'light',
'contrastDarkColors': '50 100 200 300 A100',
'contrastStrongLightColors': '400 500 600 700 A200 A400 A700'
},
'pink': {
'50': '#fce4ec',
'100': '#f8bbd0',
'200': '#f48fb1',
'300': '#f06292',
'400': '#ec407a',
'500': '#e91e63',
'600': '#d81b60',
'700': '#c2185b',
'800': '#ad1457',
'900': '#880e4f',
'A100': '#ff80ab',
'A200': '#ff4081',
'A400': '#f50057',
'A700': '#c51162',
'contrastDefaultColor': 'light',
'contrastDarkColors': '50 100 200 A100',
'contrastStrongLightColors': '500 600 A200 A400 A700'
},
'purple': {
'50': '#f3e5f5',
'100': '#e1bee7',
'200': '#ce93d8',
'300': '#ba68c8',
'400': '#ab47bc',
'500': '#9c27b0',
'600': '#8e24aa',
'700': '#7b1fa2',
'800': '#6a1b9a',
'900': '#4a148c',
'A100': '#ea80fc',
'A200': '#e040fb',
'A400': '#d500f9',
'A700': '#aa00ff',
'contrastDefaultColor': 'light',
'contrastDarkColors': '50 100 200 A100',
'contrastStrongLightColors': '300 400 A200 A400 A700'
},
'deep-purple': {
'50': '#ede7f6',
'100': '#d1c4e9',
'200': '#b39ddb',
'300': '#9575cd',
'400': '#7e57c2',
'500': '#673ab7',
'600': '#5e35b1',
'700': '#512da8',
'800': '#4527a0',
'900': '#311b92',
'A100': '#b388ff',
'A200': '#7c4dff',
'A400': '#651fff',
'A700': '#6200ea',
'contrastDefaultColor': 'light',
'contrastDarkColors': '50 100 200 A100',
'contrastStrongLightColors': '300 400 A200'
},
'indigo': {
'50': '#e8eaf6',
'100': '#c5cae9',
'200': '#9fa8da',
'300': '#7986cb',
'400': '#5c6bc0',
'500': '#3f51b5',
'600': '#3949ab',
'700': '#303f9f',
'800': '#283593',
'900': '#1a237e',
'A100': '#8c9eff',
'A200': '#536dfe',
'A400': '#3d5afe',
'A700': '#304ffe',
'contrastDefaultColor': 'light',
'contrastDarkColors': '50 100 200 A100',
'contrastStrongLightColors': '300 400 A200 A400'
},
'blue': {
'50': '#e3f2fd',
'100': '#bbdefb',
'200': '#90caf9',
'300': '#64b5f6',
'400': '#42a5f5',
'500': '#2196f3',
'600': '#1e88e5',
'700': '#1976d2',
'800': '#1565c0',
'900': '#0d47a1',
'A100': '#82b1ff',
'A200': '#448aff',
'A400': '#2979ff',
'A700': '#2962ff',
'contrastDefaultColor': 'light',
'contrastDarkColors': '50 100 200 300 400 A100',
'contrastStrongLightColors': '500 600 700 A200 A400 A700'
},
'light-blue': {
'50': '#e1f5fe',
'100': '#b3e5fc',
'200': '#81d4fa',
'300': '#4fc3f7',
'400': '#29b6f6',
'500': '#03a9f4',
'600': '#039be5',
'700': '#0288d1',
'800': '#0277bd',
'900': '#01579b',
'A100': '#80d8ff',
'A200': '#40c4ff',
'A400': '#00b0ff',
'A700': '#0091ea',
'contrastDefaultColor': 'dark',
'contrastLightColors': '600 700 800 900 A700',
'contrastStrongLightColors': '600 700 800 A700'
},
'cyan': {
'50': '#e0f7fa',
'100': '#b2ebf2',
'200': '#80deea',
'300': '#4dd0e1',
'400': '#26c6da',
'500': '#00bcd4',
'600': '#00acc1',
'700': '#0097a7',
'800': '#00838f',
'900': '#006064',
'A100': '#84ffff',
'A200': '#18ffff',
'A400': '#00e5ff',
'A700': '#00b8d4',
'contrastDefaultColor': 'dark',
'contrastLightColors': '700 800 900',
'contrastStrongLightColors': '700 800 900'
},
'teal': {
'50': '#e0f2f1',
'100': '#b2dfdb',
'200': '#80cbc4',
'300': '#4db6ac',
'400': '#26a69a',
'500': '#009688',
'600': '#00897b',
'700': '#00796b',
'800': '#00695c',
'900': '#004d40',
'A100': '#a7ffeb',
'A200': '#64ffda',
'A400': '#1de9b6',
'A700': '#00bfa5',
'contrastDefaultColor': 'dark',
'contrastLightColors': '500 600 700 800 900',
'contrastStrongLightColors': '500 600 700'
},
'green': {
'50': '#e8f5e9',
'100': '#c8e6c9',
'200': '#a5d6a7',
'300': '#81c784',
'400': '#66bb6a',
'500': '#4caf50',
'600': '#43a047',
'700': '#388e3c',
'800': '#2e7d32',
'900': '#1b5e20',
'A100': '#b9f6ca',
'A200': '#69f0ae',
'A400': '#00e676',
'A700': '#00c853',
'contrastDefaultColor': 'dark',
'contrastLightColors': '500 600 700 800 900',
'contrastStrongLightColors': '500 600 700'
},
'light-green': {
'50': '#f1f8e9',
'100': '#dcedc8',
'200': '#c5e1a5',
'300': '#aed581',
'400': '#9ccc65',
'500': '#8bc34a',
'600': '#7cb342',
'700': '#689f38',
'800': '#558b2f',
'900': '#33691e',
'A100': '#ccff90',
'A200': '#b2ff59',
'A400': '#76ff03',
'A700': '#64dd17',
'contrastDefaultColor': 'dark',
'contrastLightColors': '700 800 900',
'contrastStrongLightColors': '700 800 900'
},
'lime': {
'50': '#f9fbe7',
'100': '#f0f4c3',
'200': '#e6ee9c',
'300': '#dce775',
'400': '#d4e157',
'500': '#cddc39',
'600': '#c0ca33',
'700': '#afb42b',
'800': '#9e9d24',
'900': '#827717',
'A100': '#f4ff81',
'A200': '#eeff41',
'A400': '#c6ff00',
'A700': '#aeea00',
'contrastDefaultColor': 'dark',
'contrastLightColors': '900',
'contrastStrongLightColors': '900'
},
'yellow': {
'50': '#fffde7',
'100': '#fff9c4',
'200': '#fff59d',
'300': '#fff176',
'400': '#ffee58',
'500': '#ffeb3b',
'600': '#fdd835',
'700': '#fbc02d',
'800': '#f9a825',
'900': '#f57f17',
'A100': '#ffff8d',
'A200': '#ffff00',
'A400': '#ffea00',
'A700': '#ffd600',
'contrastDefaultColor': 'dark'
},
'amber': {
'50': '#fff8e1',
'100': '#ffecb3',
'200': '#ffe082',
'300': '#ffd54f',
'400': '#ffca28',
'500': '#ffc107',
'600': '#ffb300',
'700': '#ffa000',
'800': '#ff8f00',
'900': '#ff6f00',
'A100': '#ffe57f',
'A200': '#ffd740',
'A400': '#ffc400',
'A700': '#ffab00',
'contrastDefaultColor': 'dark'
},
'orange': {
'50': '#fff3e0',
'100': '#ffe0b2',
'200': '#ffcc80',
'300': '#ffb74d',
'400': '#ffa726',
'500': '#ff9800',
'600': '#fb8c00',
'700': '#f57c00',
'800': '#ef6c00',
'900': '#e65100',
'A100': '#ffd180',
'A200': '#ffab40',
'A400': '#ff9100',
'A700': '#ff6d00',
'contrastDefaultColor': 'dark',
'contrastLightColors': '800 900',
'contrastStrongLightColors': '800 900'
},
'deep-orange': {
'50': '#fbe9e7',
'100': '#ffccbc',
'200': '#ffab91',
'300': '#ff8a65',
'400': '#ff7043',
'500': '#ff5722',
'600': '#f4511e',
'700': '#e64a19',
'800': '#d84315',
'900': '#bf360c',
'A100': '#ff9e80',
'A200': '#ff6e40',
'A400': '#ff3d00',
'A700': '#dd2c00',
'contrastDefaultColor': 'light',
'contrastDarkColors': '50 100 200 300 400 A100 A200',
'contrastStrongLightColors': '500 600 700 800 900 A400 A700'
},
'brown': {
'50': '#efebe9',
'100': '#d7ccc8',
'200': '#bcaaa4',
'300': '#a1887f',
'400': '#8d6e63',
'500': '#795548',
'600': '#6d4c41',
'700': '#5d4037',
'800': '#4e342e',
'900': '#3e2723',
'A100': '#d7ccc8',
'A200': '#bcaaa4',
'A400': '#8d6e63',
'A700': '#5d4037',
'contrastDefaultColor': 'light',
'contrastDarkColors': '50 100 200 A100 A200',
'contrastStrongLightColors': '300 400'
},
'grey': {
'50': '#fafafa',
'100': '#f5f5f5',
'200': '#eeeeee',
'300': '#e0e0e0',
'400': '#bdbdbd',
'500': '#9e9e9e',
'600': '#757575',
'700': '#616161',
'800': '#424242',
'900': '#212121',
'A100': '#ffffff',
'A200': '#000000',
'A400': '#303030',
'A700': '#616161',
'contrastDefaultColor': 'dark',
'contrastLightColors': '600 700 800 900 A200 A400 A700'
},
'blue-grey': {
'50': '#eceff1',
'100': '#cfd8dc',
'200': '#b0bec5',
'300': '#90a4ae',
'400': '#78909c',
'500': '#607d8b',
'600': '#546e7a',
'700': '#455a64',
'800': '#37474f',
'900': '#263238',
'A100': '#cfd8dc',
'A200': '#b0bec5',
'A400': '#78909c',
'A700': '#455a64',
'contrastDefaultColor': 'light',
'contrastDarkColors': '50 100 200 300 A100 A200',
'contrastStrongLightColors': '400 500 700'
}
});
})(); // material.core.theming.pallett
(function(){
"use strict";
/**
* @ngdoc module
* @name material.core.theming
* @description
* Theming
*/
angular.module('material.core.theming', ['material.core.theming.palette'])
.directive('mdTheme', ThemingDirective)
.provider('$mdTheming', ThemingProvider)
.run(generateAllThemes);
/**
* @ngdoc service
* @name $mdThemingProvider
* @module material.core.theming
*
* @description Provider to configure the `$mdTheming` service.
*/
/**
* @ngdoc method
* @name $mdThemingProvider#setNonce
* @param {string} nonceValue The nonce to be added as an attribute to the theme style tags.
* Setting a value allows the use CSP policy without using the unsafe-inline directive.
*/
/**
* @ngdoc method
* @name $mdThemingProvider#setDefaultTheme
* @param {string} themeName Default theme name to be applied to elements. Default value is `default`.
*/
/**
* @ngdoc method
* @name $mdThemingProvider#alwaysWatchTheme
* @param {boolean} watch Whether or not to always watch themes for changes and re-apply
* classes when they change. Default is `false`. Enabling can reduce performance.
*/
/* Some Example Valid Theming Expressions
* =======================================
*
* Intention group expansion: (valid for primary, accent, warn, background)
*
* {{primary-100}} - grab shade 100 from the primary palette
* {{primary-100-0.7}} - grab shade 100, apply opacity of 0.7
* {{primary-100-contrast}} - grab shade 100's contrast color
* {{primary-hue-1}} - grab the shade assigned to hue-1 from the primary palette
* {{primary-hue-1-0.7}} - apply 0.7 opacity to primary-hue-1
* {{primary-color}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured shades set for each hue
* {{primary-color-0.7}} - Apply 0.7 opacity to each of the above rules
* {{primary-contrast}} - Generates .md-hue-1, .md-hue-2, .md-hue-3 with configured contrast (ie. text) color shades set for each hue
* {{primary-contrast-0.7}} - Apply 0.7 opacity to each of the above rules
*
* Foreground expansion: Applies rgba to black/white foreground text
*
* {{foreground-1}} - used for primary text
* {{foreground-2}} - used for secondary text/divider
* {{foreground-3}} - used for disabled text
* {{foreground-4}} - used for dividers
*
*/
// In memory generated CSS rules; registered by theme.name
var GENERATED = { };
// In memory storage of defined themes and color palettes (both loaded by CSS, and user specified)
var PALETTES;
// Text Colors on light and dark backgrounds
// @see https://www.google.com/design/spec/style/color.html#color-text-background-colors
var DARK_FOREGROUND = {
name: 'dark',
'1': 'rgba(0,0,0,0.87)',
'2': 'rgba(0,0,0,0.54)',
'3': 'rgba(0,0,0,0.38)',
'4': 'rgba(0,0,0,0.12)'
};
var LIGHT_FOREGROUND = {
name: 'light',
'1': 'rgba(255,255,255,1.0)',
'2': 'rgba(255,255,255,0.7)',
'3': 'rgba(255,255,255,0.5)',
'4': 'rgba(255,255,255,0.12)'
};
var DARK_SHADOW = '1px 1px 0px rgba(0,0,0,0.4), -1px -1px 0px rgba(0,0,0,0.4)';
var LIGHT_SHADOW = '';
var DARK_CONTRAST_COLOR = colorToRgbaArray('rgba(0,0,0,0.87)');
var LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgba(255,255,255,0.87)');
var STRONG_LIGHT_CONTRAST_COLOR = colorToRgbaArray('rgb(255,255,255)');
var THEME_COLOR_TYPES = ['primary', 'accent', 'warn', 'background'];
var DEFAULT_COLOR_TYPE = 'primary';
// A color in a theme will use these hues by default, if not specified by user.
var LIGHT_DEFAULT_HUES = {
'accent': {
'default': 'A200',
'hue-1': 'A100',
'hue-2': 'A400',
'hue-3': 'A700'
},
'background': {
'default': '50',
'hue-1': 'A100',
'hue-2': '100',
'hue-3': '300'
}
};
var DARK_DEFAULT_HUES = {
'background': {
'default': 'A400',
'hue-1': '800',
'hue-2': '900',
'hue-3': 'A200'
}
};
THEME_COLOR_TYPES.forEach(function(colorType) {
// Color types with unspecified default hues will use these default hue values
var defaultDefaultHues = {
'default': '500',
'hue-1': '300',
'hue-2': '800',
'hue-3': 'A100'
};
if (!LIGHT_DEFAULT_HUES[colorType]) LIGHT_DEFAULT_HUES[colorType] = defaultDefaultHues;
if (!DARK_DEFAULT_HUES[colorType]) DARK_DEFAULT_HUES[colorType] = defaultDefaultHues;
});
var VALID_HUE_VALUES = [
'50', '100', '200', '300', '400', '500', '600',
'700', '800', '900', 'A100', 'A200', 'A400', 'A700'
];
// Whether or not themes are to be generated on-demand (vs. eagerly).
var generateOnDemand = false;
// Nonce to be added as an attribute to the generated themes style tags.
var nonce = null;
var disableTheming = false;
function ThemingProvider($mdColorPalette) {
PALETTES = { };
var THEMES = { };
var themingProvider;
var defaultTheme = 'default';
var alwaysWatchTheme = false;
// Load JS Defined Palettes
angular.extend(PALETTES, $mdColorPalette);
// Default theme defined in core.js
ThemingService.$inject = ["$rootScope", "$log"];
return themingProvider = {
definePalette: definePalette,
extendPalette: extendPalette,
theme: registerTheme,
/**
* Easy way to disable theming without having to use
* `.constant("$MD_THEME_CSS","");` This disables
* all dynamic theme style sheet generations and injections...
*/
disableTheming: function() {
disableTheming = true;
},
setNonce: function(nonceValue) {
nonce = nonceValue;
},
setDefaultTheme: function(theme) {
defaultTheme = theme;
},
alwaysWatchTheme: function(alwaysWatch) {
alwaysWatchTheme = alwaysWatch;
},
generateThemesOnDemand: function(onDemand) {
generateOnDemand = onDemand;
},
$get: ThemingService,
_LIGHT_DEFAULT_HUES: LIGHT_DEFAULT_HUES,
_DARK_DEFAULT_HUES: DARK_DEFAULT_HUES,
_PALETTES: PALETTES,
_THEMES: THEMES,
_parseRules: parseRules,
_rgba: rgba
};
// Example: $mdThemingProvider.definePalette('neonRed', { 50: '#f5fafa', ... });
function definePalette(name, map) {
map = map || {};
PALETTES[name] = checkPaletteValid(name, map);
return themingProvider;
}
// Returns an new object which is a copy of a given palette `name` with variables from
// `map` overwritten
// Example: var neonRedMap = $mdThemingProvider.extendPalette('red', { 50: '#f5fafafa' });
function extendPalette(name, map) {
return checkPaletteValid(name, angular.extend({}, PALETTES[name] || {}, map) );
}
// Make sure that palette has all required hues
function checkPaletteValid(name, map) {
var missingColors = VALID_HUE_VALUES.filter(function(field) {
return !map[field];
});
if (missingColors.length) {
throw new Error("Missing colors %1 in palette %2!"
.replace('%1', missingColors.join(', '))
.replace('%2', name));
}
return map;
}
// Register a theme (which is a collection of color palettes to use with various states
// ie. warn, accent, primary )
// Optionally inherit from an existing theme
// $mdThemingProvider.theme('custom-theme').primaryPalette('red');
function registerTheme(name, inheritFrom) {
if (THEMES[name]) return THEMES[name];
inheritFrom = inheritFrom || 'default';
var parentTheme = typeof inheritFrom === 'string' ? THEMES[inheritFrom] : inheritFrom;
var theme = new Theme(name);
if (parentTheme) {
angular.forEach(parentTheme.colors, function(color, colorType) {
theme.colors[colorType] = {
name: color.name,
// Make sure a COPY of the hues is given to the child color,
// not the same reference.
hues: angular.extend({}, color.hues)
};
});
}
THEMES[name] = theme;
return theme;
}
function Theme(name) {
var self = this;
self.name = name;
self.colors = {};
self.dark = setDark;
setDark(false);
function setDark(isDark) {
isDark = arguments.length === 0 ? true : !!isDark;
// If no change, abort
if (isDark === self.isDark) return;
self.isDark = isDark;
self.foregroundPalette = self.isDark ? LIGHT_FOREGROUND : DARK_FOREGROUND;
self.foregroundShadow = self.isDark ? DARK_SHADOW : LIGHT_SHADOW;
// Light and dark themes have different default hues.
// Go through each existing color type for this theme, and for every
// hue value that is still the default hue value from the previous light/dark setting,
// set it to the default hue value from the new light/dark setting.
var newDefaultHues = self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES;
var oldDefaultHues = self.isDark ? LIGHT_DEFAULT_HUES : DARK_DEFAULT_HUES;
angular.forEach(newDefaultHues, function(newDefaults, colorType) {
var color = self.colors[colorType];
var oldDefaults = oldDefaultHues[colorType];
if (color) {
for (var hueName in color.hues) {
if (color.hues[hueName] === oldDefaults[hueName]) {
color.hues[hueName] = newDefaults[hueName];
}
}
}
});
return self;
}
THEME_COLOR_TYPES.forEach(function(colorType) {
var defaultHues = (self.isDark ? DARK_DEFAULT_HUES : LIGHT_DEFAULT_HUES)[colorType];
self[colorType + 'Palette'] = function setPaletteType(paletteName, hues) {
var color = self.colors[colorType] = {
name: paletteName,
hues: angular.extend({}, defaultHues, hues)
};
Object.keys(color.hues).forEach(function(name) {
if (!defaultHues[name]) {
throw new Error("Invalid hue name '%1' in theme %2's %3 color %4. Available hue names: %4"
.replace('%1', name)
.replace('%2', self.name)
.replace('%3', paletteName)
.replace('%4', Object.keys(defaultHues).join(', '))
);
}
});
Object.keys(color.hues).map(function(key) {
return color.hues[key];
}).forEach(function(hueValue) {
if (VALID_HUE_VALUES.indexOf(hueValue) == -1) {
throw new Error("Invalid hue value '%1' in theme %2's %3 color %4. Available hue values: %5"
.replace('%1', hueValue)
.replace('%2', self.name)
.replace('%3', colorType)
.replace('%4', paletteName)
.replace('%5', VALID_HUE_VALUES.join(', '))
);
}
});
return self;
};
self[colorType + 'Color'] = function() {
var args = Array.prototype.slice.call(arguments);
console.warn('$mdThemingProviderTheme.' + colorType + 'Color() has been deprecated. ' +
'Use $mdThemingProviderTheme.' + colorType + 'Palette() instead.');
return self[colorType + 'Palette'].apply(self, args);
};
});
}
function ThemingService($rootScope, $log) {
// Allow us to be invoked via a linking function signature.
var applyTheme = function (scope, el) {
if (el === undefined) { el = scope; scope = undefined; }
if (scope === undefined) { scope = $rootScope; }
applyTheme.inherit(el, el);
};
applyTheme.THEMES = angular.extend({}, THEMES);
applyTheme.PALETTES = angular.extend({}, PALETTES);
applyTheme.inherit = inheritTheme;
applyTheme.registered = registered;
applyTheme.defaultTheme = function() { return defaultTheme; };
applyTheme.generateTheme = function(name) { generateTheme(THEMES[name], name, nonce); };
return applyTheme;
/**
* Determine is specified theme name is a valid, registered theme
*/
function registered(themeName) {
if (themeName === undefined || themeName === '') return true;
return applyTheme.THEMES[themeName] !== undefined;
}
/**
* Get theme name for the element, then update with Theme CSS class
*/
function inheritTheme (el, parent) {
var ctrl = parent.controller('mdTheme');
var attrThemeValue = el.attr('md-theme-watch');
var watchTheme = (alwaysWatchTheme || angular.isDefined(attrThemeValue)) && attrThemeValue != 'false';
updateThemeClass(lookupThemeName());
el.on('$destroy', watchTheme ? $rootScope.$watch(lookupThemeName, updateThemeClass) : angular.noop );
/**
* Find the theme name from the parent controller or element data
*/
function lookupThemeName() {
// As a few components (dialog) add their controllers later, we should also watch for a controller init.
ctrl = parent.controller('mdTheme') || el.data('$mdThemeController');
return ctrl && ctrl.$mdTheme || (defaultTheme == 'default' ? '' : defaultTheme);
}
/**
* Remove old theme class and apply a new one
* NOTE: if not a valid theme name, then the current name is not changed
*/
function updateThemeClass(theme) {
if (!theme) return;
if (!registered(theme)) {
$log.warn('Attempted to use unregistered theme \'' + theme + '\'. ' +
'Register it with $mdThemingProvider.theme().');
}
var oldTheme = el.data('$mdThemeName');
if (oldTheme) el.removeClass('md-' + oldTheme +'-theme');
el.addClass('md-' + theme + '-theme');
el.data('$mdThemeName', theme);
if (ctrl) {
el.data('$mdThemeController', ctrl);
}
}
}
}
}
ThemingProvider.$inject = ["$mdColorPalette"];
function ThemingDirective($mdTheming, $interpolate, $log) {
return {
priority: 100,
link: {
pre: function(scope, el, attrs) {
var registeredCallbacks = [];
var ctrl = {
registerChanges: function (cb, context) {
if (context) {
cb = angular.bind(context, cb);
}
registeredCallbacks.push(cb);
return function () {
var index = registeredCallbacks.indexOf(cb);
if (index > -1) {
registeredCallbacks.splice(index, 1);
}
}
},
$setTheme: function (theme) {
if (!$mdTheming.registered(theme)) {
$log.warn('attempted to use unregistered theme \'' + theme + '\'');
}
ctrl.$mdTheme = theme;
registeredCallbacks.forEach(function (cb) {
cb();
})
}
};
el.data('$mdThemeController', ctrl);
ctrl.$setTheme($interpolate(attrs.mdTheme)(scope));
attrs.$observe('mdTheme', ctrl.$setTheme);
}
}
};
}
ThemingDirective.$inject = ["$mdTheming", "$interpolate", "$log"];
function parseRules(theme, colorType, rules) {
checkValidPalette(theme, colorType);
rules = rules.replace(/THEME_NAME/g, theme.name);
var generatedRules = [];
var color = theme.colors[colorType];
var themeNameRegex = new RegExp('.md-' + theme.name + '-theme', 'g');
// Matches '{{ primary-color }}', etc
var hueRegex = new RegExp('(\'|")?{{\\s*(' + colorType + ')-(color|contrast)-?(\\d\\.?\\d*)?\\s*}}(\"|\')?','g');
var simpleVariableRegex = /'?"?\{\{\s*([a-zA-Z]+)-(A?\d+|hue\-[0-3]|shadow|default)-?(\d\.?\d*)?(contrast)?\s*\}\}'?"?/g;
var palette = PALETTES[color.name];
// find and replace simple variables where we use a specific hue, not an entire palette
// eg. "{{primary-100}}"
//\(' + THEME_COLOR_TYPES.join('\|') + '\)'
rules = rules.replace(simpleVariableRegex, function(match, colorType, hue, opacity, contrast) {
if (colorType === 'foreground') {
if (hue == 'shadow') {
return theme.foregroundShadow;
} else {
return theme.foregroundPalette[hue] || theme.foregroundPalette['1'];
}
}
// `default` is also accepted as a hue-value, because the background palettes are
// using it as a name for the default hue.
if (hue.indexOf('hue') === 0 || hue === 'default') {
hue = theme.colors[colorType].hues[hue];
}
return rgba( (PALETTES[ theme.colors[colorType].name ][hue] || '')[contrast ? 'contrast' : 'value'], opacity );
});
// For each type, generate rules for each hue (ie. default, md-hue-1, md-hue-2, md-hue-3)
angular.forEach(color.hues, function(hueValue, hueName) {
var newRule = rules
.replace(hueRegex, function(match, _, colorType, hueType, opacity) {
return rgba(palette[hueValue][hueType === 'color' ? 'value' : 'contrast'], opacity);
});
if (hueName !== 'default') {
newRule = newRule.replace(themeNameRegex, '.md-' + theme.name + '-theme.md-' + hueName);
}
// Don't apply a selector rule to the default theme, making it easier to override
// styles of the base-component
if (theme.name == 'default') {
var themeRuleRegex = /((?:(?:(?: |>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)+) )?)((?:(?:\w|\.|-)+)?)\.md-default-theme((?: |>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)/g;
newRule = newRule.replace(themeRuleRegex, function(match, prefix, target, suffix) {
return match + ', ' + prefix + target + suffix;
});
}
generatedRules.push(newRule);
});
return generatedRules;
}
var rulesByType = {};
// Generate our themes at run time given the state of THEMES and PALETTES
function generateAllThemes($injector, $mdTheming) {
var head = document.head;
var firstChild = head ? head.firstElementChild : null;
var themeCss = !disableTheming && $injector.has('$MD_THEME_CSS') ? $injector.get('$MD_THEME_CSS') : '';
if ( !firstChild ) return;
if (themeCss.length === 0) return; // no rules, so no point in running this expensive task
// Expose contrast colors for palettes to ensure that text is always readable
angular.forEach(PALETTES, sanitizePalette);
// MD_THEME_CSS is a string generated by the build process that includes all the themable
// components as templates
// Break the CSS into individual rules
var rules = themeCss
.split(/\}(?!(\}|'|"|;))/)
.filter(function(rule) { return rule && rule.length; })
.map(function(rule) { return rule.trim() + '}'; });
var ruleMatchRegex = new RegExp('md-(' + THEME_COLOR_TYPES.join('|') + ')', 'g');
THEME_COLOR_TYPES.forEach(function(type) {
rulesByType[type] = '';
});
// Sort the rules based on type, allowing us to do color substitution on a per-type basis
rules.forEach(function(rule) {
var match = rule.match(ruleMatchRegex);
// First: test that if the rule has '.md-accent', it goes into the accent set of rules
for (var i = 0, type; type = THEME_COLOR_TYPES[i]; i++) {
if (rule.indexOf('.md-' + type) > -1) {
return rulesByType[type] += rule;
}
}
// If no eg 'md-accent' class is found, try to just find 'accent' in the rule and guess from
// there
for (i = 0; type = THEME_COLOR_TYPES[i]; i++) {
if (rule.indexOf(type) > -1) {
return rulesByType[type] += rule;
}
}
// Default to the primary array
return rulesByType[DEFAULT_COLOR_TYPE] += rule;
});
// If themes are being generated on-demand, quit here. The user will later manually
// call generateTheme to do this on a theme-by-theme basis.
if (generateOnDemand) return;
angular.forEach($mdTheming.THEMES, function(theme) {
if (!GENERATED[theme.name] && !($mdTheming.defaultTheme() !== 'default' && theme.name === 'default')) {
generateTheme(theme, theme.name, nonce);
}
});
// *************************
// Internal functions
// *************************
// The user specifies a 'default' contrast color as either light or dark,
// then explicitly lists which hues are the opposite contrast (eg. A100 has dark, A200 has light)
function sanitizePalette(palette, name) {
var defaultContrast = palette.contrastDefaultColor;
var lightColors = palette.contrastLightColors || [];
var strongLightColors = palette.contrastStrongLightColors || [];
var darkColors = palette.contrastDarkColors || [];
// These colors are provided as space-separated lists
if (typeof lightColors === 'string') lightColors = lightColors.split(' ');
if (typeof strongLightColors === 'string') strongLightColors = strongLightColors.split(' ');
if (typeof darkColors === 'string') darkColors = darkColors.split(' ');
// Cleanup after ourselves
delete palette.contrastDefaultColor;
delete palette.contrastLightColors;
delete palette.contrastStrongLightColors;
delete palette.contrastDarkColors;
// Change { 'A100': '#fffeee' } to { 'A100': { value: '#fffeee', contrast:DARK_CONTRAST_COLOR }
angular.forEach(palette, function(hueValue, hueName) {
if (angular.isObject(hueValue)) return; // Already converted
// Map everything to rgb colors
var rgbValue = colorToRgbaArray(hueValue);
if (!rgbValue) {
throw new Error("Color %1, in palette %2's hue %3, is invalid. Hex or rgb(a) color expected."
.replace('%1', hueValue)
.replace('%2', palette.name)
.replace('%3', hueName));
}
palette[hueName] = {
value: rgbValue,
contrast: getContrastColor()
};
function getContrastColor() {
if (defaultContrast === 'light') {
if (darkColors.indexOf(hueName) > -1) {
return DARK_CONTRAST_COLOR;
} else {
return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR
: LIGHT_CONTRAST_COLOR;
}
} else {
if (lightColors.indexOf(hueName) > -1) {
return strongLightColors.indexOf(hueName) > -1 ? STRONG_LIGHT_CONTRAST_COLOR
: LIGHT_CONTRAST_COLOR;
} else {
return DARK_CONTRAST_COLOR;
}
}
}
});
}
}
generateAllThemes.$inject = ["$injector", "$mdTheming"];
function generateTheme(theme, name, nonce) {
var head = document.head;
var firstChild = head ? head.firstElementChild : null;
if (!GENERATED[name]) {
// For each theme, use the color palettes specified for
// `primary`, `warn` and `accent` to generate CSS rules.
THEME_COLOR_TYPES.forEach(function(colorType) {
var styleStrings = parseRules(theme, colorType, rulesByType[colorType]);
while (styleStrings.length) {
var styleContent = styleStrings.shift();
if (styleContent) {
var style = document.createElement('style');
style.setAttribute('md-theme-style', '');
if (nonce) {
style.setAttribute('nonce', nonce);
}
style.appendChild(document.createTextNode(styleContent));
head.insertBefore(style, firstChild);
}
}
});
GENERATED[theme.name] = true;
}
}
function checkValidPalette(theme, colorType) {
// If theme attempts to use a palette that doesnt exist, throw error
if (!PALETTES[ (theme.colors[colorType] || {}).name ]) {
throw new Error(
"You supplied an invalid color palette for theme %1's %2 palette. Available palettes: %3"
.replace('%1', theme.name)
.replace('%2', colorType)
.replace('%3', Object.keys(PALETTES).join(', '))
);
}
}
function colorToRgbaArray(clr) {
if (angular.isArray(clr) && clr.length == 3) return clr;
if (/^rgb/.test(clr)) {
return clr.replace(/(^\s*rgba?\(|\)\s*$)/g, '').split(',').map(function(value, i) {
return i == 3 ? parseFloat(value, 10) : parseInt(value, 10);
});
}
if (clr.charAt(0) == '#') clr = clr.substring(1);
if (!/^([a-fA-F0-9]{3}){1,2}$/g.test(clr)) return;
var dig = clr.length / 3;
var red = clr.substr(0, dig);
var grn = clr.substr(dig, dig);
var blu = clr.substr(dig * 2);
if (dig === 1) {
red += red;
grn += grn;
blu += blu;
}
return [parseInt(red, 16), parseInt(grn, 16), parseInt(blu, 16)];
}
function rgba(rgbArray, opacity) {
if ( !rgbArray ) return "rgb('0,0,0')";
if (rgbArray.length == 4) {
rgbArray = angular.copy(rgbArray);
opacity ? rgbArray.pop() : opacity = rgbArray.pop();
}
return opacity && (typeof opacity == 'number' || (typeof opacity == 'string' && opacity.length)) ?
'rgba(' + rgbArray.join(',') + ',' + opacity + ')' :
'rgb(' + rgbArray.join(',') + ')';
}
})(); // material.core.theming
(function(){
"use strict";
// Polyfill angular < 1.4 (provide $animateCss)
angular
.module('material.core')
.factory('$$mdAnimate', ["$q", "$timeout", "$mdConstant", "$animateCss", function($q, $timeout, $mdConstant, $animateCss){
// Since $$mdAnimate is injected into $mdUtil... use a wrapper function
// to subsequently inject $mdUtil as an argument to the AnimateDomUtils
return function($mdUtil) {
return AnimateDomUtils( $mdUtil, $q, $timeout, $mdConstant, $animateCss);
};
}]);
/**
* Factory function that requires special injections
*/
function AnimateDomUtils($mdUtil, $q, $timeout, $mdConstant, $animateCss) {
var self;
return self = {
/**
*
*/
translate3d : function( target, from, to, options ) {
return $animateCss(target,{
from:from,
to:to,
addClass:options.transitionInClass,
removeClass:options.transitionOutClass
})
.start()
.then(function(){
// Resolve with reverser function...
return reverseTranslate;
});
/**
* Specific reversal of the request translate animation above...
*/
function reverseTranslate (newFrom) {
return $animateCss(target, {
to: newFrom || from,
addClass: options.transitionOutClass,
removeClass: options.transitionInClass
}).start();
}
},
/**
* Listen for transitionEnd event (with optional timeout)
* Announce completion or failure via promise handlers
*/
waitTransitionEnd: function (element, opts) {
var TIMEOUT = 3000; // fallback is 3 secs
return $q(function(resolve, reject){
opts = opts || { };
// If there is no transition is found, resolve immediately
//
// NOTE: using $mdUtil.nextTick() causes delays/issues
if (noTransitionFound(opts.cachedTransitionStyles)) {
TIMEOUT = 0;
}
var timer = $timeout(finished, opts.timeout || TIMEOUT);
element.on($mdConstant.CSS.TRANSITIONEND, finished);
/**
* Upon timeout or transitionEnd, reject or resolve (respectively) this promise.
* NOTE: Make sure this transitionEnd didn't bubble up from a child
*/
function finished(ev) {
if ( ev && ev.target !== element[0]) return;
if ( ev ) $timeout.cancel(timer);
element.off($mdConstant.CSS.TRANSITIONEND, finished);
// Never reject since ngAnimate may cause timeouts due missed transitionEnd events
resolve();
}
/**
* Checks whether or not there is a transition.
*
* @param styles The cached styles to use for the calculation. If null, getComputedStyle()
* will be used.
*
* @returns {boolean} True if there is no transition/duration; false otherwise.
*/
function noTransitionFound(styles) {
styles = styles || window.getComputedStyle(element[0]);
return styles.transitionDuration == '0s' || (!styles.transition && !styles.transitionProperty);
}
});
},
calculateTransformValues: function (element, originator) {
var origin = originator.element;
var bounds = originator.bounds;
if (origin || bounds) {
var originBnds = origin ? self.clientRect(origin) || currentBounds() : self.copyRect(bounds);
var dialogRect = self.copyRect(element[0].getBoundingClientRect());
var dialogCenterPt = self.centerPointFor(dialogRect);
var originCenterPt = self.centerPointFor(originBnds);
return {
centerX: originCenterPt.x - dialogCenterPt.x,
centerY: originCenterPt.y - dialogCenterPt.y,
scaleX: Math.round(100 * Math.min(0.5, originBnds.width / dialogRect.width)) / 100,
scaleY: Math.round(100 * Math.min(0.5, originBnds.height / dialogRect.height)) / 100
};
}
return {centerX: 0, centerY: 0, scaleX: 0.5, scaleY: 0.5};
/**
* This is a fallback if the origin information is no longer valid, then the
* origin bounds simply becomes the current bounds for the dialogContainer's parent
*/
function currentBounds() {
var cntr = element ? element.parent() : null;
var parent = cntr ? cntr.parent() : null;
return parent ? self.clientRect(parent) : null;
}
},
/**
* Calculate the zoom transform from dialog to origin.
*
* We use this to set the dialog position immediately;
* then the md-transition-in actually translates back to
* `translate3d(0,0,0) scale(1.0)`...
*
* NOTE: all values are rounded to the nearest integer
*/
calculateZoomToOrigin: function (element, originator) {
var zoomTemplate = "translate3d( {centerX}px, {centerY}px, 0 ) scale( {scaleX}, {scaleY} )";
var buildZoom = angular.bind(null, $mdUtil.supplant, zoomTemplate);
return buildZoom(self.calculateTransformValues(element, originator));
},
/**
* Calculate the slide transform from panel to origin.
* NOTE: all values are rounded to the nearest integer
*/
calculateSlideToOrigin: function (element, originator) {
var slideTemplate = "translate3d( {centerX}px, {centerY}px, 0 )";
var buildSlide = angular.bind(null, $mdUtil.supplant, slideTemplate);
return buildSlide(self.calculateTransformValues(element, originator));
},
/**
* Enhance raw values to represent valid css stylings...
*/
toCss : function( raw ) {
var css = { };
var lookups = 'left top right bottom width height x y min-width min-height max-width max-height';
angular.forEach(raw, function(value,key) {
if ( angular.isUndefined(value) ) return;
if ( lookups.indexOf(key) >= 0 ) {
css[key] = value + 'px';
} else {
switch (key) {
case 'transition':
convertToVendor(key, $mdConstant.CSS.TRANSITION, value);
break;
case 'transform':
convertToVendor(key, $mdConstant.CSS.TRANSFORM, value);
break;
case 'transformOrigin':
convertToVendor(key, $mdConstant.CSS.TRANSFORM_ORIGIN, value);
break;
}
}
});
return css;
function convertToVendor(key, vendor, value) {
angular.forEach(vendor.split(' '), function (key) {
css[key] = value;
});
}
},
/**
* Convert the translate CSS value to key/value pair(s).
*/
toTransformCss: function (transform, addTransition, transition) {
var css = {};
angular.forEach($mdConstant.CSS.TRANSFORM.split(' '), function (key) {
css[key] = transform;
});
if (addTransition) {
transition = transition || "all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important";
css['transition'] = transition;
}
return css;
},
/**
* Clone the Rect and calculate the height/width if needed
*/
copyRect: function (source, destination) {
if (!source) return null;
destination = destination || {};
angular.forEach('left top right bottom width height'.split(' '), function (key) {
destination[key] = Math.round(source[key])
});
destination.width = destination.width || (destination.right - destination.left);
destination.height = destination.height || (destination.bottom - destination.top);
return destination;
},
/**
* Calculate ClientRect of element; return null if hidden or zero size
*/
clientRect: function (element) {
var bounds = angular.element(element)[0].getBoundingClientRect();
var isPositiveSizeClientRect = function (rect) {
return rect && (rect.width > 0) && (rect.height > 0);
};
// If the event origin element has zero size, it has probably been hidden.
return isPositiveSizeClientRect(bounds) ? self.copyRect(bounds) : null;
},
/**
* Calculate 'rounded' center point of Rect
*/
centerPointFor: function (targetRect) {
return targetRect ? {
x: Math.round(targetRect.left + (targetRect.width / 2)),
y: Math.round(targetRect.top + (targetRect.height / 2))
} : { x : 0, y : 0 };
}
};
};
})(); // material.core
(function(){
"use strict";
"use strict";
if (angular.version.minor >= 4) {
angular.module('material.core.animate', []);
} else {
(function() {
var forEach = angular.forEach;
var WEBKIT = angular.isDefined(document.documentElement.style.WebkitAppearance);
var PREFIX = WEBKIT ? '-webkit-' : '';
var TRANSITION_EVENTS = (WEBKIT ? 'webkitTransitionEnd ' : '') + 'transitionend';
var ANIMATION_EVENTS = (WEBKIT ? 'webkitAnimationEnd ' : '') + 'animationend';
var $$ForceReflowFactory = ['$document', function($document) {
return function() {
return $document[0].body.clientWidth + 1;
}
}];
var $$rAFMutexFactory = ['$$rAF', function($$rAF) {
return function() {
var passed = false;
$$rAF(function() {
passed = true;
});
return function(fn) {
passed ? fn() : $$rAF(fn);
};
};
}];
var $$AnimateRunnerFactory = ['$q', '$$rAFMutex', function($q, $$rAFMutex) {
var INITIAL_STATE = 0;
var DONE_PENDING_STATE = 1;
var DONE_COMPLETE_STATE = 2;
function AnimateRunner(host) {
this.setHost(host);
this._doneCallbacks = [];
this._runInAnimationFrame = $$rAFMutex();
this._state = 0;
}
AnimateRunner.prototype = {
setHost: function(host) {
this.host = host || {};
},
done: function(fn) {
if (this._state === DONE_COMPLETE_STATE) {
fn();
} else {
this._doneCallbacks.push(fn);
}
},
progress: angular.noop,
getPromise: function() {
if (!this.promise) {
var self = this;
this.promise = $q(function(resolve, reject) {
self.done(function(status) {
status === false ? reject() : resolve();
});
});
}
return this.promise;
},
then: function(resolveHandler, rejectHandler) {
return this.getPromise().then(resolveHandler, rejectHandler);
},
'catch': function(handler) {
return this.getPromise()['catch'](handler);
},
'finally': function(handler) {
return this.getPromise()['finally'](handler);
},
pause: function() {
if (this.host.pause) {
this.host.pause();
}
},
resume: function() {
if (this.host.resume) {
this.host.resume();
}
},
end: function() {
if (this.host.end) {
this.host.end();
}
this._resolve(true);
},
cancel: function() {
if (this.host.cancel) {
this.host.cancel();
}
this._resolve(false);
},
complete: function(response) {
var self = this;
if (self._state === INITIAL_STATE) {
self._state = DONE_PENDING_STATE;
self._runInAnimationFrame(function() {
self._resolve(response);
});
}
},
_resolve: function(response) {
if (this._state !== DONE_COMPLETE_STATE) {
forEach(this._doneCallbacks, function(fn) {
fn(response);
});
this._doneCallbacks.length = 0;
this._state = DONE_COMPLETE_STATE;
}
}
};
return AnimateRunner;
}];
angular
.module('material.core.animate', [])
.factory('$$forceReflow', $$ForceReflowFactory)
.factory('$$AnimateRunner', $$AnimateRunnerFactory)
.factory('$$rAFMutex', $$rAFMutexFactory)
.factory('$animateCss', ['$window', '$$rAF', '$$AnimateRunner', '$$forceReflow', '$$jqLite', '$timeout', '$animate',
function($window, $$rAF, $$AnimateRunner, $$forceReflow, $$jqLite, $timeout, $animate) {
function init(element, options) {
var temporaryStyles = [];
var node = getDomNode(element);
var areAnimationsAllowed = node && $animate.enabled();
var hasCompleteStyles = false;
var hasCompleteClasses = false;
if (areAnimationsAllowed) {
if (options.transitionStyle) {
temporaryStyles.push([PREFIX + 'transition', options.transitionStyle]);
}
if (options.keyframeStyle) {
temporaryStyles.push([PREFIX + 'animation', options.keyframeStyle]);
}
if (options.delay) {
temporaryStyles.push([PREFIX + 'transition-delay', options.delay + 's']);
}
if (options.duration) {
temporaryStyles.push([PREFIX + 'transition-duration', options.duration + 's']);
}
hasCompleteStyles = options.keyframeStyle ||
(options.to && (options.duration > 0 || options.transitionStyle));
hasCompleteClasses = !!options.addClass || !!options.removeClass;
blockTransition(element, true);
}
var hasCompleteAnimation = areAnimationsAllowed && (hasCompleteStyles || hasCompleteClasses);
applyAnimationFromStyles(element, options);
var animationClosed = false;
var events, eventFn;
return {
close: $window.close,
start: function() {
var runner = new $$AnimateRunner();
waitUntilQuiet(function() {
blockTransition(element, false);
if (!hasCompleteAnimation) {
return close();
}
forEach(temporaryStyles, function(entry) {
var key = entry[0];
var value = entry[1];
node.style[camelCase(key)] = value;
});
applyClasses(element, options);
var timings = computeTimings(element);
if (timings.duration === 0) {
return close();
}
var moreStyles = [];
if (options.easing) {
if (timings.transitionDuration) {
moreStyles.push([PREFIX + 'transition-timing-function', options.easing]);
}
if (timings.animationDuration) {
moreStyles.push([PREFIX + 'animation-timing-function', options.easing]);
}
}
if (options.delay && timings.animationDelay) {
moreStyles.push([PREFIX + 'animation-delay', options.delay + 's']);
}
if (options.duration && timings.animationDuration) {
moreStyles.push([PREFIX + 'animation-duration', options.duration + 's']);
}
forEach(moreStyles, function(entry) {
var key = entry[0];
var value = entry[1];
node.style[camelCase(key)] = value;
temporaryStyles.push(entry);
});
var maxDelay = timings.delay;
var maxDelayTime = maxDelay * 1000;
var maxDuration = timings.duration;
var maxDurationTime = maxDuration * 1000;
var startTime = Date.now();
events = [];
if (timings.transitionDuration) {
events.push(TRANSITION_EVENTS);
}
if (timings.animationDuration) {
events.push(ANIMATION_EVENTS);
}
events = events.join(' ');
eventFn = function(event) {
event.stopPropagation();
var ev = event.originalEvent || event;
var timeStamp = ev.timeStamp || Date.now();
var elapsedTime = parseFloat(ev.elapsedTime.toFixed(3));
if (Math.max(timeStamp - startTime, 0) >= maxDelayTime && elapsedTime >= maxDuration) {
close();
}
};
element.on(events, eventFn);
applyAnimationToStyles(element, options);
$timeout(close, maxDelayTime + maxDurationTime * 1.5, false);
});
return runner;
function close() {
if (animationClosed) return;
animationClosed = true;
if (events && eventFn) {
element.off(events, eventFn);
}
applyClasses(element, options);
applyAnimationStyles(element, options);
forEach(temporaryStyles, function(entry) {
node.style[camelCase(entry[0])] = '';
});
runner.complete(true);
return runner;
}
}
}
}
function applyClasses(element, options) {
if (options.addClass) {
$$jqLite.addClass(element, options.addClass);
options.addClass = null;
}
if (options.removeClass) {
$$jqLite.removeClass(element, options.removeClass);
options.removeClass = null;
}
}
function computeTimings(element) {
var node = getDomNode(element);
var cs = $window.getComputedStyle(node)
var tdr = parseMaxTime(cs[prop('transitionDuration')]);
var adr = parseMaxTime(cs[prop('animationDuration')]);
var tdy = parseMaxTime(cs[prop('transitionDelay')]);
var ady = parseMaxTime(cs[prop('animationDelay')]);
adr *= (parseInt(cs[prop('animationIterationCount')], 10) || 1);
var duration = Math.max(adr, tdr);
var delay = Math.max(ady, tdy);
return {
duration: duration,
delay: delay,
animationDuration: adr,
transitionDuration: tdr,
animationDelay: ady,
transitionDelay: tdy
};
function prop(key) {
return WEBKIT ? 'Webkit' + key.charAt(0).toUpperCase() + key.substr(1)
: key;
}
}
function parseMaxTime(str) {
var maxValue = 0;
var values = (str || "").split(/\s*,\s*/);
forEach(values, function(value) {
// it's always safe to consider only second values and omit `ms` values since
// getComputedStyle will always handle the conversion for us
if (value.charAt(value.length - 1) == 's') {
value = value.substring(0, value.length - 1);
}
value = parseFloat(value) || 0;
maxValue = maxValue ? Math.max(value, maxValue) : value;
});
return maxValue;
}
var cancelLastRAFRequest;
var rafWaitQueue = [];
function waitUntilQuiet(callback) {
if (cancelLastRAFRequest) {
cancelLastRAFRequest(); //cancels the request
}
rafWaitQueue.push(callback);
cancelLastRAFRequest = $$rAF(function() {
cancelLastRAFRequest = null;
// DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable.
// PLEASE EXAMINE THE `$$forceReflow` service to understand why.
var pageWidth = $$forceReflow();
// we use a for loop to ensure that if the queue is changed
// during this looping then it will consider new requests
for (var i = 0; i < rafWaitQueue.length; i++) {
rafWaitQueue[i](pageWidth);
}
rafWaitQueue.length = 0;
});
}
function applyAnimationStyles(element, options) {
applyAnimationFromStyles(element, options);
applyAnimationToStyles(element, options);
}
function applyAnimationFromStyles(element, options) {
if (options.from) {
element.css(options.from);
options.from = null;
}
}
function applyAnimationToStyles(element, options) {
if (options.to) {
element.css(options.to);
options.to = null;
}
}
function getDomNode(element) {
for (var i = 0; i < element.length; i++) {
if (element[i].nodeType === 1) return element[i];
}
}
function blockTransition(element, bool) {
var node = getDomNode(element);
var key = camelCase(PREFIX + 'transition-delay');
node.style[key] = bool ? '-9999s' : '';
}
return init;
}]);
/**
* Older browsers [FF31] expect camelCase
* property keys.
* e.g.
* animation-duration --> animationDuration
*/
function camelCase(str) {
return str.replace(/-[a-z]/g, function(str) {
return str.charAt(1).toUpperCase();
});
}
})();
}
})(); // material.core.animate
(function(){
"use strict";
/**
* @ngdoc module
* @name material.components.autocomplete
*/
/*
* @see js folder for autocomplete implementation
*/
angular.module('material.components.autocomplete', [
'material.core',
'material.components.virtualRepeat'
]);
})(); // Autocomplete: autocomplete dependencies
(function(){
"use strict";
/**
* @ngdoc module
* @name material.components.chips
*/
/*
* @see js folder for chips implementation
*/
angular.module('material.components.chips', [
'material.core',
'material.components.autocomplete'
]);
})(); // chips: Module dependencies
(function(){
"use strict";
/**
* @ngdoc module
* @name material.components.showHide
*/
// Add additional handlers to ng-show and ng-hide that notify directives
// contained within that they should recompute their size.
// These run in addition to Angular's built-in ng-hide and ng-show directives.
angular.module('material.components.showHide', [
'material.core'
])
.directive('ngShow', createDirective('ngShow', true))
.directive('ngHide', createDirective('ngHide', false));
function createDirective(name, targetValue) {
return ['$mdUtil', function($mdUtil) {
return {
restrict: 'A',
multiElement: true,
link: function($scope, $element, $attr) {
var unregister = $scope.$on('$md-resize-enable', function() {
unregister();
var cachedTransitionStyles = window.getComputedStyle($element[0]);
$scope.$watch($attr[name], function(value) {
if (!!value === targetValue) {
$mdUtil.nextTick(function() {
$scope.$broadcast('$md-resize');
});
var opts = {
cachedTransitionStyles: cachedTransitionStyles
};
$mdUtil.dom.animator.waitTransitionEnd($element, opts).then(function() {
$scope.$broadcast('$md-resize');
});
}
});
});
}
};
}];
}
})(); // showHide: .directive('ngShow', ...), .directive('ngHide', ...);
(function(){
"use strict";
/**
* @ngdoc module
* @name material.components.virtualRepeat
*/
angular.module('material.components.virtualRepeat', [
'material.core',
'material.components.showHide'
])
.directive('mdVirtualRepeatContainer', VirtualRepeatContainerDirective)
.directive('mdVirtualRepeat', VirtualRepeatDirective);
/**
* @ngdoc directive
* @name mdVirtualRepeatContainer
* @module material.components.virtualRepeat
* @restrict E
* @description
* `md-virtual-repeat-container` provides the scroll container for md-virtual-repeat.
*
* Virtual repeat is a limited substitute for ng-repeat that renders only
* enough dom nodes to fill the container and recycling them as the user scrolls.
*
* @usage
* <hljs lang="html">
*
* <md-virtual-repeat-container md-top-index="topIndex">
* <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div>
* </md-virtual-repeat-container>
* </hljs>
*
* @param {number=} md-top-index Binds the index of the item that is at the top of the scroll
* container to $scope. It can both read and set the scroll position.
* @param {boolean=} md-orient-horizontal Whether the container should scroll horizontally
* (defaults to orientation and scrolling vertically).
* @param {boolean=} md-auto-shrink When present, the container will shrink to fit
* the number of items when that number is less than its original size.
* @param {number=} md-auto-shrink-min Minimum number of items that md-auto-shrink
* will shrink to (default: 0).
*/
function VirtualRepeatContainerDirective() {
return {
controller: VirtualRepeatContainerController,
template: virtualRepeatContainerTemplate,
compile: function virtualRepeatContainerCompile($element, $attrs) {
$element
.addClass('md-virtual-repeat-container')
.addClass($attrs.hasOwnProperty('mdOrientHorizontal')
? 'md-orient-horizontal'
: 'md-orient-vertical');
}
};
}
function virtualRepeatContainerTemplate($element) {
return '<div class="md-virtual-repeat-scroller">' +
'<div class="md-virtual-repeat-sizer"></div>' +
'<div class="md-virtual-repeat-offsetter">' +
$element[0].innerHTML +
'</div></div>';
}
/**
* Maximum size, in pixels, that can be explicitly set to an element. The actual value varies
* between browsers, but IE11 has the very lowest size at a mere 1,533,917px. Ideally we could
* *compute* this value, but Firefox always reports an element to have a size of zero if it
* goes over the max, meaning that we'd have to binary search for the value.
* @const {number}
*/
var MAX_ELEMENT_SIZE = 1533917;
/**
* Number of additional elements to render above and below the visible area inside
* of the virtual repeat container. A higher number results in less flicker when scrolling
* very quickly in Safari, but comes with a higher rendering and dirty-checking cost.
* @const {number}
*/
var NUM_EXTRA = 3;
function VirtualRepeatContainerController(
$$rAF, $mdUtil, $parse, $rootScope, $window, $scope, $element, $attrs) {
this.$rootScope = $rootScope;
this.$scope = $scope;
this.$element = $element;
this.$attrs = $attrs;
/** @type {number} The width or height of the container */
this.size = 0;
/** @type {number} The scroll width or height of the scroller */
this.scrollSize = 0;
/** @type {number} The scrollLeft or scrollTop of the scroller */
this.scrollOffset = 0;
/** @type {boolean} Whether the scroller is oriented horizontally */
this.horizontal = this.$attrs.hasOwnProperty('mdOrientHorizontal');
/** @type {!VirtualRepeatController} The repeater inside of this container */
this.repeater = null;
/** @type {boolean} Whether auto-shrink is enabled */
this.autoShrink = this.$attrs.hasOwnProperty('mdAutoShrink');
/** @type {number} Minimum number of items to auto-shrink to */
this.autoShrinkMin = parseInt(this.$attrs.mdAutoShrinkMin, 10) || 0;
/** @type {?number} Original container size when shrank */
this.originalSize = null;
/** @type {number} Amount to offset the total scroll size by. */
this.offsetSize = parseInt(this.$attrs.mdOffsetSize, 10) || 0;
/** @type {?string} height or width element style on the container prior to auto-shrinking. */
this.oldElementSize = null;
if (this.$attrs.mdTopIndex) {
/** @type {function(angular.Scope): number} Binds to topIndex on Angular scope */
this.bindTopIndex = $parse(this.$attrs.mdTopIndex);
/** @type {number} The index of the item that is at the top of the scroll container */
this.topIndex = this.bindTopIndex(this.$scope);
if (!angular.isDefined(this.topIndex)) {
this.topIndex = 0;
this.bindTopIndex.assign(this.$scope, 0);
}
this.$scope.$watch(this.bindTopIndex, angular.bind(this, function(newIndex) {
if (newIndex !== this.topIndex) {
this.scrollToIndex(newIndex);
}
}));
} else {
this.topIndex = 0;
}
this.scroller = $element[0].getElementsByClassName('md-virtual-repeat-scroller')[0];
this.sizer = this.scroller.getElementsByClassName('md-virtual-repeat-sizer')[0];
this.offsetter = this.scroller.getElementsByClassName('md-virtual-repeat-offsetter')[0];
// After the dom stablizes, measure the initial size of the container and
// make a best effort at re-measuring as it changes.
var boundUpdateSize = angular.bind(this, this.updateSize);
$$rAF(angular.bind(this, function() {
boundUpdateSize();
var debouncedUpdateSize = $mdUtil.debounce(boundUpdateSize, 10, null, false);
var jWindow = angular.element($window);
// Make one more attempt to get the size if it is 0.
// This is not by any means a perfect approach, but there's really no
// silver bullet here.
if (!this.size) {
debouncedUpdateSize();
}
jWindow.on('resize', debouncedUpdateSize);
$scope.$on('$destroy', function() {
jWindow.off('resize', debouncedUpdateSize);
});
$scope.$emit('$md-resize-enable');
$scope.$on('$md-resize', boundUpdateSize);
}));
}
VirtualRepeatContainerController.$inject = ["$$rAF", "$mdUtil", "$parse", "$rootScope", "$window", "$scope", "$element", "$attrs"];
/** Called by the md-virtual-repeat inside of the container at startup. */
VirtualRepeatContainerController.prototype.register = function(repeaterCtrl) {
this.repeater = repeaterCtrl;
angular.element(this.scroller)
.on('scroll wheel touchmove touchend', angular.bind(this, this.handleScroll_));
};
/** @return {boolean} Whether the container is configured for horizontal scrolling. */
VirtualRepeatContainerController.prototype.isHorizontal = function() {
return this.horizontal;
};
/** @return {number} The size (width or height) of the container. */
VirtualRepeatContainerController.prototype.getSize = function() {
return this.size;
};
/**
* Resizes the container.
* @private
* @param {number} size The new size to set.
*/
VirtualRepeatContainerController.prototype.setSize_ = function(size) {
var dimension = this.getDimensionName_();
this.size = size;
this.$element[0].style[dimension] = size + 'px';
};
VirtualRepeatContainerController.prototype.unsetSize_ = function() {
this.$element[0].style[this.getDimensionName_()] = this.oldElementSize;
this.oldElementSize = null;
};
/** Instructs the container to re-measure its size. */
VirtualRepeatContainerController.prototype.updateSize = function() {
// If the original size is already determined, we can skip the update.
if (this.originalSize) return;
this.size = this.isHorizontal()
? this.$element[0].clientWidth
: this.$element[0].clientHeight;
// Recheck the scroll position after updating the size. This resolves
// problems that can result if the scroll position was measured while the
// element was display: none or detached from the document.
this.handleScroll_();
this.repeater && this.repeater.containerUpdated();
};
/** @return {number} The container's scrollHeight or scrollWidth. */
VirtualRepeatContainerController.prototype.getScrollSize = function() {
return this.scrollSize;
};
VirtualRepeatContainerController.prototype.getDimensionName_ = function() {
return this.isHorizontal() ? 'width' : 'height';
};
/**
* Sets the scroller element to the specified size.
* @private
* @param {number} size The new size.
*/
VirtualRepeatContainerController.prototype.sizeScroller_ = function(size) {
var dimension = this.getDimensionName_();
var crossDimension = this.isHorizontal() ? 'height' : 'width';
// Clear any existing dimensions.
this.sizer.innerHTML = '';
// If the size falls within the browser's maximum explicit size for a single element, we can
// set the size and be done. Otherwise, we have to create children that add up the the desired
// size.
if (size < MAX_ELEMENT_SIZE) {
this.sizer.style[dimension] = size + 'px';
} else {
this.sizer.style[dimension] = 'auto';
this.sizer.style[crossDimension] = 'auto';
// Divide the total size we have to render into N max-size pieces.
var numChildren = Math.floor(size / MAX_ELEMENT_SIZE);
// Element template to clone for each max-size piece.
var sizerChild = document.createElement('div');
sizerChild.style[dimension] = MAX_ELEMENT_SIZE + 'px';
sizerChild.style[crossDimension] = '1px';
for (var i = 0; i < numChildren; i++) {
this.sizer.appendChild(sizerChild.cloneNode(false));
}
// Re-use the element template for the remainder.
sizerChild.style[dimension] = (size - (numChildren * MAX_ELEMENT_SIZE)) + 'px';
this.sizer.appendChild(sizerChild);
}
};
/**
* If auto-shrinking is enabled, shrinks or unshrinks as appropriate.
* @private
* @param {number} size The new size.
*/
VirtualRepeatContainerController.prototype.autoShrink_ = function(size) {
var shrinkSize = Math.max(size, this.autoShrinkMin * this.repeater.getItemSize());
if (this.autoShrink && shrinkSize !== this.size) {
if (this.oldElementSize === null) {
this.oldElementSize = this.$element[0].style[this.getDimensionName_()];
}
var currentSize = this.originalSize || this.size;
if (!currentSize || shrinkSize < currentSize) {
if (!this.originalSize) {
this.originalSize = this.size;
}
// Now we update the containers size, because shrinking is enabled.
this.setSize_(shrinkSize);
} else if (this.originalSize !== null) {
// Set the size back to our initial size.
this.unsetSize_();
var _originalSize = this.originalSize;
this.originalSize = null;
// We determine the repeaters size again, if the original size was zero.
// The originalSize needs to be null, to be able to determine the size.
if (!_originalSize) this.updateSize();
// Apply the original size or the determined size back to the container, because
// it has been overwritten before, in the shrink block.
this.setSize_(_originalSize || this.size);
}
this.repeater.containerUpdated();
}
};
/**
* Sets the scrollHeight or scrollWidth. Called by the repeater based on
* its item count and item size.
* @param {number} itemsSize The total size of the items.
*/
VirtualRepeatContainerController.prototype.setScrollSize = function(itemsSize) {
var size = itemsSize + this.offsetSize;
if (this.scrollSize === size) return;
this.sizeScroller_(size);
this.autoShrink_(size);
this.scrollSize = size;
};
/** @return {number} The container's current scroll offset. */
VirtualRepeatContainerController.prototype.getScrollOffset = function() {
return this.scrollOffset;
};
/**
* Scrolls to a given scrollTop position.
* @param {number} position
*/
VirtualRepeatContainerController.prototype.scrollTo = function(position) {
this.scroller[this.isHorizontal() ? 'scrollLeft' : 'scrollTop'] = position;
this.handleScroll_();
};
/**
* Scrolls the item with the given index to the top of the scroll container.
* @param {number} index
*/
VirtualRepeatContainerController.prototype.scrollToIndex = function(index) {
var itemSize = this.repeater.getItemSize();
var itemsLength = this.repeater.itemsLength;
if(index > itemsLength) {
index = itemsLength - 1;
}
this.scrollTo(itemSize * index);
};
VirtualRepeatContainerController.prototype.resetScroll = function() {
this.scrollTo(0);
};
VirtualRepeatContainerController.prototype.handleScroll_ = function() {
var doc = angular.element(document)[0];
var ltr = doc.dir != 'rtl' && doc.body.dir != 'rtl';
if(!ltr && !this.maxSize) {
this.scroller.scrollLeft = this.scrollSize;
this.maxSize = this.scroller.scrollLeft;
}
var offset = this.isHorizontal() ?
(ltr?this.scroller.scrollLeft : this.maxSize - this.scroller.scrollLeft)
: this.scroller.scrollTop;
if (offset === this.scrollOffset || offset > this.scrollSize - this.size) return;
var itemSize = this.repeater.getItemSize();
if (!itemSize) return;
var numItems = Math.max(0, Math.floor(offset / itemSize) - NUM_EXTRA);
var transform = (this.isHorizontal() ? 'translateX(' : 'translateY(') +
(!this.isHorizontal() || ltr ? (numItems * itemSize) : - (numItems * itemSize)) + 'px)';
this.scrollOffset = offset;
this.offsetter.style.webkitTransform = transform;
this.offsetter.style.transform = transform;
if (this.bindTopIndex) {
var topIndex = Math.floor(offset / itemSize);
if (topIndex !== this.topIndex && topIndex < this.repeater.getItemCount()) {
this.topIndex = topIndex;
this.bindTopIndex.assign(this.$scope, topIndex);
if (!this.$rootScope.$$phase) this.$scope.$digest();
}
}
this.repeater.containerUpdated();
};
/**
* @ngdoc directive
* @name mdVirtualRepeat
* @module material.components.virtualRepeat
* @restrict A
* @priority 1000
* @description
* `md-virtual-repeat` specifies an element to repeat using virtual scrolling.
*
* Virtual repeat is a limited substitute for ng-repeat that renders only
* enough dom nodes to fill the container and recycling them as the user scrolls.
* Arrays, but not objects are supported for iteration.
* Track by, as alias, and (key, value) syntax are not supported.
*
* @usage
* <hljs lang="html">
* <md-virtual-repeat-container>
* <div md-virtual-repeat="i in items">Hello {{i}}!</div>
* </md-virtual-repeat-container>
*
* <md-virtual-repeat-container md-orient-horizontal>
* <div md-virtual-repeat="i in items" md-item-size="20">Hello {{i}}!</div>
* </md-virtual-repeat-container>
* </hljs>
*
* @param {number=} md-item-size The height or width of the repeated elements (which must be
* identical for each element). Optional. Will attempt to read the size from the dom if missing,
* but still assumes that all repeated nodes have same height or width.
* @param {string=} md-extra-name Evaluates to an additional name to which the current iterated item
* can be assigned on the repeated scope (needed for use in `md-autocomplete`).
* @param {boolean=} md-on-demand When present, treats the md-virtual-repeat argument as an object
* that can fetch rows rather than an array.
*
* **NOTE:** This object must implement the following interface with two (2) methods:
*
* - `getItemAtIndex: function(index) [object]` The item at that index or null if it is not yet
* loaded (it should start downloading the item in that case).
* - `getLength: function() [number]` The data length to which the repeater container
* should be sized. Ideally, when the count is known, this method should return it.
* Otherwise, return a higher number than the currently loaded items to produce an
* infinite-scroll behavior.
*/
function VirtualRepeatDirective($parse) {
return {
controller: VirtualRepeatController,
priority: 1000,
require: ['mdVirtualRepeat', '^^mdVirtualRepeatContainer'],
restrict: 'A',
terminal: true,
transclude: 'element',
compile: function VirtualRepeatCompile($element, $attrs) {
var expression = $attrs.mdVirtualRepeat;
var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)\s*$/);
var repeatName = match[1];
var repeatListExpression = $parse(match[2]);
var extraName = $attrs.mdExtraName && $parse($attrs.mdExtraName);
return function VirtualRepeatLink($scope, $element, $attrs, ctrl, $transclude) {
ctrl[0].link_(ctrl[1], $transclude, repeatName, repeatListExpression, extraName);
};
}
};
}
VirtualRepeatDirective.$inject = ["$parse"];
function VirtualRepeatController($scope, $element, $attrs, $browser, $document, $rootScope,
$$rAF, $mdUtil) {
this.$scope = $scope;
this.$element = $element;
this.$attrs = $attrs;
this.$browser = $browser;
this.$document = $document;
this.$rootScope = $rootScope;
this.$$rAF = $$rAF;
/** @type {boolean} Whether we are in on-demand mode. */
this.onDemand = $mdUtil.parseAttributeBoolean($attrs.mdOnDemand);
/** @type {!Function} Backup reference to $browser.$$checkUrlChange */
this.browserCheckUrlChange = $browser.$$checkUrlChange;
/** @type {number} Most recent starting repeat index (based on scroll offset) */
this.newStartIndex = 0;
/** @type {number} Most recent ending repeat index (based on scroll offset) */
this.newEndIndex = 0;
/** @type {number} Most recent end visible index (based on scroll offset) */
this.newVisibleEnd = 0;
/** @type {number} Previous starting repeat index (based on scroll offset) */
this.startIndex = 0;
/** @type {number} Previous ending repeat index (based on scroll offset) */
this.endIndex = 0;
// TODO: measure width/height of first element from dom if not provided.
// getComputedStyle?
/** @type {?number} Height/width of repeated elements. */
this.itemSize = $scope.$eval($attrs.mdItemSize) || null;
/** @type {boolean} Whether this is the first time that items are rendered. */
this.isFirstRender = true;
/**
* @private {boolean} Whether the items in the list are already being updated. Used to prevent
* nested calls to virtualRepeatUpdate_.
*/
this.isVirtualRepeatUpdating_ = false;
/** @type {number} Most recently seen length of items. */
this.itemsLength = 0;
/**
* @type {!Function} Unwatch callback for item size (when md-items-size is
* not specified), or angular.noop otherwise.
*/
this.unwatchItemSize_ = angular.noop;
/**
* Presently rendered blocks by repeat index.
* @type {Object<number, !VirtualRepeatController.Block}
*/
this.blocks = {};
/** @type {Array<!VirtualRepeatController.Block>} A pool of presently unused blocks. */
this.pooledBlocks = [];
$scope.$on('$destroy', angular.bind(this, this.cleanupBlocks_));
}
VirtualRepeatController.$inject = ["$scope", "$element", "$attrs", "$browser", "$document", "$rootScope", "$$rAF", "$mdUtil"];
/**
* An object representing a repeated item.
* @typedef {{element: !jqLite, new: boolean, scope: !angular.Scope}}
*/
VirtualRepeatController.Block;
/**
* Called at startup by the md-virtual-repeat postLink function.
* @param {!VirtualRepeatContainerController} container The container's controller.
* @param {!Function} transclude The repeated element's bound transclude function.
* @param {string} repeatName The left hand side of the repeat expression, indicating
* the name for each item in the array.
* @param {!Function} repeatListExpression A compiled expression based on the right hand side
* of the repeat expression. Points to the array to repeat over.
* @param {string|undefined} extraName The optional extra repeatName.
*/
VirtualRepeatController.prototype.link_ =
function(container, transclude, repeatName, repeatListExpression, extraName) {
this.container = container;
this.transclude = transclude;
this.repeatName = repeatName;
this.rawRepeatListExpression = repeatListExpression;
this.extraName = extraName;
this.sized = false;
this.repeatListExpression = angular.bind(this, this.repeatListExpression_);
this.container.register(this);
};
/** @private Cleans up unused blocks. */
VirtualRepeatController.prototype.cleanupBlocks_ = function() {
angular.forEach(this.pooledBlocks, function cleanupBlock(block) {
block.element.remove();
});
};
/** @private Attempts to set itemSize by measuring a repeated element in the dom */
VirtualRepeatController.prototype.readItemSize_ = function() {
if (this.itemSize) {
// itemSize was successfully read in a different asynchronous call.
return;
}
this.items = this.repeatListExpression(this.$scope);
this.parentNode = this.$element[0].parentNode;
var block = this.getBlock_(0);
if (!block.element[0].parentNode) {
this.parentNode.appendChild(block.element[0]);
}
this.itemSize = block.element[0][
this.container.isHorizontal() ? 'offsetWidth' : 'offsetHeight'] || null;
this.blocks[0] = block;
this.poolBlock_(0);
if (this.itemSize) {
this.containerUpdated();
}
};
/**
* Returns the user-specified repeat list, transforming it into an array-like
* object in the case of infinite scroll/dynamic load mode.
* @param {!angular.Scope} The scope.
* @return {!Array|!Object} An array or array-like object for iteration.
*/
VirtualRepeatController.prototype.repeatListExpression_ = function(scope) {
var repeatList = this.rawRepeatListExpression(scope);
if (this.onDemand && repeatList) {
var virtualList = new VirtualRepeatModelArrayLike(repeatList);
virtualList.$$includeIndexes(this.newStartIndex, this.newVisibleEnd);
return virtualList;
} else {
return repeatList;
}
};
/**
* Called by the container. Informs us that the containers scroll or size has
* changed.
*/
VirtualRepeatController.prototype.containerUpdated = function() {
// If itemSize is unknown, attempt to measure it.
if (!this.itemSize) {
// Make sure to clean up watchers if we can (see #8178)
if(this.unwatchItemSize_ && this.unwatchItemSize_ !== angular.noop){
this.unwatchItemSize_();
}
this.unwatchItemSize_ = this.$scope.$watchCollection(
this.repeatListExpression,
angular.bind(this, function(items) {
if (items && items.length) {
this.$$rAF(angular.bind(this, this.readItemSize_));
}
}));
if (!this.$rootScope.$$phase) this.$scope.$digest();
return;
} else if (!this.sized) {
this.items = this.repeatListExpression(this.$scope);
}
if (!this.sized) {
this.unwatchItemSize_();
this.sized = true;
this.$scope.$watchCollection(this.repeatListExpression,
angular.bind(this, function(items, oldItems) {
if (!this.isVirtualRepeatUpdating_) {
this.virtualRepeatUpdate_(items, oldItems);
}
}));
}
this.updateIndexes_();
if (this.newStartIndex !== this.startIndex ||
this.newEndIndex !== this.endIndex ||
this.container.getScrollOffset() > this.container.getScrollSize()) {
if (this.items instanceof VirtualRepeatModelArrayLike) {
this.items.$$includeIndexes(this.newStartIndex, this.newEndIndex);
}
this.virtualRepeatUpdate_(this.items, this.items);
}
};
/**
* Called by the container. Returns the size of a single repeated item.
* @return {?number} Size of a repeated item.
*/
VirtualRepeatController.prototype.getItemSize = function() {
return this.itemSize;
};
/**
* Called by the container. Returns the size of a single repeated item.
* @return {?number} Size of a repeated item.
*/
VirtualRepeatController.prototype.getItemCount = function() {
return this.itemsLength;
};
/**
* Updates the order and visible offset of repeated blocks in response to scrolling
* or items updates.
* @private
*/
VirtualRepeatController.prototype.virtualRepeatUpdate_ = function(items, oldItems) {
this.isVirtualRepeatUpdating_ = true;
var itemsLength = items && items.length || 0;
var lengthChanged = false;
// If the number of items shrank, scroll up to the top.
if (this.items && itemsLength < this.items.length && this.container.getScrollOffset() !== 0) {
this.items = items;
this.container.resetScroll();
return;
}
if (itemsLength !== this.itemsLength) {
lengthChanged = true;
this.itemsLength = itemsLength;
}
this.items = items;
if (items !== oldItems || lengthChanged) {
this.updateIndexes_();
}
this.parentNode = this.$element[0].parentNode;
if (lengthChanged) {
this.container.setScrollSize(itemsLength * this.itemSize);
}
if (this.isFirstRender) {
this.isFirstRender = false;
var startIndex = this.$attrs.mdStartIndex ?
this.$scope.$eval(this.$attrs.mdStartIndex) :
this.container.topIndex;
this.container.scrollToIndex(startIndex);
}
// Detach and pool any blocks that are no longer in the viewport.
Object.keys(this.blocks).forEach(function(blockIndex) {
var index = parseInt(blockIndex, 10);
if (index < this.newStartIndex || index >= this.newEndIndex) {
this.poolBlock_(index);
}
}, this);
// Add needed blocks.
// For performance reasons, temporarily block browser url checks as we digest
// the restored block scopes ($$checkUrlChange reads window.location to
// check for changes and trigger route change, etc, which we don't need when
// trying to scroll at 60fps).
this.$browser.$$checkUrlChange = angular.noop;
var i, block,
newStartBlocks = [],
newEndBlocks = [];
// Collect blocks at the top.
for (i = this.newStartIndex; i < this.newEndIndex && this.blocks[i] == null; i++) {
block = this.getBlock_(i);
this.updateBlock_(block, i);
newStartBlocks.push(block);
}
// Update blocks that are already rendered.
for (; this.blocks[i] != null; i++) {
this.updateBlock_(this.blocks[i], i);
}
var maxIndex = i - 1;
// Collect blocks at the end.
for (; i < this.newEndIndex; i++) {
block = this.getBlock_(i);
this.updateBlock_(block, i);
newEndBlocks.push(block);
}
// Attach collected blocks to the document.
if (newStartBlocks.length) {
this.parentNode.insertBefore(
this.domFragmentFromBlocks_(newStartBlocks),
this.$element[0].nextSibling);
}
if (newEndBlocks.length) {
this.parentNode.insertBefore(
this.domFragmentFromBlocks_(newEndBlocks),
this.blocks[maxIndex] && this.blocks[maxIndex].element[0].nextSibling);
}
// Restore $$checkUrlChange.
this.$browser.$$checkUrlChange = this.browserCheckUrlChange;
this.startIndex = this.newStartIndex;
this.endIndex = this.newEndIndex;
this.isVirtualRepeatUpdating_ = false;
};
/**
* @param {number} index Where the block is to be in the repeated list.
* @return {!VirtualRepeatController.Block} A new or pooled block to place at the specified index.
* @private
*/
VirtualRepeatController.prototype.getBlock_ = function(index) {
if (this.pooledBlocks.length) {
return this.pooledBlocks.pop();
}
var block;
this.transclude(angular.bind(this, function(clone, scope) {
block = {
element: clone,
new: true,
scope: scope
};
this.updateScope_(scope, index);
this.parentNode.appendChild(clone[0]);
}));
return block;
};
/**
* Updates and if not in a digest cycle, digests the specified block's scope to the data
* at the specified index.
* @param {!VirtualRepeatController.Block} block The block whose scope should be updated.
* @param {number} index The index to set.
* @private
*/
VirtualRepeatController.prototype.updateBlock_ = function(block, index) {
this.blocks[index] = block;
if (!block.new &&
(block.scope.$index === index && block.scope[this.repeatName] === this.items[index])) {
return;
}
block.new = false;
// Update and digest the block's scope.
this.updateScope_(block.scope, index);
// Perform digest before reattaching the block.
// Any resulting synchronous dom mutations should be much faster as a result.
// This might break some directives, but I'm going to try it for now.
if (!this.$rootScope.$$phase) {
block.scope.$digest();
}
};
/**
* Updates scope to the data at the specified index.
* @param {!angular.Scope} scope The scope which should be updated.
* @param {number} index The index to set.
* @private
*/
VirtualRepeatController.prototype.updateScope_ = function(scope, index) {
scope.$index = index;
scope[this.repeatName] = this.items && this.items[index];
if (this.extraName) scope[this.extraName(this.$scope)] = this.items[index];
};
/**
* Pools the block at the specified index (Pulls its element out of the dom and stores it).
* @param {number} index The index at which the block to pool is stored.
* @private
*/
VirtualRepeatController.prototype.poolBlock_ = function(index) {
this.pooledBlocks.push(this.blocks[index]);
this.parentNode.removeChild(this.blocks[index].element[0]);
delete this.blocks[index];
};
/**
* Produces a dom fragment containing the elements from the list of blocks.
* @param {!Array<!VirtualRepeatController.Block>} blocks The blocks whose elements
* should be added to the document fragment.
* @return {DocumentFragment}
* @private
*/
VirtualRepeatController.prototype.domFragmentFromBlocks_ = function(blocks) {
var fragment = this.$document[0].createDocumentFragment();
blocks.forEach(function(block) {
fragment.appendChild(block.element[0]);
});
return fragment;
};
/**
* Updates start and end indexes based on length of repeated items and container size.
* @private
*/
VirtualRepeatController.prototype.updateIndexes_ = function() {
var itemsLength = this.items ? this.items.length : 0;
var containerLength = Math.ceil(this.container.getSize() / this.itemSize);
this.newStartIndex = Math.max(0, Math.min(
itemsLength - containerLength,
Math.floor(this.container.getScrollOffset() / this.itemSize)));
this.newVisibleEnd = this.newStartIndex + containerLength + NUM_EXTRA;
this.newEndIndex = Math.min(itemsLength, this.newVisibleEnd);
this.newStartIndex = Math.max(0, this.newStartIndex - NUM_EXTRA);
};
/**
* This VirtualRepeatModelArrayLike class enforces the interface requirements
* for infinite scrolling within a mdVirtualRepeatContainer. An object with this
* interface must implement the following interface with two (2) methods:
*
* getItemAtIndex: function(index) -> item at that index or null if it is not yet
* loaded (It should start downloading the item in that case).
*
* getLength: function() -> number The data legnth to which the repeater container
* should be sized. Ideally, when the count is known, this method should return it.
* Otherwise, return a higher number than the currently loaded items to produce an
* infinite-scroll behavior.
*
* @usage
* <hljs lang="html">
* <md-virtual-repeat-container md-orient-horizontal>
* <div md-virtual-repeat="i in items" md-on-demand>
* Hello {{i}}!
* </div>
* </md-virtual-repeat-container>
* </hljs>
*
*/
function VirtualRepeatModelArrayLike(model) {
if (!angular.isFunction(model.getItemAtIndex) ||
!angular.isFunction(model.getLength)) {
throw Error('When md-on-demand is enabled, the Object passed to md-virtual-repeat must implement ' +
'functions getItemAtIndex() and getLength() ');
}
this.model = model;
}
VirtualRepeatModelArrayLike.prototype.$$includeIndexes = function(start, end) {
for (var i = start; i < end; i++) {
if (!this.hasOwnProperty(i)) {
this[i] = this.model.getItemAtIndex(i);
}
}
this.length = this.model.getLength();
};
function abstractMethod() {
throw Error('Non-overridden abstract method called.');
}
})(); // virtual repeat: .directive('...', VirtualRepeatContainerDirective), .directive('...', VirtualRepeatDirective);
(function(){
"use strict";
angular
.module('material.components.autocomplete')
.controller('MdAutocompleteCtrl', MdAutocompleteCtrl);
var ITEM_HEIGHT = 41,
MAX_HEIGHT = 5.5 * ITEM_HEIGHT,
MENU_PADDING = 8,
INPUT_PADDING = 2; // Padding provided by `md-input-container`
function MdAutocompleteCtrl ($scope, $element, $mdUtil, $mdConstant, $mdTheming, $window,
$animate, $rootElement, $attrs, $q) {
//-- private variables
var ctrl = this,
itemParts = $scope.itemsExpr.split(/ in /i),
itemExpr = itemParts[ 1 ],
elements = null,
cache = {},
noBlur = false,
selectedItemWatchers = [],
hasFocus = false,
lastCount = 0,
fetchesInProgress = 0,
enableWrapScroll = null;
//-- public variables with handlers
defineProperty('hidden', handleHiddenChange, true);
//-- public variables
ctrl.scope = $scope;
ctrl.parent = $scope.$parent;
ctrl.itemName = itemParts[ 0 ];
ctrl.matches = [];
ctrl.loading = false;
ctrl.hidden = true;
ctrl.index = null;
ctrl.messages = [];
ctrl.id = $mdUtil.nextUid();
ctrl.isDisabled = null;
ctrl.isRequired = null;
ctrl.isReadonly = null;
ctrl.hasNotFound = false;
//-- public methods
ctrl.keydown = keydown;
ctrl.blur = blur;
ctrl.focus = focus;
ctrl.clear = clearValue;
ctrl.select = select;
ctrl.listEnter = onListEnter;
ctrl.listLeave = onListLeave;
ctrl.mouseUp = onMouseup;
ctrl.getCurrentDisplayValue = getCurrentDisplayValue;
ctrl.registerSelectedItemWatcher = registerSelectedItemWatcher;
ctrl.unregisterSelectedItemWatcher = unregisterSelectedItemWatcher;
ctrl.notFoundVisible = notFoundVisible;
ctrl.loadingIsVisible = loadingIsVisible;
return init();
//-- initialization methods
/**
* Initialize the controller, setup watchers, gather elements
*/
function init () {
$mdUtil.initOptionalProperties($scope, $attrs, { searchText: null, selectedItem: null });
$mdTheming($element);
configureWatchers();
$mdUtil.nextTick(function () {
gatherElements();
moveDropdown();
focusElement();
$element.on('focus', focusElement);
});
}
/**
* Calculates the dropdown's position and applies the new styles to the menu element
* @returns {*}
*/
function positionDropdown () {
if (!elements) return $mdUtil.nextTick(positionDropdown, false, $scope);
var hrect = elements.wrap.getBoundingClientRect(),
vrect = elements.snap.getBoundingClientRect(),
root = elements.root.getBoundingClientRect(),
top = vrect.bottom - root.top,
bot = root.bottom - vrect.top,
left = hrect.left - root.left,
width = hrect.width,
offset = getVerticalOffset(),
styles;
// Adjust the width to account for the padding provided by `md-input-container`
if ($attrs.mdFloatingLabel) {
left += INPUT_PADDING;
width -= INPUT_PADDING * 2;
}
styles = {
left: left + 'px',
minWidth: width + 'px',
maxWidth: Math.max(hrect.right - root.left, root.right - hrect.left) - MENU_PADDING + 'px'
};
if (top > bot && root.height - hrect.bottom - MENU_PADDING < MAX_HEIGHT) {
styles.top = 'auto';
styles.bottom = bot + 'px';
styles.maxHeight = Math.min(MAX_HEIGHT, hrect.top - root.top - MENU_PADDING) + 'px';
} else {
styles.top = (top - offset) + 'px';
styles.bottom = 'auto';
styles.maxHeight = Math.min(MAX_HEIGHT, root.bottom + $mdUtil.scrollTop() - hrect.bottom - MENU_PADDING) + 'px';
}
elements.$.scrollContainer.css(styles);
$mdUtil.nextTick(correctHorizontalAlignment, false);
/**
* Calculates the vertical offset for floating label examples to account for ngMessages
* @returns {number}
*/
function getVerticalOffset () {
var offset = 0;
var inputContainer = $element.find('md-input-container');
if (inputContainer.length) {
var input = inputContainer.find('input');
offset = inputContainer.prop('offsetHeight');
offset -= input.prop('offsetTop');
offset -= input.prop('offsetHeight');
// add in the height left up top for the floating label text
offset += inputContainer.prop('offsetTop');
}
return offset;
}
/**
* Makes sure that the menu doesn't go off of the screen on either side.
*/
function correctHorizontalAlignment () {
var dropdown = elements.scrollContainer.getBoundingClientRect(),
styles = {};
if (dropdown.right > root.right - MENU_PADDING) {
styles.left = (hrect.right - dropdown.width) + 'px';
}
elements.$.scrollContainer.css(styles);
}
}
/**
* Moves the dropdown menu to the body tag in order to avoid z-index and overflow issues.
*/
function moveDropdown () {
if (!elements.$.root.length) return;
$mdTheming(elements.$.scrollContainer);
elements.$.scrollContainer.detach();
elements.$.root.append(elements.$.scrollContainer);
if ($animate.pin) $animate.pin(elements.$.scrollContainer, $rootElement);
}
/**
* Sends focus to the input element.
*/
function focusElement () {
if ($scope.autofocus) elements.input.focus();
}
/**
* Sets up any watchers used by autocomplete
*/
function configureWatchers () {
var wait = parseInt($scope.delay, 10) || 0;
$attrs.$observe('disabled', function (value) { ctrl.isDisabled = $mdUtil.parseAttributeBoolean(value, false); });
$attrs.$observe('required', function (value) { ctrl.isRequired = $mdUtil.parseAttributeBoolean(value, false); });
$attrs.$observe('readonly', function (value) { ctrl.isReadonly = $mdUtil.parseAttributeBoolean(value, false); });
$scope.$watch('searchText', wait ? $mdUtil.debounce(handleSearchText, wait) : handleSearchText);
$scope.$watch('selectedItem', selectedItemChange);
angular.element($window).on('resize', positionDropdown);
$scope.$on('$destroy', cleanup);
}
/**
* Removes any events or leftover elements created by this controller
*/
function cleanup () {
if (!ctrl.hidden) {
$mdUtil.enableScrolling();
}
angular.element($window).off('resize', positionDropdown);
if ( elements ){
var items = 'ul scroller scrollContainer input'.split(' ');
angular.forEach(items, function(key){
elements.$[key].remove();
});
}
}
/**
* Gathers all of the elements needed for this controller
*/
function gatherElements () {
elements = {
main: $element[0],
scrollContainer: $element[0].getElementsByClassName('md-virtual-repeat-container')[0],
scroller: $element[0].getElementsByClassName('md-virtual-repeat-scroller')[0],
ul: $element.find('ul')[0],
input: $element.find('input')[0],
wrap: $element.find('md-autocomplete-wrap')[0],
root: document.body
};
elements.li = elements.ul.getElementsByTagName('li');
elements.snap = getSnapTarget();
elements.$ = getAngularElements(elements);
}
/**
* Finds the element that the menu will base its position on
* @returns {*}
*/
function getSnapTarget () {
for (var element = $element; element.length; element = element.parent()) {
if (angular.isDefined(element.attr('md-autocomplete-snap'))) return element[ 0 ];
}
return elements.wrap;
}
/**
* Gathers angular-wrapped versions of each element
* @param elements
* @returns {{}}
*/
function getAngularElements (elements) {
var obj = {};
for (var key in elements) {
if (elements.hasOwnProperty(key)) obj[ key ] = angular.element(elements[ key ]);
}
return obj;
}
//-- event/change handlers
/**
* Handles changes to the `hidden` property.
* @param hidden
* @param oldHidden
*/
function handleHiddenChange (hidden, oldHidden) {
if (!hidden && oldHidden) {
positionDropdown();
if (elements) {
$mdUtil.nextTick(function () {
$mdUtil.disableScrollAround(elements.ul);
enableWrapScroll = disableElementScrollEvents(angular.element(elements.wrap));
}, false, $scope);
}
} else if (hidden && !oldHidden) {
$mdUtil.nextTick(function () {
$mdUtil.enableScrolling();
if (enableWrapScroll) {
enableWrapScroll();
enableWrapScroll = null;
}
}, false, $scope);
}
}
/**
* Disables scrolling for a specific element
*/
function disableElementScrollEvents(element) {
function preventDefault(e) {
e.preventDefault();
}
element.on('wheel', preventDefault);
element.on('touchmove', preventDefault);
return function() {
element.off('wheel', preventDefault);
element.off('touchmove', preventDefault);
}
}
/**
* When the user mouses over the dropdown menu, ignore blur events.
*/
function onListEnter () {
noBlur = true;
}
/**
* When the user's mouse leaves the menu, blur events may hide the menu again.
*/
function onListLeave () {
if (!hasFocus) elements.input.focus();
noBlur = false;
ctrl.hidden = shouldHide();
}
/**
* When the mouse button is released, send focus back to the input field.
*/
function onMouseup () {
elements.input.focus();
}
/**
* Handles changes to the selected item.
* @param selectedItem
* @param previousSelectedItem
*/
function selectedItemChange (selectedItem, previousSelectedItem) {
if (selectedItem) {
getDisplayValue(selectedItem).then(function (val) {
$scope.searchText = val;
handleSelectedItemChange(selectedItem, previousSelectedItem);
});
}
if (selectedItem !== previousSelectedItem) announceItemChange();
}
/**
* Use the user-defined expression to announce changes each time a new item is selected
*/
function announceItemChange () {
angular.isFunction($scope.itemChange) && $scope.itemChange(getItemAsNameVal($scope.selectedItem));
}
/**
* Use the user-defined expression to announce changes each time the search text is changed
*/
function announceTextChange () {
angular.isFunction($scope.textChange) && $scope.textChange();
}
/**
* Calls any external watchers listening for the selected item. Used in conjunction with
* `registerSelectedItemWatcher`.
* @param selectedItem
* @param previousSelectedItem
*/
function handleSelectedItemChange (selectedItem, previousSelectedItem) {
selectedItemWatchers.forEach(function (watcher) { watcher(selectedItem, previousSelectedItem); });
}
/**
* Register a function to be called when the selected item changes.
* @param cb
*/
function registerSelectedItemWatcher (cb) {
if (selectedItemWatchers.indexOf(cb) == -1) {
selectedItemWatchers.push(cb);
}
}
/**
* Unregister a function previously registered for selected item changes.
* @param cb
*/
function unregisterSelectedItemWatcher (cb) {
var i = selectedItemWatchers.indexOf(cb);
if (i != -1) {
selectedItemWatchers.splice(i, 1);
}
}
/**
* Handles changes to the searchText property.
* @param searchText
* @param previousSearchText
*/
function handleSearchText (searchText, previousSearchText) {
ctrl.index = getDefaultIndex();
// do nothing on init
if (searchText === previousSearchText) return;
getDisplayValue($scope.selectedItem).then(function (val) {
// clear selected item if search text no longer matches it
if (searchText !== val) {
$scope.selectedItem = null;
// trigger change event if available
if (searchText !== previousSearchText) announceTextChange();
// cancel results if search text is not long enough
if (!isMinLengthMet()) {
ctrl.matches = [];
setLoading(false);
updateMessages();
} else {
handleQuery();
}
}
});
}
/**
* Handles input blur event, determines if the dropdown should hide.
*/
function blur () {
hasFocus = false;
if (!noBlur) {
ctrl.hidden = shouldHide();
}
}
/**
* Force blur on input element
* @param forceBlur
*/
function doBlur(forceBlur) {
if (forceBlur) {
noBlur = false;
hasFocus = false;
}
elements.input.blur();
}
/**
* Handles input focus event, determines if the dropdown should show.
*/
function focus($event) {
hasFocus = true;
//-- if searchText is null, let's force it to be a string
if (!angular.isString($scope.searchText)) $scope.searchText = '';
ctrl.hidden = shouldHide();
if (!ctrl.hidden) handleQuery();
}
/**
* Handles keyboard input.
* @param event
*/
function keydown (event) {
switch (event.keyCode) {
case $mdConstant.KEY_CODE.DOWN_ARROW:
if (ctrl.loading) return;
event.stopPropagation();
event.preventDefault();
ctrl.index = Math.min(ctrl.index + 1, ctrl.matches.length - 1);
updateScroll();
updateMessages();
break;
case $mdConstant.KEY_CODE.UP_ARROW:
if (ctrl.loading) return;
event.stopPropagation();
event.preventDefault();
ctrl.index = ctrl.index < 0 ? ctrl.matches.length - 1 : Math.max(0, ctrl.index - 1);
updateScroll();
updateMessages();
break;
case $mdConstant.KEY_CODE.TAB:
// If we hit tab, assume that we've left the list so it will close
onListLeave();
if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
select(ctrl.index);
break;
case $mdConstant.KEY_CODE.ENTER:
if (ctrl.hidden || ctrl.loading || ctrl.index < 0 || ctrl.matches.length < 1) return;
if (hasSelection()) return;
event.stopPropagation();
event.preventDefault();
select(ctrl.index);
break;
case $mdConstant.KEY_CODE.ESCAPE:
event.stopPropagation();
event.preventDefault();
if ($scope.searchText) clearValue();
// Force the component to blur if they hit escape
doBlur(true);
break;
default:
}
}
//-- getters
/**
* Returns the minimum length needed to display the dropdown.
* @returns {*}
*/
function getMinLength () {
return angular.isNumber($scope.minLength) ? $scope.minLength : 1;
}
/**
* Returns the display value for an item.
* @param item
* @returns {*}
*/
function getDisplayValue (item) {
return $q.when(getItemText(item) || item);
/**
* Getter function to invoke user-defined expression (in the directive)
* to convert your object to a single string.
*/
function getItemText (item) {
return (item && $scope.itemText) ? $scope.itemText(getItemAsNameVal(item)) : null;
}
}
/**
* Returns the locals object for compiling item templates.
* @param item
* @returns {{}}
*/
function getItemAsNameVal (item) {
if (!item) return undefined;
var locals = {};
if (ctrl.itemName) locals[ ctrl.itemName ] = item;
return locals;
}
/**
* Returns the default index based on whether or not autoselect is enabled.
* @returns {number}
*/
function getDefaultIndex () {
return $scope.autoselect ? 0 : -1;
}
/**
* Sets the loading parameter and updates the hidden state.
* @param value {boolean} Whether or not the component is currently loading.
*/
function setLoading(value) {
if (ctrl.loading != value) {
ctrl.loading = value;
}
// Always refresh the hidden variable as something else might have changed
ctrl.hidden = shouldHide();
}
/**
* Determines if the menu should be hidden.
* @returns {boolean}
*/
function shouldHide () {
if (ctrl.loading && !hasMatches()) return true; // Hide while loading initial matches
else if (hasSelection()) return true; // Hide if there is already a selection
else if (!hasFocus) return true; // Hide if the input does not have focus
else return !shouldShow(); // Defer to standard show logic
}
/**
* Determines if the menu should be shown.
* @returns {boolean}
*/
function shouldShow() {
return (isMinLengthMet() && hasMatches()) || notFoundVisible();
}
/**
* Returns true if the search text has matches.
* @returns {boolean}
*/
function hasMatches() {
return ctrl.matches.length ? true : false;
}
/**
* Returns true if the autocomplete has a valid selection.
* @returns {boolean}
*/
function hasSelection() {
return ctrl.scope.selectedItem ? true : false;
}
/**
* Returns true if the loading indicator is, or should be, visible.
* @returns {boolean}
*/
function loadingIsVisible() {
return ctrl.loading && !hasSelection();
}
/**
* Returns the display value of the current item.
* @returns {*}
*/
function getCurrentDisplayValue () {
return getDisplayValue(ctrl.matches[ ctrl.index ]);
}
/**
* Determines if the minimum length is met by the search text.
* @returns {*}
*/
function isMinLengthMet () {
return ($scope.searchText || '').length >= getMinLength();
}
//-- actions
/**
* Defines a public property with a handler and a default value.
* @param key
* @param handler
* @param value
*/
function defineProperty (key, handler, value) {
Object.defineProperty(ctrl, key, {
get: function () { return value; },
set: function (newValue) {
var oldValue = value;
value = newValue;
handler(newValue, oldValue);
}
});
}
/**
* Selects the item at the given index.
* @param index
*/
function select (index) {
//-- force form to update state for validation
$mdUtil.nextTick(function () {
getDisplayValue(ctrl.matches[ index ]).then(function (val) {
var ngModel = elements.$.input.controller('ngModel');
ngModel.$setViewValue(val);
ngModel.$render();
}).finally(function () {
$scope.selectedItem = ctrl.matches[ index ];
setLoading(false);
});
}, false);
}
/**
* Clears the searchText value and selected item.
*/
function clearValue ($event) {
// Set the loading to true so we don't see flashes of content.
// The flashing will only occur when an async request is running.
// So the loading process will stop when the results had been retrieved.
setLoading(true);
// Reset our variables
ctrl.index = 0;
ctrl.matches = [];
$scope.searchText = '';
// Per http://www.w3schools.com/jsref/event_oninput.asp
var eventObj = document.createEvent('CustomEvent');
eventObj.initCustomEvent('input', true, true, { value: '' });
elements.input.dispatchEvent(eventObj);
// For some reason, firing the above event resets the value of $scope.searchText if
// $scope.searchText has a space character at the end, so we blank it one more time and then
// focus.
elements.input.blur();
$scope.searchText = '';
elements.input.focus();
}
/**
* Fetches the results for the provided search text.
* @param searchText
*/
function fetchResults (searchText) {
var items = $scope.$parent.$eval(itemExpr),
term = searchText.toLowerCase(),
isList = angular.isArray(items),
isPromise = !!items.then; // Every promise should contain a `then` property
if (isList) handleResults(items);
else if (isPromise) handleAsyncResults(items);
function handleAsyncResults(items) {
if ( !items ) return;
items = $q.when(items);
fetchesInProgress++;
setLoading(true);
$mdUtil.nextTick(function () {
items
.then(handleResults)
.finally(function(){
if (--fetchesInProgress === 0) {
setLoading(false);
}
});
},true, $scope);
}
function handleResults (matches) {
cache[ term ] = matches;
if ((searchText || '') !== ($scope.searchText || '')) return; //-- just cache the results if old request
ctrl.matches = matches;
ctrl.hidden = shouldHide();
// If loading is in progress, then we'll end the progress. This is needed for example,
// when the `clear` button was clicked, because there we always show the loading process, to prevent flashing.
if (ctrl.loading) setLoading(false);
if ($scope.selectOnMatch) selectItemOnMatch();
updateMessages();
positionDropdown();
}
}
/**
* Updates the ARIA messages
*/
function updateMessages () {
getCurrentDisplayValue().then(function (msg) {
ctrl.messages = [ getCountMessage(), msg ];
});
}
/**
* Returns the ARIA message for how many results match the current query.
* @returns {*}
*/
function getCountMessage () {
if (lastCount === ctrl.matches.length) return '';
lastCount = ctrl.matches.length;
switch (ctrl.matches.length) {
case 0:
return 'There are no matches available.';
case 1:
return 'There is 1 match available.';
default:
return 'There are ' + ctrl.matches.length + ' matches available.';
}
}
/**
* Makes sure that the focused element is within view.
*/
function updateScroll () {
if (!elements.li[0]) return;
var height = elements.li[0].offsetHeight,
top = height * ctrl.index,
bot = top + height,
hgt = elements.scroller.clientHeight,
scrollTop = elements.scroller.scrollTop;
if (top < scrollTop) {
scrollTo(top);
} else if (bot > scrollTop + hgt) {
scrollTo(bot - hgt);
}
}
function isPromiseFetching() {
return fetchesInProgress !== 0;
}
function scrollTo (offset) {
elements.$.scrollContainer.controller('mdVirtualRepeatContainer').scrollTo(offset);
}
function notFoundVisible () {
var textLength = (ctrl.scope.searchText || '').length;
return ctrl.hasNotFound && !hasMatches() && (!ctrl.loading || isPromiseFetching()) && textLength >= getMinLength() && (hasFocus || noBlur) && !hasSelection();
}
/**
* Starts the query to gather the results for the current searchText. Attempts to return cached
* results first, then forwards the process to `fetchResults` if necessary.
*/
function handleQuery () {
var searchText = $scope.searchText || '',
term = searchText.toLowerCase();
//-- if results are cached, pull in cached results
if (!$scope.noCache && cache[ term ]) {
ctrl.matches = cache[ term ];
updateMessages();
setLoading(false);
} else {
fetchResults(searchText);
}
ctrl.hidden = shouldHide();
}
/**
* If there is only one matching item and the search text matches its display value exactly,
* automatically select that item. Note: This function is only called if the user uses the
* `md-select-on-match` flag.
*/
function selectItemOnMatch () {
var searchText = $scope.searchText,
matches = ctrl.matches,
item = matches[ 0 ];
if (matches.length === 1) getDisplayValue(item).then(function (displayValue) {
var isMatching = searchText == displayValue;
if ($scope.matchInsensitive && !isMatching) {
isMatching = searchText.toLowerCase() == displayValue.toLowerCase();
}
if (isMatching) select(0);
});
}
}
MdAutocompleteCtrl.$inject = ["$scope", "$element", "$mdUtil", "$mdConstant", "$mdTheming", "$window", "$animate", "$rootElement", "$attrs", "$q"];
})(); // autocomplete: .controller('MdAutocompleteCtrl', MdAutocompleteCtrl);
(function(){
"use strict";
angular
.module('material.components.autocomplete')
.directive('mdAutocomplete', MdAutocomplete);
function MdAutocomplete () {
return {
controller: 'MdAutocompleteCtrl',
controllerAs: '$mdAutocompleteCtrl',
scope: {
inputName: '@mdInputName',
inputMinlength: '@mdInputMinlength',
inputMaxlength: '@mdInputMaxlength',
searchText: '=?mdSearchText',
selectedItem: '=?mdSelectedItem',
itemsExpr: '@mdItems',
itemText: '&mdItemText',
placeholder: '@placeholder',
noCache: '=?mdNoCache',
selectOnMatch: '=?mdSelectOnMatch',
matchInsensitive: '=?mdMatchCaseInsensitive',
itemChange: '&?mdSelectedItemChange',
textChange: '&?mdSearchTextChange',
minLength: '=?mdMinLength',
delay: '=?mdDelay',
autofocus: '=?mdAutofocus',
floatingLabel: '@?mdFloatingLabel',
autoselect: '=?mdAutoselect',
menuClass: '@?mdMenuClass',
inputId: '@?mdInputId'
},
link: function(scope, element, attrs, controller) {
// Retrieve the state of using a md-not-found template by using our attribute, which will
// be added to the element in the template function.
controller.hasNotFound = !!element.attr('md-has-not-found');
},
template: function (element, attr) {
var noItemsTemplate = getNoItemsTemplate(),
itemTemplate = getItemTemplate(),
leftover = element.html(),
tabindex = attr.tabindex;
// Set our attribute for the link function above which runs later.
// We will set an attribute, because otherwise the stored variables will be trashed when
// removing the element is hidden while retrieving the template. For example when using ngIf.
if (noItemsTemplate) element.attr('md-has-not-found', true);
// Always set our tabindex of the autocomplete directive to -1, because our input
// will hold the actual tabindex.
element.attr('tabindex', '-1');
return '\
<md-autocomplete-wrap\
layout="row"\
ng-class="{ \'md-whiteframe-z1\': !floatingLabel, \'md-menu-showing\': !$mdAutocompleteCtrl.hidden }">\
' + getInputElement() + '\
<md-progress-linear\
class="' + (attr.mdFloatingLabel ? 'md-inline' : '') + '"\
ng-if="$mdAutocompleteCtrl.loadingIsVisible()"\
md-mode="indeterminate"></md-progress-linear>\
<md-virtual-repeat-container\
md-auto-shrink\
md-auto-shrink-min="1"\
ng-mouseenter="$mdAutocompleteCtrl.listEnter()"\
ng-mouseleave="$mdAutocompleteCtrl.listLeave()"\
ng-mouseup="$mdAutocompleteCtrl.mouseUp()"\
ng-hide="$mdAutocompleteCtrl.hidden"\
class="md-autocomplete-suggestions-container md-whiteframe-z1"\
ng-class="{ \'md-not-found\': $mdAutocompleteCtrl.notFoundVisible() }"\
role="presentation">\
<ul class="md-autocomplete-suggestions"\
ng-class="::menuClass"\
id="ul-{{$mdAutocompleteCtrl.id}}">\
<li md-virtual-repeat="item in $mdAutocompleteCtrl.matches"\
ng-class="{ selected: $index === $mdAutocompleteCtrl.index }"\
ng-click="$mdAutocompleteCtrl.select($index)"\
md-extra-name="$mdAutocompleteCtrl.itemName"\
title="{{ item }}">\
' + itemTemplate + '\
</li>' + noItemsTemplate + '\
</ul>\
</md-virtual-repeat-container>\
</md-autocomplete-wrap>\
<aria-status\
class="_md-visually-hidden"\
role="status"\
aria-live="assertive">\
<p ng-repeat="message in $mdAutocompleteCtrl.messages track by $index" ng-if="message">{{message}}</p>\
</aria-status>';
function getItemTemplate() {
var templateTag = element.find('md-item-template').detach(),
html = templateTag.length ? templateTag.html() : element.html();
if (!templateTag.length) element.empty();
return '<md-autocomplete-parent-scope md-autocomplete-replace>' + html + '</md-autocomplete-parent-scope>';
}
function getNoItemsTemplate() {
var templateTag = element.find('md-not-found').detach(),
template = templateTag.length ? templateTag.html() : '';
return template
? '<li ng-if="$mdAutocompleteCtrl.notFoundVisible()"\
md-autocomplete-parent-scope>' + template + '</li>'
: '';
}
function getInputElement () {
if (attr.mdFloatingLabel) {
return '\
<md-input-container flex ng-if="floatingLabel">\
<label>{{floatingLabel}}</label>\
<input type="search"\
' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
id="{{ inputId || \'fl-input-\' + $mdAutocompleteCtrl.id }}"\
name="{{inputName}}"\
autocomplete="off"\
ng-required="$mdAutocompleteCtrl.isRequired"\
ng-readonly="$mdAutocompleteCtrl.isReadonly"\
ng-minlength="inputMinlength"\
ng-maxlength="inputMaxlength"\
ng-disabled="$mdAutocompleteCtrl.isDisabled"\
ng-model="$mdAutocompleteCtrl.scope.searchText"\
ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
ng-blur="$mdAutocompleteCtrl.blur()"\
' + (attr.mdNoAsterisk != null ? 'md-no-asterisk="' + attr.mdNoAsterisk + '"' : '') + '\
ng-focus="$mdAutocompleteCtrl.focus($event)"\
aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
' + (attr.mdSelectOnFocus != null ? 'md-select-on-focus=""' : '') + '\
aria-label="{{floatingLabel}}"\
aria-autocomplete="list"\
role="combobox"\
aria-haspopup="true"\
aria-activedescendant=""\
aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
<div md-autocomplete-parent-scope md-autocomplete-replace>' + leftover + '</div>\
</md-input-container>';
} else {
return '\
<input flex type="search"\
' + (tabindex != null ? 'tabindex="' + tabindex + '"' : '') + '\
id="{{ inputId || \'input-\' + $mdAutocompleteCtrl.id }}"\
name="{{inputName}}"\
ng-if="!floatingLabel"\
autocomplete="off"\
ng-required="$mdAutocompleteCtrl.isRequired"\
ng-disabled="$mdAutocompleteCtrl.isDisabled"\
ng-readonly="$mdAutocompleteCtrl.isReadonly"\
ng-model="$mdAutocompleteCtrl.scope.searchText"\
ng-keydown="$mdAutocompleteCtrl.keydown($event)"\
ng-blur="$mdAutocompleteCtrl.blur()"\
ng-focus="$mdAutocompleteCtrl.focus($event)"\
placeholder="{{placeholder}}"\
aria-owns="ul-{{$mdAutocompleteCtrl.id}}"\
' + (attr.mdSelectOnFocus != null ? 'md-select-on-focus=""' : '') + '\
aria-label="{{placeholder}}"\
aria-autocomplete="list"\
role="combobox"\
aria-haspopup="true"\
aria-activedescendant=""\
aria-expanded="{{!$mdAutocompleteCtrl.hidden}}"/>\
<button\
type="button"\
tabindex="-1"\
ng-if="$mdAutocompleteCtrl.scope.searchText && !$mdAutocompleteCtrl.isDisabled"\
ng-click="$mdAutocompleteCtrl.clear($event)">\
<span class="MD_ICON_WAS_HERE">X</span>\
<span class="_md-visually-hidden">Clear</span>\
</button>\
';
}
}
}
};
}
})(); // autocomplete: .directive('mdAutocomplete', MdAutocomplete);
(function(){
"use strict";
angular
.module('material.components.autocomplete')
.directive('mdAutocompleteParentScope', MdAutocompleteItemScopeDirective);
function MdAutocompleteItemScopeDirective($compile, $mdUtil) {
return {
restrict: 'AE',
compile: compile,
terminal: true,
transclude: 'element'
};
function compile(tElement, tAttr, transclude) {
return function postLink(scope, element, attr) {
var ctrl = scope.$mdAutocompleteCtrl;
var newScope = ctrl.parent.$new();
var itemName = ctrl.itemName;
// Watch for changes to our scope's variables and copy them to the new scope
watchVariable('$index', '$index');
watchVariable('item', itemName);
// Ensure that $digest calls on our scope trigger $digest on newScope.
connectScopes();
// Link the element against newScope.
transclude(newScope, function(clone) {
element.after(clone);
});
/**
* Creates a watcher for variables that are copied from the parent scope
* @param variable
* @param alias
*/
function watchVariable(variable, alias) {
newScope[alias] = scope[variable];
scope.$watch(variable, function(value) {
$mdUtil.nextTick(function() {
newScope[alias] = value;
});
});
}
/**
* Creates watchers on scope and newScope that ensure that for any
* $digest of scope, newScope is also $digested.
*/
function connectScopes() {
var scopeDigesting = false;
var newScopeDigesting = false;
scope.$watch(function() {
if (newScopeDigesting || scopeDigesting) {
return;
}
scopeDigesting = true;
scope.$$postDigest(function() {
if (!newScopeDigesting) {
newScope.$digest();
}
scopeDigesting = newScopeDigesting = false;
});
});
newScope.$watch(function() {
newScopeDigesting = true;
});
}
};
}
}
MdAutocompleteItemScopeDirective.$inject = ["$compile", "$mdUtil"];
})(); // autocomplete: .directive('mdAutocompleteParentScope', MdAutocompleteItemScopeDirective);
(function(){
"use strict";
angular
.module('material.components.autocomplete')
.controller('MdHighlightCtrl', MdHighlightCtrl);
function MdHighlightCtrl ($scope, $element, $attrs) {
this.init = init;
function init (termExpr, unsafeTextExpr) {
var text = null,
regex = null,
flags = $attrs.mdHighlightFlags || '',
watcher = $scope.$watch(function($scope) {
return {
term: termExpr($scope),
unsafeText: unsafeTextExpr($scope)
};
}, function (state, prevState) {
if (text === null || state.unsafeText !== prevState.unsafeText) {
text = angular.element('<div>').text(state.unsafeText).html()
}
if (regex === null || state.term !== prevState.term) {
regex = getRegExp(state.term, flags);
}
$element.html(text.replace(regex, '<span class="highlight">$&</span>'));
}, true);
$element.on('$destroy', watcher);
}
function sanitize (term) {
return term && term.replace(/[\\\^\$\*\+\?\.\(\)\|\{}\[\]]/g, '\\$&');
}
function getRegExp (text, flags) {
var startFlag = '', endFlag = '';
if (flags.indexOf('^') >= 0) startFlag = '^';
if (flags.indexOf('$') >= 0) endFlag = '$';
return new RegExp(startFlag + sanitize(text) + endFlag, flags.replace(/[\$\^]/g, ''));
}
}
MdHighlightCtrl.$inject = ["$scope", "$element", "$attrs"];
})(); // autocomplete: .controller('MdHighlightCtrl', MdHighlightCtrl);
(function(){
"use strict";
angular
.module('material.components.autocomplete')
.directive('mdHighlightText', MdHighlight);
/**
* @ngdoc directive
* @name mdHighlightText
* @module material.components.autocomplete
*
* @description
* The `md-highlight-text` directive allows you to specify text that should be highlighted within
* an element. Highlighted text will be wrapped in `<span class="highlight"></span>` which can
* be styled through CSS. Please note that child elements may not be used with this directive.
*
* @param {string} md-highlight-text A model to be searched for
* @param {string=} md-highlight-flags A list of flags (loosely based on JavaScript RexExp flags).
* #### **Supported flags**:
* - `g`: Find all matches within the provided text
* - `i`: Ignore case when searching for matches
* - `$`: Only match if the text ends with the search term
* - `^`: Only match if the text begins with the search term
*
* @usage
* <hljs lang="html">
* <input placeholder="Enter a search term..." ng-model="searchTerm" type="text" />
* <ul>
* <li ng-repeat="result in results" md-highlight-text="searchTerm">
* {{result.text}}
* </li>
* </ul>
* </hljs>
*/
function MdHighlight ($interpolate, $parse) {
return {
terminal: true,
controller: 'MdHighlightCtrl',
compile: function mdHighlightCompile(tElement, tAttr) {
var termExpr = $parse(tAttr.mdHighlightText);
var unsafeTextExpr = $interpolate(tElement.html());
return function mdHighlightLink(scope, element, attr, ctrl) {
ctrl.init(termExpr, unsafeTextExpr);
};
}
};
}
MdHighlight.$inject = ["$interpolate", "$parse"];
})(); // autocomplete: .directive('mdHighlightText', MdHighlight);
(function(){
"use strict";
angular
.module('material.components.chips')
.controller('MdChipCtrl', MdChipCtrl);
/**
* Controller for the MdChip component. Responsible for handling keyboard
* events and editting the chip if needed.
*
* @param $scope
* @param $element
* @param $mdConstant
* @param $timeout
* @param $mdUtil
* @constructor
*/
function MdChipCtrl ($scope, $element, $mdConstant, $timeout, $mdUtil) {
/**
* @type {$scope}
*/
this.$scope = $scope;
/**
* @type {$element}
*/
this.$element = $element;
/**
* @type {$mdConstant}
*/
this.$mdConstant = $mdConstant;
/**
* @type {$timeout}
*/
this.$timeout = $timeout;
/**
* @type {$mdUtil}
*/
this.$mdUtil = $mdUtil;
/**
* @type {boolean}
*/
this.isEditting = false;
/**
* @type {MdChipsCtrl}
*/
this.parentController = undefined;
/**
* @type {boolean}
*/
this.enableChipEdit = false;
}
MdChipCtrl.$inject = ["$scope", "$element", "$mdConstant", "$timeout", "$mdUtil"];
/**
* @param {MdChipsCtrl} controller
*/
MdChipCtrl.prototype.init = function(controller) {
this.parentController = controller;
this.enableChipEdit = this.parentController.enableChipEdit;
if (this.enableChipEdit) {
this.$element.on('keydown', this.chipKeyDown.bind(this));
this.$element.on('mousedown', this.chipMouseDown.bind(this));
this.getChipContent().addClass('_md-chip-content-edit-is-enabled');
}
};
/**
* @return {Object}
*/
MdChipCtrl.prototype.getChipContent = function() {
var chipContents = this.$element[0].getElementsByClassName('_md-chip-content');
return angular.element(chipContents[0]);
};
/**
* @return {Object}
*/
MdChipCtrl.prototype.getContentElement = function() {
return angular.element(this.getChipContent().children()[0]);
};
/**
* @return {number}
*/
MdChipCtrl.prototype.getChipIndex = function() {
return parseInt(this.$element.attr('index'));
};
/**
* Presents an input element to edit the contents of the chip.
*/
MdChipCtrl.prototype.goOutOfEditMode = function() {
if (!this.isEditting) return;
this.isEditting = false;
this.$element.removeClass('_md-chip-editing');
this.getChipContent()[0].contentEditable = 'false';
var chipIndex = this.getChipIndex();
var content = this.getContentElement().text();
if (content) {
this.parentController.updateChipContents(
chipIndex,
this.getContentElement().text()
);
this.$mdUtil.nextTick(function() {
if (this.parentController.selectedChip === chipIndex) {
this.parentController.focusChip(chipIndex);
}
}.bind(this));
} else {
this.parentController.removeChipAndFocusInput(chipIndex);
}
};
/**
* Given an HTML element. Selects contents of it.
* @param node
*/
MdChipCtrl.prototype.selectNodeContents = function(node) {
var range, selection;
if (document.body.createTextRange) {
range = document.body.createTextRange();
range.moveToElementText(node);
range.select();
} else if (window.getSelection) {
selection = window.getSelection();
range = document.createRange();
range.selectNodeContents(node);
selection.removeAllRanges();
selection.addRange(range);
}
};
/**
* Presents an input element to edit the contents of the chip.
*/
MdChipCtrl.prototype.goInEditMode = function() {
this.isEditting = true;
this.$element.addClass('_md-chip-editing');
this.getChipContent()[0].contentEditable = 'true';
this.getChipContent().on('blur', function() {
this.goOutOfEditMode();
}.bind(this));
this.selectNodeContents(this.getChipContent()[0]);
};
/**
* Handles the keydown event on the chip element. If enable-chip-edit attribute is
* set to true, space or enter keys can trigger going into edit mode. Enter can also
* trigger submitting if the chip is already being edited.
* @param event
*/
MdChipCtrl.prototype.chipKeyDown = function(event) {
if (!this.isEditting &&
(event.keyCode === this.$mdConstant.KEY_CODE.ENTER ||
event.keyCode === this.$mdConstant.KEY_CODE.SPACE)) {
event.preventDefault();
this.goInEditMode();
} else if (this.isEditting &&
event.keyCode === this.$mdConstant.KEY_CODE.ENTER) {
event.preventDefault();
this.goOutOfEditMode();
}
};
/**
* Handles the double click event
*/
MdChipCtrl.prototype.chipMouseDown = function() {
if(this.getChipIndex() == this.parentController.selectedChip &&
this.enableChipEdit &&
!this.isEditting) {
this.goInEditMode();
}
};
})(); // chips: .controller('MdChipCtrl', MdChipCtrl);
(function(){
"use strict";
angular
.module('material.components.chips')
.directive('mdChip', MdChip);
// This hint text is hidden within a chip but used by screen readers to
// inform the user how they can interact with a chip.
var DELETE_HINT_TEMPLATE = '\
<span ng-if="!$mdChipsCtrl.readonly" class="_md-visually-hidden">\
{{$mdChipsCtrl.deleteHint}}\
</span>';
function MdChip($mdTheming, $mdUtil) {
var hintTemplate = $mdUtil.processTemplate(DELETE_HINT_TEMPLATE);
return {
restrict: 'E',
require: ['^?mdChips', 'mdChip'],
compile: compile,
controller: 'MdChipCtrl'
};
function compile(element, attr) {
// Append the delete template
element.append($mdUtil.processTemplate(hintTemplate));
return function postLink(scope, element, attr, ctrls) {
var chipsController = ctrls.shift();
var chipController = ctrls.shift();
$mdTheming(element);
if (chipsController) {
chipController.init(chipsController);
angular
.element(element[0]
.querySelector('._md-chip-content'))
.on('blur', function () {
chipsController.resetSelectedChip();
chipsController.$scope.$applyAsync();
});
}
};
}
}
MdChip.$inject = ["$mdTheming", "$mdUtil"];
})(); // chips: .directive('mdChip', MdChip);
(function(){
"use strict";
angular
.module('material.components.chips')
.directive('mdChipRemove', MdChipRemove);
/**
* @ngdoc directive
* @name mdChipRemove
* @module material.components.chips
*
* @description
* `<md-chip-remove>`
* Designates an element to be used as the delete button for a chip. This
* element is passed as a child of the `md-chips` element.
*
* @usage
* <hljs lang="html">
* <md-chips><button md-chip-remove>DEL</button></md-chips>
* </hljs>
*/
/**
* MdChipRemove Directive Definition.
*
* @param $compile
* @param $timeout
* @returns {{restrict: string, require: string[], link: Function, scope: boolean}}
* @constructor
*/
function MdChipRemove ($timeout) {
return {
restrict: 'A',
require: '^mdChips',
scope: false,
link: postLink
};
function postLink(scope, element, attr, ctrl) {
element.on('click', function(event) {
scope.$apply(function() {
ctrl.removeChip(scope.$$replacedScope.$index);
});
});
// Child elements aren't available until after a $timeout tick as they are hidden by an
// `ng-if`. see http://goo.gl/zIWfuw
$timeout(function() {
element.attr({ tabindex: -1, 'aria-hidden': true });
element.find('button').attr('tabindex', '-1');
});
}
}
MdChipRemove.$inject = ["$timeout"];
})(); // chips: .directive('mdChipRemove', MdChipRemove);
(function(){
"use strict";
angular
.module('material.components.chips')
.directive('mdChipTransclude', MdChipTransclude);
function MdChipTransclude ($compile) {
return {
restrict: 'EA',
terminal: true,
link: link,
scope: false
};
function link (scope, element, attr) {
var ctrl = scope.$parent.$mdChipsCtrl,
newScope = ctrl.parent.$new(false, ctrl.parent);
newScope.$$replacedScope = scope;
newScope.$chip = scope.$chip;
newScope.$index = scope.$index;
newScope.$mdChipsCtrl = ctrl;
var newHtml = ctrl.$scope.$eval(attr.mdChipTransclude);
element.html(newHtml);
$compile(element.contents())(newScope);
}
}
MdChipTransclude.$inject = ["$compile"];
})(); // chips: .directive('mdChipTransclude', MdChipTransclude);
(function(){
"use strict";
angular
.module('material.components.chips')
.controller('MdChipsCtrl', MdChipsCtrl);
/**
* Controller for the MdChips component. Responsible for adding to and
* removing from the list of chips, marking chips as selected, and binding to
* the models of various input components.
*
* @param $scope
* @param $mdConstant
* @param $log
* @param $element
* @param $mdUtil
* @constructor
*/
function MdChipsCtrl ($scope, $mdConstant, $log, $element, $timeout, $mdUtil) {
/** @type {$timeout} **/
this.$timeout = $timeout;
/** @type {Object} */
this.$mdConstant = $mdConstant;
/** @type {angular.$scope} */
this.$scope = $scope;
/** @type {angular.$scope} */
this.parent = $scope.$parent;
/** @type {$log} */
this.$log = $log;
/** @type {$element} */
this.$element = $element;
/** @type {angular.NgModelController} */
this.ngModelCtrl = null;
/** @type {angular.NgModelController} */
this.userInputNgModelCtrl = null;
/** @type {Element} */
this.userInputElement = null;
/** @type {Array.<Object>} */
this.items = [];
/** @type {number} */
this.selectedChip = -1;
/** @type {boolean} */
this.hasAutocomplete = false;
/** @type {string} */
this.enableChipEdit = $mdUtil.parseAttributeBoolean(this.mdEnableChipEdit);
/**
* Hidden hint text for how to delete a chip. Used to give context to screen readers.
* @type {string}
*/
this.deleteHint = 'Press delete to remove this chip.';
/**
* Hidden label for the delete button. Used to give context to screen readers.
* @type {string}
*/
this.deleteButtonLabel = 'Remove';
/**
* Model used by the input element.
* @type {string}
*/
this.chipBuffer = '';
/**
* Whether to use the transformChip expression to transform the chip buffer
* before appending it to the list.
* @type {boolean}
*/
this.useTransformChip = false;
/**
* Whether to use the onAdd expression to notify of chip additions.
* @type {boolean}
*/
this.useOnAdd = false;
/**
* Whether to use the onRemove expression to notify of chip removals.
* @type {boolean}
*/
this.useOnRemove = false;
/**
* Whether to use the onSelect expression to notify the component's user
* after selecting a chip from the list.
* @type {boolean}
*/
this.useOnSelect = false;
}
MdChipsCtrl.$inject = ["$scope", "$mdConstant", "$log", "$element", "$timeout", "$mdUtil"];
/**
* Handles the keydown event on the input element: by default <enter> appends
* the buffer to the chip list, while backspace removes the last chip in the
* list if the current buffer is empty.
* @param event
*/
MdChipsCtrl.prototype.inputKeydown = function(event) {
var chipBuffer = this.getChipBuffer();
// If we have an autocomplete, and it handled the event, we have nothing to do
if (this.hasAutocomplete && event.isDefaultPrevented && event.isDefaultPrevented()) {
return;
}
if (event.keyCode === this.$mdConstant.KEY_CODE.BACKSPACE) {
if (chipBuffer) return;
event.preventDefault();
event.stopPropagation();
if (this.items.length) this.selectAndFocusChipSafe(this.items.length - 1);
return;
}
// By default <enter> appends the buffer to the chip list.
if (!this.separatorKeys || this.separatorKeys.length < 1) {
this.separatorKeys = [this.$mdConstant.KEY_CODE.ENTER];
}
// Support additional separator key codes in an array of `md-separator-keys`.
if (this.separatorKeys.indexOf(event.keyCode) !== -1) {
if ((this.hasAutocomplete && this.requireMatch) || !chipBuffer) return;
event.preventDefault();
// Only append the chip and reset the chip buffer if the max chips limit isn't reached.
if (this.hasMaxChipsReached()) return;
this.appendChip(chipBuffer.trim());
this.resetChipBuffer();
}
};
/**
* Updates the content of the chip at given index
* @param chipIndex
* @param chipContents
*/
MdChipsCtrl.prototype.updateChipContents = function(chipIndex, chipContents){
if(chipIndex >= 0 && chipIndex < this.items.length) {
this.items[chipIndex] = chipContents;
this.ngModelCtrl.$setDirty();
}
};
/**
* Returns true if a chip is currently being edited. False otherwise.
* @return {boolean}
*/
MdChipsCtrl.prototype.isEditingChip = function(){
return !!this.$element[0].getElementsByClassName('_md-chip-editing').length;
};
/**
* Handles the keydown event on the chip elements: backspace removes the selected chip, arrow
* keys switch which chips is active
* @param event
*/
MdChipsCtrl.prototype.chipKeydown = function (event) {
if (this.getChipBuffer()) return;
if (this.isEditingChip()) return;
switch (event.keyCode) {
case this.$mdConstant.KEY_CODE.BACKSPACE:
case this.$mdConstant.KEY_CODE.DELETE:
if (this.selectedChip < 0) return;
event.preventDefault();
this.removeAndSelectAdjacentChip(this.selectedChip);
break;
case this.$mdConstant.KEY_CODE.LEFT_ARROW:
event.preventDefault();
if (this.selectedChip < 0) this.selectedChip = this.items.length;
if (this.items.length) this.selectAndFocusChipSafe(this.selectedChip - 1);
break;
case this.$mdConstant.KEY_CODE.RIGHT_ARROW:
event.preventDefault();
this.selectAndFocusChipSafe(this.selectedChip + 1);
break;
case this.$mdConstant.KEY_CODE.ESCAPE:
case this.$mdConstant.KEY_CODE.TAB:
if (this.selectedChip < 0) return;
event.preventDefault();
this.onFocus();
break;
}
};
/**
* Get the input's placeholder - uses `placeholder` when list is empty and `secondary-placeholder`
* when the list is non-empty. If `secondary-placeholder` is not provided, `placeholder` is used
* always.
*/
MdChipsCtrl.prototype.getPlaceholder = function() {
// Allow `secondary-placeholder` to be blank.
var useSecondary = (this.items && this.items.length &&
(this.secondaryPlaceholder == '' || this.secondaryPlaceholder));
return useSecondary ? this.secondaryPlaceholder : this.placeholder;
};
/**
* Removes chip at {@code index} and selects the adjacent chip.
* @param index
*/
MdChipsCtrl.prototype.removeAndSelectAdjacentChip = function(index) {
var selIndex = this.getAdjacentChipIndex(index);
this.removeChip(index);
this.$timeout(angular.bind(this, function () {
this.selectAndFocusChipSafe(selIndex);
}));
};
/**
* Sets the selected chip index to -1.
*/
MdChipsCtrl.prototype.resetSelectedChip = function() {
this.selectedChip = -1;
};
/**
* Gets the index of an adjacent chip to select after deletion. Adjacency is
* determined as the next chip in the list, unless the target chip is the
* last in the list, then it is the chip immediately preceding the target. If
* there is only one item in the list, -1 is returned (select none).
* The number returned is the index to select AFTER the target has been
* removed.
* If the current chip is not selected, then -1 is returned to select none.
*/
MdChipsCtrl.prototype.getAdjacentChipIndex = function(index) {
var len = this.items.length - 1;
return (len == 0) ? -1 :
(index == len) ? index -1 : index;
};
/**
* Append the contents of the buffer to the chip list. This method will first
* call out to the md-transform-chip method, if provided.
*
* @param newChip
*/
MdChipsCtrl.prototype.appendChip = function(newChip) {
if (this.useTransformChip && this.transformChip) {
var transformedChip = this.transformChip({'$chip': newChip});
// Check to make sure the chip is defined before assigning it, otherwise, we'll just assume
// they want the string version.
if (angular.isDefined(transformedChip)) {
newChip = transformedChip;
}
}
// If items contains an identical object to newChip, do not append
if (angular.isObject(newChip)){
var identical = this.items.some(function(item){
return angular.equals(newChip, item);
});
if (identical) return;
}
// Check for a null (but not undefined), or existing chip and cancel appending
if (newChip == null || this.items.indexOf(newChip) + 1) return;
// Append the new chip onto our list
var index = this.items.push(newChip);
// Update model validation
this.ngModelCtrl.$setDirty();
this.validateModel();
// If they provide the md-on-add attribute, notify them of the chip addition
if (this.useOnAdd && this.onAdd) {
this.onAdd({ '$chip': newChip, '$index': index });
}
};
/**
* Sets whether to use the md-transform-chip expression. This expression is
* bound to scope and controller in {@code MdChipsDirective} as
* {@code transformChip}. Due to the nature of directive scope bindings, the
* controller cannot know on its own/from the scope whether an expression was
* actually provided.
*/
MdChipsCtrl.prototype.useTransformChipExpression = function() {
this.useTransformChip = true;
};
/**
* Sets whether to use the md-on-add expression. This expression is
* bound to scope and controller in {@code MdChipsDirective} as
* {@code onAdd}. Due to the nature of directive scope bindings, the
* controller cannot know on its own/from the scope whether an expression was
* actually provided.
*/
MdChipsCtrl.prototype.useOnAddExpression = function() {
this.useOnAdd = true;
};
/**
* Sets whether to use the md-on-remove expression. This expression is
* bound to scope and controller in {@code MdChipsDirective} as
* {@code onRemove}. Due to the nature of directive scope bindings, the
* controller cannot know on its own/from the scope whether an expression was
* actually provided.
*/
MdChipsCtrl.prototype.useOnRemoveExpression = function() {
this.useOnRemove = true;
};
/*
* Sets whether to use the md-on-select expression. This expression is
* bound to scope and controller in {@code MdChipsDirective} as
* {@code onSelect}. Due to the nature of directive scope bindings, the
* controller cannot know on its own/from the scope whether an expression was
* actually provided.
*/
MdChipsCtrl.prototype.useOnSelectExpression = function() {
this.useOnSelect = true;
};
/**
* Gets the input buffer. The input buffer can be the model bound to the
* default input item {@code this.chipBuffer}, the {@code selectedItem}
* model of an {@code md-autocomplete}, or, through some magic, the model
* bound to any inpput or text area element found within a
* {@code md-input-container} element.
* @return {Object|string}
*/
MdChipsCtrl.prototype.getChipBuffer = function() {
return !this.userInputElement ? this.chipBuffer :
this.userInputNgModelCtrl ? this.userInputNgModelCtrl.$viewValue :
this.userInputElement[0].value;
};
/**
* Resets the input buffer for either the internal input or user provided input element.
*/
MdChipsCtrl.prototype.resetChipBuffer = function() {
if (this.userInputElement) {
if (this.userInputNgModelCtrl) {
this.userInputNgModelCtrl.$setViewValue('');
this.userInputNgModelCtrl.$render();
} else {
this.userInputElement[0].value = '';
}
} else {
this.chipBuffer = '';
}
};
MdChipsCtrl.prototype.hasMaxChipsReached = function() {
if (angular.isString(this.maxChips)) this.maxChips = parseInt(this.maxChips, 10) || 0;
return this.maxChips > 0 && this.items.length >= this.maxChips;
};
/**
* Updates the validity properties for the ngModel.
*/
MdChipsCtrl.prototype.validateModel = function() {
this.ngModelCtrl.$setValidity('md-max-chips', !this.hasMaxChipsReached());
};
/**
* Removes the chip at the given index.
* @param index
*/
MdChipsCtrl.prototype.removeChip = function(index) {
var removed = this.items.splice(index, 1);
// Update model validation
this.ngModelCtrl.$setDirty();
this.validateModel();
if (removed && removed.length && this.useOnRemove && this.onRemove) {
this.onRemove({ '$chip': removed[0], '$index': index });
}
};
MdChipsCtrl.prototype.removeChipAndFocusInput = function (index) {
this.removeChip(index);
this.onFocus();
};
/**
* Selects the chip at `index`,
* @param index
*/
MdChipsCtrl.prototype.selectAndFocusChipSafe = function(index) {
if (!this.items.length) {
this.selectChip(-1);
this.onFocus();
return;
}
if (index === this.items.length) return this.onFocus();
index = Math.max(index, 0);
index = Math.min(index, this.items.length - 1);
this.selectChip(index);
this.focusChip(index);
};
/**
* Marks the chip at the given index as selected.
* @param index
*/
MdChipsCtrl.prototype.selectChip = function(index) {
if (index >= -1 && index <= this.items.length) {
this.selectedChip = index;
// Fire the onSelect if provided
if (this.useOnSelect && this.onSelect) {
this.onSelect({'$chip': this.items[this.selectedChip] });
}
} else {
this.$log.warn('Selected Chip index out of bounds; ignoring.');
}
};
/**
* Selects the chip at `index` and gives it focus.
* @param index
*/
MdChipsCtrl.prototype.selectAndFocusChip = function(index) {
this.selectChip(index);
if (index != -1) {
this.focusChip(index);
}
};
/**
* Call `focus()` on the chip at `index`
*/
MdChipsCtrl.prototype.focusChip = function(index) {
this.$element[0].querySelector('md-chip[index="' + index + '"] ._md-chip-content').focus();
};
/**
* Configures the required interactions with the ngModel Controller.
* Specifically, set {@code this.items} to the {@code NgModelCtrl#$viewVale}.
* @param ngModelCtrl
*/
MdChipsCtrl.prototype.configureNgModel = function(ngModelCtrl) {
this.ngModelCtrl = ngModelCtrl;
var self = this;
ngModelCtrl.$render = function() {
// model is updated. do something.
self.items = self.ngModelCtrl.$viewValue;
};
};
MdChipsCtrl.prototype.onFocus = function () {
var input = this.$element[0].querySelector('input');
input && input.focus();
this.resetSelectedChip();
};
MdChipsCtrl.prototype.onInputFocus = function () {
this.inputHasFocus = true;
this.resetSelectedChip();
};
MdChipsCtrl.prototype.onInputBlur = function () {
this.inputHasFocus = false;
};
/**
* Configure event bindings on a user-provided input element.
* @param inputElement
*/
MdChipsCtrl.prototype.configureUserInput = function(inputElement) {
this.userInputElement = inputElement;
// Find the NgModelCtrl for the input element
var ngModelCtrl = inputElement.controller('ngModel');
// `.controller` will look in the parent as well.
if (ngModelCtrl != this.ngModelCtrl) {
this.userInputNgModelCtrl = ngModelCtrl;
}
var scope = this.$scope;
var ctrl = this;
// Run all of the events using evalAsync because a focus may fire a blur in the same digest loop
var scopeApplyFn = function(event, fn) {
scope.$evalAsync(angular.bind(ctrl, fn, event));
};
// Bind to keydown and focus events of input
inputElement
.attr({ tabindex: 0 })
.on('keydown', function(event) { scopeApplyFn(event, ctrl.inputKeydown) })
.on('focus', function(event) { scopeApplyFn(event, ctrl.onInputFocus) })
.on('blur', function(event) { scopeApplyFn(event, ctrl.onInputBlur) })
};
MdChipsCtrl.prototype.configureAutocomplete = function(ctrl) {
if ( ctrl ) {
this.hasAutocomplete = true;
ctrl.registerSelectedItemWatcher(angular.bind(this, function (item) {
if (item) {
// Only append the chip and reset the chip buffer if the max chips limit isn't reached.
if (this.hasMaxChipsReached()) return;
this.appendChip(item);
this.resetChipBuffer();
}
}));
this.$element.find('input')
.on('focus',angular.bind(this, this.onInputFocus) )
.on('blur', angular.bind(this, this.onInputBlur) );
}
};
MdChipsCtrl.prototype.hasFocus = function () {
return this.inputHasFocus || this.selectedChip >= 0;
};
})(); // chips: .controller('MdChipsCtrl', MdChipsCtrl);
(function(){
"use strict";
angular
.module('material.components.chips')
.directive('mdChips', MdChips);
var MD_CHIPS_TEMPLATE = '\
<md-chips-wrap\
ng-keydown="$mdChipsCtrl.chipKeydown($event)"\
ng-class="{ \'md-focused\': $mdChipsCtrl.hasFocus(), \'md-readonly\': !$mdChipsCtrl.ngModelCtrl || $mdChipsCtrl.readonly}"\
class="md-chips">\
<md-chip ng-repeat="$chip in $mdChipsCtrl.items"\
index="{{$index}}"\
ng-class="{\'md-focused\': $mdChipsCtrl.selectedChip == $index, \'md-readonly\': !$mdChipsCtrl.ngModelCtrl || $mdChipsCtrl.readonly}">\
<div class="_md-chip-content"\
tabindex="-1"\
title="{{ $chip }}"\
aria-hidden="true"\
ng-click="!$mdChipsCtrl.readonly && $mdChipsCtrl.focusChip($index)"\
ng-focus="!$mdChipsCtrl.readonly && $mdChipsCtrl.selectChip($index)"\
md-chip-transclude="$mdChipsCtrl.chipContentsTemplate"></div>\
<div ng-if="!$mdChipsCtrl.readonly"\
class="_md-chip-remove-container"\
md-chip-transclude="$mdChipsCtrl.chipRemoveTemplate"></div>\
</md-chip>\
<div class="_md-chip-input-container">\
<div ng-if="!$mdChipsCtrl.readonly && $mdChipsCtrl.ngModelCtrl"\
md-chip-transclude="$mdChipsCtrl.chipInputTemplate"></div>\
</div>\
</md-chips-wrap>';
var CHIP_INPUT_TEMPLATE = '\
<input\
class="md-input"\
tabindex="0"\
placeholder="{{$mdChipsCtrl.getPlaceholder()}}"\
aria-label="{{$mdChipsCtrl.getPlaceholder()}}"\
ng-model="$mdChipsCtrl.chipBuffer"\
ng-focus="$mdChipsCtrl.onInputFocus()"\
ng-blur="$mdChipsCtrl.onInputBlur()"\
ng-trim="false"\
ng-keydown="$mdChipsCtrl.inputKeydown($event)">';
var CHIP_DEFAULT_TEMPLATE = '\
<span>{{$chip}}</span>';
var CHIP_REMOVE_TEMPLATE = '\
<button\
class="_md-chip-remove"\
ng-if="!$mdChipsCtrl.readonly"\
ng-click="$mdChipsCtrl.removeChipAndFocusInput($$replacedScope.$index)"\
type="button"\
aria-hidden="true"\
tabindex="-1">\
<span class=""><i class="fa fa-times"></i></span>\
<span class="_md-visually-hidden">\
{{$mdChipsCtrl.deleteButtonLabel}}\
</span>\
</button>';
/**
* MDChips Directive Definition
*/
function MdChips ($mdTheming, $mdUtil, $compile, $log, $timeout) {
// Run our templates through $mdUtil.processTemplate() to allow custom start/end symbols
var templates = getTemplates();
return {
template: function(element, attrs) {
// Clone the element into an attribute. By prepending the attribute
// name with '$', Angular won't write it into the DOM. The cloned
// element propagates to the link function via the attrs argument,
// where various contained-elements can be consumed.
attrs['$mdUserTemplate'] = element.clone();
return templates.chips;
},
require: ['mdChips'],
restrict: 'E',
controller: 'MdChipsCtrl',
controllerAs: '$mdChipsCtrl',
bindToController: true,
compile: compile,
scope: {
readonly: '=readonly',
placeholder: '@',
mdEnableChipEdit: '@',
secondaryPlaceholder: '@',
maxChips: '@mdMaxChips',
transformChip: '&mdTransformChip',
onAppend: '&mdOnAppend',
onAdd: '&mdOnAdd',
onRemove: '&mdOnRemove',
onSelect: '&mdOnSelect',
deleteHint: '@',
deleteButtonLabel: '@',
separatorKeys: '=?mdSeparatorKeys',
requireMatch: '=?mdRequireMatch'
}
};
/**
* Builds the final template for `md-chips` and returns the postLink function.
*
* Building the template involves 3 key components:
* static chips
* chip template
* input control
*
* If no `ng-model` is provided, only the static chip work needs to be done.
*
* If no user-passed `md-chip-template` exists, the default template is used. This resulting
* template is appended to the chip content element.
*
* The remove button may be overridden by passing an element with an md-chip-remove attribute.
*
* If an `input` or `md-autocomplete` element is provided by the caller, it is set aside for
* transclusion later. The transclusion happens in `postLink` as the parent scope is required.
* If no user input is provided, a default one is appended to the input container node in the
* template.
*
* Static Chips (i.e. `md-chip` elements passed from the caller) are gathered and set aside for
* transclusion in the `postLink` function.
*
*
* @param element
* @param attr
* @returns {Function}
*/
function compile(element, attr) {
// Grab the user template from attr and reset the attribute to null.
var userTemplate = attr['$mdUserTemplate'];
attr['$mdUserTemplate'] = null;
var chipTemplate = getTemplateByQuery('md-chips>md-chip-template');
var chipRemoveSelector = $mdUtil
.prefixer()
.buildList('md-chip-remove')
.map(function(attr) {
return 'md-chips>*[' + attr + ']';
})
.join(',');
// Set the chip remove, chip contents and chip input templates. The link function will put
// them on the scope for transclusion later.
var chipRemoveTemplate = getTemplateByQuery(chipRemoveSelector) || templates.remove,
chipContentsTemplate = chipTemplate || templates.default,
chipInputTemplate = getTemplateByQuery('md-chips>md-autocomplete')
|| getTemplateByQuery('md-chips>input')
|| templates.input,
staticChips = userTemplate.find('md-chip');
// Warn of malformed template. See #2545
if (userTemplate[0].querySelector('md-chip-template>*[md-chip-remove]')) {
$log.warn('invalid placement of md-chip-remove within md-chip-template.');
}
function getTemplateByQuery (query) {
if (!attr.ngModel) return;
var element = userTemplate[0].querySelector(query);
return element && element.outerHTML;
}
/**
* Configures controller and transcludes.
*/
return function postLink(scope, element, attrs, controllers) {
$mdUtil.initOptionalProperties(scope, attr);
$mdTheming(element);
var mdChipsCtrl = controllers[0];
if(chipTemplate) {
// Chip editing functionality assumes we are using the default chip template.
mdChipsCtrl.enableChipEdit = false;
}
mdChipsCtrl.chipContentsTemplate = chipContentsTemplate;
mdChipsCtrl.chipRemoveTemplate = chipRemoveTemplate;
mdChipsCtrl.chipInputTemplate = chipInputTemplate;
element
.attr({ 'aria-hidden': true, tabindex: -1 })
.on('focus', function () { mdChipsCtrl.onFocus(); });
if (attr.ngModel) {
mdChipsCtrl.configureNgModel(element.controller('ngModel'));
// If an `md-transform-chip` attribute was set, tell the controller to use the expression
// before appending chips.
if (attrs.mdTransformChip) mdChipsCtrl.useTransformChipExpression();
// If an `md-on-append` attribute was set, tell the controller to use the expression
// when appending chips.
//
// DEPRECATED: Will remove in official 1.0 release
if (attrs.mdOnAppend) mdChipsCtrl.useOnAppendExpression();
// If an `md-on-add` attribute was set, tell the controller to use the expression
// when adding chips.
if (attrs.mdOnAdd) mdChipsCtrl.useOnAddExpression();
// If an `md-on-remove` attribute was set, tell the controller to use the expression
// when removing chips.
if (attrs.mdOnRemove) mdChipsCtrl.useOnRemoveExpression();
// If an `md-on-select` attribute was set, tell the controller to use the expression
// when selecting chips.
if (attrs.mdOnSelect) mdChipsCtrl.useOnSelectExpression();
// The md-autocomplete and input elements won't be compiled until after this directive
// is complete (due to their nested nature). Wait a tick before looking for them to
// configure the controller.
if (chipInputTemplate != templates.input) {
// The autocomplete will not appear until the readonly attribute is not true (i.e.
// false or undefined), so we have to watch the readonly and then on the next tick
// after the chip transclusion has run, we can configure the autocomplete and user
// input.
scope.$watch('$mdChipsCtrl.readonly', function(readonly) {
if (!readonly) {
$mdUtil.nextTick(function(){
if (chipInputTemplate.indexOf('<md-autocomplete') === 0)
mdChipsCtrl
.configureAutocomplete(element.find('md-autocomplete')
.controller('mdAutocomplete'));
mdChipsCtrl.configureUserInput(element.find('input'));
});
}
});
}
// At the next tick, if we find an input, make sure it has the md-input class
$mdUtil.nextTick(function() {
var input = element.find('input');
input && input.toggleClass('md-input', true);
});
}
// Compile with the parent's scope and prepend any static chips to the wrapper.
if (staticChips.length > 0) {
var compiledStaticChips = $compile(staticChips.clone())(scope.$parent);
$timeout(function() { element.find('md-chips-wrap').prepend(compiledStaticChips); });
}
};
}
function getTemplates() {
return {
chips: $mdUtil.processTemplate(MD_CHIPS_TEMPLATE),
input: $mdUtil.processTemplate(CHIP_INPUT_TEMPLATE),
default: $mdUtil.processTemplate(CHIP_DEFAULT_TEMPLATE),
remove: $mdUtil.processTemplate(CHIP_REMOVE_TEMPLATE)
};
}
}
MdChips.$inject = ["$mdTheming", "$mdUtil", "$compile", "$log", "$timeout"];
})(); // chips: .directive('mdChips', MdChips);
(function(){
angular.module("material.core").constant("$MD_THEME_CSS", `html.md-THEME_NAME-theme, body.md-THEME_NAME-theme {
color: '{{foreground-1}}';
background-color: '{{background-color}}';
}
md-autocomplete.md-THEME_NAME-theme {
background: '{{background-A100}}';
}
md-autocomplete.md-THEME_NAME-theme[disabled]:not([md-floating-label]) {
background: '{{background-100}}';
}
md-autocomplete.md-THEME_NAME-theme button:after {
background: '{{background-600-0.3}}';
}
.md-autocomplete-suggestions-container.md-THEME_NAME-theme {
background: '{{background-A100}}';
}
.md-autocomplete-suggestions-container.md-THEME_NAME-theme li {
color: '{{background-900}}';
}
.md-autocomplete-suggestions-container.md-THEME_NAME-theme li .highlight {
color: '{{background-600}}';
}
.md-autocomplete-suggestions-container.md-THEME_NAME-theme li:hover, .md-autocomplete-suggestions-container.md-THEME_NAME-theme li.selected {
background: '{{background-200}}';
}
md-backdrop {
background-color: '{{background-900-0.0}}';
}
md-backdrop.md-opaque.md-THEME_NAME-theme {
background-color: '{{background-900-1.0}}';
}
md-chips.md-THEME_NAME-theme .md-chips {
box-shadow: 0 1px '{{foreground-4}}';
}
md-chips.md-THEME_NAME-theme .md-chips.md-focused {
box-shadow: 0 2px '{{primary-color}}';
}
md-chips.md-THEME_NAME-theme .md-chips ._md-chip-input-container input {
color: '{{foreground-1}}';
}
md-chips.md-THEME_NAME-theme .md-chips ._md-chip-input-container input::-webkit-input-placeholder {
color: '{{foreground-3}}';
}
md-chips.md-THEME_NAME-theme .md-chips ._md-chip-input-container input:-moz-placeholder {
color: '{{foreground-3}}';
}
md-chips.md-THEME_NAME-theme .md-chips ._md-chip-input-container input::-moz-placeholder {
color: '{{foreground-3}}';
}
md-chips.md-THEME_NAME-theme .md-chips ._md-chip-input-container input:-ms-input-placeholder {
color: '{{foreground-3}}';
}
md-chips.md-THEME_NAME-theme .md-chips ._md-chip-input-container input::-webkit-input-placeholder {
color: '{{foreground-3}}';
}
md-chips.md-THEME_NAME-theme md-chip {
background: '{{background-300}}';
color: '{{background-800}}';
}
md-chips.md-THEME_NAME-theme md-chip.md-focused {
background: '{{primary-color}}';
color: '{{primary-contrast}}';
}
md-chips.md-THEME_NAME-theme md-chip._md-chip-editing {
background: transparent;
color: '{{background-800}}';
}
md-content.md-THEME_NAME-theme {
color: '{{foreground-1}}';
background-color: '{{background-default}}';
}
`);
})(); // core: constant('...');
})(window, window.angular);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment