summaryrefslogtreecommitdiff
path: root/blockly/demos/blocklyfactory/workspacefactory/wfactory_view.js
blob: 6bfb8f9e8fc2687acddad3906f0bcef7d3c6be74 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
/**
 * @license
 * Visual Blocks Editor
 *
 * Copyright 2016 Google Inc.
 * https://developers.google.com/blockly/
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Controls the UI elements for workspace factory, mainly the category tabs.
 * Also includes downloading files because that interacts directly with the DOM.
 * Depends on WorkspaceFactoryController (for adding mouse listeners). Tabs for
 * each category are stored in tab map, which associates a unique ID for a
 * category with a particular tab.
 *
 * @author Emma Dauterman (edauterman)
 */

 goog.require('FactoryUtils');

 /**
  * Class for a WorkspaceFactoryView
  * @constructor
  */

WorkspaceFactoryView = function() {
  // For each tab, maps ID of a ListElement to the td DOM element.
  this.tabMap = Object.create(null);
};

/**
 * Adds a category tab to the UI, and updates tabMap accordingly.
 *
 * @param {!string} name The name of the category being created
 * @param {!string} id ID of category being created
 * @return {!Element} DOM element created for tab
 */
WorkspaceFactoryView.prototype.addCategoryRow = function(name, id) {
  var table = document.getElementById('categoryTable');
  var count = table.rows.length;

  // Delete help label and enable category buttons if it's the first category.
  if (count == 0) {
    document.getElementById('categoryHeader').textContent = 'Your Categories:';
  }

  // Create tab.
  var row = table.insertRow(count);
  var nextEntry = row.insertCell(0);
  // Configure tab.
  nextEntry.id = this.createCategoryIdName(name);
  nextEntry.textContent = name;
  // Store tab.
  this.tabMap[id] = table.rows[count].cells[0];
  // Return tab.
  return nextEntry;
};

/**
 * Deletes a category tab from the UI and updates tabMap accordingly.
 *
 * @param {!string} id ID of category to be deleted.
 * @param {!string} name The name of the category to be deleted.
 */
WorkspaceFactoryView.prototype.deleteElementRow = function(id, index) {
  // Delete tab entry.
  delete this.tabMap[id];
  // Delete tab row.
  var table = document.getElementById('categoryTable');
  var count = table.rows.length;
  table.deleteRow(index);

  // If last category removed, add category help text and disable category
  // buttons.
  this.addEmptyCategoryMessage();
};

/**
 * If there are no toolbox elements created, adds a help message to show
 * where categories will appear. Should be called when deleting list elements
 * in case the last element is deleted.
 */
WorkspaceFactoryView.prototype.addEmptyCategoryMessage = function() {
  var table = document.getElementById('categoryTable');
  if (table.rows.length == 0) {
    document.getElementById('categoryHeader').textContent =
        'Your categories will appear here';
  }
}

/**
 * Given the index of the currently selected element, updates the state of
 * the buttons that allow the user to edit the list elements. Updates the edit
 * and arrow buttons. Should be called when adding or removing elements
 * or when changing to a new element or when swapping to a different element.
 *
 * TODO(evd2014): Switch to using CSS to add/remove styles.
 *
 * @param {int} selectedIndex The index of the currently selected category,
 * -1 if no categories created.
 * @param {ListElement} selected The selected ListElement.
 */
WorkspaceFactoryView.prototype.updateState = function(selectedIndex, selected) {
  // Disable/enable editing buttons as necessary.
  document.getElementById('button_editCategory').disabled = selectedIndex < 0 ||
      selected.type != ListElement.TYPE_CATEGORY;
  document.getElementById('button_remove').disabled = selectedIndex < 0;
  document.getElementById('button_up').disabled =
      selectedIndex <= 0 ? true : false;
  var table = document.getElementById('categoryTable');
  document.getElementById('button_down').disabled = selectedIndex >=
      table.rows.length - 1 || selectedIndex < 0 ? true : false;
  // Disable/enable the workspace as necessary.
  this.disableWorkspace(this.shouldDisableWorkspace(selected));
};

