提交 b6f511c6 编写于 作者: A Adam Barth

Port Sky widgets demo to Dart

This CL updates sky-box, sky-button, sky-checkbox, sky-input, and sky-radio to
work in Dart. We don't have a data binding system yet, so there's a bit more
plumbing in the code.

This CL adds support for sky-element@attributes, which lets you specify which
attributes your element supports. We use this information to synthesize getters
and setters for those attributes and to dispatch to mumbleChanged methods when
the attributes change.

I've also wrapped the widgets demo itself in a sky-scrollable so the whole
thing scrolls.

R=eseidel@chromium.org

Review URL: https://codereview.chromium.org/946813005
上级 2cbb0911
......@@ -9,7 +9,7 @@
boolean hasAttribute(DOMString name);
[TreatReturnedNullStringAs=Null] DOMString getAttribute(DOMString name);
[CustomElementCallbacks, RaisesException] void setAttribute(DOMString name, optional DOMString value);
[CustomElementCallbacks, RaisesException] void setAttribute(DOMString name, optional DOMString? value);
[CustomElementCallbacks] void removeAttribute(DOMString name);
sequence<Attr> getAttributes();
......
#!mojo mojo:sky_viewer
<!--
// Copyright 2014 The Chromium Authors. All rights reserved.
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-->
<sky>
<import src="/sky/framework/sky-box.sky"/>
<import src="/sky/framework/sky-button.sky"/>
<import src="/sky/framework/sky-checkbox.sky"/>
<import src="/sky/framework/sky-element/sky-element.sky" as="SkyElement"/>
<import src="/sky/framework/sky-input.sky"/>
<import src="/sky/framework/sky-radio.sky"/>
<sky-element name="widget-root">
<template>
<style>
div {
display: flex;
align-items: center;
}
sky-checkbox {
margin: 5px;
}
.output {
margin-left: 48px;
}
</style>
<sky-box title='Text'>
<sky-input id="text" value="{{ inputValue }}" />
<div>value = {{ inputValue }}</div>
</sky-box>
<sky-box title='Buttons'>
<sky-button id='button' on-click='handleClick'>Button</sky-button>
<div>highlight: {{ myButton.highlight }}</div>
<div>clickCount: {{ clickCount }}</div>
</sky-box>
<sky-box title='Checkboxes'>
<div><sky-checkbox id='checkbox' checked='{{ checked }}'/>Checkbox</div>
<div class="output">highlight: {{ myCheckbox.highlight }}</div>
<div class="output">checked: {{ myCheckbox.checked }}</div>
<div><sky-checkbox id='checkbox' checked="true"/>Checkbox, default checked.</div>
<div class="output">checked: {{ checked }}</div>
</sky-box>
<sky-box title='Radios'>
<sky-box title='Group One'>
<div><sky-radio group='foo'/>one</div>
<div><sky-radio group='foo' selected='true' />two</div>
<div><sky-radio group='foo'/>three</div>
</sky-box>
<sky-box title='Group Two'>
<div><sky-radio group='bar'/>A</div>
<div><sky-radio group='bar'/>B</div>
<div><sky-radio group='bar' selected='true' />C</div>
</sky-box>
</sky-box>
</template>
<script>
module.exports = class extends SkyElement {
created() {
this.myButton = null;
this.myCheckbox = null;
this.myText = null;
this.clickCount = 0;
this.inputValue = "Ready";
this.checked = false;
}
attached() {
this.myButton = this.shadowRoot.getElementById('button');
this.myCheckbox = this.shadowRoot.getElementById('checkbox');
this.myText = this.shadowRoot.getElementById('text');
this.clickCount = 0;
}
handleClick(event) {
this.clickCount++;
this.checked = !this.checked;
this.inputValue = "Moar clicking " + this.clickCount;
}
}.register();
</script>
</sky-element>
<import src="widget-root.sky"/>
<widget-root />
</sky>
<!--
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-->
<import src="/sky/framework/sky-box.sky" />
<import src="/sky/framework/sky-button.sky" />
<import src="/sky/framework/sky-checkbox.sky" />
<import src="/sky/framework/sky-element.sky" />
<import src="/sky/framework/sky-input.sky" />
<import src="/sky/framework/sky-radio.sky" />
<import src="/sky/framework/sky-scrollable.sky" />
<sky-element>
<template>
<style>
div {
display: flex;
align-items: center;
}
sky-checkbox {
margin: 5px;
}
.output {
margin-left: 48px;
}
sky-scrollable {
height: -webkit-fill-available;
}
</style>
<sky-scrollable>
<sky-box title='Text'>
<sky-input id="text" value="{{ inputValue }}" />
<div>value = {{ inputValue }}</div>
</sky-box>
<sky-box title='Buttons'>
<sky-button id='button'>Button</sky-button>
<div>highlight: {{ myButton.highlight }}</div>
<div>clickCount: {{ clickCount }}</div>
</sky-box>
<sky-box title='Checkboxes'>
<div><sky-checkbox id='checkbox' checked='{{ checked }}'/>Checkbox</div>
<div class="output">highlight: {{ myCheckbox.highlight }}</div>
<div class="output">checked: {{ myCheckbox.checked }}</div>
<div><sky-checkbox id='checkbox' checked="true"/>Checkbox, default checked.</div>
<div class="output">checked: {{ checked }}</div>
</sky-box>
<sky-box title='Radios'>
<sky-box title='Group One'>
<div><sky-radio group='foo'/>one</div>
<div><sky-radio group='foo' selected='true' />two</div>
<div><sky-radio group='foo'/>three</div>
</sky-box>
<sky-box title='Group Two'>
<div><sky-radio group='bar'/>A</div>
<div><sky-radio group='bar'/>B</div>
<div><sky-radio group='bar' selected='true' />C</div>
</sky-box>
</sky-box>
</sky-scrollable>
</template>
<script>
import "dart:sky";
@Tagname('widget-root')
class WidgetRoot extends SkyElement {
Element _button;
Element _checkbox;
Element _text;
int _clickCount = 0;
String _inputValue = "Ready";
bool _checked = false;
void shadowRootReady() {
_button = this.shadowRoot.getElementById('button');
_checkbox = this.shadowRoot.getElementById('checkbox');
_text = this.shadowRoot.getElementById('text');
_button.addEventListener('click', _handleClick);
}
void _handleClick(_) {
_clickCount++;
_checked = !_checked;
_inputValue = "Moar clicking ${_clickCount}";
}
}
_init(script) => register(script, WidgetRoot);
</script>
</sky-element>
<!--
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-->
<script>
import "dart:sky";
......
<!--
// Copyright 2014 The Chromium Authors. All rights reserved.
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-->
<import src="sky-element/sky-element.sky" as="SkyElement" />
<import src="sky-element.sky" />
<sky-element name="sky-box" attributes="title:string">
<sky-element attributes="title:string">
<template>
<style>
:host {
......@@ -15,23 +15,39 @@
border: 1px solid gray;
margin: 10px;
}
.title {
#title {
text-align: center;
font-size: 10px;
padding: 8px 8px 4px 8px;
}
.content {
#content {
padding: 4px 8px 8px 8px;
}
div {
flex: 1;
}
</style>
<div class="title">{{ title }}</div>
<div class="content"><content/></div>
<div id="title"></div>
<div id="content"><content/></div>
</template>
<script>
module.exports = class extends SkyElement {
}.register();
import "dart:sky";
@Tagname('sky-box')
class SkyBox extends SkyElement {
Element _title;
void shadowRootReady() {
_title = shadowRoot.getElementById('title');
_title.setChild(new Text(title));
}
void titleChanged(String oldValue, String newValue) {
if (_title != null)
_title.setChild(new Text(newValue));
}
}
_init(script) => register(script, SkyBox);
</script>
</sky-element>
<!--
// Copyright 2014 The Chromium Authors. All rights reserved.
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-->
<import src="sky-element/sky-element.sky" as="SkyElement" />
<import src="sky-element.sky" />
<sky-element
name="sky-button"
attributes="highlight:boolean"
on-pointerdown="handlePointerDown"
on-pointerup="handlePointerUp"
on-pointercancel="handlePointerCancel">
<sky-element attributes="highlight:boolean">
<template>
<style>
:host {
......@@ -29,21 +24,31 @@
<content />
</template>
<script>
module.exports = class extends SkyElement {
created() {
super.created();
import "dart:sky";
this.tabIndex = 0; // Make focusable.
@Tagname('sky-button')
class SkyButton extends SkyElement {
SkyButton() {
addEventListener('pointerdown', _handlePointerDown);
addEventListener('pointerup', _handlePointerUp);
addEventListener('pointercancel', _handlePointerCancel);
tabIndex = 0; // Make focusable.
}
handlePointerDown() {
this.highlight = true;
void _handlePointerDown(_) {
highlight = true;
}
handlePointerUp() {
this.highlight = false;
void _handlePointerUp(_) {
highlight = false;
}
handlePointerCancel() {
this.highlight = false;
void _handlePointerCancel(_) {
highlight = false;
}
}.register();
}
_init(script) => register(script, SkyButton);
</script>
</sky-element>
<!--
// Copyright 2014 The Chromium Authors. All rights reserved.
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-->
<import src="sky-button.sky" as="SkyButton" />
<import src="sky-button.sky" />
<import src="sky-element.sky" />
<sky-element
name="sky-checkbox"
attributes="checked:boolean"
on-click="handleClick">
<sky-element attributes="checked:boolean, highlight:boolean">
<template>
<style>
:host {
......@@ -47,27 +45,47 @@
border-color: #0f9d58;
}
</style>
<div id="container" class="{{ containerStyle }}">
<div id="check" class="{{ checkStyle }}" />
<div id="container">
<div id="check" />
</div>
</template>
<script>
module.exports = class extends SkyButton {
created() {
super.created();
import "dart:sky";
this.containerStyle = '';
this.checkStyle = '';
@Tagname('sky-checkbox')
class SkyCheckbox extends SkyButton {
Element _container;
Element _check;
SkyCheckbox() {
addEventListener('click', _handleClick);
}
static String _checkClassForValue(bool value) => value ? 'checked' : '';
static String _containerClassForValue(bool value) => value ? 'highlight' : '';
void shadowRootReady() {
_container = shadowRoot.getElementById('container');
_container.setAttribute('class', _containerClassForValue(highlight));
_check = shadowRoot.getElementById('check');
_check.setAttribute('class', _checkClassForValue(checked));
}
checkedChanged(oldValue, newValue) {
this.checkStyle = newValue ? 'checked' : '';
void checkedChanged(bool oldValue, bool newValue) {
if (_check != null)
_check.setAttribute('class', _checkClassForValue(newValue));
}
highlightChanged(oldValue, newValue) {
this.containerStyle = newValue ? 'highlight' : '';
void highlightChanged(bool oldValue, bool newValue) {
if (_container != null)
_container.setAttribute('class', _containerClassForValue(newValue));
}
handleClick() {
this.checked = !this.checked;
void _handleClick(_) {
checked = !checked;
}
}.register();
}
_init(script) => register(script, SkyCheckbox);
</script>
</sky-element>
......@@ -7,9 +7,61 @@
import "dart:mirrors";
import "dart:sky";
typedef dynamic _Converter(String value);
final Map<String, _Converter> _kAttributeConverters = {
'boolean': (String value) {
return value == 'true';
},
'number': (String value) {
return double.parse(value);
},
'string': (String value) {
return value == null ? '' : value;
},
};
class _Registration {
Element template;
final Element template;
final Map<String, _Converter> attributes = new Map();
_Registration(this.template);
void parseAttributeSpec(definition) {
String spec = definition.getAttribute('attributes');
if (spec == null)
return;
for (String token in spec.split(',')) {
List<String> parts = token.split(':');
if (parts.length != 2) {
window.console.error(
'Invalid attribute spec "${spec}", attributes must'
' be {name}:{type}, where type is one of boolean, number or'
' string.');
continue;
}
var name = parts[0].trim();
var type = parts[1].trim();
defineAttribute(name, type);
}
}
void defineAttribute(String name, String type) {
_Converter converter = _kAttributeConverters[type];
if (converter == null) {
window.console.error(
'Invalid attribute type "${type}", type must be one of boolean,'
' number or string.');
return;
}
attributes[name] = converter;
}
}
final Map<String, _Registration> _registery = new Map<String, _Registration>();
......@@ -33,21 +85,21 @@ abstract class SkyElement extends Element {
void shadowRootReady() {}
String get tagName => _getTagName(runtimeType);
_Registration _registration;
SkyElement() {
created();
_registration = _registery[tagName];
// Invoke attributeChanged callback when element is first created too.
// TODO(abarth): Is this necessary? We shouldn't have any attribute yet...
for (Attr attribute in getAttributes())
attributeChangedCallback(attribute.name, null, attribute.value);
}
attachedCallback() {
if (shadowRoot == null) {
var registration = _registery[tagName];
if (registration.template != null) {
if (_registration.template != null) {
ShadowRoot shadow = ensureShadowRoot();
Node content = registration.template.content;
Node content = _registration.template.content;
shadow.appendChild(document.importNode(content, deep: true));
shadowRootReady();
}
......@@ -61,6 +113,30 @@ abstract class SkyElement extends Element {
attributeChangedCallback(name, oldValue, newValue) {
attributeChanged(name, oldValue, newValue);
_Converter converter = _registration.attributes[name];
if (converter == null)
return;
Symbol callback = new Symbol('${name}Changed');
InstanceMirror mirror = reflect(this);
if (mirror.type.instanceMembers.containsKey(callback))
mirror.invoke(callback, [converter(oldValue), converter(newValue)]);
}
noSuchMethod(Invocation invocation) {
String name = MirrorSystem.getName(invocation.memberName);
if (name.endsWith('='))
name = name.substring(0, name.length - 1);
_Converter converter = _registration.attributes[name];
if (converter != null) {
if (invocation.isGetter) {
return converter(getAttribute(name));
} else if (invocation.isSetter) {
setAttribute(name, invocation.positionalArguments[0].toString());
return;
}
}
return super.noSuchMethod(invocation);
}
}
......@@ -78,6 +154,7 @@ void register(Element script, Type type) {
Element template = definition.querySelector('template');
document.registerElement(tagName, type);
_registery[tagName] = new _Registration(template);
_registery[tagName] = new _Registration(template)
..parseAttributeSpec(definition);
}
</script>
......@@ -3,9 +3,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-->
<import src="/gen/mojo/services/keyboard/public/interfaces/keyboard.mojom.sky" as="keyboard" />
<import src="shell.sky" as="shell" />
<import src="sky-element/sky-element.sky" as="SkyElement" />
<import src="sky-element.sky" />
<sky-element name="sky-input" attributes="value:string">
<template>
......@@ -24,48 +22,49 @@
overflow: hidden;
}
</style>
<div id="control" contenteditable
on-focus="handleFocus"
on-blur="handleBlur"
on-keydown="handleKeyDown">{{ value }}</div>
<div id="control" contenteditable />
</template>
<script>
var keyboard = shell.connectToService("mojo:keyboard", keyboard.Keyboard);
module.exports = class extends SkyElement {
import "dart:sky";
// TODO(abarth): Connect to the mojo:keyboard service.
@Tagname('sky-input')
class SkyInput extends SkyElement {
Element _control;
static String _textForValue(String value) => value == null ? '' : value;
shadowRootReady() {
var control = this.shadowRoot.getElementById('control');
var text = control.firstChild;
_control = shadowRoot.getElementById('control');
_control.setChild(new Text(_textForValue(getAttribute('text'))));
_control.addEventListener('focus', _handleFocus);
_control.addEventListener('blur', _handleBlur);
_control.addEventListener('keydown', _handleKeyDown);
}
var observer = new MutationObserver(function() {
// contenteditable might remove the text node, but we need to keep it
// since that's where the data binding is connected to.
if (!text.parentNode)
control.appendChild(text);
if (this.value == control.textContent)
return;
this.value = control.textContent;
this.dispatchEvent(new CustomEvent('change', {
bubbles: true,
}));
}.bind(this));
String get text => _control == null ? null : _control.textContent;
observer.observe(control, {
subtree: true,
characterData: true,
childList: true,
});
void textChanged(String oldValue, String newValue) {
if (_control != null)
_control.setChild(new Text(_textForValue(newValue)));
}
handleKeyDown(event) {
void _handleKeyDown(KeyboardEvent event) {
// TODO(abarth): You can still get newlines if the user pastes them.
if (event.key == 0xD)
event.preventDefault();
}
handleFocus(event) {
keyboard.show();
void _handleFocus(_) {
// TODO(abarth): Show the keyboard.
}
handleBlur(event) {
keyboard.hide();
void _handleBlur(_) {
// TODO(abarth): Hide the keyboard.
}
}.register();
}
_init(script) => register(script, SkyInput);
</script>
</sky-element>
<!--
// Copyright 2014 The Chromium Authors. All rights reserved.
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-->
<import src="sky-button.sky" as="SkyButton" />
<import src="sky-button.sky" />
<import src="sky-element.sky" />
<sky-element
name="sky-radio"
attributes="selected:boolean, group:string"
on-click="handleClick">
<sky-element attributes="selected:boolean, group:string, highlight:boolean">
<template>
<style>
:host {
......@@ -20,9 +18,6 @@
border: 1px solid blue;
margin: 0 5px;
}
:host([highlight=true]) box {
background-color: orange;
}
dot {
-webkit-user-select: none;
width: 10px;
......@@ -32,74 +27,95 @@
margin: 2px;
}
</style>
<template if="{{ selected }}">
<dot />
</template>
<dot />
</template>
<script>
const kControllerMap = new WeakMap();
class RadioGroupController {
static forRadio(radio) {
var scope = radio.ownerScope;
var controller = kControllerMap.get(scope);
if (!controller)
kControllerMap.set(scope, new RadioGroupController());
return kControllerMap.get(scope);
}
constructor() {
this.radios = new Set();
import "dart:sky";
final Map<Node, _RadioGroupController> _controllerMap = new Map();
class _RadioGroupController {
static _RadioGroupController forRadio(radio) {
Node owner = radio.owner;
return _controllerMap.putIfAbsent(owner, () =>
new _RadioGroupController(owner));
}
addRadio(radio) {
this.radios.add(radio);
final Node _scope;
final Set<SkyRadio> _radios = new Set<SkyRadio>();
_RadioGroupController(this._scope);
void addRadio(SkyRadio radio) {
_radios.add(radio);
// If this new radio is default-selected, take selection from the group.
if (radio.selected)
this.takeSelectionFromGroup(radio);
takeSelectionFromGroup(radio);
}
removeRadio(radio) {
this.radios.remove(radio);
void removeRadio(SkyRadio radio) {
_radios.remove(radio);
if (_radios.isEmpty)
_controllerMap.remove(_scope);
}
takeSelectionFromGroup(selectedRadio) {
// Emtpy/null/undefined group means an isolated radio.
if (!selectedRadio.group)
void takeSelectionFromGroup(SkyRadio selectedRadio) {
String group = selectedRadio.group;
if (group == null)
return;
this.radios.forEach(function(radio) {
if (selectedRadio === radio)
_radios.forEach((SkyRadio radio) {
if (selectedRadio == radio)
return;
if (radio.group != selectedRadio.group)
if (radio.group != group)
return;
radio.selected = false;
});
}
};
}
module.exports = class extends SkyButton {
created() {
super.created();
@Tagname('sky-radio')
class SkyRadio extends SkyButton {
_RadioGroupController _controller;
Element _dot;
this.controller = null;
SkyRadio() {
addEventListener('click', _handleClick);
}
attached() {
void shadowRootReady() {
_dot = shadowRoot.querySelector('dot');
_dot.style['display'] = selected ? 'block' : 'none';
}
void attached() {
super.attached();
this.controller = RadioGroupController.forRadio(this);
this.controller.addRadio(this);
_controller = _RadioGroupController.forRadio(this);
_controller.addRadio(this);
}
detached() {
void detached() {
super.detached();
this.controller.removeRadio(this);
this.controller = null;
_controller.removeRadio(this);
_controller = null;
}
selectedChanged(oldValue, newValue) {
if (newValue && this.controller)
this.controller.takeSelectionFromGroup(this);
void selectedChanged(bool oldValue, bool newValue) {
if (_dot != null)
_dot.style['display'] = newValue ? 'block' : 'none';
if (newValue && _controller != null)
_controller.takeSelectionFromGroup(this);
}
groupChanged(oldValue, newValue) {
if (this.selected && this.controller)
this.controller.takeSelectionFromGroup(this);
void groupChanged(String oldValue, String newValue) {
if (selected && _controller != null)
_controller.takeSelectionFromGroup(this);
}
handleClick() {
_handleClick(_) {
this.selected = true;
}
}.register();
}
_init(script) => register(script, SkyRadio);
</script>
</sky-element>
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册