utils.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. /* BlocksCAD utilities for languages and dialogs */
  2. 'use strict';
  3. var BSUtils = BSUtils || {};
  4. /**
  5. * Lookup for names of supported languages. Keys should be in ISO 639 format.
  6. */
  7. BSUtils.LANGUAGE_NAME = {
  8. 'en': 'English',
  9. // 'de': 'Deutsch',
  10. // 'fr': 'Français',
  11. // 'sl' : 'Slovenščina',
  12. 'zh-hant': '繁體中文',
  13. 'zh-hans': '简体中文'
  14. // 'es': 'Español'
  15. };
  16. // BSUtils.LANGUAGE_NAME = {
  17. // 'ar': 'العربية',
  18. // 'be-tarask': 'Taraškievica',
  19. // 'br': 'Brezhoneg',
  20. // 'ca': 'Català',
  21. // 'cs': 'Česky',
  22. // 'da': 'Dansk',
  23. // 'el': 'Ελληνικά',
  24. // 'en': 'English',
  25. // 'es': 'Español',
  26. // 'fa': 'فارسی',
  27. // 'he': 'עברית',
  28. // 'hrx': 'Hunsrik',
  29. // 'hu': 'Magyar',
  30. // 'ia': 'Interlingua',
  31. // 'is': 'Íslenska',
  32. // 'it': 'Italiano',
  33. // 'ja': '日本語',
  34. // 'ko': '한국어',
  35. // 'mk': 'Македонски',
  36. // 'ms': 'Bahasa Melayu',
  37. // 'nb': 'Norsk Bokmål',
  38. // 'nl': 'Nederlands, Vlaams',
  39. // 'oc': 'Lenga d\'òc',
  40. // 'pl': 'Polski',
  41. // 'pms': 'Piemontèis',
  42. // 'pt-br': 'Português Brasileiro',
  43. // 'ro': 'Română',
  44. // 'ru': 'Русский',
  45. // 'sc': 'Sardu',
  46. // 'sk': 'Slovenčina',
  47. // 'sr': 'Српски',
  48. // 'sv': 'Svenska',
  49. // 'th': 'ภาษาไทย',
  50. // 'tlh': 'tlhIngan Hol',
  51. // 'tr': 'Türkçe',
  52. // 'uk': 'Українська',
  53. // 'vi': 'Tiếng Việt',
  54. // 'zh-hans': '簡體中文',
  55. // 'zh-hant': '正體中文'
  56. // };
  57. /**
  58. * List of RTL languages.
  59. */
  60. BSUtils.LANGUAGE_RTL = ['ar', 'fa', 'he'];
  61. /**
  62. * List of languages supported by this app. Values should be in ISO 639 format.
  63. * @type !Array.<string>=
  64. */
  65. BSUtils.LANGUAGES = undefined;
  66. /**
  67. * Extracts a parameter from the URL.
  68. * If the parameter is absent default_value is returned.
  69. * @param {string} name The name of the parameter.
  70. * @param {string} defaultValue Value to return if paramater not found.
  71. * @return {string} The parameter value or the default value if not found.
  72. */
  73. BSUtils.getStringParamFromUrl = function(name, defaultValue) {
  74. var val =
  75. window.location.search.match(new RegExp('[?&]' + name + '=([^&]+)'));
  76. return val ? decodeURIComponent(val[1].replace(/\+/g, '%20')) : defaultValue;
  77. };
  78. /**
  79. * Get the language of this user from the URL.
  80. * @return {string} User's language.
  81. */
  82. BSUtils.getLang = function() {
  83. var lang = BSUtils.getStringParamFromUrl('lang', '');
  84. if (BSUtils.LANGUAGE_NAME[lang] === undefined) {
  85. // check to see if we had a previous language stored in localStorage.
  86. var pLang = Blockscad.getLangFromLS();
  87. if (pLang && pLang != 'null') {
  88. lang = pLang;
  89. }
  90. // Default to English.
  91. else lang = 'en';
  92. }
  93. Blockscad.pLang = lang;
  94. return lang;
  95. };
  96. /**
  97. * User's language (e.g. "en").
  98. * @type string=
  99. */
  100. BSUtils.LANG = BSUtils.getLang();
  101. /**
  102. * Is the current language (BSUtils.LANG) an RTL language?
  103. * @return {boolean} True if RTL, false if LTR.
  104. */
  105. BSUtils.isRtl = function() {
  106. return BSUtils.LANGUAGE_RTL.indexOf(BSUtils.LANG) != -1;
  107. };
  108. /**
  109. * Initialize Blockly for a readonly iframe. Called on page load.
  110. * XML argument may be generated from the console with:
  111. * encodeURIComponent(Blockly.Xml.domToText(Blockly.Xml.workspaceToDom(Blockly.mainWorkspace)).slice(5, -6))
  112. */
  113. BSUtils.initReadonly = function() {
  114. Blockly.inject(document.getElementById('blockly'),
  115. {path: './',
  116. readOnly: true,
  117. rtl: BSUtils.isRtl(),
  118. scrollbars: false});
  119. // Add the blocks.
  120. var xml = BSUtils.getStringParamFromUrl('xml', '');
  121. xml = Blockly.Xml.textToDom('<xml>' + xml + '</xml>');
  122. Blockly.Xml.domToWorkspace(xml, Blockly.mainWorkspace);
  123. };
  124. /**
  125. * Load blocks saved on App Engine Storage or in session/local storage.
  126. * @param {string} defaultXml Text representation of default blocks.
  127. */
  128. BSUtils.loadBlocks = function(defaultXml) {
  129. if (defaultXml) {
  130. // Load the editor with default starting blocks.
  131. var xml = Blockly.Xml.textToDom(defaultXml);
  132. Blockly.Xml.domToWorkspace(xml, Blockly.mainWorkspace);
  133. } else if ('BlocklyStorage' in window) {
  134. // Restore saved blocks in a separate thread so that subsequent
  135. // initialization is not affected from a failed load.
  136. window.setTimeout(BlocklyStorage.restoreBlocks, 0);
  137. }
  138. };
  139. /**
  140. * Save the blocks and reload with a different language.
  141. */
  142. BSUtils.changeLanguage = function() {
  143. // Store the blocks for the duration of the reload.
  144. console.log("in changeLanguage");
  145. BlocklyStorage.backupBlocks_();
  146. var newLang = encodeURIComponent($(this).data("lang"));
  147. console.log("newLang is:",newLang);
  148. // var newLang = encodeURIComponent(
  149. // languageMenu.options[languageMenu.selectedIndex].value);
  150. // console.log("newLang is:",newLang);
  151. var search = window.location.search;
  152. if (search.length <= 1) {
  153. search = '?lang=' + newLang;
  154. } else if (search.match(/[?&]lang=[^&]*/)) {
  155. search = search.replace(/([?&]lang=)[^&]*/, '$1' + newLang);
  156. } else {
  157. search = search.replace(/\?/, '?lang=' + newLang + '&');
  158. }
  159. window.location = window.location.protocol + '//' +
  160. window.location.host + window.location.pathname + search;
  161. };
  162. /**
  163. * Is the dialog currently onscreen?
  164. * @private
  165. */
  166. BSUtils.isDialogVisible_ = false;
  167. /**
  168. * A closing dialog should animate towards this element.
  169. * @type Element
  170. * @private
  171. */
  172. BSUtils.dialogOrigin_ = null;
  173. /**
  174. * A function to call when a dialog closes.
  175. * @type Function
  176. * @private
  177. */
  178. BSUtils.dialogDispose_ = null;
  179. /**
  180. * Show the dialog pop-up.
  181. * @param {!Element} content DOM element to display in the dialog.
  182. * @param {Element} origin Animate the dialog opening/closing from/to this
  183. * DOM element. If null, don't show any animations for opening or closing.
  184. * @param {boolean} animate Animate the dialog opening (if origin not null).
  185. * @param {boolean} modal If true, grey out background and prevent interaction.
  186. * @param {!Object} style A dictionary of style rules for the dialog.
  187. * @param {Function} disposeFunc An optional function to call when the dialog
  188. * closes. Normally used for unhooking events.
  189. */
  190. BSUtils.showDialog = function(content, origin, animate, modal, style,
  191. disposeFunc) {
  192. if (BSUtils.isDialogVisible_) {
  193. BSUtils.hideDialog(false);
  194. }
  195. BSUtils.isDialogVisible_ = true;
  196. BSUtils.dialogOrigin_ = origin;
  197. BSUtils.dialogDispose_ = disposeFunc;
  198. var dialog = document.getElementById('dialog');
  199. var shadow = document.getElementById('dialogShadow');
  200. var border = document.getElementById('dialogBorder');
  201. // Copy all the specified styles to the dialog.
  202. for (var name in style) {
  203. dialog.style[name] = style[name];
  204. }
  205. if (modal) {
  206. shadow.style.visibility = 'visible';
  207. shadow.style.opacity = 0.3;
  208. var header = document.createElement('div');
  209. header.id = 'dialogHeader';
  210. dialog.appendChild(header);
  211. BSUtils.dialogMouseDownWrapper_ =
  212. Blockly.bindEvent_(header, 'mousedown', null,
  213. BSUtils.dialogMouseDown_);
  214. }
  215. dialog.appendChild(content);
  216. content.className = content.className.replace('dialogHiddenContent', '');
  217. function endResult() {
  218. // Check that the dialog wasn't closed during opening.
  219. if (BSUtils.isDialogVisible_) {
  220. dialog.style.visibility = 'visible';
  221. dialog.style.zIndex = 1;
  222. border.style.visibility = 'hidden';
  223. }
  224. }
  225. if (animate && origin) {
  226. BSUtils.matchBorder_(origin, false, 0.2);
  227. BSUtils.matchBorder_(dialog, true, 0.8);
  228. // In 175ms show the dialog and hide the animated border.
  229. window.setTimeout(endResult, 175);
  230. } else {
  231. // No animation. Just set the final state.
  232. endResult();
  233. }
  234. };
  235. /**
  236. * Horizontal start coordinate of dialog drag.
  237. */
  238. BSUtils.dialogStartX_ = 0;
  239. /**
  240. * Vertical start coordinate of dialog drag.
  241. */
  242. BSUtils.dialogStartY_ = 0;
  243. /**
  244. * Handle start of drag of dialog.
  245. * @param {!Event} e Mouse down event.
  246. * @private
  247. */
  248. BSUtils.dialogMouseDown_ = function(e) {
  249. BSUtils.dialogUnbindDragEvents_();
  250. if (Blockly.isRightButton(e)) {
  251. // Right-click.
  252. return;
  253. }
  254. // Left click (or middle click).
  255. // Record the starting offset between the current location and the mouse.
  256. var dialog = document.getElementById('dialog');
  257. BSUtils.dialogStartX_ = dialog.offsetLeft - e.clientX;
  258. BSUtils.dialogStartY_ = dialog.offsetTop - e.clientY;
  259. BSUtils.dialogMouseUpWrapper_ = Blockly.bindEvent_(document,
  260. 'mouseup', null, BSUtils.dialogUnbindDragEvents_);
  261. BSUtils.dialogMouseMoveWrapper_ = Blockly.bindEvent_(document,
  262. 'mousemove', null, BSUtils.dialogMouseMove_);
  263. // This event has been handled. No need to bubble up to the document.
  264. e.stopPropagation();
  265. };
  266. /**
  267. * Drag the dialog to follow the mouse.
  268. * @param {!Event} e Mouse move event.
  269. * @private
  270. */
  271. BSUtils.dialogMouseMove_ = function(e) {
  272. var dialog = document.getElementById('dialog');
  273. var dialogLeft = BSUtils.dialogStartX_ + e.clientX;
  274. var dialogTop = BSUtils.dialogStartY_ + e.clientY;
  275. dialogTop = Math.max(dialogTop, 0);
  276. dialogTop = Math.min(dialogTop, window.innerHeight - dialog.offsetHeight);
  277. dialogLeft = Math.max(dialogLeft, 0);
  278. dialogLeft = Math.min(dialogLeft, window.innerWidth - dialog.offsetWidth);
  279. dialog.style.left = dialogLeft + 'px';
  280. dialog.style.top = dialogTop + 'px';
  281. };
  282. /**
  283. * Stop binding to the global mouseup and mousemove events.
  284. * @private
  285. */
  286. BSUtils.dialogUnbindDragEvents_ = function() {
  287. if (BSUtils.dialogMouseUpWrapper_) {
  288. Blockly.unbindEvent_(BSUtils.dialogMouseUpWrapper_);
  289. BSUtils.dialogMouseUpWrapper_ = null;
  290. }
  291. if (BSUtils.dialogMouseMoveWrapper_) {
  292. Blockly.unbindEvent_(BSUtils.dialogMouseMoveWrapper_);
  293. BSUtils.dialogMouseMoveWrapper_ = null;
  294. }
  295. };
  296. /**
  297. * Hide the dialog pop-up.
  298. * @param {boolean} opt_animate Animate the dialog closing. Defaults to true.
  299. * Requires that origin was not null when dialog was opened.
  300. */
  301. BSUtils.hideDialog = function(opt_animate) {
  302. if (!BSUtils.isDialogVisible_) {
  303. return;
  304. }
  305. BSUtils.dialogUnbindDragEvents_();
  306. if (BSUtils.dialogMouseDownWrapper_) {
  307. Blockly.unbindEvent_(BSUtils.dialogMouseDownWrapper_);
  308. BSUtils.dialogMouseDownWrapper_ = null;
  309. }
  310. BSUtils.isDialogVisible_ = false;
  311. BSUtils.dialogDispose_ && BSUtils.dialogDispose_();
  312. BSUtils.dialogDispose_ = null;
  313. var origin = (opt_animate === false) ? null : BSUtils.dialogOrigin_;
  314. var dialog = document.getElementById('dialog');
  315. var shadow = document.getElementById('dialogShadow');
  316. var border = document.getElementById('dialogBorder');
  317. shadow.style.opacity = 0;
  318. function endResult() {
  319. shadow.style.visibility = 'hidden';
  320. border.style.visibility = 'hidden';
  321. }
  322. if (origin) {
  323. BSUtils.matchBorder_(dialog, false, 0.8);
  324. BSUtils.matchBorder_(origin, true, 0.2);
  325. // In 175ms hide both the shadow and the animated border.
  326. window.setTimeout(endResult, 175);
  327. } else {
  328. // No animation. Just set the final state.
  329. endResult();
  330. }
  331. dialog.style.visibility = 'hidden';
  332. dialog.style.zIndex = -1;
  333. var header = document.getElementById('dialogHeader');
  334. if (header) {
  335. header.parentNode.removeChild(header);
  336. }
  337. while (dialog.firstChild) {
  338. var content = dialog.firstChild;
  339. content.className += ' dialogHiddenContent';
  340. document.body.appendChild(content);
  341. }
  342. };
  343. /**
  344. * Match the animated border to the a element's size and location.
  345. * @param {!Element} element Element to match.
  346. * @param {boolean} animate Animate to the new location.
  347. * @param {number} opacity Opacity of border.
  348. * @private
  349. */
  350. BSUtils.matchBorder_ = function(element, animate, opacity) {
  351. if (!element) {
  352. return;
  353. }
  354. var border = document.getElementById('dialogBorder');
  355. var bBox = BSUtils.getBBox_(element);
  356. function change() {
  357. border.style.width = bBox.width + 'px';
  358. border.style.height = bBox.height + 'px';
  359. border.style.left = bBox.x + 'px';
  360. border.style.top = bBox.y + 'px';
  361. border.style.opacity = opacity;
  362. }
  363. if (animate) {
  364. border.className = 'dialogAnimate';
  365. window.setTimeout(change, 1);
  366. } else {
  367. border.className = '';
  368. change();
  369. }
  370. border.style.visibility = 'visible';
  371. };
  372. /**
  373. * Compute the absolute coordinates and dimensions of an HTML or SVG element.
  374. * @param {!Element} element Element to match.
  375. * @return {!Object} Contains height, width, x, and y properties.
  376. * @private
  377. */
  378. BSUtils.getBBox_ = function(element) {
  379. var height = element.offsetHeight;
  380. var width = element.offsetWidth;
  381. var x = 0;
  382. var y = 0;
  383. do {
  384. x += element.offsetLeft;
  385. y += element.offsetTop;
  386. element = element.offsetParent;
  387. } while (element);
  388. return {
  389. height: height,
  390. width: width,
  391. x: x,
  392. y: y
  393. };
  394. };
  395. /**
  396. * Display a storage-related modal dialog.
  397. * @param {string} message Text to alert.
  398. */
  399. BSUtils.storageAlert = function(message) {
  400. var container = document.getElementById('containerStorage');
  401. container.textContent = '';
  402. var lines = message.split('\n');
  403. for (var i = 0; i < lines.length; i++) {
  404. var p = document.createElement('p');
  405. p.appendChild(document.createTextNode(lines[i]));
  406. container.appendChild(p);
  407. }
  408. var content = document.getElementById('dialogStorage');
  409. var origin = document.getElementById('linkButton');
  410. var style = {
  411. width: '50%',
  412. left: '25%',
  413. top: '5em'
  414. };
  415. BSUtils.showDialog(content, origin, true, true, style,
  416. BSUtils.stopDialogKeyDown());
  417. BSUtils.startDialogKeyDown();
  418. };
  419. /**
  420. * If the user preses enter, escape, or space, hide the dialog.
  421. * @param {!Event} e Keyboard event.
  422. * @private
  423. */
  424. BSUtils.dialogKeyDown_ = function(e) {
  425. if (BSUtils.isDialogVisible_) {
  426. if (e.keyCode == 13 ||
  427. e.keyCode == 27 ||
  428. e.keyCode == 32) {
  429. BSUtils.hideDialog(true);
  430. e.stopPropagation();
  431. e.preventDefault();
  432. }
  433. }
  434. };
  435. /**
  436. * Start listening for BSUtils.dialogKeyDown_.
  437. */
  438. BSUtils.startDialogKeyDown = function() {
  439. document.body.addEventListener('keydown',
  440. BSUtils.dialogKeyDown_, true);
  441. };
  442. /**
  443. * Stop listening for BSUtils.dialogKeyDown_.
  444. */
  445. BSUtils.stopDialogKeyDown = function() {
  446. document.body.removeEventListener('keydown',
  447. BSUtils.dialogKeyDown_, true);
  448. };
  449. /**
  450. * Gets the message with the given key from the document.
  451. * @param {string} key The key of the document element.
  452. * @return {string} The textContent of the specified element,
  453. * or an error message if the element was not found.
  454. */
  455. BSUtils.getMsg = function(key) {
  456. var msg = BSUtils.getMsgOrNull(key);
  457. return msg === null ? '[Unknown message: ' + key + ']' : msg;
  458. };
  459. /**
  460. * Gets the message with the given key from the document.
  461. * @param {string} key The key of the document element.
  462. * @return {string} The textContent of the specified element,
  463. * or null if the element was not found.
  464. */
  465. BSUtils.getMsgOrNull = function(key) {
  466. var element = document.getElementById(key);
  467. if (element) {
  468. var text = element.textContent;
  469. // Convert newline sequences.
  470. text = text.replace(/\\n/g, '\n');
  471. return text;
  472. } else {
  473. return null;
  474. }
  475. };
  476. /**
  477. * On touch enabled browsers, add touch-friendly variants of event handlers
  478. * for elements such as buttons whose event handlers are specified in the
  479. * markup. For example, ontouchend is treated as equivalent to onclick.
  480. */
  481. BSUtils.addTouchEvents = function() {
  482. // Do nothing if the browser doesn't support touch.
  483. if (!('ontouchstart' in document.documentElement)) {
  484. return;
  485. }
  486. // Treat ontouchend as equivalent to onclick for buttons.
  487. var buttons = document.getElementsByTagName('button');
  488. for (var i = 0, button; button = buttons[i]; i++) {
  489. if (!button.ontouchend) {
  490. button.ontouchend = button.onclick;
  491. }
  492. }
  493. };
  494. // Add events for touch devices when the window is done loading.
  495. window.addEventListener('load', BSUtils.addTouchEvents, false);
  496. /**
  497. * Bind a function to a button's click event.
  498. * On touch enabled browsers, ontouchend is treated as equivalent to onclick.
  499. * @param {string} id ID of button element.
  500. * @param {!Function} func Event handler to bind.
  501. */
  502. BSUtils.bindClick = function(el, func) {
  503. if (typeof el == 'string') {
  504. el = document.getElementById(el);
  505. }
  506. el.addEventListener('click', func, true);
  507. el.addEventListener('touchend', func, true);
  508. };