/**
 * Determines the DOM id for a category given its name.
 *
 * @param {!string} name Name of category
 * @return {!string} ID of category tab
 */
WorkspaceFactoryView.prototype.createCategoryIdName = function(name) {
  return 'tab_' + name;
};

/**
 * Switches a tab on or off.
 *
 * @param {!string} id ID of the tab to switch on or off.
 * @param {boolean} selected True if tab should be on, false if tab should be
 * off.
 */
WorkspaceFactoryView.prototype.setCategoryTabSelection =
    function(id, selected) {
  if (!this.tabMap[id]) {
    return;   // Exit if tab does not exist.
  }
  this.tabMap[id].className = selected ? 'tabon' : 'taboff';
};

/**
 * Used to bind a click to a certain DOM element (used for category tabs).
 * Taken directly from code.js
 *
 * @param {string|!Element} e1 tab element or corresponding id string
 * @param {!Function} func Function to be executed on click
 */
WorkspaceFactoryView.prototype.bindClick = function(el, func) {
  if (typeof el == 'string') {
    el = document.getElementById(el);
  }
  el.addEventListener('click', func, true);
  el.addEventListener('touchend', func, true);
};

/**
 * Creates a file and downloads it. In some browsers downloads, and in other
 * browsers, opens new tab with contents.
 *
 * @param {!string} filename Name of file
 * @param {!Blob} data Blob containing contents to download
 */
WorkspaceFactoryView.prototype.createAndDownloadFile =
    function(filename, data) {
   var clickEvent = new MouseEvent("click", {
     "view": window,
     "bubbles": true,
     "cancelable": false
   });
   var a = document.createElement('a');
   a.href = window.URL.createObjectURL(data);
   a.download = filename;
   a.textContent = 'Download file!';
   a.dispatchEvent(clickEvent);
 };

/**
 * Given the ID of a certain category, updates the corresponding tab in
 * the DOM to show a new name.
 *
 * @param {!string} newName Name of string to be displayed on tab
 * @param {!string} id ID of category to be updated
 *
 */
WorkspaceFactoryView.prototype.updateCategoryName = function(newName, id) {
  this.tabMap[id].textContent = newName;
  this.tabMap[id].id = this.createCategoryIdName(newName);
};

/**
 * Moves a tab from one index to another. Adjusts index inserting before
 * based on if inserting before or after. Checks that the indexes are in
 * bounds, throws error if not.
 *
 * @param {!string} id The ID of the category to move.
 * @param {int} newIndex The index to move the category to.
 * @param {int} oldIndex The index the category is currently at.
 */
WorkspaceFactoryView.prototype.moveTabToIndex =
    function(id, newIndex, oldIndex) {
  var table = document.getElementById('categoryTable');
  // Check that indexes are in bounds
  if (newIndex < 0 || newIndex >= table.rows.length || oldIndex < 0 ||
      oldIndex >= table.rows.length) {
    throw new Error('Index out of bounds when moving tab in the view.');
  }

  if (newIndex < oldIndex) {
    // Inserting before.
    var row = table.insertRow(newIndex);
    row.appendChild(this.tabMap[id]);
    table.deleteRow(oldIndex + 1);
  } else {
    // Inserting after.
    var row = table.insertRow(newIndex + 1);
    row.appendChild(this.tabMap[id]);
    table.deleteRow(oldIndex);
  }
};

/**
 * Given a category ID and color, use that color to color the left border of the
 * tab for that category.
 *
 * @param {!string} id The ID of the category to color.
 * @param {!string} color The color for to be used for the border of the tab.
 * Must be a valid CSS string.
 */
WorkspaceFactoryView.prototype.setBorderColor = function(id, color) {
  var tab = this.tabMap[id];
  tab.style.borderLeftWidth = "8px";
  tab.style.borderLeftStyle = "solid";
  tab.style.borderColor = color;
};

/**
 * Given a separator ID, creates a corresponding tab in the view, updates
 * tab map, and returns the tab.
 *
 * @param {!string} id The ID of the separator.
 * @param {!Element} The td DOM element representing the separator.
 */
