From ef55c29d5285debdc6fa1887d5e02630153f3129 Mon Sep 17 00:00:00 2001 From: Rodrigo Queiro Date: Tue, 9 Aug 2016 15:33:45 +0200 Subject: [PATCH 01/10] Add a test for domToWorkspace This would have caught a recent [Scratch Blocks regression](https://github.com/LLK/scratch-blocks/pull/557) where domToWorkspace failed unless given a WorkspaceSvg instance. --- tests/jsunit/xml_test.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/jsunit/xml_test.js b/tests/jsunit/xml_test.js index 7487b6f3e..25e2655ea 100644 --- a/tests/jsunit/xml_test.js +++ b/tests/jsunit/xml_test.js @@ -59,6 +59,29 @@ function test_domToText() { text.replace(/\s+/g, '')); } +function test_domToWorkspace() { + Blockly.Blocks.test_block = { + init: function() { + this.jsonInit({ + message0: 'test', + }); + } + }; + + try { + var dom = Blockly.Xml.textToDom( + '' + + ' ' + + ' ' + + ''); + var workspace = new Blockly.Workspace(); + Blockly.Xml.domToWorkspace(dom, workspace); + assertEquals('Block count', 1, workspace.getAllBlocks().length); + } finally { + delete Blockly.Blocks.test_block; + } +} + function test_domToPrettyText() { var dom = Blockly.Xml.textToDom(XML_TEXT); var text = Blockly.Xml.domToPrettyText(dom); From 3998ecec3af164bca19469680c9b748611e26377 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Tue, 9 Aug 2016 16:34:59 -0700 Subject: [PATCH 02/10] Parse separators in xml in always-open flyouts --- core/flyout.js | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/core/flyout.js b/core/flyout.js index 0484b53fc..a216e60f5 100644 --- a/core/flyout.js +++ b/core/flyout.js @@ -637,18 +637,37 @@ Blockly.Flyout.prototype.show = function(xmlList) { // Create the blocks to be shown in this flyout. var blocks = []; var gaps = []; + var lastBlock = null; this.permanentlyDisabled_.length = 0; for (var i = 0, xml; xml = xmlList[i]; i++) { - if (xml.tagName && xml.tagName.toUpperCase() == 'BLOCK') { - var curBlock = Blockly.Xml.domToBlock(xml, this.workspace_); - if (curBlock.disabled) { - // Record blocks that were initially disabled. - // Do not enable these blocks as a result of capacity filtering. - this.permanentlyDisabled_.push(curBlock); + if (xml.tagName) { + if (xml.tagName.toUpperCase() == 'BLOCK') { + lastBlock = xml; + var curBlock = Blockly.Xml.domToBlock(xml, this.workspace_); + if (curBlock.disabled) { + // Record blocks that were initially disabled. + // Do not enable these blocks as a result of capacity filtering. + this.permanentlyDisabled_.push(curBlock); + } + blocks.push(curBlock); + var gap = parseInt(xml.getAttribute('gap'), 10); + gaps.push(isNaN(gap) ? this.MARGIN * 3 : gap); + } else if (xml.tagName.toUpperCase() == 'SEP') { + // Change the gap between two blocks. + // + // The default gap is 24, can be set larger or smaller. + // Note that a deprecated method is to add a gap to a block. + // + var newGap = parseInt(xml.getAttribute('gap'), 10); + // Ignore gaps before the first block. + if (!isNaN(newGap) && lastBlock) { + var oldGap = parseInt(lastBlock.getAttribute('gap')); + var gap = isNaN(oldGap) ? newGap : oldGap + newGap; + gaps[gaps.length - 1] = gap; + } else { + gaps.push(this.MARGIN * 3); + } } - blocks.push(curBlock); - var gap = parseInt(xml.getAttribute('gap'), 10); - gaps.push(isNaN(gap) ? this.MARGIN * 3 : gap); } } From 93af9c59b36176e1fe3f4f9bd9f9063671cc3e3a Mon Sep 17 00:00:00 2001 From: Sean Lip Date: Tue, 9 Aug 2016 17:29:53 -0700 Subject: [PATCH 03/10] Add functionality for playing audio files. --- accessible/README | 10 ++-- accessible/app.component.js | 3 +- accessible/audio.service.js | 57 +++++++++++++++++++++ accessible/clipboard.service.js | 6 ++- {media => accessible/media}/accessible.css | 0 accessible/media/click.mp3 | Bin 0 -> 2304 bytes accessible/media/click.ogg | Bin 0 -> 4865 bytes accessible/media/click.wav | Bin 0 -> 3782 bytes accessible/media/delete.mp3 | Bin 0 -> 3123 bytes accessible/media/delete.ogg | Bin 0 -> 5731 bytes accessible/media/delete.wav | Bin 0 -> 9164 bytes accessible/workspace-tree.component.js | 5 +- demos/accessible/index.html | 13 +++-- 13 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 accessible/audio.service.js rename {media => accessible/media}/accessible.css (100%) create mode 100644 accessible/media/click.mp3 create mode 100644 accessible/media/click.ogg create mode 100644 accessible/media/click.wav create mode 100644 accessible/media/delete.mp3 create mode 100644 accessible/media/delete.ogg create mode 100644 accessible/media/delete.wav diff --git a/accessible/README b/accessible/README index d133fa828..37ea67879 100644 --- a/accessible/README +++ b/accessible/README @@ -25,15 +25,16 @@ the main component to be loaded. This will usually be blocklyApp.AppView, but if you have another component that wraps it, use that one instead. -Customizing the Toolbar ------------------------ +Customizing the Toolbar and Audio +--------------------------------- The Accessible Blockly workspace comes with a customizable toolbar. To customize the toolbar, you will need to declare an ACCESSIBLE_GLOBALS object in the global scope that looks like this: var ACCESSIBLE_GLOBALS = { - toolbarButtonConfig: [] + toolbarButtonConfig: [], + mediaPathPrefix: null }; The value corresponding to 'toolbarButtonConfig' can be modified by adding @@ -43,6 +44,9 @@ two keys: - 'text' (the text to display on the button) - 'action' (the function that gets run when the button is clicked) +In addition, if you want audio to be played, set mediaPathPrefix to the +location of the accessible/media folder. + Limitations ----------- diff --git a/accessible/app.component.js b/accessible/app.component.js index f220af98b..063a5ea50 100644 --- a/accessible/app.component.js +++ b/accessible/app.component.js @@ -64,7 +64,8 @@ blocklyApp.AppView = ng.core // https://www.sitepoint.com/angular-2-components-providers-classes-factories-values/ providers: [ blocklyApp.ClipboardService, blocklyApp.NotificationsService, - blocklyApp.TreeService, blocklyApp.UtilsService] + blocklyApp.TreeService, blocklyApp.UtilsService, + blocklyApp.AudioService] }) .Class({ constructor: [blocklyApp.NotificationsService, function(_notificationsService) { diff --git a/accessible/audio.service.js b/accessible/audio.service.js new file mode 100644 index 000000000..c358c083b --- /dev/null +++ b/accessible/audio.service.js @@ -0,0 +1,57 @@ +/** + * AccessibleBlockly + * + * 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. + */ + +/** + * @fileoverview Angular2 Service that plays audio files. + * @author sll@google.com (Sean Lip) + */ + +blocklyApp.AudioService = ng.core + .Class({ + constructor: [function() { + // We do not play any audio unless a media path prefix is specified. + this.canPlayAudio = false; + if (ACCESSIBLE_GLOBALS.hasOwnProperty('mediaPathPrefix')) { + this.canPlayAudio = true; + var mediaPathPrefix = ACCESSIBLE_GLOBALS['mediaPathPrefix']; + this.AUDIO_PATHS_ = { + 'connect': mediaPathPrefix + 'click.mp3', + 'delete': mediaPathPrefix + 'delete.mp3' + }; + } + + // TODO(sll): Add ogg and mp3 fallbacks. + this.cachedAudioFiles_ = {}; + }], + play_: function(audioId) { + if (this.canPlayAudio) { + if (!this.cachedAudioFiles_.hasOwnProperty(audioId)) { + this.cachedAudioFiles_[audioId] = new Audio( + this.AUDIO_PATHS_[audioId]); + } + this.cachedAudioFiles_[audioId].play(); + } + }, + playConnectSound: function() { + this.play_('connect'); + }, + playDeleteSound: function() { + this.play_('delete'); + } + }); diff --git a/accessible/clipboard.service.js b/accessible/clipboard.service.js index 84b0b4f52..05b0afe63 100644 --- a/accessible/clipboard.service.js +++ b/accessible/clipboard.service.js @@ -26,7 +26,8 @@ blocklyApp.ClipboardService = ng.core .Class({ constructor: [ blocklyApp.NotificationsService, blocklyApp.UtilsService, - function(_notificationsService, _utilsService) { + blocklyApp.AudioService, + function(_notificationsService, _utilsService, _audioService) { this.clipboardBlockXml_ = null; this.clipboardBlockPreviousConnection_ = null; this.clipboardBlockNextConnection_ = null; @@ -34,6 +35,7 @@ blocklyApp.ClipboardService = ng.core this.markedConnection_ = null; this.notificationsService = _notificationsService; this.utilsService = _utilsService; + this.audioService = _audioService; }], areConnectionsCompatible_: function(blockConnection, connection) { // Check that both connections exist, that it's the right kind of @@ -130,6 +132,7 @@ blocklyApp.ClipboardService = ng.core default: connection.connect(reconstitutedBlock.outputConnection); } + this.audioService.playConnectSound(); this.notificationsService.setStatusMessage( this.utilsService.getBlockDescription(reconstitutedBlock) + ' ' + Blockly.Msg.PASTED_BLOCK_FROM_CLIPBOARD_MSG); @@ -151,6 +154,7 @@ blocklyApp.ClipboardService = ng.core if (this.areConnectionsCompatible_( this.markedConnection_, potentialConnections[i])) { this.markedConnection_.connect(potentialConnections[i]); + this.audioService.playConnectSound(); connectionSuccessful = true; break; } diff --git a/media/accessible.css b/accessible/media/accessible.css similarity index 100% rename from media/accessible.css rename to accessible/media/accessible.css diff --git a/accessible/media/click.mp3 b/accessible/media/click.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..4534b0ddca7424a441a5992f78d7defb7ea01834 GIT binary patch literal 2304 zcmeZtF=l1}0!F_O&k!RZLl%ew@(T(w^U@W3GE)@t(|}Y#QesZ7LU2iDa&}0hYY5S_413-^&w3C0*Djhb23xn^V5J7*a`8)ASWpJ=BH$)Wu~SmB<7_k z6s6{7Rsh*al?v|p`RO^S3Z8k%djEe z|8W7JU6wwMzOKf4mKH^fXJM`pX;9&~;>5tv0CLC9h9lfCiIGA>B1ot=-=`$KfxBg% z!2xvxQN{%u1ZVItq(kKX|Nq&bz`W(yxy6|b2}}%!60c^lW>9qJNYT78cK7X1G`=?E8tc&Ve+j=v1Tvih0*@(UOg z)*t^hr9ta#q~@}-;m&2rD@0cDT)bT*!L`fq5ld(5;h8Pk2Y6UQCK#ON^K*&Q;bhss z9BrK3;2d=3SdT^1nUz-!lTNNk@Mmq?@6Ud%@A{J`(*oW^&e)mc81_B)Si14_drS7G zzq-2c_uuE{Khk^7tTS$rj-Ikr)v)XCym_1Z3znXKSJt+4_U@f=e}9{n-@o_q*!$1_ zEB<}`ef{>-}am~&Ni$6GlPLakVQmBho5g+#P@|YVu44#{>+Kski1s6Sk9!K zfj8soyG^C-EUk(PA#6;{Q6^WIn`c~D`^7+Hh82hGUKz=t+05gThWnaIbh@zJJ~KkT2if{=by{ zTQ@IoW{I(Pu>SSez~uDu?f?G|PglO@n5t)*8uVuWesE08RaG}_le~O;$&!upuPt3# z8=v`Re&nesyE{%Tj?H`UbN#;vFNOjJhOlWaokA)e6P3DTmN+f&5Mr6SvEXOSJm;zB zK{3S7d@XXazRmOo4h9AjU^KF=ZEbV+q!+WU9HsZvvK=+^CR?=u0PulcRlXv+U&QSpI=?S zCH&&rkF9I7s}8nGoo8jOQ&`u;r3EVh^^8^xAfUf1KrvLqe1L&*2{3~xKx1eCLtxZu H^bi05jng?> literal 0 HcmV?d00001 diff --git a/accessible/media/click.ogg b/accessible/media/click.ogg new file mode 100644 index 0000000000000000000000000000000000000000..e8ae42a6106dadbb6861981a696b7f8b8f64c9ae GIT binary patch literal 4865 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 literal 0 HcmV?d00001 diff --git a/accessible/media/delete.mp3 b/accessible/media/delete.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..442bd9c1f4c9cee39ca9cc776c7cd594c90f2795 GIT binary patch literal 3123 zcmeHJXH*m07M>&!dX=iEktT2fsh48B1PId6#Ly%{0to>Gg7l^YAtC|_JX+`=MMRMz z%|=4KfCcqIM5>~Qpi&e;ROW`Ku5}mx?vMBDy>G33W`Ad9&EEU$Z_XTRv@Q(b0CEUt zjnm;cq8u|14hsnK_YI^Vf`~!nV4SN3mNU!EF&!;!Z5$eF{&#xRLYqT*Id&|R9OOd` zG(Tc*&tbkC+rlwwSg;q_7~vBd8f1LnKtx1@W)LZa8WukCkYH%o-gs`WQe7$`+4l$5~2qp*kMsj@5D1;@IO7SNntOLC?!LRAvKcf9@ zv<~#9a;ON$-~b@t52$T}<95)t;p%n_ZwI&yoLEM7ruG)Pnns4fkO#lAk)SE^SeXF; zZ7UCz7B9tLH~v?yKN=(v=Ao(A3&8&P?V5oB77DOCAk1_{K7L_V>G{%k=1@Xx3l8dO zEQ;Xh5RJBl0zvil(`Yg2v9WKO`FktsM0UE{FSY4QEWT_XQxf{Lu#@iE>PKaz6z^1S#Kk6E| zG-kc6w!8meF>Y{bCCjCuXMue~ilz2C!X4IBS^mk%roZYV3EJ%*WEKJ4CXdY$UQ4Z)#EROyxblolH`S9mE z2QJKK*G*4-eWTyn(ffXymH0Jo*N+cx#lj%oWXLh=832^!1zpK5Gt?{Zu6~>g=Qj~g z;kv01&+284q4hmg6M7n=LU-_69`#Y6$IHWWue=~!%*zSP3s;u%z(unHZ6oA6>5wpA z0tUusc++ve)J0)^ot6*o<=*A4(6PhKg-GAljOBMKHQ4gC4?a(i7x@ucDN0>VX$46b zs_@PuxW{RY&!YHs-HH!6&(>VsY>jx?(Z1VQ#7!ci>cbIby!@Yaoay>{p&^lq}H#A~FK*Wpum_g(k9vo6T>j89*1Bax3k zbT?{hF?OPO2hY7UaB)<*`XH)mlCdl$c?$Zt#HhLHK)t*(Z)~hqV@I<0$hhCi6GisK zQqs=-E2aO0K*XV4AY z!)OJaCpY7M?urlK8Q0=@t2Y=qzcT^2a?j1DdZgooQGn>Jm3&*B3!-+(yl*41&FJb8 z%B8%lrQDQp^lfz);i~u6+0hrQ!-U4_E0AzUwT{P@C(xp^>g7$30wuI5Qm zHXns9?$9qO{c4Bl;se?U9=>zj5DZg2HdRcIS8=3SR_2nB?){D;FR_fg79G`}9cu4GN#xS_ z*Spjxg7uKy;zd=iJ1eai0tw$HESr@aaK;1VibBOdNH)79IuxFx*KgTjnc=sowdyWO zdL--2d_Pskq{g}N^Uu2^dq0TCWobt zdDo-+Bj7i)?x%kFm6xe+bkX}ZV63vAeH)8yy)9Sn`RKCQbD{H7^;pMaDv&aNv z+Z91w$7vqa(F7jXR5tS9Y%^%s3Iy|29127mmMeD7muocCUcW=X!}xolGP$F6HYPB4VL7_aI(MCd}qs6aUj^1?Gh@W3yLA@0ZwTW z)*=GR(#7yAd~pe<;oqly+u}=$xo>X5oPm`PV=M0;7|ng5ziXP)deG<5Gq{P zV8SlyRFD}Ba3lEn<>S3fCj@-K^~4@gX;D=``xW47B?H~p0+@o}Hz+)QUzwNroMxnk z)lGVO8nn%?1O%Cir7lCA5P6!OmOHWbu%2aEd}63QJr{mofy&9TTS;2wVmyFx{B`Eh m)Y*Qf`iFe0Ly8GZ08$|b00;okqjC1dfB267KmOlbfxiHkb?GSp literal 0 HcmV?d00001 diff --git a/accessible/media/delete.ogg b/accessible/media/delete.ogg new file mode 100644 index 0000000000000000000000000000000000000000..67f84ac19a05075a52de49829b3a2bc17af9e29c GIT binary patch literal 5731 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 literal 0 HcmV?d00001 diff --git a/accessible/media/delete.wav b/accessible/media/delete.wav new file mode 100644 index 0000000000000000000000000000000000000000..18debcf96d6f76e36f295feb6de2f09dffaec1f5 GIT binary patch literal 9164 zcmXAu2Yip$_s7qDzDa}-Dr%Jc>{XkXr6@AY+M`sB5G#loTWQP)MXjn$P^v{yVr#9^ z+N<`agpf$`eeU_c_5bRtvcAuK?mg#oKA+Dyw@pN7=+WXL9UFFN(r3`{BDqAw@L%H} zMW%N$;z%xO9?`a4BEOGl8QLnMWy2QlKhr2?#GoMq`$hLHGNi|l$QX%e*ruI7zi&G{ za!6EX zIe74Z?*FaCzkMTzb?-53*x;Dqk-dww7~H#GpMHGPBf58yn8-o>M)Q5o*dk$r2lpKq zStKI5S4DQ#s7+%(-d448l`0~=dkpW9Tuh{>Jk==s!uFI);v+-#qW&OvYgSyeKvfpSiNzigSQ0M7e{Yr*QV;L-YOb2tp95aXHv^Lgknr@%!TS*lw z*QBk~kf}0K{wJSGQPbP`)A4ckn_f;a&l}G`pPQZ!JTuJ;ov7BXaED|jW{mcJ@~P**n7iD4&>l^4i4v4E2rl+2d*CRFKE&wH@8Z-binV_pG<4+t8NL4jQ9DQpp54 zo6Iz+Wwtw)ozNTz2U%T3Qn1dkHtS zq?s*YE9*^ZWq#H{BdMr~uGkYc!8WuN?R`5{HW`o8*IDH>acYo@w7d(Tdn7ADmkH_OaX zGt%sl-V$W0nY8Yr9bfWARQ!Qq@nrFERaK5T4(96GTd}AyCj+%?6kk= z6Md$;^m83AwVjd1N2}Q{^tfD?cbceob*}hJG3}>6Ykx8)a+UXfVID}PmN2!PeUgnc z6_9Z02)=JvuidGiXq5Jpn>tvtkc}{1poOKB+yEJQrJMP{SeU0Uft3kR|m-$TU z6T_ieU2;oR>7?E4HG4pxNupfUe7x(n{I2nuF1<`ka~7QKkqIC#FFPA;3YqJ=&34jo zlW0z`pCOt_L`txuS$f8PrDvo#NZux;rMW)U?J`cfa;ioWDq%882ieV3X*E+@M91)s zmHM{~6xYV+J&88|>O?zC|BwZ;fxo?w2y;YcOI~RpIb}2TAEG<;kk*zeri}cif9ra( z^&7FQDAOf7Yd*J++$y$<+$CdKar(`=G0WazepI47Mz&jO#e zeg)pSPs1}x_(f>e)C(yGeRg~h?OQqRSw^(~S6N%AefLS`K2I;tK0V|4!E8?}oH|pp zd1~98Qp0zK^FQL6!&6u;xy^K-xo>8g+S=AT-u+E-m<;>MK9GabTKC!#`i{6pNqf`P z6ftq2X_F>M4>Q%glBJph45hdS>?~cZr|cR%4wto&6=tDnAiH(Bd?!_;EN7es&&`#7 zri$4BgKX05ve=Y0t#q;twta1V-jH2x>r3;k$8l!rY5OaYnr|cB!aB}uGds1s_F

  • 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 R6c)kuZMgU8%=~+f{2#xTwtoNs literal 0 HcmV?d00001 diff --git a/accessible/workspace-tree.component.js b/accessible/workspace-tree.component.js index 8d7d02e7c..39e33b048 100644 --- a/accessible/workspace-tree.component.js +++ b/accessible/workspace-tree.component.js @@ -102,13 +102,15 @@ blocklyApp.WorkspaceTreeComponent = ng.core constructor: [ blocklyApp.ClipboardService, blocklyApp.NotificationsService, blocklyApp.TreeService, blocklyApp.UtilsService, + blocklyApp.AudioService, function( _clipboardService, _notificationsService, _treeService, - _utilsService) { + _utilsService, _audioService) { this.clipboardService = _clipboardService; this.notificationsService = _notificationsService; this.treeService = _treeService; this.utilsService = _utilsService; + this.audioService = _audioService; }], getBlockDescription: function() { return this.utilsService.getBlockDescription(this.block); @@ -172,6 +174,7 @@ blocklyApp.WorkspaceTreeComponent = ng.core var that = this; this.removeBlockAndSetFocus_(this.block, function() { that.block.dispose(true); + that.audioService.playDeleteSound(); }); setTimeout(function() { diff --git a/demos/accessible/index.html b/demos/accessible/index.html index 14bc2c952..af424764d 100644 --- a/demos/accessible/index.html +++ b/demos/accessible/index.html @@ -19,6 +19,7 @@ + @@ -31,7 +32,7 @@ - + -

    Blockly > - Demos > Accessible Blockly

    +

    + Blockly > + Demos > Accessible Blockly +

    This is a simple demo of a version of Blockly designed for screen readers.

    @@ -70,7 +73,9 @@ var ACCESSIBLE_GLOBALS = { // Additional buttons for the workspace toolbar that // go before the "Clear Workspace" button. - toolbarButtonConfig: [] + toolbarButtonConfig: [], + // Prefix of path to sound files. + mediaPathPrefix: '../../accessible/media/' }; document.addEventListener('DOMContentLoaded', function() { ng.platform.browser.bootstrap(blocklyApp.AppView); From dc02dfb8ff4a9a45cb2c7a1ecc59f0193922f4c4 Mon Sep 17 00:00:00 2001 From: Rachel Fenichel Date: Tue, 9 Aug 2016 17:51:50 -0700 Subject: [PATCH 04/10] Separators specified in toolbox XML should replace, not add to, previous gaps. --- core/flyout.js | 9 +++------ core/toolbox.js | 6 ++---- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/core/flyout.js b/core/flyout.js index a216e60f5..07dfe2ce6 100644 --- a/core/flyout.js +++ b/core/flyout.js @@ -637,12 +637,10 @@ Blockly.Flyout.prototype.show = function(xmlList) { // Create the blocks to be shown in this flyout. var blocks = []; var gaps = []; - var lastBlock = null; this.permanentlyDisabled_.length = 0; for (var i = 0, xml; xml = xmlList[i]; i++) { if (xml.tagName) { if (xml.tagName.toUpperCase() == 'BLOCK') { - lastBlock = xml; var curBlock = Blockly.Xml.domToBlock(xml, this.workspace_); if (curBlock.disabled) { // Record blocks that were initially disabled. @@ -656,14 +654,13 @@ Blockly.Flyout.prototype.show = function(xmlList) { // Change the gap between two blocks. // // The default gap is 24, can be set larger or smaller. + // This overwrites the gap attribute on the previous block. // Note that a deprecated method is to add a gap to a block. // var newGap = parseInt(xml.getAttribute('gap'), 10); // Ignore gaps before the first block. - if (!isNaN(newGap) && lastBlock) { - var oldGap = parseInt(lastBlock.getAttribute('gap')); - var gap = isNaN(oldGap) ? newGap : oldGap + newGap; - gaps[gaps.length - 1] = gap; + if (!isNaN(newGap) && gaps.length > 0) { + gaps[gaps.length - 1] = newGap; } else { gaps.push(this.MARGIN * 3); } diff --git a/core/toolbox.js b/core/toolbox.js index 421a1af8e..db3ba63a2 100644 --- a/core/toolbox.js +++ b/core/toolbox.js @@ -333,10 +333,8 @@ Blockly.Toolbox.prototype.syncTrees_ = function(treeIn, treeOut, pathToMedia) { // Note that a deprecated method is to add a gap to a block. // var newGap = parseFloat(childIn.getAttribute('gap')); - if (!isNaN(newGap)) { - var oldGap = parseFloat(lastElement.getAttribute('gap')); - var gap = isNaN(oldGap) ? newGap : oldGap + newGap; - lastElement.setAttribute('gap', gap); + if (!isNaN(newGap) && lastElement) { + lastElement.setAttribute('gap', newGap); } } } From 9819d677a9d45033a4754a54e1ba7c4c75d8dc7c Mon Sep 17 00:00:00 2001 From: Tina Quach Date: Wed, 10 Aug 2016 13:49:19 -0400 Subject: [PATCH 05/10] Blockly Factory: Block Library & Block Exporter (#530) Created Blockly Factory, an expansion of the Block Factory demo that adds the Block Library and Block Exporter to the Block Factory. The Block Library provides the interfaces for the user to save their blocks to local storage so that upon opening and closing the page, their blocks will still persist. In Blockly Factory, Users can re-open saved blocks for edit through a dropdown interface, delete blocks from their library, clear their block library, and import and export their block library. Importing and exporting their block library may be useful for creating specific sets of in-progress blocks for editing. The Block Exporter allows users to export block definitions and generator stubs of their saved blocks easily by using a visual interface powered by Blockly. It contains a selector workspace in which users add and remove blocks to the workspace to select and deselect them for export. The exporter also contains an export settings form through which people may export block definitions and generator stubs. --- demos/blocklyfactory/app_controller.js | 453 ++++++++ .../block_exporter_controller.js | 272 +++++ demos/blocklyfactory/block_exporter_tools.js | 222 ++++ demos/blocklyfactory/block_exporter_view.js | 145 +++ .../block_library_controller.js | 219 ++++ demos/blocklyfactory/block_library_storage.js | 167 +++ demos/blocklyfactory/block_library_view.js | 117 +++ demos/blocklyfactory/factory.css | 199 ++++ demos/blocklyfactory/factory.js | 989 ++++++++++++++++++ demos/blocklyfactory/index.html | 264 +++++ demos/blocklyfactory/link.png | Bin 0 -> 228 bytes 11 files changed, 3047 insertions(+) create mode 100644 demos/blocklyfactory/app_controller.js create mode 100644 demos/blocklyfactory/block_exporter_controller.js create mode 100644 demos/blocklyfactory/block_exporter_tools.js create mode 100644 demos/blocklyfactory/block_exporter_view.js create mode 100644 demos/blocklyfactory/block_library_controller.js create mode 100644 demos/blocklyfactory/block_library_storage.js create mode 100644 demos/blocklyfactory/block_library_view.js create mode 100644 demos/blocklyfactory/factory.css create mode 100644 demos/blocklyfactory/factory.js create mode 100644 demos/blocklyfactory/index.html create mode 100644 demos/blocklyfactory/link.png diff --git a/demos/blocklyfactory/app_controller.js b/demos/blocklyfactory/app_controller.js new file mode 100644 index 000000000..4e05e720f --- /dev/null +++ b/demos/blocklyfactory/app_controller.js @@ -0,0 +1,453 @@ +/** + * @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. + */ + +/** + * @fileoverview The AppController Class brings together the Block + * Factory, Block Library, and Block Exporter functionality into a single web + * app. + * + * @author quachtina96 (Tina Quach) + */ +goog.provide('AppController'); + +goog.require('BlockFactory'); +goog.require('BlockLibraryController'); +goog.require('BlockExporterController'); +goog.require('goog.dom.classlist'); +goog.require('goog.string'); + +/** + * Controller for the Blockly Factory + * @constructor + */ +AppController = function() { + // Initialize Block Library + this.blockLibraryName = 'blockLibrary'; + this.blockLibraryController = + new BlockLibraryController(this.blockLibraryName); + this.blockLibraryController.populateBlockLibrary(); + + // Initialize Block Exporter + this.exporter = + new BlockExporterController(this.blockLibraryController.storage); +}; + +/** + * Tied to the 'Import Block Library' button. Imports block library from file to + * Block Factory. Expects user to upload a single file of JSON mapping each + * block type to its xml text representation. + */ +AppController.prototype.importBlockLibraryFromFile = function() { + var self = this; + var files = document.getElementById('files'); + // If the file list is empty, the user likely canceled in the dialog. + if (files.files.length > 0) { + // The input tag doesn't have the "multiple" attribute + // so the user can only choose 1 file. + var file = files.files[0]; + var fileReader = new FileReader(); + + // Create a map of block type to xml text from the file when it has been + // read. + fileReader.addEventListener('load', function(event) { + var fileContents = event.target.result; + // Create empty object to hold the read block library information. + var blockXmlTextMap = Object.create(null); + try { + // Parse the file to get map of block type to xml text. + blockXmlTextMap = self.formatBlockLibForImport_(fileContents); + } catch (e) { + var message = 'Could not load your block library file.\n' + window.alert(message + '\nFile Name: ' + file.name); + return; + } + + // Create a new block library storage object with inputted block library. + var blockLibStorage = new BlockLibraryStorage( + self.blockLibraryName, blockXmlTextMap); + + // Update block library controller with the new block library + // storage. + self.blockLibraryController.setBlockLibStorage(blockLibStorage); + // Update the block library dropdown. + self.blockLibraryController.populateBlockLibrary(); + // Update the exporter's block library storage. + self.exporter.setBlockLibStorage(blockLibStorage); + }); + // Read the file. + fileReader.readAsText(file); + } +}; + +/** + * Tied to the 'Export Block Library' button. Exports block library to file that + * contains JSON mapping each block type to its xml text representation. + */ +AppController.prototype.exportBlockLibraryToFile = function() { + // Get map of block type to xml. + var blockLib = this.blockLibraryController.getBlockLibrary(); + // Concatenate the xmls, each separated by a blank line. + var blockLibText = this.formatBlockLibForExport_(blockLib); + // Get file name. + var filename = prompt('Enter the file name under which to save your block' + + 'library.'); + // Download file if all necessary parameters are provided. + if (filename) { + BlockFactory.createAndDownloadFile_(blockLibText, filename, 'xml'); + } else { + alert('Could not export Block Library without file name under which to ' + + 'save library.'); + } +}; + +/** + * Converts an object mapping block type to xml to text file for output. + * @private + * + * @param {!Object} blockXmlMap - object mapping block type to xml + * @return {string} String of each block's xml separated by a new line. + */ +AppController.prototype.formatBlockLibForExport_ = function(blockXmlMap) { + var blockXmls = []; + for (var blockType in blockXmlMap) { + blockXmls.push(blockXmlMap[blockType]); + } + return blockXmls.join("\n\n"); +}; + +/** + * Converts imported block library to an object mapping block type to block xml. + * @private + * + * @param {string} xmlText - String containing each block's xml optionally + * separated by whitespace. + * @return {!Object} object mapping block type to xml text. + */ +AppController.prototype.formatBlockLibForImport_ = function(xmlText) { + // Get array of xmls. + var xmlText = goog.string.collapseWhitespace(xmlText); + var blockXmls = goog.string.splitLimit(xmlText, '', 500); + + // Create and populate map. + var blockXmlTextMap = Object.create(null); + // The line above is equivalent of {} except that this object is TRULY + // empty. It doesn't have built-in attributes/functions such as length or + // toString. + for (var i = 0, xml; xml = blockXmls[i]; i++) { + var blockType = this.getBlockTypeFromXml_(xml); + blockXmlTextMap[blockType] = xml; + } + + return blockXmlTextMap; +}; + +/** + * Extracts out block type from xml text, the kind that is saved in block + * library storage. + * @private + * + * @param {!string} xmlText - A block's xml text. + * @return {string} The block type that corresponds to the provided xml text. + */ +AppController.prototype.getBlockTypeFromXml_ = function(xmlText) { + var xmlText = Blockly.Options.parseToolboxTree(xmlText); + // Find factory base block. + var factoryBaseBlockXml = xmlText.getElementsByTagName('block')[0]; + // Get field elements from factory base. + var fields = factoryBaseBlockXml.getElementsByTagName('field'); + for (var i = 0; i < fields.length; i++) { + // The field whose name is 'NAME' holds the block type as its value. + if (fields[i].getAttribute('name') == 'NAME') { + return fields[i].childNodes[0].nodeValue; + } + } +}; + +/** + * Updates the Block Factory tab to show selected block when user selects a + * different block in the block library dropdown. Tied to block library dropdown + * in index.html. + * + * @param {!Element} blockLibraryDropdown - HTML select element from which the + * user selects a block to work on. + */ +AppController.prototype.onSelectedBlockChanged = function(blockLibraryDropdown) { + // Get selected block type. + var blockType = this.blockLibraryController.getSelectedBlockType( + blockLibraryDropdown); + // Update Block Factory page by showing the selected block. + this.blockLibraryController.openBlock(blockType); +}; + +/** + * Add tab handlers to allow switching between the Block Factory + * tab and the Block Exporter tab. + * + * @param {string} blockFactoryTabID - ID of element containing Block Factory + * tab + * @param {string} blockExporterTabID - ID of element containing Block + * Exporter tab + */ +AppController.prototype.addTabHandlers = + function(blockFactoryTabID, blockExporterTabID) { + // Assign this instance of Block Factory Expansion to self in order to + // keep the reference to this object upon tab click. + var self = this; + // Get div elements representing tabs + var blockFactoryTab = goog.dom.getElement(blockFactoryTabID); + var blockExporterTab = goog.dom.getElement(blockExporterTabID); + // Add event listeners. + blockFactoryTab.addEventListener('click', + function() { + self.onFactoryTab(blockFactoryTab, blockExporterTab); + }); + blockExporterTab.addEventListener('click', + function() { + self.onExporterTab(blockFactoryTab, blockExporterTab); + }); +}; + +/** + * Tied to 'Block Factory' Tab. Shows Block Factory and Block Library. + * + * @param {string} blockFactoryTab - div element that is the Block Factory tab + * @param {string} blockExporterTab - div element that is the Block Exporter tab + */ +AppController.prototype.onFactoryTab = + function(blockFactoryTab, blockExporterTab) { + // Turn factory tab on and exporter tab off. + goog.dom.classlist.addRemove(blockFactoryTab, 'taboff', 'tabon'); + goog.dom.classlist.addRemove(blockExporterTab, 'tabon', 'taboff'); + + // Hide container of exporter. + BlockFactory.hide('blockLibraryExporter'); + + // Resize to render workspaces' toolboxes correctly. + window.dispatchEvent(new Event('resize')); +}; + +/** + * Tied to 'Block Exporter' Tab. Shows Block Exporter. + * + * @param {string} blockFactoryTab - div element that is the Block Factory tab + * @param {string} blockExporterTab - div element that is the Block Exporter tab + */ +AppController.prototype.onExporterTab = + function(blockFactoryTab, blockExporterTab) { + // Turn exporter tab on and factory tab off. + goog.dom.classlist.addRemove(blockFactoryTab, 'tabon', 'taboff'); + goog.dom.classlist.addRemove(blockExporterTab, 'taboff', 'tabon'); + + // Update toolbox to reflect current block library. + this.exporter.updateToolbox(); + + // Show container of exporter. + BlockFactory.show('blockLibraryExporter'); + + // Resize to render workspaces' toolboxes correctly. + window.dispatchEvent(new Event('resize')); +}; + +/** + * Assign button click handlers for the exporter. + */ +AppController.prototype.assignExporterClickHandlers = function() { + var self = this; + // Export blocks when the user submits the export settings. + document.getElementById('exporterSubmitButton').addEventListener('click', + function() { + self.exporter.exportBlocks(); + }); + document.getElementById('clearSelectedButton').addEventListener('click', + function() { + self.exporter.clearSelectedBlocks(); + }); + document.getElementById('addAllButton').addEventListener('click', + function() { + self.exporter.addAllBlocksToWorkspace(); + }); +}; + +/** + * Assign button click handlers for the block library. + */ +AppController.prototype.assignLibraryClickHandlers = function() { + var self = this; + // Assign button click handlers for Block Library. + document.getElementById('saveToBlockLibraryButton').addEventListener('click', + function() { + self.blockLibraryController.saveToBlockLibrary(); + }); + + document.getElementById('removeBlockFromLibraryButton').addEventListener( + 'click', + function() { + self.blockLibraryController.removeFromBlockLibrary(); + }); + + document.getElementById('clearBlockLibraryButton').addEventListener('click', + function() { + self.blockLibraryController.clearBlockLibrary(); + }); + + var dropdown = document.getElementById('blockLibraryDropdown'); + dropdown.addEventListener('change', + function() { + self.onSelectedBlockChanged(dropdown); + }); +}; + +/** + * Assign button click handlers for the block factory. + */ +AppController.prototype.assignFactoryClickHandlers = function() { + var self = this; + // Assign button event handlers for Block Factory. + document.getElementById('localSaveButton') + .addEventListener('click', function() { + self.exportBlockLibraryToFile(); + }); + document.getElementById('helpButton').addEventListener('click', + function() { + open('https://developers.google.com/blockly/custom-blocks/block-factory', + 'BlockFactoryHelp'); + }); + document.getElementById('downloadBlocks').addEventListener('click', + function() { + BlockFactory.downloadTextArea('blocks', 'languagePre'); + }); + document.getElementById('downloadGenerator').addEventListener('click', + function() { + BlockFactory.downloadTextArea('generator', 'generatorPre'); + }); + document.getElementById('files').addEventListener('change', + function() { + // Warn user. + var replace = confirm('This imported block library will ' + + 'replace your current block library.'); + if (replace) { + self.importBlockLibraryFromFile(); + // Clear this so that the change event still fires even if the + // same file is chosen again. If the user re-imports a file, we + // want to reload the workspace with its contents. + this.value = null; + } + }); + document.getElementById('createNewBlockButton') + .addEventListener('click', function() { + BlockFactory.mainWorkspace.clear(); + BlockFactory.showStarterBlock(); + BlockLibraryView.selectDefaultOption('blockLibraryDropdown'); + }); +}; + +/** + * Add event listeners for the block factory. + */ +AppController.prototype.addFactoryEventListeners = function() { + BlockFactory.mainWorkspace.addChangeListener(BlockFactory.updateLanguage); + document.getElementById('direction') + .addEventListener('change', BlockFactory.updatePreview); + document.getElementById('languageTA') + .addEventListener('change', BlockFactory.updatePreview); + document.getElementById('languageTA') + .addEventListener('keyup', BlockFactory.updatePreview); + document.getElementById('format') + .addEventListener('change', BlockFactory.formatChange); + document.getElementById('language') + .addEventListener('change', BlockFactory.updatePreview); +}; + +/** + * Handle Blockly Storage with App Engine. + */ +AppController.prototype.initializeBlocklyStorage = function() { + BlocklyStorage.HTTPREQUEST_ERROR = + 'There was a problem with the request.\n'; + BlocklyStorage.LINK_ALERT = + 'Share your blocks with this link:\n\n%1'; + BlocklyStorage.HASH_ERROR = + 'Sorry, "%1" doesn\'t correspond with any saved Blockly file.'; + BlocklyStorage.XML_ERROR = 'Could not load your saved file.\n' + + 'Perhaps it was created with a different version of Blockly?'; + var linkButton = document.getElementById('linkButton'); + linkButton.style.display = 'inline-block'; + linkButton.addEventListener('click', + function() { + BlocklyStorage.link(BlockFactory.mainWorkspace);}); + BlockFactory.disableEnableLink(); +}; +/** + * Initialize Blockly and layout. Called on page load. + */ +AppController.prototype.init = function() { + // Handle Blockly Storage with App Engine + if ('BlocklyStorage' in window) { + this.initializeBlocklyStorage(); + } + + // Assign click handlers. + this.assignExporterClickHandlers(); + this.assignLibraryClickHandlers(); + this.assignFactoryClickHandlers(); + + // Handle resizing of Block Factory elements. + var expandList = [ + document.getElementById('blockly'), + document.getElementById('blocklyMask'), + document.getElementById('preview'), + document.getElementById('languagePre'), + document.getElementById('languageTA'), + document.getElementById('generatorPre') + ]; + + var onresize = function(e) { + for (var i = 0, expand; expand = expandList[i]; i++) { + expand.style.width = (expand.parentNode.offsetWidth - 2) + 'px'; + expand.style.height = (expand.parentNode.offsetHeight - 2) + 'px'; + } + }; + onresize(); + window.addEventListener('resize', onresize); + + // Inject Block Factory Main Workspace. + var toolbox = document.getElementById('toolbox'); + BlockFactory.mainWorkspace = Blockly.inject('blockly', + {collapse: false, + toolbox: toolbox, + media: '../../media/'}); + + // Add tab handlers for switching between Block Factory and Block Exporter. + this.addTabHandlers("blockfactory_tab", "blocklibraryExporter_tab"); + + this.exporter.addChangeListenersToSelectorWorkspace(); + + // Create the root block on Block Factory main workspace. + if ('BlocklyStorage' in window && window.location.hash.length > 1) { + BlocklyStorage.retrieveXml(window.location.hash.substring(1), + BlockFactory.mainWorkspace); + } else { + BlockFactory.showStarterBlock(); + } + BlockFactory.mainWorkspace.clearUndo(); + + // Add Block Factory event listeners. + this.addFactoryEventListeners(); +}; diff --git a/demos/blocklyfactory/block_exporter_controller.js b/demos/blocklyfactory/block_exporter_controller.js new file mode 100644 index 000000000..04892e5d8 --- /dev/null +++ b/demos/blocklyfactory/block_exporter_controller.js @@ -0,0 +1,272 @@ +/** + * @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. + */ + +/** + * @fileoverview Javascript for the Block Exporter Controller class. Allows + * users to export block definitions and generator stubs of their saved blocks + * easily using a visual interface. Depends on Block Exporter View and Block + * Exporter Tools classes. Interacts with Export Settings in the index.html. + * + * @author quachtina96 (Tina Quach) + */ + +'use strict'; + +goog.provide('BlockExporterController'); +goog.require('BlockExporterView'); +goog.require('BlockExporterTools'); +goog.require('goog.dom.xml'); + +/** + * BlockExporter Controller Class + * @constructor + * + * @param {!BlockLibrary.Storage} blockLibStorage - Block Library Storage. + */ +BlockExporterController = function(blockLibStorage) { + // BlockLibrary.Storage object containing user's saved blocks + this.blockLibStorage = blockLibStorage; + // Utils for generating code to export + this.tools = new BlockExporterTools(); + // View provides the selector workspace and export settings UI. + this.view = new BlockExporterView( + //Xml representation of the toolbox + this.tools.generateToolboxFromLibrary(this.blockLibStorage)); +}; + +/** + * Set the block library storage object from which exporter exports. + * + * @param {!BlockLibraryStorage} blockLibStorage - Block Library Storage object + * that stores the blocks. + */ +BlockExporterController.prototype.setBlockLibStorage = + function(blockLibStorage) { + this.blockLibStorage = blockLibStorage; +}; + +/** + * Get the block library storage object from which exporter exports. + * + * @return {!BlockLibraryStorage} blockLibStorage - Block Library Storage object + * that stores the blocks. + */ +BlockExporterController.prototype.getBlockLibStorage = + function(blockLibStorage) { + return this.blockLibStorage; +}; + +/** + * Get the selected block types. + * @private + * + * @return {!Array.} Types of blocks in workspace. + */ +BlockExporterController.prototype.getSelectedBlockTypes_ = function() { + var selectedBlocks = this.view.getSelectedBlocks(); + var blockTypes = []; + for (var i = 0, block; block = selectedBlocks[i]; i++) { + blockTypes.push(block.type); + } + return blockTypes; +}; + +/** + * Get selected blocks from selector workspace, pulls info from the Export + * Settings form in Block Exporter, and downloads block code accordingly. + */ +BlockExporterController.prototype.exportBlocks = function() { + var blockTypes = this.getSelectedBlockTypes_(); + var blockXmlMap = this.blockLibStorage.getBlockXmlMap(blockTypes); + + // Pull inputs from the Export Settings form. + var definitionFormat = document.getElementById('exportFormat').value; + var language = document.getElementById('exportLanguage').value; + var blockDef_filename = document.getElementById('blockDef_filename').value; + var generatorStub_filename = document.getElementById( + 'generatorStub_filename').value; + var wantBlockDef = document.getElementById('blockDefCheck').checked; + var wantGenStub = document.getElementById('genStubCheck').checked; + + if (wantBlockDef) { + // User wants to export selected blocks' definitions. + if (!blockDef_filename) { + // User needs to enter filename. + alert('Please enter a filename for your block definition(s) download.'); + } else { + // Get block definition code in the selected format for the blocks. + var blockDefs = this.tools.getBlockDefs(blockXmlMap, + definitionFormat); + // Download the file. + BlockFactory.createAndDownloadFile_( + blockDefs, blockDef_filename, definitionFormat); + } + } + + if (wantGenStub) { + // User wants to export selected blocks' generator stubs. + if (!generatorStub_filename) { + // User needs to enter filename. + alert('Please enter a filename for your generator stub(s) download.'); + } else { + // Get generator stub code in the selected language for the blocks. + var genStubs = this.tools.getGeneratorCode(blockXmlMap, + language); + // Download the file. + BlockFactory.createAndDownloadFile_( + genStubs, generatorStub_filename, language); + } + } +}; + +/** + * Update the Exporter's toolbox with either the given toolbox xml or toolbox + * xml generated from blocks stored in block library. + * + * @param {Element} opt_toolboxXml - Xml to define toolbox of the selector + * workspace. + */ +BlockExporterController.prototype.updateToolbox = function(opt_toolboxXml) { + // Use given xml or xml generated from updated block library. + var updatedToolbox = opt_toolboxXml || + this.tools.generateToolboxFromLibrary(this.blockLibStorage); + // Update the view's toolbox. + this.view.setToolbox(updatedToolbox); + // Render the toolbox in the selector workspace. + this.view.renderToolbox(updatedToolbox); + // Disable any selected blocks. + var selectedBlocks = this.getSelectedBlockTypes_(); + for (var i = 0, blockType; blockType = selectedBlocks[i]; i++) { + this.setBlockEnabled(blockType, false); + } +}; + +/** + * Enable or Disable block in selector workspace's toolbox. + * + * @param {!string} blockType - Type of block to disable or enable. + * @param {!boolean} enable - True to enable the block, false to disable block. + */ +BlockExporterController.prototype.setBlockEnabled = + function(blockType, enable) { + // Get toolbox xml, category, and block elements. + var toolboxXml = this.view.toolbox; + var category = goog.dom.xml.selectSingleNode(toolboxXml, + '//category[@name="' + blockType + '"]'); + var block = goog.dom.getFirstElementChild(category); + // Enable block. + goog.dom.xml.setAttributes(block, {disabled: !enable}); +}; + +/** + * Add change listeners to the exporter's selector workspace. + */ +BlockExporterController.prototype.addChangeListenersToSelectorWorkspace + = function() { + // Assign the BlockExporterController to 'self' to be called in the change + // listeners. This keeps it in scope--otherwise, 'this' in the change + // listeners refers to the wrong thing. + var self = this; + var selector = this.view.selectorWorkspace; + selector.addChangeListener( + function(event) { + self.onSelectBlockForExport_(event); + }); + selector.addChangeListener( + function(event) { + self.onDeselectBlockForExport_(event); + }); +}; + +/** + * Callback function for when a user selects a block for export in selector + * workspace. Disables selected block so that the user only exports one + * copy of starter code per block. Attached to the blockly create event in block + * factory expansion's init. + * @private + * + * @param {!Blockly.Events} event - The fired Blockly event. + */ +BlockExporterController.prototype.onSelectBlockForExport_ = function(event) { + // The user created a block in selector workspace. + if (event.type == Blockly.Events.CREATE) { + // Get type of block created. + var block = this.view.selectorWorkspace.getBlockById(event.blockId); + var blockType = block.type; + // Disable the selected block. Users can only export one copy of starter + // code per block. + this.setBlockEnabled(blockType, false); + // Show currently selected blocks in helper text. + this.view.listSelectedBlocks(this.getSelectedBlockTypes_()); + } +}; + +/** + * Callback function for when a user deselects a block in selector + * workspace by deleting it. Re-enables block so that the user may select it for + * export + * @private + * + * @param {!Blockly.Events} event - The fired Blockly event. + */ +BlockExporterController.prototype.onDeselectBlockForExport_ = function(event) { + // The user deleted a block in selector workspace. + if (event.type == Blockly.Events.DELETE) { + // Get type of block created. + var deletedBlockXml = event.oldXml; + var blockType = deletedBlockXml.getAttribute('type'); + // Enable the deselected block. + this.setBlockEnabled(blockType, true); + // Show currently selected blocks in helper text. + this.view.listSelectedBlocks(this.getSelectedBlockTypes_()); + } +}; + +/** + * Tied to the 'Clear Selected Blocks' button in the Block Exporter. + * Deselects all blocks on the selector workspace by deleting them and updating + * text accordingly. + */ +BlockExporterController.prototype.clearSelectedBlocks = function() { + // Clear selector workspace. + this.view.clearSelectorWorkspace(); +}; + +/** + * Tied to the 'Add All Stored Blocks' button in the Block Exporter. + * Adds all blocks stored in block library to the selector workspace. + */ +BlockExporterController.prototype.addAllBlocksToWorkspace = function() { + // Clear selector workspace. + this.view.clearSelectorWorkspace(); + + // Add and evaluate all blocks' definitions. + var allBlockTypes = this.blockLibStorage.getBlockTypes(); + var blockXmlMap = this.blockLibStorage.getBlockXmlMap(allBlockTypes); + this.tools.addBlockDefinitions(blockXmlMap); + + // For every block, render in selector workspace. + for (var i = 0, blockType; blockType = allBlockTypes[i]; i++) { + this.view.addBlock(blockType); + } + + // Clean up workspace. + this.view.cleanUpSelectorWorkspace(); +}; diff --git a/demos/blocklyfactory/block_exporter_tools.js b/demos/blocklyfactory/block_exporter_tools.js new file mode 100644 index 000000000..65c50ddad --- /dev/null +++ b/demos/blocklyfactory/block_exporter_tools.js @@ -0,0 +1,222 @@ +/** + * @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. + */ + +/** + * @fileoverview Javascript for the BlockExporter Tools class, which generates + * block definitions and generator stubs for given block types. Also generates + * toolbox xml for the exporter's workspace. Depends on the BlockFactory for + * its code generation functions. + * + * @author quachtina96 (Tina Quach) + */ +'use strict'; + +goog.provide('BlockExporterTools'); + +goog.require('BlockFactory'); +goog.require('goog.dom'); +goog.require('goog.dom.xml'); + +/** +* Block Exporter Tools Class +* @constructor +*/ +BlockExporterTools = function() { + // Create container for hidden workspace. + this.container = goog.dom.createDom('div', { + 'id': 'blockExporterTools_hiddenWorkspace' + }, ''); // Empty quotes for empty div. + // Hide hidden workspace. + this.container.style.display = 'none'; + goog.dom.appendChild(document.body, this.container); + /** + * Hidden workspace for the Block Exporter that holds pieces that make + * up the block + * @type {Blockly.Workspace} + */ + this.hiddenWorkspace = Blockly.inject(this.container.id, + {collapse: false, + media: '../../media/'}); +}; + +/** + * Get Blockly Block object from xml that encodes the blocks used to design + * the block. + * @private + * + * @param {!Element} xml - Xml element that encodes the blocks used to design + * the block. For example, the block xmls saved in block library. + * @return {!Blockly.Block} - Root block (factory_base block) which contains + * all information needed to generate block definition or null. + */ +BlockExporterTools.prototype.getRootBlockFromXml_ = function(xml) { + // Render xml in hidden workspace. + this.hiddenWorkspace.clear(); + Blockly.Xml.domToWorkspace(xml, this.hiddenWorkspace); + // Get root block. + var rootBlock = this.hiddenWorkspace.getTopBlocks()[0] || null; + return rootBlock; +}; + +/** + * Get Blockly Block by rendering pre-defined block in workspace. + * @private + * + * @param {!Element} blockType - Type of block. + * @return {!Blockly.Block} the Blockly.Block of desired type. + */ +BlockExporterTools.prototype.getDefinedBlock_ = function(blockType) { + this.hiddenWorkspace.clear(); + return this.hiddenWorkspace.newBlock(blockType); +}; + +/** + * Return the given language code of each block type in an array. + * + * @param {!Object} blockXmlMap - Map of block type to xml. + * @param {string} definitionFormat - 'JSON' or 'JavaScript' + * @return {string} The concatenation of each block's language code in the + * desired format. + */ +BlockExporterTools.prototype.getBlockDefs = + function(blockXmlMap, definitionFormat) { + var blockCode = []; + for (var blockType in blockXmlMap) { + var xml = blockXmlMap[blockType]; + if (xml) { + // Render and get block from hidden workspace. + var rootBlock = this.getRootBlockFromXml_(xml); + if (rootBlock) { + // Generate the block's definition. + var code = BlockFactory.getBlockDefinition(blockType, rootBlock, + definitionFormat, this.hiddenWorkspace); + // Add block's definition to the definitions to return. + } else { + // Append warning comment and write to console. + var code = '// No block definition generated for ' + blockType + + '. Could not find root block in xml stored for this block.'; + console.log('No block definition generated for ' + blockType + + '. Could not find root block in xml stored for this block.'); + } + } else { + // Append warning comment and write to console. + var code = '// No block definition generated for ' + blockType + + '. Block was not found in Block Library Storage.'; + console.log('No block definition generated for ' + blockType + + '. Block was not found in Block Library Storage.'); + } + blockCode.push(code); + } + return blockCode.join("\n\n"); +}; + +/** + * Return the generator code of each block type in an array in a given language. + * + * @param {!Object} blockXmlMap - Map of block type to xml. + * @param {string} generatorLanguage - e.g.'JavaScript', 'Python', 'PHP', 'Lua', + * 'Dart' + * @return {string} The concatenation of each block's generator code in the + * desired format. + */ +BlockExporterTools.prototype.getGeneratorCode = + function(blockXmlMap, generatorLanguage) { + var multiblockCode = []; + // Define the custom blocks in order to be able to create instances of + // them in the exporter workspace. + this.addBlockDefinitions(blockXmlMap); + + for (var blockType in blockXmlMap) { + var xml = blockXmlMap[blockType]; + if (xml) { + // Render the preview block in the hidden workspace. + var tempBlock = this.getDefinedBlock_(blockType); + // Get generator stub for the given block and add to generator code. + var blockGenCode = + BlockFactory.getGeneratorStub(tempBlock, generatorLanguage); + } else { + // Append warning comment and write to console. + var blockGenCode = '// No generator stub generated for ' + blockType + + '. Block was not found in Block Library Storage.'; + console.log('No block generator stub generated for ' + blockType + + '. Block was not found in Block Library Storage.'); + } + multiblockCode.push(blockGenCode); + } + return multiblockCode.join("\n\n"); +}; + +/** + * Evaluates block definition code of each block in given object mapping + * block type to xml. Called in order to be able to create instances of the + * blocks in the exporter workspace. + * + * @param {!Object} blockXmlMap - Map of block type to xml. + */ +BlockExporterTools.prototype.addBlockDefinitions = function(blockXmlMap) { + var blockDefs = this.getBlockDefs(blockXmlMap, 'JavaScript'); + eval(blockDefs); +}; + +/** + * Pulls information about all blocks in the block library to generate xml + * for the selector workpace's toolbox. + * + * @return {!Element} Xml representation of the toolbox. + */ +BlockExporterTools.prototype.generateToolboxFromLibrary + = function(blockLibStorage) { + // Create DOM for XML. + var xmlDom = goog.dom.createDom('xml', { + 'id' : 'blockExporterTools_toolbox', + 'style' : 'display:none' + }); + + var allBlockTypes = blockLibStorage.getBlockTypes(); + // Object mapping block type to XML. + var blockXmlMap = blockLibStorage.getBlockXmlMap(allBlockTypes); + + // Define the custom blocks in order to be able to create instances of + // them in the exporter workspace. + this.addBlockDefinitions(blockXmlMap); + + for (var blockType in blockXmlMap) { + // Create category DOM element. + var categoryElement = goog.dom.createDom('category'); + categoryElement.setAttribute('name',blockType); + + // Get block. + var block = this.getDefinedBlock_(blockType); + + // Get preview block XML. + var blockChild = Blockly.Xml.blockToDom(block); + blockChild.removeAttribute('id'); + + // Add block to category and category to XML. + categoryElement.appendChild(blockChild); + xmlDom.appendChild(categoryElement); + } + + // If there are no blocks in library, append dummy category. + var categoryElement = goog.dom.createDom('category'); + categoryElement.setAttribute('name','Next Saved Block'); + xmlDom.appendChild(categoryElement); + return xmlDom; +}; diff --git a/demos/blocklyfactory/block_exporter_view.js b/demos/blocklyfactory/block_exporter_view.js new file mode 100644 index 000000000..ee1d76c6c --- /dev/null +++ b/demos/blocklyfactory/block_exporter_view.js @@ -0,0 +1,145 @@ +/** + * @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. + */ + +/** + * @fileoverview Javascript for the Block Exporter View class. Takes care of + * generating the selector workspace through which users select blocks to + * export. + * + * @author quachtina96 (Tina Quach) + */ + +'use strict'; + +goog.provide('BlockExporterView'); + +goog.require('goog.dom'); + +/** + * BlockExporter View Class + * @constructor + * + * @param {Element} toolbox - Xml for the toolbox of the selector workspace. + */ +BlockExporterView = function(toolbox) { + // Xml representation of the toolbox + if (toolbox.hasChildNodes) { + this.toolbox = toolbox; + } else { + // Toolbox is empty. Append dummy category to toolbox because toolbox + // cannot switch between category and flyout-only mode after injection. + var categoryElement = goog.dom.createDom('category'); + categoryElement.setAttribute('name', 'Next Saved Block'); + toolbox.appendChild(categoryElement); + this.toolbox = toolbox; + } + // Workspace users use to select blocks for export + this.selectorWorkspace = + Blockly.inject('exportSelector', + {collapse: false, + toolbox: this.toolbox, + grid: + {spacing: 20, + length: 3, + colour: '#ccc', + snap: true} + }); +}; + +/** + * Update the toolbox of this instance of BlockExporterView. + * + * @param {Element} toolboxXml - Xml for the toolbox of the selector workspace. + */ +BlockExporterView.prototype.setToolbox = function(toolboxXml) { + // Parse the provided toolbox tree into a consistent DOM format. + this.toolbox = Blockly.Options.parseToolboxTree(toolboxXml); +}; + +/** + * Renders the toolbox in the workspace. Used to update the toolbox upon + * switching between Block Factory tab and Block Exporter Tab. + */ +BlockExporterView.prototype.renderToolbox = function() { + this.selectorWorkspace.updateToolbox(this.toolbox); +}; + +/** + * Updates the helper text. + * + * @param {string} newText - New helper text. + * @param {boolean} opt_append - True if appending to helper Text, false if + * replacing. + */ +BlockExporterView.prototype.updateHelperText = function(newText, opt_append) { + if (opt_append) { + goog.dom.getElement('helperText').textContent = + goog.dom.getElement('helperText').textContent + newText; + } else { + goog.dom.getElement('helperText').textContent = newText; + } +}; + +/** + * Updates the helper text to show list of currently selected blocks. + * + * @param {!Array.} selectedBlockTypes - Array of blocks selected in workspace. + */ +BlockExporterView.prototype.listSelectedBlocks = function(selectedBlockTypes) { + var selectedBlocksText = selectedBlockTypes.join(', '); + this.updateHelperText('Currently Selected: ' + selectedBlocksText); +}; + +/** + * Renders block of given type on selector workspace assuming block has already + * been defined. + * + * @param {string} blockType - Type of block to add to selector workspce. + */ +BlockExporterView.prototype.addBlock = function(blockType) { + var newBlock = this.selectorWorkspace.newBlock(blockType); + newBlock.initSvg(); + newBlock.render(); +}; + +/** + * Clears selector workspace. + */ +BlockExporterView.prototype.clearSelectorWorkspace = function() { + this.selectorWorkspace.clear(); +}; + +/** + * Neatly layout the blocks in selector workspace. + */ +BlockExporterView.prototype.cleanUpSelectorWorkspace = function() { + this.selectorWorkspace.cleanUp_(); +}; + +/** + * Returns array of selected blocks. + * + * @return {Array.} Array of all blocks in selector workspace. + */ +BlockExporterView.prototype.getSelectedBlocks = function() { + return this.selectorWorkspace.getAllBlocks(); +}; + + diff --git a/demos/blocklyfactory/block_library_controller.js b/demos/blocklyfactory/block_library_controller.js new file mode 100644 index 000000000..0b7210118 --- /dev/null +++ b/demos/blocklyfactory/block_library_controller.js @@ -0,0 +1,219 @@ +/** + * @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. + */ + +/** + * @fileoverview Contains the code for Block Library Controller, which + * depends on Block Library Storage and Block Library UI. Provides the + * interfaces for the user to + * - save their blocks to the browser + * - re-open and edit saved blocks + * - delete blocks + * - clear their block library + * Depends on BlockFactory functions defined in factory.js. + * + * @author quachtina96 (Tina Quach) + */ +'use strict'; + +goog.provide('BlockLibraryController'); + +goog.require('BlockLibraryStorage'); +goog.require('BlockLibraryView'); +goog.require('BlockFactory'); + +/** + * Block Library Controller Class + * @constructor + * + * @param {string} blockLibraryName - Desired name of Block Library, also used + * to create the key for where it's stored in local storage. + * @param {!BlockLibraryStorage} opt_blockLibraryStorage - optional storage + * object that allows user to import a block library. + */ +BlockLibraryController = function(blockLibraryName, opt_blockLibraryStorage) { + this.name = blockLibraryName; + // Create a new, empty Block Library Storage object, or load existing one. + this.storage = opt_blockLibraryStorage || new BlockLibraryStorage(this.name); +}; + +/** + * Returns the block type of the block the user is building. + * @private + * + * @return {string} The current block's type. + */ +BlockLibraryController.prototype.getCurrentBlockType_ = function() { + var rootBlock = BlockFactory.getRootBlock(BlockFactory.mainWorkspace); + var blockType = rootBlock.getFieldValue('NAME').trim().toLowerCase(); + // Replace white space with underscores + return blockType.replace(/\W/g, '_').replace(/^(\d)/, '_\\1'); +}; + +/** + * Removes current block from Block Library + * + * @param {string} blockType - Type of block. + */ +BlockLibraryController.prototype.removeFromBlockLibrary = function() { + var blockType = this.getCurrentBlockType_(); + this.storage.removeBlock(blockType); + this.storage.saveToLocalStorage(); + this.populateBlockLibrary(); +}; + +/** + * Updates the workspace to show the block user selected from library + * + * @param {string} blockType - Block to edit on block factory. + */ +BlockLibraryController.prototype.openBlock = function(blockType) { + var xml = this.storage.getBlockXml(blockType); + BlockFactory.mainWorkspace.clear(); + Blockly.Xml.domToWorkspace(xml, BlockFactory.mainWorkspace); +}; + +/** + * Returns type of block selected from library. + * + * @param {Element} blockLibraryDropdown - The block library dropdown. + * @return {string} Type of block selected. + */ +BlockLibraryController.prototype.getSelectedBlockType = + function(blockLibraryDropdown) { + return BlockLibraryView.getSelected(blockLibraryDropdown); +}; + +/** + * Confirms with user before clearing the block library in local storage and + * updating the dropdown. + */ +BlockLibraryController.prototype.clearBlockLibrary = function() { + var check = confirm( + 'Click OK to clear your block library.'); + if (check) { + // Clear Block Library Storage. + this.storage.clear(); + this.storage.saveToLocalStorage(); + // Update dropdown. + BlockLibraryView.clearOptions('blockLibraryDropdown'); + // Add a default, blank option to dropdown for when no block from library is + // selected. + BlockLibraryView.addDefaultOption('blockLibraryDropdown'); + } +}; + +/** + * Saves current block to local storage and updates dropdown. + */ +BlockLibraryController.prototype.saveToBlockLibrary = function() { + var blockType = this.getCurrentBlockType_(); + // If block under that name already exists, confirm that user wants to replace + // saved block. + if (this.isInBlockLibrary(blockType)) { + var replace = confirm('You already have a block called ' + blockType + + ' in your library. Click OK to replace.'); + if (!replace) { + // Do not save if user doesn't want to replace the saved block. + return; + } + } + + // Save block. + var xmlElement = Blockly.Xml.workspaceToDom(BlockFactory.mainWorkspace); + this.storage.addBlock(blockType, xmlElement); + this.storage.saveToLocalStorage(); + + // Do not add another option to dropdown if replacing. + if (replace) { + return; + } + BlockLibraryView.addOption( + blockType, blockType, 'blockLibraryDropdown', true, true); +}; + +/** + * Checks to see if the given blockType is already in Block Library + * + * @param {string} blockType - Type of block. + * @return {boolean} Boolean indicating whether or not block is in the library. + */ +BlockLibraryController.prototype.isInBlockLibrary = function(blockType) { + var blockLibrary = this.storage.blocks; + return (blockType in blockLibrary && blockLibrary[blockType] != null); +}; + +/** + * Populates the dropdown menu. + */ +BlockLibraryController.prototype.populateBlockLibrary = function() { + BlockLibraryView.clearOptions('blockLibraryDropdown'); + // Add a default, blank option to dropdown for when no block from library is + // selected. + BlockLibraryView.addDefaultOption('blockLibraryDropdown'); + // Add option for each saved block. + var blockLibrary = this.storage.blocks; + for (var block in blockLibrary) { + // Make sure the block wasn't deleted. + if (blockLibrary[block] != null) { + BlockLibraryView.addOption( + block, block, 'blockLibraryDropdown', false, true); + } + } +}; + +/** + * Return block library mapping block type to xml. + * + * @return {Object} Object mapping block type to xml text. + */ +BlockLibraryController.prototype.getBlockLibrary = function() { + return this.storage.getBlockXmlTextMap(); +}; + +/** + * Set the block library storage object from which exporter exports. + * + * @param {!BlockLibraryStorage} blockLibStorage - Block Library Storage + * object. + */ +BlockLibraryController.prototype.setBlockLibStorage + = function(blockLibStorage) { + this.storage = blockLibStorage; +}; + +/** + * Get the block library storage object from which exporter exports. + * + * @return {!BlockLibraryStorage} blockLibStorage - Block Library Storage object + * that stores the blocks. + */ +BlockLibraryController.prototype.getBlockLibStorage = + function(blockLibStorage) { + return this.blockLibStorage; +}; + +/** + * Get the block library storage object from which exporter exports. + * + * @return {boolean} True if the Block Library is empty, false otherwise. + */ +BlockLibraryController.prototype.hasEmptyBlockLib = function() { + return this.storage.isEmpty(); +}; diff --git a/demos/blocklyfactory/block_library_storage.js b/demos/blocklyfactory/block_library_storage.js new file mode 100644 index 000000000..4bca70242 --- /dev/null +++ b/demos/blocklyfactory/block_library_storage.js @@ -0,0 +1,167 @@ +/** + * @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. + */ + +/** + * @fileoverview Javascript for Block Library's Storage Class. + * Depends on Block Library for its namespace. + * + * @author quachtina96 (Tina Quach) + */ + +'use strict'; + +goog.provide('BlockLibraryStorage'); + +/** + * Represents a block library's storage. + * @constructor + * + * @param {string} blockLibraryName - Desired name of Block Library, also used + * to create the key for where it's stored in local storage. + * @param {Object} opt_blocks - Object mapping block type to xml. + */ +BlockLibraryStorage = function(blockLibraryName, opt_blocks) { + // Add prefix to this.name to avoid collisions in local storage. + this.name = 'BlockLibraryStorage.' + blockLibraryName; + if (!opt_blocks) { + // Initialize this.blocks by loading from local storage. + this.loadFromLocalStorage(); + if (this.blocks == null) { + this.blocks = Object.create(null); + // The line above is equivalent of {} except that this object is TRULY + // empty. It doesn't have built-in attributes/functions such as length or + // toString. + this.saveToLocalStorage(); + } + } else { + this.blocks = opt_blocks; + this.saveToLocalStorage(); + } +}; + +/** + * Reads the named block library from local storage and saves it in this.blocks. + */ +BlockLibraryStorage.prototype.loadFromLocalStorage = function() { + // goog.global is synonymous to window, and allows for flexibility + // between browsers. + var object = goog.global.localStorage[this.name]; + this.blocks = object ? JSON.parse(object) : null; +}; + +/** + * Writes the current block library (this.blocks) to local storage. + */ +BlockLibraryStorage.prototype.saveToLocalStorage = function() { + goog.global.localStorage[this.name] = JSON.stringify(this.blocks); +}; + +/** + * Clears the current block library. + */ +BlockLibraryStorage.prototype.clear = function() { + this.blocks = Object.create(null); + // The line above is equivalent of {} except that this object is TRULY + // empty. It doesn't have built-in attributes/functions such as length or + // toString. +}; + +/** + * Saves block to block library. + * + * @param {string} blockType - Type of block. + * @param {Element} blockXML - The block's XML pulled from workspace. + */ +BlockLibraryStorage.prototype.addBlock = function(blockType, blockXML) { + var prettyXml = Blockly.Xml.domToPrettyText(blockXML); + this.blocks[blockType] = prettyXml; +}; + +/** + * Removes block from current block library (this.blocks). + * + * @param {string} blockType - Type of block. + */ +BlockLibraryStorage.prototype.removeBlock = function(blockType) { + delete this.blocks[blockType]; +}; + +/** + * Returns the xml of given block type stored in current block library + * (this.blocks). + * + * @param {string} blockType - Type of block. + * @return {Element} The xml that represents the block type or null. + */ +BlockLibraryStorage.prototype.getBlockXml = function(blockType) { + var xml = this.blocks[blockType] || null; + if (xml) { + var xml = Blockly.Xml.textToDom(xml); + } + return xml; +}; + + +/** + * Returns map of each block type to its corresponding xml stored in current + * block library (this.blocks). + * + * @param {Array.} blockTypes - Types of blocks. + * @return {!Object} Map of block type to corresponding xml. + */ +BlockLibraryStorage.prototype.getBlockXmlMap = function(blockTypes) { + var blockXmlMap = {}; + for (var i = 0; i < blockTypes.length; i++) { + var blockType = blockTypes[i]; + var xml = this.getBlockXml(blockType); + blockXmlMap[blockType] = xml; + } + return blockXmlMap; +}; + +/** + * Returns array of all block types stored in current block library. + * + * @return {!Array.} Array of block types stored in library. + */ +BlockLibraryStorage.prototype.getBlockTypes = function() { + return Object.keys(this.blocks); +}; + +/** + * Checks to see if block library is empty. + * + * @return {boolean} True if empty, false otherwise. + */ +BlockLibraryStorage.prototype.isEmpty = function() { + for (var blockType in this.blocks) { + return false; + } + return true; +}; + +/** + * Returns array of all block types stored in current block library. + * + * @return {!Array.} Map of block type to corresponding xml text. + */ +BlockLibraryStorage.prototype.getBlockXmlTextMap = function() { + return this.blocks; +}; diff --git a/demos/blocklyfactory/block_library_view.js b/demos/blocklyfactory/block_library_view.js new file mode 100644 index 000000000..151055a42 --- /dev/null +++ b/demos/blocklyfactory/block_library_view.js @@ -0,0 +1,117 @@ +/** + * @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. + */ + +/** + * @fileoverview Javascript for Block Library's UI for pulling blocks from the + * Block Library's storage to edit in Block Factory. + * + * @author quachtina96 (Tina Quach) + */ + +'use strict'; + +goog.provide('BlockLibraryView'); + +/** + * Creates a node of a given element type and appends to the node with given id. + * + * @param {string} optionIdentifier - String used to identify option. + * @param {string} optionText - Text to display in the dropdown for the option. + * @param {string} dropdownID - ID for HTML select element. + * @param {boolean} selected - Whether or not the option should be selected on + * the dropdown. + * @param {boolean} enabled - Whether or not the option should be enabled. + */ +BlockLibraryView.addOption + = function(optionIdentifier, optionText, dropdownID, selected, enabled) { + var dropdown = document.getElementById(dropdownID); + var option = document.createElement('option'); + // The value attribute of a dropdown's option is not visible in the UI, but is + // useful for identifying different options that may have the same text. + option.value = optionIdentifier; + // The text attribute is what the user sees in the dropdown for the option. + option.text = optionText; + option.selected = selected; + option.disabled = !enabled; + dropdown.add(option); +}; + +/** + * Adds a default, blank option to dropdown for when no block from library is + * selected. + * + * @param {string} dropdownID - ID of HTML select element + */ +BlockLibraryView.addDefaultOption = function(dropdownID) { + BlockLibraryView.addOption( + 'BLOCK_LIBRARY_DEFAULT_BLANK', '', dropdownID, true, false); +}; + +/** + * Selects the default, blank option in dropdown identified by given ID. + * + * @param {string} dropdownID - ID of HTML select element + */ +BlockLibraryView.selectDefaultOption = function(dropdownID) { + var dropdown = document.getElementById(dropdownID); + // Deselect currently selected option. + var index = dropdown.selectedIndex; + dropdown.options[index].selected = false; + // Select default option, always the first in the dropdown. + var defaultOption = dropdown.options[0]; + defaultOption.selected = true; +}; + +/** + * Returns block type of selected block. + * + * @param {Element} dropdown - HTML select element. + * @return {string} Type of block selected. + */ +BlockLibraryView.getSelected = function(dropdown) { + var index = dropdown.selectedIndex; + return dropdown.options[index].value; +}; + +/** + * Removes option currently selected in dropdown from dropdown menu. + * + * @param {string} dropdownID - ID of HTML select element within which to find + * the selected option. + */ +BlockLibraryView.removeSelectedOption = function(dropdownID) { + var dropdown = document.getElementById(dropdownID); + if (dropdown) { + dropdown.remove(dropdown.selectedIndex); + } +}; + +/** + * Removes all options from dropdown. + * + * @param {string} dropdownID - ID of HTML select element to clear options of. + */ +BlockLibraryView.clearOptions = function(dropdownID) { + var dropdown = document.getElementById(dropdownID); + while (dropdown.length > 0) { + dropdown.remove(dropdown.length - 1); + } +}; + diff --git a/demos/blocklyfactory/factory.css b/demos/blocklyfactory/factory.css new file mode 100644 index 000000000..171e7927e --- /dev/null +++ b/demos/blocklyfactory/factory.css @@ -0,0 +1,199 @@ +/** + * @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. + */ + +html, body { + height: 100%; +} + +body { + background-color: #fff; + font-family: sans-serif; + margin: 0 5px; + overflow: hidden +} + +h1 { + font-weight: normal; + font-size: 140%; +} + +h3 { + margin-top: 5px; + margin-bottom: 0; +} + +table { + height: 100%; + width: 100%; +} + +td { + vertical-align: top; + padding: 0; +} + +p { + display: block; + -webkit-margin-before: 0em; + -webkit-margin-after: 0em; + -webkit-margin-start: 0px; + -webkit-margin-end: 0px; + padding: 5px 0px; +} + + +#blockly { + position: fixed; +} + +#blocklyMask { + background-color: #000; + cursor: not-allowed; + display: none; + position: fixed; + opacity: 0.2; + z-index: 9; +} + +#preview { + position: absolute; +} + +pre, +#languageTA { + border: #ddd 1px solid; + margin-top: 0; + position: absolute; + overflow: scroll; +} + +#languageTA { + display: none; + font: 10pt monospace; +} + +.downloadButton { + padding: 5px; +} + +button:disabled, .buttonStyle:disabled { + opacity: 0.6; +} + +button>*, .buttonStyle>* { + opacity: 1; + vertical-align: text-bottom; +} + +button, .buttonStyle { + border-radius: 4px; + border: 1px solid #ddd; + background-color: #eee; + color: #000; + padding: 10px; + margin: 10px 5px; + font-size: small; +} + +.buttonStyle:hover:not(:disabled), button:hover:not(:disabled) { + box-shadow: 2px 2px 5px #888; +} + +.buttonStyle:hover:not(:disabled)>*, button:hover:not(:disabled)>* { + opacity: 1; +} + +#linkButton { + display: none; +} + +#blockFactoryContent { + height: 87%; +} + +#blockLibraryContainer { + vertical-align: bottom; +} + +#blockLibraryControls { + text-align: right; + vertical-align: middle; +} + +#previewContainer { + vertical-align: bottom; +} + +#buttonContainer { + text-align: right; + vertical-align: middle; +} + +#files { + position: absolute; + visibility: hidden; +} + +#toolbox { + display: none; +} + +#blocklyWorkspaceContainer { + height: 95%; + padding: 2px; + width: 50%; +} + +#blockLibraryExporter { + clear: both; + display: none; + height: 100%; +} + +#exportSelector { + float: left; + height: 75%; + width: 60%; +} + +#exportSettings { + margin: auto; + padding: 16px; + overflow: hidden; +} + +#exporterHiddenWorkspace { + display: none; +} + +/* Tabs */ + +.tab { + float: left; + padding: 5px 19px; +} + +.tab.tabon { + background-color: #ddd; +} + +.tab.taboff { + cursor: pointer; +} diff --git a/demos/blocklyfactory/factory.js b/demos/blocklyfactory/factory.js new file mode 100644 index 000000000..12712a0a6 --- /dev/null +++ b/demos/blocklyfactory/factory.js @@ -0,0 +1,989 @@ +/** + * @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. + */ + +/** + * @fileoverview JavaScript for Blockly's Block Factory application through + * which users can build blocks using a visual interface and dynamically + * generate a preview block and starter code for the block (block definition and + * generator stub. Uses the Block Factory namespace. + * + * @author fraser@google.com (Neil Fraser), quachtina96 (Tina Quach) + */ +'use strict'; + +/** + * Namespace for Block Factory. + */ +goog.provide('BlockFactory'); + +goog.require('goog.dom.classes'); + +/** + * Workspace for user to build block. + * @type {Blockly.Workspace} + */ +BlockFactory.mainWorkspace = null; + +/** + * Workspace for preview of block. + * @type {Blockly.Workspace} + */ +BlockFactory.previewWorkspace = null; + +/** + * Name of block if not named. + */ +BlockFactory.UNNAMED = 'unnamed'; + +/** + * Existing direction ('ltr' vs 'rtl') of preview. + */ +BlockFactory.oldDir = null; + +// UI + +/** + * Inject code into a pre tag, with syntax highlighting. + * Safe from HTML/script injection. + * @param {string} code Lines of code. + * @param {string} id ID of
     element to inject into.
    + */
    +BlockFactory.injectCode = function(code, id) {
    +  var pre = document.getElementById(id);
    +  pre.textContent = code;
    +  code = pre.innerHTML;
    +  code = prettyPrintOne(code, 'js');
    +  pre.innerHTML = code;
    +};
    +
    +// Utils
    +
    +/**
    + * Escape a string.
    + * @param {string} string String to escape.
    + * @return {string} Escaped string surrouned by quotes.
    + */
    +BlockFactory.escapeString = function(string) {
    +  return JSON.stringify(string);
    +};
    +
    +/**
    + * Return the uneditable container block that everything else attaches to in
    + * given workspace
    + *
    + * @param {!Blockly.Workspace} workspace - where the root block lives
    + * @return {Blockly.Block} root block
    + */
    +BlockFactory.getRootBlock = function(workspace) {
    +  var blocks = workspace.getTopBlocks(false);
    +  for (var i = 0, block; block = blocks[i]; i++) {
    +    if (block.type == 'factory_base') {
    +      return block;
    +    }
    +  }
    +  return null;
    +};
    +
    +// Language Code: Block Definitions
    +
    +/**
    + * Change the language code format.
    + */
    +BlockFactory.formatChange = function() {
    +  var mask = document.getElementById('blocklyMask');
    +  var languagePre = document.getElementById('languagePre');
    +  var languageTA = document.getElementById('languageTA');
    +  if (document.getElementById('format').value == 'Manual') {
    +    Blockly.hideChaff();
    +    mask.style.display = 'block';
    +    languagePre.style.display = 'none';
    +    languageTA.style.display = 'block';
    +    var code = languagePre.textContent.trim();
    +    languageTA.value = code;
    +    languageTA.focus();
    +    BlockFactory.updatePreview();
    +  } else {
    +    mask.style.display = 'none';
    +    languageTA.style.display = 'none';
    +    languagePre.style.display = 'block';
    +    BlockFactory.updateLanguage();
    +  }
    +  BlockFactory.disableEnableLink();
    +};
    +
    +/**
    + * Get block definition code for the current block.
    + *
    + * @param {string} blockType - Type of block.
    + * @param {!Blockly.Block} rootBlock - RootBlock from main workspace in which
    + *    user uses Block Factory Blocks to create a custom block.
    + * @param {string} format - 'JSON' or 'JavaScript'.
    + * @param {!Blockly.Workspace} workspace - Where the root block lives.
    + * @return {string} Block definition.
    + */
    +BlockFactory.getBlockDefinition = function(blockType, rootBlock, format, workspace) {
    +  blockType = blockType.replace(/\W/g, '_').replace(/^(\d)/, '_\\1');
    +  switch (format) {
    +    case 'JSON':
    +      var code = BlockFactory.formatJson_(blockType, rootBlock);
    +      break;
    +    case 'JavaScript':
    +      var code = BlockFactory.formatJavaScript_(blockType, rootBlock, workspace);
    +      break;
    +  }
    +  return code;
    +};
    +
    +/**
    + * Update the language code based on constructs made in Blockly.
    + */
    +BlockFactory.updateLanguage = function() {
    +  var rootBlock = BlockFactory.getRootBlock(BlockFactory.mainWorkspace);
    +  if (!rootBlock) {
    +    return;
    +  }
    +  var blockType = rootBlock.getFieldValue('NAME').trim().toLowerCase();
    +  if (!blockType) {
    +    blockType = BlockFactory.UNNAMED;
    +  }
    +  var format = document.getElementById('format').value;
    +  var code = BlockFactory.getBlockDefinition(blockType, rootBlock, format,
    +      BlockFactory.mainWorkspace);
    +  BlockFactory.injectCode(code, 'languagePre');
    +  BlockFactory.updatePreview();
    +};
    +
    +/**
    + * Update the language code as JSON.
    + * @param {string} blockType Name of block.
    + * @param {!Blockly.Block} rootBlock Factory_base block.
    + * @return {string} Generanted language code.
    + * @private
    + */
    +BlockFactory.formatJson_ = function(blockType, rootBlock) {
    +  var JS = {};
    +  // Type is not used by Blockly, but may be used by a loader.
    +  JS.type = blockType;
    +  // Generate inputs.
    +  var message = [];
    +  var args = [];
    +  var contentsBlock = rootBlock.getInputTargetBlock('INPUTS');
    +  var lastInput = null;
    +  while (contentsBlock) {
    +    if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) {
    +      var fields = BlockFactory.getFieldsJson_(
    +          contentsBlock.getInputTargetBlock('FIELDS'));
    +      for (var i = 0; i < fields.length; i++) {
    +        if (typeof fields[i] == 'string') {
    +          message.push(fields[i].replace(/%/g, '%%'));
    +        } else {
    +          args.push(fields[i]);
    +          message.push('%' + args.length);
    +        }
    +      }
    +
    +      var input = {type: contentsBlock.type};
    +      // Dummy inputs don't have names.  Other inputs do.
    +      if (contentsBlock.type != 'input_dummy') {
    +        input.name = contentsBlock.getFieldValue('INPUTNAME');
    +      }
    +      var check = JSON.parse(
    +          BlockFactory.getOptTypesFrom(contentsBlock, 'TYPE') || 'null');
    +      if (check) {
    +        input.check = check;
    +      }
    +      var align = contentsBlock.getFieldValue('ALIGN');
    +      if (align != 'LEFT') {
    +        input.align = align;
    +      }
    +      args.push(input);
    +      message.push('%' + args.length);
    +      lastInput = contentsBlock;
    +    }
    +    contentsBlock = contentsBlock.nextConnection &&
    +        contentsBlock.nextConnection.targetBlock();
    +  }
    +  // Remove last input if dummy and not empty.
    +  if (lastInput && lastInput.type == 'input_dummy') {
    +    var fields = lastInput.getInputTargetBlock('FIELDS');
    +    if (fields && BlockFactory.getFieldsJson_(fields).join('').trim() != '') {
    +      var align = lastInput.getFieldValue('ALIGN');
    +      if (align != 'LEFT') {
    +        JS.lastDummyAlign0 = align;
    +      }
    +      args.pop();
    +      message.pop();
    +    }
    +  }
    +  JS.message0 = message.join(' ');
    +  if (args.length) {
    +    JS.args0 = args;
    +  }
    +  // Generate inline/external switch.
    +  if (rootBlock.getFieldValue('INLINE') == 'EXT') {
    +    JS.inputsInline = false;
    +  } else if (rootBlock.getFieldValue('INLINE') == 'INT') {
    +    JS.inputsInline = true;
    +  }
    +  // Generate output, or next/previous connections.
    +  switch (rootBlock.getFieldValue('CONNECTIONS')) {
    +    case 'LEFT':
    +      JS.output =
    +          JSON.parse(
    +              BlockFactory.getOptTypesFrom(rootBlock, 'OUTPUTTYPE') || 'null');
    +      break;
    +    case 'BOTH':
    +      JS.previousStatement =
    +          JSON.parse(
    +              BlockFactory.getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null');
    +      JS.nextStatement =
    +          JSON.parse(
    +              BlockFactory.getOptTypesFrom(rootBlock, 'BOTTOMTYPE') || 'null');
    +      break;
    +    case 'TOP':
    +      JS.previousStatement =
    +          JSON.parse(
    +              BlockFactory.getOptTypesFrom(rootBlock, 'TOPTYPE') || 'null');
    +      break;
    +    case 'BOTTOM':
    +      JS.nextStatement =
    +          JSON.parse(
    +              BlockFactory.getOptTypesFrom(rootBlock, 'BOTTOMTYPE') || 'null');
    +      break;
    +  }
    +  // Generate colour.
    +  var colourBlock = rootBlock.getInputTargetBlock('COLOUR');
    +  if (colourBlock && !colourBlock.disabled) {
    +    var hue = parseInt(colourBlock.getFieldValue('HUE'), 10);
    +    JS.colour = hue;
    +  }
    +  JS.tooltip = '';
    +  JS.helpUrl = 'http://www.example.com/';
    +  return JSON.stringify(JS, null, '  ');
    +};
    +
    +/**
    + * Update the language code as JavaScript.
    + * @param {string} blockType Name of block.
    + * @param {!Blockly.Block} rootBlock Factory_base block.
    + * @param {!Blockly.Workspace} workspace - Where the root block lives.
    +
    + * @return {string} Generated language code.
    + * @private
    + */
    +BlockFactory.formatJavaScript_ = function(blockType, rootBlock, workspace) {
    +  var code = [];
    +  code.push("Blockly.Blocks['" + blockType + "'] = {");
    +  code.push("  init: function() {");
    +  // Generate inputs.
    +  var TYPES = {'input_value': 'appendValueInput',
    +               'input_statement': 'appendStatementInput',
    +               'input_dummy': 'appendDummyInput'};
    +  var contentsBlock = rootBlock.getInputTargetBlock('INPUTS');
    +  while (contentsBlock) {
    +    if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) {
    +      var name = '';
    +      // Dummy inputs don't have names.  Other inputs do.
    +      if (contentsBlock.type != 'input_dummy') {
    +        name =
    +            BlockFactory.escapeString(contentsBlock.getFieldValue('INPUTNAME'));
    +      }
    +      code.push('    this.' + TYPES[contentsBlock.type] + '(' + name + ')');
    +      var check = BlockFactory.getOptTypesFrom(contentsBlock, 'TYPE');
    +      if (check) {
    +        code.push('        .setCheck(' + check + ')');
    +      }
    +      var align = contentsBlock.getFieldValue('ALIGN');
    +      if (align != 'LEFT') {
    +        code.push('        .setAlign(Blockly.ALIGN_' + align + ')');
    +      }
    +      var fields = BlockFactory.getFieldsJs_(
    +          contentsBlock.getInputTargetBlock('FIELDS'));
    +      for (var i = 0; i < fields.length; i++) {
    +        code.push('        .appendField(' + fields[i] + ')');
    +      }
    +      // Add semicolon to last line to finish the statement.
    +      code[code.length - 1] += ';';
    +    }
    +    contentsBlock = contentsBlock.nextConnection &&
    +        contentsBlock.nextConnection.targetBlock();
    +  }
    +  // Generate inline/external switch.
    +  if (rootBlock.getFieldValue('INLINE') == 'EXT') {
    +    code.push('    this.setInputsInline(false);');
    +  } else if (rootBlock.getFieldValue('INLINE') == 'INT') {
    +    code.push('    this.setInputsInline(true);');
    +  }
    +  // Generate output, or next/previous connections.
    +  switch (rootBlock.getFieldValue('CONNECTIONS')) {
    +    case 'LEFT':
    +      code.push(BlockFactory.connectionLineJs_('setOutput', 'OUTPUTTYPE', workspace));
    +      break;
    +    case 'BOTH':
    +      code.push(
    +          BlockFactory.connectionLineJs_('setPreviousStatement', 'TOPTYPE', workspace));
    +      code.push(
    +          BlockFactory.connectionLineJs_('setNextStatement', 'BOTTOMTYPE', workspace));
    +      break;
    +    case 'TOP':
    +      code.push(
    +          BlockFactory.connectionLineJs_('setPreviousStatement', 'TOPTYPE', workspace));
    +      break;
    +    case 'BOTTOM':
    +      code.push(
    +          BlockFactory.connectionLineJs_('setNextStatement', 'BOTTOMTYPE', workspace));
    +      break;
    +  }
    +  // Generate colour.
    +  var colourBlock = rootBlock.getInputTargetBlock('COLOUR');
    +  if (colourBlock && !colourBlock.disabled) {
    +    var hue = parseInt(colourBlock.getFieldValue('HUE'), 10);
    +    if (!isNaN(hue)) {
    +      code.push('    this.setColour(' + hue + ');');
    +    }
    +  }
    +  code.push("    this.setTooltip('');");
    +  code.push("    this.setHelpUrl('http://www.example.com/');");
    +  code.push('  }');
    +  code.push('};');
    +  return code.join('\n');
    +};
    +
    +/**
    + * Create JS code required to create a top, bottom, or value connection.
    + * @param {string} functionName JavaScript function name.
    + * @param {string} typeName Name of type input.
    + * @param {!Blockly.Workspace} workspace - Where the root block lives.
    + * @return {string} Line of JavaScript code to create connection.
    + * @private
    + */
    +BlockFactory.connectionLineJs_ = function(functionName, typeName, workspace) {
    +  var type = BlockFactory.getOptTypesFrom(
    +      BlockFactory.getRootBlock(workspace), typeName);
    +  if (type) {
    +    type = ', ' + type;
    +  } else {
    +    type = '';
    +  }
    +  return '    this.' + functionName + '(true' + type + ');';
    +};
    +
    +/**
    + * Returns field strings and any config.
    + * @param {!Blockly.Block} block Input block.
    + * @return {!Array.} Field strings.
    + * @private
    + */
    +BlockFactory.getFieldsJs_ = function(block) {
    +  var fields = [];
    +  while (block) {
    +    if (!block.disabled && !block.getInheritedDisabled()) {
    +      switch (block.type) {
    +        case 'field_static':
    +          // Result: 'hello'
    +          fields.push(BlockFactory.escapeString(block.getFieldValue('TEXT')));
    +          break;
    +        case 'field_input':
    +          // Result: new Blockly.FieldTextInput('Hello'), 'GREET'
    +          fields.push('new Blockly.FieldTextInput(' +
    +              BlockFactory.escapeString(block.getFieldValue('TEXT')) + '), ' +
    +              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
    +          break;
    +        case 'field_number':
    +          // Result: new Blockly.FieldNumber(10, 0, 100, 1), 'NUMBER'
    +          var args = [
    +            Number(block.getFieldValue('VALUE')),
    +            Number(block.getFieldValue('MIN')),
    +            Number(block.getFieldValue('MAX')),
    +            Number(block.getFieldValue('PRECISION'))
    +          ];
    +          // Remove any trailing arguments that aren't needed.
    +          if (args[3] == 0) {
    +            args.pop();
    +            if (args[2] == Infinity) {
    +              args.pop();
    +              if (args[1] == -Infinity) {
    +                args.pop();
    +              }
    +            }
    +          }
    +          fields.push('new Blockly.FieldNumber(' + args.join(', ') + '), ' +
    +              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
    +          break;
    +        case 'field_angle':
    +          // Result: new Blockly.FieldAngle(90), 'ANGLE'
    +          fields.push('new Blockly.FieldAngle(' +
    +              parseFloat(block.getFieldValue('ANGLE')) + '), ' +
    +              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
    +          break;
    +        case 'field_checkbox':
    +          // Result: new Blockly.FieldCheckbox('TRUE'), 'CHECK'
    +          fields.push('new Blockly.FieldCheckbox(' +
    +              BlockFactory.escapeString(block.getFieldValue('CHECKED')) +
    +               '), ' +
    +              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
    +          break;
    +        case 'field_colour':
    +          // Result: new Blockly.FieldColour('#ff0000'), 'COLOUR'
    +          fields.push('new Blockly.FieldColour(' +
    +              BlockFactory.escapeString(block.getFieldValue('COLOUR')) +
    +              '), ' +
    +              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
    +          break;
    +        case 'field_date':
    +          // Result: new Blockly.FieldDate('2015-02-04'), 'DATE'
    +          fields.push('new Blockly.FieldDate(' +
    +              BlockFactory.escapeString(block.getFieldValue('DATE')) + '), ' +
    +              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
    +          break;
    +        case 'field_variable':
    +          // Result: new Blockly.FieldVariable('item'), 'VAR'
    +          var varname
    +              = BlockFactory.escapeString(block.getFieldValue('TEXT') || null);
    +          fields.push('new Blockly.FieldVariable(' + varname + '), ' +
    +              BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
    +          break;
    +        case 'field_dropdown':
    +          // Result:
    +          // new Blockly.FieldDropdown([['yes', '1'], ['no', '0']]), 'TOGGLE'
    +          var options = [];
    +          for (var i = 0; i < block.optionCount_; i++) {
    +            options[i] = '[' +
    +                BlockFactory.escapeString(block.getFieldValue('USER' + i)) +
    +                ', ' +
    +                BlockFactory.escapeString(block.getFieldValue('CPU' + i)) + ']';
    +          }
    +          if (options.length) {
    +            fields.push('new Blockly.FieldDropdown([' +
    +                options.join(', ') + ']), ' +
    +                BlockFactory.escapeString(block.getFieldValue('FIELDNAME')));
    +          }
    +          break;
    +        case 'field_image':
    +          // Result: new Blockly.FieldImage('http://...', 80, 60)
    +          var src = BlockFactory.escapeString(block.getFieldValue('SRC'));
    +          var width = Number(block.getFieldValue('WIDTH'));
    +          var height = Number(block.getFieldValue('HEIGHT'));
    +          var alt = BlockFactory.escapeString(block.getFieldValue('ALT'));
    +          fields.push('new Blockly.FieldImage(' +
    +              src + ', ' + width + ', ' + height + ', ' + alt + ')');
    +          break;
    +      }
    +    }
    +    block = block.nextConnection && block.nextConnection.targetBlock();
    +  }
    +  return fields;
    +};
    +
    +/**
    + * Returns field strings and any config.
    + * @param {!Blockly.Block} block Input block.
    + * @return {!Array.} Array of static text and field configs.
    + * @private
    + */
    +BlockFactory.getFieldsJson_ = function(block) {
    +  var fields = [];
    +  while (block) {
    +    if (!block.disabled && !block.getInheritedDisabled()) {
    +      switch (block.type) {
    +        case 'field_static':
    +          // Result: 'hello'
    +          fields.push(block.getFieldValue('TEXT'));
    +          break;
    +        case 'field_input':
    +          fields.push({
    +            type: block.type,
    +            name: block.getFieldValue('FIELDNAME'),
    +            text: block.getFieldValue('TEXT')
    +          });
    +          break;
    +        case 'field_number':
    +          var obj = {
    +            type: block.type,
    +            name: block.getFieldValue('FIELDNAME'),
    +            value: parseFloat(block.getFieldValue('VALUE'))
    +          };
    +          var min = parseFloat(block.getFieldValue('MIN'));
    +          if (min > -Infinity) {
    +            obj.min = min;
    +          }
    +          var max = parseFloat(block.getFieldValue('MAX'));
    +          if (max < Infinity) {
    +            obj.max = max;
    +          }
    +          var precision = parseFloat(block.getFieldValue('PRECISION'));
    +          if (precision) {
    +            obj.precision = precision;
    +          }
    +          fields.push(obj);
    +          break;
    +        case 'field_angle':
    +          fields.push({
    +            type: block.type,
    +            name: block.getFieldValue('FIELDNAME'),
    +            angle: Number(block.getFieldValue('ANGLE'))
    +          });
    +          break;
    +        case 'field_checkbox':
    +          fields.push({
    +            type: block.type,
    +            name: block.getFieldValue('FIELDNAME'),
    +            checked: block.getFieldValue('CHECKED') == 'TRUE'
    +          });
    +          break;
    +        case 'field_colour':
    +          fields.push({
    +            type: block.type,
    +            name: block.getFieldValue('FIELDNAME'),
    +            colour: block.getFieldValue('COLOUR')
    +          });
    +          break;
    +        case 'field_date':
    +          fields.push({
    +            type: block.type,
    +            name: block.getFieldValue('FIELDNAME'),
    +            date: block.getFieldValue('DATE')
    +          });
    +          break;
    +        case 'field_variable':
    +          fields.push({
    +            type: block.type,
    +            name: block.getFieldValue('FIELDNAME'),
    +            variable: block.getFieldValue('TEXT') || null
    +          });
    +          break;
    +        case 'field_dropdown':
    +          var options = [];
    +          for (var i = 0; i < block.optionCount_; i++) {
    +            options[i] = [block.getFieldValue('USER' + i),
    +                block.getFieldValue('CPU' + i)];
    +          }
    +          if (options.length) {
    +            fields.push({
    +              type: block.type,
    +              name: block.getFieldValue('FIELDNAME'),
    +              options: options
    +            });
    +          }
    +          break;
    +        case 'field_image':
    +          fields.push({
    +            type: block.type,
    +            src: block.getFieldValue('SRC'),
    +            width: Number(block.getFieldValue('WIDTH')),
    +            height: Number(block.getFieldValue('HEIGHT')),
    +            alt: block.getFieldValue('ALT')
    +          });
    +          break;
    +      }
    +    }
    +    block = block.nextConnection && block.nextConnection.targetBlock();
    +  }
    +  return fields;
    +};
    +
    +/**
    + * Fetch the type(s) defined in the given input.
    + * Format as a string for appending to the generated code.
    + * @param {!Blockly.Block} block Block with input.
    + * @param {string} name Name of the input.
    + * @return {?string} String defining the types.
    + */
    +BlockFactory.getOptTypesFrom = function(block, name) {
    +  var types = BlockFactory.getTypesFrom_(block, name);
    +  if (types.length == 0) {
    +    return undefined;
    +  } else if (types.indexOf('null') != -1) {
    +    return 'null';
    +  } else if (types.length == 1) {
    +    return types[0];
    +  } else {
    +    return '[' + types.join(', ') + ']';
    +  }
    +};
    +
    +/**
    + * Fetch the type(s) defined in the given input.
    + * @param {!Blockly.Block} block Block with input.
    + * @param {string} name Name of the input.
    + * @return {!Array.} List of types.
    + * @private
    + */
    +BlockFactory.getTypesFrom_ = function(block, name) {
    +  var typeBlock = block.getInputTargetBlock(name);
    +  var types;
    +  if (!typeBlock || typeBlock.disabled) {
    +    types = [];
    +  } else if (typeBlock.type == 'type_other') {
    +    types = [BlockFactory.escapeString(typeBlock.getFieldValue('TYPE'))];
    +  } else if (typeBlock.type == 'type_group') {
    +    types = [];
    +    for (var n = 0; n < typeBlock.typeCount_; n++) {
    +      types = types.concat(BlockFactory.getTypesFrom_(typeBlock, 'TYPE' + n));
    +    }
    +    // Remove duplicates.
    +    var hash = Object.create(null);
    +    for (var n = types.length - 1; n >= 0; n--) {
    +      if (hash[types[n]]) {
    +        types.splice(n, 1);
    +      }
    +      hash[types[n]] = true;
    +    }
    +  } else {
    +    types = [BlockFactory.escapeString(typeBlock.valueType)];
    +  }
    +  return types;
    +};
    +
    +// Generator Code
    +
    +/**
    + * Get the generator code for a given block.
    + *
    + * @param {!Blockly.Block} block - Rendered block in preview workspace.
    + * @param {string} generatorLanguage - 'JavaScript', 'Python', 'PHP', 'Lua',
    + *     'Dart'.
    + * @return {string} Generator code for multiple blocks.
    + */
    +BlockFactory.getGeneratorStub = function(block, generatorLanguage) {
    +  function makeVar(root, name) {
    +    name = name.toLowerCase().replace(/\W/g, '_');
    +    return '  var ' + root + '_' + name;
    +  }
    +  // The makevar function lives in the original update generator.
    +  var language = generatorLanguage;
    +  var code = [];
    +  code.push("Blockly." + language + "['" + block.type +
    +            "'] = function(block) {");
    +
    +  // Generate getters for any fields or inputs.
    +  for (var i = 0, input; input = block.inputList[i]; i++) {
    +    for (var j = 0, field; field = input.fieldRow[j]; j++) {
    +      var name = field.name;
    +      if (!name) {
    +        continue;
    +      }
    +      if (field instanceof Blockly.FieldVariable) {
    +        // Subclass of Blockly.FieldDropdown, must test first.
    +        code.push(makeVar('variable', name) +
    +                  " = Blockly." + language +
    +                  ".variableDB_.getName(block.getFieldValue('" + name +
    +                  "'), Blockly.Variables.NAME_TYPE);");
    +      } else if (field instanceof Blockly.FieldAngle) {
    +        // Subclass of Blockly.FieldTextInput, must test first.
    +        code.push(makeVar('angle', name) +
    +                  " = block.getFieldValue('" + name + "');");
    +      } else if (Blockly.FieldDate && field instanceof Blockly.FieldDate) {
    +        // Blockly.FieldDate may not be compiled into Blockly.
    +        code.push(makeVar('date', name) +
    +                  " = block.getFieldValue('" + name + "');");
    +      } else if (field instanceof Blockly.FieldColour) {
    +        code.push(makeVar('colour', name) +
    +                  " = block.getFieldValue('" + name + "');");
    +      } else if (field instanceof Blockly.FieldCheckbox) {
    +        code.push(makeVar('checkbox', name) +
    +                  " = block.getFieldValue('" + name + "') == 'TRUE';");
    +      } else if (field instanceof Blockly.FieldDropdown) {
    +        code.push(makeVar('dropdown', name) +
    +                  " = block.getFieldValue('" + name + "');");
    +      } else if (field instanceof Blockly.FieldNumber) {
    +        code.push(makeVar('number', name) +
    +                  " = block.getFieldValue('" + name + "');");
    +      } else if (field instanceof Blockly.FieldTextInput) {
    +        code.push(makeVar('text', name) +
    +                  " = block.getFieldValue('" + name + "');");
    +      }
    +    }
    +    var name = input.name;
    +    if (name) {
    +      if (input.type == Blockly.INPUT_VALUE) {
    +        code.push(makeVar('value', name) +
    +                  " = Blockly." + language + ".valueToCode(block, '" + name +
    +                  "', Blockly." + language + ".ORDER_ATOMIC);");
    +      } else if (input.type == Blockly.NEXT_STATEMENT) {
    +        code.push(makeVar('statements', name) +
    +                  " = Blockly." + language + ".statementToCode(block, '" +
    +                  name + "');");
    +      }
    +    }
    +  }
    +  // Most languages end lines with a semicolon.  Python does not.
    +  var lineEnd = {
    +    'JavaScript': ';',
    +    'Python': '',
    +    'PHP': ';',
    +    'Dart': ';'
    +  };
    +  code.push("  // TODO: Assemble " + language + " into code variable.");
    +  if (block.outputConnection) {
    +    code.push("  var code = '...';");
    +    code.push("  // TODO: Change ORDER_NONE to the correct strength.");
    +    code.push("  return [code, Blockly." + language + ".ORDER_NONE];");
    +  } else {
    +    code.push("  var code = '..." + (lineEnd[language] || '') + "\\n';");
    +    code.push("  return code;");
    +  }
    +  code.push("};");
    +
    +  return code.join('\n');
    +};
    +
    +/**
    + * Update the generator code.
    + * @param {!Blockly.Block} block Rendered block in preview workspace.
    + */
    +BlockFactory.updateGenerator = function(block) {
    +  var language = document.getElementById('language').value;
    +  var generatorStub = BlockFactory.getGeneratorStub(block, language);
    +  BlockFactory.injectCode(generatorStub, 'generatorPre');
    +};
    +
    +// Preview Block
    +
    +/**
    + * Update the preview display.
    + */
    +BlockFactory.updatePreview = function() {
    +  // Toggle between LTR/RTL if needed (also used in first display).
    +  var newDir = document.getElementById('direction').value;
    +  if (BlockFactory.oldDir != newDir) {
    +    if (BlockFactory.previewWorkspace) {
    +      BlockFactory.previewWorkspace.dispose();
    +    }
    +    var rtl = newDir == 'rtl';
    +    BlockFactory.previewWorkspace = Blockly.inject('preview',
    +        {rtl: rtl,
    +         media: '../../media/',
    +         scrollbars: true});
    +    BlockFactory.oldDir = newDir;
    +  }
    +  BlockFactory.previewWorkspace.clear();
    +
    +  // Fetch the code and determine its format (JSON or JavaScript).
    +  var format = document.getElementById('format').value;
    +  if (format == 'Manual') {
    +    var code = document.getElementById('languageTA').value;
    +    // If the code is JSON, it will parse, otherwise treat as JS.
    +    try {
    +      JSON.parse(code);
    +      format = 'JSON';
    +    } catch (e) {
    +      format = 'JavaScript';
    +    }
    +  } else {
    +    var code = document.getElementById('languagePre').textContent;
    +  }
    +  if (!code.trim()) {
    +    // Nothing to render.  Happens while cloud storage is loading.
    +    return;
    +  }
    +
    +  // Backup Blockly.Blocks object so that main workspace and preview don't
    +  // collide if user creates a 'factory_base' block, for instance.
    +  var backupBlocks = Blockly.Blocks;
    +  console.log(backupBlocks);
    +  try {
    +    // Make a shallow copy.
    +    Blockly.Blocks = Object.create(null);
    +    for (var prop in backupBlocks) {
    +      Blockly.Blocks[prop] = backupBlocks[prop];
    +    }
    +
    +    if (format == 'JSON') {
    +      var json = JSON.parse(code);
    +      Blockly.Blocks[json.type || BlockFactory.UNNAMED] = {
    +        init: function() {
    +          this.jsonInit(json);
    +        }
    +      };
    +    } else if (format == 'JavaScript') {
    +      eval(code);
    +    } else {
    +      throw 'Unknown format: ' + format;
    +    }
    +
    +    // Look for a block on Blockly.Blocks that does not match the backup.
    +    var blockType = null;
    +    console.log('Blockly Blocks types');
    +    for (var type in Blockly.Blocks) {
    +      console.log(type);
    +      if (typeof Blockly.Blocks[type].init == 'function' &&
    +          Blockly.Blocks[type] != backupBlocks[type]) {
    +        blockType = type;
    +        console.log('found non matching type');
    +        console.log(blockType);
    +        break;
    +      }
    +    }
    +    if (!blockType) {
    +      console.log('non matching type NOT FOUND');
    +      return;
    +    }
    +
    +    // Create the preview block.
    +    var previewBlock = BlockFactory.previewWorkspace.newBlock(blockType);
    +    previewBlock.initSvg();
    +    previewBlock.render();
    +    previewBlock.setMovable(false);
    +    previewBlock.setDeletable(false);
    +    previewBlock.moveBy(15, 10);
    +    BlockFactory.previewWorkspace.clearUndo();
    +    BlockFactory.updateGenerator(previewBlock);
    +  } finally {
    +    Blockly.Blocks = backupBlocks;
    +  }
    +};
    +
    +// File Import, Creation, Download
    +
    +/**
    + * Generate a file from the contents of a given text area and
    + * download that file.
    + * @param {string} filename The name of the file to create.
    + * @param {string} id The text area to download.
    +*/
    +BlockFactory.downloadTextArea = function(filename, id) {
    +  var code = document.getElementById(id).textContent;
    +  BlockFactory.createAndDownloadFile_(code, filename, 'plain');
    +};
    +
    +/**
    + * Create a file with the given attributes and download it.
    + * @param {string} contents - The contents of the file.
    + * @param {string} filename - The name of the file to save to.
    + * @param {string} fileType - The type of the file to save.
    + * @private
    + */
    +BlockFactory.createAndDownloadFile_ = function(contents, filename, fileType) {
    +  var data = new Blob([contents], {type: 'text/' + fileType});
    +  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);
    +};
    +
    +/**
    + * Save the workspace's xml representation to a file.
    + * @private
    + */
    +BlockFactory.saveWorkspaceToFile = function() {
    +  var xmlElement = Blockly.Xml.workspaceToDom(BlockFactory.mainWorkspace);
    +  var prettyXml = Blockly.Xml.domToPrettyText(xmlElement);
    +  BlockFactory.createAndDownloadFile_(prettyXml, 'blockXml', 'xml');
    +};
    +
    +/**
    + * Imports xml file for a block to the workspace.
    + */
    +BlockFactory.importBlockFromFile = function() {
    +  var files = document.getElementById('files');
    +  // If the file list is empty, they user likely canceled in the dialog.
    +  if (files.files.length > 0) {
    +    // The input tag doesn't have the "mulitple" attribute
    +    // so the user can only choose 1 file.
    +    var file = files.files[0];
    +    var fileReader = new FileReader();
    +    fileReader.addEventListener('load', function(event) {
    +      var fileContents = event.target.result;
    +      var xml = '';
    +      try {
    +        xml = Blockly.Xml.textToDom(fileContents);
    +      } catch (e) {
    +        var message = 'Could not load your saved file.\n'+
    +          'Perhaps it was created with a different version of Blockly?';
    +        window.alert(message + '\nXML: ' + fileContents);
    +        return;
    +      }
    +      BlockFactory.mainWorkspace.clear();
    +      Blockly.Xml.domToWorkspace(xml, BlockFactory.mainWorkspace);
    +    });
    +
    +    fileReader.readAsText(file);
    +  }
    +};
    +
    +/**
    + * Disable link and save buttons if the format is 'Manual', enable otherwise.
    + */
    +BlockFactory.disableEnableLink = function() {
    +  var linkButton = document.getElementById('linkButton');
    +  var saveBlockButton = document.getElementById('localSaveButton');
    +  var saveToLibButton = document.getElementById('saveToBlockLibraryButton');
    +  var disabled = document.getElementById('format').value == 'Manual';
    +  linkButton.disabled = disabled;
    +  saveBlockButton.disabled = disabled;
    +  saveToLibButton.disabled = disabled;
    +};
    +
    +// Block Factory Expansion View Utils
    +
    +/**
    + * Render starter block (factory_base).
    + */
    + BlockFactory.showStarterBlock = function() {
    +    var xml = '';
    +    Blockly.Xml.domToWorkspace(
    +        Blockly.Xml.textToDom(xml), BlockFactory.mainWorkspace);
    +};
    +
    +/**
    + * Hides element so that it's invisible and doesn't take up space.
    + *
    + * @param {string} elementID - ID of element to hide.
    + */
    +BlockFactory.hide = function(elementID) {
    +  document.getElementById(elementID).style.display = 'none';
    +};
    +
    +/**
    + * Un-hides an element.
    + *
    + * @param {string} elementID - ID of element to hide.
    + */
    +BlockFactory.show = function(elementID) {
    +  document.getElementById(elementID).style.display = 'block';
    +};
    +
    +/**
    + * Hides element so that it's invisible but still takes up space.
    + *
    + * @param {string} elementID - ID of element to hide.
    + */
    +BlockFactory.makeInvisible = function(elementID) {
    +  document.getElementById(elementID).visibility = 'hidden';
    +};
    +
    +/**
    + * Makes element visible.
    + *
    + * @param {string} elementID - ID of element to hide.
    + */
    +BlockFactory.makeVisible = function(elementID) {
    +  document.getElementById(elementID).visibility = 'visible';
    +};
    +
    diff --git a/demos/blocklyfactory/index.html b/demos/blocklyfactory/index.html
    new file mode 100644
    index 000000000..4ebe83837
    --- /dev/null
    +++ b/demos/blocklyfactory/index.html
    @@ -0,0 +1,264 @@
    +
    +
    +
    +
    +
    +  
    +  
    +  Blockly Demo: Blockly Factory
    +  
    +  
    +  
    +  
    +  
    +  
    +  
    +  
    +  
    +  
    +  
    +  
    +  
    +  
    +  
    +  
    +  
    +
    +
    +  

    Blockly > + Demos > Blockly Factory

    + +
    +
    Block Factory
    +
    +
    Block Library Exporter
    +
    + +
    +
    + + +
    +
    +

    Drag blocks into your workspace to select them for download.

    +
    + +
    + +
    +

    Block Export Settings

    +
    +
    + Download Block Definition: +
    + Language code: +
    + Block Definition(s) File Name:
    +
    +
    + Download Generator Stubs: +
    +
    + Block Generator Stub(s) File Name:
    +
    +
    +
    + +
    +
    + + + + + + + + + +
    + + + + + +
    + +

    Block Library:

    + +
    +
    + + + +
    +
    + + + + + +
    +

    Preview: + +

    +
    + + + + + + + +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + +
    +
    +
    +

    Language code: + + +

    +
    +
    
    +              
    +            
    +

    Generator stub: + + +

    +
    +
    
    +            
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 20 + 65 + 120 + 160 + 210 + 230 + 260 + 290 + 330 + + + + \ No newline at end of file diff --git a/demos/blocklyfactory/link.png b/demos/blocklyfactory/link.png new file mode 100644 index 0000000000000000000000000000000000000000..11dfd82845e582b4272969694e305b71226546ff GIT binary patch literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^q9Dw{1|(OCFP#RY*pj^6T^Rm@;DWu&Cj&(|3p^r= z85p>QL70(Y)*K0-AbW|YuPggKc0N&VvE7#n?*fGiJzX3_EKVmUNU$z$NLw(!vGJk3 zRNeUl2Lu|Hs4-}@ty?!gf_WluTjTeND+hVgS}y%xxJ8kf`7o=-?Z$@ld=8n+Y;0|x zGoBsf{oB9nr9ESh?^-5iAmE<0gmIF$Y-bNQv&uK-S*@DB$@L;VOj?W#_t@Nv|4M%| Q16t1D>FVdQ&MBb@0ONT`aR2}S literal 0 HcmV?d00001 From 8211bb30f408eaf892bfc8a5adbf0dfabfdc8257 Mon Sep 17 00:00:00 2001 From: Emma Dauterman Date: Wed, 10 Aug 2016 11:03:11 -0700 Subject: [PATCH 06/10] Workspace Factory (#522) Workspace factory helps developers configure their workspace by allowing them to drag blocks into the workspace to add them to their toolbox. Current features: supports categories or a single flyout of blocks updates a preview workspace automatically imports toolbox XML already written exports toolbox XML to a file prints toolbox XML to the console imports a standard Blockly category supports shadow blocks (allowing the user to move shadow blocks and toggle between shadow blocks and normal blocks), disabled blocks, block groups allows the user to add/move/delete/rename/color categories and separators. --- .../workspacefactory/index.html | 682 +++++++++++++++++ .../workspacefactory/standard_categories.js | 375 ++++++++++ .../blocklyfactory/workspacefactory/style.css | 238 ++++++ .../workspacefactory/wfactory_controller.js | 704 ++++++++++++++++++ .../workspacefactory/wfactory_generator.js | 159 ++++ .../workspacefactory/wfactory_model.js | 450 +++++++++++ .../workspacefactory/wfactory_view.js | 341 +++++++++ 7 files changed, 2949 insertions(+) create mode 100644 demos/blocklyfactory/workspacefactory/index.html create mode 100644 demos/blocklyfactory/workspacefactory/standard_categories.js create mode 100644 demos/blocklyfactory/workspacefactory/style.css create mode 100644 demos/blocklyfactory/workspacefactory/wfactory_controller.js create mode 100644 demos/blocklyfactory/workspacefactory/wfactory_generator.js create mode 100644 demos/blocklyfactory/workspacefactory/wfactory_model.js create mode 100644 demos/blocklyfactory/workspacefactory/wfactory_view.js diff --git a/demos/blocklyfactory/workspacefactory/index.html b/demos/blocklyfactory/workspacefactory/index.html new file mode 100644 index 000000000..061749f59 --- /dev/null +++ b/demos/blocklyfactory/workspacefactory/index.html @@ -0,0 +1,682 @@ + + +Blockly Workspace Factory + + + + + + + + + + + + + + + + + + + + + +
    +

    Blockly‏ > + Demos‏ > + Workspace Factory +

    +
    +

    + + + + + +

    +
    + +
    +

    Drag blocks into your toolbox:

    +
    +
    +
    +
    + +
    + + + + + + diff --git a/demos/blocklyfactory/workspacefactory/standard_categories.js b/demos/blocklyfactory/workspacefactory/standard_categories.js new file mode 100644 index 000000000..5112c5ee2 --- /dev/null +++ b/demos/blocklyfactory/workspacefactory/standard_categories.js @@ -0,0 +1,375 @@ +/** + * @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. + */ + +/** + * @fileoverview Contains a map of standard Blockly categories used to load + * standard Blockly categories into the user's toolbox. The map is keyed by + * the lower case name of the category, and contains the Category object for + * that particular category. + * + * @author Emma Dauterman (evd2014) + */ + +FactoryController.prototype.standardCategories = Object.create(null); + +FactoryController.prototype.standardCategories['logic'] = + new ListElement(ListElement.TYPE_CATEGORY, 'Logic'); +FactoryController.prototype.standardCategories['logic'].xml = + Blockly.Xml.textToDom( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + ''); +FactoryController.prototype.standardCategories['logic'].color = '#5C81A6'; + +FactoryController.prototype.standardCategories['loops'] = + new ListElement(ListElement.TYPE_CATEGORY, 'Loops'); +FactoryController.prototype.standardCategories['loops'].xml = + Blockly.Xml.textToDom( + '' + + '' + + '' + + '' + + '10' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '10' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '' + + ''); +FactoryController.prototype.standardCategories['loops'].color = '#5CA65C'; + +FactoryController.prototype.standardCategories['math'] = + new ListElement(ListElement.TYPE_CATEGORY, 'Math'); +FactoryController.prototype.standardCategories['math'].xml = + Blockly.Xml.textToDom( + '' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '' + + '' + + '9' + + '' + + '' + + '' + + '' + + '' + + '' + + '45' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '0' + + '' + + '' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '' + + '' + + '3.1' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '64' + + '' + + '' + + '' + + '' + + '10'+ + '' + + '' + + '' + + '' + + '' + + '' + + '50' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '100' + + '' + + '' + + '' + + '' + + '' + + '' + + '1' + + '' + + '' + + '' + + '' + + '100' + + '' + + '' + + '' + + '' + + ''); +FactoryController.prototype.standardCategories['math'].color = '#5C68A6'; + +FactoryController.prototype.standardCategories['text'] = + new ListElement(ListElement.TYPE_CATEGORY, 'Text'); +FactoryController.prototype.standardCategories['text'].xml = + Blockly.Xml.textToDom( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + 'abc' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + 'text' + + '' + + '' + + '' + + '' + + 'abc' + + '' + + '' + + '' + + '' + + '' + + '' + + 'text' + + '' + + '' + + '' + + '' + + '' + + '' + + 'text' + + '' + + '' + + '' + + '' + + '' + + '' + + 'abc' + + '' + + '' + + '' + + '' + + '' + + '' + + 'abc' + + '' + + '' + + '' + + '' + + '' + + '' + + 'abc' + + '' + + '' + + '' + + '' + + '' + + '' + + 'abc' + + '' + + '' + + '' + + ''); +FactoryController.prototype.standardCategories['text'].color = '#5CA68D'; + +FactoryController.prototype.standardCategories['lists'] = + new ListElement(ListElement.TYPE_CATEGORY, 'Lists'); +FactoryController.prototype.standardCategories['lists'].xml = + Blockly.Xml.textToDom( + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '5' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + 'list' + + '' + + '' + + '' + + '' + + '' + + '' + + 'list' + + '' + + '' + + '' + + '' + + '' + + '' + + 'list' + + '' + + '' + + '' + + '' + + '' + + '' + + 'list' + + '' + + '' + + '' + + '' + + '' + + '' + + ',' + + '' + + '' + + '' + + '' + + ''); +FactoryController.prototype.standardCategories['lists'].color = '#745CA6'; + +FactoryController.prototype.standardCategories['colour'] = + new ListElement(ListElement.TYPE_CATEGORY, 'Colour'); +FactoryController.prototype.standardCategories['colour'].xml = + Blockly.Xml.textToDom( + '' + + '' + + '' + + '' + + '' + + '' + + '100' + + '' + + '' + + '' + + '' + + '50' + + '' + + '' + + '' + + '' + + '0' + + '' + + '' + + '' + + '' + + '' + + '' + + '#ff0000' + + '' + + '' + + '' + + '' + + '#3333ff' + + '' + + '' + + '' + + '' + + '0.5' + + '' + + '' + + '' + + ''); +FactoryController.prototype.standardCategories['colour'].color = '#A6745C'; + +FactoryController.prototype.standardCategories['functions'] = + new ListElement(ListElement.TYPE_CATEGORY, 'Functions'); +FactoryController.prototype.standardCategories['functions'].color = '#9A5CA6' +FactoryController.prototype.standardCategories['functions'].custom = + 'PROCEDURE'; + +FactoryController.prototype.standardCategories['variables'] = + new ListElement(ListElement.TYPE_CATEGORY, 'Variables'); +FactoryController.prototype.standardCategories['variables'].color = '#A65C81'; +FactoryController.prototype.standardCategories['variables'].custom = 'VARIABLE'; diff --git a/demos/blocklyfactory/workspacefactory/style.css b/demos/blocklyfactory/workspacefactory/style.css new file mode 100644 index 000000000..efa5897fa --- /dev/null +++ b/demos/blocklyfactory/workspacefactory/style.css @@ -0,0 +1,238 @@ +body { + background-color: #fff; + font-family: sans-serif; +} + +h1 { + font-weight: normal; + font-size: 140%; +} + +section { + float: left; +} + +aside { + float: right; +} + +#categoryTable>table { + border: 1px solid #ccc; + border-bottom: none; +} + +td.tabon { + border-bottom-color: #ddd !important; + background-color: #ddd; + padding: 5px 19px; +} + +td.taboff { + cursor: pointer; + padding: 5px 19px; +} + +td.taboff:hover { + background-color: #eee; +} + +button { + border-radius: 4px; + border: 1px solid #ddd; + background-color: #eee; + color: #000; + font-size: large; + margin: 0 5px; + padding: 10px; +} + +button:hover:not(:disabled) { + box-shadow: 2px 2px 5px #888; +} + +button:disabled { + opacity: .6; +} + +button>* { + opacity: .6; + vertical-align: text-bottom; +} + +button:hover:not(:disabled)>* { + opacity: 1; +} + +label { + border-radius: 4px; + border: 1px solid #ddd; + background-color: #eee; + color: #000; + font-size: large; + margin: 0 5px; + padding: 10px; +} + +label:hover:not(:disabled) { + box-shadow: 2px 2px 5px #888; +} + +label:disabled { + opacity: .6; +} + +label>* { + opacity: .6; + vertical-align: text-bottom; +} + +label:hover:not(:disabled)>* { + opacity: 1; +} + +table { + border: none; + border-collapse: collapse; + margin: 0; + padding: 0; +} + +td { + padding: 0; + vertical-align: top; +} + +.inputfile { + height: 0; + opacity: 0; + overflow: hidden; + position: absolute; + width: 0; + z-index: -1; +} + +#toolbox_section { + height: 480px; + width: 80%; + position: relative; +} + +#toolbox_blocks { + height: 100%; + width: 100%; +} + +#preview_blocks { + height: 300px; + width: 100%; +} + +#createDiv { + width: 70%; +} + +#previewDiv { + width: 30%; +} + +#category_section { + width: 20%; +} + +#disable_div { + background-color: white; + height: 100%; + left: 0; + opacity: .5; + position: absolute; + top: 0; + width: 100%; + z-index: -1; /* Start behind workspace */ +} + +/* Rules for Closure popup color picker */ +.goog-palette { + outline: none; + cursor: default; +} + +.goog-palette-cell { + height: 13px; + width: 15px; + margin: 0; + border: 0; + text-align: center; + vertical-align: middle; + border-right: 1px solid #000000; + font-size: 1px; +} + +.goog-palette-colorswatch { + border: 1px solid #000000; + height: 13px; + position: relative; + width: 15px; +} + +.goog-palette-cell-hover .goog-palette-colorswatch { + border: 1px solid #FFF; +} + +.goog-palette-cell-selected .goog-palette-colorswatch { + border: 1px solid #000; + color: #fff; +} + +.goog-palette-table { + border: 1px solid #000; + border-collapse: collapse; +} + +.goog-popupcolorpicker { + position: absolute; +} + +/* The container
    - needed to position the dropdown content */ +.dropdown { + position: relative; + display: inline-block; +} + +/* Dropdown Content (Hidden by Default) */ +.dropdown-content { + background-color: #f9f9f9; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,.2); + display: none; + min-width: 170px; + opacity: 1; + position: absolute; + z-index: 1; +} + +/* Links inside the dropdown */ +.dropdown-content a { + color: black; + display: block; + padding: 12px 16px; + text-decoration: none; +} + +/* Change color of dropdown links on hover */ +.dropdown-content a:hover { + background-color: #f1f1f1 +} + +/* Show the dropdown menu */ +.show { + display: block; +} + +.shadowBlock>.blocklyPath { + fill-opacity: .5; + stroke-opacity: .5; +} + +.shadowBlock>.blocklyPathLight, +.shadowBlock>.blocklyPathDark { + display: none; +} diff --git a/demos/blocklyfactory/workspacefactory/wfactory_controller.js b/demos/blocklyfactory/workspacefactory/wfactory_controller.js new file mode 100644 index 000000000..994f0442e --- /dev/null +++ b/demos/blocklyfactory/workspacefactory/wfactory_controller.js @@ -0,0 +1,704 @@ +/** + * @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. + */ + +/** + * @fileoverview Contains the controller code for workspace factory. Depends + * on the model and view objects (created as internal variables) and interacts + * with previewWorkspace and toolboxWorkspace (internal references stored to + * both). Also depends on standard_categories.js for standard Blockly + * categories. Provides the functionality for the actions the user can initiate: + * - adding and removing categories + * - switching between categories + * - printing and downloading configuration xml + * - updating the preview workspace + * - changing a category name + * - moving the position of a category. + * + * @author Emma Dauterman (evd2014) + */ + +/** + * Class for a FactoryController + * @constructor + * @param {!Blockly.workspace} toolboxWorkspace workspace where blocks are + * dragged into corresponding categories + * @param {!Blockly.workspace} previewWorkspace workspace that shows preview + * of what workspace would look like using generated XML + */ +FactoryController = function(toolboxWorkspace, previewWorkspace) { + // Workspace for user to drag blocks in for a certain category. + this.toolboxWorkspace = toolboxWorkspace; + // Workspace for user to preview their changes. + this.previewWorkspace = previewWorkspace; + // Model to keep track of categories and blocks. + this.model = new FactoryModel(); + // Updates the category tabs. + this.view = new FactoryView(); + // Generates XML for categories. + this.generator = new FactoryGenerator(this.model); +}; + +/** + * Currently prompts the user for a name, checking that it's valid (not used + * before), and then creates a tab and switches to it. + */ +FactoryController.prototype.addCategory = function() { + // Check if it's the first category added. + var firstCategory = !this.model.hasToolbox(); + // Give the option to save blocks if their workspace is not empty and they + // are creating their first category. + if (firstCategory && this.toolboxWorkspace.getAllBlocks().length > 0) { + var confirmCreate = confirm('Do you want to save your work in another ' + + 'category? If you don\'t, the blocks in your workspace will be ' + + 'deleted.'); + // Create a new category for current blocks. + if (confirmCreate) { + var name = prompt('Enter the name of the category for your ' + + 'current blocks: '); + if (!name) { // Exit if cancelled. + return; + } + this.createCategory(name, true); + this.model.setSelectedById(this.model.getCategoryIdByName(name)); + } + } + // After possibly creating a category, check again if it's the first category. + firstCategory = !this.model.hasToolbox(); + // Get name from user. + name = this.promptForNewCategoryName('Enter the name of your new category: '); + if (!name) { //Exit if cancelled. + return; + } + // Create category. + this.createCategory(name, firstCategory); + // Switch to category. + this.switchElement(this.model.getCategoryIdByName(name)); + // Update preview. + this.updatePreview(); +}; + +/** + * Helper method for addCategory. Adds a category to the view given a name, ID, + * and a boolean for if it's the first category created. Assumes the category + * has already been created in the model. Does not switch to category. + * + * @param {!string} name Name of category being added. + * @param {!string} id The ID of the category being added. + * @param {boolean} firstCategory True if it's the first category created, + * false otherwise. + */ +FactoryController.prototype.createCategory = function(name, firstCategory) { + // Create empty category + var category = new ListElement(ListElement.TYPE_CATEGORY, name); + this.model.addElementToList(category); + // Create new category. + var tab = this.view.addCategoryRow(name, category.id, firstCategory); + this.addClickToSwitch(tab, category.id); +}; + +/** + * Given a tab and a ID to be associated to that tab, adds a listener to + * that tab so that when the user clicks on the tab, it switches to the + * element associated with that ID. + * + * @param {!Element} tab The DOM element to add the listener to. + * @param {!string} id The ID of the element to switch to when tab is clicked. + */ +FactoryController.prototype.addClickToSwitch = function(tab, id) { + var self = this; + var clickFunction = function(id) { // Keep this in scope for switchElement + return function() { + self.switchElement(id); + }; + }; + this.view.bindClick(tab, clickFunction(id)); +}; + +/** + * Attached to "-" button. Checks if the user wants to delete + * the current element. Removes the element and switches to another element. + * When the last element is removed, it switches to a single flyout mode. + * + */ +FactoryController.prototype.removeElement = function() { + // Check that there is a currently selected category to remove. + if (!this.model.getSelected()) { + return; + } + // Check if user wants to remove current category. + var check = confirm('Are you sure you want to delete the currently selected ' + + this.model.getSelected().type + '?'); + if (!check) { // If cancelled, exit. + return; + } + var selectedId = this.model.getSelectedId(); + var selectedIndex = this.model.getIndexByElementId(selectedId); + // Delete element visually. + this.view.deleteElementRow(selectedId, selectedIndex); + // Delete element in model. + this.model.deleteElementFromList(selectedIndex); + // Find next logical element to switch to. + var next = this.model.getElementByIndex(selectedIndex); + if (!next && this.model.hasToolbox()) { + next = this.model.getElementByIndex(selectedIndex - 1); + } + var nextId = next ? next.id : null; + // Open next element. + this.clearAndLoadElement(nextId); + if (!nextId) { + alert('You currently have no categories or separators. All your blocks' + + ' will be displayed in a single flyout.'); + } + // Update preview. + this.updatePreview(); +}; + +/** + * Gets a valid name for a new category from the user. + * + * @param {!string} promptString Prompt for the user to enter a name. + * @return {string} Valid name for a new category, or null if cancelled. + */ +FactoryController.prototype.promptForNewCategoryName = function(promptString) { + do { + var name = prompt(promptString); + if (!name) { // If cancelled. + return null; + } + } while (this.model.hasCategoryByName(name)); + return name; +} + +/** + * Switches to a new tab for the element given by ID. Stores XML and blocks + * to reload later, updates selected accordingly, and clears the workspace + * and clears undo, then loads the new element. + * + * @param {!string} id ID of tab to be opened, must be valid element ID. + */ +FactoryController.prototype.switchElement = function(id) { + // Disables events while switching so that Blockly delete and create events + // don't update the preview repeatedly. + Blockly.Events.disable(); + // Caches information to reload or generate xml if switching to/from element. + // Only saves if a category is selected. + if (this.model.getSelectedId() != null && id != null) { + this.model.getSelected().saveFromWorkspace(this.toolboxWorkspace); + } + // Load element. + this.clearAndLoadElement(id); + // Enable Blockly events again. + Blockly.Events.enable(); +}; + +/** + * Switches to a new tab for the element by ID. Helper for switchElement. + * Updates selected, clears the workspace and clears undo, loads a new element. + * + * @param {!string} id ID of category to load + */ +FactoryController.prototype.clearAndLoadElement = function(id) { + // Unselect current tab if switching to and from an element. + if (this.model.getSelectedId() != null && id != null) { + this.view.setCategoryTabSelection(this.model.getSelectedId(), false); + } + + // If switching from a separator, enable workspace in view. + if (this.model.getSelectedId() != null && this.model.getSelected().type == + ListElement.TYPE_SEPARATOR) { + this.view.disableWorkspace(false); + } + + // Set next category. + this.model.setSelectedById(id); + + // Clear workspace. + this.toolboxWorkspace.clear(); + this.toolboxWorkspace.clearUndo(); + + // Loads next category if switching to an element. + if (id != null) { + this.view.setCategoryTabSelection(id, true); + Blockly.Xml.domToWorkspace(this.model.getSelectedXml(), + this.toolboxWorkspace); + // Disable workspace if switching to a separator. + if (this.model.getSelected().type == ListElement.TYPE_SEPARATOR) { + this.view.disableWorkspace(true); + } + } + + // Mark all shadow blocks laoded and order blocks as if shown in a flyout. + this.view.markShadowBlocks(this.model.getShadowBlocksInWorkspace + (toolboxWorkspace.getAllBlocks())); + this.toolboxWorkspace.cleanUp_(); + + // Update category editing buttons. + this.view.updateState(this.model.getIndexByElementId + (this.model.getSelectedId()), this.model.getSelected()); +}; + +/** + * Tied to "Export Config" button. Gets a file name from the user and downloads + * the corresponding configuration xml to that file. + */ +FactoryController.prototype.exportConfig = function() { + // Generate XML. + var configXml = Blockly.Xml.domToPrettyText + (this.generator.generateConfigXml(this.toolboxWorkspace)); + // Get file name. + var fileName = prompt("File Name: "); + if (!fileName) { // If cancelled + return; + } + // Download file. + var data = new Blob([configXml], {type: 'text/xml'}); + this.view.createAndDownloadFile(fileName, data); + }; + +/** + * Tied to "Print Config" button. Mainly used for debugging purposes. Prints + * the configuration XML to the console. + */ +FactoryController.prototype.printConfig = function() { + window.console.log(Blockly.Xml.domToPrettyText + (this.generator.generateConfigXml(this.toolboxWorkspace))); +}; + +/** + * Updates the preview workspace based on the toolbox workspace. If switching + * from no categories to categories or categories to no categories, reinjects + * Blockly with reinjectPreview, otherwise just updates without reinjecting. + * Called whenever a list element is created, removed, or modified and when + * Blockly move and delete events are fired. Do not call on create events + * or disabling will cause the user to "drop" their current blocks. + */ +FactoryController.prototype.updatePreview = function() { + // Disable events to stop updatePreview from recursively calling itself + // through event handlers. + Blockly.Events.disable(); + var tree = Blockly.Options.parseToolboxTree + (this.generator.generateConfigXml(this.toolboxWorkspace)); + // No categories, creates a simple flyout. + if (tree.getElementsByTagName('category').length == 0) { + if (this.previewWorkspace.toolbox_) { + this.reinjectPreview(tree); // Switch to simple flyout, more expensive. + } else { + this.previewWorkspace.flyout_.show(tree.childNodes); + } + // Uses categories, creates a toolbox. + } else { + if (!previewWorkspace.toolbox_) { + this.reinjectPreview(tree); // Create a toolbox, more expensive. + } else { + this.previewWorkspace.toolbox_.populate_(tree); + } + } + // Reenable events. + Blockly.Events.enable(); +}; + +/** + * Used to completely reinject the preview workspace. This should be used only + * when switching from simple flyout to categories, or categories to simple + * flyout. More expensive than simply updating the flyout or toolbox. + * + * @param {!Element} tree of xml elements + * @package + */ +FactoryController.prototype.reinjectPreview = function(tree) { + this.previewWorkspace.dispose(); + previewToolbox = Blockly.Xml.domToPrettyText(tree); + this.previewWorkspace = Blockly.inject('preview_blocks', + {grid: + {spacing: 25, + length: 3, + colour: '#ccc', + snap: true}, + media: '../../../media/', + toolbox: previewToolbox, + zoom: + {controls: true, + wheel: true} + }); +}; + +/** + * Tied to "change name" button. Changes the name of the selected category. + * Continues prompting the user until they input a category name that is not + * currently in use, exits if user presses cancel. + */ +FactoryController.prototype.changeCategoryName = function() { + // Return if no category selected or element a separator. + if (!this.model.getSelected() || + this.model.getSelected().type == ListElement.TYPE_SEPARATOR) { + return; + } + // Get new name from user. + var newName = this.promptForNewCategoryName('What do you want to change this' + + ' category\'s name to?'); + if (!newName) { // If cancelled. + return; + } + // Change category name. + this.model.getSelected().changeName(newName); + this.view.updateCategoryName(newName, this.model.getSelectedId()); + // Update preview. + this.updatePreview(); +}; + +/** + * Tied to arrow up and arrow down buttons. Swaps with the element above or + * below the currently selected element (offset categories away from the + * current element). Updates state to enable the correct element editing + * buttons. + * + * @param {int} offset The index offset from the currently selected element + * to swap with. Positive if the element to be swapped with is below, negative + * if the element to be swapped with is above. + */ +FactoryController.prototype.moveElement = function(offset) { + var curr = this.model.getSelected(); + if (!curr) { // Return if no selected element. + return; + } + var currIndex = this.model.getIndexByElementId(curr.id); + var swapIndex = this.model.getIndexByElementId(curr.id) + offset; + var swap = this.model.getElementByIndex(swapIndex); + if (!swap) { // Return if cannot swap in that direction. + return; + } + // Move currently selected element to index of other element. + // Indexes must be valid because confirmed that curr and swap exist. + this.moveElementToIndex(curr, swapIndex, currIndex); + // Update element editing buttons. + this.view.updateState(swapIndex, this.model.getSelected()); + // Update preview. + this.updatePreview(); +}; + +/** + * Moves a element to a specified index and updates the model and view + * accordingly. Helper functions throw an error if indexes are out of bounds. + * + * @param {!Element} element The element to move. + * @param {int} newIndex The index to insert the element at. + * @param {int} oldIndex The index the element is currently at. + */ +FactoryController.prototype.moveElementToIndex = function(element, newIndex, + oldIndex) { + this.model.moveElementToIndex(element, newIndex, oldIndex); + this.view.moveTabToIndex(element.id, newIndex, oldIndex); +}; + +/** + * Changes the color of the selected category. Return if selected element is + * a separator. + * + * @param {!string} color The color to change the selected category. Must be + * a valid CSS string. + */ +FactoryController.prototype.changeSelectedCategoryColor = function(color) { + // Return if no category selected or element a separator. + if (!this.model.getSelected() || + this.model.getSelected().type == ListElement.TYPE_SEPARATOR) { + return; + } + // Change color of selected category. + this.model.getSelected().changeColor(color); + this.view.setBorderColor(this.model.getSelectedId(), color); + this.updatePreview(); +}; + +/** + * Tied to the "Standard Category" dropdown option, this function prompts + * the user for a name of a standard Blockly category (case insensitive) and + * loads it as a new category and switches to it. Leverages standardCategories + * map in standard_categories.js. + */ +FactoryController.prototype.loadCategory = function() { + // Prompt user for the name of the standard category to load. + do { + var name = prompt('Enter the name of the category you would like to import ' + + '(Logic, Loops, Math, Text, Lists, Colour, Variables, or Functions)'); + if (!name) { + return; // Exit if cancelled. + } + } while (!this.isStandardCategoryName(name)); + + // Check if the user can create that standard category. + if (this.model.hasVariables() && name.toLowerCase() == 'variables') { + alert('A Variables category already exists. You cannot create multiple' + + ' variables categories.'); + return; + } + if (this.model.hasProcedures() && name.toLowerCase() == 'functions') { + alert('A Functions category already exists. You cannot create multiple' + + ' functions categories.'); + return; + } + // Check if the user can create a category with that name. + var standardCategory = this.standardCategories[name.toLowerCase()] + if (this.model.hasCategoryByName(standardCategory.name)) { + alert('You already have a category with the name ' + standardCategory.name + + '. Rename your category and try again.'); + return; + } + + // Copy the standard category in the model. + var copy = standardCategory.copy(); + + // Add the copy in the view. + var tab = this.view.addCategoryRow(copy.name, copy.id, + !this.model.hasToolbox()); + + // Add it to the model. + this.model.addElementToList(copy); + + // Update the view. + this.addClickToSwitch(tab, copy.id); + // Color the category tab in the view. + if (copy.color) { + this.view.setBorderColor(copy.id, copy.color); + } + // Switch to loaded category. + this.switchElement(copy.id); + // Convert actual shadow blocks to user-generated shadow blocks. + this.convertShadowBlocks(); + // Update preview. + this.updatePreview(); +}; + +/** + * Given the name of a category, determines if it's the name of a standard + * category (case insensitive). + * + * @param {string} name The name of the category that should be checked if it's + * in standardCategories + * @return {boolean} True if name is a standard category name, false otherwise. + */ +FactoryController.prototype.isStandardCategoryName = function(name) { + for (var category in this.standardCategories) { + if (name.toLowerCase() == category) { + return true; + } + } + return false; +}; + +/** + * Connected to the "add separator" dropdown option. If categories already + * exist, adds a separator to the model and view. Does not switch to select + * the separator, and updates the preview. + */ +FactoryController.prototype.addSeparator = function() { + // Don't allow the user to add a separator if a category has not been created. + if (!this.model.hasToolbox()) { + alert('Add a category before adding a separator.'); + return; + } + // Create the separator in the model. + var separator = new ListElement(ListElement.TYPE_SEPARATOR); + this.model.addElementToList(separator); + // Create the separator in the view. + var tab = this.view.addSeparatorTab(separator.id); + this.addClickToSwitch(tab, separator.id); + // Switch to the separator and update the preview. + this.switchElement(separator.id); + this.updatePreview(); +}; + +/** + * Connected to the import button. Given the file path inputted by the user + * from file input, this function loads that toolbox XML to the workspace, + * creating category and separator tabs as necessary. This allows the user + * to be able to edit toolboxes given their XML form. Catches errors from + * file reading and prints an error message alerting the user. + * + * @param {string} file The path for the file to be imported into the workspace. + * Should contain valid toolbox XML. + */ +FactoryController.prototype.importFile = function(file) { + // Exit if cancelled. + if (!file) { + return; + } + + var reader = new FileReader(); + // To be executed when the reader has read the file. + reader.onload = function() { + // Try to parse XML from file and load it into toolbox editing area. + // Print error message if fail. + try { + var tree = Blockly.Xml.textToDom(reader.result); + controller.importFromTree_(tree); + } catch(e) { + alert('Cannot load XML from file.'); + console.log(e); + } + } + + // Read the file. + reader.readAsText(file); +}; + +/** + * Given a XML DOM tree, loads it into the toolbox editing area so that the + * user can continue editing their work. Assumes that tree is in valid toolbox + * XML format. + * @private + * + * @param {!Element} tree XML tree to be loaded to toolbox editing area. + */ +FactoryController.prototype.importFromTree_ = function(tree) { + // Clear current editing area. + this.model.clearToolboxList(); + this.view.clearToolboxTabs(); + + if (tree.getElementsByTagName('category').length == 0) { + // No categories present. + // Load all the blocks into a single category evenly spaced. + Blockly.Xml.domToWorkspace(tree, this.toolboxWorkspace); + this.toolboxWorkspace.cleanUp_(); + + // Convert actual shadow blocks to user-generated shadow blocks. + this.convertShadowBlocks(); + + // Add message to denote empty category. + this.view.addEmptyCategoryMessage(); + } else { + // Categories/separators present. + for (var i = 0, item; item = tree.children[i]; i++) { + if (item.tagName == 'category') { + // If the element is a category, create a new category and switch to it. + this.createCategory(item.getAttribute('name'), false); + var category = this.model.getElementByIndex(i); + this.switchElement(category.id); + + // Load all blocks in that category to the workspace to be evenly + // spaced and saved to that category. + for (var j = 0, blockXml; blockXml = item.children[j]; j++) { + Blockly.Xml.domToBlock(blockXml, this.toolboxWorkspace); + } + + // Evenly space the blocks. + // TODO(evd2014): Change to cleanUp once cleanUp_ is made public in + // master. + this.toolboxWorkspace.cleanUp_(); + + // Convert actual shadow blocks to user-generated shadow blocks. + this.convertShadowBlocks(); + + // Set category color. + if (item.getAttribute('colour')) { + category.changeColor(item.getAttribute('colour')); + this.view.setBorderColor(category.id, category.color); + } + // Set any custom tags. + if (item.getAttribute('custom')) { + this.model.addCustomTag(category, item.getAttribute('custom')); + } + } else { + // If the element is a separator, add the separator and switch to it. + this.addSeparator(); + this.switchElement(this.model.getElementByIndex(i).id); + } + } + } + this.view.updateState(this.model.getIndexByElementId + (this.model.getSelectedId()), this.model.getSelected()); + this.updatePreview(); +}; + +/** + * Clears the toolbox editing area completely, deleting all categories and all + * blocks in the model and view. + */ +FactoryController.prototype.clear = function() { + this.model.clearToolboxList(); + this.view.clearToolboxTabs(); + this.view.addEmptyCategoryMessage(); + this.view.updateState(-1, null); + this.toolboxWorkspace.clear(); + this.toolboxWorkspace.clearUndo(); + this.updatePreview(); +}; + +/* + * Makes the currently selected block a user-generated shadow block. These + * blocks are not made into real shadow blocks, but recorded in the model + * and visually marked as shadow blocks, allowing the user to move and edit + * them (which would be impossible with actual shadow blocks). Updates the + * preview when done. + * + */ +FactoryController.prototype.addShadow = function() { + // No block selected to make a shadow block. + if (!Blockly.selected) { + return; + } + this.view.markShadowBlock(Blockly.selected); + this.model.addShadowBlock(Blockly.selected.id); + this.updatePreview(); +}; + +/** + * If the currently selected block is a user-generated shadow block, this + * function makes it a normal block again, removing it from the list of + * shadow blocks and loading the workspace again. Updates the preview again. + * + */ +FactoryController.prototype.removeShadow = function() { + // No block selected to modify. + if (!Blockly.selected) { + return; + } + this.model.removeShadowBlock(Blockly.selected.id); + this.view.unmarkShadowBlock(Blockly.selected); + this.updatePreview(); +}; + +/** + * Given a unique block ID, uses the model to determine if a block is a + * user-generated shadow block. + * + * @param {!string} blockId The unique ID of the block to examine. + * @return {boolean} True if the block is a user-generated shadow block, false + * otherwise. + */ +FactoryController.prototype.isUserGenShadowBlock = function(blockId) { + return this.model.isShadowBlock(blockId); +} + +/** + * Call when importing XML containing real shadow blocks. This function turns + * all real shadow blocks loaded in the workspace into user-generated shadow + * blocks, meaning they are marked as shadow blocks by the model and appear as + * shadow blocks in the view but are still editable and movable. + */ +FactoryController.prototype.convertShadowBlocks = function() { + var blocks = this.toolboxWorkspace.getAllBlocks(); + for (var i = 0, block; block = blocks[i]; i++) { + if (block.isShadow()) { + block.setShadow(false); + this.model.addShadowBlock(block.id); + this.view.markShadowBlock(block); + } + } +}; diff --git a/demos/blocklyfactory/workspacefactory/wfactory_generator.js b/demos/blocklyfactory/workspacefactory/wfactory_generator.js new file mode 100644 index 000000000..2de701e80 --- /dev/null +++ b/demos/blocklyfactory/workspacefactory/wfactory_generator.js @@ -0,0 +1,159 @@ +/** + * @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. + */ + +/** + * @fileoverview Generates the configuration xml used to update the preview + * workspace or print to the console or download to a file. Leverages + * Blockly.Xml and depends on information in the model (holds a reference). + * Depends on a hidden workspace created in the generator to load saved XML in + * order to generate toolbox XML. + * + * @author Emma Dauterman (evd2014) + */ + +/** + * Class for a FactoryGenerator + * @constructor + */ +FactoryGenerator = function(model) { + // Model to share information about categories and shadow blocks. + this.model = model; + // Create hidden workspace to load saved XML to generate toolbox XML. + var hiddenBlocks = document.createElement('div'); + // Generate a globally unique ID for the hidden div element to avoid + // collisions. + var hiddenBlocksId = Blockly.genUid(); + hiddenBlocks.id = hiddenBlocksId; + hiddenBlocks.style.display = 'none'; + document.body.appendChild(hiddenBlocks); + this.hiddenWorkspace = Blockly.inject(hiddenBlocksId); +}; + +/** + * Generates the xml for the toolbox or flyout with information from + * toolboxWorkspace and the model. Uses the hiddenWorkspace to generate XML. + * + * @param {!Blockly.workspace} toolboxWorkspace Toolbox editing workspace where + * blocks are added by user to be part of the toolbox. + * @return {!Element} XML element representing toolbox or flyout corresponding + * to toolbox workspace. + */ +FactoryGenerator.prototype.generateConfigXml = function(toolboxWorkspace) { + // Create DOM for XML. + var xmlDom = goog.dom.createDom('xml', + { + 'id' : 'toolbox', + 'style' : 'display:none' + }); + if (!this.model.hasToolbox()) { + // Toolbox has no categories. Use XML directly from workspace. + this.loadToHiddenWorkspaceAndSave_ + (Blockly.Xml.workspaceToDom(toolboxWorkspace), xmlDom); + } else { + // Toolbox has categories. + // Assert that selected != null + if (!this.model.getSelected()) { + throw new Error('Selected is null when the toolbox is empty.'); + } + + // Capture any changes made by user before generating XML. + this.model.getSelected().saveFromWorkspace(toolboxWorkspace); + var xml = this.model.getSelectedXml(); + var toolboxList = this.model.getToolboxList(); + + // Iterate through each category to generate XML for each using the + // hidden workspace. Load each category to the hidden workspace to make sure + // that all the blocks that are not top blocks are also captured as block + // groups in the flyout. + for (var i = 0; i < toolboxList.length; i++) { + var element = toolboxList[i]; + if (element.type == ListElement.TYPE_SEPARATOR) { + // If the next element is a separator. + var nextElement = goog.dom.createDom('sep'); + } else { + // If the next element is a category. + var nextElement = goog.dom.createDom('category'); + nextElement.setAttribute('name', element.name); + // Add a colour attribute if one exists. + if (element.color != null) { + nextElement.setAttribute('colour', element.color); + } + // Add a custom attribute if one exists. + if (element.custom != null) { + nextElement.setAttribute('custom', element.custom); + } + // Load that category to hidden workspace, setting user-generated shadow + // blocks as real shadow blocks. + this.loadToHiddenWorkspaceAndSave_(element.xml, nextElement); + } + xmlDom.appendChild(nextElement); + } + } + return xmlDom; + }; + +/** + * Load the given XML to the hidden workspace, set any user-generated shadow + * blocks to be actual shadow blocks, then append the XML from the workspace + * to the DOM element passed in. + * @private + * + * @param {!Element} xml The XML to be loaded to the hidden workspace. + * @param {!Element} dom The DOM element to append the generated XML to. + */ +FactoryGenerator.prototype.loadToHiddenWorkspaceAndSave_ = function(xml, dom) { + this.hiddenWorkspace.clear(); + Blockly.Xml.domToWorkspace(xml, this.hiddenWorkspace); + this.setShadowBlocksInHiddenWorkspace_(); + this.appendHiddenWorkspaceToDom_(dom); +} + + /** + * Encodes blocks in the hidden workspace in a XML DOM element. Very + * similar to workspaceToDom, but doesn't capture IDs. Uses the top-level + * blocks loaded in hiddenWorkspace. + * @private + * + * @param {!Element} xmlDom Tree of XML elements to be appended to. + */ +FactoryGenerator.prototype.appendHiddenWorkspaceToDom_ = function(xmlDom) { + var blocks = this.hiddenWorkspace.getTopBlocks(); + for (var i = 0, block; block = blocks[i]; i++) { + var blockChild = Blockly.Xml.blockToDom(block); + blockChild.removeAttribute('id'); + xmlDom.appendChild(blockChild); + } +}; + +/** + * Sets the user-generated shadow blocks loaded into hiddenWorkspace to be + * actual shadow blocks. This is done so that blockToDom records them as + * shadow blocks instead of regular blocks. + * @private + * + */ +FactoryGenerator.prototype.setShadowBlocksInHiddenWorkspace_ = function() { + var blocks = this.hiddenWorkspace.getAllBlocks(); + for (var i = 0; i < blocks.length; i++) { + if (this.model.isShadowBlock(blocks[i].id)) { + blocks[i].setShadow(true); + } + } +}; diff --git a/demos/blocklyfactory/workspacefactory/wfactory_model.js b/demos/blocklyfactory/workspacefactory/wfactory_model.js new file mode 100644 index 000000000..3d8585c9a --- /dev/null +++ b/demos/blocklyfactory/workspacefactory/wfactory_model.js @@ -0,0 +1,450 @@ +/** + * @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. + */ + +/** + * @fileoverview Stores and updates information about state and categories + * in workspace factory. Each list element is either a separator or a category, + * and each category stores its name, XML to load that category, color, + * custom tags, and a unique ID making it possible to change category names and + * move categories easily. Keeps track of the currently selected list + * element. Also keeps track of all the user-created shadow blocks and + * manipulates them as necessary. + * + * @author Emma Dauterman (evd2014) + */ + +/** + * Class for a FactoryModel + * @constructor + */ +FactoryModel = function() { + // Ordered list of ListElement objects. + this.toolboxList = []; + // Array of block IDs for all user created shadow blocks. + this.shadowBlocks = []; + // String name of current selected list element, null if no list elements. + this.selected = null; + // Boolean for if a Variable category has been added. + this.hasVariableCategory = false; + // Boolean for if a Procedure category has been added. + this.hasProcedureCategory = false; +}; + +// String name of current selected list element, null if no list elements. +FactoryModel.prototype.selected = null; + +/** + * Given a name, determines if it is the name of a category already present. + * Used when getting a valid category name from the user. + * + * @param {string} name String name to be compared against. + * @return {boolean} True if string is a used category name, false otherwise. + */ +FactoryModel.prototype.hasCategoryByName = function(name) { + for (var i = 0; i < this.toolboxList.length; i++) { + if (this.toolboxList[i].type == ListElement.TYPE_CATEGORY && + this.toolboxList[i].name == name) { + return true; + } + } + return false; +}; + +/** + * Determines if a category with the 'VARIABLE' tag exists. + * + * @return {boolean} True if there exists a category with the Variables tag, + * false otherwise. + */ +FactoryModel.prototype.hasVariables = function() { + return this.hasVariableCategory; +}; + +/** + * Determines if a category with the 'PROCEDURE' tag exists. + * + * @return {boolean} True if there exists a category with the Procedures tag, + * false otherwise. + */ +FactoryModel.prototype.hasProcedures = function() { + return this.hasFunctionCategory; +}; + +/** + * Determines if the user has any elements in the toolbox. Uses the length of + * toolboxList. + * + * @return {boolean} True if categories exist, false otherwise. + */ +FactoryModel.prototype.hasToolbox = function() { + return this.toolboxList.length > 0; +}; + +/** + * Given a ListElement, adds it to the toolbox list. + * + * @param {!ListElement} element The element to be added to the list. + */ +FactoryModel.prototype.addElementToList = function(element) { + // Update state if the copied category has a custom tag. + this.hasVariableCategory = element.custom == 'VARIABLE' ? true : + this.hasVariableCategory; + this.hasProcedureCategory = element.custom == 'PROCEDURE' ? true : + this.hasProcedureCategory; + // Add element to toolboxList. + this.toolboxList.push(element); +}; + +/** + * Given an index, deletes a list element and all associated data. + * + * @param {int} index The index of the list element to delete. + */ +FactoryModel.prototype.deleteElementFromList = function(index) { + // Check if index is out of bounds. + if (index < 0 || index >= this.toolboxList.length) { + return; // No entry to delete. + } + // Check if need to update flags. + this.hasVariableCategory = this.toolboxList[index].custom == 'VARIABLE' ? + false : this.hasVariableCategory; + this.hasProcedureCategory = this.toolboxList[index].custom == 'PROCEDURE' ? + false : this.hasProcedureCategory; + // Remove element. + this.toolboxList.splice(index, 1); +}; + +/** + * Moves a list element to a certain position in toolboxList by removing it + * and then inserting it at the correct index. Checks that indices are in + * bounds (throws error if not), but assumes that oldIndex is the correct index + * for list element. + * + * @param {!ListElement} element The element to move in toolboxList. + * @param {int} newIndex The index to insert the element at. + * @param {int} oldIndex The index the element is currently at. + */ +FactoryModel.prototype.moveElementToIndex = function(element, newIndex, + oldIndex) { + // Check that indexes are in bounds. + if (newIndex < 0 || newIndex >= this.toolboxList.length || oldIndex < 0 || + oldIndex >= this.toolboxList.length) { + throw new Error('Index out of bounds when moving element in the model.'); + } + this.deleteElementFromList(oldIndex); + this.toolboxList.splice(newIndex, 0, element); +} + +/** + * Returns the ID of the currently selected element. Returns null if there are + * no categories (if selected == null). + * + * @return {string} The ID of the element currently selected. + */ +FactoryModel.prototype.getSelectedId = function() { + return this.selected ? this.selected.id : null; +}; + +/** + * Returns the name of the currently selected category. Returns null if there + * are no categories (if selected == null) or the selected element is not + * a category (in which case its name is null). + * + * @return {string} The name of the category currently selected. + */ +FactoryModel.prototype.getSelectedName = function() { + return this.selected ? this.selected.name : null; +}; + +/** + * Returns the currently selected list element object. + * + * @return {ListElement} The currently selected ListElement + */ +FactoryModel.prototype.getSelected = function() { + return this.selected; +}; + +/** + * Sets list element currently selected by id. + * + * @param {string} id ID of list element that should now be selected. + */ +FactoryModel.prototype.setSelectedById = function(id) { + this.selected = this.getElementById(id); +}; + +/** + * Given an ID of a list element, returns the index of that list element in + * toolboxList. Returns -1 if ID is not present. + * + * @param {!string} id The ID of list element to search for. + * @return {int} The index of the list element in toolboxList, or -1 if it + * doesn't exist. + */ + +FactoryModel.prototype.getIndexByElementId = function(id) { + for (var i = 0; i < this.toolboxList.length; i++) { + if (this.toolboxList[i].id == id) { + return i; + } + } + return -1; // ID not present in toolboxList. +}; + +/** + * Given the ID of a list element, returns that ListElement object. + * + * @param {!string} id The ID of element to search for. + * @return {ListElement} Corresponding ListElement object in toolboxList, or + * null if that element does not exist. + */ +FactoryModel.prototype.getElementById = function(id) { + for (var i = 0; i < this.toolboxList.length; i++) { + if (this.toolboxList[i].id == id) { + return this.toolboxList[i]; + } + } + return null; // ID not present in toolboxList. +}; + +/** + * Given the index of a list element in toolboxList, returns that ListElement + * object. + * + * @param {int} index The index of the element to return. + * @return {ListElement} The corresponding ListElement object in toolboxList. + */ +FactoryModel.prototype.getElementByIndex = function(index) { + if (index < 0 || index >= this.toolboxList.length) { + return null; + } + return this.toolboxList[index]; +}; + +/** + * Returns the xml to load the selected element. + * + * @return {!Element} The XML of the selected element, or null if there is + * no selected element. + */ +FactoryModel.prototype.getSelectedXml = function() { + return this.selected ? this.selected.xml : null; +}; + +/** + * Return ordered list of ListElement objects. + * + * @return {!Array} ordered list of ListElement objects + */ +FactoryModel.prototype.getToolboxList = function() { + return this.toolboxList; +}; + +/** + * Gets the ID of a category given its name. + * + * @param {string} name Name of category. + * @return {int} ID of category + */ +FactoryModel.prototype.getCategoryIdByName = function(name) { + for (var i = 0; i < this.toolboxList.length; i++) { + if (this.toolboxList[i].name == name) { + return this.toolboxList[i].id; + } + } + return null; // Name not present in toolboxList. +}; + +/** + * Clears the toolbox list, deleting all ListElements. + */ +FactoryModel.prototype.clearToolboxList = function() { + this.toolboxList = []; + this.hasVariableCategory = false; + this.hasVariableCategory = false; + // TODO(evd2014): When merge changes, also clear shadowList. +}; + +/** + * Class for a ListElement + * Adds a shadow block to the list of shadow blocks. + * + * @param {!string} blockId The unique ID of block to be added. + */ +FactoryModel.prototype.addShadowBlock = function(blockId) { + this.shadowBlocks.push(blockId); +}; + +/** + * Removes a shadow block ID from the list of shadow block IDs if that ID is + * in the list. + * + * @param {!string} blockId The unique ID of block to be removed. + */ +FactoryModel.prototype.removeShadowBlock = function(blockId) { + for (var i = 0; i < this.shadowBlocks.length; i++) { + if (this.shadowBlocks[i] == blockId) { + this.shadowBlocks.splice(i, 1); + return; + } + } +}; + +/** + * Determines if a block is a shadow block given a unique block ID. + * + * @param {!string} blockId The unique ID of the block to examine. + * @return {boolean} True if the block is a user-generated shadow block, false + * otherwise. + */ +FactoryModel.prototype.isShadowBlock = function(blockId) { + for (var i = 0; i < this.shadowBlocks.length; i++) { + if (this.shadowBlocks[i] == blockId) { + return true; + } + } + return false; +}; + +/** + * Given a set of blocks currently loaded, returns all blocks in the workspace + * that are user generated shadow blocks. + * + * @param {!} blocks Array of blocks currently loaded. + * @return {!} Array of user-generated shadow blocks currently + * loaded. + */ +FactoryModel.prototype.getShadowBlocksInWorkspace = function(workspaceBlocks) { + var shadowsInWorkspace = []; + for (var i = 0; i < workspaceBlocks.length; i++) { + if (this.isShadowBlock(workspaceBlocks[i].id)) { + shadowsInWorkspace.push(workspaceBlocks[i]); + } + } + return shadowsInWorkspace; +}; + +/** + * Adds a custom tag to a category, updating state variables accordingly. + * Only accepts 'VARIABLE' and 'PROCEDURE' tags. + * + * @param {!ListElement} category The category to add the tag to. + * @param {!string} tag The custom tag to add to the category. + */ +FactoryModel.prototype.addCustomTag = function(category, tag) { + // Only update list elements that are categories. + if (category.type != ListElement.TYPE_CATEGORY) { + return; + } + // Only update the tag to be 'VARIABLE' or 'PROCEDURE'. + if (tag == 'VARIABLE') { + this.hasVariableCategory = true; + category.custom = 'VARIABLE'; + } else if (tag == 'PROCEDURE') { + this.hasProcedureCategory = true; + category.custom = 'PROCEDURE'; + } +}; + + +/** + * Class for a ListElement. + * @constructor + */ +ListElement = function(type, opt_name) { + this.type = type; + // XML DOM element to load the element. + this.xml = Blockly.Xml.textToDom(''); + // Name of category. Can be changed by user. Null if separator. + this.name = opt_name ? opt_name : null; + // Unique ID of element. Does not change. + this.id = Blockly.genUid(); + // Color of category. Default is no color. Null if separator. + this.color = null; + // Stores a custom tag, if necessary. Null if no custom tag or separator. + this.custom = null; +}; + +// List element types. +ListElement.TYPE_CATEGORY = 'category'; +ListElement.TYPE_SEPARATOR = 'separator'; + +/** + * Saves a category by updating its XML (does not save XML for + * elements that are not categories). + * + * @param {!Blockly.workspace} workspace The workspace to save category entry + * from. + */ +ListElement.prototype.saveFromWorkspace = function(workspace) { + // Only save list elements that are categories. + if (this.type != ListElement.TYPE_CATEGORY) { + return; + } + this.xml = Blockly.Xml.workspaceToDom(workspace); +}; + + +/** + * Changes the name of a category object given a new name. Returns if + * not a category. + * + * @param {string} name New name of category. + */ +ListElement.prototype.changeName = function (name) { + // Only update list elements that are categories. + if (this.type != ListElement.TYPE_CATEGORY) { + return; + } + this.name = name; +}; + +/** + * Sets the color of a category. If tries to set the color of something other + * than a category, returns. + * + * @param {!string} color The color that should be used for that category. + */ +ListElement.prototype.changeColor = function (color) { + if (this.type != ListElement.TYPE_CATEGORY) { + return; + } + this.color = color; +}; + +/** + * Makes a copy of the original element and returns it. Everything about the + * copy is identical except for its ID. + * + * @return {!ListElement} The copy of the ListElement. + */ +ListElement.prototype.copy = function() { + copy = new ListElement(this.type); + // Generate a unique ID for the element. + copy.id = Blockly.genUid(); + // Copy all attributes except ID. + copy.name = this.name; + copy.xml = this.xml; + copy.color = this.color; + copy.custom = this.custom; + // Return copy. + return copy; +}; diff --git a/demos/blocklyfactory/workspacefactory/wfactory_view.js b/demos/blocklyfactory/workspacefactory/wfactory_view.js new file mode 100644 index 000000000..6ad6bf51d --- /dev/null +++ b/demos/blocklyfactory/workspacefactory/wfactory_view.js @@ -0,0 +1,341 @@ +/** + * @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 FactoryController (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) + */ + + /** + * Class for a FactoryView + * @constructor + */ + +FactoryView = 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 + * @param {boolean} firstCategory true if it's the first category, false + * otherwise + * @return {!Element} DOM element created for tab + */ +FactoryView.prototype.addCategoryRow = function(name, id, firstCategory) { + var table = document.getElementById('categoryTable'); + // Delete help label and enable category buttons if it's the first category. + if (firstCategory) { + table.deleteRow(0); + } + // Create tab. + var count = table.rows.length; + 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. + */ +FactoryView.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. + */ +FactoryView.prototype.addEmptyCategoryMessage = function() { + var table = document.getElementById('categoryTable'); + if (table.rows.length == 0) { + var row = table.insertRow(0); + row.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. + */ +FactoryView.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 + */ +FactoryView.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. + */ +FactoryView.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 + */ +FactoryView.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 + */ +FactoryView.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 + * + */ +FactoryView.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. + */ +FactoryView.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. + */ +FactoryView.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. + */ +FactoryView.prototype.addSeparatorTab = function(id) { + // Create separator. + var table = document.getElementById('categoryTable'); + var count = table.rows.length; + 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. + */ +FactoryView.prototype.disableWorkspace = function(disable) { + document.getElementById('disable_div').style.zIndex = disable ? 1 : -1; +}; + +/** + * 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. + */ +FactoryView.prototype.shouldDisableWorkspace = function(category) { + return category != null && (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. + */ +FactoryView.prototype.clearToolboxTabs = function() { + this.tabMap = []; + var oldCategoryTable = document.getElementById('categoryTable'); + var newCategoryTable = document.createElement('table'); + newCategoryTable.id = 'categoryTable'; + 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 {!} blocks Array of user-generated shadow blocks + * currently loaded. + */ +FactoryView.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). + */ +FactoryView.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.'); + } +}; + +/** + * 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). + */ +FactoryView.prototype.unmarkShadowBlock = function(block) { + // Remove Blockly CSS for user-generated shadow blocks. + if (Blockly.hasClass_(block.svgGroup_, 'shadowBlock')) { + Blockly.removeClass_(block.svgGroup_, 'shadowBlock'); + } +}; From 73fbc06d4a22c7813de328facd838b012d9555c9 Mon Sep 17 00:00:00 2001 From: Tina Quach Date: Wed, 10 Aug 2016 17:56:21 -0400 Subject: [PATCH 07/10] Blockly Factory: Foundation for Integrating Workspace Factory (#533) * working tabs using closure expanded export settings menu added old blockfactory and moved new files into blocklyfactory expanded export to lay groundwork for workspace factory integration fixed BlockFactory escapeString bug * added TODO for refactoring onTab --- demos/blocklyfactory/app_controller.js | 135 +++++++++++------- .../block_exporter_controller.js | 40 +++++- demos/blocklyfactory/block_exporter_tools.js | 1 + demos/blocklyfactory/factory.css | 22 ++- demos/blocklyfactory/index.html | 38 +++-- 5 files changed, 166 insertions(+), 70 deletions(-) diff --git a/demos/blocklyfactory/app_controller.js b/demos/blocklyfactory/app_controller.js index 4e05e720f..2c195d510 100644 --- a/demos/blocklyfactory/app_controller.js +++ b/demos/blocklyfactory/app_controller.js @@ -47,6 +47,16 @@ AppController = function() { // Initialize Block Exporter this.exporter = new BlockExporterController(this.blockLibraryController.storage); + + // Map of tab type to the div element for the tab. + this.tabMap = { + 'BLOCK_FACTORY' : goog.dom.getElement('blockFactory_tab'), + 'WORKSPACE_FACTORY': goog.dom.getElement('workspaceFactory_tab'), + 'EXPORTER' : goog.dom.getElement('blocklibraryExporter_tab') + }; + + // Selected tab. + this.selectedTab = 'BLOCK_FACTORY'; }; /** @@ -197,71 +207,92 @@ AppController.prototype.onSelectedBlockChanged = function(blockLibraryDropdown) }; /** - * Add tab handlers to allow switching between the Block Factory - * tab and the Block Exporter tab. + * Add click handlers to each tab to allow switching between the Block Factory, + * Workspace Factory, and Block Exporter tab. * - * @param {string} blockFactoryTabID - ID of element containing Block Factory - * tab - * @param {string} blockExporterTabID - ID of element containing Block - * Exporter tab + * @param {!Object} tabMap - Map of tab name to div element that is the tab. */ -AppController.prototype.addTabHandlers = - function(blockFactoryTabID, blockExporterTabID) { - // Assign this instance of Block Factory Expansion to self in order to - // keep the reference to this object upon tab click. +AppController.prototype.addTabHandlers = function(tabMap) { var self = this; - // Get div elements representing tabs - var blockFactoryTab = goog.dom.getElement(blockFactoryTabID); - var blockExporterTab = goog.dom.getElement(blockExporterTabID); - // Add event listeners. - blockFactoryTab.addEventListener('click', - function() { - self.onFactoryTab(blockFactoryTab, blockExporterTab); - }); - blockExporterTab.addEventListener('click', - function() { - self.onExporterTab(blockFactoryTab, blockExporterTab); - }); + for (var tabName in tabMap) { + var tab = tabMap[tabName]; + // Use an additional closure to correctly assign the tab callback. + tab.addEventListener('click', self.makeTabClickHandler_(tabName)); + } }; /** - * Tied to 'Block Factory' Tab. Shows Block Factory and Block Library. + * Set the selected tab. + * @private * - * @param {string} blockFactoryTab - div element that is the Block Factory tab - * @param {string} blockExporterTab - div element that is the Block Exporter tab + * @param {string} tabName 'BLOCK_FACTORY', 'WORKSPACE_FACTORY', or 'EXPORTER' */ -AppController.prototype.onFactoryTab = - function(blockFactoryTab, blockExporterTab) { - // Turn factory tab on and exporter tab off. - goog.dom.classlist.addRemove(blockFactoryTab, 'taboff', 'tabon'); - goog.dom.classlist.addRemove(blockExporterTab, 'tabon', 'taboff'); - - // Hide container of exporter. - BlockFactory.hide('blockLibraryExporter'); - - // Resize to render workspaces' toolboxes correctly. - window.dispatchEvent(new Event('resize')); +AppController.prototype.setSelected_ = function(tabName) { + this.selectedTab = tabName; }; /** - * Tied to 'Block Exporter' Tab. Shows Block Exporter. + * Creates the tab click handler specific to the tab specified. + * @private * - * @param {string} blockFactoryTab - div element that is the Block Factory tab - * @param {string} blockExporterTab - div element that is the Block Exporter tab + * @param {string} tabName 'BLOCK_FACTORY', 'WORKSPACE_FACTORY', or 'EXPORTER' + * @return {Function} The tab click handler. */ -AppController.prototype.onExporterTab = - function(blockFactoryTab, blockExporterTab) { - // Turn exporter tab on and factory tab off. - goog.dom.classlist.addRemove(blockFactoryTab, 'tabon', 'taboff'); - goog.dom.classlist.addRemove(blockExporterTab, 'taboff', 'tabon'); +AppController.prototype.makeTabClickHandler_ = function(tabName) { + var self = this; + return function() { + self.setSelected_(tabName); + self.onTab(); + }; +}; - // Update toolbox to reflect current block library. - this.exporter.updateToolbox(); +/** + * Called on each tab click. Hides and shows specific content based on which tab + * (Block Factory, Workspace Factory, or Exporter) is selected. + * + * TODO(quachtina96): Refactor the code to avoid repetition of addRemove. + */ +AppController.prototype.onTab = function() { + // Get tab div elements. + var blockFactoryTab = this.tabMap['BLOCK_FACTORY']; + var exporterTab = this.tabMap['EXPORTER']; + var workspaceFactoryTab = this.tabMap['WORKSPACE_FACTORY']; - // Show container of exporter. - BlockFactory.show('blockLibraryExporter'); + if (this.selectedTab == 'EXPORTER') { + // Turn exporter tab on and other tabs off. + goog.dom.classlist.addRemove(exporterTab, 'taboff', 'tabon'); + goog.dom.classlist.addRemove(blockFactoryTab, 'tabon', 'taboff'); + goog.dom.classlist.addRemove(workspaceFactoryTab, 'tabon', 'taboff'); - // Resize to render workspaces' toolboxes correctly. + // Update toolbox to reflect current block library. + this.exporter.updateToolbox(); + + // Show container of exporter. + BlockFactory.show('blockLibraryExporter'); + BlockFactory.hide('workspaceFactoryContent'); + + } else if (this.selectedTab == 'BLOCK_FACTORY') { + // Turn factory tab on and other tabs off. + goog.dom.classlist.addRemove(blockFactoryTab, 'taboff', 'tabon'); + goog.dom.classlist.addRemove(exporterTab, 'tabon', 'taboff'); + goog.dom.classlist.addRemove(workspaceFactoryTab, 'tabon', 'taboff'); + + // Hide container of exporter. + BlockFactory.hide('blockLibraryExporter'); + BlockFactory.hide('workspaceFactoryContent'); + + } else if (this.selectedTab == 'WORKSPACE_FACTORY') { + console.log('workspaceFactoryTab'); + goog.dom.classlist.addRemove(workspaceFactoryTab, 'taboff', 'tabon'); + goog.dom.classlist.addRemove(blockFactoryTab, 'tabon', 'taboff'); + goog.dom.classlist.addRemove(exporterTab, 'tabon', 'taboff'); + // Hide container of exporter. + BlockFactory.hide('blockLibraryExporter'); + // Show workspace factory container. + BlockFactory.show('workspaceFactoryContent'); + } + + // Resize to render workspaces' toolboxes correctly for all tabs. window.dispatchEvent(new Event('resize')); }; @@ -273,13 +304,13 @@ AppController.prototype.assignExporterClickHandlers = function() { // Export blocks when the user submits the export settings. document.getElementById('exporterSubmitButton').addEventListener('click', function() { - self.exporter.exportBlocks(); + self.exporter.export(); }); document.getElementById('clearSelectedButton').addEventListener('click', function() { self.exporter.clearSelectedBlocks(); }); - document.getElementById('addAllButton').addEventListener('click', + document.getElementById('addAllFromLibButton').addEventListener('click', function() { self.exporter.addAllBlocksToWorkspace(); }); @@ -435,7 +466,7 @@ AppController.prototype.init = function() { media: '../../media/'}); // Add tab handlers for switching between Block Factory and Block Exporter. - this.addTabHandlers("blockfactory_tab", "blocklibraryExporter_tab"); + this.addTabHandlers(this.tabMap); this.exporter.addChangeListenersToSelectorWorkspace(); diff --git a/demos/blocklyfactory/block_exporter_controller.js b/demos/blocklyfactory/block_exporter_controller.js index 04892e5d8..10e7601ce 100644 --- a/demos/blocklyfactory/block_exporter_controller.js +++ b/demos/blocklyfactory/block_exporter_controller.js @@ -90,20 +90,47 @@ BlockExporterController.prototype.getSelectedBlockTypes_ = function() { /** * Get selected blocks from selector workspace, pulls info from the Export - * Settings form in Block Exporter, and downloads block code accordingly. + * Settings form in Block Exporter, and downloads code accordingly. + * + * TODO(quachtina96): allow export as zip. */ -BlockExporterController.prototype.exportBlocks = function() { +BlockExporterController.prototype.export = function() { + // Get selected blocks' information. var blockTypes = this.getSelectedBlockTypes_(); var blockXmlMap = this.blockLibStorage.getBlockXmlMap(blockTypes); - // Pull inputs from the Export Settings form. + // Pull workspace-related settings from the Export Settings form. + var wantToolbox = document.getElementById('toolboxCheck').checked; + var wantPreloadedWorkspace = + document.getElementById('preloadedWorkspaceCheck').checked; + var wantWorkspaceOptions = + document.getElementById('workspaceOptsCheck').checked; + + // Pull block definition(s) settings from the Export Settings form. + var wantBlockDef = document.getElementById('blockDefCheck').checked; var definitionFormat = document.getElementById('exportFormat').value; - var language = document.getElementById('exportLanguage').value; var blockDef_filename = document.getElementById('blockDef_filename').value; + + // Pull block generator stub(s) settings from the Export Settings form. + var wantGenStub = document.getElementById('genStubCheck').checked; + var language = document.getElementById('exportLanguage').value; var generatorStub_filename = document.getElementById( 'generatorStub_filename').value; - var wantBlockDef = document.getElementById('blockDefCheck').checked; - var wantGenStub = document.getElementById('genStubCheck').checked; + + if (wantToolbox) { + // TODO(quachtina96): create and download file once wfactory has been + // integrated. + } + + if (wantPreloadedWorkspace) { + // TODO(quachtina96): create and download file once wfactory has been + // integrated. + } + + if (wantWorkspaceOptions) { + // TODO(quachtina96): create and download file once wfactory has been + // integrated. + } if (wantBlockDef) { // User wants to export selected blocks' definitions. @@ -134,6 +161,7 @@ BlockExporterController.prototype.exportBlocks = function() { genStubs, generatorStub_filename, language); } } + }; /** diff --git a/demos/blocklyfactory/block_exporter_tools.js b/demos/blocklyfactory/block_exporter_tools.js index 65c50ddad..dc88cb8ea 100644 --- a/demos/blocklyfactory/block_exporter_tools.js +++ b/demos/blocklyfactory/block_exporter_tools.js @@ -179,6 +179,7 @@ BlockExporterTools.prototype.addBlockDefinitions = function(blockXmlMap) { * Pulls information about all blocks in the block library to generate xml * for the selector workpace's toolbox. * + * @param {!BlockLibraryStorage} blockLibStorage - Block Library Storage object. * @return {!Element} Xml representation of the toolbox. */ BlockExporterTools.prototype.generateToolboxFromLibrary diff --git a/demos/blocklyfactory/factory.css b/demos/blocklyfactory/factory.css index 171e7927e..d2a72bc64 100644 --- a/demos/blocklyfactory/factory.css +++ b/demos/blocklyfactory/factory.css @@ -161,6 +161,16 @@ button, .buttonStyle { width: 50%; } +/* Workspace Factory */ + +#workspaceFactoryContent { + clear: both; + display: none; + height: 100%; +} + +/* Exporter */ + #blockLibraryExporter { clear: both; display: none; @@ -170,12 +180,13 @@ button, .buttonStyle { #exportSelector { float: left; height: 75%; - width: 60%; + width: 30%; } #exportSettings { - margin: auto; + float: left; padding: 16px; + width: 30%; overflow: hidden; } @@ -183,6 +194,13 @@ button, .buttonStyle { display: none; } +#exporterPreview { + float: right; + padding: 16px; + overflow: hidden; + background-color: blue; +} + /* Tabs */ .tab { diff --git a/demos/blocklyfactory/index.html b/demos/blocklyfactory/index.html index 4ebe83837..86c2ef442 100644 --- a/demos/blocklyfactory/index.html +++ b/demos/blocklyfactory/index.html @@ -40,27 +40,41 @@ Demos > Blockly Factory
    -
    Block Factory
    +
    Block Factory
    -
    Block Library Exporter
    +
    Workspace Factory
    +
    +
    Exporter
    -
    - - -
    +

    Block Selector

    Drag blocks into your workspace to select them for download.

    +
    + + + +
    -

    Block Export Settings

    +

    Export Settings


    - Download Block Definition: + Toolbox Xml: + +
    + Pre-loaded Workspace: + +
    + Workspace Option(s): + +
    +
    + Block Definition(s):
    Language code:
    Block Definition(s) File Name:
    -
    +
    - Download Generator Stubs: +
    + Generator Stub(s):
    + [attr.aria-label]="disabled ? 'Disabled text field' : 'Press Enter to edit text'" + tabindex="-1"> + [attr.aria-label]="disabled ? 'Disabled number field' : 'Press Enter to edit number'" + tabindex="-1">
    @@ -43,7 +45,7 @@ blocklyApp.FieldComponent = ng.core
  • diff --git a/accessible/toolbox-tree.component.js b/accessible/toolbox-tree.component.js index 0bd31ac8f..e5bbb75be 100644 --- a/accessible/toolbox-tree.component.js +++ b/accessible/toolbox-tree.component.js @@ -37,14 +37,14 @@ blocklyApp.ToolboxTreeComponent = ng.core
  • -
  • -
  • @@ -52,7 +52,7 @@ blocklyApp.ToolboxTreeComponent = ng.core [attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap['sendToSelectedButton'], 'blockly-button', !canBeCopiedToMarkedConnection())" [attr.aria-level]="level + 2">
  • diff --git a/accessible/toolbox.component.js b/accessible/toolbox.component.js index a20d6c152..a7d5acd93 100644 --- a/accessible/toolbox.component.js +++ b/accessible/toolbox.component.js @@ -30,7 +30,7 @@ blocklyApp.ToolboxComponent = ng.core

    Toolbox

      diff --git a/accessible/tree.service.js b/accessible/tree.service.js index ec175649c..0c463e191 100644 --- a/accessible/tree.service.js +++ b/accessible/tree.service.js @@ -82,21 +82,19 @@ blocklyApp.TreeService = ng.core } return null; }, - focusOnNextTree_: function(treeId) { + getIdOfNextTree_: function(treeId) { var trees = this.getAllTreeNodes_(); for (var i = 0; i < trees.length - 1; i++) { if (trees[i].id == treeId) { - trees[i + 1].focus(); return trees[i + 1].id; } } return null; }, - focusOnPreviousTree_: function(treeId) { + getIdOfPreviousTree_: function(treeId) { var trees = this.getAllTreeNodes_(); for (var i = trees.length - 1; i > 0; i--) { if (trees[i].id == treeId) { - trees[i - 1].focus(); return trees[i - 1].id; } } @@ -190,12 +188,11 @@ blocklyApp.TreeService = ng.core if (e.keyCode == 9) { // Tab key. var destinationTreeId = - e.shiftKey ? this.focusOnPreviousTree_(treeId) : - this.focusOnNextTree_(treeId); - this.notifyUserAboutCurrentTree_(destinationTreeId); - - e.preventDefault(); - e.stopPropagation(); + e.shiftKey ? this.getIdOfPreviousTree_(treeId) : + this.getIdOfNextTree_(treeId); + if (destinationTreeId) { + this.notifyUserAboutCurrentTree_(destinationTreeId); + } } }, isButtonOrFieldNode_: function(node) { @@ -260,16 +257,20 @@ blocklyApp.TreeService = ng.core // For Esc and Tab keys, the focus is removed from the input field. this.focusOnCurrentTree_(treeId); - // In addition, for Tab keys, the user tabs to the previous/next tree. if (e.keyCode == 9) { var destinationTreeId = - e.shiftKey ? this.focusOnPreviousTree_(treeId) : - this.focusOnNextTree_(treeId); - this.notifyUserAboutCurrentTree_(destinationTreeId); + e.shiftKey ? this.getIdOfPreviousTree_(treeId) : + this.getIdOfNextTree_(treeId); + if (destinationTreeId) { + this.notifyUserAboutCurrentTree_(destinationTreeId); + } } - e.preventDefault(); - e.stopPropagation(); + // Allow Tab keypresses to go through. + if (e.keyCode == 27) { + e.preventDefault(); + e.stopPropagation(); + } } } else { // Outside an input field, Enter, Tab and navigation keys are all @@ -302,14 +303,14 @@ blocklyApp.TreeService = ng.core } } } else if (e.keyCode == 9) { - // Tab key. + // Tab key. Note that allowing the event to propagate through is + // intentional. var destinationTreeId = - e.shiftKey ? this.focusOnPreviousTree_(treeId) : - this.focusOnNextTree_(treeId); - this.notifyUserAboutCurrentTree_(destinationTreeId); - - e.preventDefault(); - e.stopPropagation(); + e.shiftKey ? this.getIdOfPreviousTree_(treeId) : + this.getIdOfNextTree_(treeId); + if (destinationTreeId) { + this.notifyUserAboutCurrentTree_(destinationTreeId); + } } else if (e.keyCode >= 35 && e.keyCode <= 40) { // End, home, and arrow keys. if (e.keyCode == 35) { diff --git a/accessible/workspace-tree.component.js b/accessible/workspace-tree.component.js index 39e33b048..b024101d6 100644 --- a/accessible/workspace-tree.component.js +++ b/accessible/workspace-tree.component.js @@ -60,7 +60,7 @@ blocklyApp.WorkspaceTreeComponent = ng.core [attr.aria-level]="level + 2"> @@ -78,7 +78,7 @@ blocklyApp.WorkspaceTreeComponent = ng.core [attr.aria-labelledBy]="generateAriaLabelledByAttr(idMap[buttonInfo.baseIdKey + 'Button'], 'blockly-button', buttonInfo.isDisabled())" [attr.aria-level]="level + 2"> diff --git a/accessible/workspace.component.js b/accessible/workspace.component.js index a2c940ec0..255735020 100644 --- a/accessible/workspace.component.js +++ b/accessible/workspace.component.js @@ -47,7 +47,7 @@ blocklyApp.WorkspaceComponent = ng.core
        From bbd57a9a16e316a91e09b86d107f64da13deea80 Mon Sep 17 00:00:00 2001 From: Katelyn Mann Date: Thu, 11 Aug 2016 11:00:02 -0700 Subject: [PATCH 09/10] Fix #536 by changing workspace's dispose method to remove the injectDiv wrapper of the svg. The wrapper div was introduced in #512. --- core/workspace_svg.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/workspace_svg.js b/core/workspace_svg.js index 8e06fb415..bddde64f1 100644 --- a/core/workspace_svg.js +++ b/core/workspace_svg.js @@ -275,8 +275,9 @@ Blockly.WorkspaceSvg.prototype.dispose = function() { this.zoomControls_ = null; } if (!this.options.parentWorkspace) { - // Top-most workspace. Dispose of the SVG too. - goog.dom.removeNode(this.getParentSvg()); + // Top-most workspace. Dispose of the div that the + // svg is injected into (i.e. injectionDiv). + goog.dom.removeNode(this.getParentSvg().parentNode); } if (this.resizeHandlerWrapper_) { Blockly.unbindEvent_(this.resizeHandlerWrapper_); From 3748b6f8bd282ecbc8d7b5f53c4205359127f256 Mon Sep 17 00:00:00 2001 From: Tina Quach Date: Fri, 12 Aug 2016 16:17:18 -0500 Subject: [PATCH 10/10] refactored onTab to call a single function for visually turning tabs on or off depending on whether it is selected (#544) --- demos/blocklyfactory/app_controller.js | 33 +++++++++++++------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/demos/blocklyfactory/app_controller.js b/demos/blocklyfactory/app_controller.js index 2c195d510..43f66bba6 100644 --- a/demos/blocklyfactory/app_controller.js +++ b/demos/blocklyfactory/app_controller.js @@ -249,8 +249,6 @@ AppController.prototype.makeTabClickHandler_ = function(tabName) { /** * Called on each tab click. Hides and shows specific content based on which tab * (Block Factory, Workspace Factory, or Exporter) is selected. - * - * TODO(quachtina96): Refactor the code to avoid repetition of addRemove. */ AppController.prototype.onTab = function() { // Get tab div elements. @@ -258,12 +256,10 @@ AppController.prototype.onTab = function() { var exporterTab = this.tabMap['EXPORTER']; var workspaceFactoryTab = this.tabMap['WORKSPACE_FACTORY']; - if (this.selectedTab == 'EXPORTER') { - // Turn exporter tab on and other tabs off. - goog.dom.classlist.addRemove(exporterTab, 'taboff', 'tabon'); - goog.dom.classlist.addRemove(blockFactoryTab, 'tabon', 'taboff'); - goog.dom.classlist.addRemove(workspaceFactoryTab, 'tabon', 'taboff'); + // Turn selected tab on and other tabs off. + this.styleTabs_(); + if (this.selectedTab == 'EXPORTER') { // Update toolbox to reflect current block library. this.exporter.updateToolbox(); @@ -272,20 +268,11 @@ AppController.prototype.onTab = function() { BlockFactory.hide('workspaceFactoryContent'); } else if (this.selectedTab == 'BLOCK_FACTORY') { - // Turn factory tab on and other tabs off. - goog.dom.classlist.addRemove(blockFactoryTab, 'taboff', 'tabon'); - goog.dom.classlist.addRemove(exporterTab, 'tabon', 'taboff'); - goog.dom.classlist.addRemove(workspaceFactoryTab, 'tabon', 'taboff'); - // Hide container of exporter. BlockFactory.hide('blockLibraryExporter'); BlockFactory.hide('workspaceFactoryContent'); } else if (this.selectedTab == 'WORKSPACE_FACTORY') { - console.log('workspaceFactoryTab'); - goog.dom.classlist.addRemove(workspaceFactoryTab, 'taboff', 'tabon'); - goog.dom.classlist.addRemove(blockFactoryTab, 'tabon', 'taboff'); - goog.dom.classlist.addRemove(exporterTab, 'tabon', 'taboff'); // Hide container of exporter. BlockFactory.hide('blockLibraryExporter'); // Show workspace factory container. @@ -296,6 +283,20 @@ AppController.prototype.onTab = function() { window.dispatchEvent(new Event('resize')); }; +/** + * Called on each tab click. Styles the tabs to reflect which tab is selected. + * @private + */ +AppController.prototype.styleTabs_ = function() { + for (var tabName in this.tabMap) { + if (this.selectedTab == tabName) { + goog.dom.classlist.addRemove(this.tabMap[tabName], 'taboff', 'tabon'); + } else { + goog.dom.classlist.addRemove(this.tabMap[tabName], 'tabon', 'taboff'); + } + } +}; + /** * Assign button click handlers for the exporter. */