timeline.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. function customTimeline() {
  2. let colors, titles, partTitles;
  3. let partDurations = [];
  4. let totalTime;
  5. let tlContainer, tlContainerHeight;
  6. let partBlockHeights = [];
  7. let activePartIndex = 0;
  8. let steps = {
  9. tlElSteps: [],
  10. scrollSteps: []
  11. }
  12. let previousScroll;
  13. let grabbing = false;
  14. let titresFrise;
  15. let cursor = document.querySelector('#cursor');
  16. let titresCursor;
  17. let partRects;
  18. let isScrollingFromGrab = false;
  19. let prevPartIndex = 0;
  20. let lastCallTimestamp = 0;
  21. let timestampDebounce = 200;
  22. // titres de parties dans le tableau
  23. function setPartTitlesInPartition() {
  24. let mainpartTitles = document.querySelectorAll('.isMainPart');
  25. for (let mainpartTitle of mainpartTitles) {
  26. if (mainpartTitle.nextElementSibling.classList.contains('isSubPart')) {
  27. mainpartTitle.firstElementChild.firstElementChild.style.height = "1.5rem";
  28. mainpartTitle.firstElementChild.style.marginBottom = "-0.25rem";
  29. mainpartTitle.style.borderBottom = "0";
  30. mainpartTitle.style.paddingBottom = "0";
  31. mainpartTitle.nextElementSibling.style.paddingTop = "0";
  32. }
  33. }
  34. }
  35. // couleurs titres
  36. function setPartsColors() {
  37. colors = [
  38. {'red' : 'cf0118'},
  39. {'blue1': '0101c4'},
  40. {'blue2': '01049e'},
  41. {'blue3': '010678'},
  42. {'blue4': '010952'},
  43. {'blue5': '00113c'},
  44. {'yellow1': 'ade719'},
  45. {'yellow2': '8cc700'},
  46. {'yellow3': '74af00'},
  47. {'yellow4': '5c9900'},
  48. {'yellow5': '377600'},
  49. {'pink1': 'cf0118'},
  50. {'pink2': 'a10418'}
  51. ];
  52. titles = document.querySelectorAll('.isMainPart, .isSubPart');
  53. partTitles = [];
  54. for (let title of titles) {
  55. if (!title.nextElementSibling.classList.contains('isSubPart')) {
  56. partTitles.push(title);
  57. }
  58. }
  59. for (let i = 0; i < partTitles.length; i++) {
  60. if (partTitles[i].previousElementSibling?.classList.contains('isMainPart')) {
  61. partTitles[i].previousElementSibling.firstElementChild.firstElementChild.style.backgroundColor = "#" + Object.values(colors[i])[0];
  62. }
  63. partTitles[i].firstElementChild.firstElementChild.style.backgroundColor = "#" + Object.values(colors[i])[0];
  64. }
  65. }
  66. // set parts rectangles
  67. function convertToSeconds(timeStr) {
  68. let [hourStr, minStr] = timeStr.split('h');
  69. if (!minStr) {
  70. minStr = hourStr;
  71. hourStr = '0';
  72. }
  73. minStr = minStr.replace('’', ':');
  74. minStr = minStr.replace("'", ':');
  75. const [min, sec] = minStr.split(':');
  76. return parseInt(hourStr) * 3600 + parseInt(min) * 60 + parseInt(sec);
  77. }
  78. function getPartsTimes() {
  79. let partTimesNodes = document.querySelectorAll('body #app main tbody tr:not(.isContentPart) + tr:not(.isSubPart) td:first-of-type');
  80. let partTimes = Array.from(partTimesNodes);
  81. for (let i = 0; i < partTimes.length; i++) {
  82. partTimes[i] = convertToSeconds(partTimes[i].innerText);
  83. }
  84. let lastTime = document.querySelectorAll('body #app main tbody tr:last-of-type td:first-of-type');
  85. partTimes.push(convertToSeconds(lastTime[0].innerText));
  86. for (let i = 0; i < partTimes.length - 1; i++) {
  87. partDurations.push(partTimes[i + 1] - partTimes[i]);
  88. }
  89. totalTime = convertToSeconds(lastTime[0].innerText);
  90. }
  91. function getAllHeights() {
  92. partBlockHeights = [];
  93. tlContainer = document.querySelector('#timeline_container');
  94. let header = document.querySelector('header');
  95. let footer = document.querySelector('footer');
  96. tlContainer.style.height = `calc(100vh - ${header.offsetHeight}px - ${footer.offsetHeight}px - 60px)`;
  97. tlContainer.style.top = `${header.offsetHeight + 30}px`;
  98. tlContainerHeight = tlContainer.offsetHeight;
  99. for (let partDuration of partDurations) {
  100. partBlockHeights.push(partDuration / totalTime * tlContainerHeight);
  101. }
  102. }
  103. function drawFriseRects() {
  104. for (let i = 0; i < partBlockHeights.length; i++) {
  105. let partDiv = document.createElement('div');
  106. partDiv.classList.add('tlRectPart');
  107. partDiv.style.height = partBlockHeights[i] + "px";
  108. partDiv.style.backgroundColor = "#" + Object.values(colors[i])[0];
  109. tlContainer.prepend(partDiv);
  110. tlContainer.children[0].addEventListener("mouseenter", function() {
  111. let el = tlContainer.children[tlContainer.children.length - 1 - i];
  112. if (Array.from(el.parentNode.children).length - Array.from(el.parentNode.children).indexOf(el) - 1 != activePartIndex) {
  113. el.style.width = "32px";
  114. toggleTitleHover(i, 'show');
  115. }
  116. });
  117. tlContainer.children[0].addEventListener("mouseleave", function() {
  118. let el = tlContainer.children[tlContainer.children.length - 1 - i];
  119. if (Array.from(el.parentNode.children).length - Array.from(el.parentNode.children).indexOf(el) - 1 != activePartIndex) {
  120. el.style.width = "22px";
  121. }
  122. toggleTitleHover(i, 'hide');
  123. });
  124. tlContainer.children[0].addEventListener("click", function() {
  125. isScrollingFromGrab = false;
  126. titresFrise[i].el.scrollIntoView({ behavior: 'smooth', block: 'center' });
  127. });
  128. }
  129. }
  130. // detect if is scrolling
  131. function isWindowScrolling() {
  132. let currentScroll = window.scrollY;
  133. if (currentScroll !== previousScroll) {
  134. previousScroll = currentScroll;
  135. return true;
  136. } else {
  137. return false;
  138. }
  139. }
  140. // titres parties frise
  141. function displayTimelineTitles() {
  142. let mainWithoutSubAfter = [];
  143. let mainWithSubAfter = [];
  144. let subWithoutMainBefore = [];
  145. let elements = Array.from(document.querySelectorAll('.isMainPart, .isSubPart'));
  146. let lastMain = null;
  147. for (let i = 0; i < elements.length; i++) {
  148. let current = elements[i];
  149. let next = elements[i + 1];
  150. if (current.classList.contains('isMainPart')) {
  151. lastMain = current;
  152. if (next && next.classList.contains('isSubPart')) {
  153. mainWithSubAfter.push({ index: i, main: current.innerText, sub: next.innerText, el: elements[i] });
  154. } else {
  155. mainWithoutSubAfter.push({ index: i, main: current.innerText, sub: "", el: elements[i] });
  156. }
  157. }
  158. if (current.classList.contains('isSubPart')) {
  159. let prevMain = lastMain && lastMain.classList.contains('isMainPart') ? lastMain.innerText : "";
  160. subWithoutMainBefore.push({ index: i, main: prevMain, sub: current.innerText, el: elements[i] });
  161. }
  162. }
  163. titresFrise = [...mainWithoutSubAfter, ...mainWithSubAfter, ...subWithoutMainBefore];
  164. titresFrise.sort((a, b) => a.index - b.index);
  165. for (let i = titresFrise.length - 1; i > 0; i--) {
  166. let current = titresFrise[i];
  167. let next = titresFrise[i - 1];
  168. if (current.main === next.main && current.sub === next.sub) {
  169. titresFrise.splice(i, 1);
  170. }
  171. }
  172. let titreFriseEl = document.createElement('div');
  173. titreFriseEl.setAttribute('id', 'titres_frise');
  174. titreFriseEl.innerHTML = `
  175. <p class="uppercase">${getCurrentTime(titresFrise[0].el)}</p>
  176. <p>${titresFrise[0].main}</p>
  177. <p class="font-authentic-60">${titresFrise[0].sub}</p>
  178. `;
  179. titreFriseEl.style.top = `${document.querySelector('#timeline_container').getBoundingClientRect().top}px`;
  180. let main = document.querySelector('#main');
  181. main.prepend(titreFriseEl);
  182. titresCursor = document.querySelector('#titres_frise');
  183. setTimeout(() => {
  184. titreFriseEl.firstElementChild.innerText = getCurrentTime(titresFrise[0].el);
  185. }, 10);
  186. }
  187. // création et togle des titres au survol des div de la timeline
  188. function drawFixedTitles() {
  189. let tlContainer = document.querySelector('#timeline_container');
  190. let fixedTitlesContainer = document.createElement('div');
  191. fixedTitlesContainer.setAttribute('id', 'fixedTitlesContainer');
  192. main.prepend(fixedTitlesContainer);
  193. for (let index = 0; index < titresFrise.length; index++) {
  194. let titreFixedEl = document.createElement('div');
  195. titreFixedEl.classList.add('tlFixedTitle');
  196. titreFixedEl.style.top = `${tlContainer.children[index].getBoundingClientRect().top - 8}px`;
  197. let titreFixedElContent = document.createElement('p');
  198. titreFixedElContent.innerHTML = `
  199. <p>${titresFrise[titresFrise.length - index - 1].main}</p>
  200. <p class="font-authentic-60">${titresFrise[titresFrise.length - index - 1].sub}</p>
  201. `;
  202. titreFixedEl.append(titreFixedElContent);
  203. fixedTitlesContainer.prepend(titreFixedEl);
  204. }
  205. }
  206. function toggleTitleHover(elIndex, state) {
  207. let fixedTitlesContainer = document.querySelector('#fixedTitlesContainer');
  208. let el = fixedTitlesContainer.children[elIndex];
  209. if (state === 'show') {
  210. el.style.display = 'block';
  211. setTimeout(() => {
  212. el.style.opacity = '1';
  213. }, 1);
  214. } else if (state === 'hide') {
  215. el.style.opacity = '0';
  216. setTimeout(() => {
  217. el.style.display = 'none';
  218. }, 300);
  219. }
  220. }
  221. // make the cursor move on scroll
  222. function setupCursor() {
  223. cursor.style.top = `${document.querySelector('#timeline_container').getBoundingClientRect().top}px`;
  224. cursor.style.cursor = 'grab';
  225. titresCursor.style.cursor = 'grab';
  226. }
  227. function setPartRects() {
  228. partRects = document.querySelectorAll('#timeline_container div');
  229. partRects = Array.from(partRects);
  230. partRects = partRects.reverse();
  231. partRects[0].style.width = '32px';
  232. }
  233. document.addEventListener("scroll", () => {
  234. if (!grabbing) {
  235. displayCurrentPartTitle(getCurrentPartFromScroll());
  236. if (document.documentElement.scrollTop === 0) {
  237. let firstTimeText = document.querySelector('tbody tr:nth-of-type(2) td:first-of-type');
  238. titresCursor.firstElementChild.innerText = firstTimeText.innerText;
  239. } else if (document.documentElement.scrollTop + window.innerHeight >= document.body.scrollHeight) {
  240. let lastTimeText = document.querySelector('tbody tr:last-of-type td:first-of-type');
  241. titresCursor.firstElementChild.innerText = lastTimeText.innerText;
  242. }
  243. moveCursorFromScroll(getCurrentPartFromScroll());
  244. }
  245. });
  246. function makeElementDraggable(element, relatedEl) {
  247. let offsetY;
  248. element.addEventListener('mousedown', (e) => {
  249. let elTransformY = element.style.transform ? +element.style.transform.split('(')[1].split('p')[0] : 0;
  250. e.preventDefault();
  251. grabbing = true;
  252. offsetY = e.clientY - elTransformY;
  253. element.style.cursor = 'grabbing';
  254. });
  255. document.addEventListener('mousemove', (e) => {
  256. if (grabbing) {
  257. const y = e.clientY - offsetY;
  258. if (e.clientY < tlContainerHeight + tlContainer.offsetTop && e.clientY > tlContainer.offsetTop && y > 0) {
  259. element.style.transform = `translateY(${y}px)`;
  260. relatedEl.style.transform = `translateY(${y}px)`;
  261. if (!isNaN(y)) displayCurrentPartTitle(getCurrentPartFromCursor(y));
  262. } else if (e.clientY < tlContainer.offsetTop || y <= 0) {
  263. element.style.transform = `translateY(0px)`;
  264. relatedEl.style.transform = `translateY(0px)`;
  265. setTimeout(() => {
  266. let firstTimeText = document.querySelector('tbody tr:nth-of-type(2) td:first-of-type');
  267. titresCursor.firstElementChild.innerText = firstTimeText.innerText;
  268. }, 100);
  269. } else if (e.clientY >= tlContainerHeight + tlContainer.offsetTop) {
  270. element.style.transform = `translateY(${tlContainerHeight}px)`;
  271. relatedEl.style.transform = `translateY(${tlContainerHeight}px)`;
  272. let lastTimeText = document.querySelector('tbody tr:last-of-type td:first-of-type');
  273. titresCursor.firstElementChild.innerText = lastTimeText.innerText;
  274. }
  275. }
  276. });
  277. document.addEventListener('mouseup', (e) => {
  278. if (grabbing) {
  279. scrollOnGrab(element);
  280. let elTransformY = element.style.transform ? +element.style.transform.split('(')[1].split('p')[0] : 0;
  281. offsetY = e.clientY - elTransformY;
  282. const y = e.clientY - offsetY;
  283. if (e.clientY < tlContainer.offsetTop || y <= 0) {
  284. window.scrollTo(0, 0);
  285. } else {
  286. setTimeout(() => {
  287. titresCursor.firstElementChild.innerText = getCurrentTime(titresFrise[getCurrentPartFromCursor(y)]?.el);
  288. }, 1000);
  289. }
  290. }
  291. element.style.cursor = 'grab';
  292. grabbing = false;
  293. });
  294. }
  295. // get heights of parts dans le tableau et dans la timeline
  296. function getSteps() {
  297. steps.tlElSteps = [];
  298. steps.scrollSteps = [];
  299. for (let i = 0; i < partRects.length; i++) {
  300. // steps.tlElSteps.push(partRects[i].offsetTop);
  301. steps.tlElSteps.push(partRects[i].getBoundingClientRect().top);
  302. }
  303. let titles = document.querySelectorAll('.isMainPart, .isSubPart');
  304. for (let title of titles) {
  305. let nextLine = title.nextElementSibling;
  306. if (!nextLine?.classList.contains('isSubPart')) {
  307. // steps.scrollSteps.push(title.offsetTop);
  308. steps.scrollSteps.push(title.offsetTop);
  309. }
  310. }
  311. }
  312. function scrollOnGrab(el) {
  313. let scrollValue;
  314. if (getCursorPositionInTimelinePart(el).stepAfterMouseUp === steps.scrollSteps.length - 1) {
  315. scrollValue =
  316. ((document.documentElement.scrollHeight - steps.scrollSteps[getCursorPositionInTimelinePart(el).stepAfterMouseUp]) * getCursorPositionInTimelinePart(el).proportionInPart)
  317. + steps.scrollSteps[getCursorPositionInTimelinePart(el).stepAfterMouseUp];
  318. } else {
  319. scrollValue =
  320. ((steps.scrollSteps[getCursorPositionInTimelinePart(el).stepAfterMouseUp + 1] - steps.scrollSteps[getCursorPositionInTimelinePart(el).stepAfterMouseUp]) * getCursorPositionInTimelinePart(el).proportionInPart)
  321. + steps.scrollSteps[getCursorPositionInTimelinePart(el).stepAfterMouseUp];
  322. }
  323. isScrollingFromGrab = true;
  324. window.scrollTo({ top: scrollValue, behavior: 'smooth' });
  325. setTimeout(() => {
  326. isScrollingFromGrab = false;
  327. }, 1000);
  328. }
  329. function getCursorPositionInTimelinePart(el) {
  330. let elTransformY;
  331. if (!el.style.transform) {
  332. elTransformY = 0;
  333. } else {
  334. elTransformY = +el.style.transform.split('(')[1].split('p')[0];
  335. }
  336. let tlPartHeight, tlPartBottom, tlPartTop, proportionInPart, stepAfterMouseUp;
  337. tlPartHeight = partRects[getCurrentPartFromCursor(elTransformY) || 0].getBoundingClientRect().height;
  338. tlPartBottom = partRects[getCurrentPartFromCursor(elTransformY) || 0].getBoundingClientRect().bottom - tlContainer.getBoundingClientRect().top;
  339. tlPartTop = steps.tlElSteps[getCurrentPartFromCursor(elTransformY) || 0] - steps.tlElSteps[0];
  340. proportionInPart = 1 - ((tlPartHeight - (elTransformY - tlPartTop)) / tlPartHeight);
  341. if (proportionInPart > 0 && proportionInPart < 1) {
  342. stepAfterMouseUp = getCurrentPartFromCursor(elTransformY);
  343. return {stepAfterMouseUp, proportionInPart};
  344. } else {
  345. stepAfterMouseUp = getCurrentPartFromCursor(elTransformY);
  346. proportionInPart = 0;
  347. return {stepAfterMouseUp, proportionInPart};
  348. }
  349. }
  350. function moveCursorFromScroll(currentPartIndex) {
  351. if (!isScrollingFromGrab) {
  352. let currentScroll = window.scrollY;
  353. let tlPartHeight = parseInt(partRects[currentPartIndex].style.height);
  354. let cursorTopValue = partRects[currentPartIndex].getBoundingClientRect().top - parseInt(tlContainer.style.top);
  355. let currentScrollPartTop = steps.scrollSteps[currentPartIndex] || 0;
  356. let currentScrollPartHeight;
  357. if (steps.scrollSteps[currentPartIndex + 1]) {
  358. currentScrollPartHeight = steps.scrollSteps[currentPartIndex + 1] - currentScrollPartTop;
  359. } else {
  360. currentScrollPartHeight = document.querySelector('body').scrollHeight - currentScrollPartTop;
  361. }
  362. let currentScrollSincePartBottom = currentScroll - currentScrollPartTop;
  363. let scrollPartProportion = currentScrollSincePartBottom / currentScrollPartHeight;
  364. cursorTopValue = cursorTopValue + tlPartHeight * scrollPartProportion;
  365. if (cursorTopValue > 0) {
  366. cursor.style.transform = `translateY(${cursorTopValue}px)`;
  367. titresCursor.style.transform = `translateY(${cursorTopValue}px)`;
  368. } else {
  369. cursor.style.transform = `translateY(0px)`;
  370. titresCursor.style.transform = `translateY(0px)`;
  371. }
  372. }
  373. }
  374. function displayCurrentPartTitle(currentPartIndex) {
  375. if (isNaN(currentPartIndex)) currentPartIndex = 0;
  376. const currentTime = performance.now();
  377. if (currentTime - lastCallTimestamp >= timestampDebounce) {
  378. lastCallTimestamp = currentTime;
  379. if (titresCursor) titresCursor.firstElementChild.innerText = getCurrentTime(titresFrise[currentPartIndex]?.el); // ICI POUR METTRE LE TEMPS BIEN
  380. }
  381. let mainEl = titresCursor.children[1];
  382. let subEl = titresCursor.lastElementChild;
  383. if (mainEl.innerText != titresFrise[currentPartIndex]?.main || subEl.innerText != titresFrise[currentPartIndex]?.sub) {
  384. mainEl.innerText = titresFrise[currentPartIndex]?.main;
  385. subEl.innerText = titresFrise[currentPartIndex]?.sub;
  386. partRects[prevPartIndex].style.width = '22px';
  387. partRects[currentPartIndex].style.width = '32px';
  388. }
  389. activePartIndex = currentPartIndex;
  390. prevPartIndex = currentPartIndex;
  391. }
  392. function getCurrentPartFromScroll() {
  393. let currentScroll = window.scrollY;
  394. for (let i = 0; i < steps.scrollSteps.length; i++) {
  395. if (
  396. (currentScroll + window.innerHeight / 2 >= steps.scrollSteps[i]
  397. && currentScroll + window.innerHeight / 2 <= steps.scrollSteps[i + 1]) ||
  398. (currentScroll + window.innerHeight / 2 >= steps.scrollSteps[i]
  399. && i === steps.scrollSteps.length - 1)
  400. ) {
  401. return(i);
  402. }
  403. }
  404. }
  405. function getCurrentPartFromCursor(cursorTransformY) {
  406. for (let i = 0; i < steps.tlElSteps.length; i++) {
  407. if (cursorTransformY >= steps.tlElSteps[i] - steps.tlElSteps[0] && cursorTransformY < steps.tlElSteps[i + 1] - steps.tlElSteps[0]) {
  408. return(i);
  409. } else if (cursorTransformY > steps.tlElSteps[steps.tlElSteps.length - 1] - steps.tlElSteps[0]) {
  410. return(steps.tlElSteps.length - 1);
  411. }
  412. }
  413. }
  414. function getCurrentTime(titleEl) {
  415. let nextRow = titleEl?.nextElementSibling;
  416. let allRowsUnder = [];
  417. if (!grabbing) {
  418. while(nextRow) {
  419. if (nextRow.offsetTop - window.innerHeight / 4 > window.scrollY &&
  420. nextRow.classList.contains('isContentPart')) {
  421. return nextRow.firstElementChild.innerText;
  422. }
  423. allRowsUnder.push(nextRow.firstElementChild.innerText);
  424. nextRow = nextRow.nextElementSibling;
  425. }
  426. return allRowsUnder[allRowsUnder.length-1];
  427. } else {
  428. let cursor = document.querySelector('#cursor');
  429. if (nextRow.classList.contains('isSubPart')) nextRow = nextRow.nextElementSibling.nextElementSibling;
  430. while(nextRow) {
  431. if (nextRow.classList.contains('isMainPart') || nextRow.classList.contains('isSubPart')) {
  432. break;
  433. }
  434. allRowsUnder.push(nextRow.firstElementChild.innerText);
  435. nextRow = nextRow.nextElementSibling;
  436. }
  437. let currentRowIndex = Math.floor(allRowsUnder.length * getCursorPositionInTimelinePart(cursor).proportionInPart);
  438. return allRowsUnder[currentRowIndex];
  439. }
  440. }
  441. setPartTitlesInPartition();
  442. setPartsColors();
  443. getPartsTimes();
  444. setTimeout(() => {
  445. getAllHeights();
  446. drawFriseRects();
  447. displayTimelineTitles();
  448. setupCursor();
  449. drawFixedTitles();
  450. setPartRects();
  451. makeElementDraggable(cursor, titresCursor);
  452. makeElementDraggable(titresCursor, cursor);
  453. getSteps();
  454. }, 100);
  455. let resizeTimeout;
  456. window.addEventListener('resize', () => {
  457. tlContainer.innerHTML = '';
  458. document.querySelector("#titres_frise")?.remove();
  459. clearTimeout(resizeTimeout);
  460. resizeTimeout = setTimeout(() => {
  461. getAllHeights();
  462. drawFriseRects();
  463. displayTimelineTitles();
  464. setupCursor();
  465. setPartRects();
  466. makeElementDraggable(titresCursor, cursor);
  467. getSteps();
  468. }, 100);
  469. });
  470. }
  471. export { customTimeline };