-
start
-
start
+
+
+
+ The layout-align directive takes two words.
+ The first word says how the children will be aligned in the layout's direction, and the second word says how the children will be aligned perpendicular to the layout's direction.
+
+
Only one value is required for this directive.
+ For example, layout="row" layout-align="center" would make the elements
+ center horizontally and use the default behavior vertically.
+
+
layout="column" layout-align="center end" would make
+ children align along the center vertically and along the end (right) horizontally.
+
+
+
+
+
+ | API |
+ Sets child alignments within the layout container |
+
+
+
+ | layout-align |
+ Sets default alignment unless overridden by another breakpoint. |
+
+
+ | layout-align-xs |
+ width < 600px |
+
+
+ | layout-align-gt-xs |
+ width >= 600px |
+
+
+ | layout-align-sm |
+ 600px <= width < 960px |
+
+
+ | layout-align-gt-sm |
+ width >= 960px |
+
+
+ | layout-align-md |
+ 960px <= width < 1280px |
+
+
+ | layout-align-gt-md |
+ width >= 1280px |
+
+
+ | layout-align-lg |
+ 1280px <= width < 1920px |
+
+
+ | layout-align-gt-lg |
+ width >= 1920px |
+
+
+ | layout-align-xl |
+ width >= 1920px |
+
+
+
+
+
+
+ Below is an interactive demo that lets you explore the visual results of the different settings:
+
+
+
+
+
+
+
+ Layout Direction
+
+ row
+ column
+
+
+
+ Alignment in Layout Direction ({{layoutDemo.direction == 'row' ? 'horizontal' : 'vertical'}})
+
+ none
+ start (default)
+ center
+ end
+ space-around
+ space-between
+
+
+
+ Alignment in Perpendicular Direction ({{layoutDemo.direction == 'column' ? 'horizontal' : 'vertical'}})
+
+ none
+ start
+ center
+ end
+ stretch (default)
+
-
-
-
-
-
Main Axis: End
-
- The layout-align="end" attribute will position the cell to the right for a
- a horizontal layout, and at the bottom for a vertical layout. (Stretches the cross axis by default)
-
-
-
-
-
-
Main Axis: Space Around
-
- The layout-align="space-around" attribute will evenly distribute
- the cells and the space around them. (Stretches the cross axis by default)
-
-
-
-
-
-
-
Main Axis: Space Between
-
- The layout-align="space-between" attribute will evenly distribute
- the cells and the free space between them, with the first cell is aligned
- at the start, and the last cell aligned at the end. (Stretches the cross axis by default)
-
-
-
-
-
-
-
Main Axis: Flex Cells
-
- This demo shows how a layout's child cells react when the flex attributes
- are added to the cells. Here the free space is evenly divided up within the two
- cells, giving each cell 50% of the area. Note how setting an align attribute wouldn't matter
- because there is no free space between the cells for the cells to be aligned to. Since
- each flex cell is using up all of the free space, there's no arranging that could happen.
-
-
-
-
-
-
-
Cross Axis
-
- The "cross axis" is the imaginary line perpendicular to the "main axis", which in each
- demo is the center black line. The value within the value of the layout-align
- attribute is always the main axis, and by default the cross axis is always "stretch", as seen
- in the demos above. However, to also explicitly set the cross axis by providing
- both the main and cross axis values, which are separated by a space.
-
-
-
-
-
Centered Horizontal And Vertical
-
- Is it possible to center a layout's child cells by both the main and cross axes?
- Yes. Yes it is.
-
-
-
-
center center
-
center center
-
-
-
-
-
center center
-
center center
-
-
-
-
-
-
Main Axis Start, Cross Axis Center
-
- The layout-align="start center" attribute packs up all of the cells at the "start" of the main axis,
- and in the "center" of the cross axis. Note that each demo's center black line represents the
- "main axis", which makes the imaginary perpendicular line the "cross axis".
-
-
-
-
start center
-
start center
-
-
-
-
-
start center
-
start center
-
-
-
-
-
-
Main Axis End, Cross Axis Center
-
- The layout-align="end center" attribute packs up all of the cells at the
- "end" of the main axis, and in the "center" of the cross axis.
-
-
-
-
end center
-
end center
-
-
-
-
-
end center
-
end center
-
-
-
-
-
-
Main Axis Center, Cross Axis Start
-
- The layout-align="center start" attribute packs up all of the cells at the
- "center" of the main axis, and at the "start" of the cross axis.
-
-
-
-
center start
-
center start
-
-
-
-
-
center start
-
center start
-
-
-
-
-
-
Main Axis Start, Cross Axis Start
-
- The layout-align="start start" attribute packs up all of the cells at the
- "start" of the main axis, and at the "start" of the cross axis.
-
-
-
-
start start
-
start start
-
-
-
-
-
start start
-
start start
-
-
-
-
-
-
Main Axis End, Cross Axis Start
-
- The layout-align="end start" attribute packs up all of the cells at the
- "end" of the main axis, and at the "start" of the cross axis.
-
-
-
-
end start
-
end start
-
-
-
-
-
end start
-
end start
-
-
-
-
-
-
Main Axis Center, Cross Axis End
-
- The layout-align="center end" attribute packs up all of the cells at the
- "center" of the main axis, and at the "end" of the cross axis.
-
-
-
-
center end
-
center end
-
-
-
-
-
center end
-
center end
-
-
-
-
-
-
Main Axis Start, Cross Axis End
-
- The layout-align="start end" attribute packs up all of the cells at the
- "start" of the main axis, and at the "end" of the cross axis.
-
-
-
-
start end
-
start end
-
-
-
-
-
start end
-
start end
-
-
-
-
-
-
Main Axis End, Cross Axis End
-
- The layout-align="end end" attribute packs up all of the cells at the
- "end" of the main axis, and at the "end" of the cross axis.
-
-
-
-
-
diff --git a/docs/app/partials/layout-children.tmpl.html b/docs/app/partials/layout-children.tmpl.html
new file mode 100644
index 0000000000..5fb3ad8870
--- /dev/null
+++ b/docs/app/partials/layout-children.tmpl.html
@@ -0,0 +1,332 @@
+
+
+
Children within a Layout Container
+
+
+ To customize the size and position of elements in a layout container, use the
+ flex, flex-order, and flex-offset attributes on the container's child elements:
+
+
+
+
+
+
+ [flex="20"]
+
+
+ [flex="70"]
+
+
+ [flex]
+
+
+
+
+
+
+ Add the flex directive to a layout's child element and the element will flex (grow or shrink) to fit
+ the area unused by other elements. flex defines how the element will adjust its size with respect to its
+ parent container and the other elements within the container.
+
+
+
+
+
+
+ [flex="30"]
+
+
+ [flex="45"]
+
+
+ [flex="25"]
+
+
+ [flex="33"]
+
+
+ [flex="66"]
+
+
+ [flex="50"]
+
+
+ [flex]
+
+
+
+
+
+
+
+ A layout child's flex directive can be given an integer value from 0-100.
+ The element will stretch to the percentage of available space matching the value. Currently, the flex
+ directive value is restricted to multiples of five, 33 or 66.
+
+
+
For example: flex="5", flex="20", flex="33", flex="50", flex="66", flex="75", ... flex="100".
+
+
+
+
+
+ flex 33% on mobile,
and 66% on gt-sm devices.
+
+
+ flex 66% on mobile,
and 33% on gt-sm devices.
+
+
+
+
+
+
+
+ See the layout options page for more information on responsive flex directives like
+ hide-sm and layout-wrap used in the above examples.
+
+
+
+
+
+
+
Additional Flex Values
+
+
+ There are additional flex values provided by Angular Material to improve flexibility and to make the API
+ easier to understand.
+
+
+
+
+
+
+ [flex="none"]
+
+
+ [flex]
+
+
+ [flex="nogrow"]
+
+
+ [flex="grow"]
+
+
+ [flex="initial"]
+
+
+ [flex="auto"]
+
+
+ [flex="noshrink"]
+
+
+ [flex="0"]
+
+
+
+
+
+
+
+
+ | flex |
+ Will grow and shrink as needed. Starts with a size of 0%. Same as flex="0". |
+
+
+ | flex="none" |
+ Will not grow or shrink. Sized based on its width and height values. |
+
+
+ | flex="initial" |
+ Will shrink as needed. Starts with a size based on its width and height values. |
+
+
+ | flex="auto" |
+ Will grow and shrink as needed. Starts with a size based on its width and height values. |
+
+
+ | flex="grow" |
+ Will grow and shrink as needed. Starts with a size of 100%. Same as flex="100". |
+
+
+ | flex="nogrow" |
+ Will shrink as needed, but won't grow. Starts with a size based on its width and height values. |
+
+
+ | flex="noshrink" |
+ Will grow as needed, but won't shrink. Starts with a size based on its width and height values. |
+
+
+
+
+
+
+
+
+
Ordering HTML Elements
+
+
+ Add the flex-order directive to a layout child to set its
+ order position within the layout container. Any integer value from -20 to 20 is accepted.
+
+
+
+
+
+
+
+
[flex-order="1"]
+
[flex-order-gt-md="3"]
+
+
+
+
[flex-order="3"]
+
[flex-order-gt-md="1"]
+
+
+
+
+
+
+
+
+ | API |
+ Device width when breakpoint overrides default |
+
+
+
+ | flex-order |
+ Sets default layout order unless overridden by another breakpoint. |
+
+
+ | flex-order-xs |
+ width < 600px |
+
+
+ | flex-order-gt-xs |
+ width >= 600px |
+
+
+ | flex-order-sm |
+ 600px <= width < 960px |
+
+
+ | flex-order-gt-sm |
+ width >= 960px |
+
+
+ | flex-order-md |
+ 960px <= width < 1280px |
+
+
+ | flex-order-gt-md |
+ width >= 1280px |
+
+
+ | flex-order-lg |
+ 1280px <= width < 1920px |
+
+
+ | flex-order-gt-lg |
+ width >= 1920px |
+
+
+ | flex-order-xl |
+ width >= 1920px |
+
+
+
+
+
+ See the layout options page for more information on directives like
+ hide, hide-gt-md, and show-gt-md used in the above examples.
+
+
+
+
+
+
+
Add Offsets to the Preceding HTML Elements
+
+
+ Add the flex-offset directive to a layout child to set its
+ offset percentage within the layout container. Values must be multiples
+ of 5 or 33 / 66. These offsets establish a margin-left
+ with respect to the preceding element or the containers left boundary.
+
+
+
+ When using flex-offset the margin-left offset is applied... regardless of your choice of flex-order.
+ or if you use a flex-direction: reverse.
+
+
+
+
+
+
+ [flex-offset="15"]
+ [flex="66"]
+
+
+ [flex]
+
+
+
+
+
+
+
+
+ | API |
+ Device width when breakpoint overrides default |
+
+
+
+ | flex-offset |
+ Sets default margin-left offset (%-based) unless overridden by another breakpoint. |
+
+
+ | flex-offset-xs |
+ width < 600px |
+
+
+ | flex-offset-gt-xs |
+ width >= 600px |
+
+
+ | flex-offset-sm |
+ 600px <= width < 960px |
+
+
+ | flex-offset-gt-sm |
+ width >= 960px |
+
+
+ | flex-offset-md |
+ 960px <= width < 1280px |
+
+
+ | flex-offset-gt-md |
+ width >= 1280px |
+
+
+ | flex-offset-lg |
+ 1280px <= width < 1920px |
+
+
+ | flex-offset-gt-lg |
+ width >= 1920px |
+
+
+ | flex-offset-xl |
+ width >= 1920px |
+
+
+
+
+
diff --git a/docs/app/partials/layout-container.tmpl.html b/docs/app/partials/layout-container.tmpl.html
index 86d381e1e3..3a2f35b9f4 100644
--- a/docs/app/partials/layout-container.tmpl.html
+++ b/docs/app/partials/layout-container.tmpl.html
@@ -1,79 +1,145 @@
-
+
+
+
Layout and Containers
-
Overview
- Angular Md uses a responsive CSS layout based on
- flexbox, rather than
- using CSS floats. Traditionally, floats proved to be the best solution for CSS
- layouts, but with flexbox now available in a majority of browsers,
- we're moving onto a new era with improved layout capabilities and features.
+ Use the layout directive on a container element to specify the layout direction for its children:
+ horizontally in a row (layout="row") or vertically in a column (layout="column").
+ Note that row is the default layout direction if you specify the layout directive without a value.
+
+
+ | row |
+ Items arranged horizontally. max-height = 100% and max-width is the width of the items in the container. |
+
+
+ | column |
+ Items arranged vertically. max-width = 100% and max-height is the height of the items in the container. |
+
+
+
+
+
+
+
+
+
First item in row
+
Second item in row
+
+
+
First item in column
+
Second item in column
+
+
+
+
+
+ Note that layout only affects the flow direction for that container's immediate children.
+
+
+
+
+
+
Layouts and Responsive Breakpoints
+
+
+ As discussed in the Layout Introduction page you can
+ make your layout change depending upon the device view size by using breakpoint alias suffixes.
+
+
- The layout system uses element attributes, rather than CSS classes.
- By doing so we're able to simplify creating layouts and make it easier to quickly generate
- the structure required at various viewport sizes. The attribute system allows
- assigning values following standard HTML conventions, which makes it easier to read and
- understand compared to long strung together CSS classes. Additionally, the layout attributes
- help to separate custom app styles from the core structure, which further simplifies code.
+ To make your layout automatically change depending upon the device screen size, use one to the following layout
+ APIs to set the layout direction for devices with view widths:
-
-
Layout Attribute
+
+
+
+ | API |
+ Device width when breakpoint overrides default |
+
+
+
+ | layout |
+ Sets default layout direction unless overridden by another breakpoint. |
+
+
+ | layout-xs |
+ width < 600px |
+
+
+ | layout-gt-xs |
+ width >= 600px |
+
+
+ | layout-sm |
+ 600px <= width < 960px |
+
+
+ | layout-gt-sm |
+ width >= 960px |
+
+
+ | layout-md |
+ 960px <= width < 1280px |
+
+
+ | layout-gt-md |
+ width >= 1280px |
+
+
+ | layout-lg |
+ 1280px <= width < 1920px |
+
+
+ | layout-gt-lg |
+ width >= 1920px |
+
+
+ | layout-xl |
+ width >= 1920px |
+
+
+
+
+
+
+
+
+
- Use the layout attribute on an element to arrange its children
- horizontally in a row (layout="horizontal"), or vertically in
- a column (layout="vertical"). The layout attribute's value
- describes what the "main axis" is for its children.
+ For the demo below, as you shrink your browser window width notice the flow direction changes to column
+ for mobile devices (xs). And as you expand it will reset to row
+ for gt-sm view sizes.
+
-
-
-
I'm left.
-
I'm right.
-
+
+
+
+
+ I'm above on mobile, and to the left on larger devices.
+
+
+ I'm below on mobile, and to the right on larger devices.
+
+
+
+
-
-
I'm above.
-
I'm below.
-
-
-
-
Responsive Layout
- From the ground up the layout system is built around being responsive to viewport size.
- Out of the box it comes with useful and customizable breakpoints, which can be easily reused.
- The layout attribute used with "horizontal" or "vertical" values define arrangement on all device sizes.
- If you wish to define a layout depending upon the device size, there are
- other layout attributes available:
+ See the Layout Options page for more options such as padding, alignments, etc.
-
- -
-
layout sets the default layout on all devices.
-
- -
-
layout-sm sets the layout on device >=600px wide.
-
- -
-
layout-md sets the layout on devices >=960px wide.
-
- -
-
layout-lg sets the layout on devices >=1200px wide.
-
-
-
-
-
-
- I'm above on tiny devices, and to the left on devices wider than 600px.
-
-
- I'm below on tiny devices, and to the right on devices wider than 600px.
-
-
-
-
-
+
+
+
+
diff --git a/docs/app/partials/layout-grid.tmpl.html b/docs/app/partials/layout-grid.tmpl.html
deleted file mode 100644
index 2ee5588441..0000000000
--- a/docs/app/partials/layout-grid.tmpl.html
+++ /dev/null
@@ -1,104 +0,0 @@
-
-
-
- The grid system is different than most because of its use of the
- CSS Flexible Box Layout Module standard. With flexbox
- you have the power to simply add in the columns and rows required, and they'll evenly take up the
- available space of their parent layout container. There's no restriction to a 12 column grid, or
- having to explicitly state how large each column should be.
-
-
-
-
-
Flex Attribute
-
- Child elements of a layout container with the flex
- attribute will "flex", or stretch, to fill the available area within its parent layout container.
- If a design requires three evenly spaced elements, simply add three elements with a
- flex attribute. Elements without the flex attribute will take up their natural size,
- whereas elements with the flex attribute will evenly fill in the available area.
-
-
-
-
-
-
-
-
Layout Padding
-
- This layout has padding, or gutters, between each cell by adding the
- layout-padding attribute. The previous examples added in their own
- margins/padding manually, whereas the layout-padding attribute spaces it all out
- evenly.
-
-
-
-
-
-
-
-
-
Layout Padding with Available Space Percents
-
- Flex attributes can be given integers to represent the percentage of the available space
- it should take up. Note that elements which are not given a percentage still
- fill up the available area. Adjust the window size to see
- the demo's responsive layout attributes at work.
-
-
-
-
-
-
-
-
-
Layout Padding with Offsets
-
- Elements can be offset from the left, but still allow the gutters to line up correctly.
- Adjust the window size to see the demo's responsive layout attributes at work.
-
-
-
-
-
-
-
-
-
Layout Order
-
- Child elements of a layout container can have their order changed through the use of the
- layout-order attribute, and respective responsive
- attributes such as: layout-order-sm, layout-order-md, and layout-order-lg. Reordering elements
- is useful when the placement of a cell should be changed depending on the viewport size.
-
-
- In the demo, notice how the default size (the smallest) has each of the elements in their
- natural order determined by its markup. However, as the viewport changes size then each
- individual cell can change their order using the layout-order attribute.
-
-
-
-
-
-
-
-
-
Header, Menu, Main, Footer, no padding
-
- This layout lets each of the header and footer tracks height be the size of their
- respective content, and the middle track (menu and main) flex vertically to fill the
- space of the entire layout. This is also an example of nested layouts, where the entire
- body is a vertical layout (header/content/footer), and the content row is its own layout
- (menu/main), and each layout has their own settings.
-
-
- The body element has the layout="vertical" attribute.
- By default the middle track has the menu and main elements stacked vertically. However,
- the elements line up horizontally starting at the "small" size, which is determined by the
- media query.
-
-
-
-
-
-
diff --git a/docs/app/partials/layout-introduction.tmpl.html b/docs/app/partials/layout-introduction.tmpl.html
new file mode 100644
index 0000000000..046e3e3b0a
--- /dev/null
+++ b/docs/app/partials/layout-introduction.tmpl.html
@@ -0,0 +1,367 @@
+
+
+
Overview
+
+ Angular Material's Layout features provide sugar to enable developers to more easily create modern,
+ responsive layouts on top of CSS3 flexbox.
+ The layout API consists of a set of Angular directives that can
+ be applied to any of your application's HTML content.
+
+
+
+
+ Using HTML Directives as the API provides an easy way to set a value (eg. layout="row") and
+ helps with separation of concerns: Attributes define layout while CSS classes assign styling.
+
+
+
+
+
+
+ | HTML Markup API |
+ Allowed values (raw or interpolated) |
+
+
+
+
+ | layout |
+ row | column |
+
+
+ | flex |
+ integer (increments of 5 for 0%->100%) |
+
+
+ | flex-order |
+ integer values from -20 to 20 |
+
+
+ | flex-offset |
+ integer (increments of 5 for 0%->100%, 100%/3, 200%/3) |
+
+
+ | layout-align |
+ start|center|end|space-around|space-between start|center|end|stretch |
+
+
+ | layout-fill |
+ |
+
+
+ | layout-wrap |
+ |
+
+
+ | layout-nowrap |
+ |
+
+
+ | layout-margin |
+ |
+
+
+ | layout-padding |
+ |
+
+
+ | show |
+ |
+
+
+ | hide |
+ |
+
+
+
+
+
+
And if we use Breakpoints as specified in Material Design:
+
+
+
+
+
We can associate breakpoints with mediaQuery definitions using breakpoint alias(es):
+
+
+
+
+ | Breakpoint |
+ MediaQuery (pixel range) |
+
+
+
+
+ | 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)' |
+
+
+
+
+
+
+
+ API with Responsive Breakpoints
+
+
+
Now we can combine the breakpoint alias with the Layout API to easily support Responsive breakpoints
+ with our simple Layout markup convention. The alias is simply used as suffix extensions to the Layout
+ API keyword.
+
e.g.
+
+
+
+ This notation results in, for example, the following table for the layout and flex APIs:
+
+
+
+
+
+ | layout API |
+ flex API |
+ Activates when device |
+
+
+
+ | layout |
+ flex |
+ Sets default layout direction & flex unless overridden by another breakpoint. |
+
+
+ | layout-xs |
+ flex-xs |
+ width < 600px |
+
+
+ | layout-gt-xs |
+ flex-gt-xs |
+ width >= 600px |
+
+
+ | layout-sm |
+ flex-sm |
+ 600px <= width < 960px |
+
+
+ | layout-gt-sm |
+ flex-gt-sm |
+ width >= 960px |
+
+
+ | layout-md |
+ flex-md |
+ 960px <= width < 1280px |
+
+
+ | layout-gt-md |
+ flex-gt-md |
+ width >= 1280px |
+
+
+ | layout-lg |
+ flex-lg |
+ 1280px <= width < 1920px |
+
+
+ | layout-gt-lg |
+ flex-gt-lg |
+ width >= 1920/b>px |
+
+
+ | layout-xl |
+ flex-xl |
+ width >= 1920px |
+
+
+
+
Below is an example usage of the Responsive Layout API:
+
+
+
+
+
+
+
+
+ This Layout API means it is much easier to set up and maintain flexbox layouts vs. defining everything via CSS.
+ The developer uses the Layout HTML API to specify intention and the Layout engine handles all the issues of CSS flexbox styling.
+
+
+
+ The Layout engine will log console warnings when it encounters conflicts or known issues.
+
+
+
+
+
+
+
+
Under-the-Hood
+
+
+ Due to performance problems when using Attribute Selectors with Internet Explorer browsers; see the following for more details:
+
+
+
+ Layout directives dynamically generate class selectors at runtime. And the Layout CSS classNames and styles have each been
+ predefined in the angular-material.css stylesheet.
+
+
+
+ Developers should continue to use Layout directives in the HTML
+ markup as the classes may change between releases.
+
+
+
+ Let's see how this directive-to-className transformation works. Consider the simple use of the layout and flex directives (API):
+
+
+
+
+
+
+
+
First item in row
+
Second item in row
+
+
+
+
+
First item in column
+
Second item in column
+
+
+
+
+
+
+
+
+ At runtime, these attributes are transformed to CSS classes.
+
+
+
+
+
+
+
+
First item in row
+
Second item in row
+
+
+
+
+
First item in column
+
Second item in column
+
+
+
+
+
+
+
+ Using the style classes above defined in angular-material.css
+
+
+
+
+ .flex {
+ -webkit-flex: 1 1 0%;
+ -ms-flex: 1 1 0%;
+ flex: 1 1 0%;
+ box-sizing: border-box;
+ }
+ .flex-20 {
+ -webkit-flex: 1 1 20%;
+ -ms-flex: 1 1 20%;
+ flex: 1 1 20%;
+ max-width: 20%;
+ max-height: 100%;
+ box-sizing: border-box;
+ }
+
+ .layout-row .flex-33 {
+ -webkit-flex: 1 1 calc(100% / 3);
+ -ms-flex: 1 1 calc(100% / 3);
+ flex: 1 1 calc(100% / 3);
+ box-sizing: border-box;
+ }
+
+ .layout-row .flex-66 {
+ -webkit-flex: 1 1 calc(200% / 3);
+ -ms-flex: 1 1 calc(200% / 3);
+ flex: 1 1 calc(200% / 3);
+ box-sizing: border-box;
+ }
+
+
+ .layout-row .flex-33 {
+ max-width: calc(100% / 3);
+ max-height: 100%;
+ }
+
+ .layout-row .flex-66 {
+ max-width: calc(200% / 3);
+ max-height: 100%;
+ }
+
+ .layout-column .flex-33 {
+ max-width: 100%;
+ max-height: calc(100% / 3);
+ }
+
+ .layout-column .flex-66 {
+ max-width: 100%;
+ max-height: calc(200% / 3);
+ }
+
+
+
diff --git a/docs/app/partials/layout-options.tmpl.html b/docs/app/partials/layout-options.tmpl.html
index f844e693c9..d4c0466ded 100644
--- a/docs/app/partials/layout-options.tmpl.html
+++ b/docs/app/partials/layout-options.tmpl.html
@@ -1,111 +1,222 @@
-
-
-
-
Responsive Layout
+
+
+
+
+
+
+ I'm above on mobile, and to the left on larger devices.
+
+
+ I'm below on mobile, and to the right on larger devices.
+
+
+
+
- From the ground up the layout system is built around being responsive to viewport size.
- Out of the box it comes with useful and customizable breakpoints, which can be easily reused.
- The layout attribute used with "horizontal" or "vertical" values define arrangement on all device sizes.
- If you wish to define a layout depending upon the device size, there are
- other layout attributes available:
+ See the Container Elements page for a basic explanation
+ of layout directives.
+
+ To make your layout change depending upon the device screen size, there are
+ other layout directives available:
-
- -
-
layout sets the default layout on all devices.
-
- -
-
layout-sm sets the layout on device >=600px wide.
-
- -
-
layout-md sets the layout on devices >=960px wide.
-
- -
-
layout-lg sets the layout on devices >=1200px wide.
-
-
-
-
-
-
Layout Options
+
+
+
+ | API |
+ Activates when device |
+
+
+
+ | layout |
+ Sets default layout direction unless overridden by another breakpoint. |
+
+
+ | layout-xs |
+ width < 600px |
+
+
+ | layout-gt-xs |
+ width >= 600px |
+
+
+ | layout-sm |
+ 600px <= width < 960px |
+
+
+ | layout-gt-sm |
+ width >= 960px |
+
+
+ | layout-md |
+ 960px <= width < 1280px |
+
+
+ | layout-gt-md |
+ width >= 1280px |
+
+
+ | layout-lg |
+ 1280px <= width < 1920px |
+
+
+ | layout-gt-lg |
+ width >= 1920px |
+
+
+ | layout-xl |
+ width >= 1920px |
+
+
+
+
+
+
+
+
+
Layout Margin, Padding, Wrap and Fill
+
+
+
+
+
+
+
Parent layout and this element have margins.
+
+
+
Parent layout and this element have padding.
+
+
+
Parent layout is set to fill available space.
+
+
+
I am using all three at once.
+
+
+
- layout-padding adds padding between this layout's children with
- the flex attribute.
+ layout-margin adds margin around each flex child. It also adds a margin to the layout
+ container itself.
+
+ layout-padding adds padding inside each flex child. It also adds padding to the layout
+ container itself.
+
+ layout-fill forces the layout element to fill its parent container.
-
-
-
I'm on the left, and there's an empty area between.
-
I'm on the right, and there's an empty area between.
-
-
+
Here is a non-trivial group of flex elements using layout-wrap
+
+
+
+
+
[flex=33]
+
[flex=66]
+
[flex=66]
+
[flex=33]
+
[flex=33]
+
[flex=33]
+
[flex=33]
+
+
+
- layout-fill forces the layout element to fill its parent container.
+ layout-wrap allows flex children to wrap within the container if the elements use more
+ than 100%.
+
-
-
- This layout fills up 100% of its parent's width and height.
-
-
-
+
-
+
+
+
-
Utility Attributes
+
Show & Hide
-
-
- | block |
- display: block |
-
-
- | block-sm |
- display: block when the small media query is active. |
-
-
- | block-md |
- display: block when the medium media query is active. |
-
+ Use the show hide APIs to responsively show or hide elements. While these work similar
+ to ng-show and ng-hide, these Angular Material Layout directives are mediaQuery-aware.
+
+
+
+
+
+
+ Only show on gt-sm devices.
+
+
+ Shown on small and medium.
+ Hidden on gt-sm devices.
+
+
+ Shown on small and medium size devices.
+ Hidden on gt-md devices.
+
+
+ Shown on medium size devices only.
+
+
+ Shown on devices larger than 1200px wide only.
+
+
+
+
+
+
+
+
+ | hide (display: none) |
+ show (negates hide) |
+ Activates when: |
+
+
- | block-lg |
- display: block when the large media query is active. |
+ hide-xs |
+ show-xs |
+ width < 600px |
- | inline-block |
- display: inline-block |
+ hide-gt-xs |
+ show-gt-xs |
+ width >= 600px |
- | inline-block-sm |
- display: inline-block when the small media query is active. |
+ hide-sm |
+ show-sm |
+ 600px <= width < 960px |
- | inline-block-md |
- display: inline-block when the medium media query is active. |
+ hide-gt-sm |
+ show-gt-sm |
+ width >= 960px |
- | inline-block-lg |
- display: inline-block when the large media query is active. |
+ hide-md |
+ show-md |
+ 960px <= width < 1280px |
- | hide |
- display: none |
+ hide-gt-md |
+ show-gt-md |
+ width >= 1280px |
- | hide-sm |
- display: none when the small media query is active. |
+ hide-lg |
+ show-lg |
+ 1280px <= width < 1920px |
- | hide-md |
- display: none when the medium media query is active. |
+ hide-gt-lg |
+ show-gt-lg |
+ width >= 1920px |
- | hide-lg |
- display: none when the large media query is active. |
+ hide-xl |
+ show-xl |
+ width >= 1920px |
diff --git a/docs/app/partials/layout-tips.tmpl.html b/docs/app/partials/layout-tips.tmpl.html
new file mode 100644
index 0000000000..fa9736fd81
--- /dev/null
+++ b/docs/app/partials/layout-tips.tmpl.html
@@ -0,0 +1,234 @@
+
+
+
Overview
+
+
+ The Angular Material Layout system uses the current
+ Flexbox feature set. More importantly, it also
+ adds syntactic sugar to allow developers to easily and quickly add Responsive features to HTML
+ containers and elements.
+
+
+
+ As you use the Layout features, you may encounter scenarios where the layouts do not render as
+ expected; especially with IE 10 and 11 browsers. There may also be cases where you need to add
+ custom HTML, CSS and Javascript to achieve your desired results.
+
+
+
+
+
+
+
Resources
+
+
+ If you are experiencing an issue in a particular browser, we highly recommend using the
+ following resources for known issues and workarounds.
+
+
+
+
+
+
+
+
+
General Tips
+
+
+ Below, you will find solutions to some of the more common scenarios and problems that may arise
+ when using Material's Layout system. The following sections offer general guidelines and tips when using the flex and
+ layout directives within your own applications.
+
+
+
+ -
+ When building your application's Layout, it is usually best to start with a mobile version
+ that looks and works correctly, and then apply styling for larger devices using the
+
flex-gt-* or hide-gt-* attributes. This approach typically leads
+ to less frustration than starting big and attempting to fix issues on smaller devices.
+
+
+ -
+ Some elements like
<fieldset> and <button> do not always
+ work correctly with flex. Additionally, some of the Angular Material components provide their
+ own styles. If you are having difficulty with a specific element/component, but not
+ others, try applying the flex attributes to a parent or child <div> of the
+ element instead.
+
+
+ -
+ Some Flexbox properties such as
flex-direction cannot be animated.
+
+
+ -
+ Flexbox can behave differently on different browsers. You should test as many as possible on
+ a regular basis so that you can catch and fix layout issues more quickly.
+
+
+
+
+
+
+
Layout Column
+
+
+ In some scenarios layout="column" and breakpoints (xs, gt-xs, xs, gt-sm, etc.) may not work
+ as expected due to CSS specificity rules.
+
+
+
+
+
+ This is easily fixed simply by inverting the layout logic so that the default is layout='row'.
+ To see how the layout changes, shrink the browser window its width is <600px;
+
+
+
+
+
+
+
Layout Column and Container Heights
+
+
+ In Flexbox, some browsers will determine size of the flex containers based on the size of their
+ content. This is particularly noticable if you have a non-flex item (say a toolbar), followed by
+ two flex items in a column layout.
+
+
+
+
+
+
Toolbar
+
+
+
+ Flex with smaller content...
+
+ Line {{i}}
+
+
+
+
+ Flex with larger content...
+
+
+ Line {{i}}
+
+
+
+
+
+
+
+
+
+ Notice how in Chrome the second scrollable area is nearly twice as tall as the first. This is
+ because we are using nested flex items and the contents of the second
+ <md-content> are twice as large as the first. Try clicking the button below
+ to toggle the light blue box; this will make the containers the same size.
+
+
+
+
+ {{tips.toggleButtonText}} Blue Box
+
+
+
+
+ In order to fix this, we must specify the height of the outer flex item. The easiest way to
+ achieve this is to simply set the height to 100%. When paired with the
+ flex attribute, this achieves the desired result.
+
+
+
+
+ Note: When height: 100% is used without the flex
+ attribute, the container will take up as much space as available and squish the toolbar which
+ has been set to a height of 50px.
+
+
+
+
+
+
+
Toolbar
+
+
+
+ Flex with smaller content...
+
+ Line {{i}}
+
+
+
+
+ Flex with larger content...
+
+
+ Line {{i}}
+
+
+
+
+
+
+
+
+
+
+
+
+
Flex Element Heights
+
+
+ Firefox currently has an issue calculating the proper height of flex containers whose children
+ are flex, but have more content than can properly fit within the container.
+
+
+
+ This is particularly problematic if the flex children are md-content components as
+ it will prevent the content from scrolling correctly, instead scrolling the entire body.
+
+
+
+
+
+ Notice in the above Codepen how we must set overflow: auto on the div with the
+ change-my-css class in order for Firefox to properly calculate the height and
+ shrink to the available space.
+
+
+
diff --git a/docs/app/partials/license.tmpl.html b/docs/app/partials/license.tmpl.html
new file mode 100644
index 0000000000..8bf4999155
--- /dev/null
+++ b/docs/app/partials/license.tmpl.html
@@ -0,0 +1,34 @@
+
+
+ The MIT License
+
+
+ Copyright (c) 2014-2016 Google, Inc.
+ http://angularjs.org
+
+
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+
+
+
\ No newline at end of file
diff --git a/docs/app/partials/menu-link.tmpl.html b/docs/app/partials/menu-link.tmpl.html
new file mode 100644
index 0000000000..2c5bcfce2f
--- /dev/null
+++ b/docs/app/partials/menu-link.tmpl.html
@@ -0,0 +1,10 @@
+
+ {{section | humanizeDoc}}
+
+ current page
+
+
diff --git a/docs/app/partials/menu-toggle.tmpl.html b/docs/app/partials/menu-toggle.tmpl.html
new file mode 100644
index 0000000000..3f4a32ebb3
--- /dev/null
+++ b/docs/app/partials/menu-toggle.tmpl.html
@@ -0,0 +1,22 @@
+
+
+ {{section.name}}
+
+
+
+
+
+
+ Toggle {{isOpen()? 'expanded' : 'collapsed'}}
+
+
+
+
diff --git a/docs/app/partials/view-source.tmpl.html b/docs/app/partials/view-source.tmpl.html
index 236f8543fc..48061c277b 100644
--- a/docs/app/partials/view-source.tmpl.html
+++ b/docs/app/partials/view-source.tmpl.html
@@ -8,18 +8,18 @@
-
+
-
-
+
+
Done
-
+
diff --git a/docs/app/svg-assets-cache.js b/docs/app/svg-assets-cache.js
new file mode 100644
index 0000000000..dc97336fb1
--- /dev/null
+++ b/docs/app/svg-assets-cache.js
@@ -0,0 +1,76 @@
+(function() {
+ /**
+ * This 'svg-assets-cache.js' file should be loaded to a CDN or edge-server (currently S3).
+ * The CDN url (for this file) is then used in `doc/app/js/codepen.js#L59` to identify an
+ * external JS file that CodePen should load for 'launched' demos.
+ */
+ var assetMap = {
+ 'img/icons/ic_euro_24px.svg': '',
+ 'img/icons/ic_card_giftcard_24px.svg': '',
+ 'img/icons/addShoppingCart.svg' : '',
+ 'img/icons/android.svg' : '',
+ 'img/icons/angular-logo.svg' : '',
+ 'img/icons/bower-logo.svg' : '',
+ 'img/icons/cake.svg' : '',
+ 'img/icons/codepen-logo.svg' : '',
+ 'img/icons/copy.svg' : '',
+ 'img/icons/copy2.svg' : '',
+ 'img/icons/facebook.svg' : '',
+ 'img/icons/favorite.svg' : '',
+ 'img/icons/github-icon.svg' : '',
+ 'img/icons/github.svg' : '',
+ 'img/icons/hangout.svg' : '',
+ 'img/icons/ic_access_time_24px.svg' : '',
+ 'img/icons/ic_arrow_back_24px.svg' : '',
+ 'img/icons/ic_build_24px.svg' : '',
+ 'img/icons/ic_chevron_right_24px.svg' : '',
+ 'img/icons/ic_close_24px.svg' : '',
+ 'img/icons/ic_code_24px.svg' : '',
+ 'img/icons/ic_comment_24px.svg' : '',
+ 'img/icons/ic_email_24px.svg' : '',
+ 'img/icons/ic_insert_drive_file_24px.svg' : '',
+ 'img/icons/ic_label_24px.svg' : '',
+ 'img/icons/ic_launch_24px.svg' : '',
+ 'img/icons/ic_menu_24px.svg' : '',
+ 'img/icons/ic_more_vert_24px.svg' : '',
+ 'img/icons/ic_ondemand_video_24px.svg' : '',
+ 'img/icons/ic_people_24px.svg' : '',
+ 'img/icons/ic_person_24px.svg' : '',
+ 'img/icons/ic_phone_24px.svg' : '',
+ 'img/icons/ic_photo_24px.svg' : '',
+ 'img/icons/ic_place_24px.svg' : '',
+ 'img/icons/ic_play_arrow_24px.svg' : '',
+ 'img/icons/ic_play_circle_fill_24px.svg' : '',
+ 'img/icons/ic_refresh_24px.svg' : '',
+ 'img/icons/ic_school_24px.svg' : '',
+ 'img/icons/ic_visibility_24px.svg' : '',
+ 'img/icons/launch.svg' : '',
+ 'img/icons/mail.svg' : '',
+ 'img/icons/menu.svg' : '',
+ 'img/icons/message.svg' : '',
+ 'img/icons/more_vert.svg' : '',
+ 'img/icons/npm-logo.svg' : '',
+ 'img/icons/octicon-repo.svg' : '',
+ 'img/icons/print.svg' : '',
+ 'img/icons/separator.svg' : '',
+ 'img/icons/sets/communication-icons.svg' : '',
+ 'img/icons/sets/core-icons.svg' : '',
+ 'img/icons/sets/device-icons.svg' : '',
+ 'img/icons/sets/social-icons.svg' : '',
+ 'img/icons/share-arrow.svg' : '',
+ 'img/icons/tabs-arrow.svg' : '',
+ 'img/icons/twitter.svg' : '',
+ 'img/icons/upload.svg' : '',
+ 'icons/avatar-icons.svg' : ''
+};
+
+ // Note: 'material.svgAssetsCache' is used with Angular Material docs
+ // when launching code pen demos; @see codepen.js
+
+ angular.module('material.svgAssetsCache',[])
+ .run(function($templateCache) {
+ angular.forEach(assetMap, function(value, key) {
+ $templateCache.put(key, value);
+ });
+ });
+})();
diff --git a/docs/app/version.json b/docs/app/version.json
deleted file mode 100644
index b554cf0a4d..0000000000
--- a/docs/app/version.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "sha":"",
- "url":"https://github.com/angular/material/tree/",
- "_comment": "see gulp task 'docs-version' for usage"
-}
diff --git a/docs/config/index.js b/docs/config/index.js
index fa8c7a8749..950b8e9610 100644
--- a/docs/config/index.js
+++ b/docs/config/index.js
@@ -29,7 +29,8 @@ module.exports = new Package('angular-md', [
.config(function(readFilesProcessor, writeFilesProcessor) {
readFilesProcessor.basePath = projectPath;
readFilesProcessor.sourceFiles = [
- { include: 'dist/angular-material.js', basePath: 'dist' },
+ { include: 'src/components/**/*.js', basePath: '.' },
+ { include: 'src/core/**/*.js', basePath: '.' },
{ include: 'docs/content/**/*.md', basePath: 'docs/content', fileReader: 'ngdocFileReader' }
];
@@ -45,6 +46,7 @@ module.exports = new Package('angular-md', [
getAliases: function(doc) { return [doc.id]; }
});
+ // Build custom paths and outputPaths for "content" pages (theming and CSS).
computePathsProcessor.pathTemplates.push({
docTypes: ['content'],
getPath: function(doc) {
@@ -61,6 +63,17 @@ module.exports = new Package('angular-md', [
doc.fileInfo.baseName) + '.html';
}
});
+
+ // The default dgeni path for directives and services is something like
+ // "api/material.components.autocomplete/directive/mdAutocomplete".
+ // The module name is rather unnecessary, so we override with the shorter
+ // "api/directive/mdAutocomplete".
+ computePathsProcessor.pathTemplates.push({
+ docTypes: ['directive', 'service'],
+ getPath: function(doc) {
+ return path.join(doc.area, doc.docType, doc.name);
+ }
+ });
})
.config(function(generateComponentGroupsProcessor) {
diff --git a/docs/config/processors/buildConfig.js b/docs/config/processors/buildConfig.js
index ea63f97b26..90cd4b48b2 100644
--- a/docs/config/processors/buildConfig.js
+++ b/docs/config/processors/buildConfig.js
@@ -5,24 +5,55 @@ var exec = require('child_process').exec;
module.exports = function buildConfigProcessor(log) {
return {
$runBefore: ['rendering-docs'],
- $unAfter: ['indexPageProcessor'],
+ $runAfter: ['indexPageProcessor'],
$process: process
};
function process(docs) {
+ docs.push({
+ template: 'build-config.js',
+ outputPath: 'js/build-config.js',
+ buildConfig: buildConfig
+ });
+
+ return q.all([ getSHA(), getCommitDate() ])
+ .then( function(){
+ return docs;
+ });
+ }
+
+ /**
+ * Git the SHA associated with the most recent commit on origin/master
+ * @param deferred
+ * @returns {*}
+ */
+ function getSHA() {
var deferred = q.defer();
+
exec('git rev-parse HEAD', function(error, stdout, stderr) {
buildConfig.commit = stdout && stdout.toString().trim();
+ deferred.resolve(buildConfig.commit);
+ });
- docs.push({
- template: 'build-config.js',
- outputPath: 'js/build-config.js',
- buildConfig: buildConfig
- });
- deferred.resolve(docs);
+ return deferred.promise;
+ }
+
+ /**
+ * Get the commit date for the most recent commit on origin/master
+ * @param deferred
+ * @returns {*}
+ */
+ function getCommitDate() {
+ var deferred = q.defer();
+
+ exec('git show -s --format=%ci HEAD', function(error, stdout, stderr) {
+ buildConfig.date = stdout && stdout.toString().trim();
+ deferred.resolve(buildConfig.date);
});
return deferred.promise;
}
+
+
};
diff --git a/docs/config/processors/componentsData.js b/docs/config/processors/componentsData.js
index 2ef7b97a78..4c99f7fff6 100644
--- a/docs/config/processors/componentsData.js
+++ b/docs/config/processors/componentsData.js
@@ -1,16 +1,37 @@
var _ = require('lodash');
+var buildConfig = require('../../../config/build.config.js');
// We don't need to publish all of a doc's data to the app, that will
// add many kilobytes of loading overhead.
function publicDocData(doc, extraData) {
- doc = doc || {};
+ var options = _.assign(extraData || {}, { hasDemo: (doc.docType === 'directive') });
+ return buildDocData(doc, options, 'components');
+}
+
+function coreServiceData(doc, extraData) {
+ var options = _.assign(extraData || {}, { hasDemo: false });
+ return buildDocData(doc, options, 'core');
+}
+
+function buildDocData(doc, extraData, descriptor) {
+ var module = 'material.' + descriptor;
+ var githubBaseUrl = buildConfig.repository + '/blob/master/src/' + descriptor + '/';
+ var jsName = doc.module.split(module + '.').pop();
+
+ var basePathFromProjectRoot = 'src/' + descriptor + '/';
+ var filePath = doc.fileInfo.filePath;
+ var indexOfBasePath = filePath.indexOf(basePathFromProjectRoot);
+ var path = filePath.substr(indexOfBasePath + basePathFromProjectRoot.length, filePath.length);
+
return _.assign({
name: doc.name,
type: doc.docType,
outputPath: doc.outputPath,
url: doc.path,
- label: doc.label || doc.name
- }, extraData || {});
+ label: doc.label || doc.name,
+ module: module,
+ githubUrl: githubBaseUrl + path
+ }, extraData);
}
module.exports = function componentsGenerateProcessor(log, moduleMap) {
@@ -51,13 +72,26 @@ module.exports = function componentsGenerateProcessor(log, moduleMap) {
.filter() //remove null items
.value();
+ var EXPOSED_CORE_SERVICES = '$mdMedia';
+
+ var services = _(docs).filter(function(doc) {
+ return doc.docType == 'service' &&
+ doc.module == 'material.core' &&
+ EXPOSED_CORE_SERVICES.indexOf(doc.name) != -1;
+ }).map(coreServiceData).value();
+
+ docs.push({
+ name: 'SERVICES',
+ template: 'constant-data.template.js',
+ outputPath: 'js/services-data.js',
+ items: services
+ });
+
docs.push({
name: 'COMPONENTS',
template: 'constant-data.template.js',
outputPath: 'js/components-data.js',
items: components
});
-
}
};
-
diff --git a/docs/config/processors/content.js b/docs/config/processors/content.js
index b6a610073a..7a8e6c3041 100644
--- a/docs/config/processors/content.js
+++ b/docs/config/processors/content.js
@@ -30,4 +30,4 @@ module.exports = function contentProcessor(templateFinder) {
});
}
};
-};
\ No newline at end of file
+};
diff --git a/docs/config/template/demo-index.template.html b/docs/config/template/demo-index.template.html
new file mode 100644
index 0000000000..6bd1c42040
--- /dev/null
+++ b/docs/config/template/demo-index.template.html
@@ -0,0 +1,26 @@
+
+
+
+ <%= name %>
+
+
+
+
+
+
+
+
+
+
+ <% js.forEach(function(file) { %>
+
+ <% }); %>
+ <% css.forEach(function(file) { %>
+
+ <% }); %>
+
+
+
+<%= index.contents %>
+
+
diff --git a/docs/config/template/index.template.html b/docs/config/template/index.template.html
index 85a99b9938..50b1bbd169 100644
--- a/docs/config/template/index.template.html
+++ b/docs/config/template/index.template.html
@@ -1,107 +1,186 @@
-
+
-Material Design
-
-
-
-
-
-
-
-
+
+
+ Angular Material
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-### 2) Set the md-theme attribute
-
-To set a custom theme within an area of your application, set the `md-theme` attribute on an element. Then that element and all of its children will inherit that theme (see [how it works in detail](#/Theming/04_how_it_works)).
-
-An element will use the default theme if no `md-theme` is defined on that element or any of its ancestors.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Hello
-
-
-
-
-Individual components can also override the inherited theme:
-
-
-
-
-
-
-
-
-
-
-
-
-### Watching Themes
-To optimize performance, themable components do not watch a theme after it is
-set. This means, that if your theme is assigned dynamically, the component will
-not update to reflect it. If you have a dynamic attribute (ie.
-interpolated) for a theme, you will want to use the attribute `md-theme-watch="true"` on the
-relevant components so that it will watch the theme for changes. For an example
-of this see [this plunkr](http://plnkr.co/edit/0Ga0BSJgjGIiEMVXgWJd?p=preview).
-
-If you would like themable directives to *always* watch for theme changes by
-default, and are willing to take a performance hit for it, you may configure
-this default behavior by enabling it on `$mdThemingProvider`.
-
-
-app.config(function($mdThemingProvider) {
- $mdThemingProvider.alwaysWatchTheme(true);
-});
-
diff --git a/docs/content/Theming/03_building.md b/docs/content/Theming/03_building.md
deleted file mode 100644
index eb07e0426e..0000000000
--- a/docs/content/Theming/03_building.md
+++ /dev/null
@@ -1,78 +0,0 @@
-@ngdoc content
-@name Building Themes
-@description
-
-To build your own theme, you must write a `scss` file that overrides only the variables that you want to customize.
-
-The settings in your custom `scss` will override the settings in the `default-theme` to generate your custom theme. And using the conventions specified in [Material Colors](http://www.google.com/design/spec/style/color.html#color-ui-color-palette), developers can also [optionally] create 1-n **named** color palettes for use with Sass map().
-
-- - -
-
-For example, let's prepare a custom theme file `themes/my-custom-theme.scss`:
-
-
-$theme-name: 'my-custom';
-
-// Named color palettes
-
-$my-color: (
- '50': #fde0dc,
- '100': #f9bdbb,
- '200': #f69988,
- '300': #f36c60,
- '400': #e84e40,
- '500': #e51c23,
- '600': #dd191d,
- '700': #d01716,
- '800': #c41411,
- '900': #b0120a,
- 'A100': #ff7997,
- 'A200': #ff5177,
- 'A400': #ff2d6f,
- 'A700': #e00032
-);
-
-$forest-green: (
- '50': #d0f8ce,
- '100': #a3e9a4,
- '200': #72d572,
- '300': #42bd41,
- '400': #2baf2b,
- '500': #259b24,
- '600': #0a8f08,
- '700': #0a7e07,
- '800': #056f00,
- '900': #0d5302,
- 'A100': #a2f78d,
- 'A200': #5af158,
- 'A400': #14e715,
- 'A700': #12c700
-);
-
-$primary-color-palette: $my-color; // set the primary color palette for this theme
-$background-color-base: #333;
-
-$checkbox-color-palette: $forest-green; // set the 'forest green' theme for the checkbox only
-$tabs-color-palette: $color-indigo; // set the 'indigo' theme for the tabs only
-
-
-Then run the shell command `gulp build-theme -t my-custom`, or build everything with `gulp build`.
-These commands will create css files in `/dist/themes` matching the sass files found in `/themes/`.
-
-- - -
-
-Now, in order to apply this custom theme to your application:
-
-- Include the stylesheet in your html head:
-
-
-
-
-
-
-- And set the `md-theme` attribute in your app's markup:
-
-
-
-
-
diff --git a/docs/content/Theming/03_configuring_a_theme.md b/docs/content/Theming/03_configuring_a_theme.md
new file mode 100644
index 0000000000..9d400d7bcf
--- /dev/null
+++ b/docs/content/Theming/03_configuring_a_theme.md
@@ -0,0 +1,134 @@
+@ngdoc content
+@name Configuring a Theme
+@description
+
+## Configuring a theme
+
+By default your Angular Material application will use the default theme, a theme
+that is pre-configured with the following palettes for intention groups:
+
+- *primary* - indigo
+- *accent* - pink
+- *warn* - red
+- *background* - grey (note that white is in this palette)
+
+Configuring of the default theme is done by using the `$mdThemingProvider`
+during application configuration.
+
+### Configuring Color Intentions
+
+You can specify a color palette for a given color intention by calling the
+appropriate configuration method (`theme.primaryPalette`, `theme.accentPalette`,
+`theme.warnPalette`, `theme.backgroundPalette`).
+
+
+angular.module('myApp', ['ngMaterial'])
+.config(function($mdThemingProvider) {
+ $mdThemingProvider.theme('default')
+ .primaryPalette('pink')
+ .accentPalette('orange');
+});
+
+
+### Specifying Dark Themes
+
+You can mark a theme as dark by calling `theme.dark()`.
+
+
+angular.module('myApp', ['ngMaterial'])
+.config(function($mdThemingProvider) {
+ $mdThemingProvider.theme('default')
+ .dark();
+});
+
+
+### Specifying Custom Hues For Color Intentions
+
+You can specify the hues from a palette that will be used by an intention group
+by default and for the `md-hue-1`, `md-hue-2`, `md-hue-3` classes.
+
+By default, shades `500`, `300` `800` and `A100` are used for `primary` and
+`warn` intentions, while `A200`, `A100`, `A400` and `A700` are used for `accent`.
+
+
+angular.module('myApp', ['ngMaterial'])
+.config(function($mdThemingProvider) {
+
+ $mdThemingProvider.theme('default')
+ .primaryPalette('pink', {
+ 'default': '400', // by default use shade 400 from the pink palette for primary intentions
+ 'hue-1': '100', // use shade 100 for the `md-hue-1` class
+ 'hue-2': '600', // use shade 600 for the `md-hue-2` class
+ 'hue-3': 'A100' // use shade A100 for the `md-hue-3` class
+ })
+ // If you specify less than all of the keys, it will inherit from the
+ // default shades
+ .accentPalette('purple', {
+ 'default': '200' // use shade 200 for default, and keep all other shades the same
+ });
+
+});
+
+
+### Defining Custom Palettes
+
+As mentioned before, Angular Material ships with the Material Design
+Spec's color palettes built in. In the event that you need to define a custom color palette, you can use `$mdThemingProvider` to define it, thereby making
+it available to your theme for use in its intention groups. Note that you must
+specify all hues in the definition map.
+
+
+angular.module('myApp', ['ngMaterial'])
+.config(function($mdThemingProvider) {
+
+ $mdThemingProvider.definePalette('amazingPaletteName', {
+ '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', // whether, by default, text (contrast)
+ // on this palette should be dark or light
+
+ 'contrastDarkColors': ['50', '100', //hues which contrast should be 'dark' by default
+ '200', '300', '400', 'A100'],
+ 'contrastLightColors': undefined // could also specify this if default was 'dark'
+ });
+
+ $mdThemingProvider.theme('default')
+ .primaryPalette('amazingPaletteName')
+
+});
+
+
+Sometimes it is easier to extend an existing color palette to overwrite a few
+colors than define a whole new one. You can use `$mdThemingProvider.extendPalette`
+to quickly extend an existing color palette.
+
+
+angular.module('myApp', ['ngMaterial'])
+.config(function($mdThemingProvider) {
+
+ // Extend the red theme with a few different colors
+ var neonRedMap = $mdThemingProvider.extendPalette('red', {
+ '500': 'ff0000'
+ });
+
+ // Register the new color palette map with the name `neonRed`
+ $mdThemingProvider.definePalette('neonRed', neonRedMap);
+
+ // Use that theme for the primary intentions
+ $mdThemingProvider.theme('default')
+ .primaryPalette('neonRed')
+
+});
+
diff --git a/docs/content/Theming/04_how_it_works.md b/docs/content/Theming/04_how_it_works.md
deleted file mode 100644
index c10f4eb755..0000000000
--- a/docs/content/Theming/04_how_it_works.md
+++ /dev/null
@@ -1,88 +0,0 @@
-@ngdoc content
-@name How it Works
-@description
-
-Themes are implemented through a combination of Sass and JavaScript.
-
-#### Color Palettes
-
-Angular Material has implemented the spec's [color palettes](http://www.google.com/design/spec/style/color.html#color-ui-color-palette) as Sass maps in [src/core/style/color-palette.scss](https://github.com/angular/material/blob/master/src/core/style/color-palette.scss).
-
-The palettes can be used in your own theme by calling Sass's `map-get` function:
-
-
-body {
- background-color: map-get($color-red, '500');
-}
-
-
-#### Global Style Variables
-
-Most components' colors are derived from variables in [src/core/style/variables.scss](https://github.com/angular/material/blob/master/src/core/style/variables.scss). Here are some guidelines regarding these variables:
-
-| Variable | Purpose |
-|--------|--------|
-| $theme-name | name of the theme, matching what will be used in the `md-theme` attribute |
-| $foreground-color-palette | the color palette used for foreground colors (such as text, hints, and dividers) |
-| $background-color-palette | the color palette used to determine the background color |
-| $primary-color-palette | the primary color used for things like buttons, spinners, etc. |
-| $warn-color-palette | the color palette used for warnings within the app |
-| $primary-color-palette-contrast-color | the color used for text with a `primary-color` as a background |
-
-#### Component Styles
-
-Each component within Angular Material has custom styles specified in its `*-theme.scss` file. For example:
-
-- [src/components/textField/textField-theme.scss](https://github.com/angular/material/blob/master/src/components/textField/textField-theme.scss),
-- [src/components/tabs/tabs-theme.scss](https://github.com/angular/material/blob/master/src/components/tabs/tabs-theme.scss),
-- [src/components/slider/slider-theme.scss](https://github.com/angular/material/blob/master/src/components/slider/slider-theme.scss),
-- etc.
-
-These component-specific styles are concatenated with **variables.scss** and **color-palettes.scss** to generate `/themes/default-theme.scss`.
-Additionally, each custom theme file in `/themes/*.scss` overrides the variables from default-theme and compiles as css to `/dist/themes/`.
-
-#### JavaScript Features
-
-Angular Material uses both an `md-theme` directive and an `$mdTheming` service to provide JavaScript support for theming.
-
-- `md-theme` is a directive that will set and watch the element's theme so all children elements can easily inherit the styles.
-- `$mdTheming` is an internal service that registers an element as 'themable': it can inherit the theme from parent elements.
-
-`$mdTheming` will look up the DOM for the nearest parent with an `md-theme` attribute, and add that parent's theme class to its own element.
-
-
-- - -
-
-Here is an **simple** directive that uses **$mdTheming** to add theming support within its feature set:
-
-
-app.directive('simpleDirective', function($mdTheming) {
- return {
- template: 'Hello world
',
- link: function(scope, element, attr) {
- $mdTheming(element);
- // Other features go here...
- }
- }
-});
-
-
-Here is an example of using the **simple-directive** and themes in the HTML:
-
-
-
-
-
-
-
-At runtime, both the elements **div#myForm** and **simple-directive** will now have a `class="md-green-theme"` attribute. And if we want to override a child element as shown below:
-
-
-
-
-
-
-
-
-At runtime, the elements **div#myForm** will have a `class="md-green-theme"` attribute and the element **simple-directive** will now have a `class="md-yellow-theme"` attribute.
-
diff --git a/docs/content/Theming/04_multiple_themes.md b/docs/content/Theming/04_multiple_themes.md
new file mode 100644
index 0000000000..0d4c675292
--- /dev/null
+++ b/docs/content/Theming/04_multiple_themes.md
@@ -0,0 +1,77 @@
+@ngdoc content
+@name Multiple Themes
+@description
+
+In most applications, declaring multiple themes is **not** necessary. Instead,
+you should configure the `default` theme for your needs. If you need multiple
+themes in a single application, Angular Material does provide tools
+to make this possible.
+
+### Registering another theme
+
+Use the `$mdThemingProvider` to register a second theme within your application.
+By default all themes will inherit from the `default` theme. Once you have
+registered the second theme, you can configure it with the same chainable
+interface used on the default theme.
+
+
+angular.module('myApp', ['ngMaterial'])
+.config(function($mdThemingProvider) {
+ $mdThemingProvider.theme('altTheme')
+ .primaryPalette('purple') // specify primary color, all
+ // other color intentions will be inherited
+ // from default
+});
+
+
+### Using another theme
+
+#### Via the Provider
+
+You can change the default theme to be used across your entire application using
+the provider:
+
+
+$mdThemingProvider.setDefaultTheme('altTheme');
+
+
+#### Via a Directive
+
+Angular Material also exposes the `md-theme` directive which will set the theme
+on an element and all child elements.
+
+In the following example, the application will use the `default` theme, while
+the second child `div` will use the `altTheme`. This allows you to theme
+different parts of your application differently.
+
+
+
+
I will be blue (by default)
+
+ I will be purple (altTheme)
+
+
+
+
+#### Dynamic Themes
+
+By default, to save on performance, theming directives will **not** watch
+`md-theme` for changes. If you need themes to be dynamically modified, you will
+need to use the `md-theme-watch` directive.
+
+
+
+
Default
+
altTheme
+
+ I'm dynamic
+
+
+
+
+If you need this behavior in your entire application (ie. on all `md-theme`
+directives) you can use the `$mdThemingProvider` to enable it.
+
+
+$mdThemingProvider.alwaysWatchTheme(true);
+
diff --git a/docs/content/Theming/05_under_the_hood.md b/docs/content/Theming/05_under_the_hood.md
new file mode 100644
index 0000000000..36f58f5d29
--- /dev/null
+++ b/docs/content/Theming/05_under_the_hood.md
@@ -0,0 +1,26 @@
+@ngdoc content
+@name Theming under the hood
+@description
+
+### Under the Hood
+
+Angular Material dynamically generates CSS for registered themes by injecting several
+`
-
-
-
-
-
-<% js.forEach(function(file) { %>
-
-<% }); %>
-<% css.forEach(function(file) { %>
-
-<% }); %>
-
-
-
-<%= index.contents %>
-
-
diff --git a/docs/guides/BUILD.md b/docs/guides/BUILD.md
new file mode 100644
index 0000000000..b2d80673f9
--- /dev/null
+++ b/docs/guides/BUILD.md
@@ -0,0 +1,204 @@
+## Build Instructions
+
+* [Introduction](#intro)
+* [Build Commands](#commands)
+* [Building the Documentation](#livedocs)
+* [Building the Library](#builds)
+* [Using the Library with Bower](#bower)
+* [Introducing Components](#comp)
+* [Building Individual Components](#comp_builds)
+* [Component Debugging](#comp_debug)
+* [Theming](#themes)
+
+
+## Introduction
+
+Angular Material has a sophisticated collection of build process and commands available... to deploy
+distribution files, test components, and more.
+
+These commands are defined within two (2) **gulp** files:
+
+* [Project Gulp](../../gulpfile.js)
+* [Documentation Gulp](../gulpfile.js)
+
+
+### Build Commands
+
+For each milestone release, always run:
+
+- `npm update` to update your local gulp dependencies
+- `bower update` to update AngularJS dependencies
+
+The following command line tasks are available:
+
+- `gulp build` (alias `gulp`) to build, add `--release` flag to uglify & strip `console.log`
+- `gulp docs` to build the Live Docs into dist/docs
+- `gulp watch` to build & rebuild on changes
+
+
+
+- `gulp karma` to test once
+- `gulp karma-watch` to test & watch for changes
+
+### Building the Documentation
+
+The Angular Material **Live Docs** are generated from the source code and demos and actually use the
+Angular Material components and themes.
+
+> Our build process uses **[dgeni](https://github.com/angular/dgeni)**, the wonderful documentation
+ generator built by [Pete Bacon Darwin](https://github.com/petebacondarwin).
+
+See the [Building the Live Documentation](../README.md#docs) document for details.
+
+### Building the Library
+
+Developers can build the entire Angular Material library or individual component modules. The
+library comprises:
+
+* `angular-material.js` - components
+* `angular-material.css` - styles and default theme stylesheet
+* `/themes/**.css` - default theme override stylesheets
+
+To build from the source, simply use:
+
+```bash
+# Build and deploy the library to
+#
+# - `dist/angular-material.js`
+# - `dist/angular-material.css`
+# - `dist/themes`
+
+gulp build
+
+# Build minified assets
+#
+# - `dist/angular-material.min.js`
+# - `dist/angular-material.min.css`
+# - `dist/themes`
+
+gulp build --release
+```
+
+### Using the Library with Bower
+
+For developers not interested in building the Angular Material library, use **bower** to install and
+use the Angular Material distribution files.
+
+Change to your project's root directory.
+
+```bash
+# To get the latest stable version, use Bower from the command line.
+bower install angular-material
+
+# To get the most recent, latest committed-to-master version use:
+bower install angular-material#master
+```
+
+Visit [Bower-Material](https://github.com/angular/bower-material/blob/master/README.md) for more
+details on how to install and use the Angular Material distribution files within your own local
+project.
+
+
+## Introducing Components
+
+Angular Material supports the construction and deployment of individual component builds. Within
+Angular Material, each component is contained within its own module and specifies its own
+dependencies.
+
+> At a minimum, all components have a dependency upon the `core` module.
+
+For example, the **slider** component is registered as a **material.components.slider** module.
+
+### Building Individual Components
+
+To build and deploy assets for each component individually, run the command
+
+```bash
+gulp build-all-modules
+```
+
+All component modules are compiled and distributed to:
+
+```text
+ -- dist
+ -- modules
+ -- js
+ -- core
+ --
+```
+
+Let's consider the Slider component with its module definition:
+
+
+```js
+/**
+ * @ngdoc module
+ * @name material.components.slider
+ */
+angular.module('material.components.slider', [
+ 'material.core'
+]);
+```
+
+First build all the component modules.
+
+To use - for example - the Slider component within your own application, simply load the stylesheets
+and JS from both the **slider** and the **core** modules:
+
+
+```text
+ -- dist
+ -- modules
+ -- js
+ -- core
+ -- core.js
+ -- core.css
+ -- slider
+ -- slider.js
+ -- slider.css
+ -- slider-default-theme.css
+```
+
+### Component Debugging
+
+Debugging a demo in the Live Docs is complicated due the multiple demos loading and initializing. A
+more practical approach is to open and debug a specific, standalone Component demo.
+
+To open a Component demo outside of the Docs application, just build, deploy and debug that
+component's demo(s).
+
+For example, to debug the **textfield** component:
+
+```bash
+# Watch, build and deploy the 'textfield' demos
+#
+# NOTE: watch-demo will rebuild your component source
+# and demos upon each `save`
+#
+gulp watch-demo -c textfield
+
+# launch the liveReload server on port 8080
+#
+# Note: livereload will reload demos after updates are
+# deployed (by watch-demo) to the dist/demos/
+#
+gulp server
+```
+
+The demo build process will deploy a *self-contained* Angular application that runs the specified
+component's demo(s). E.g.:
+
+* `dist/demos/textfield/**/*.*`
+* `dist/demos/tabs/**/*.*`
+* etc.
+
+After running `gulp server` to start a *LiveReload* server in your project root:
+
+* Open browser to url `http://localhost:8080/`
+* Navigate to `dist/demos///index.html`
+* Open Dev Tools and debug...
+
+
+## Theming
+
+https://material.angularjs.org/#/Theming/01_introduction
diff --git a/docs/guides/CODEPEN.md b/docs/guides/CODEPEN.md
new file mode 100644
index 0000000000..b10211ad5c
--- /dev/null
+++ b/docs/guides/CODEPEN.md
@@ -0,0 +1,73 @@
+# Editable Demos in Codepen
+
+## Description
+
+Users will be able to click a button on each demo to open in codepen
+to edit. From there the user can edit, save or make other
+modifications to the example.
+
+## Why Codepen?
+
+Codepen appears to be one the most stable and active online sandboxes.
+It has less accessibility problems then some of the other tools.
+
+## How does it work?
+
+When the user clicks on the **'Edit on codepen'** button, all files including
+html, css, js, templates are used to create the new codepen by posting
+to the [Codepen API](http://blog.codepen.io/documentation/api/prefill/). An
+additional script is appended to the example to initialize the
+[cache](#asset_cache), which is responsible for serving assets.
+
+## As a contributor, what do I need to know?
+
+* [SVG images are served from a cache](#asset_cache)
+* [Adding a new SVG requires a change to the asset cache](#build_cache)
+* Anytime a new dependency is added to an example, the [asset-cache.js](../app/asset-cache.js)
+ will need to be updated with the new dependency and [uploaded to the
+ CDN](#update_cdn)
+* Images used in demos must use full paths
+* Code examples are modified prior to sending to codepen with the same
+ module defined in the [asset-cache.js](../app/asset-cache.js)
+* Additional HTML template files located in the demo directory are appended to your index file using `ng-template`. [See docs](https://docs.angularjs.org/api/ng/directive/script)
+
+## Asset Cache
+
+SVG images are stored in an asset cache using `$templateCache`. A
+script is delivered to codepen that initializes the cache within the
+demo module.
+
+### Why is an asset cache needed for Codepen?
+
+Components within angular material at times use icons or SVG. Images
+are fetched over http. Without having a server that will allow cross
+site scripting (`Access-Control-Allow-Origin: *`), the request will
+fail with a [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS)
+error.
+
+The asset cache is intended to bypass any http request for an image
+and serve the cached content.
+
+### How do I populate the cache?
+
+* Make all changes necessary to add or update any svg images
+* run `./scripts/build-asset-cache.sh | pbcopy` to add an object
+ literal to your paste buffer.
+* paste object literal as `var assetMap = { ... }` in the
+ [asset-cache.js](../app/asset-cache.js)
+* [update](#update_cdn) the CDN with the new script
+* commit asset-cache.js
+
+### Update Codepen Asset Cache
+
+CDN is located on the Codepen PRO account.
+
+* Follow the [instructions](http://blog.codepen.io/documentation/pro-features/asset-hosting/#asset-manager) on how to update the script.
+* NOTE: be sure to update the script. DO NOT upload a new script. The URL should remain the same
+
+## Deployment Considerations
+
+The step to generate and deploy the asset-cache.js is currently a
+manual process. Keep in mind that if changes are made to
+asset-cache.js then you will need to follow the [steps](#update_cdn)
+to update the cache on the CDN.
diff --git a/docs/guides/CODING.md b/docs/guides/CODING.md
new file mode 100644
index 0000000000..d7eddd2324
--- /dev/null
+++ b/docs/guides/CODING.md
@@ -0,0 +1,167 @@
+# Coding Conventions and Guidelines
+
+ - [Project Structure](#structure)
+ - [Coding Rules](#rules)
+
+
+## Project Structure
+
+All component modules are defined in:
+
+```text
+ -- /src
+ -- /components
+ -- /
+
+ -- .js
+ -- .spec.js
+ -- .scss
+ -- -theme.scss
+
+ -- /demo
+
+ -- index.html
+ -- style.css
+ -- script.js
+```
+
+All component modules are compiled and distributed individually to:
+
+```text
+ -- /dist
+ -- /modules
+ -- /js
+ -- /core
+ -- /
+```
+
+Additionally, all component modules are compiled and deployed as a library to:
+
+```text
+ -- /dist
+ -- angular.material.js
+ -- angular.material.css
+```
+
+> NOTE: the `dist` directory is **not** version controlled.
+
+
+## Coding Rules
+
+#### Coding conventions:
+
+The best guidance is a coding approach that implements both code and comments in a clear,
+understandable, concise, and DRY fashion.
+
+Below is a sample code that demonstrates some of our rules and conventions:
+
+```js
+(function() {
+'use strict';
+
+/**
+ * @ngdoc module
+ * @name material.components.slider
+ */
+angular.module('material.components.slider', [
+ 'material.core'
+])
+ .directive('mdSlider', SliderDirective);
+
+/**
+ * @ngdoc directive
+ * @name mdSlider
+ * @module material.components.slider
+ * @restrict E
+ * @description
+ * The `` component allows the user to choose from a range of values.
+ *
+ * It has two modes: 'normal' mode, where the user slides between a wide range of values, and
+ * 'discrete' mode, where the user slides between only a few select values.
+ *
+ * To enable discrete mode, add the `md-discrete` attribute to a slider, and use the `step`
+ * attribute to change the distance between values the user is allowed to pick.
+ *
+ * @usage
+ * Normal Mode
+ *
+ *
+ *
+ *
+ *
+ * Discrete Mode
+ *
+ *
+ *
+ *
+ *
+ * @param {boolean=} mdDiscrete Whether to enable discrete mode.
+ * @param {number=} step The distance between values the user is allowed to pick. Default 1.
+ * @param {number=} min The minimum value the user is allowed to pick. Default 0.
+ * @param {number=} max The maximum value the user is allowed to pick. Default 100.
+ */
+function SliderDirective($mdTheming) {
+ //...
+}
+
+})();
+```
+
+* With the exceptions listed in this document, follow the rules contained in
+ [Google's JavaScript Style Guide](https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml).
+* All components must have unique, understandable module names; prefixed with
+ 'material.components.'.
+* All components must depend upon the 'material.core' module.
+* Do not use `$inject` to annotate arguments.
+ ngAnnotate is used as part of the build process to automatically create the annotations.
+* All public API methods **must** be documented with ngdoc, an extended version of jsdoc (we added
+ support for markdown and templating via @ngdoc tag). To see how we document our APIs, please
+ check out the existing ngdocs and see
+ [this wiki page](https://github.com/angular/angular.js/wiki/Writing-AngularJS-Documentation).
+* All directives must use the `md-` prefix for both the directive name and any directive
+ attributes.
+ Directive **templates** should be defined inline.
+
+
+#### Testing
+
+* All components must have valid, **passing** unit tests.
+* All features or bug fixes **must be tested** by one or more
+ [specs](https://docs.angularjs.org/guide/unit-testing).
+
+#### Coding
+
+* Wrap all code at **100 characters**.
+* Do not use tabs. Use two (2) spaces to represent a tab or indent.
+* Constructors are PascalCase, closures and variables are lowerCamelCase.
+* When enhancing or fixing existing code
+ * Do not reformat the author's code
+ * Conform to standards and practices used within that code; unless overridden by best practices or patterns.
+ * Provide jsDocs for functions and single-line comments for code blocks
+ * Be careful of regression errors introduce by your changes
+ * **Always** test your changes with unit tests and manual user testing.
+
+#### Patterns
+
+* All components should be wrapped in an anonymous closure using the Module Pattern.
+* Use the **Revealing Pattern** as a coding style.
+* Do **not** use the global variable namespace, export our API explicitly via Angular DI.
+* Instead of complex inheritance hierarchies, use **simple** objects.
+* Avoid prototypal inheritance unless warranted by performance or other considerations.
+* We **love** functions and closures and, whenever possible, prefer them over objects.
+
+ > Do not use anonymous functions. All closures should be named.
+
+#### Documentation
+
+* All non-trivial functions should have a jsdoc description.
+* To write concise code that can be better minified, we **use aliases internally** that map to the
+ external API.
+* Use of argument **type annotations** for private internal APIs is not encouraged, unless it's an
+ internal API that is used throughout Angular Material.
diff --git a/docs/guides/COMMIT_LEVELS.md b/docs/guides/COMMIT_LEVELS.md
new file mode 100644
index 0000000000..bc6637933e
--- /dev/null
+++ b/docs/guides/COMMIT_LEVELS.md
@@ -0,0 +1,48 @@
+## Understanding the Team Commit Process
+
+The Angular Material team has a very specific process for change commits.
+
+Our commit process is intentionally restrictive to (a) support the rapid evolution of Angular Material and (b) manage change complexity and coding standards within the framework.
+
+Angular Material uses a "Pull Request" process to allow team leaders opportunities to maintain code reviews, ensure sanity checks, encourage coding standards, and provide feedback to the developer.
+
+#### General Rules
+
+* Please **do not** commit directly to master; unless explicitly authorized by the Team Leadership
+* Team developers should fork the repository and work on fixes, features, or enhancements on their own fork.
+ * Use the Pull Request feature to submit your changes to the 'upstream' Angular Material repository
+* When a developer has changes ready for merging into master, those fixes or updates must be submitted via a Pull Request
+* All PRs must be rebased (with master) and commits squashed prior to the final merge process
+* Please **NEVER** use the *green* merge button on GitHub
+ * Angular Material maintains a *flat*, linear change history using a specific **manual merge process**
+ * Do not use Github's automated merge process
+* Please include unit tests with all your source (component or core) changes
+* All unit tests must have 100% passing before the PR will be accepted/merged.
+
+> These process guidelines are especially important: our framework features and the team membership is growing rapidly.
+
+#### Commit Authorization Rules
+
+The development team has defined three (3) Github levels of **commit authorization** within [Angular/Material](https://github.com/angular/material/):
+
+* General:
+ * Developers in this group includes any team members not listed under Core or Team Leads below
+ * For any and all changes, developers must use a fork of the Angular Material repository
+ * Please do not make or submit any changes from the master branch.
+ * Are not authorized to merge PRs
+ * Should not reassign issue or change issue milestones
+ * Should ensure their issue labels are correct
+ * Should ensure their issues are tested with latest HEAD versions of Angular Material
+ * Should ensure their issues are tested with latest releases of Angular (1.3.x, 1.4.x, 1.5.x)
+* Core:
+ * Includes: [Ryan Schmukler](https://github.com/rschmukler), [Robert Messerlee](https://github.com/robertmesserle), [Topher Fangio](https://github.com/topherfangio)
+ * Should not merge PRs (unless explicitly requested)
+ * Should use Angular Material branches for major, non-trivial changes.
+ * For minor changes, developers in this group may elect to commit direct to master.
+* Team Leads:
+ * Includes: [Naomi Black](https://github.com/naomiblack), [Thomas Burleson](https://github.com/ThomasBurleson), [Jeremy Elbourn](https://github.com/jelbourn)
+ * May review PRs
+ * ThomasBurleson is the primary PR reviewer
+ * Should confirm Karma tests pass
+ * Should squash as need
+ * Should ensure the PR is closed when the merge finishes.
diff --git a/docs/guides/MERGE_REQUESTS.md b/docs/guides/MERGE_REQUESTS.md
new file mode 100644
index 0000000000..1989fea226
--- /dev/null
+++ b/docs/guides/MERGE_REQUESTS.md
@@ -0,0 +1,165 @@
+## Merging a Pull Request
+
+* [Copy the PR into a local branch](#curl)
+* [Squashing everything into one commit](#squash)
+* [Dealing with Conflicts](#conflicts)
+* [Merging With Master](#merging)
+
+
+
+### Bringing a pull request into your local git
+
+To bring in a pull request, first create a new branch for that pull request:
+
+```sh
+git checkout -b wip-pr-143
+```
+
+Then run the following to bring all of the commits from that pull request
+in on top of your branch's local history:
+
+```sh
+curl https://github.com/angular/material/pull/143.patch | git am -3
+```
+
+If there are any conflicts, go to the [Dealing with conflicts](#conflicts) section below.
+
+If the merge succeeds, use `git diff origin/master` to see all the new changes that will happen
+post-merge.
+
+### Squashing everything into one commit
+
+Before merging a pull request into master, make sure there is only one commit
+representing the changes in the pull request, so the git log stays lean.
+
+We will use git's interactive rebase to let us manipulate, merge, and rename
+commits in our local history.
+
+To interactively rebase all of your commits that occur after the latest in master, run:
+
+```sh
+git rebase --interactive origin/master
+```
+
+This will bring up an interactive dialog in your text editor. Follow the instructions
+to squash all of your commits into the top one, then rename the top one.
+
+Once this is done, run `git log` and you will see only one commit after master, representing
+everything from the pull request.
+
+Finally, we'll pull from master with rebase to put all of our local commits on top of
+the latest remote.
+
+```sh
+git pull --rebase origin master
+```
+
+This may cause conflicts, see below for how to deal with these.
+
+### Dealing with conflicts
+
+Run the following to see which files are conflicted:
+
+```sh
+git status
+```
+
+You can open the conflicted files and fix them manually, or if the conflict isn't relevant, run:
+
+```sh
+git checkout --theirs
+```
+
+To checkout *your local* version of the file.
+
+```sh
+git checkout --ours
+```
+
+To checkout *their remote* version of the file. (yes, it's backwards).
+
+After all the conflicted files are fixed, run:
+
+```sh
+git add -A
+git rebase --continue
+```
+
+Or if you're pulling from `git am` and fixing conflicts, run:
+
+```sh
+git add -A
+git am --continue
+```
+
+### Merging with master
+
+Finally, after you've squashed the whole pull request into one commit and made sure
+it has no conflicts with the latest master and tests are run, you're ready to merge it in.
+
+Simply go back to the master branch:
+
+```sh
+git checkout master
+```
+
+Make sure you're up to date in the master branch too:
+
+```sh
+git pull --rebase origin master
+```
+
+And finally, rebase your pull request in from your WIP pull request branch:
+
+```sh
+git rebase wip-pr-143
+```
+
+This will rebase the commits from wip-pr-143 into master.
+
+Finally, verify that tests pass and the docs look fine, and make sure
+the commit message for the pull request closes the proper issues and lists
+breaking changes.
+
+You can amend the commit to change the message:
+
+```sh
+git commit --amend
+```
+
+This will open the latest commit in your text editor and allow you to add
+text. Do **not** forget to add `Close #xxx` to also close the PR; in this case you will add
+`Close #143`.
+
+For example:
+
+> fix(constant): rename $mdConstant.SOMETHING_ELSE to $mdConstant.SOMETHING
+
+> Close #143. Close #156.
+
+> BREAKING CHANGE: $mdConstant.SOMETHING_ELSE has been renamed to $mdConstant.SOMETHING.
+
+> Change your code from this:
+
+> ```js
+ $mdConstant.SOMETHING_ELSE
+> ```
+
+> To this:
+
+> ```js
+ $mdConstant.SOMETHING
+> ```
+
+For more examples of how to format commit messages, see
+[the guidelines in CONTRIBUTING.md](../../CONTRIBUTING.md#-git-commit-guidelines).
+
+```sh
+git push origin master
+```
+
+Lastly, be sure to cleanup any branches that you were using for this pull request.
+
+```sh
+git branch -D wip-pr-143
+```
diff --git a/docs/guides/PULL_REQUESTS.md b/docs/guides/PULL_REQUESTS.md
new file mode 100644
index 0000000000..d031e34544
--- /dev/null
+++ b/docs/guides/PULL_REQUESTS.md
@@ -0,0 +1,94 @@
+### Submitting a Pull Request
+Before you submit your pull request consider the following guidelines:
+
+* Search [GitHub](https://github.com/angular/material/pulls) for an open or closed Pull Request
+ that relates to your submission. You don't want to duplicate effort.
+
+* Please sign our [Contributor License Agreement (CLA)](../../CONTRIBUTING.md#cla) before sending pull
+ requests. We cannot accept code without this.
+
+* Make your changes in a new git branch:
+
+ ```shell
+ git checkout -b my-fix-branch master
+ ```
+
+* Follow our [Coding Rules](CODING.md#rules).
+
+* **Include appropriate test cases.**
+
+* Create your patch.
+
+* Run the full Angular Material test suite, as described in the [developer documentation](BUILD.md),
+ and ensure that all tests pass.
+
+* Commit your changes using a descriptive commit message that follows our
+ [commit message conventions](../../CONTRIBUTING.md#commit-message-format). Adherence to the [commit message conventions](../../CONTRIBUTING.md#commit-message-format) is required
+ because release notes are automatically generated from these messages.
+
+ ```shell
+ git commit -a
+ ```
+ Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
+
+* Build your changes locally to ensure all the tests pass:
+
+ ```shell
+ gulp karma
+ ```
+
+* Push your branch to GitHub:
+
+ ```shell
+ git push origin my-fix-branch
+ ```
+
+* In GitHub, send a pull request to `angular:master`.
+
+* If we suggest changes then:
+ * Make the required updates.
+
+ * Re-run the Angular Material test suite to ensure tests are still passing.
+
+ * Rebase your branch and force push to your GitHub repository (this will update your Pull Request):
+
+ ```shell
+ git rebase master -i
+ git push -f
+ ```
+
+
+
+
+That's it... thank you for your contribution!
+
+
+
+#### After your pull request is merged
+
+After your pull request is merged, you can safely delete your branch and pull the changes
+from the main (upstream) repository:
+
+* Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows:
+
+ ```shell
+ git push origin --delete my-fix-branch
+ ```
+
+* Check out the master branch:
+
+ ```shell
+ git checkout master -f
+ ```
+
+* Delete the local branch:
+
+ ```shell
+ git branch -D my-fix-branch
+ ```
+
+* Update your master with the latest upstream version:
+
+ ```shell
+ git pull --ff upstream master
+ ```
diff --git a/docs/guides/THEMES_IMPL_NOTES.md b/docs/guides/THEMES_IMPL_NOTES.md
new file mode 100644
index 0000000000..32dacf266e
--- /dev/null
+++ b/docs/guides/THEMES_IMPL_NOTES.md
@@ -0,0 +1,38 @@
+# Notes on ng-material's theme implementation
+
+#### TL;DR
+Themes are configured by users with `$mdThemingProvider`. The CSS is then generated at run-time by
+the `$mdTheming` service and tacked into the document head.
+
+## Actual explanation
+
+### At build time
+* All of the styles associated with **color** are defined in a separate scss file of the form
+`xxx-theme.scss`.
+* Instead of using hard-coded color or a SCSS variable, the colors are defined with a mini-DSL
+(described deblow).
+* The build process takes all of those `-theme.scss` files and globs them up into one enourmous
+string.
+* The build process wraps that string with code to set it an angular module constant:
+ ``` angular.module('material.core').constant('$MD_THEME_CSS', 'HUGE_THEME_STRING'); ```
+* That code gets dumped at the end of `angular-material.js`
+
+### At run time
+* The user defines some themes with `$mdThemingProvider` during the module config phase.
+* At module run time, the theming service takes `$MD_THEME_CSS` and, for each theme, evaluates the
+mini-DSL, applies the colors for the theme, and appends the resulting CSS into the document head.
+
+
+### The mini-DSL
+* Each color is written in the form `'{{palette-hue-opacity}}'`, where opacity is optional.
+* For example, `'{{primary-500}}'`
+* Palettes are `primary`, `accent`, `warn`, `background`, `foreground`
+* The hues for each type except `foreground` use the Material Design hues.
+* The `forground` palette is a number from one to four:
+ * `foreground-1`: text
+ * `foreground-2`: secondary text, icons
+ * `foreground-3`: disabled text, hint text
+ * `foreground-4`: dividers
+* There is also a special hue called `contrast` that will give a contrast color (for text).
+For example, `accent-contrast` will be a contrast color for the accent color, for use as a text
+color on an accent-colored background.
diff --git a/docs/guides/component/README.md b/docs/guides/component/README.md
new file mode 100644
index 0000000000..af91735fac
--- /dev/null
+++ b/docs/guides/component/README.md
@@ -0,0 +1,17 @@
+### Component Templates
+
+This folder and its contents provide a starting template for any Angular Material component. It is easy to use these assets for your own custom component development.
+
+> Please do all of your development work in a Git branch; labelled with a WIP prefix!!
+
+Simply:
+
+- Create a work-in-progress Git branch;
+- Clone this directory to the `/src/components/`
+- Rename all files from `_name_*.js` with the name of your component;
+> e.g. gridlist.js, select.js
+- Rename **jsDoc** ** references to `_name_`
+- Rename **source** references to `_name_`
+
+
+
diff --git a/docs/guides/component/_name_/_name_-theme.scss b/docs/guides/component/_name_/_name_-theme.scss
new file mode 100644
index 0000000000..74d95b74f8
--- /dev/null
+++ b/docs/guides/component/_name_/_name_-theme.scss
@@ -0,0 +1,8 @@
+$g_name_-color-palette: $primary-color-palette !default;
+$g_name_-background-color: $foreground-tertiary-color !default;
+$g_name_-disabled-color: map-get($foreground-color-palette, '400') !default;
+
+md-_name_.md-#{$theme-name}-theme {
+
+
+}
diff --git a/docs/guides/component/_name_/_name_.js b/docs/guides/component/_name_/_name_.js
new file mode 100644
index 0000000000..66bf9e5d9d
--- /dev/null
+++ b/docs/guides/component/_name_/_name_.js
@@ -0,0 +1,42 @@
+(function() {
+'use strict';
+
+/**
+ * @ngdoc module
+ * @name material.components._name_list
+ */
+angular.module('material.components._name_list', ['material.core'])
+ .directive('md_name_list', _name_listDirective);
+
+/**
+ * @ngdoc directive
+ * @name md_name_list
+ * @module material.components._name_list
+ * @restrict E
+ * @description
+ *
+ * @usage
+ *
+ */
+function _name_listDirective() {
+ return {
+ scope: {},
+ require: [],
+ controller: _name_listController,
+ link: postLink
+ };
+
+ function postLink(scope, element, attr, ctrls) {
+
+ }
+}
+
+/**
+ * We use a controller for all the logic so that we can expose a few
+ * things to unit tests
+ */
+function _name_listController($scope, $element, $attrs, $mdAria, $mdUtil, $mdConstant) {
+
+}
+
+})();
diff --git a/docs/guides/component/_name_/_name_.scss b/docs/guides/component/_name_/_name_.scss
new file mode 100644
index 0000000000..0aba279479
--- /dev/null
+++ b/docs/guides/component/_name_/_name_.scss
@@ -0,0 +1,4 @@
+
+md-_name_ {
+
+}
diff --git a/docs/guides/component/_name_/_name_.spec.js b/docs/guides/component/_name_/_name_.spec.js
new file mode 100644
index 0000000000..9f53246ed8
--- /dev/null
+++ b/docs/guides/component/_name_/_name_.spec.js
@@ -0,0 +1,11 @@
+
+describe('md-_name_', function() {
+
+ beforeEach(module('ngAria'));
+ beforeEach(module('material.components._name_'));
+
+ it('should ', function() {
+
+ });
+
+});
diff --git a/docs/guides/component/_name_/demoBasicUsage/index.html b/docs/guides/component/_name_/demoBasicUsage/index.html
new file mode 100644
index 0000000000..8cbbf57b10
--- /dev/null
+++ b/docs/guides/component/_name_/demoBasicUsage/index.html
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/docs/guides/component/_name_/demoBasicUsage/script.js b/docs/guides/component/_name_/demoBasicUsage/script.js
new file mode 100644
index 0000000000..88adf8a4b1
--- /dev/null
+++ b/docs/guides/component/_name_/demoBasicUsage/script.js
@@ -0,0 +1,7 @@
+
+angular.module('_name_Demo1', ['ngMaterial'])
+
+.controller('AppCtrl', function($scope) {
+
+
+});
diff --git a/src/components/whiteframe/README.md b/docs/guides/component/_name_/demoBasicUsage/style.css
similarity index 100%
rename from src/components/whiteframe/README.md
rename to docs/guides/component/_name_/demoBasicUsage/style.css
diff --git a/docs/gulpfile.js b/docs/gulpfile.js
index 2b94f1d3b7..3be1e259fc 100644
--- a/docs/gulpfile.js
+++ b/docs/gulpfile.js
@@ -1,3 +1,4 @@
+var gulp = require('gulp');
var Dgeni = require('dgeni');
var _ = require('lodash');
var concat = require('gulp-concat');
@@ -11,132 +12,172 @@ var sass = require('gulp-sass');
var through2 = require('through2');
var uglify = require('gulp-uglify');
var utils = require('../scripts/gulp-utils.js');
+var karma = require('karma').server;
+var argv = require('minimist')(process.argv.slice(2));
+var gutil = require('gulp-util');
+var series = require('stream-series');
var config = {
demoFolder: 'demo-partials'
};
-module.exports = function(gulp, IS_RELEASE_BUILD) {
- gulp.task('docs', ['docs-js', 'docs-css', 'docs-demo-scripts']);
-
- gulp.task('demos', function() {
- var demos = [];
- return generateDemos()
- .pipe(through2.obj(function(demo, enc, next) {
- // Don't include file contents into the docs app,
- // it saves space
- demo.css.concat(demo.js).concat(demo.html).concat(demo.index)
- .forEach(function(file) {
- delete file.contents;
- });
- demos.push(demo);
- next();
- }, function(done) {
- var demoIndex = _(demos)
- .groupBy('moduleName')
- .map(function(moduleDemos, moduleName) {
- return {
- name: moduleName,
- label: utils.humanizeCamelCase( moduleName.split('.').pop() ),
- demos: moduleDemos,
- url: '/demo/' + moduleName
- };
- })
- .value();
+gulp.task('demos', function() {
+ var demos = [];
+ return generateDemos()
+ .pipe(through2.obj(function(demo, enc, next) {
+ // Don't include file contents into the docs app,
+ // it saves space
+ demo.css.concat(demo.js).concat(demo.html).concat(demo.index)
+ .forEach(function(file) {
+ delete file.contents;
+ });
+ demos.push(demo);
+ next();
+ }, function(done) {
+ var demoIndex = _(demos)
+ .groupBy('moduleName')
+ .map(function(moduleDemos, moduleName) {
+ var componentName = moduleName.split('.').pop();
+ return {
+ name: componentName,
+ moduleName: moduleName,
+ label: utils.humanizeCamelCase(componentName),
+ demos: moduleDemos,
+ url: 'demo/' + componentName
+ };
+ })
+ .value();
- var dest = path.resolve(__dirname, '../dist/docs/js');
- var file = "angular.module('docsApp').constant('DEMOS', " +
- JSON.stringify(demoIndex, null, 2) + ");";
- mkdirp.sync(dest);
- fs.writeFileSync(dest + '/demo-data.js', file);
+ var dest = path.resolve(__dirname, '../dist/docs/js');
+ var file = "angular.module('docsApp').constant('DEMOS', " +
+ JSON.stringify(demoIndex, null, 2) + ");";
+ mkdirp.sync(dest);
+ fs.writeFileSync(dest + '/demo-data.js', file);
- done();
- }));
- });
+ done();
+ }));
+});
- function generateDemos() {
- return gulp.src('src/{components,services}/*/')
- .pipe(through2.obj(function(folder, enc, next) {
- var self = this;
- var split = folder.path.split(path.sep);
- var name = split.pop();
- var moduleName = 'material.' + split.pop() + '.' + name;
-
- utils.readModuleDemos(moduleName, function(demoId) {
- return lazypipe()
- .pipe(gulpif, /.css$/, transformCss(demoId))
- .pipe(gulp.dest, 'dist/docs/demo-partials/' + name)
- ();
- })
+function generateDemos() {
+ return gulp.src('src/{components,services}/*/')
+ .pipe(through2.obj(function(folder, enc, next) {
+ var self = this;
+ var split = folder.path.split(path.sep);
+ var name = split.pop();
+ var moduleName = 'material.' + split.pop() + '.' + name;
+
+ utils.copyDemoAssets(name, 'src/components/', 'dist/docs/demo-partials/');
+
+ utils.readModuleDemos(moduleName, function(demoId) {
+ return lazypipe()
+ .pipe(gulpif, /.css$/, transformCss(demoId))
+ .pipe(gulp.dest, 'dist/docs/demo-partials/' + name)
+ ();
+ })
.on('data', function(demo) {
self.push(demo);
})
.on('end', next);
- function transformCss(demoId) {
- return lazypipe()
- .pipe(through2.obj, function(file, enc, next) {
- file.contents = new Buffer(
- '.' + demoId + ' {\n' + file.contents.toString() + '\n}'
- );
- next(null, file);
- })
- .pipe(sass)
- ();
- }
- }));
- }
-
- gulp.task('docs-generate', ['build'], function() {
- var dgeni = new Dgeni([
- require('./config')
- ]);
- return dgeni.generate();
- });
+ function transformCss(demoId) {
+ return lazypipe()
+ .pipe(through2.obj, function(file, enc, next) {
+ file.contents = new Buffer(
+ '.' + demoId + ' {\n' + file.contents.toString() + '\n}'
+ );
+ next(null, file);
+ })
+ .pipe(sass)
+ ();
+ }
+ }));
+}
- gulp.task('docs-app', ['docs-generate'], function() {
- return gulp.src(['docs/app/**/*', '!docs/app/partials/**/*.html'])
- .pipe(gulp.dest('dist/docs'));
- });
+gulp.task('docs-generate', ['build'], function() {
+ var dgeni = new Dgeni([
+ require('./config')
+ ]);
+ return dgeni.generate();
+});
- gulp.task('docs-demo-scripts', ['demos'], function() {
- return gulp.src('dist/docs/demo-partials/**/*.js')
- .pipe(concat('docs-demo-scripts.js'))
- .pipe(gulp.dest('dist/docs'));
- });
+gulp.task('docs-app', ['docs-generate'], function() {
+ return gulp.src(['docs/app/**/*', '!docs/app/partials/**/*.html'])
+ .pipe(gulp.dest('dist/docs'));
+});
+
+gulp.task('docs-demo-scripts', ['demos'], function() {
+ return gulp.src('dist/docs/demo-partials/**/*.js')
+ .pipe(concat('docs-demo-scripts.js'))
+ .pipe(gulp.dest('dist/docs'));
+});
- gulp.task('docs-js', ['docs-app', 'docs-html2js', 'demos', 'build'], function() {
- return gulp.src([
- 'bower_components/angularytics/dist/angularytics.js',
- 'bower_components/hammerjs/hammer.js',
- 'dist/angular-material.js',
- 'dist/docs/js/**/*.js',
+gulp.task('docs-js-dependencies', ['build'], function() {
+ return gulp.src(['dist/angular-material.js', 'dist/angular-material.min.js'])
+ .pipe(gulp.dest('dist/docs'));
+});
+
+gulp.task('docs-js', ['docs-app', 'docs-html2js', 'demos', 'build', 'docs-js-dependencies'], function() {
+ var preLoadJs = ['docs/app/js/preload.js'];
+ if (process.argv.indexOf('--jquery') != -1) {
+ preLoadJs.push('node_modules/jquery/dist/jquery.js');
+ }
+
+ return series(
+ gulp.src([
+ 'node_modules/angularytics/dist/angularytics.js',
+ 'dist/docs/js/**/*.js'
])
.pipe(concat('docs.js'))
- .pipe(gulpif(IS_RELEASE_BUILD, uglify()))
- .pipe(gulp.dest('dist/docs'));
- });
+ .pipe(gulpif(!argv.dev, uglify())),
+ gulp.src(preLoadJs)
+ .pipe(concat('preload.js'))
+ .pipe(gulpif(!argv.dev, uglify()))
+ )
+ .pipe(gulp.dest('dist/docs'));
+});
- gulp.task('docs-css', ['docs-app', 'build'], function() {
- return gulp.src([
- 'dist/angular-material.css',
- 'dist/themes/*.css',
- 'docs/app/css/highlightjs-github.css',
- 'docs/app/css/layout-demo.css',
- 'docs/app/css/style.css'
- ])
- .pipe(concat('docs.css'))
- .pipe(gulp.dest('dist/docs'));
- });
+gulp.task('docs-css-dependencies', ['build'], function() {
+ return gulp.src([
+ 'dist/angular-material.css',
+ 'dist/angular-material.min.css'
+ ])
+ .pipe(gulp.dest('dist/docs'));
+});
- gulp.task('docs-html2js', function() {
- return gulp.src('docs/app/**/*.tmpl.html')
- .pipe(ngHtml2js({
- moduleName: 'docsApp',
- declareModule: false
- }))
- .pipe(concat('docs-templates.js'))
- .pipe(gulp.dest('dist/docs/js'));
- });
+gulp.task('docs-css', ['docs-app', 'build', 'docs-css-dependencies'], function() {
+ return gulp.src([
+ 'dist/themes/*.css',
+ 'docs/app/css/highlightjs-material.css',
+ 'docs/app/css/layout-demo.css',
+ 'docs/app/css/style.css'
+ ])
+ .pipe(concat('docs.css'))
+ .pipe(gulp.dest('dist/docs'));
+});
-};
+gulp.task('docs-html2js', function() {
+ return gulp.src('docs/app/**/*.tmpl.html')
+ .pipe(ngHtml2js({
+ moduleName: 'docsApp',
+ declareModule: false
+ }))
+ .pipe(concat('docs-templates.js'))
+ .pipe(gulp.dest('dist/docs/js'));
+});
+
+gulp.task('docs-karma', ['docs-js'], function(done) {
+ var karmaConfig = {
+ singleRun: true,
+ autoWatch: false,
+ browsers: argv.browsers ? argv.browsers.trim().split(',') : ['Chrome'],
+ configFile: __dirname + '/../config/karma-docs.conf.js'
+ };
+
+ karma.start(karmaConfig, function(exitCode) {
+ if (exitCode != 0) {
+ gutil.log(gutil.colors.red("Karma exited with the following exit code: " + exitCode));
+ process.exit(exitCode);
+ }
+ done();
+ });
+});
diff --git a/docs/spec/codepen.spec.js b/docs/spec/codepen.spec.js
new file mode 100644
index 0000000000..4f8a8f7b2d
--- /dev/null
+++ b/docs/spec/codepen.spec.js
@@ -0,0 +1,170 @@
+describe('CodepenDataAdapter', function() {
+
+ var codepenDataAdapter, demo, data, externalScripts;
+ beforeEach(module('docsApp'));
+
+ beforeEach(inject(function(_codepenDataAdapter_) {
+ codepenDataAdapter = _codepenDataAdapter_;
+ }));
+
+ beforeEach(function() {
+ externalScripts = [
+ 'http://some-url-to-external-js-files-required-for-codepen'
+ ];
+
+ demo = {
+ id: 'spec-demo',
+ title: 'demo-title',
+ module: 'demo-module',
+ files: {
+ index: {
+ contents: ''
+ },
+ html: [],
+ css: [
+ { contents: '.fake-class { color: red }' }
+ ],
+ js: [
+ { contents: 'angular.module("SomeOtherModule", ["Dependency1"]);' }
+ ]
+ }
+ }
+ });
+
+ describe('#translate', function() {
+
+ describe('the most common usage', function() {
+ beforeEach(function() {
+ data = codepenDataAdapter.translate(demo, externalScripts);
+ });
+
+ it('provides the title of the codepen', function() {
+ expect(data.title).toBe(demo.title);
+ });
+
+ it('includes the core angular css', function() {
+
+ // NOTE: the release script replaces this localhost reference with
+ // 'https://gitcdn.link/repo/angular/bower-material/master/angular-material.css'
+
+ expect(data.css_external).toBe('http://localhost:8080/angular-material.css');
+ });
+
+ it('includes the external js files, including the asset cache required to serve svgs to codepen', function() {
+
+ var expected = [
+ 'http://some-url-to-external-js-files-required-for-codepen',
+ 'http://localhost:8080/angular-material.js',
+ 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/t-114/assets-cache.js'
+ ].join(';');
+ expect(data.js_external).toBe(expected)
+ });
+
+ it('adds ng-app attribute to the index', function() {
+ expect(angular.element(data.html).attr('ng-app')).toBe('MyApp');
+ });
+
+ it('adds the demo id as a class attribute to the parent element on the index.html', function() {
+ expect(angular.element(data.html).hasClass(demo.id)).toBe(true);
+ });
+
+ it('replaces the demo module with the codepen module', function() {
+ expect(data.js).toBe("angular.module('MyApp');");
+ });
+
+ it('includes the demo css', function() {
+ expect(data.css).toBe('.fake-class { color: red }');
+ });
+ });
+
+ describe('when html templates are included in the demo', function() {
+
+ var template, $script;
+ beforeEach(function() {
+ template = {
+ name: 'template-name',
+ contents: "{{bar}}
"
+ };
+
+ demo.files.html.push(template);
+
+ data = codepenDataAdapter.translate(demo, externalScripts);
+
+ script = angular.element(data.html).find('script');
+ });
+
+ it('appends the template to the index', function() {
+ expect(script.html()).toBe(template.contents);
+ });
+
+ it('adds the ngTemplate to the script tag', function() {
+ expect(script.attr('type')).toBe('text/ng-template');
+ });
+
+ it('adds the template name as the id', function() {
+ expect(script.attr('id')).toBe(template.name);
+ });
+ });
+
+ describe('when the demo html includes a block', function() {
+
+ it('escapes the ampersand, so that codepen does not unescape the angle brackets', function() {
+ demo.files.index.contents = '>md-autocomplete<
';
+ data = codepenDataAdapter.translate(demo, externalScripts);
+ expect(angular.element(data.html).html()).toBe('>md-autocomplete<');
+ });
+
+ it('handles multiple code blocks', function() {
+ demo.files.index.contents = '>md-autocomplete<>md-input<
';
+ data = codepenDataAdapter.translate(demo, externalScripts);
+ expect(angular.element(data.html).html()).toBe('>md-autocomplete<>md-input<');
+ });
+
+ });
+
+ describe('when the html example includes ', function() {
+
+ it('escapes the ampersand, so that the codepen does not translate to an invalid character', function() {
+ demo.files.index.contents = '
';
+ data = codepenDataAdapter.translate(demo, externalScripts);
+ expect(angular.element(data.html).html()).toBe(' ');
+ });
+ });
+
+ describe('when the module definition in the js file is formatted in different ways', function() {
+
+ it('handles second argument on a new line', function() {
+ var script = "angular.module('test',\n \
+[]);";
+ demo.files.js = [{ contents: script }];
+
+ data = codepenDataAdapter.translate(demo, externalScripts);
+ expect(data.js).toBe("angular.module('MyApp');");
+ });
+
+ it('handles dependencies on new lines', function() {
+ var script = "angular.module('test', [\n \
+'Dep1',\n \
+'Dep2',\n \
+]);";
+ demo.files.js = [{ contents: script }];
+
+ data = codepenDataAdapter.translate(demo, externalScripts);
+ expect(data.js).toBe("angular.module('MyApp');");
+ });
+
+ it('handles module on a new line', function() {
+ var script = "angular\n\
+.module('test', [\n \
+'Dep1',\n \
+'Dep2',\n \
+]);";
+ demo.files.js = [{ contents: script }];
+
+ data = codepenDataAdapter.translate(demo, externalScripts);
+ expect(data.js).toBe("angular\n\
+.module('MyApp');");
+ });
+ });
+ });
+});
diff --git a/docs/spec/demo.spec.js b/docs/spec/demo.spec.js
new file mode 100644
index 0000000000..e72d087d7e
--- /dev/null
+++ b/docs/spec/demo.spec.js
@@ -0,0 +1,78 @@
+describe('docsDemo', function() {
+
+ beforeEach(module('docsApp', 'ngMaterial'));
+
+ var codepen, element, $httpBackend, demoModel;
+
+ beforeEach(inject(function($rootScope, $compile, $q, _codepen_, _$httpBackend_) {
+ codepen = _codepen_;
+ $httpBackend = _$httpBackend_;
+
+ spyOn(codepen, 'editOnCodepen');
+
+ stubHttpRequestsForImages();
+
+ var filePromise = $q.defer();
+
+ filePromise.resolve('');
+
+ $rootScope.demo = demoModel = {
+ id: 'id',
+ name: 'name',
+ moduleName: 'moduleName',
+ $files: [
+ { name: 'index.html', httpPromise: filePromise.promise }
+ ]
+ };
+
+ element = $compile("")($rootScope);
+
+ $rootScope.$digest();
+ }));
+
+ describe('clicking the edit on codepen button', function() {
+
+ beforeEach(function() {
+ var codepenButton = element.find('button').eq(1);
+ codepenButton.triggerHandler({type: 'click'});
+ });
+
+ it('sends codepen the demo information', function() {
+ expect(codepen.editOnCodepen).toHaveBeenCalled();
+ });
+
+ describe('demo information supplied to codepen', function() {
+
+ var demo;
+ beforeEach(function() {
+ demo = codepen.editOnCodepen.calls.mostRecent().args[0];
+ });
+
+ it('includes the title of the demo', function() {
+ expect(demo.title).toBe('basic-usage');
+ });
+
+ it('includes the demo id', function() {
+ expect(demo.id).toBe('1234');
+ });
+
+ it('includes the module name', function() {
+ expect(demo.module).toBe('foo-module');
+ });
+
+ it('includes the files for the demo', function() {
+ var index = demo.files.index;
+ expect(index.name).toBe('HTML');
+ expect(index.fileType).toBe('html');
+ expect(index.contents).toBe('');
+ });
+ });
+ });
+
+ function stubHttpRequestsForImages() {
+ $httpBackend.whenGET('img/icons/ic_visibility_24px.svg').respond('');
+ $httpBackend.expectGET('img/icons/ic_visibility_24px.svg');
+ $httpBackend.whenGET('img/icons/codepen-logo.svg').respond('');
+ $httpBackend.expectGET('img/icons/codepen-logo.svg');
+ };
+});
diff --git a/gulp/config.js b/gulp/config.js
new file mode 100644
index 0000000000..804bcc5d08
--- /dev/null
+++ b/gulp/config.js
@@ -0,0 +1,48 @@
+var args = require('minimist')(process.argv.slice(2));
+var VERSION = args.version || require('../package.json').version;
+
+module.exports = {
+ banner:
+ '/*!\n' +
+ ' * Angular Material Design\n' +
+ ' * https://github.com/angular/material\n' +
+ ' * @license MIT\n' +
+ ' * v' + VERSION + '\n' +
+ ' */\n',
+ jsBaseFiles: [
+ 'src/core/**/*.js'
+ ],
+ jsFiles: [
+ 'src/**/*.js',
+ '!src/**/*.spec.js'
+ ],
+ mockFiles : [
+ 'test/angular-material-mocks.js'
+ ],
+ themeBaseFiles: [
+ 'src/core/style/variables.scss',
+ 'src/core/style/mixins.scss'
+ ],
+ scssBaseFiles: [
+ 'src/core/style/color-palette.scss',
+ 'src/core/style/variables.scss',
+ 'src/core/style/mixins.scss',
+ 'src/core/style/structure.scss',
+ 'src/core/style/typography.scss',
+ 'src/core/style/layout.scss'
+ ],
+ scssLayoutAttributeFiles: [
+ 'src/core/style/variables.scss',
+ 'src/core/style/mixins.scss',
+ 'src/core/services/layout/layout-attributes.scss'
+ ],
+ scssPaths : [
+ 'src/components/**/*.scss',
+ 'src/core/services/**/*.scss'
+ ],
+ paths: 'src/{components, services}/**',
+ outputDir: 'dist/',
+ demoFolder: 'demo-partials'
+};
+
+
diff --git a/gulp/const.js b/gulp/const.js
new file mode 100644
index 0000000000..9fdf322ee1
--- /dev/null
+++ b/gulp/const.js
@@ -0,0 +1,35 @@
+var config = require('./config');
+var path = require('path');
+var args = require('minimist')(process.argv.slice(2));
+var utils = require('../scripts/gulp-utils.js');
+
+exports.ROOT = path.normalize(__dirname + '/..');
+exports.VERSION = args.version || require('../package.json').version;
+exports.LR_PORT = args.port || args.p || 8080;
+exports.IS_DEV = args.dev;
+exports.SHA = args.sha;
+exports.BUILD_MODE = getBuildMode();
+
+function getBuildMode () {
+ var mode = (args.module || args.m || args.c) ? 'demos' : args.mode;
+ switch (mode) {
+ case 'closure': return {
+ name: 'closure',
+ transform: utils.addClosurePrefixes,
+ outputDir: path.join(config.outputDir, 'modules/closure') + path.sep,
+ useBower: false
+ };
+ case 'demos': return {
+ name: 'demos',
+ transform: utils.addJsWrapper,
+ outputDir: path.join(config.outputDir, 'demos') + path.sep,
+ useBower: false
+ };
+ default: return {
+ name: 'default',
+ transform: utils.addJsWrapper,
+ outputDir: path.join(config.outputDir, 'modules/js') + path.sep,
+ useBower: true
+ };
+ }
+}
diff --git a/gulp/tasks/build-all-modules.js b/gulp/tasks/build-all-modules.js
new file mode 100644
index 0000000000..5f13b82dc5
--- /dev/null
+++ b/gulp/tasks/build-all-modules.js
@@ -0,0 +1,27 @@
+var BUILD_MODE = require('../const').BUILD_MODE;
+
+var gulp = require('gulp');
+var path = require('path');
+var through2 = require('through2');
+var series = require('stream-series');
+var util = require('../util');
+var gulpif = require('gulp-if');
+var utils = require('../../scripts/gulp-utils.js');
+
+exports.task = function() {
+ var isRelease = process.argv.indexOf('--release') != -1;
+ return gulp.src(['src/core/', 'src/components/*' ])
+ .pipe(through2.obj(function(folder, enc, next) {
+ var moduleId = folder.path.indexOf('components') > -1
+ ? 'material.components.' + path.basename(folder.path)
+ : 'material.' + path.basename(folder.path);
+ var stream = isRelease ?
+ series(
+ util.buildModule(moduleId, { minify: true, useBower: BUILD_MODE.useBower }),
+ util.buildModule(moduleId)
+ ) : util.buildModule(moduleId);
+ stream.on('end', function() { next(); });
+ }))
+ .pipe(BUILD_MODE.transform());
+
+};
diff --git a/gulp/tasks/build-demo.js b/gulp/tasks/build-demo.js
new file mode 100644
index 0000000000..dcb8d84e34
--- /dev/null
+++ b/gulp/tasks/build-demo.js
@@ -0,0 +1,7 @@
+var util = require('../util');
+
+exports.dependencies = ['build', 'build-module-demo'];
+
+exports.task = function() {
+ return util.buildModule(util.readModuleArg(), false);
+};
diff --git a/gulp/tasks/build-js.js b/gulp/tasks/build-js.js
new file mode 100644
index 0000000000..8a1485e10e
--- /dev/null
+++ b/gulp/tasks/build-js.js
@@ -0,0 +1,5 @@
+var util = require('../util');
+
+exports.task = function() {
+ return util.buildJs(true);
+};
diff --git a/gulp/tasks/build-module-demo.js b/gulp/tasks/build-module-demo.js
new file mode 100644
index 0000000000..5620e5984f
--- /dev/null
+++ b/gulp/tasks/build-module-demo.js
@@ -0,0 +1,41 @@
+var BUILD_MODE = require('../const').BUILD_MODE;
+var ROOT = require('../const').ROOT;
+
+var gulp = require('gulp');
+var gutil = require('gulp-util');
+var fs = require('fs');
+var path = require('path');
+var through2 = require('through2');
+var lazypipe = require('lazypipe');
+var sass = require('gulp-sass');
+var gulpif = require('gulp-if');
+var _ = require('lodash');
+
+var util = require('../util');
+var utils = require('../../scripts/gulp-utils.js');
+
+exports.task = function() {
+ var mod = util.readModuleArg();
+ var name = mod.split('.').pop();
+ var demoIndexTemplate = fs.readFileSync(
+ ROOT + '/docs/config/template/demo-index.template.html', 'utf8'
+ ).toString();
+
+ gutil.log('Building demos for', mod, '...');
+
+ return utils.readModuleDemos(mod, function() {
+ return lazypipe()
+ .pipe(gulpif, /.css$/, sass())
+ .pipe(gulpif, /.css$/, util.autoprefix())
+ .pipe(gulp.dest, BUILD_MODE.outputDir + name)
+ ();
+ })
+ .pipe(through2.obj(function(demo, enc, next) {
+ fs.writeFileSync(
+ path.resolve(BUILD_MODE.outputDir, name, demo.name, 'index.html'),
+ _.template(demoIndexTemplate)(demo)
+ );
+ next();
+ }));
+
+};
diff --git a/gulp/tasks/build-scss.js b/gulp/tasks/build-scss.js
new file mode 100644
index 0000000000..ec4cc65119
--- /dev/null
+++ b/gulp/tasks/build-scss.js
@@ -0,0 +1,88 @@
+var config = require('../config');
+var gulp = require('gulp');
+var gutil = require('gulp-util');
+var fs = require('fs');
+var path = require('path');
+var rename = require('gulp-rename');
+var filter = require('gulp-filter');
+var concat = require('gulp-concat');
+var series = require('stream-series');
+var util = require('../util');
+var sassUtils = require('../../scripts/gulp-utils');
+var sass = require('gulp-sass');
+var minifyCss = require('gulp-minify-css');
+var insert = require('gulp-insert');
+var gulpif = require('gulp-if');
+var args = util.args;
+var IS_DEV = require('../const').IS_DEV;
+
+exports.task = function() {
+ var streams = [];
+ var modules = args['modules'],
+ overrides = args['override'],
+ dest = args['output-dir'] || config.outputDir,
+ filename = args['filename'] || 'angular-material',
+ baseFiles = config.scssBaseFiles,
+ scssPipe = undefined;
+
+ gutil.log("Building css files...");
+
+ // create SCSS file for distribution
+ streams.push(
+ scssPipe = gulp.src(getPaths())
+ .pipe(util.filterNonCodeFiles())
+ .pipe(filter(['**', '!**/*-theme.scss']))
+ .pipe(filter(['**', '!**/*-print.scss']))
+ .pipe(filter(['**', '!**/*-attributes.scss']))
+ .pipe(concat('angular-material.scss'))
+ .pipe(gulp.dest(dest)) // raw uncompiled SCSSS
+ );
+
+ streams.push(
+ scssPipe
+ .pipe(sass())
+ .pipe(rename({extname: '.css'})) // unminified
+ .pipe(rename({ basename: filename }))
+ .pipe(util.autoprefix())
+ .pipe(insert.prepend(config.banner))
+ .pipe(gulp.dest(dest))
+ .pipe(gulpif(!IS_DEV, minifyCss()))
+ .pipe(rename({extname: '.min.css'}))
+ .pipe(gulp.dest(dest)) // minified
+ );
+
+ // Layout API using Attribute Selectors
+ // TO BE Deprecated...
+
+ streams.push(
+ gulp.src(config.scssLayoutAttributeFiles)
+ .pipe(concat('layouts.scss'))
+ .pipe(sassUtils.hoistScssVariables())
+ .pipe(sass())
+ .pipe(util.autoprefix())
+ .pipe(rename({ extname : '.css'}))
+ .pipe(rename({ prefix : 'angular-material.'}))
+ .pipe(insert.prepend(config.banner))
+ .pipe(gulp.dest(dest))
+ .pipe(gulpif(!IS_DEV, minifyCss()))
+ .pipe(rename({extname: '.min.css'}))
+ .pipe(gulp.dest(dest))
+ );
+
+ return series(streams);
+
+
+ function getPaths () {
+ var paths = config.scssBaseFiles.slice();
+ if ( modules ) {
+ paths.push.apply(paths, modules.split(',').map(function (module) {
+ return 'src/components/' + module + '/*.scss';
+ }));
+ } else {
+ paths.push('src/components/**/*.scss');
+ paths.push('src/core/services/layout/**/*.scss');
+ }
+ overrides && paths.unshift(overrides);
+ return paths;
+ }
+};
diff --git a/gulp/tasks/build.js b/gulp/tasks/build.js
new file mode 100644
index 0000000000..ba35b81dd7
--- /dev/null
+++ b/gulp/tasks/build.js
@@ -0,0 +1 @@
+exports.dependencies = ['build-scss', 'build-js'];
diff --git a/gulp/tasks/changelog.js b/gulp/tasks/changelog.js
new file mode 100644
index 0000000000..4c3f1b19f2
--- /dev/null
+++ b/gulp/tasks/changelog.js
@@ -0,0 +1,20 @@
+var fs = require('fs');
+var changelog = require('conventional-changelog');
+var util = require('../util');
+var ROOT = require('../const').ROOT;
+var SHA = require('../const').SHA;
+var VERSION = require('../const').VERSION;
+
+exports.task = function () {
+ var options = {
+ repository: 'https://github.com/angular/material',
+ version: VERSION,
+ file: 'CHANGELOG.md'
+ };
+ if (SHA) {
+ options.from = SHA;
+ }
+ changelog(options, function(err, log) {
+ fs.writeFileSync(ROOT + '/CHANGELOG.md', log);
+ });
+};
diff --git a/gulp/tasks/ddescribe-iit.js b/gulp/tasks/ddescribe-iit.js
new file mode 100644
index 0000000000..a3d4b9b26c
--- /dev/null
+++ b/gulp/tasks/ddescribe-iit.js
@@ -0,0 +1,97 @@
+var BUILD_MODE = require('../const').BUILD_MODE;
+
+var gulp = require('gulp');
+var PluginError = require('gulp-util').PluginError;
+var path = require('path');
+var through2 = require('through2');
+var series = require('stream-series');
+var util = require('../util');
+var gulpif = require('gulp-if');
+var utils = require('../../scripts/gulp-utils.js');
+
+var kDisallowedFunctions = [
+ // Allow xit/xdescribe --- disabling tests is okay
+ 'fit',
+ 'iit',
+ //'xit',
+ 'fdescribe',
+ 'ddescribe',
+ //'xdescribe',
+ 'describe.only',
+ 'it.only'
+];
+
+function disallowedIndex(largeString, disallowedString) {
+ var notFunctionName = '[^A-Za-z0-9$_]';
+ var regex = new RegExp('(^|' + notFunctionName + ')(' + disallowedString + ')' + notFunctionName + '*\\(', 'gm');
+ var match = regex.exec(largeString);
+ // Return the match accounting for the first submatch length.
+ return match != null ? match.index + match[1].length : -1;
+}
+
+function checkFile(fileContents, disallowed) {
+ var res = void 0;
+ if (Array.isArray(disallowed)) {
+ disallowed.forEach(function(str) {
+ var index = disallowedIndex(fileContents, str);
+ if (index !== -1) {
+ res = res || [];
+ res.push({
+ str: str,
+ line: fileContents.substr(0, index).split('\n').length,
+ index: index
+ });
+ }
+ });
+ }
+ return res;
+}
+
+exports.task = function() {
+ var failures = void 0;
+ return gulp.src(['src/**/*.spec.js', 'test/**/*-spec.js' ])
+ .pipe(through2.obj(function(file, enc, next) {
+ var errors = checkFile(file.contents.toString(), kDisallowedFunctions);
+ if (errors) {
+ failures = failures || [];
+ failures.push({
+ file: file,
+ contents: file.contents.toString(),
+ errors: errors
+ });
+ }
+ next();
+ }, function(callback) {
+ if (failures) {
+ var indented = true;
+ this.emit('error', new PluginError('ddescribe-iit', {
+ message: '\n' + failures.map(function(failure) {
+ var filename = path.relative(process.cwd(), failure.file.path);
+ var lines = failure.contents.split('\n');
+ var start = 0;
+ var starts = lines.map(function(line) { var s = start; start += line.length + 1; return s; });
+ return failure.errors.map(function(error) {
+ var line = lines[error.line - 1];
+ var start = starts[error.line - 1];
+ var col = (error.index - start);
+ var msg = ' `' + error.str + '` found at ' +filename + ':' + error.line + ':' + (col+1) + '\n' +
+ ' ' + line + '\n' +
+ ' ' + repeat(' ', col) + repeat('^', error.str.length);
+ return msg;
+ function repeat(c, len) {
+ var s = '';
+ if (len > 0) {
+ for (var i = 0; i < len; ++i) {
+ s += c;
+ }
+ }
+ return s;
+ }
+ }).join('\n\n');
+ }).join('\n\n'),
+ showStack: false
+ }));
+ }
+ callback();
+ }));
+};
diff --git a/gulp/tasks/default.js b/gulp/tasks/default.js
new file mode 100644
index 0000000000..1009eec8e4
--- /dev/null
+++ b/gulp/tasks/default.js
@@ -0,0 +1 @@
+exports.dependencies = ['build'];
diff --git a/gulp/tasks/docs.js b/gulp/tasks/docs.js
new file mode 100644
index 0000000000..4a689ed81b
--- /dev/null
+++ b/gulp/tasks/docs.js
@@ -0,0 +1,6 @@
+var gulp = require('gulp');
+var connect = require('gulp-connect');
+
+exports.dependencies = ['docs-js', 'docs-css', 'docs-demo-scripts'];
+
+exports.task = function () { gulp.src('.').pipe(connect.reload()); };
diff --git a/gulp/tasks/jshint.js b/gulp/tasks/jshint.js
new file mode 100644
index 0000000000..0628ac019b
--- /dev/null
+++ b/gulp/tasks/jshint.js
@@ -0,0 +1,15 @@
+var config = require('../config');
+var gulp = require('gulp');
+var jshint = require('gulp-jshint');
+
+exports.task = function() {
+ return gulp.src(config.jsFiles)
+ .pipe(jshint('.jshintrc'))
+ .pipe(jshint.reporter(require('jshint-summary')({
+ fileColCol: ',bold',
+ positionCol: ',bold',
+ codeCol: 'green,bold',
+ reasonCol: 'cyan'
+ })))
+ .pipe(jshint.reporter('fail'));
+};
diff --git a/gulp/tasks/karma-fast.js b/gulp/tasks/karma-fast.js
new file mode 100644
index 0000000000..548cece442
--- /dev/null
+++ b/gulp/tasks/karma-fast.js
@@ -0,0 +1,66 @@
+var gutil = require('gulp-util');
+var karma = require('karma').server;
+var util = require('../util');
+var ROOT = require('../const').ROOT;
+var Server = require('karma').Server;
+var karmaConfig = {
+ logLevel: 'warn',
+ singleRun: true,
+ autoWatch: false,
+ configFile: ROOT + '/config/karma.conf.js'
+};
+
+var args = util.args;
+
+// NOTE: `karma-fast` does NOT pre-make a full build of JS and CSS
+// exports.dependencies = ['build'];
+
+exports.task = function (done) {
+ var errorCount = 0;
+
+ if ( args.browsers ) {
+ karmaConfig.browsers = args.browsers.trim().split(',');
+ }
+ // NOTE: `karma-fast` does NOT test Firefox by default.
+
+ if ( args.reporters ) {
+ karmaConfig.reporters = args.reporters.trim().split(',');
+ }
+
+
+ gutil.log('Running unit tests on unminified source.');
+
+ karma = new Server(karmaConfig, captureError(clearEnv,clearEnv));
+ karma.start();
+
+
+ function clearEnv() {
+ process.env.KARMA_TEST_COMPRESSED = undefined;
+ process.env.KARMA_TEST_JQUERY = undefined;
+
+ if (errorCount > 0) { process.exit(errorCount); }
+ done();
+ }
+
+ /**
+ * For each version of testings (unminified, minified, minified w/ jQuery)
+ * capture the exitCode and update the error count...
+ *
+ * When all versions are done, report any errors that may manifest
+ * [e.g. perhaps in the minified tests]
+ *
+ * NOTE: All versions must pass before the CI server will announce 'success'
+ */
+ function captureError(next,done) {
+ return function(exitCode) {
+ if (exitCode != 0) {
+ gutil.log(gutil.colors.red("Karma exited with the following exit code: " + exitCode));
+ errorCount++;
+ }
+ // Do not process next set of tests if current set had >0 errors.
+ (errorCount > 0) && done() || next();
+ };
+ }
+
+
+};
diff --git a/gulp/tasks/karma-sauce.js b/gulp/tasks/karma-sauce.js
new file mode 100644
index 0000000000..556389385e
--- /dev/null
+++ b/gulp/tasks/karma-sauce.js
@@ -0,0 +1,6 @@
+var karma = require('karma').server;
+var ROOT = require('../const').ROOT;
+
+exports.task = function(done) {
+ karma.start(require(ROOT + '/config/karma-sauce.conf.js'), done);
+};
diff --git a/gulp/tasks/karma-watch.js b/gulp/tasks/karma-watch.js
new file mode 100644
index 0000000000..b679831b4d
--- /dev/null
+++ b/gulp/tasks/karma-watch.js
@@ -0,0 +1,18 @@
+var karma = require('karma').server;
+var ROOT = require('../const').ROOT;
+var args = require('../util').args;
+var Server = require('karma').Server;
+var config = {
+ singleRun: false,
+ autoWatch: true,
+ configFile: ROOT + '/config/karma.conf.js',
+ browsers : args.browsers ? args.browsers.trim().split(',') : ['Chrome']
+ };
+
+// Make full build of JS and CSS
+exports.dependencies = ['build'];
+
+exports.task = function(done) {
+ var server = new Server(config, done);
+ server. start();
+};
diff --git a/gulp/tasks/karma.js b/gulp/tasks/karma.js
new file mode 100644
index 0000000000..42f68c98d9
--- /dev/null
+++ b/gulp/tasks/karma.js
@@ -0,0 +1,74 @@
+var gutil = require('gulp-util');
+var karma = require('karma').server;
+var util = require('../util');
+var ROOT = require('../const').ROOT;
+var args = util.args;
+var Server = require('karma').Server;
+
+var karma; // karma server instance using `new Server()`
+var karmaConfig = {
+ logLevel: 'warn',
+ configFile: ROOT + '/config/karma.conf.js'
+};
+
+// Make full build of JS and CSS
+exports.dependencies = ['build'];
+
+exports.task = function (done) {
+ var errorCount = 0;
+
+ if ( args.browsers ) karmaConfig.browsers = args.browsers.trim().split(',');
+ if ( args.reporters ) karmaConfig.reporters = args.reporters.trim().split(',');
+ if ( args.config ) karmaConfig.configFile = ROOT + '/' + args.config.trim();
+
+
+ gutil.log(gutil.colors.blue('Running unit tests on unminified source.'));
+
+ karma = new Server( karmaConfig, captureError(testMinified,clearEnv));
+ karma.start();
+
+ function testMinified() {
+ gutil.log(gutil.colors.blue('Running unit tests on minified source.'));
+ process.env.KARMA_TEST_COMPRESSED = true;
+
+ karma = new Server(karmaConfig, captureError(testMinifiedJquery,clearEnv));
+ karma.start();
+ }
+
+ function testMinifiedJquery() {
+ gutil.log(gutil.colors.blue('Running unit tests on minified source w/ jquery.'));
+ process.env.KARMA_TEST_COMPRESSED = true;
+ process.env.KARMA_TEST_JQUERY = true;
+
+ karma = new Server(karmaConfig, done);
+ karma.start();
+ }
+
+ function clearEnv() {
+ process.env.KARMA_TEST_COMPRESSED = false;
+ process.env.KARMA_TEST_JQUERY = false;
+ }
+
+ /**
+ * For each version of testings (unminified, minified, minified w/ jQuery)
+ * capture the exitCode and update the error count...
+ *
+ * When all versions are done, report any errors that may manifest
+ * [e.g. perhaps in the minified tests]
+ *
+ * NOTE: All versions must pass before the CI server will announce 'success'
+ */
+ function captureError(next,abort) {
+ return function(exitCode, errorCode) {
+ if (exitCode != 0) {
+ errorCount++;
+ gutil.log(gutil.colors.red("Karma exited with the following exit code: " + exitCode));
+ if ( errorCode ) gutil.log(gutil.colors.red("Karma error code: " + errorCode));
+ }
+
+ if( errorCount > 0) process.exit(errorCount);
+ else next();
+ };
+ }
+
+};
diff --git a/gulp/tasks/server.js b/gulp/tasks/server.js
new file mode 100644
index 0000000000..dcfaff7bd4
--- /dev/null
+++ b/gulp/tasks/server.js
@@ -0,0 +1,20 @@
+var gulp = require('gulp');
+var webserver = require('gulp-webserver');
+var LR_PORT = require('../const').LR_PORT;
+var util = require('../util');
+
+exports.task = function() {
+ var openUrl = false;
+ if (typeof util.args.o === 'string' ||
+ typeof util.args.o === 'boolean') {
+ openUrl = util.args.o;
+ }
+ return gulp.src('.')
+ .pipe(webserver({
+ host: '0.0.0.0',
+ livereload: true,
+ port: LR_PORT,
+ directoryListing: true,
+ open: openUrl
+ }));
+};
diff --git a/gulp/tasks/site.js b/gulp/tasks/site.js
new file mode 100644
index 0000000000..6e9690de16
--- /dev/null
+++ b/gulp/tasks/site.js
@@ -0,0 +1,15 @@
+var connect = require('gulp-connect');
+var LR_PORT = require('../const').LR_PORT;
+
+exports.task = function () {
+ connect.server({
+ root: './dist/docs',
+ livereload: true,
+ port: LR_PORT,
+
+ // For any 404, respond with index.html. This enables html5Mode routing.
+ // In a production environment, this would be done with much more
+ // fine-grained URL rewriting rules.
+ fallback: './dist/docs/index.html'
+ });
+};
diff --git a/gulp/tasks/validate.js b/gulp/tasks/validate.js
new file mode 100644
index 0000000000..e70bf6359d
--- /dev/null
+++ b/gulp/tasks/validate.js
@@ -0,0 +1 @@
+exports.dependencies = ['jshint', 'ddescribe-iit', 'karma'];
diff --git a/gulp/tasks/watch-demo.js b/gulp/tasks/watch-demo.js
new file mode 100644
index 0000000000..0736244a2f
--- /dev/null
+++ b/gulp/tasks/watch-demo.js
@@ -0,0 +1,20 @@
+var gulp = require('gulp');
+var gutil = require('gulp-util');
+var util = require('../util');
+
+exports.dependencies = ['build-demo'];
+
+exports.task = function() {
+ var module = util.readModuleArg();
+ var name = module.split('.').pop();
+ var dir = "/dist/demos/"+name.trim();
+ gutil.log('\n',
+ '-- Rebuilding', dir, 'when source files change...\n',
+ '--', gutil.colors.green('Hint:'), 'Run',
+ gutil.colors.cyan('`gulp server`'),
+ 'to start a livereload server in root, then navigate to\n',
+ '--', gutil.colors.green('"dist/demos/' + name + '/"'), 'in your browser to develop.'
+ );
+
+ return gulp.watch('src/**/*', ['build-demo']);
+};
diff --git a/gulp/tasks/watch.js b/gulp/tasks/watch.js
new file mode 100644
index 0000000000..9023047d81
--- /dev/null
+++ b/gulp/tasks/watch.js
@@ -0,0 +1,7 @@
+var gulp = require('gulp');
+
+exports.dependencies = ['docs'];
+
+exports.task = function() {
+ gulp.watch(['docs/**/*', 'src/**/*'], ['docs']);
+};
diff --git a/gulp/util.js b/gulp/util.js
new file mode 100644
index 0000000000..2ac8211250
--- /dev/null
+++ b/gulp/util.js
@@ -0,0 +1,194 @@
+var config = require('./config');
+var gulp = require('gulp');
+var gutil = require('gulp-util');
+var frep = require('gulp-frep');
+var fs = require('fs');
+var args = require('minimist')(process.argv.slice(2));
+var path = require('path');
+var rename = require('gulp-rename');
+var filter = require('gulp-filter');
+var concat = require('gulp-concat');
+var autoprefixer = require('gulp-autoprefixer');
+var series = require('stream-series');
+var lazypipe = require('lazypipe');
+var glob = require('glob').sync;
+var uglify = require('gulp-uglify');
+var sass = require('gulp-sass');
+var plumber = require('gulp-plumber');
+var ngAnnotate = require('gulp-ng-annotate');
+var minifyCss = require('gulp-minify-css');
+var insert = require('gulp-insert');
+var gulpif = require('gulp-if');
+var constants = require('./const');
+var VERSION = constants.VERSION;
+var BUILD_MODE = constants.BUILD_MODE;
+var IS_DEV = constants.IS_DEV;
+var ROOT = constants.ROOT;
+var utils = require('../scripts/gulp-utils.js');
+
+exports.buildJs = buildJs;
+exports.autoprefix = autoprefix;
+exports.buildModule = buildModule;
+exports.filterNonCodeFiles = filterNonCodeFiles;
+exports.readModuleArg = readModuleArg;
+exports.themeBuildStream = themeBuildStream;
+exports.args = args;
+
+/**
+ * Builds the entire component library javascript.
+ * @param {boolean} isRelease Whether to build in release mode.
+ */
+function buildJs () {
+ var jsFiles = config.jsBaseFiles.concat([path.join(config.paths, '*.js')]);
+
+ gutil.log("building js files...");
+
+ var jsBuildStream = gulp.src( jsFiles )
+ .pipe(filterNonCodeFiles())
+ .pipe(utils.buildNgMaterialDefinition())
+ .pipe(plumber())
+ .pipe(ngAnnotate())
+ .pipe(utils.addJsWrapper(true));
+
+ var jsProcess = series(jsBuildStream, themeBuildStream() )
+ .pipe(concat('angular-material.js'))
+ .pipe(BUILD_MODE.transform())
+ .pipe(insert.prepend(config.banner))
+ .pipe(insert.append(';window.ngMaterial={version:{full: "' + VERSION +'"}};'))
+ .pipe(gulp.dest(config.outputDir))
+ .pipe(gulpif(!IS_DEV, uglify({ preserveComments: 'some' })))
+ .pipe(rename({ extname: '.min.js' }))
+ .pipe(gulp.dest(config.outputDir));
+
+ return series(jsProcess, deployMaterialMocks());
+
+ // Deploy the `angular-material-mocks.js` file to the `dist` directory
+ function deployMaterialMocks() {
+ return gulp.src(config.mockFiles)
+ .pipe(gulp.dest(config.outputDir));
+ }
+}
+
+function autoprefix () {
+ return autoprefixer({browsers: [
+ 'last 2 versions', 'last 4 Android versions'
+ ]});
+}
+
+function buildModule(module, opts) {
+ opts = opts || {};
+ if ( module.indexOf(".") < 0) {
+ module = "material.components." + module;
+ }
+ gutil.log('Building ' + module + (opts.isRelease && ' minified' || '') + ' ...');
+
+ var name = module.split('.').pop();
+ utils.copyDemoAssets(name, 'src/components/', 'dist/demos/');
+
+ var stream = utils.filesForModule(module)
+ .pipe(filterNonCodeFiles())
+ .pipe(gulpif('*.scss', buildModuleStyles(name)))
+ .pipe(gulpif('*.js', buildModuleJs(name)));
+ if (module === 'material.core') {
+ stream = splitStream(stream);
+ }
+ return stream
+ .pipe(BUILD_MODE.transform())
+ .pipe(insert.prepend(config.banner))
+ .pipe(gulpif(opts.minify, buildMin()))
+ .pipe(gulpif(opts.useBower, buildBower()))
+ .pipe(gulp.dest(BUILD_MODE.outputDir + name));
+
+ function splitStream (stream) {
+ var js = series(stream, themeBuildStream())
+ .pipe(filter('*.js'))
+ .pipe(concat('core.js'));
+ var css = stream.pipe(filter('*.css'));
+ return series(js, css);
+ }
+
+ function buildMin() {
+ return lazypipe()
+ .pipe(gulpif, /.css$/, minifyCss(),
+ uglify({ preserveComments: 'some' })
+ .on('error', function(e) {
+ console.log('\x07',e.message);
+ return this.end();
+ }
+ )
+ )
+ .pipe(rename, function(path) {
+ path.extname = path.extname
+ .replace(/.js$/, '.min.js')
+ .replace(/.css$/, '.min.css');
+ })
+ ();
+ }
+
+ function buildBower() {
+ return lazypipe()
+ .pipe(utils.buildModuleBower, name, VERSION)();
+ }
+
+ function buildModuleJs(name) {
+ var patterns = [
+ {
+ pattern: /\@ngInject/g,
+ replacement: 'ngInject'
+ }
+ ];
+ return lazypipe()
+ .pipe(plumber)
+ .pipe(ngAnnotate)
+ .pipe(frep, patterns)
+ .pipe(concat, name + '.js')
+ ();
+ }
+
+ function buildModuleStyles(name) {
+ var files = [];
+ config.themeBaseFiles.forEach(function(fileGlob) {
+ files = files.concat(glob(fileGlob, { cwd: ROOT }));
+ });
+ var baseStyles = files.map(function(fileName) {
+ return fs.readFileSync(fileName, 'utf8').toString();
+ }).join('\n');
+
+ return lazypipe()
+ .pipe(insert.prepend, baseStyles)
+ .pipe(gulpif, /theme.scss/,
+ rename(name + '-default-theme.scss'), concat(name + '.scss')
+ )
+ .pipe(sass)
+ .pipe(autoprefix)
+ (); // invoke the returning fn to create our pipe
+ }
+
+}
+
+function readModuleArg() {
+ var module = args.c ? 'material.components.' + args.c : (args.module || args.m);
+ if (!module) {
+ gutil.log('\nProvide a compnent argument via `-c`:',
+ '\nExample: -c toast');
+ gutil.log('\nOr provide a module argument via `--module` or `-m`.',
+ '\nExample: --module=material.components.toast or -m material.components.dialog');
+ process.exit(1);
+ }
+ return module;
+}
+
+function filterNonCodeFiles() {
+ return filter(function(file) {
+ return !/demo|module\.json|script\.js|\.spec.js|README/.test(file.path);
+ });
+}
+
+// builds the theming related css and provides it as a JS const for angular
+function themeBuildStream() {
+ return gulp.src( config.themeBaseFiles.concat(path.join(config.paths, '*-theme.scss')) )
+ .pipe(concat('default-theme.scss'))
+ .pipe(utils.hoistScssVariables())
+ .pipe(sass())
+ .pipe(utils.cssToNgConstant('material.core', '$MD_THEME_CSS'));
+}
diff --git a/gulpfile.js b/gulpfile.js
index 0df0dfada8..9d9aea4bb5 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -1,329 +1,20 @@
-
-var _ = require('lodash');
-var changelog = require('conventional-changelog');
-var fs = require('fs');
-var path = require('path');
-var glob = require('glob').sync;
var gulp = require('gulp');
-var karma = require('karma').server;
-var pkg = require('./package.json');
-var exec = require('child_process').exec;
-var mergeStream = require('merge-stream');
-
-var argv = require('minimist')(process.argv.slice(2));
-
-var autoprefixer = require('gulp-autoprefixer');
-var concat = require('gulp-concat');
-var filter = require('gulp-filter');
-var gulpif = require('gulp-if');
-var gutil = require('gulp-util');
-var insert = require('gulp-insert');
-var jshint = require('gulp-jshint');
-var lazypipe = require('lazypipe');
-var minifyCss = require('gulp-minify-css');
-var rename = require('gulp-rename');
-var sass = require('gulp-sass');
-var through2 = require('through2');
-var uglify = require('gulp-uglify');
-var webserver = require('gulp-webserver');
-
-var buildConfig = require('./config/build.config');
-var karma = require('karma').server;
-
-
-var utils = require('./scripts/gulp-utils.js');
-
-var IS_RELEASE_BUILD = !!argv.release;
-
-if (IS_RELEASE_BUILD) {
- console.log(
- gutil.colors.red('--release:'),
- 'Building release version (minified, debugs stripped)...'
- );
-}
-function readModuleArg() {
- var module = argv.module || argv.m;
- if (!module) {
- gutil.log('\nProvide a module argument via `--module` or `-m`.',
- '\nExample: --module=material.components.toast or -m material.components.dialog');
- process.exit(1);
- }
- return module;
-}
-
-require('./docs/gulpfile')(gulp, IS_RELEASE_BUILD);
-
-gulp.task('default', ['build']);
-//gulp.task('build', ['scripts', 'sass', 'sass-src']);
-gulp.task('validate', ['jshint', 'karma']);
-
-gulp.task('changelog', function(done) {
- changelog({
- repository: 'https://github.com/angular/material',
- version: pkg.version,
- file: 'CHANGELOG.md'
- }, function(err, log) {
- fs.writeFileSync(__dirname + '/CHANGELOG.md', log);
- });
-});
-
-/**
- * JSHint
- */
-gulp.task('jshint', function() {
- return gulp.src(
- buildConfig.paths.js.concat(buildConfig.paths.test)
- )
- .pipe(jshint('.jshintrc'))
- .pipe(jshint.reporter(require('jshint-summary')({
- fileColCol: ',bold',
- positionCol: ',bold',
- codeCol: 'green,bold',
- reasonCol: 'cyan'
- })))
- .pipe(jshint.reporter('fail'));
-});
-
-/**
- * Karma Tests
- */
-gulp.task('karma', function(done) {
- karma.start({
- singleRun:true,
- autoWatch:false,
- browsers : argv.browsers ? argv.browsers.trim().split(',') : ['Chrome'],
- configFile: __dirname + '/config/karma.conf.js'
- },done);
-});
-
-gulp.task('karma-watch', function(done) {
- karma.start({
- singleRun:false,
- autoWatch:true,
- configFile: __dirname + '/config/karma.conf.js'
- },done);
-});
-
-gulp.task('karma-sauce', function(done) {
- karma.start(require('./config/karma-sauce.conf.js'), done);
-});
-
-
-var config = {
- banner:
- '/*!\n' +
- ' * Angular Material Design\n' +
- ' * https://github.com/angular/material\n' +
- ' * @license MIT\n' +
- ' * v' + pkg.version + '\n' +
- ' */\n',
- jsBaseFiles: ['src/core/core.js', 'src/core/util/*.js'],
- themeBaseFiles: ['src/core/style/color-palette.scss', 'src/core/style/variables.scss', 'src/core/style/mixins.scss'],
- scssBaseFiles: ['src/core/style/color-palette.scss', 'src/core/style/variables.scss', 'src/core/style/mixins.scss', 'src/core/style/{structure,layout}.scss'],
- paths: 'src/{components,services}/**',
- outputDir: 'dist/'
-};
-
-
-
-/**
- * Project wide build related tasks
- */
-
-gulp.task('build', ['build-themes', 'build-scss', 'build-js'], function() {
-});
-
-gulp.task('watch', ['build'], function() {
- gulp.watch('src/**/*', ['build']);
-});
-
-var LR_PORT = argv.port || argv.p || 8080;
-gulp.task('watch-module', ['build', 'build-demo'], function() {
- var module = readModuleArg();
- var name = module.split('.').pop();
- gutil.log('\n',
- '-- Rebuilding', module, 'when source files change...\n',
- '--', gutil.colors.green('Hint:'), 'Run',
- gutil.colors.cyan('`gulp server`'),
- 'to start a livereload server in root, then navigate to\n',
- '--', gutil.colors.green('"dist/' + name + '/"'), 'in your browser to develop.'
- );
-
- return gulp.watch('src/**/*', ['build', 'build-demo']);
-});
-
-gulp.task('server', function() {
- return gulp.src('.')
- .pipe(webserver({
- livereload: true,
- port: LR_PORT,
- directoryListing: true
- }));
-});
-
-gulp.task('build-default-theme', function() {
- return gulp.src(config.themeBaseFiles.concat(path.join(config.paths, '*-theme.scss')))
- .pipe(concat('_default-theme.scss'))
- .pipe(utils.hoistScssVariables())
- .pipe(gulp.dest('themes/'));
-});
-
-gulp.task('build-theme', ['build-default-theme'], function() {
- var theme = argv.theme || argv.t || 'default';
- theme = theme.replace(/-theme$/, '');
- return buildTheme(theme);
-});
-
-gulp.task('build-themes', ['build-default-theme'], function() {
- var stream = mergeStream();
- var themes = glob('themes/**.scss', { cwd: __dirname }).filter(function(themeName) { return themeName.split('/')[1].charAt(0) != '_'; });
- themes.forEach(function(themeFile) {
- var name = themeFile.match(/((\w|-)+)-theme\.scss/)[1];
- stream.add(buildTheme(name));
- });
- return stream;
-});
-
-function buildTheme(theme) {
- gutil.log("Building theme " + theme + "...");
- return gulp.src(['src/core/style/color-palette.scss', 'themes/' + theme + '-theme.scss', 'themes/_default-theme.scss'])
- .pipe(concat(theme + '-theme.scss'))
- .pipe(utils.hoistScssVariables())
- .pipe(sass())
- .pipe(gulp.dest(config.outputDir + 'themes/'));
-}
-
-gulp.task('build-scss', ['build-default-theme'], function() {
- var defaultThemeContents = fs.readFileSync('themes/_default-theme.scss');
-
-
- var scssGlob = path.join(config.paths, '*.scss');
- gutil.log("Building css files...");
- return gulp.src(config.scssBaseFiles.concat(scssGlob))
- .pipe(filterNonCodeFiles())
- .pipe(filter(['**', '!**/*-theme.scss'])) // remove once ported
- .pipe(concat('angular-material.scss'))
- .pipe(insert.append(defaultThemeContents))
- .pipe(sass())
- .pipe(autoprefix())
- .pipe(insert.prepend(config.banner))
- .pipe(gulp.dest(config.outputDir))
- .pipe(gulpif(IS_RELEASE_BUILD, lazypipe()
- .pipe(minifyCss)
- .pipe(rename, {extname: '.min.css'})
- .pipe(gulp.dest, config.outputDir)
- ()
- ));
-});
-
-gulp.task('build-js', function() {
- var jsGlob = path.join(config.paths, '*.js');
- gutil.log("Building js files...");
- return gulp.src(config.jsBaseFiles.concat([jsGlob]))
- .pipe(filterNonCodeFiles())
- .pipe(utils.buildNgMaterialDefinition())
- .pipe(insert.wrap('(function() {\n', '})();\n'))
- .pipe(concat('angular-material.js'))
- .pipe(insert.prepend(config.banner))
- .pipe(gulp.dest(config.outputDir))
- .pipe(gulpif(IS_RELEASE_BUILD, lazypipe()
- .pipe(uglify)
- .pipe(rename, {extname: '.min.js'})
- .pipe(gulp.dest, config.outputDir)
- ()
- ));
-});
-
-/**
- * Module specific build tasks:
- * e.g.
- * ```sh
- * gulp build-module -m material.components.button
- * gulp watch-module -m material.components.button
- * gulp server
- * ```
- */
-gulp.task('build-module', function() {
- var mod = readModuleArg();
- var name = mod.split('.').pop();
-
- gutil.log('Building module', mod, '...');
- return utils.filesForModule(mod)
- .pipe(filterNonCodeFiles())
- .pipe(gulpif('*.scss', buildModuleStyles(name)))
- .pipe(gulpif('*.js', buildModuleJs(name)))
- .pipe(insert.prepend(config.banner))
- .pipe(gulpif(IS_RELEASE_BUILD, utils.buildModuleBower(name, pkg.version)))
- .pipe(gulp.dest(config.outputDir + name));
-});
-
-gulp.task('build-demo', function() {
- var mod = readModuleArg();
- var name = mod.split('.').pop();
- var demoIndexTemplate = fs.readFileSync(
- __dirname + '/docs/demos/demo-index.template.html', 'utf8'
- ).toString();
-
- gutil.log('Building demos for', mod, '...');
- return utils.readModuleDemos(mod, function() {
- return lazypipe()
- .pipe(gulpif, /.css$/, sass())
- .pipe(gulp.dest, config.outputDir + name)
- ();
- })
- .pipe(through2.obj(function(demo, enc, next) {
- fs.writeFileSync(
- path.resolve(config.outputDir, name, demo.name, 'index.html'),
- _.template(demoIndexTemplate, demo)
- );
- next();
- }));
-});
-
-function buildModuleStyles(name) {
- var files = [];
- config.themeBaseFiles.forEach(function(fileGlob) {
- files = files.concat(glob(fileGlob, { cwd: __dirname }));
- });
- var baseStyles = files.map(function(fileName) {
- return fs.readFileSync(fileName, 'utf8').toString();
- }).join('\n');
- return lazypipe()
- .pipe(insert.prepend, baseStyles)
- .pipe(gulpif, /theme.scss/,
- rename(name + '-default-theme.scss'), concat(name + '-core.scss')
- )
- .pipe(sass)
- .pipe(autoprefix)
- .pipe(gulpif, IS_RELEASE_BUILD, minifyCss())
- (); // invoke the returning fn to create our pipe
-}
-
-function buildModuleJs(name) {
- return lazypipe()
- .pipe(insert.wrap, '(function() {\n', '})();\n')
- .pipe(concat, name + '.js')
- .pipe(gulpif, IS_RELEASE_BUILD, uglify({preserveComments: 'some'}))
- ();
-}
-
-
-/**
- * Preconfigured gulp plugin invocations
- */
-
-function filterNonCodeFiles() {
- return filter(function(file) {
- if (/demo/.test(file.path)) return false;
- if (/README/.test(file.path)) return false;
- if (/module\.json/.test(file.path)) return false;
- if (/\.spec\.js/.test(file.path)) return false;
- return true;
- });
-}
+var fs = require('fs');
-function autoprefix() {
- return autoprefixer({browsers: [
- 'last 2 versions', 'last 3 Android versions'
- ]});
-}
+//-- include docs gulpfile (should eventually be factored out)
+require('./docs/gulpfile');
+
+//-- read in all files from gulp/tasks and create tasks for them
+fs.readdirSync('./gulp/tasks')
+ .filter(function (filename) {
+ return filename.match(/\.js$/i);
+ })
+ .map(function (filename) {
+ return {
+ name: filename.substr(0, filename.length - 3),
+ contents: require('./gulp/tasks/' + filename)
+ };
+ })
+ .forEach(function (file) {
+ gulp.task(file.name, file.contents.dependencies, file.contents.task);
+ });
diff --git a/material-icons b/material-icons
deleted file mode 160000
index dab0ab9e6e..0000000000
--- a/material-icons
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit dab0ab9e6eea003c9167cdaaa0287722e1556261
diff --git a/package.json b/package.json
index fd7ae2edd0..8ed635e745 100644
--- a/package.json
+++ b/package.json
@@ -1,52 +1,83 @@
{
- "name": "material-design",
- "version": "0.5.1",
+ "name": "angular-material-source",
+ "version": "1.0.5",
+ "description": "The Angular Material project is an implementation of Material Design in Angular.js. This project provides a set of reusable, well-tested, and accessible UI components based on the Material Design system. Similar to the Polymer project's Paper elements collection, Angular Material is supported internally at Google by the Angular.js, Material Design UX and other product teams.",
+ "keywords": "client-side, browser, material, material-design, design, angular, css, components, google",
+ "homepage": "https://material.angularjs.org",
+ "bugs": "https://github.com/angular/material/issues",
+ "license": "MIT",
+ "licenses": [
+ {
+ "type": "MIT",
+ "url": "https://github.com/angular/material/blob/master/LICENSE"
+ }
+ ],
"repository": {
+ "type": "git",
"url": "git://github.com/angular/material.git"
},
"devDependencies": {
- "batch": "^0.5.1",
+ "angular": "^1.4.8",
+ "angular-animate": "^1.4.8",
+ "angular-aria": "^1.4.8",
+ "angular-messages": "^1.4.8",
+ "angular-mocks": "^1.4.8",
+ "angular-route": "^1.4.8",
+ "angular-sanitize": "^1.4.8",
+ "angular-touch": "^1.4.8",
+ "angularytics": "^0.3.0",
"canonical-path": "0.0.2",
- "conventional-changelog": "0.0.9",
+ "cli-color": "^1.0.0",
+ "colors": "^1.1.0",
+ "conventional-changelog": "git://github.com/robertmesserle/conventional-changelog.git",
"dgeni": "^0.4.1",
"dgeni-packages": "^0.10.3",
"esprima": "^1.2.2",
- "event-stream": "^3.1.5",
"glob": "~4.0.2",
- "gulp": "^3.6.2",
+ "gulp": "^3.9.0",
"gulp-autoprefixer": "^1.0.1",
"gulp-concat": "^2.2.0",
+ "gulp-connect": "^2.2.0",
"gulp-filter": "^1.0.2",
- "gulp-footer": "^1.0.4",
- "gulp-header": "^1.0.2",
+ "gulp-frep": "^0.1.3",
"gulp-if": "^1.2.0",
"gulp-insert": "^0.4.0",
"gulp-jshint": "^1.5.5",
"gulp-minify-css": "^0.3.4",
- "gulp-minify-html": "^0.1.6",
+ "gulp-ng-annotate": "^0.5.2",
"gulp-ng-html2js": "^0.1.8",
+ "gulp-plumber": "^1.0.0",
"gulp-rename": "^1.2.0",
- "gulp-replace": "^0.3.0",
- "gulp-sass": "ajoslin/gulp-sass#master",
- "gulp-strip-debug": "^0.3.0",
- "gulp-uglify": "^0.3.0",
- "gulp-uncss": "^0.4.5",
+ "gulp-sass": "^2.0.4",
+ "gulp-uglify": "^1.2.0",
"gulp-util": "^3.0.1",
- "gulp-webserver": "^0.8.3",
+ "gulp-webserver": "^0.9.1",
+ "jasmine-core": "^2.2.0",
+ "jquery": "^2.1.3",
"jshint-summary": "^0.3.0",
- "karma": "^0.12.16",
- "karma-chrome-launcher": "^0.1.4",
+ "karma": "^0.13.15",
+ "karma-chrome-launcher": "^0.2.2",
"karma-firefox-launcher": "^0.1.3",
- "karma-jasmine": "^0.1.5",
- "karma-phantomjs-launcher": "~0.1.4",
+ "karma-jasmine": "^0.3.6",
+ "karma-phantomjs2-launcher": "0.4.0",
"karma-safari-launcher": "^0.1.1",
"karma-sauce-launcher": "^0.2.10",
"lazypipe": "^0.2.2",
- "lodash": "^2.4.1",
+ "lodash": "^3.0.0",
"merge-stream": "^0.1.6",
- "minimist": "^0.1.0",
+ "minimist": "^1.1.0",
"mkdirp": "^0.5.0",
+ "prompt-sync": "^1.0.0",
"q": "^1.0.1",
+ "stream-series": "^0.1.1",
"through2": "^0.6.3"
+ },
+ "scripts": {
+ "watch": "gulp watch site --dev",
+ "prom": "git pull --rebase origin master",
+ "current-branch": "git rev-parse --abbrev-ref HEAD",
+ "merge-base": "git merge-base $(npm run -s current-branch) origin/master",
+ "squash": "git rebase -i $(npm run -s merge-base)"
}
}
+
diff --git a/release b/release
new file mode 100755
index 0000000000..44f827b636
--- /dev/null
+++ b/release
@@ -0,0 +1 @@
+node --harmony release.js
diff --git a/release.js b/release.js
new file mode 100644
index 0000000000..492cbf38cd
--- /dev/null
+++ b/release.js
@@ -0,0 +1,460 @@
+(function () {
+ 'use strict';
+
+ var colors = require('colors');
+ var strip = require('cli-color/strip');
+ var fs = require('fs');
+ var prompt = require('prompt-sync');
+ var child_process = require('child_process');
+ var pkg = require('./package.json');
+ var oldVersion = pkg.version;
+ var abortCmds = [ 'git reset --hard', 'git checkout staging', 'rm abort push' ];
+ var pushCmds = [ 'rm abort push' ];
+ var cleanupCmds = [];
+ var defaultOptions = { encoding: 'utf-8' };
+ var origin = 'https://github.com/angular/material.git';
+ var lineWidth = 80;
+ var lastMajorVer = JSON.parse(exec('curl https://material.angularjs.org/docs.json')).latest;
+ var newVersion;
+ var dryRun;
+
+ header();
+ write('Is this a dry-run? {{"[yes/no]".cyan}} ');
+ dryRun = prompt() !== 'no';
+
+ if (dryRun) {
+ write('What would you like the old version to be? (default: {{oldVersion.cyan}}) ');
+ oldVersion = prompt() || oldVersion;
+ build();
+ } else if (validate()) {
+ build();
+ }
+
+ function build () {
+ newVersion = getNewVersion();
+
+ line();
+
+ checkoutVersionBranch();
+ updateVersion();
+ createChangelog();
+ commitChanges();
+ tagRelease();
+ cloneRepo('bower-material');
+ updateBowerVersion();
+ cloneRepo('code.material.angularjs.org');
+ updateSite();
+ updateMaster();
+ writeScript('abort', abortCmds.concat(cleanupCmds));
+ if (!dryRun) writeScript('push', pushCmds.concat(cleanupCmds));
+
+ line();
+ log('Your repo is ready to be pushed.');
+ log('Please look over {{"CHANGELOG.md".cyan}} and make any changes.');
+ log('When you are ready, please run "{{"./push".cyan}}" to finish the process.');
+ log('If you would like to cancel this release, please run "./abort"');
+ }
+
+ //-- utility methods
+
+ /** confirms that you will be able to perform the release before attempting */
+ function validate () {
+ if (exec('npm whoami') !== 'angularcore') {
+ err('You must be authenticated with npm as "angularcore" to perform a release.');
+ } else if (exec('git rev-parse --abbrev-ref HEAD') !== 'staging') {
+ err('Releases can only performed from "staging" at this time.');
+ } else {
+ return true;
+ }
+ function err (msg) {
+ var str = 'Error: ' + msg;
+ log(str.red);
+ }
+ }
+
+ /** creates the version branch and adds abort steps */
+ function checkoutVersionBranch () {
+ exec('git checkout -q -b release/{{newVersion}}');
+ abortCmds.push('git branch -D release/{{newVersion}}');
+ }
+
+ /** writes the new version to package.json */
+ function updateVersion () {
+ start('Updating {{"package.json".cyan}} version from {{oldVersion.cyan}} to {{newVersion.cyan}}...');
+ pkg.version = newVersion;
+ fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2));
+ done();
+ abortCmds.push('git checkout package.json');
+ pushCmds.push('git add package.json');
+ }
+
+ /** generates the changelog from the commits since the last release */
+ function createChangelog () {
+ start('Generating changelog from {{oldVersion.cyan}} to {{newVersion.cyan}}...');
+ exec([
+ 'git fetch --tags',
+ 'git checkout v{{lastMajorVer}} -- CHANGELOG.md',
+ 'gulp changelog --sha=$(git merge-base v{{lastMajorVer}} HEAD)'
+ ]);
+ done();
+ abortCmds.push('git checkout CHANGELOG.md');
+ pushCmds.push('git add CHANGELOG.md');
+ }
+
+ /** utility method for clearing the terminal */
+ function clear () {
+ write("\u001b[2J\u001b[0;0H");
+ }
+
+ /** prompts the user for the new version */
+ function getNewVersion () {
+ header();
+ var options = getVersionOptions(oldVersion), key, type, version;
+ log('The current version is {{oldVersion.cyan}}.');
+ log('');
+ log('What should the next version be?');
+ for (key in options) { log((+key + 1) + ') ' + options[ key ].cyan); }
+ log('');
+ write('Please select a new version: ');
+ type = prompt();
+
+ if (options[ type - 1 ]) version = options[ type - 1 ];
+ else if (type.match(/^\d+\.\d+\.\d+(-rc\d+)?$/)) version = type;
+ else throw new Error('Your entry was invalid.');
+
+ log('');
+ log('The new version will be ' + version.cyan + '.');
+ write('Is this correct? {{"[yes/no]".cyan}} ');
+ return prompt() === 'yes' ? version : getNewVersion();
+
+ function getVersionOptions (version) {
+ return version.match(/-rc\d+$/)
+ ? [ increment(version, 'rc'), increment(version, 'minor') ]
+ : [ increment(version, 'patch'), addRC(increment(version, 'minor')) ];
+
+ function increment (versionString, type) {
+ var version = parseVersion(versionString);
+ if (version.rc) {
+ switch (type) {
+ case 'minor':
+ version.rc = 0;
+ break;
+ case 'rc':
+ version.rc++;
+ break;
+ }
+ } else {
+ version[ type ]++;
+ //-- reset any version numbers lower than the one changed
+ switch (type) {
+ case 'minor':
+ version.patch = 0;
+ case 'patch':
+ version.rc = 0;
+ }
+ }
+ return getVersionString(version);
+
+ function parseVersion (version) {
+ var parts = version.split(/\.|\-rc/g);
+ return {
+ string: version,
+ major: parts[ 0 ],
+ minor: parts[ 1 ],
+ patch: parts[ 2 ],
+ rc: parts[ 3 ] || 0
+ };
+ }
+
+ function getVersionString (version) {
+ var str = version.major + '.' + version.minor + '.' + version.patch;
+ if (version.rc) str += '-rc' + version.rc;
+ return str;
+ }
+ }
+
+ function addRC (str) {
+ return str + '-rc1';
+ }
+ }
+ }
+
+ /** adds git tag for release and pushes to github */
+ function tagRelease () {
+ pushCmds.push(
+ 'git tag v{{newVersion}}',
+ 'git push {{origin}} HEAD',
+ 'git push --tags'
+ );
+ }
+
+ /** amends the commit to include local changes (ie. changelog) */
+ function commitChanges () {
+ start('Committing changes...');
+ exec('git commit -am "release: version {{newVersion}}"');
+ done();
+ pushCmds.push('git commit --amend --no-edit');
+ }
+
+ /** utility method for cloning github repos */
+ function cloneRepo (repo) {
+ start('Cloning ' + repo.cyan + ' from Github...');
+ exec('rm -rf ' + repo);
+ exec('git clone https://github.com/angular/' + repo + '.git --depth=1');
+ done();
+ cleanupCmds.push('rm -rf ' + repo);
+ }
+
+ // TODO: Remove this method and use template strings instead
+ /** utility method for inserting variables into a string */
+ function fill (str) {
+ return str.replace(/\{\{[^\}]+\}\}/g, function (match) {
+ return eval(match.substr(2, match.length - 4));
+ });
+ }
+
+ /** writes an array of commands to a bash script */
+ function writeScript (name, cmds) {
+ fs.writeFileSync(name, '#!/usr/bin/env bash\n\n' + fill(cmds.join('\n')));
+ exec('chmod +x ' + name);
+ }
+
+ /** updates the version for bower-material in package.json and bower.json */
+ function updateBowerVersion () {
+ start('Updating bower version...');
+ var options = { cwd: './bower-material' },
+ bower = require(options.cwd + '/bower.json'),
+ pkg = require(options.cwd + '/package.json');
+ //-- update versions in config files
+ bower.version = pkg.version = newVersion;
+ fs.writeFileSync(options.cwd + '/package.json', JSON.stringify(pkg, null, 2));
+ fs.writeFileSync(options.cwd + '/bower.json', JSON.stringify(bower, null, 2));
+ done();
+ start('Building bower files...');
+ //-- build files for bower
+ exec([
+ 'rm -rf dist',
+ 'gulp build',
+ 'gulp build-all-modules --mode=default',
+ 'gulp build-all-modules --mode=closure',
+ 'rm -rf dist/demos',
+ ]);
+ done();
+ start('Copy files into bower repo...');
+ //-- copy files over to bower repo
+ exec([
+ 'cp -Rf ../dist/* ./',
+ 'git add -A',
+ 'git commit -m "release: version {{newVersion}}"',
+ 'rm -rf ../dist'
+ ], options);
+ done();
+ //-- add steps to push script
+ pushCmds.push(
+ comment('push to bower (master and tag) and publish to npm'),
+ 'cd ' + options.cwd,
+ 'cp ../CHANGELOG.md .',
+ 'git add CHANGELOG.md',
+ 'git commit --amend --no-edit',
+ 'git tag -f v{{newVersion}}',
+ 'git pull --rebase',
+ 'git push',
+ 'git push --tags',
+ ( newVersion.indexOf('rc') < 0 ? 'npm publish' : '# skipped npm publish due to RC version' ),
+ 'cd ..'
+ );
+ }
+
+ /** builds the website for the new version */
+ function updateSite () {
+ start('Adding new version of the docs site...');
+ var options = { cwd: './code.material.angularjs.org' };
+ writeDocsJson();
+
+ //-- build files for bower
+ exec([
+ 'rm -rf dist',
+ 'gulp docs'
+ ]);
+ replaceFilePaths();
+
+ //-- copy files over to site repo
+ exec([
+ 'rm -rf ./*-rc*',
+ 'cp -Rf ../dist/docs {{newVersion}}',
+ 'rm -rf latest && cp -Rf ../dist/docs latest',
+ 'git add -A',
+ 'git commit -m "release: version {{newVersion}}"',
+ 'rm -rf ../dist'
+ ], options);
+ replaceBaseHref(newVersion);
+ replaceBaseHref('latest');
+
+ //-- update firebase.json file
+ writeFirebaseJson();
+ exec([ 'git commit --amend --no-edit -a' ], options);
+ done();
+
+ //-- add steps to push script
+ pushCmds.push(
+ comment('push the site'),
+ 'cd ' + options.cwd,
+ 'git pull --rebase --strategy=ours',
+ 'git push',
+ 'cd ..'
+ );
+
+ function writeFirebaseJson () {
+ fs.writeFileSync(options.cwd + '/firebase.json', getFirebaseJson());
+ function getFirebaseJson () {
+ var json = require(options.cwd + '/firebase.json');
+ json.rewrites = json.rewrites || [];
+ switch (json.rewrites.length) {
+ case 0:
+ json.rewrites.push(getRewrite('HEAD'));
+ case 1:
+ json.rewrites.push(getRewrite('latest'));
+ default:
+ json.rewrites.push(getRewrite(newVersion));
+ }
+ return JSON.stringify(json, null, 2);
+ function getRewrite (str) {
+ return {
+ source: '/' + str + '/**/!(*.@(js|html|css|json|svg|png|jpg|jpeg))',
+ destination: '/' + str + '/index.html'
+ };
+ }
+ }
+ }
+
+ function writeDocsJson () {
+ var config = require(options.cwd + '/docs.json');
+ config.versions = config.versions.filter(function (version) {
+ return version.indexOf('rc') < 0;
+ });
+ config.versions.unshift(newVersion);
+
+ //-- only set to default if not a release candidate
+ config.latest = newVersion;
+ fs.writeFileSync(options.cwd + '/docs.json', JSON.stringify(config, null, 2));
+ }
+ }
+
+ /** replaces localhost file paths with public URLs */
+ function replaceFilePaths () {
+ //-- handle docs.js
+ var path = __dirname + '/dist/docs/docs.js';
+ var file = fs.readFileSync(path);
+ var contents = file.toString()
+ .replace(/http:\/\/localhost:8080\/angular-material/g, 'https://cdn.gitcdn.link/cdn/angular/bower-material/v' + newVersion + '/angular-material')
+ .replace(/http:\/\/localhost:8080\/docs.css/g, 'https://material.angularjs.org/' + newVersion + '/docs.css');
+ fs.writeFileSync(path, contents);
+
+ }
+
+ /** replaces base href in index.html for new version as well as latest */
+ function replaceBaseHref (folder) {
+ //-- handle index.html
+ var path = __dirname + '/code.material.angularjs.org/' + folder + '/index.html';
+ var file = fs.readFileSync(path);
+ var contents = file.toString().replace(/base href="\//g, 'base href="/' + folder + '/');
+ fs.writeFileSync(path, contents);
+ }
+
+ /** copies the changelog back over to master branch */
+ function updateMaster () {
+ pushCmds.push(
+ comment('update package.json in master'),
+ 'git co master',
+ 'git pull --rebase {{origin}} master',
+ 'git checkout release/{{newVersion}} -- CHANGELOG.md',
+ 'node -e "' + stringifyFunction(buildCommand) + '"',
+ 'git add CHANGELOG.md',
+ 'git add package.json',
+ 'git commit -m "update version number in package.json to {{newVersion}}"',
+ 'git push'
+ );
+ function buildCommand () {
+ require('fs').writeFileSync('package.json', JSON.stringify(getUpdatedJson(), null, 2));
+ function getUpdatedJson () {
+ var json = require('./package.json');
+ json.version = '{{newVersion}}';
+ return json;
+ }
+ }
+
+ function stringifyFunction (method) {
+ return method
+ .toString()
+ .split('\n')
+ .slice(1, -1)
+ .map(function (line) { return line.trim(); })
+ .join(' ')
+ .replace(/"/g, '\\"');
+ }
+ }
+
+ /** utility method to output header */
+ function header () {
+ clear();
+ line();
+ log(center('Angular Material Release'));
+ line();
+ }
+
+ /** outputs a centered message in the terminal */
+ function center (msg) {
+ msg = ' ' + msg.trim() + ' ';
+ var length = msg.length;
+ var spaces = Math.floor((lineWidth - length) / 2);
+ return Array(spaces + 1).join('-') + msg.green + Array(lineWidth - msg.length - spaces + 1).join('-');
+ }
+
+ /** outputs done text when a task is completed */
+ function done () {
+ log('done'.green);
+ }
+
+ /** utility method for executing terminal commands */
+ function exec (cmd, userOptions) {
+ if (cmd instanceof Array) {
+ return cmd.map(function (cmd) { return exec(cmd, userOptions); });
+ }
+ try {
+ var options = Object.create(defaultOptions);
+ for (var key in userOptions) options[ key ] = userOptions[ key ];
+ return child_process.execSync(fill(cmd) + ' 2> /dev/null', options).toString().trim();
+ } catch (err) {
+ return err;
+ }
+ }
+
+ /** returns a commented message for use in bash scripts */
+ function comment (msg) {
+ return '\n# ' + msg + '\n';
+ }
+
+ /** prints the left side of a task while it is being performed */
+ function start (msg) {
+ var parsedMsg = fill(msg),
+ msgLength = strip(parsedMsg).length,
+ diff = lineWidth - 4 - msgLength;
+ write(parsedMsg + Array(diff + 1).join(' '));
+ }
+
+ /** outputs to the terminal with string variable replacement */
+ function log (msg) {
+ msg = msg || '';
+ console.log(fill(msg));
+ }
+
+ /** writes a message without a newline */
+ function write (msg) {
+ process.stdout.write(fill(msg));
+ }
+
+ /** prints a horizontal line to the terminal */
+ function line () {
+ log(Array(lineWidth + 1).join('-'));
+ }
+})();
diff --git a/scripts/bower-material-release.sh b/scripts/bower-material-release.sh
new file mode 100755
index 0000000000..551c43b680
--- /dev/null
+++ b/scripts/bower-material-release.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+
+ARG_DEFS=(
+ "--version=(.*)"
+)
+
+function run {
+ cd ../
+
+ echo "-- Building release..."
+ rm -rf dist
+ gulp build --release --version=$VERSION
+ gulp build-all-modules --release --mode=default --version=$VERSION
+ gulp build-all-modules --release --mode=closure --version=$VERSION
+ rm -rf dist/demos
+
+ echo "-- Cloning bower-material..."
+ rm -rf bower-material
+ git clone https://angular:$GH_TOKEN@github.com/angular/bower-material \
+ bower-material --depth=2
+
+ echo "-- Copying in build files..."
+ cp -Rf dist/* bower-material/
+
+ cd bower-material
+
+ echo "-- Committing and tagging..."
+ replaceJsonProp "bower.json" "version" "$VERSION"
+ replaceJsonProp "package.json" "version" "$VERSION"
+
+ git add -A
+ git commit -am "release: version $VERSION"
+ git tag -f v$VERSION
+
+ echo "-- Pushing to bower-material..."
+ git push -q origin master
+ git push -q origin v$VERSION
+
+ echo "-- Version $VERSION pushed successfully to angular/bower-material!"
+
+ cd ../
+}
+
+source $(dirname $0)/utils.inc
diff --git a/scripts/bower-release.sh b/scripts/bower-release.sh
deleted file mode 100755
index bd02006e5e..0000000000
--- a/scripts/bower-release.sh
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/bin/bash
-
-ARG_DEFS=(
- "--new-version=(.*)"
-)
-
-function run {
- cd ../
-
- echo "-- Building release..."
- gulp build --release
-
- echo "-- Cloning bower-material to dist..."
- rm -rf dist/bower-material
- git clone https://angular:$GH_TOKEN@github.com/angular/bower-material \
- dist/bower-material
-
- echo "-- Copying in build files..."
- cp dist/angular-material* dist/bower-material
- cp -R dist/themes dist/bower-material
-
- cd dist/bower-material
-
- echo "-- Committing and tagging..."
- replaceJsonProp "bower.json" "version" "$NEW_VERSION"
- git add -A
-
- git commit -am "release: version $NEW_VERSION"
- git tag -f v$NEW_VERSION
-
- echo "-- Pushing to bower-material..."
- git push -q origin master
- git push -q origin v$NEW_VERSION
-}
-
-source $(dirname $0)/utils.inc
diff --git a/scripts/build-asset-cache.sh b/scripts/build-asset-cache.sh
new file mode 100755
index 0000000000..62b61d055f
--- /dev/null
+++ b/scripts/build-asset-cache.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+# Intended to generate an object literal of SVG images
+# key - relative path to file.
+# value - contents of the SVG.
+#
+# See the following guide about the process to update the asset cache:
+# https://github.com/angular/material/blob/feature/edit-example-on-codepen/docs/guides/CODEPEN.md
+
+echo {
+for i in $(find docs/app/{img/icons,icons} -type f -name *.svg); do
+ filename=`echo $i | sed "s/docs\/app\///"`
+ contents=`cat $i | tr -d '\r\n'`
+ echo -e \'$filename\' : \'$contents\',
+done
+echo }
diff --git a/scripts/find-max-version.js b/scripts/find-max-version.js
new file mode 100755
index 0000000000..26270f0acf
--- /dev/null
+++ b/scripts/find-max-version.js
@@ -0,0 +1,94 @@
+// INPUT = $1 = VERSION
+// (something like 1.3 or 1.4.5)
+
+// OUTPUT = a specific version number
+// (something like 1.3.19 or 1.5.0-beta.2)
+
+// this is used so that we can run a `git`
+// command which is required to parse the
+// collection of tags so that we can figure out
+// the max version for the provided branch value
+var exec = require('child_process').exec;
+
+// this is the provided input value which could
+// be a general branch like `1.3` or a specific
+// version like `1.4.6`.
+var version = process.argv[2];
+if (!version) return;
+
+exec("git --git-dir ./tmp/angular.js/.git tag", function(error, output) {
+ var v = findMaxVersion(version, output);
+ if (v) {
+ // drop the `v` prefix from the version
+ // `v1.3.5` => `1.3.5`
+ //
+ // this will relay the version number over
+ // to the build script that used this
+ console.log(v.substr(1));
+ }
+});
+
+function findMaxVersion(branch, output) {
+ var lines;
+ var highestVersion;
+ var majorBranch;
+
+ // these weights are used to figure out which
+ // releases are more important than others
+ // (e.g. 1.3.0 > 1.3.0.beta.2)
+ var WEIGHTS = {
+ 'beta': 10,
+ 'rc': 1000,
+ 'stable': 1000000
+ };
+
+ // this happens if the user provides a specific
+ // version like `1.3.5` or `1.5.0-beta.2` instead
+ // of a general branch like `1.3` or `1.5`.
+ if (branch.match(/\./g).length > 1) {
+ lines = ['v' + branch];
+ majorBranch = branch.match(/^\d+\.\d+/)[0];
+ } else {
+ majorBranch = branch;
+ lines = output.split("\n");
+ }
+
+ var versionRegex = new RegExp('^v' + majorBranch + '.(\\d+)(?:-(beta|rc).(\\d+))?');
+
+ for (var i = lines.length - 1; i >= 0; i--) {
+ var line = lines[i];
+ var result = line.match(versionRegex);
+ if (result && result.length > 0) {
+ // stable releases have a higher weight than beta/RC versions
+ // so we want to include
+ var weight = result[1] * WEIGHTS.stable;
+
+ if (result[2]) {
+ // something like "1.3.0-beta.2" will have a weight
+ // of 20 while "1.3.0-rc.2" will have a weight of
+ // 2000 while "1.3.2" will have a weight of 2000000
+ // (which is calculated a few lines above here).
+ // we add "1" to the end incase we match something
+ // like "beta.0"
+ //
+ // so 1.3.0-beta.0 => 10 and 1.3.0-rc.0 => 1000
+ var multiplier = WEIGHTS[result[2]];
+ weight += multiplier * (result[3] + 1);
+ } else {
+ // this adds an extra 1 as well so that
+ // 1.3.0 => 1000000
+ weight += WEIGHTS.stable;
+ }
+
+ if (!highestVersion || highestVersion.weight < weight) {
+ highestVersion = { version: line, weight: weight };
+ }
+ }
+ }
+
+ // in the event that we don't figure out a version
+ // due to mismatched branches/tags we should not
+ // return anything. The build scripts will detect
+ // this and set a failing exit code.
+ return highestVersion && highestVersion.version;
+}
diff --git a/scripts/gulp-utils.js b/scripts/gulp-utils.js
index 7f95b9ef57..23ff51a655 100644
--- a/scripts/gulp-utils.js
+++ b/scripts/gulp-utils.js
@@ -4,16 +4,45 @@ var through2 = require('through2');
var lazypipe = require('lazypipe');
var gutil = require('gulp-util');
var Buffer = require('buffer').Buffer;
+var fs = require('fs');
var path = require('path');
-
-var getModuleInfo = require('../config/ngModuleData.js');
+var findModule = require('../config/ngModuleData.js');
exports.humanizeCamelCase = function(str) {
- return str.charAt(0).toUpperCase() +
- str.substring(1).replace(/[A-Z]/g, function($1) {
- return ' ' + $1.toUpperCase();
- });
+ switch (str) {
+ case 'fabSpeedDial':
+ return 'FAB Speed Dial';
+ case 'fabToolbar':
+ return 'FAB Toolbar';
+ default:
+ return str.charAt(0).toUpperCase() + str.substring(1).replace(/[A-Z]/g, function($1) {
+ return ' ' + $1.toUpperCase();
+ });
+ }
+};
+
+/**
+ * Copy all the demo assets to the dist directory
+ * NOTE: this excludes the modules demo .js,.css, .html files
+ */
+exports.copyDemoAssets = function(component, srcDir, distDir) {
+ gulp.src(srcDir + component + '/demo*/')
+ .pipe(through2.obj( copyAssetsFor ));
+
+ function copyAssetsFor( demo, enc, next){
+ var demoID = component + "/" + path.basename(demo.path);
+ var demoDir = demo.path + "/**/*";
+
+ var notJS = '!' + demoDir + '.js';
+ var notCSS = '!' + demoDir + '.css';
+ var notHTML= '!' + demoDir + '.html';
+
+ gulp.src([demoDir, notJS, notCSS, notHTML])
+ .pipe(gulp.dest(distDir + demoID));
+
+ next();
+ }
};
// Gives back a pipe with an array of the parsed data from all of the module's demos
@@ -28,16 +57,17 @@ exports.readModuleDemos = function(moduleName, fileTasks) {
var srcPath = demoFolder.path.substring(demoFolder.path.indexOf('src/') + 4);
var split = srcPath.split('/');
- var demo = {
+ var demo = {
+ ngModule: '',
id: demoId,
- css:[], html:[], js:[]
+ css:[], html:[], js:[]
};
- gulp.src(demoFolder.path + '**/*', { base: path.dirname(demoFolder.path) })
+ gulp.src(demoFolder.path + '/**/*', { base: path.dirname(demoFolder.path) })
.pipe(fileTasks(demoId))
.pipe(through2.obj(function(file, enc, cb) {
if (/index.html$/.test(file.path)) {
- demo.moduleName = moduleName,
+ demo.moduleName = moduleName;
demo.name = path.basename(demoFolder.path);
demo.label = exports.humanizeCamelCase(path.basename(demoFolder.path).replace(/^demo/, ''));
demo.id = demoId;
@@ -46,7 +76,7 @@ exports.readModuleDemos = function(moduleName, fileTasks) {
} else {
var fileType = path.extname(file.path).substring(1);
if (fileType == 'js') {
- demo.ngModule = demo.ngModule || getModuleInfo(file.contents.toString());
+ demo.ngModule = demo.ngModule || findModule.any(file.contents.toString());
}
demo[fileType] && demo[fileType].push(toDemoObject(file));
}
@@ -61,81 +91,164 @@ exports.readModuleDemos = function(moduleName, fileTasks) {
name: path.basename(file.path),
label: path.basename(file.path),
fileType: path.extname(file.path).substring(1),
- outputPath: 'demo-partials/' + name + '/' + path.basename(demoFolder.path) + '/' + path.basename(file.path),
+ outputPath: 'demo-partials/' + name + '/' + path.basename(demoFolder.path) + '/' + path.basename(file.path)
};
}
}));
};
var pathsForModules = {};
+
+exports.pathsForModule = function(name) {
+ return pathsForModules[name] || lookupPath();
+
+ function lookupPath() {
+ gulp.src('src/{services,components,core}/**/*')
+ .pipe(through2.obj(function(file, enc, next) {
+ var module = findModule.any(file.contents);
+ if (module && module.name == name) {
+ var modulePath = file.path.split(path.sep).slice(0, -1).join(path.sep);
+ pathsForModules[name] = modulePath + '/**';
+ }
+ next();
+ }));
+ return pathsForModules[name];
+ }
+}
+
exports.filesForModule = function(name) {
if (pathsForModules[name]) {
- return gulp.src(pathsForModules[name]);
+ return srcFiles(pathsForModules[name]);
} else {
- return gulp.src('src/{services,components}/**')
+ return gulp.src('src/{services,components,core}/**/*')
.pipe(through2.obj(function(file, enc, next) {
- var modName = getModuleInfo(file.contents).module;
- if (modName == name) {
+ var module = findModule.any(file.contents);
+ if (module && (module.name == name)) {
var modulePath = file.path.split(path.sep).slice(0, -1).join(path.sep);
pathsForModules[name] = modulePath + '/**';
var self = this;
- gulp.src(pathsForModules[name])
- .on('data', function(data) {
- self.push(data);
- });
+ srcFiles(pathsForModules[name]).on('data', function(data) {
+ self.push(data);
+ });
}
next();
}));
}
+
+ function srcFiles(path) {
+ return gulp.src(path)
+ .pipe(through2.obj(function(file, enc, next) {
+ if (file.stat.isFile()) next(null, file);
+ else next();
+ }));
+ }
+};
+
+exports.appendToFile = function(filePath) {
+ var bufferedContents;
+ return through2.obj(function(file, enc, next) {
+ bufferedContents = file.contents.toString('utf8') + '\n';
+ next();
+ }, function(done) {
+ var existing = fs.readFileSync(filePath, 'utf8');
+ bufferedContents = existing + '\n' + bufferedContents;
+ var outputFile = new gutil.File({
+ cwd: process.cwd(),
+ base: path.dirname(filePath),
+ path: filePath,
+ contents: new Buffer(bufferedContents)
+ });
+ this.push(outputFile);
+ done();
+ });
};
exports.buildNgMaterialDefinition = function() {
- var buffer = [];
+ var srcBuffer = [];
var modulesSeen = [];
+ var count = 0;
return through2.obj(function(file, enc, next) {
- var moduleName;
- if (moduleName = getModuleInfo(file.contents).module) {
- modulesSeen.push(moduleName);
- }
- buffer.push(file);
+ var module = findModule.material(file.contents);
+ if (module) modulesSeen.push(module.name);
+ srcBuffer.push(file);
next();
}, function(done) {
- var EXPLICIT_DEPS = ['ng', 'ngAnimate', 'ngAria'];
- var angularFileContents = "angular.module('ngMaterial', " + JSON.stringify(EXPLICIT_DEPS.concat(modulesSeen)) + ');';
+ var self = this;
+ var requiredLibs = ['ng', 'ngAnimate', 'ngAria'];
+ var dependencies = JSON.stringify(requiredLibs.concat(modulesSeen));
+ var ngMaterialModule = "angular.module('ngMaterial', " + dependencies + ');';
var angularFile = new gutil.File({
base: process.cwd(),
path: process.cwd() + '/ngMaterial.js',
- contents: new Buffer(angularFileContents)
+ contents: new Buffer(ngMaterialModule)
});
- this.push(angularFile);
- var self = this;
- buffer.forEach(function(file) {
+
+ // Elevate ngMaterial module registration to first in queue
+ self.push(angularFile);
+
+ srcBuffer.forEach(function(file) {
self.push(file);
});
- buffer = [];
+
+ srcBuffer = [];
done();
});
};
-exports.buildModuleBower = function(name, version) {
+function moduleNameToClosureName(name) {
+ return 'ng.' + name;
+}
+exports.addJsWrapper = function(enforce) {
return through2.obj(function(file, enc, next) {
+ var module = findModule.any(file.contents);
+ if (!!enforce || module) {
+ file.contents = new Buffer([
+ !!enforce ? '(function(){' : '(function( window, angular, undefined ){',
+ '"use strict";\n',
+ file.contents.toString(),
+ !!enforce ? '})();' : '})(window, window.angular);'
+ ].join('\n'));
+ }
this.push(file);
+ next();
+ });
+};
+exports.addClosurePrefixes = function() {
+ return through2.obj(function(file, enc, next) {
+ var module = findModule.any(file.contents);
+ if (module) {
+ var closureModuleName = moduleNameToClosureName(module.name);
+ var requires = (module.dependencies || []).sort().map(function(dep) {
+ return dep.indexOf(module.name) === 0 ? '' : 'goog.require(\'' + moduleNameToClosureName(dep) + '\');';
+ }).join('\n');
-
- var moduleInfo = getModuleInfo(file.contents);
- if (moduleInfo.module) {
- var bowerDeps = {};
+ file.contents = new Buffer([
+ 'goog.provide(\'' + closureModuleName + '\');',
+ requires,
+ file.contents.toString(),
+ closureModuleName + ' = angular.module("' + module.name + '");'
+ ].join('\n'));
+ }
+ this.push(file);
+ next();
+ });
+};
- moduleInfo.dependencies && moduleInfo.dependencies.forEach(function(dep) {
+exports.buildModuleBower = function(name, version) {
+ return through2.obj(function(file, enc, next) {
+ this.push(file);
+ var module = findModule.any(file.contents);
+ if (module) {
+ var bowerDeps = {};
+ (module.dependencies || []).forEach(function(dep) {
var convertedName = 'angular-material-' + dep.split('.').pop();
bowerDeps[convertedName] = version;
});
-
var bowerContents = JSON.stringify({
name: 'angular-material-' + name,
version: version,
dependencies: bowerDeps
- });
+ }, null, 2);
var bowerFile = new gutil.File({
base: file.base,
path: file.base + '/bower.json',
@@ -159,8 +272,7 @@ exports.hoistScssVariables = function() {
for( var currentLine = 0; currentLine < contents.length; ++currentLine) {
var line = contents[currentLine];
- if (openBlock || /^\s*\$/.test(line)) {
- if (openBlock) debugger;
+ if (openBlock || /^\s*\$/.test(line) && !/^\s+/.test(line)) {
openCount += (line.match(/\(/g) || []).length;
closeCount += (line.match(/\)/g) || []).length;
openBlock = openCount != closeCount;
@@ -173,3 +285,24 @@ exports.hoistScssVariables = function() {
next();
});
};
+
+exports.cssToNgConstant = function(ngModule, factoryName) {
+ return through2.obj(function(file, enc, next) {
+
+ var template = '(function(){ \nangular.module("%1").constant("%2", "%3"); \n})();\n\n';
+ var output = file.contents.toString().replace(/\n/g, '').replace(/\"/g,'\\"');
+
+ var jsFile = new gutil.File({
+ base: file.base,
+ path: file.path.replace('css', 'js'),
+ contents: new Buffer(
+ template.replace('%1', ngModule)
+ .replace('%2', factoryName)
+ .replace('%3', output)
+ )
+ });
+
+ this.push(jsFile);
+ next();
+ });
+};
diff --git a/scripts/release.sh b/scripts/release.sh
index 0a3b8f3525..b020392563 100755
--- a/scripts/release.sh
+++ b/scripts/release.sh
@@ -2,7 +2,6 @@
ARG_DEFS=(
"--version=(.*)"
- "[--optional=([A-Za-z])]"
)
function run {
@@ -15,40 +14,12 @@ function run {
exit 1
fi
- echo "-- Building release..."
- gulp build --release
+ ./scripts/bower-material-release.sh --version=$VERSION
- echo "-- Cloning bower-material to dist..."
- rm -rf dist/bower-material
- git clone https://angular:$GH_TOKEN@github.com/angular/bower-material \
- dist/bower-material
-
- echo "-- Copying in build files..."
- cp dist/angular-material* dist/bower-material
- cp -R dist/themes dist/bower-material
-
- cd dist/bower-material
-
- echo "-- Committing and tagging..."
- replaceJsonProp "bower.json" "version" "$VERSION"
-
- git add -A
- git commit -am "release: version $VERSION"
- git tag -f v$VERSION
-
- echo "-- Pushing to bower material..."
- git push -q origin master
- git push -q origin v$VERSION
-
- echo "-- Version $VERSION pushed successfully to angular/bower-material!"
-
- echo "-- Updating package.json & bower.json in angular-material main..."
- cd ../..
- replaceJsonProp "bower.json" "version" "$VERSION"
replaceJsonProp "package.json" "version" "$VERSION"
echo "-- Committing, tagging and pushing bower.json and package.json..."
- git commit bower.json package.json -m "release: version $VERSION"
+ git commit package.json -m "release: version $VERSION"
git tag -f v$VERSION
git push -q origin master
git push -q origin v$VERSION
diff --git a/scripts/snapshot-docs-site.sh b/scripts/snapshot-docs-site.sh
new file mode 100755
index 0000000000..a107877352
--- /dev/null
+++ b/scripts/snapshot-docs-site.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+
+ARG_DEFS=(
+ "--version=(.*)"
+)
+
+function run {
+ cd ../
+
+ echo "-- Building docs site release..."
+ rm -rf dist
+ gulp docs --release
+
+ echo "-- Cloning code.material.angularjs.org..."
+ rm -rf code.material.angularjs.org
+ git clone https://angular:$GH_TOKEN@github.com/angular/code.material.angularjs.org.git --depth=1
+
+ echo "-- Remove previous snapshot..."
+ rm -rf code.material.angularjs.org/HEAD
+
+ echo "-- Copying docs site to snapshot..."
+ sed -i "s,http://localhost:8080/angular-material,https://cdn.gitcdn.link/cdn/angular/bower-material/v$VERSION/angular-material,g" dist/docs/docs.js
+ sed -i "s,http://localhost:8080/docs.css,https://material.angularjs.org/$VERSION/docs.css,g" dist/docs/docs.js
+ sed -i "s,base href=\",base href=\"/HEAD,g" dist/docs/index.html
+
+
+ cp -Rf dist/docs code.material.angularjs.org/HEAD
+
+ cd code.material.angularjs.org
+
+ echo "-- Commiting snapshot..."
+ git add -A
+ git commit -m "snapshot: $VERSION"
+
+ echo "-- Pushing snapshot..."
+ git push -q origin master
+
+ cd ../
+
+ echo "-- Cleanup..."
+ rm -rf code.material.angularjs.org
+
+ echo "-- Successfully pushed the snapshot to angular/code.material.angularjs.org!!"
+}
+
+source $(dirname $0)/utils.inc
diff --git a/scripts/test-versions.sh b/scripts/test-versions.sh
new file mode 100755
index 0000000000..cf05d16fd1
--- /dev/null
+++ b/scripts/test-versions.sh
@@ -0,0 +1,133 @@
+# The purpose of this file is to download
+# assigned AngularJS source files and test
+# them against this build of AngularMaterial.
+
+# This works by pulling in all of the tags
+# from angular.js, finding the highest version
+# numbers for each branch (e.g. 1.3 => 1.3.X where
+# X is the highest patch release). For each
+# detected version it will then copy over each
+# of the source files to the node_modules/angular-X
+# folder and then run `gulp karma` to see if
+# they pass. If there are one or more failed tests
+# then this script will propagate a failed exit code.
+
+# [INPUT]
+# just run `./scripts/test-versions.sh`
+
+# [OUTPUT]
+# an exit code of "0" (passing) or "1" (failing)
+
+# [CONFIG VALUES]
+
+# Available Options are: 1.X, 1.X.X, 1.X.X-(beta|rc).X or snapshot
+VERSIONS=(1.3 1.4 1.5 snapshot)
+BROWSERS="Firefox,Chrome,Safari"
+
+#
+# DO NOT EDIT PAST THIS LINE
+#
+CDN="https://code.angularjs.org"
+FAILED=false
+ANGULAR_FILES=(
+ angular
+ angular-animate
+ angular-route
+ angular-aria
+ angular-messages
+ angular-mocks
+ angular-sanitize
+ angular-touch
+)
+
+if [ ${#VERSIONS[@]} == 0 ]; then
+ echo "Error: please specify one or more versions of AngularJS to test..."
+ exit 1
+fi;
+
+if [ ! -e ./tmp ]; then
+ mkdir -p ./tmp
+fi
+
+if [ ! -e ./tmp/angular.js ]; then
+ git clone https://github.com/angular/angular.js ./tmp/angular.js
+fi
+
+# this will guarantee that we have the latest versions
+# of AngularJS when testing material in case the HEAD
+# of ./tmp/angular.js is outdated.
+git --git-dir ./tmp/angular.js/.git fetch
+
+for VERSION in "${VERSIONS[@]}"; do
+ if [ $VERSION == "snapshot" ]; then
+ ZIP_FILE_SHA=$(curl "$CDN/snapshot/version.txt")
+ ZIP_URL="$CDN/snapshot/angular-$ZIP_FILE_SHA.zip"
+ else
+ LATEST_VERSION=$(node ./scripts/find-max-version.js $VERSION)
+ if [ ! $LATEST_VERSION ]; then
+ echo "Error: version "$VERSION" of angular does not exist..."
+ exit 1
+ fi
+
+ VERSION=$LATEST_VERSION
+ ZIP_FILE_SHA=$VERSION
+ ZIP_URL="$CDN/$VERSION/angular-$VERSION.zip"
+ fi
+
+ BASE_DIR="./tmp/angular-$VERSION"
+
+ if [ ! -d $BASE_DIR ]; then
+ ZIP_FILE="angular-$VERSION.zip"
+ ZIP_FILE_PATH="./tmp/$ZIP_FILE"
+
+ curl $ZIP_URL > $ZIP_FILE_PATH
+ unzip -d $BASE_DIR $ZIP_FILE_PATH
+ mv "$BASE_DIR/angular-$ZIP_FILE_SHA" "$BASE_DIR/files"
+ fi
+
+ echo "\n\n--- Testing AngularMaterial against AngularJS (${VERSION}) ---\n"
+
+ for ANGULAR_FILE in "${ANGULAR_FILES[@]}"; do
+ REPLACEMENT_FILE="$BASE_DIR/files/$ANGULAR_FILE.js"
+ MIN_REPLACEMENT_FILE="$BASE_DIR/files/$ANGULAR_FILE.min.js"
+
+ NODE_LIB_FILE="./node_modules/$ANGULAR_FILE/$ANGULAR_FILE.js"
+ MIN_NODE_LIB_FILE="./node_modules/$ANGULAR_FILE/$ANGULAR_FILE.min.js"
+
+ rm $NODE_LIB_FILE
+ cp $REPLACEMENT_FILE $NODE_LIB_FILE
+ echo "[copy] copied over $REPLACEMENT_FILE to $NODE_LIB_FILE"
+
+ if [ -e $MIN_NODE_LIB_FILE ]; then
+ rm $MIN_NODE_LIB_FILE
+ fi
+
+ if [ -e $MIN_REPLACEMENT_FILE ]; then
+ cp $MIN_REPLACEMENT_FILE $MIN_NODE_LIB_FILE
+ fi
+ echo "[copy] copied over $MIN_REPLACEMENT_FILE to $MIN_NODE_LIB_FILE"
+ done
+
+ echo "\n"
+ pwd
+ node ./node_modules/gulp/bin/gulp.js karma --config=config/karma-ci.conf.js --reporters='dots' --browsers=$BROWSERS
+ LAST_EXIT_CODE=$?
+
+ echo "\n\n--- Finished Testing AngularMaterial against AngularJS (${VERSION}) ---"
+
+ if [ $LAST_EXIT_CODE != "0" ]; then
+ echo "STATUS: FAILED"
+ FAILED=true
+ else
+ echo "STATUS: SUCCESS"
+ fi
+
+ echo "\n\n"
+done
+
+if [ $FAILED == true ]; then
+ echo "Error: One or more of the karma tests have failed..."
+ exit 1
+else
+ echo "All tests have passed successfully..."
+fi
diff --git a/scripts/travis-build-init.sh b/scripts/travis-build-init.sh
index 40d8b20e54..6bbcffb195 100755
--- a/scripts/travis-build-init.sh
+++ b/scripts/travis-build-init.sh
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/bin/bash
ARG_DEFS=(
"--sha=(.*)"
@@ -21,7 +21,8 @@ function run {
NEW_VERSION="$(readJsonProp "package.json" "version")-master-$(echo $SHA | head -c 7)"
- ./scripts/bower-release.sh --new-version=$NEW_VERSION
+ ./scripts/bower-material-release.sh --version=$NEW_VERSION
+ ./scripts/snapshot-docs-site.sh --version=$NEW_VERSION
}
source $(dirname $0)/utils.inc
diff --git a/src/components/animate/_effects.scss b/src/components/animate/_effects.scss
deleted file mode 100644
index 9d86b578f6..0000000000
--- a/src/components/animate/_effects.scss
+++ /dev/null
@@ -1,50 +0,0 @@
-// Button ripple: keep same opacity, but expand to 0.75 scale.
-// Then, fade out and expand to 2.0 scale.
-@keyframes inkRippleButton {
- 0% {
- transform: scale(0);
- opacity: 0.15;
- }
- 50% {
- transform: scale(0.75);
- opacity: 0.15;
- }
- 100% {
- transform: scale(2.0);
- opacity: 0;
- }
-}
-
-// Checkbox ripple: fully expand, then fade out.
-@keyframes inkRippleCheckbox {
- 0% {
- transform: scale(0);
- opacity: 0.4;
- }
- 50% {
- transform: scale(1.0);
- opacity: 0.4;
- }
- 100% {
- transform: scale(1.0);
- opacity: 0;
- }
-}
-
-/*
- * A container inside of a rippling element (eg a button),
- * which contains all of the individual ripples
- */
-.md-ripple-container {
- pointer-events: none;
- position: absolute;
- overflow: hidden;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
-}
-
-.md-ripple {
- position: absolute;
-}
diff --git a/src/components/animate/effects.js b/src/components/animate/effects.js
deleted file mode 100644
index cb661eb410..0000000000
--- a/src/components/animate/effects.js
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * @ngdoc module
- * @name material.components.animate
- * @description
- *
- * Ink and Popup Effects
- */
-angular.module('material.animations', ['material.core'])
- .service('$mdEffects', [
- '$rootElement',
- '$$rAF',
- '$sniffer',
- '$q',
- MdEffects
- ]);
-
-/*
- * @ngdoc service
- * @name $mdEffects
- * @module material.components.animate
- *
- * @description
- * The `$mdEffects` service provides a simple API for various
- * Material Design effects.
- *
- * @returns A `$mdEffects` object with the following properties:
- * - `{function(element,styles,duration)}` `inkBar` - starts ink bar
- * animation on specified DOM element
- * - `{function(element,parentElement,clickElement)}` `popIn` - animated show of element overlayed on parent element
- * - `{function(element,parentElement)}` `popOut` - animated close of popup overlay
- *
- */
-function MdEffects($rootElement, $$rAF, $sniffer, $q) {
-
- var webkit = /webkit/i.test($sniffer.vendorPrefix);
- function vendorProperty(name) {
- return webkit ?
- ('webkit' + name.charAt(0).toUpperCase() + name.substring(1)) :
- name;
- }
-
- var self;
- // Publish API for effects...
- return self = {
- popIn: popIn,
-
- /* Constants */
- TRANSITIONEND_EVENT: 'transitionend' + (webkit ? ' webkitTransitionEnd' : ''),
- ANIMATIONEND_EVENT: 'animationend' + (webkit ? ' webkitAnimationEnd' : ''),
-
- TRANSFORM: vendorProperty('transform'),
- 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')
- };
-
- // **********************************************************
- // API Methods
- // **********************************************************
- function popIn(element, parentElement, clickElement) {
- var deferred = $q.defer();
- parentElement.append(element);
-
- var startPos;
- if (clickElement) {
- var clickRect = clickElement[0].getBoundingClientRect();
- startPos = translateString(
- clickRect.left - element[0].offsetWidth,
- clickRect.top - element[0].offsetHeight,
- 0
- ) + ' scale(0.2)';
- } else {
- startPos = 'translate3d(0,100%,0) scale(0.5)';
- }
-
- element
- .css(self.TRANSFORM, startPos)
- .css('opacity', 0);
-
- $$rAF(function() {
- $$rAF(function() {
- element
- .addClass('md-active')
- .css(self.TRANSFORM, '')
- .css('opacity', '')
- .on(self.TRANSITIONEND_EVENT, finished);
- });
- });
-
- function finished(ev) {
- //Make sure this transitionend didn't bubble up from a child
- if (ev.target === element[0]) {
- element.off(self.TRANSITIONEND_EVENT, finished);
- deferred.resolve();
- }
- }
-
- return deferred.promise;
- }
-
- // **********************************************************
- // Utility Methods
- // **********************************************************
-
-
- function translateString(x, y, z) {
- return 'translate3d(' + Math.floor(x) + 'px,' + Math.floor(y) + 'px,' + Math.floor(z) + 'px)';
- }
-
-}
-
diff --git a/src/components/animate/inkCssRipple.js b/src/components/animate/inkCssRipple.js
deleted file mode 100644
index 34c69aa451..0000000000
--- a/src/components/animate/inkCssRipple.js
+++ /dev/null
@@ -1,164 +0,0 @@
-
-angular.module('material.animations')
-
-.directive('inkRipple', [
- '$mdInkRipple',
- InkRippleDirective
-])
-
-.factory('$mdInkRipple', [
- '$window',
- '$$rAF',
- '$mdEffects',
- '$timeout',
- '$mdUtil',
- InkRippleService
-]);
-
-function InkRippleDirective($mdInkRipple) {
- return function(scope, element, attr) {
- if (attr.inkRipple == 'checkbox') {
- $mdInkRipple.attachCheckboxBehavior(element);
- } else {
- $mdInkRipple.attachButtonBehavior(element);
- }
- };
-}
-
-function InkRippleService($window, $$rAF, $mdEffects, $timeout, $mdUtil) {
-
- return {
- attachButtonBehavior: attachButtonBehavior,
- attachCheckboxBehavior: attachCheckboxBehavior,
- attach: attach
- };
-
- function attachButtonBehavior(element) {
- return attach(element, {
- mousedown: true,
- center: false,
- animationDuration: 350,
- mousedownPauseTime: 175,
- animationName: 'inkRippleButton',
- animationTimingFunction: 'linear'
- });
- }
-
- function attachCheckboxBehavior(element) {
- return attach(element, {
- mousedown: true,
- center: true,
- animationDuration: 300,
- mousedownPauseTime: 180,
- animationName: 'inkRippleCheckbox',
- animationTimingFunction: 'linear'
- });
- }
-
- function attach(element, options) {
- // Parent element with noink attr? Abort.
- if (element.controller('noink')) return angular.noop;
- var contentParent = element.controller('mdContent');
-
- options = angular.extend({
- mousedown: true,
- hover: true,
- focus: true,
- center: false,
- animationDuration: 300,
- mousedownPauseTime: 150,
- animationName: '',
- animationTimingFunction: 'linear'
- }, options || {});
-
- var rippleContainer;
- var node = element[0];
- var hammertime = new Hammer(node);
-
- if (options.mousedown) {
- hammertime.on('hammer.input', onInput);
- }
-
- // Publish self-detach method if desired...
- return function detach() {
- hammertime.destroy();
- if (rippleContainer) {
- rippleContainer.remove();
- }
- };
-
- function rippleIsAllowed() {
- return !element[0].hasAttribute('disabled');
- }
-
- function createRipple(left, top, positionsAreAbsolute) {
-
- var rippleEl = angular.element('')
- .css($mdEffects.ANIMATION_DURATION, options.animationDuration + 'ms')
- .css($mdEffects.ANIMATION_NAME, options.animationName)
- .css($mdEffects.ANIMATION_TIMING, options.animationTimingFunction)
- .on($mdEffects.ANIMATIONEND_EVENT, function() {
- rippleEl.remove();
- });
-
- if (!rippleContainer) {
- rippleContainer = angular.element('
');
- element.append(rippleContainer);
- }
- rippleContainer.append(rippleEl);
-
- var containerWidth = rippleContainer.prop('offsetWidth');
-
- if (options.center) {
- left = containerWidth / 2;
- top = rippleContainer.prop('offsetHeight') / 2;
- } else if (positionsAreAbsolute) {
- var elementRect = node.getBoundingClientRect();
- left -= elementRect.left;
- top -= elementRect.top;
- }
-
- if (contentParent) {
- top += contentParent.$element.prop('scrollTop');
- }
-
- var css = {
- 'background-color': $window.getComputedStyle(rippleEl[0]).color ||
- $window.getComputedStyle(node).color,
- 'border-radius': (containerWidth / 2) + 'px',
-
- left: (left - containerWidth / 2) + 'px',
- width: containerWidth + 'px',
-
- top: (top - containerWidth / 2) + 'px',
- height: containerWidth + 'px'
- };
- css[$mdEffects.ANIMATION_DURATION] = options.fadeoutDuration + 'ms';
- rippleEl.css(css);
-
- return rippleEl;
- }
-
- var pauseTimeout;
- var rippleEl;
- function onInput(ev) {
- if (ev.eventType === Hammer.INPUT_START && ev.isFirst && rippleIsAllowed()) {
-
- rippleEl = createRipple(ev.center.x, ev.center.y, true);
- pauseTimeout = $timeout(function() {
- rippleEl && rippleEl.css($mdEffects.ANIMATION_PLAY_STATE, 'paused');
- }, options.mousedownPauseTime, false);
-
- rippleEl.on('$destroy', function() {
- rippleEl = null;
- });
-
- } else if (ev.eventType === Hammer.INPUT_END && ev.isFinal) {
- $timeout.cancel(pauseTimeout);
- rippleEl && rippleEl.css($mdEffects.ANIMATION_PLAY_STATE, '');
- }
- }
-
- }
-
-}
diff --git a/src/components/animate/noEffect.js b/src/components/animate/noEffect.js
deleted file mode 100644
index 3d8185ba7b..0000000000
--- a/src/components/animate/noEffect.js
+++ /dev/null
@@ -1,46 +0,0 @@
-angular.module('material.animations')
-
-/**
- * noink/nobar/nostretch directive: make any element that has one of
- * these attributes be given a controller, so that other directives can
- * `require:` these and see if there is a `no
` parent attribute.
- *
- * @usage
- *
- *
- *
- *
- *
- *
- *
- *
- * myApp.directive('detectNo', function() {
- * return {
- * require: ['^?noink', ^?nobar'],
- * link: function(scope, element, attr, ctrls) {
- * var noinkCtrl = ctrls[0];
- * var nobarCtrl = ctrls[1];
- * if (noInkCtrl) {
- * alert("the noink flag has been specified on an ancestor!");
- * }
- * if (nobarCtrl) {
- * alert("the nobar flag has been specified on an ancestor!");
- * }
- * }
- * };
- * });
- *
- */
-.directive({
- noink: attrNoDirective(),
- nobar: attrNoDirective(),
- nostretch: attrNoDirective()
-});
-
-function attrNoDirective() {
- return function() {
- return {
- controller: angular.noop
- };
- };
-}
diff --git a/src/components/autocomplete/autocomplete-theme.scss b/src/components/autocomplete/autocomplete-theme.scss
new file mode 100644
index 0000000000..a80fdd2d45
--- /dev/null
+++ b/src/components/autocomplete/autocomplete-theme.scss
@@ -0,0 +1,29 @@
+md-autocomplete.md-THEME_NAME-theme {
+ background: '{{background-50}}';
+ &[disabled] {
+ background: '{{background-100}}';
+ }
+ button {
+ md-icon {
+ path {
+ fill: '{{background-600}}';
+ }
+ }
+ &:after {
+ background: '{{background-600-0.3}}';
+ }
+ }
+}
+.md-autocomplete-suggestions-container.md-THEME_NAME-theme {
+ background: '{{background-50}}';
+ li {
+ color: '{{background-900}}';
+ .highlight {
+ color: '{{background-600}}';
+ }
+ &:hover,
+ &.selected {
+ background: '{{background-200}}';
+ }
+ }
+}
diff --git a/src/components/autocomplete/autocomplete.js b/src/components/autocomplete/autocomplete.js
new file mode 100644
index 0000000000..f2c657e6d1
--- /dev/null
+++ b/src/components/autocomplete/autocomplete.js
@@ -0,0 +1,12 @@
+/**
+ * @ngdoc module
+ * @name material.components.autocomplete
+ */
+/*
+ * @see js folder for autocomplete implementation
+ */
+angular.module('material.components.autocomplete', [
+ 'material.core',
+ 'material.components.icon',
+ 'material.components.virtualRepeat'
+]);
diff --git a/src/components/autocomplete/autocomplete.scss b/src/components/autocomplete/autocomplete.scss
new file mode 100644
index 0000000000..14371b9207
--- /dev/null
+++ b/src/components/autocomplete/autocomplete.scss
@@ -0,0 +1,240 @@
+$autocomplete-option-height: 48px;
+$input-container-padding: 2px !default;
+$input-error-height: 24px !default;
+
+@keyframes md-autocomplete-list-out {
+ 0% {
+ animation-timing-function: linear;
+ }
+ 50% {
+ opacity: 0;
+ height: 40px;
+ animation-timing-function: ease-in;
+ }
+ 100% {
+ height: 0;
+ opacity: 0;
+ }
+}
+
+@keyframes md-autocomplete-list-in {
+ 0% {
+ opacity: 0;
+ height: 0;
+ 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;
+ &[disabled] {
+ input {
+ cursor: default;
+ }
+ }
+ &[md-floating-label] {
+ border-radius: 0;
+ background: transparent;
+ height: auto;
+
+ md-input-container {
+ padding-bottom: $input-container-padding + $input-error-height;
+
+ // When we have ng-messages, remove the input error height from our bottom padding, since the
+ // ng-messages wrapper has a min-height of 1 error (so we don't adjust height as often; see
+ // input.scss file)
+ &.md-input-has-messages {
+ padding-bottom: $input-container-padding;
+ }
+ }
+ md-autocomplete-wrap {
+ height: auto;
+ }
+ button {
+ position: absolute;
+ top: auto;
+ bottom: 0;
+ right: 0;
+ width: 30px;
+ height: 30px;
+ }
+ }
+ md-autocomplete-wrap {
+ display: block;
+ position: relative;
+ overflow: visible;
+ height: 40px;
+ &.md-menu-showing {
+ z-index: $z-index-backdrop + 1;
+ }
+ md-progress-linear {
+ position: absolute;
+ bottom: -2px;
+ left: 0;
+ // When `md-inline` is present, we adjust the offset to go over the `ng-message` space
+ &.md-inline {
+ bottom: 40px;
+ right: 2px;
+ left: 2px;
+ width: auto;
+ }
+ .md-mode-indeterminate {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 3px;
+ transition: none;
+
+ .md-container {
+ transition: none;
+ height: 3px;
+ }
+ &.ng-enter {
+ transition: opacity 0.15s linear;
+ &.ng-enter-active {
+ opacity: 1;
+ }
+ }
+ &.ng-leave {
+ transition: opacity 0.15s linear;
+ &.ng-leave-active {
+ opacity: 0;
+ }
+ }
+ }
+ }
+ }
+ input:not(.md-input) {
+ @include md-flat-input();
+ width: 100%;
+ padding: 0 15px;
+ line-height: 40px;
+ height: 40px;
+ }
+ button {
+ position: relative;
+ line-height: 20px;
+ text-align: center;
+ width: 30px;
+ height: 30px;
+ cursor: pointer;
+ border: none;
+ border-radius: 50%;
+ padding: 0;
+ font-size: 12px;
+ background: transparent;
+ margin: auto 5px;
+ &:after {
+ content: '';
+ position: absolute;
+ top: -6px;
+ right: -6px;
+ bottom: -6px;
+ left: -6px;
+ border-radius: 50%;
+ transform: scale(0);
+ opacity: 0;
+ transition: $swift-ease-out;
+ }
+ &:focus {
+ outline: none;
+
+ &:after {
+ transform: scale(1);
+ opacity: 1;
+ }
+ }
+ md-icon {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate3d(-50%, -50%, 0) scale(0.9);
+ path {
+ stroke-width: 0;
+ }
+ }
+ &.ng-enter {
+ transform: scale(0);
+ transition: transform 0.15s ease-out;
+ &.ng-enter-active {
+ transform: scale(1);
+ }
+ }
+ &.ng-leave {
+ transition: transform 0.15s ease-out;
+ &.ng-leave-active {
+ transform: scale(0);
+ }
+ }
+ }
+ // IE Only
+ @media screen and (-ms-high-contrast: active) {
+ $border-color: #fff;
+
+ input {
+ border: 1px solid $border-color;
+ }
+ li:focus {
+ color: #fff;
+ }
+ }
+}
+
+.md-virtual-repeat-container.md-autocomplete-suggestions-container {
+ position: absolute;
+ box-shadow: 0 2px 5px rgba(black, 0.25);
+ height: 41px * 5.5;
+ max-height: 41px * 5.5;
+ z-index: $z-index-tooltip;
+}
+
+.md-virtual-repeat-container.md-not-found {
+ height: 48px;
+}
+
+.md-autocomplete-suggestions {
+ margin: 0;
+ list-style: none;
+ padding: 0;
+ li {
+ font-size: 14px;
+ overflow: hidden;
+ padding: 0 15px;
+ line-height: $autocomplete-option-height;
+ height: $autocomplete-option-height;
+ transition: background 0.15s linear;
+ margin: 0;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ &:focus {
+ outline: none;
+ }
+
+ &:not(.md-not-found-wrapper) {
+ cursor: pointer;
+ }
+ }
+}
+
+// IE Only
+@media screen and (-ms-high-contrast: active) {
+ md-autocomplete,
+ .md-autocomplete-suggestions {
+ border: 1px solid #fff;
+ }
+}
diff --git a/src/components/autocomplete/autocomplete.spec.js b/src/components/autocomplete/autocomplete.spec.js
new file mode 100644
index 0000000000..0d5634f2c6
--- /dev/null
+++ b/src/components/autocomplete/autocomplete.spec.js
@@ -0,0 +1,883 @@
+describe('', function() {
+
+ beforeEach(module('material.components.autocomplete'));
+
+ function compile(str, scope) {
+ var container;
+ inject(function($compile) {
+ container = $compile(str)(scope);
+ scope.$apply();
+ });
+ return container;
+ }
+
+ function createScope(items, obj, matchLowercase) {
+ var scope;
+ items = items || ['foo', 'bar', 'baz'].map(function(item) {
+ return {display: item};
+ });
+ inject(function($rootScope) {
+ scope = $rootScope.$new();
+ scope.match = function(term) {
+ return items.filter(function(item) {
+ return item.display.indexOf(matchLowercase ? term.toLowerCase() : term) === 0;
+ });
+ };
+ scope.searchText = '';
+ scope.selectedItem = null;
+ for (var key in obj) scope[key] = obj[key];
+ });
+ return scope;
+ }
+
+ function keydownEvent(keyCode) {
+ return {
+ keyCode: keyCode,
+ stopPropagation: angular.noop,
+ preventDefault: angular.noop
+ };
+ }
+
+ function waitForVirtualRepeat(element) {
+ // Because the autocomplete does not make the suggestions menu visible
+ // off the bat, the virtual repeat needs a couple more iterations to
+ // figure out how tall it is and then how tall the repeated items are.
+
+ // Using md-item-size would reduce this to a single flush, but given that
+ // autocomplete allows for custom row templates, it's better to measure
+ // rather than assuming a given size.
+ inject(function($material, $timeout) {
+ $material.flushOutstandingAnimations();
+ $timeout.flush();
+ });
+ }
+
+ describe('basic functionality', function() {
+ it('should update selected item and search text', inject(function($timeout, $mdConstant, $material) {
+ var scope = createScope();
+ var template = '\
+ \
+ {{item.display}}\
+ ';
+ var element = compile(template, scope);
+ var ctrl = element.controller('mdAutocomplete');
+ var ul = element.find('ul');
+
+ $material.flushInterimElement();
+
+ expect(scope.searchText).toBe('');
+ expect(scope.selectedItem).toBe(null);
+
+ // Focus the input
+ ctrl.focus();
+
+ // Update the scope
+ element.scope().searchText = 'fo';
+ waitForVirtualRepeat(element);
+
+ // Check expectations
+ expect(scope.searchText).toBe('fo');
+ expect(scope.match(scope.searchText).length).toBe(1);
+
+ expect(ul.find('li').length).toBe(1);
+
+ // Run our key events
+ ctrl.keydown(keydownEvent($mdConstant.KEY_CODE.DOWN_ARROW));
+ ctrl.keydown(keydownEvent($mdConstant.KEY_CODE.ENTER));
+ $timeout.flush();
+
+ // Check expectations again
+ expect(scope.searchText).toBe('foo');
+ expect(scope.selectedItem).toBe(scope.match(scope.searchText)[0]);
+
+ element.remove();
+ }));
+
+ // @TODO - re-enable test
+ xit('should allow receiving focus on the autocomplete', function() {
+ var scope = createScope(null, {inputId: 'custom-input-id'});
+ var template = '' +
+ '{{item.display}}' +
+ '';
+ var element = compile(template, scope);
+ var focusSpy = jasmine.createSpy('focus');
+
+ document.body.appendChild(element[0]);
+
+ element.on('focus', focusSpy);
+
+ element.focus();
+
+ expect(focusSpy).toHaveBeenCalled();
+ });
+
+ it('should allow you to set an input id without floating label', inject(function() {
+ var scope = createScope(null, {inputId: 'custom-input-id'});
+ var template = '\
+ \
+ {{item.display}}\
+ ';
+ var element = compile(template, scope);
+ var input = element.find('input');
+
+ expect(input.attr('id')).toBe(scope.inputId);
+
+ element.remove();
+ }));
+
+ it('should allow you to set an input id with floating label', inject(function() {
+ var scope = createScope(null, {inputId: 'custom-input-id'});
+ var template = '\
+ \
+ {{item.display}}\
+ ';
+ var element = compile(template, scope);
+ var input = element.find('input');
+
+ expect(input.attr('id')).toBe(scope.inputId);
+
+ element.remove();
+ }));
+
+ it('should forward the tabindex to the input', inject(function() {
+ var scope = createScope(null, {inputId: 'custom-input-id'});
+ var template =
+ '' +
+ '{{item.display}}' +
+ '';
+
+ var element = compile(template, scope);
+ var input = element.find('input');
+
+ expect(input.attr('tabindex')).toBe('3');
+
+ element.remove();
+ }));
+
+ it('should always set the tabindex of the autcomplete to `-1`', inject(function() {
+ var scope = createScope(null, {inputId: 'custom-input-id'});
+ var template =
+ '' +
+ '{{item.display}}' +
+ '';
+
+ var element = compile(template, scope);
+
+ expect(element.attr('tabindex')).toBe('-1');
+
+ element.remove();
+ }));
+
+ it('should clear value when hitting escape', inject(function($mdConstant, $timeout) {
+ var scope = createScope();
+ var template = '\
+ \
+ {{item.display}}\
+ ';
+ var element = compile(template, scope);
+ var input = element.find('input');
+ var ctrl = element.controller('mdAutocomplete');
+
+ expect(scope.searchText).toBe('');
+
+ scope.$apply('searchText = "test"');
+
+ expect(scope.searchText).toBe('test');
+
+ $timeout.flush();
+ scope.$apply(function() {
+ ctrl.keydown(keydownEvent($mdConstant.KEY_CODE.ESCAPE));
+ });
+
+ expect(scope.searchText).toBe('');
+
+ element.remove();
+ }));
+
+ it('should not close list on ENTER key if nothing is selected', inject(function($timeout, $mdConstant, $material) {
+ var scope = createScope();
+ var template = '\
+ \
+ {{item.display}}\
+ ';
+ var element = compile(template, scope);
+ var ctrl = element.controller('mdAutocomplete');
+ var ul = element.find('ul');
+
+ $material.flushInterimElement();
+
+ // Update the scope
+ element.scope().searchText = 'fo';
+ waitForVirtualRepeat(element);
+
+ // Focus the input
+ ctrl.focus();
+ $timeout.flush();
+
+ expect(ctrl.hidden).toBe(false);
+
+ // Run our key events
+ ctrl.keydown(keydownEvent($mdConstant.KEY_CODE.ENTER));
+ $timeout.flush();
+
+ // Check expectations again
+ expect(ctrl.hidden).toBe(false);
+
+ element.remove();
+ }));
+ });
+
+ describe('basic functionality with template', function() {
+ it('should update selected item and search text', inject(function($timeout, $material, $mdConstant) {
+ var scope = createScope();
+ var template = '\
+ \
+ \
+ {{item.display}}\
+ \
+ ';
+ var element = compile(template, scope);
+ var ctrl = element.controller('mdAutocomplete');
+ var ul = element.find('ul');
+
+ expect(scope.searchText).toBe('');
+ expect(scope.selectedItem).toBe(null);
+
+ $material.flushInterimElement();
+
+ // Focus the input
+ ctrl.focus();
+
+ element.scope().searchText = 'fo';
+ waitForVirtualRepeat(element);
+
+ expect(scope.searchText).toBe('fo');
+ expect(scope.match(scope.searchText).length).toBe(1);
+ expect(ul.find('li').length).toBe(1);
+
+ ctrl.keydown(keydownEvent($mdConstant.KEY_CODE.DOWN_ARROW));
+ ctrl.keydown(keydownEvent($mdConstant.KEY_CODE.ENTER));
+
+ $timeout.flush();
+
+ expect(scope.searchText).toBe('foo');
+ expect(scope.selectedItem).toBe(scope.match(scope.searchText)[0]);
+
+ element.remove();
+ }));
+
+ it('should compile the template against the parent scope', inject(function($timeout, $material) {
+ var scope = createScope(null, {bang: 'boom'});
+ var template =
+ '' +
+ ' ' +
+ ' {{bang}}' +
+ ' {{$index}}' +
+ ' {{item.display}}' +
+ ' ' +
+ '';
+ var element = compile(template, scope);
+ var ctrl = element.controller('mdAutocomplete');
+ var ul = element.find('ul');
+
+ $material.flushOutstandingAnimations();
+
+ expect(scope.bang).toBe('boom');
+
+ // Focus the input
+ ctrl.focus();
+
+ element.scope().searchText = 'fo';
+
+ // Run our initial flush
+ $timeout.flush();
+ waitForVirtualRepeat(element);
+
+ // Wait for the next tick when the values will be updated
+ $timeout.flush();
+
+ var li = ul.find('li')[0];
+
+ // Expect it to be compiled against the parent scope and have our variables copied
+ expect(li.querySelector('.find-parent-scope').innerHTML).toBe('boom');
+ expect(li.querySelector('.find-index').innerHTML).toBe('0');
+ expect(li.querySelector('.find-item').innerHTML).toBe('foo');
+
+ // Make sure we wrap up anything and remove the element
+ $timeout.flush();
+ element.remove();
+ }));
+
+ it('should ensure the parent scope digests along with the current scope', inject(function($timeout, $material) {
+ var scope = createScope(null, {bang: 'boom'});
+ var template =
+ '' +
+ ' ' +
+ ' {{bang}}' +
+ ' {{$index}}' +
+ ' {{item.display}}' +
+ ' ' +
+ '';
+ var element = compile(template, scope);
+ var ctrl = element.controller('mdAutocomplete');
+ var ul = element.find('ul');
+
+ $material.flushOutstandingAnimations();
+
+ // Focus the input
+ ctrl.focus();
+
+ element.scope().searchText = 'fo';
+
+ // Run our initial flush
+ $timeout.flush();
+ waitForVirtualRepeat(element);
+
+ // Wait for the next tick when the values will be updated
+ $timeout.flush();
+
+ var li = ul.find('li')[0];
+ var parentScope = angular.element(li.querySelector('.find-parent-scope')).scope();
+
+ // When the autocomplete item's scope digests, ensure that the parent
+ // scope does too.
+ parentScope.bang = 'big';
+ scope.$digest();
+
+ expect(li.querySelector('.find-parent-scope').innerHTML).toBe('big');
+
+ // Make sure we wrap up anything and remove the element
+ $timeout.flush();
+ element.remove();
+ }));
+
+ it('is hidden when no matches are found without an md-not-found template', inject(function($timeout, $material) {
+ var scope = createScope();
+ var template =
+ '' +
+ ' {{item.display}}' +
+ '';
+ var element = compile(template, scope);
+ var ctrl = element.controller('mdAutocomplete');
+
+ $material.flushOutstandingAnimations();
+
+ // Focus our input
+ ctrl.focus();
+
+ // Set our search text to a value that we know doesn't exist
+ scope.searchText = 'somethingthatdoesnotexist';
+
+ // Run our initial flush
+ $timeout.flush();
+ waitForVirtualRepeat(element);
+
+ // Wait for the next tick when the values will be updated
+ $timeout.flush();
+
+ // We should be hidden since no md-not-found template was provided
+ expect(ctrl.hidden).toBe(true);
+
+ // Make sure we wrap up anything and remove the element
+ $timeout.flush();
+ element.remove();
+ }));
+
+ it('is visible when no matches are found with an md-not-found template', inject(function($timeout, $material) {
+ var scope = createScope();
+ var template =
+ '' +
+ ' {{item.display}}' +
+ ' Sorry, not found...' +
+ '';
+ var element = compile(template, scope);
+ var ctrl = element.controller('mdAutocomplete');
+
+ $material.flushOutstandingAnimations();
+
+ // Focus our input
+ ctrl.focus();
+
+ // Set our search text to a value that we know doesn't exist
+ scope.searchText = 'somethingthatdoesnotexist';
+
+ // Run our initial flush
+ $timeout.flush();
+ waitForVirtualRepeat(element);
+
+ // Wait for the next tick when the values will be updated
+ $timeout.flush();
+
+ // We should be visible since an md-not-found template was provided
+ expect(ctrl.hidden).toBe(false);
+
+ // Make sure we wrap up anything and remove the element
+ $timeout.flush();
+ element.remove();
+ }));
+
+ it('properly sets hasNotFound with multiple autocompletes', inject(function($timeout, $material) {
+ var scope = createScope();
+ var template1 =
+ '' +
+ ' {{item.display}}' +
+ ' Sorry, not found...' +
+ '';
+ var element1 = compile(template1, scope);
+ var ctrl1 = element1.controller('mdAutocomplete');
+
+ var template2 =
+ '' +
+ ' {{item.display}}' +
+ '';
+ var element2 = compile(template2, scope);
+ var ctrl2 = element2.controller('mdAutocomplete');
+
+ // The first autocomplete has a template, the second one does not
+ expect(ctrl1.hasNotFound).toBe(true);
+ expect(ctrl2.hasNotFound).toBe(false);
+ }));
+
+ it('should even show the md-not-found template if we have lost focus', inject(function($timeout) {
+ var scope = createScope();
+ var template =
+ '' +
+ ' {{item.display}}' +
+ ' Sorry, not found...' +
+ '';
+
+ var element = compile(template, scope);
+ var controller = element.controller('mdAutocomplete');
+
+ controller.focus();
+
+ scope.searchText = 'somethingthatdoesnotexist';
+
+ $timeout.flush();
+
+ controller.listEnter();
+ expect(controller.notFoundVisible()).toBe(true);
+
+ controller.blur();
+ expect(controller.notFoundVisible()).toBe(true);
+
+ controller.listLeave();
+ expect(controller.notFoundVisible()).toBe(false);
+
+ $timeout.flush();
+ element.remove();
+
+ }));
+
+ it('should not show the md-not-found template if we lost focus and left the list', inject(function($timeout) {
+ var scope = createScope();
+ var template =
+ '' +
+ ' {{item.display}}' +
+ ' Sorry, not found...' +
+ '';
+
+ var element = compile(template, scope);
+ var controller = element.controller('mdAutocomplete');
+
+ controller.focus();
+
+ scope.searchText = 'somethingthatdoesnotexist';
+
+ $timeout.flush();
+
+ controller.listEnter();
+ expect(controller.notFoundVisible()).toBe(true);
+
+ controller.listLeave();
+ controller.blur();
+ expect(controller.notFoundVisible()).toBe(false);
+
+ $timeout.flush();
+ element.remove();
+ }));
+
+ });
+
+ describe('xss prevention', function() {
+ it('should not allow html to slip through', inject(function($timeout, $material) {
+ var html = 'foo
';
+ var scope = createScope([{display: html}]);
+ var template = '\
+ \
+ {{item.display}}\
+ ';
+ var element = compile(template, scope);
+ var ctrl = element.controller('mdAutocomplete');
+ var ul = element.find('ul');
+
+ $material.flushOutstandingAnimations();
+
+ expect(scope.searchText).toBe('');
+ expect(scope.selectedItem).toBe(null);
+
+ // Focus the input
+ ctrl.focus();
+
+ scope.$apply('searchText = "fo"');
+ $timeout.flush();
+ waitForVirtualRepeat(element);
+
+ expect(scope.searchText).toBe('fo');
+ expect(scope.match(scope.searchText).length).toBe(1);
+ expect(ul.find('li').length).toBe(1);
+ expect(ul.find('li').find('img').length).toBe(0);
+
+ element.remove();
+ }));
+ });
+
+ describe('API access', function() {
+ it('should clear the selected item', inject(function($timeout) {
+ var scope = createScope();
+ var template = '\
+ \
+ {{item.display}}\
+ ';
+ var element = compile(template, scope);
+ var ctrl = element.controller('mdAutocomplete');
+
+ element.scope().searchText = 'fo';
+ $timeout.flush();
+
+ ctrl.select(0);
+ $timeout.flush();
+
+ expect(scope.searchText).toBe('foo');
+ expect(scope.selectedItem).not.toBeNull();
+ expect(scope.selectedItem.display).toBe('foo');
+ expect(scope.match(scope.searchText).length).toBe(1);
+
+ ctrl.clear();
+ element.scope().$apply();
+
+ expect(scope.searchText).toBe('');
+ expect(scope.selectedItem).toBe(null);
+
+ element.remove();
+ }));
+
+ it('should notify selected item watchers', inject(function($timeout) {
+ var scope = createScope();
+ scope.itemChanged = jasmine.createSpy('itemChanged');
+
+ var registeredWatcher = jasmine.createSpy('registeredWatcher');
+
+ var template = '\
+ \
+ {{item.display}}\
+ ';
+ var element = compile(template, scope);
+ var ctrl = element.controller('mdAutocomplete');
+
+ ctrl.registerSelectedItemWatcher(registeredWatcher);
+
+ element.scope().searchText = 'fo';
+ $timeout.flush();
+
+ ctrl.select(0);
+ $timeout.flush();
+
+ expect(scope.itemChanged).toHaveBeenCalled();
+ expect(scope.itemChanged.calls.mostRecent().args[0].display).toBe('foo');
+ expect(registeredWatcher).toHaveBeenCalled();
+ expect(registeredWatcher.calls.mostRecent().args[0].display).toBe('foo');
+ expect(registeredWatcher.calls.mostRecent().args[1]).toBeNull();
+ expect(scope.selectedItem).not.toBeNull();
+ expect(scope.selectedItem.display).toBe('foo');
+
+ // Un-register the watcher
+ ctrl.unregisterSelectedItemWatcher(registeredWatcher);
+
+ ctrl.clear();
+ element.scope().$apply();
+
+ expect(registeredWatcher.calls.count()).toBe(1);
+ expect(scope.itemChanged.calls.count()).toBe(2);
+ expect(scope.itemChanged.calls.mostRecent().args[0]).toBeNull();
+ expect(scope.selectedItem).toBeNull();
+
+ element.remove();
+ }));
+ it('should pass value to item watcher', inject(function($timeout) {
+ var scope = createScope();
+ var itemValue = null;
+ var template = '\
+ \
+ {{item.display}}\
+ ';
+ scope.itemChanged = function(item) {
+ itemValue = item;
+ };
+ var element = compile(template, scope);
+ var ctrl = element.controller('mdAutocomplete');
+
+ element.scope().searchText = 'fo';
+ $timeout.flush();
+
+ ctrl.select(0);
+ $timeout.flush();
+
+ expect(itemValue).not.toBeNull();
+ expect(itemValue.display).toBe('foo');
+
+ ctrl.clear();
+ element.scope().$apply();
+
+ element.remove();
+ }));
+ });
+
+ describe('md-select-on-match', function() {
+ it('should select matching item on exact match when `md-select-on-match` is toggled', inject(function($timeout) {
+ var scope = createScope();
+ var template = '\
+ \
+ {{item.display}}\
+ ';
+ var element = compile(template, scope);
+
+ expect(scope.searchText).toBe('');
+ expect(scope.selectedItem).toBe(null);
+
+ element.scope().searchText = 'foo';
+ $timeout.flush();
+
+ expect(scope.selectedItem).not.toBe(null);
+ expect(scope.selectedItem.display).toBe('foo');
+
+ element.remove();
+ }));
+ it('should not select matching item on exact match when `md-select-on-match` is NOT toggled', inject(function($timeout) {
+ var scope = createScope();
+ var template = '\
+ \
+ {{item.display}}\
+ ';
+ var element = compile(template, scope);
+
+ expect(scope.searchText).toBe('');
+ expect(scope.selectedItem).toBe(null);
+
+ element.scope().searchText = 'foo';
+ $timeout.flush();
+
+ expect(scope.selectedItem).toBe(null);
+
+ element.remove();
+ }));
+
+ it('should select matching item using case insensitive', inject(function($timeout) {
+ var scope = createScope(null, null, true);
+ var template =
+ '' +
+ '{{item.display}}' +
+ '';
+ var element = compile(template, scope);
+
+ expect(scope.searchText).toBe('');
+ expect(scope.selectedItem).toBe(null);
+
+ element.scope().searchText = 'FoO';
+ $timeout.flush();
+
+ expect(scope.selectedItem).not.toBe(null);
+ expect(scope.selectedItem.display).toBe('foo');
+
+ element.remove();
+ }));
+ });
+
+ describe('when required', function() {
+ it('properly handles md-min-length="0" and undefined searchText', function() {
+ var scope = createScope();
+ var template = '\
+ \
+ {{item.display}}\
+ ';
+
+ var error;
+
+ try {
+ var element = compile(template, scope);
+
+ scope.searchText = undefined;
+ scope.$digest();
+ } catch (e) {
+ error = e;
+ }
+
+ expect(error).toBe(undefined);
+
+ element.remove();
+ });
+ });
+
+ describe('md-highlight-text', function() {
+ it('should update when content is modified', inject(function() {
+ var template = '{{message}}
';
+ var scope = createScope(null, {message: 'some text', query: 'some'});
+ var element = compile(template, scope);
+
+ expect(element.html()).toBe('some text');
+
+ scope.query = 'so';
+ scope.$apply();
+
+ expect(element.html()).toBe('some text');
+
+ scope.message = 'some more text';
+ scope.$apply();
+
+ expect(element.html()).toBe('some more text');
+
+ element.remove();
+ }));
+ });
+
+});
diff --git a/src/components/autocomplete/demoBasicUsage/index.html b/src/components/autocomplete/demoBasicUsage/index.html
new file mode 100644
index 0000000000..c23b66cc0f
--- /dev/null
+++ b/src/components/autocomplete/demoBasicUsage/index.html
@@ -0,0 +1,32 @@
+
diff --git a/src/components/autocomplete/demoBasicUsage/script.js b/src/components/autocomplete/demoBasicUsage/script.js
new file mode 100644
index 0000000000..6991ed5595
--- /dev/null
+++ b/src/components/autocomplete/demoBasicUsage/script.js
@@ -0,0 +1,85 @@
+(function () {
+ 'use strict';
+ angular
+ .module('autocompleteDemo', ['ngMaterial'])
+ .controller('DemoCtrl', DemoCtrl);
+
+ function DemoCtrl ($timeout, $q, $log) {
+ var self = this;
+
+ self.simulateQuery = false;
+ self.isDisabled = false;
+
+ // list of `state` value/display objects
+ self.states = loadAll();
+ self.querySearch = querySearch;
+ self.selectedItemChange = selectedItemChange;
+ self.searchTextChange = searchTextChange;
+
+ self.newState = newState;
+
+ function newState(state) {
+ alert("Sorry! You'll need to create a Constituion for " + state + " first!");
+ }
+
+ // ******************************
+ // Internal methods
+ // ******************************
+
+ /**
+ * Search for states... use $timeout to simulate
+ * remote dataservice call.
+ */
+ function querySearch (query) {
+ var results = query ? self.states.filter( createFilterFor(query) ) : self.states,
+ deferred;
+ if (self.simulateQuery) {
+ deferred = $q.defer();
+ $timeout(function () { deferred.resolve( results ); }, Math.random() * 1000, false);
+ return deferred.promise;
+ } else {
+ return results;
+ }
+ }
+
+ function searchTextChange(text) {
+ $log.info('Text changed to ' + text);
+ }
+
+ function selectedItemChange(item) {
+ $log.info('Item changed to ' + JSON.stringify(item));
+ }
+
+ /**
+ * Build `states` list of key/value pairs
+ */
+ function loadAll() {
+ var allStates = 'Alabama, Alaska, Arizona, Arkansas, California, Colorado, Connecticut, Delaware,\
+ Florida, Georgia, Hawaii, Idaho, Illinois, Indiana, Iowa, Kansas, Kentucky, Louisiana,\
+ Maine, Maryland, Massachusetts, Michigan, Minnesota, Mississippi, Missouri, Montana,\
+ Nebraska, Nevada, New Hampshire, New Jersey, New Mexico, New York, North Carolina,\
+ North Dakota, Ohio, Oklahoma, Oregon, Pennsylvania, Rhode Island, South Carolina,\
+ South Dakota, Tennessee, Texas, Utah, Vermont, Virginia, Washington, West Virginia,\
+ Wisconsin, Wyoming';
+
+ return allStates.split(/, +/g).map( function (state) {
+ return {
+ value: state.toLowerCase(),
+ display: state
+ };
+ });
+ }
+
+ /**
+ * Create filter function for a query string
+ */
+ function createFilterFor(query) {
+ var lowercaseQuery = angular.lowercase(query);
+
+ return function filterFn(state) {
+ return (state.value.indexOf(lowercaseQuery) === 0);
+ };
+
+ }
+ }
+})();
diff --git a/src/components/autocomplete/demoCustomTemplate/img/icons/octicon-repo.svg b/src/components/autocomplete/demoCustomTemplate/img/icons/octicon-repo.svg
new file mode 100644
index 0000000000..2bf9537797
--- /dev/null
+++ b/src/components/autocomplete/demoCustomTemplate/img/icons/octicon-repo.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/src/components/autocomplete/demoCustomTemplate/index.html b/src/components/autocomplete/demoCustomTemplate/index.html
new file mode 100644
index 0000000000..2242edaa65
--- /dev/null
+++ b/src/components/autocomplete/demoCustomTemplate/index.html
@@ -0,0 +1,35 @@
+
+
diff --git a/src/components/autocomplete/demoCustomTemplate/script.js b/src/components/autocomplete/demoCustomTemplate/script.js
new file mode 100644
index 0000000000..dac3f00e3b
--- /dev/null
+++ b/src/components/autocomplete/demoCustomTemplate/script.js
@@ -0,0 +1,100 @@
+(function () {
+ 'use strict';
+ angular
+ .module('autocompleteCustomTemplateDemo', ['ngMaterial'])
+ .controller('DemoCtrl', DemoCtrl);
+
+ function DemoCtrl ($timeout, $q, $log) {
+ var self = this;
+
+ self.simulateQuery = false;
+ self.isDisabled = false;
+
+ self.repos = loadAll();
+ self.querySearch = querySearch;
+ self.selectedItemChange = selectedItemChange;
+ self.searchTextChange = searchTextChange;
+
+ // ******************************
+ // Internal methods
+ // ******************************
+
+ /**
+ * Search for repos... use $timeout to simulate
+ * remote dataservice call.
+ */
+ function querySearch (query) {
+ var results = query ? self.repos.filter( createFilterFor(query) ) : self.repos,
+ deferred;
+ if (self.simulateQuery) {
+ deferred = $q.defer();
+ $timeout(function () { deferred.resolve( results ); }, Math.random() * 1000, false);
+ return deferred.promise;
+ } else {
+ return results;
+ }
+ }
+
+ function searchTextChange(text) {
+ $log.info('Text changed to ' + text);
+ }
+
+ function selectedItemChange(item) {
+ $log.info('Item changed to ' + JSON.stringify(item));
+ }
+
+ /**
+ * Build `components` list of key/value pairs
+ */
+ function loadAll() {
+ var repos = [
+ {
+ 'name' : 'Angular 1',
+ 'url' : 'https://github.com/angular/angular.js',
+ 'watchers' : '3,623',
+ 'forks' : '16,175',
+ },
+ {
+ 'name' : 'Angular 2',
+ 'url' : 'https://github.com/angular/angular',
+ 'watchers' : '469',
+ 'forks' : '760',
+ },
+ {
+ 'name' : 'Angular Material',
+ 'url' : 'https://github.com/angular/material',
+ 'watchers' : '727',
+ 'forks' : '1,241',
+ },
+ {
+ 'name' : 'Bower Material',
+ 'url' : 'https://github.com/angular/bower-material',
+ 'watchers' : '42',
+ 'forks' : '84',
+ },
+ {
+ 'name' : 'Material Start',
+ 'url' : 'https://github.com/angular/material-start',
+ 'watchers' : '81',
+ 'forks' : '303',
+ }
+ ];
+ return repos.map( function (repo) {
+ repo.value = repo.name.toLowerCase();
+ return repo;
+ });
+ }
+
+ /**
+ * Create filter function for a query string
+ */
+ function createFilterFor(query) {
+ var lowercaseQuery = angular.lowercase(query);
+
+ return function filterFn(item) {
+ return (item.value.indexOf(lowercaseQuery) === 0);
+ };
+
+ }
+ }
+})();
diff --git a/src/components/autocomplete/demoCustomTemplate/style.css b/src/components/autocomplete/demoCustomTemplate/style.css
new file mode 100644
index 0000000000..ea0bc1326f
--- /dev/null
+++ b/src/components/autocomplete/demoCustomTemplate/style.css
@@ -0,0 +1,19 @@
+.autocomplete-custom-template li {
+ border-bottom: 1px solid #ccc;
+ height: auto;
+ padding-top: 8px;
+ padding-bottom: 8px;
+ white-space: normal;
+}
+.autocomplete-custom-template li:last-child {
+ border-bottom-width: 0;
+}
+.autocomplete-custom-template .item-title,
+.autocomplete-custom-template .item-metadata {
+ display: block;
+ line-height: 2;
+}
+.autocomplete-custom-template .item-title md-icon {
+ height: 18px;
+ width: 18px;
+}
diff --git a/src/components/autocomplete/demoFloatingLabel/index.html b/src/components/autocomplete/demoFloatingLabel/index.html
new file mode 100644
index 0000000000..ac51f2104c
--- /dev/null
+++ b/src/components/autocomplete/demoFloatingLabel/index.html
@@ -0,0 +1,32 @@
+
diff --git a/src/components/autocomplete/demoFloatingLabel/script.js b/src/components/autocomplete/demoFloatingLabel/script.js
new file mode 100644
index 0000000000..4c3f7d8f3b
--- /dev/null
+++ b/src/components/autocomplete/demoFloatingLabel/script.js
@@ -0,0 +1,61 @@
+(function () {
+ 'use strict';
+ angular
+ .module('autocompleteFloatingLabelDemo', ['ngMaterial', 'ngMessages'])
+ .controller('DemoCtrl', DemoCtrl);
+
+ function DemoCtrl ($timeout, $q) {
+ var self = this;
+
+ // list of `state` value/display objects
+ self.states = loadAll();
+ self.selectedItem = null;
+ self.searchText = null;
+ self.querySearch = querySearch;
+
+ // ******************************
+ // Internal methods
+ // ******************************
+
+ /**
+ * Search for states... use $timeout to simulate
+ * remote dataservice call.
+ */
+ function querySearch (query) {
+ var results = query ? self.states.filter( createFilterFor(query) ) : [];
+ return results;
+ }
+
+ /**
+ * Build `states` list of key/value pairs
+ */
+ function loadAll() {
+ var allStates = 'Alabama, Alaska, Arizona, Arkansas, California, Colorado, Connecticut, Delaware,\
+ Florida, Georgia, Hawaii, Idaho, Illinois, Indiana, Iowa, Kansas, Kentucky, Louisiana,\
+ Maine, Maryland, Massachusetts, Michigan, Minnesota, Mississippi, Missouri, Montana,\
+ Nebraska, Nevada, New Hampshire, New Jersey, New Mexico, New York, North Carolina,\
+ North Dakota, Ohio, Oklahoma, Oregon, Pennsylvania, Rhode Island, South Carolina,\
+ South Dakota, Tennessee, Texas, Utah, Vermont, Virginia, Washington, West Virginia,\
+ Wisconsin, Wyoming';
+
+ return allStates.split(/, +/g).map( function (state) {
+ return {
+ value: state.toLowerCase(),
+ display: state
+ };
+ });
+ }
+
+ /**
+ * Create filter function for a query string
+ */
+ function createFilterFor(query) {
+ var lowercaseQuery = angular.lowercase(query);
+
+ return function filterFn(state) {
+ return (state.value.indexOf(lowercaseQuery) === 0);
+ };
+
+ }
+ }
+})();
diff --git a/src/components/autocomplete/demoInsideDialog/dialog.tmpl.html b/src/components/autocomplete/demoInsideDialog/dialog.tmpl.html
new file mode 100644
index 0000000000..03017213dc
--- /dev/null
+++ b/src/components/autocomplete/demoInsideDialog/dialog.tmpl.html
@@ -0,0 +1,37 @@
+
+
+
+
Autocomplete Dialog Example
+
+
+
+
+
+
+
+
+
+
+
+
+ Finished
+
+
diff --git a/src/components/autocomplete/demoInsideDialog/index.html b/src/components/autocomplete/demoInsideDialog/index.html
new file mode 100644
index 0000000000..6e29286918
--- /dev/null
+++ b/src/components/autocomplete/demoInsideDialog/index.html
@@ -0,0 +1,9 @@
+
+
+
+ Click the button below to open the dialog with an autocomplete.
+
+
+ Open Dialog
+
+
diff --git a/src/components/autocomplete/demoInsideDialog/script.js b/src/components/autocomplete/demoInsideDialog/script.js
new file mode 100644
index 0000000000..d794b650a2
--- /dev/null
+++ b/src/components/autocomplete/demoInsideDialog/script.js
@@ -0,0 +1,84 @@
+(function () {
+ 'use strict';
+ angular
+ .module('autocompleteDemoInsideDialog', ['ngMaterial'])
+ .controller('DemoCtrl', DemoCtrl);
+
+ function DemoCtrl($mdDialog) {
+ var self = this;
+
+ self.openDialog = function($event) {
+ $mdDialog.show({
+ controller: DialogCtrl,
+ controllerAs: 'ctrl',
+ templateUrl: 'dialog.tmpl.html',
+ parent: angular.element(document.body),
+ targetEvent: $event,
+ clickOutsideToClose:true
+ })
+ }
+ }
+
+ function DialogCtrl ($timeout, $q, $scope, $mdDialog) {
+ var self = this;
+
+ // list of `state` value/display objects
+ self.states = loadAll();
+ self.querySearch = querySearch;
+
+ // ******************************
+ // Template methods
+ // ******************************
+
+ self.cancel = function($event) {
+ $mdDialog.cancel();
+ };
+ self.finish = function($event) {
+ $mdDialog.hide();
+ };
+
+ // ******************************
+ // Internal methods
+ // ******************************
+
+ /**
+ * Search for states... use $timeout to simulate
+ * remote dataservice call.
+ */
+ function querySearch (query) {
+ return query ? self.states.filter( createFilterFor(query) ) : self.states;
+ }
+
+ /**
+ * Build `states` list of key/value pairs
+ */
+ function loadAll() {
+ var allStates = 'Alabama, Alaska, Arizona, Arkansas, California, Colorado, Connecticut, Delaware,\
+ Florida, Georgia, Hawaii, Idaho, Illinois, Indiana, Iowa, Kansas, Kentucky, Louisiana,\
+ Maine, Maryland, Massachusetts, Michigan, Minnesota, Mississippi, Missouri, Montana,\
+ Nebraska, Nevada, New Hampshire, New Jersey, New Mexico, New York, North Carolina,\
+ North Dakota, Ohio, Oklahoma, Oregon, Pennsylvania, Rhode Island, South Carolina,\
+ South Dakota, Tennessee, Texas, Utah, Vermont, Virginia, Washington, West Virginia,\
+ Wisconsin, Wyoming';
+
+ return allStates.split(/, +/g).map( function (state) {
+ return {
+ value: state.toLowerCase(),
+ display: state
+ };
+ });
+ }
+
+ /**
+ * Create filter function for a query string
+ */
+ function createFilterFor(query) {
+ var lowercaseQuery = angular.lowercase(query);
+
+ return function filterFn(state) {
+ return (state.value.indexOf(lowercaseQuery) === 0);
+ };
+
+ }
+ }
+})();
diff --git a/src/components/autocomplete/js/autocompleteController.js b/src/components/autocomplete/js/autocompleteController.js
new file mode 100644
index 0000000000..0efaa9d03a
--- /dev/null
+++ b/src/components/autocomplete/js/autocompleteController.js
@@ -0,0 +1,761 @@
+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,
+ promiseFetch = false;
+
+ //-- 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.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 = !!value; });
+ $attrs.$observe('required', function (value) { ctrl.isRequired = !!value; });
+ $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 () {
+ 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);
+ }, false, $scope);
+ }
+ } else if (hidden && !oldHidden) {
+ $mdUtil.nextTick(function () {
+ $mdUtil.enableScrolling();
+ }, false, $scope);
+ }
+ }
+
+ /**
+ * 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 () {
+ 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();
+ 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 () {
+ // Set the loading to true so we don't see flashes of content
+ setLoading(true);
+
+ // Reset our variables
+ ctrl.index = 0;
+ ctrl.matches = [];
+ $scope.searchText = '';
+
+ // Tell the select to fire and select nothing
+ select(-1);
+
+ // Per http://www.w3schools.com/jsref/event_oninput.asp
+ var eventObj = document.createEvent('CustomEvent');
+ eventObj.initCustomEvent('input', true, true, { value: $scope.searchText });
+ elements.input.dispatchEvent(eventObj);
+
+ 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);
+
+ if ( isList ) handleResults(items);
+ else handleAsyncResults(items);
+
+ function handleAsyncResults(items) {
+ if ( !items ) return;
+
+ items = $q.when(items);
+ setLoading(true);
+ promiseFetch = true;
+
+ $mdUtil.nextTick(function () {
+ items
+ .then(handleResults)
+ .finally(function(){
+ setLoading(false);
+ promiseFetch = 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 ($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 scrollTo (offset) {
+ elements.$.scrollContainer.controller('mdVirtualRepeatContainer').scrollTo(offset);
+ }
+
+ function notFoundVisible () {
+ var textLength = (ctrl.scope.searchText || '').length;
+
+ return ctrl.hasNotFound && !hasMatches() && (!ctrl.loading || promiseFetch) && 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();
+ } 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);
+ });
+ }
+
+}
diff --git a/src/components/autocomplete/js/autocompleteDirective.js b/src/components/autocomplete/js/autocompleteDirective.js
new file mode 100644
index 0000000000..96d864853b
--- /dev/null
+++ b/src/components/autocomplete/js/autocompleteDirective.js
@@ -0,0 +1,282 @@
+angular
+ .module('material.components.autocomplete')
+ .directive('mdAutocomplete', MdAutocomplete);
+
+/**
+ * @ngdoc directive
+ * @name mdAutocomplete
+ * @module material.components.autocomplete
+ *
+ * @description
+ * `` is a special input component with a drop-down of all possible matches to a
+ * custom query. This component allows you to provide real-time suggestions as the user types
+ * in the input area.
+ *
+ * To start, you will need to specify the required parameters and provide a template for your
+ * results. The content inside `md-autocomplete` will be treated as a template.
+ *
+ * In more complex cases, you may want to include other content such as a message to display when
+ * no matches were found. You can do this by wrapping your template in `md-item-template` and
+ * adding a tag for `md-not-found`. An example of this is shown below.
+ *
+ * ### Validation
+ *
+ * You can use `ng-messages` to include validation the same way that you would normally validate;
+ * however, if you want to replicate a standard input with a floating label, you will have to
+ * do the following:
+ *
+ * - Make sure that your template is wrapped in `md-item-template`
+ * - Add your `ng-messages` code inside of `md-autocomplete`
+ * - Add your validation properties to `md-autocomplete` (ie. `required`)
+ * - Add a `name` to `md-autocomplete` (to be used on the generated `input`)
+ *
+ * There is an example below of how this should look.
+ *
+ *
+ * @param {expression} md-items An expression in the format of `item in items` to iterate over
+ * matches for your search.
+ * @param {expression=} md-selected-item-change An expression to be run each time a new item is
+ * selected
+ * @param {expression=} md-search-text-change An expression to be run each time the search text
+ * updates
+ * @param {expression=} md-search-text A model to bind the search query text to
+ * @param {object=} md-selected-item A model to bind the selected item to
+ * @param {expression=} md-item-text An expression that will convert your object to a single string.
+ * @param {string=} placeholder Placeholder text that will be forwarded to the input.
+ * @param {boolean=} md-no-cache Disables the internal caching that happens in autocomplete
+ * @param {boolean=} ng-disabled Determines whether or not to disable the input field
+ * @param {number=} md-min-length Specifies the minimum length of text before autocomplete will
+ * make suggestions
+ * @param {number=} md-delay Specifies the amount of time (in milliseconds) to wait before looking
+ * for results
+ * @param {boolean=} md-autofocus If true, will immediately focus the input element
+ * @param {boolean=} md-autoselect If true, the first item will be selected by default
+ * @param {string=} md-menu-class This will be applied to the dropdown menu for styling
+ * @param {string=} md-floating-label This will add a floating label to autocomplete and wrap it in
+ * `md-input-container`
+ * @param {string=} md-input-name The name attribute given to the input element to be used with
+ * FormController
+ * @param {string=} md-input-id An ID to be added to the input element
+ * @param {number=} md-input-minlength The minimum length for the input's value for validation
+ * @param {number=} md-input-maxlength The maximum length for the input's value for validation
+ * @param {boolean=} md-select-on-match When set, autocomplete will automatically select exact
+ * the item if the search text is an exact match
+ * @param {boolean=} md-match-case-insensitive When set and using `md-select-on-match`, autocomplete
+ * will select on case-insensitive match
+ *
+ * @usage
+ * ### Basic Example
+ *
+ *
+ * {{item.display}}
+ *
+ *
+ *
+ * ### Example with "not found" message
+ *
+ *
+ *
+ * {{item.display}}
+ *
+ *
+ * No matches found.
+ *
+ *
+ *
+ *
+ * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the
+ * different parts that make up our component.
+ *
+ * ### Example with validation
+ *
+ *
+ *
+ *
+ * In this example, our code utilizes `md-item-template` and `md-not-found` to specify the
+ * different parts that make up our component.
+ */
+
+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) {
+ controller.hasNotFound = element.hasNotFoundTemplate;
+ delete element.hasNotFoundTemplate;
+ },
+ template: function (element, attr) {
+ var noItemsTemplate = getNoItemsTemplate(),
+ itemTemplate = getItemTemplate(),
+ leftover = element.html(),
+ tabindex = attr.tabindex;
+
+ // Set our variable for the link function above which runs later
+ element.hasNotFoundTemplate = !!noItemsTemplate;
+
+ // Always set our tabindex of the autocomplete directive to -1, because our input
+ // will hold the actual tabindex.
+ element.attr('tabindex', '-1');
+
+ return '\
+ \
+ ' + getInputElement() + '\
+ \
+ \
+ \
+ - \
+ ' + itemTemplate + '\
+
' + noItemsTemplate + '\
+
\
+ \
+ \
+ \
+ {{message}}
\
+ ';
+
+ function getItemTemplate() {
+ var templateTag = element.find('md-item-template').detach(),
+ html = templateTag.length ? templateTag.html() : element.html();
+ if (!templateTag.length) element.empty();
+ return '' + html + '';
+ }
+
+ function getNoItemsTemplate() {
+ var templateTag = element.find('md-not-found').detach(),
+ template = templateTag.length ? templateTag.html() : '';
+ return template
+ ? '' + template + ''
+ : '';
+
+ }
+
+ function getInputElement () {
+ if (attr.mdFloatingLabel) {
+ return '\
+ \
+ \
+ \
+ ' + leftover + '
\
+ ';
+ } else {
+ return '\
+ \
+ \
+ ';
+ }
+ }
+ }
+ };
+}
diff --git a/src/components/autocomplete/js/autocompleteParentScopeDirective.js b/src/components/autocomplete/js/autocompleteParentScopeDirective.js
new file mode 100644
index 0000000000..10618e83bc
--- /dev/null
+++ b/src/components/autocomplete/js/autocompleteParentScopeDirective.js
@@ -0,0 +1,75 @@
+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;
+ });
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/components/autocomplete/js/highlightController.js b/src/components/autocomplete/js/highlightController.js
new file mode 100644
index 0000000000..65c78d608a
--- /dev/null
+++ b/src/components/autocomplete/js/highlightController.js
@@ -0,0 +1,41 @@
+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('').text(state.unsafeText).html()
+ }
+ if (regex === null || state.term !== prevState.term) {
+ regex = getRegExp(state.term, flags);
+ }
+
+ $element.html(text.replace(regex, '
$&'));
+ }, true);
+ $element.on('$destroy', watcher);
+ }
+
+ function sanitize (term) {
+ return term && term.replace(/[\\\^\$\*\+\?\.\(\)\|\{}\[\]]/g, '\\$&');
+ }
+
+ function getRegExp (text, flags) {
+ var str = '';
+ if (flags.indexOf('^') >= 1) str += '^';
+ str += text;
+ if (flags.indexOf('$') >= 1) str += '$';
+ return new RegExp(sanitize(str), flags.replace(/[\$\^]/g, ''));
+ }
+}
diff --git a/src/components/autocomplete/js/highlightDirective.js b/src/components/autocomplete/js/highlightDirective.js
new file mode 100644
index 0000000000..dca31e232f
--- /dev/null
+++ b/src/components/autocomplete/js/highlightDirective.js
@@ -0,0 +1,47 @@
+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 `
` 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
+ *
+ *
+ *
+ * -
+ * {{result.text}}
+ *
+ *
+ *
+ */
+
+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);
+ };
+ }
+ };
+}
diff --git a/src/components/backdrop/_backdrop.js b/src/components/backdrop/_backdrop.js
deleted file mode 100644
index 72a0bb0992..0000000000
--- a/src/components/backdrop/_backdrop.js
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * @ngdoc module
- * @name material.components.backdrop
- * @description Backdrop
- */
-
-/**
- * @ngdoc directive
- * @name mdBackdrop
- * @module material.components.backdrop
- *
- * @restrict E
- *
- * @description
- * `
` is a backdrop element used by other coponents, such as dialog and bottom sheet.
- * Apply class `opaque` to make the backdrop use the theme backdrop color.
- *
- */
-angular.module('material.components.backdrop', [
- 'material.services.theming'
-])
-.directive('mdBackdrop', [
- '$mdTheming',
- BackdropDirective
-]);
-
-function BackdropDirective($mdTheming) {
- return $mdTheming;
-}
diff --git a/src/components/backdrop/_backdrop.scss b/src/components/backdrop/_backdrop.scss
deleted file mode 100644
index e994a32ebf..0000000000
--- a/src/components/backdrop/_backdrop.scss
+++ /dev/null
@@ -1,24 +0,0 @@
-md-backdrop {
- z-index: $z-index-backdrop;
- background-color: rgba(0,0,0,0);
-
- position: fixed;
- left: 0;
- top: 0;
- right: 0;
- bottom: 0;
-
- transition: all 0.2s ease-out;
-
- &.ng-enter {
- transition-delay: 0.1s;
- }
- &.ng-enter,
- &.ng-leave.ng-leave-active {
- opacity: 0;
- }
- &.ng-leave,
- &.ng-enter.ng-enter-active {
- opacity: 1;
- }
-}
diff --git a/src/components/backdrop/backdrop-theme.scss b/src/components/backdrop/backdrop-theme.scss
index 67d3797c83..af1c80988d 100644
--- a/src/components/backdrop/backdrop-theme.scss
+++ b/src/components/backdrop/backdrop-theme.scss
@@ -1,6 +1,9 @@
-$backdrop-color: rgba(0,0,0,0.3);
+md-backdrop {
+
+ background-color: '{{background-900-0.0}}';
+
+ &.md-opaque.md-THEME_NAME-theme {
+ background-color: '{{background-900-1.0}}';
+ }
-md-backdrop.md-opaque.md-#{$theme-name}-theme {
- background-color: $backdrop-color;
- position:absolute
}
diff --git a/src/components/backdrop/backdrop.js b/src/components/backdrop/backdrop.js
new file mode 100644
index 0000000000..7b3a2c317f
--- /dev/null
+++ b/src/components/backdrop/backdrop.js
@@ -0,0 +1,68 @@
+/*
+ * @ngdoc module
+ * @name material.components.backdrop
+ * @description Backdrop
+ */
+
+/**
+ * @ngdoc directive
+ * @name mdBackdrop
+ * @module material.components.backdrop
+ *
+ * @restrict E
+ *
+ * @description
+ * `` is a backdrop element used by other components, such as dialog and bottom sheet.
+ * Apply class `opaque` to make the backdrop use the theme backdrop color.
+ *
+ */
+
+angular
+ .module('material.components.backdrop', ['material.core'])
+ .directive('mdBackdrop', function BackdropDirective($mdTheming, $animate, $rootElement, $window, $log, $$rAF, $document) {
+ var ERROR_CSS_POSITION = " may not work properly in a scrolled, static-positioned parent container.";
+
+ return {
+ restrict: 'E',
+ link: postLink
+ };
+
+ function postLink(scope, element, attrs) {
+
+ // If body scrolling has been disabled using mdUtil.disableBodyScroll(),
+ // adjust the 'backdrop' height to account for the fixed 'body' top offset
+ var body = $window.getComputedStyle($document[0].body);
+ if (body.position == 'fixed') {
+ var hViewport = parseInt(body.height, 10) + Math.abs(parseInt(body.top, 10));
+ element.css({
+ height: hViewport + 'px'
+ });
+ }
+
+ // backdrop may be outside the $rootElement, tell ngAnimate to animate regardless
+ if ($animate.pin) $animate.pin(element, $rootElement);
+
+ $$rAF(function () {
+
+ // Often $animate.enter() is used to append the backDrop element
+ // so let's wait until $animate is done...
+ var parent = element.parent()[0];
+ if (parent) {
+
+ if ( parent.nodeName == 'BODY' ) {
+ element.css({position : 'fixed'});
+ }
+
+ var styles = $window.getComputedStyle(parent);
+ if (styles.position == 'static') {
+ // backdrop uses position:absolute and will not work properly with parent position:static (default)
+ $log.warn(ERROR_CSS_POSITION);
+ }
+ }
+
+ $mdTheming.inherit(element, element.parent());
+ });
+
+ }
+
+ });
diff --git a/src/components/backdrop/backdrop.scss b/src/components/backdrop/backdrop.scss
new file mode 100644
index 0000000000..46b2916679
--- /dev/null
+++ b/src/components/backdrop/backdrop.scss
@@ -0,0 +1,58 @@
+// !!Important - Theme-based Background-color can be configured in backdrop-theme.scss
+// - Animate background-color opacityy only for `.md-opaque` styles
+
+md-backdrop {
+ transition: opacity 450ms;
+
+ position: absolute;
+ top:0;
+ bottom:0;
+ left: 0;
+ right: 0;
+
+ z-index: $z-index-backdrop;
+
+ &.md-menu-backdrop {
+ position: fixed !important;
+ z-index: $z-index-menu - 1;
+ }
+ &.md-select-backdrop {
+ z-index: $z-index-dialog + 1;
+ transition-duration: 0;
+ }
+ &.md-dialog-backdrop {
+ z-index: $z-index-dialog - 1;
+ }
+ &.md-bottom-sheet-backdrop {
+ z-index: $z-index-bottom-sheet - 1;
+ }
+ &.md-sidenav-backdrop {
+ z-index: $z-index-sidenav - 1;
+ }
+
+
+ &.md-click-catcher {
+ position: absolute;
+ }
+
+ &.md-opaque {
+
+ opacity: .48;
+
+ &.ng-enter {
+ opacity: 0;
+ }
+ &.ng-enter.md-opaque.ng-enter-active {
+ opacity: .48;
+ }
+ &.ng-leave {
+ opacity: .48;
+ transition: opacity 400ms;
+ }
+ &.ng-leave.md-opaque.ng-leave-active {
+ opacity: 0;
+ }
+ }
+
+}
+
diff --git a/src/components/bottomSheet/README.md b/src/components/bottomSheet/README.md
deleted file mode 100644
index c2e23c11d1..0000000000
--- a/src/components/bottomSheet/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-One way to present a set of actions to a user is with bottom sheets, a sheet of paper that slides up from the bottom edge of the screen. Bottom sheets offer flexibility in the display of clear and simple actions that do not need explanation.
-
-[Bottom Sheet](https://www.google.com/design/spec/components/bottom-sheets.html) components are invoked using the '$mdBottomSheet' service.
diff --git a/src/components/bottomSheet/bottom-sheet-theme.scss b/src/components/bottomSheet/bottom-sheet-theme.scss
new file mode 100644
index 0000000000..286a9b475c
--- /dev/null
+++ b/src/components/bottomSheet/bottom-sheet-theme.scss
@@ -0,0 +1,19 @@
+
+md-bottom-sheet.md-THEME_NAME-theme {
+ background-color: '{{background-50}}';
+ border-top-color: '{{background-300}}';
+
+ &.md-list {
+ md-list-item {
+ color: '{{foreground-1}}';
+ }
+ }
+
+ .md-subheader {
+ background-color: '{{background-50}}';
+ }
+
+ .md-subheader {
+ color: '{{foreground-1}}';
+ }
+}
diff --git a/src/components/bottomSheet/bottom-sheet.js b/src/components/bottomSheet/bottom-sheet.js
new file mode 100644
index 0000000000..1b7b341d76
--- /dev/null
+++ b/src/components/bottomSheet/bottom-sheet.js
@@ -0,0 +1,263 @@
+/**
+ * @ngdoc module
+ * @name material.components.bottomSheet
+ * @description
+ * BottomSheet
+ */
+angular
+ .module('material.components.bottomSheet', [
+ 'material.core',
+ 'material.components.backdrop'
+ ])
+ .directive('mdBottomSheet', MdBottomSheetDirective)
+ .provider('$mdBottomSheet', MdBottomSheetProvider);
+
+/* @ngInject */
+function MdBottomSheetDirective($mdBottomSheet) {
+ return {
+ restrict: 'E',
+ link : function postLink(scope, element, attr) {
+ // When navigation force destroys an interimElement, then
+ // listen and $destroy() that interim instance...
+ scope.$on('$destroy', function() {
+ $mdBottomSheet.destroy();
+ });
+ }
+ };
+}
+
+
+/**
+ * @ngdoc service
+ * @name $mdBottomSheet
+ * @module material.components.bottomSheet
+ *
+ * @description
+ * `$mdBottomSheet` opens a bottom sheet over the app and provides a simple promise API.
+ *
+ * ## Restrictions
+ *
+ * - The bottom sheet's template must have an outer `` element.
+ * - Add the `md-grid` class to the bottom sheet for a grid layout.
+ * - Add the `md-list` class to the bottom sheet for a list layout.
+ *
+ * @usage
+ *
+ *
+ *
+ * Open a Bottom Sheet!
+ *
+ *
+ *
+ *
+ * var app = angular.module('app', ['ngMaterial']);
+ * app.controller('MyController', function($scope, $mdBottomSheet) {
+ * $scope.openBottomSheet = function() {
+ * $mdBottomSheet.show({
+ * template: 'Hello!'
+ * });
+ * };
+ * });
+ *
+ */
+
+ /**
+ * @ngdoc method
+ * @name $mdBottomSheet#show
+ *
+ * @description
+ * Show a bottom sheet with the specified options.
+ *
+ * @param {object} options An options object, with the following properties:
+ *
+ * - `templateUrl` - `{string=}`: The url of an html template file that will
+ * be used as the content of the bottom sheet. Restrictions: the template must
+ * have an outer `md-bottom-sheet` element.
+ * - `template` - `{string=}`: Same as templateUrl, except this is an actual
+ * template string.
+ * - `scope` - `{object=}`: the scope to link the template / controller to. If none is specified, it will create a new child scope.
+ * This scope will be destroyed when the bottom sheet is removed unless `preserveScope` is set to true.
+ * - `preserveScope` - `{boolean=}`: whether to preserve the scope when the element is removed. Default is false
+ * - `controller` - `{string=}`: The controller to associate with this bottom sheet.
+ * - `locals` - `{string=}`: An object containing key/value pairs. The keys will
+ * be used as names of values to inject into the controller. For example,
+ * `locals: {three: 3}` would inject `three` into the controller with the value
+ * of 3.
+ * - `clickOutsideToClose` - `{boolean=}`: Whether the user can click outside the bottom sheet to
+ * close it. Default true.
+ * - `escapeToClose` - `{boolean=}`: Whether the user can press escape to close the bottom sheet.
+ * Default true.
+ * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values
+ * and the bottom sheet will not open until the promises resolve.
+ * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
+ * - `parent` - `{element=}`: The element to append the bottom sheet to. The `parent` may be a `function`, `string`,
+ * `object`, or null. Defaults to appending to the body of the root element (or the root element) of the application.
+ * e.g. angular.element(document.getElementById('content')) or "#content"
+ * - `disableParentScroll` - `{boolean=}`: Whether to disable scrolling while the bottom sheet is open.
+ * Default true.
+ *
+ * @returns {promise} A promise that can be resolved with `$mdBottomSheet.hide()` or
+ * rejected with `$mdBottomSheet.cancel()`.
+ */
+
+/**
+ * @ngdoc method
+ * @name $mdBottomSheet#hide
+ *
+ * @description
+ * Hide the existing bottom sheet and resolve the promise returned from
+ * `$mdBottomSheet.show()`. This call will close the most recently opened/current bottomsheet (if any).
+ *
+ * @param {*=} response An argument for the resolved promise.
+ *
+ */
+
+/**
+ * @ngdoc method
+ * @name $mdBottomSheet#cancel
+ *
+ * @description
+ * Hide the existing bottom sheet and reject the promise returned from
+ * `$mdBottomSheet.show()`.
+ *
+ * @param {*=} response An argument for the rejected promise.
+ *
+ */
+
+function MdBottomSheetProvider($$interimElementProvider) {
+ // how fast we need to flick down to close the sheet, pixels/ms
+ var CLOSING_VELOCITY = 0.5;
+ var PADDING = 80; // same as css
+
+ return $$interimElementProvider('$mdBottomSheet')
+ .setDefaults({
+ methods: ['disableParentScroll', 'escapeToClose', 'clickOutsideToClose'],
+ options: bottomSheetDefaults
+ });
+
+ /* @ngInject */
+ function bottomSheetDefaults($animate, $mdConstant, $mdUtil, $mdTheming, $mdBottomSheet, $rootElement, $mdGesture) {
+ var backdrop;
+
+ return {
+ themable: true,
+ onShow: onShow,
+ onRemove: onRemove,
+ escapeToClose: true,
+ clickOutsideToClose: true,
+ disableParentScroll: true
+ };
+
+
+ function onShow(scope, element, options, controller) {
+
+ element = $mdUtil.extractElementByName(element, 'md-bottom-sheet');
+
+ // Add a backdrop that will close on click
+ backdrop = $mdUtil.createBackdrop(scope, "md-bottom-sheet-backdrop md-opaque");
+
+ if (options.clickOutsideToClose) {
+ backdrop.on('click', function() {
+ $mdUtil.nextTick($mdBottomSheet.cancel,true);
+ });
+ }
+
+ $mdTheming.inherit(backdrop, options.parent);
+
+ $animate.enter(backdrop, options.parent, null);
+
+ var bottomSheet = new BottomSheet(element, options.parent);
+ options.bottomSheet = bottomSheet;
+
+ $mdTheming.inherit(bottomSheet.element, options.parent);
+
+ if (options.disableParentScroll) {
+ options.restoreScroll = $mdUtil.disableScrollAround(bottomSheet.element, options.parent);
+ }
+
+ return $animate.enter(bottomSheet.element, options.parent)
+ .then(function() {
+ var focusable = $mdUtil.findFocusTarget(element) || angular.element(
+ element[0].querySelector('button') ||
+ element[0].querySelector('a') ||
+ element[0].querySelector('[ng-click]')
+ );
+ focusable.focus();
+
+ if (options.escapeToClose) {
+ options.rootElementKeyupCallback = function(e) {
+ if (e.keyCode === $mdConstant.KEY_CODE.ESCAPE) {
+ $mdUtil.nextTick($mdBottomSheet.cancel,true);
+ }
+ };
+ $rootElement.on('keyup', options.rootElementKeyupCallback);
+ }
+ });
+
+ }
+
+ function onRemove(scope, element, options) {
+
+ var bottomSheet = options.bottomSheet;
+
+ $animate.leave(backdrop);
+ return $animate.leave(bottomSheet.element).then(function() {
+ if (options.disableParentScroll) {
+ options.restoreScroll();
+ delete options.restoreScroll;
+ }
+
+ bottomSheet.cleanup();
+ });
+ }
+
+ /**
+ * BottomSheet class to apply bottom-sheet behavior to an element
+ */
+ function BottomSheet(element, parent) {
+ var deregister = $mdGesture.register(parent, 'drag', { horizontal: false });
+ parent.on('$md.dragstart', onDragStart)
+ .on('$md.drag', onDrag)
+ .on('$md.dragend', onDragEnd);
+
+ return {
+ element: element,
+ cleanup: function cleanup() {
+ deregister();
+ parent.off('$md.dragstart', onDragStart);
+ parent.off('$md.drag', onDrag);
+ parent.off('$md.dragend', onDragEnd);
+ }
+ };
+
+ function onDragStart(ev) {
+ // Disable transitions on transform so that it feels fast
+ element.css($mdConstant.CSS.TRANSITION_DURATION, '0ms');
+ }
+
+ function onDrag(ev) {
+ var transform = ev.pointer.distanceY;
+ if (transform < 5) {
+ // Slow down drag when trying to drag up, and stop after PADDING
+ transform = Math.max(-PADDING, transform / 2);
+ }
+ element.css($mdConstant.CSS.TRANSFORM, 'translate3d(0,' + (PADDING + transform) + 'px,0)');
+ }
+
+ function onDragEnd(ev) {
+ if (ev.pointer.distanceY > 0 &&
+ (ev.pointer.distanceY > 20 || Math.abs(ev.pointer.velocityY) > CLOSING_VELOCITY)) {
+ var distanceRemaining = element.prop('offsetHeight') - ev.pointer.distanceY;
+ var transitionDuration = Math.min(distanceRemaining / ev.pointer.velocityY * 0.75, 500);
+ element.css($mdConstant.CSS.TRANSITION_DURATION, transitionDuration + 'ms');
+ $mdUtil.nextTick($mdBottomSheet.cancel,true);
+ } else {
+ element.css($mdConstant.CSS.TRANSITION_DURATION, '');
+ element.css($mdConstant.CSS.TRANSFORM, '');
+ }
+ }
+ }
+
+ }
+
+}
diff --git a/src/components/bottomSheet/_bottomSheet.scss b/src/components/bottomSheet/bottom-sheet.scss
similarity index 74%
rename from src/components/bottomSheet/_bottomSheet.scss
rename to src/components/bottomSheet/bottom-sheet.scss
index d4414812ee..994a9cc5b8 100644
--- a/src/components/bottomSheet/_bottomSheet.scss
+++ b/src/components/bottomSheet/bottom-sheet.scss
@@ -1,10 +1,10 @@
-$bottom-sheet-horizontal-padding: 2 * $baseline-grid;
-$bottom-sheet-vertical-padding: 1 * $baseline-grid;
-$bottom-sheet-icon-after-margin: 4 * $baseline-grid;
-$bottom-sheet-list-item-height: 6 * $baseline-grid;
-$bottom-sheet-hidden-bottom-padding: 80px;
-$bottom-sheet-header-height: 7 * $baseline-grid;
-$bottom-sheet-grid-font-weight: 300;
+$bottom-sheet-horizontal-padding: 2 * $baseline-grid !default;
+$bottom-sheet-vertical-padding: 1 * $baseline-grid !default;
+$bottom-sheet-icon-after-margin: 4 * $baseline-grid !default;
+$bottom-sheet-list-item-height: 6 * $baseline-grid !default;
+$bottom-sheet-hidden-bottom-padding: 80px !default;
+$bottom-sheet-header-height: 7 * $baseline-grid !default;
+$bottom-sheet-grid-font-weight: 400 !default;
md-bottom-sheet {
position: absolute;
@@ -14,10 +14,11 @@ md-bottom-sheet {
padding: $bottom-sheet-vertical-padding $bottom-sheet-horizontal-padding $bottom-sheet-vertical-padding + $bottom-sheet-hidden-bottom-padding $bottom-sheet-horizontal-padding;
z-index: $z-index-bottom-sheet;
- border-top: 1px solid;
+ border-top-width: 1px;
+ border-top-style: solid;
transform: translate3d(0, $bottom-sheet-hidden-bottom-padding, 0);
- transition: 0.2s linear;
+ transition: $swift-ease-out;
transition-property: transform;
&.md-has-header {
@@ -38,6 +39,7 @@ md-bottom-sheet {
&.ng-leave-active {
transform: translate3d(0, 100%, 0) !important;
+ transition: $swift-ease-in;
}
.md-subheader {
@@ -55,7 +57,7 @@ md-bottom-sheet {
fill: #444;
}
- md-item {
+ md-list-item {
display: flex;
outline: none;
@@ -65,7 +67,8 @@ md-bottom-sheet {
}
&.md-list {
- md-item {
+ md-list-item {
+ padding: 0;
align-items: center;
height: $bottom-sheet-list-item-height;
@@ -92,7 +95,7 @@ md-bottom-sheet {
align-items: center;
}
- md-item {
+ md-list-item {
flex-direction: column;
align-items: center;
transition: all 0.5s;
@@ -118,25 +121,25 @@ md-bottom-sheet {
}
}
- @media screen and (max-width: $layout-breakpoint-sm) {
+ @media (max-width: $layout-breakpoint-sm) {
@include grid-items-per-row(3, true);
}
- @media screen and (min-width: $layout-breakpoint-sm) and (max-width: $layout-breakpoint-md) {
+ @media (min-width: $layout-breakpoint-sm) and (max-width: $layout-breakpoint-md - 1) {
@include grid-items-per-row(4);
}
- @media screen and (min-width: $layout-breakpoint-md) and (max-width: $layout-breakpoint-lg) {
+ @media (min-width: $layout-breakpoint-md) and (max-width: $layout-breakpoint-lg - 1) {
@include grid-items-per-row(6);
}
- @media screen and (min-width: $layout-breakpoint-lg) {
+ @media (min-width: $layout-breakpoint-lg) {
@include grid-items-per-row(7);
}
- .md-item-content {
+ .md-list-item-content {
display: flex;
flex-direction: column;
align-items: center;
@@ -145,6 +148,7 @@ md-bottom-sheet {
}
.md-grid-item-content {
+ border: 1px solid transparent;
display: flex;
flex-direction: column;
align-items: center;
@@ -159,7 +163,7 @@ md-bottom-sheet {
margin: 0 0;
}
- p.md-grid-text {
+ .md-grid-text {
font-weight: $bottom-sheet-grid-font-weight;
line-height: 2 * $baseline-grid;
font-size: 2 * $baseline-grid - 3;
@@ -167,8 +171,16 @@ md-bottom-sheet {
white-space: nowrap;
width: 8 * $baseline-grid;
text-align: center;
+ text-transform: none;
padding-top: 1 * $baseline-grid;
}
}
}
}
+
+// IE only
+@media screen and (-ms-high-contrast: active) {
+ md-bottom-sheet {
+ border: 1px solid #fff;
+ }
+}
diff --git a/src/components/bottomSheet/bottom-sheet.spec.js b/src/components/bottomSheet/bottom-sheet.spec.js
new file mode 100644
index 0000000000..8c0ff30623
--- /dev/null
+++ b/src/components/bottomSheet/bottom-sheet.spec.js
@@ -0,0 +1,158 @@
+describe('$mdBottomSheet service', function () {
+ beforeEach(module('material.components.bottomSheet'));
+
+ describe('#build()', function () {
+ it('should not close when `clickOutsideToClose == true`',
+ inject(function ($mdBottomSheet, $rootElement, $material) {
+ var parent = angular.element('');
+ $mdBottomSheet.show({
+ template: '
',
+ parent: parent,
+ clickOutsideToClose: true
+ });
+
+ $material.flushOutstandingAnimations();
+
+ expect(parent.find('md-bottom-sheet').length).toBe(1);
+
+ var backdrop = parent.find('md-backdrop');
+
+ backdrop.triggerHandler({
+ type: 'click',
+ target: backdrop[0]
+ });
+
+ $material.flushInterimElement();
+ expect(parent.find('md-bottom-sheet').length).toBe(0);
+ }));
+
+ it('should not close when `clickOutsideToClose == false`',
+ inject(function ($mdBottomSheet, $rootElement, $material) {
+ var parent = angular.element('');
+ $mdBottomSheet.show({
+ template: '
',
+ parent: parent,
+ clickOutsideToClose: false
+ });
+
+ $material.flushOutstandingAnimations();
+
+ expect(parent.find('md-bottom-sheet').length).toBe(1);
+
+ var backdrop = parent.find('md-backdrop');
+
+ backdrop.triggerHandler({
+ type: 'click',
+ target: backdrop[0]
+ });
+
+ $material.flushInterimElement();
+ expect(parent.find('md-bottom-sheet').length).toBe(1);
+ }));
+
+ it('should close when `escapeToClose == true`',
+ inject(function ($mdBottomSheet, $rootElement, $material, $mdConstant) {
+ var parent = angular.element('');
+ $mdBottomSheet.show({
+ template: '
',
+ parent: parent,
+ escapeToClose: true
+ });
+
+ $material.flushOutstandingAnimations();
+
+ expect(parent.find('md-bottom-sheet').length).toBe(1);
+
+ $rootElement.triggerHandler({
+ type: 'keyup',
+ keyCode: $mdConstant.KEY_CODE.ESCAPE
+ });
+
+ $material.flushInterimElement();
+ expect(parent.find('md-bottom-sheet').length).toBe(0);
+ }));
+
+ it('should not close when `escapeToClose == false`',
+ inject(function ($mdBottomSheet, $rootScope, $rootElement, $timeout, $animate, $mdConstant) {
+ var parent = angular.element('');
+ $mdBottomSheet.show({
+ template: '
',
+ parent: parent,
+ escapeToClose: false
+ });
+ $rootScope.$apply();
+
+ expect(parent.find('md-bottom-sheet').length).toBe(1);
+
+ $rootElement.triggerHandler({type: 'keyup', keyCode: $mdConstant.KEY_CODE.ESCAPE});
+
+ expect(parent.find('md-bottom-sheet').length).toBe(1);
+ }));
+
+ it('should close when navigation fires `scope.$destroy()`',
+ inject(function ($mdBottomSheet, $rootScope, $rootElement, $timeout, $material) {
+ var parent = angular.element('');
+ $mdBottomSheet.show({
+ template: '
',
+ parent: parent,
+ escapeToClose: false
+ });
+
+ $rootScope.$apply();
+ $material.flushOutstandingAnimations();
+
+ expect(parent.find('md-bottom-sheet').length).toBe(1);
+
+ $rootScope.$destroy();
+ $material.flushInterimElement();
+ expect(parent.find('md-bottom-sheet').length).toBe(0);
+ }));
+
+ it('should focus child with md-autofocus',
+ inject(function ($rootScope, $animate, $document, $mdBottomSheet) {
+ jasmine.mockElementFocus(this);
+ var parent = angular.element('');
+ var markup = '' +
+ '
' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ '';
+
+ $mdBottomSheet.show({
+ template: '',
+ parent: parent,
+ escapeToClose: false
+ });
+ $rootScope.$apply();
+
+ var sheet = parent.find('md-bottom-sheet');
+ expect(sheet.length).toBe(1);
+ var focusEl = sheet.find('input');
+
+ // Focus should be on the last md-autofocus element
+ expect($document.activeElement).toBe(focusEl[1]);
+ }));
+
+ // This test is mainly for touch devices as the -webkit-overflow-scrolling causes z-index issues
+ // if the scroll mask is appended to the body element
+ it('appends the scroll mask to the same parent',
+ inject(function ($mdBottomSheet, $rootScope) {
+ var parent = angular.element('');
+
+ $mdBottomSheet.show({
+ template: '
',
+ parent: parent
+ });
+
+ $rootScope.$apply();
+
+ var scrollMask = parent[0].querySelector('.md-scroll-mask');
+
+ expect(scrollMask).not.toBeNull();
+ }));
+ });
+});
diff --git a/src/components/bottomSheet/bottomSheet-theme.scss b/src/components/bottomSheet/bottomSheet-theme.scss
deleted file mode 100644
index 20cc555ad0..0000000000
--- a/src/components/bottomSheet/bottomSheet-theme.scss
+++ /dev/null
@@ -1,24 +0,0 @@
-$bottom-sheet-color-palette: $background-color-palette !default;
-$bottom-sheet-background-color: map-get($bottom-sheet-color-palette, '50') !default;
-$bottom-sheet-border-top-color: map-get($bottom-sheet-color-palette, '400') !default;
-$bottom-sheet-header-color: $foreground-secondary-color !default;
-$bottom-sheet-list-color: $foreground-secondary-color !default;
-
-md-bottom-sheet.md-#{$theme-name}-theme {
- background-color: $bottom-sheet-background-color;
- border-top-color: $bottom-sheet-border-top-color;
-
- &.md-list {
- md-item {
- color: $bottom-sheet-list-color;
- }
- }
-
- .md-subheader {
- background-color: $bottom-sheet-background-color;
- }
-
- .md-subheader {
- color: $bottom-sheet-header-color;
- }
-}
diff --git a/src/components/bottomSheet/bottomSheet.js b/src/components/bottomSheet/bottomSheet.js
deleted file mode 100644
index b28df3f8e6..0000000000
--- a/src/components/bottomSheet/bottomSheet.js
+++ /dev/null
@@ -1,262 +0,0 @@
-/**
- * @ngdoc module
- * @name material.components.bottomSheet
- * @description
- * BottomSheet
- */
-angular.module('material.components.bottomSheet', [
- 'material.components.backdrop',
- 'material.services.interimElement',
- 'material.services.theming'
-])
-.directive('mdBottomSheet', [
- MdBottomSheetDirective
-])
-.factory('$mdBottomSheet', [
- '$$interimElement',
- '$animate',
- '$mdEffects',
- '$timeout',
- '$$rAF',
- '$compile',
- '$mdTheming',
- MdBottomSheet
-]);
-
-function MdBottomSheetDirective() {
- return {
- restrict: 'E'
- };
-}
-
-/**
- * @ngdoc service
- * @name $mdBottomSheet
- * @module material.components.bottomSheet
- *
- * @description
- * `$mdBottomSheet` opens a bottom sheet over the app and provides a simple promise API.
- *
- * ### Restrictions
- *
- * - The bottom sheet's template must have an outer `` element.
- * - Add the `md-grid` class to the bottom sheet for a grid layout.
- * - Add the `md-list` class to the bottom sheet for a list layout.
- *
- * @usage
- *
- *
- *
- * Open a Bottom Sheet!
- *
- *
- *
- *
- * var app = angular.module('app', ['ngMaterial']);
- * app.controller('MyController', function($scope, $mdBottomSheet) {
- * $scope.openBottomSheet = function() {
- * $mdBottomSheet.show({
- * template: 'Hello!'
- * });
- * };
- * });
- *
- */
-
- /**
- * @ngdoc method
- * @name $mdBottomSheet#show
- *
- * @description
- * Show a bottom sheet with the specified options.
- *
- * @param {object} options An options object, with the following properties:
- *
- * - `templateUrl` - `{string=}`: The url of an html template file that will
- * be used as the content of the bottom sheet. Restrictions: the template must
- * have an outer `md-bottom-sheet` element.
- * - `template` - `{string=}`: Same as templateUrl, except this is an actual
- * template string.
- * - `controller` - `{string=}`: The controller to associate with this bottom sheet.
- * - `locals` - `{string=}`: An object containing key/value pairs. The keys will
- * be used as names of values to inject into the controller. For example,
- * `locals: {three: 3}` would inject `three` into the controller with the value
- * of 3.
- * - `targetEvent` - `{DOMClickEvent=}`: A click's event object. When passed in as an option,
- * the location of the click will be used as the starting point for the opening animation
- * of the the dialog.
- * - `resolve` - `{object=}`: Similar to locals, except it takes promises as values
- * and the bottom sheet will not open until the promises resolve.
- * - `controllerAs` - `{string=}`: An alias to assign the controller to on the scope.
- *
- * @returns {promise} A promise that can be resolved with `$mdBottomSheet.hide()` or
- * rejected with `$mdBottomSheet.cancel()`.
- */
-
-/**
- * @ngdoc method
- * @name $mdBottomSheet#hide
- *
- * @description
- * Hide the existing bottom sheet and resolve the promise returned from
- * `$mdBottomSheet.show()`.
- *
- * @param {*=} response An argument for the resolved promise.
- *
- */
-
-/**
- * @ngdoc method
- * @name $mdBottomSheet#cancel
- *
- * @description
- * Hide the existing bottom sheet and reject the promise returned from
- * `$mdBottomSheet.show()`.
- *
- * @param {*=} response An argument for the rejected promise.
- *
- */
-
-function MdBottomSheet($$interimElement, $animate, $mdEffects, $timeout, $$rAF, $compile, $mdTheming) {
- var backdrop;
-
- var $mdBottomSheet;
- return $mdBottomSheet = $$interimElement({
- themable: true,
- targetEvent: null,
- onShow: onShow,
- onRemove: onRemove,
- });
-
- function onShow(scope, element, options) {
- // Add a backdrop that will close on click
- backdrop = $compile('')(scope);
- backdrop.on('click touchstart', function() {
- $timeout($mdBottomSheet.cancel);
- });
- $mdTheming.inherit(backdrop, options.parent);
-
- $animate.enter(backdrop, options.parent, null);
-
- var bottomSheet = new BottomSheet(element);
- options.bottomSheet = bottomSheet;
-
- // Give up focus on calling item
- options.targetEvent && angular.element(options.targetEvent.target).blur();
- $mdTheming.inherit(bottomSheet.element, options.parent);
-
- return $animate.enter(bottomSheet.element, options.parent);
-
- }
-
- function onRemove(scope, element, options) {
- var bottomSheet = options.bottomSheet;
- $animate.leave(backdrop);
- return $animate.leave(bottomSheet.element).then(function() {
- bottomSheet.cleanup();
-
- // Restore focus
- options.targetEvent && angular.element(options.targetEvent.target).focus();
- });
- }
-
- /**
- * BottomSheet class to apply bottom-sheet behavior to an element
- */
- function BottomSheet(element) {
- var MAX_OFFSET = 80; // amount past the bottom of the element that we can drag down, this is same as in _bottomSheet.scss
- var WIGGLE_AMOUNT = 20; // point where it starts to get "harder" to drag
- var CLOSING_VELOCITY = 10; // how fast we need to flick down to close the sheet
- var startY, lastY, velocity, transitionDelay, startTarget;
-
- // coercion incase $mdCompiler returns multiple elements
- element = element.eq(0);
-
- element.on('touchstart', onTouchStart);
- element.on('touchmove', onTouchMove);
- element.on('touchend', onTouchEnd);
-
- return {
- element: element,
- cleanup: function cleanup() {
- element.off('touchstart', onTouchStart);
- element.off('touchmove', onTouchMove);
- element.off('touchend', onTouchEnd);
- }
- };
-
- function onTouchStart(e) {
- e.preventDefault();
- startTarget = e.target;
- startY = getY(e);
-
- // Disable transitions on transform so that it feels fast
- transitionDelay = element.css($mdEffects.TRANSITION_DURATION);
- element.css($mdEffects.TRANSITION_DURATION, '0s');
- }
-
- function onTouchEnd(e) {
- // Re-enable the transitions on transforms
- element.css($mdEffects.TRANSITION_DURATION, transitionDelay);
-
- var currentY = getY(e);
- // If we didn't scroll much, and we didn't change targets, assume its a click
- if ( Math.abs(currentY - startY) < 5 && e.target == startTarget) {
- angular.element(e.target).triggerHandler('click');
- } else {
- // If they went fast enough, trigger a close.
- if (velocity > CLOSING_VELOCITY) {
- $timeout($mdBottomSheet.cancel);
-
- // Otherwise, untransform so that we go back to our normal position
- } else {
- setTransformY(undefined);
- }
- }
- }
-
- function onTouchMove(e) {
- var currentY = getY(e);
- var delta = currentY - startY;
-
- velocity = currentY - lastY;
- lastY = currentY;
-
- // Do some conversion on delta to get a friction-like effect
- delta = adjustedDelta(delta);
- setTransformY(delta + MAX_OFFSET);
- }
-
- /**
- * Helper function to find the Y aspect of various touch events.
- **/
- function getY(e) {
- var touch = e.touches && e.touches.length ? e.touches[0] : e.changedTouches[0];
- return touch.clientY;
- }
-
- /**
- * Transform the element along the y-axis
- **/
- function setTransformY(amt) {
- if (amt === null || amt === undefined) {
- element.css($mdEffects.TRANSFORM, '');
- } else {
- element.css($mdEffects.TRANSFORM, 'translate3d(0, ' + amt + 'px, 0)');
- }
- }
-
- // Returns a new value for delta that will never exceed MAX_OFFSET_AMOUNT
- // Will get harder to exceed it as you get closer to it
- function adjustedDelta(delta) {
- if ( delta < 0 && delta < -MAX_OFFSET + WIGGLE_AMOUNT) {
- delta = -delta;
- var base = MAX_OFFSET - WIGGLE_AMOUNT;
- delta = Math.max(-MAX_OFFSET, -Math.min(MAX_OFFSET - 5, base + ( WIGGLE_AMOUNT * (delta - base)) / MAX_OFFSET) - delta / 50);
- }
-
- return delta;
- }
- }
-
-}
diff --git a/src/components/bottomSheet/bottomSheet.spec.js b/src/components/bottomSheet/bottomSheet.spec.js
deleted file mode 100644
index 140e82ec13..0000000000
--- a/src/components/bottomSheet/bottomSheet.spec.js
+++ /dev/null
@@ -1,3 +0,0 @@
-describe('$mdBottomSheet service', function() {
- beforeEach(module('material.components.mdBottomSheet', 'ngAnimateMock'));
-});
diff --git a/src/components/bottomSheet/demoBasicUsage/bottom-sheet-grid-template.html b/src/components/bottomSheet/demoBasicUsage/bottom-sheet-grid-template.html
index f625e0d085..897a27d6c9 100644
--- a/src/components/bottomSheet/demoBasicUsage/bottom-sheet-grid-template.html
+++ b/src/components/bottomSheet/demoBasicUsage/bottom-sheet-grid-template.html
@@ -1,14 +1,17 @@
-
-
-
+
+
+
With clickOutsideToClose option, drag down or press ESC to close
+
+
+
+
-
-
-
-
- {{ item.name }}
-
+
+
+ {{ item.name }}
+
-
-
+
+
+
diff --git a/src/components/bottomSheet/demoBasicUsage/bottom-sheet-list-template.html b/src/components/bottomSheet/demoBasicUsage/bottom-sheet-list-template.html
index 98c37cd7ca..5bfb6bfb66 100644
--- a/src/components/bottomSheet/demoBasicUsage/bottom-sheet-list-template.html
+++ b/src/components/bottomSheet/demoBasicUsage/bottom-sheet-list-template.html
@@ -1,12 +1,16 @@
-