Merge pull request #1 from catseye/develop-1.0-2018.1
Develop 1.0 2018.1
Chris Pressey authored 7 years ago
GitHub committed 7 years ago
0 | function launch(prefix, container, config) { | |
1 | if (typeof container === 'string') { | |
2 | container = document.getElementById(container); | |
3 | } | |
4 | config = config || {}; | |
5 | var deps = [ | |
6 | "yoob/element-factory.js", | |
7 | "yoob/playfield.js", | |
8 | "yoob/playfield-html-view.js", | |
9 | "yoob/controller.js", | |
10 | "yoob/source-manager.js", | |
11 | "yoob/preset-manager.js", | |
12 | "../script/braktif.js" | |
13 | ]; | |
14 | var loaded = 0; | |
15 | var onload = function() { | |
16 | if (++loaded < deps.length) return; | |
17 | /* ----- launch, phase 1: create the UI ----- */ | |
18 | var controlPanel = yoob.makeDiv(container); | |
19 | controlPanel.id = "panel_container"; | |
20 | ||
21 | var subPanel = yoob.makeDiv(container); | |
22 | var selectConfiguration = yoob.makeSelect(subPanel, 'example configuration:', []); | |
23 | ||
24 | var displayContainer = yoob.makeDiv(container); | |
25 | displayContainer.id = 'display_container'; | |
26 | ||
27 | var generationDisplay = yoob.makePre(displayContainer); | |
28 | generationDisplay.id = 'generation_display'; | |
29 | ||
30 | var editor = yoob.makeTextArea(displayContainer, 40, 25); | |
31 | ||
32 | var generationView = new yoob.PlayfieldHTMLView().init({ | |
33 | element: generationDisplay | |
34 | }); | |
35 | ||
36 | // NOTE this is kind of chintzy, but, oh well | |
37 | generationView.draw = function() { | |
38 | this.element.innerHTML = this.pf.dump(dumpMapper); | |
39 | }; | |
40 | ||
41 | /* ----- launch, phase 2: connect the controller ----- */ | |
42 | var pf; | |
43 | var controller = (new yoob.Controller()).init({ | |
44 | panelContainer: controlPanel, | |
45 | step: function() { | |
46 | var newPf = (new yoob.Playfield()).init({ defaultValue: 'Space' }); | |
47 | evolve_playfield(pf, newPf); | |
48 | pf = newPf; | |
49 | generationView.setPlayfield(pf); | |
50 | generationView.draw(); | |
51 | }, | |
52 | reset: function(text) { | |
53 | pf = (new yoob.Playfield()).init({ defaultValue: 'Space' }); | |
54 | pf.load(0, 0, text, loadMapper); | |
55 | generationView.setPlayfield(pf); | |
56 | generationView.draw(); | |
57 | } | |
58 | }); | |
59 | controller.clickStop(); | |
60 | ||
61 | var sourceManager = (new yoob.SourceManager()).init({ | |
62 | panelContainer: controlPanel, | |
63 | editor: editor, | |
64 | hideDuringEdit: [generationDisplay], | |
65 | disableDuringEdit: [controller.panel], | |
66 | storageKey: 'braktif.alp', | |
67 | onDone: function() { | |
68 | controller.performReset(this.getEditorText()); | |
69 | } | |
70 | }); | |
71 | var p = (new yoob.PresetManager()).init({ | |
72 | selectElem: selectConfiguration, | |
73 | controller: controller | |
74 | }).populateFromPairs(sourceManager, exampleConfigurations); | |
75 | }; | |
76 | for (var i = 0; i < deps.length; i++) { | |
77 | var elem = document.createElement('script'); | |
78 | elem.src = prefix + deps[i]; | |
79 | elem.onload = onload; | |
80 | document.body.appendChild(elem); | |
81 | } | |
82 | } |
1 | 1 | <head> |
2 | 2 | <meta charset="utf-8"> |
3 | 3 | <title>Braktif Demo</title> |
4 | <style> | |
5 | #output { | |
6 | border: 1px solid blue; | |
7 | } | |
8 | .example { | |
9 | display: none; | |
10 | } | |
11 | </style> | |
12 | 4 | </head> |
13 | 5 | <body> |
14 | 6 | |
15 | 7 | <h1>Braktif Demo</h1> |
16 | 8 | |
17 | <button id="load">Load</button> | |
18 | <button id="edit">Edit</button> | |
19 | <button id="start">Start</button> | |
20 | <button id="stop">Stop</button> | |
21 | <button id="step">Step</button> | |
22 | <button id="reset">Reset</button> | |
23 | Speed: <input id="speed" type="range" min="0" max="200" value="0" /> | |
24 | ||
25 | <select id="select_source"></select> | |
26 | ||
27 | <textarea id="input" rows="10" cols="50"></textarea> | |
28 | ||
29 | <pre id="output" style="border: 1px solid blue"> | |
30 | </pre> | |
31 | ||
32 | <div class="example" id="test" | |
33 | >000000000000000000 <*<*>*>*<*<*>*>* | |
34 | -----------------d-i--------------- | |
35 | </div> | |
36 | ||
37 | ||
38 | <div class="example" id="test2" | |
39 | > <* | |
40 | 000000000000000000000000000000000000000000000000000000000 *[---] | |
41 | --------------------------------------------------------d-i- -- | |
42 | </div> | |
43 | ||
44 | ||
45 | <div class="example" id="test3" | |
46 | > * | |
47 | <<*[--]* | |
48 | 000000000000000000 *[----- --] | |
49 | -----------------d-i-- -------- | |
50 | ||
51 | </div> | |
9 | <div id="container"></div> | |
52 | 10 | |
53 | 11 | </body> |
54 | <script src="yoob/controller.js"></script> | |
55 | <script src="yoob/playfield.js"></script> | |
56 | <script src="yoob/preset-manager.js"></script> | |
57 | <script src="../script/braktif.js"></script> | |
12 | <script src="../eg/index.js"></script> | |
13 | <script src="braktif-launcher.js"></script> | |
58 | 14 | <script> |
59 | var output = document.getElementById('output'); | |
60 | var c = new yoob.Controller(); | |
61 | var pf; | |
62 | ||
63 | c.load = function(text) { | |
64 | pf = new yoob.Playfield(); | |
65 | pf.setDefault('Space'); | |
66 | pf.load(0, 0, text, loadMapper); | |
67 | output.innerHTML = pf.dump(dumpMapper); | |
68 | }; | |
69 | ||
70 | c.step = function() { | |
71 | var newPf = new yoob.Playfield(); | |
72 | newPf.setDefault('Space'); | |
73 | evolve_playfield(pf, newPf); | |
74 | pf = newPf; | |
75 | output.innerHTML = pf.dump(dumpMapper); | |
76 | }; | |
77 | ||
78 | c.connect({ | |
79 | 'start': 'start', | |
80 | 'stop': 'stop', | |
81 | 'step': 'step', | |
82 | 'load': 'load', | |
83 | 'edit': 'edit', | |
84 | 'reset': 'reset', | |
85 | 'speed': 'speed', | |
86 | 'source': 'input', | |
87 | 'display': 'output' | |
88 | }); | |
89 | var p = (new yoob.PresetManager()).init({ | |
90 | selectElem: document.getElementById("select_source"), | |
91 | controller: c | |
92 | }).populateFromClass('example').select('test3'); | |
93 | c.click_load(); | |
15 | launch('', 'container'); | |
94 | 16 | </script> |
0 | 0 | /* |
1 | * This file is part of yoob.js version 0.7-2015.0108 | |
1 | * This file is part of yoob.js version 0.12 | |
2 | 2 | * Available from https://github.com/catseye/yoob.js/ |
3 | 3 | * This file is in the public domain. See http://unlicense.org/ for details. |
4 | 4 | */ |
10 | 10 | * convenience, we will refer to this as the _program state_, even though |
11 | 11 | * it is of course highly adaptable and might not represent a "program". |
12 | 12 | * |
13 | * The controller can be connected to a UI in the DOM, consisting of: | |
13 | * Like most yoob objects, it is initialized after creation by calling the | |
14 | * method `init` with a configuration object. If a DOM element is passed | |
15 | * for `panelContainer` in the configuration, a panel containing a number | |
16 | * of UI controls will be created and appended to that container. These | |
17 | * are: | |
14 | 18 | * |
15 | 19 | * - a set of buttons which control the evolution of the state: |
16 | 20 | * - start |
17 | 21 | * - stop |
18 | 22 | * - step |
19 | 23 | * - load |
20 | * - edit | |
21 | 24 | * - reset |
22 | 25 | * |
23 | 26 | * - a slider control which adjusts the speed of program state evolution. |
24 | * | |
25 | * - a `source` element from which an program state can be loaded, | |
26 | * and which is generally assumed to support user-editing of the source. | |
27 | * The `edit` button will cause the `source` to be shown and the `display` | |
28 | * to be hidden, while the `load` button will load the program state from | |
29 | * the `source`, hide the `source`, and show the `display`. | |
30 | * | |
31 | * - a `display` element on which the current program state will be | |
32 | * depicted. Note that the controller is not directly responsible for | |
33 | * rendering the program state; use something like yoob.PlayfieldCanvasView | |
34 | * for that instead. The controller only knows about the `display` in order | |
35 | * to hide it while the `source` is being edited and to show it after the | |
36 | * `source` has been loaded. | |
37 | * | |
38 | * - an `input` element, which provides input to the running program. | |
39 | * | |
40 | * Each of these is optional, and if not configured, will not be used. | |
41 | 27 | * |
42 | 28 | * To use a Controller, create a subclass of yoob.Controller and override |
43 | 29 | * the following methods: |
44 | 30 | * - make it evolve the state by one tick in the step() method |
45 | * - make it load the state from a multiline string in the load() method | |
31 | * - make it load the initial state from a string in the reset(s) method | |
46 | 32 | * |
47 | 33 | * In these methods, you will need to store the state (in whatever |
48 | 34 | * representation you find convenient for processing and for depicting on |
54 | 40 | * |
55 | 41 | * You should *not* store it in the `.state` attribute, as a yoob.Controller |
56 | 42 | * uses this to track its own state (yes, it has its own state independent of |
57 | * the program state. at least potentially.) | |
43 | * the program state.) | |
44 | * | |
45 | * Some theory of operation: | |
46 | * | |
47 | * For every action 'foo', three methods are exposed on the yoob.Controller | |
48 | * object: | |
49 | * | |
50 | * - clickFoo | |
51 | * | |
52 | * Called when the button associated button is clicked. | |
53 | * Client code may call this method to simulate the button having been | |
54 | * clicked, including respecting and changing the state of the buttons panel. | |
55 | * | |
56 | * - performFoo | |
57 | * | |
58 | * Called by clickFoo to request the 'foo' action be performed. | |
59 | * Responsible also for any Controller-related housekeeping involved with | |
60 | * the 'foo' action. Client code may call this method when it wants the | |
61 | * controller to perform this action without respecting or changing the | |
62 | * state of the button panel. | |
63 | * | |
64 | * - foo | |
65 | * | |
66 | * Overridden (if necessary) by a subclass, or supplied by an instantiator, | |
67 | * of yoob.Controller to implement some action. In particular, 'step' needs | |
68 | * to be implemented this way. Client code should not call these methods | |
69 | * directly. | |
70 | * | |
71 | * The clickFoo methods take one argument, an event structure. None of the | |
72 | * other functions take an argument, with the exception of performReset() and | |
73 | * reset(), which take a single argument, the text-encoded state to reset to. | |
58 | 74 | */ |
59 | 75 | yoob.Controller = function() { |
60 | 76 | var STOPPED = 0; // the program has terminated (itself) |
62 | 78 | var RUNNING = 2; // the program is running |
63 | 79 | var BLOCKED = 3; // the program is waiting for more input |
64 | 80 | |
65 | this.intervalId = undefined; | |
66 | this.delay = 100; | |
67 | this.state = STOPPED; | |
68 | ||
69 | this.source = undefined; | |
70 | this.input = undefined; | |
71 | this.display = undefined; | |
72 | ||
73 | this.speed = undefined; | |
74 | this.controls = {}; | |
75 | ||
76 | 81 | /* |
77 | * This is not a public method. | |
78 | */ | |
79 | this._makeEventHandler = function(control, key) { | |
80 | if (this['click_' + key] !== undefined) { | |
81 | key = 'click_' + key; | |
82 | } | |
82 | * panelContainer: an element into which to add the created button panel | |
83 | * (if you do not give this, no panel will be created. You're on your own.) | |
84 | * step: if given, if a function, it becomes the step() method on this | |
85 | * reset: if given, if a function, it becomes the reset() method on this | |
86 | */ | |
87 | this.init = function(cfg) { | |
88 | this.delay = 100; | |
89 | this.state = STOPPED; | |
90 | this.controls = {}; | |
91 | this.resetState = undefined; | |
92 | if (cfg.panelContainer) { | |
93 | this.panel = this.makePanel(); | |
94 | cfg.panelContainer.appendChild(this.panel); | |
95 | } | |
96 | if (cfg.step) { | |
97 | this.step = cfg.step; | |
98 | } | |
99 | if (cfg.reset) { | |
100 | this.reset = cfg.reset; | |
101 | } | |
102 | return this; | |
103 | }; | |
104 | ||
105 | /****************** | |
106 | * UI | |
107 | */ | |
108 | this.makePanel = function() { | |
109 | var panel = document.createElement('div'); | |
83 | 110 | var $this = this; |
84 | return function(e) { | |
85 | $this[key](control); | |
111 | ||
112 | var makeEventHandler = function(control, upperAction) { | |
113 | return function(e) { | |
114 | $this['click' + upperAction](control); | |
115 | }; | |
86 | 116 | }; |
117 | ||
118 | var makeButton = function(action) { | |
119 | var button = document.createElement('button'); | |
120 | var upperAction = action.charAt(0).toUpperCase() + action.slice(1); | |
121 | button.innerHTML = upperAction; | |
122 | button.style.width = "5em"; | |
123 | panel.appendChild(button); | |
124 | button.onclick = makeEventHandler(button, upperAction); | |
125 | $this.controls[action] = button; | |
126 | return button; | |
127 | }; | |
128 | ||
129 | var keys = ["start", "stop", "step", "reset"]; | |
130 | for (var i = 0; i < keys.length; i++) { | |
131 | makeButton(keys[i]); | |
132 | } | |
133 | ||
134 | var slider = document.createElement('input'); | |
135 | slider.type = "range"; | |
136 | slider.min = 0; | |
137 | slider.max = 200; | |
138 | slider.value = 100; | |
139 | slider.onchange = function(e) { | |
140 | $this.setDelayFrom(slider); | |
141 | if ($this.intervalId !== undefined) { | |
142 | $this.stop(); | |
143 | $this.start(); | |
144 | } | |
145 | }; | |
146 | ||
147 | panel.appendChild(slider); | |
148 | $this.controls.speed = slider; | |
149 | ||
150 | return panel; | |
151 | }; | |
152 | ||
153 | this.connectInput = function(elem) { | |
154 | this.input = elem; | |
155 | this.input.onchange = function(e) { | |
156 | if (this.value.length > 0) { | |
157 | // weird, where is this from? | |
158 | $this.unblock(); | |
159 | } | |
160 | } | |
87 | 161 | }; |
88 | 162 | |
89 | 163 | /* |
90 | * Single argument is a dictionary (object) where the keys | |
91 | * are the actions a controller can undertake, and the values | |
92 | * are either DOM elements or strings; if strings, DOM elements | |
93 | * with those ids will be obtained from the document and used. | |
94 | * | |
95 | * When the button associated with e.g. 'start' is clicked, | |
96 | * the corresponding method (in this case, 'click_start()') | |
97 | * on this Controller will be called. These functions are | |
98 | * responsible for changing the state of the Controller (both | |
99 | * the internal state, and the enabled status, etc. of the | |
100 | * controls), and for calling other methods on the Controller | |
101 | * to implement the particulars of the action. | |
102 | * | |
103 | * For example, 'click_step()' calls 'performStep()' which | |
104 | * calls 'step()' (which a subclass or instantiator must | |
105 | * provide an implementation for.) | |
106 | * | |
107 | * To simulate one of the buttons being clicked, you may | |
108 | * call 'click_foo()' yourself in code. However, that will | |
109 | * be subject to the current restrictions of the interface. | |
110 | * You may be better off calling one of the "internal" methods | |
111 | * like 'performStep()'. | |
112 | */ | |
113 | this.connect = function(dict) { | |
114 | var $this = this; | |
115 | ||
116 | var keys = ["start", "stop", "step", "load", "edit", "reset"]; | |
117 | for (var i in keys) { | |
118 | var key = keys[i]; | |
119 | var value = dict[key]; | |
120 | if (typeof value === 'string') { | |
121 | value = document.getElementById(value); | |
122 | } | |
123 | if (value) { | |
124 | value.onclick = this._makeEventHandler(value, key); | |
125 | this.controls[key] = value; | |
126 | } | |
127 | } | |
128 | ||
129 | var keys = ["speed", "source", "input", "display"]; | |
130 | for (var i in keys) { | |
131 | var key = keys[i]; | |
132 | var value = dict[key]; | |
133 | if (typeof value === 'string') { | |
134 | value = document.getElementById(value); | |
135 | } | |
136 | if (value) { | |
137 | this[key] = value; | |
138 | // special cases | |
139 | if (key === 'speed') { | |
140 | this.speed.value = this.delay; | |
141 | this.speed.onchange = function(e) { | |
142 | $this.setDelayFrom($this.speed); | |
143 | if ($this.intervalId !== undefined) { | |
144 | $this.stop(); | |
145 | $this.start(); | |
146 | } | |
147 | } | |
148 | } else if (key === 'input') { | |
149 | this.input.onchange = function(e) { | |
150 | if (this.value.length > 0) { | |
151 | $this.unblock(); | |
152 | } | |
153 | } | |
154 | } | |
155 | } | |
156 | } | |
157 | ||
158 | this.click_stop(); | |
159 | }; | |
160 | ||
161 | this.click_step = function(e) { | |
164 | * Override this to change how the delay is acquired from the 'speed' | |
165 | * control. | |
166 | */ | |
167 | this.setDelayFrom = function(elem) { | |
168 | this.delay = elem.max - elem.value; // parseInt(elem.value, 10) | |
169 | }; | |
170 | ||
171 | /****************** | |
172 | * action: Step | |
173 | */ | |
174 | this.clickStep = function(e) { | |
162 | 175 | if (this.state === STOPPED) return; |
163 | this.click_stop(); | |
176 | this.clickStop(); | |
164 | 177 | this.state = PAUSED; |
165 | 178 | this.performStep(); |
179 | }; | |
180 | ||
181 | this.performStep = function() { | |
182 | var code = this.step(); | |
183 | if (code === 'stop') { | |
184 | this.clickStop(); | |
185 | this.state = STOPPED; | |
186 | } else if (code === 'block') { | |
187 | this.state = BLOCKED; | |
188 | } | |
166 | 189 | }; |
167 | 190 | |
168 | 191 | /* |
173 | 196 | * - `block` to indicate that the program is waiting for more input. |
174 | 197 | */ |
175 | 198 | this.step = function() { |
176 | alert("step() NotImplementedError"); | |
177 | }; | |
178 | ||
179 | this.performStep = function() { | |
180 | var code = this.step(); | |
181 | if (code === 'stop') { | |
182 | this.terminate(); | |
183 | } else if (code === 'block') { | |
184 | this.state = BLOCKED; | |
185 | } | |
186 | }; | |
187 | ||
188 | this.click_load = function(e) { | |
189 | this.click_stop(); | |
190 | this.load(this.source.value); | |
191 | this.state = PAUSED; | |
192 | if (this.controls.edit) this.controls.edit.style.display = "inline"; | |
193 | if (this.controls.load) this.controls.load.style.display = "none"; | |
194 | if (this.controls.start) this.controls.start.disabled = false; | |
195 | if (this.controls.step) this.controls.step.disabled = false; | |
196 | if (this.controls.stop) this.controls.stop.disabled = true; | |
197 | if (this.controls.reset) this.controls.reset.disabled = false; | |
198 | if (this.display) this.display.style.display = "block"; | |
199 | if (this.source) this.source.style.display = "none"; | |
200 | }; | |
201 | ||
202 | this.load = function(text) { | |
203 | alert("load() NotImplementedError"); | |
204 | }; | |
205 | ||
206 | /* | |
207 | * Loads a source text into the source element. | |
208 | */ | |
209 | this.loadSource = function(text) { | |
210 | if (this.source) this.source.value = text; | |
211 | this.load(text); | |
212 | this.state = PAUSED; | |
213 | }; | |
214 | ||
215 | /* | |
216 | * Loads a source text into the source element. | |
217 | * Assumes it comes from an element in the document, so it translates | |
218 | * the basic HTML escapes (but no others) to plain text. | |
219 | */ | |
220 | this.loadSourceFromHTML = function(html) { | |
221 | var text = html; | |
222 | text = text.replace(/\</g, '<'); | |
223 | text = text.replace(/\>/g, '>'); | |
224 | text = text.replace(/\&/g, '&'); | |
225 | this.loadSource(text); | |
226 | }; | |
227 | ||
228 | /* | |
229 | * This is the basic idea, but not fleshed out yet. | |
230 | * - Should we cache the source somewhere? | |
231 | * - While we're waiting, should we disable the UI / show a spinny? | |
232 | */ | |
233 | this.loadSourceFromURL = function(url, errorCallback) { | |
234 | var http = new XMLHttpRequest(); | |
235 | var $this = this; | |
236 | if (!errorCallback) { | |
237 | errorCallback = function(http) { | |
238 | $this.loadSource( | |
239 | "Error: could not load " + url + ": " + http.statusText | |
240 | ); | |
241 | } | |
242 | } | |
243 | http.open("get", url, true); | |
244 | http.onload = function(e) { | |
245 | if (http.readyState === 4 && http.responseText) { | |
246 | if (http.status === 200) { | |
247 | $this.loadSource(http.responseText); | |
248 | } else { | |
249 | errorCallback(http); | |
250 | } | |
251 | } | |
252 | }; | |
253 | http.send(null); | |
254 | }; | |
255 | ||
256 | this.click_edit = function(e) { | |
257 | this.click_stop(); | |
258 | if (this.controls.edit) this.controls.edit.style.display = "none"; | |
259 | if (this.controls.load) this.controls.load.style.display = "inline"; | |
260 | if (this.controls.start) this.controls.start.disabled = true; | |
261 | if (this.controls.step) this.controls.step.disabled = true; | |
262 | if (this.controls.stop) this.controls.stop.disabled = true; | |
263 | if (this.controls.reset) this.controls.reset.disabled = true; | |
264 | if (this.display) this.display.style.display = "none"; | |
265 | if (this.source) this.source.style.display = "block"; | |
266 | }; | |
267 | ||
268 | this.click_start = function(e) { | |
269 | this.start(); | |
199 | throw new Error("step() NotImplementedError"); | |
200 | }; | |
201 | ||
202 | /****************** | |
203 | * action: Start | |
204 | */ | |
205 | this.clickStart = function(e) { | |
206 | this.performStart(); | |
270 | 207 | if (this.controls.start) this.controls.start.disabled = true; |
271 | 208 | if (this.controls.step) this.controls.step.disabled = false; |
272 | 209 | if (this.controls.stop) this.controls.stop.disabled = false; |
273 | 210 | }; |
274 | 211 | |
212 | this.performStart = function() { | |
213 | this.start(); | |
214 | }; | |
215 | ||
275 | 216 | this.start = function() { |
276 | 217 | if (this.intervalId !== undefined) |
277 | 218 | return; |
278 | this.step(); | |
219 | this.performStep(); | |
279 | 220 | var $this = this; |
280 | 221 | this.intervalId = setInterval(function() { |
281 | 222 | $this.performStep(); |
283 | 224 | this.state = RUNNING; |
284 | 225 | }; |
285 | 226 | |
286 | this.click_stop = function(e) { | |
287 | this.stop(); | |
288 | this.state = PAUSED; | |
289 | /* why is this check here? ... */ | |
290 | if (this.controls.stop && this.controls.stop.disabled) { | |
291 | return; | |
292 | } | |
227 | /****************** | |
228 | * action: Stop | |
229 | */ | |
230 | this.clickStop = function(e) { | |
231 | this.performStop(); | |
293 | 232 | if (this.controls.start) this.controls.start.disabled = false; |
294 | 233 | if (this.controls.step) this.controls.step.disabled = false; |
295 | 234 | if (this.controls.stop) this.controls.stop.disabled = true; |
296 | 235 | }; |
297 | 236 | |
298 | this.terminate = function(e) { | |
237 | this.performStop = function() { | |
299 | 238 | this.stop(); |
300 | this.state = STOPPED; | |
301 | if (this.controls.start) this.controls.start.disabled = true; | |
302 | if (this.controls.step) this.controls.step.disabled = true; | |
303 | if (this.controls.stop) this.controls.stop.disabled = true; | |
239 | this.state = PAUSED; | |
304 | 240 | }; |
305 | 241 | |
306 | 242 | this.stop = function() { |
310 | 246 | this.intervalId = undefined; |
311 | 247 | }; |
312 | 248 | |
313 | this.click_reset = function(e) { | |
314 | this.click_stop(); | |
315 | this.load(this.source.value); | |
249 | /****************** | |
250 | * action: Reset | |
251 | */ | |
252 | this.clickReset = function(e) { | |
253 | this.clickStop(); | |
254 | this.performReset(); | |
316 | 255 | if (this.controls.start) this.controls.start.disabled = false; |
317 | 256 | if (this.controls.step) this.controls.step.disabled = false; |
318 | 257 | if (this.controls.stop) this.controls.stop.disabled = true; |
319 | 258 | }; |
320 | 259 | |
321 | /* | |
322 | * Override this to change how the delay is acquired from the 'speed' | |
323 | * element. | |
324 | */ | |
325 | this.setDelayFrom = function(elem) { | |
326 | this.delay = elem.max - elem.value; | |
260 | this.performReset = function(state) { | |
261 | if (state !== undefined) { | |
262 | this.setResetState(state); | |
263 | } | |
264 | this.reset(this.resetState); | |
265 | }; | |
266 | ||
267 | this.reset = function(state) { | |
268 | throw new Error("reset() NotImplementedError"); | |
269 | }; | |
270 | ||
271 | this.setResetState = function(state) { | |
272 | this.resetState = state; | |
327 | 273 | }; |
328 | 274 | }; |
0 | /* | |
1 | * This file is part of yoob.js version 0.13 | |
2 | * Available from https://github.com/catseye/yoob.js/ | |
3 | * This file is in the public domain. See http://unlicense.org/ for details. | |
4 | */ | |
5 | if (window.yoob === undefined) yoob = {}; | |
6 | ||
7 | /* | |
8 | * Functions for creating elements. | |
9 | */ | |
10 | ||
11 | yoob.makeCanvas = function(container, width, height) { | |
12 | var canvas = document.createElement('canvas'); | |
13 | if (width) { | |
14 | canvas.width = width; | |
15 | } | |
16 | if (height) { | |
17 | canvas.height = height; | |
18 | } | |
19 | container.appendChild(canvas); | |
20 | return canvas; | |
21 | }; | |
22 | ||
23 | yoob.makeButton = function(container, labelText, fun) { | |
24 | var button = document.createElement('button'); | |
25 | button.innerHTML = labelText; | |
26 | container.appendChild(button); | |
27 | if (fun) { | |
28 | button.onclick = fun; | |
29 | } | |
30 | return button; | |
31 | }; | |
32 | ||
33 | yoob.checkBoxNumber = 0; | |
34 | yoob.makeCheckbox = function(container, checked, labelText, fun) { | |
35 | var checkbox = document.createElement('input'); | |
36 | checkbox.type = "checkbox"; | |
37 | checkbox.id = 'cfzzzb_' + yoob.checkBoxNumber; | |
38 | checkbox.checked = checked; | |
39 | var label = document.createElement('label'); | |
40 | label.htmlFor = 'cfzzzb_' + yoob.checkBoxNumber; | |
41 | yoob.checkBoxNumber += 1; | |
42 | label.appendChild(document.createTextNode(labelText)); | |
43 | ||
44 | container.appendChild(checkbox); | |
45 | container.appendChild(label); | |
46 | ||
47 | if (fun) { | |
48 | checkbox.onchange = function(e) { | |
49 | fun(checkbox.checked); | |
50 | }; | |
51 | } | |
52 | return checkbox; | |
53 | }; | |
54 | ||
55 | yoob.makeTextInput = function(container, size, value) { | |
56 | var input = document.createElement('input'); | |
57 | input.size = "" + (size || 12); | |
58 | input.value = value || ""; | |
59 | container.appendChild(input); | |
60 | return input; | |
61 | }; | |
62 | ||
63 | yoob.makeSlider = function(container, min, max, value, fun) { | |
64 | var slider = document.createElement('input'); | |
65 | slider.type = "range"; | |
66 | slider.min = min; | |
67 | slider.max = max; | |
68 | slider.value = value || 0; | |
69 | if (fun) { | |
70 | slider.onchange = function(e) { | |
71 | fun(parseInt(slider.value, 10)); | |
72 | }; | |
73 | } | |
74 | container.appendChild(slider); | |
75 | return slider; | |
76 | }; | |
77 | ||
78 | yoob.makeParagraph = function(container, innerHTML) { | |
79 | var p = document.createElement('p'); | |
80 | p.innerHTML = innerHTML || ''; | |
81 | container.appendChild(p); | |
82 | return p; | |
83 | }; | |
84 | ||
85 | yoob.makeSpan = function(container, innerHTML) { | |
86 | var span = document.createElement('span'); | |
87 | span.innerHTML = innerHTML || ''; | |
88 | container.appendChild(span); | |
89 | return span; | |
90 | }; | |
91 | ||
92 | yoob.makeDiv = function(container, innerHTML) { | |
93 | var div = document.createElement('div'); | |
94 | div.innerHTML = innerHTML || ''; | |
95 | container.appendChild(div); | |
96 | return div; | |
97 | }; | |
98 | ||
99 | yoob.makePre = function(container, innerHTML) { | |
100 | var elem = document.createElement('pre'); | |
101 | elem.innerHTML = innerHTML || ''; | |
102 | container.appendChild(elem); | |
103 | return elem; | |
104 | }; | |
105 | ||
106 | yoob.makePanel = function(container, title, isOpen) { | |
107 | isOpen = !!isOpen; | |
108 | var panelContainer = document.createElement('div'); | |
109 | var button = document.createElement('button'); | |
110 | var innerContainer = document.createElement('div'); | |
111 | innerContainer.style.display = isOpen ? "block" : "none"; | |
112 | ||
113 | button.innerHTML = (isOpen ? "∇" : "⊳") + " " + title; | |
114 | button.onclick = function(e) { | |
115 | isOpen = !isOpen; | |
116 | button.innerHTML = (isOpen ? "∇" : "⊳") + " " + title; | |
117 | innerContainer.style.display = isOpen ? "block" : "none"; | |
118 | }; | |
119 | ||
120 | panelContainer.appendChild(button); | |
121 | panelContainer.appendChild(innerContainer); | |
122 | container.appendChild(panelContainer); | |
123 | return innerContainer; | |
124 | }; | |
125 | ||
126 | yoob.makeTextArea = function(container, cols, rows, initial) { | |
127 | var textarea = document.createElement('textarea'); | |
128 | textarea.rows = "" + rows; | |
129 | textarea.cols = "" + cols; | |
130 | if (initial) { | |
131 | textarea.value = initial; | |
132 | } | |
133 | container.appendChild(textarea); | |
134 | return textarea; | |
135 | }; | |
136 | ||
137 | yoob.makeLineBreak = function(container) { | |
138 | var br = document.createElement('br'); | |
139 | container.appendChild(br); | |
140 | return br; | |
141 | }; | |
142 | ||
143 | yoob.makeSelect = function(container, labelText, optionsArray, fun, def) { | |
144 | var label = document.createElement('label'); | |
145 | label.innerHTML = labelText; | |
146 | container.appendChild(label); | |
147 | ||
148 | var select = document.createElement("select"); | |
149 | ||
150 | for (var i = 0; i < optionsArray.length; i++) { | |
151 | var op = document.createElement("option"); | |
152 | op.value = optionsArray[i][0]; | |
153 | op.text = optionsArray[i][1]; | |
154 | if (optionsArray[i].length > 2) { | |
155 | op.selected = optionsArray[i][2]; | |
156 | } else { | |
157 | op.selected = false; | |
158 | } | |
159 | select.options.add(op); | |
160 | } | |
161 | ||
162 | if (fun) { | |
163 | select.onchange = function(e) { | |
164 | fun(optionsArray[select.selectedIndex][0]); | |
165 | }; | |
166 | } | |
167 | ||
168 | if (def) { | |
169 | var i = 0; | |
170 | var opt = select.options[i]; | |
171 | while (opt) { | |
172 | if (opt.value === def) { | |
173 | select.selectedIndex = i; | |
174 | if (fun) fun(def); | |
175 | break; | |
176 | } | |
177 | i++; | |
178 | opt = select.options[i]; | |
179 | } | |
180 | } | |
181 | ||
182 | container.appendChild(select); | |
183 | return select; | |
184 | }; | |
185 | ||
186 | var SliderPlusTextInput = function() { | |
187 | this.init = function(cfg) { | |
188 | this.slider = cfg.slider; | |
189 | this.textInput = cfg.textInput; | |
190 | this.callback = cfg.callback; | |
191 | return this; | |
192 | }; | |
193 | ||
194 | this.set = function(value) { | |
195 | this.slider.value = "" + value; | |
196 | this.textInput.value = "" + value; | |
197 | this.callback(value); | |
198 | }; | |
199 | }; | |
200 | ||
201 | yoob.makeSliderPlusTextInput = function(container, label, min_, max_, size, value, fun) { | |
202 | yoob.makeSpan(container, label); | |
203 | var slider = yoob.makeSlider(container, min_, max_, value); | |
204 | var s = "" + value; | |
205 | var textInput = yoob.makeTextInput(container, size, s); | |
206 | slider.onchange = function(e) { | |
207 | textInput.value = slider.value; | |
208 | fun(parseInt(slider.value, 10)); | |
209 | }; | |
210 | textInput.onchange = function(e) { | |
211 | var v = parseInt(textInput.value, 10); | |
212 | if (!isNaN(v)) { | |
213 | slider.value = "" + v; | |
214 | fun(v); | |
215 | } | |
216 | }; | |
217 | return new SliderPlusTextInput().init({ | |
218 | 'slider': slider, | |
219 | 'textInput': textInput, | |
220 | 'callback': fun | |
221 | }); | |
222 | }; | |
223 | ||
224 | var RangeControl = function() { | |
225 | this.init = function(cfg) { | |
226 | this.slider = cfg.slider; | |
227 | this.textInput = cfg.textInput; | |
228 | this.callback = cfg.callback; | |
229 | this.incButton = cfg.incButton; | |
230 | this.decButton = cfg.decButton; | |
231 | return this; | |
232 | }; | |
233 | ||
234 | this.set = function(value) { | |
235 | this.slider.value = "" + value; | |
236 | this.textInput.value = "" + value; | |
237 | this.callback(value); | |
238 | }; | |
239 | }; | |
240 | ||
241 | yoob.makeRangeControl = function(container, config) { | |
242 | var label = config.label; | |
243 | var min_ = config['min']; | |
244 | var max_ = config['max']; | |
245 | var value = config.value || min_; | |
246 | var callback = config.callback || function(v) {}; | |
247 | var textInputSize = config.textInputSize || 5; | |
248 | var withButtons = config.withButtons === false ? false : true; | |
249 | ||
250 | yoob.makeSpan(container, label); | |
251 | var slider = yoob.makeSlider(container, min_, max_, value); | |
252 | var s = "" + value; | |
253 | var textInput = yoob.makeTextInput(container, textInputSize, s); | |
254 | slider.onchange = function(e) { | |
255 | textInput.value = slider.value; | |
256 | callback(parseInt(slider.value, 10)); | |
257 | }; | |
258 | textInput.onchange = function(e) { | |
259 | var v = parseInt(textInput.value, 10); | |
260 | if (!isNaN(v)) { | |
261 | slider.value = "" + v; | |
262 | callback(v); | |
263 | } | |
264 | }; | |
265 | var incButton; | |
266 | var decButton; | |
267 | if (withButtons) { | |
268 | decButton = yoob.makeButton(container, "-", function() { | |
269 | var v = parseInt(textInput.value, 10); | |
270 | if ((!isNaN(v)) && v > min_) { | |
271 | v--; | |
272 | textInput.value = "" + v; | |
273 | slider.value = "" + v; | |
274 | callback(v); | |
275 | } | |
276 | }); | |
277 | incButton = yoob.makeButton(container, "+", function() { | |
278 | var v = parseInt(textInput.value, 10); | |
279 | if ((!isNaN(v)) && v < max_) { | |
280 | v++; | |
281 | textInput.value = "" + v; | |
282 | slider.value = "" + v; | |
283 | callback(v); | |
284 | } | |
285 | }); | |
286 | } | |
287 | return new RangeControl().init({ | |
288 | 'slider': slider, | |
289 | 'incButton': incButton, | |
290 | 'decButton': decButton, | |
291 | 'textInput': textInput, | |
292 | 'callback': callback | |
293 | }); | |
294 | }; | |
295 | ||
296 | yoob.makeSVG = function(container) { | |
297 | var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); | |
298 | /* <svg viewBox = "0 0 200 200" version = "1.1"> */ | |
299 | container.appendChild(svg); | |
300 | return svg; | |
301 | }; | |
302 | ||
303 | yoob.makeSVGElem = function(svg, tag, cfg) { | |
304 | var elem = document.createElementNS(svg.namespaceURI, tag); | |
305 | Object.keys(cfg).forEach(function(key) { | |
306 | elem.setAttribute(key, cfg[key]); | |
307 | }); | |
308 | svg.appendChild(elem); | |
309 | return elem; | |
310 | }; |
0 | /* | |
1 | * This file is part of yoob.js version 0.11 | |
2 | * Available from https://github.com/catseye/yoob.js/ | |
3 | * This file is in the public domain. See http://unlicense.org/ for details. | |
4 | */ | |
5 | if (window.yoob === undefined) yoob = {}; | |
6 | ||
7 | /* | |
8 | * A view (in the MVC sense) for depicting a yoob.Playfield (-compatible) | |
9 | * object onto any DOM element that supports innerHTML. | |
10 | */ | |
11 | yoob.PlayfieldHTMLView = function() { | |
12 | this.init = function(cfg) { | |
13 | this.pf = cfg.playfield; | |
14 | this.element = cfg.element; | |
15 | return this; | |
16 | }; | |
17 | ||
18 | /*** Chainable setters ***/ | |
19 | ||
20 | this.setPlayfield = function(pf) { | |
21 | this.pf = pf; | |
22 | return this; | |
23 | }; | |
24 | ||
25 | this.setElement = function(element) { | |
26 | this.element = element; | |
27 | return this; | |
28 | }; | |
29 | ||
30 | /* | |
31 | * For compatibility with PlayfieldCanvasView. Sets the font size. | |
32 | */ | |
33 | this.setCellDimensions = function(cellWidth, cellHeight) { | |
34 | this.element.style.fontSize = cellHeight + "px"; | |
35 | return this; | |
36 | }; | |
37 | ||
38 | /* | |
39 | * Override to convert Playfield values to HTML. | |
40 | */ | |
41 | this.render = function(value) { | |
42 | if (value === undefined) return ' '; | |
43 | return value; | |
44 | }; | |
45 | ||
46 | /* | |
47 | * Override if you like. | |
48 | */ | |
49 | this.wrapCursorText = function(cursor, text) { | |
50 | var fillStyle = this.cursorFillStyle || "#50ff50"; | |
51 | return '<span style="background: ' + fillStyle + '">' + | |
52 | text + '</span>'; | |
53 | }; | |
54 | ||
55 | /* | |
56 | * Render the playfield, as HTML, on the DOM element. | |
57 | */ | |
58 | this.draw = function() { | |
59 | var text = ""; | |
60 | var cursors = this.pf.cursors; | |
61 | var lowerY = this.pf.getLowerY(); | |
62 | var upperY = this.pf.getUpperY(); | |
63 | var lowerX = this.pf.getLowerX(); | |
64 | var upperX = this.pf.getUpperX(); | |
65 | for (var y = lowerY; y <= upperY; y++) { | |
66 | var row = ""; | |
67 | for (var x = lowerX; x <= upperX; x++) { | |
68 | var rendered = this.render(this.pf.get(x, y)); | |
69 | for (var i = 0; i < cursors.length; i++) { | |
70 | if (cursors[i].x === x && cursors[i].y === y) { | |
71 | rendered = this.wrapCursorText(cursors[i], rendered); | |
72 | } | |
73 | } | |
74 | row += rendered; | |
75 | } | |
76 | text += row + "\n"; | |
77 | } | |
78 | this.element.innerHTML = text; | |
79 | }; | |
80 | ||
81 | }; |
0 | 0 | /* |
1 | * This file is part of yoob.js version 0.6 | |
1 | * This file is part of yoob.js version 0.13 | |
2 | 2 | * Available from https://github.com/catseye/yoob.js/ |
3 | 3 | * This file is in the public domain. See http://unlicense.org/ for details. |
4 | 4 | */ |
8 | 8 | * A two-dimensional Cartesian grid of values. |
9 | 9 | */ |
10 | 10 | yoob.Playfield = function() { |
11 | this._store = {}; | |
12 | this.minX = undefined; | |
13 | this.minY = undefined; | |
14 | this.maxX = undefined; | |
15 | this.maxY = undefined; | |
16 | this._default = undefined; | |
11 | this.init = function(cfg) { | |
12 | cfg = cfg || {}; | |
13 | this._default = cfg.defaultValue; | |
14 | this.cursors = cfg.cursors || []; | |
15 | this.clear(); | |
16 | return this; | |
17 | }; | |
18 | ||
19 | /*** Chainable setters ***/ | |
17 | 20 | |
18 | 21 | /* |
19 | 22 | * Set the default value for this Playfield. This |
24 | 27 | this._default = v; |
25 | 28 | return this; |
26 | 29 | }; |
30 | ||
31 | /* | |
32 | * Set the list of cursors to the given list of yoob.Cursor (or compatible) | |
33 | * objects. | |
34 | */ | |
35 | this.setCursors = function(cursors) { | |
36 | this.cursors = cursors; | |
37 | return this; | |
38 | }; | |
39 | ||
40 | /*** Accessors, etc. ***/ | |
27 | 41 | |
28 | 42 | /* |
29 | 43 | * Obtain the value at (x, y). The default value will |
44 | 58 | this.put = function(x, y, value) { |
45 | 59 | var key = x+','+y; |
46 | 60 | if (value === undefined || value === this._default) { |
61 | // NOTE: this does not recalculate the bounds, nor | |
62 | // will it set the bounds back to 'undefined' | |
63 | // if the playfield is now empty. | |
47 | 64 | delete this._store[key]; |
48 | 65 | return; |
49 | 66 | } |
101 | 118 | this.minY = undefined; |
102 | 119 | this.maxX = undefined; |
103 | 120 | this.maxY = undefined; |
121 | return this; | |
104 | 122 | }; |
105 | 123 | |
106 | 124 | /* |
114 | 132 | this.put(x, y, this.get(x, y - dy)); |
115 | 133 | } |
116 | 134 | } |
117 | } else { alert("scrollRectangleY(" + dy + ") notImplemented"); } | |
135 | } else { | |
136 | throw new Error("scrollRectangleY(" + dy + ") notImplemented"); | |
137 | } | |
118 | 138 | }; |
119 | 139 | |
120 | 140 | this.clearRectangle = function(minX, minY, maxX, maxY) { |
192 | 212 | * fun is a callback which takes three parameters: |
193 | 213 | * x, y, and value. If this callback returns a value, |
194 | 214 | * it is written into the Playfield at that position. |
195 | * This function ensures a particular order. | |
215 | * This function ensures a particular order. For efficiency, | |
216 | * This function knows about the structure of the backing | |
217 | * store, so if you override .get() or .put() in a subclass, | |
218 | * you should also override this. | |
196 | 219 | */ |
197 | 220 | this.foreach = function(fun) { |
198 | 221 | for (var y = this.minY; y <= this.maxY; y++) { |
202 | 225 | if (value === undefined) |
203 | 226 | continue; |
204 | 227 | var result = fun(x, y, value); |
228 | // TODO: Playfield.UNDEFINED vs. undefined meaning "no change"? | |
205 | 229 | if (result !== undefined) { |
206 | if (result === ' ') { | |
207 | result = undefined; | |
208 | } | |
230 | this.put(x, y, result); | |
231 | } | |
232 | } | |
233 | } | |
234 | }; | |
235 | ||
236 | this.foreachVonNeumannNeighbour = function(x, y, fun) { | |
237 | for (var dx = -1; dx <= 1; dx++) { | |
238 | for (var dy = -1; dy <= 1; dy++) { | |
239 | if (dx === 0 && dy === 0) | |
240 | continue; | |
241 | var value = this.get(x + dx, y + dy); | |
242 | if (value === undefined) | |
243 | continue; | |
244 | var result = fun(x, y, value); | |
245 | // TODO: Playfield.UNDEFINED vs. undefined meaning "no change"? | |
246 | if (result !== undefined) { | |
209 | 247 | this.put(x, y, result); |
210 | 248 | } |
211 | 249 | } |
235 | 273 | if (maxDy === undefined) maxDy = 0; |
236 | 274 | for (var y = this.minY + minDy; y <= this.maxY + maxDy; y++) { |
237 | 275 | for (var x = this.minX + minDx; x <= this.maxX + maxDx; x++) { |
238 | destPf.putDirty(x, y, fun(pf, x, y)); | |
276 | destPf.putDirty(x, y, fun(this, x, y)); | |
239 | 277 | } |
240 | 278 | } |
241 | 279 | destPf.recalculateBounds(); |
282 | 320 | return this.maxY - this.minY + 1; |
283 | 321 | } |
284 | 322 | }; |
323 | ||
324 | /* | |
325 | * Return the requested bounds of the occupied portion of the playfield. | |
326 | * "Occupation" in this sense includes all cursors. | |
327 | * | |
328 | * These may return 'undefined' if there is nothing in the playfield. | |
329 | * | |
330 | * Override these if you want to draw some portion of the | |
331 | * playfield which is not the whole playfield. | |
332 | */ | |
333 | this.getLowerX = function() { | |
334 | var minX = this.getMinX(); | |
335 | for (var i = 0; i < this.cursors.length; i++) { | |
336 | if (minX === undefined || this.cursors[i].x < minX) { | |
337 | minX = this.cursors[i].x; | |
338 | } | |
339 | } | |
340 | return minX; | |
341 | }; | |
342 | this.getUpperX = function() { | |
343 | var maxX = this.getMaxX(); | |
344 | for (var i = 0; i < this.cursors.length; i++) { | |
345 | if (maxX === undefined || this.cursors[i].x > maxX) { | |
346 | maxX = this.cursors[i].x; | |
347 | } | |
348 | } | |
349 | return maxX; | |
350 | }; | |
351 | this.getLowerY = function() { | |
352 | var minY = this.getMinY(); | |
353 | for (var i = 0; i < this.cursors.length; i++) { | |
354 | if (minY === undefined || this.cursors[i].y < minY) { | |
355 | minY = this.cursors[i].y; | |
356 | } | |
357 | } | |
358 | return minY; | |
359 | }; | |
360 | this.getUpperY = function() { | |
361 | var maxY = this.getMaxY(); | |
362 | for (var i = 0; i < this.cursors.length; i++) { | |
363 | if (maxY === undefined || this.cursors[i].y > maxY) { | |
364 | maxY = this.cursors[i].y; | |
365 | } | |
366 | } | |
367 | return maxY; | |
368 | }; | |
369 | ||
370 | /* | |
371 | * Returns the number of occupied cells in the x direction. | |
372 | * "Occupation" in this sense includes all cursors. | |
373 | */ | |
374 | this.getCursoredExtentX = function() { | |
375 | if (this.getLowerX() === undefined || this.getUpperX() === undefined) { | |
376 | return 0; | |
377 | } else { | |
378 | return this.getUpperX() - this.getLowerX() + 1; | |
379 | } | |
380 | }; | |
381 | ||
382 | /* | |
383 | * Returns the number of occupied cells in the y direction. | |
384 | * "Occupation" in this sense includes all cursors. | |
385 | */ | |
386 | this.getCursoredExtentY = function() { | |
387 | if (this.getLowerY() === undefined || this.getUpperY() === undefined) { | |
388 | return 0; | |
389 | } else { | |
390 | return this.getUpperY() - this.getLowerY() + 1; | |
391 | } | |
392 | }; | |
393 | ||
394 | /* | |
395 | * Cursored read/write interface | |
396 | */ | |
397 | this.read = function(index) { | |
398 | var cursor = this.cursors[index || 0]; | |
399 | return this.get(cursor.getX(), cursor.getY()); | |
400 | }; | |
401 | ||
402 | this.write = function(value, index) { | |
403 | var cursor = this.cursors[index || 0]; | |
404 | this.put(cursor.getX(), cursor.getY(), value); | |
405 | return this; | |
406 | }; | |
285 | 407 | }; |
0 | 0 | /* |
1 | * This file is part of yoob.js version 0.6 | |
1 | * This file is part of yoob.js version 0.13 | |
2 | 2 | * Available from https://github.com/catseye/yoob.js/ |
3 | 3 | * This file is in the public domain. See http://unlicense.org/ for details. |
4 | 4 | */ |
21 | 21 | * will cause the .select() method of this manager to be called. |
22 | 22 | * it will also call .onselect if that method is present. |
23 | 23 | * |
24 | * controller: a yoob.Controller (or compatible object) that will | |
25 | * be informed of the selection, if no callback was supplied | |
26 | * when the item was added. | |
24 | * setPreset: (optional) a callback which will be called whenever | |
25 | * a new preset is selected. If this is not given, an individual | |
26 | * callback must be supplied with each preset as it is added. | |
27 | 27 | */ |
28 | 28 | this.init = function(cfg) { |
29 | 29 | this.selectElem = cfg.selectElem; |
30 | this.controller = cfg.controller || null; | |
30 | if (cfg.setPreset) { | |
31 | this.setPreset = cfg.setPreset; | |
32 | } | |
31 | 33 | this.clear(); |
32 | 34 | var $this = this; |
33 | 35 | this.selectElem.onchange = function() { |
51 | 53 | /* |
52 | 54 | * Adds a preset to this PresetManager. When it is selected, |
53 | 55 | * the given callback will be called, being passed the id as the |
54 | * first argument. If no callback is provided, a default callback, | |
55 | * which loads the contents of the element with the specified id | |
56 | * into the configured yoob.Controller, will be used. | |
56 | * first argument. If no callback is provided, the default callback, | |
57 | * configured with setPreset in the init() configuration, will be used. | |
57 | 58 | */ |
58 | 59 | this.add = function(id, callback) { |
59 | 60 | var opt = document.createElement("option"); |
61 | 62 | opt.value = id; |
62 | 63 | this.selectElem.options.add(opt); |
63 | 64 | var $this = this; |
64 | this.reactTo[id] = callback || function(id) { | |
65 | $this.controller.click_stop(); // in case it is currently running | |
66 | $this.controller.loadSourceFromHTML( | |
67 | document.getElementById(id).innerHTML | |
68 | ); | |
69 | }; | |
65 | this.reactTo[id] = callback || this.setPreset; | |
70 | 66 | return this; |
67 | }; | |
68 | ||
69 | this.setPreset = function(id) { | |
70 | throw new Error("No default setPreset callback configured"); | |
71 | 71 | }; |
72 | 72 | |
73 | 73 | /* |
119 | 119 | } |
120 | 120 | return this; |
121 | 121 | }; |
122 | ||
123 | /* | |
124 | * When called with a yoob.SourceManager and an array of | |
125 | * 2-element arrays of a name and a source text, this preset | |
126 | * manager will be populated with each source text as a | |
127 | * named preset. A callback to load the source text with | |
128 | * the SourceManager will be automatically supplied. | |
129 | */ | |
130 | this.populateFromPairs = function(sourceManager, pairs) { | |
131 | function makeCallback(sourceText) { | |
132 | return function(id) { | |
133 | sourceManager.loadSource(sourceText); | |
134 | } | |
135 | } | |
136 | ||
137 | for (var i = 0; i < pairs.length; i++) { | |
138 | this.add(pairs[i][0], makeCallback(pairs[i][1])); | |
139 | } | |
140 | this.select(pairs[0][0]); | |
141 | return this; | |
142 | }; | |
122 | 143 | }; |
0 | /* | |
1 | * This file is part of yoob.js version 0.8 | |
2 | * Available from https://github.com/catseye/yoob.js/ | |
3 | * This file is in the public domain. See http://unlicense.org/ for details. | |
4 | */ | |
5 | if (window.yoob === undefined) yoob = {}; | |
6 | ||
7 | /* | |
8 | * A SourceManager co-operates with a Controller and maybe a PresetManager. | |
9 | * It is for editing a program/configuration in some editing interface | |
10 | * which is mutually exclusive, UI-wise, with the run/animation interface. | |
11 | */ | |
12 | yoob.SourceManager = function() { | |
13 | /* | |
14 | * editor: an element (usually a textarea) which stores the source code | |
15 | * hideDuringEdit: a list of elements which will be hidden when editing | |
16 | * (typically this will be the animation display of a yoob.Controller) | |
17 | * disableDuringEdit: a list of elements which will be disabled when editing | |
18 | * storageKey: key under which sources will be saved/loaded from localStorage | |
19 | * panelContainer: an element into which to add the created button panel | |
20 | * (if you do not give this, no panel will be created. You're on your own.) | |
21 | * onDone: if given, if a function, it becomes the onDone method on this | |
22 | */ | |
23 | this.init = function(cfg) { | |
24 | this.supportsLocalStorage = ( | |
25 | window['localStorage'] !== undefined && | |
26 | window['localStorage'] !== null | |
27 | ); | |
28 | this.editor = cfg.editor; | |
29 | this.hideDuringEdit = cfg.hideDuringEdit; | |
30 | this.prevDisplay = {}; | |
31 | for (var i = 0; i < this.hideDuringEdit.length; i++) { | |
32 | this.prevDisplay[this.hideDuringEdit[i]] = this.hideDuringEdit[i].display; | |
33 | } | |
34 | this.disableDuringEdit = cfg.disableDuringEdit; | |
35 | this.storageKey = cfg.storageKey || 'default'; | |
36 | this.controls = {}; | |
37 | if (cfg.panelContainer) { | |
38 | this.panel = this.makePanel(); | |
39 | cfg.panelContainer.appendChild(this.panel); | |
40 | } | |
41 | if (cfg.onDone) { | |
42 | this.onDone = cfg.onDone; | |
43 | } | |
44 | this.clickDone(); | |
45 | return this; | |
46 | }; | |
47 | ||
48 | this.makePanel = function() { | |
49 | var panel = document.createElement('div'); | |
50 | var $this = this; | |
51 | var makeButton = function(action) { | |
52 | var button = document.createElement('button'); | |
53 | var upperAction = action.charAt(0).toUpperCase() + action.slice(1); | |
54 | button.innerHTML = upperAction; | |
55 | button.style.width = "5em"; | |
56 | panel.appendChild(button); | |
57 | button.onclick = function(e) { | |
58 | if ($this['click' + upperAction]) { | |
59 | $this['click' + upperAction](); | |
60 | } | |
61 | } | |
62 | $this.controls[action] = button; | |
63 | }; | |
64 | var keys = ["edit", "done", "load", "save"]; | |
65 | for (var i = 0; i < keys.length; i++) { | |
66 | makeButton(keys[i]); | |
67 | } | |
68 | return panel; | |
69 | }; | |
70 | ||
71 | this.clickEdit = function() { | |
72 | var hde = this.hideDuringEdit; | |
73 | for (var i = 0; i < hde.length; i++) { | |
74 | this.prevDisplay[hde[i]] = hde[i].style.display; | |
75 | hde[i].style.display = 'none'; | |
76 | } | |
77 | for (var i = 0; i < this.disableDuringEdit.length; i++) { | |
78 | this.disableDuringEdit[i].disabled = true; | |
79 | /* But if it's not a form control, disabled is meaningless. */ | |
80 | this.disableDuringEdit[i].style.pointerEvents = 'none'; | |
81 | this.disableDuringEdit[i].style.opacity = '0.5'; | |
82 | } | |
83 | this.editor.style.display = 'block'; | |
84 | this.controls.edit.disabled = true; | |
85 | var keys = ["done", "load", "save"]; | |
86 | for (var i = 0; i < keys.length; i++) { | |
87 | this.controls[keys[i]].disabled = false; | |
88 | } | |
89 | this.onEdit(); | |
90 | }; | |
91 | ||
92 | this.clickDone = function() { | |
93 | var hde = this.hideDuringEdit; | |
94 | for (var i = 0; i < hde.length; i++) { | |
95 | hde[i].style.display = this.prevDisplay[hde[i]]; | |
96 | } | |
97 | for (var i = 0; i < this.disableDuringEdit.length; i++) { | |
98 | this.disableDuringEdit[i].disabled = false; | |
99 | /* But if it's not a form control, disabled is meaningless. */ | |
100 | this.disableDuringEdit[i].style.pointerEvents = 'auto'; | |
101 | this.disableDuringEdit[i].style.opacity = '1.0'; | |
102 | } | |
103 | this.editor.style.display = 'none'; | |
104 | this.controls.edit.disabled = false; | |
105 | var keys = ["done", "load", "save"]; | |
106 | for (var i = 0; i < keys.length; i++) { | |
107 | this.controls[keys[i]].disabled = true; | |
108 | } | |
109 | this.onDone(); | |
110 | }; | |
111 | ||
112 | this.clickLoad = function() { | |
113 | if (!this.supportsLocalStorage) { | |
114 | var s = "Your browser does not support Local Storage.\n\n"; | |
115 | s += "You may instead open a local file in a text editor, "; | |
116 | s += "select all, copy to clipboard, then paste into "; | |
117 | s += "the textarea, to load a source you have saved locally."; | |
118 | alert(s); | |
119 | return; | |
120 | } | |
121 | this.loadSource( | |
122 | localStorage.getItem('yoob:' + this.storageKey + ':default') | |
123 | ); | |
124 | }; | |
125 | ||
126 | this.clickSave = function() { | |
127 | if (!this.supportsLocalStorage) { | |
128 | var s = "Your browser does not support Local Storage.\n\n"; | |
129 | s += "You may instead select all in the textarea, copy to "; | |
130 | s += "clipboard, open a local text editor, paste in there, "; | |
131 | s += "and save, to save a source locally."; | |
132 | alert(s); | |
133 | return; | |
134 | } | |
135 | localStorage.setItem( | |
136 | 'yoob:' + this.storageKey + ':default', | |
137 | this.getEditorText() | |
138 | ); | |
139 | }; | |
140 | ||
141 | this.onEdit = function() { | |
142 | }; | |
143 | ||
144 | /* | |
145 | * Override this to load it into the controller | |
146 | */ | |
147 | this.onDone = function() { | |
148 | }; | |
149 | ||
150 | /* | |
151 | * Loads a source text into the editor element. | |
152 | */ | |
153 | this.loadSource = function(text) { | |
154 | this.setEditorText(text); | |
155 | this.onDone(); | |
156 | }; | |
157 | ||
158 | /* | |
159 | * You may need to override if your editor is not a textarea. | |
160 | */ | |
161 | this.setEditorText = function(text) { | |
162 | this.editor.value = text; | |
163 | }; | |
164 | ||
165 | /* | |
166 | * You may need to override if your editor is not a textarea. | |
167 | */ | |
168 | this.getEditorText = function() { | |
169 | return this.editor.value; | |
170 | }; | |
171 | ||
172 | /* | |
173 | * Loads a source text into the source element. | |
174 | * Assumes it comes from an element in the document, so it translates | |
175 | * the basic HTML escapes (but no others) to plain text. | |
176 | */ | |
177 | this.loadSourceFromHTML = function(html) { | |
178 | var text = html; | |
179 | text = text.replace(/\</g, '<'); | |
180 | text = text.replace(/\>/g, '>'); | |
181 | text = text.replace(/\&/g, '&'); | |
182 | this.loadSource(text); | |
183 | }; | |
184 | ||
185 | /* | |
186 | * This is the basic idea, but not fleshed out yet. | |
187 | * - Should we cache the source somewhere? | |
188 | * - While we're waiting, should we disable the UI / show a spinny? | |
189 | */ | |
190 | this.loadSourceFromURL = function(url, errorCallback) { | |
191 | var http = new XMLHttpRequest(); | |
192 | var $this = this; | |
193 | if (!errorCallback) { | |
194 | errorCallback = function(http) { | |
195 | $this.loadSource( | |
196 | "Error: could not load " + url + ": " + http.statusText | |
197 | ); | |
198 | } | |
199 | } | |
200 | http.open("get", url, true); | |
201 | http.onload = function(e) { | |
202 | if (http.readyState === 4 && http.responseText) { | |
203 | if (http.status === 200) { | |
204 | $this.loadSource(http.responseText); | |
205 | } else { | |
206 | errorCallback(http); | |
207 | } | |
208 | } | |
209 | }; | |
210 | http.send(null); | |
211 | }; | |
212 | }; |
0 | exampleConfigurations = [ | |
1 | [ | |
2 | "test.brak", | |
3 | "000000000000000000 <*<*>*>*<*<*>*>*\n-----------------d-i---------------\n" | |
4 | ], | |
5 | [ | |
6 | "test2.brak", | |
7 | " <*\n000000000000000000000000000000000000000000000000000000000 *[---]\n--------------------------------------------------------d-i- --\n" | |
8 | ], | |
9 | [ | |
10 | "test3.brak", | |
11 | " *\n <<*[--]*\n000000000000000000 *[----- --]\n-----------------d-i-- --------\n\n" | |
12 | ] | |
13 | ]; |