inject.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. /**
  2. * @license
  3. * Visual Blocks Editor
  4. *
  5. * Copyright 2011 Google Inc.
  6. * https://developers.google.com/blockly/
  7. *
  8. * Licensed under the Apache License, Version 2.0 (the "License");
  9. * you may not use this file except in compliance with the License.
  10. * You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing, software
  15. * distributed under the License is distributed on an "AS IS" BASIS,
  16. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. * See the License for the specific language governing permissions and
  18. * limitations under the License.
  19. */
  20. /**
  21. * @fileoverview Functions for injecting Blockly into a web page.
  22. * @author fraser@google.com (Neil Fraser)
  23. */
  24. 'use strict';
  25. goog.provide('Blockly.inject');
  26. goog.require('Blockly.Css');
  27. goog.require('Blockly.Options');
  28. goog.require('Blockly.WorkspaceSvg');
  29. goog.require('goog.dom');
  30. goog.require('goog.ui.Component');
  31. goog.require('goog.userAgent');
  32. /**
  33. * Inject a Blockly editor into the specified container element (usually a div).
  34. * @param {!Element|string} container Containing element, or its ID,
  35. * or a CSS selector.
  36. * @param {Object=} opt_options Optional dictionary of options.
  37. * @return {!Blockly.Workspace} Newly created main workspace.
  38. */
  39. Blockly.inject = function(container, opt_options) {
  40. if (goog.isString(container)) {
  41. container = document.getElementById(container) ||
  42. document.querySelector(container);
  43. }
  44. // Verify that the container is in document.
  45. if (!goog.dom.contains(document, container)) {
  46. throw 'Error: container is not in current document.';
  47. }
  48. var options = new Blockly.Options(opt_options || {});
  49. var svg = Blockly.createDom_(container, options);
  50. var workspace = Blockly.createMainWorkspace_(svg, options);
  51. Blockly.init_(workspace);
  52. workspace.markFocused();
  53. Blockly.bindEvent_(svg, 'focus', workspace, workspace.markFocused);
  54. Blockly.svgResize(workspace);
  55. return workspace;
  56. };
  57. /**
  58. * Create the SVG image.
  59. * @param {!Element} container Containing element.
  60. * @param {!Blockly.Options} options Dictionary of options.
  61. * @return {!Element} Newly created SVG image.
  62. * @private
  63. */
  64. Blockly.createDom_ = function(container, options) {
  65. // Sadly browsers (Chrome vs Firefox) are currently inconsistent in laying
  66. // out content in RTL mode. Therefore Blockly forces the use of LTR,
  67. // then manually positions content in RTL as needed.
  68. container.setAttribute('dir', 'LTR');
  69. // Closure can be trusted to create HTML widgets with the proper direction.
  70. goog.ui.Component.setDefaultRightToLeft(options.RTL);
  71. // Load CSS.
  72. Blockly.Css.inject(options.hasCss, options.pathToMedia);
  73. // Build the SVG DOM.
  74. /*
  75. <svg
  76. xmlns="http://www.w3.org/2000/svg"
  77. xmlns:html="http://www.w3.org/1999/xhtml"
  78. xmlns:xlink="http://www.w3.org/1999/xlink"
  79. version="1.1"
  80. class="blocklySvg">
  81. ...
  82. </svg>
  83. */
  84. var svg = Blockly.createSvgElement('svg', {
  85. 'xmlns': 'http://www.w3.org/2000/svg',
  86. 'xmlns:html': 'http://www.w3.org/1999/xhtml',
  87. 'xmlns:xlink': 'http://www.w3.org/1999/xlink',
  88. 'version': '1.1',
  89. 'class': 'blocklySvg'
  90. }, container);
  91. /*
  92. <defs>
  93. ... filters go here ...
  94. </defs>
  95. */
  96. var defs = Blockly.createSvgElement('defs', {}, svg);
  97. var rnd = String(Math.random()).substring(2);
  98. /*
  99. <filter id="blocklyEmbossFilter837493">
  100. <feGaussianBlur in="SourceAlpha" stdDeviation="1" result="blur"/>
  101. <feSpecularLighting in="blur" surfaceScale="1" specularConstant="0.5"
  102. specularExponent="10" lighting-color="white"
  103. result="specOut">
  104. <fePointLight x="-5000" y="-10000" z="20000"/>
  105. </feSpecularLighting>
  106. <feComposite in="specOut" in2="SourceAlpha" operator="in"
  107. result="specOut"/>
  108. <feComposite in="SourceGraphic" in2="specOut" operator="arithmetic"
  109. k1="0" k2="1" k3="1" k4="0"/>
  110. </filter>
  111. */
  112. var embossFilter = Blockly.createSvgElement('filter',
  113. {'id': 'blocklyEmbossFilter' + rnd}, defs);
  114. Blockly.createSvgElement('feGaussianBlur',
  115. {'in': 'SourceAlpha', 'stdDeviation': 1, 'result': 'blur'}, embossFilter);
  116. var feSpecularLighting = Blockly.createSvgElement('feSpecularLighting',
  117. {'in': 'blur', 'surfaceScale': 1, 'specularConstant': 0.5,
  118. 'specularExponent': 10, 'lighting-color': 'white', 'result': 'specOut'},
  119. embossFilter);
  120. Blockly.createSvgElement('fePointLight',
  121. {'x': -5000, 'y': -10000, 'z': 20000}, feSpecularLighting);
  122. Blockly.createSvgElement('feComposite',
  123. {'in': 'specOut', 'in2': 'SourceAlpha', 'operator': 'in',
  124. 'result': 'specOut'}, embossFilter);
  125. Blockly.createSvgElement('feComposite',
  126. {'in': 'SourceGraphic', 'in2': 'specOut', 'operator': 'arithmetic',
  127. 'k1': 0, 'k2': 1, 'k3': 1, 'k4': 0}, embossFilter);
  128. options.embossFilterId = embossFilter.id;
  129. /*
  130. <pattern id="blocklyDisabledPattern837493" patternUnits="userSpaceOnUse"
  131. width="10" height="10">
  132. <rect width="10" height="10" fill="#aaa" />
  133. <path d="M 0 0 L 10 10 M 10 0 L 0 10" stroke="#cc0" />
  134. </pattern>
  135. */
  136. var disabledPattern = Blockly.createSvgElement('pattern',
  137. {'id': 'blocklyDisabledPattern' + rnd,
  138. 'patternUnits': 'userSpaceOnUse',
  139. 'width': 10, 'height': 10}, defs);
  140. Blockly.createSvgElement('rect',
  141. {'width': 10, 'height': 10, 'fill': '#aaa'}, disabledPattern);
  142. Blockly.createSvgElement('path',
  143. {'d': 'M 0 0 L 10 10 M 10 0 L 0 10', 'stroke': '#cc0'}, disabledPattern);
  144. options.disabledPatternId = disabledPattern.id;
  145. /*
  146. <pattern id="blocklyGridPattern837493" patternUnits="userSpaceOnUse">
  147. <rect stroke="#888" />
  148. <rect stroke="#888" />
  149. </pattern>
  150. */
  151. var gridPattern = Blockly.createSvgElement('pattern',
  152. {'id': 'blocklyGridPattern' + rnd,
  153. 'patternUnits': 'userSpaceOnUse'}, defs);
  154. if (options.gridOptions['length'] > 0 && options.gridOptions['spacing'] > 0) {
  155. Blockly.createSvgElement('line',
  156. {'stroke': options.gridOptions['colour']},
  157. gridPattern);
  158. if (options.gridOptions['length'] > 1) {
  159. Blockly.createSvgElement('line',
  160. {'stroke': options.gridOptions['colour']},
  161. gridPattern);
  162. }
  163. // x1, y1, x1, x2 properties will be set later in updateGridPattern_.
  164. }
  165. options.gridPattern = gridPattern;
  166. return svg;
  167. };
  168. /**
  169. * Create a main workspace and add it to the SVG.
  170. * @param {!Element} svg SVG element with pattern defined.
  171. * @param {!Blockly.Options} options Dictionary of options.
  172. * @return {!Blockly.Workspace} Newly created main workspace.
  173. * @private
  174. */
  175. Blockly.createMainWorkspace_ = function(svg, options) {
  176. options.parentWorkspace = null;
  177. options.getMetrics = Blockly.getMainWorkspaceMetrics_;
  178. options.setMetrics = Blockly.setMainWorkspaceMetrics_;
  179. var mainWorkspace = new Blockly.WorkspaceSvg(options);
  180. mainWorkspace.scale = options.zoomOptions.startScale;
  181. svg.appendChild(mainWorkspace.createDom('blocklyMainBackground'));
  182. // A null translation will also apply the correct initial scale.
  183. mainWorkspace.translate(0, 0);
  184. mainWorkspace.markFocused();
  185. if (!options.readOnly && !options.hasScrollbars) {
  186. var workspaceChanged = function() {
  187. if (Blockly.dragMode_ == Blockly.DRAG_NONE) {
  188. var metrics = mainWorkspace.getMetrics();
  189. var edgeLeft = metrics.viewLeft + metrics.absoluteLeft;
  190. var edgeTop = metrics.viewTop + metrics.absoluteTop;
  191. if (metrics.contentTop < edgeTop ||
  192. metrics.contentTop + metrics.contentHeight >
  193. metrics.viewHeight + edgeTop ||
  194. metrics.contentLeft <
  195. (options.RTL ? metrics.viewLeft : edgeLeft) ||
  196. metrics.contentLeft + metrics.contentWidth > (options.RTL ?
  197. metrics.viewWidth : metrics.viewWidth + edgeLeft)) {
  198. // One or more blocks may be out of bounds. Bump them back in.
  199. var MARGIN = 25;
  200. var blocks = mainWorkspace.getTopBlocks(false);
  201. for (var b = 0, block; block = blocks[b]; b++) {
  202. var blockXY = block.getRelativeToSurfaceXY();
  203. var blockHW = block.getHeightWidth();
  204. // Bump any block that's above the top back inside.
  205. var overflowTop = edgeTop + MARGIN - blockHW.height - blockXY.y;
  206. if (overflowTop > 0) {
  207. block.moveBy(0, overflowTop);
  208. }
  209. // Bump any block that's below the bottom back inside.
  210. var overflowBottom =
  211. edgeTop + metrics.viewHeight - MARGIN - blockXY.y;
  212. if (overflowBottom < 0) {
  213. block.moveBy(0, overflowBottom);
  214. }
  215. // Bump any block that's off the left back inside.
  216. var overflowLeft = MARGIN + edgeLeft -
  217. blockXY.x - (options.RTL ? 0 : blockHW.width);
  218. if (overflowLeft > 0) {
  219. block.moveBy(overflowLeft, 0);
  220. }
  221. // Bump any block that's off the right back inside.
  222. var overflowRight = edgeLeft + metrics.viewWidth - MARGIN -
  223. blockXY.x + (options.RTL ? blockHW.width : 0);
  224. if (overflowRight < 0) {
  225. block.moveBy(overflowRight, 0);
  226. }
  227. }
  228. }
  229. }
  230. };
  231. mainWorkspace.addChangeListener(workspaceChanged);
  232. }
  233. // The SVG is now fully assembled.
  234. Blockly.svgResize(mainWorkspace);
  235. Blockly.WidgetDiv.createDom();
  236. Blockly.Tooltip.createDom();
  237. return mainWorkspace;
  238. };
  239. /**
  240. * Initialize Blockly with various handlers.
  241. * @param {!Blockly.Workspace} mainWorkspace Newly created main workspace.
  242. * @private
  243. */
  244. Blockly.init_ = function(mainWorkspace) {
  245. var options = mainWorkspace.options;
  246. var svg = mainWorkspace.getParentSvg();
  247. // Supress the browser's context menu.
  248. // Note: for blockscad, we have to suppress context menus on the whole document
  249. // not just the svg for some reason.??
  250. Blockly.bindEvent_(document, 'contextmenu', null,
  251. function(e) {
  252. if (!Blockly.isTargetInput_(e)) {
  253. e.preventDefault();
  254. }
  255. });
  256. var workspaceResizeHandler = Blockly.bindEvent_(window, 'resize', null,
  257. function() {
  258. Blockly.hideChaff(true);
  259. Blockly.svgResize(mainWorkspace);
  260. });
  261. mainWorkspace.setResizeHandlerWrapper(workspaceResizeHandler);
  262. Blockly.inject.bindDocumentEvents_();
  263. if (options.languageTree) {
  264. if (mainWorkspace.toolbox_) {
  265. mainWorkspace.toolbox_.init(mainWorkspace);
  266. } else if (mainWorkspace.flyout_) {
  267. // Build a fixed flyout with the root blocks.
  268. mainWorkspace.flyout_.init(mainWorkspace);
  269. mainWorkspace.flyout_.show(options.languageTree.childNodes);
  270. mainWorkspace.flyout_.scrollToStart();
  271. // Translate the workspace sideways to avoid the fixed flyout.
  272. mainWorkspace.scrollX = mainWorkspace.flyout_.width_;
  273. if (options.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
  274. mainWorkspace.scrollX *= -1;
  275. }
  276. mainWorkspace.translate(mainWorkspace.scrollX, 0);
  277. }
  278. }
  279. if (options.hasScrollbars) {
  280. mainWorkspace.scrollbar = new Blockly.ScrollbarPair(mainWorkspace);
  281. mainWorkspace.scrollbar.resize();
  282. }
  283. // Load the sounds.
  284. if (options.hasSounds) {
  285. Blockly.inject.loadSounds_(options.pathToMedia, mainWorkspace);
  286. }
  287. };
  288. /**
  289. * Bind document events, but only once. Destroying and reinjecting Blockly
  290. * should not bind again.
  291. * Bind events for scrolling the workspace.
  292. * Most of these events should be bound to the SVG's surface.
  293. * However, 'mouseup' has to be on the whole document so that a block dragged
  294. * out of bounds and released will know that it has been released.
  295. * Also, 'keydown' has to be on the whole document since the browser doesn't
  296. * understand a concept of focus on the SVG image.
  297. * @private
  298. */
  299. Blockly.inject.bindDocumentEvents_ = function() {
  300. if (!Blockly.documentEventsBound_) {
  301. Blockly.bindEvent_(document, 'keydown', null, Blockly.onKeyDown_);
  302. Blockly.bindEvent_(document, 'touchend', null, Blockly.longStop_);
  303. Blockly.bindEvent_(document, 'touchcancel', null, Blockly.longStop_);
  304. // Don't use bindEvent_ for document's mouseup since that would create a
  305. // corresponding touch handler that would squeltch the ability to interact
  306. // with non-Blockly elements.
  307. document.addEventListener('mouseup', Blockly.onMouseUp_, false);
  308. // Some iPad versions don't fire resize after portrait to landscape change.
  309. if (goog.userAgent.IPAD) {
  310. Blockly.bindEvent_(window, 'orientationchange', document, function() {
  311. // TODO(#397): Fix for multiple blockly workspaces.
  312. Blockly.svgResize(Blockly.getMainWorkspace());
  313. });
  314. }
  315. }
  316. Blockly.documentEventsBound_ = true;
  317. };
  318. /**
  319. * Load sounds for the given workspace.
  320. * @param {string} pathToMedia The path to the media directory.
  321. * @param {!Blockly.Workspace} workspace The workspace to load sounds for.
  322. * @private
  323. */
  324. Blockly.inject.loadSounds_ = function(pathToMedia, workspace) {
  325. workspace.loadAudio_(
  326. [pathToMedia + 'click.mp3',
  327. pathToMedia + 'click.wav',
  328. pathToMedia + 'click.ogg'], 'click');
  329. workspace.loadAudio_(
  330. [pathToMedia + 'disconnect.wav',
  331. pathToMedia + 'disconnect.mp3',
  332. pathToMedia + 'disconnect.ogg'], 'disconnect');
  333. workspace.loadAudio_(
  334. [pathToMedia + 'delete.mp3',
  335. pathToMedia + 'delete.ogg',
  336. pathToMedia + 'delete.wav'], 'delete');
  337. // Bind temporary hooks that preload the sounds.
  338. var soundBinds = [];
  339. var unbindSounds = function() {
  340. while (soundBinds.length) {
  341. Blockly.unbindEvent_(soundBinds.pop());
  342. }
  343. workspace.preloadAudio_();
  344. };
  345. // Android ignores any sound not loaded as a result of a user action.
  346. soundBinds.push(
  347. Blockly.bindEvent_(document, 'mousemove', null, unbindSounds));
  348. soundBinds.push(
  349. Blockly.bindEvent_(document, 'touchstart', null, unbindSounds));
  350. };
  351. /**
  352. * Modify the block tree on the existing toolbox.
  353. * @param {Node|string} tree DOM tree of blocks, or text representation of same.
  354. */
  355. Blockly.updateToolbox = function(tree) {
  356. console.warn('Deprecated call to Blockly.updateToolbox, ' +
  357. 'use workspace.updateToolbox instead.');
  358. Blockly.getMainWorkspace().updateToolbox(tree);
  359. };