├── example ├── index.dart ├── autocomplete │ ├── index.dart │ └── index.html ├── tooltip │ └── index.html ├── secret │ └── index.html ├── loading │ └── index.html ├── overlay │ ├── index.dart │ └── index.html ├── hover-tooltip │ └── index.html ├── popover │ └── index.html ├── button │ └── index.html ├── textarea │ └── index.html └── index.html ├── .gitignore ├── lib ├── components │ ├── tooltip.dart │ ├── tooltip │ │ ├── tooltip.dart │ │ ├── tooltip.html │ │ └── tooltip.css │ ├── loading.dart │ ├── secret.html │ ├── safe-html │ │ ├── safe-html.html │ │ └── safe-html.dart │ ├── button │ │ ├── button.dart │ │ ├── button.html │ │ └── button.css │ ├── loading │ │ ├── loading.html │ │ ├── loading.dart │ │ └── loading.css │ ├── overlay │ │ ├── overlay.html │ │ ├── overlay.css │ │ └── overlay.dart │ ├── textarea │ │ ├── textarea.html │ │ ├── textarea.css │ │ └── textarea.dart │ ├── hover-tooltip │ │ ├── hover_tooltip.html │ │ ├── hover_tooltip.css │ │ └── hover_tooltip.dart │ ├── button.dart │ ├── popover │ │ ├── popover.css │ │ ├── popover.html │ │ └── popover.dart │ ├── hover_tooltip.html │ ├── secret │ │ ├── secret.html │ │ ├── secret.dart │ │ └── secret.css │ ├── loading.html │ ├── textarea.html │ ├── hover_tooltip.dart │ ├── overlay.html │ ├── autocomplete │ │ ├── autocomplete.css │ │ ├── autocomplete.html │ │ └── autocomplete.dart │ ├── autocomplete.html │ ├── popover.html │ ├── tooltip.html │ ├── secret.dart │ ├── popover.dart │ ├── overlay.dart │ ├── textarea.dart │ ├── autocomplete.dart │ └── button.html └── utils │ └── html_helpers.dart ├── CHANGELOG.md ├── LICENSE ├── pubspec.yaml └── README.md /example/index.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | import 'package:web_ui/web_ui.dart'; 3 | 4 | void main() { 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | packages 3 | web/out 4 | example/out 5 | test/out 6 | pubspec.lock 7 | .buildlog 8 | -------------------------------------------------------------------------------- /lib/components/tooltip.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | import 'package:web_ui/web_ui.dart'; 3 | 4 | class TooltipComponent extends WebComponent {} 5 | -------------------------------------------------------------------------------- /lib/components/tooltip/tooltip.dart: -------------------------------------------------------------------------------- 1 | import 'package:polymer/polymer.dart'; 2 | 3 | @CustomTag('b-tooltip') 4 | class BeeTooltip extends PolymerElement { 5 | BeeTooltip.created() : super.created() {} 6 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.0.2 2 | 3 | Updated web_ui dependency 4 | 5 | ## v0.0.1 6 | 7 | Initial release of Bee. The following components have been added 8 | 9 | * Button 10 | * Secret 11 | * Loading 12 | * Popover 13 | * Overlay 14 | -------------------------------------------------------------------------------- /lib/components/loading.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | import 'package:web_ui/web_ui.dart'; 3 | 4 | class LoadingComponent extends WebComponent { 5 | String color = "#505050"; 6 | 7 | void inserted() { 8 | getShadowRoot('b-loading').query('.q-b-loading').style.color = this.color; 9 | } 10 | } -------------------------------------------------------------------------------- /lib/components/secret.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lib/components/safe-html/safe-html.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /lib/components/button/button.dart: -------------------------------------------------------------------------------- 1 | import 'package:polymer/polymer.dart'; 2 | import 'dart:html'; 3 | 4 | @CustomTag('b-button') 5 | class BeeButton extends ButtonElement with Polymer, Observable { 6 | 7 | @published String size = "medium"; 8 | @published String look = "default"; 9 | 10 | 11 | BeeButton.created() : super.created() { 12 | polymerCreated(); 13 | } 14 | 15 | 16 | } -------------------------------------------------------------------------------- /lib/components/button/button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /example/autocomplete/index.dart: -------------------------------------------------------------------------------- 1 | import 'package:polymer/polymer.dart'; 2 | import 'dart:html'; 3 | 4 | void main() { 5 | // manually initialize Polymer 6 | initPolymer().run(() {}); 7 | 8 | Polymer.onReady.then((e) { 9 | querySelector('b-autocomplete').onSelect.listen((CustomEvent event) { 10 | querySelector('.selected-item').innerHtml = 'Selected: ' + event.detail.toString(); 11 | }); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /lib/components/loading/loading.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /lib/components/tooltip/tooltip.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | -------------------------------------------------------------------------------- /lib/utils/html_helpers.dart: -------------------------------------------------------------------------------- 1 | library htmlHelpers; 2 | 3 | import 'dart:html'; 4 | 5 | bool insideNodeWhere(Element element, bool f(Element element)) { 6 | if (element.parent != null) { 7 | if (f(element.parent)) { 8 | return true; 9 | } else { 10 | return insideNodeWhere(element.parent, f); 11 | } 12 | } else { 13 | return false; 14 | } 15 | } 16 | 17 | bool insideOrIsNodeWhere(Element element, bool f(Element element)) { 18 | if (f(element)) { 19 | return true; 20 | } else { 21 | return insideNodeWhere(element, f); 22 | } 23 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 Allan Berger, Nikolaus Graf, Thomas Schranz 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /lib/components/overlay/overlay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/components/textarea/textarea.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /lib/components/hover-tooltip/hover_tooltip.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /example/tooltip/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | b-tooltip example 8 | 9 | 10 | 11 | 12 | 13 | 14 |

b-tooltip example

15 | 16 | 17 |
Tooltip Content
18 |
19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /lib/components/loading/loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:polymer/polymer.dart'; 2 | import 'dart:html'; 3 | 4 | @CustomTag('b-loading') 5 | class BeeLoading extends PolymerElement { 6 | 7 | @published String color; 8 | String _defaultColor = "#505050"; 9 | 10 | BeeLoading.created() : super.created() {} 11 | 12 | void attached() { 13 | // Setting the color to the default color in case no color 14 | // has been provided through the color attribute. 15 | if (color == null) { 16 | updateColor(_defaultColor); 17 | } 18 | } 19 | 20 | void colorChanged(String oldValue, String newValue) { 21 | updateColor(newValue); 22 | } 23 | 24 | void updateColor(String value) { 25 | shadowRoot.querySelector('.q-b-loading').style.color = value; 26 | } 27 | } -------------------------------------------------------------------------------- /example/secret/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | b-secret example 8 | 9 | 10 | 11 | 12 | 13 | 14 |

b-secret example

15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/loading/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | b-loading example 8 | 9 | 10 | 11 | 12 | 13 | 14 |

b-loading example

15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /lib/components/hover-tooltip/hover_tooltip.css: -------------------------------------------------------------------------------- 1 | .b-hover-tooltip-tooltip-wrapper { 2 | position: absolute; 3 | left: 50%; 4 | /*opacity: 0;*/ 5 | -webkit-transition:.2s ease all; 6 | -moz-transition:.2s ease all; 7 | transition:.2s ease all; 8 | opacity:0; 9 | display:none; 10 | } 11 | .b-hover-tooltip-tooltip { 12 | margin: 0 auto; 13 | /* 14 | bee/bootstrap tooltip z-index 15 | */ 16 | z-index: 1030; 17 | position: relative; 18 | left: -50%; 19 | } 20 | .b-hover-tooltip-show { 21 | display:block; 22 | } 23 | .b-hover-tooltip-animate { 24 | opacity:1; 25 | } 26 | .q-hover-tooltip-hover-area-wrapper { 27 | position: relative; 28 | /* 29 | same as overlay backdrop but lower than popover to hide 30 | it under the due date popover 31 | */ 32 | z-index: 998; 33 | } -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: bee 2 | version: 0.0.3 3 | authors: 4 | - Allan Berger 5 | - Nik Graf 6 | - Patrick Wied 7 | - Thomas Schranz 8 | description: A Collection of lightweight Dart Polymer Web Components 9 | homepage: https://www.blossom.io/bee 10 | dependencies: 11 | escape_handler: '>=0.0.2 <0.1.0' 12 | polymer: '>=0.11.0 <0.12.0' 13 | transformers: 14 | - polymer: 15 | entry_points: 16 | - example/button/index.html 17 | - example/secret/index.html 18 | - example/loading/index.html 19 | - example/popover/index.html 20 | - example/tooltip/index.html 21 | - example/overlay/index.html 22 | - example/hover-tooltip/index.html 23 | - example/textarea/index.html 24 | - example/autocomplete/index.html 25 | -------------------------------------------------------------------------------- /lib/components/textarea/textarea.css: -------------------------------------------------------------------------------- 1 | .b-textarea-textarea { 2 | resize: none; 3 | width: 100%; 4 | height: 0px; 5 | margin: 0; 6 | padding: 0; 7 | color: #505050; 8 | border: 0px #fff solid; 9 | background: none; 10 | -moz-box-sizing: border-box; 11 | -webkit-box-sizing: border-box; 12 | box-sizing: border-box; 13 | /* with display inline the b-textarea would 14 | use up more space than needed at the bottom */ 15 | display: block; 16 | } 17 | 18 | .b-textarea-shadow { 19 | position: absolute; 20 | /* moving shadow out of the visible screen so it doesn't 21 | displace other content after the text area */ 22 | top: -10000px; 23 | white-space: pre-wrap; 24 | visibility: hidden; 25 | word-wrap: break-word; 26 | -moz-box-sizing: border-box; 27 | -webkit-box-sizing: border-box; 28 | box-sizing: border-box; 29 | } -------------------------------------------------------------------------------- /example/overlay/index.dart: -------------------------------------------------------------------------------- 1 | import 'package:polymer/polymer.dart'; 2 | import 'dart:html'; 3 | 4 | void main() { 5 | // manually initialize Polymer 6 | initPolymer().run(() {}); 7 | 8 | Polymer.onReady.then((e) { 9 | querySelector('#launch-example1').onClick.listen((MouseEvent event) { 10 | querySelector('.q-example1-overlay').show(); 11 | }); 12 | 13 | querySelector('#launch-example2').onClick.listen((MouseEvent event) { 14 | querySelector('.q-example2-overlay').show(); 15 | }); 16 | 17 | querySelector('.q-example3-overlay').onHide.listen((CustomEvent event) { 18 | window.alert('Overlay closed.'); 19 | }); 20 | querySelector('.q-example3-overlay').onShow.listen((CustomEvent event) { 21 | window.alert('Overlay opened.'); 22 | }); 23 | querySelector('#launch-example3').onClick.listen((MouseEvent event) { 24 | querySelector('.q-example3-overlay').show(); 25 | }); 26 | }); 27 | } -------------------------------------------------------------------------------- /example/hover-tooltip/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | b-hover-tooltip example 8 | 9 | 10 | 11 | 12 | 13 | 14 |

b-hover-tooltip example

15 | 16 | 17 |
My Tooltip Hover Area
18 | 19 |
20 |
My Tooltip Content
21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /lib/components/loading/loading.css: -------------------------------------------------------------------------------- 1 | :host { 2 | border: none; 3 | background:none; 4 | } 5 | 6 | @-webkit-keyframes b-loading-dot { 7 | 0% { opacity: 1; } 8 | 50% { opacity: 0; } 9 | 100% { opacity: 1; } 10 | } 11 | 12 | @keyframes b-loading-dot { 13 | 0% { opacity: 1; } 14 | 50% { opacity: 0; } 15 | 100% { opacity: 1; } 16 | } 17 | 18 | .b-loading-one { 19 | opacity: 1; 20 | -webkit-animation: b-loading-dot 1.3s infinite; 21 | -webkit-animation-delay: 0.0s; 22 | animation: b-loading-dot 1.3s infinite; 23 | animation-delay: 0.0s; 24 | } 25 | 26 | .b-loading-two { 27 | opacity: 1; 28 | -webkit-animation: b-loading-dot 1.3s infinite; 29 | -webkit-animation-delay: 0.2s; 30 | animation: b-loading-dot 1.3s infinite; 31 | animation-delay: 0.2s; 32 | } 33 | 34 | .b-loading-three { 35 | opacity: 1; 36 | -webkit-animation: b-loading-dot 1.3s infinite; 37 | -webkit-animation-delay: 0.3s; 38 | animation: b-loading-dot 1.3s infinite; 39 | animation-delay: 0.3s; 40 | } -------------------------------------------------------------------------------- /lib/components/button.dart: -------------------------------------------------------------------------------- 1 | import 'package:web_ui/web_ui.dart'; 2 | import 'dart:html'; 3 | 4 | class BeeButtonComponent extends WebComponent { 5 | /* 6 | * look = default, primary, link 7 | * size = default, small, medium 8 | * type = button, submit 9 | */ 10 | String type = 'button'; 11 | String look = 'default'; 12 | String size = 'default'; 13 | bool disabled = false; 14 | 15 | String paddingLeft; 16 | String paddingRight; 17 | String paddingTop; 18 | String paddingBottom; 19 | 20 | void inserted() { 21 | this._setStyles(); 22 | } 23 | 24 | void _setStyles() { 25 | Element button = getShadowRoot('b-button').query('.q-b-button'); 26 | if (paddingLeft != null) { button.style.paddingLeft = "${paddingLeft}px"; } 27 | if (paddingRight != null) { button.style.paddingRight = "${paddingRight}px"; } 28 | if (paddingTop != null) { button.style.paddingTop = "${paddingTop}px"; } 29 | if (paddingBottom != null) { button.style.paddingBottom = "${paddingBottom}px"; } 30 | } 31 | 32 | void focus() { 33 | getShadowRoot('b-button').query('.q-b-button').focus(); 34 | } 35 | } -------------------------------------------------------------------------------- /lib/components/safe-html/safe-html.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import "dart:html"; 3 | import "package:polymer/polymer.dart"; 4 | 5 | /* 6 | * based on Guenter Zoechbauer's feedback on stackoverflow. 7 | * Thank you! ( http://stackoverflow.com/a/20869025/837709 ) 8 | * 9 | */ 10 | 11 | 12 | @CustomTag("b-safe-html") 13 | class SafeHtml extends PolymerElement { 14 | 15 | @published String model; 16 | 17 | NodeValidator nodeValidator; 18 | bool isInitialized = false; 19 | 20 | SafeHtml.created() : super.created() { 21 | nodeValidator = new NodeValidatorBuilder() 22 | ..allowHtml5(); 23 | } 24 | 25 | void modelChanded(old) { 26 | if(isInitialized) { 27 | _addFragment(); 28 | } 29 | } 30 | 31 | void _addFragment() { 32 | var fragment = new DocumentFragment.html(model, validator: nodeValidator); 33 | $["container"].nodes 34 | ..clear() 35 | ..add(fragment); 36 | 37 | } 38 | 39 | @override 40 | void attached() { 41 | super.attached(); 42 | Timer.run(() { 43 | _addFragment(); 44 | isInitialized = true; 45 | }); 46 | } 47 | } -------------------------------------------------------------------------------- /lib/components/popover/popover.css: -------------------------------------------------------------------------------- 1 | :host { 2 | border: none; 3 | background:none; 4 | } 5 | 6 | .b-popover-dialog { 7 | position: absolute; 8 | top: 8px; 9 | z-index: 999; 10 | border: 1px solid #cccccc; 11 | -moz-box-shadow: -1px 1px 1px rgba(0,0,0,.2); 12 | -webkit-box-shadow: 0 2px 4px rgba(0,0,0,.2); 13 | box-shadow: 0 2px 4px rgba(0,0,0,.2); 14 | background: #ffffff; 15 | } 16 | 17 | .b-popover-arrow, 18 | .b-popover-arrow:after { 19 | position: absolute; 20 | display: block; 21 | width: 0; 22 | height: 0; 23 | border-color: transparent; 24 | border-style: solid; 25 | } 26 | .b-popover-arrow { 27 | border-width: 8px; 28 | border-top-width: 0; 29 | border-bottom-color: #999999; 30 | border-bottom-color: rgba(0, 0, 0, 0.25); 31 | top: -8px; 32 | } 33 | .b-popover-arrow:after { 34 | border-width: 7px; 35 | content: " "; 36 | top: 1px; 37 | margin-left: -7px; 38 | border-top-width: 0; 39 | border-bottom-color: #ffffff; 40 | } 41 | 42 | .b-popover-body { 43 | padding: 0; 44 | } 45 | .x-popover-launch-area { 46 | display: inline; 47 | } 48 | .x-popover-launch-area:hover { 49 | cursor: pointer; 50 | } -------------------------------------------------------------------------------- /example/popover/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | b-popover example 8 | 9 | 10 | 11 | 20 | 21 | 22 | 23 |

b-popover example

24 | 25 |
Click me
26 |
Content
27 |
28 | 29 |



30 | 31 | 32 | 33 |
Click me 2
34 |
Content 2
35 |
36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /example/button/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | b-button example 8 | 9 | 10 | 11 | 12 | 13 |

b-button example

14 | 15 | 16 |
17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/textarea/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | b-textarea example 8 | 9 | 10 | 11 | 12 | 13 | 14 |

b-textarea example

15 | 16 |

Simple Example:

17 | 18 | 19 |

Placeholder Example:

20 | 21 | 22 |

Styled Example:

23 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /lib/components/overlay/overlay.css: -------------------------------------------------------------------------------- 1 | :host { 2 | border: none; 3 | background:none; 4 | } 5 | 6 | /* overflow-y scroll is important for preventing to jump to top of the page when opening an overlay */ 7 | body { 8 | overflow: visible; 9 | overflow-y: scroll; 10 | } 11 | .overlay-backdrop-active body { 12 | overflow: hidden; 13 | } 14 | 15 | .b-overlay-backdrop { 16 | background-color: rgba(252, 252, 252, 0.7); 17 | -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#B3FCFCFC, endColorstr=#B3FCFCFC)"; 18 | position: fixed; 19 | top: 0; 20 | left: 0; 21 | right: 0; 22 | bottom: 0; 23 | z-index: 998; 24 | overflow-x: auto; 25 | overflow-y: scroll; 26 | display: none; 27 | } 28 | 29 | .b-overlay-backdrop-close { 30 | color: #aaa; 31 | font-size: 30px; 32 | width: 30px; 33 | height: 30px; 34 | line-height: 30px; 35 | text-align: center; 36 | border: 0; 37 | position: fixed; 38 | top: 10px; 39 | right: 10px; 40 | cursor: pointer; 41 | } 42 | 43 | .b-overlay-body { 44 | border: 1px solid #cccccc; 45 | z-index: 999; 46 | -moz-box-shadow: -1px 1px 1px rgba(0,0,0,.2); 47 | -webkit-box-shadow: 0 2px 4px rgba(0,0,0,.2); 48 | box-shadow: 0 2px 4px rgba(0,0,0,.2); 49 | background: #ffffff; 50 | position: static; 51 | margin: 60px auto; 52 | padding: 60px; 53 | } -------------------------------------------------------------------------------- /lib/components/hover-tooltip/hover_tooltip.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | import 'dart:async'; 3 | 4 | import 'package:polymer/polymer.dart'; 5 | 6 | @CustomTag('b-hover-tooltip') 7 | class BeeHoverTooltip extends PolymerElement { 8 | StreamSubscription _mouseEnterStream; 9 | StreamSubscription _mouseLeaveStream; 10 | Element _tooltip; 11 | Element _hoverArea; 12 | 13 | 14 | BeeHoverTooltip.created() : super.created() {} 15 | 16 | void attached() { 17 | _tooltip = shadowRoot.querySelector('.b-hover-tooltip-tooltip-wrapper'); 18 | _hoverArea = shadowRoot.querySelector('.q-hover-tooltip-hover-area-wrapper'); 19 | 20 | _mouseEnterStream = _hoverArea.onMouseEnter.listen(null)..onData(showTooltip); 21 | _mouseLeaveStream = _hoverArea.onMouseLeave.listen(null)..onData(hideTooltip); 22 | } 23 | 24 | void showTooltip(MouseEvent event) { 25 | _tooltip.classes.add('b-hover-tooltip-show'); 26 | new Timer(const Duration(milliseconds: 10), _toggleAnimation); 27 | } 28 | 29 | void _toggleAnimation() { 30 | _tooltip.classes.toggle('b-hover-tooltip-animate'); 31 | } 32 | 33 | void hideTooltip(MouseEvent event) { 34 | _toggleAnimation(); 35 | new Timer(const Duration(milliseconds: 200), () { 36 | if (_tooltip.classes.contains('b-hover-tooltip-animate')) { 37 | // abort if there's an animation in progress after 200ms timeout 38 | return; 39 | } 40 | _tooltip.classes.remove('b-hover-tooltip-show'); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/components/popover/popover.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 37 | 38 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Bee Examples 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 |

Click to Button to the overlay containing our form.

20 | 21 | Open the Form 22 | 23 | 24 | 25 |

Example Overlay

26 |

This Overlay contains a 'b-secret' element and a 'b-button' of type submit.

27 | 28 |
29 | 30 | 31 | Save 32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /example/autocomplete/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | b-autocomplete example 8 | 9 | 10 | 11 | 12 | 13 | 14 |

b-autocomplete example

15 | 16 | 20 |
21 | 22 |
23 |
24 |
25 | T1 26 |
27 |
28 | T2 29 |
30 |
31 | T3 32 |
33 |
34 | T4 35 |
36 |
37 |
38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /lib/components/hover_tooltip.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /lib/components/secret/secret.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 36 | 37 | -------------------------------------------------------------------------------- /lib/components/loading.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /lib/components/textarea.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /lib/components/hover_tooltip.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | import 'package:web_ui/web_ui.dart'; 3 | import 'package:css_animation/css_animation.dart'; 4 | 5 | class HoverTooltipComponent extends WebComponent { 6 | 7 | /* 8 | * Returns true if the relatedTarget is or is inside the given Element. 9 | * 10 | * This helps to simulate mouseenter and mouseleave event which is not 11 | * supported by Safari. 12 | * 13 | * This has been inspired by jQuery's way to emulate mouseenter & mouseleave 14 | * event. 15 | */ 16 | static bool previousElementOfEventIsOrIsInsideElement(Element element, MouseEvent event) { 17 | if (event.relatedTarget == null) { 18 | return false; 19 | } 20 | return (event.relatedTarget != element && 21 | !element.contains(event.relatedTarget)); 22 | } 23 | 24 | void showTooltip(MouseEvent event) { 25 | Element hoverAreaWrapper = getShadowRoot('b-hover-tooltip').query('.q-hover-tooltip-hover-area-wrapper'); 26 | if (previousElementOfEventIsOrIsInsideElement(hoverAreaWrapper, event)) { 27 | Element tooltip = getShadowRoot('b-hover-tooltip').query('.q-hover-tooltip-tooltip'); 28 | tooltip.style.display = 'block'; 29 | var animation = new CssAnimation('opacity', 0, 1); 30 | animation.apply(tooltip, duration: 200); 31 | } 32 | } 33 | 34 | void hideTooltip(MouseEvent event) { 35 | Element hoverAreaWrapper = getShadowRoot('b-hover-tooltip').query('.q-hover-tooltip-hover-area-wrapper'); 36 | if (previousElementOfEventIsOrIsInsideElement(hoverAreaWrapper, event)) { 37 | Element tooltip = getShadowRoot('b-hover-tooltip').query('.q-hover-tooltip-tooltip'); 38 | var animation = new CssAnimation('opacity', 1, 0); 39 | animation.apply(tooltip, duration: 100, onComplete: () { 40 | tooltip.style.display = 'none'; 41 | }); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/components/overlay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /lib/components/autocomplete/autocomplete.css: -------------------------------------------------------------------------------- 1 | .b-autocomplete-data-source-wrapper { 2 | display: none; 3 | } 4 | .b-autocomplete-prefix-area-wrapper { 5 | float: left; 6 | display: block; 7 | } 8 | .b-autocomplete-main-area { 9 | display: block; 10 | float: left; 11 | position: relative; 12 | } 13 | .b-autocomplete-active { 14 | background: #ccc; 15 | cursor: pointer; 16 | } 17 | .b-autocomplete-results { 18 | display: block; 19 | overflow-y: scroll; 20 | background: #fff; 21 | position: absolute; 22 | left: 0; 23 | right: 0; 24 | top: 24px; 25 | z-index: 10000; 26 | border: 1px solid #b2b2b2; 27 | margin-top: 0px !important; 28 | border-radius: 3px; 29 | border-top-right-radius: 0; 30 | border-top-left-radius: 0; 31 | -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); 32 | -moz-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); 33 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.148438); 34 | } 35 | .b-autocomplete-results-li { 36 | background-color: #fff; 37 | margin: 0; 38 | width: auto; 39 | border-bottom: 1px solid #eee; 40 | overflow: hidden; 41 | cursor: pointer; 42 | list-style: none; 43 | white-space: nowrap; 44 | } 45 | .b-autocomplete-results-li:last-child { 46 | border-bottom: none; 47 | } 48 | .b-autocomplete-results-li.b-autocomplete-active { 49 | background-color: #f2f2f2; 50 | } 51 | .no-touch .b-autocomplete-results-li:hover { 52 | background-color: #f2f2f2; 53 | } 54 | .b-autocomplete-activation-area { 55 | display: block; 56 | overflow: hidden; 57 | margin-bottom: 2px; 58 | } 59 | .b-autocomplete-form { 60 | float: left !important; 61 | display: none; 62 | } 63 | .b-autocomplete-form-input { 64 | /* need to overwrite width from bootstrap input width */ 65 | width: 100% !important; 66 | height: 20px; 67 | font-size: 14px; 68 | line-height: normal !important; 69 | color: #666; 70 | margin: 0 !important; 71 | margin-top: 2px !important; 72 | padding: 0 !important; 73 | -webkit-border-radius: 0px !important; 74 | mox-border-radius: 0px !important; 75 | border-radius: 0px !important; 76 | border-top: none !important; 77 | border-right: none !important; 78 | border-left: none !important; 79 | border-bottom: 1px dotted #333 !important; 80 | background: transparent; 81 | box-shadow: none; 82 | letter-spacing: 0; 83 | resize: none; 84 | word-wrap: break-word; 85 | } 86 | .b-autocomplete-activation-area-add-text { 87 | display: block; 88 | float: left; 89 | color: #505050; 90 | margin-top: 3px; 91 | } -------------------------------------------------------------------------------- /lib/components/tooltip/tooltip.css: -------------------------------------------------------------------------------- 1 | .b-tooltip-wrapper { 2 | position: relative; 3 | z-index: 1030; 4 | display: block; 5 | font-size: 12px; 6 | line-height: 1.4; 7 | opacity: 1; 8 | filter: alpha(opacity=0); 9 | visibility: visible; 10 | } 11 | 12 | .b-tooltip-wrapper.b-tooltip-in { 13 | opacity: 0.8; 14 | filter: alpha(opacity=80); 15 | } 16 | 17 | .b-tooltip-wrapper.b-tooltip-top { 18 | padding: 5px 0; 19 | } 20 | 21 | .b-tooltip-wrapper.b-tooltip-right { 22 | padding: 0 5px; 23 | } 24 | 25 | .b-tooltip-wrapper.b-tooltip-bottom { 26 | padding: 5px 0; 27 | } 28 | 29 | .b-tooltip-wrapper.b-tooltip-left { 30 | padding: 0 5px; 31 | } 32 | 33 | .b-tooltip-inner { 34 | padding: 3px 8px; 35 | color: #ffffff; 36 | text-align: center; 37 | text-decoration: none; 38 | background-color: #000000; 39 | -webkit-border-radius: 4px; 40 | -moz-border-radius: 4px; 41 | border-radius: 4px; 42 | } 43 | 44 | .b-tooltip-arrow { 45 | position: absolute; 46 | width: 0; 47 | height: 0; 48 | border-color: transparent; 49 | border-style: solid; 50 | } 51 | 52 | .b-tooltip-wrapper.b-tooltip-top .b-tooltip-arrow { 53 | bottom: 0; 54 | left: 50%; 55 | margin-left: -5px; 56 | border-top-color: #000000; 57 | border-width: 5px 5px 0; 58 | } 59 | 60 | .b-tooltip-wrapper.b-tooltip-right .b-tooltip-arrow { 61 | top: 50%; 62 | left: 0; 63 | margin-top: -5px; 64 | border-right-color: #000000; 65 | border-width: 5px 5px 5px 0; 66 | } 67 | 68 | .b-tooltip-wrapper.b-tooltip-left .b-tooltip-arrow { 69 | top: 50%; 70 | right: 0; 71 | margin-top: -5px; 72 | border-left-color: #000000; 73 | border-width: 5px 0 5px 5px; 74 | } 75 | 76 | .b-tooltip-wrapper.b-tooltip-bottom .b-tooltip-arrow { 77 | top: 0; 78 | left: 50%; 79 | margin-left: -5px; 80 | border-bottom-color: #000000; 81 | border-width: 0 5px 5px; 82 | } -------------------------------------------------------------------------------- /lib/components/autocomplete/autocomplete.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 58 | 59 | -------------------------------------------------------------------------------- /lib/components/autocomplete.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /lib/components/popover.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /lib/components/tooltip.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /lib/components/secret.dart: -------------------------------------------------------------------------------- 1 | import 'package:web_ui/web_ui.dart'; 2 | import 'dart:async'; 3 | import 'dart:html'; 4 | 5 | @observable 6 | class SecretComponent extends WebComponent { 7 | static const EventStreamProvider inputEvent = const EventStreamProvider('input'); 8 | static const EventStreamProvider blurEvent = const EventStreamProvider('blur'); 9 | static const EventStreamProvider focusEvent = const EventStreamProvider('focus'); 10 | String placeholder = ''; 11 | String name = 'secret'; 12 | bool required = false; 13 | String value = ''; 14 | bool _hasFocus = false; 15 | int _selectionStart = 0; 16 | int _selectionEnd = 0; 17 | bool _passwordActive = true; 18 | DivElement _passwordWrapper; 19 | DivElement _textWrapper; 20 | 21 | void inserted() { 22 | this._passwordWrapper = getShadowRoot('b-secret').query('.q-b-secret-password-wrapper'); 23 | this._textWrapper = getShadowRoot('b-secret').query('.q-b-secret-text-wrapper'); 24 | this._updateState(); 25 | } 26 | 27 | void focus() { 28 | this._activeInput.focus(); 29 | } 30 | 31 | void hideSecret() { 32 | this._passwordActive = true; 33 | this._updateState(); 34 | } 35 | 36 | void _toggleShowSecret (event) { 37 | event.preventDefault(); 38 | // retrieve the current selection before the field is toggled 39 | // after the toggle the same selection will be applied again 40 | this._retrieveSelection(); 41 | this._passwordActive = !this._passwordActive; 42 | this._updateState(); 43 | this._activeInput.focus(); 44 | this._updateSelection(); 45 | } 46 | 47 | _updateState() { 48 | if (this._passwordActive) { 49 | this._passwordWrapper.style.display = 'block'; 50 | this._textWrapper.style.display = 'none'; 51 | } else { 52 | this._passwordWrapper.style.display = 'none'; 53 | this._textWrapper.style.display = 'block'; 54 | } 55 | } 56 | 57 | void _input() { 58 | this.dispatchEvent(new CustomEvent("input")); 59 | } 60 | 61 | void _blur(event) { 62 | // TODO find a better way 63 | // figure out if the user toggled the password or blurred the password field 64 | // by checking which element is focused 65 | 66 | // in Chrome Version 26.0.1410.65 the focus event gets fired around 12x milli seconds after the blur event 67 | // in Firefox the focus event is fired before the blur event 68 | // in IE9 the focus event is fired around 9x milliseconds after the blur event 69 | // picked 200 milliseconds for our timer to be on the safe side 70 | new Timer(new Duration(milliseconds:200), () { 71 | Element textField = getShadowRoot('b-secret').query('.q-text-field'); 72 | Element passwordField = getShadowRoot('b-secret').query('.q-password-field'); 73 | if (document.activeElement != textField && document.activeElement != passwordField) { 74 | this.dispatchEvent(new CustomEvent("blur")); 75 | this._hasFocus = false; 76 | } 77 | }); 78 | } 79 | 80 | void _focus(event) { 81 | if (!this._hasFocus) { 82 | this.dispatchEvent(new CustomEvent("focus")); 83 | } 84 | this._hasFocus = true; 85 | } 86 | 87 | String _retrieveSelection() { 88 | this._selectionStart = this._activeInput.selectionStart; 89 | this._selectionEnd = this._activeInput.selectionEnd; 90 | return ''; 91 | } 92 | 93 | String _updateSelection() { 94 | this._activeInput.selectionStart = this._selectionStart; 95 | this._activeInput.selectionEnd = this._selectionEnd; 96 | return ''; 97 | } 98 | 99 | InputElement get _activeInput { 100 | if (_passwordActive) { 101 | return getShadowRoot('b-secret').query('.q-password-field'); 102 | } else { 103 | return getShadowRoot('b-secret').query('.q-text-field'); 104 | } 105 | } 106 | 107 | get _focusClass { 108 | if (_hasFocus) { 109 | return 'secret-has-focus'; 110 | } else { 111 | return ''; 112 | } 113 | } 114 | 115 | Stream get onInput => inputEvent.forTarget(this); 116 | Stream get onBlur => blurEvent.forTarget(this); 117 | Stream get onFocus => focusEvent.forTarget(this); 118 | } -------------------------------------------------------------------------------- /lib/components/secret/secret.dart: -------------------------------------------------------------------------------- 1 | import 'package:polymer/polymer.dart'; 2 | import 'dart:async'; 3 | import 'dart:html'; 4 | import 'dart:js' as js; 5 | 6 | @CustomTag('b-secret') 7 | class BeeSecret extends PolymerElement { 8 | 9 | @published String placeholder = 'Enter password here'; 10 | @published String value; 11 | @published String name = 'secret'; 12 | @published bool required = false; 13 | 14 | @observable String focusClass = ''; 15 | @observable bool hasFocus = false; 16 | DivElement _passwordWrapper; 17 | DivElement _textWrapper; 18 | bool _passwordActive = true; 19 | int _selectionStart = 0; 20 | int _selectionEnd = 0; 21 | 22 | BeeSecret.created() : super.created() { 23 | } 24 | 25 | void attached() { 26 | _passwordWrapper = shadowRoot.querySelector('.q-b-secret-password-wrapper'); 27 | _textWrapper = shadowRoot.querySelector('.q-b-secret-text-wrapper'); 28 | _updateState(); 29 | } 30 | 31 | /** 32 | * Sets focus on the b-secret element. 33 | */ 34 | void focus() { 35 | _activeInput.focus(); 36 | } 37 | 38 | /** 39 | * Updates the b-secret input to display the value like a password field. 40 | */ 41 | void hideSecret() { 42 | _passwordActive = true; 43 | _updateState(); 44 | } 45 | 46 | /** 47 | * Toggle between the plain text and obfuscate version of the input. 48 | * 49 | * The focus and selection/caret positions stay the same. 50 | */ 51 | void toggleShowSecret (event) { 52 | event.preventDefault(); 53 | // retrieve the current selection before the field is toggled 54 | // after the toggle the same selection will be applied again 55 | _retrieveSelection(); 56 | _passwordActive = !this._passwordActive; 57 | _updateState(); 58 | _activeInput.focus(); 59 | _updateSelection(); 60 | } 61 | 62 | void _updateState() { 63 | if (_passwordActive) { 64 | _passwordWrapper.style.display = 'block'; 65 | _textWrapper.style.display = 'none'; 66 | } else { 67 | _passwordWrapper.style.display = 'none'; 68 | _textWrapper.style.display = 'block'; 69 | } 70 | } 71 | 72 | void handleInput() { 73 | dispatchEvent(new CustomEvent("input")); 74 | } 75 | 76 | void handleBlur(event) { 77 | // TODO find a better way 78 | // Figure out if the user toggled the password or blurred the password field 79 | // by checking which element is focused. 80 | 81 | // In Chrome Version 26.0.1410.65 the focus event gets fired around 82 | // 12x milli seconds after the blur event. 83 | // In Firefox the focus event is fired before the blur event. 84 | // In IE9 the focus event is fired around 9x milliseconds after the blur 85 | // event. 86 | // We chose 200 milliseconds for our timer to be on the safe side. 87 | new Future.delayed(new Duration(milliseconds:200), () { 88 | var textField = shadowRoot.querySelector('.q-text-field'); 89 | var passwordField = shadowRoot.querySelector('.q-password-field'); 90 | 91 | var activeElement = js.context.callMethod('wrap', 92 | [document.activeElement]); 93 | 94 | // For Browsers with ShadowDOM support the shadowRoot.host matches while 95 | // for Browsers without ShadowDOM support text or password field matches. 96 | if (activeElement.hashCode != hashCode && 97 | activeElement.hashCode != textField.hashCode && 98 | activeElement.hashCode != passwordField.hashCode) { 99 | dispatchEvent(new CustomEvent("blur")); 100 | hasFocus = false; 101 | } 102 | }); 103 | } 104 | 105 | void handleFocus(event) { 106 | hasFocus = true; 107 | } 108 | 109 | String _retrieveSelection() { 110 | _selectionStart = _activeInput.selectionStart; 111 | _selectionEnd = _activeInput.selectionEnd; 112 | return ''; 113 | } 114 | 115 | String _updateSelection() { 116 | _activeInput.selectionStart = _selectionStart; 117 | _activeInput.selectionEnd = _selectionEnd; 118 | return ''; 119 | } 120 | 121 | InputElement get _activeInput { 122 | if (_passwordActive) { 123 | return shadowRoot.querySelector('.q-password-field'); 124 | } else { 125 | return shadowRoot.querySelector('.q-text-field'); 126 | } 127 | } 128 | 129 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bee 2 | 3 | Bee is a collection of lightweight interaction elements for modern web applications. It is built on top of [Dart's Web UI](http://www.dartlang.org/articles/web-ui/) package. It contains frequently used components like Buttons, Popovers, Overlays, Input Fields and more. 4 | 5 | ## Install 6 | 7 | Bee is a [Pub Package](http://pub.dartlang.org/packages/bee). To install Bee you can add it to your pubspec.yaml. 8 | 9 | ```yaml 10 | name: my-app 11 | dependencies: 12 | bee: any 13 | ``` 14 | 15 | ## Getting started 16 | 17 | To use a component you need to import it via a link tag. 18 | 19 | ```html 20 | 21 | ``` 22 | 23 | Add the custom Bee element inside the same file where you imported the component. 24 | 25 | ```html 26 | Primary 27 | ``` 28 | 29 | You might want to check out the [example](https://github.com/blossom/bee/tree/master/example). 30 | 31 | ## Components 32 | 33 | ### Button 34 | 35 | ```html 36 | 37 | ``` 38 | 39 | ```html 40 | Primary 41 | ``` 42 | 43 | ### Show Password 44 | 45 | ```html 46 | 47 | ``` 48 | 49 | ```html 50 | 51 | ``` 52 | 53 | ### Loading Indicator 54 | 55 | ```html 56 | 57 | ``` 58 | 59 | ```html 60 | 61 | ``` 62 | 63 | ### Popover 64 | 65 | ```html 66 | 67 | ``` 68 | 69 | ```html 70 | 71 | Launch Popover 72 |
This is a Popover
73 |
74 | ``` 75 | 76 | ### Overlay 77 | 78 | ```html 79 | 80 | ``` 81 | 82 | ```html 83 | Launch Overlay 84 | 85 |

Bee

86 |

Bee is a collection of lightweight interaction elements for modern web applications. It is built on top of Dart's Web UI package. It contains frequently used components like Buttons, Popovers, Overlays, Input Fields and more.

87 |
88 | ``` 89 | 90 | ### Tooltip 91 | 92 | ```html 93 | 94 | ``` 95 | 96 | ```html 97 | 98 | ``` 99 | 100 | ### Textarea (growable) 101 | 102 | ```html 103 | 104 | ``` 105 | 106 | ```html 107 | 108 | ``` 109 | 110 | ## Nexted Example 111 | 112 | A button which opens an overlay on click. The overlay contains a popover. 113 | Note: Pressing 'ESC' closes popovers as well as overlays but only closes the youngest (last shown) component. 114 | 115 | ```html 116 | Launch Overlay 117 | 118 |

Bee

119 |

Bee is a collection of lightweight interaction elements for modern web applications. It is built on top of Dart's Web UI package. It contains frequently used components like Buttons, Popovers, Overlays, Input Fields and more.

120 | 121 | Launch Popover inside Overlay 122 |
This is a Popover
123 |
124 |

Bee is a collection of lightweight interaction elements for modern web applications. It is built on top of Dart's Web UI package. It contains frequently used components like Buttons, Popovers, Overlays, Input Fields and more.

125 |
126 | ``` 127 | 128 | ## Coming Soon 129 | 130 | This is just the initial release and we'll add a bunch of additional components, examples, documentation and polish going forward :) 131 | 132 | * Convert to Polymer.dart 133 | * Tests, Tests, Tests 134 | * Component: Date Picker 135 | -------------------------------------------------------------------------------- /example/overlay/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | b-overlay example 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

b-overlay example

16 | 17 |
Launch Basic Overlay
18 |
Launch Overlay with a lot of Content
19 |
Launch Overlay with event listeners bind to Show/Hide
20 | 21 | 22 |

Bee

23 |

24 | Bee is a collection of lightweight interaction elements for modern web applications. It is built on top of Dart's Web UI package. It contains frequently used components like Buttons, Popovers, Overlays, Input Fields and more. 25 |

26 |
27 | 28 | 29 |

Hint: See how smooth you can scroll down, while everything behind the backdrop doesn't scroll.

30 | 31 |

Rubber duck debugging

32 |

33 | Rubber duck debugging, rubber ducking,[1] and the rubber duckie test[2] are informal terms used in software engineering to refer to a method of debugging code. The name is a reference to a story in the book The Pragmatic Programmer in which a programmer would carry around a rubber duck and debug his code by forcing himself to explain it, line-by-line, to the duck.[3] 34 |

35 | 36 |

37 | Many programmers have had the experience of explaining a programming problem to someone else, possibly even to someone who knows nothing about programming, and then hitting upon the solution in the process of explaining the problem. In describing what the code is supposed to do and observing what it actually does, any incongruity between these two becomes apparent.[4] By using an inanimate object, such as a rubber duck, the programmer can try to accomplish this without having to involve another person. 38 |

39 | 40 |

41 | This concept is also known as "Talk to the Bear", dating from Kernighan's 1999 book The Practice of Programming.[5] 42 |

43 | 44 |

Single Source of Truth

45 |

46 | In Information Systems design and theory Single Source Of Truth (SSOT) refers to the practice of structuring information models and associated schemata such that every data element is stored exactly once (e.g., in no more than a single row of a single table). Any possible linkages to this data element (possibly in other areas of the relational schema or even in distant federated databases) are by reference only. Thus, when any such data element is updated, this update propagates to the enterprise at large, without the possibility of a duplicate value somewhere in the distant enterprise not being updated (because there would be no duplicate values that needed updating).[citation needed] 47 |

48 |

49 | Deployment of an SSOT architecture is becoming increasingly important in enterprise settings where incorrectly linked duplicate or de-normalized data elements (a direct consequence of intentional or unintentional denormalization of any explicit data model) poses a risk for retrieval of outdated, and therefore incorrect, information. A common example would be the electronic health record, where it is imperative to accurately validate patient identity against a single referential repository, which serves as the SSOT. Duplicate representations of data within the enterprise would be implemented by the use of pointers rather than duplicate database tables, rows, or cells. This ensures that data updates to elements in the authoritative location are comprehensively distributed to all federated database constituencies in the larger overall enterprise architecture.[citation needed] 50 |

51 |

52 | SSOT systems provide data that is authentic, relevant, and referable.[1] 53 |

54 | 55 |

Source: Wikipedia

56 |
57 | 58 | 59 |

Bee

60 |

61 | Bee is a collection of lightweight interaction elements for modern web applications. It is built on top of Dart's Web UI package. It contains frequently used components like Buttons, Popovers, Overlays, Input Fields and more. 62 |

63 |
64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /lib/components/popover/popover.dart: -------------------------------------------------------------------------------- 1 | import 'package:polymer/polymer.dart'; 2 | import 'dart:html'; 3 | import 'dart:async'; 4 | 5 | import 'package:escape_handler/escape_handler.dart'; 6 | import '../../utils/html_helpers.dart'; 7 | 8 | class State { 9 | static const ACTIVE = const State._(0); 10 | static const INACTIVE = const State._(1); 11 | 12 | final int value; 13 | const State._(this.value); 14 | } 15 | 16 | @CustomTag('b-popover') 17 | class BeePopover extends PolymerElement { 18 | 19 | // CSS Styles 20 | @published String position = "relative"; 21 | @published String left; 22 | @published String right; 23 | @published String top; 24 | @published String bottom; 25 | @published String arrowLeft; 26 | @published String arrowRight; 27 | @published String arrowTop; 28 | @published String arrowBottom; 29 | static const EventStreamProvider showEvent = const EventStreamProvider('show'); 30 | static const EventStreamProvider hideEvent = const EventStreamProvider('hide'); 31 | @observable int elementTimestamp = 0; 32 | StreamSubscription _documentClick; 33 | StreamSubscription _documentTouch; 34 | StreamSubscription _toggleClick; 35 | StreamSubscription _toggleTouch; 36 | State _state = State.INACTIVE; 37 | EscapeHandler _escapeHandler = new EscapeHandler(); 38 | 39 | BeePopover.created() : super.created() {} 40 | 41 | void attached() { 42 | _updateState(_state); 43 | _setCssStyles(); 44 | _documentClick = document.onClick.listen(null); 45 | _documentClick.onData(_hideClickHandler); 46 | _documentTouch = document.onTouchStart.listen(null); 47 | _documentTouch.onData(_hideClickHandler); 48 | _toggleClick = shadowRoot.querySelector('.q-launch-area').onClick.listen(null); 49 | _toggleClick.onData(toggle); 50 | _toggleTouch = shadowRoot.querySelector('.q-launch-area').onTouchStart.listen(null); 51 | _toggleTouch.onData(toggle); 52 | } 53 | 54 | void _setCssStyles() { 55 | Element popoverWrapper = shadowRoot.querySelector('.q-b-popover-wrapper'); 56 | Element arrow = shadowRoot.querySelector('.q-b-popover-arrow'); 57 | if (position != null) { popoverWrapper.style.position = position; } 58 | if (left != null) { popoverWrapper.style.left = left; } 59 | if (right != null) { popoverWrapper.style.right = right; } 60 | if (top != null) { popoverWrapper.style.top = top; } 61 | if (bottom != null) { popoverWrapper.style.bottom = bottom; } 62 | if (arrowLeft != null) { arrow.style.left = arrowLeft; } 63 | if (arrowRight != null) { arrow.style.right = arrowRight; } 64 | if (arrowTop != null) { arrow.style.top = arrowTop; } 65 | if (arrowBottom != null) { arrow.style.bottom = arrowBottom; } 66 | } 67 | 68 | void detached() { 69 | if (_documentClick != null) { try { _documentClick.cancel(); } on StateError {}; } 70 | if (_documentTouch != null) { try { _documentTouch.cancel(); } on StateError {}; } 71 | if (_toggleClick != null) { try { _toggleClick.cancel(); } on StateError {}; } 72 | if (_toggleTouch != null) { try { _toggleTouch.cancel(); } on StateError {}; } 73 | } 74 | 75 | void toggle(event) { 76 | if (event != null) {event.preventDefault(); } 77 | if (_state == State.ACTIVE) { 78 | _updateState(State.INACTIVE); 79 | } else { 80 | _updateState(State.ACTIVE); 81 | } 82 | } 83 | 84 | void _updateState(var newState) { 85 | Element popoverWrapper = shadowRoot.querySelector('.q-b-popover-wrapper'); 86 | _state = newState; 87 | if (_state == State.ACTIVE) { 88 | popoverWrapper.style.display = 'block'; 89 | // the attribute elementTimestamp represents the time the popover was activated which is important for 2 reasons 90 | // * identify the popover in the dom 91 | // * find out which layer to close on esc 92 | // this implmentation assumes that multiple elements can't be activated at the exact same millisecond 93 | elementTimestamp = new DateTime.now().millisecondsSinceEpoch; 94 | var deactivateFuture = _escapeHandler.addWidget(elementTimestamp); 95 | deactivateFuture.then((_) { 96 | _updateState(State.INACTIVE); 97 | }); 98 | dispatchEvent(new CustomEvent("show")); 99 | } else { 100 | popoverWrapper.style.display = 'none'; 101 | _escapeHandler.removeWidget(elementTimestamp); 102 | // the element is deactive and we give it 0 as timestamp to make sure 103 | // you can't find it by getting the max of all elements with the data attribute 104 | elementTimestamp = 0; 105 | dispatchEvent(new CustomEvent("hide")); 106 | } 107 | } 108 | 109 | /** 110 | * Close the popover-body in case the user clicked outside of it. 111 | * 112 | * The only exception is when the user clicked on the toggle area (this case is handled by toggle) 113 | * We can close this popover if the user clicked outside of this component 114 | * if the user clicked inside of the component we are closing it through the togglehandler. 115 | */ 116 | void _hideClickHandler(Event event) { 117 | bool clickOutsideComponent = !insideOrIsNodeWhere(event.target, (element) => element.hashCode == shadowRoot.host.hashCode); 118 | 119 | if (clickOutsideComponent) { 120 | _updateState(State.INACTIVE); 121 | } 122 | } 123 | 124 | Stream get onShow => showEvent.forTarget(this); 125 | Stream get onHide => hideEvent.forTarget(this); 126 | } -------------------------------------------------------------------------------- /lib/components/popover.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:html'; 3 | import 'package:web_ui/web_ui.dart'; 4 | import 'package:escape_handler/escape_handler.dart'; 5 | import '../utils/html_helpers.dart'; 6 | 7 | class State { 8 | static const ACTIVE = const State._(0); 9 | static const DEACTIVE = const State._(1); 10 | 11 | final int value; 12 | const State._(this.value); 13 | } 14 | 15 | @observable 16 | class PopoverComponent extends WebComponent { 17 | static const EventStreamProvider showEvent = const EventStreamProvider('show'); 18 | static const EventStreamProvider hideEvent = const EventStreamProvider('hide'); 19 | 20 | StreamSubscription documentClick; 21 | StreamSubscription documentTouch; 22 | StreamSubscription toggleClick; 23 | StreamSubscription toggleTouch; 24 | String elementTimestamp = "0"; 25 | String width; 26 | State state = State.DEACTIVE; 27 | DivElement _popoverWrapper; 28 | EscapeHandler _escapeHandler = new EscapeHandler(); 29 | 30 | // CSS Styles 31 | String position = "relative"; 32 | String left; 33 | String right; 34 | String top; 35 | String bottom; 36 | String arrowLeft; 37 | String arrowRight; 38 | String arrowTop; 39 | String arrowBottom; 40 | 41 | void inserted() { 42 | this._popoverWrapper = getShadowRoot('b-popover').query('.q-b-popover-wrapper'); 43 | this._updateState(this.state); 44 | this._setCssStyles(); 45 | this.documentClick = document.onClick.listen(null); 46 | this.documentClick.onData(this._hideClickHandler); 47 | this.documentTouch = document.onTouchStart.listen(null); 48 | this.documentTouch.onData(this._hideClickHandler); 49 | this.toggleClick = getShadowRoot('b-popover').query('.q-launch-area').onClick.listen(null); 50 | this.toggleClick.onData(this.toggle); 51 | this.toggleTouch = getShadowRoot('b-popover').query('.q-launch-area').onTouchStart.listen(null); 52 | this.toggleTouch.onData(this.toggle); 53 | } 54 | 55 | void removed() { 56 | if (this.documentClick != null) { try { this.documentClick.cancel(); } on StateError {}; } 57 | if (this.documentTouch != null) { try { this.documentTouch.cancel(); } on StateError {}; } 58 | if (this.toggleClick != null) { try { this.toggleClick.cancel(); } on StateError {}; } 59 | if (this.toggleTouch != null) { try { this.toggleTouch.cancel(); } on StateError {}; } 60 | } 61 | 62 | void toggle(event) { 63 | if (event != null) {event.preventDefault(); } 64 | if (this.state == State.ACTIVE) { 65 | _updateState(State.DEACTIVE); 66 | } else { 67 | _updateState(State.ACTIVE); 68 | } 69 | } 70 | 71 | Stream get onShow => showEvent.forTarget(this); 72 | Stream get onHide => hideEvent.forTarget(this); 73 | 74 | void _setCssStyles() { 75 | Element arrow = getShadowRoot('b-popover').query('.q-b-popover-arrow'); 76 | if (this.position != null) { this._popoverWrapper.style.position = this.position; } 77 | if (this.left != null) { this._popoverWrapper.style.left = this.left; } 78 | if (this.right != null) { this._popoverWrapper.style.right = this.right; } 79 | if (this.top != null) { this._popoverWrapper.style.top = this.top; } 80 | if (this.bottom != null) { this._popoverWrapper.style.bottom = this.bottom; } 81 | if (this.arrowLeft != null) { arrow.style.left = this.arrowLeft; } 82 | if (this.arrowRight != null) { arrow.style.right = this.arrowRight; } 83 | if (this.arrowTop != null) { arrow.style.top = this.arrowTop; } 84 | if (this.arrowBottom != null) { arrow.style.bottom = this.arrowBottom; } 85 | } 86 | 87 | void _hideClickHandler(Event event) { 88 | // close the overlay in case the user clicked outside of the overlay content area 89 | // only exception is when the user clicked on the toggle area (this case is handled by toggle) 90 | bool clickOutsidePopover = !insideOrIsNodeWhere(event.target, (element) => element.hashCode == _popoverWrapper.hashCode); 91 | Element launchArea = getShadowRoot('b-popover').query('.q-launch-area'); 92 | bool clickOnToggleArea = insideOrIsNodeWhere(event.target, (element) => element.hashCode == launchArea.hashCode); 93 | if (clickOutsidePopover && !clickOnToggleArea) { 94 | _updateState(State.DEACTIVE); 95 | } 96 | } 97 | 98 | void _updateState(var newState) { 99 | state = newState; 100 | if (state == State.ACTIVE) { 101 | this._popoverWrapper.style.display = 'block'; 102 | // the attribute elementTimestamp represents the time the popover was activated which is important for 2 reasons 103 | // * identify the popover in the dom 104 | // * find out which layer to close on esc 105 | // this implmentation assumes that multiple elements can't be activated at the exact same millisecond 106 | this.elementTimestamp = new DateTime.now().millisecondsSinceEpoch.toString(); 107 | var deactivateFuture = _escapeHandler.addWidget(int.parse(elementTimestamp)); 108 | deactivateFuture.then((_) { 109 | _updateState(State.DEACTIVE); 110 | }); 111 | dispatchEvent(new CustomEvent("show")); 112 | } else { 113 | _popoverWrapper.style.display = 'none'; 114 | _escapeHandler.removeWidget(int.parse(elementTimestamp)); 115 | // the element is deactive and we give it 0 as timestamp to make sure 116 | // you can't find it by getting the max of all elements with the data attribute 117 | elementTimestamp = "0"; 118 | dispatchEvent(new CustomEvent("hide")); 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /lib/components/secret/secret.css: -------------------------------------------------------------------------------- 1 | .b-secret-wrapper { 2 | position: relative; 3 | } 4 | 5 | .b-secret-wrapper.secret-has-focus { 6 | /* TODO fix focus ring on FF/IE/OPERA */ 7 | outline: 5px auto -webkit-focus-ring-color; 8 | outline: 5px solid -moz-mac-focusring; 9 | outline-offset: -2px; 10 | } 11 | 12 | input[type="password"].b-secret-input { 13 | -moz-box-sizing: border-box; 14 | -webkit-box-sizing: border-box; 15 | box-sizing: border-box; 16 | display: block; 17 | width: 100%; 18 | height: 40px; 19 | padding: 10px 126px 10px 35px; 20 | margin: 0; 21 | float: left; 22 | font-size: 14px; 23 | line-height: 19px; 24 | color: #808080; 25 | font-family: "Courier New"; 26 | background: 27 | url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAFUCAMAAAB2lR6YAAAAMFBMVEXu7u719fXn5+fz8/Pm5ubj4+Pk5OT9/f3w8PD4+Pjp6en6+vrx8fHs7Ozi4uL///9TeN5xAAABeUlEQVR42u3a0XKDIBCFYQEBYWX3/d+2EY3SyMa2Y2c6nfPnCvMFvEAvmAxy2bdJKqP3YyGdTLw1a8TwXlKIPUjuE+Km2CWpJfQjoi90P4nXRMIhRumT4SCzQmgXPipEypMU0QhtwpNCjt3gRCM055XkmXokGstN1p2IGfml0XwixnKnbHZCmZVyWsnMb5oWUoXe/CDhPQkPwhfdRq4CAQEBAQEBAQEBAQEBAQEBAbmPmPVM1aiELG9Z6hPjec+nHiHPTYE6JNdfD84N1eYzSfV6rPON/IhOpDQHgrSQ4SDNOqUd5BMJ7enl9DVi/y5hJRCQmwleqiAgICAgICAgICAgICAgIP+V3PjnuVDUwkaKqJXfIcmyL1HETbUOiYG5jiabl0+HGF7ycqQQFknOuaiSOksJIaQuSZUEOVJudxChtNQjMi0k1S+YQ5fEsA5irUNoqAtZF5V7iYWfBdcls+emkc4k8efCmTh+KZ5nmV+6fUtdb++bnqMPIIKYz/xqIW0AAAAASUVORK5CYII=) 28 | 10px -69px no-repeat; 29 | background-size: 17px 170px; 30 | border: 1px solid #ccc; 31 | -webkit-border-radius: 3px; 32 | -moz-border-radius: 3px; 33 | -webkit-border-radius: 3px; 34 | -moz-border-radius: 3px; 35 | border-radius: 3px; 36 | -webkit-transition: border linear .2s, box-shadow linear .2s; 37 | -moz-transition: border linear .2s, box-shadow linear .2s; 38 | -ms-transition: border linear .2s, box-shadow linear .2s; 39 | -o-transition: border linear .2s, box-shadow linear .2s; 40 | transition: border linear .2s, box-shadow linear .2s; 41 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .1); 42 | -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .1); 43 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .1); 44 | } 45 | 46 | input[type="password"].b-secret-input:hover { 47 | border: 1px solid #6c6c6c; 48 | } 49 | 50 | .b-secret-toggle-visibility { 51 | margin-right: 1px; 52 | margin-left: 0px; 53 | margin-top: 1px; 54 | position: absolute; 55 | right: 0; 56 | background: whiteSmoke; 57 | border-right: 1px solid transparent; 58 | border-top: 1px solid transparent; 59 | border-bottom: 1px solid transparent; 60 | border-left: 1px solid #cccccc; 61 | z-index: 2; 62 | float: left; 63 | display: block; 64 | width: 95px; 65 | height: 16px; 66 | padding: 11px 5px 9px 5px; 67 | font-size: 12px; 68 | text-align: center; 69 | text-shadow: 0 1px 0 white; 70 | border-image: initial; 71 | border-radius: 0 3px 3px 0; 72 | -webkit-border-radius: 0 3px 3px 0; 73 | -moz-border-radius: 0 3px 3px 0; 74 | white-space: nowrap; 75 | -webkit-touch-callout: none; 76 | -webkit-user-select: none; 77 | -khtml-user-select: none; 78 | -moz-user-select: none; 79 | -ms-user-select: none; 80 | user-select: none; 81 | } 82 | 83 | .b-secret-toggle-visibility:hover,.b-secret-toggle-visibility:focus { 84 | cursor: pointer; 85 | background-color: #f0f0f0; 86 | } 87 | 88 | .b-secret-toggle-visibility::selection { 89 | background-color: transparent; 90 | } 91 | 92 | .b-secret-toggle-visibility::-moz-selection { 93 | background-color: transparent; 94 | } 95 | 96 | .b-secret-toggle-visibility::-webkit-selection { 97 | background-color: transparent; 98 | } 99 | 100 | input[type="text"].text-field { 101 | -moz-box-sizing: border-box; 102 | -webkit-box-sizing: border-box; 103 | box-sizing: border-box; 104 | display: block; 105 | width: 100%; 106 | height: 40px; 107 | padding: 10px 126px 10px 35px; 108 | margin: 0; 109 | float: left; 110 | font-size: 14px; 111 | line-height: 19px; 112 | color: #808080; 113 | font-family: "Courier New"; 114 | background: 115 | url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAFUCAMAAAB2lR6YAAAAMFBMVEXu7u719fXn5+fz8/Pm5ubj4+Pk5OT9/f3w8PD4+Pjp6en6+vrx8fHs7Ozi4uL///9TeN5xAAABeUlEQVR42u3a0XKDIBCFYQEBYWX3/d+2EY3SyMa2Y2c6nfPnCvMFvEAvmAxy2bdJKqP3YyGdTLw1a8TwXlKIPUjuE+Km2CWpJfQjoi90P4nXRMIhRumT4SCzQmgXPipEypMU0QhtwpNCjt3gRCM055XkmXokGstN1p2IGfml0XwixnKnbHZCmZVyWsnMb5oWUoXe/CDhPQkPwhfdRq4CAQEBAQEBAQEBAQEBAQEBAbmPmPVM1aiELG9Z6hPjec+nHiHPTYE6JNdfD84N1eYzSfV6rPON/IhOpDQHgrSQ4SDNOqUd5BMJ7enl9DVi/y5hJRCQmwleqiAgICAgICAgICAgICAgIP+V3PjnuVDUwkaKqJXfIcmyL1HETbUOiYG5jiabl0+HGF7ycqQQFknOuaiSOksJIaQuSZUEOVJudxChtNQjMi0k1S+YQ5fEsA5irUNoqAtZF5V7iYWfBdcls+emkc4k8efCmTh+KZ5nmV+6fUtdb++bnqMPIIKYz/xqIW0AAAAASUVORK5CYII=) 116 | 10px -69px no-repeat; 117 | background-size: 17px 170px; 118 | border: 1px solid #ccc; 119 | -webkit-border-radius: 3px; 120 | -moz-border-radius: 3px; 121 | -webkit-border-radius: 3px; 122 | -moz-border-radius: 3px; 123 | border-radius: 3px; 124 | -webkit-transition: border linear .2s, box-shadow linear .2s; 125 | -moz-transition: border linear .2s, box-shadow linear .2s; 126 | -ms-transition: border linear .2s, box-shadow linear .2s; 127 | -o-transition: border linear .2s, box-shadow linear .2s; 128 | transition: border linear .2s, box-shadow linear .2s; 129 | -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .1); 130 | -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .1); 131 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, .1); 132 | } 133 | 134 | input[type="text"].q-text-field:hover { 135 | border: 1px solid #6c6c6c; 136 | } 137 | 138 | input:focus { 139 | outline: 0px; 140 | } -------------------------------------------------------------------------------- /lib/components/overlay/overlay.dart: -------------------------------------------------------------------------------- 1 | import 'package:polymer/polymer.dart'; 2 | import 'dart:async'; 3 | import 'dart:html'; 4 | import 'package:escape_handler/escape_handler.dart'; 5 | 6 | /* 7 | * Possible things to tests: 8 | * 9 | * test esc behavior -> should close on esc of no younger element is active 10 | * test scrollbar callculation 11 | * test should hide itself, 12 | * test if provided closeCallback gets called 13 | * test if component still work without provided closeCallback 14 | * test component still works but shows a warning if closeCallback is not callable 15 | * 16 | */ 17 | 18 | class State { 19 | static const ACTIVE = const State._(0); 20 | static const DEACTIVE = const State._(1); 21 | 22 | final int value; 23 | const State._(this.value); 24 | } 25 | 26 | 27 | /** 28 | * B-Overlay is scrollable modal spanning over the entire visisble html area. 29 | */ 30 | // Listen to touch start for improved UX by immedialty performing the close 31 | // action. Some Browsers like iOS Safari have a delay until the click event is 32 | // fired which is not desired for closing the overlay. 33 | @CustomTag('b-overlay') 34 | class BeeOverlay extends PolymerElement { 35 | static const EventStreamProvider showEvent = const EventStreamProvider('show'); 36 | static const EventStreamProvider hideEvent = const EventStreamProvider('hide'); 37 | 38 | @published String width = "600px"; 39 | @published int elementTimestamp = 0; 40 | State _state = State.DEACTIVE; 41 | DivElement _backdrop; 42 | EscapeHandler _escapeHandler = new EscapeHandler(); 43 | 44 | BeeOverlay.created() : super.created() {} 45 | 46 | void attached() { 47 | _add_scrollbar_info(); 48 | _backdrop = shadowRoot.querySelector('.q-b-overlay-backdrop'); 49 | _updateState(_state); 50 | shadowRoot.querySelector('.q-overlay').style.width = width; 51 | } 52 | 53 | void hide() { 54 | _updateState(State.DEACTIVE); 55 | } 56 | 57 | void show() { 58 | _updateState(State.ACTIVE); 59 | } 60 | 61 | void detached() { 62 | _hide(); 63 | } 64 | 65 | Stream get onShow => showEvent.forTarget(this); 66 | Stream get onHide => hideEvent.forTarget(this); 67 | 68 | /** 69 | * Scollbar width detection. 70 | * 71 | * Adds either the class scrollbar0, scrollbar15 or 72 | * scrollbar20 to the body element. At the time we investigated other 73 | * solutions we figured out Facebook detects 3 different scrollbar widths and 74 | * we decided to do the same. 75 | * 76 | * See http://jdsharp.us/jQuery/minute/calculate-scrollbar-width.php 77 | */ 78 | void _add_scrollbar_info() { 79 | var validator = new NodeValidatorBuilder()..allowElement('div', attributes: ['style']); 80 | var template = """ 81 |
82 |
83 |
84 | """; 85 | Element div = new Element.html(template, validator: validator); 86 | // append the div, do the calculation and then remove it 87 | querySelector('body').append(div); 88 | int width1 = div.clientWidth; 89 | div.style.overflowY = 'scroll'; 90 | int width2 = div.clientWidth; 91 | div.remove(); 92 | int scrollbarWidth = width1 - width2; 93 | switch (scrollbarWidth) { 94 | case 0: 95 | querySelector('body').classes.add('scrollbar0'); 96 | break; 97 | case 20: 98 | querySelector('body').classes.add('scrollbar20'); 99 | break; 100 | default: 101 | querySelector('body').classes.add('scrollbar15'); 102 | } 103 | } 104 | 105 | /** 106 | * Close the overlay in case the user clicked outside of the overlay 107 | * content area. 108 | */ 109 | void handleHideAction(Event event, var detail, Node target) { 110 | Element backdrop; 111 | 112 | if (event.target.classes.contains('q-b-overlay-backdrop')) { 113 | backdrop = event.target; 114 | } else if (event.target.classes.contains('q-b-overlay-backdrop-close')) { 115 | backdrop = event.target.parent; 116 | } 117 | if (backdrop != null && backdrop.contains(shadowRoot.querySelector('.q-overlay'))) { 118 | event.preventDefault(); 119 | _updateState(State.DEACTIVE); 120 | } 121 | } 122 | 123 | _updateState(var newState) { 124 | _state = newState; 125 | if (_state == State.ACTIVE) { 126 | _show(); 127 | } else { 128 | _hide(); 129 | } 130 | } 131 | 132 | void _show() { 133 | _backdrop.style.display = 'block'; 134 | // The attribute elementTimestamp represents the time the popover was 135 | // activated which is important for 2 reasons: 136 | // * identify the overlay in the dom 137 | // * find out which layer/element to close on esc 138 | // This implmentation assumes that multiple elements can't be activated at 139 | // the exact same millisecond. 140 | elementTimestamp = new DateTime.now().millisecondsSinceEpoch; 141 | var hideFuture = _escapeHandler.addWidget(elementTimestamp); 142 | hideFuture.then((_) { 143 | _updateState(State.DEACTIVE); 144 | }); 145 | querySelector("html").classes.add('overlay-backdrop-active'); 146 | dispatchEvent(new CustomEvent("show")); 147 | } 148 | 149 | void _hide() { 150 | _backdrop.style.display = 'none'; 151 | _escapeHandler.removeWidget(elementTimestamp); 152 | // The element is deactive and we give it 0 as timestamp to make sure it 153 | // can't be found by getting the max of all elements with the data attribute 154 | elementTimestamp = 0; 155 | 156 | List backdrops = querySelectorAll('.q-b-overlay-backdrop'); 157 | // TODO check for visible getter in the future 158 | // see https://code.google.com/p/dart/issues/detail?id=6526 159 | Iterable visibleBackdrops = backdrops.where((Element backdrop) => backdrop.style.display != 'none'); 160 | if (visibleBackdrops.length == 0) { 161 | // To re-enable scrolling we reset the body's style attribute. 162 | // (but only if we are hiding the last overlay) 163 | querySelector("html").classes.remove('overlay-backdrop-active'); 164 | } 165 | dispatchEvent(new CustomEvent("hide")); 166 | } 167 | } -------------------------------------------------------------------------------- /lib/components/overlay.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:html'; 3 | import 'package:web_ui/web_ui.dart'; 4 | import 'package:escape_handler/escape_handler.dart'; 5 | 6 | /* 7 | * 8 | * tests: 9 | * 10 | * test esc behavior -> should close on esc of no younger element is active 11 | * test scrollbar callculation 12 | * test should hide itself, 13 | * test if provided closeCallback gets calles 14 | * test if component still work without provided closeCallback 15 | * test component still works but shows a warning if closeCallback is not callable 16 | * 17 | */ 18 | 19 | class State { 20 | static const ACTIVE = const State._(0); 21 | static const DEACTIVE = const State._(1); 22 | 23 | final int value; 24 | const State._(this.value); 25 | } 26 | 27 | class OverlayComponent extends WebComponent { 28 | static const EventStreamProvider showEvent = const EventStreamProvider('show'); 29 | static const EventStreamProvider hideEvent = const EventStreamProvider('hide'); 30 | StreamSubscription clickSubscription; 31 | StreamSubscription touchSubscription; 32 | String width = "600px"; 33 | DivElement _backdrop; 34 | @observable String elementTimestamp = "0"; 35 | @observable State state = State.DEACTIVE; 36 | EscapeHandler _escapeHandler = new EscapeHandler(); 37 | 38 | void created() { 39 | this._add_scrollbar_info(); 40 | } 41 | 42 | void inserted() { 43 | this._backdrop = getShadowRoot('b-overlay').query('.q-b-overlay-backdrop'); 44 | this._updateState(this.state); 45 | getShadowRoot('b-overlay').query('.q-overlay').style.width = this.width; 46 | } 47 | 48 | void hide() { 49 | _updateState(State.DEACTIVE); 50 | } 51 | 52 | void show() { 53 | _updateState(State.ACTIVE); 54 | } 55 | 56 | void removed() { 57 | this._hide(); 58 | } 59 | 60 | Stream get onShow => showEvent.forTarget(this); 61 | Stream get onHide => hideEvent.forTarget(this); 62 | 63 | /* 64 | * Scollbar width detection. Adds either the class scrollbar0, scrollbar15 or scrollbar20 65 | * to the body element. 66 | * 67 | * See http://jdsharp.us/jQuery/minute/calculate-scrollbar-width.php 68 | */ 69 | void _add_scrollbar_info() { 70 | var validator = new NodeValidatorBuilder()..allowElement('div', attributes: ['style']); 71 | var template = """ 72 |
73 |
74 |
75 | """; 76 | Element div = new Element.html(template, validator: validator); 77 | // append the div, do the calculation and then remove it 78 | querySelector('body').append(div); 79 | int width1 = div.clientWidth; 80 | div.style.overflowY = 'scroll'; 81 | int width2 = div.clientWidth; 82 | div.remove(); 83 | int scrollbarWidth = width1 - width2; 84 | switch (scrollbarWidth) { 85 | case 0: 86 | querySelector('body').classes.add('scrollbar0'); 87 | break; 88 | case 20: 89 | querySelector('body').classes.add('scrollbar20'); 90 | break; 91 | default: 92 | querySelector('body').classes.add('scrollbar15'); 93 | } 94 | } 95 | 96 | /* 97 | * Close the overlay in case the user clicked outside of the overlay 98 | * content area. 99 | */ 100 | void _removeClickHandler(event) { 101 | Element backdrop; 102 | if (event.target.classes.contains('q-b-overlay-backdrop')) { 103 | backdrop = event.target; 104 | } else if (event.target.classes.contains('q-b-overlay-backdrop-close')) { 105 | backdrop = event.target.parent; 106 | } 107 | if (backdrop != null && backdrop.contains(getShadowRoot('b-overlay').query('.q-overlay'))) { 108 | event.preventDefault(); 109 | _updateState(State.DEACTIVE); 110 | } 111 | } 112 | 113 | _updateState(var state) { 114 | this.state = state; 115 | if (this.state == State.ACTIVE) { 116 | this._show(); 117 | } else { 118 | this._hide(); 119 | } 120 | } 121 | 122 | void _show() { 123 | this._backdrop.style.display = 'block'; 124 | // the attribute elementTimestamp represents the time the popover was activated which is important for 2 reasons 125 | // * identify the overlay in the dom 126 | // * find out which layer/element to close on esc 127 | // this implmentation assumes that multiple elements can't be activated at the exact same millisecond 128 | this.elementTimestamp = new DateTime.now().millisecondsSinceEpoch.toString(); 129 | var hideFuture = _escapeHandler.addWidget(int.parse(elementTimestamp)); 130 | hideFuture.then((_) { 131 | _updateState(State.DEACTIVE); 132 | }); 133 | querySelector("html").classes.add('overlay-backdrop-active'); 134 | this.clickSubscription = document.onClick.listen(null); 135 | this.clickSubscription.onData(this._removeClickHandler); 136 | this.touchSubscription = document.onTouchStart.listen(null); 137 | this.touchSubscription.onData(this._removeClickHandler); 138 | this.dispatchEvent(new CustomEvent("show")); 139 | } 140 | 141 | void _hide() { 142 | this._backdrop.style.display = 'none'; 143 | _escapeHandler.removeWidget(int.parse(elementTimestamp)); 144 | // the element is deactive and we give it 0 as timestamp to make sure 145 | // you can't find it by getting the max of all elements with the data attribute 146 | this.elementTimestamp = "0"; 147 | if (this.clickSubscription != null) { try { this.clickSubscription.cancel(); } on StateError {}; } 148 | if (this.touchSubscription != null) { try { this.touchSubscription.cancel(); } on StateError {}; } 149 | List backdrops = querySelectorAll('.q-b-overlay-backdrop'); 150 | // TODO check for visible getter in the future, see https://code.google.com/p/dart/issues/detail?id=6526 151 | Iterable visibleBackdrops = backdrops.where((Element backdrop) => backdrop.style.display != 'none'); 152 | if (visibleBackdrops.length == 0) { 153 | // to reenable scrolling we reset the body's style attribute (but only if we are hiding the last overlay) 154 | querySelector("html").classes.remove('overlay-backdrop-active'); 155 | } 156 | this.dispatchEvent(new CustomEvent("hide")); 157 | } 158 | } -------------------------------------------------------------------------------- /lib/components/textarea.dart: -------------------------------------------------------------------------------- 1 | import 'package:web_ui/web_ui.dart'; 2 | import 'dart:html'; 3 | import 'dart:math'; 4 | import 'dart:async'; 5 | import 'dart:convert'; 6 | 7 | class TextareaComponent extends WebComponent { 8 | static const EventStreamProvider blurEvent = const EventStreamProvider('blur'); 9 | static const EventStreamProvider focusEvent = const EventStreamProvider('focus'); 10 | @observable String value; 11 | @observable String placeholder = ''; 12 | String minHeight; 13 | String paddingTop = '0'; 14 | String paddingRight = '0'; 15 | String paddingBottom = '0'; 16 | String paddingLeft = '0'; 17 | String fontSize = '14'; 18 | String lineHeight = '21'; 19 | String color = "505050"; 20 | 21 | var _windowResize; 22 | var _textarea; 23 | var _shadow; 24 | final _htmlEscape = new HtmlEscape(); 25 | 26 | void focus() { 27 | if (_textarea != null) { 28 | _textarea.focus(); 29 | } 30 | } 31 | 32 | void inserted() { 33 | _textarea = getShadowRoot('b-textarea').query('.q-textarea-textarea'); 34 | _shadow = getShadowRoot('b-textarea').query('.q-textarea-shadow'); 35 | 36 | _textarea.style.paddingTop = '${paddingTop}px'; 37 | _textarea.style.paddingRight = '${paddingRight}px'; 38 | _textarea.style.paddingBottom = '${paddingBottom}px'; 39 | _textarea.style.paddingLeft = '${paddingLeft}px'; 40 | _textarea.style.fontSize = '${fontSize}px'; 41 | _textarea.style.color = '#${color}'; 42 | if (lineHeight == 'normal' || lineHeight == 'inherit') { 43 | _textarea.style.lineHeight = lineHeight; 44 | } else { 45 | _textarea.style.lineHeight = '${lineHeight}px'; 46 | } 47 | 48 | if (minHeight != null) { 49 | _textarea.style.minHeight = '${minHeight}px'; 50 | _shadow.style.minHeight = '${minHeight}px'; 51 | } 52 | 53 | _shadow.style.fontSize = _textarea.getComputedStyle().fontSize; 54 | _shadow.style.fontFamily = _textarea.getComputedStyle().fontFamily; 55 | _shadow.style.fontWeight = _textarea.getComputedStyle().fontWeight; 56 | _shadow.style.lineHeight = _textarea.getComputedStyle().lineHeight; 57 | _shadow.style.paddingTop = _textarea.getComputedStyle().paddingTop; 58 | _shadow.style.paddingRight = _textarea.getComputedStyle().paddingRight; 59 | _shadow.style.paddingBottom = _textarea.getComputedStyle().paddingBottom; 60 | _shadow.style.paddingLeft = _textarea.getComputedStyle().paddingLeft; 61 | 62 | 63 | // when the text field span the whole window the width the real 64 | // width might change after resizing the window 65 | _windowResize = window.onResize.listen((_) { 66 | resize(); 67 | }); 68 | 69 | // run resize once to set the correct size 70 | resize(); 71 | } 72 | 73 | /* 74 | * Allow to set the selection range of the text carret. 75 | */ 76 | setSelectionRange(int start, int end) { 77 | _textarea = getShadowRoot('b-textarea').query('.q-textarea-textarea'); 78 | _textarea.setSelectionRange(start, end); 79 | } 80 | 81 | /* 82 | * Resizing the textarea after every change of the value or in case the textarea has been resized. 83 | * Only works in case the window has been resized. 84 | * 85 | * The content of the shadow div used to calculate the size gets sanitzied since the provided 86 | * value could also include content from someone else then the currently editing user. 87 | * 88 | * Note: 89 | * If you fill a text area only with spaces and the cursor reaches the right 90 | * side it won't break the line. This can lead to unexpected behaviour for the 91 | * autogrowing. Didn't find any solution and I noticed Facebook has the same 92 | * issue with the status update form. 93 | */ 94 | void resize() { 95 | _shadow.style.width = _textarea.getComputedStyle().width; 96 | var validator = new NodeValidatorBuilder()..allowElement('br'); 97 | _shadow.setInnerHtml(_sanitizeInput(value), validator: validator); 98 | var _shadowHeight = _shadow.getComputedStyle().height; 99 | 100 | // Wait with the resize until the widget is rendered in the DOM. A textarea 101 | // has the height auto if it isn't a block element or not in the DOM. 102 | // Scheduling a microtask is fast enough to resize the textarea before it is 103 | // shown and delayed enough to get the proper pixel height. 104 | if (_shadowHeight == 'auto') { 105 | scheduleMicrotask(() => resize()); 106 | return; 107 | } 108 | 109 | var newHeight; 110 | if (minHeight != null) { 111 | // There are edge cases where pixel values can have decimal places. That's 112 | // why we parse the num and then round to int. 113 | var height = num.parse(_shadowHeight.replaceAll('px', '')).round(); 114 | newHeight = '${max(height, num.parse(minHeight).round())}px'; 115 | } else { 116 | newHeight = _shadowHeight; 117 | } 118 | _textarea.style.height = newHeight; 119 | } 120 | 121 | void removed() { 122 | _windowResize.cancel(); 123 | } 124 | 125 | Stream get onBlur => blurEvent.forTarget(this); 126 | 127 | _blur(Event event) { 128 | dispatchEvent(new CustomEvent("blur")); 129 | } 130 | 131 | Stream get onFocus => focusEvent.forTarget(this); 132 | 133 | _focus(Event event) { 134 | dispatchEvent(new CustomEvent("focus")); 135 | } 136 | 137 | /* 138 | * Sanitizes the input. 139 | * 140 | * The input gets escaped based on 141 | * https://www.owasp.org/index.php/XSS_%28Cross_Site_Scripting%29_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content 142 | * 143 | * Further whitespaces and new lines are used to create a container with a proper height. 144 | */ 145 | String _sanitizeInput(input) { 146 | var computedHtml; 147 | if (input != null) { 148 | var escaptedHtml = _htmlEscape.convert(input); 149 | computedHtml = escaptedHtml 150 | .replaceAll(new RegExp(r'\n$'), '
 ') 151 | .replaceAll('\n', '
'); 152 | // fill in at least one space to make sure the textarea is at least one line high 153 | if (computedHtml.length == 0) { 154 | computedHtml = " "; 155 | } 156 | // fill in a non-breaking space in case there is only one whitespace 157 | // regex taken from http://stackoverflow.com/a/3469155/837709 158 | if (computedHtml.length == 1 && new RegExp(r'[^\S\n]').hasMatch(computedHtml)) { 159 | computedHtml = " "; 160 | } 161 | } else { 162 | computedHtml = " "; 163 | } 164 | return computedHtml; 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /lib/components/autocomplete.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:html'; 3 | import 'package:web_ui/web_ui.dart'; 4 | import 'package:escape_handler/escape_handler.dart'; 5 | 6 | 7 | class State { 8 | static const ACTIVE = const State._(0); 9 | static const INACTIVE = const State._(1); 10 | 11 | final int value; 12 | const State._(this.value); 13 | } 14 | 15 | class AutocompleteEntry { 16 | 17 | final String id; 18 | final String searchableText; 19 | final Element _element; 20 | 21 | AutocompleteEntry(this.id, this.searchableText, this._element) { 22 | // remove the data-id and data-text since we don't need them in the html 23 | _element.dataset.remove('text'); 24 | _element.dataset.remove('id'); 25 | } 26 | 27 | get sanitizedHtml { 28 | var validator = new NodeValidatorBuilder()..allowHtml5(); 29 | var documentFragment = document.body.createFragment(_element.outerHtml, validator: validator); 30 | return documentFragment; 31 | } 32 | } 33 | 34 | @observable 35 | class AutocompleteComponent extends WebComponent { 36 | 37 | static const EventStreamProvider selectEvent = const EventStreamProvider('select'); 38 | 39 | String maxHeight = "200px"; 40 | String width = "200px"; 41 | String addText = "Add …"; 42 | String placeholder = ""; 43 | String fontSize = "14px"; 44 | 45 | StreamSubscription _keyUp; 46 | String _elementTimestamp = "0"; 47 | EscapeHandler _escapeHandler = new EscapeHandler(); 48 | @observable String _filterQuery = ""; 49 | List _entries = toObservable([]); 50 | List _filteredEntries = toObservable([]); 51 | AutocompleteEntry _activeEntry = null; 52 | State _state = State.INACTIVE; 53 | Timer updateDataSourceTimer; 54 | 55 | void inserted() { 56 | _keyUp = document.onKeyUp.listen(null); 57 | _keyUp.onData(_keyUpHandler); 58 | updateEntriesFromDataSource(); 59 | this._setCssStyles(); 60 | } 61 | 62 | void _setCssStyles() { 63 | Element mainArea = getShadowRoot('b-autocomplete').query('.q-autocomplete-main-area'); 64 | mainArea.style.width = width; 65 | } 66 | 67 | void activate(Event event) { 68 | event.preventDefault(); 69 | if (_state != State.ACTIVE) { 70 | _state = State.ACTIVE; 71 | Element field = getShadowRoot('b-autocomplete').querySelector('.q-autocomplete-activation-area'); 72 | field.style.display = 'none'; 73 | } 74 | } 75 | 76 | void updateEntriesFromDataSource() { 77 | _entries.clear(); 78 | Element dataSource = getShadowRoot('b-autocomplete').querySelector('.data-source'); 79 | if (dataSource != null) { 80 | for (Element element in dataSource.children) { 81 | bool containsText = element.dataset.containsKey('text'); 82 | bool containsID = element.dataset.containsKey('id'); 83 | if (containsText && containsID) { 84 | _entries.add(new AutocompleteEntry(element.dataset['id'], 85 | element.dataset['text'], element.clone(true))); 86 | } else if (dataSource.children.first is TemplateElement) { 87 | } else { 88 | print("Missing data-text or data-id from an source entry."); 89 | } 90 | } 91 | } else { 92 | print("Missing a data source like
Dart
"); 93 | } 94 | } 95 | 96 | void clear() { 97 | _filterQuery = ""; 98 | _filteredEntries.clear(); 99 | } 100 | 101 | void reset() { 102 | _filterQuery = ""; 103 | _updateFilteredEntries(); 104 | } 105 | 106 | void removeSourceEntry(String dataID) { 107 | _entries.removeWhere((AutocompleteEntry entry) => entry.id == dataID); 108 | _updateFilteredEntries(); 109 | } 110 | 111 | String focusOnInput() { 112 | Element field = getShadowRoot('b-autocomplete').querySelector('.q-autocomplete-form-input'); 113 | field.focus(); 114 | return ''; 115 | } 116 | 117 | Stream get onSelect => selectEvent.forTarget(this); 118 | 119 | void _focused() { 120 | _elementTimestamp = new DateTime.now().millisecondsSinceEpoch.toString(); 121 | var deactivateFuture = _escapeHandler.addWidget(int.parse(_elementTimestamp)); 122 | deactivateFuture.then((_) { 123 | _blurred(); 124 | }); 125 | _updateFilteredEntries(); 126 | } 127 | 128 | void _blurred() { 129 | _escapeHandler.removeWidget(int.parse(_elementTimestamp)); 130 | // the element is deactive and we give it 0 as timestamp to make sure 131 | // you can't find it by getting the max of all elements with the data attribute 132 | _elementTimestamp = "0"; 133 | clear(); 134 | _state = State.INACTIVE; 135 | Element field = getShadowRoot('b-autocomplete').querySelector('.q-autocomplete-activation-area'); 136 | field.style.display = 'block'; 137 | } 138 | 139 | void _setToActiveEntry(AutocompleteEntry entry) { 140 | _activeEntry = entry; 141 | } 142 | 143 | void _select(Event event) { 144 | event.preventDefault(); 145 | var detail = {'id': _activeEntry.id, 'text': _activeEntry.searchableText}; 146 | dispatchEvent(new CustomEvent("select", detail: detail)); 147 | reset(); 148 | focusOnInput(); 149 | } 150 | 151 | void _updateFilteredEntries() { 152 | var sanitizedQuery = _filterQuery.trim().toLowerCase(); 153 | var filteredEntries = []; 154 | if (sanitizedQuery == "") { 155 | filteredEntries = new List.from(_entries); 156 | } else { 157 | filteredEntries = _entries.where((AutocompleteEntry entry) { 158 | return entry.searchableText.trim().toLowerCase().contains(sanitizedQuery); 159 | }); 160 | } 161 | _filteredEntries.clear(); 162 | _filteredEntries.addAll(filteredEntries); 163 | if (_filteredEntries.isNotEmpty) { 164 | _activeEntry = _filteredEntries.first; 165 | } 166 | } 167 | 168 | void _keyUpHandler(KeyboardEvent event) { 169 | Element input = getShadowRoot('b-autocomplete').querySelector('.q-autocomplete-form-input'); 170 | if (document.activeElement == input) { 171 | switch (new KeyEvent.wrap(event).keyCode) { 172 | case KeyCode.UP: 173 | _moveUp(); 174 | break; 175 | case KeyCode.DOWN: 176 | _moveDown(); 177 | break; 178 | } 179 | } 180 | } 181 | 182 | _moveUp() { 183 | var tmp = _filteredEntries.reversed.skipWhile((entry) => entry != _activeEntry); 184 | if (tmp.length >= 2) { 185 | _activeEntry = tmp.elementAt(1); 186 | } 187 | } 188 | 189 | _moveDown() { 190 | var tmp = _filteredEntries.skipWhile((entry) => entry != _activeEntry); 191 | if (tmp.length >= 2) { 192 | _activeEntry = tmp.elementAt(1); 193 | } 194 | } 195 | 196 | void removed() { 197 | if (this._keyUp != null) { try { this._keyUp.cancel(); } on StateError {}; } 198 | } 199 | } -------------------------------------------------------------------------------- /lib/components/textarea/textarea.dart: -------------------------------------------------------------------------------- 1 | import 'package:polymer/polymer.dart'; 2 | 3 | import 'dart:html'; 4 | import 'dart:math'; 5 | import 'dart:async'; 6 | import 'dart:convert'; 7 | 8 | @CustomTag('b-textarea') 9 | class BeeTextarea extends PolymerElement { 10 | 11 | static const EventStreamProvider blurEvent = const EventStreamProvider('blur'); 12 | static const EventStreamProvider focusEvent = const EventStreamProvider('focus'); 13 | 14 | @published String value = ''; 15 | @published String placeholder = ''; 16 | @published String minHeight; 17 | @published String paddingTop = '0'; 18 | @published String paddingRight = '0'; 19 | @published String paddingBottom = '0'; 20 | @published String paddingLeft = '0'; 21 | @published String fontSize = '14'; 22 | @published String lineHeight = '21'; 23 | @published String color = "#505050"; 24 | 25 | var _windowResize; 26 | var _textarea; 27 | var _shadow; 28 | final _htmlEscape = new HtmlEscape(); 29 | 30 | BeeTextarea.created() : super.created() {} 31 | 32 | void focus() { 33 | if (_textarea != null) { 34 | _textarea.focus(); 35 | } 36 | } 37 | 38 | void attached() { 39 | _textarea = shadowRoot.querySelector('.q-textarea-textarea'); 40 | _shadow = shadowRoot.querySelector('.q-textarea-shadow'); 41 | 42 | _textarea.style.paddingTop = '${paddingTop}px'; 43 | _textarea.style.paddingRight = '${paddingRight}px'; 44 | _textarea.style.paddingBottom = '${paddingBottom}px'; 45 | _textarea.style.paddingLeft = '${paddingLeft}px'; 46 | _textarea.style.fontSize = '${fontSize}px'; 47 | _textarea.style.color = '${color}'; 48 | if (lineHeight == 'normal' || lineHeight == 'inherit') { 49 | _textarea.style.lineHeight = lineHeight; 50 | } else { 51 | _textarea.style.lineHeight = '${lineHeight}px'; 52 | } 53 | 54 | if (minHeight != null) { 55 | _textarea.style.minHeight = '${minHeight}px'; 56 | _shadow.style.minHeight = '${minHeight}px'; 57 | } 58 | 59 | _shadow.style.fontSize = _textarea.getComputedStyle().fontSize; 60 | _shadow.style.fontFamily = _textarea.getComputedStyle().fontFamily; 61 | _shadow.style.fontWeight = _textarea.getComputedStyle().fontWeight; 62 | _shadow.style.lineHeight = _textarea.getComputedStyle().lineHeight; 63 | _shadow.style.paddingTop = _textarea.getComputedStyle().paddingTop; 64 | _shadow.style.paddingRight = _textarea.getComputedStyle().paddingRight; 65 | _shadow.style.paddingBottom = _textarea.getComputedStyle().paddingBottom; 66 | _shadow.style.paddingLeft = _textarea.getComputedStyle().paddingLeft; 67 | 68 | 69 | // when the text field span the whole window the width the real 70 | // width might change after resizing the window 71 | _windowResize = window.onResize.listen((_) { 72 | resize(); 73 | }); 74 | 75 | // run resize once to set the correct size 76 | resize(); 77 | } 78 | 79 | /** 80 | * Allow to set the selection range of the text carret. 81 | */ 82 | void setSelectionRange(int start, int end) { 83 | _textarea = shadowRoot.querySelector('.q-textarea-textarea'); 84 | _textarea.setSelectionRange(start, end); 85 | } 86 | 87 | /** 88 | * Resizing the textarea after every change of the value or in case the 89 | * textarea has been resized. Only works in case the window has been resized. 90 | * 91 | * The content of the shadow div used to calculate the size gets sanitzied 92 | * since the provided value could also include content from someone else then 93 | * the currently editing user. 94 | * 95 | * Note: 96 | * If you fill a textarea only with spaces and the cursor reaches the right 97 | * side it won't break the line. This can lead to unexpected behaviour for the 98 | * autogrowing. Didn't find any solution and I (Nik) noticed Facebook has the 99 | * same issue with the status update form. 100 | */ 101 | void resize() { 102 | _shadow.style.width = _textarea.getComputedStyle().width; 103 | var validator = new NodeValidatorBuilder()..allowElement('br'); 104 | _shadow.setInnerHtml(_sanitizeInput(_textarea.value), validator: validator); 105 | var _shadowHeight = _shadow.getComputedStyle().height; 106 | 107 | // Wait with the resize until the widget is rendered in the DOM. A textarea 108 | // has the height auto if it isn't a block element or not in the DOM. 109 | // Scheduling a microtask is fast enough to resize the textarea before it is 110 | // shown and delayed enough to get the proper pixel height. 111 | if (_shadowHeight == 'auto') { 112 | scheduleMicrotask(() => resize()); 113 | return; 114 | } 115 | 116 | var newHeight; 117 | if (minHeight != null) { 118 | // There are edge cases where pixel values can have decimal places. That's 119 | // why we parse the num and then round to int. 120 | var height = num.parse(_shadowHeight.replaceAll('px', '')).round(); 121 | newHeight = '${max(height, num.parse(minHeight).round())}px'; 122 | } else { 123 | newHeight = _shadowHeight; 124 | } 125 | _textarea.style.height = newHeight; 126 | } 127 | 128 | void removed() { 129 | _windowResize.cancel(); 130 | } 131 | 132 | ElementStream get onBlur => blurEvent.forTarget(this); 133 | 134 | handleBlur(Event event) { 135 | dispatchEvent(new CustomEvent("blur")); 136 | } 137 | 138 | ElementStream get onFocus => focusEvent.forTarget(this); 139 | 140 | handleFocus(Event event) { 141 | dispatchEvent(new CustomEvent("focus")); 142 | } 143 | 144 | /** 145 | * Sanitizes the input. 146 | * 147 | * The input gets escaped based on 148 | * https://www.owasp.org/index.php/XSS_%28Cross_Site_Scripting%29_Prevention_Cheat_Sheet#RULE_.231_-_HTML_Escape_Before_Inserting_Untrusted_Data_into_HTML_Element_Content 149 | * It is important to escape the test content to prevent a script injection 150 | * done by the shadow div which is used to determine the proper height. 151 | * 152 | * Further whitespaces and new lines are used to create a container with a proper height. 153 | */ 154 | String _sanitizeInput(input) { 155 | var computedHtml; 156 | if (input != null) { 157 | var escaptedHtml = _htmlEscape.convert(input); 158 | computedHtml = escaptedHtml 159 | .replaceAll(new RegExp(r'\n$'), '
 ') 160 | .replaceAll('\n', '
'); 161 | // fill in at least one space to make sure the textarea is at least one line high 162 | if (computedHtml.length == 0) { 163 | computedHtml = " "; 164 | } 165 | // fill in a non-breaking space in case there is only one whitespace 166 | // regex taken from http://stackoverflow.com/a/3469155/837709 167 | if (computedHtml.length == 1 && new RegExp(r'[^\S\n]').hasMatch(computedHtml)) { 168 | computedHtml = " "; 169 | } 170 | } else { 171 | computedHtml = " "; 172 | } 173 | return computedHtml; 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /lib/components/autocomplete/autocomplete.dart: -------------------------------------------------------------------------------- 1 | import 'package:escape_handler/escape_handler.dart'; 2 | import 'dart:async'; 3 | import 'dart:html'; 4 | import 'package:polymer/polymer.dart'; 5 | import 'dart:js' as js; 6 | 7 | 8 | class AutocompleteEntry { 9 | 10 | final String id; 11 | final String searchableText; 12 | final Element _element; 13 | 14 | AutocompleteEntry(this.id, this.searchableText, this._element) { 15 | // remove the data-id and data-text since we don't need them in the html 16 | _element.dataset.remove('text'); 17 | _element.dataset.remove('id'); 18 | } 19 | 20 | get elementHtml { 21 | return _element.outerHtml; 22 | } 23 | } 24 | 25 | @CustomTag('b-autocomplete') 26 | class BeeAutocompleteComponent extends PolymerElement { 27 | 28 | static const EventStreamProvider selectEvent = const EventStreamProvider('select'); 29 | Stream get onSelect => selectEvent.forTarget(this); 30 | 31 | @published 32 | String maxHeight = "200px"; 33 | @published 34 | String width = "200px"; 35 | @published 36 | String addText = "Add …"; 37 | @published 38 | String placeholder = ""; 39 | @published 40 | String fontSize = "14px"; 41 | 42 | int _elementTimestamp = 0; 43 | EscapeHandler _escapeHandler = new EscapeHandler(); 44 | @observable AutocompleteEntry activeEntry = null; 45 | StreamSubscription _keyUp; 46 | 47 | @observable bool isActive = false; 48 | ObservableList entries = new ObservableList(); 49 | ObservableList filteredEntries = new ObservableList(); 50 | @observable String filterQuery = ''; 51 | 52 | BeeAutocompleteComponent.created(): super.created(); 53 | 54 | void attached() { 55 | _keyUp = document.onKeyUp.listen(null); 56 | _keyUp.onData(keyUpHandler); 57 | updateEntriesFromDataSource(); 58 | this._setCssStyles(); 59 | } 60 | 61 | void _setCssStyles() { 62 | Element mainArea = shadowRoot.querySelector('.q-autocomplete-main-area'); 63 | mainArea.style.width = width; 64 | } 65 | 66 | void focusOnInput(var x) { 67 | Element field = shadowRoot.querySelector('.q-autocomplete-form-input'); 68 | field.focus(); 69 | } 70 | 71 | void updateEntriesFromDataSource() { 72 | entries.clear(); 73 | Element dataSource = this.querySelector('.data-source'); 74 | 75 | if (dataSource != null) { 76 | for (Element element in dataSource.children) { 77 | 78 | bool containsText = element.dataset.containsKey('text'); 79 | bool containsID = element.dataset.containsKey('id'); 80 | if (containsText && containsID) { 81 | entries.add(new AutocompleteEntry(element.dataset['id'], 82 | element.dataset['text'], element.clone(true))); 83 | } else if (dataSource.children.first is TemplateElement) { 84 | } else { 85 | print("Missing data-text or data-id from an source entry."); 86 | } 87 | } 88 | } else { 89 | print("Missing a data source like
Dart
"); 90 | } 91 | } 92 | 93 | void clear() { 94 | filterQuery = ''; 95 | filteredEntries.clear(); 96 | } 97 | 98 | void reset() { 99 | filterQuery = ''; 100 | updateFilteredEntries(); 101 | } 102 | 103 | 104 | void activate(Event event, var details, Node node) { 105 | event.preventDefault(); 106 | if (!isActive) { 107 | isActive = true; 108 | Element field = shadowRoot.querySelector('.q-autocomplete-activation-area'); 109 | field.style.display = 'none'; 110 | 111 | new Future(() => null).then(focusOnInput); 112 | } 113 | } 114 | 115 | void select(Event event, var details, Node node) { 116 | if (event != null) { 117 | event.preventDefault(); 118 | } 119 | var detail = {'id': activeEntry.id, 'text': activeEntry.searchableText}; 120 | dispatchEvent(new CustomEvent("select", detail: detail)); 121 | filterQuery = activeEntry.searchableText; 122 | // clear suggestions because entry has been chosen. 123 | filteredEntries.clear(); 124 | focusOnInput(null); 125 | } 126 | 127 | void blurred(Event event, var details, Node node) { 128 | _escapeHandler.removeWidget(_elementTimestamp); 129 | // the element is deactive and we give it 0 as timestamp to make sure 130 | // you can't find it by getting the max of all elements with the data attribute 131 | _elementTimestamp = 0; 132 | clear(); 133 | isActive = false; 134 | Element field = shadowRoot.querySelector('.q-autocomplete-activation-area'); 135 | field.style.display = 'block'; 136 | } 137 | 138 | void focused() { 139 | _elementTimestamp = new DateTime.now().millisecondsSinceEpoch; 140 | var deactivateFuture = _escapeHandler.addWidget(_elementTimestamp); 141 | deactivateFuture.then((_) { 142 | blurred(null, {}, null); 143 | }); 144 | updateFilteredEntries(); 145 | } 146 | 147 | void setToActiveEntry(Event event, var details, Node node) { 148 | activeEntry = filteredEntries.singleWhere((entry) => entry.id == node.dataset['entry-id']); 149 | } 150 | 151 | /** 152 | * updates the filteredEntries based on the current filterQuery and available entries 153 | */ 154 | void updateFilteredEntries() { 155 | var sanitizedQuery = filterQuery.trim().toLowerCase(); 156 | var tmpFilteredEntries = []; 157 | if (sanitizedQuery == "") { 158 | tmpFilteredEntries = new List.from(entries); 159 | } else { 160 | tmpFilteredEntries = entries.where((AutocompleteEntry entry) { 161 | return entry.searchableText.trim().toLowerCase().contains(sanitizedQuery); 162 | }); 163 | } 164 | filteredEntries.clear(); 165 | filteredEntries.addAll(tmpFilteredEntries); 166 | 167 | if (filteredEntries.isNotEmpty) { 168 | activeEntry = filteredEntries.first; 169 | } 170 | } 171 | 172 | void keyUpHandler(KeyboardEvent event) { 173 | Element input = shadowRoot.querySelector('.q-autocomplete-form-input'); 174 | var activeElement = js.context.callMethod('wrap', 175 | [document.activeElement]); 176 | 177 | if (activeElement == this || activeElement == input) { 178 | switch (new KeyEvent.wrap(event).keyCode) { 179 | case KeyCode.UP: 180 | _moveUp(); 181 | break; 182 | case KeyCode.DOWN: 183 | _moveDown(); 184 | break; 185 | case KeyCode.ENTER: 186 | select(null, null, null); 187 | break; 188 | } 189 | } 190 | } 191 | _moveUp() { 192 | var tmp = filteredEntries.reversed.skipWhile((entry) => entry != activeEntry); 193 | if (tmp.length >= 2) { 194 | activeEntry = tmp.elementAt(1); 195 | } 196 | } 197 | 198 | _moveDown() { 199 | var tmp = filteredEntries.skipWhile((entry) => entry != activeEntry); 200 | if (tmp.length >= 2) { 201 | activeEntry = tmp.elementAt(1); 202 | } 203 | } 204 | 205 | void removed() { 206 | if (this._keyUp != null) { try { this._keyUp.cancel(); } on StateError {}; } 207 | } 208 | } -------------------------------------------------------------------------------- /lib/components/button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 376 | 377 | 378 | -------------------------------------------------------------------------------- /lib/components/button/button.css: -------------------------------------------------------------------------------- 1 | :host { 2 | border: none; 3 | background:none; 4 | } 5 | 6 | /* button sizes */ 7 | 8 | 9 | /* button looks */ 10 | .btn-default { 11 | font-family: "ff-kievit-web-pro-1", "ff-kievit-web-pro-2", sans-serif; 12 | -webkit-border-radius: 2px; 13 | -moz-border-radius: 2px; 14 | border-radius: 2px; 15 | -moz-box-shadow: 0 1px 1px 0 rgba(255, 255, 255, 0.3) inset; 16 | -webkit-box-shadow: 0 1px 1px 0 rgba(255, 255, 255, 0.3) inset; 17 | box-shadow: 0 1px 1px 0 rgba(255, 255, 255, 0.3) inset; 18 | cursor: pointer; 19 | letter-spacing: 0px; 20 | height: 36px; 21 | line-height: 38px; 22 | padding: 0 15px; 23 | text-align: center; 24 | float: none; 25 | background: #f5f5f5; 26 | background-repeat: no-repeat; 27 | background-image: -khtml-gradient(linear, left top, left bottom, from(#ffffff), 28 | to(#f5f5f5)); 29 | background-image: -moz-linear-gradient(top, #ffffff, #f5f5f5); 30 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), 31 | color-stop(100%, #f5f5f5)); 32 | background-image: -webkit-linear-gradient(top, #ffffff, #ffffff 25%, #f5f5f5); 33 | background-image: -o-linear-gradient(top, #ffffff, #f5f5f5); 34 | background-image: -ms-linear-gradient(#ffffff, #f5f5f5); 35 | background-image: linear-gradient(#ffffff, #f5f5f5); 36 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffffff, 37 | endColorstr=#f5f5f5, GradientType=0); 38 | border: 1px solid #cccccc; 39 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 40 | border-bottom-color: #b3b3b3; 41 | color: #333333; 42 | text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); 43 | font-size: 16px; 44 | } 45 | .btn-default:hover { 46 | background-position: 0 -15px; 47 | } 48 | .btn-default:active { 49 | font-family: "ff-kievit-web-pro-1", "ff-kievit-web-pro-2", sans-serif; 50 | -webkit-border-radius: 2px; 51 | -moz-border-radius: 2px; 52 | border-radius: 2px; 53 | cursor: pointer; 54 | letter-spacing: 0px; 55 | height: 36px; 56 | line-height: 38px; 57 | padding: 0 15px; 58 | text-align: center; 59 | float: none; 60 | background: #f5f5f5; 61 | background-repeat: no-repeat; 62 | background-image: -khtml-gradient(linear, left top, left bottom, from(#ffffff), to(#f5f5f5)); 63 | background-image: -moz-linear-gradient(top, #ffffff, #f5f5f5); 64 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, #f5f5f5)); 65 | background-image: -webkit-linear-gradient(top, #ffffff, #ffffff 25%, #f5f5f5); 66 | background-image: -o-linear-gradient(top, #ffffff, #f5f5f5); 67 | background-image: -ms-linear-gradient(#ffffff, #f5f5f5); 68 | background-image: linear-gradient(#ffffff, #f5f5f5); 69 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffffff, endColorstr=#f5f5f5, GradientType=0); 70 | border: 1px solid #cccccc; 71 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 72 | border-bottom-color: #b3b3b3; 73 | color: #333333; 74 | text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); 75 | font-size: 16px; 76 | -webkit-box-shadow: inset 0 2px 4px rgba(0,0,0,0.25), 0 1px 2px rgba(0,0,0,0.05); 77 | -moz-box-shadow: inset 0 2px 4px rgba(0,0,0,0.25), 0 1px 2px rgba(0,0,0,0.05); 78 | box-shadow: inset 0 2px 4px rgba(0,0,0,0.25), 0 1px 2px rgba(0,0,0,0.05); 79 | position: relative; 80 | top: 1px; 81 | } 82 | .btn-default:focus { 83 | outline: 5px auto -webkit-focus-ring-color; 84 | outline-offset: -2px; 85 | } 86 | .btn-medium { 87 | font-family: "ff-kievit-web-pro-1", "ff-kievit-web-pro-2", sans-serif; 88 | -webkit-border-radius: 2px; 89 | -moz-border-radius: 2px; 90 | border-radius: 2px; 91 | -moz-box-shadow: 0 1px 1px 0 rgba(255,255,255,0.3) inset; 92 | -webkit-box-shadow: 0 1px 1px 0 rgba(255,255,255,0.3) inset; 93 | box-shadow: 0 1px 1px 0 rgba(255,255,255,0.3) inset; 94 | cursor: pointer; 95 | letter-spacing: 0px; 96 | height: 28px; 97 | line-height: 30px; 98 | padding: 0 10px; 99 | text-align: center; 100 | float: none; 101 | background: #f5f5f5; 102 | background-repeat: no-repeat; 103 | background-image: -khtml-gradient(linear, left top, left bottom, from(#ffffff), to(#f5f5f5)); 104 | background-image: -moz-linear-gradient(top, #ffffff, #f5f5f5); 105 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, #f5f5f5)); 106 | background-image: -webkit-linear-gradient(top, #ffffff, #ffffff 25%, #f5f5f5); 107 | background-image: -o-linear-gradient(top, #ffffff, #f5f5f5); 108 | background-image: -ms-linear-gradient(#ffffff, #f5f5f5); 109 | background-image: linear-gradient(#ffffff, #f5f5f5); 110 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffffff, endColorstr=#f5f5f5, GradientType=0); 111 | border: 1px solid #cccccc; 112 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 113 | border-bottom-color: #b3b3b3; 114 | color: #333333; 115 | text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); 116 | font-size: 14px; 117 | } 118 | .btn-medium:hover { 119 | background-position: 0 -15px; 120 | } 121 | .btn-medium:active { 122 | font-family: "ff-kievit-web-pro-1", "ff-kievit-web-pro-2", sans-serif; 123 | -webkit-border-radius: 2px; 124 | -moz-border-radius: 2px; 125 | border-radius: 2px; 126 | cursor: pointer; 127 | letter-spacing: 0px; 128 | height: 28px; 129 | line-height: 30px; 130 | padding: 0 10px; 131 | text-align: center; 132 | float: none; 133 | background: #f5f5f5; 134 | background-repeat: no-repeat; 135 | background-image: -khtml-gradient(linear, left top, left bottom, from(#ffffff), to(#f5f5f5)); 136 | background-image: -moz-linear-gradient(top, #ffffff, #f5f5f5); 137 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, #f5f5f5)); 138 | background-image: -webkit-linear-gradient(top, #ffffff, #ffffff 25%, #f5f5f5); 139 | background-image: -o-linear-gradient(top, #ffffff, #f5f5f5); 140 | background-image: -ms-linear-gradient(#ffffff, #f5f5f5); 141 | background-image: linear-gradient(#ffffff, #f5f5f5); 142 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffffff, endColorstr=#f5f5f5, GradientType=0); 143 | border: 1px solid #cccccc; 144 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 145 | border-bottom-color: #b3b3b3; 146 | color: #333333; 147 | text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); 148 | font-size: 14px; 149 | -webkit-box-shadow: inset 0 2px 4px rgba(0,0,0,0.25), 0 1px 2px rgba(0,0,0,0.05); 150 | -moz-box-shadow: inset 0 2px 4px rgba(0,0,0,0.25), 0 1px 2px rgba(0,0,0,0.05); 151 | box-shadow: inset 0 2px 4px rgba(0,0,0,0.25), 0 1px 2px rgba(0,0,0,0.05); 152 | position: relative; 153 | top: 1px; 154 | } 155 | .btn-medium:focus { 156 | outline: 5px auto -webkit-focus-ring-color; 157 | outline-offset: -2px; 158 | } 159 | .btn-primary { 160 | font-family: "ff-kievit-web-pro-1", "ff-kievit-web-pro-2", sans-serif; 161 | -webkit-border-radius: 2px; 162 | -moz-border-radius: 2px; 163 | border-radius: 2px; 164 | -moz-box-shadow: 0 1px 1px 0 rgba(255,255,255,0.3) inset; 165 | -webkit-box-shadow: 0 1px 1px 0 rgba(255,255,255,0.3) inset; 166 | box-shadow: 0 1px 1px 0 rgba(255,255,255,0.3) inset; 167 | cursor: pointer; 168 | letter-spacing: 0px; 169 | height: 36px; 170 | line-height: 38px; 171 | padding: 0 15px; 172 | text-align: center; 173 | float: none; 174 | background: #3c60a4; 175 | background-repeat: no-repeat; 176 | background-image: -khtml-gradient(linear, left top, left bottom, from(#446fba), to(#3c60a4)); 177 | background-image: -moz-linear-gradient(top, #446fba, #3c60a4); 178 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #446fba), color-stop(100%, #3c60a4)); 179 | background-image: -webkit-linear-gradient(top, #446fba, #446fba 25%, #3c60a4); 180 | background-image: -o-linear-gradient(top, #446fba, #3c60a4); 181 | background-image: -ms-linear-gradient(#446fba, #3c60a4); 182 | background-image: linear-gradient(#446fba, #3c60a4); 183 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#446fba, endColorstr=#3c60a4, GradientType=0); 184 | border: 1px solid #265d84; 185 | color: #fff; 186 | text-shadow: 0 -1px 0 rgba(0,0,0,0.75); 187 | font-size: 18px; 188 | } 189 | .btn-primary:hover { 190 | background: #234b95; 191 | } 192 | .btn-primary:active { 193 | font-family: "ff-kievit-web-pro-1", "ff-kievit-web-pro-2", sans-serif; 194 | -webkit-border-radius: 2px; 195 | -moz-border-radius: 2px; 196 | border-radius: 2px; 197 | cursor: pointer; 198 | letter-spacing: 0px; 199 | height: 36px; 200 | line-height: 38px; 201 | padding: 0 15px; 202 | text-align: center; 203 | float: none; 204 | background: #3c60a4; 205 | background-repeat: no-repeat; 206 | background-image: -khtml-gradient(linear, left top, left bottom, from(#446fba), to(#3c60a4)); 207 | background-image: -moz-linear-gradient(top, #446fba, #3c60a4); 208 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #446fba), color-stop(100%, #3c60a4)); 209 | background-image: -webkit-linear-gradient(top, #446fba, #446fba 25%, #3c60a4); 210 | background-image: -o-linear-gradient(top, #446fba, #3c60a4); 211 | background-image: -ms-linear-gradient(#446fba, #3c60a4); 212 | background-image: linear-gradient(#446fba, #3c60a4); 213 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#446fba, endColorstr=#3c60a4, GradientType=0); 214 | border: 1px solid #265d84; 215 | color: #fff; 216 | text-shadow: 0 -1px 0 rgba(0,0,0,0.75); 217 | font-size: 18px; 218 | -webkit-box-shadow: inset 0 2px 4px rgba(0,0,0,0.25), 0 1px 2px rgba(0,0,0,0.05); 219 | -moz-box-shadow: inset 0 2px 4px rgba(0,0,0,0.25), 0 1px 2px rgba(0,0,0,0.05); 220 | box-shadow: inset 0 2px 4px rgba(0,0,0,0.25), 0 1px 2px rgba(0,0,0,0.05); 221 | position: relative; 222 | top: 1px; 223 | } 224 | .btn-primary:focus { 225 | outline: 5px auto -webkit-focus-ring-color; 226 | outline-offset: -2px; 227 | } 228 | .btn-link { 229 | border: none; 230 | border-radius: 0; 231 | background: none; 232 | background-image: none; 233 | color: #265199; 234 | font-size: 14px; 235 | height: 26px; 236 | line-height: 21px; 237 | padding: 0; 238 | float: none; 239 | } 240 | .btn-link:hover { 241 | border: none; 242 | text-decoration: underline; 243 | color: #505050; 244 | } 245 | .btn-link:active { 246 | border: none; 247 | border-radius: 0; 248 | background: none; 249 | background-image: none; 250 | font-size: 14px; 251 | height: 26px; 252 | line-height: 21px; 253 | padding: 0; 254 | text-decoration: underline; 255 | color: #505050; 256 | -webkit-box-shadow: none; 257 | -moz-box-shadow: none; 258 | box-shadow: none; 259 | } 260 | .btn-link:focus { 261 | outline: 5px auto -webkit-focus-ring-color; 262 | outline-offset: -2px; 263 | } 264 | .btn-link.btn-small { 265 | border: none; 266 | border-radius: 0; 267 | background: none; 268 | background-image: none; 269 | color: #265199; 270 | font-size: 12px; 271 | height: 21px; 272 | line-height: 16px; 273 | padding: 0; 274 | float: none; 275 | } 276 | .btn-link.btn-small:hover { 277 | border: none; 278 | text-decoration: underline; 279 | color: #505050; 280 | } 281 | .btn-link.btn-small:active { 282 | border: none; 283 | border-radius: 0; 284 | background: none; 285 | background-image: none; 286 | font-size: 12px; 287 | height: 21px; 288 | line-height: 16px; 289 | padding: 0; 290 | text-decoration: underline; 291 | color: #505050; 292 | -webkit-box-shadow: none; 293 | -moz-box-shadow: none; 294 | box-shadow: none; 295 | } 296 | .btn-link.btn-small:focus { 297 | outline: 5px auto -webkit-focus-ring-color; 298 | outline-offset: -2px; 299 | } 300 | .btn-small { 301 | font-family: "ff-kievit-web-pro-1", "ff-kievit-web-pro-2", sans-serif; 302 | -webkit-border-radius: 2px; 303 | -moz-border-radius: 2px; 304 | border-radius: 2px; 305 | -moz-box-shadow: 0 1px 1px 0 rgba(255,255,255,0.3) inset; 306 | -webkit-box-shadow: 0 1px 1px 0 rgba(255,255,255,0.3) inset; 307 | box-shadow: 0 1px 1px 0 rgba(255,255,255,0.3) inset; 308 | cursor: pointer; 309 | letter-spacing: 0px; 310 | height: 28px; 311 | line-height: 30px; 312 | padding: 0 10px; 313 | text-align: center; 314 | float: none; 315 | background: #f5f5f5; 316 | background-repeat: no-repeat; 317 | background-image: -khtml-gradient(linear, left top, left bottom, from(#ffffff), to(#f5f5f5)); 318 | background-image: -moz-linear-gradient(top, #ffffff, #f5f5f5); 319 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, #f5f5f5)); 320 | background-image: -webkit-linear-gradient(top, #ffffff, #ffffff 25%, #f5f5f5); 321 | background-image: -o-linear-gradient(top, #ffffff, #f5f5f5); 322 | background-image: -ms-linear-gradient(#ffffff, #f5f5f5); 323 | background-image: linear-gradient(#ffffff, #f5f5f5); 324 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffffff, endColorstr=#f5f5f5, GradientType=0); 325 | border: 1px solid #cccccc; 326 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 327 | border-bottom-color: #b3b3b3; 328 | color: #333333; 329 | text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); 330 | font-size: 12px; 331 | margin-bottom: 0; 332 | } 333 | .btn-small:hover { 334 | background-position: 0 -15px; 335 | } 336 | .btn-small:active { 337 | font-family: "ff-kievit-web-pro-1", "ff-kievit-web-pro-2", sans-serif; 338 | -webkit-border-radius: 2px; 339 | -moz-border-radius: 2px; 340 | border-radius: 2px; 341 | cursor: pointer; 342 | letter-spacing: 0px; 343 | height: 28px; 344 | line-height: 30px; 345 | padding: 0 10px; 346 | text-align: center; 347 | float: none; 348 | background: #f5f5f5; 349 | background-repeat: no-repeat; 350 | background-image: -khtml-gradient(linear, left top, left bottom, from(#ffffff), to(#f5f5f5)); 351 | background-image: -moz-linear-gradient(top, #ffffff, #f5f5f5); 352 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, #f5f5f5)); 353 | background-image: -webkit-linear-gradient(top, #ffffff, #ffffff 25%, #f5f5f5); 354 | background-image: -o-linear-gradient(top, #ffffff, #f5f5f5); 355 | background-image: -ms-linear-gradient(#ffffff, #f5f5f5); 356 | background-image: linear-gradient(#ffffff, #f5f5f5); 357 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffffff, endColorstr=#f5f5f5, GradientType=0); 358 | border: 1px solid #cccccc; 359 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 360 | border-bottom-color: #b3b3b3; 361 | color: #333333; 362 | text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); 363 | -webkit-box-shadow: inset 0 2px 4px rgba(0,0,0,0.25), 0 1px 2px rgba(0,0,0,0.05); 364 | -moz-box-shadow: inset 0 2px 4px rgba(0,0,0,0.25), 0 1px 2px rgba(0,0,0,0.05); 365 | box-shadow: inset 0 2px 4px rgba(0,0,0,0.25), 0 1px 2px rgba(0,0,0,0.05); 366 | position: relative; 367 | top: 1px; 368 | font-size: 12px; 369 | } 370 | .btn-small:focus { 371 | outline: 5px auto -webkit-focus-ring-color; 372 | outline-offset: -2px; 373 | } 374 | 375 | polyfill-unscoped-rule { 376 | content: '.btn-default'; 377 | font-family: "ff-kievit-web-pro-1", "ff-kievit-web-pro-2", sans-serif; 378 | -webkit-border-radius: 2px; 379 | -moz-border-radius: 2px; 380 | border-radius: 2px; 381 | -moz-box-shadow: 0 1px 1px 0 rgba(255, 255, 255, 0.3) inset; 382 | -webkit-box-shadow: 0 1px 1px 0 rgba(255, 255, 255, 0.3) inset; 383 | box-shadow: 0 1px 1px 0 rgba(255, 255, 255, 0.3) inset; 384 | cursor: pointer; 385 | letter-spacing: 0px; 386 | height: 36px; 387 | line-height: 38px; 388 | padding: 0 15px; 389 | text-align: center; 390 | float: none; 391 | background: #f5f5f5; 392 | background-repeat: no-repeat; 393 | background-image: -khtml-gradient(linear, left top, left bottom, from(#ffffff), 394 | to(#f5f5f5)); 395 | background-image: -moz-linear-gradient(top, #ffffff, #f5f5f5); 396 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), 397 | color-stop(100%, #f5f5f5)); 398 | background-image: -webkit-linear-gradient(top, #ffffff, #ffffff 25%, #f5f5f5); 399 | background-image: -o-linear-gradient(top, #ffffff, #f5f5f5); 400 | background-image: -ms-linear-gradient(#ffffff, #f5f5f5); 401 | background-image: linear-gradient(#ffffff, #f5f5f5); 402 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#ffffff, 403 | endColorstr=#f5f5f5, GradientType=0); 404 | border: 1px solid #cccccc; 405 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 406 | border-bottom-color: #b3b3b3; 407 | color: #333333; 408 | text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); 409 | font-size: 16px; 410 | } --------------------------------------------------------------------------------