/// <summary> /// Destroy the widget and hide the div if it is being used by the specified /// object. /// </summary> /// <param name="oldOwner">The object that was using this container.</param> public static void hideIfOwner(object oldOwner) { if (WidgetDiv.owner_ == oldOwner) { WidgetDiv.hide(); } }
/// <summary> /// Show the inline free-text editor on top of the text. /// </summary> /// <param name="opt_quietInput">True if editor should be created without /// focus.Defaults to false.</param> public override void showEditor_(bool opt_quietInput) { this.workspace_ = (WorkspaceSvg)this.sourceBlock_.workspace; var quietInput = opt_quietInput || false; if (!quietInput && (goog.userAgent.MOBILE || goog.userAgent.ANDROID || goog.userAgent.IPAD)) { // Mobile browsers have issues with in-line textareas (focus & keyboards). var newValue = Window.Prompt(Msg.CHANGE_VALUE_TITLE, this.text_); if (this.sourceBlock_ != null) { newValue = this.callValidator(newValue); } this.setValue(newValue); return; } WidgetDiv.show(this, this.sourceBlock_.RTL, this.widgetDispose_()); var div = WidgetDiv.DIV; // Create the input. var htmlInput = (HTMLInputElement) goog.dom.createDom(goog.dom.TagName.INPUT, "blocklyHtmlInput"); htmlInput.SetAttribute("spellcheck", this.spellcheck_.ToString()); var fontSize = (FieldTextInput.FONTSIZE * this.workspace_.scale) + "pt"; div.Style.FontSize = fontSize; htmlInput.Style.FontSize = fontSize; /** @type {!HTMLInputElement} */ FieldTextInput.htmlInput_ = htmlInput; div.AppendChild(htmlInput); htmlInput.Value = htmlInput.DefaultValue = this.text_; htmlInput["oldValue_"] = null; this.validate_(); this.resizeEditor_(); if (!quietInput) { htmlInput.Focus(); htmlInput.Select(); } // Bind to keydown -- trap Enter without IME and Esc to hide. htmlInput["onKeyDownWrapper_"] = Core.bindEventWithChecks_(htmlInput, "keydown", this, new Action <KeyboardEvent>(this.onHtmlInputKeyDown_)); // Bind to keyup -- trap Enter; resize after every keystroke. htmlInput["onKeyUpWrapper_"] = Core.bindEventWithChecks_(htmlInput, "keyup", this, new Action <Event>(this.onHtmlInputChange_)); // Bind to keyPress -- repeatedly resize when holding down a key. htmlInput["onKeyPressWrapper_"] = Core.bindEventWithChecks_(htmlInput, "keypress", this, new Action <Event>(this.onHtmlInputChange_)); htmlInput["onWorkspaceChangeWrapper_"] = new Action <Events.Abstract>(this.resizeEditor_); this.workspace_.addChangeListener((Action <Events.Abstract>)htmlInput["onWorkspaceChangeWrapper_"]); }
/// <summary> /// Initialize and display the widget div. Close the old one if needed. /// </summary> /// <param name="newOwner">The object that will be using this container.</param> /// <param name="rtl">Right-to-left (true) or left-to-right (false).</param> /// <param name="dispose">Optional cleanup function to be run when the widget /// is closed.</param> public static void show(object newOwner, bool rtl, Action dispose) { WidgetDiv.hide(); WidgetDiv.owner_ = newOwner; WidgetDiv.dispose_ = dispose; // Temporarily move the widget to the top of the screen so that it does not // cause a scrollbar jump in Firefox when displayed. var xy = goog.style.getViewportPageOffset(Document.Instance); WidgetDiv.DIV.Style.Top = xy.y + "px"; WidgetDiv.DIV.Style.Direction = rtl ? Direction.Rtl : Direction.Ltr; WidgetDiv.DIV.Style.Display = Display.Block; }
/// <summary> /// Close tooltips, context menus, dropdown selections, etc. /// </summary> /// <param name="opt_allowToolbox">If true, don't close the toolbox.</param> public static void hideChaff(bool opt_allowToolbox = false) { Tooltip.hide(); WidgetDiv.hide(); if (!opt_allowToolbox) { var workspace = Core.getMainWorkspace(); if (workspace.toolbox_ != null && workspace.toolbox_.flyout_ != null && workspace.toolbox_.flyout_.autoClose) { workspace.toolbox_.clearSelection(); } } }
/// <summary> /// Destroy the widget and hide the div. /// </summary> public static void hide(Event e = null) { if (WidgetDiv.owner_ != null) { WidgetDiv.owner_ = null; WidgetDiv.DIV.Style.Display = Display.None; WidgetDiv.DIV.Style.Left = ""; WidgetDiv.DIV.Style.Top = ""; if (WidgetDiv.dispose_ != null) { WidgetDiv.dispose_(); } WidgetDiv.dispose_ = null; goog.dom.removeChildren(WidgetDiv.DIV); } }
/// <summary> /// Handle key down to the editor. /// </summary> /// <param name="e">Keyboard event.</param> private void onHtmlInputKeyDown_(KeyboardEvent e) { var htmlInput = FieldTextInput.htmlInput_; int tabKey = 9, enterKey = 13, escKey = 27; if (e.KeyCode == enterKey) { WidgetDiv.hide(); } else if (e.KeyCode == escKey) { htmlInput.Value = htmlInput.DefaultValue; WidgetDiv.hide(); } else if (e.KeyCode == tabKey) { WidgetDiv.hide(); this.sourceBlock_.tab(this, !e.ShiftKey); e.PreventDefault(); } }
/// <summary> /// When hovering over an element, schedule a tooltip to be shown. If a tooltip /// is already visible, hide it if the mouse strays out of a certain radius. /// </summary> /// <param name="e">Mouse event.</param> private static void onMouseMove_(MouseEvent e) { if (Tooltip.element_ == null || Tooltip.tooltip_ == null) { // No tooltip here to show. return; } else if (Core.dragMode_ != Core.DRAG_NONE) { // Don't display a tooltip during a drag. return; } else if (WidgetDiv.isVisible()) { // Don't display a tooltip if a widget is open (tooltip would be under it). return; } if (Tooltip.visible) { // Compute the distance between the mouse position when the tooltip was // shown and the current mouse position. Pythagorean theorem. var dx = Tooltip.lastX_ - e.PageX; var dy = Tooltip.lastY_ - e.PageY; if (System.Math.Sqrt(dx * dx + dy * dy) > Tooltip.RADIUS_OK) { Tooltip.hide(); } } else if (Tooltip.poisonedElement_ != Tooltip.element_) { // The mouse moved, clear any previously scheduled tooltip. Window.ClearTimeout(Tooltip.showPid_); // Maybe this time the mouse will stay put. Schedule showing of tooltip. Tooltip.lastX_ = e.PageX; Tooltip.lastY_ = e.PageY; Tooltip.showPid_ = Window.SetTimeout(Tooltip.show_, Tooltip.HOVER_MS); } }
/// <summary> /// Create a date picker under the date field. /// </summary> public override void showEditor_(bool opt_quietInput) { WidgetDiv.show(this, this.sourceBlock_.RTL, new Action(FieldDate.widgetDispose_)); // Create the date picker using Closure. FieldDate.loadLanguage_(); var picker = new goog.ui.DatePicker(); picker.setAllowNone(false); picker.setShowWeekNum(false); // Position the picker to line up with the field. // Record windowSize and scrollOffset before adding the picker. var windowSize = goog.dom.getViewportSize(); var scrollOffset = goog.style.getViewportPageOffset(Document.Instance); var xy = this.getAbsoluteXY_(); var borderBBox = this.getScaledBBox_(); var div = WidgetDiv.DIV; picker.render(div); picker.setDate(new Date(this.getValue())); // Record pickerSize after adding the date picker. var pickerSize = goog.style.getSize(picker.getElement()); // Flip the picker vertically if off the bottom. if (xy.y + pickerSize.height + borderBBox.height >= windowSize.height + scrollOffset.y) { xy.y -= pickerSize.height - 1; } else { xy.y += borderBBox.height - 1; } if (this.sourceBlock_.RTL) { xy.x += borderBBox.width; xy.x -= pickerSize.width; // Don't go offscreen left. if (xy.x < scrollOffset.x) { xy.x = scrollOffset.x; } } else { // Don't go offscreen right. if (xy.x > windowSize.width + scrollOffset.x - pickerSize.width) { xy.x = windowSize.width + scrollOffset.x - pickerSize.width; } } WidgetDiv.position(xy.x, xy.y, windowSize, scrollOffset, this.sourceBlock_.RTL); // Configure event handler. var thisField = this; FieldDate.changeEventKey_ = goog.events.listen(picker, goog.ui.DatePicker.Events.CHANGE, new Action <Bridge.Html5.Event>((e) => { var date = e.Date != null ? e.Date.ToIsoString(true) : ""; WidgetDiv.hide(); if (thisField.sourceBlock_ != null) { // Call any validation function, and allow it to override. date = thisField.callValidator(date); } thisField.setValue(date); })); }
/// <summary> /// Close the colour picker if this input is being deleted. /// </summary> public override void dispose() { WidgetDiv.hideIfOwner(this); base.dispose(); }
/// <summary> /// Create a main workspace and add it to the SVG. /// </summary> /// <param name="svg">SVG element with pattern defined.</param> /// <param name="options">Dictionary of options.</param> /// <returns>Newly created main workspace.</returns> private static WorkspaceSvg createMainWorkspace_(SVGElement svg, Options options) { options.parentWorkspace = null; var mainWorkspace = new WorkspaceSvg(options); mainWorkspace.scale = options.zoomOptions.startScale; svg.AppendChild(mainWorkspace.createDom("blocklyMainBackground")); // A null translation will also apply the correct initial scale. mainWorkspace.translate(0, 0); mainWorkspace.markFocused(null); if (!options.readOnly && !options.hasScrollbars) { var workspaceChanged = new Action <Events.Abstract>((e) => { if (Core.dragMode_ == Core.DRAG_NONE) { var metrics = mainWorkspace.getMetrics(); var edgeLeft = metrics.viewLeft + metrics.absoluteLeft; var edgeTop = metrics.viewTop + metrics.absoluteTop; if (metrics.contentTop < edgeTop || metrics.contentTop + metrics.contentHeight > metrics.viewHeight + edgeTop || metrics.contentLeft < (options.RTL ? metrics.viewLeft : edgeLeft) || metrics.contentLeft + metrics.contentWidth > (options.RTL ? metrics.viewWidth : metrics.viewWidth + edgeLeft)) { // One or more blocks may be out of bounds. Bump them back in. var MARGIN = 25; var blocks = mainWorkspace.getTopBlocks(false); foreach (var block in blocks) { var blockXY = block.getRelativeToSurfaceXY(); var blockHW = ((BlockSvg)block).getHeightWidth(); // Bump any block that's above the top back inside. var overflowTop = edgeTop + MARGIN - blockHW.height - blockXY.y; if (overflowTop > 0) { block.moveBy(0, overflowTop); } // Bump any block that's below the bottom back inside. var overflowBottom = edgeTop + metrics.viewHeight - MARGIN - blockXY.y; if (overflowBottom < 0) { block.moveBy(0, overflowBottom); } // Bump any block that's off the left back inside. var overflowLeft = MARGIN + edgeLeft - blockXY.x - (options.RTL ? 0 : blockHW.width); if (overflowLeft > 0) { block.moveBy(overflowLeft, 0); } // Bump any block that's off the right back inside. var overflowRight = edgeLeft + metrics.viewWidth - MARGIN - blockXY.x + (options.RTL ? blockHW.width : 0); if (overflowRight < 0) { block.moveBy(overflowRight, 0); } } } } }); mainWorkspace.addChangeListener(workspaceChanged); } // The SVG is now fully assembled. Core.svgResize(mainWorkspace); WidgetDiv.createDom(); Tooltip.createDom(); return(mainWorkspace); }
/// <summary> /// Create a dropdown menu under the text. /// </summary> public override void showEditor_(bool opt_quietInput) { WidgetDiv.show(this, this.sourceBlock_.RTL, null); var thisField = this; var callback = new Action <goog.events.Event>((e) => { var menuItem = (goog.ui.MenuItem)e.target; if (menuItem != null) { var value = menuItem.getValue(); if (thisField.sourceBlock_ != null) { // Call any validation function, and allow it to override. value = thisField.callValidator(value); } if (value != null) { thisField.setValue(value); } } WidgetDiv.hideIfOwner(thisField); }); var menu = new goog.ui.Menu(); menu.setRightToLeft(this.sourceBlock_.RTL); var options = this.getOptions_(); for (var i = 0; i < options.Length; i++) { var text = options[i].text; // Human-readable text. var value = options[i].value; // Language-neutral value. var menuItem = new goog.ui.MenuItem(text); menuItem.setRightToLeft(this.sourceBlock_.RTL); menuItem.setValue(value); menuItem.setCheckable(true); menu.addChild(menuItem, true); menuItem.setChecked(value == this.value_); } // Listen for mouse/keyboard events. goog.events.listen(menu, goog.ui.Component.EventType.ACTION, callback); // Listen for touch events (why doesn't Closure handle this already?). var callbackTouchStart = new Action <goog.events.BrowserEvent>((e) => { var control = menu.getOwnerControl((Node)e.target); // Highlight the menu item. control.handleMouseDown(e); }); var callbackTouchEnd = new Action <goog.events.Event>((e) => { var control = menu.getOwnerControl((Node)e.target); // Activate the menu item. control.performActionInternal(e); }); menu.getHandler().listen(menu.getElement(), goog.events.EventType.TOUCHSTART, callbackTouchStart); menu.getHandler().listen(menu.getElement(), goog.events.EventType.TOUCHEND, callbackTouchEnd); // Record windowSize and scrollOffset before adding menu. var windowSize = goog.dom.getViewportSize(); var scrollOffset = goog.style.getViewportPageOffset(Document.Instance); var xy = this.getAbsoluteXY_(); var borderBBox = this.getScaledBBox_(); var div = WidgetDiv.DIV; menu.render(div); var menuDom = menu.getElement(); Core.addClass_(menuDom, "blocklyDropdownMenu"); // Record menuSize after adding menu. var menuSize = goog.style.getSize(menuDom); // Recalculate height for the total content, not only box height. menuSize.height = menuDom.ScrollHeight; // Position the menu. // Flip menu vertically if off the bottom. if (xy.y + menuSize.height + borderBBox.height >= windowSize.height + scrollOffset.y) { xy.y -= menuSize.height + 2; } else { xy.y += borderBBox.height; } if (this.sourceBlock_.RTL) { xy.x += borderBBox.width; xy.x += FieldDropdown.CHECKMARK_OVERHANG; // Don't go offscreen left. if (xy.x < scrollOffset.x + menuSize.width) { xy.x = scrollOffset.x + menuSize.width; } } else { xy.x -= FieldDropdown.CHECKMARK_OVERHANG; // Don't go offscreen right. if (xy.x > windowSize.width + scrollOffset.x - menuSize.width) { xy.x = windowSize.width + scrollOffset.x - menuSize.width; } } WidgetDiv.position(xy.x, xy.y, windowSize, scrollOffset, this.sourceBlock_.RTL); menu.setAllowAutoFocus(true); menuDom.Focus(); }
/// <summary> /// Create a palette under the colour field. /// </summary> public override void showEditor_(bool opt_quietInput) { WidgetDiv.show(this, this.sourceBlock_.RTL, new Action(FieldColour.widgetDispose_)); // Create the palette using Closure. var picker = new goog.ui.ColorPicker(); picker.setSize(this.columns_ ?? FieldColour.COLUMNS); picker.setColors(new JsArray <string>(this.colours_ ?? FieldColour.COLOURS)); // Position the palette to line up with the field. // Record windowSize and scrollOffset before adding the palette. var windowSize = goog.dom.getViewportSize(); var scrollOffset = goog.style.getViewportPageOffset(Document.Instance); var xy = this.getAbsoluteXY_(); var borderBBox = this.getScaledBBox_(); var div = WidgetDiv.DIV; picker.render(div); picker.setSelectedColor(this.getValue()); // Record paletteSize after adding the palette. var paletteSize = goog.style.getSize(picker.getElement()); // Flip the palette vertically if off the bottom. if (xy.y + paletteSize.height + borderBBox.height >= windowSize.height + scrollOffset.y) { xy.y -= paletteSize.height - 1; } else { xy.y += borderBBox.height - 1; } if (this.sourceBlock_.RTL) { xy.x += borderBBox.width; xy.x -= paletteSize.width; // Don't go offscreen left. if (xy.x < scrollOffset.x) { xy.x = scrollOffset.x; } } else { // Don't go offscreen right. if (xy.x > windowSize.width + scrollOffset.x - paletteSize.width) { xy.x = windowSize.width + scrollOffset.x - paletteSize.width; } } WidgetDiv.position(xy.x, xy.y, windowSize, scrollOffset, this.sourceBlock_.RTL); // Configure event handler. var thisField = this; FieldColour.changeEventKey_ = goog.events.listen(picker, goog.ui.ColorPicker.EventType.CHANGE, new Action <goog.events.Event>((e) => { string colour = ((goog.ui.ColorPicker)e.target).getSelectedColor() ?? "#000000"; WidgetDiv.hide(); if (thisField.sourceBlock_ != null) { // Call any validation function, and allow it to override. colour = thisField.callValidator(colour); } if (colour != null) { thisField.setValue(colour); } })); }
/// <summary> /// Construct the menu based on the list of options and show the menu. /// </summary> /// <param name="e">Mouse event.</param> /// <param name="options">Array of menu options.</param> /// <param name="rtl">True if RTL, false if LTR.</param> public static void show(MouseEvent e, ContextMenuOption[] options, bool rtl) { WidgetDiv.show(Core.ContextMenu, rtl, null); if (options.Length == 0) { ContextMenu.hide(); return; } /* Here's what one option object looks like: * {text: 'Make It So', * enabled: true, * callback: Blockly.MakeItSo} */ var menu = new goog.ui.Menu(); menu.setRightToLeft(rtl); foreach (var option in options) { var menuItem = new goog.ui.MenuItem(option.text); menuItem.setRightToLeft(rtl); menu.addChild(menuItem, true); menuItem.setEnabled(option.enabled); if (option.enabled) { goog.events.listen(menuItem, goog.ui.Component.EventType.ACTION, option.callback); } } goog.events.listen(menu, goog.ui.Component.EventType.ACTION, new Action <goog.events.Event>((ev) => ContextMenu.hide())); // Record windowSize and scrollOffset before adding menu. var windowSize = goog.dom.getViewportSize(); var scrollOffset = goog.style.getViewportPageOffset(Document.Instance); var div = WidgetDiv.DIV; menu.render(div); var menuDom = menu.getElement(); Core.addClass_(menuDom, "blocklyContextMenu"); // Prevent system context menu when right-clicking a Blockly context menu. Core.bindEventWithChecks_(menuDom, "contextmenu", null, new Action <Event>(Core.noEvent)); // Record menuSize after adding menu. var menuSize = goog.style.getSize(menuDom); // Position the menu. var x = e.ClientX + scrollOffset.x; var y = e.ClientY + scrollOffset.y; // Flip menu vertically if off the bottom. if (e.ClientY + menuSize.height >= windowSize.height) { y -= menuSize.height; } // Flip menu horizontally if off the edge. if (rtl) { if (menuSize.width >= e.ClientX) { x += menuSize.width; } } else { if (e.ClientX + menuSize.width >= windowSize.width) { x -= menuSize.width; } } WidgetDiv.position(x, y, windowSize, scrollOffset, rtl); menu.setAllowAutoFocus(true); // 1ms delay is required for focusing on context menus because some other // mouse event is still waiting in the queue and clears focus. Window.SetTimeout(() => { menuDom.Focus(); }, 1); ContextMenu.currentBlock = null; // May be set by Blockly.Block. }
/// <summary> /// Hide the context menu. /// </summary> public static void hide() { WidgetDiv.hideIfOwner(Core.ContextMenu); ContextMenu.currentBlock = null; }