diff --git a/src/ColumnsBlock/ColumnsBlockEdit.jsx b/src/ColumnsBlock/ColumnsBlockEdit.jsx index 3ff35ce..baafbb5 100644 --- a/src/ColumnsBlock/ColumnsBlockEdit.jsx +++ b/src/ColumnsBlock/ColumnsBlockEdit.jsx @@ -12,6 +12,7 @@ import { connect } from 'react-redux'; import { BlocksForm } from '@plone/volto/components'; import { Button } from 'semantic-ui-react'; import config from '@plone/volto/registry'; +import cx from 'classnames'; import { ColumnsBlockSchema } from './schema'; import { @@ -345,7 +346,10 @@ class ColumnsBlockEdit extends React.Component { {columnList.map(([colId, column], index) => ( { + return `${v?.[side] ? `${v[side]}${v.unit ? v.unit : 'px'}` : '0'}`; +}; + +const getSides = (v) => { + return `${getSide('top', v)} ${getSide('right', v)} ${getSide( + 'bottom', + v, + )} ${getSide('left', v)}`; +}; + const ColumnsBlockView = (props) => { const { gridSizes } = config.blocks.blocksConfig[COLUMNSBLOCK]; const { data = {}, gridSize = 12, gridCols = [] } = props.data; @@ -33,7 +44,17 @@ const ColumnsBlockView = (props) => { )} {...getStyle(column.settings || {})} > - +
+ +
); })} diff --git a/src/Styles/schema.js b/src/Styles/schema.js index e31c06d..ce95058 100644 --- a/src/Styles/schema.js +++ b/src/Styles/schema.js @@ -4,7 +4,17 @@ export const StyleSchema = () => ({ { id: 'default', title: 'Style', - fields: ['backgroundColor', 'grid_vertical_align', 'column_class'], + fields: ['grid_vertical_align'], + }, + { + id: 'styling', + title: 'Styling', + fields: ['backgroundColor', 'padding'], + }, + { + id: 'advanced', + title: 'Advanced', + fields: ['column_class'], }, ], properties: { @@ -21,6 +31,10 @@ export const StyleSchema = () => ({ ['top', 'Top'], ], }, + padding: { + title: 'Padding', + widget: 'quad_size', + }, column_class: { title: 'Custom CSS Class', description: 'A custom CSS class, aplicable to this column', diff --git a/src/Widgets/QuadSize.jsx b/src/Widgets/QuadSize.jsx new file mode 100644 index 0000000..54cc90d --- /dev/null +++ b/src/Widgets/QuadSize.jsx @@ -0,0 +1,151 @@ +import React from 'react'; +import { Field, FormFieldWrapper } from '@plone/volto/components'; +import { Grid } from 'semantic-ui-react'; +import { Slider } from './Slider'; + +const fields = { + unitField: { + title: 'Unit', + columns: 2, + placeholder: 'Unit', + defaultValue: 'px', + choices: [ + ['px', 'px'], + ['%', 'percentage'], + ['em', 'em'], + ['rem', 'rem'], + ], + }, +}; + +const getMax = (unit) => { + switch (unit) { + case '%': + return 100; + case 'px': + return 100; + case 'em': + return 24; + case 'rem': + return 24; + default: + return 10; + } +}; + +const QuadSizeWidget = (props) => { + const { + value = {}, + id, + onChange, + sliderSettings = { + max: 12, + min: 0, + step: 1, + start: 0, + }, + } = props; + const { + top = 0, + right = 0, + bottom = 0, + left = 0, + unit = 'px', + unlock = false, + } = value; + const settings = { + ...sliderSettings, + max: getMax(unit), + }; + // console.log('value', value); + + return ( + + onChange(id, { ...value, unit: val })} + value={value.unit || 'px'} + /> + + {unlock ? ( + + + + onChange(id, { ...value, top: val }), + ...settings, + }} + value={top} + extra={{top}} + /> + + + + onChange(id, { ...value, left: val }), + ...settings, + }} + value={left} + extra={{left}} + /> + + + onChange(id, { ...value, right: val }), + ...settings, + }} + value={right} + extra={{right}} + /> + + + + onChange(id, { ...value, bottom: val }), + ...settings, + }} + extra={{bottom}} + value={bottom} + /> + + + + ) : ( + { + onChange(id, { + ...value, + top: val, + left: val, + bottom: val, + right: val, + }); + }} + value={top} + title="Size" + widget="slider" + columns={2} + /> + )} + + onChange(id, { ...value, unlock: val })} + value={unlock} + title="Customize" + type="boolean" + columns={1} + /> + + ); +}; + +export default QuadSizeWidget; diff --git a/src/Widgets/Slider.jsx b/src/Widgets/Slider.jsx new file mode 100644 index 0000000..caebb73 --- /dev/null +++ b/src/Widgets/Slider.jsx @@ -0,0 +1,501 @@ +// Copied from MIT-licensed https://github.com/iozbeyli/react-semantic-ui-range + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import ReactDOM from 'react-dom'; +import { FormFieldWrapper } from '@plone/volto/components'; + +import styles from './range.css.js'; +import './range.css'; + +function isNumeric(str) { + if (typeof str != 'string') return false; // we only process strings! + return !isNaN(str); +} + +export class Slider extends Component { + constructor(props) { + super(props); + let value = this.props.value + ? this.props.value + : props.multiple + ? [...props.settings.start] + : props.settings.start; + this.state = { + value: value, + position: props.multiple ? [] : 0, + numberOfKnobs: props.multiple ? value.length : 1, + offset: 10, + precision: 0, + mouseDown: false, + showNumericInput: false, + }; + this.determinePosition = this.determinePosition.bind(this); + this.rangeMouseUp = this.rangeMouseUp.bind(this); + this.refresh = this.refresh.bind(this); + this.handleKnobClick = this.handleKnobClick.bind(this); + } + + componentDidMount() { + this.determinePrecision(); + const value = this.props.value ? this.props.value : this.state.value; + this.setValuesAndPositions(value, false); + window.addEventListener('mouseup', this.rangeMouseUp); + window.addEventListener('resize', this.refresh); + } + + refresh() { + const value = this.props.value ? this.props.value : this.state.value; + this.setValuesAndPositions(value, false); + } + + UNSAFE_componentWillReceiveProps(nextProps) { + const isValueUnset = + nextProps.value === null || nextProps.value === undefined; + + if (!isValueUnset && nextProps.value !== this.state.value) { + if (this.props.multiple) { + const different = this.isDifferentArrays( + nextProps.value, + this.state.value, + ); + if (different) { + this.setValuesAndPositions(nextProps.value, true); + } + } else { + this.setValuesAndPositions(nextProps.value, true); + } + } + } + + componentWillUnmount() { + this.inner = undefined; + this.innerLeft = undefined; + this.innerRight = undefined; + window.removeEventListener('mouseup', this.rangeMouseUp); + window.removeEventListener('resize', this.refresh); + } + + setValuesAndPositions(value, triggeredByUser) { + if (this.props.multiple) { + const positions = [...this.state.position]; + value.forEach((val, i) => { + this.setValue(val, triggeredByUser, i); + positions[i] = this.determinePosition(val); + }); + this.setState({ + position: positions, + }); + } else { + this.setValue(value, triggeredByUser); + this.setState({ + position: this.determinePosition(value), + }); + } + } + + isDifferentArrays(a, b) { + let different = false; + a.some((val, i) => { + if (val !== b[i]) { + different = true; + return true; + } + return false; + }); + return different; + } + + determinePosition(value) { + const trackLeft = ReactDOM.findDOMNode(this.track).getBoundingClientRect() + .left; + const innerLeft = ReactDOM.findDOMNode(this.inner).getBoundingClientRect() + .left; + const ratio = + (value - this.props.settings.min) / + (this.props.settings.max - this.props.settings.min); + const position = + Math.round(ratio * this.inner.offsetWidth) + + trackLeft - + innerLeft - + this.state.offset; + return position; + } + + determinePrecision() { + let split = String(this.props.settings.step).split('.'); + let decimalPlaces; + if (split.length === 2) { + decimalPlaces = split[1].length; + } else { + decimalPlaces = 0; + } + this.setState({ + precision: Math.pow(10, decimalPlaces), + }); + } + + determineValue(startPos, endPos, currentPos) { + let ratio = (currentPos - startPos) / (endPos - startPos); + let range = this.props.settings.max - this.props.settings.min; + let difference = + Math.round((ratio * range) / this.props.settings.step) * + this.props.settings.step; + // Use precision to avoid ugly Javascript floating point rounding issues + // (like 35 * .01 = 0.35000000000000003) + difference = + Math.round(difference * this.state.precision) / this.state.precision; + return difference + this.props.settings.min; + } + + determineKnob(position, value) { + if (!this.props.multiple) { + return 0; + } + if (position <= this.state.position[0]) { + return 0; + } + if (position >= this.state.position[this.state.numberOfKnobs - 1]) { + return this.state.numberOfKnobs - 1; + } + let index = 0; + + for (let i = 0; i < this.state.numberOfKnobs - 1; i++) { + if ( + position >= this.state.position[i] && + position < this.state.position[i + 1] + ) { + const distanceToSecond = Math.abs( + position - this.state.position[i + 1], + ); + const distanceToFirst = Math.abs(position - this.state.position[i]); + if (distanceToSecond <= distanceToFirst) { + return i + 1; + } else { + return i; + } + } + } + return index; + } + + setValue(value, triggeredByUser, knobIndex) { + if (typeof triggeredByUser === 'undefined') { + triggeredByUser = true; + } + const currentValue = this.props.multiple + ? this.state.value[knobIndex] + : this.state.value; + if (currentValue !== value) { + let newValue = []; + if (this.props.multiple) { + newValue = [...this.state.value]; + newValue[knobIndex] = value; + this.setState({ + value: newValue, + }); + } else { + newValue = value; + this.setState({ + value: value, + }); + } + if (this.props.settings.onChange) { + this.props.settings.onChange(newValue, { + triggeredByUser: triggeredByUser, + }); + } + } + } + + setValuePosition(value, triggeredByUser, knobIndex) { + if (this.props.multiple) { + const positions = [...this.state.position]; + positions[knobIndex] = this.determinePosition(value); + this.setValue(value, triggeredByUser, knobIndex); + this.setState({ + position: positions, + }); + } else { + this.setValue(value, triggeredByUser); + this.setState({ + position: this.determinePosition(value), + }); + } + } + + setPosition(position, knobIndex) { + if (this.props.multiple) { + const newPosition = [...this.state.position]; + newPosition[knobIndex] = position; + this.setState({ + position: newPosition, + }); + } else { + this.setState({ + position: position, + }); + } + } + + rangeMouseDown(isTouch, e) { + e.stopPropagation(); + if (!this.props.disabled) { + if (!isTouch) { + e.preventDefault(); + } + + this.setState({ + mouseDown: true, + }); + let innerBoundingClientRect = ReactDOM.findDOMNode( + this.inner, + ).getBoundingClientRect(); + this.innerLeft = innerBoundingClientRect.left; + this.innerRight = this.innerLeft + this.inner.offsetWidth; + this.rangeMouse(isTouch, e); + } + } + + rangeMouse(isTouch, e) { + let pageX; + let event = isTouch ? e.touches[0] : e; + if (event.pageX) { + pageX = event.pageX; + } else { + // eslint-disable-next-line + console.log('PageX undefined'); + } + let value = this.determineValue(this.innerLeft, this.innerRight, pageX); + if (pageX >= this.innerLeft && pageX <= this.innerRight) { + if ( + value >= this.props.settings.min && + value <= this.props.settings.max + ) { + const position = pageX - this.innerLeft - this.state.offset; + const knobIndex = this.props.multiple + ? this.determineKnob(position) + : undefined; + if (this.props.discrete) { + this.setValuePosition(value, false, knobIndex); + } else { + this.setPosition(position, knobIndex); + this.setValue(value, undefined, knobIndex); + } + } + } + } + + rangeMouseMove(isTouch, e) { + e.stopPropagation(); + if (!isTouch) { + e.preventDefault(); + } + if (this.state.mouseDown) { + this.rangeMouse(isTouch, e); + } + } + + rangeMouseUp() { + this.setState({ + mouseDown: false, + }); + } + + handleKnobClick(e) { + if (e.detail > 1 && !this.state.showNumericInput) { + this.setState({ showNumericInput: true }); + } + } + + render() { + return ( +
+ {this.state.showNumericInput ? ( + { + // TODO: handle multiple knobs + if (e.key === 'Enter' && isNumeric(e.target.value)) { + const value = e.target.value; + this.setState({ showNumericInput: false }, () => { + this.props.settings.onChange(parseInt(value)); + }); + } + }} + /> + ) : ( +
this.rangeMouseDown(false, event)} + onMouseMove={(event) => this.rangeMouseMove(false, event)} + onMouseUp={(event) => this.rangeMouseUp(false, event)} + onTouchEnd={(event) => this.rangeMouseUp(true, event)} + onTouchMove={(event) => this.rangeMouseMove(true, event)} + onTouchStart={(event) => this.rangeMouseDown(true, event)} + style={{ + ...styles.range, + ...(this.props.disabled ? styles.disabled : {}), + ...(this.props.style ? this.props.style : {}), + }} + > +
{ + this.inner = inner; + }} + style={{ + ...styles.inner, + ...(this.props.style + ? this.props.style.inner + ? this.props.style.inner + : {} + : {}), + }} + > +
{ + this.track = track; + }} + style={{ + ...styles.track, + ...(this.props.inverted ? styles.invertedTrack : {}), + ...(this.props.style + ? this.props.style.track + ? this.props.style.track + : {} + : {}), + }} + /> +
{ + this.trackFill = trackFill; + }} + style={{ + ...styles.trackFill, + ...(this.props.inverted ? styles.invertedTrackFill : {}), + ...styles[ + this.props.inverted + ? 'inverted-' + this.props.color + : this.props.color + ], + ...(this.props.style + ? this.props.style.trackFill + ? this.props.style.trackFill + : {} + : {}), + ...(this.props.disabled ? styles.disabledTrackFill : {}), + ...(this.props.style + ? this.props.style.disabledTrackFill + ? this.props.style.disabledTrackFill + : {} + : {}), + ...{ width: this.state.position + this.state.offset + 'px' }, + ...(this.props.multiple && this.state.position.length > 0 + ? { + left: this.state.position[0], + width: + this.state.position[this.state.numberOfKnobs - 1] - + this.state.position[0], + } + : {}), + }} + /> + + {this.props.multiple ? ( + this.state.position.map((pos, i) => ( +
+ )) + ) : ( +
+ {this.props.extra} +
+ )} +
+
+ )} +
+ ); + } +} + +Slider.defaultProps = { + color: 'red', + settings: { + min: 0, + max: 10, + step: 1, + start: 0, + }, +}; + +Slider.propTypes = { + color: PropTypes.string, + disabled: PropTypes.bool, + discrete: PropTypes.bool, + inverted: PropTypes.bool, + multiple: PropTypes.bool, + settings: PropTypes.shape({ + min: PropTypes.number, + max: PropTypes.number, + step: PropTypes.number, + start: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.arrayOf(PropTypes.number), + ]), + onChange: PropTypes.func, + }), +}; + +const SliderWidget = (props) => { + const { id, onChange, value, settings = {}, ...rest } = props; + return ( + + { + // console.log('onchange', value); + onChange(id, value); + }, + }} + value={value} + extra={{value}} + /> + + ); +}; + +export default SliderWidget; diff --git a/src/Widgets/index.js b/src/Widgets/index.js index 2acb1b8..121c057 100644 --- a/src/Widgets/index.js +++ b/src/Widgets/index.js @@ -1,2 +1,4 @@ export ColumnsWidget from './ColumnsWidget'; export LayoutSelectWidget from './LayoutSelectWidget'; +export QuadSizeWidget from './QuadSize'; +export SliderWidget from './Slider'; diff --git a/src/Widgets/range.css b/src/Widgets/range.css new file mode 100644 index 0000000..7c813d4 --- /dev/null +++ b/src/Widgets/range.css @@ -0,0 +1,85 @@ +.slider-widget-wrapper { + display: flex; + flex-direction: column; + justify-content: center; +} + +.stretch { + position: relative; + right: 50%; + left: 50%; + width: 100vw !important; + margin-right: -50vw !important; + margin-left: -50vw !important; +} + +/* detect if is authenticated or anon */ +body:not(.is-authenticated) .stretch { + padding: 0 2rem; +} + +/* detect if is logged in but in view with toolbar open */ +body:not(.view-editview):not(.is-anonymous):not(.has-toolbar-collapsed) + .stretch { + padding: 0 4.5rem; +} + +/* detect if its in edit mode */ + +/* detect if both sidebars open */ +body:not(.view-viewview):not(.has-sidebar-collapsed):not(.has-toolbar-collapsed) + .stretch { + padding: 0 16.2rem; +} +/* detect if both sidebars closed */ +body:not(.view-viewview):not(.has-sidebar):not(.has-toolbar) .stretch { + padding: 0 3.2rem; +} + +/* detect if left sidebar open right sidebar closed */ +body:not(.view-viewview):not(.has-sidebar):not(.has-toolbar-collapsed) + .stretch { + padding: 0 5.2rem; +} +/* detect if left sidebar closed right sidebar open */ +body:not(.view-viewview):not(.has-sidebar-collapsed):not(.has-toolbar) + .stretch { + padding: 0 14.3rem; +} + +body:not(.is-anonymous):not(.has-toolbar) .stretch { + padding: 0 2.6rem; +} +@media (min-width: 1700px) { + .stretch { + margin-right: 0 !important; + margin-left: 0 !important; + transform: translate(-50%, 0%); + } + + body:not(.view-viewview) .stretch { + width: auto !important; + max-width: 1790px !important; + padding: 0 !important; + margin-right: -7.2rem !important; + transform: translate(-50%, 0%); + } + + body:not(.is-anonymous):not(.has-toolbar) .stretch { + max-width: 1720px; + } + + body:not(.is-anonymous):not(.has-toolbar-collapsed) .stretch { + max-width: 1778px; + } + + body:not(.is-authenticated) .stretch { + max-width: 1700px; + } +} + +@media (max-width: 786px) { + .stretch { + padding: 0 !important; + } +} diff --git a/src/Widgets/range.css.js b/src/Widgets/range.css.js new file mode 100644 index 0000000..76fc2f2 --- /dev/null +++ b/src/Widgets/range.css.js @@ -0,0 +1,179 @@ +const styles = { + range: { + cursor: 'pointer', + width: '100%', + height: '30px', + }, + inner: { + margin: '0 10px 0 10px', + height: '30px', + position: 'relative', + }, + /* + .ui.range .inner:hover { + cursor: pointer; + }*/ + track: { + position: 'absolute', + width: '100%', + height: '4px', + borderRadius: '4px', + top: '12px', + left: '0', + backgroundColor: 'rgba(0,0,0,.05)', + }, + invertedTrack: { + backgroundColor: 'rgba(255,255,255,.08)', + }, + trackFill: { + position: 'absolute', + width: '0', + height: '4px', + borderRadius: '4px', + top: '12px', + left: '0', + backgroundColor: '#1b1c1d', + }, + invertedTrackFill: { + backgroundColor: '#545454', + }, + knob: { + position: 'absolute', + top: '0px', + left: '0', + height: '30px', + width: '20px', + background: '#fff linear-gradient(transparent, rgba(0, 0, 0, 0.5))', + // background: '#fff -webkit-linear-gradient(transparent, rgba(0, 0, 0, 0.5))', + // background: '#fff -o-linear-gradient(transparent, rgba(0, 0, 0, 0.5))', + // background: '#fff -moz-linear-gradient(transparent, rgba(0, 0, 0, 0.5))', + borderRadius: '6px', + backgroundColor: '#205c90', + boxShadow: + '0 1px 2px 0 rgba(34,36,38,.15),0 0 0 1px rgba(34,36,38,.15) inset', + display: 'flex', + color: 'white', + flexDirection: 'column', + textAlign: 'center', + fontSize: 'xx-small', + }, + red: { + backgroundColor: '#DB2828', + }, + 'inverted-red': { + backgroundColor: '#FF695E', + }, + /* Orange */ + orange: { + backgroundColor: '#F2711C', + }, + 'inverted-orange': { + backgroundColor: '#FF851B', + }, + /* Yellow */ + yellow: { + backgroundColor: '#FBBD08', + }, + 'inverted-yellow': { + backgroundColor: '#FFE21F', + }, + /* Olive */ + olive: { + backgroundColor: '#B5CC18', + }, + 'inverted-olive': { + backgroundColor: '#D9E778', + }, + /* Green */ + green: { + backgroundColor: '#21BA45', + }, + 'inverted-green': { + backgroundColor: '#2ECC40', + }, + /* Teal */ + teal: { + backgroundColor: '#00B5AD', + }, + 'inverted-teal': { + backgroundColor: '#6DFFFF', + }, + /* Blue */ + blue: { + backgroundColor: '#2185D0', + }, + 'inverted-blue': { + backgroundColor: '#54C8FF', + }, + /* Violet */ + violet: { + backgroundColor: '#6435C9', + }, + 'inverted-violet': { + backgroundColor: '#A291FB', + }, + /* Purple */ + purple: { + backgroundColor: '#A333C8', + }, + 'inverted-purple': { + backgroundColor: '#DC73FF', + }, + /* Pink */ + pink: { + backgroundColor: '#E03997', + }, + 'inverted-pink': { + backgroundColor: '#FF8EDF', + }, + /* Brown */ + brown: { + backgroundColor: '#A5673F', + }, + 'inverted-brown': { + backgroundColor: '#D67C1C', + }, + /* Grey */ + grey: { + backgroundColor: '#767676', + }, + 'inverted-grey': { + backgroundColor: '#DCDDDE', + }, + /* Black */ + black: { + backgroundColor: '#1b1c1d', + }, + 'inverted-black': { + backgroundColor: '#545454', + }, + /*-------------- + Disabled +---------------*/ + disabled: { + cursor: 'not-allowed', + opacity: '.5', + }, + + /*-------------- + Disabled +---------------*/ + + disabledTrackFill: { + backgroundColor: '#ccc', + }, + + /*-------------- + Invalid-Input +---------------*/ + invalidInputTrack: { + cursor: 'not-allowed', + opacity: '.3', + background: '#ff0000', + }, + invalidInputTrackFill: { + opacity: '.0', + }, +}; + +export default styles; diff --git a/src/index.js b/src/index.js index 8002ea3..8741e21 100644 --- a/src/index.js +++ b/src/index.js @@ -5,7 +5,12 @@ import { ColumnsBlockEdit, ColumnsLayoutSchema, } from './ColumnsBlock'; -import { ColumnsWidget, LayoutSelectWidget } from './Widgets'; +import { + ColumnsWidget, + LayoutSelectWidget, + SliderWidget, + QuadSizeWidget, +} from './Widgets'; import ColorPickerWidget from './Widgets/SimpleColorPickerWidget.jsx'; import { gridSizes, variants } from './grid'; import { COLUMNSBLOCK } from './constants'; @@ -103,6 +108,8 @@ export default function install(config) { config.widgets.type.columns = ColumnsWidget; config.widgets.widget.simple_color_picker = ColorPickerWidget; config.widgets.widget.layout_select = LayoutSelectWidget; + config.widgets.widget.slider = SliderWidget; + config.widgets.widget.quad_size = QuadSizeWidget; return config; } diff --git a/src/less/columns.less b/src/less/columns.less index 2fe9f3b..6c235fa 100644 --- a/src/less/columns.less +++ b/src/less/columns.less @@ -26,16 +26,16 @@ border-style: dashed; } - .column-grid { - margin-right: -0.2rem !important; - margin-left: -0.2rem !important; + .ui.grid.column-grid { + margin-right: -0.2rem; + margin-left: -0.2rem; .column:first-child { - padding-left: 0.2rem !important; + padding-left: 0.2rem; } .column:last-child { - padding-right: 0.2rem !important; + padding-right: 0.2rem; } &:focus { @@ -58,8 +58,8 @@ text-align: center; } - .block-column { - padding: 0.3em !important; + .ui.grid.block-column { + padding: 0.3em; .blocks-form { padding: 1rem !important; @@ -183,9 +183,10 @@ display: initial !important; } - .column-blocks-wrapper { - padding-top: 0.5em !important; - padding-bottom: 0.5em !important; + .ui.grid > .column-blocks-wrapper, + .ui.grid > .column-blocks-wrapper:not(.row) { + padding-top: 0.5em; + padding-bottom: 0.5em; } }