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";