diff --git a/.stylelintrc b/.stylelintrc index 65b06ed4..8adea27a 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -51,7 +51,7 @@ "property-case" : "lower", "plugin/no-unsupported-browser-features" : [true, { "severity": "warning", - "ignore": ["calc", "user-select-none", "multicolumn", "css-appearance", "border-radius", "pointer"] + "ignore": ["calc", "user-select-none", "multicolumn", "css-appearance", "border-radius", "pointer", "css-filters"] }], "rule-empty-line-before" : ["always", { "ignore": ["after-comment"], "except": ["inside-block-and-after-rule", "first-nested"] }], "selector-attribute-brackets-space-inside" : "never", diff --git a/src/assets/sass/base/mixins.scss b/src/assets/sass/base/mixins.scss new file mode 100644 index 00000000..58357551 --- /dev/null +++ b/src/assets/sass/base/mixins.scss @@ -0,0 +1,7 @@ +@mixin vendor($property, $value) { + $prefixes: ('moz', 'webkit', 'ms', 'o'); + @each $prefix in $prefixes { + -#{$prefix}-#{$property}: #{$value}; + } + #{$property}: #{$value}; +} \ No newline at end of file diff --git a/src/assets/sass/scratch/_blockly-toolbox.scss b/src/assets/sass/scratch/_blockly-toolbox.scss deleted file mode 100644 index b4c024a3..00000000 --- a/src/assets/sass/scratch/_blockly-toolbox.scss +++ /dev/null @@ -1,115 +0,0 @@ -@import 'color'; - -.blocklyText { - fill: $black !important; -} - -.blocklyTreeSelected .blocklyTreeLabel { - color: $white !important; -} - -.blocklyTreeRow.blocklyTreeSelected .blocklyTreeIcon.blocklyTreeIconOpen, -.blocklyTreeRow.blocklyTreeSelected .blocklyTreeIcon.blocklyTreeIconClosedLtr { - filter: invert(100%); /* stylelint-disable-line */ -} - -.blocklyTreeIcon.blocklyTreeIconOpen { - // background-image: url('dist/media/image/down-arrow.svg') !important; - background-position: -0.19em -0.38em !important; -} - -.blocklyTreeIcon.blocklyTreeIconClosedLtr { - // background-image: url('dist/media/image/down-arrow.svg') !important; - background-position: -0.19em -0.38em !important; -} - -.blocklyTreeRow.blocklyTreeSelected { - background-color: #2a3052 !important; -} - -.blocklyTreeRow:not(.blocklyTreeSelected):hover { - background-color: $brand-dark-gray !important; -} - -.blocklyToolboxDiv .blocklyTreeRow { - color: $black; -} - -.blocklyEditableText tspan { - fill: $black !important; -} - -.blocklyEditableText rect { - fill-opacity: 1 !important; -} - -.blocklyFlyoutBackground { - fill: #fcfcfc !important; - fill-opacity: 1 !important; - stroke: $brand-dark-gray; - stroke-width: 0.15em; - stroke-linecap: round; -} - -.blocklyToolboxDiv { - border-width: thin; - color: $brand-dark-gray; - border-right: 0.06em solid; - width: 11em; - z-index: 99 !important; - left: -100%; - overflow-x: hidden !important; -} - -.blocklyIconShape { - fill: #2a3052 !important; -} - -.blocklyIconGroup:not(:hover), -.blocklyIconGroupReadonly { - opacity: 1 !important; -} - -.blocklySelected > .blocklyPath { - stroke: #fc3; - stroke-width: 2px; -} - -.blocklyTreeRow { - // box-shadow: inset 0 -0.06em 0 0 #dedede; - margin-bottom: 0px !important; - height: 1.9em !important; - padding-top: 0.25em; -} - -.blocklySvg { - background-color: $white !important; - position: absolute; -} - -.scratchCategoryMenu { - width: 100%; - color: #575e75; - font-size: 1em; - user-select: none; -} - -.scratchCategoryMenuItem { - padding: 0.5em 1em; - cursor: pointer; - text-align: left; -} - -.scratchCategoryItemBubble { - display: none; -} - -#scratch_area { - position: absolute; - height: 100%; - width: 100%; -} - -#scratch_div { - position: absolute; -} \ No newline at end of file diff --git a/src/assets/sass/scratch/_flyout.scss b/src/assets/sass/scratch/_flyout.scss index 9219903d..d43f3ea1 100644 --- a/src/assets/sass/scratch/_flyout.scss +++ b/src/assets/sass/scratch/_flyout.scss @@ -1,7 +1,11 @@ +@import '../base/mixins'; + +.blocklyFlyout { + @include vendor(filter, drop-shadow(0 10px 5px #999)); +} + .blocklyFlyoutBackground { height: calc(100% - 60px) !important; fill: #fff !important; fill-opacity: 0.95 !important; - stroke: #ebebeb; - stroke-width: 5px; -} \ No newline at end of file +} diff --git a/src/assets/sass/scratch/_toolbox.scss b/src/assets/sass/scratch/_toolbox.scss index 7d0b11e2..6b741eb7 100644 --- a/src/assets/sass/scratch/_toolbox.scss +++ b/src/assets/sass/scratch/_toolbox.scss @@ -1,3 +1,5 @@ +@import '../base/mixins'; + $category-colours: ( trade-definition: #303f9f, before-purchase : #00897b, @@ -30,7 +32,7 @@ $category-colours: ( display: flex; flex-direction: column; margin-top: 20px; - max-height: calc(100% - 40px) !important; + max-height: calc(100vh - 100px); overflow: hidden; position: absolute; user-select: none; @@ -63,8 +65,8 @@ $category-colours: ( position: absolute; top: 0.6em; right: 0.6em; - transform: rotate(90deg); - transition: transform 0.25s ease; + @include vendor(transform, rotate(90deg)); + @include vendor(transition, transform 0.25s ease); } &__item { position: relative; @@ -90,7 +92,7 @@ $category-colours: ( } #{$toolbox}__arrow { position: relative; - transform: rotate(270deg); + @include vendor(transform, rotate(270deg)); margin-top: -2px; top: 0; right: -2px; diff --git a/src/assets/sass/scratch/_workspace.scss b/src/assets/sass/scratch/_workspace.scss index 31bc0dec..aa99b0a6 100644 --- a/src/assets/sass/scratch/_workspace.scss +++ b/src/assets/sass/scratch/_workspace.scss @@ -12,4 +12,8 @@ .blocklyText { fill: #000 !important; +} + +.blocklyMainWorkspaceScrollbar { + display: none; } \ No newline at end of file diff --git a/src/scratch/hooks/block_svg.js b/src/scratch/hooks/block_svg.js index 771aa8a6..9ff8f019 100644 --- a/src/scratch/hooks/block_svg.js +++ b/src/scratch/hooks/block_svg.js @@ -1,6 +1,10 @@ -/* eslint-disable func-names, no-underscore-dangle */ import { translate } from '../../utils/lang/i18n'; +/* eslint-disable func-names, no-underscore-dangle */ + +// deriv-bot: Blockly value, Scratch resets this to 0, req for correct spacing in flyout. +Blockly.BlockSvg.TAB_WIDTH = 8; + /** * Set whether the block is disabled or not. * @param {boolean} disabled True if disabled. diff --git a/src/scratch/hooks/flyout_base.js b/src/scratch/hooks/flyout_base.js index 2ab51744..cdb83795 100644 --- a/src/scratch/hooks/flyout_base.js +++ b/src/scratch/hooks/flyout_base.js @@ -1,10 +1,34 @@ /* eslint-disable func-names, no-underscore-dangle */ + +/** + * Corner radius of the flyout background. + * @type {number} + * @const + */ +Blockly.Flyout.prototype.CORNER_RADIUS = 10; + /** * Margin around the edges of the blocks in the flyout. * @type {number} * @const */ -Blockly.Flyout.prototype.MARGIN = 24; +Blockly.Flyout.prototype.MARGIN = 30; + +/** + * Top/bottom padding between scrollbar and edge of flyout background. + * deriv-bot: Should be equal to CORNER_RADIUS + * @type {number} + * @const + */ +Blockly.Flyout.prototype.SCROLLBAR_PADDING = 20; + +/** + * The fraction of the distance to the scroll target to move the flyout on + * each animation frame, when auto-scrolling. Values closer to 1.0 will make + * the scroll animation complete faster. Use 1.0 for no animation. + * @type {number} + */ +Blockly.Flyout.prototype.scrollAnimationFraction = 1.0; /** * Update the view based on coordinates calculated in position(). @@ -13,26 +37,31 @@ Blockly.Flyout.prototype.MARGIN = 24; * @param {number} x The computed x origin of the flyout's SVG group. * @param {number} y The computed y origin of the flyout's SVG group. * @protected - * deriv-bot: Imported from Blockly, used in flyout_vertical.js */ Blockly.Flyout.prototype.positionAt_ = function(width, height, x, y) { this.svgGroup_.setAttribute('width', width); this.svgGroup_.setAttribute('height', height); + if (this.svgGroup_.tagName === 'svg') { const transform = `translate(${x}px,${y}px)`; + Blockly.utils.setCssTransform(this.svgGroup_, transform); } else { // IE and Edge don't support CSS transforms on SVG elements so // it's important to set the transform on the SVG element itself const transform = `translate(${x},${y})`; + this.svgGroup_.setAttribute('transform', transform); } // Update the scrollbar (if one exists). if (this.scrollbar_) { + const newX = x - this.ARROW_SIZE; + // Set the scrollbars origin to be the top left of the flyout. - this.scrollbar_.setOrigin(x, y); + this.scrollbar_.setOrigin(newX, y); this.scrollbar_.resize(); + // Set the position again so that if the metrics were the same (and the // resize failed) our position is still updated. this.scrollbar_.setPosition_(this.scrollbar_.position_.x, this.scrollbar_.position_.y); diff --git a/src/scratch/hooks/flyout_vertical.js b/src/scratch/hooks/flyout_vertical.js index 9952bf3e..873ea5f2 100644 --- a/src/scratch/hooks/flyout_vertical.js +++ b/src/scratch/hooks/flyout_vertical.js @@ -1,10 +1,111 @@ /* eslint-disable func-names, no-underscore-dangle */ -// deriv-bot: Blockly value, Scratch resets this to, req for correct spacing in flyout. -Blockly.BlockSvg.TAB_WIDTH = 8; + +// deriv-bot: We render an arrow pointing to the current category. This value +// determines the size of that arrow. +Blockly.VerticalFlyout.prototype.ARROW_SIZE = 15; + +/** + * Return an object with all the metrics required to size scrollbars for the + * flyout. The following properties are computed: + * .viewHeight: Height of the visible rectangle, + * .viewWidth: Width of the visible rectangle, + * .contentHeight: Height of the contents, + * .contentWidth: Width of the contents, + * .viewTop: Offset of top edge of visible rectangle from parent, + * .contentTop: Offset of the top-most content from the y=0 coordinate, + * .absoluteTop: Top-edge of view. + * .viewLeft: Offset of the left edge of visible rectangle from parent, + * .contentLeft: Offset of the left-most content from the x=0 coordinate, + * .absoluteLeft: Left-edge of view. + * @return {Object} Contains size and position metrics of the flyout. + * @private + */ +Blockly.VerticalFlyout.prototype.getMetrics_ = function() { + if (!this.isVisible()) { + // Flyout is hidden. + return null; + } + + const optionBox = this.getContentBoundingBox_(); + + // Padding for the end of the scrollbar. + const absoluteTop = this.SCROLLBAR_PADDING; + const absoluteLeft = 0; + + // Add padding to the bottom of the flyout, so we can scroll to the top of + // the last category. deriv-bot: Add some extra padding. + const contentHeight = (optionBox.height + (this.SCROLLBAR_PADDING * 2)) * this.workspace_.scale; + const bottomPadding = this.MARGIN; + const metrics = { + viewHeight : this.height_ - this.MARGIN * 2, + viewWidth : this.getWidth() - this.SCROLLBAR_PADDING, + contentHeight: contentHeight + bottomPadding, + contentWidth : optionBox.width * this.workspace_.scale + 2 * this.MARGIN, + viewTop : -this.workspace_.scrollY + optionBox.y, + viewLeft : -this.workspace_.scrollX, + contentTop : optionBox.y, + contentLeft : optionBox.x, + absoluteTop, + absoluteLeft, + }; + return metrics; +}; + +/** + * Used to put the blocks on top of the flyout. + * Lay out the blocks in the flyout. + * @param {!Array.} contents The blocks and buttons to lay out. + * @param {!Array.} gaps The visible gaps between blocks. + * @private + */ +Blockly.VerticalFlyout.prototype.layout_ = function(contents, gaps) { + // Take workspace scale into consideration for correct positioning + const cursorX = (this.MARGIN / this.targetWorkspace_.scale) + this.CORNER_RADIUS; + let cursorY = this.CORNER_RADIUS + this.SCROLLBAR_PADDING; + + contents.forEach((item, index) => { + if (item.type === 'block') { + const { block } = item; + const root = block.getSvgRoot(); + const blockHW = block.getHeightWidth(); + + // Mark blocks as being inside a flyout. This is used to detect and + // prevent the closure of the flyout if the user right-clicks on such a + // block. + const allBlocks = block.getDescendants(false); + allBlocks.forEach(child => child.isInFlyout = true); + + // The block moves a bit extra for the hat, but the block's rectangle + // doesn't. That's because the hat actually extends up from 0. + const extra_hat_height = (block.startHat_ ? Blockly.BlockSvg.START_HAT_HEIGHT : 0); + const rect = this.createRect_(block, cursorX, cursorY, blockHW, index); + + block.moveBy(cursorX, cursorY + extra_hat_height); + this.addBlockListeners_(root, block, rect); + cursorY += blockHW.height + gaps[index] + extra_hat_height; + + } else if (item.type === 'button') { + // For both buttons and labels + const { button } = item; + const buttonSvg = button.createDom(); + + button.moveTo(cursorX, cursorY); + button.show(); + + // Clicking on a flyout button or label is a lot like clicking on the + // flyout background. + this.listeners_.push( + Blockly.bindEventWithChecks_(buttonSvg, 'mousedown', this, this.onMouseDown_) + ); + + this.buttons_.push(button); + cursorY += button.height + gaps[index]; + } + }); +}; /** * Move the flyout to the edge of the workspace. - * deriv-bot: Custom dimensions for flyout & support dynamic widths. */ Blockly.VerticalFlyout.prototype.position = function() { if (!this.isVisible()) { @@ -16,38 +117,53 @@ Blockly.VerticalFlyout.prototype.position = function() { // Hidden components will return null. return; } + // Consider the stroke thickness for spacing. + if (this.stroke_thickness_ === undefined) { + const css_style = window.getComputedStyle(this.svgBackground_); + this.stroke_thickness_ = (css_style.stroke !== 'none' && parseInt(css_style.strokeWidth) || 0); + } + // Toolbox bounds shouldn't change, keep in memory instead of continuously accessing DOM. + if (!this.toolbox_bounds_) { + this.toolbox_bounds_ = this.parentToolbox_.HtmlDiv.getBoundingClientRect(); + } + + const fullscreenFlyoutHeight = targetWorkspaceMetrics.viewHeight - (this.toolbox_bounds_.top * 2); + const toolboxFlyoutHeight = this.toolbox_bounds_.height + (this.MARGIN * 2) + (this.SCROLLBAR_PADDING * 2); + const flyoutContentHeight = this.getMetrics_().contentHeight + (this.toolbox_bounds_.top * 2); - // Record the height for Blockly.Flyout.getMetrics_ - // deriv-bot: Set to workspace height - this.height_ = targetWorkspaceMetrics.viewHeight - 40; + this.height_ = Math.min( + Math.max(toolboxFlyoutHeight, flyoutContentHeight), + fullscreenFlyoutHeight + ); - const edgeWidth = this.width_ - this.CORNER_RADIUS; - // deriv-bot: use this.height_ instead of targetWorkspaceMetrics.viewHeight - const edgeHeight = this.height_ - 2 * this.CORNER_RADIUS; + // edgeWidth and edgeHeight control the background SVG + const edgeHeight = + this.height_ + - (this.stroke_thickness_ * 2) + - (this.CORNER_RADIUS * 2) + - (this.toolbox_bounds_.top / 2); + + const edgeWidth = + this.width_ + - (this.stroke_thickness_ * 2) + - (this.CORNER_RADIUS * 2) + - this.ARROW_SIZE + - Blockly.Scrollbar.scrollbarThickness; this.setBackgroundPath_(edgeWidth, edgeHeight); // deriv-bot: Ensure flyout is rendered at same y-point as parent toolbox. - const y = this.parentToolbox_.HtmlDiv.offsetTop; - let x; + const y = this.toolbox_bounds_.top; + let x = 0; // If this flyout is the toolbox flyout. if (this.targetWorkspace_.toolboxPosition === this.toolboxPosition_) { // If there is a category toolbox. if (targetWorkspaceMetrics.toolboxWidth) { - if (this.toolboxPosition_ === Blockly.TOOLBOX_AT_LEFT) { - // deriv-bot: Allow for dynamic toolbox width. - x = this.parentToolbox_.HtmlDiv.clientWidth + Blockly.BlockSvg.TAB_WIDTH; - } else { - x = targetWorkspaceMetrics.viewWidth - this.width_; - } - } else if (this.toolboxPosition_ === Blockly.TOOLBOX_AT_LEFT) { - x = 0; - } else { - x = targetWorkspaceMetrics.viewWidth; + // deriv-bot: Allow for dynamic toolbox width, specifies where to position + // the flyout. + x = this.toolbox_bounds_.width + Blockly.BlockSvg.TAB_WIDTH; } - } else if (this.toolboxPosition_ === Blockly.TOOLBOX_AT_LEFT) { - x = 0; } else { // Because the anchor point of the flyout is on the left, but we want // to align the right edge of the flyout with the right edge of the @@ -56,21 +172,21 @@ Blockly.VerticalFlyout.prototype.position = function() { x = targetWorkspaceMetrics.viewWidth + targetWorkspaceMetrics.absoluteLeft - this.width_; } - this.positionAt_(this.width_, this.height_, x, y); + const svg_width = this.width_ - this.MARGIN + this.CORNER_RADIUS; + const svg_height = this.height_ - this.MARGIN + this.CORNER_RADIUS; + + this.positionAt_(svg_width, svg_height, x, y); }; /** * Compute width of flyout. Position mat under each block. - * For RTL: Lay out the blocks and buttons to be right-aligned. - * deriv-bot: Allow for dynamic width flyout. * @private */ Blockly.VerticalFlyout.prototype.reflowInternal_ = function() { this.workspace_.scale = this.targetWorkspace_.scale; let flyoutWidth = 0; - const blocks = this.workspace_.getTopBlocks(false); - blocks.forEach(block => { + this.workspace_.getTopBlocks(false).forEach(block => { let { width } = block.getHeightWidth(); if (block.outputConnection) { width -= Blockly.BlockSvg.TAB_WIDTH; @@ -82,33 +198,58 @@ Blockly.VerticalFlyout.prototype.reflowInternal_ = function() { flyoutWidth = Math.max(flyoutWidth, button.width); }); - flyoutWidth += this.MARGIN * 1.5 + Blockly.BlockSvg.TAB_WIDTH; + // Consider workspace scale when calculating margin, also consider the category arrow. + flyoutWidth += ((this.MARGIN * 1.5 / this.workspace_.scale) + Blockly.BlockSvg.TAB_WIDTH); + flyoutWidth += (this.CORNER_RADIUS * 2); flyoutWidth *= this.workspace_.scale; + flyoutWidth += this.ARROW_SIZE; flyoutWidth += Blockly.Scrollbar.scrollbarThickness; if (this.width_ !== flyoutWidth) { - blocks.forEach(block => { - if (this.RTL) { - // With the flyoutWidth known, right-align the blocks. - const oldX = block.getRelativeToSurfaceXY().x; - const newX = flyoutWidth / this.workspace_.scale - this.MARGIN - Blockly.BlockSvg.TAB_WIDTH; - - block.moveBy(newX - oldX, 0); - } - }); - - if (this.RTL) { - // With the flyoutWidth known, right-align the buttons. - this.buttons_.forEach(button => { - const { y } = button.getPosition(); - const x = flyoutWidth / this.workspace_.scale - button.width - this.MARGIN - Blockly.BlockSvg.TAB_WIDTH; - - button.moveTo(x, y); - }); - } - // Record the width for .getMetrics_ and .position. this.width_ = flyoutWidth; this.position(); } }; + +/** + * Create and set the path for the visible boundaries of the flyout. + * @param {number} width The width of the flyout, not including the rounded corners. + * @param {number} height The height of the flyout, not including rounded corners. + * @private + */ +Blockly.VerticalFlyout.prototype.setBackgroundPath_ = function(width, height) { + const el_selected_category = this.parentToolbox_.selectedItem_.parentHtml_; + const category_bounds = el_selected_category.getBoundingClientRect(); + + // Starting position of the SVG + const start_x = this.ARROW_SIZE + this.CORNER_RADIUS + this.stroke_thickness_; + const start_y = this.stroke_thickness_; + + // Calculate the line between the each of the top & bottom rounded corners. + const flyout_rectangle_width = Math.abs(width + this.ARROW_SIZE - this.MARGIN); + const has_arrow = category_bounds.top < (height - this.ARROW_SIZE * 2); + const path = []; + + path.push('M', start_x, start_y); + path.push('h', flyout_rectangle_width); + path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1, this.CORNER_RADIUS, this.CORNER_RADIUS); + path.push('v', height - this.CORNER_RADIUS); + path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1, -this.CORNER_RADIUS, this.CORNER_RADIUS); + path.push('h', -flyout_rectangle_width); + path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1, -this.CORNER_RADIUS, -this.CORNER_RADIUS); + + if (has_arrow) { + const bottom_to_arrow = category_bounds.top + (category_bounds.height / 2) - 3; + + path.push('V', bottom_to_arrow); + path.push('l', -this.ARROW_SIZE, -this.ARROW_SIZE); + path.push('l', this.ARROW_SIZE, -this.ARROW_SIZE); + } + + path.push('V', this.stroke_thickness_ + this.CORNER_RADIUS); + path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1, this.CORNER_RADIUS, -this.CORNER_RADIUS); + path.push('z'); + + this.svgBackground_.setAttribute('d', path.join(' ')); +}; diff --git a/src/scratch/hooks/toolbox.js b/src/scratch/hooks/toolbox.js index 8e9b7cca..00464994 100644 --- a/src/scratch/hooks/toolbox.js +++ b/src/scratch/hooks/toolbox.js @@ -103,6 +103,16 @@ Blockly.Toolbox.CategoryMenu.prototype.createDom = function() { this.table = goog.dom.createDom('div', className); this.parentHtml_.appendChild(this.table); + + // Hide flyout on scrolling the toolbox category menu in + // order to ensure correct positioning of flyout. + this.table.addEventListener('scroll', () => { + const toolbox = this.parent_; + const flyout = toolbox.flyout_; + + toolbox.setSelectedItem(null); + flyout.hide(); + }); }; /** diff --git a/src/scratch/index.js b/src/scratch/index.js index 44d772e8..d7521feb 100644 --- a/src/scratch/index.js +++ b/src/scratch/index.js @@ -61,8 +61,10 @@ export const scratchWorkspaceInit = async (scratch_area_name, scratch_div_name) el_scratch_div.style.top = `${y}px`; el_scratch_div.style.width = `${scratch_area.offsetWidth}px`; el_scratch_div.style.height = `${scratch_area.offsetHeight}px`; - + Blockly.svgResize(workspace); + // eslint-disable-next-line no-underscore-dangle + workspace.toolbox_.flyout_.position(); }; // Resize workspace on workspace event, workaround for jumping workspace.