WorkspaceFactoryView.prototype.addSeparatorTab = function(id) {
  var table = document.getElementById('categoryTable');
  var count = table.rows.length;

  if (count == 0) {
    document.getElementById('categoryHeader').textContent = 'Your Categories:';
  }
  // Create separator.
  var row = table.insertRow(count);
  var nextEntry = row.insertCell(0);
  // Configure separator.
  nextEntry.style.height = '10px';
  // Store and return separator.
  this.tabMap[id] = table.rows[count].cells[0];
  return nextEntry;
};

/**
 * Disables or enables the workspace by putting a div over or under the
 * toolbox workspace, depending on the value of disable. Used when switching
 * to/from separators where the user shouldn't be able to drag blocks into
 * the workspace.
 *
 * @param {boolean} disable True if the workspace should be disabled, false
 * if it should be enabled.
 */
WorkspaceFactoryView.prototype.disableWorkspace = function(disable) {
  if (disable) {
    document.getElementById('toolbox_section').className = 'disabled';
    document.getElementById('toolbox_blocks').style.pointerEvents = 'none';
  } else {
    document.getElementById('toolbox_section').className = '';
    document.getElementById('toolbox_blocks').style.pointerEvents = 'auto';
  }

};

/**
 * Determines if the workspace should be disabled. The workspace should be
 * disabled if category is a separator or has VARIABLE or PROCEDURE tags.
 *
 * @return {boolean} True if the workspace should be disabled, false otherwise.
 */
WorkspaceFactoryView.prototype.shouldDisableWorkspace = function(category) {
  return category != null && category.type != ListElement.TYPE_FLYOUT &&
      (category.type == ListElement.TYPE_SEPARATOR ||
      category.custom == 'VARIABLE' || category.custom == 'PROCEDURE');
};

/*
 * Removes all categories and separators in the view. Clears the tabMap to
 * reflect this.
 */
WorkspaceFactoryView.prototype.clearToolboxTabs = function() {
  this.tabMap = [];
  var oldCategoryTable = document.getElementById('categoryTable');
  var newCategoryTable = document.createElement('table');
  newCategoryTable.id = 'categoryTable';
  newCategoryTable.style.width = 'auto';
  oldCategoryTable.parentElement.replaceChild(newCategoryTable,
      oldCategoryTable);
};

/**
 * Given a set of blocks currently loaded user-generated shadow blocks, visually
 * marks them without making them actual shadow blocks (allowing them to still
 * be editable and movable).
 *
 * @param {!<Blockly.Block>} blocks Array of user-generated shadow blocks
 * currently loaded.
 */
WorkspaceFactoryView.prototype.markShadowBlocks = function(blocks) {
  for (var i = 0; i < blocks.length; i++) {
    this.markShadowBlock(blocks[i]);
  }
};

/**
 * Visually marks a user-generated shadow block as a shadow block in the
 * workspace without making the block an actual shadow block (allowing it
 * to be moved and edited).
 *
 * @param {!Blockly.Block} block The block that should be marked as a shadow
 *    block (must be rendered).
 */
WorkspaceFactoryView.prototype.markShadowBlock = function(block) {
  // Add Blockly CSS for user-generated shadow blocks.
  Blockly.addClass_(block.svgGroup_, 'shadowBlock');
  // If not a valid shadow block, add a warning message.
  if (!block.getSurroundParent()) {
      block.setWarningText('Shadow blocks must be nested inside' +
          ' other blocks to be displayed.');
  }

  if (FactoryUtils.hasVariableField(block)) {
    block.setWarningText('Cannot make variable blocks shadow blocks.');
  }
};

/**
 * Removes visual marking for a shadow block given a rendered block.
 *
 * @param {!Blockly.Block} block The block that should be unmarked as a shadow
 *    block (must be rendered).
 */
WorkspaceFactoryView.prototype.unmarkShadowBlock = function(block) {
  // Remove Blockly CSS for user-generated shadow blocks.
  if (Blockly.hasClass_(block.svgGroup_, 'shadowBlock')) {
    Blockly.removeClass_(block.svgGroup_, 'shadowBlock');
  }
};

