diff --git a/ember-cli-build.js b/ember-cli-build.js
index c76c8afe39b3..95e39df7194a 100644
--- a/ember-cli-build.js
+++ b/ember-cli-build.js
@@ -9,14 +9,14 @@ var broccoliAutoprefixer = require('broccoli-autoprefixer');
var autoprefixerOptions = require('./build/autoprefixer-options');
module.exports = function(defaults) {
- var demoAppCssTree = new BroccoliSass(['src/demo-app'], './demo-app.scss', 'demo-app.css');
+ var demoAppCssTree = new BroccoliSass(['src/demo-app'], './demo-app.scss', 'demo-app/demo-app.css');
var componentCssTree = getComponentsCssTree();
var angularAppTree = new Angular2App(defaults);
return mergeTrees([
angularAppTree.toTree(),
componentCssTree,
- demoAppCssTree,
+ demoAppCssTree
]);
};
diff --git a/karma-test-shim.js b/karma-test-shim.js
index 6cca7c7f829f..536922f266fb 100644
--- a/karma-test-shim.js
+++ b/karma-test-shim.js
@@ -8,10 +8,11 @@ __karma__.loaded = function() {};
/**
* Gets map of module alias to location or package.
* @param dir Directory name under `src/` for create a map for.
+ * @param core Is that a map of the core files
*/
-function getPathsMap(dir) {
+function getPathsMap(dir, core) {
return Object.keys(window.__karma__.files)
- .filter(isComponentsFile)
+ .filter(!!core ? isCoreFile : isComponentsFile)
.reduce(function(pathsMapping, appPath) {
var pathToReplace = new RegExp('^/base/dist/' + dir + '/');
var moduleName = appPath.replace(pathToReplace, './').replace(/\.js$/, '');
@@ -26,6 +27,11 @@ System.config({
defaultExtension: false,
format: 'register',
map: getPathsMap('components')
+ },
+ 'base/dist/core': {
+ defaultExtension: false,
+ format: 'register',
+ map: getPathsMap('core', true)
}
}
});
@@ -46,6 +52,10 @@ System.import('angular2/platform/browser').then(function(browser_adapter) {
__karma__.error(error.stack || error);
});
+function isCoreFile(filePath) {
+ return /^\/base\/dist\/core\/(?!spec)([a-z0-9-_\/]+)\.js$/.test(filePath);
+}
+
function isComponentsFile(filePath) {
return /^\/base\/dist\/components\/(?!spec)([a-z0-9-_\/]+)\.js$/.test(filePath);
}
diff --git a/karma.conf.js b/karma.conf.js
index 2858b845050a..063e9d90d04a 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -31,7 +31,7 @@ module.exports = function(config) {
proxies: {
// required for component assests fetched by Angular's compiler
"/demo-app/": "/base/dist/demo-app/",
- "/components/": "/base/dist/components/",
+ "/components/": "/base/dist/components/"
},
exclude: [],
preprocessors: {},
diff --git a/src/components/switch/switch.html b/src/components/switch/switch.html
new file mode 100644
index 000000000000..5ce0617ad0ed
--- /dev/null
+++ b/src/components/switch/switch.html
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/components/switch/switch.scss b/src/components/switch/switch.scss
new file mode 100644
index 000000000000..37b0dd332002
--- /dev/null
+++ b/src/components/switch/switch.scss
@@ -0,0 +1,220 @@
+@import "variables";
+@import "shadows";
+@import "mixins";
+
+//TODO Temporary Theme
+@import "default-theme";
+
+$switch-width: 36px !default;
+$switch-height: 8px * 3 !default;
+$switch-bar-height: 14px !default;
+$switch-thumb-size: 20px !default;
+$switch-margin: 16px !default;
+
+.md-inline-form.md-switch {
+ margin-top: 18px;
+ margin-bottom: 19px;
+}
+
+md-switch {
+ margin: $switch-margin 0;
+ white-space: nowrap;
+ cursor: pointer;
+ outline: none;
+ user-select: none;
+ height: 30px;
+ line-height: 28px;
+ align-items: center;
+ display: flex;
+
+ @include rtl(margin-left, inherit, $switch-margin);
+ @include rtl(margin-right, $switch-margin, inherit);
+
+ &:last-of-type {
+ @include rtl(margin-left, inherit, 0);
+ @include rtl(margin-right, 0, inherit);
+ }
+
+ &[disabled] {
+ cursor: default;
+
+ .md-container {
+ cursor: default;
+ }
+ }
+
+ .md-container {
+ cursor: grab;
+ width: $switch-width;
+ height: $switch-height;
+ position: relative;
+ user-select: none;
+ margin-right: 8px;
+ float: left;
+ }
+
+ &:not([disabled]) {
+ .md-dragging,
+ &.md-dragging .md-container {
+ cursor: grabbing;
+ }
+ }
+
+ &.md-focused:not([disabled]) {
+ .md-thumb:before {
+ left: -8px;
+ top: -8px;
+ right: -8px;
+ bottom: -8px;
+ }
+
+ &:not(.md-checked).md-thumb:before {
+ background-color: rgba(0, 0, 0, 0.12);
+ }
+ }
+
+ .md-label {
+ border: 0 transparent;
+ float: left;
+ }
+
+ .md-bar {
+ left: 1px;
+ width: $switch-width - 2px;
+ top: $switch-height / 2 - $switch-bar-height / 2;
+ height: $switch-bar-height;
+ border-radius: 8px;
+ position: absolute;
+ }
+
+ .md-thumb-container {
+ top: $switch-height / 2 - $switch-thumb-size / 2;
+ left: 0;
+ width: $switch-width - $switch-thumb-size;
+ position: absolute;
+ transform: translate3d(0,0,0);
+ z-index: 1;
+ }
+ &.md-checked .md-thumb-container {
+ transform: translate3d(100%,0,0);
+ }
+
+ .md-thumb {
+ position: absolute;
+ margin: 0;
+ left: 0;
+ top: 0;
+ outline: none;
+ height: $switch-thumb-size;
+ width: $switch-thumb-size;
+ border-radius: 50%;
+ box-shadow: $md-shadow-bottom-z-1;
+
+ &:before {
+ background-color: transparent;
+ border-radius: 50%;
+ content: '';
+ position: absolute;
+ display: block;
+ height: auto;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ transition: all 0.5s;
+ width: auto;
+ }
+ }
+
+ &:not(.md-dragging) {
+ .md-bar,
+ .md-thumb-container,
+ .md-thumb {
+ transition: $swift-linear;
+ transition-property: transform, background-color;
+ }
+
+ .md-bar,
+ .md-thumb {
+ transition-delay: 0.05s;
+ }
+ }
+
+ // COLOR THEMING
+ .md-thumb {
+ background-color: md-color($md-background, 50);
+ }
+ .md-bar {
+ background-color: md-color($md-background, 500);
+ }
+
+ &.md-checked {
+ .md-ink-ripple {
+ color: md-color($md-accent);
+ }
+
+ .md-thumb {
+ background-color: md-color($md-accent);
+ }
+
+ .md-bar {
+ background-color: md-color($md-accent, 0.5);
+ }
+
+ &.md-focused .md-thumb:before {
+ background-color: md-color($md-accent, 0.26);
+ }
+
+ &.md-primary {
+ .md-ink-ripple {
+ color: md-color($md-primary);
+ }
+ .md-thumb {
+ background-color: md-color($md-primary);
+ }
+ .md-bar {
+ background-color: md-color($md-primary, 0.5);
+ }
+ &.md-focused .md-thumb:before {
+ background-color: md-color($md-primary, 0.26);
+ }
+ }
+
+ &.md-warn {
+ .md-ink-ripple {
+ color: md-color($md-warn);
+ }
+ .md-thumb {
+ background-color: md-color($md-warn);
+ }
+ .md-bar {
+ background-color: md-color($md-warn, 0.5);
+ }
+ &.md-focused .md-thumb:before {
+ background-color: md-color($md-warn, 0.26);
+ }
+ }
+ }
+
+ &[disabled] {
+ .md-thumb {
+ background-color: md-color($md-background, 400);
+ }
+ .md-bar {
+ background-color: md-color($md-foreground, 'divider');
+ }
+ }
+}
+
+@media screen and (-ms-high-contrast: active) {
+ md-switch.md-default-theme .md-bar {
+ background-color: #666;
+ }
+ md-switch.md-default-theme.md-checked .md-bar {
+ background-color: #9E9E9E;
+ }
+ md-switch.md-default-theme .md-thumb {
+ background-color: #fff;
+ }
+}
+
diff --git a/src/components/switch/switch.spec.ts b/src/components/switch/switch.spec.ts
new file mode 100644
index 000000000000..3e450e0dc515
--- /dev/null
+++ b/src/components/switch/switch.spec.ts
@@ -0,0 +1,72 @@
+import {
+ it,
+ iit,
+ describe,
+ ddescribe,
+ expect,
+ inject,
+ injectAsync,
+ TestComponentBuilder,
+ beforeEachProviders,
+ beforeEach,
+} from 'angular2/testing';
+import {provide, Component} from 'angular2/core';
+import {DebugElement} from "angular2/core";
+import {MdSwitch} from './switch';
+import {AsyncTestFn} from "angular2/testing";
+import {FORM_DIRECTIVES} from "angular2/common";
+import {Input} from "angular2/core";
+import {By} from 'angular2/platform/browser';
+
+describe('MdSwitch', () => {
+ let builder: TestComponentBuilder;
+
+ beforeEach(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => { builder = tcb; }));
+
+ describe('md-switch', () => {
+ it('should change the model value', (done:() => void) => {
+ return builder.createAsync(TestApp).then((fixture) => {
+ let testComponent = fixture.debugElement.componentInstance;
+ let switchElement = fixture.debugElement.query(By.css('md-switch'));
+
+ expect(switchElement.nativeElement.classList.contains('md-checked')).toBe(false);
+
+ testComponent.testSwitch = true;
+
+ fixture.detectChanges();
+
+ expect(switchElement.nativeElement.classList.contains('md-checked')).toBe(true);
+ done();
+ });
+ });
+
+ it('should not change the model if disabled', (done:() => void) => {
+ return builder.createAsync(TestApp).then((fixture) => {
+ let testComponent = fixture.debugElement.componentInstance;
+ let switchElement = fixture.debugElement.query(By.css('md-switch'));
+
+ expect(switchElement.nativeElement.classList.contains('md-checked')).toBe(false);
+
+ testComponent.isDisabled = true;
+ fixture.detectChanges();
+
+ switchElement.nativeElement.click();
+
+ expect(switchElement.nativeElement.classList.contains('md-checked')).toBe(false);
+ done();
+ });
+ });
+ });
+});
+
+/** Test component that contains an MdSwitch. */
+@Component({
+ selector: 'test-app',
+ directives: [MdSwitch, FORM_DIRECTIVES],
+ template:
+ 'Test Switch',
+})
+class TestApp {
+ testSwitch = false;
+ isDisabled = false;
+}
diff --git a/src/components/switch/switch.ts b/src/components/switch/switch.ts
new file mode 100644
index 000000000000..473d72289c93
--- /dev/null
+++ b/src/components/switch/switch.ts
@@ -0,0 +1,147 @@
+import {Component, ViewEncapsulation, ElementRef} from 'angular2/core';
+import {MdDrag} from '../../core/services/drag/drag';
+import {ControlValueAccessor} from "angular2/common";
+import {NgControl} from "angular2/common";
+import {Optional} from "angular2/core";
+import {Renderer} from "angular2/core";
+
+@Component({
+ selector: 'md-switch',
+ inputs: ['disabled'],
+ host: {
+ '[attr.aria-disabled]': 'disabled',
+ '(click)': 'onClick()'
+ },
+ templateUrl: './components/switch/switch.html',
+ styleUrls: ['./components/switch/switch.css'],
+ encapsulation: ViewEncapsulation.None,
+})
+export class MdSwitch implements ControlValueAccessor {
+
+ elementRef: ElementRef;
+ componentElement: HTMLElement;
+ switchContainer: HTMLElement;
+ thumbContainer: HTMLElement;
+
+ dragData: any;
+ dragClick = false;
+
+ // Accessor Values
+ onChange = (_:any) => {};
+ onTouched = () => {};
+
+ // storage values
+ checked_: any;
+ disabled_: boolean;
+
+ constructor(private _elementRef: ElementRef, private _renderer: Renderer, @Optional() ngControl: NgControl) {
+ this.componentElement = _elementRef.nativeElement;
+ this.elementRef = _elementRef;
+
+ if (ngControl) {
+ ngControl.valueAccessor = this;
+ }
+ }
+
+ ngOnInit() {
+ this.switchContainer = this.componentElement.querySelector('.md-container');
+ this.thumbContainer = this.componentElement.querySelector('.md-thumb-container');
+
+ MdDrag.register(this.switchContainer);
+
+ this.switchContainer.addEventListener('$md.dragstart', (ev: CustomEvent) => this.onDragStart(ev));
+ this.switchContainer.addEventListener('$md.drag', (ev: CustomEvent) => this.onDrag(ev));
+ this.switchContainer.addEventListener('$md.dragend', (ev: CustomEvent) => this.onDragEnd(ev));
+
+ }
+
+
+ onDragStart(event: CustomEvent) {
+ if (this.disabled) return;
+
+ this.componentElement.classList.add('md-dragging');
+
+ this.dragData = {
+ width: this.thumbContainer.offsetWidth
+ };
+
+ this.componentElement.classList.remove('transition')
+ }
+
+ onDrag(event: CustomEvent) {
+ if (this.disabled) return;
+
+ let percent = event.detail.pointer.distanceX / this.dragData.width;
+
+ let translate = this.checked ? 1 + percent : percent;
+ translate = Math.max(0, Math.min(1, translate));
+
+ this.thumbContainer.style.transform = 'translate3d(' + (100 * translate) + '%,0,0)';
+ this.dragData.translate = translate;
+ }
+
+ onDragEnd(event: CustomEvent) {
+ if (this.disabled) return;
+
+ this.componentElement.classList.remove('md-dragging');
+ this.thumbContainer.style.transform = null;
+
+
+ var isChanged = this.checked ? this.dragData.translate < 0.5 : this.dragData.translate > 0.5;
+ if (isChanged || !this.dragData.translate) {
+ this.checked = !this.checked;
+ }
+
+ this.dragData = null;
+
+ // Wait for incoming mouseup click
+ this.dragClick = true;
+ setTimeout(() => this.dragClick = false, 1);
+ }
+
+ onClick() {
+ if (!this.dragClick && !this.disabled) {
+ this.checked = !this.checked;
+ }
+ }
+
+
+ writeValue(value: any): void {
+ this.checked = value;
+ }
+
+ registerOnChange(fn: any): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ this.onTouched = fn;
+ }
+
+ get disabled(): string|boolean {
+ return this.disabled_;
+ }
+
+ set disabled(value: string|boolean) {
+ if (typeof value == 'string') {
+ this.disabled_ = (value === 'true' || value === '');
+ } else {
+ this.disabled_ = value;
+ }
+
+ this._renderer.setElementAttribute(this._elementRef, 'disabled', this.disabled_ ? 'true' : undefined);
+ }
+
+ get checked() {
+ return !!this.checked_;
+ }
+
+ set checked(value) {
+ this.checked_ = !!value;
+ this.onChange(this.checked_);
+
+ this._renderer.setElementAttribute(this._elementRef, 'aria-checked', this.checked_);
+ this.componentElement.classList.toggle('md-checked', this.checked);
+ }
+
+}
\ No newline at end of file
diff --git a/src/core/services/drag/drag.ts b/src/core/services/drag/drag.ts
new file mode 100644
index 000000000000..e5a26f1a472c
--- /dev/null
+++ b/src/core/services/drag/drag.ts
@@ -0,0 +1,113 @@
+import {DOM} from "angular2/src/platform/dom/dom_adapter";
+
+export class MdDrag {
+
+ private static START_EVENTS = ['mousedown', 'touchstart', 'pointerdown'];
+ private static MOVE_EVENTS = ['mousemove', 'touchmove', 'pointermove'];
+ private static END_EVENTS = ['mouseup', 'mouseleave', 'touchend', 'touchcancel', 'pointerup', 'pointercancel'];
+
+ private static handlers: any[] = [];
+ private static currentItem: any;
+
+ public static init() {
+ this.registerEvents();
+ }
+
+ public static terminate() {
+ this.START_EVENTS.forEach(entry => document.removeEventListener(entry, (ev: PointerEvent) => this.onStartDrag(ev)));
+ this.MOVE_EVENTS.forEach(entry => document.removeEventListener(entry, (ev: PointerEvent) => this.onMoveDrag(ev)));
+ this.END_EVENTS.forEach(entry => document.removeEventListener(entry, (ev: PointerEvent) => this.onStopDrag(ev)));
+ }
+
+ public static register(element: any) {
+ element.$mdDrag = true;
+ this.handlers.push({
+ element: element
+ });
+ }
+
+ private static registerEvents() {
+ this.START_EVENTS.forEach(entry => document.addEventListener(entry, (ev:PointerEvent) => this.onStartDrag(ev)));
+ this.MOVE_EVENTS.forEach(entry => document.addEventListener(entry, (ev:PointerEvent) => this.onMoveDrag(ev)));
+ this.END_EVENTS.forEach(entry => document.addEventListener(entry, (ev:PointerEvent) => this.onStopDrag(ev)));
+ }
+ private static createPointer(event: PointerEvent): any {
+ return {
+ startX: event.pageX,
+ startY: event.pageY,
+ distanceX: 0,
+ distanceY: 0
+ };
+ }
+
+ private static updatePointer(event: PointerEvent, pointer: any): any {
+ pointer.distanceX = event.pageX - pointer.startX;
+ pointer.distanceY = event.pageY - pointer.startY;
+ return pointer;
+ }
+
+ private static triggerEvent(element: Node, suffix: string, pointer: any) {
+ element.dispatchEvent(new CustomEvent('$md.' + suffix, {
+ detail: {
+ pointer: pointer
+ }
+ }));
+ }
+
+ private static onStartDrag(event: PointerEvent) {
+ var element = this.getNearestParent(event.srcElement);
+ if (!element || this.handlers.indexOf(element) != -1) return;
+ event.preventDefault();
+
+ var item = this.findElement(this.handlers, 'element', element);
+ item.pointer = this.createPointer(event);
+
+ this.triggerEvent(element, 'dragstart', item.pointer);
+
+ this.currentItem = item;
+ }
+
+ private static onMoveDrag(event: PointerEvent) {
+ if (!this.currentItem) return;
+ event.preventDefault();
+
+ this.currentItem.pointer = this.updatePointer(event, this.currentItem.pointer);
+
+ this.triggerEvent(this.currentItem.element, 'drag', this.currentItem.pointer);
+ }
+
+ private static onStopDrag(event: PointerEvent) {
+ if (!this.currentItem) return;
+ event.preventDefault();
+
+ this.currentItem.pointer = this.updatePointer(event, this.currentItem.pointer);
+
+ this.triggerEvent(this.currentItem.element, 'dragend', this.currentItem.pointer);
+
+ this.currentItem = null;
+ }
+
+ //TODO Should be accesible in a util class
+ private static getNearestParent(node: any): Node {
+ var current = node;
+ while (current) {
+ if (current.$mdDrag) {
+ return current;
+ }
+ current = current.parentNode;
+ }
+ return null;
+ }
+
+ private static findElement(arr: any[], propName: string, propValue: any): any {
+ for (var i = 0; i < arr.length; i++) {
+ if (arr[i][propName] == propValue) {
+ return arr[i];
+ }
+ }
+ }
+
+}
+
+// Init Class
+MdDrag.init();
\ No newline at end of file
diff --git a/src/core/style/_mixins.scss b/src/core/style/_mixins.scss
new file mode 100644
index 000000000000..ae3a85e57dfd
--- /dev/null
+++ b/src/core/style/_mixins.scss
@@ -0,0 +1,21 @@
+@mixin rtl($prop, $value, $rtl-value) {
+ #{$prop}: $value;
+
+ html[dir=rtl] & {
+ #{$prop}: $rtl-value;
+ unicode-bidi: embed;
+ }
+ body[dir=rtl] & {
+ #{$prop}: $rtl-value;
+ unicode-bidi: embed;
+ }
+
+ bdo[dir=rtl] {
+ direction: rtl;
+ unicode-bidi: bidi-override;
+ }
+ bdo[dir=ltr] {
+ direction: ltr;
+ unicode-bidi: bidi-override;
+ }
+}
\ No newline at end of file
diff --git a/src/core/style/_variables.scss b/src/core/style/_variables.scss
index 880a00913366..a98e27665e73 100644
--- a/src/core/style/_variables.scss
+++ b/src/core/style/_variables.scss
@@ -20,3 +20,7 @@ $swift-ease-in: all $swift-ease-in-duration $swift-ease-in-timing-function !defa
$swift-ease-in-out-duration: 0.5s !default;
$swift-ease-in-out-timing-function: cubic-bezier(0.35, 0, 0.25, 1) !default;
$swift-ease-in-out: all $swift-ease-in-out-duration $swift-ease-in-out-timing-function !default;
+
+$swift-linear-duration: 0.08s !default;
+$swift-linear-timing-function: linear !default;
+$swift-linear: all $swift-linear-duration $swift-linear-timing-function !default;
\ No newline at end of file
diff --git a/src/demo-app/demo-app.html b/src/demo-app/demo-app.html
index 8cfb86efe370..79f13fe18d1d 100644
--- a/src/demo-app/demo-app.html
+++ b/src/demo-app/demo-app.html
@@ -4,4 +4,7 @@
+
+ Toggle Angular Version
+ Value: {{ switchValue }}
diff --git a/src/demo-app/demo-app.ts b/src/demo-app/demo-app.ts
index 9589221d781d..9edcb5dd2092 100644
--- a/src/demo-app/demo-app.ts
+++ b/src/demo-app/demo-app.ts
@@ -1,12 +1,15 @@
import {Component} from 'angular2/core';
import {MdButton} from '../components/button/button';
+import {MdSwitch} from '../components/switch/switch';
+import {FORM_DIRECTIVES} from "angular2/common";
@Component({
selector: 'demo-app',
providers: [],
templateUrl: 'demo-app/demo-app.html',
- directives: [MdButton],
+ styleUrls: ['demo-app/demo-app.css'],
+ directives: [MdButton, MdSwitch, FORM_DIRECTIVES],
pipes: []
})
export class DemoApp { }
diff --git a/src/index.html b/src/index.html
index c89b6cfc4383..a0b6f07ac945 100644
--- a/src/index.html
+++ b/src/index.html
@@ -6,6 +6,12 @@
{{content-for 'head'}}
+
+
@@ -23,6 +29,10 @@
format: 'register',
defaultExtension: 'js'
},
+ 'core': {
+ format: 'register',
+ defaultExtension: 'js'
+ },
'components': {
format: 'register',
defaultExtension: 'js'