diff --git a/block-languages/benenson-blocks.pot b/block-languages/benenson-blocks.pot index 00e5518..d1b82e6 100644 --- a/block-languages/benenson-blocks.pot +++ b/block-languages/benenson-blocks.pot @@ -322,12 +322,14 @@ msgstr "" #: src/scripts/blocks/call-to-action/DisplayComponent.js:74 #: src/scripts/blocks/slider/DisplayComponent.js:313 +#: src/scripts/blocks/timeline/DisplayComponent.js:180 msgid "(Heading)" msgstr "" #: src/scripts/blocks/call-to-action/DisplayComponent.js:83 #: src/scripts/blocks/image/BlockEdit.js:286 #: src/scripts/blocks/slider/DisplayComponent.js:335 +#: src/scripts/blocks/timeline/DisplayComponent.js:202 msgid "(Content)" msgstr "" @@ -632,6 +634,7 @@ msgid "Only has an effect on images smaller than their container" msgstr "" #: src/scripts/blocks/image/BlockEdit.js:280 +#: src/scripts/blocks/timeline/DisplayComponent.js:192 msgid "(Title)" msgstr "" @@ -1135,6 +1138,7 @@ msgid "(Sub-Heading)" msgstr "" #: src/scripts/blocks/slider/DisplayComponent.js:379 +#: src/scripts/blocks/timeline/DisplayComponent.js:234 msgid "(No Title)" msgstr "" @@ -1174,6 +1178,34 @@ msgstr "" msgid "Scroller" msgstr "" +#: src/scripts/blocks/timeline/DisplayComponent.js:146 +msgid "Timeline Milestone Options" +msgstr "" + +#: src/scripts/blocks/timeline/DisplayComponent.js:170 +msgid "Add a milestone below." +msgstr "" + +#: src/scripts/blocks/timeline/DisplayComponent.js:171 +msgid "Add milestone" +msgstr "" + +#: src/scripts/blocks/timeline/DisplayComponent.js:217 +msgid "Remove Milestone" +msgstr "" + +#: src/scripts/blocks/timeline/DisplayComponent.js:218 +msgid "Add Milestone" +msgstr "" + +#: src/scripts/blocks/timeline/DisplayComponent.js:91 +msgid "Are you sure you want to delete this milestone from the timeline?" +msgstr "" + +#: src/scripts/blocks/timeline/index.js:14 +msgid "Timeline" +msgstr "" + #: src/scripts/blocks/tweet/index.js:109 msgid "(Action Title)" msgstr "" diff --git a/src/scripts/app.js b/src/scripts/app.js index 22a140f..844580f 100755 --- a/src/scripts/app.js +++ b/src/scripts/app.js @@ -15,6 +15,7 @@ import subcatDrops from './modules/subcategory-dropdown'; import categoryExpander from './modules/category-expander'; import fluidText from './modules/fluid-text'; import scrollTo from './modules/scrollTo'; +import timeline from './modules/timeline'; import './polyfills'; @@ -34,6 +35,7 @@ const App = () => { subcatDrops(); categoryExpander(); scrollTo(); + timeline(); fluidText(document.getElementsByClassName('article-shareTitle'), 0.9); diff --git a/src/scripts/blocks.js b/src/scripts/blocks.js index a99adc8..887883e 100755 --- a/src/scripts/blocks.js +++ b/src/scripts/blocks.js @@ -28,6 +28,7 @@ import './blocks/category-list'; import './blocks/logo-list'; import './blocks/link'; import './blocks/media-aside'; +import './blocks/timeline'; wp.blocks.registerBlockStyle('core/table', { name: 'responsive', diff --git a/src/scripts/blocks/timeline/DisplayComponent.js b/src/scripts/blocks/timeline/DisplayComponent.js new file mode 100644 index 0000000..0fa5f6a --- /dev/null +++ b/src/scripts/blocks/timeline/DisplayComponent.js @@ -0,0 +1,245 @@ +const randId = () => Math.random().toString(36).replace(/[^a-z]+/g, '').substr(2, 10); + +const { __ } = wp.i18n; +const { Component, Fragment } = wp.element; +const { + PanelBody, Button, DateTimePicker, +} = wp.components; + +const { + InspectorControls, RichText, +} = wp.editor; + +const { PostMediaSelector } = benenson.components; + +class DisplayComponent extends Component { + static emptyMilestone = { + id: '', + heading: '', + title: '', + content: '', + }; + + constructor(...props) { + super(...props); + + this.state = { + selectedMilestone: 0, + }; + } + + /** + * Higher order component that takes the attribute key, + * this then returns a function which takes a value, + * when called it updates the attribute with the key. + * @param key + * @returns {function(*): *} + */ + createUpdateAttribute = key => value => this.props.setAttributes({ [key]: value }); + + createUpdateMilestoneAttribute = + index => + key => + value => + this.props.setAttributes({ + milestones: [ + ...this.props.attributes.milestones + .slice(0, Math.max(0, index)), + { + ...this.props.attributes.milestones[index], + [key]: value, + }, + ...this.props.attributes + .milestones.slice(index + 1, this.props.attributes.milestones.length), + ], + }); + + addMilestone = () => { + this.setState({ + selectedMilestone: this.props.attributes.milestones.length, + }); + + this.props.setAttributes({ + milestones: [ + ...this.props.attributes.milestones, + { + ...DisplayComponent.emptyMilestone, + id: randId(), + }, + ], + }); + }; + + deleteMilestone = (index) => { + if (index === this.props.attributes.Milestones.length - 1) { + this.setState({ + selectedMilestone: index - 1, + }); + } + + this.props.setAttributes({ + milestones: [ + ...this.props.attributes + .milestones.slice(0, Math.max(0, index)), + ...this.props.attributes + .milestones.slice(index + 1, this.props.attributes.milestones.length), + ], + }); + }; + + initiateDelete = () => { + if (confirm(__('Are you sure you want to delete this milestone from the timeline?', 'benenson'))) { // eslint-disable-line no-restricted-globals, no-alert + this.deleteMilestone(this.state.selectedMilestone); + } + }; + + selectMilestone = index => this.setState({ + selectedMilestone: index, + }); + + createSelectMilestone = index => () => this.selectMilestone(index); + + movePrev = () => { + const { selectedMilestone } = this.state; + + const blockOrder = [...this.props.attributes.milestones]; + const temp = blockOrder[selectedMilestone]; + blockOrder[selectedMilestone] = blockOrder[selectedMilestone - 1]; + blockOrder[selectedMilestone - 1] = temp; + + this.props.setAttributes({ + milestones: blockOrder, + }); + + this.setState({ + selectedMilestone: selectedMilestone - 1, + }); + }; + + moveNext = () => { + const { selectedMilestone } = this.state; + + const blockOrder = [...this.props.attributes.milestones]; + const temp = blockOrder[selectedMilestone]; + blockOrder[selectedMilestone] = blockOrder[selectedMilestone + 1]; + blockOrder[selectedMilestone + 1] = temp; + + this.props.setAttributes({ + milestones: blockOrder, + }); + + this.setState({ + selectedMilestone: selectedMilestone + 1, + }); + }; + + render() { + const { attributes } = this.props; + const { selectedMilestone } = this.state; + + const currentMilestone = attributes.milestones[selectedMilestone]; + const updateMilestone = this.createUpdateMilestoneAttribute(selectedMilestone); + + const controls = ( + + { currentMilestone && ( + + { attributes.milestones.length >= 2 && ( +

