├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bin ├── README.md ├── ace_src_files.txt ├── ice-appcache.sh ├── ice-compile.sh └── ice-de-symlink-packages.sh ├── ice_code_editor.png ├── lib ├── css │ └── ice.css ├── editor.dart ├── fonts │ └── inconsolata.woff ├── full.dart ├── full │ ├── copy_dialog.dart │ ├── default_project.dart │ ├── dialog.dart │ ├── download_dialog.dart │ ├── export_dialog.dart │ ├── help_action.dart │ ├── image_list_dialog.dart │ ├── image_upload_dialog.dart │ ├── import_dialog.dart │ ├── menu_action.dart │ ├── menu_item.dart │ ├── new_project_dialog.dart │ ├── notify.dart │ ├── open_dialog.dart │ ├── remove_dialog.dart │ ├── rename_dialog.dart │ ├── save_action.dart │ ├── share_dialog.dart │ ├── snapshotter.dart │ ├── templates.dart │ ├── validate.dart │ └── whats_new_action.dart ├── gzip.dart ├── html │ ├── code-OFFLINE.html │ └── code.html ├── ice.dart ├── images │ └── clipboard.png ├── js │ ├── ace │ │ └── worker-javascript.js │ └── deflate │ │ ├── rawdeflate.js │ │ └── rawinflate.js ├── polymer │ ├── ice_code_editor.html │ └── ice_code_editor_element.dart ├── settings.dart └── store.dart ├── pubspec.yaml ├── test ├── editor_test.dart ├── embed.html ├── full │ ├── copy_dialog_test.dart │ ├── download_test.dart │ ├── export_test.dart │ ├── hide_button_test.dart │ ├── image_upload_test.dart │ ├── import_test.dart │ ├── keyboard_shortcuts_test.dart │ ├── new_project_dialog_test.dart │ ├── notification_test.dart │ ├── open_dialog_test.dart │ ├── remove_dialog_test.dart │ ├── rename_dialog_test.dart │ ├── save_dialog_test.dart │ ├── share_dialog_test.dart │ ├── show_button_test.dart │ ├── snapshotter_test.dart │ ├── update_button_test.dart │ └── whats_new_test.dart ├── full_test.dart ├── gzip_test.dart ├── helpers.dart ├── html5.html ├── html5_test.dart ├── ice_test.dart ├── polymer │ ├── embed_bar.html │ ├── embed_baz.html │ ├── embed_foo.html │ ├── index.html │ └── test.dart ├── settings_test.dart └── store_test.dart ├── tool └── dartdoc └── web ├── embed_a.html ├── embed_b.html ├── embed_c.html ├── full.dart ├── full.html ├── index.html ├── main.dart └── polymer.html /.gitignore: -------------------------------------------------------------------------------- 1 | packages 2 | pubspec.lock 3 | out 4 | *.js.deps 5 | *.js.map 6 | *.dart.js 7 | docs 8 | .DS_Store 9 | build 10 | .pub 11 | .packages 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: dart 2 | dart_task: 3 | - dartanalyzer: --fatal-warnings lib/ice.dart 4 | - test: --platform dartium test/ice_test.dart 5 | install_dartium: true 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the MIT License. 2 | 3 | Copyright Chris Strom, 2013. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ICE Code Editor 2 | 3 | [![Build Status](https://travis-ci.org/eee-c/ice-code-editor.svg?branch=master)](https://travis-ci.org/eee-c/ice-code-editor) 4 | 5 | __*** [Try it now!](https://code3dgames.com/3de/) ***__ 6 | 7 | The Code Editor + Visualization Preview used in the book “[3D Game Programming for Kids](https://code3dgames.com).” Written in **[Dart](http://dartlang.org)**. 8 | 9 | ![ICE Code Editor Screenshot](https://raw.github.com/eee-c/ice-code-editor/master/ice_code_editor.png) 10 | 11 | The old [JavaScript version](https://github.com/eee-c/code-editor) proved unmaintainable, hence the switch to **Dart**. This version leverages many of the benefits of **Dart**: cross-browser support, testing, documentation. 12 | 13 | ## Running the Example App 14 | 15 | You'll need **[Dart](http://dartlang.org)** installed. To run the examples: 16 | 17 | 1. Install dependencies with `pub install` 18 | 2. Start the [pub](http://pub.dartlang.org) web server with `pub serve` 19 | 3. Open the full-screen version of ICE at http://localhost:8080/full.html with Dartium 20 | 21 | Examples are contained in the `web` directory. 22 | 23 | ## Features 24 | 25 | * Update button 26 | * Hide Code button 27 | * Main Menu button 28 | * Open 29 | * New 30 | * Make a Copy 31 | * Save 32 | * Rename 33 | * Share 34 | * Download 35 | * Remove 36 | * Help 37 | 38 | ## Build 39 | 40 | Because ICE relies on [js-interop](http://dart-lang.github.io/js-interop/docs/js.html), not just `dart:js`, the build process requires that it is always built for development release (even in production): 41 | 42 | ````sh 43 | $ pub build --mode=development 44 | ```` 45 | 46 | ## Core Collaborators 47 | 48 | * [Srdjan Pejic](http://batasrki.github.io/) 49 | * [James Hurford](https://github.com/terrasea) 50 | * [Kashyap Kondamudi](https://github.com/kgrz) 51 | 52 | ## Emeritus Collaborators 53 | 54 | * [Santiago Arias](https://github.com/santiaago) 55 | * [Timothy King](https://github.com/lordzork) 56 | 57 | ## Contributors 58 | 59 | * [Sangeet Agarwal](https://github.com/SangeetAgarwal) 60 | * [Robert Åkerblom-Andersson](https://github.com/scorpiion) 61 | * [Luke Barbuto](https://github.com/lexun) 62 | * [Kate Bladow](https://github.com/kbladow) 63 | * [Stephen Cagle](https://github.com/samedhi) 64 | * [Alex Chacon](https://github.com/alexgchacon) 65 | * [Joe Curtis](http://github.com/toklok) 66 | * [Jon Davison](https://github.com/jcdavison) 67 | * [Damon Douglas](https://github.com/damondouglas) 68 | * [Ashok Dudhade](https://github.com/ashokdudhade) 69 | * [William Estoque](https://github.com/westoque) 70 | * [Daniel Gempesaw](https://github.com/gempesaw) 71 | * [Abtin Ghods](https://github.com/abetss) 72 | * [Simone Giacomelli](https://github.com/simonegiacomelli) 73 | * [Richard Gould](https://github.com/rgould) 74 | * [Nik Graf](https://github.com/nikgraf) 75 | * [Marty Hines](https://github.com/martyhines) 76 | * [Erik Isaksen](https://github.com/nevraeka) 77 | * [Jonathan Kaye](https://github.com/jonkaye) 78 | * [Colin Kennedy](https://github.com/cmkcmk) 79 | * [Jon Kirkman](https://github.com/jonkirkman) 80 | * [Anita Kuno](https://github.com/anteaya) 81 | * [Lindsey Miller](https://github.com/tech-bluenette) 82 | * [Morgan Nelson](https://github.com/korishev) 83 | * [Michael Reynolds](https://github.com/mr170) 84 | * [Michael Risse](https://github.com/rissem) 85 | * [Chris Sciolla](https://github.com/chrisski) 86 | * [Christian Smith](https://github.com/christiansmith) 87 | * [Paul Spain](https://github.com/pvspain) 88 | * [Alper Sunar](https://github.com/asunar) 89 | * [Dmitriy Vasilyev](https://github.com/kelegorm) 90 | * Stefan Dausend-Werner 91 | 92 | ## Want to Help? 93 | 94 | [![#pairwithme](http://www.pairprogramwith.me/badge.png)](https://calendar.google.com/calendar/selfsched?sstoken=UUNwdmNwR09IRm4wfGRlZmF1bHR8NmVjZjU2MGY0MzU4MTBlMjFkZTE0ZDgzYjdkMGU4ZjM) 95 | 96 | Chris ([twitter](https://twitter.com/eee_c) / [blog](http://japhr.blogspot.com/)) runs nightly (1030pm EDT / 0230 UTC) pairing sessions. [Sign up for free](https://www.google.com/calendar/selfsched?sstoken=UUNwdmNwR09IRm4wfGRlZmF1bHR8NmVjZjU2MGY0MzU4MTBlMjFkZTE0ZDgzYjdkMGU4ZjM) to help out and learn some [Dart](http://dartlang.org)! _Absolutely no experience required. Really :)_ 97 | -------------------------------------------------------------------------------- /bin/README.md: -------------------------------------------------------------------------------- 1 | # Build Scripts 2 | 3 | Scripts are run in the following order: 4 | 5 | ``` 6 | ~/repos/ice-code-editor/bin/ice-de-symlink-packages.sh 7 | ~/repos/ice-code-editor/bin/ice-appcache.sh 8 | ~/repos/ice-code-editor/bin/ice-compile.sh main.dart 9 | ``` 10 | 11 | 12 | To use, the scripts expect the following directory structure (used on code3dgames.com): 13 | 14 | ``` 15 | appcache.js 16 | index.html 17 | main.dart 18 | pubspec.yaml 19 | ``` 20 | 21 | Where appcache.js is the standard Application Cache JavaScript file. 22 | 23 | Index.html is: 24 | 25 | ```html 26 | 27 | 28 | 29 | code editor 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ``` 38 | 39 | main.dart is: 40 | 41 | ```dart 42 | library main; 43 | 44 | import 'package:ice_code_editor/ice.dart' as ICE; 45 | 46 | main()=> new ICE.Full(); 47 | ``` 48 | 49 | And pubspec.yaml is: 50 | 51 | ```yaml 52 | name: full_screen_ice 53 | version: 0.0.1 54 | description: Full screen ICE 55 | dependencies: 56 | ice_code_editor: any 57 | ``` 58 | -------------------------------------------------------------------------------- /bin/ace_src_files.txt: -------------------------------------------------------------------------------- 1 | ace/src/js/mode-css.js$ 2 | ace/src/js/ace.js$ 3 | ace/src/js/theme-textmate.js$ 4 | ace/src/js/theme-chrome.js$ 5 | ace/src/js/ext-searchbox.js$ 6 | ace/src/js/mode-html.js$ 7 | ace/src/js/mode-javascript.js$ 8 | ace/src/js/keybinding-emacs.js$ 9 | -------------------------------------------------------------------------------- /bin/ice-appcache.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Generates an application cache manifest file suitable for 4 | # deployment. The EXTERNAL_LIBS shown are specific to the code3dgames.com 5 | # site, but can be tweaked for deployments to other sites. 6 | 7 | # TODO: investigate excluding main.dart.precompiled.js from manifest 8 | 9 | # Name of the manifest file: 10 | APPCACHE=editor.appcache 11 | 12 | # Change this list to suit different deployment needs: 13 | read -d '' EXTERNAL_LIBS < $APPCACHE 38 | date +'# %Y-%m-%d %H:%M:%S' >> $APPCACHE 39 | echo >> $APPCACHE 40 | 41 | ## 42 | # Cache 43 | 44 | echo 'CACHE:' >> $APPCACHE 45 | 46 | # Fixed files used within the editor for creating Three.js worlds: 47 | echo "$LIST_FIRST" >> $APPCACHE 48 | echo >> $APPCACHE 49 | echo "$EXTERNAL_LIBS" >> $APPCACHE 50 | echo >> $APPCACHE 51 | 52 | # Dart and JS code used to make the editor: 53 | find | \ 54 | grep -e \\.js$ -e \\.map$ -e \\.html$ -e \\.css$ | \ 55 | grep -v -e unittest -e /lib/ -e polymer -e web_components -e js/deflate | \ 56 | grep -v packages/ace/src | \ 57 | sed 's/^\.\///' \ 58 | >> $APPCACHE 59 | 60 | find packages/ace/src | \ 61 | grep -f ~/repos/ice-code-editor/bin/ace_src_files.txt | \ 62 | sed 's/^\.\///' \ 63 | >> $APPCACHE 64 | 65 | 66 | ## 67 | # Network 68 | cat <> $APPCACHE 69 | 70 | NETWORK: 71 | * 72 | EOF 73 | -------------------------------------------------------------------------------- /bin/ice-compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | dart2js -m -o main.dart.js $1 4 | -------------------------------------------------------------------------------- /bin/ice-de-symlink-packages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # For deployment targets such as GitHub pages, this script replaces 4 | # the symbolic links in Dart's packages directory with the actual 5 | # contents of those libraries. 6 | 7 | cd packages 8 | for file in *; do 9 | if [ ! -L $file ]; then 10 | echo "Symlinked packages already replaced." 11 | exit 1 12 | fi 13 | 14 | link=`readlink $file` 15 | echo "$file ($link)" 16 | rm $file 17 | cp -r $link $file 18 | done 19 | -------------------------------------------------------------------------------- /ice_code_editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eee-c/ice-code-editor/9fc0e4d254a33e3da48527f9e2ccfbe17e6e45aa/ice_code_editor.png -------------------------------------------------------------------------------- /lib/css/ice.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'inconsolata'; 3 | src: url('../fonts/inconsolata.woff') format('woff'); 4 | font-weight: normal; 5 | font-style: normal; 6 | } 7 | 8 | .ice-code-editor-editor .ace_content { 9 | /* font-family: inconsolata; */ 10 | /* Don't set font-size here. Use ACE setFontSize() method to get proper */ 11 | /* line spacing */ 12 | } 13 | 14 | .ice-code-editor-editor, .ice-code-editor-preview, 15 | .ice-code-editor-editor.ace_editor { 16 | margin: 0; 17 | top: 0; 18 | bottom: 0; 19 | left: 0; 20 | right: 0; 21 | } 22 | 23 | .ice-code-editor-editor, 24 | .ice-code-editor-editor.ace-chrome .ace_scroller, 25 | .ice-code-editor-editor.ace-tm .ace_scroller { 26 | background-color: rgba(255,255,255,0.0); 27 | } 28 | 29 | .ace_content { 30 | background: rgba(255,255,255,0.75); 31 | } 32 | 33 | .ace-chrome .ace_content .ace_marker-layer .ace_selection { 34 | background: rgba(0,0,255,0.1); 35 | } 36 | 37 | button { 38 | font-family: sans-serif; 39 | font-size: 10px; 40 | line-height: 12px; 41 | text-transform: uppercase; 42 | text-decoration: none; 43 | color: rgba(0,0,0,0.5); 44 | border: 1px solid rgba(0,0,0,0.25); 45 | border-radius: 2px; 46 | background-color: rgba(255,255,255,0.5); 47 | margin: 2px; 48 | padding: 8px 10px; 49 | cursor: pointer; 50 | } 51 | 52 | button:hover { 53 | background-color: rgba(0,0,0,0.66); 54 | color: white; 55 | } 56 | 57 | button input { 58 | margin: -10px 4px; 59 | } 60 | 61 | .ice-menu, .ice-dialog { 62 | margin: 2px; 63 | padding: 5px; 64 | border: 1px solid rgba(0,0,0,0.25); 65 | border-radius: 2px; 66 | font-family: Arial; 67 | font-size: 12px; 68 | text-transform: uppercase; 69 | text-decoration: none; 70 | color: rgba(0,0,0,0.5); 71 | background-color: rgba(255,255,255,0.5); 72 | 73 | position: absolute; 74 | margin: 2px; 75 | right: 20px; 76 | top: 45px; 77 | z-index: 999; 78 | } 79 | 80 | div.ice-menu h1 { 81 | display: none; 82 | } 83 | 84 | div.ice-menu ul { 85 | margin: 2px; 86 | padding: 5px; 87 | text-transform: none; 88 | font-size: 13px; 89 | } 90 | 91 | .ice-menu li { 92 | display: block; 93 | padding: 4px; 94 | cursor: pointer; 95 | } 96 | 97 | .ice-menu li.empty { 98 | display: block; 99 | padding: 0px; 100 | cursor: auto; 101 | } 102 | 103 | .ice-menu li:hover, .ice-menu li:focus { 104 | color: white; 105 | background-color: rgba(0,0,0,0.66); 106 | } 107 | 108 | .ice-menu li.empty:hover, .ice-menu li.empty:focus { 109 | color: rgba(0,0,0,0.5); 110 | background-color: white; 111 | } 112 | 113 | .ice-menu a { 114 | text-decoration: none; 115 | color: rgba(0,0,0,0.5); 116 | } 117 | 118 | .ice-menu li:hover a, .ice-menu li:focus a { 119 | color: white; 120 | } 121 | 122 | .ice-menu li.highlighted { 123 | font-weight: bold; 124 | color: red; 125 | } 126 | 127 | .ice-menu li:hover.highlighted { 128 | color: white; 129 | background-color: red; 130 | } 131 | 132 | .ice-dialog h1 { 133 | margin: 0px 0px 5px; 134 | font-size: 100%; 135 | } 136 | 137 | .ice-menu .instructions { 138 | text-transform: none; 139 | padding: 0px; 140 | } 141 | 142 | .ice-dialog .instructions { 143 | padding-top: 0.25em; 144 | float: right; 145 | text-transform: lowercase; 146 | } 147 | 148 | #info { 149 | position: absolute; 150 | top: 10px; 151 | opacity: 0.8; 152 | border-radius: 4px; 153 | z-index: 999; 154 | background-color: black; 155 | color: white; 156 | padding: 10px; 157 | font-family: sans-serif; 158 | left: 50%; 159 | -webkit-transform: translateX(-50%); 160 | transform: translateX(-50%); 161 | } 162 | 163 | img.clipboard { 164 | height: 17px; 165 | vertical-align: bottom; 166 | padding-left: 3px; 167 | } 168 | -------------------------------------------------------------------------------- /lib/editor.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class Editor { 4 | bool autoupdate = true; 5 | bool _edit_only = false; 6 | bool _read_only = false; 7 | 8 | var _el; 9 | Element __el, _editor_el, _preview_el; 10 | 11 | var _ace; 12 | Completer _waitForAce; 13 | 14 | static bool disableJavaScriptWorker = false; 15 | 16 | Editor(this._el, {preview_el}) { 17 | if (preview_el != null) { 18 | this._preview_el = preview_el 19 | ..classes.add('ice-code-editor-preview'); 20 | } 21 | this._startAce(); 22 | this.applyStyles(); 23 | } 24 | 25 | set content(String data) { 26 | if (!_waitForAce.isCompleted) { 27 | editorReady.then((_) => this.content = data); 28 | return; 29 | } 30 | 31 | var original_autoupdate = autoupdate; 32 | autoupdate = false; 33 | _ace.value = data; 34 | _ace.focus(); 35 | updatePreview(); 36 | 37 | var subscribe; 38 | subscribe = onPreviewChange.listen((_) { 39 | this.autoupdate = original_autoupdate; 40 | subscribe.cancel(); 41 | }); 42 | } 43 | 44 | Timer _update_timer; 45 | void delayedUpdatePreview() { 46 | if (!this.autoupdate) return; 47 | if (_update_timer != null) _update_timer.cancel(); 48 | 49 | var wait = new Duration(seconds: 2); 50 | _update_timer = new Timer(wait, (){ 51 | this.updatePreview(); 52 | _update_timer = null; 53 | }); 54 | } 55 | 56 | void _extendDelayedUpdatePreview() { 57 | if (_update_timer == null) return; 58 | delayedUpdatePreview(); 59 | } 60 | 61 | bool get edit_only => _edit_only; 62 | void set edit_only(v) { 63 | _edit_only = v; 64 | if (v) removePreview(); 65 | } 66 | 67 | bool get read_only => _read_only; 68 | void set read_only(v) { 69 | _read_only = v; 70 | if (v) { 71 | _ace.readOnly = true; 72 | } 73 | } 74 | 75 | // worry about waitForAce? 76 | String get content => _ace.value; 77 | Future get editorReady => _waitForAce.future; 78 | 79 | int get lineNumber => _ace.lineNumber; 80 | set lineNumber(int v) { _ace.lineNumber = v; } 81 | String get lineContent => _ace.lineContent; 82 | 83 | /// Update the preview layer with the current contents of the editor 84 | /// layer. 85 | updatePreview() { 86 | if (this.edit_only) return; 87 | 88 | this.removePreview(); 89 | 90 | var iframe = this.createPreviewIframe(); 91 | iframe.onLoad.first.then((_) { 92 | if (iframe.contentWindow == null) return; 93 | 94 | iframe 95 | ..height = "${this.preview_el.clientHeight}"; 96 | 97 | var url = new RegExp(r'^file://').hasMatch(window.location.href) 98 | ? '*': window.location.href; 99 | iframe.contentWindow.postMessage(_ace.value, url); 100 | 101 | _previewChangeController.add(true); 102 | }); 103 | } 104 | 105 | removePreview() { 106 | while (this.preview_el.children.length > 0) { 107 | this.preview_el.children.first.remove(); 108 | } 109 | } 110 | 111 | createPreviewIframe() { 112 | var src = "code${window.navigator.onLine ? '' : '-OFFLINE'}"; 113 | 114 | var iframe = new IFrameElement(); 115 | iframe 116 | ..width = "${this.preview_el.clientWidth}" 117 | ..height = "${this.preview_el.clientHeight}" 118 | ..style.border = '0' 119 | ..src = 'packages/ice_code_editor/html/${src}.html'; 120 | 121 | this.preview_el.children.add( iframe ); 122 | 123 | return iframe; 124 | } 125 | 126 | Stream get onChange => _ace.session.onChange; 127 | Stream get onPreviewChange => 128 | _previewChangeController.stream.asBroadcastStream(); 129 | 130 | StreamController __previewChangeController; 131 | StreamController get _previewChangeController { 132 | if (__previewChangeController != null) return __previewChangeController; 133 | return __previewChangeController = new StreamController.broadcast(); 134 | } 135 | 136 | /// Show the code layer, calling the ACE resize methods to ensure that 137 | /// the display is correct. 138 | // worry about waitForAce? 139 | showCode() { 140 | editor_el.style.visibility = 'visible'; 141 | querySelectorAll('.ace_print-margin').forEach((e) { e.style.visibility = 'visible'; }); 142 | 143 | _ace.resize(); 144 | focus(); 145 | } 146 | 147 | /// Hide the code layer 148 | hideCode() { 149 | editor_el.style.visibility = 'hidden'; 150 | querySelector('.ace_print-margin').style.visibility = 'hidden'; 151 | 152 | if (this.edit_only) return; 153 | focus(); 154 | } 155 | 156 | focus() { 157 | if (isCodeVisible) { 158 | _ace.focus(); 159 | } 160 | else { 161 | preview_el.children.first.focus(); 162 | } 163 | } 164 | 165 | bool get isCodeVisible=> editor_el.style.visibility != 'hidden'; 166 | 167 | Element get el { 168 | if (__el != null) return __el; 169 | 170 | if (this._el.runtimeType.toString().contains('Element')) { 171 | __el = _el; 172 | } 173 | else { 174 | __el = document.querySelector(_el); 175 | } 176 | return __el; 177 | } 178 | 179 | Element get editor_el { 180 | if (_editor_el != null) return _editor_el; 181 | 182 | _editor_el = new DivElement() 183 | ..classes.add('ice-code-editor-editor'); 184 | this.el.children.add(_editor_el); 185 | return _editor_el; 186 | } 187 | 188 | Element get preview_el { 189 | if (_preview_el != null) return _preview_el; 190 | 191 | _preview_el = new DivElement() 192 | ..classes.add('ice-code-editor-preview'); 193 | 194 | if (!this.edit_only) { 195 | this.el.append(_preview_el); 196 | } 197 | 198 | return _preview_el; 199 | } 200 | 201 | static List _scripts; 202 | static bool get _isAceJsAttached => (_scripts != null); 203 | static _attachScripts() { 204 | if (_scripts != null) return []; 205 | 206 | var script_paths = [ 207 | "packages/ace/src/js/ace.js", 208 | "packages/ace/src/js/keybinding-emacs.js", 209 | "packages/ice_code_editor/js/deflate/rawdeflate.js", 210 | "packages/ice_code_editor/js/deflate/rawinflate.js" 211 | ]; 212 | 213 | var scripts = script_paths. 214 | map((path) { 215 | var script = new ScriptElement() 216 | ..async = false 217 | ..src = path; 218 | document.head.nodes.add(script); 219 | return script; 220 | }). 221 | toList(); 222 | 223 | return _scripts = scripts; 224 | } 225 | 226 | static Completer _waitForJS; 227 | static Future get jsReady { 228 | if (!_isAceJsAttached) { 229 | _waitForJS = new Completer(); 230 | _attachScripts(). 231 | first. 232 | onLoad. 233 | listen((_)=> _waitForJS.complete()); 234 | } 235 | 236 | return _waitForJS.future; 237 | } 238 | 239 | _startAce() { 240 | this._waitForAce = new Completer(); 241 | jsReady.then((_)=> _startJsAce()); 242 | _attachKeyHandlersForAce(); 243 | } 244 | 245 | _startJsAce() { 246 | 247 | _ace = Ace.edit(editor_el); 248 | 249 | _ace 250 | ..theme = "ace/theme/chrome" 251 | ..fontSize = '18px' 252 | ..printMarginColumn = false 253 | ..displayIndentGuides = false; 254 | 255 | if (!disableJavaScriptWorker) { 256 | _ace.session 257 | ..mode = "ace/mode/javascript" 258 | ..useWrapMode = true 259 | ..useSoftTabs = true 260 | ..tabSize = 2; 261 | } 262 | 263 | _ace.session.onChange.listen((e)=> this.delayedUpdatePreview()); 264 | 265 | _waitForAce.complete(); 266 | } 267 | 268 | _attachKeyHandlersForAce() { 269 | // Using keyup b/c ACE swallows keydown events 270 | document.onKeyUp.listen((event) { 271 | // only handling arrow keys 272 | if (event.keyCode < 37) return; 273 | if (event.keyCode > 40) return; 274 | _extendDelayedUpdatePreview(); 275 | }); 276 | } 277 | 278 | applyStyles() { 279 | var style = new LinkElement() 280 | ..type = "text/css" 281 | ..rel = "stylesheet" 282 | ..href = "packages/ice_code_editor/css/ice.css"; 283 | document.head.nodes.add(style); 284 | 285 | this.el.style 286 | ..position = 'relative'; 287 | 288 | this.editor_el.style 289 | ..position = 'absolute' 290 | ..zIndex = '20'; 291 | 292 | var offset = this.el.documentOffset; 293 | this.preview_el.style 294 | ..position = 'absolute' 295 | ..width = this.el.style.width 296 | ..height = this.el.style.height 297 | ..top = '${offset.y}' 298 | ..left = '${offset.x}' 299 | ..zIndex = '10'; 300 | } 301 | } 302 | 303 | class Ace { 304 | // This line won't work in Dartium because of a Polymer.dart bug 305 | static Ace edit(Element el) => 306 | new Ace(js.context['ace'].callMethod('edit', [el])); 307 | 308 | var jsAce; 309 | 310 | Ace(this.jsAce) { 311 | js.context['ace']['config']. 312 | callMethod('set', ["workerPath", "packages/ice_code_editor/js/ace"]); 313 | jsAce[r'$blockScrolling'] = double.INFINITY; 314 | } 315 | 316 | set fontSize(String size) => 317 | jsAce.callMethod('setFontSize', [size]); 318 | set theme(String theme) => 319 | jsAce.callMethod('setTheme', [theme]); 320 | set printMarginColumn(bool b) => 321 | jsAce.callMethod('setPrintMarginColumn', [b]); 322 | set displayIndentGuides(bool b) => 323 | jsAce.callMethod('setDisplayIndentGuides', [b]); 324 | set readOnly(bool b) => 325 | jsAce.callMethod('setReadOnly', [b]); 326 | 327 | String get value => jsAce.callMethod('getValue'); 328 | set value(String content) { 329 | jsAce.callMethod('setValue', [content, -1]); 330 | 331 | var UndoManager = js.context['ace'].callMethod('require', ['ace/undomanager'])['UndoManager']; 332 | session.undoManager = new js.JsObject(UndoManager); 333 | } 334 | 335 | void focus() => jsAce.callMethod('focus'); 336 | 337 | // This is way crazy, but... getLine() and getCursorPosition() are zero 338 | // indexed while gotoLine() and scrollToLine() are 1 indexed o_O 339 | String get lineContent => session.getLine(lineNumber - 1); 340 | 341 | int get lineNumber => jsAce.callMethod('getCursorPosition')['row'] + 1; 342 | 343 | set lineNumber(int row) { 344 | jsAce.callMethod('gotoLine', [row, 0, false]); 345 | jsAce.callMethod('scrollToLine', [row-1, false, false]); 346 | } 347 | 348 | get renderer => jsAce['renderer']; 349 | 350 | int resize() => renderer.callMethod('onResize'); 351 | 352 | var _session; 353 | AceSession get session { 354 | if (_session != null) return _session; 355 | return _session = new AceSession(jsAce.callMethod('getSession')); 356 | } 357 | 358 | void toggleEmacs() { 359 | if (jsAce.callMethod('getKeyboardHandler') == commandManager) { 360 | jsAce.callMethod('setKeyboardHandler', [emacsManager]); 361 | } 362 | else { 363 | jsAce.callMethod('setKeyboardHandler', [commandManager]); 364 | } 365 | } 366 | 367 | var _commandManager; 368 | get commandManager { 369 | if (_commandManager != null) return _commandManager; 370 | _commandManager = jsAce.callMethod('getKeyboardHandler'); 371 | return _commandManager; 372 | } 373 | 374 | var _emacsManager; 375 | get emacsManager { 376 | if (_emacsManager != null) return _emacsManager; 377 | _emacsManager = js.context['ace'].callMethod('require', ["ace/keyboard/emacs"])['handler']; 378 | return _emacsManager; 379 | } 380 | } 381 | 382 | class AceSession { 383 | var jsSession; 384 | AceSession(this.jsSession); 385 | 386 | set mode(String m) => jsSession.callMethod('setMode', [m]); 387 | set useWrapMode(bool b) { 388 | jsSession.callMethod('setUseWrapMode', [b]); 389 | } 390 | set useSoftTabs(bool b) => jsSession.callMethod('setUseSoftTabs', [b]); 391 | set tabSize(int size) => jsSession.callMethod('setTabSize', [size]); 392 | 393 | get undoManager => jsSession.callMethod('getUndoManager', [false]); 394 | set undoManager(u) { 395 | jsSession.callMethod('setUndoManager', [u]); 396 | } 397 | 398 | void insertAt(String content, int row, int col) { 399 | var pos = jsSession.callMethod('screenToDocumentPosition', [row, col]); 400 | jsSession.callMethod('insert', [pos, content]); 401 | } 402 | 403 | String getLine(int row) => jsSession.callMethod('getLine', [row]); 404 | 405 | StreamController _onChange; 406 | get onChange { 407 | if (_onChange != null) return _onChange.stream; 408 | 409 | _onChange = new StreamController.broadcast(); 410 | 411 | jsSession.callMethod('on', [ 412 | 'change', 413 | (e,a){ _onChange.add(e); } 414 | ]); 415 | 416 | return _onChange.stream; 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /lib/fonts/inconsolata.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eee-c/ice-code-editor/9fc0e4d254a33e3da48527f9e2ccfbe17e6e45aa/lib/fonts/inconsolata.woff -------------------------------------------------------------------------------- /lib/full.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class Full { 4 | Element el; 5 | Editor ice; 6 | Store store; 7 | Snapshotter snapshotter; 8 | Settings settings; 9 | 10 | String mode; 11 | String compressedContent; 12 | 13 | Full({mode, this.compressedContent:''}) { 14 | el = new Element.html('
'); 15 | document.body.nodes.add(el); 16 | 17 | ice = new Editor('#ice'); 18 | store = new Store(); 19 | settings = new Settings(); 20 | this.mode = mode?.replaceFirst(new RegExp(r'^\?'), ''); 21 | 22 | _attachKeyboardHandlers(); 23 | _attachMouseHandlers(); 24 | _attachMessageHandlers(); 25 | _attachDropHandlers(); 26 | 27 | _fullScreenStyles(); 28 | 29 | editorReady 30 | ..then((_)=> _startSnapshotter()) 31 | ..then((_)=> _attachCodeToolbar()) 32 | ..then((_)=> _attachPreviewToolbar()) 33 | ..then((_)=> _startAutoSave()) 34 | ..then((_)=> _openProject()) 35 | ..then((_)=> _initializeSettingForFirstUse()) 36 | ..then((_)=> _initializeMode()) 37 | ..then((_)=> _applyStyles()); 38 | } 39 | 40 | Stream get onPreviewChange => ice.onPreviewChange; 41 | Future get editorReady => Future.wait([ice.editorReady, Gzip._ready]); 42 | String get content => ice.content; 43 | void set content(data) { ice.content = data; } 44 | 45 | String get lineContent => ice.lineContent; 46 | int get lineNumber => ice.lineNumber; 47 | set lineNumber(int v) { ice.lineNumber = v; } 48 | 49 | void remove() { 50 | Keys.cancel(); 51 | el.remove(); 52 | } 53 | 54 | _attachCodeToolbar() { 55 | var toolbar = new Element.html('
'); 56 | toolbar.style 57 | ..position = 'absolute' 58 | ..top = '10px' 59 | ..right = '20px' 60 | ..zIndex = '89'; // below ACE's searchbox 61 | 62 | toolbar.children 63 | ..add(_updateButton) 64 | ..add(_leaveSnapshotModeButton) 65 | ..add(_hideCodeButton) 66 | ..add(_mainMenuButton); 67 | 68 | editor_el.children.add(toolbar); 69 | 70 | if (!_isFirstUse) _drawWhatsNewIndicator(toolbar); 71 | } 72 | 73 | _attachPreviewToolbar() { 74 | var toolbar = new Element.html('
'); 75 | toolbar.style 76 | ..position = 'absolute' 77 | ..top = '10px' 78 | ..right = '20px' 79 | ..zIndex = '999'; 80 | 81 | toolbar.children 82 | ..add(_showCodeButton); 83 | 84 | el.children.add(toolbar); 85 | } 86 | 87 | get _update_tooltip => ''' 88 | If not checked, then the preview is not auto-updated. The 89 | only way to see changes is to click the button. 90 | 91 | If checked, the preview is updated whenever the code is 92 | changed.'''; 93 | 94 | toggleCode() { 95 | if (ice.isCodeVisible) { 96 | hideCode(); 97 | } 98 | else { 99 | showCode(); 100 | } 101 | } 102 | 103 | void hideCode() { 104 | ice.hideCode(); 105 | _showCodeButton.style.display = ''; 106 | _hideCodeButton.style.display = 'none'; 107 | _mainMenuButton.style.display = 'none'; 108 | _updateButton.style.display = 'none'; 109 | _leaveSnapshotModeButton.style.display = 'none'; 110 | _focusAfterPreviewChange(); 111 | } 112 | 113 | void showCode() { 114 | ice.showCode(); 115 | _showCodeButton.style.display = 'none'; 116 | _hideCodeButton.style.display = ''; 117 | _mainMenuButton.style.display = ''; 118 | _updateButton.style.display = ''; 119 | _leaveSnapshotModeButton.style.display = ''; 120 | } 121 | 122 | Element _show_code_button; 123 | get _showCodeButton { 124 | if (_show_code_button != null) return _show_code_button; 125 | 126 | return _show_code_button = new Element.html('') 127 | ..style.display = 'none' 128 | ..onClick.listen((e)=> showCode()); 129 | } 130 | 131 | Element _hide_code_button; 132 | get _hideCodeButton { 133 | if (_hide_code_button != null) return _hide_code_button; 134 | 135 | return _hide_code_button = new Element.html('') 136 | ..onClick.listen((e)=> hideCode()); 137 | } 138 | 139 | Element _main_menu_button; 140 | get _mainMenuButton { 141 | if (_main_menu_button != null) return _main_menu_button; 142 | 143 | return _main_menu_button = new Element.html('') 144 | ..onClick.listen((e) { 145 | this.toggleMainMenu(); 146 | e.stopPropagation(); 147 | }); 148 | } 149 | 150 | Element _update_button; 151 | get _updateButton { 152 | if (_update_button != null) return _update_button; 153 | if (store.show_snapshots) return _update_button = new Element.span(); 154 | 155 | return _update_button = new Element.html(''' 156 | ''' 160 | ) 161 | ..onClick.listen((e)=> ice.updatePreview()) 162 | ..query("input").onClick.listen((e)=> e.stopPropagation()) 163 | ..query("input").onChange.listen((e)=> _toggleAutoupdate(e.target)) 164 | ..query("input").onChange. 165 | where((e) => e.target.checked). 166 | listen((e)=> ice.updatePreview()); 167 | } 168 | 169 | Element _leave_snapshot_mode_button; 170 | get _leaveSnapshotModeButton { 171 | if (_leave_snapshot_mode_button != null) return _leave_snapshot_mode_button; 172 | if (!store.show_snapshots) return _leave_snapshot_mode_button = new Element.span(); 173 | 174 | return _leave_snapshot_mode_button = new Element.html(''' 175 | ''' 176 | ) 177 | ..onClick.listen((e){ 178 | window.location.search = ''; 179 | }) 180 | ..style.color = 'red' 181 | ..style.fontWeight = 'bold'; 182 | } 183 | 184 | _focusAfterPreviewChange() { 185 | var listener; 186 | 187 | listener = ice.onPreviewChange.listen((e) { 188 | ice.focus(); 189 | listener.cancel(); 190 | }); 191 | new Timer(new Duration(milliseconds: 2500), (){ 192 | listener.cancel(); 193 | }); 194 | } 195 | 196 | _drawWhatsNewIndicator(toolbar) { 197 | if (_whatsNewClicked) return; 198 | 199 | var new_indicator = new Element.html(''); 200 | new_indicator.style 201 | ..color = 'red' 202 | ..fontSize = '36px' 203 | ..position = 'absolute' 204 | ..top = '-20px' 205 | ..right = '-7px' 206 | ..textShadow = '2px 2px 2px #000000' 207 | ..zIndex = '99'; 208 | 209 | toolbar.append(new_indicator); 210 | } 211 | 212 | _toggleAutoupdate(CheckboxInputElement e){ 213 | ice.autoupdate = e.checked; 214 | } 215 | 216 | toggleMainMenu() { 217 | if (queryAll('.ice-menu,.ice-dialog').isEmpty) _showMainMenu(); 218 | else {_hideMenu(); _hideDialog();} 219 | } 220 | 221 | _showMainMenu() { 222 | var menu = new Element.html('
    '); 223 | el.append(menu); 224 | 225 | if (store.show_snapshots) { 226 | menu 227 | ..append(_openDialog) 228 | ..append(_copyDialog) 229 | ..append(_helpDialog); 230 | return; 231 | } 232 | 233 | menu 234 | ..append(_newProjectDialog) 235 | ..append(_openDialog) 236 | ..append(_copyDialog) 237 | ..append(_renameDialog) 238 | ..append(_saveDialog) 239 | ..append(_shareDialog) 240 | ..append(_downloadDialog) 241 | ..append(_removeDialog) 242 | ..append(_menuDivider) 243 | ..append(_imageUploadDialog) 244 | ..append(_imageListDialog) 245 | ..append(_menuDivider) 246 | ..append(_exportDialog) 247 | ..append(_importDialog) 248 | ..append(_menuDivider) 249 | ..append(_whatsNewDialog) 250 | ..append(_helpDialog); 251 | } 252 | 253 | get _openDialog=> new MenuItem(new OpenDialog(this)).el; 254 | get _newProjectDialog=> new MenuItem(new NewProjectDialog(this)).el; 255 | get _renameDialog=> new MenuItem(new RenameDialog(this)).el; 256 | get _copyDialog=> new MenuItem(new CopyDialog(this)).el; 257 | get _saveDialog=> new MenuItem(new SaveAction(this)).el; 258 | get _shareDialog=> new MenuItem(new ShareDialog(this)).el; 259 | get _removeDialog=> new MenuItem(new RemoveDialog(this)).el; 260 | get _downloadDialog=> new MenuItem(new DownloadDialog(this)).el; 261 | get _exportDialog=> new MenuItem(new ExportDialog(this)).el; 262 | get _importDialog=> new MenuItem(new ImportDialog(this)).el; 263 | get _imageUploadDialog=> new MenuItem(new ImageUploadDialog(this)).el; 264 | get _imageListDialog=> new MenuItem(new ImageListDialog(this)).el; 265 | get _whatsNewDialog=> new MenuItem( 266 | new WhatsNewAction(this), 267 | isHighlighted: _highlightWhatsNew 268 | ).el; 269 | get _helpDialog=> new MenuItem(new HelpAction(this)).el; 270 | get _menuDivider=> new Element.hr(); 271 | 272 | rememberWhatsNewClicked() { 273 | settings['clicked_whats_new'] = true; 274 | query("#somethingsnew").remove(); 275 | } 276 | 277 | bool get _highlightWhatsNew => !_whatsNewClicked; 278 | 279 | bool get _whatsNewClicked => 280 | (settings['clicked_whats_new'] != null) && settings['clicked_whats_new']; 281 | 282 | bool get _isFirstUse => 283 | store.isEmpty || 284 | (store.length == 1 && store.currentProjectTitle == 'Untitled'); 285 | 286 | String get encodedContent => Gzip.encode(ice.content); 287 | 288 | _attachKeyboardHandlers() { 289 | Keys.shortcuts({ 290 | 'Ctrl+N, Ctrl+O, ⌘+O, Ctrl+Shift+H': ()=> _hideDialog(focus: false) 291 | }); 292 | Keys.shortcuts({ 293 | 'Esc': ()=> _hideDialog(), 294 | 'Ctrl+N': ()=> new NewProjectDialog(this).open(), 295 | 'Ctrl+O, ⌘+O': ()=> new OpenDialog(this).open(), 296 | 'Ctrl+Shift+H': ()=> toggleCode() 297 | }); 298 | 299 | editorReady.then((_){ 300 | el.onFocus.listen((e)=> ice.focus()); 301 | }); 302 | } 303 | 304 | _attachMouseHandlers() { 305 | editorReady.then((_){ 306 | el.query('.ice-code-editor-editor'). 307 | onClick. 308 | listen((e){ 309 | _hideMenu(); 310 | _hideDialog(); 311 | }); 312 | }); 313 | } 314 | 315 | _attachMessageHandlers() { 316 | window.onMessage.listen((e) { 317 | showCode(); 318 | }); 319 | } 320 | 321 | _attachDropHandlers() { 322 | document.onDragOver.listen((e) { 323 | e.preventDefault(); 324 | e.stopPropagation(); 325 | }); 326 | 327 | document.onDrop.listen((e) { 328 | e.preventDefault(); 329 | e.stopPropagation(); 330 | 331 | var file = e.dataTransfer.files[0]; 332 | var title = file.name; 333 | 334 | var reader = new FileReader(); 335 | reader.onLoad.listen((e) { 336 | import(reader.result.toString(), title); 337 | }); 338 | reader.readAsText(file); 339 | }); 340 | } 341 | 342 | import(String contents, String title) { 343 | // TODO: don't use exceptions for normal workflow 344 | try { 345 | _importFromJson(contents); 346 | } on FormatException { 347 | _importProjectFile(contents, title); 348 | } 349 | } 350 | 351 | _importProjectFile(String contents, String title) { 352 | _createNewProject({'code': contents}, named: title); 353 | showCurrentProject(); 354 | } 355 | 356 | _importFromJson(String json) { 357 | var projects = JSON.decode(json); 358 | projects.reversed.forEach((project) { 359 | _createNewProject(new HashMap.from(project)); 360 | }); 361 | showCurrentProject(); 362 | } 363 | 364 | _createNewProject(Map project, {named}) { 365 | var name = (named != null) ? named : project['filename']; 366 | if (store.containsKey(name)) { 367 | name = store.nextProjectNamed(name); 368 | } 369 | store[name] = project; 370 | } 371 | 372 | showCurrentProject() { 373 | content = store.projects[0]['code']; 374 | } 375 | 376 | 377 | _fullScreenStyles() { 378 | document.body.style 379 | ..margin = '0px' 380 | ..overflow = 'hidden'; 381 | } 382 | 383 | _openProject() { 384 | var matchCompressedContent = new RegExp(r'#?B/'); 385 | if (compressedContent.startsWith(matchCompressedContent)) { 386 | var title = store.nextProjectNamed('Untitled'); 387 | content = Gzip.decode(compressedContent.replaceFirst(matchCompressedContent, '')); 388 | store[title] = {'code': content}; 389 | return; 390 | } 391 | 392 | content = store.isEmpty ? 393 | DefaultProject.content : store.projects.first['code']; 394 | } 395 | 396 | _startAutoSave() { 397 | ice.onChange.listen((_){ 398 | var title = store.isEmpty ? 'Untitled' : store.currentProjectTitle; 399 | 400 | store[title] = { 401 | 'code': ice.content, 402 | 'lineNumber': ice.lineNumber 403 | }; 404 | }); 405 | } 406 | 407 | _initializeSettingForFirstUse() { 408 | if (_isFirstUse) { 409 | settings['clicked_whats_new'] = true; 410 | } 411 | } 412 | 413 | _initializeMode() { 414 | if (mode == null) return; 415 | if (mode.startsWith('e')) ice.edit_only = true; 416 | if (mode.startsWith('g')) hideCode(); 417 | } 418 | 419 | _startSnapshotter() { 420 | if (mode != null && mode.startsWith('s')) { 421 | store.show_snapshots = true; 422 | ice.read_only = true; 423 | } 424 | else { 425 | snapshotter = new Snapshotter(this); 426 | } 427 | } 428 | 429 | Element get editor_el => ice.editor_el; 430 | Element get preview_el => ice.preview_el; 431 | 432 | _applyStyles() { 433 | // Both of these height settings are required for ICE to play nicely 434 | // with HTML5 documents 435 | document.documentElement.style.height = '100%'; 436 | document.body.style.height = '100%'; 437 | 438 | editor_el.style 439 | ..top = '0' 440 | ..bottom = '0' 441 | ..left = '0' 442 | ..right = '0' 443 | ..backgroundColor = 'rgba(255,255,255,0.0)'; 444 | 445 | el.style 446 | ..height = '100%' 447 | ..width = '100%'; 448 | } 449 | } 450 | 451 | _hideMenu({focus:true}) => _hideDialog(focus:focus); 452 | 453 | _hideDialog({focus:true}) { 454 | queryAll('.ice-menu').forEach((e)=> e.remove()); 455 | queryAll('.ice-dialog').forEach((e)=> e.remove()); 456 | if (focus) _maybeFocus(); 457 | } 458 | 459 | _maybeFocus() { 460 | if (document.activeElement.tagName == 'INPUT') return; 461 | query('#ice').dispatchEvent(new UIEvent('focus')); 462 | } 463 | -------------------------------------------------------------------------------- /lib/full/copy_dialog.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class CopyDialog extends Dialog implements MenuAction { 4 | CopyDialog(Full f): super(f); 5 | 6 | get name => 'Make a Copy'; 7 | 8 | open() { 9 | var dialog = new Element.html( 10 | ''' 11 |
    12 | 13 | 14 | 15 |
    16 | ''' 17 | ); 18 | 19 | // Hack in lieu of KeyEvent tests 20 | dialog.query('button#fake_enter_key') 21 | ..onClick.listen((e)=> _copyProject()) 22 | ..style.display = 'none'; 23 | 24 | dialog.query('button').onClick 25 | ..listen((_)=> _copyProject()); 26 | 27 | dialog.query('input').onKeyDown. 28 | listen((e) { 29 | if (e.keyCode != KeyCode.ENTER) return; 30 | _copyProject(); 31 | }); 32 | 33 | parent.children.add(dialog); 34 | 35 | dialog.query('input') 36 | ..focus(); 37 | } 38 | 39 | get _copiedProjectName => store.nextProjectNamed(); 40 | 41 | _copyProject() { 42 | store.show_snapshots = false; 43 | 44 | var title = _field.value; 45 | if (!new Validate(title, store, parent).isValid) return; 46 | 47 | store[title] = {'code': ice.content}; 48 | 49 | query('.ice-dialog').remove(); 50 | 51 | if (window.location.search.contains('s')) window.location.search = ''; 52 | } 53 | 54 | InputElement get _field => query('.ice-dialog').query('input'); 55 | } 56 | -------------------------------------------------------------------------------- /lib/full/default_project.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class DefaultProject { 4 | static String get content => ''' 5 | 6 | 7 | 8 | '''; 78 | } 79 | -------------------------------------------------------------------------------- /lib/full/dialog.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class Dialog { 4 | Element parent; 5 | Editor ice; 6 | Store store; 7 | 8 | Dialog(Full full): this.fromParts(full.el, full.ice, full.store); 9 | Dialog.fromParts(this.parent, this.ice, this.store); 10 | } 11 | -------------------------------------------------------------------------------- /lib/full/download_dialog.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class DownloadDialog extends Dialog implements MenuAction { 4 | DownloadDialog(Full f): super(f); 5 | 6 | get name => "Download"; 7 | 8 | open()=> el.click(); 9 | 10 | get el { 11 | var blob = new Blob([ice.content], "text/plain"); 12 | var object_url = Url.createObjectUrl(blob); 13 | 14 | return new AnchorElement(href: object_url) 15 | ..download = store.currentProjectTitle; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib/full/export_dialog.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class ExportDialog extends Dialog implements MenuAction { 4 | 5 | ExportDialog(Full f): super(f); 6 | 7 | get name => "Export All Projects"; 8 | 9 | open()=> el.click(); 10 | 11 | get el { 12 | var blob = new Blob([JSON.encode(store.projects)], "text/plain"); 13 | var object_url = Url.createObjectUrl(blob); 14 | 15 | return new AnchorElement(href: object_url) 16 | ..download = 'Export'; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/full/help_action.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class HelpAction implements MenuAction { 4 | HelpAction(_); 5 | 6 | get name => "Help"; 7 | 8 | 9 | open(){ 10 | AnchorElement el = new Element.html(''); 11 | 12 | el 13 | ..style.display = 'none' 14 | ..href = 'https://github.com/eee-c/ice-code-editor/wiki'; 15 | 16 | document.body.append(el); 17 | 18 | el 19 | ..click() 20 | ..remove(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/full/image_list_dialog.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class ImageListDialog extends Dialog implements MenuAction { 4 | Full full; 5 | ImageListDialog(Full f): super(f) { 6 | full = f; 7 | } 8 | 9 | get name => "List Uploaded Images"; 10 | 11 | Element menu; 12 | 13 | open() { 14 | menu = new Element.html( 15 | ''' 16 |
    17 |

    3DE Images

    18 |

    You can use the following images in your code:

    19 |
      20 |

      Click an image path to copy to your clipboard.
      Then paste it into your code.

      21 |
      22 | ''' 23 | ); 24 | 25 | parent.append(menu); 26 | addImagesToMenu(); 27 | 28 | menu.style 29 | ..maxHeight = '560px' 30 | ..overflowY = 'auto'; 31 | } 32 | 33 | addImagesToMenu({filter:''}) { 34 | var menu = query('.ice-menu'); 35 | 36 | var uploadedImages = JSON.decode(window.localStorage['uploaded_images']); 37 | 38 | var images = uploadedImages.keys.map((image){ 39 | return new Element.html('
    • /3de/${image}
    • ') 40 | ..tabIndex = 0 41 | ..onClick.listen((e) => _copyToClipboard('/3de/${image}')) 42 | ..onClick.listen((e) => _hideMenu()) 43 | ..onClick.listen((e) => _notifyClipboardReady()); 44 | }); 45 | 46 | menu.query('ul').children.addAll(images); 47 | } 48 | 49 | _copyToClipboard(String path) { 50 | var clipboard = js.context['navigator']['clipboard']; 51 | clipboard.callMethod('writeText', [path]); 52 | } 53 | 54 | _notifyClipboardReady() { 55 | Notify.info('Copied path to clipboard. Now paste it into your code!'); 56 | } 57 | } -------------------------------------------------------------------------------- /lib/full/image_upload_dialog.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class ImageUploadDialog extends Dialog implements MenuAction { 4 | Full full; 5 | ImageUploadDialog(Full f): super(f) { 6 | full = f; 7 | } 8 | 9 | get name => "Upload Image"; 10 | 11 | open() => el.click(); 12 | 13 | get el => 14 | new FileUploadInputElement() 15 | ..onChange.listen((e) { 16 | var files = e.target.files; 17 | if (files.length != 1) return; 18 | var filename = e.target.files[0].name; 19 | var reader = new FileReader(); 20 | 21 | reader.onLoad.listen((e) { 22 | import(reader.result.toString(), filename); 23 | }); 24 | reader.readAsDataUrl(files[0]); 25 | }); 26 | 27 | void import(String image, String filename) { 28 | String json = window.localStorage.putIfAbsent('uploaded_images', ()=>'{}'); 29 | Map images = JSON.decode(json); 30 | images[filename] = image; 31 | window.localStorage['uploaded_images'] = JSON.encode(images); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/full/import_dialog.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class ImportDialog extends Dialog implements MenuAction { 4 | Full full; 5 | ImportDialog(Full f): super(f) { 6 | full = f; 7 | } 8 | 9 | get name => "Import Projects"; 10 | 11 | open() => el.click(); 12 | 13 | get el => 14 | new FileUploadInputElement() 15 | ..onChange.listen((e) { 16 | var files = e.target.files; 17 | if (files.length != 1) return; 18 | 19 | var reader = new FileReader(); 20 | reader.onLoad.listen((e) { 21 | import(reader.result.toString()); 22 | }); 23 | reader.readAsText(files[0]); 24 | }); 25 | 26 | void import(String json) { 27 | try { 28 | full._importFromJson(json); 29 | } on FormatException { 30 | var message = "This does not look like a ICE project file. Unable to import."; 31 | Notify.alert(message, parent: parent); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/full/menu_action.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | abstract class MenuAction { 4 | String get name; 5 | void open(); 6 | } 7 | -------------------------------------------------------------------------------- /lib/full/menu_item.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class MenuItem { 4 | MenuAction action; 5 | bool isHighlighted; 6 | MenuItem(this.action, {this.isHighlighted: false}); 7 | Element get el { 8 | var className = isHighlighted ? 'highlighted' : ''; 9 | return new Element.html('
    • ${action.name}
    • ') 10 | ..onClick.listen((e)=> _hideMenu(focus:false)) 11 | ..onClick.listen((e)=> action.open()) 12 | ..onClick.listen((e)=> _maybeFocus()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/full/new_project_dialog.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class NewProjectDialog extends Dialog implements MenuAction { 4 | NewProjectDialog(Full f): super(f); 5 | 6 | get name => 'New'; 7 | 8 | open() { 9 | var dialog = new Element.html( 10 | ''' 11 |
      12 | 13 | 14 | 15 |
      16 | 22 |
      23 |
      24 | ''' 25 | ); 26 | 27 | // Hack in lieu of KeyEvent tests 28 | dialog.query('button#fake_enter_key') 29 | ..onClick.listen((e)=> _create()) 30 | ..style.display = 'none'; 31 | 32 | dialog.query('button') 33 | ..onClick.listen((e)=> _create()); 34 | 35 | dialog.query('input').onKeyDown. 36 | listen((e) { 37 | if (e.keyCode != KeyCode.ENTER) return; 38 | _create(); 39 | e.preventDefault(); 40 | }); 41 | 42 | parent.append(dialog); 43 | dialog.query('input').focus(); 44 | } 45 | 46 | _create() { 47 | var title = _field.value; 48 | if (!new Validate(title, store, parent).isValid) return; 49 | 50 | var template = _list.value, 51 | code = Templates.byTitle(template); 52 | 53 | store[title] = {'code': code}; 54 | ice.content = code; 55 | _hideDialog(); 56 | } 57 | 58 | InputElement get _field => query('.ice-dialog').query('input'); 59 | SelectElement get _list => query('.ice-dialog').query('select'); 60 | } 61 | -------------------------------------------------------------------------------- /lib/full/notify.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | // TODO: convert to a singleton to decrease the arity 4 | class Notify { 5 | // consider making this a full-fledged property 6 | static get testMode => Editor.disableJavaScriptWorker; 7 | 8 | static alert(message, {parent}) { 9 | if (parent == null) parent = document.body; 10 | 11 | var el = new Element.html('
      $message
      '); 12 | parent.children.add(el..style.visibility="hidden"); 13 | if (!testMode) window.alert(message); 14 | } 15 | 16 | static bool confirm(message, {parent}) { 17 | if (parent == null) parent = document.body; 18 | 19 | var el = new Element.html('
      $message
      '); 20 | parent.children.add(el..style.visibility="hidden"); 21 | if (!testMode) return window.confirm(message); 22 | return true; 23 | } 24 | 25 | static info(message, {parent}) { 26 | if (parent == null) parent = document.body; 27 | 28 | var el = new Element.html('
      $message
      '); 29 | parent.children.add(el); 30 | 31 | const timeout = const Duration(seconds: 5); 32 | new Timer(timeout, ()=> el.remove()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/full/open_dialog.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class OpenDialog extends Dialog implements MenuAction { 4 | OpenDialog(Full f): super(f); 5 | 6 | get name => "Open"; 7 | 8 | Element menu; 9 | 10 | open() { 11 | menu = new Element.html( 12 | ''' 13 |
      14 |

      Saved Projects

      15 | ${filterField} 16 |
        17 | 18 | 19 | 20 |
        21 | ''' 22 | ); 23 | 24 | menu. 25 | queryAll('button'). 26 | forEach((b){ b.style.display = 'none';}); 27 | 28 | parent.append(menu); 29 | addProjectsToMenu(); 30 | 31 | menu.queryAll('input').forEach(_addListeners); 32 | _focus(menu); 33 | _handleArrowKeys(menu); 34 | 35 | menu.style 36 | ..maxHeight = '560px' 37 | ..overflowY = 'auto'; 38 | } 39 | 40 | get filterField { 41 | if (store.length < 10) return ''; 42 | return ''; 43 | } 44 | 45 | addProjectsToMenu({filter:''}) { 46 | var menu = query('.ice-menu'); 47 | 48 | var projects = store.projects. 49 | where((project) { 50 | return project['filename'].toLowerCase().contains(filter.toLowerCase()); 51 | }). 52 | map((project) { 53 | var title = project["filename"], 54 | created_at = project["created_at"], 55 | updated_at = project["updated_at"]; 56 | 57 | var tooltip = (created_at == null || updated_at == null) ? '' : ''' 58 | Created: ${created_at.substring(0,10)} 59 | Last Updated: ${updated_at.substring(0,10)}'''; 60 | 61 | return new Element.html('
      • ${title}
      • ') 62 | ..tabIndex = 0 63 | ..onClick.listen((e)=> _openProject(title)) 64 | ..onClick.listen((e)=> _hideMenu()) 65 | ..onKeyDown.listen((e) { 66 | if (!Keys.isEnter(e)) return; 67 | e.preventDefault(); 68 | e.target.click(); 69 | }); 70 | }). 71 | toList(); 72 | 73 | if (projects.length == 0) { 74 | projects = [new Element.html('
      • No matching projects.
      • ')]; 75 | } 76 | 77 | menu.query('ul').children.addAll(projects); 78 | } 79 | 80 | _openProject(title) { 81 | var old = store.currentProject, 82 | old_title = old['filename']; 83 | old['lineNumber'] = ice.lineNumber; 84 | store[old_title] = old; 85 | 86 | // TODO: Move this into Store (should be a way to make a project as 87 | // current) 88 | var project = store.remove(title); 89 | store[title] = project; 90 | ice.content = project['code']; 91 | if (project['lineNumber'] != null) { 92 | ice.lineNumber = project['lineNumber']; 93 | } 94 | } 95 | 96 | _addListeners(el) { 97 | _initializeFilter(el); 98 | _handleEnter(el); 99 | } 100 | 101 | _initializeFilter(el) { 102 | el.onKeyUp.listen((e) { 103 | query('.ice-menu ul') 104 | ..children.clear(); 105 | addProjectsToMenu(filter: el.value); 106 | }); 107 | } 108 | 109 | _handleEnter(el) { 110 | clickActive() { 111 | if (el.value.isEmpty) return; 112 | query('.ice-menu ul').children.first.click(); 113 | } 114 | 115 | el.onKeyDown.listen((e){ 116 | if (e.keyCode != KeyCode.ENTER) return; 117 | clickActive(); 118 | }); 119 | 120 | // Hack in lieu of KeyEvent tests 121 | menu.query('#fake_enter_key').onClick.listen((_)=> clickActive()); 122 | } 123 | 124 | _focus(el) { 125 | if (el.query('input') == null) { 126 | el.query('li').focus(); 127 | } 128 | else { 129 | el.query('input').focus(); 130 | } 131 | } 132 | 133 | _handleArrowKeys(el) { 134 | el.onKeyDown.listen(_handleDown); 135 | el.onKeyDown.listen(_handleUp); 136 | 137 | // Hacks in lieu of KeyEvent tests 138 | menu.query('#fake_down_key').onClick.listen(_handleDown); 139 | menu.query('#fake_up_key').onClick.listen(_handleUp); 140 | } 141 | 142 | _handleDown(e) { 143 | if (e.type == 'keydown' && e.keyCode != KeyCode.DOWN) return; 144 | 145 | var next = document.activeElement.nextElementSibling; 146 | if (next == null) { 147 | next = document.activeElement; 148 | } 149 | if (next.tagName == 'UL') { 150 | next = next.children.first; 151 | } 152 | next.focus(); 153 | } 154 | 155 | _handleUp(e) { 156 | if (e.type == 'keydown' && e.keyCode != KeyCode.UP) return; 157 | 158 | var prev = document.activeElement.previousElementSibling; 159 | if (prev == null) { 160 | prev = menu.query('input'); 161 | } 162 | if (prev == null) { 163 | prev = document.activeElement; 164 | } 165 | prev.focus(); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /lib/full/remove_dialog.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class RemoveDialog extends Dialog implements MenuAction { 4 | RemoveDialog(Full f): super(f); 5 | 6 | get name => "Remove"; 7 | 8 | open() { 9 | var message = ''' 10 | Remove: ${store.currentProjectTitle}. 11 | 12 | Once this project is removed, you will not be 13 | able to get it back. 14 | 15 | Are you sure you want to remove this project?'''; 16 | 17 | if (Notify.confirm(message, parent: parent)) _removeCurrentProject(); 18 | } 19 | 20 | _removeCurrentProject() { 21 | var title = store.currentProjectTitle; 22 | store.remove(title); 23 | 24 | title = store.currentProjectTitle; 25 | ice.content = (title == 'Untitled') ? 26 | DefaultProject.content : store[title]['code']; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/full/rename_dialog.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class RenameDialog extends Dialog implements MenuAction { 4 | RenameDialog(Full f): super(f); 5 | 6 | get name => 'Rename'; 7 | 8 | open(){ 9 | var dialog = new Element.html( 10 | ''' 11 |
        12 | 13 | 14 | 15 |
        16 | ''' 17 | ); 18 | 19 | // Hack in lieu of KeyEvent tests 20 | dialog.query('button#fake_enter_key') 21 | ..onClick.listen((e)=> _renameProject()) 22 | ..style.display = 'none'; 23 | 24 | dialog.query('button').onClick 25 | .listen((_)=> _renameProject()); 26 | 27 | dialog.query('input').onKeyDown. 28 | listen((e) { 29 | if (e.keyCode != KeyCode.ENTER) return; 30 | _renameProject(); 31 | }); 32 | 33 | parent.children.add(dialog); 34 | 35 | dialog.query('input').focus(); 36 | } 37 | 38 | _renameProject() { 39 | var title = _field.value; 40 | if (!new Validate(title, store, parent).isValid) return; 41 | 42 | var project = store.remove(_currentProjectTitle); 43 | store[title] = project; 44 | _hideDialog(); 45 | } 46 | 47 | InputElement get _field => query('.ice-dialog').query('input'); 48 | String get _currentProjectTitle => store.currentProjectTitle; 49 | } 50 | -------------------------------------------------------------------------------- /lib/full/save_action.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class SaveAction extends Dialog implements MenuAction { 4 | SaveAction(Full f): super(f); 5 | 6 | get name => 'Save'; 7 | 8 | void open() { 9 | var title = store.isEmpty ? 'Untitled' : store.currentProjectTitle; 10 | 11 | store[title] = { 12 | 'code': ice.content, 13 | 'lineNumber': ice.lineNumber 14 | }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/full/share_dialog.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class ShareDialog extends Dialog implements MenuAction { 4 | Full full; 5 | ShareDialog(Full f): super(f) { 6 | full = f; 7 | } 8 | 9 | get name => 'Share'; 10 | 11 | open() { 12 | parent.append( 13 | new Element.html(''' 14 |
        15 |

        Copy this link to share your creation:

        16 | 17 |
        18 | 22 |
        23 |
        24 | ..or, for easier sharing 25 | make a short link 26 |
        27 |
        ''' 28 | ) 29 | ); 30 | 31 | _share_link.href = short_url; 32 | 33 | _field 34 | ..focus() 35 | ..select() 36 | ..disabled = true 37 | ..style.width = '350px' 38 | ..style.padding = '5px' 39 | ..style.border = '0px'; 40 | 41 | _checkbox 42 | ..onChange.listen((e)=> _toggleGameMode(e.target.checked)) 43 | ..onChange.listen((e)=> _updateShortUrl(e.target.checked)); 44 | } 45 | 46 | String get short_url => 'http://is.gd/create.php?url=${encoded_url}'; 47 | 48 | String get encoded_url => Uri.encodeComponent(content_for_sharing); 49 | 50 | String get content_for_sharing { 51 | var query_string = game_mode ? '?g' : ''; 52 | 53 | return "https://www.code3dgames.com/3de/${query_string}#B/${full.encodedContent}"; 54 | } 55 | 56 | InputElement get _field => query('.ice-dialog').query('input'); 57 | InputElement get _checkbox => query('.ice-dialog input[type=checkbox]'); 58 | AnchorElement get _share_link => query('.ice-dialog a'); 59 | 60 | bool get game_mode { 61 | if (_checkbox == null) return false; 62 | return _checkbox.checked; 63 | } 64 | 65 | _toggleGameMode(bool enabled) { 66 | _field.value = enabled ? 67 | _field.value.replaceFirst('ice/#B', 'ice/?g#B') : 68 | _field.value.replaceFirst('?g', ''); 69 | } 70 | 71 | _updateShortUrl(bool enabled) { 72 | _share_link.href = short_url; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/full/snapshotter.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class Snapshotter { 4 | Full full; 5 | final duration = const Duration(minutes: 10); 6 | 7 | static get dateStr => new DateTime. 8 | now(). 9 | toIso8601String(). 10 | replaceFirst('T', ' '). 11 | replaceFirst(new RegExp(r':\d\d\.\d\d\d.*'), ''); 12 | 13 | Snapshotter(this.full) { 14 | _startTimer(); 15 | } 16 | 17 | void take() { 18 | var currentProjectTitle = full.store.currentProjectTitle; 19 | 20 | var lastSnapshot = full.store.snapshots. 21 | firstWhere((item) => item[Store.title]. 22 | startsWith('SNAPSHOT: $currentProjectTitle ('), 23 | orElse: () => {} 24 | ); 25 | 26 | if (lastSnapshot['code'] == full.store.currentProject['code']) return; 27 | 28 | var title = 'SNAPSHOT: $currentProjectTitle ($dateStr)'; 29 | 30 | full.store[title] = new Map.from(full.store.currentProject) 31 | ..[Store.title] = title 32 | ..['snapshot'] = true; 33 | 34 | trimTo20(full.store); 35 | } 36 | 37 | static trimTo20(store) { 38 | var all = store.snapshots; 39 | 40 | if (all.length <= 20) return; 41 | 42 | for (var i=all.length-1; i [ 5 | '3D starter project', 6 | '3D starter project (with Animation)', 7 | '3D starter project (with Physics)', 8 | 'Empty project' 9 | ]; 10 | 11 | static String byTitle(title) { 12 | if (title == '3D starter project') return threeD; 13 | if (title == 'Empty project') return empty; 14 | if (title == '3D starter project (with Physics)') return physics; 15 | if (title == '3D starter project (with Animation)') return animation; 16 | return ''; 17 | } 18 | 19 | static String get threeD => ''' 20 | 21 | 22 | '''; 48 | 49 | static String get animation => ''' 50 | 51 | 52 | '''; 89 | 90 | static String get empty => ''' 91 | 92 | 93 | '''; 96 | 97 | static String get physics => ''' 98 | 99 | 100 | 101 | '''; 143 | 144 | } 145 | -------------------------------------------------------------------------------- /lib/full/validate.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class Validate { 4 | String title; 5 | 6 | Element parent; 7 | Editor ice; 8 | Store store; 9 | 10 | // TODO: generalize so that these are not passed directly to Validate 11 | Validate(this.title, this.store, this.parent); 12 | 13 | bool get isValid { 14 | if (store.containsKey(title)) { 15 | var message = "There is already a project with that name"; 16 | Notify.alert(message, parent: parent); 17 | return false; 18 | } 19 | 20 | if (title.trim().isEmpty) { 21 | var message = "The project name cannot be blank"; 22 | Notify.alert(message, parent: parent); 23 | return false; 24 | } 25 | 26 | return true; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/full/whats_new_action.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class WhatsNewAction extends Dialog implements MenuAction { 4 | Full full; 5 | WhatsNewAction(f) : super(f) { full = f; } 6 | 7 | get name => "What's New"; 8 | 9 | open(){ 10 | document.body.append(el); 11 | 12 | el 13 | ..click() 14 | ..remove(); 15 | 16 | full.rememberWhatsNewClicked(); 17 | } 18 | 19 | AnchorElement _el; 20 | get el { 21 | if (_el != null) return _el; 22 | 23 | _el = new AnchorElement() 24 | ..target = '_blank' 25 | ..style.display = 'none' 26 | ..href = 'https://github.com/eee-c/ice-code-editor/wiki/What\'s-New'; 27 | 28 | return _el; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/gzip.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class Gzip { 4 | static String encode(String string) => 5 | js.context['RawDeflate'].callMethod('deflateToBase64', [string]); 6 | 7 | static String decode(String string) => 8 | js.context['RawDeflate'].callMethod('inflateFromBase64', [string]); 9 | 10 | static Future get _ready { 11 | var completer = new Completer(); 12 | 13 | new Timer.periodic( 14 | new Duration(milliseconds: 50), 15 | (timer) { 16 | if (js.context['RawDeflate'] != null) { 17 | timer.cancel(); 18 | completer.complete(); 19 | } 20 | }); 21 | 22 | return completer.future; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/html/code-OFFLINE.html: -------------------------------------------------------------------------------- 1 | 2 | 50 | -------------------------------------------------------------------------------- /lib/html/code.html: -------------------------------------------------------------------------------- 1 | 2 | 92 | -------------------------------------------------------------------------------- /lib/ice.dart: -------------------------------------------------------------------------------- 1 | library ice; 2 | 3 | import 'dart:async'; 4 | import 'dart:collection'; 5 | import 'dart:convert'; 6 | import 'dart:html'; 7 | import 'dart:js' as js; 8 | 9 | import 'package:ctrl_alt_foo/keys.dart'; 10 | 11 | part 'editor.dart'; 12 | part 'store.dart'; 13 | part 'settings.dart'; 14 | part 'gzip.dart'; 15 | 16 | part 'full.dart'; 17 | part 'full/default_project.dart'; 18 | part 'full/templates.dart'; 19 | part 'full/notify.dart'; 20 | part 'full/validate.dart'; 21 | 22 | part 'full/menu_item.dart'; 23 | part 'full/menu_action.dart'; 24 | part 'full/dialog.dart'; 25 | 26 | part 'full/open_dialog.dart'; 27 | part 'full/new_project_dialog.dart'; 28 | part 'full/rename_dialog.dart'; 29 | part 'full/save_action.dart'; 30 | part 'full/share_dialog.dart'; 31 | part 'full/download_dialog.dart'; 32 | part 'full/export_dialog.dart'; 33 | part 'full/import_dialog.dart'; 34 | part 'full/image_upload_dialog.dart'; 35 | part 'full/image_list_dialog.dart'; 36 | part 'full/copy_dialog.dart'; 37 | part 'full/remove_dialog.dart'; 38 | part 'full/whats_new_action.dart'; 39 | part 'full/help_action.dart'; 40 | part 'full/snapshotter.dart'; 41 | -------------------------------------------------------------------------------- /lib/images/clipboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eee-c/ice-code-editor/9fc0e4d254a33e3da48527f9e2ccfbe17e6e45aa/lib/images/clipboard.png -------------------------------------------------------------------------------- /lib/polymer/ice_code_editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lib/polymer/ice_code_editor_element.dart: -------------------------------------------------------------------------------- 1 | import 'package:polymer/polymer.dart'; 2 | 3 | import 'dart:html'; 4 | import 'dart:async'; 5 | 6 | import 'package:ice_code_editor/ice.dart' as ICE; 7 | 8 | @CustomTag('ice-code-editor') 9 | class IceCodeEditorElement extends PolymerElement { 10 | @published String src; 11 | @published int line_number = 0; 12 | ICE.Editor editor; 13 | 14 | int width = 600; 15 | int height = 400; 16 | 17 | IceCodeEditorElement.created(): super.created() { 18 | var container = new DivElement() 19 | ..id = 'ice-${this.hashCode}' 20 | ..style.width = '${width}px' 21 | ..style.height = '${height}px'; 22 | 23 | // This only works because we have tag to append to 24 | append(container); 25 | 26 | var preview_el = new DivElement() 27 | ..style.top = '-${height}px'; 28 | var wrapper = new DivElement() 29 | ..style.position = 'relative'; 30 | wrapper.append(preview_el); 31 | 32 | append(wrapper); 33 | 34 | editor = new ICE.Editor(container, preview_el: preview_el); 35 | 36 | loadContent(); 37 | } 38 | 39 | void attached() { 40 | super.attached(); 41 | editor.applyStyles(); 42 | } 43 | 44 | void attributeChanged(String name, String oldValue, String newValue) { 45 | super.attributeChanged(name, oldValue, newValue); 46 | loadContent(); 47 | } 48 | 49 | Future loadContent() { 50 | if (src == null) return; 51 | 52 | HttpRequest.getString(src).then((response) { 53 | editor.content = response; 54 | editor.editorReady.then((_)=> editor.lineNumber = line_number); 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/settings.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | class Settings implements Map { 4 | /// The key used to identify the data in localStorage. 5 | String storage_key; 6 | 7 | /// The default key used to identify the data in localStorage. 8 | static const String codeEditor = 'codeeditor_settings'; 9 | 10 | Map _model; 11 | 12 | Settings({this.storage_key: codeEditor}); 13 | 14 | // Be Map-like 15 | int get length => model.length; 16 | List get keys => model.keys; 17 | List get values => model.values; 18 | putIfAbsent(String k, Function ifAbsent) => model.putIfAbsent(k, ifAbsent); 19 | forEach(Function f) => model.forEach(f); 20 | bool get isEmpty => model.isEmpty; 21 | bool get isNotEmpty => model.isNotEmpty; 22 | addAll(Map all) => model.addAll(all); 23 | remove(String k) => model.remove(k); 24 | bool containsKey(String k) => model.containsKey(k); 25 | bool containsValue(Object v) => model.containsValue(v); 26 | void clear() { 27 | model.clear(); 28 | _sync(); 29 | window.localStorage.remove(storage_key); 30 | } 31 | 32 | Object operator [](String key) => model[key]; 33 | 34 | void operator []=(String key, Object data) { 35 | model[key] = data; 36 | _sync(); 37 | } 38 | 39 | Map get model { 40 | if (_model != null) return _model; 41 | 42 | var json = window.localStorage[storage_key]; 43 | return _model = (json == null) ? {} : JSON.decode(json); 44 | } 45 | 46 | refresh() => _model = null; 47 | 48 | void _sync() { 49 | window.localStorage[storage_key] = JSON.encode(_model); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/store.dart: -------------------------------------------------------------------------------- 1 | part of ice; 2 | 3 | /** 4 | * Persistent storage for ICE projects. 5 | * 6 | * Projects are unique by title, which is used to lookup projects in the 7 | * store. The list of projects is guaranteed to remain ordered by last 8 | * modification so that the most recently worked on projects are listed 9 | * first. After every update, the list is sync'd with localStorage to prevent 10 | * work from being lost. 11 | */ 12 | class Store implements HashMap { 13 | /// The key used to identify the data in localStorage. 14 | String storage_key; 15 | 16 | /// The default key used to identify the data in localStorage. 17 | static const String codeEditor = 'codeeditor'; 18 | 19 | /// The record ID attribute 20 | static const String title = 'filename'; 21 | LinkedHashMap _projects; 22 | 23 | /// Include snapshots by default (normally they are not displayed). 24 | bool show_snapshots = false; 25 | 26 | Store({this.storage_key: codeEditor}) { 27 | _projects = deserialize(); 28 | } 29 | // // Uncomment this (and method below) to migrate development data 30 | // // _migrateFromTitleIdToFilename(); 31 | // } 32 | 33 | 34 | LinkedHashMap deserialize() { 35 | var json = window.localStorage[storage_key]; 36 | var projects = (json == null) ? [] : JSON.decode(json); 37 | 38 | var map = new LinkedHashMap(); 39 | projects.reversed.forEach((p){ 40 | map[p[title]] = new HashMap.from(p); 41 | }); 42 | return map; 43 | } 44 | 45 | HashMap get currentProject { 46 | if (this.isEmpty) 47 | return {'code': ''}..[title] = 'Untitled'; 48 | 49 | return _projectsExcludingSnapshots.first; 50 | } 51 | 52 | String get currentProjectTitle => currentProject[title]; 53 | 54 | int get length => projects.length; 55 | 56 | HashMap operator [](String key)=> _projects[key]; 57 | 58 | void operator []=(String key, HashMap data) { 59 | data[title] = key; // store filename directly in DB record 60 | 61 | data.putIfAbsent('updated_at', ()=> new DateTime.now().toString()); 62 | 63 | if (_projects.containsKey(key)) { 64 | data['created_at'] = _projects[key]['created_at']; 65 | _projects.remove(key); 66 | } 67 | else { 68 | data.putIfAbsent('created_at', ()=> new DateTime.now().toString()); 69 | } 70 | _projects[key] = data; 71 | 72 | _sync(); 73 | } 74 | 75 | bool get isEmpty => _projects.isEmpty; 76 | bool get isNotEmpty => _projects.isNotEmpty; 77 | Iterable get keys => _projects.keys; 78 | Iterable get values => _projects.values; 79 | bool containsKey(key) => _projects.containsKey(key); 80 | bool containsValue(value) => _projects.containsValue(value); 81 | void forEach(f) { _projects.forEach(f); } 82 | 83 | HashMap remove(key) { 84 | var removed = _projects.remove(key); 85 | _sync(); 86 | return removed; 87 | } 88 | void clear() { 89 | _projects = new LinkedHashMap(); 90 | _sync(); 91 | window.localStorage.remove(storage_key); 92 | } 93 | HashMap putIfAbsent(key, f)=> _projects.putIfAbsent(key, f); 94 | 95 | void addAll(recs)=> _projects.addAll(recs); 96 | 97 | String nextProjectNamed([original_title]) { 98 | if (this.isEmpty) return "Untitled"; 99 | 100 | if (original_title == null) original_title = currentProjectTitle; 101 | if (!containsKey(original_title)) return original_title; 102 | 103 | RegExp copy_number_re = new RegExp(r"\s+\((\d+)\)$"); 104 | var title = original_title.replaceFirst(copy_number_re, ""); 105 | 106 | var same_base = values.where((p) { 107 | return p['filename'].startsWith(title); 108 | }); 109 | 110 | var copy_numbers = same_base.map((p) { 111 | var stringCount = copy_number_re.firstMatch(p['filename']); 112 | return stringCount == null ? 0 : int.parse(stringCount[1]); 113 | }) 114 | .toList() 115 | ..sort(); 116 | 117 | var count = copy_numbers.last; 118 | 119 | return "$title (${count+1})"; 120 | } 121 | 122 | /// The list of all projects in the store. 123 | List get projects { 124 | return show_snapshots ? 125 | _projectSnapshots : _projectsExcludingSnapshots; 126 | } 127 | 128 | List get snapshots { 129 | return _projectsIncludingSnapshots. 130 | where((p) => p['snapshot'] == true). 131 | toList(); 132 | } 133 | 134 | List get _projectsExcludingSnapshots { 135 | return _projectsIncludingSnapshots. 136 | where((p)=> p['snapshot'] != true). 137 | toList(); 138 | } 139 | 140 | List get _projectsIncludingSnapshots { 141 | if (_projects == null) return []; 142 | 143 | return _projects. 144 | values. 145 | toList(). 146 | reversed. 147 | toList(); 148 | } 149 | 150 | List get _projectSnapshots { 151 | return _projectsIncludingSnapshots. 152 | where((p)=> p['snapshot'] == true). 153 | toList(); 154 | } 155 | 156 | 157 | /// Refresh projects data from localStorage. 158 | void refresh() { 159 | _projects = deserialize(); 160 | } 161 | 162 | bool _frozen = false; 163 | /// Prevent further syncs to localStorage 164 | void freeze() { _frozen = true; } 165 | 166 | void _sync() { 167 | if (_frozen) return; 168 | if (show_snapshots) return; 169 | 170 | window.localStorage[storage_key] = JSON.encode(_projectsIncludingSnapshots); 171 | _syncController.add(true); 172 | } 173 | 174 | /// Stream that will see events whenever data is synchronized with 175 | /// localStorage (create, update, delete). 176 | Stream get onSync => _syncController.stream.asBroadcastStream(); 177 | StreamController __syncController; 178 | StreamController get _syncController { 179 | if (__syncController != null) return __syncController; 180 | return __syncController = new StreamController(); 181 | } 182 | 183 | // _migrateFromTitleIdToFilename() { 184 | // if (currentProject.containsKey(title)) return; 185 | // _projects = projects.map((p) { 186 | // p[title] = p['title']; 187 | // return p; 188 | // }). 189 | // toList();; 190 | // _sync(); 191 | // } 192 | } 193 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: ice_code_editor 2 | version: 1.0.0 3 | authors: 4 | - Chris Strom 5 | - Santiago Arias 6 | - James Hurford 7 | - Srdjan Pejic 8 | description: Code Editor + Preview 9 | homepage: https://github.com/eee-c/ice-code-editor 10 | dependencies: 11 | browser: '>=0.10.0 <0.11.0' 12 | ctrl_alt_foo: '>=0.3.1 <0.3.2' 13 | ace: ">=0.5.0 <0.6.0" 14 | # polymer: any 15 | dev_dependencies: 16 | test: any 17 | transformers: 18 | # - polymer: 19 | # entry_points: web/polymer.html 20 | - test/pub_serve: 21 | $include: test/ice_test.dart 22 | -------------------------------------------------------------------------------- /test/editor_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | editor_tests() { 4 | group("defaults", () { 5 | var el; 6 | setUp(() { 7 | el = new Element.html('
        '); 8 | document.body.nodes.add(el); 9 | }); 10 | tearDown(()=> el.remove()); 11 | 12 | test("defaults to auto-update the preview", () { 13 | var it = new Editor(el); 14 | expect(it.autoupdate, equals(true)); 15 | expect(it.editorReady, completes); 16 | }); 17 | 18 | test("starts an ACE instance", (){ 19 | var it = new Editor(el); 20 | it.editorReady.then( 21 | expectAsync((_) { 22 | expect(document.query('.ace_content'), isNotNull); 23 | }) 24 | ); 25 | }); 26 | 27 | test("defaults to disable edit-only mode", () { 28 | var it = new Editor(el); 29 | expect(it.edit_only, equals(false)); 30 | expect(it.editorReady, completes); 31 | }); 32 | 33 | }); 34 | 35 | group("content", () { 36 | var el; 37 | setUp(() { 38 | el = new DivElement(); 39 | document.body.append(el); 40 | }); 41 | tearDown(()=> el.remove()); 42 | 43 | test("can set the content", () { 44 | var it = new Editor(el); 45 | 46 | it.content = 'asdf'; 47 | 48 | it.editorReady.then( 49 | expectAsync((_) { 50 | expect(it.content, equals('asdf')); 51 | }) 52 | ); 53 | 54 | }); 55 | }); 56 | 57 | group("focus", (){ 58 | var el, editor; 59 | 60 | setUp((){ 61 | el = new DivElement(); 62 | document.body.append(el); 63 | 64 | editor = new Editor(el)..content = '

        preview

        '; 65 | 66 | var preview_ready = new Completer(); 67 | editor.onPreviewChange.listen((e){ 68 | if (preview_ready.isCompleted) return; 69 | preview_ready.complete(); 70 | }); 71 | return preview_ready.future; 72 | }); 73 | 74 | tearDown(()=> el.remove()); 75 | 76 | test('code is visibile by default', (){ 77 | var _el = el.query('.ice-code-editor-editor'); 78 | 79 | expect(_el.style.visibility, isNot('hidden')); 80 | }); 81 | 82 | test('focus goes to code if code is visibile', (){ 83 | var _el = el. 84 | query('.ice-code-editor-editor'). 85 | query('textarea.ace_text-input'); 86 | 87 | expect(document.activeElement, _el); 88 | }); 89 | 90 | test('focus goes to preview if code is hidden', (){ 91 | editor.hideCode(); 92 | 93 | var _el = el.query('iframe'); 94 | 95 | expect(document.activeElement, _el); 96 | }); 97 | 98 | group("hide code after an update", (){ 99 | setUp((){ 100 | editor.focus(); 101 | editor.content = '

        Force Update

        '; 102 | editor.hideCode(); 103 | 104 | var preview_ready = new Completer(); 105 | editor.onPreviewChange.listen((e){ 106 | preview_ready.complete(); 107 | }); 108 | return preview_ready.future; 109 | }); 110 | 111 | test('focus goes to preview if code is hidden', (){ 112 | var _el = el.query('iframe'); 113 | 114 | expect(document.activeElement, _el); 115 | }); 116 | }); 117 | // focus goes to preview if code is hidden 118 | // focus goes to preview after update if code is hidden 119 | }); 120 | 121 | group("line number", (){ 122 | var el, editor; 123 | 124 | setUp((){ 125 | el = new DivElement(); 126 | document.body.append(el); 127 | 128 | editor = new Editor(el) 129 | ..content = ''' 130 | Code line 01 131 | Code line 02 132 | Code line 03 133 | Code line 04 134 | Code line 05 135 | Code line 06 136 | Code line 07'''; 137 | 138 | var preview_ready = new Completer(); 139 | editor.onPreviewChange.listen((e){ 140 | if (preview_ready.isCompleted) return; 141 | preview_ready.complete(); 142 | }); 143 | return preview_ready.future; 144 | }); 145 | 146 | tearDown(()=> el.remove()); 147 | 148 | test("defaults to 1", (){ 149 | expect(editor.lineNumber, 1); 150 | }); 151 | 152 | test("can be changed", (){ 153 | editor.lineNumber = 6; 154 | expect(editor.lineNumber, 6); 155 | }); 156 | 157 | test("can read current content", (){ 158 | editor.lineNumber = 6; 159 | expect(editor.lineContent, 'Code line 06'); 160 | }); 161 | }); 162 | } 163 | -------------------------------------------------------------------------------- /test/embed.html: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /test/full/copy_dialog_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | copy_dialog_tests() { 4 | group("Copy Dialog", (){ 5 | var editor; 6 | 7 | setUp((){ 8 | editor = new Full() 9 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 10 | 11 | editor.store 12 | ..clear() 13 | ..['Current Project'] = {'code': 'Test'}; 14 | 15 | return editor.editorReady; 16 | }); 17 | 18 | tearDown(() { 19 | editor.remove(); 20 | editor.store..clear()..freeze(); 21 | }); 22 | 23 | test("can open copy dialog", (){ 24 | helpers.click('button', text: '☰'); 25 | helpers.click('li', text: 'Make a Copy'); 26 | 27 | expect( 28 | queryAll('button'), 29 | helpers.elementsContain('Save') 30 | ); 31 | }); 32 | 33 | test("project name field has focus", (){ 34 | helpers.click('button', text: '☰'); 35 | helpers.click('li', text: 'Make a Copy'); 36 | 37 | expect( 38 | query('.ice-dialog input[type=text]'), 39 | equals(document.activeElement) 40 | ); 41 | }); 42 | 43 | test("hitting the enter key saves", (){ 44 | helpers.click('button', text: '☰'); 45 | helpers.click('li', text: 'Make a Copy'); 46 | 47 | helpers.typeIn('My Copied Project'); 48 | helpers.hitEnter(); 49 | 50 | helpers.click('button', text: '☰'); 51 | helpers.click('li', text: 'Open'); 52 | 53 | expect( 54 | queryAll('div'), 55 | helpers.elementsContain('My Copied Project') 56 | ); 57 | }); 58 | 59 | test("works with existing projects", (){ 60 | helpers.click('button', text: '☰'); 61 | helpers.click('li', text: 'New'); 62 | 63 | helpers.typeIn('Project #1'); 64 | 65 | helpers.click('button', text: 'Save'); 66 | 67 | editor.content = 'Code #1'; 68 | helpers.click('button', text: '☰'); 69 | helpers.click('li', text: 'Save'); 70 | 71 | helpers.click('button', text: '☰'); 72 | helpers.click('li', text: 'Make a Copy'); 73 | 74 | helpers.typeIn('Copy of Project #1'); 75 | helpers.click('button', text: 'Save'); 76 | 77 | editor.content = 'Code #2'; 78 | helpers.click('button', text: '☰'); 79 | helpers.click('li', text: 'Save'); 80 | 81 | helpers.click('button', text: '☰'); 82 | helpers.click('li', text: 'Open'); 83 | helpers.click('li', text: 'Project #1'); 84 | 85 | expect( 86 | editor.content, 87 | equals('Code #1') 88 | ); 89 | 90 | helpers.click('button', text: '☰').then(() { 91 | helpers.click('li', text: 'Open').then(() { 92 | helpers.click('li', text: 'Copy of Project #1'); 93 | }); 94 | }); 95 | 96 | expect( 97 | editor.content, 98 | equals('Code #2') 99 | ); 100 | }); 101 | 102 | test("project name field is pre-populated", (){ 103 | helpers.click('button', text: '☰'); 104 | helpers.click('li', text: 'New'); 105 | 106 | helpers.typeIn('Project #1'); 107 | helpers.click('button', text: 'Save'); 108 | 109 | helpers.click('button', text: '☰'); 110 | helpers.click('li', text: 'Make a Copy'); 111 | 112 | expect( 113 | query('.ice-dialog input[type=text]').value, 114 | equals("Project #1 (1)") 115 | ); 116 | }); 117 | 118 | test("copied project includes copy number in parentheses", (){ 119 | helpers.createProject('Project #1'); 120 | 121 | helpers.click('button', text: '☰'); 122 | helpers.click('li', text: 'Make a Copy'); 123 | 124 | expect( 125 | query('.ice-dialog input[type=text]').value, 126 | equals("Project #1 (1)") 127 | ); 128 | }); 129 | 130 | test("project name ending in parens", (){ 131 | helpers.createProject('projectNamedForFunction()'); 132 | 133 | helpers.click('button', text: '☰'); 134 | helpers.click('li', text: 'Make a Copy'); 135 | 136 | expect( 137 | query('.ice-dialog input[type=text]').value, 138 | equals("projectNamedForFunction() (1)") 139 | ); 140 | }); 141 | 142 | test("project name field is pre-populated", (){ 143 | helpers.click('button', text: '☰'); 144 | helpers.click('li', text: 'New'); 145 | 146 | helpers.typeIn('Foo'); 147 | helpers.click('button', text: 'Save'); 148 | 149 | helpers.click('button', text: '☰'); 150 | helpers.click('li', text: 'Make a Copy'); 151 | helpers.click('button', text: 'Save'); 152 | 153 | helpers.click('button', text: '☰'); 154 | helpers.click('li', text: 'Open'); 155 | helpers.click('li', text: 'Foo'); 156 | 157 | helpers.click('button', text: '☰'); 158 | helpers.click('li', text: 'Make a Copy'); 159 | 160 | expect( 161 | query('.ice-dialog input[type=text]').value, 162 | equals("Foo (2)") 163 | ); 164 | }); 165 | 166 | test("project name field is incremented with multiple tests", (){ 167 | helpers.click('button', text: '☰'); 168 | helpers.click('li', text: 'New'); 169 | 170 | helpers.typeIn('Project #1'); 171 | helpers.click('button', text: 'Save'); 172 | 173 | helpers.click('button', text: '☰'); 174 | helpers.click('li', text: 'Make a Copy'); 175 | helpers.click('button', text: 'Save'); 176 | 177 | helpers.click('button', text: '☰'); 178 | helpers.click('li', text: 'Make a Copy'); 179 | 180 | expect( 181 | query('.ice-dialog input[type=text]').value, 182 | equals("Project #1 (2)") 183 | ); 184 | }); 185 | 186 | test("cannot have a duplicate name", () { 187 | helpers.createProject('Project #1'); 188 | 189 | //a duplicate 190 | helpers.click('button', text: '☰'); 191 | helpers.click('li', text: 'Make a Copy'); 192 | helpers.typeIn('Project #1'); 193 | helpers.click('button', text: 'Save'); 194 | 195 | expect( 196 | query('#alert').text, 197 | "There is already a project with that name" 198 | ); 199 | }); 200 | 201 | test("cannot have a blank name", () { 202 | helpers.click('button', text: '☰'); 203 | helpers.click('li', text: 'Make a Copy'); 204 | helpers.typeIn(' '); 205 | helpers.click('button', text: 'Save'); 206 | 207 | expect( 208 | query('#alert').text, 209 | 'The project name cannot be blank' 210 | ); 211 | }); 212 | }); 213 | 214 | group('Copy Dialog in Snapshot mode', () { 215 | var editor; 216 | 217 | setUp((){ 218 | var store = new Store(storage_key: "ice-test-${currentTestCase.id}"); 219 | store['SNAPSHOT: Saved Project (2014-11-03 16:58)'] = {'code': 'asdf', 'snapshot': true}; 220 | 221 | editor = new Full(mode: 'snapshot') 222 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 223 | 224 | return editor.editorReady.then((_) { 225 | helpers.click('button', text: '☰'); 226 | helpers.click('li', text: 'Make a Copy'); 227 | helpers.typeIn('Project #1'); 228 | helpers.click('button', text: 'Save'); 229 | }); 230 | }); 231 | 232 | tearDown(() { 233 | editor.remove(); 234 | editor.store..clear()..freeze(); 235 | }); 236 | 237 | test('creates a new Project', () { 238 | helpers.click('button', text: '☰'); 239 | helpers.click('li', text: 'Open'); 240 | 241 | expect( 242 | queryAll('div'), 243 | helpers.elementsContain('Project #1') 244 | ); 245 | }); 246 | 247 | test('leaves snapshot mode', () { 248 | expect(window.location.search, ''); 249 | }, skip: "Cannot test window.location changes with current test runner"); 250 | }); 251 | 252 | } 253 | -------------------------------------------------------------------------------- /test/full/download_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | download_tests() { 4 | group("Download", () { 5 | var editor; 6 | 7 | setUp((){ 8 | editor = new Full() 9 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 10 | 11 | editor.store 12 | ..clear() 13 | ..['Current Project'] = {'code': 'Test'}; 14 | 15 | return editor.editorReady; 16 | }); 17 | 18 | tearDown(() { 19 | editor.remove(); 20 | editor.store..clear()..freeze(); 21 | }); 22 | 23 | test("it downloads the source as a file", (){ 24 | helpers.createProject( 25 | "Downloadable one", 26 | content: "This is some content, all right.", 27 | editor: editor 28 | ); 29 | 30 | var el = new DownloadDialog(editor).el; 31 | expect(el.download, equals("Downloadable one")); 32 | expect(el.href, startsWith("blob:")); 33 | }); 34 | 35 | test("closes the main menu", () { 36 | helpers.createProject("My Downloadable Project"); 37 | helpers.click('button', text: '☰'); 38 | helpers.click('li', text: 'Download'); 39 | 40 | expect(queryAll('li'), helpers.elementsAreEmpty); 41 | }); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /test/full/export_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | export_tests() { 4 | group("Export", () { 5 | var editor; 6 | 7 | setUp((){ 8 | editor = new Full() 9 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 10 | 11 | editor.store 12 | ..clear() 13 | ..['Project 01'] = {'code': 'Test 01'} 14 | ..['Project 02'] = {'code': 'Test 02'} 15 | ..['Project 03'] = {'code': 'Test 03'}; 16 | 17 | return editor.editorReady; 18 | }); 19 | 20 | tearDown(() { 21 | editor.remove(); 22 | editor.store..clear()..freeze(); 23 | }); 24 | 25 | test("it downloads the source as a file", (){ 26 | var el = new ExportDialog(editor).el; 27 | expect(el.download, equals("Export")); 28 | expect(el.href, startsWith("blob:")); 29 | }); 30 | 31 | test("closes the main menu", () { 32 | helpers.click('button', text: '☰'); 33 | helpers.click('li', text: 'Export'); 34 | 35 | expect(queryAll('li'), helpers.elementsAreEmpty); 36 | }); 37 | 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /test/full/hide_button_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | hide_button_tests() { 4 | group("Hide Button", (){ 5 | var editor; 6 | 7 | setUp((){ 8 | editor = new Full() 9 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 10 | 11 | editor.store 12 | ..clear() 13 | ..['Current Project'] = {'code': 'Test'}; 14 | 15 | return editor.editorReady; 16 | }); 17 | 18 | tearDown(() { 19 | editor.remove(); 20 | editor.store..clear()..freeze(); 21 | }); 22 | 23 | test("hides code", () { 24 | helpers.click('button', text: 'Hide Code'); 25 | expect( 26 | query('.ice-code-editor-editor').style.visibility, 27 | equals('hidden') 28 | ); 29 | }); 30 | 31 | test("hides itself", (){ 32 | helpers.click('button', text: 'Hide Code'); 33 | expect( 34 | helpers.queryWithContent('button', 'Hide Code').style.display, 35 | equals('none') 36 | ); 37 | }); 38 | 39 | test("hides main menu button", (){ 40 | helpers.click('button', text: 'Hide Code'); 41 | expect( 42 | helpers.queryWithContent('button', '☰').style.display, 43 | equals('none') 44 | ); 45 | }); 46 | 47 | test("hides update button", (){ 48 | helpers.click('button', text: 'Hide Code'); 49 | expect( 50 | helpers.queryWithContent('button', ' Update').style.display, 51 | equals('none') 52 | ); 53 | }); 54 | 55 | test("shows the show button", (){ 56 | helpers.click('button', text: 'Hide Code'); 57 | expect( 58 | helpers.queryWithContent('button', 'Show Code').style.display, 59 | equals('') 60 | ); 61 | }); 62 | 63 | // TODO: show button shows everything 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /test/full/image_upload_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | image_upload_tests() { 4 | group("Image Upload Dialog", () { 5 | var editor; 6 | 7 | setUp((){ 8 | editor = new Full() 9 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 10 | 11 | return editor.editorReady; 12 | }); 13 | 14 | tearDown(() { 15 | editor.remove(); 16 | editor.store..clear()..freeze(); 17 | }); 18 | 19 | test("First imported project should be at the top of project list", (){ 20 | var dialog = new ImageUplaodDialog(editor) 21 | ..import('asdf', 'asdf.png'); 22 | 23 | helpers.click('button', text: '☰'); 24 | helpers.click('li', text: 'List Uploaded Images'); 25 | var menu_items = queryAll('li'); 26 | expect(menu_items[0].text, '/3de/adsf.png'); 27 | }); 28 | }); 29 | } -------------------------------------------------------------------------------- /test/full/import_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | import_tests() { 4 | group("Import Dialog", () { 5 | var editor; 6 | 7 | setUp((){ 8 | editor = new Full() 9 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 10 | 11 | editor.store 12 | ..clear() 13 | ..['Project #2'] = {'code': 'Original Project #2'}; 14 | 15 | return editor.editorReady; 16 | }); 17 | 18 | tearDown(() { 19 | editor.remove(); 20 | editor.store..clear()..freeze(); 21 | }); 22 | 23 | // Delete this (only left on the off chance it might help 24 | // when starting the next test.... 25 | // test("it downloads the source as a file", (){ 26 | // var el = new ImportDialog(editor).el; 27 | // expect(el.download, equals("Import")); 28 | // expect(el.href, startsWith("blob:")); 29 | // }); 30 | 31 | test("closes the main menu", () { 32 | helpers.click('button', text: '☰'); 33 | helpers.click('li', text: 'Import'); 34 | 35 | expect(queryAll('li'), helpers.elementsAreEmpty); 36 | }); 37 | 38 | final json = '''[{ 39 | "filename":"Project #1", 40 | "code":"imported code", 41 | "lineNumber":0, 42 | "updated_at":"2014-01-02 00:00:00.000", 43 | "created_at":"2013-01-01 00:00:00.000" 44 | }, 45 | { 46 | "filename":"Project #2", 47 | "code":"imported code", 48 | "lineNumber":0, 49 | "updated_at":"2014-01-01 00:00:00.000", 50 | "created_at":"2013-01-01 00:00:00.000" 51 | }]'''; 52 | 53 | test("First imported project should be at the top of project list", (){ 54 | var dialog = new ImportDialog(editor) 55 | ..import(json); 56 | 57 | helpers.click('button', text: '☰'); 58 | helpers.click('li', text: 'Open'); 59 | 60 | var menu_items = queryAll('li'); 61 | expect(menu_items[0].text, 'Project #1'); 62 | }); 63 | 64 | test("afterwards, first project should be opened", (){ 65 | var dialog = new ImportDialog(editor) 66 | ..import(json); 67 | 68 | expect( 69 | editor.content, 70 | equals('imported code') 71 | ); 72 | }); 73 | 74 | test("shouldn't clobber existing projects with the same name", (){ 75 | var dialog = new ImportDialog(editor) 76 | ..import(json); 77 | 78 | expect( 79 | editor.store['Project #2']['code'], 80 | equals('Original Project #2') 81 | ); 82 | }); 83 | 84 | test("it still imports the project as a copy of the original project", () { 85 | var dialog = new ImportDialog(editor) 86 | ..import(json); 87 | 88 | expect( 89 | editor.store['Project #2 (1)']['code'], 90 | equals('imported code') 91 | ); 92 | }); 93 | 94 | 95 | test("alerts user if they import non json", () { 96 | var dialog = new ImportDialog(editor) 97 | ..import("not JSON"); 98 | 99 | expect( 100 | query('#alert').text, 101 | matches('This does not look like a ICE project file. Unable to import.') 102 | ); 103 | }); 104 | }); 105 | } 106 | -------------------------------------------------------------------------------- /test/full/keyboard_shortcuts_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | keyboard_shortcuts_tests() { 4 | group("Keyboard Shortcuts", (){ 5 | var editor; 6 | 7 | setUp((){ 8 | editor = new Full() 9 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 10 | 11 | editor.store.clear(); 12 | 13 | new Iterable.generate(12, (i){ 14 | editor.store['Project ${i}'] = {'code': 'code ${i}'}; 15 | }).toList(); 16 | 17 | editor.store 18 | ..['Old Project'] = {'code': 'Old'} 19 | ..['Current Project'] = {'code': 'Current'}; 20 | 21 | return editor.editorReady; 22 | }); 23 | 24 | tearDown(() { 25 | editor.remove(); 26 | editor.store..clear()..freeze(); 27 | }); 28 | 29 | 30 | group("opening the new dialog", (){ 31 | setUp((){ 32 | var preview_ready = new Completer(); 33 | editor.onPreviewChange.listen((e){ 34 | preview_ready.complete(); 35 | }); 36 | return preview_ready.future; 37 | }); 38 | 39 | test("can open the new dialog", (){ 40 | helpers.typeCtrl('n'); 41 | expect( 42 | queryAll('button'), 43 | helpers.elementsContain('Save') 44 | ); 45 | }); 46 | 47 | test("opening new project gives input field focus", (){ 48 | helpers.typeCtrl('n'); 49 | 50 | var wait = new Duration(milliseconds: 500); 51 | new Timer(wait, expectAsync((){ 52 | expect( 53 | document.activeElement, 54 | equals(query('.ice-dialog input[type=text]')) 55 | ); 56 | })); 57 | }); 58 | }); 59 | 60 | group("Open Projects Dialog", (){ 61 | test("can open the project dialog", (){ 62 | helpers.typeCtrl("o"); 63 | expect( 64 | queryAll('div'), 65 | helpers.elementsContain('Saved Projects') 66 | ); 67 | }); 68 | 69 | test("hitting enter with no filter text does nothing", (){ 70 | helpers.typeCtrl('o'); 71 | helpers.hitEnter(); 72 | 73 | expect( 74 | editor.content, 75 | equals('Current') 76 | ); 77 | }); 78 | 79 | test("enter opens the top project", (){ 80 | helpers.typeCtrl('o'); 81 | helpers.typeIn('old'); 82 | helpers.hitEnter(); 83 | 84 | expect( 85 | editor.content, 86 | equals('Old') 87 | ); 88 | }); 89 | 90 | test("tab key moves forward in list", (){ 91 | helpers.typeCtrl('o'); 92 | helpers.typeIn('old'); 93 | 94 | expect( 95 | queryAll('li').where((e)=> e.tabIndex==0).first.text, 96 | equals('Old Project') 97 | ); 98 | }); 99 | 100 | test("down arrow key moves forward in list", (){ 101 | helpers.typeCtrl('O'); 102 | helpers.typeIn('project 1'); 103 | 104 | // project 11 105 | // project 10 * 106 | // project 1 107 | 108 | helpers.arrowDown(2); 109 | 110 | expect( 111 | document.activeElement.text, 112 | equals('Project 10') 113 | ); 114 | }); 115 | 116 | test("up arrow key moves backward in list", (){ 117 | helpers.typeCtrl('o'); 118 | helpers.typeIn('project 1'); 119 | // project 11 120 | // project 10 * 121 | // project 1 122 | 123 | helpers.arrowDown(3); 124 | helpers.arrowUp(); 125 | 126 | expect( 127 | document.activeElement.text, 128 | equals('Project 10') 129 | ); 130 | }); 131 | 132 | test("up arrow at top of list moves back into filter", (){ 133 | helpers.typeCtrl('o'); 134 | helpers.typeIn('project 1'); 135 | // *filter* 136 | // project 11 137 | // project 10 138 | // project 1 139 | 140 | helpers.arrowDown(); 141 | helpers.arrowUp(); 142 | 143 | expect( 144 | document.activeElement.tagName, 145 | 'INPUT' 146 | ); 147 | }); 148 | 149 | group("With Less Than 10 Projects", (){ 150 | setUp((){ 151 | editor.store 152 | ..remove('Project 2') 153 | ..remove('Project 3') 154 | ..remove('Project 4') 155 | ..remove('Project 5') 156 | ..remove('Project 6') 157 | ..remove('Project 7'); 158 | }); 159 | test("first project has keyboard focus", (){ 160 | helpers.typeCtrl('o'); 161 | // Current Project 162 | // Old Project 163 | // project 11 164 | // ... 165 | 166 | expect( 167 | document.activeElement.text, 168 | equals('Current Project') 169 | ); 170 | }); 171 | 172 | test("up arrow at top of list stays at top of list", (){ 173 | helpers.typeCtrl('o'); 174 | // Current Project 175 | // Old Project 176 | // project 11 177 | // ... 178 | helpers.arrowUp(); 179 | 180 | expect( 181 | document.activeElement.text, 182 | equals('Current Project') 183 | ); 184 | }); 185 | 186 | test("down arrow at bottom of list stays at bottom of list", (){ 187 | helpers.typeCtrl('O'); 188 | // Current Project 189 | // Old Project 190 | // project 11 191 | // ... 192 | helpers.arrowDown(11); 193 | 194 | expect( 195 | document.activeElement.text, 196 | equals('Project 0') 197 | ); 198 | }); 199 | }); 200 | }); 201 | 202 | test("opening project dialog after new dialog closes the first", (){ 203 | helpers.typeCtrl('N'); 204 | helpers.typeCtrl('O'); 205 | 206 | expect( 207 | queryAll('button'), 208 | helpers.elementsDoNotContain('Save') 209 | ); 210 | }); 211 | 212 | test("opening new dialog after open dialog closes the first", (){ 213 | helpers.typeCtrl('O'); 214 | helpers.typeCtrl('N'); 215 | 216 | expect( 217 | queryAll('div'), 218 | helpers.elementsDoNotContain('Saved Projects') 219 | ); 220 | }); 221 | 222 | test("toggling code after new dialog closes the dialog", (){ 223 | helpers.typeCtrl('N'); 224 | helpers.typeCtrlShift('H'); 225 | 226 | expect( 227 | queryAll('button'), 228 | helpers.elementsDoNotContain('Save') 229 | ); 230 | }); 231 | }); 232 | 233 | group("Toggling the code editor", (){ 234 | var editor; 235 | 236 | setUp((){ 237 | editor = new Full() 238 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 239 | 240 | editor.store.clear(); 241 | 242 | editor.store 243 | ..['Old Project'] = {'code': 'Old'} 244 | ..['Current Project'] = {'code': 'Current'}; 245 | 246 | return editor.editorReady; 247 | }); 248 | 249 | tearDown(() { 250 | editor.remove(); 251 | editor.store..clear()..freeze(); 252 | }); 253 | 254 | test("with hotkey", (){ 255 | helpers.typeCtrlShift('H'); 256 | expect( 257 | query('.ice-code-editor-editor').style.visibility, 258 | 'hidden' 259 | ); 260 | }); 261 | 262 | test("with post message", () { 263 | var url = new RegExp(r'^file://').hasMatch(window.location.href) 264 | ? '*': window.location.href; 265 | editor.hideCode(); 266 | window.postMessage('showCode', url); 267 | Timer.run(expectAsync( () { 268 | expect( 269 | query('.ice-code-editor-editor').style.visibility, 270 | 'visible' 271 | ); 272 | })); 273 | }); 274 | }); 275 | } 276 | -------------------------------------------------------------------------------- /test/full/new_project_dialog_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | new_project_dialog_tests(){ 4 | group("New Project Dialog", (){ 5 | var editor; 6 | 7 | setUp((){ 8 | editor = new Full() 9 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 10 | 11 | editor.store 12 | ..clear() 13 | ..['Current Project'] = {'code': 'Test'}; 14 | 15 | return editor.editorReady; 16 | }); 17 | 18 | tearDown(() { 19 | editor.remove(); 20 | editor.store..clear()..freeze(); 21 | }); 22 | 23 | // TODO: see keyboard shortcut tests for more realistic setup and apply same 24 | // approach here. 25 | test("new project input field has focus", (){ 26 | helpers.click('button', text: '☰'); 27 | helpers.click('li', text: 'New'); 28 | 29 | expect( 30 | query('.ice-dialog input[type=text]'), 31 | equals(document.activeElement) 32 | ); 33 | }); 34 | 35 | group("after preview is rendered", (){ 36 | setUp((){ 37 | var preview_ready = new Completer(); 38 | editor.onPreviewChange.listen((e){ 39 | preview_ready.complete(); 40 | }); 41 | return preview_ready.future; 42 | }); 43 | 44 | test("input field retains focus if nothing else happens", (){ 45 | helpers.click('button', text: '☰'); 46 | helpers.click('li', text: 'New'); 47 | 48 | Timer.run(expectAsync((){ 49 | expect( 50 | document.activeElement, 51 | equals(query('.ice-dialog input[type=text]')) 52 | ); 53 | })); 54 | }); 55 | }); 56 | 57 | test("cannot have a duplicate name", () { 58 | helpers.click('button', text: '☰'); 59 | helpers.click('li', text: 'New'); 60 | 61 | helpers.typeIn('My New Project'); 62 | 63 | helpers.click('button', text: 'Save'); 64 | 65 | //a duplicate 66 | helpers.click('button', text: '☰'); 67 | helpers.click('li', text: 'New'); 68 | 69 | helpers.typeIn('My New Project'); 70 | 71 | helpers.click('button', text: 'Save'); 72 | 73 | expect(query('#alert'). 74 | text, equals("There is already a project with that name")); 75 | }); 76 | 77 | test("cannot have a blank name", () { 78 | helpers.click('button', text: '☰'); 79 | helpers.click('li', text: 'New'); 80 | helpers.typeIn(' '); 81 | 82 | helpers.click('button', text: 'Save'); 83 | 84 | expect( 85 | query('#alert').text, 86 | 'The project name cannot be blank' 87 | ); 88 | }); 89 | 90 | test("can be named", (){ 91 | helpers.click('button', text: '☰'); 92 | helpers.click('li', text: 'New'); 93 | 94 | helpers.typeIn('My New Project'); 95 | 96 | helpers.click('button', text: 'Save'); 97 | 98 | editor.content = 'asdf'; 99 | 100 | helpers.click('button', text: '☰'); 101 | helpers.click('li', text: 'Save'); 102 | 103 | // TODO: check the projects menu (once implemented) 104 | expect(editor.store['My New Project'], isNotNull); 105 | expect(editor.store['My New Project']['code'], 'asdf'); 106 | }); 107 | 108 | test("clicking the new menu item closes the main menu", (){ 109 | helpers.click('button', text: '☰'); 110 | helpers.click('li', text: 'New'); 111 | 112 | expect(queryAll('li'), helpers.elementsAreEmpty); 113 | }); 114 | 115 | test("the escape key closes the new project dialog", (){ 116 | helpers.click('button', text: '☰'); 117 | helpers.click('li', text: 'New'); 118 | helpers.hitEscape(); 119 | 120 | expect( 121 | queryAll('button'), 122 | helpers.elementsDoNotContain('Save') 123 | ); 124 | }); 125 | 126 | test("hitting the enter key saves", (){ 127 | helpers.click('button', text: '☰'); 128 | helpers.click('li', text: 'New'); 129 | 130 | helpers.typeIn('My New Project'); 131 | helpers.hitEnter(); 132 | 133 | helpers.click('button', text: '☰'); 134 | helpers.click('li', text: 'Open'); 135 | 136 | expect( 137 | queryAll('div'), 138 | helpers.elementsContain('My New Project') 139 | ); 140 | }); 141 | 142 | test("creating a new project opens it immediately", (){ 143 | helpers.click('button', text: '☰'); 144 | helpers.click('li', text: 'New'); 145 | helpers.typeIn('My Project'); 146 | helpers.click('button', text: 'Save'); 147 | editor.content = 'asdf'; 148 | helpers.click('button', text: '☰'); 149 | helpers.click('li', text: 'Save'); 150 | 151 | helpers.click('button', text: '☰'); 152 | helpers.click('li', text: 'New'); 153 | helpers.typeIn('My New Project'); 154 | helpers.click('button', text: 'Save'); 155 | 156 | expect( 157 | editor.content, 158 | equals(Templates.threeD) 159 | ); 160 | }); 161 | 162 | test("has a select list of templates", (){ 163 | helpers.click('button', text: '☰'); 164 | helpers.click('li', text: 'New'); 165 | expect( 166 | query('.ice-dialog').queryAll('option'), 167 | helpers.elementsArePresent 168 | ); 169 | }); 170 | test("defaults to 3D starter project", (){ 171 | helpers.click('button', text: '☰'); 172 | helpers.click('li', text: 'New'); 173 | expect( 174 | query('.ice-dialog').queryAll('option[selected]'), 175 | helpers.elementsAreEmpty 176 | ); 177 | expect( 178 | query('.ice-dialog').query('option').text, 179 | '3D starter project' 180 | ); 181 | }); 182 | test("can create from default template", (){ 183 | helpers.click('button', text: '☰'); 184 | helpers.click('li', text: 'New'); 185 | helpers.typeIn('My Project'); 186 | helpers.click('button', text: 'Save'); 187 | 188 | expect( 189 | editor.content, 190 | equals(Templates.threeD) 191 | ); 192 | }); 193 | test("can create from any template", (){ 194 | helpers.click('button', text: '☰'); 195 | helpers.click('li', text: 'New'); 196 | helpers.typeIn('My Project'); 197 | 198 | query('.ice-dialog').query('select').value = 'Empty project'; 199 | 200 | helpers.click('button', text: 'Save'); 201 | 202 | expect( 203 | editor.content, 204 | equals(Templates.empty) 205 | ); 206 | }); 207 | 208 | }); 209 | // TODO: blank name behavior 210 | } 211 | -------------------------------------------------------------------------------- /test/full/notification_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | notification_tests(){ 4 | group("Notification System", (){ 5 | tearDown((){ queryAll('#alert').forEach((e)=> e.remove());}); 6 | 7 | test("can create alerts", (){ 8 | Notify.alert('Something went wrong'); 9 | 10 | expect( 11 | query('#alert').text, 12 | equals('Something went wrong') 13 | ); 14 | }); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /test/full/open_dialog_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | open_dialog_tests() { 4 | group("Open Dialog", (){ 5 | var editor; 6 | 7 | setUp((){ 8 | editor = new Full() 9 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 10 | 11 | editor.store 12 | ..clear() 13 | ..['Old Project'] = {'code': 'Old Test'} 14 | ..['Current Project'] = {'code': 'Test'}; 15 | 16 | return editor.editorReady; 17 | }); 18 | tearDown(() { 19 | editor.remove(); 20 | editor.store..clear()..freeze(); 21 | }); 22 | 23 | test("clicking the project menu item opens the project dialog", (){ 24 | helpers.click('button', text: '☰'); 25 | helpers.click('li', text: 'Open'); 26 | 27 | expect( 28 | queryAll('div'), 29 | helpers.elementsContain('Saved Projects') 30 | ); 31 | }); 32 | 33 | test("clicking the project menu item closes the main menu", (){ 34 | helpers.click('button', text: '☰'); 35 | helpers.click('li', text: 'Open'); 36 | 37 | expect( 38 | queryAll('li'), 39 | helpers.elementsDoNotContain('Help') 40 | ); 41 | }); 42 | 43 | test("the escape key closes the project dialog", (){ 44 | helpers.click('button', text: '☰'); 45 | helpers.click('li', text: 'Open'); 46 | helpers.hitEscape(); 47 | 48 | expect( 49 | queryAll('div'), 50 | helpers.elementsDoNotContain(new RegExp('Saved')) 51 | ); 52 | }); 53 | 54 | test("the menu button closes the projects dialog", (){ 55 | helpers.click('button', text: '☰'); 56 | helpers.click('li', text: 'Open'); 57 | helpers.click('button', text: '☰'); 58 | 59 | expect( 60 | queryAll('div'), 61 | helpers.elementsDoNotContain('Saved Projects') 62 | ); 63 | }); 64 | 65 | test("lists project names", (){ 66 | helpers.click('button', text: '☰'); 67 | helpers.click('li', text: 'New'); 68 | 69 | helpers.typeIn('My New Project'); 70 | 71 | helpers.click('button', text: 'Save'); 72 | 73 | helpers.click('button', text: '☰'); 74 | helpers.click('li', text: 'Open'); 75 | 76 | expect( 77 | queryAll('div'), 78 | helpers.elementsContain('My New Project') 79 | ); 80 | }); 81 | 82 | test("includes the create and update date", (){ 83 | helpers.click('button', text: '☰'); 84 | helpers.click('li', text: 'New'); 85 | helpers.typeIn('My New Project'); 86 | helpers.click('button', text: 'Save'); 87 | 88 | helpers.click('button', text: '☰'); 89 | helpers.click('li', text: 'Open'); 90 | 91 | String today = new DateTime.now().toString().substring(0, 10); 92 | expect( 93 | query('li').title, 94 | ''' 95 | Created: ${today} 96 | Last Updated: ${today}''' 97 | ); 98 | }); 99 | 100 | test("does not include timestamps if they are null", (){ 101 | // Give auto-save a chance to kick-in: 102 | Timer.run(expectAsync((){ 103 | editor.store 104 | ..['Current Project'] = {'code': 'Current', 'updated_at': null}; 105 | 106 | helpers.click('button', text: '☰'); 107 | helpers.click('li', text: 'Open'); 108 | 109 | expect( 110 | query('li').title, 111 | isEmpty 112 | ); 113 | })); 114 | }); 115 | 116 | test("does not change the created at timestamp", (){ 117 | var original = editor.store['Old Project']['created_at']; 118 | 119 | helpers.click('button', text: '☰'); 120 | helpers.click('li', text: 'Open'); 121 | helpers.click('li', text: 'Old Project'); 122 | 123 | expect( 124 | editor.store['Old Project']['created_at'], 125 | original 126 | ); 127 | }); 128 | 129 | test("click names to switch between projects", (){ 130 | helpers.click('button', text: '☰'); 131 | helpers.click('li', text: 'New'); 132 | 133 | helpers.typeIn('Project #1'); 134 | helpers.click('button', text: 'Save'); 135 | 136 | editor.content = 'Code #1'; 137 | helpers.click('button', text: '☰'); 138 | helpers.click('li', text: 'Save'); 139 | 140 | helpers.click('button', text: '☰'); 141 | helpers.click('li', text: 'New'); 142 | 143 | helpers.typeIn('Project #2'); 144 | helpers.click('button', text: 'Save'); 145 | 146 | editor.content = 'Code #2'; 147 | helpers.click('button', text: '☰'); 148 | helpers.click('li', text: 'Save'); 149 | 150 | helpers.click('button', text: '☰'); 151 | helpers.click('li', text: 'Open'); 152 | helpers.click('li', text: 'Project #1'); 153 | 154 | expect( 155 | editor.content, 156 | equals('Code #1') 157 | ); 158 | }); 159 | 160 | test("closes when a project is selected", (){ 161 | helpers.click('button', text: '☰'); 162 | helpers.click('li', text: 'New'); 163 | 164 | helpers.typeIn('Project #1'); 165 | helpers.click('button', text: 'Save'); 166 | 167 | helpers.click('button', text: '☰'); 168 | helpers.click('li', text: 'Open'); 169 | helpers.click('li', text: 'Project #1'); 170 | 171 | expect(queryAll('li'), helpers.elementsAreEmpty); 172 | }); 173 | }); 174 | 175 | group("Open Dialog Filter", (){ 176 | var editor; 177 | 178 | setUp((){ 179 | editor = new Full() 180 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 181 | 182 | editor.store.clear(); 183 | new Iterable.generate(12, (i){ 184 | editor.store['Project ${i}'] = {'code': 'code ${i}'}; 185 | }).toList(); 186 | 187 | return editor.editorReady; 188 | }); 189 | tearDown(() { 190 | editor.remove(); 191 | editor.store..clear()..freeze(); 192 | }); 193 | 194 | test("is not visible when there are fewer than 10 projects", (){ 195 | editor.store.clear(); 196 | new Iterable.generate(9, (i){ 197 | editor.store['Project ${i}'] = {'code': 'code ${i}'}; 198 | }).toList(); 199 | 200 | helpers.click('button', text: '☰'); 201 | helpers.click('li', text: 'Open'); 202 | 203 | expect( 204 | queryAll('.ice-menu input'), 205 | helpers.elementsAreEmpty 206 | ); 207 | }); 208 | test("is visible when there are 10 or more projects", (){ 209 | helpers.click('button', text: '☰'); 210 | helpers.click('li', text: 'Open'); 211 | 212 | expect( 213 | queryAll('.ice-menu input'), 214 | helpers.elementsArePresent 215 | ); 216 | }); 217 | test("has focus by default", (){ 218 | helpers.click('button', text: '☰'); 219 | helpers.click('li', text: 'Open'); 220 | 221 | expect( 222 | query('.ice-menu input'), 223 | document.activeElement 224 | ); 225 | }); 226 | test("can filter projects", (){ 227 | helpers.click('button', text: '☰'); 228 | helpers.click('li', text: 'Open'); 229 | 230 | document.activeElement 231 | ..dispatchEvent( 232 | new TextEvent('textInput', data: '1') 233 | ) 234 | ..dispatchEvent( 235 | new KeyboardEvent('keyup') 236 | ); 237 | 238 | expect( 239 | queryAll('.ice-menu li').length, 240 | 3 241 | ); 242 | }); 243 | test("is not case sensitive", (){ 244 | helpers.click('button', text: '☰'); 245 | helpers.click('li', text: 'Open'); 246 | 247 | document.activeElement 248 | ..dispatchEvent( 249 | new TextEvent('textInput', data: 'project 1') 250 | ) 251 | ..dispatchEvent( 252 | new KeyboardEvent('keyup') 253 | ); 254 | 255 | expect( 256 | queryAll('.ice-menu li').length, 257 | 3 258 | ); 259 | }); 260 | test("updates on new text input", (){ 261 | helpers.click('button', text: '☰'); 262 | helpers.click('li', text: 'Open'); 263 | 264 | document.activeElement 265 | ..dispatchEvent( 266 | new TextEvent('textInput', data: 'project 1') 267 | ) 268 | ..dispatchEvent( 269 | new KeyboardEvent('keyup') 270 | ); 271 | 272 | expect( 273 | queryAll('.ice-menu li').length, 274 | 3 275 | ); 276 | 277 | document.activeElement 278 | ..dispatchEvent( 279 | new TextEvent('textInput', data: '1') 280 | ) 281 | ..dispatchEvent( 282 | new KeyboardEvent('keyup') 283 | ); 284 | 285 | expect( 286 | queryAll('.ice-menu li').length, 287 | 1 288 | ); 289 | }); 290 | test("shows message when no projects match", (){ 291 | helpers.click('button', text: '☰'); 292 | helpers.click('li', text: 'Open'); 293 | 294 | document.activeElement 295 | ..dispatchEvent( 296 | new TextEvent('textInput', data: 'asdf') 297 | ) 298 | ..dispatchEvent( 299 | new KeyboardEvent('keyup') 300 | ); 301 | 302 | expect( 303 | query('.ice-menu li').text, 304 | contains('No matching projects') 305 | ); 306 | }); 307 | }); 308 | 309 | group("Switching Between Projects", (){ 310 | var editor; 311 | 312 | setUp((){ 313 | editor = new Full() 314 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 315 | 316 | editor.store 317 | ..clear() 318 | ..['Other Project'] = {'code': 'Test'} 319 | ..['Current Project'] = {'code': 'Test'}; 320 | 321 | return editor.editorReady; 322 | }); 323 | tearDown(() { 324 | editor.remove(); 325 | editor.store..clear()..freeze(); 326 | }); 327 | 328 | test("restores previous line number", (){ 329 | editor.content = ''' 330 | Code line 01 331 | Code line 02 332 | Code line 03 333 | Code line 04 334 | Code line 05 335 | Code line 06 336 | Code line 07 337 | '''; 338 | 339 | editor.lineNumber = 6; 340 | 341 | helpers.click('button', text: '☰'); 342 | helpers.click('li', text: 'Save'); 343 | 344 | helpers.click('button', text: '☰'); 345 | helpers.click('li', text: 'Open'); 346 | helpers.click('li', text: 'Other Project'); 347 | 348 | helpers.click('button', text: '☰'); 349 | helpers.click('li', text: 'Open'); 350 | helpers.click('li', text: 'Current Project'); 351 | 352 | expect(editor.lineNumber, 6); 353 | }); 354 | 355 | test("remembers last active before switch (even w/o save)", (){ 356 | editor.content = ''' 357 | Code line 01 358 | Code line 02 359 | Code line 03 360 | Code line 04 361 | Code line 05 362 | Code line 06 363 | Code line 07 364 | '''; 365 | 366 | helpers.click('button', text: '☰'); 367 | helpers.click('li', text: 'Save'); 368 | 369 | editor.lineNumber = 6; 370 | 371 | helpers.click('button', text: '☰'); 372 | helpers.click('li', text: 'Open'); 373 | helpers.click('li', text: 'Other Project'); 374 | 375 | helpers.click('button', text: '☰'); 376 | helpers.click('li', text: 'Open'); 377 | helpers.click('li', text: 'Current Project'); 378 | 379 | expect(editor.lineNumber, 6); 380 | }); 381 | 382 | }); 383 | } 384 | -------------------------------------------------------------------------------- /test/full/remove_dialog_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | remove_dialog_tests() { 4 | group("Remove Dialog", (){ 5 | var editor; 6 | 7 | setUp((){ 8 | editor = new Full() 9 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 10 | 11 | editor.store 12 | ..clear() 13 | ..['Current Project'] = {'code': 'Initial Test Code'}; 14 | 15 | return editor.editorReady; 16 | }); 17 | 18 | tearDown(() { 19 | editor.remove(); 20 | editor.store..clear()..freeze(); 21 | }); 22 | 23 | test("can open the remove dialog", (){ 24 | helpers.click('button', text: '☰'); 25 | helpers.click('li', text: 'New'); 26 | helpers.typeIn('My Old Project'); 27 | helpers.click('button', text: 'Save'); 28 | 29 | helpers.click('button', text: '☰'); 30 | helpers.click('li', text: 'New'); 31 | helpers.typeIn('My New Project'); 32 | helpers.click('button', text: 'Save'); 33 | 34 | helpers.click('button', text: '☰'); 35 | helpers.click('li', text: 'Remove'); 36 | 37 | helpers.click('button', text: '☰'); 38 | helpers.click('li', text: 'Open'); 39 | 40 | expect( 41 | queryAll('li'), 42 | helpers.elementsDoNotContain('My New Project') 43 | ); 44 | }); 45 | 46 | test("confirms that it's ok to remove", (){ 47 | helpers.click('button', text: '☰'); 48 | helpers.click('li', text: 'New'); 49 | helpers.typeIn('My Project'); 50 | helpers.click('button', text: 'Save'); 51 | 52 | helpers.click('button', text: '☰'); 53 | helpers.click('li', text: 'Remove'); 54 | 55 | expect( 56 | query('#confirmation').text, 57 | matches('Are you sure you want to remove this project?') 58 | ); 59 | }); 60 | 61 | test("open previous project on remove", (){ 62 | helpers.createProject( 63 | 'My Old Project', 64 | content: "Old content", 65 | editor: editor 66 | ); 67 | 68 | helpers.createProject( 69 | 'My New Project', 70 | content: "New content", 71 | editor: editor 72 | ); 73 | 74 | helpers.click('button', text: '☰'); 75 | helpers.click('li', text: 'Remove'); 76 | 77 | expect( 78 | editor.content, 79 | equals("Old content") 80 | ); 81 | 82 | }); 83 | 84 | test("open default project when no more projects exist", (){ 85 | helpers.createProject('My New Project'); 86 | 87 | helpers.click('button', text: '☰'); 88 | helpers.click('li', text: 'Remove'); 89 | 90 | expect( 91 | editor.content, 92 | matches("Initial Test Code") 93 | ); 94 | }); 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /test/full/rename_dialog_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | rename_dialog_tests() { 4 | group("Rename Dialog", (){ 5 | var editor; 6 | 7 | setUp((){ 8 | editor = new Full() 9 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 10 | 11 | editor.store 12 | ..clear() 13 | ..['Untitled'] = {'code': 'Test'}; 14 | 15 | return editor.editorReady; 16 | }); 17 | 18 | tearDown(() { 19 | editor.remove(); 20 | editor.settings.clear(); 21 | editor.store..clear()..freeze(); 22 | }); 23 | 24 | test("can open the rename dialog", (){ 25 | helpers.click('button', text: '☰'); 26 | 27 | helpers.click('li', text: 'New'); 28 | helpers.typeIn('My New Project'); 29 | helpers.click('button', text: 'Save'); 30 | 31 | helpers.click('button', text: '☰'); 32 | helpers.click('li', text: 'Rename'); 33 | 34 | expect( 35 | queryAll('button'), 36 | helpers.elementsContain('Rename') 37 | ); 38 | }); 39 | 40 | test("rename the first project as untitled", (){ 41 | helpers.click('button', text: '☰'); 42 | helpers.click('li', text: 'Rename'); 43 | 44 | expect( 45 | query('.ice-dialog input[type=text]').value, 46 | equals("Untitled") 47 | ); 48 | }); 49 | 50 | test("can rename a project", (){ 51 | helpers.click('button', text: '☰'); 52 | 53 | helpers.click('li', text: 'New'); 54 | helpers.typeIn('My New Project'); 55 | helpers.click('button', text: 'Save'); 56 | 57 | helpers.click('button', text: '☰'); 58 | helpers.click('li', text: 'Rename'); 59 | 60 | helpers.typeIn('Project #1'); 61 | helpers.click('button', text: 'Rename'); 62 | 63 | helpers.click('button', text: '☰'); 64 | helpers.click('li', text: 'Open'); 65 | 66 | expect( 67 | queryAll('li'), 68 | helpers.elementsContain('Project #1') 69 | ); 70 | }); 71 | 72 | test("rename input field has focus", (){ 73 | helpers.click('button', text: '☰'); 74 | helpers.click('li', text: 'Rename'); 75 | 76 | expect( 77 | query('.ice-dialog input[type=text]'), 78 | equals(document.activeElement) 79 | ); 80 | }); 81 | 82 | test("cannot have a duplicate name", () { 83 | helpers.click('button', text: '☰'); 84 | helpers.click('li', text: 'New'); 85 | 86 | helpers.typeIn('My Project #1'); 87 | helpers.click('button', text: 'Save'); 88 | 89 | helpers.click('button', text: '☰'); 90 | helpers.click('li', text: 'New'); 91 | 92 | helpers.typeIn('My Project #2'); 93 | helpers.click('button', text: 'Save'); 94 | 95 | //a duplicate 96 | helpers.click('button', text: '☰'); 97 | helpers.click('li', text: 'Rename'); 98 | 99 | helpers.typeIn('My Project #1'); 100 | helpers.click('button', text: 'Rename'); 101 | 102 | expect( 103 | query('#alert').text, 104 | equals("There is already a project with that name") 105 | ); 106 | }); 107 | 108 | test("cannot have a blank name", () { 109 | helpers.click('button', text: '☰'); 110 | helpers.click('li', text: 'Rename'); 111 | helpers.typeIn(' '); 112 | helpers.click('button', text: 'Rename'); 113 | 114 | expect( 115 | query('#alert').text, 116 | equals("The project name cannot be blank") 117 | ); 118 | }); 119 | 120 | test("hitting the enter key renames", (){ 121 | helpers.click('button', text: '☰'); 122 | 123 | helpers.click('li', text: 'New'); 124 | helpers.typeIn('My New Project'); 125 | helpers.click('button', text: 'Save'); 126 | 127 | helpers.click('button', text: '☰'); 128 | helpers.click('li', text: 'Rename'); 129 | 130 | helpers.typeIn('Project #1'); 131 | helpers.hitEnter(); 132 | 133 | helpers.click('button', text: '☰'); 134 | helpers.click('li', text: 'Open'); 135 | 136 | expect( 137 | queryAll('li'), 138 | helpers.elementsContain('Project #1') 139 | ); 140 | }); 141 | 142 | test("stays active after alert", () { 143 | helpers.click('button', text: '☰'); 144 | helpers.click('li', text: 'New'); 145 | 146 | helpers.typeIn('My Project #1'); 147 | helpers.click('button', text: 'Save'); 148 | 149 | helpers.click('button', text: '☰'); 150 | helpers.click('li', text: 'New'); 151 | 152 | helpers.typeIn('My Project #2'); 153 | helpers.click('button', text: 'Save'); 154 | 155 | //a duplicate 156 | helpers.click('button', text: '☰'); 157 | helpers.click('li', text: 'Rename'); 158 | 159 | helpers.typeIn('My Project #1'); 160 | helpers.click('button', text: 'Rename'); 161 | 162 | expect( 163 | queryAll('button'), 164 | helpers.elementsContain('Rename') 165 | ); 166 | }); 167 | }); 168 | } 169 | -------------------------------------------------------------------------------- /test/full/save_dialog_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | save_dialog_tests(){ 4 | group("Save Dialog", (){ 5 | var editor; 6 | 7 | setUp((){ 8 | editor = new Full() 9 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 10 | 11 | editor.store..clear()..['Saved Project'] = {'code': 'asdf'}; 12 | 13 | return editor.editorReady; 14 | }); 15 | 16 | tearDown(() { 17 | editor.remove(); 18 | editor.store..clear()..freeze(); 19 | }); 20 | 21 | test("a saved project is loaded when the editor starts", (){ 22 | expect(editor.content, 'asdf'); 23 | }); 24 | 25 | test("clicking save closes the main menu", (){ 26 | helpers.click('button', text: '☰'); 27 | helpers.click('li', text: 'Save'); 28 | 29 | expect(queryAll('li'), helpers.elementsAreEmpty); 30 | }); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /test/full/share_dialog_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | share_dialog_tests() { 4 | group("Opening Shared Link", (){ 5 | var editor, store; 6 | 7 | setUp((){ 8 | editor = new Full(compressedContent: '#B/88gvT6nUUXDKT1IEAA==') 9 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 10 | 11 | store = editor.store 12 | ..clear() 13 | ..['Current Project'] = {'code': 'Test'}; 14 | 15 | return editor.editorReady; 16 | }); 17 | 18 | tearDown((){ 19 | editor.remove(); 20 | editor.store..clear()..freeze(); 21 | }); 22 | 23 | test("shared content is opened in the editor", (){ 24 | expect(editor.content, 'Howdy, Bob!'); 25 | }); 26 | 27 | test("shared project is named \"Untitled\" by default", (){ 28 | helpers.click('button', text: '☰'); 29 | helpers.click('li', text: 'Save'); 30 | 31 | expect( 32 | store['Untitled']['code'], 33 | 'Howdy, Bob!' 34 | ); 35 | }); 36 | }); 37 | 38 | group("Opening Shared Link with an existing untitled project", (){ 39 | var editor, store; 40 | 41 | setUp((){ 42 | editor = new Full(compressedContent: 'B/88gvT6nUUXDKT1IEAA==') 43 | ..store.storage_key = "ice-test-${currentTestCase.id}" 44 | ..store['Untitled'] = {'code': 'Hi, Fred!'}; 45 | 46 | store = editor.store; 47 | 48 | return editor.editorReady; 49 | }); 50 | 51 | 52 | tearDown((){ 53 | editor.remove(); 54 | editor.store..clear()..freeze(); 55 | }); 56 | 57 | test("does not clobber the existing project", (){ 58 | helpers.click('button', text: '☰'); 59 | helpers.click('li', text: 'Save'); 60 | 61 | expect(store['Untitled']['code'], 'Hi, Fred!'); 62 | expect(store['Untitled (1)']['code'], 'Howdy, Bob!'); 63 | }); 64 | }); 65 | 66 | group("Share Dialog", (){ 67 | var editor; 68 | 69 | setUp((){ 70 | editor = new Full() 71 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 72 | 73 | editor.store 74 | ..clear() 75 | ..['Current Project'] = {'code': 'Test'}; 76 | 77 | return editor.editorReady; 78 | }); 79 | 80 | tearDown(() { 81 | editor.remove(); 82 | editor.store..clear()..freeze(); 83 | }); 84 | 85 | test("clicking the share link shows the share dialog", (){ 86 | helpers.click('button', text: '☰'); 87 | helpers.click('li', text: 'Share'); 88 | 89 | expect( 90 | queryAll('div'), 91 | helpers.elementsContain('Copy this link') 92 | ); 93 | }); 94 | 95 | test("share dialog contains a game mode checkbox", (){ 96 | helpers.click('button', text: '☰'); 97 | helpers.click('li', text: 'Share'); 98 | 99 | expect( 100 | queryAll('.ice-dialog label'), 101 | helpers.elementsContain('start in game mode') 102 | ); 103 | expect( 104 | queryAll('.ice-dialog label input[type=checkbox]'), 105 | helpers.elementsArePresent 106 | ); 107 | }); 108 | test("share dialog has a link to a URL shortener", (){ 109 | helpers.click('button', text: '☰'); 110 | helpers.click('li', text: 'Share'); 111 | expect(query('.ice-dialog a'), isNotNull); 112 | }); 113 | 114 | test("share dialog's URL shortener points to is.gd", (){ 115 | helpers.click('button', text: '☰'); 116 | helpers.click('li', text: 'Share'); 117 | expect(query('.ice-dialog a').href, contains('is.gd')); 118 | }); 119 | 120 | test("share dialog's URL shortener href link is properly encoded", (){ 121 | helpers.click('button', text: '☰'); 122 | helpers.click('li', text: 'Share'); 123 | expect( 124 | query('.ice-dialog a').href, 125 | contains('url=http%3A%2F%2Fgamingjs.com%2Fice%2F%23B%2FC0ktLgEA') 126 | ); 127 | }); 128 | 129 | test("clicking the game mode checkbox adds game mode to link", (){ 130 | helpers.click('button', text: '☰'); 131 | helpers.click('li', text: 'Share'); 132 | helpers.click('.ice-dialog label input[type=checkbox]'); 133 | 134 | expect( 135 | query('.ice-dialog input').value, 136 | matches('\\?g') 137 | ); 138 | }); 139 | 140 | test("share dialog's shortened URL include game-only mode (if set)", (){ 141 | helpers.click('button', text: '☰'); 142 | helpers.click('li', text: 'Share'); 143 | helpers.click('.ice-dialog label input[type=checkbox]'); 144 | expect( 145 | query('.ice-dialog a').href, 146 | contains('url=http%3A%2F%2Fgamingjs.com%2Fice%2F%3Fg%23B%2FC0ktLgEA') 147 | ); 148 | }); 149 | 150 | test("clicking the game mode twice removes game mode from link", (){ 151 | helpers.click('button', text: '☰'); 152 | helpers.click('li', text: 'Share'); 153 | helpers.click('.ice-dialog label input[type=checkbox]'); 154 | helpers.click('.ice-dialog label input[type=checkbox]'); 155 | 156 | expect( 157 | query('.ice-dialog input').value, 158 | isNot(matches('\\?g')) 159 | ); 160 | }); 161 | 162 | // input has focus after game mode is clicked 163 | test("share field has focus", (){ 164 | helpers.click('button', text: '☰'); 165 | helpers.click('li', text: 'Share'); 166 | 167 | expect( 168 | query('.ice-dialog input'), 169 | equals(document.activeElement) 170 | ); 171 | }); 172 | 173 | test("clicking in the editor closes the share dialog", (){ 174 | helpers.click('button', text: '☰'); 175 | helpers.click('li', text: 'Share'); 176 | 177 | helpers.click('.ice-code-editor-editor'); 178 | 179 | expect( 180 | queryAll('div'), 181 | helpers.elementsDoNotContain('Copy this link') 182 | ); 183 | }); 184 | 185 | test("clicking the share link closes the main menu", (){ 186 | helpers.click('button', text: '☰'); 187 | helpers.click('li', text: 'Share'); 188 | 189 | expect(queryAll('li'), helpers.elementsAreEmpty); 190 | }); 191 | 192 | test("the menu button closes the share dialog", (){ 193 | helpers.click('button', text: '☰'); 194 | helpers.click('li', text: 'Share'); 195 | helpers.click('button', text: '☰'); 196 | 197 | expect( 198 | queryAll('div'), 199 | helpers.elementsDoNotContain('Copy this link') 200 | ); 201 | }); 202 | }); 203 | } 204 | -------------------------------------------------------------------------------- /test/full/show_button_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | show_button_tests() { 4 | group("Show Button", (){ 5 | var editor; 6 | 7 | setUp((){ 8 | editor = new Full() 9 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 10 | 11 | editor.store 12 | ..clear() 13 | ..['Current Project'] = {'code': 'Test'}; 14 | 15 | return editor.editorReady; 16 | }); 17 | 18 | tearDown(() { 19 | editor.remove(); 20 | editor.store..clear()..freeze(); 21 | }); 22 | 23 | test("is hidden by default", (){ 24 | expect( 25 | helpers.queryWithContent('button', 'Show Code').style.display, 26 | equals('none') 27 | ); 28 | }); 29 | 30 | test("shows code", () { 31 | helpers.click('button', text: 'Hide Code'); 32 | helpers.click('button', text: 'Show Code'); 33 | expect( 34 | query('.ice-code-editor-editor').style.display, 35 | equals('') 36 | ); 37 | }); 38 | 39 | test("hides itself", (){ 40 | helpers.click('button', text: 'Hide Code'); 41 | helpers.click('button', text: 'Show Code'); 42 | expect( 43 | helpers.queryWithContent('button', 'Show Code').style.display, 44 | equals('none') 45 | ); 46 | }); 47 | 48 | test("shows main menu button", (){ 49 | helpers.click('button', text: 'Hide Code'); 50 | helpers.click('button', text: 'Show Code'); 51 | expect( 52 | helpers.queryWithContent('button', '☰').style.display, 53 | equals('') 54 | ); 55 | }); 56 | 57 | test("shows update button", (){ 58 | helpers.click('button', text: 'Hide Code'); 59 | helpers.click('button', text: 'Show Code'); 60 | expect( 61 | helpers.queryWithContent('button', ' Update').style.display, 62 | equals('') 63 | ); 64 | }); 65 | 66 | test("shows the hide button", (){ 67 | helpers.click('button', text: 'Hide Code'); 68 | helpers.click('button', text: 'Show Code'); 69 | expect( 70 | helpers.queryWithContent('button', 'Hide Code').style.display, 71 | equals('') 72 | ); 73 | }); 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /test/full/snapshotter_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | snapshotter_tests() { 4 | group('Snapshotter', () { 5 | var editor; 6 | 7 | setUp((){ 8 | editor = new Full() 9 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 10 | 11 | editor.store..clear()..['Saved Project'] = {'code': 'asdf'}; 12 | 13 | 14 | return editor.editorReady; 15 | }); 16 | 17 | tearDown(() { 18 | editor.remove(); 19 | editor.store..clear()..freeze(); 20 | }); 21 | 22 | test('take a snapshot of code', () { 23 | editor.snapshotter.take(); 24 | expect(editor.store['SNAPSHOT: Saved Project (${Snapshotter.dateStr})'], isNotNull); 25 | }); 26 | 27 | test('Snapshot flag set to true', () { 28 | editor.snapshotter.take(); 29 | var snapshot = editor.store['SNAPSHOT: Saved Project (${Snapshotter.dateStr})']; 30 | expect(snapshot['snapshot'], isTrue); 31 | }); 32 | 33 | test('snapshot should have copy of most recent code', () { 34 | editor.snapshotter.take(); 35 | var snapshot = editor.store['SNAPSHOT: Saved Project (${Snapshotter.dateStr})']; 36 | expect(snapshot['code'], equals('asdf')); 37 | }); 38 | 39 | test('snapshot should not be taken if code is not changed', () { 40 | editor.store['SNAPSHOT: Saved Project (2014-08-23 23:08)'] = {'code': 'asdf', 'snapshot': true}; 41 | editor.snapshotter.take(); 42 | var snapshot = editor.store['SNAPSHOT: Saved Project (${Snapshotter.dateStr})']; 43 | expect(snapshot, isNull); 44 | }); 45 | 46 | test('snapshot deletes the oldest if if 20 snapshots exist', (){ 47 | for (var i=0; i<20; i++) { 48 | editor.store['SNAPSHOT: Saved Project #$i (2014-08-30: 22:48)'] = { 49 | 'code': 'asdf', 50 | 'snapshot': true 51 | }; 52 | } 53 | 54 | editor.snapshotter.take(); 55 | 56 | var snapshot = editor.store['SNAPSHOT: Saved Project #0 (2014-08-30: 22:48)']; 57 | expect(snapshot, isNull); 58 | }); 59 | 60 | 61 | test('works with parens in the title', () { 62 | editor.store..clear()..['Parens Project (1)'] = {'code': 'asdf'}; 63 | editor.snapshotter.take(); 64 | expect(editor.store['SNAPSHOT: Parens Project (1) (${Snapshotter.dateStr})'], isNotNull); 65 | }); 66 | 67 | test('works with parens in title when previous snapshot exists', () { 68 | editor.store 69 | ..clear() 70 | ..['SNAPSHOT: Parens Project (1) (2014-11-22: 22:48)'] = { 71 | 'code': 'old code', 72 | 'snapshot': true 73 | } 74 | ..['Parens Project (1)'] = {'code': 'asdf'}; 75 | 76 | editor.snapshotter.take(); 77 | expect(editor.store['SNAPSHOT: Parens Project (1) (${Snapshotter.dateStr})'], isNotNull); 78 | }); 79 | }); 80 | } 81 | -------------------------------------------------------------------------------- /test/full/update_button_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | update_button_tests() { 4 | group("Update Button", (){ 5 | var editor; 6 | 7 | setUp((){ 8 | editor = new Full() 9 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 10 | 11 | editor.store 12 | ..clear() 13 | ..['Current Project'] = {'code': 'Test'}; 14 | 15 | var preview_ready = new Completer(); 16 | editor.onPreviewChange.listen((e){ 17 | if (preview_ready.isCompleted) return; 18 | preview_ready.complete(); 19 | }); 20 | return preview_ready.future; 21 | }); 22 | 23 | tearDown(() { 24 | editor.remove(); 25 | editor.store..clear()..freeze(); 26 | }); 27 | 28 | test("updates the preview layer", (){ 29 | document.activeElement. 30 | dispatchEvent(new TextEvent('textInput', data: '

        Hello

        ')); 31 | 32 | editor.onPreviewChange.listen(expectAsync((_)=> true)); 33 | 34 | helpers.click('button', text: " Update"); 35 | }); 36 | 37 | test("Checkbox is on by default", (){ 38 | var button = helpers.queryWithContent("button","Update"); 39 | var checkbox = button.query("input[type=checkbox]"); 40 | expect(checkbox.checked, isTrue); 41 | }); 42 | 43 | test("Autoupdate is set in the editor by default", (){ 44 | expect(editor.ice.autoupdate, isTrue); 45 | }); 46 | 47 | test("When you uncheck the checkbox autoupdate is disabled", (){ 48 | var button = helpers.queryWithContent("button","Update"); 49 | var checkbox = button.query("input[type=checkbox]"); 50 | 51 | checkbox.click(); 52 | expect(editor.ice.autoupdate, isFalse); 53 | }); 54 | 55 | // If this test starts failing randomly, try bumping up the wait 56 | test("checking the checkbox updates the preview layer", (){ 57 | var button = helpers.queryWithContent("button","Update"); 58 | var checkbox = button.query("input[type=checkbox]"); 59 | checkbox.click(); 60 | 61 | document.activeElement. 62 | dispatchEvent(new TextEvent('textInput', data: '

        Hello

        ')); 63 | 64 | editor.onPreviewChange.listen(expectAsync((_)=> true)); 65 | 66 | var wait = new Duration(milliseconds: 10); 67 | new Timer(wait, (){ 68 | checkbox.click(); 69 | }); 70 | }); 71 | 72 | test("focuses code", (){ 73 | helpers.click('button', text: " Update"); 74 | 75 | expect(document.activeElement.tagName, 'TEXTAREA'); 76 | }); 77 | }); 78 | } 79 | -------------------------------------------------------------------------------- /test/full/whats_new_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | whats_new_tests() { 4 | group("What's New Menu Item", (){ 5 | var editor; 6 | 7 | setUp((){ 8 | editor = new Full() 9 | ..settings.storage_key = "ice-test-settings-${currentTestCase.id}" 10 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 11 | 12 | editor.store 13 | ..clear() 14 | ..['Current Project'] = {'code': 'Test'}; 15 | 16 | return editor.editorReady; 17 | }); 18 | 19 | tearDown(() { 20 | editor.remove(); 21 | editor.store..clear()..freeze(); 22 | editor.settings..clear(); 23 | }); 24 | 25 | test("links to friendly release summary", (){ 26 | var action = new WhatsNewAction(editor); 27 | expect(action.el.href, contains('github')); 28 | }); 29 | 30 | test("it opens in a new window", (){ 31 | var action = new WhatsNewAction(editor); 32 | expect(action.el.target, '_blank'); 33 | }); 34 | 35 | group("existing editor, what's new hasn't been clicked", (){ 36 | test("what's new menu item should be highlighted", (){ 37 | helpers.click('button', text: '☰'); 38 | expect(query('.ice-menu li.highlighted').text, "What's New"); 39 | }); 40 | }); 41 | 42 | group("existing editor, what's new has been clicked", () { 43 | setUp(() { 44 | editor.settings..clear(); 45 | editor.rememberWhatsNewClicked(); 46 | }); 47 | 48 | test("what's new menu item should not be highlighted", () { 49 | helpers.click('button', text: '☰'); 50 | expect(query('.ice-menu li.highlighted'), isNull); 51 | }); 52 | 53 | test("the star should be removed when what's new is clicked", () { 54 | expect(query('#somethingsnew'), isNull); 55 | }); 56 | }); 57 | }); 58 | 59 | group("what's new clicked in a previous session", (){ 60 | var editor; 61 | 62 | setUp((){ 63 | editor = new Full() 64 | ..store.storage_key = "ice-test-${currentTestCase.id}" 65 | ..settings.storage_key = "ice-test-settings-${currentTestCase.id}" 66 | ..settings['clicked_whats_new'] = true; 67 | 68 | editor.store 69 | ..clear() 70 | ..['Untitled'] = {'code': 'Test'}; 71 | 72 | return editor.editorReady; 73 | }); 74 | 75 | tearDown(() { 76 | editor.remove(); 77 | editor.store..clear()..freeze(); 78 | editor.settings..clear(); 79 | }); 80 | 81 | test("what's new menu item should not be highlighted", () { 82 | helpers.click('button', text: '☰'); 83 | expect(query('.ice-menu li.highlighted'), isNull); 84 | }); 85 | 86 | test("the star should not be present", () { 87 | expect(query('#somethingsnew'), isNull); 88 | }); 89 | }); 90 | 91 | group("new user", (){ 92 | var editor; 93 | 94 | setUp((){ 95 | editor = new Full() 96 | ..settings.storage_key = "ice-test-settings-${currentTestCase.id}" 97 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 98 | 99 | editor.store 100 | ..clear() 101 | ..['Untitled'] = {'code': 'Test'}; 102 | 103 | return editor.editorReady; 104 | }); 105 | 106 | tearDown(() { 107 | editor.remove(); 108 | editor.settings..clear(); 109 | editor.store..clear()..freeze(); 110 | }); 111 | 112 | test("it should not show something's new indicators", (){ 113 | helpers.click('button', text: '☰'); 114 | expect(query('.ice-menu li.highlighted'), isNull); 115 | }); 116 | 117 | test("it should not show the something-is-new star", (){ 118 | expect(query('#somethingsnew'), null); 119 | }); 120 | 121 | group("after creating their first project", (){ 122 | setUp((){ 123 | helpers.click('button', text: '☰'); 124 | helpers.click('li', text: 'New'); 125 | 126 | helpers.typeIn('My New Project'); 127 | 128 | helpers.click('button', text: 'Save'); 129 | }); 130 | 131 | test("it should not show something's new indicators", (){ 132 | helpers.click('button', text: '☰'); 133 | expect(query('.ice-menu li.highlighted'), isNull); 134 | }); 135 | }); 136 | }); 137 | 138 | group("new user before default project is created", (){ 139 | var editor; 140 | 141 | setUp((){ 142 | editor = new Full() 143 | ..settings.storage_key = "ice-test-settings-${currentTestCase.id}" 144 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 145 | 146 | editor.store.clear(); 147 | 148 | return editor.editorReady; 149 | }); 150 | 151 | tearDown(() { 152 | editor.remove(); 153 | editor.settings..clear(); 154 | editor.store..clear()..freeze(); 155 | }); 156 | 157 | test("it should not show the something-is-new star", (){ 158 | expect(query('#somethingsnew'), null); 159 | }); 160 | }); 161 | } 162 | -------------------------------------------------------------------------------- /test/full_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | full_tests() { 4 | group("initial UI", (){ 5 | var editor; 6 | 7 | setUp((){ 8 | editor = new Full() 9 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 10 | 11 | editor.store 12 | ..clear() 13 | ..['Current Project'] = {'code': 'Test'}; 14 | 15 | return editor.editorReady; 16 | }); 17 | 18 | tearDown(() { 19 | editor.remove(); 20 | editor.store..clear()..freeze(); 21 | }); 22 | 23 | test("the editor is full-screen", (){ 24 | var el = document.query('#ice'); 25 | var editor_el = el.query('.ice-code-editor-editor'); 26 | expect(editor_el.clientWidth, window.innerWidth); 27 | expect(editor_el.clientHeight, closeTo(window.innerHeight,1.0)); 28 | }); 29 | 30 | test("does not show leave snapshot mode button", (){ 31 | expect( 32 | queryAll('button'), 33 | helpers.elementsDoNotContain('Leave Snapshot Mode') 34 | ); 35 | }); 36 | }); 37 | 38 | group("main toolbar", (){ 39 | var editor; 40 | 41 | setUp((){ 42 | editor = new Full() 43 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 44 | 45 | editor.store 46 | ..clear() 47 | ..['Current Project'] = {'code': 'Test'}; 48 | 49 | return editor.editorReady; 50 | }); 51 | 52 | tearDown(() { 53 | editor.remove(); 54 | editor.store..clear()..freeze(); 55 | }); 56 | 57 | test("it has a menu button", (){ 58 | var buttons = document.queryAll('button'); 59 | expect(buttons.any((e)=> e.text=='☰'), isTrue); 60 | }); 61 | 62 | test("clicking the menu button brings up the menu", (){ 63 | helpers.click('button', text: '☰'); 64 | 65 | var menu = queryAll('li'). 66 | firstWhere((e)=> e.text.contains('Help')); 67 | 68 | expect(menu, isNotNull); 69 | }); 70 | 71 | test("clicking the menu button a second time hides the menu", (){ 72 | helpers.click('button', text: '☰'); 73 | expect(queryAll('li'), helpers.elementsContain('Help')); 74 | 75 | helpers.click('button', text: '☰'); 76 | expect(queryAll('li'), helpers.elementsAreEmpty); 77 | }); 78 | }); 79 | 80 | group("Auto Save", (){ 81 | var editor; 82 | 83 | setUp((){ 84 | editor = new Full() 85 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 86 | 87 | editor.store 88 | ..clear() 89 | ..['Current Project'] = {'code': 'Test'}; 90 | 91 | return editor.editorReady; 92 | }); 93 | 94 | tearDown(() { 95 | editor.remove(); 96 | editor.store..clear()..freeze(); 97 | }); 98 | 99 | test("is on by default", (){ 100 | var _test = expectAsync( 101 | ()=> expect(editor.content, equals('

        test

        ')), 102 | count: 3 103 | ); 104 | 105 | editor.editorReady.then(expectAsync((_){ 106 | helpers.createProject('Project #1'); 107 | 108 | editor.ice.onChange.listen((_)=> _test()); 109 | 110 | editor.content = '

        test

        '; 111 | })); 112 | }); 113 | }); 114 | 115 | group("Edit Only Mode", (){ 116 | var editor; 117 | 118 | setUp((){ 119 | editor = new Full(mode: 'edit-only') 120 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 121 | 122 | editor.store 123 | ..clear() 124 | ..['Current Project'] = {'code': 'Test'}; 125 | 126 | return editor.editorReady; 127 | }); 128 | 129 | tearDown(() { 130 | editor.remove(); 131 | editor.store..clear()..freeze(); 132 | }); 133 | 134 | test("is enabled when the ?e query param is present", (){ 135 | expect(editor.ice.edit_only,isTrue); 136 | }); 137 | }); 138 | 139 | group("Gaming Mode", (){ 140 | var editor; 141 | 142 | setUp((){ 143 | editor = new Full(mode: '?g') 144 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 145 | 146 | editor.store 147 | ..clear() 148 | ..['Current Project'] = {'code': 'Test'}; 149 | 150 | return editor.editorReady; 151 | }); 152 | 153 | tearDown(() { 154 | editor.remove(); 155 | editor.store..clear()..freeze(); 156 | }); 157 | 158 | test("hides the code when the ?g query param is present", (){ 159 | expect( 160 | query('.ice-code-editor-editor').style.visibility, 161 | equals('hidden') 162 | ); 163 | }); 164 | 165 | test("hides the show code button when the ?g query param is present", (){ 166 | expect( 167 | helpers.queryWithContent('button', 'Show Code').style.display, 168 | equals('') 169 | ); 170 | }); 171 | }); 172 | 173 | group("Snapshot Mode", (){ 174 | var editor; 175 | 176 | setUp((){ 177 | editor = new Full(mode: '?s') 178 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 179 | 180 | return editor.editorReady; 181 | }); 182 | 183 | tearDown(() { 184 | editor.remove(); 185 | editor.store..clear()..freeze(); 186 | }); 187 | 188 | test("is enabled when the ?s query param is present", (){ 189 | expect(editor.store.show_snapshots, isTrue); 190 | }); 191 | 192 | test("does not have a running snapshotter", (){ 193 | expect(editor.snapshotter, isNull); 194 | }); 195 | 196 | test("does not include update button", (){ 197 | expect( 198 | queryAll('button'), 199 | helpers.elementsDoNotContain('Update') 200 | ); 201 | }); 202 | 203 | test("shows leave snapshot mode button", (){ 204 | expect( 205 | helpers.queryWithContent('button', 'Leave Snapshot Mode'), 206 | isNotNull 207 | ); 208 | }); 209 | 210 | test("clicking leave snapshot mode button leaves snapshot mode", (){ 211 | helpers. 212 | queryWithContent('button', 'Leave Snapshot Mode'). 213 | click(); 214 | 215 | expect(window.location.search, ''); 216 | }, skip: 'Cannot test window.location changes with current test runner'); 217 | 218 | test("menu only includes Open, Make a Copy, and Help", (){ 219 | helpers.click('button', text: '☰'); 220 | 221 | var items = queryAll('li'); 222 | 223 | expect( 224 | items.map((i)=> i.text), 225 | ['Open', 'Make a Copy', 'Help'] 226 | ); 227 | }); 228 | }); 229 | 230 | 231 | group("Focus", (){ 232 | var editor; 233 | 234 | setUp((){ 235 | editor = new Full() 236 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 237 | 238 | editor.store 239 | ..clear() 240 | ..['Current Project'] = {'code': 'Test'}; 241 | 242 | var preview_ready = new Completer(); 243 | editor.onPreviewChange.listen((e){ 244 | if (preview_ready.isCompleted) return; 245 | preview_ready.complete(); 246 | }); 247 | return preview_ready.future; 248 | }); 249 | 250 | tearDown(() { 251 | editor.remove(); 252 | editor.store..clear()..freeze(); 253 | }); 254 | 255 | test("editor has focus after closing a dialog", (){ 256 | helpers.click('button', text: '☰'); 257 | helpers.click('li', text: 'Make a Copy'); 258 | helpers.hitEscape(); 259 | 260 | var el = document. 261 | query('.ice-code-editor-editor'). 262 | query('textarea.ace_text-input'); 263 | 264 | expect(document.activeElement, el); 265 | }); 266 | 267 | group("hiding code after update", (){ 268 | setUp((){ 269 | editor.ice.focus(); 270 | 271 | document. 272 | query('#ice'). 273 | dispatchEvent( 274 | new TextEvent('textInput', data: '

        Force Update

        ') 275 | ); 276 | 277 | helpers.click('button', text: 'Hide Code'); 278 | 279 | var preview_ready = new Completer(); 280 | editor.onPreviewChange.listen((e){ 281 | preview_ready.complete(); 282 | }); 283 | return preview_ready.future; 284 | }); 285 | 286 | test("preview has focus", (){ 287 | expect(document.activeElement.tagName, 'IFRAME'); 288 | }); 289 | }); 290 | }); 291 | 292 | group("Import", (){ 293 | var editor; 294 | 295 | setUp((){ 296 | editor = new Full() 297 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 298 | 299 | editor.store 300 | ..clear() 301 | ..['Current Project'] = {'code': 'Test'}; 302 | 303 | var preview_ready = new Completer(); 304 | editor.onPreviewChange.listen((e){ 305 | if (preview_ready.isCompleted) return; 306 | preview_ready.complete(); 307 | }); 308 | return preview_ready.future; 309 | }); 310 | 311 | tearDown(() { 312 | editor.remove(); 313 | editor.store..clear()..freeze(); 314 | }); 315 | 316 | final json = '''[{ 317 | "filename":"Project #1", 318 | "code":"imported code", 319 | "lineNumber":0, 320 | "updated_at":"2014-01-02 00:00:00.000", 321 | "created_at":"2013-01-01 00:00:00.000" 322 | }, 323 | { 324 | "filename":"Project #2", 325 | "code":"imported code", 326 | "lineNumber":0, 327 | "updated_at":"2014-01-01 00:00:00.000", 328 | "created_at":"2013-01-01 00:00:00.000" 329 | }]'''; 330 | 331 | test("from JSON creates new projects", (){ 332 | editor.import(json, 'exported_data_file.json'); 333 | 334 | helpers.click('button', text: '☰'); 335 | helpers.click('li', text: 'Open'); 336 | 337 | var menu_items = queryAll('li'); 338 | expect(menu_items[0].text, 'Project #1'); 339 | }); 340 | 341 | test("from a regular file create a new project", (){ 342 | editor.import('regular code', 'regular_code.txt'); 343 | 344 | helpers.click('button', text: '☰'); 345 | helpers.click('li', text: 'Open'); 346 | 347 | var menu_items = queryAll('li'); 348 | expect(menu_items[0].text, 'regular_code.txt'); 349 | }); 350 | }); 351 | 352 | // TODO: Need to see this fail with the undo manager commented out in 353 | // editor.dart before we have confidence in this test. Manually calling the 354 | // session's undo has no effect -- maybe using a different undoManager under 355 | // test? 356 | group("undo", (){ 357 | var editor; 358 | 359 | setUp((){ 360 | editor = new Full() 361 | ..store.storage_key = "ice-test-${currentTestCase.id}"; 362 | 363 | editor.store 364 | ..clear() 365 | ..['Old Project'] = {'code': 'Old Test'} 366 | ..['Current Project'] = {'code': 'Test'}; 367 | 368 | return editor.editorReady; 369 | }); 370 | 371 | tearDown(() { 372 | editor.remove(); 373 | editor.store..clear()..freeze(); 374 | }); 375 | 376 | test("Cannot undo past the initial content", (){ 377 | expect(editor.content, 'Test'); 378 | 379 | helpers.click('button', text: '☰'); 380 | helpers.click('li', text: 'Open'); 381 | helpers.click('li', text: 'Old Project'); 382 | expect(editor.content, 'Old Test'); 383 | 384 | // TODO: some combination of the following should work for this test to 385 | // work... 386 | // editor.ice.insertAt('asdf', 12); 387 | // editor.ice.undo(); 388 | // editor.ice.undo(); 389 | // editor.ice.undo(); 390 | 391 | expect(editor.content, 'Old Test'); 392 | }); 393 | }, skip: true); 394 | 395 | // TODO: put current project title in the browser title 396 | } 397 | -------------------------------------------------------------------------------- /test/gzip_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | gzip_tests() { 4 | group("gzipping", () { 5 | test("it can encode text", (){ 6 | expect(Gzip.encode("Howdy, Bob!"), equals("88gvT6nUUXDKT1IEAA==")); 7 | }); 8 | 9 | test("it can decode as text", (){ 10 | expect(Gzip.decode("88gvT6nUUXDKT1IEAA=="), equals("Howdy, Bob!")); 11 | }); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /test/helpers.dart: -------------------------------------------------------------------------------- 1 | library ice_test_helpers; 2 | 3 | import 'dart:html'; 4 | import 'package:matcher/matcher.dart'; 5 | import 'package:ctrl_alt_foo/keys.dart'; 6 | import 'package:ctrl_alt_foo/helpers.dart'; 7 | export 'package:ctrl_alt_foo/helpers.dart'; 8 | 9 | createProject(String title, {content, editor}) { 10 | click('button', text: '☰'); 11 | click('li', text: 'New'); 12 | typeIn(title); 13 | click('button', text: 'Save'); 14 | 15 | if (content != null) { 16 | if (editor == null) throw new Exception("Need an editor instance"); 17 | editor.content = content; 18 | click('button', text: '☰'); 19 | click('li', text: 'Save'); 20 | } 21 | } 22 | 23 | class FakeCompleter { 24 | then(cb) => cb(); 25 | } 26 | 27 | click(String selector, {text}) { 28 | if (text == null) { 29 | query(selector).click(); 30 | } 31 | else { 32 | queryWithContent(selector, text).click(); 33 | } 34 | 35 | return new FakeCompleter(); 36 | } 37 | 38 | typeIn(String text) { 39 | document.activeElement.value = text; 40 | 41 | var last_char = new String.fromCharCode(text.runes.last); 42 | document.activeElement.dispatchEvent( 43 | new KeyboardEvent( 44 | 'keyup' 45 | ) 46 | ); 47 | } 48 | 49 | arrowDown([times=1]) { 50 | // var e = new KeyEvent('keydown', keyCode: KeyCode.DOWN).wrapped; 51 | var fake_button = document.query('#fake_down_key'); 52 | if (fake_button == null) return; 53 | 54 | new Iterable.generate(times, (i) { 55 | // document.activeElement.dispatchEvent(e); 56 | fake_button.click(); 57 | }).toList(); 58 | } 59 | 60 | arrowUp([times=1]) { 61 | // var e = new KeyEvent('keydown', keyCode: KeyCode.UP).wrapped; 62 | var fake_button = document.query('#fake_up_key'); 63 | if (fake_button == null) return; 64 | 65 | new Iterable.generate(times, (i) { 66 | // document.activeElement.dispatchEvent(e); 67 | fake_button.click(); 68 | }).toList(); 69 | } 70 | 71 | hitEnter() { 72 | // var e = new KeyEvent('keydown', keyCode: KeyCode.ENTER).wrapped; 73 | 74 | // document. 75 | // activeElement. 76 | // dispatchEvent(e); 77 | 78 | var fake_button = document.query('#fake_enter_key'); 79 | if (fake_button == null) return; 80 | fake_button.click(); 81 | } 82 | 83 | queryWithContent(selector, text) { 84 | var re = new RegExp(r"^\s*" + text + r"\s*$"); 85 | 86 | return queryAll(selector). 87 | firstWhere((e)=> re.hasMatch(e.text)); 88 | } 89 | 90 | get elementsAreEmpty => 91 | new ElementListMatcher(isEmpty); 92 | 93 | get elementsArePresent => 94 | new ElementListMatcher(isNot(isEmpty)); 95 | 96 | elementsContain(Pattern content) => 97 | new ElementListMatcher(contains(matches(content))); 98 | 99 | elementsDoNotContain(Pattern content) => 100 | new ElementListMatcher(isNot(contains(matches(content)))); 101 | 102 | class ElementListMatcher extends CustomMatcher { 103 | ElementListMatcher(matcher) : 104 | super("List of elements", "Element list content", matcher); 105 | 106 | featureValueOf(elements) => elements.map((e)=> e.text).toList(); 107 | } 108 | -------------------------------------------------------------------------------- /test/html5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/html5_test.dart: -------------------------------------------------------------------------------- 1 | library ice_html5_test; 2 | 3 | import 'package:unittest/unittest.dart'; 4 | import 'dart:html'; 5 | import 'dart:async'; 6 | 7 | import 'helpers.dart' as helpers; 8 | import 'package:ice_code_editor/ice.dart'; 9 | 10 | main() { 11 | group("HTML5", (){ 12 | group("Full Screen ICE", (){ 13 | tearDown(()=> document.query('#ice').remove()); 14 | 15 | test("the editor is full-screen", (){ 16 | var it = new Full(); 17 | 18 | _test(_) { 19 | var el = document.query('#ice'); 20 | var editor_el = el.query('.ice-code-editor-editor'); 21 | expect(editor_el.clientWidth, window.innerWidth); 22 | expect(editor_el.clientHeight, closeTo(window.innerHeight,1.0)); 23 | expect(document.body.clientHeight, greaterThan(10)); 24 | }; 25 | it.editorReady.then(expectAsync(_test)); 26 | }); 27 | }); 28 | }); 29 | 30 | pollForDone(testCases); 31 | } 32 | 33 | pollForDone(List tests) { 34 | if (tests.every((t)=> t.isComplete)) { 35 | window.postMessage('done', window.location.href); 36 | return; 37 | } 38 | 39 | var wait = new Duration(milliseconds: 100); 40 | new Timer(wait, ()=> pollForDone(tests)); 41 | } 42 | -------------------------------------------------------------------------------- /test/ice_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn('dartium || content-shell || firefox || chrome') 2 | library ice_test; 3 | 4 | import 'package:test/test.dart'; 5 | import 'dart:html'; 6 | import 'dart:async'; 7 | import 'dart:developer'; 8 | 9 | import 'helpers.dart' as helpers; 10 | import 'package:ice_code_editor/ice.dart'; 11 | 12 | part 'editor_test.dart'; 13 | part 'store_test.dart'; 14 | part 'settings_test.dart'; 15 | part 'gzip_test.dart'; 16 | 17 | part 'full_test.dart'; 18 | part 'full/update_button_test.dart'; 19 | part 'full/hide_button_test.dart'; 20 | part 'full/show_button_test.dart'; 21 | part 'full/notification_test.dart'; 22 | part 'full/new_project_dialog_test.dart'; 23 | part 'full/open_dialog_test.dart'; 24 | part 'full/copy_dialog_test.dart'; 25 | part 'full/rename_dialog_test.dart'; 26 | part 'full/save_dialog_test.dart'; 27 | part 'full/share_dialog_test.dart'; 28 | part 'full/download_test.dart'; 29 | part 'full/export_test.dart'; 30 | part 'full/import_test.dart'; 31 | part 'full/image_upload_test.dart'; 32 | part 'full/whats_new_test.dart'; 33 | part 'full/remove_dialog_test.dart'; 34 | part 'full/keyboard_shortcuts_test.dart'; 35 | part 'full/snapshotter_test.dart'; 36 | 37 | void main(){ 38 | Editor.disableJavaScriptWorker = true; 39 | 40 | editor_tests(); 41 | 42 | store_tests(); 43 | settings_tests(); 44 | gzip_tests(); 45 | 46 | full_tests(); 47 | 48 | update_button_tests(); 49 | hide_button_tests(); 50 | show_button_tests(); 51 | notification_tests(); 52 | new_project_dialog_tests(); 53 | open_dialog_tests(); 54 | copy_dialog_tests(); 55 | rename_dialog_tests(); 56 | save_dialog_tests(); 57 | share_dialog_tests(); 58 | download_tests(); 59 | export_tests(); 60 | import_tests(); 61 | image_upload_tests(); 62 | whats_new_tests(); 63 | remove_dialog_tests(); 64 | snapshotter_tests(); 65 | 66 | // Leave these tests last b/c they were failing at one point, but only when 67 | // last (hoping to see this again). 68 | keyboard_shortcuts_tests(); 69 | } 70 | 71 | get currentTestCase => new CurrentTestCase(); 72 | 73 | class CurrentTestCase { 74 | String id; 75 | CurrentTestCase(){ 76 | id = new DateTime.now().millisecondsSinceEpoch.toString(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/polymer/embed_bar.html: -------------------------------------------------------------------------------- 1 | bar 2 | -------------------------------------------------------------------------------- /test/polymer/embed_baz.html: -------------------------------------------------------------------------------- 1 | baz 2 | -------------------------------------------------------------------------------- /test/polymer/embed_foo.html: -------------------------------------------------------------------------------- 1 | foo 2 | -------------------------------------------------------------------------------- /test/polymer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/polymer/test.dart: -------------------------------------------------------------------------------- 1 | library ice_polymer_test; 2 | 3 | import 'package:scheduled_test/scheduled_test.dart'; 4 | import 'dart:html'; 5 | import 'dart:async'; 6 | import 'package:polymer/polymer.dart'; 7 | import 'package:ice_code_editor/ice.dart'; 8 | 9 | class PageComponent { 10 | final PolymerElement el; 11 | 12 | PageComponent(this.el); 13 | 14 | Future flush() { 15 | Completer completer = new Completer(); 16 | el.async((_) => completer.complete()); 17 | 18 | return completer.future; 19 | } 20 | } 21 | 22 | createElement(String html) => 23 | new Element.html(html, treeSanitizer: new NullTreeSanitizer()); 24 | 25 | class NullTreeSanitizer implements NodeTreeSanitizer { 26 | void sanitizeTree(node) {} 27 | } 28 | 29 | main() { 30 | var component1; 31 | var component2; 32 | initPolymer(); 33 | 34 | setUp((){ 35 | schedule(() => Polymer.onReady ); 36 | 37 | schedule(() { 38 | var elements = queryAll('ice-code-editor'); 39 | 40 | component1 = new PageComponent(elements[0]); 41 | component2 = new PageComponent(elements[1]); 42 | 43 | return Future.wait([component1.flush(), component2.flush()]); 44 | }); 45 | }); 46 | 47 | group("[polymer]", (){ 48 | test("can embed code", (){ 49 | schedule(() { 50 | expect( 51 | query('ice-code-editor').shadowRoot.query('p').text, 52 | contains('embed_foo.html') 53 | ); 54 | }); 55 | }); 56 | test("can set line number", (){ 57 | expect( 58 | query('ice-code-editor').shadowRoot.query('p').text, 59 | contains('42') 60 | ); 61 | }); 62 | test("creates a shadow preview", (){ 63 | expect( 64 | query('ice-code-editor').query('.ice-code-editor-preview'), 65 | isNotNull 66 | ); 67 | }); 68 | test("creates an editor", (){ 69 | expect( 70 | query('ice-code-editor').query('.ice-code-editor-editor'), 71 | isNotNull 72 | ); 73 | }); 74 | 75 | group("multiple elements", (){ 76 | test("can embed code", (){ 77 | expect( 78 | queryAll('ice-code-editor').last.shadowRoot.query('p').text, 79 | contains('embed_bar.html') 80 | ); 81 | }); 82 | 83 | test("can still embed code after JS is loaded and evaluated", (){ 84 | schedule(()=> Editor.jsReady); 85 | 86 | schedule((){ 87 | var later = createElement(''); 88 | document.body.append(later); 89 | currentSchedule.onComplete.schedule(() => later.remove()); 90 | 91 | return new Future.delayed(Duration.ZERO); 92 | }); 93 | 94 | schedule((){ 95 | expect( 96 | queryAll('ice-code-editor').last.shadowRoot.query('p').text, 97 | contains('embed_baz.html') 98 | ); 99 | }); 100 | }); 101 | 102 | 103 | }); 104 | }); 105 | 106 | pollForDone(testCases); 107 | } 108 | 109 | pollForDone(List tests) { 110 | if (tests.every((t)=> t.isComplete)) { 111 | window.postMessage('dart-main-done', '*'); 112 | return; 113 | } 114 | 115 | new Timer( 116 | new Duration(milliseconds: 100), 117 | ()=> pollForDone(tests) 118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /test/settings_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | settings_tests() { 4 | group("store/retrieve", (){ 5 | var it; 6 | setUp(()=> it = new Settings()..clear()); 7 | tearDown(()=> it.clear()); 8 | 9 | test("it can store data", (){ 10 | it['foo'] = 42; 11 | expect(it['foo'], equals(42)); 12 | }); 13 | 14 | test("it can store multiple records", (){ 15 | it['one'] = 1; 16 | it['two'] = 2; 17 | it['foo'] = 42; 18 | expect(it['foo'], equals(42)); 19 | }); 20 | 21 | test("it can overwrite existing records", (){ 22 | it['one'] = 1; 23 | it['two'] = 2; 24 | it['foo'] = 42; 25 | it['two'] = 7; 26 | expect(it['two'], equals(7)); 27 | }); 28 | 29 | test("it does not create new records when overwriting old ones", (){ 30 | it['one'] = 1; 31 | it['two'] = 2; 32 | it['foo'] = 42; 33 | it['two'] = 7; 34 | expect(it.length, equals(3)); 35 | }); 36 | }); 37 | 38 | group("localStorage", (){ 39 | var it; 40 | setUp(()=> it = new Settings()..clear()); 41 | tearDown(()=> it.clear()); 42 | 43 | test("records persist", (){ 44 | it['foo'] = 42; 45 | it.refresh(); 46 | 47 | expect(it['foo'], equals(42)); 48 | }); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /test/store_test.dart: -------------------------------------------------------------------------------- 1 | part of ice_test; 2 | 3 | store_tests() { 4 | group("store/retrieve", (){ 5 | var it; 6 | setUp(()=> it = new Store()..clear()); 7 | 8 | test("it can store data", (){ 9 | it['foo'] = {'bar': 42}; 10 | expect(it['foo']['bar'], equals(42)); 11 | }); 12 | 13 | test("it can store multiple records", (){ 14 | it['one'] = {'bar': 1}; 15 | it['two'] = {'bar': 2}; 16 | it['foo'] = {'bar': 42}; 17 | expect(it['foo']['bar'], equals(42)); 18 | }); 19 | 20 | test("it can retrive arbitrary records", (){ 21 | it['one'] = {'bar': 1}; 22 | it['two'] = {'bar': 2}; 23 | it['foo'] = {'bar': 42}; 24 | expect(it['one']['bar'], equals(1)); 25 | }); 26 | 27 | test("it can overwrite existing records", (){ 28 | it['one'] = {'bar': 1}; 29 | it['two'] = {'bar': 2}; 30 | it['foo'] = {'bar': 42}; 31 | it['two'] = {'bar': 7}; 32 | expect(it['two']['bar'], equals(7)); 33 | }); 34 | 35 | test("it does not create new records when overwriting old ones", (){ 36 | it['one'] = {'bar': 1}; 37 | it['two'] = {'bar': 2}; 38 | it['foo'] = {'bar': 42}; 39 | it['two'] = {'bar': 7}; 40 | expect(it.length, equals(3)); 41 | }); 42 | }); 43 | 44 | group("localStorage", (){ 45 | var it; 46 | setUp(()=> it = new Store()..clear()); 47 | 48 | test("records persist", (){ 49 | it['foo'] = {'bar': 42}; 50 | it.refresh(); 51 | 52 | expect(it['foo']['bar'], equals(42)); 53 | }); 54 | 55 | test("records cease to persist if the store is frozen", (){ 56 | it['foo'] = {'bar': 42}; 57 | it.freeze(); 58 | 59 | it['foo'] = {'bar': 43}; 60 | it.refresh(); 61 | 62 | expect(it['foo']['bar'], equals(42)); 63 | }); 64 | }); 65 | 66 | group("Multiple Stores", (){ 67 | test("can be created with optional named construtor params", (){ 68 | var store1 = new Store(storage_key: 'test1') 69 | ..['one'] = {'id': 'foo'} 70 | ..refresh(); 71 | 72 | var store2 = new Store(storage_key: 'test2') 73 | ..['one'] = {'id': 'bar'} 74 | ..refresh(); 75 | 76 | expect(store1['one']['id'], 'foo'); 77 | expect(store2['one']['id'], 'bar'); 78 | 79 | store1.clear(); 80 | store2.clear(); 81 | }); 82 | }); 83 | 84 | group("current project title", (){ 85 | test("is \"Untitled\" when there are no projects", (){ 86 | var it = new Store()..clear(); 87 | expect( 88 | it.currentProjectTitle, 89 | equals('Untitled') 90 | ); 91 | }); 92 | test("is the title of the first project when there are projects", (){ 93 | var it = new Store()..clear(); 94 | it['one'] = {'id': 1}; 95 | it['two'] = {'id': 2}; 96 | expect( 97 | it.currentProjectTitle, 98 | equals('two') 99 | ); 100 | }); 101 | }); 102 | 103 | group("destructive operations", (){ 104 | var it; 105 | setUp((){ 106 | it = new Store()..clear(); 107 | it['one'] = {'id': 1}; 108 | it['two'] = {'id': 2}; 109 | it['three'] = {'id': 3}; 110 | }); 111 | 112 | test("it can remove records by title", (){ 113 | it.remove('two'); 114 | expect(it['two'], isNull); 115 | expect(it.length, equals(2)); 116 | }); 117 | 118 | test("it returns null when removing a non-existent record", (){ 119 | expect(it.remove('four'), isNull); 120 | }); 121 | 122 | test("persist removes", (){ 123 | it.remove('two'); 124 | it.refresh(); 125 | expect(it['two'], isNull); 126 | expect(it.length, equals(2)); 127 | }); 128 | 129 | test("it can clear all elements", (){ 130 | it.clear(); 131 | expect(it['two'], isNull); 132 | expect(it.length, isZero); 133 | }); 134 | 135 | test("clearing all elements persists", (){ 136 | it.clear(); 137 | it.refresh(); 138 | expect(it['two'], isNull); 139 | expect(it.length, isZero); 140 | }); 141 | 142 | test("clearing deletes the storage_key from localStorage", (){ 143 | it.clear(); 144 | expect(window.localStorage[Store.codeEditor], isNull); 145 | }); 146 | 147 | test("it can add a new element if absent", (){ 148 | it.putIfAbsent('four', ()=> {'id': 4}); 149 | expect(it['four'], isNotNull); 150 | expect(it.length, equals(4)); 151 | }); 152 | 153 | test("it gets back existing elements from putIfAbsent", (){ 154 | var existing = it.putIfAbsent('three', ()=> {'id': 42}); 155 | expect(existing['id'], equals(3)); 156 | expect(it['three']['id'], equals(3)); 157 | expect(it.length, equals(3)); 158 | }); 159 | 160 | group("addAll", (){ 161 | setUp(()=> it.addAll({'three': {'id': 42}, 'four': {'id': 4}})); 162 | 163 | test("it overwrites existing records with same key", (){ 164 | expect(it['three']['id'], equals(42)); 165 | }); 166 | 167 | test("it adds new records", (){ 168 | expect(it.length, equals(4)); 169 | expect(it['four'], isNotNull); 170 | }); 171 | }); 172 | }); 173 | 174 | group("onSync", (){ 175 | tearDown(()=> new Store()..clear()); 176 | 177 | test("it generates a stream action when a sync operation occurs", (){ 178 | var store = new Store()..clear(); 179 | 180 | _test(_)=> expect(store, isNot(isEmpty)); 181 | 182 | // Once for clear, once for new Test Project 183 | store.onSync.listen(expectAsync(_test, count: 2)); 184 | 185 | store['Test Project'] = {'code': 'Test Code'}; 186 | }); 187 | }); 188 | 189 | group("Next Project Named", (){ 190 | var it; 191 | setUp((){ 192 | it = new Store()..clear(); 193 | it['one'] = {'code': 1}; 194 | it['two'] = {'code': 2}; 195 | }); 196 | 197 | tearDown(()=> new Store()..clear()); 198 | 199 | test("does not have trailing parens if name unique", (){ 200 | expect( 201 | it.nextProjectNamed('Untitled'), 202 | equals('Untitled') 203 | ); 204 | }); 205 | 206 | test("does have trailing parens if name is not unique", (){ 207 | expect( 208 | it.nextProjectNamed('one'), 209 | equals('one (1)') 210 | ); 211 | }); 212 | 213 | test("defaults to current project with parens", (){ 214 | expect( 215 | it.nextProjectNamed(), 216 | equals('two (1)') 217 | ); 218 | }); 219 | }); 220 | 221 | group("Store", (){ 222 | var it; 223 | setUp((){ 224 | it = new Store() 225 | ..clear() 226 | ..['one'] = {'code': 1} 227 | ..['two'] = {'code': 2}; 228 | }); 229 | 230 | tearDown(()=> new Store()..clear()); 231 | 232 | test("automatically includes creation date", (){ 233 | expect( 234 | DateTime.parse(it.currentProject['created_at']).millisecondsSinceEpoch, 235 | closeTo(new DateTime.now().millisecondsSinceEpoch, 100) 236 | ); 237 | }); 238 | 239 | test("creation date does not change on update", (){ 240 | var original = it.currentProject['created_at']; 241 | 242 | Timer.run(expectAsync((){ 243 | it['two'] = {'code': 3}; 244 | expect( 245 | it.currentProject['created_at'], 246 | original 247 | ); 248 | })); 249 | }); 250 | 251 | test("automatically includes update date", (){ 252 | expect( 253 | DateTime.parse(it.currentProject['updated_at']).millisecondsSinceEpoch, 254 | closeTo(new DateTime.now().millisecondsSinceEpoch, 100) 255 | ); 256 | }); 257 | 258 | test("update date changes… on update", (){ 259 | var original = it.currentProject['updated_at']; 260 | 261 | Timer.run(expectAsync((){ 262 | it['two'] = {'code': 3}; 263 | expect( 264 | it.currentProject['updated_at'], 265 | isNot(original) 266 | ); 267 | })); 268 | }); 269 | 270 | test("initial order is insertion order", (){ 271 | var titles = it.projects.map((p) => p[Store.title]); 272 | expect(titles, ['two', 'one']); 273 | }); 274 | 275 | test("order persists after save / reload", (){ 276 | it.refresh(); 277 | var titles = it.projects.map((p) => p[Store.title]); 278 | expect(titles, ['two', 'one']); 279 | }); 280 | 281 | test("updating a record moves it to the top of the list", (){ 282 | it['one'] = {'code': 'updated code'}; 283 | 284 | var titles = it.projects.map((p) => p[Store.title]); 285 | expect(titles, equals(['one', 'two'])); 286 | }); 287 | }); 288 | 289 | group("Snapshots", (){ 290 | var it; 291 | setUp((){ 292 | it = new Store() 293 | ..clear() 294 | ..['one'] = {'code': 1} 295 | ..['two'] = {'code': 2, 'snapshot': true} 296 | ..['three'] = {'code': 3}; 297 | }); 298 | 299 | tearDown(()=> new Store()..clear()); 300 | 301 | test('project list does not inlcude snapshots by default', (){ 302 | expect(it.length, equals(2)); 303 | }); 304 | 305 | test("existing snapshots persist", (){ 306 | it.refresh(); 307 | 308 | expect(it['two']['code'], equals(2)); 309 | expect(it['two']['snapshot'], isTrue); 310 | }); 311 | 312 | test("new snapshots persist", (){ 313 | it['foo'] = {'code': 42, 'snapshot': true}; 314 | it.refresh(); 315 | 316 | expect(it['foo']['code'], equals(42)); 317 | expect(it['foo']['snapshot'], isTrue); 318 | }); 319 | 320 | test("project list is real projects only when show_snapshots==false", (){ 321 | it.show_snapshots = false; 322 | expect(it.projects.length, 2); 323 | expect(it['one']['code'], equals(1)); 324 | expect(it['three']['code'], equals(3)); 325 | }); 326 | 327 | test("project list is snapshots only when show_snapshots==true", (){ 328 | it.show_snapshots = true; 329 | expect(it.projects.length, 1); 330 | expect(it['two']['code'], equals(2)); 331 | expect(it['two']['snapshot'], isTrue); 332 | }); 333 | 334 | test("when they are the only projects in the list, still persist", (){ 335 | it 336 | ..clear() 337 | ..['two'] = {'code': 2, 'snapshot': true} 338 | ..['four'] = {'code': 4, 'snapshot': true}; 339 | 340 | it.refresh(); 341 | 342 | expect(it.projects.length, 0); 343 | }); 344 | 345 | test('does not persist in snapshot mode', (){ 346 | it.show_snapshots = true; 347 | it['four'] = {'code': 4}; 348 | 349 | it.refresh(); 350 | 351 | it.show_snapshots = false; 352 | 353 | expect(it.length, equals(2)); 354 | }); 355 | }); 356 | } 357 | -------------------------------------------------------------------------------- /tool/dartdoc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | dartdoc \ 4 | --package-root=packages \ 5 | --exclude-lib=js,js.wrapping,meta,metadata \ 6 | lib/ice.dart 7 | 8 | mv docs/ice.html docs/dev.html 9 | 10 | cat docs/dev.html | \ 11 | grep -v Dialog.html | \ 12 | grep -v ice/Ace \ 13 | > docs/ice.html 14 | -------------------------------------------------------------------------------- /web/embed_a.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 52 | -------------------------------------------------------------------------------- /web/embed_b.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 72 | -------------------------------------------------------------------------------- /web/embed_c.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 69 | -------------------------------------------------------------------------------- /web/full.dart: -------------------------------------------------------------------------------- 1 | import 'dart:html'; 2 | import 'package:ice_code_editor/ice.dart' as ICE; 3 | 4 | main() { 5 | new ICE.Full( 6 | mode: window.location.search, 7 | compressedContent: window.location.hash 8 | ); 9 | 10 | window.location.hash = ''; 11 | } 12 | -------------------------------------------------------------------------------- /web/full.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

        Hello

        6 |
        7 | -------------------------------------------------------------------------------- /web/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:ice_code_editor/ice.dart' as ICE; 2 | 3 | main() { 4 | var ice = new ICE.Editor('#ice'); 5 | 6 | ice.content = ''' 7 | 8 | 9 | 78 | '''; 79 | 80 | } 81 | -------------------------------------------------------------------------------- /web/polymer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Demo: Polymer Version of ICE 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 |
        20 |

        Demo: Inline ICE Code Editor via Polymer

        21 | 22 |
        23 |
        24 |

        25 | The 26 | ICE Code Editor, 27 | which is used throughout 28 | 3D 29 | Game Programming for Kids, 30 | can also be used on web pages to illustrate progressing 31 | concepts. The two live code editors below 32 | are added to the document with 33 | <ice-code-editor> 34 | tags, thanks to the power of 35 | Polymer. 36 |

        37 |

        38 | This is a work in progress. If you are interested 39 | in learning more, I am currently writing an amazing book on 40 | Polymer titled, 41 | Patterns in 42 | Polymer. 43 | If you want to help, 44 | #pairwithme! 45 |

        46 |
        47 |
        48 | 49 |

        Simple Three.js Shapes

        50 | 51 |
        52 |
        53 |

        54 | The live demo below shows some simple 55 | Three.js shapes. What do I 56 | mean by live? Change the numbers in the code below and see 57 | the shapes change size and position! 58 |

        59 |
        60 |
        61 | 62 | 63 | 64 |

        Three.js Animation!

        65 | 66 |
        67 |
        68 |

        69 | Where's the fun in shapes that just sit there? Let's animate! 70 |

        71 |
        72 |
        73 | 74 | 75 | 76 |
        77 |
        78 |

        79 | Play with animation! What happen if you change the donut 80 | rotation values on line 62 to 8*t, t, 0? 81 | Experiment with any of these numbers! 82 |

        83 |

        84 | Challenge: Can you animate movement as 85 | well as rotation? 86 |

        87 |
        88 |
        89 | 90 | 91 |

        Want More?

        92 | 93 |
        94 |
        95 |

        96 | These examples are just the first chapter in 97 | 3D 98 | Game Programming for Kids. There is also game 99 | programming, space simulations, sounds, WebGL, physics and 100 | so much more. Buy it from 101 | The 102 | Pragmatic Programmers today! 103 |

        104 | 105 |

        Made with love by 106 | Chris Strom. 107 |

        108 |
        109 |
        110 | 111 |
        112 | 113 | --------------------------------------------------------------------------------