motimeline 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -0
- package/dist/moTimeline.cjs +6 -0
- package/dist/moTimeline.css +3 -0
- package/dist/moTimeline.js +127 -0
- package/dist/moTimeline.umd.js +6 -0
- package/package.json +42 -0
- package/src/moTimeline.css +250 -0
- package/src/moTimeline.js +258 -0
package/README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# moTimeline
|
|
2
|
+
|
|
3
|
+
Responsive two-column timeline layout library — plain JavaScript, zero dependencies, MIT licensed.
|
|
4
|
+
|
|
5
|
+
**[Live demo & docs → mattopen.github.io/moTimeline](https://mattopen.github.io/moTimeline/)**
|
|
6
|
+
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
[](https://mattopen.github.io/moTimeline/)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Zero dependencies** — no jQuery, no frameworks required
|
|
16
|
+
- **Responsive** — two columns on desktop, single column on mobile
|
|
17
|
+
- **Configurable breakpoints** — control column count at xs / sm / md / lg
|
|
18
|
+
- **Badges & arrows** — numbered badges on the center line, directional arrows
|
|
19
|
+
- **Optional theme** — built-in card theme with image banners and overlapping avatars
|
|
20
|
+
- **CSS custom properties** — override colors and sizes with one line of CSS
|
|
21
|
+
- **Dynamic items** — append new `<li>` elements at any time via `initNewItems()`
|
|
22
|
+
- **Bootstrap compatible** — wrap the `<ul>` in a Bootstrap `.container`, no config needed
|
|
23
|
+
- **ESM · CJS · UMD** — works with any bundler or as a plain `<script>` tag
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install motimeline
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### ESM
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
import MoTimeline from 'motimeline';
|
|
37
|
+
import 'motimeline/dist/moTimeline.css';
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### UMD (no bundler)
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<link rel="stylesheet" href="moTimeline.css">
|
|
44
|
+
<script src="moTimeline.umd.js"></script>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<ul id="my-timeline">
|
|
53
|
+
<li>
|
|
54
|
+
<div class="mo-card">
|
|
55
|
+
<div class="mo-card-body">
|
|
56
|
+
<h3>Title</h3>
|
|
57
|
+
<p class="mo-meta">Date</p>
|
|
58
|
+
<p>Text…</p>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</li>
|
|
62
|
+
<!-- more <li> items -->
|
|
63
|
+
</ul>
|
|
64
|
+
|
|
65
|
+
<script type="module">
|
|
66
|
+
import MoTimeline from 'motimeline';
|
|
67
|
+
|
|
68
|
+
const tl = new MoTimeline('#my-timeline', {
|
|
69
|
+
badgeShow: true,
|
|
70
|
+
arrowShow: true,
|
|
71
|
+
theme: true,
|
|
72
|
+
});
|
|
73
|
+
</script>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### With banner image and avatar (`theme: true`)
|
|
77
|
+
|
|
78
|
+
```html
|
|
79
|
+
<li>
|
|
80
|
+
<div class="mo-card">
|
|
81
|
+
<div class="mo-card-image">
|
|
82
|
+
<img class="mo-banner" src="banner.jpg" alt="">
|
|
83
|
+
<img class="mo-avatar" src="avatar.jpg" alt=""> <!-- optional -->
|
|
84
|
+
</div>
|
|
85
|
+
<div class="mo-card-body">
|
|
86
|
+
<h3>Title</h3>
|
|
87
|
+
<p class="mo-meta">Date</p>
|
|
88
|
+
<p>Text…</p>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</li>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Options
|
|
97
|
+
|
|
98
|
+
| Option | Type | Default | Description |
|
|
99
|
+
|---|---|---|---|
|
|
100
|
+
| `columnCount` | object | `{xs:1, sm:2, md:2, lg:2}` | Columns at each breakpoint (xs <600 px, sm <992 px, md <1200 px, lg ≥1200 px) |
|
|
101
|
+
| `badgeShow` | boolean | `false` | Show numbered badges on the center line |
|
|
102
|
+
| `arrowShow` | boolean | `false` | Show triangle arrows pointing from each card toward the center line |
|
|
103
|
+
| `theme` | boolean | `false` | Enable the built-in card theme (banners, avatars, styled badges) |
|
|
104
|
+
| `showCounter` | boolean | `true` | Render the badge number. `false` keeps the badge for layout but sets it transparent |
|
|
105
|
+
| `showCounterStyle` | string | `'counter'` | `'counter'` shows the item number; `'image'` shows the icon from `data-mo-icon` on the `<li>`, or a built-in SVG fallback |
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## API
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
const tl = new MoTimeline(elementOrSelector, options);
|
|
113
|
+
|
|
114
|
+
tl.refresh(); // re-layout all items (called automatically on resize)
|
|
115
|
+
tl.initNewItems(); // pick up newly appended <li> elements
|
|
116
|
+
tl.destroy(); // remove listeners and reset DOM classes
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## CSS custom properties
|
|
122
|
+
|
|
123
|
+
```css
|
|
124
|
+
#my-timeline {
|
|
125
|
+
--mo-line-color: #dde1e7;
|
|
126
|
+
--mo-badge-bg: #4f46e5;
|
|
127
|
+
--mo-badge-color: #fff;
|
|
128
|
+
--mo-badge-size: 26px;
|
|
129
|
+
--mo-badge-font-size: 12px;
|
|
130
|
+
--mo-arrow-color: #dde1e7;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Bootstrap integration
|
|
137
|
+
|
|
138
|
+
No framework option needed. Wrap the `<ul>` inside a Bootstrap `.container`:
|
|
139
|
+
|
|
140
|
+
```html
|
|
141
|
+
<div class="container">
|
|
142
|
+
<ul id="my-timeline">…</ul>
|
|
143
|
+
</div>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Examples
|
|
149
|
+
|
|
150
|
+
| Folder | Description |
|
|
151
|
+
|---|---|
|
|
152
|
+
| [`example/`](example/) | Main example — run with `npm run dev` |
|
|
153
|
+
| [`example/mattopen/`](example/mattopen/) | Bootstrap 5 integration |
|
|
154
|
+
| [`example/livestamp/`](example/livestamp/) | Livestamp.js + Moment.js relative timestamps |
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Changelog
|
|
159
|
+
|
|
160
|
+
### v2.3.0
|
|
161
|
+
- Added `showCounter` (opacity toggle) and `showCounterStyle` (`'counter'` | `'image'`) badge options
|
|
162
|
+
- `data-mo-icon` attribute on `<li>` sets a custom icon in image mode; built-in flat SVG used as fallback
|
|
163
|
+
|
|
164
|
+
### v2.2.0
|
|
165
|
+
- All library-managed classes renamed to consistent `mo-` prefix (`mo-item`, `mo-badge`, `mo-arrow`, `mo-twocol`, `mo-offset`)
|
|
166
|
+
- Added parallel `js-mo-*` classes for JS-only selectors alongside `mo-*` styling classes
|
|
167
|
+
|
|
168
|
+
### v2.1.0
|
|
169
|
+
- Opt-in card theme (`theme: true`) with `mo-card`, `mo-banner`, `mo-avatar`
|
|
170
|
+
- Badges repositioned to the center line with directional arrows
|
|
171
|
+
- CSS custom properties for easy color/size overrides
|
|
172
|
+
- Badge offset algorithm: later DOM item always gets the offset on collision
|
|
173
|
+
|
|
174
|
+
### v2.0.0
|
|
175
|
+
- Complete rewrite — removed jQuery, zero dependencies
|
|
176
|
+
- Class-based API: `new MoTimeline(element, options)`
|
|
177
|
+
- Vite build pipeline: ESM, CJS, UMD outputs
|
|
178
|
+
- Debounced resize listener, `WeakMap` instance data storage
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT © [MattOpen](http://www.mattopen.com)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";var b=Object.defineProperty;var E=(i,e,t)=>e in i?b(i,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):i[e]=t;var p=(i,e,t)=>E(i,typeof e!="symbol"?e+"":e,t);Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});/*!
|
|
2
|
+
* moTimeline v2.3.0
|
|
3
|
+
* Responsive two-column timeline layout library
|
|
4
|
+
* https://github.com/MattOpen/moTimeline
|
|
5
|
+
* MIT License
|
|
6
|
+
*/const a=new WeakMap,w={columnCount:{xs:1,sm:2,md:2,lg:2},badgeShow:!1,arrowShow:!1,theme:!1,showCounter:!0,showCounterStyle:"counter"},I="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='11' fill='%234f46e5'/><circle cx='12' cy='12' r='4.5' fill='white'/></svg>";function v(){const i=window.innerWidth;return i<600?"xs":i<992?"sm":i<1200?"md":"lg"}function y(i,e=100){let t;return(...s)=>{clearTimeout(t),t=setTimeout(()=>i(...s),e)}}function m(i){return i?{o:i.offsetTop,h:i.offsetHeight,gppu:i.offsetTop+i.offsetHeight}:{o:0,h:0,gppu:0}}function _(i,e){const t=[];let s=i.previousElementSibling;for(;s;)(!e||s.matches(e))&&t.push(s),s=s.previousElementSibling;return t}const l=class l{constructor(e,t={}){if(typeof e=="string"&&(e=document.querySelector(e)),!e)throw new Error("moTimeline: element not found");this.element=e,this.settings=Object.assign({},w,t),this.settings.columnCount=Object.assign({},w.columnCount,t.columnCount),this._resizeHandler=y(()=>this.refresh(),100),this._initialized=!1,this.init()}init(){const e=this.element;if(a.has(e)){this.refresh();return}const t=Object.assign({},this.settings,{lastItemIdx:0});a.set(e,t),l.instances.add(this),e.classList.add("mo-timeline"),t.theme&&e.classList.add("mo-theme"),Array.from(e.children).length!==0&&(this._initItems(),this._initialized=!0,window.addEventListener("resize",this._resizeHandler))}refresh(){l.instances.forEach(e=>{const t=e.element,s=a.get(t);s&&(s.col=s.columnCount[v()],e._setDivider(),Array.from(t.children).forEach(o=>{e._setPostPosition(o)}))})}initNewItems(){this._initItems()}destroy(){window.removeEventListener("resize",this._resizeHandler),a.delete(this.element),l.instances.delete(this),this.element.classList.remove("mo-timeline","mo-theme","mo-twocol"),Array.from(this.element.children).forEach(e=>{e.classList.remove("mo-item","js-mo-item","mo-inverted","js-mo-inverted","mo-offset"),e.querySelectorAll(".js-mo-badge, .js-mo-arrow").forEach(t=>t.remove())})}_getData(){return a.get(this.element)}_setDivider(){const e=this._getData();e&&(e.col=e.columnCount[v()],this.element.classList.toggle("mo-twocol",e.col>1))}_initItems(){const e=this.element,t=this._getData();if(!t)return;const s=t.lastItemIdx,o=Array.from(e.children),n=o.slice(s);n.length!==0&&(n.forEach((r,c)=>{r.id||(r.id="moT"+crypto.randomUUID()+"_"+(c+s)),r.classList.add("mo-item","js-mo-item")}),this._setDivider(),n.forEach((r,c)=>{t.badgeShow&&this._createBadge(r,c+s+1),t.arrowShow&&this._createArrow(r)}),t.lastItemIdx=o.length,a.set(e,t),this.refresh())}_setPostPosition(e){const t=this._getLeftOrRight(e);t&&(e.classList.toggle("mo-inverted",t.lr>0),e.classList.toggle("js-mo-inverted",t.lr>0),e.classList.toggle("mo-offset",t.badge_offset>0))}_getLeftOrRight(e){if(!e)return null;const t=this._getData();if(!t)return null;const s=t.col,o=_(e,".js-mo-inverted")[0]||null,n=_(e,".js-mo-item:not(.js-mo-inverted)")[0]||null,r=m(n),c=m(o),u=m(e);let h=0,f=0;if(s>1){r.gppu>u.o&&(h=1),c.gppu>r.gppu&&(h=0);const g=e.previousElementSibling;g&&Math.abs(u.o-m(g).o)<40&&(f=1)}return{lr:h,badge_offset:f}}_createBadge(e,t){const s=this._getData(),o=document.createElement("span");if(o.className="mo-badge js-mo-badge",s.showCounter||(o.style.opacity="0"),s.showCounterStyle==="image"){const n=document.createElement("img");n.className="mo-badge-icon",n.alt="",n.src=e.dataset.moIcon||I,o.appendChild(n)}else o.textContent=t;e.prepend(o)}_createArrow(e){const t=document.createElement("span");t.className="mo-arrow js-mo-arrow",e.prepend(t)}};p(l,"instances",new Set);let d=l;exports.MoTimeline=d;exports.default=d;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* moTimeline v2.3.0 — CSS
|
|
3
|
+
*/:root{--mo-line-color: #dde1e7;--mo-badge-bg: #4f46e5;--mo-badge-color: #fff;--mo-badge-size: 26px;--mo-badge-font-size: 12px;--mo-arrow-color: #dde1e7}.mo-timeline{display:block;list-style:none;margin:0;padding:0;position:relative;width:100%}.mo-timeline:after{content:"";display:table;clear:both}.mo-timeline.mo-twocol:before{background-color:var(--mo-line-color);bottom:0;content:"";left:50%;margin-left:-1.5px;position:absolute;top:0;width:3px;z-index:0}.mo-timeline>.mo-item{box-sizing:border-box;display:block;float:left;position:relative;width:50%}.mo-timeline:not(.mo-twocol)>.mo-item{float:none;width:100%}.mo-timeline>.mo-item.mo-inverted{float:right}.mo-badge{align-items:center;background:var(--mo-badge-bg);border-radius:50%;color:var(--mo-badge-color);display:flex;font-size:var(--mo-badge-font-size);font-weight:700;height:var(--mo-badge-size);justify-content:center;min-width:var(--mo-badge-size);overflow:hidden;position:absolute;top:18px;z-index:2}.mo-badge .mo-badge-icon{border-radius:50%;height:100%;object-fit:cover;width:100%}.mo-timeline.mo-twocol>.mo-item:not(.mo-inverted) .mo-badge{left:auto;right:calc(var(--mo-badge-size) / -2)}.mo-timeline.mo-twocol>.mo-item.mo-inverted .mo-badge{left:calc(var(--mo-badge-size) / -2);right:auto}.mo-timeline.mo-twocol>.mo-item.mo-offset .mo-badge{top:calc(18px + var(--mo-badge-size) + 10px)}.mo-timeline.mo-twocol>.mo-item.mo-offset .mo-arrow{top:calc(26px + var(--mo-badge-size) + 10px)}.mo-timeline:not(.mo-twocol)>.mo-item .mo-badge{right:12px;left:auto;top:12px}.mo-arrow{border:8px solid transparent;display:block;height:0;position:absolute;top:26px;width:0;z-index:1}.mo-timeline.mo-twocol>.mo-item:not(.mo-inverted) .mo-arrow{border-left:8px solid var(--mo-arrow-color);border-right:none;left:auto;right:0}.mo-timeline.mo-twocol>.mo-item.mo-inverted .mo-arrow{border-right:8px solid var(--mo-arrow-color);border-left:none;left:0;right:auto}.mo-timeline:not(.mo-twocol)>.mo-item .mo-arrow{display:none}.mo-theme>.mo-item .mo-card{background:#fff;border-radius:8px;box-shadow:0 2px 14px #0000001a;margin:.5rem 1.25rem .5rem .5rem;overflow:hidden;position:relative}.mo-theme>.mo-item.mo-inverted .mo-card{margin:.5rem .5rem .5rem 1.25rem}.mo-theme>.mo-item .mo-banner{display:block;height:160px;object-fit:cover;width:100%}.mo-theme>.mo-item .mo-card-image{overflow:visible;position:relative}.mo-theme>.mo-item .mo-avatar{border:3px solid #fff;border-radius:50%;bottom:-22px;box-shadow:0 2px 8px #0000002e;height:50px;object-fit:cover;position:absolute;right:14px;width:50px;z-index:1}.mo-theme>.mo-item.mo-inverted .mo-avatar{left:14px;right:auto}.mo-theme>.mo-item .mo-card-body{padding:1.75rem 1rem 1rem}.mo-theme>.mo-item .mo-card-body h3{font-size:1rem;font-weight:700;margin:0 0 .3rem}.mo-theme>.mo-item .mo-card-body .mo-meta{color:#9ca3af;font-size:.75rem;margin-bottom:.5rem}.mo-theme>.mo-item .mo-card-body p{color:#6b7280;font-size:.875rem;line-height:1.55;margin:0}.mo-theme.mo-twocol>.mo-item:not(.mo-inverted) .mo-arrow{border-left-color:#e5e7eb;right:10px}.mo-theme.mo-twocol>.mo-item.mo-inverted .mo-arrow{border-right-color:#e5e7eb;left:10px}.mo-theme .mo-badge{background:#fff;border:2px solid var(--mo-line-color);box-shadow:0 2px 6px #0000001a;color:#374151}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
var b = Object.defineProperty;
|
|
2
|
+
var E = (i, t, e) => t in i ? b(i, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : i[t] = e;
|
|
3
|
+
var g = (i, t, e) => E(i, typeof t != "symbol" ? t + "" : t, e);
|
|
4
|
+
/*!
|
|
5
|
+
* moTimeline v2.3.0
|
|
6
|
+
* Responsive two-column timeline layout library
|
|
7
|
+
* https://github.com/MattOpen/moTimeline
|
|
8
|
+
* MIT License
|
|
9
|
+
*/
|
|
10
|
+
const a = /* @__PURE__ */ new WeakMap(), p = {
|
|
11
|
+
columnCount: { xs: 1, sm: 2, md: 2, lg: 2 },
|
|
12
|
+
badgeShow: !1,
|
|
13
|
+
arrowShow: !1,
|
|
14
|
+
theme: !1,
|
|
15
|
+
showCounter: !0,
|
|
16
|
+
showCounterStyle: "counter"
|
|
17
|
+
// 'counter' | 'image'
|
|
18
|
+
}, I = "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='11' fill='%234f46e5'/><circle cx='12' cy='12' r='4.5' fill='white'/></svg>";
|
|
19
|
+
function w() {
|
|
20
|
+
const i = window.innerWidth;
|
|
21
|
+
return i < 600 ? "xs" : i < 992 ? "sm" : i < 1200 ? "md" : "lg";
|
|
22
|
+
}
|
|
23
|
+
function L(i, t = 100) {
|
|
24
|
+
let e;
|
|
25
|
+
return (...s) => {
|
|
26
|
+
clearTimeout(e), e = setTimeout(() => i(...s), t);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function m(i) {
|
|
30
|
+
return i ? {
|
|
31
|
+
o: i.offsetTop,
|
|
32
|
+
h: i.offsetHeight,
|
|
33
|
+
gppu: i.offsetTop + i.offsetHeight
|
|
34
|
+
} : { o: 0, h: 0, gppu: 0 };
|
|
35
|
+
}
|
|
36
|
+
function v(i, t) {
|
|
37
|
+
const e = [];
|
|
38
|
+
let s = i.previousElementSibling;
|
|
39
|
+
for (; s; )
|
|
40
|
+
(!t || s.matches(t)) && e.push(s), s = s.previousElementSibling;
|
|
41
|
+
return e;
|
|
42
|
+
}
|
|
43
|
+
const c = class c {
|
|
44
|
+
constructor(t, e = {}) {
|
|
45
|
+
if (typeof t == "string" && (t = document.querySelector(t)), !t) throw new Error("moTimeline: element not found");
|
|
46
|
+
this.element = t, this.settings = Object.assign({}, p, e), this.settings.columnCount = Object.assign({}, p.columnCount, e.columnCount), this._resizeHandler = L(() => this.refresh(), 100), this._initialized = !1, this.init();
|
|
47
|
+
}
|
|
48
|
+
init() {
|
|
49
|
+
const t = this.element;
|
|
50
|
+
if (a.has(t)) {
|
|
51
|
+
this.refresh();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const e = Object.assign({}, this.settings, { lastItemIdx: 0 });
|
|
55
|
+
a.set(t, e), c.instances.add(this), t.classList.add("mo-timeline"), e.theme && t.classList.add("mo-theme"), Array.from(t.children).length !== 0 && (this._initItems(), this._initialized = !0, window.addEventListener("resize", this._resizeHandler));
|
|
56
|
+
}
|
|
57
|
+
refresh() {
|
|
58
|
+
c.instances.forEach((t) => {
|
|
59
|
+
const e = t.element, s = a.get(e);
|
|
60
|
+
s && (s.col = s.columnCount[w()], t._setDivider(), Array.from(e.children).forEach((o) => {
|
|
61
|
+
t._setPostPosition(o);
|
|
62
|
+
}));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
initNewItems() {
|
|
66
|
+
this._initItems();
|
|
67
|
+
}
|
|
68
|
+
destroy() {
|
|
69
|
+
window.removeEventListener("resize", this._resizeHandler), a.delete(this.element), c.instances.delete(this), this.element.classList.remove("mo-timeline", "mo-theme", "mo-twocol"), Array.from(this.element.children).forEach((t) => {
|
|
70
|
+
t.classList.remove("mo-item", "js-mo-item", "mo-inverted", "js-mo-inverted", "mo-offset"), t.querySelectorAll(".js-mo-badge, .js-mo-arrow").forEach((e) => e.remove());
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// ─── Private ────────────────────────────────────────────────────────────────
|
|
74
|
+
_getData() {
|
|
75
|
+
return a.get(this.element);
|
|
76
|
+
}
|
|
77
|
+
_setDivider() {
|
|
78
|
+
const t = this._getData();
|
|
79
|
+
t && (t.col = t.columnCount[w()], this.element.classList.toggle("mo-twocol", t.col > 1));
|
|
80
|
+
}
|
|
81
|
+
_initItems() {
|
|
82
|
+
const t = this.element, e = this._getData();
|
|
83
|
+
if (!e) return;
|
|
84
|
+
const s = e.lastItemIdx, o = Array.from(t.children), n = o.slice(s);
|
|
85
|
+
n.length !== 0 && (n.forEach((r, l) => {
|
|
86
|
+
r.id || (r.id = "moT" + crypto.randomUUID() + "_" + (l + s)), r.classList.add("mo-item", "js-mo-item");
|
|
87
|
+
}), this._setDivider(), n.forEach((r, l) => {
|
|
88
|
+
e.badgeShow && this._createBadge(r, l + s + 1), e.arrowShow && this._createArrow(r);
|
|
89
|
+
}), e.lastItemIdx = o.length, a.set(t, e), this.refresh());
|
|
90
|
+
}
|
|
91
|
+
_setPostPosition(t) {
|
|
92
|
+
const e = this._getLeftOrRight(t);
|
|
93
|
+
e && (t.classList.toggle("mo-inverted", e.lr > 0), t.classList.toggle("js-mo-inverted", e.lr > 0), t.classList.toggle("mo-offset", e.badge_offset > 0));
|
|
94
|
+
}
|
|
95
|
+
_getLeftOrRight(t) {
|
|
96
|
+
if (!t) return null;
|
|
97
|
+
const e = this._getData();
|
|
98
|
+
if (!e) return null;
|
|
99
|
+
const s = e.col, o = v(t, ".js-mo-inverted")[0] || null, n = v(t, ".js-mo-item:not(.js-mo-inverted)")[0] || null, r = m(n), l = m(o), d = m(t);
|
|
100
|
+
let h = 0, f = 0;
|
|
101
|
+
if (s > 1) {
|
|
102
|
+
r.gppu > d.o && (h = 1), l.gppu > r.gppu && (h = 0);
|
|
103
|
+
const u = t.previousElementSibling;
|
|
104
|
+
u && Math.abs(d.o - m(u).o) < 40 && (f = 1);
|
|
105
|
+
}
|
|
106
|
+
return { lr: h, badge_offset: f };
|
|
107
|
+
}
|
|
108
|
+
_createBadge(t, e) {
|
|
109
|
+
const s = this._getData(), o = document.createElement("span");
|
|
110
|
+
if (o.className = "mo-badge js-mo-badge", s.showCounter || (o.style.opacity = "0"), s.showCounterStyle === "image") {
|
|
111
|
+
const n = document.createElement("img");
|
|
112
|
+
n.className = "mo-badge-icon", n.alt = "", n.src = t.dataset.moIcon || I, o.appendChild(n);
|
|
113
|
+
} else
|
|
114
|
+
o.textContent = e;
|
|
115
|
+
t.prepend(o);
|
|
116
|
+
}
|
|
117
|
+
_createArrow(t) {
|
|
118
|
+
const e = document.createElement("span");
|
|
119
|
+
e.className = "mo-arrow js-mo-arrow", t.prepend(e);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
g(c, "instances", /* @__PURE__ */ new Set());
|
|
123
|
+
let _ = c;
|
|
124
|
+
export {
|
|
125
|
+
_ as MoTimeline,
|
|
126
|
+
_ as default
|
|
127
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
(function(n,i){typeof exports=="object"&&typeof module<"u"?i(exports):typeof define=="function"&&define.amd?define(["exports"],i):(n=typeof globalThis<"u"?globalThis:n||self,i(n.MoTimeline={}))})(this,function(n){"use strict";var I=Object.defineProperty;var j=(n,i,c)=>i in n?I(n,i,{enumerable:!0,configurable:!0,writable:!0,value:c}):n[i]=c;var b=(n,i,c)=>j(n,typeof i!="symbol"?i+"":i,c);/*!
|
|
2
|
+
* moTimeline v2.3.0
|
|
3
|
+
* Responsive two-column timeline layout library
|
|
4
|
+
* https://github.com/MattOpen/moTimeline
|
|
5
|
+
* MIT License
|
|
6
|
+
*/const i=new WeakMap,c={columnCount:{xs:1,sm:2,md:2,lg:2},badgeShow:!1,arrowShow:!1,theme:!1,showCounter:!0,showCounterStyle:"counter"},y="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='11' fill='%234f46e5'/><circle cx='12' cy='12' r='4.5' fill='white'/></svg>";function g(){const o=window.innerWidth;return o<600?"xs":o<992?"sm":o<1200?"md":"lg"}function E(o,e=100){let t;return(...s)=>{clearTimeout(t),t=setTimeout(()=>o(...s),e)}}function f(o){return o?{o:o.offsetTop,h:o.offsetHeight,gppu:o.offsetTop+o.offsetHeight}:{o:0,h:0,gppu:0}}function p(o,e){const t=[];let s=o.previousElementSibling;for(;s;)(!e||s.matches(e))&&t.push(s),s=s.previousElementSibling;return t}const m=class m{constructor(e,t={}){if(typeof e=="string"&&(e=document.querySelector(e)),!e)throw new Error("moTimeline: element not found");this.element=e,this.settings=Object.assign({},c,t),this.settings.columnCount=Object.assign({},c.columnCount,t.columnCount),this._resizeHandler=E(()=>this.refresh(),100),this._initialized=!1,this.init()}init(){const e=this.element;if(i.has(e)){this.refresh();return}const t=Object.assign({},this.settings,{lastItemIdx:0});i.set(e,t),m.instances.add(this),e.classList.add("mo-timeline"),t.theme&&e.classList.add("mo-theme"),Array.from(e.children).length!==0&&(this._initItems(),this._initialized=!0,window.addEventListener("resize",this._resizeHandler))}refresh(){m.instances.forEach(e=>{const t=e.element,s=i.get(t);s&&(s.col=s.columnCount[g()],e._setDivider(),Array.from(t.children).forEach(r=>{e._setPostPosition(r)}))})}initNewItems(){this._initItems()}destroy(){window.removeEventListener("resize",this._resizeHandler),i.delete(this.element),m.instances.delete(this),this.element.classList.remove("mo-timeline","mo-theme","mo-twocol"),Array.from(this.element.children).forEach(e=>{e.classList.remove("mo-item","js-mo-item","mo-inverted","js-mo-inverted","mo-offset"),e.querySelectorAll(".js-mo-badge, .js-mo-arrow").forEach(t=>t.remove())})}_getData(){return i.get(this.element)}_setDivider(){const e=this._getData();e&&(e.col=e.columnCount[g()],this.element.classList.toggle("mo-twocol",e.col>1))}_initItems(){const e=this.element,t=this._getData();if(!t)return;const s=t.lastItemIdx,r=Array.from(e.children),a=r.slice(s);a.length!==0&&(a.forEach((l,d)=>{l.id||(l.id="moT"+crypto.randomUUID()+"_"+(d+s)),l.classList.add("mo-item","js-mo-item")}),this._setDivider(),a.forEach((l,d)=>{t.badgeShow&&this._createBadge(l,d+s+1),t.arrowShow&&this._createArrow(l)}),t.lastItemIdx=r.length,i.set(e,t),this.refresh())}_setPostPosition(e){const t=this._getLeftOrRight(e);t&&(e.classList.toggle("mo-inverted",t.lr>0),e.classList.toggle("js-mo-inverted",t.lr>0),e.classList.toggle("mo-offset",t.badge_offset>0))}_getLeftOrRight(e){if(!e)return null;const t=this._getData();if(!t)return null;const s=t.col,r=p(e,".js-mo-inverted")[0]||null,a=p(e,".js-mo-item:not(.js-mo-inverted)")[0]||null,l=f(a),d=f(r),w=f(e);let u=0,v=0;if(s>1){l.gppu>w.o&&(u=1),d.gppu>l.gppu&&(u=0);const _=e.previousElementSibling;_&&Math.abs(w.o-f(_).o)<40&&(v=1)}return{lr:u,badge_offset:v}}_createBadge(e,t){const s=this._getData(),r=document.createElement("span");if(r.className="mo-badge js-mo-badge",s.showCounter||(r.style.opacity="0"),s.showCounterStyle==="image"){const a=document.createElement("img");a.className="mo-badge-icon",a.alt="",a.src=e.dataset.moIcon||y,r.appendChild(a)}else r.textContent=t;e.prepend(r)}_createArrow(e){const t=document.createElement("span");t.className="mo-arrow js-mo-arrow",e.prepend(t)}};b(m,"instances",new Set);let h=m;n.MoTimeline=h,n.default=h,Object.defineProperties(n,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "motimeline",
|
|
3
|
+
"version": "2.3.0",
|
|
4
|
+
"description": "Responsive two-column timeline layout library. Plain JavaScript, no dependencies.",
|
|
5
|
+
"main": "./dist/moTimeline.cjs",
|
|
6
|
+
"module": "./dist/moTimeline.js",
|
|
7
|
+
"style": "./dist/moTimeline.css",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/moTimeline.js",
|
|
11
|
+
"require": "./dist/moTimeline.cjs"
|
|
12
|
+
},
|
|
13
|
+
"./dist/moTimeline.css": "./dist/moTimeline.css"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "vite build",
|
|
21
|
+
"dev": "vite serve example"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"vite": "^5.0.0"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"timeline",
|
|
28
|
+
"JavaScript",
|
|
29
|
+
"responsive",
|
|
30
|
+
"two-column"
|
|
31
|
+
],
|
|
32
|
+
"author": "mattopen - www.mattopen.com",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/MattOpen/moTimeline.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/MattOpen/moTimeline/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://mattopen.github.io/moTimeline/"
|
|
42
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* moTimeline v2.3.0 — CSS
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/* ── CSS custom properties (easy override) ─────────────────── */
|
|
6
|
+
:root {
|
|
7
|
+
--mo-line-color: #dde1e7;
|
|
8
|
+
--mo-badge-bg: #4f46e5;
|
|
9
|
+
--mo-badge-color: #fff;
|
|
10
|
+
--mo-badge-size: 26px;
|
|
11
|
+
--mo-badge-font-size: 12px;
|
|
12
|
+
--mo-arrow-color: #dde1e7;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* ── Container ──────────────────────────────────────────────── */
|
|
16
|
+
|
|
17
|
+
.mo-timeline {
|
|
18
|
+
display: block;
|
|
19
|
+
list-style: none;
|
|
20
|
+
margin: 0;
|
|
21
|
+
padding: 0;
|
|
22
|
+
position: relative;
|
|
23
|
+
width: 100%;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Clearfix */
|
|
27
|
+
.mo-timeline::after {
|
|
28
|
+
content: '';
|
|
29
|
+
display: table;
|
|
30
|
+
clear: both;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Center vertical line — only in two-column mode */
|
|
34
|
+
.mo-timeline.mo-twocol::before {
|
|
35
|
+
background-color: var(--mo-line-color);
|
|
36
|
+
bottom: 0;
|
|
37
|
+
content: '';
|
|
38
|
+
left: 50%;
|
|
39
|
+
margin-left: -1.5px;
|
|
40
|
+
position: absolute;
|
|
41
|
+
top: 0;
|
|
42
|
+
width: 3px;
|
|
43
|
+
z-index: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* ── Items ──────────────────────────────────────────────────── */
|
|
47
|
+
|
|
48
|
+
.mo-timeline > .mo-item {
|
|
49
|
+
box-sizing: border-box;
|
|
50
|
+
display: block;
|
|
51
|
+
float: left;
|
|
52
|
+
position: relative;
|
|
53
|
+
width: 50%;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Single column */
|
|
57
|
+
.mo-timeline:not(.mo-twocol) > .mo-item {
|
|
58
|
+
float: none;
|
|
59
|
+
width: 100%;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Right-column item */
|
|
63
|
+
.mo-timeline > .mo-item.mo-inverted {
|
|
64
|
+
float: right;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* ── Badge — circle sitting ON the center line ──────────────── */
|
|
68
|
+
|
|
69
|
+
.mo-badge {
|
|
70
|
+
align-items: center;
|
|
71
|
+
background: var(--mo-badge-bg);
|
|
72
|
+
border-radius: 50%;
|
|
73
|
+
color: var(--mo-badge-color);
|
|
74
|
+
display: flex;
|
|
75
|
+
font-size: var(--mo-badge-font-size);
|
|
76
|
+
font-weight: 700;
|
|
77
|
+
height: var(--mo-badge-size);
|
|
78
|
+
justify-content: center;
|
|
79
|
+
min-width: var(--mo-badge-size);
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
position: absolute;
|
|
82
|
+
top: 18px;
|
|
83
|
+
z-index: 2;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Badge image mode */
|
|
87
|
+
.mo-badge .mo-badge-icon {
|
|
88
|
+
border-radius: 50%;
|
|
89
|
+
height: 100%;
|
|
90
|
+
object-fit: cover;
|
|
91
|
+
width: 100%;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* Left item → badge pokes out to the RIGHT (= center line) */
|
|
95
|
+
.mo-timeline.mo-twocol > .mo-item:not(.mo-inverted) .mo-badge {
|
|
96
|
+
left: auto;
|
|
97
|
+
right: calc(var(--mo-badge-size) / -2);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Right item → badge pokes out to the LEFT (= center line) */
|
|
101
|
+
.mo-timeline.mo-twocol > .mo-item.mo-inverted .mo-badge {
|
|
102
|
+
left: calc(var(--mo-badge-size) / -2);
|
|
103
|
+
right: auto;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* Either item starts near the opposite column's badge →
|
|
107
|
+
push the badge & arrow down so they don't overlap on the center line */
|
|
108
|
+
.mo-timeline.mo-twocol > .mo-item.mo-offset .mo-badge {
|
|
109
|
+
top: calc(18px + var(--mo-badge-size) + 10px);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.mo-timeline.mo-twocol > .mo-item.mo-offset .mo-arrow {
|
|
113
|
+
top: calc(26px + var(--mo-badge-size) + 10px);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Single column: badge in top-right of item */
|
|
117
|
+
.mo-timeline:not(.mo-twocol) > .mo-item .mo-badge {
|
|
118
|
+
right: 12px;
|
|
119
|
+
left: auto;
|
|
120
|
+
top: 12px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* ── Arrow — triangle pointing FROM card TOWARD center line ─── */
|
|
124
|
+
|
|
125
|
+
.mo-arrow {
|
|
126
|
+
border: 8px solid transparent;
|
|
127
|
+
display: block;
|
|
128
|
+
height: 0;
|
|
129
|
+
position: absolute;
|
|
130
|
+
top: 26px;
|
|
131
|
+
width: 0;
|
|
132
|
+
z-index: 1;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Left item → tip at right edge (center line), pointing right → */
|
|
136
|
+
.mo-timeline.mo-twocol > .mo-item:not(.mo-inverted) .mo-arrow {
|
|
137
|
+
border-left: 8px solid var(--mo-arrow-color);
|
|
138
|
+
border-right: none;
|
|
139
|
+
left: auto;
|
|
140
|
+
right: 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Right item → tip at left edge (center line), pointing left ← */
|
|
144
|
+
.mo-timeline.mo-twocol > .mo-item.mo-inverted .mo-arrow {
|
|
145
|
+
border-right: 8px solid var(--mo-arrow-color);
|
|
146
|
+
border-left: none;
|
|
147
|
+
left: 0;
|
|
148
|
+
right: auto;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Hide arrow in single-column mode */
|
|
152
|
+
.mo-timeline:not(.mo-twocol) > .mo-item .mo-arrow {
|
|
153
|
+
display: none;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
157
|
+
THEME — enabled via new MoTimeline(el, { theme: true })
|
|
158
|
+
or by adding .mo-theme manually to the container
|
|
159
|
+
═══════════════════════════════════════════════════════════════ */
|
|
160
|
+
|
|
161
|
+
/* Themed card shell */
|
|
162
|
+
.mo-theme > .mo-item .mo-card {
|
|
163
|
+
background: #fff;
|
|
164
|
+
border-radius: 8px;
|
|
165
|
+
box-shadow: 0 2px 14px rgba(0, 0, 0, .10);
|
|
166
|
+
margin: 0.5rem 1.25rem 0.5rem 0.5rem;
|
|
167
|
+
overflow: hidden;
|
|
168
|
+
position: relative;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.mo-theme > .mo-item.mo-inverted .mo-card {
|
|
172
|
+
margin: 0.5rem 0.5rem 0.5rem 1.25rem;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* Banner image */
|
|
176
|
+
.mo-theme > .mo-item .mo-banner {
|
|
177
|
+
display: block;
|
|
178
|
+
height: 160px;
|
|
179
|
+
object-fit: cover;
|
|
180
|
+
width: 100%;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* Image wrapper (needed for avatar overlap) */
|
|
184
|
+
.mo-theme > .mo-item .mo-card-image {
|
|
185
|
+
overflow: visible;
|
|
186
|
+
position: relative;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* Avatar — overlaps bottom of banner image */
|
|
190
|
+
.mo-theme > .mo-item .mo-avatar {
|
|
191
|
+
border: 3px solid #fff;
|
|
192
|
+
border-radius: 50%;
|
|
193
|
+
bottom: -22px;
|
|
194
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, .18);
|
|
195
|
+
height: 50px;
|
|
196
|
+
object-fit: cover;
|
|
197
|
+
position: absolute;
|
|
198
|
+
right: 14px;
|
|
199
|
+
width: 50px;
|
|
200
|
+
z-index: 1;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* Mirror: right-column items → avatar on left side (toward center) */
|
|
204
|
+
.mo-theme > .mo-item.mo-inverted .mo-avatar {
|
|
205
|
+
left: 14px;
|
|
206
|
+
right: auto;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Card body text area */
|
|
210
|
+
.mo-theme > .mo-item .mo-card-body {
|
|
211
|
+
padding: 1.75rem 1rem 1rem;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.mo-theme > .mo-item .mo-card-body h3 {
|
|
215
|
+
font-size: 1rem;
|
|
216
|
+
font-weight: 700;
|
|
217
|
+
margin: 0 0 0.3rem;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.mo-theme > .mo-item .mo-card-body .mo-meta {
|
|
221
|
+
color: #9ca3af;
|
|
222
|
+
font-size: 0.75rem;
|
|
223
|
+
margin-bottom: 0.5rem;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.mo-theme > .mo-item .mo-card-body p {
|
|
227
|
+
color: #6b7280;
|
|
228
|
+
font-size: 0.875rem;
|
|
229
|
+
line-height: 1.55;
|
|
230
|
+
margin: 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/* Theme: arrow color matches card shadow edge */
|
|
234
|
+
.mo-theme.mo-twocol > .mo-item:not(.mo-inverted) .mo-arrow {
|
|
235
|
+
border-left-color: #e5e7eb;
|
|
236
|
+
right: 10px;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.mo-theme.mo-twocol > .mo-item.mo-inverted .mo-arrow {
|
|
240
|
+
border-right-color: #e5e7eb;
|
|
241
|
+
left: 10px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* Theme: badge styled to match card aesthetic */
|
|
245
|
+
.mo-theme .mo-badge {
|
|
246
|
+
background: #fff;
|
|
247
|
+
border: 2px solid var(--mo-line-color);
|
|
248
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, .10);
|
|
249
|
+
color: #374151;
|
|
250
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* moTimeline v2.3.0
|
|
3
|
+
* Responsive two-column timeline layout library
|
|
4
|
+
* https://github.com/MattOpen/moTimeline
|
|
5
|
+
* MIT License
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import './moTimeline.css';
|
|
9
|
+
|
|
10
|
+
const instanceData = new WeakMap();
|
|
11
|
+
|
|
12
|
+
const BREAKPOINTS = {
|
|
13
|
+
xs: 0,
|
|
14
|
+
sm: 600,
|
|
15
|
+
md: 992,
|
|
16
|
+
lg: 1200,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const DEFAULTS = {
|
|
20
|
+
columnCount: { xs: 1, sm: 2, md: 2, lg: 2 },
|
|
21
|
+
badgeShow: false,
|
|
22
|
+
arrowShow: false,
|
|
23
|
+
theme: false,
|
|
24
|
+
showCounter: true,
|
|
25
|
+
showCounterStyle: 'counter', // 'counter' | 'image'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const DEFAULT_BADGE_ICON = "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><circle cx='12' cy='12' r='11' fill='%234f46e5'/><circle cx='12' cy='12' r='4.5' fill='white'/></svg>";
|
|
29
|
+
|
|
30
|
+
function getBreakpoint() {
|
|
31
|
+
const w = window.innerWidth;
|
|
32
|
+
if (w < 600) return 'xs';
|
|
33
|
+
if (w < 992) return 'sm';
|
|
34
|
+
if (w < 1200) return 'md';
|
|
35
|
+
return 'lg';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function debounce(fn, delay = 100) {
|
|
39
|
+
let timer;
|
|
40
|
+
return (...args) => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
timer = setTimeout(() => fn(...args), delay);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getPosition(el) {
|
|
47
|
+
if (!el) return { o: 0, h: 0, gppu: 0 };
|
|
48
|
+
return {
|
|
49
|
+
o: el.offsetTop,
|
|
50
|
+
h: el.offsetHeight,
|
|
51
|
+
gppu: el.offsetTop + el.offsetHeight,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function prevAll(el, selector) {
|
|
56
|
+
const results = [];
|
|
57
|
+
let sibling = el.previousElementSibling;
|
|
58
|
+
while (sibling) {
|
|
59
|
+
if (!selector || sibling.matches(selector)) results.push(sibling);
|
|
60
|
+
sibling = sibling.previousElementSibling;
|
|
61
|
+
}
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class MoTimeline {
|
|
66
|
+
static instances = new Set();
|
|
67
|
+
|
|
68
|
+
constructor(element, options = {}) {
|
|
69
|
+
if (typeof element === 'string') {
|
|
70
|
+
element = document.querySelector(element);
|
|
71
|
+
}
|
|
72
|
+
if (!element) throw new Error('moTimeline: element not found');
|
|
73
|
+
|
|
74
|
+
this.element = element;
|
|
75
|
+
this.settings = Object.assign({}, DEFAULTS, options);
|
|
76
|
+
this.settings.columnCount = Object.assign({}, DEFAULTS.columnCount, options.columnCount);
|
|
77
|
+
this._resizeHandler = debounce(() => this.refresh(), 100);
|
|
78
|
+
this._initialized = false;
|
|
79
|
+
|
|
80
|
+
this.init();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
init() {
|
|
84
|
+
const el = this.element;
|
|
85
|
+
|
|
86
|
+
// Already initialized — just refresh
|
|
87
|
+
if (instanceData.has(el)) {
|
|
88
|
+
this.refresh();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const data = Object.assign({}, this.settings, { lastItemIdx: 0 });
|
|
93
|
+
instanceData.set(el, data);
|
|
94
|
+
MoTimeline.instances.add(this);
|
|
95
|
+
|
|
96
|
+
el.classList.add('mo-timeline');
|
|
97
|
+
if (data.theme) el.classList.add('mo-theme');
|
|
98
|
+
|
|
99
|
+
const children = Array.from(el.children);
|
|
100
|
+
if (children.length === 0) return;
|
|
101
|
+
|
|
102
|
+
this._initItems();
|
|
103
|
+
this._initialized = true;
|
|
104
|
+
|
|
105
|
+
window.addEventListener('resize', this._resizeHandler);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
refresh() {
|
|
109
|
+
MoTimeline.instances.forEach((instance) => {
|
|
110
|
+
const el = instance.element;
|
|
111
|
+
const data = instanceData.get(el);
|
|
112
|
+
if (!data) return;
|
|
113
|
+
|
|
114
|
+
data.col = data.columnCount[getBreakpoint()];
|
|
115
|
+
instance._setDivider();
|
|
116
|
+
|
|
117
|
+
Array.from(el.children).forEach((child) => {
|
|
118
|
+
instance._setPostPosition(child);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
initNewItems() {
|
|
124
|
+
this._initItems();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
destroy() {
|
|
128
|
+
window.removeEventListener('resize', this._resizeHandler);
|
|
129
|
+
instanceData.delete(this.element);
|
|
130
|
+
MoTimeline.instances.delete(this);
|
|
131
|
+
this.element.classList.remove('mo-timeline', 'mo-theme', 'mo-twocol');
|
|
132
|
+
Array.from(this.element.children).forEach((child) => {
|
|
133
|
+
child.classList.remove('mo-item', 'js-mo-item', 'mo-inverted', 'js-mo-inverted', 'mo-offset');
|
|
134
|
+
child.querySelectorAll('.js-mo-badge, .js-mo-arrow').forEach((b) => b.remove());
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Private ────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
_getData() {
|
|
141
|
+
return instanceData.get(this.element);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_setDivider() {
|
|
145
|
+
const data = this._getData();
|
|
146
|
+
if (!data) return;
|
|
147
|
+
data.col = data.columnCount[getBreakpoint()];
|
|
148
|
+
this.element.classList.toggle('mo-twocol', data.col > 1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
_initItems() {
|
|
152
|
+
const el = this.element;
|
|
153
|
+
const data = this._getData();
|
|
154
|
+
if (!data) return;
|
|
155
|
+
|
|
156
|
+
const lastItemIdx = data.lastItemIdx;
|
|
157
|
+
const allChildren = Array.from(el.children);
|
|
158
|
+
const newItems = allChildren.slice(lastItemIdx);
|
|
159
|
+
|
|
160
|
+
if (newItems.length === 0) return;
|
|
161
|
+
|
|
162
|
+
// Assign IDs and base class
|
|
163
|
+
newItems.forEach((item, i) => {
|
|
164
|
+
if (!item.id) {
|
|
165
|
+
item.id = 'moT' + crypto.randomUUID() + '_' + (i + lastItemIdx);
|
|
166
|
+
}
|
|
167
|
+
item.classList.add('mo-item', 'js-mo-item');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
this._setDivider();
|
|
171
|
+
|
|
172
|
+
// Badges / arrows
|
|
173
|
+
newItems.forEach((item, i) => {
|
|
174
|
+
if (data.badgeShow) {
|
|
175
|
+
this._createBadge(item, i + lastItemIdx + 1);
|
|
176
|
+
}
|
|
177
|
+
if (data.arrowShow) {
|
|
178
|
+
this._createArrow(item);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
data.lastItemIdx = allChildren.length;
|
|
183
|
+
instanceData.set(el, data);
|
|
184
|
+
|
|
185
|
+
this.refresh();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
_setPostPosition(el) {
|
|
189
|
+
const result = this._getLeftOrRight(el);
|
|
190
|
+
if (!result) return;
|
|
191
|
+
|
|
192
|
+
el.classList.toggle('mo-inverted', result.lr > 0);
|
|
193
|
+
el.classList.toggle('js-mo-inverted', result.lr > 0);
|
|
194
|
+
el.classList.toggle('mo-offset', result.badge_offset > 0);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
_getLeftOrRight(el) {
|
|
198
|
+
if (!el) return null;
|
|
199
|
+
|
|
200
|
+
const data = this._getData();
|
|
201
|
+
if (!data) return null;
|
|
202
|
+
|
|
203
|
+
const col = data.col;
|
|
204
|
+
|
|
205
|
+
const prevInverted = prevAll(el, '.js-mo-inverted')[0] || null;
|
|
206
|
+
const prevLeft = prevAll(el, '.js-mo-item:not(.js-mo-inverted)')[0] || null;
|
|
207
|
+
|
|
208
|
+
const l = getPosition(prevLeft);
|
|
209
|
+
const r = getPosition(prevInverted);
|
|
210
|
+
const e = getPosition(el);
|
|
211
|
+
|
|
212
|
+
let pos = 0;
|
|
213
|
+
let bo = 0;
|
|
214
|
+
|
|
215
|
+
if (col > 1) {
|
|
216
|
+
if (l.gppu > e.o) pos = 1;
|
|
217
|
+
if (r.gppu > l.gppu) pos = 0;
|
|
218
|
+
|
|
219
|
+
// Badge collision: the LATER item (current, higher DOM index) gets the
|
|
220
|
+
// offset — never the earlier one. Compare against the immediately
|
|
221
|
+
// preceding sibling regardless of which column it is in.
|
|
222
|
+
const prev = el.previousElementSibling;
|
|
223
|
+
if (prev && Math.abs(e.o - getPosition(prev).o) < 40) bo = 1;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { lr: pos, badge_offset: bo };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
_createBadge(el, idx) {
|
|
230
|
+
const data = this._getData();
|
|
231
|
+
const span = document.createElement('span');
|
|
232
|
+
span.className = 'mo-badge js-mo-badge';
|
|
233
|
+
|
|
234
|
+
if (!data.showCounter) {
|
|
235
|
+
span.style.opacity = '0';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (data.showCounterStyle === 'image') {
|
|
239
|
+
const img = document.createElement('img');
|
|
240
|
+
img.className = 'mo-badge-icon';
|
|
241
|
+
img.alt = '';
|
|
242
|
+
img.src = el.dataset.moIcon || DEFAULT_BADGE_ICON;
|
|
243
|
+
span.appendChild(img);
|
|
244
|
+
} else {
|
|
245
|
+
span.textContent = idx;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
el.prepend(span);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
_createArrow(el) {
|
|
252
|
+
const span = document.createElement('span');
|
|
253
|
+
span.className = 'mo-arrow js-mo-arrow';
|
|
254
|
+
el.prepend(span);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export default MoTimeline;
|