mirror of
https://github.com/scratchfoundation/scratch-blocks.git
synced 2025-07-03 15:40:26 -04:00
834 lines
26 KiB
JavaScript
834 lines
26 KiB
JavaScript
/**
|
|
* Blockly Apps: Common code
|
|
*
|
|
* Copyright 2013 Google Inc.
|
|
* http://blockly.googlecode.com/
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
/**
|
|
* @fileoverview Common support code for Blockly apps.
|
|
* @author fraser@google.com (Neil Fraser)
|
|
*/
|
|
'use strict';
|
|
|
|
var BlocklyApps = {};
|
|
|
|
/**
|
|
* Lookup for names of languages. Keys should be in ISO 639 format.
|
|
*/
|
|
BlocklyApps.LANGUAGE_NAME = {
|
|
'af': 'Afrikaans',
|
|
'ar': 'العربية',
|
|
'be-tarask': 'Taraškievica',
|
|
'br': 'Brezhoneg',
|
|
'ca': 'Català',
|
|
'cdo': '閩東語',
|
|
'cs': 'Česky',
|
|
'da': 'Dansk',
|
|
'de': 'Deutsch',
|
|
'el': 'Ελληνικά',
|
|
'en': 'English',
|
|
'es': 'Español',
|
|
'eu': 'Euskara',
|
|
'fa': 'فارسی',
|
|
'fi': 'Suomi',
|
|
'fo': 'Føroyskt',
|
|
'fr': 'Français',
|
|
'frr': 'Frasch',
|
|
'gl': 'Galego',
|
|
'hak': '客家話',
|
|
'he': 'עברית',
|
|
'hu': 'Magyar',
|
|
'ia': 'Interlingua',
|
|
'it': 'Italiano',
|
|
'ja': '日本語',
|
|
'ka': 'ქართული',
|
|
'km': 'ភាសាខ្មែរ',
|
|
'ko': '한국어',
|
|
'ksh': 'Ripoarėsch',
|
|
'ky': 'Кыргызча',
|
|
'la': 'Latine',
|
|
'lb': 'Lëtzebuergesch',
|
|
'lt': 'Lietuvių',
|
|
'lv': 'Latviešu',
|
|
'ml': 'മലയാളം',
|
|
'mk': 'Македонски',
|
|
'mr': 'मराठी',
|
|
'ms': 'Bahasa Melayu',
|
|
'mzn': 'مازِرونی',
|
|
'nb': 'Norsk Bokmål',
|
|
'nl': 'Nederlands, Vlaams',
|
|
'oc': 'Lenga d\'òc',
|
|
'pa': 'पंजाबी',
|
|
'pl': 'Polski',
|
|
'pms': 'Piemontèis',
|
|
'ps': 'پښتو',
|
|
'pt': 'Português',
|
|
'ro': 'Română',
|
|
'pt-br': 'Português Brasileiro',
|
|
'ru': 'Русский',
|
|
'sk': 'Slovenčina',
|
|
'sr': 'Српски',
|
|
'sv': 'Svenska',
|
|
'sw': 'Kishwahili',
|
|
'th': 'ภาษาไทย',
|
|
'tr': 'Türkçe',
|
|
'uk': 'Українська',
|
|
'vi': 'Tiếng Việt',
|
|
'zh-hans': '简体字',
|
|
'zh-hant': '中文',
|
|
'zh-tw': '國語'
|
|
};
|
|
|
|
/**
|
|
* List of RTL languages.
|
|
*/
|
|
BlocklyApps.LANGUAGE_RTL = ['ar', 'fa', 'he', 'mzn', 'ps'];
|
|
|
|
/**
|
|
* Lookup for Blockly core block language pack.
|
|
*/
|
|
BlocklyApps.LANGUAGE_PACK = {
|
|
'cdo': 'msg/js/zh_tw.js',
|
|
'de': 'msg/js/de.js',
|
|
'fa': 'msg/js/fa.js',
|
|
'frr': 'msg/js/de.js',
|
|
'hu': 'msg/js/hu.js',
|
|
'it': 'msg/js/it.js',
|
|
'ksh': 'msg/js/de.js',
|
|
'lb': 'msg/js/de.js',
|
|
'pt': 'msg/js/pt_br.js',
|
|
'pt-br': 'msg/js/pt_br.js',
|
|
'ru': 'msg/js/ru.js',
|
|
'uk': 'msg/js/uk.js',
|
|
'vi': 'msg/js/vi.js',
|
|
'zh-hans': 'msg/js/zh_tw.js',
|
|
'zh-hant': 'msg/js/zh_tw.js',
|
|
'zh-tw': 'msg/js/zh_tw.js',
|
|
'default': 'msg/js/en.js'
|
|
};
|
|
|
|
/**
|
|
* User's language (e.g. "en").
|
|
* @type string=
|
|
*/
|
|
BlocklyApps.LANG = undefined;
|
|
|
|
/**
|
|
* List of languages supported by this app. Values should be in ISO 639 format.
|
|
* @type !Array.<string>=
|
|
*/
|
|
BlocklyApps.LANGUAGES = undefined;
|
|
|
|
/**
|
|
* Extracts a parameter from the URL.
|
|
* If the parameter is absent default_value is returned.
|
|
* @param {string} name The name of the parameter.
|
|
* @param {string} defaultValue Value to return if paramater not found.
|
|
* @return {string} The parameter value or the default value if not found.
|
|
*/
|
|
BlocklyApps.getStringParamFromUrl = function(name, defaultValue) {
|
|
var val =
|
|
window.location.search.match(new RegExp('[?&]' + name + '=([^&]+)'));
|
|
return val ? decodeURIComponent(val[1].replace(/\+/g, '%20')) : defaultValue;
|
|
};
|
|
|
|
/**
|
|
* Extracts a numeric parameter from the URL.
|
|
* If the parameter is absent or less than min_value, min_value is
|
|
* returned. If it is greater than max_value, max_value is returned.
|
|
* @param {string} name The name of the parameter.
|
|
* @param {number} minValue The minimum legal value.
|
|
* @param {number} maxValue The maximum legal value.
|
|
* @return {number} A number in the range [min_value, max_value].
|
|
*/
|
|
BlocklyApps.getNumberParamFromUrl = function(name, minValue, maxValue) {
|
|
var val = Number(BlocklyApps.getStringParamFromUrl(name, 'NaN'));
|
|
return isNaN(val) ? minValue : Math.min(Math.max(minValue, val), maxValue);
|
|
};
|
|
|
|
/**
|
|
* Use a series of heuristics that determine the likely language of this user.
|
|
* Use a session cookie to load/save the language preference.
|
|
* @return {string} User's language.
|
|
* @throws {string} If no languages exist in this app.
|
|
*/
|
|
BlocklyApps.getLang = function() {
|
|
// First choice: The URL specified language.
|
|
var lang = BlocklyApps.getStringParamFromUrl('lang', '');
|
|
if (BlocklyApps.LANGUAGES.indexOf(lang) != -1) {
|
|
// Save this explicit choice as cookie.
|
|
// Use of a session cookie for saving language is explicitly permitted
|
|
// in the EU's Cookie Consent Exemption policy. Section 3.6:
|
|
// http://ec.europa.eu/justice/data-protection/article-29/documentation/
|
|
// opinion-recommendation/files/2012/wp194_en.pdf
|
|
document.cookie = 'lang=' + escape(lang) + '; path=/';
|
|
return lang;
|
|
}
|
|
// Second choice: Language cookie.
|
|
var cookie = document.cookie.match(/(^|;)\s*lang=(\w+)/);
|
|
if (cookie) {
|
|
lang = unescape(cookie[2]);
|
|
if (BlocklyApps.LANGUAGES.indexOf(lang) != -1) {
|
|
return lang;
|
|
}
|
|
}
|
|
// Third choice: The browser's language.
|
|
lang = navigator.language;
|
|
if (BlocklyApps.LANGUAGES.indexOf(lang) != -1) {
|
|
return lang;
|
|
}
|
|
// Fourth choice: English.
|
|
lang = 'en';
|
|
if (BlocklyApps.LANGUAGES.indexOf(lang) != -1) {
|
|
return lang;
|
|
}
|
|
// Fifth choice: I'm feeling lucky.
|
|
if (BlocklyApps.LANGUAGES.length) {
|
|
return BlocklyApps.LANGUAGES[0];
|
|
}
|
|
// Sixth choice: Die.
|
|
throw 'No languages available.';
|
|
};
|
|
|
|
/**
|
|
* Is the current language (BlocklyApps.LANG) an RTL language?
|
|
* @return {boolean} True if RTL, false if LTR.
|
|
*/
|
|
BlocklyApps.isRtl = function() {
|
|
return BlocklyApps.LANGUAGE_RTL.indexOf(BlocklyApps.LANG) != -1;
|
|
};
|
|
|
|
/**
|
|
* Look up the Blockly language pack for current language (BlocklyApps.LANG).
|
|
* @return {string} URL to langugae pack (e.g. 'msg/js/en.js').
|
|
*/
|
|
BlocklyApps.languagePack = function() {
|
|
return BlocklyApps.LANGUAGE_PACK[BlocklyApps.LANG] ||
|
|
BlocklyApps.LANGUAGE_PACK['default'];
|
|
};
|
|
|
|
/**
|
|
* Common startup tasks for all apps.
|
|
*/
|
|
BlocklyApps.init = function() {
|
|
// Set the page title with the content of the H1 title.
|
|
document.title = document.getElementById('title').textContent;
|
|
|
|
// Set the HTML's language and direction.
|
|
// document.dir fails in Mozilla, use document.body.parentNode.dir instead.
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=151407
|
|
var rtl = BlocklyApps.isRtl();
|
|
document.head.parentElement.setAttribute('dir', rtl ? 'rtl' : 'ltr');
|
|
document.head.parentElement.setAttribute('lang', BlocklyApps.LANG);
|
|
|
|
// Sort languages alphabetically.
|
|
var languages = [];
|
|
for (var i = 0; i < BlocklyApps.LANGUAGES.length; i++) {
|
|
var lang = BlocklyApps.LANGUAGES[i];
|
|
languages.push([BlocklyApps.LANGUAGE_NAME[lang], lang]);
|
|
}
|
|
var comp = function(a, b) {
|
|
// Sort based on first argument ('English', 'Русский', '简体字', etc).
|
|
if (a[0] > b[0]) return 1;
|
|
if (a[0] < b[0]) return -1;
|
|
return 0;
|
|
};
|
|
languages.sort(comp);
|
|
// Populate the language selection menu.
|
|
var languageMenu = document.getElementById('languageMenu');
|
|
languageMenu.options.length = 0;
|
|
for (var i = 0; i < languages.length; i++) {
|
|
var tuple = languages[i];
|
|
var lang = tuple[tuple.length - 1];
|
|
var option = new Option(tuple[0], lang);
|
|
if (lang == BlocklyApps.LANG) {
|
|
option.selected = true;
|
|
}
|
|
languageMenu.options.add(option);
|
|
}
|
|
languageMenu.addEventListener('change', BlocklyApps.changeLanguage, true);
|
|
|
|
// Disable the link button if page isn't backed by App Engine storage.
|
|
var linkButton = document.getElementById('linkButton');
|
|
if ('BlocklyStorage' in window) {
|
|
BlocklyStorage['HTTPREQUEST_ERROR'] =
|
|
BlocklyApps.getMsg('httpRequestError');
|
|
BlocklyStorage['LINK_ALERT'] = BlocklyApps.getMsg('linkAlert');
|
|
BlocklyStorage['HASH_ERROR'] = BlocklyApps.getMsg('hashError');
|
|
BlocklyStorage['XML_ERROR'] = BlocklyApps.getMsg('xmlError');
|
|
// Swap out the BlocklyStorage's alert() for a nicer dialog.
|
|
BlocklyStorage.alert = BlocklyApps.storageAlert;
|
|
BlocklyApps.bindClick('linkButton', BlocklyStorage.link);
|
|
} else if (linkButton) {
|
|
linkButton.className = 'disabled';
|
|
}
|
|
|
|
if (document.getElementById('codeButton')) {
|
|
BlocklyApps.bindClick('codeButton', BlocklyApps.showCode);
|
|
}
|
|
|
|
// Fixes viewport for small screens.
|
|
var viewport = document.querySelector('meta[name="viewport"]');
|
|
if (viewport && screen.availWidth < 725) {
|
|
viewport.setAttribute('content',
|
|
'width=725, initial-scale=.35, user-scalable=no');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Initialize Blockly for a readonly iframe. Called on page load.
|
|
* XML argument may be generated from the console with:
|
|
* encodeURIComponent(Blockly.Xml.domToText(Blockly.Xml.workspaceToDom(Blockly.mainWorkspace)).slice(5, -6))
|
|
*/
|
|
BlocklyApps.initReadonly = function() {
|
|
Blockly.inject(document.getElementById('blockly'),
|
|
{path: '../../',
|
|
readOnly: true,
|
|
rtl: BlocklyApps.isRtl(),
|
|
scrollbars: false});
|
|
|
|
// Add the blocks.
|
|
var xml = BlocklyApps.getStringParamFromUrl('xml', '');
|
|
xml = Blockly.Xml.textToDom('<xml>' + xml + '</xml>');
|
|
Blockly.Xml.domToWorkspace(Blockly.mainWorkspace, xml);
|
|
};
|
|
|
|
/**
|
|
* Load blocks saved on App Engine Storage or in session/local storage.
|
|
* @param {string} defaultXml Text representation of default blocks.
|
|
*/
|
|
BlocklyApps.loadBlocks = function(defaultXml) {
|
|
try {
|
|
var loadOnce = window.sessionStorage.loadOnceBlocks;
|
|
} catch(e) {
|
|
// Firefox sometimes throws a SecurityError when accessing sessionStorage.
|
|
// Restarting Firefox fixes this, so it looks like a bug.
|
|
var loadOnce = null;
|
|
}
|
|
if ('BlocklyStorage' in window && window.location.hash.length > 1) {
|
|
// An href with #key trigers an AJAX call to retrieve saved blocks.
|
|
BlocklyStorage.retrieveXml(window.location.hash.substring(1));
|
|
} else if (loadOnce) {
|
|
// Language switching stores the blocks during the reload.
|
|
delete window.sessionStorage.loadOnceBlocks;
|
|
var xml = Blockly.Xml.textToDom(loadOnce);
|
|
Blockly.Xml.domToWorkspace(Blockly.mainWorkspace, xml);
|
|
} else if (defaultXml) {
|
|
// Load the editor with default starting blocks.
|
|
var xml = Blockly.Xml.textToDom(defaultXml);
|
|
Blockly.Xml.domToWorkspace(Blockly.mainWorkspace, xml);
|
|
} else if ('BlocklyStorage' in window) {
|
|
// Restore saved blocks in a separate thread so that subsequent
|
|
// initialization is not affected from a failed load.
|
|
window.setTimeout(BlocklyStorage.restoreBlocks, 0);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Save the blocks and reload with a different language.
|
|
*/
|
|
BlocklyApps.changeLanguage = function() {
|
|
// Store the blocks for the duration of the reload.
|
|
var xml = Blockly.Xml.workspaceToDom(Blockly.mainWorkspace);
|
|
var text = Blockly.Xml.domToText(xml);
|
|
window.sessionStorage.loadOnceBlocks = text;
|
|
|
|
var languageMenu = document.getElementById('languageMenu');
|
|
var newLang = encodeURIComponent(
|
|
languageMenu.options[languageMenu.selectedIndex].value);
|
|
var search = window.location.search;
|
|
if (search.length <= 1) {
|
|
search = '?lang=' + newLang;
|
|
} else if (search.match(/[?&]lang=[^&]*/)) {
|
|
search = search.replace(/([?&]lang=)[^&]*/, '$1' + newLang);
|
|
} else {
|
|
search = search.replace(/\?/, '?lang=' + newLang + '&');
|
|
}
|
|
|
|
window.location = window.location.protocol + '//' +
|
|
window.location.host + window.location.pathname + search;
|
|
};
|
|
|
|
/**
|
|
* Highlight the block (or clear highlighting).
|
|
* @param {?string} id ID of block that triggered this action.
|
|
*/
|
|
BlocklyApps.highlight = function(id) {
|
|
if (id) {
|
|
var m = id.match(/^block_id_(\d+)$/);
|
|
if (m) {
|
|
id = m[1];
|
|
}
|
|
}
|
|
Blockly.mainWorkspace.highlightBlock(id);
|
|
};
|
|
|
|
/**
|
|
* If the user has executed too many actions, we're probably in an infinite
|
|
* loop. Sadly I wasn't able to solve the Halting Problem.
|
|
* @param {?string} opt_id ID of loop block to highlight.
|
|
* @throws {Infinity} Throws an error to terminate the user's program.
|
|
*/
|
|
BlocklyApps.checkTimeout = function(opt_id) {
|
|
if (opt_id) {
|
|
BlocklyApps.log.push([null, opt_id]);
|
|
}
|
|
if (BlocklyApps.ticks-- < 0) {
|
|
throw Infinity;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Is the dialog currently onscreen?
|
|
* @private
|
|
*/
|
|
BlocklyApps.isDialogVisible_ = false;
|
|
|
|
/**
|
|
* A closing dialog should animate towards this element.
|
|
* @type Element
|
|
* @private
|
|
*/
|
|
BlocklyApps.dialogOrigin_ = null;
|
|
|
|
/**
|
|
* A function to call when a dialog closes.
|
|
* @type Function
|
|
* @private
|
|
*/
|
|
BlocklyApps.dialogDispose_ = null;
|
|
|
|
/**
|
|
* Show the dialog pop-up.
|
|
* @param {!Element} content DOM element to display in the dialog.
|
|
* @param {Element} origin Animate the dialog opening/closing from/to this
|
|
* DOM element. If null, don't show any animations for opening or closing.
|
|
* @param {boolean} animate Animate the dialog opening (if origin not null).
|
|
* @param {boolean} modal If true, grey out background and prevent interaction.
|
|
* @param {!Object} style A dictionary of style rules for the dialog.
|
|
* @param {Function} disposeFunc An optional function to call when the dialog
|
|
* closes. Normally used for unhooking events.
|
|
*/
|
|
BlocklyApps.showDialog = function(content, origin, animate, modal, style,
|
|
disposeFunc) {
|
|
if (BlocklyApps.isDialogVisible_) {
|
|
BlocklyApps.hideDialog(false);
|
|
}
|
|
BlocklyApps.isDialogVisible_ = true;
|
|
BlocklyApps.dialogOrigin_ = origin;
|
|
BlocklyApps.dialogDispose_ = disposeFunc;
|
|
var dialog = document.getElementById('dialog');
|
|
var shadow = document.getElementById('dialogShadow');
|
|
var border = document.getElementById('dialogBorder');
|
|
|
|
// Copy all the specified styles to the dialog.
|
|
for (var name in style) {
|
|
dialog.style[name] = style[name];
|
|
}
|
|
if (modal) {
|
|
shadow.style.visibility = 'visible';
|
|
shadow.style.opacity = 0.3;
|
|
var header = document.createElement('div');
|
|
header.id = 'dialogHeader';
|
|
dialog.appendChild(header);
|
|
BlocklyApps.dialogMouseDownWrapper_ =
|
|
Blockly.bindEvent_(header, 'mousedown', null,
|
|
BlocklyApps.dialogMouseDown_);
|
|
}
|
|
dialog.appendChild(content);
|
|
content.className = content.className.replace('dialogHiddenContent', '');
|
|
|
|
function endResult() {
|
|
// Check that the dialog wasn't closed during opening.
|
|
if (BlocklyApps.isDialogVisible_) {
|
|
dialog.style.visibility = 'visible';
|
|
dialog.style.zIndex = 1;
|
|
border.style.visibility = 'hidden';
|
|
}
|
|
}
|
|
if (animate && origin) {
|
|
BlocklyApps.matchBorder_(origin, false, 0.2);
|
|
BlocklyApps.matchBorder_(dialog, true, 0.8);
|
|
// In 175ms show the dialog and hide the animated border.
|
|
window.setTimeout(endResult, 175);
|
|
} else {
|
|
// No animation. Just set the final state.
|
|
endResult();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Horizontal start coordinate of dialog drag.
|
|
*/
|
|
BlocklyApps.dialogStartX_ = 0;
|
|
|
|
/**
|
|
* Vertical start coordinate of dialog drag.
|
|
*/
|
|
BlocklyApps.dialogStartY_ = 0;
|
|
|
|
/**
|
|
* Handle start of drag of dialog.
|
|
* @param {!Event} e Mouse down event.
|
|
* @private
|
|
*/
|
|
BlocklyApps.dialogMouseDown_ = function(e) {
|
|
BlocklyApps.dialogUnbindDragEvents_();
|
|
if (Blockly.isRightButton(e)) {
|
|
// Right-click.
|
|
return;
|
|
}
|
|
// Left click (or middle click).
|
|
// Record the starting offset between the current location and the mouse.
|
|
var dialog = document.getElementById('dialog');
|
|
BlocklyApps.dialogStartX_ = dialog.offsetLeft - e.clientX;
|
|
BlocklyApps.dialogStartY_ = dialog.offsetTop - e.clientY;
|
|
|
|
BlocklyApps.dialogMouseUpWrapper_ = Blockly.bindEvent_(document,
|
|
'mouseup', null, BlocklyApps.dialogUnbindDragEvents_);
|
|
BlocklyApps.dialogMouseMoveWrapper_ = Blockly.bindEvent_(document,
|
|
'mousemove', null, BlocklyApps.dialogMouseMove_);
|
|
// This event has been handled. No need to bubble up to the document.
|
|
e.stopPropagation();
|
|
};
|
|
|
|
/**
|
|
* Drag the dialog to follow the mouse.
|
|
* @param {!Event} e Mouse move event.
|
|
* @private
|
|
*/
|
|
BlocklyApps.dialogMouseMove_ = function(e) {
|
|
var dialog = document.getElementById('dialog');
|
|
var dialogLeft = BlocklyApps.dialogStartX_ + e.clientX;
|
|
var dialogTop = BlocklyApps.dialogStartY_ + e.clientY;
|
|
dialogTop = Math.max(dialogTop, 0);
|
|
dialogTop = Math.min(dialogTop, window.innerHeight - dialog.offsetHeight);
|
|
dialogLeft = Math.max(dialogLeft, 0);
|
|
dialogLeft = Math.min(dialogLeft, window.innerWidth - dialog.offsetWidth);
|
|
dialog.style.left = dialogLeft + 'px';
|
|
dialog.style.top = dialogTop + 'px';
|
|
};
|
|
|
|
/**
|
|
* Stop binding to the global mouseup and mousemove events.
|
|
* @private
|
|
*/
|
|
BlocklyApps.dialogUnbindDragEvents_ = function() {
|
|
if (BlocklyApps.dialogMouseUpWrapper_) {
|
|
Blockly.unbindEvent_(BlocklyApps.dialogMouseUpWrapper_);
|
|
BlocklyApps.dialogMouseUpWrapper_ = null;
|
|
}
|
|
if (BlocklyApps.dialogMouseMoveWrapper_) {
|
|
Blockly.unbindEvent_(BlocklyApps.dialogMouseMoveWrapper_);
|
|
BlocklyApps.dialogMouseMoveWrapper_ = null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Hide the dialog pop-up.
|
|
* @param {boolean} opt_animate Animate the dialog closing. Defaults to true.
|
|
* Requires that origin was not null when dialog was opened.
|
|
*/
|
|
BlocklyApps.hideDialog = function(opt_animate) {
|
|
if (!BlocklyApps.isDialogVisible_) {
|
|
return;
|
|
}
|
|
BlocklyApps.dialogUnbindDragEvents_();
|
|
if (BlocklyApps.dialogMouseDownWrapper_) {
|
|
Blockly.unbindEvent_(BlocklyApps.dialogMouseDownWrapper_);
|
|
BlocklyApps.dialogMouseDownWrapper_ = null;
|
|
}
|
|
|
|
BlocklyApps.isDialogVisible_ = false;
|
|
BlocklyApps.dialogDispose_ && BlocklyApps.dialogDispose_();
|
|
BlocklyApps.dialogDispose_ = null;
|
|
var origin = (opt_animate === false) ? null : BlocklyApps.dialogOrigin_;
|
|
var dialog = document.getElementById('dialog');
|
|
var shadow = document.getElementById('dialogShadow');
|
|
var border = document.getElementById('dialogBorder');
|
|
|
|
shadow.style.opacity = 0;
|
|
|
|
function endResult() {
|
|
shadow.style.visibility = 'hidden';
|
|
border.style.visibility = 'hidden';
|
|
}
|
|
if (origin) {
|
|
BlocklyApps.matchBorder_(dialog, false, 0.8);
|
|
BlocklyApps.matchBorder_(origin, true, 0.2);
|
|
// In 175ms hide both the shadow and the animated border.
|
|
window.setTimeout(endResult, 175);
|
|
} else {
|
|
// No animation. Just set the final state.
|
|
endResult();
|
|
}
|
|
dialog.style.visibility = 'hidden';
|
|
dialog.style.zIndex = -1;
|
|
var header = document.getElementById('dialogHeader');
|
|
if (header) {
|
|
header.parentNode.removeChild(header);
|
|
}
|
|
while (dialog.firstChild) {
|
|
var content = dialog.firstChild;
|
|
content.className += ' dialogHiddenContent';
|
|
document.body.appendChild(content);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Match the animated border to the a element's size and location.
|
|
* @param {!Element} element Element to match.
|
|
* @param {boolean} animate Animate to the new location.
|
|
* @param {number} opacity Opacity of border.
|
|
* @private
|
|
*/
|
|
BlocklyApps.matchBorder_ = function(element, animate, opacity) {
|
|
if (!element) {
|
|
return;
|
|
}
|
|
var border = document.getElementById('dialogBorder');
|
|
var bBox = BlocklyApps.getBBox_(element);
|
|
function change() {
|
|
border.style.width = bBox.width + 'px';
|
|
border.style.height = bBox.height + 'px';
|
|
border.style.left = bBox.x + 'px';
|
|
border.style.top = bBox.y + 'px';
|
|
border.style.opacity = opacity;
|
|
}
|
|
if (animate) {
|
|
border.className = 'dialogAnimate';
|
|
window.setTimeout(change, 1);
|
|
} else {
|
|
border.className = '';
|
|
change();
|
|
}
|
|
border.style.visibility = 'visible';
|
|
};
|
|
|
|
/**
|
|
* Compute the absolute coordinates and dimensions of an HTML or SVG element.
|
|
* @param {!Element} element Element to match.
|
|
* @return {!Object} Contains height, width, x, and y properties.
|
|
* @private
|
|
*/
|
|
BlocklyApps.getBBox_ = function(element) {
|
|
if (element.getBBox) {
|
|
// SVG element.
|
|
var bBox = element.getBBox();
|
|
var height = bBox.height;
|
|
var width = bBox.width;
|
|
var xy = Blockly.getAbsoluteXY_(element);
|
|
var x = xy.x;
|
|
var y = xy.y;
|
|
} else {
|
|
// HTML element.
|
|
var height = element.offsetHeight;
|
|
var width = element.offsetWidth;
|
|
var x = 0;
|
|
var y = 0;
|
|
do {
|
|
x += element.offsetLeft;
|
|
y += element.offsetTop;
|
|
element = element.offsetParent;
|
|
} while (element);
|
|
}
|
|
return {
|
|
height: height,
|
|
width: width,
|
|
x: x,
|
|
y: y
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Display a storage-related modal dialog.
|
|
* @param {string} message Text to alert.
|
|
*/
|
|
BlocklyApps.storageAlert = function(message) {
|
|
var container = document.getElementById('containerStorage');
|
|
container.textContent = '';
|
|
var lines = message.split('\n');
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var p = document.createElement('p');
|
|
p.appendChild(document.createTextNode(lines[i]));
|
|
container.appendChild(p);
|
|
}
|
|
|
|
var content = document.getElementById('dialogStorage');
|
|
var origin = document.getElementById('linkButton');
|
|
var style = {
|
|
width: '50%',
|
|
left: '25%',
|
|
top: '5em'
|
|
};
|
|
BlocklyApps.showDialog(content, origin, true, true, style,
|
|
BlocklyApps.stopDialogKeyDown());
|
|
BlocklyApps.startDialogKeyDown();
|
|
};
|
|
|
|
/**
|
|
* Convert the user's code to raw JavaScript.
|
|
* @param {string} code Generated code.
|
|
* @return {string} The code without serial numbers and timeout checks.
|
|
*/
|
|
BlocklyApps.stripCode = function(code) {
|
|
// Strip out serial numbers.
|
|
code = code.replace(/(,\s*)?'block_id_\d+'\)/g, ')');
|
|
// Remove timeouts.
|
|
var regex = new RegExp(Blockly.JavaScript.INFINITE_LOOP_TRAP
|
|
.replace('(%1)', '\\((\'\\d+\')?\\)'), 'g');
|
|
return code.replace(regex, '');
|
|
};
|
|
|
|
/**
|
|
* Show the user's code in raw JavaScript.
|
|
* @param {!Event} e Mouse or touch event.
|
|
*/
|
|
BlocklyApps.showCode = function(e) {
|
|
var origin = e.target;
|
|
var code = Blockly.JavaScript.workspaceToCode();
|
|
code = BlocklyApps.stripCode(code);
|
|
var pre = document.getElementById('containerCode');
|
|
pre.textContent = code;
|
|
if (typeof prettyPrintOne == 'function') {
|
|
code = pre.innerHTML;
|
|
code = prettyPrintOne(code, 'js');
|
|
pre.innerHTML = code;
|
|
}
|
|
|
|
var content = document.getElementById('dialogCode');
|
|
var style = {
|
|
width: '40%',
|
|
left: '30%',
|
|
top: '5em'
|
|
};
|
|
BlocklyApps.showDialog(content, origin, true, true, style,
|
|
BlocklyApps.stopDialogKeyDown);
|
|
BlocklyApps.startDialogKeyDown();
|
|
};
|
|
|
|
/**
|
|
* If the user preses enter, escape, or space, hide the dialog.
|
|
* @param {!Event} e Keyboard event.
|
|
* @private
|
|
*/
|
|
BlocklyApps.dialogKeyDown_ = function(e) {
|
|
if (BlocklyApps.isDialogVisible_) {
|
|
if (e.keyCode == 13 ||
|
|
e.keyCode == 27 ||
|
|
e.keyCode == 32) {
|
|
BlocklyApps.hideDialog(true);
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Start listening for BlocklyApps.dialogKeyDown_.
|
|
*/
|
|
BlocklyApps.startDialogKeyDown = function() {
|
|
document.body.addEventListener('keydown',
|
|
BlocklyApps.dialogKeyDown_, true);
|
|
};
|
|
|
|
/**
|
|
* Stop listening for BlocklyApps.dialogKeyDown_.
|
|
*/
|
|
BlocklyApps.stopDialogKeyDown = function() {
|
|
document.body.removeEventListener('keydown',
|
|
BlocklyApps.dialogKeyDown_, true);
|
|
};
|
|
|
|
/**
|
|
* Gets the message with the given key from the document.
|
|
* @param {string} key The key of the document element.
|
|
* @return {string} The textContent of the specified element,
|
|
* or an error message if the element was not found.
|
|
*/
|
|
BlocklyApps.getMsg = function(key) {
|
|
var msg = BlocklyApps.getMsgOrNull(key);
|
|
return msg === null ? '[Unknown message: ' + key + ']' : msg;
|
|
};
|
|
|
|
/**
|
|
* Gets the message with the given key from the document.
|
|
* @param {string} key The key of the document element.
|
|
* @return {string} The textContent of the specified element,
|
|
* or null if the element was not found.
|
|
*/
|
|
BlocklyApps.getMsgOrNull = function(key) {
|
|
var element = document.getElementById(key);
|
|
if (element) {
|
|
var text = element.textContent;
|
|
// Convert newline sequences.
|
|
text = text.replace(/\\n/g, '\n');
|
|
return text;
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* On touch enabled browsers, add touch-friendly variants of event handlers
|
|
* for elements such as buttons whose event handlers are specified in the
|
|
* markup. For example, ontouchend is treated as equivalent to onclick.
|
|
*/
|
|
BlocklyApps.addTouchEvents = function() {
|
|
// Do nothing if the browser doesn't support touch.
|
|
if (!('ontouchstart' in document.documentElement)) {
|
|
return;
|
|
}
|
|
// Treat ontouchend as equivalent to onclick for buttons.
|
|
var buttons = document.getElementsByTagName('button');
|
|
for (var i = 0, button; button = buttons[i]; i++) {
|
|
if (!button.ontouchend) {
|
|
button.ontouchend = button.onclick;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Add events for touch devices when the window is done loading.
|
|
window.addEventListener('load', BlocklyApps.addTouchEvents, false);
|
|
|
|
/**
|
|
* Bind a function to a button's click event.
|
|
* On touch enabled browsers, ontouchend is treated as equivalent to onclick.
|
|
* @param {string} id ID of button element.
|
|
* @param {!Function} func Event handler to bind.
|
|
*/
|
|
BlocklyApps.bindClick = function(id, func) {
|
|
var el = document.getElementById(id);
|
|
el.addEventListener('click', func, true);
|
|
el.addEventListener('touchend', func, true);
|
|
};
|
|
|
|
/**
|
|
* Load the Prettify CSS and JavaScript.
|
|
*/
|
|
BlocklyApps.importPrettify = function() {
|
|
//<link rel="stylesheet" type="text/css" href="../prettify.css">
|
|
//<script type="text/javascript" src="../prettify.js"></script>
|
|
var link = document.createElement('link');
|
|
link.setAttribute('rel', 'stylesheet');
|
|
link.setAttribute('type', 'text/css');
|
|
link.setAttribute('href', '../prettify.css');
|
|
document.head.appendChild(link);
|
|
var script = document.createElement('script');
|
|
script.setAttribute('type', 'text/javascript');
|
|
script.setAttribute('src', '../prettify.js');
|
|
document.head.appendChild(script);
|
|
};
|