inject.js 15 KB

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