Change milestone position:

+ )} + { selectedMilestone !== 0 && ( + + )} + { selectedMilestone < attributes.milestones.length - 1 && ( + + )} +
+ ) } +
+ ); + + return ( + + { controls } +
+
+
+ { attributes.milestones.length === 0 && ( +
+
+

{ __('Add a milestone below.', 'benenson') }

+ +
+
+ ) } + { currentMilestone && ( +
+

+ +

+
+ + +
+
+ ) } +
+
+ +
+
+ ); + } +} + +export default DisplayComponent; diff --git a/src/scripts/blocks/timeline/index.js b/src/scripts/blocks/timeline/index.js new file mode 100644 index 0000000..50a1a56 --- /dev/null +++ b/src/scripts/blocks/timeline/index.js @@ -0,0 +1,59 @@ +import assign from 'lodash-es/assign'; +import DisplayComponent from './DisplayComponent'; + +const { __ } = wp.i18n; +const { registerBlockType } = wp.blocks; + +const { dateI18n, format, __experimentalGetSettings } = wp.date; + +registerBlockType('benenson/timeline', { + title: __('Timeline', 'benenson'), + icon: 'admin-post', + category: 'benenson', + keywords: [ + __('Timeline', 'benenson'), + ], + supports: { + multiple: true, + }, + attributes: { + timelineId: { + type: 'string', + }, + milestones: { + type: 'array', + default: [], + }, + }, + + edit: DisplayComponent, + + save({ attributes }) { + const dateFormat = __experimentalGetSettings().formats.date; + + return ( +
+
+
+
+ { attributes.milestones.length > 0 && attributes.milestones.map((milestone, index) => { + const milestoneDate = milestone.date !== '' ?

{ dateI18n(dateFormat, milestone.date) }

: null; + const milestoneTitle = milestone.title !== '' ?

{ milestone.title }

: null; + const milestoneContent = milestone.content !== '' ?

{ milestone.content }

: null; + + return ( +
+ { milestoneDate } +
+ { milestoneTitle } + { milestoneContent } +
+
+ ); + }) } +
+
+
+ ); + }, +}); diff --git a/src/scripts/modules/timeline.js b/src/scripts/modules/timeline.js new file mode 100644 index 0000000..dc5e882 --- /dev/null +++ b/src/scripts/modules/timeline.js @@ -0,0 +1,109 @@ +const getPos = (element) => { + const childrenPos = element.getBoundingClientRect(); + const parentPos = element.parentElement.parentElement.getBoundingClientRect(); + const relativePos = {}; + + relativePos.top = childrenPos.top - parentPos.top; + relativePos.right = childrenPos.right - parentPos.right; + relativePos.bottom = childrenPos.bottom - parentPos.bottom; + relativePos.left = childrenPos.left - parentPos.left; + + return relativePos; +}; + +class Timeline { + constructor(timeline) { + this.timeline = timeline; + this.items = Array.from(timeline.querySelectorAll('.timelineMilestone-heading')); + this.line = timeline.querySelector('.timeline-line'); + this.milestones = timeline.querySelector('.timelineMilestones'); + this.timeline.addEventListener('mousedown', (e) => { this.enableMove(e); }); + this.timeline.addEventListener('touchstart', (e) => { this.enableMove(e); }); + document.documentElement.addEventListener('mousemove', (e) => { this.move(e); }); + document.documentElement.addEventListener('touchmove', (e) => { this.move(e); }); + document.documentElement.addEventListener('mouseup', () => { this.disableMove(); }); + document.documentElement.addEventListener('touchend', () => { this.disableMove(); }); + this.moveAllowed = false; + this.startPosition = false; + this.translate = 0; + + this.workout(timeline); + } + + workout() { + const heighest = this.items + .filter((item, i) => (i + 1) % 2 === 0) + .reduce((carry, current) => { + const { top } = getPos(current); + + if (top < carry) { + return carry; + } + + return top; + }, 0); + + this.items + .filter((item, i) => i % 2 === 0) + .forEach((item) => { + item.parentElement.style.marginTop = `${heighest}px`; // eslint-disable-line no-param-reassign + }); + + this.items + .filter((item, i) => (i + 1) % 2 === 0) + .forEach((item) => { + const pos = getPos(item); + item.parentElement.style.marginTop = `${(heighest - pos.top) + 11}px`; // eslint-disable-line no-param-reassign + }); + + this.line.style.minWidth = `${this.timeline.querySelector('.timelineMilestones').offsetWidth}px`; + this.line.style.top = `${(heighest + 52)}px`; + } + + enableMove(e) { + this.moveAllowed = true; + this.startPosition = e.x || e.targetTouches[0].clientX; + this.milestones.classList.remove('is-animate'); + } + + move(e) { + if (!this.moveAllowed) { + return; + } + + const x = e.clientX || e.targetTouches[0].clientX; + this.translate += x - this.startPosition; + this.startPosition = x; + this.milestones.style.transform = `translateX(${this.translate}px)`; + } + + disableMove() { + this.moveAllowed = false; + this.startPosition = false; + this.milestones.classList.add('is-animate'); + + this.translate = Math.max(Math.min(this.translate, 0), -(this.milestones.clientWidth - this.timeline.clientWidth)); // eslint-disable-line max-len + this.milestones.style.transform = `translateX(${this.translate}px)`; + } + + refresh() { + this.items.forEach((item) => { + item.parentElement.style.marginTop = '0px'; // eslint-disable-line no-param-reassign + }); + + this.workout(this.timeline); + } +} + +const init = () => { + const timelines = Array.from(document.querySelectorAll('.timeline')); + const timelineClasses = timelines.map(timeline => new Timeline(timeline)); + + window.addEventListener('resize', () => { + timelineClasses.forEach((timeline) => { + timeline.refresh(); + }); + }); +}; + +export default init; diff --git a/src/styles/components/timeline/_editor.scss b/src/styles/components/timeline/_editor.scss new file mode 100644 index 0000000..6a5dc4c --- /dev/null +++ b/src/styles/components/timeline/_editor.scss @@ -0,0 +1,36 @@ +.timelineMilestone { + width: 100%; +} + +.timeline-nav { + display: flex; + flex-wrap: wrap; + margin-top: 20px; +} + +.timeline-nav div, +.timeline-nav button { + flex-grow: 1; + + & + * { + margin-left: 1px; + } +} + +.timeline-nav button { + margin-bottom: 1px; +} + +.timeline-nav .timeline-navActions { + flex-grow: 1; + width: 100%; + flex-basis: 100%; +} + +.timeline-nav .timeline-navActions .timeline-navButton { + width: calc(50% - .5px); +} + +.timeline-nav .is-active { + @extend .btn--white; +} diff --git a/src/styles/components/timeline/_main.scss b/src/styles/components/timeline/_main.scss new file mode 100644 index 0000000..c195c17 --- /dev/null +++ b/src/styles/components/timeline/_main.scss @@ -0,0 +1,122 @@ +.timeline .timeline-container { + position: relative; + width: 100%; + overflow: hidden; + cursor: ew-resize; + user-select: none; +} + +.timeline .timeline-container .timelineMilestones { + position: relative; + will-change: transform; + overflow: scroll; + display: flex; + flex-wrap: nowrap; + align-items: flex-start; + width: max-content; + padding: 18px 0; +} + +.timeline .timeline-container .timelineMilestones.is-animate { + transition: .3s ease-in-out; +} + +.timeline-line { + height: 3px; + width: 100%; + position: absolute; + top: 50%; + margin-top: - 1.5px; + background-color: $color-black; +} + +.timelineMilestone { + display: flex; + flex-direction: column; + width: 400px; + margin-right: 20px; +} + +.timelineMilestone .timelineMilestone-heading { + @include font-size(18px); + position: relative; + display: inline-block; + margin: 0; + padding-bottom: 50px; + margin-left: 50px; + font-weight: bold; + text-align: center; + + &::before { + content: ""; + position: absolute; + width: 4px; + height: 36px; + background-color: $color-black; + bottom: 0; + left: 50%; + margin-left: -2px; + } + + &::after { + content: ""; + height: 15px; + width: 15px; + border: 4px solid $color-black; + border-radius: 50%; + background-color: $color-white; + position: absolute; + left: 50%; + margin-left: -7.5px; + bottom: 36px; + } +} + +.timelineMilestone .timelineMilestone-content { + position: relative; + background-color: $color-black; + color: $color-white; + padding: 20px; +} + +.timelineMilestone .timelineMilestone-title { + @include font-size(24px); + font-weight: bold; + margin-bottom: 5px; +} + +html:not(.no-js) .timelineMilestone:nth-child(2n) { + flex-direction: column-reverse; +} + +html:not(.no-js) .timelineMilestone:nth-child(2n) .timelineMilestone-heading { + padding-top: 50px; + padding-bottom: 0; + + &::before { + bottom: initial; + top: 0; + } + + &::after { + top: 34px; + bottom: initial; + } +} + +html.no-js .timeline .timeline-container .timelineMilestones { + flex-direction: column; + width: 100%; +} + +html.no-js .timelineMilestone { + width: 100%; +} + +html.no-js .timelineMilestone + .timelineMilestone { + margin-top: 30px; +} + +html.no-js .timelineMilestone .timelineMilestone-heading { + margin-left: 0; +} diff --git a/src/styles/style-editor.scss b/src/styles/style-editor.scss index c7c9282..2214a7d 100755 --- a/src/styles/style-editor.scss +++ b/src/styles/style-editor.scss @@ -87,6 +87,8 @@ @import "components/split-grid/main"; @import "components/media-aside/main"; @import "components/media-aside/editor"; +@import "components/timeline/main"; +@import "components/timeline/editor"; // helpers @import "utils/helpers/type"; diff --git a/src/styles/style.scss b/src/styles/style.scss index 5294582..4f33b58 100755 --- a/src/styles/style.scss +++ b/src/styles/style.scss @@ -99,6 +99,7 @@ @import "components/logo-list/main"; @import "components/post-grid/main"; @import "components/media-aside/main"; +@import "components/timeline/main"; // Pages @import "pages/home";