/**
 * Sets the tabs for modes according to which mode the user is currenly
 * editing in.
 *
 * @param {!string} mode The mode being switched to
 *    (WorkspaceFactoryController.MODE_TOOLBOX or WorkspaceFactoryController.MODE_PRELOAD).
 */
WorkspaceFactoryView.prototype.setModeSelection = function(mode) {
  document.getElementById('tab_preload').className = mode ==
      WorkspaceFactoryController.MODE_PRELOAD ? 'tabon' : 'taboff';
  document.getElementById('preload_div').style.display = mode ==
      WorkspaceFactoryController.MODE_PRELOAD ? 'block' : 'none';
  document.getElementById('tab_toolbox').className = mode ==
      WorkspaceFactoryController.MODE_TOOLBOX ? 'tabon' : 'taboff';
  document.getElementById('toolbox_div').style.display = mode ==
      WorkspaceFactoryController.MODE_TOOLBOX ? 'block' : 'none';
};

/**
 * Updates the help text above the workspace depending on the selected mode.
 *
 * @param {!string} mode The selected mode (WorkspaceFactoryController.MODE_TOOLBOX or
 *    WorkspaceFactoryController.MODE_PRELOAD).
 */
WorkspaceFactoryView.prototype.updateHelpText = function(mode) {
  if (mode == WorkspaceFactoryController.MODE_TOOLBOX) {
    var helpText = 'Drag blocks into the workspace to configure the toolbox '
        + 'in your custom workspace.';
  } else {
    var helpText = 'Drag blocks into the workspace to pre-load them in your '
        + 'custom workspace.'
  }
  document.getElementById('editHelpText').textContent = helpText;
};

/**
 * Sets the basic options that are not dependent on if there are categories
 * or a single flyout of blocks. Updates checkboxes and text fields.
 */
WorkspaceFactoryView.prototype.setBaseOptions = function() {
  // Set basic options.
  document.getElementById('option_css_checkbox').checked = true;
    document.getElementById('option_infiniteBlocks_checkbox').checked = true;
  document.getElementById('option_maxBlocks_number').value = 100;
  document.getElementById('option_media_text').value =
      'https://blockly-demo.appspot.com/static/media/';
  document.getElementById('option_readOnly_checkbox').checked = false;
  document.getElementById('option_rtl_checkbox').checked = false;
  document.getElementById('option_sounds_checkbox').checked = true;

  // Uncheck grid and zoom options and hide suboptions.
  document.getElementById('option_grid_checkbox').checked = false;
  document.getElementById('grid_options').style.display = 'none';
  document.getElementById('option_zoom_checkbox').checked = false;
  document.getElementById('zoom_options').style.display = 'none';

  // Set grid options.
  document.getElementById('gridOption_spacing_number').value = 0;
  document.getElementById('gridOption_length_number').value = 1;
  document.getElementById('gridOption_colour_text').value = '#888';
  document.getElementById('gridOption_snap_checkbox').checked = false;

  // Set zoom options.
  document.getElementById('zoomOption_controls_checkbox').checked = true;
  document.getElementById('zoomOption_wheel_checkbox').checked = true;
  document.getElementById('zoomOption_startScale_number').value = 1.0;
  document.getElementById('zoomOption_maxScale_number').value = 3;
  document.getElementById('zoomOption_minScale_number').value = 0.3;
  document.getElementById('zoomOption_scaleSpeed_number').value = 1.2;
};

/**
 * Updates category specific options depending on if there are categories
 * currently present. Updates checkboxes and text fields in the view.
 *
 * @param {boolean} hasCategories True if categories are present, false if all
 *    blocks are displayed in a single flyout.
 */
WorkspaceFactoryView.prototype.setCategoryOptions = function(hasCategories) {
  document.getElementById('option_collapse_checkbox').checked = hasCategories;
  document.getElementById('option_comments_checkbox').checked = hasCategories;
  document.getElementById('option_disable_checkbox').checked = hasCategories;
  document.getElementById('option_scrollbars_checkbox').checked = hasCategories;
  document.getElementById('option_trashcan_checkbox').checked = hasCategories;
}