// Processing (Searching / Filtering) // var CineraProps = { IsMobile: IsMobile(), Orientation: null, }; var Search = { ResultsSummary: null, ResultsContainer: null, ProjectsContainer: null, QueryElement: null, Projects: [], LastQuery: null, MarkerList: null, ProjectContainer: null, ResultsMarkerIndex: -1, RenderHandle: undefined, Rendering: false, Prototypes: { ProjectContainer: null, DayContainer: null, Marker: null, Highlight: null, } }; function hideEntriesOfProject(ProjectElement) { if(!ProjectElement.classList.contains("off")) { ProjectElement.classList.add("off"); } var baseURL = ProjectElement.attributes.getNamedItem("data-baseURL").value; var searchLocation = ProjectElement.attributes.getNamedItem("data-searchLocation").value; var playerLocation = ProjectElement.attributes.getNamedItem("data-playerLocation").value; for(var i = 0; i < Search.Projects.length; ++i) { var ThisProject = Search.Projects[i]; if(baseURL === ThisProject.baseURL && searchLocation === ThisProject.searchLocation && playerLocation === ThisProject.playerLocation) { ThisProject.filteredOut = true; if(ThisProject.entriesContainer != null) { ThisProject.entriesContainer.style.display = "none"; disableSprite(ThisProject.entriesContainer.parentElement); } } } } function showEntriesOfProject(ProjectElement) { if(ProjectElement.classList.contains("off")) { ProjectElement.classList.remove("off"); } var baseURL = ProjectElement.attributes.getNamedItem("data-baseURL").value; var searchLocation = ProjectElement.attributes.getNamedItem("data-searchLocation").value; var playerLocation = ProjectElement.attributes.getNamedItem("data-playerLocation").value; for(var i = 0; i < Search.Projects.length; ++i) { var ThisProject = Search.Projects[i]; if(baseURL === ThisProject.baseURL && searchLocation === ThisProject.searchLocation && playerLocation === ThisProject.playerLocation) { ThisProject.filteredOut = false; if(ThisProject.entriesContainer != null) { ThisProject.entriesContainer.style.display = "flex"; enableSprite(ThisProject.entriesContainer.parentElement); } } } } function hideProjectSearchResults(baseURL, searchLocation, playerLocation) { var cineraResults = document.getElementById("cineraResults"); if(cineraResults) { var cineraResultsProjects = cineraResults.querySelectorAll(".projectContainer"); for(var i = 0; i < cineraResultsProjects.length; ++i) { var resultBaseURL = cineraResultsProjects[i].attributes.getNamedItem("data-baseURL").value; var resultSearchLocation = cineraResultsProjects[i].attributes.getNamedItem("data-searchLocation").value; var resultPlayerLocation = cineraResultsProjects[i].attributes.getNamedItem("data-playerLocation").value; if(baseURL === resultBaseURL && searchLocation === resultSearchLocation && playerLocation === resultPlayerLocation) { cineraResultsProjects[i].style.display = "none"; return; } } } } function showProjectSearchResults(baseURL, searchLocation, playerLocation) { var cineraResults = document.getElementById("cineraResults"); if(cineraResults) { var cineraResultsProjects = cineraResults.querySelectorAll(".projectContainer"); for(var i = 0; i < cineraResultsProjects.length; ++i) { var resultBaseURL = cineraResultsProjects[i].attributes.getNamedItem("data-baseURL").value; var resultSearchLocation = cineraResultsProjects[i].attributes.getNamedItem("data-searchLocation").value; var resultPlayerLocation = cineraResultsProjects[i].attributes.getNamedItem("data-playerLocation").value; if(baseURL === resultBaseURL && searchLocation === resultSearchLocation && playerLocation === resultPlayerLocation) { cineraResultsProjects[i].style.display = "flex"; return; } } } } function toggleEntriesOfProjectAndChildren(ProjectFilterElement) { var baseURL = ProjectFilterElement.attributes.getNamedItem("data-baseURL").value; var searchLocation = ProjectFilterElement.attributes.getNamedItem("data-searchLocation").value; var playerLocation = ProjectFilterElement.attributes.getNamedItem("data-playerLocation").value; var shouldShow = ProjectFilterElement.classList.contains("off"); if(shouldShow) { ProjectFilterElement.classList.remove("off"); enableSprite(ProjectFilterElement); } else { ProjectFilterElement.classList.add("off"); disableSprite(ProjectFilterElement); } for(var i = 0; i < Search.Projects.length; ++i) { var ThisProject = Search.Projects[i]; if(baseURL === ThisProject.baseURL && searchLocation === ThisProject.searchLocation && playerLocation === ThisProject.playerLocation) { if(shouldShow) { ThisProject.filteredOut = false; enableSprite(ThisProject.projectTitleElement.parentElement); if(ThisProject.entriesContainer != null) { ThisProject.entriesContainer.style.display = "flex"; } showProjectSearchResults(ThisProject.baseURL, ThisProject.searchLocation, ThisProject.playerLocation); } else { ThisProject.filteredOut = true; disableSprite(ThisProject.projectTitleElement.parentElement); if(ThisProject.entriesContainer != null) { ThisProject.entriesContainer.style.display = "none"; } hideProjectSearchResults(ThisProject.baseURL, ThisProject.searchLocation, ThisProject.playerLocation); } } } var indexChildFilterProjects = ProjectFilterElement.querySelectorAll(".cineraFilterProject"); for(var j = 0; j < indexChildFilterProjects.length; ++j) { var ThisElement = indexChildFilterProjects[j]; var baseURL = ThisElement.attributes.getNamedItem("data-baseURL").value; var searchLocation = ThisElement.attributes.getNamedItem("data-searchLocation").value; var playerLocation = ThisElement.attributes.getNamedItem("data-playerLocation").value; if(shouldShow) { showEntriesOfProject(ThisElement); showProjectSearchResults(baseURL, searchLocation, playerLocation); } else { hideEntriesOfProject(ThisElement); hideProjectSearchResults(baseURL, searchLocation, playerLocation); } } } function prepareToParseIndexFile(project) { project.xhr.addEventListener("load", function() { var contents = project.xhr.response; var lines = contents.split("\n"); var mode = "none"; var episode = null; for (var i = 0; i < lines.length; ++i) { var line = lines[i]; if (line.trim().length == 0) { continue; } if (line == "---") { if (episode != null && episode.filename != null && episode.number != null && episode.title != null) { episode.day = getEpisodeName(project.unit, episode.number); episode.dayContainerPrototype = project.dayContainerPrototype; episode.markerPrototype = Search.Prototypes.Marker; episode.playerURLPrefix = project.playerURLPrefix; project.episodes.push(episode); } episode = {}; mode = "none"; } else if (line.startsWith("location:")) { episode.filename = line.slice(10); } else if (line.startsWith("number:")) { episode.number = line.slice(8).trim().slice(1, -1); } else if (line.startsWith("title:")) { episode.title = line.slice(7).trim().slice(1, -1); } else if (line.startsWith("markers")) { mode = "markers"; episode.markers = []; } else if (mode == "markers") { var match = line.match(/"(\d+.\d+)": "(.+)"/); if (match == null) { console.log(name, line); } else { var totalTime = parseFloat(line.slice(1)); var marker = { totalTime: totalTime, prettyTime: markerTime(totalTime), text: match[2].replace(/\\"/g, "\"") } episode.markers.push(marker); } } } document.querySelector(".spinner").classList.remove("show"); project.parsed = true; runSearch(true); }); project.xhr.addEventListener("error", function() { console.error("Failed to load content"); }); } function prepareProjects() { for(var i = 0; i < Search.ProjectsContainer.length; ++i) { var ID = Search.ProjectsContainer[i].attributes.getNamedItem("data-project").value; var baseURL = Search.ProjectsContainer[i].attributes.getNamedItem("data-baseURL").value; var searchLocation = Search.ProjectsContainer[i].attributes.getNamedItem("data-searchLocation").value; var playerLocation = Search.ProjectsContainer[i].attributes.getNamedItem("data-playerLocation").value; var unit = Search.ProjectsContainer[i].attributes.getNamedItem("data-unit").value; var theme = Search.ProjectsContainer[i].classList.item(1); Search.Projects[i] = { baseURL: baseURL, searchLocation: searchLocation, playerLocation: playerLocation, unit: unit, playerURLPrefix: (baseURL ? baseURL + "/" : "") + (playerLocation ? playerLocation + "/" : ""), indexLocation: (baseURL ? baseURL + "/" : "") + (searchLocation ? searchLocation + "/" : "") + ID + ".index", projectTitleElement: Search.ProjectsContainer[i].querySelector(":scope > .cineraProjectTitle"), entriesContainer: Search.ProjectsContainer[i].querySelector(":scope > .cineraIndexEntries"), dayContainerPrototype: Search.Prototypes.DayContainer.cloneNode(true), filteredOut: false, parsed: false, searched: false, resultsToRender: [], resultsIndex: 0, theme: theme, episodes: [], xhr: new XMLHttpRequest(), } Search.Projects[i].dayContainerPrototype.classList.add(theme); Search.Projects[i].dayContainerPrototype.children[1].classList.add(theme); document.querySelector(".spinner").classList.add("show"); Search.Projects[i].xhr.open("GET", Search.Projects[i].indexLocation); Search.Projects[i].xhr.setRequestHeader("Content-Type", "text/plain"); Search.Projects[i].xhr.send(); prepareToParseIndexFile(Search.Projects[i]); } } function getEpisodeName(unit, number) { var day = null; if(unit) { day = unit + " " + number; } return day; } function markerTime(totalTime) { var markTime = "("; var hours = Math.floor(totalTime / 60 / 60); var minutes = Math.floor(totalTime / 60) % 60; var seconds = Math.floor(totalTime) % 60; if (hours > 0) { markTime += padTimeComponent(hours) + ":"; } markTime += padTimeComponent(minutes) + ":" + padTimeComponent(seconds) + ")"; return markTime; } function padTimeComponent(component) { return (component < 10 ? "0" + component : component); } function resetProjectsForSearch() { for(var i = 0; i < Search.Projects.length; ++i) { var project = Search.Projects[i]; project.searched = false; project.resultsToRender = []; } } function IsQuery() { return Search.LastQuery && Search.LastQuery.length > 0; } function runSearch(refresh) { var queryStr = document.getElementById("query").value; if (refresh || Search.LastQuery != queryStr) { var oldResultsContainer = Search.ResultsContainer; Search.ResultsContainer = oldResultsContainer.cloneNode(false); oldResultsContainer.parentNode.insertBefore(Search.ResultsContainer, oldResultsContainer); oldResultsContainer.remove(); for(var i = 0; i < Search.Projects.length; ++i) { Search.Projects[i].resultsIndex = 0; } Search.ResultsMarkerIndex = -1; } Search.LastQuery = queryStr; resetProjectsForSearch(); var numEpisodes = 0; var numMarkers = 0; var totalSeconds = 0; // NOTE(matt): Function defined within runSearch() so that we can modify numEpisodes, numMarkers and totalSeconds function runSearchInterior(resultsToRender, query, episode) { var matches = []; for (var k = 0; k < episode.markers.length; ++k) { query.lastIndex = 0; var result = query.exec(episode.markers[k].text); if (result && result[0].length > 0) { numMarkers++; matches.push(episode.markers[k]); if (k < episode.markers.length-1) { totalSeconds += episode.markers[k+1].totalTime - episode.markers[k].totalTime; } } } if (matches.length > 0) { numEpisodes++; resultsToRender.push({ query: query, episode: episode, matches: matches }); } } if (IsQuery()) { switch(Nav.ViewType) { case view_type.LIST: { Nav.List.classList.add("hidden"); } break; case view_type.GRID: { Nav.GridContainer.classList.add("hidden"); } break; } Search.ResultsSummary.style.display = "block"; var shouldRender = false; var query = new RegExp(Search.LastQuery.replace("(", "\\(").replace(")", "\\)").replace(/\|+/, "\|").replace(/\|$/, "").replace(/(^|[^\\])\\$/, "$1"), "gi"); // Visible for(var i = 0; i < Search.Projects.length; ++i) { var project = Search.Projects[i]; if(project.parsed && !project.filteredOut && project.episodes.length > 0) { if(Nav.SortChronological) { for(var j = 0; j < project.episodes.length; ++j) { var episode = project.episodes[j]; runSearchInterior(project.resultsToRender, query, episode); } } else { for(var j = project.episodes.length; j > 0; --j) { var episode = project.episodes[j - 1]; runSearchInterior(project.resultsToRender, query, episode); } } shouldRender = true; project.searched = true; } } // Invisible for(var i = 0; i < Search.Projects.length; ++i) { var project = Search.Projects[i]; if(project.parsed && project.filteredOut && !project.searched && project.episodes.length > 0) { if(Nav.SortChronological) { for(var j = 0; j < project.episodes.length; ++j) { var episode = project.episodes[j]; runSearchInterior(project.resultsToRender, query, episode); } } else { for(var j = project.episodes.length; j > 0; --j) { var episode = project.episodes[j - 1]; runSearchInterior(project.resultsToRender, query, episode); } } shouldRender = true; project.searched = true; } } if(shouldRender) { if (Search.Rendering) { clearTimeout(Search.RenderHandle); } renderResults(); } } else { switch(Nav.ViewType) { case view_type.LIST: { Nav.List.classList.remove("hidden"); } break; case view_type.GRID: { Nav.GridContainer.classList.remove("hidden"); } break; } Search.ResultsSummary.style.display = "none"; } var totalTime = Math.floor(totalSeconds/60/60) + "h " + Math.floor(totalSeconds/60)%60 + "m " + Math.floor(totalSeconds)%60 + "s "; Search.ResultsSummary.textContent = "Found: " + numEpisodes + " episodes, " + numMarkers + " markers, " + totalTime + "total."; } function renderResults() { var maxItems = 42; var numItems = 0; for(var i = 0; i < Search.Projects.length; ++i) { var project = Search.Projects[i]; if (project.resultsIndex < project.resultsToRender.length) { Search.Rendering = true; while (numItems < maxItems && project.resultsIndex < project.resultsToRender.length) { var query = project.resultsToRender[project.resultsIndex].query; var episode = project.resultsToRender[project.resultsIndex].episode; var matches = project.resultsToRender[project.resultsIndex].matches; if (Search.ResultsMarkerIndex == -1) { if(project.resultsIndex == 0 || project.resultsToRender[project.resultsIndex - 1].episode.playerURLPrefix != episode.playerURLPrefix) { Search.ProjectContainer = Search.Prototypes.ProjectContainer.cloneNode(true); for(var i = 0; i < Search.Projects.length; ++i) { if(Search.Projects[i].playerURLPrefix === episode.playerURLPrefix) { Search.ProjectContainer.setAttribute("data-baseURL", Search.Projects[i].baseURL); Search.ProjectContainer.setAttribute("data-searchLocation", Search.Projects[i].searchLocation); Search.ProjectContainer.setAttribute("data-playerLocation", Search.Projects[i].playerLocation); if(Search.Projects[i].filteredOut) { Search.ProjectContainer.style.display = "none"; } } } Search.ResultsContainer.appendChild(Search.ProjectContainer); } else { Search.ProjectContainer = Search.ResultsContainer.lastElementChild; } var dayContainer = episode.dayContainerPrototype.cloneNode(true); var dayName = dayContainer.children[0]; Search.MarkerList = dayContainer.children[1]; // TODO(matt): Maybe prepend the entire lineage? dayName.textContent = (project.projectTitleElement.textContent ? project.projectTitleElement.textContent + " / " : "") + (episode.day ? episode.day + ": " : "") + episode.title; Search.ProjectContainer.appendChild(dayContainer); Search.ResultsMarkerIndex = 0; numItems++; } while (numItems < maxItems && Search.ResultsMarkerIndex < matches.length) { var match = matches[Search.ResultsMarkerIndex]; var marker = episode.markerPrototype.cloneNode(true); marker.setAttribute("href", episode.playerURLPrefix + episode.filename.replace(/"/g, "") + "/#" + match.totalTime); query.lastIndex = 0; var cursor = 0; var text = match.text; var result = null; marker.appendChild(document.createTextNode(match.prettyTime + " ")); while (result = query.exec(text)) { if (result.index > cursor) { marker.appendChild(document.createTextNode(text.slice(cursor, result.index))); } var highlightEl = Search.Prototypes.Highlight.cloneNode(); highlightEl.textContent = result[0]; marker.appendChild(highlightEl); cursor = result.index + result[0].length; } if (cursor < text.length) { marker.appendChild(document.createTextNode(text.slice(cursor, text.length))); } Search.MarkerList.appendChild(marker); numItems++; Search.ResultsMarkerIndex++; } if (Search.ResultsMarkerIndex == matches.length) { Search.ResultsMarkerIndex = -1; project.resultsIndex++; } } Search.RenderHandle = setTimeout(renderResults, 0); } else { Search.Rendering = false; } } } function InitQuery(QueryElement) { if(location.hash && location.hash.length > 0) { var initialQuery = location.hash; if(initialQuery[0] == "#") { initialQuery = initialQuery.slice(1); } QueryElement.value = decodeURIComponent(initialQuery); } if(document.hasFocus() && IsVisible(QueryElement, GetWindowDim(CineraProps.IsMobile))) { QueryElement.focus(); } } function InitPrototypes(ResultsContainer) { Search.Prototypes.ProjectContainer = document.createElement("DIV"); Search.Prototypes.ProjectContainer.classList.add("projectContainer"); Search.Prototypes.DayContainer = document.createElement("DIV"); Search.Prototypes.DayContainer.classList.add("dayContainer"); var DayName = document.createElement("SPAN"); DayName.classList.add("dayName"); Search.Prototypes.DayContainer.appendChild(DayName); var MarkerList = document.createElement("DIV"); MarkerList.classList.add("markerList"); Search.Prototypes.DayContainer.appendChild(MarkerList); Search.Prototypes.Marker = document.createElement("A"); Search.Prototypes.Marker.classList.add("marker"); if(ResultsContainer.getAttribute("data-single") == 0) { Search.Prototypes.Marker.setAttribute("target", "_blank"); } Search.Prototypes.Highlight = document.createElement("B"); } // // Processing (Searching / Filtering) // Presenting / Navigating (Laying out and traversing the grid, and sorting) // var state_bit = { DISABLE_ANIMATIONS: 1 << 0, SORT_REVERSED: 1 << 1, VIEW_LIST: 1 << 2, VIEW_GRID: 1 << 3, NO_SAVE: 1 << 31, }; var siblings = { PREV: 0, NEXT: 1, }; var item_type = { PROJECT: 0, ENTRY: 1, }; var item_end = { HEAD: 0, TAIL: 1, }; var view_type = { LIST: 0, GRID: 1, }; var transition_type = { SIBLING_SHIFT_PREV: 0, SIBLING_SHIFT_NEXT: 1, PROJECT_ENTRY: 2, PROJECT_EXIT: 3, SUBDIVISION_DESCENT: 4, SUBDIVISION_ASCENT: 5, }; var interaction_type = { PUSH_BUTTON: 0, SIBLING_SHIFT_PREV: 1, SIBLING_SHIFT_NEXT: 2, ASCEND: 3, SORT: 4, }; var Nav = { Nexus: null, GridContainer: null, ButtonsContainer: null, GridSize: { X: null, Y: null, }, GridMinCellsPerDimension: 1, GridMaxCellsPerDimension: 4, GridDim: { X: null, Y: null, }, MinButtonDim: 100, ButtonDim: null, GridColumnGap: null, GridRowGap: null, SortChronological: true, ViewType: view_type.LIST, List: null, Grid: null, // NOTE(matt): Controls Controls: { Header: null, Sort: null, View: null, Anim: null, Save: null, Help: null, HelpDocumentation: null, HelpKeys: [], GridTraversal: { Container: null, Header: null, Ascend: null, Prev: null, PrevAscends: false, Next: null, NextAscends: false, }, }, Buttons: [], Transition: { Enabled: true, ButtonsTransitionContainer: null, ButtonsContainerCloneElement: null, RelevantButtonElement: null, StageDurations: [], Transforms: { ButtonsTransitionContainer: { Initial: { Pos: { X: 0, Y: 0, }, Scale: { X: 1, Y: 1, }, Rotation: { X: 0, Y: 0, Z: 0, }, Opacity: 1, ScrollX: 0, ZIndex: 0, }, Current: { Pos: { X: 0, Y: 0, }, Scale: { X: 1, Y: 1, }, Rotation: { X: 0, Y: 0, Z: 0, }, Opacity: 1, ScrollX: 0, ZIndex: 0, }, TargetStages: [], }, ButtonsContainer: { Initial: { Pos: { X: 0, Y: 0, }, Scale: { X: 1, Y: 1, }, Rotation: { X: 0, Y: 0, Z: 0, }, Opacity: 1, ScrollX: 0, ZIndex: 0, }, Current: { Pos: { X: 0, Y: 0, }, Scale: { X: 1, Y: 1, }, Rotation: { X: 0, Y: 0, Z: 0, }, Opacity: 1, ScrollX: 0, ZIndex: 0, }, TargetStages: [], }, ButtonsContainerClone: { Initial: { Pos: { X: 0, Y: 0, }, Scale: { X: 1, Y: 1, }, Rotation: { X: 0, Y: 0, Z: 0, }, Opacity: 1, ScrollX: 0, ZIndex: 0, }, Current: { Pos: { X: 0, Y: 0, }, Scale: { X: 1, Y: 1, }, Rotation: { X: 0, Y: 0, Z: 0, }, Opacity: 1, ScrollX: 0, ZIndex: 0, }, TargetStages: [], }, RelevantButton: { Initial: { Pos: { X: 0, Y: 0, }, Scale: { X: 1, Y: 1, }, Rotation: { X: 0, Y: 0, Z: 0, }, Opacity: 1, ScrollX: 0, ZIndex: 0, }, Current: { Pos: { X: 0, Y: 0, }, Scale: { X: 1, Y: 1, }, Rotation: { X: 0, Y: 0, Z: 0, }, Opacity: 1, ScrollX: 0, ZIndex: 0, }, TargetStages: [], }, }, StartTime: undefined, RequestedFrame: undefined, }, InteractionQueue: [], TraversalStack: [], State: null, }; function StateBitIsSet(Bit) { return Nav.State & Bit; } function StateBitIsClear(Bit) { return !(Nav.State & Bit); } function MaintainingState() { return StateBitIsClear(state_bit.NO_SAVE); } function SaveState() { localStorage.setItem("CineraState", Nav.State); } function SetStateBit(Bit) { if(MaintainingState()) { Nav.State |= Bit; SaveState(); } } function ClearStateBit(Bit) { if(MaintainingState()) { Nav.State &= ~Bit; SaveState(); } } function SetHelpKeyAvailability(GridSize) { for(var i = 0; i < Nav.Controls.HelpKeys.length; ++i) { Nav.Controls.HelpKeys[i].classList.remove("unavailable"); } /* NOTE(matt): Key layout: 0 4 8 12 1 5 9 13 2 6 10 14 3 7 11 15 */ if(GridSize.X < 4) { Nav.Controls.HelpKeys[12].classList.add("unavailable"); Nav.Controls.HelpKeys[13].classList.add("unavailable"); Nav.Controls.HelpKeys[14].classList.add("unavailable"); Nav.Controls.HelpKeys[15].classList.add("unavailable"); if(GridSize.X < 3) { Nav.Controls.HelpKeys[8].classList.add("unavailable"); Nav.Controls.HelpKeys[9].classList.add("unavailable"); Nav.Controls.HelpKeys[10].classList.add("unavailable"); Nav.Controls.HelpKeys[11].classList.add("unavailable"); if(GridSize.X < 2) { Nav.Controls.HelpKeys[4].classList.add("unavailable"); Nav.Controls.HelpKeys[5].classList.add("unavailable"); Nav.Controls.HelpKeys[6].classList.add("unavailable"); Nav.Controls.HelpKeys[7].classList.add("unavailable"); if(GridSize.X < 1) { Nav.Controls.HelpKeys[0].classList.add("unavailable"); Nav.Controls.HelpKeys[1].classList.add("unavailable"); Nav.Controls.HelpKeys[2].classList.add("unavailable"); Nav.Controls.HelpKeys[3].classList.add("unavailable"); } } } } if(GridSize.Y < 4) { Nav.Controls.HelpKeys[3].classList.add("unavailable"); Nav.Controls.HelpKeys[7].classList.add("unavailable"); Nav.Controls.HelpKeys[11].classList.add("unavailable"); Nav.Controls.HelpKeys[15].classList.add("unavailable"); if(GridSize.Y < 3) { Nav.Controls.HelpKeys[2].classList.add("unavailable"); Nav.Controls.HelpKeys[6].classList.add("unavailable"); Nav.Controls.HelpKeys[10].classList.add("unavailable"); Nav.Controls.HelpKeys[14].classList.add("unavailable"); if(GridSize.Y < 2) { Nav.Controls.HelpKeys[1].classList.add("unavailable"); Nav.Controls.HelpKeys[5].classList.add("unavailable"); Nav.Controls.HelpKeys[9].classList.add("unavailable"); Nav.Controls.HelpKeys[13].classList.add("unavailable"); if(GridSize.Y < 1) { Nav.Controls.HelpKeys[0].classList.add("unavailable"); Nav.Controls.HelpKeys[4].classList.add("unavailable"); Nav.Controls.HelpKeys[8].classList.add("unavailable"); Nav.Controls.HelpKeys[12].classList.add("unavailable"); } } } } } function InitHelpKeys(HelpDocumentation) { var Paragraph = HelpDocumentation.querySelector(".help_paragraph"); Nav.Controls.HelpKeys = Paragraph.querySelectorAll(".help_key"); } function InitView() { // NOTE(matt): Nav.ViewType is initialised to view_type.LIST and InitNexus() leaves the List View visible if(!StateBitIsSet(state_bit.VIEW_LIST)) { if(GridSizeIsSupported(Nav.GridSize)) { PickGridView(); } else { // NOTE(matt): Silently swap the state bits, leaving the default List View visible ClearStateBit(state_bit.VIEW_GRID); SetStateBit(state_bit.VIEW_LIST); } } } function SyncNavState() { Nav.State = localStorage.getItem("CineraState"); if(Nav.State) { if(MaintainingState()) { if(StateBitIsSet(state_bit.DISABLE_ANIMATIONS)) { ToggleAnimations(); } if(StateBitIsSet(state_bit.SORT_REVERSED)) { Sort(true); } InitView(); } else { Nav.Controls.Save.textContent = "Save Settings: ✘"; // Nav.ViewType was initialised to view_type.LIST if(Nav.ViewType == view_type.GRID && GridSizeIsSupported(Nav.GridSize)) { PickGridView(); } } } else { Nav.State = 0; switch(Nav.ViewType) { case view_type.LIST: SetStateBit(state_bit.VIEW_LIST); break case view_type.GRID: SetStateBit(state_bit.VIEW_GRID); break } InitView(); } } function InitTraversalStack() { Nav.List = document.getElementById("cineraIndexList"); var Projects = Nav.List.querySelectorAll(":scope > .cineraIndexProject"); var Level = { Projects: null, Entries: null, HeadIndex: null, TailIndex: null, } if(Projects.length === 1) { // NOTE(matt): Automatically descend into the lone project var QueriedProjects = Projects[0].querySelectorAll(":scope > .cineraIndexProject"); if(QueriedProjects.length > 0) { Level.Projects = QueriedProjects; } Level.Entries = Projects[0].querySelectorAll(":scope > .cineraIndexEntries > div"); } else { Level.Projects = Projects; // NOTE(matt): The top-level "root" cannot itself contain any entries } Nav.TraversalStack.push(Level); } function ComputeFullButtonItemCount(ParentItemCount, AvailableButtonCount) { return ParentItemCount > 0 ? Math.ceil(ParentItemCount / AvailableButtonCount) : 0; } function EmptyElement(Element) { while(Element.firstChild) { Element.removeChild(Element.firstChild); } } function SetDim(Element, X, Y) { Element.style.width = X; Element.style.height = Y; } function EmptyAndResetButton(Button) { EmptyElement(Button.Element); Button.Element.style.fontSize = null; Button.Element.style.fontWeight = null; SetDim(Button.Element, null, null); for(var i = 0; i < Button.Element.classList.length;) { var Class = Button.Element.classList[i]; if(Class != "cineraButton" && Class != "subdivision") { Button.Element.classList.remove(Class); } else { ++i; } } Button.Projects = null; Button.Entries = null; Button.HeadIndex = null; Button.TailIndex = null; } function HasPrevSibling(Level) { return Level.HeadIndex && Level.HeadIndex > 0; } function HasNextSibling(Level) { return Level.TailIndex && Level.TailIndex < (Level.Entries ? Level.Entries.length : Level.Projects.length) - 1; } function Diff(A, B) { return Math.abs(A - B); } function SetButtonInfo(NewButton, Prev, Level, Distribution) { var Result = { HeadIndex: null, TailIndex: null, ItemCount: null, Theme: null, } var ItemsToPlace = null; var FullButtonItemCount = null; if(Distribution.ProjectsToPlace) { ItemsToPlace = Distribution.ProjectsToPlace; FullButtonItemCount = Distribution.FullButtonProjectCount; } else if(Distribution.EntriesToPlace) { ItemsToPlace = Distribution.EntriesToPlace; FullButtonItemCount = Distribution.FullButtonEntryCount; Result.Theme = Level.Entries[0].parentElement.parentElement.classList[1]; } Result.ItemCount = ItemsToPlace > FullButtonItemCount ? FullButtonItemCount : ItemsToPlace; if(Result.ItemCount > 0) { Result.HeadIndex = Prev ? Prev.TailIndex + 1 : Level.HeadIndex ? Level.HeadIndex : 0; Result.TailIndex = Result.HeadIndex + Result.ItemCount - 1; if(Result.ItemCount > 1) { if(Result.Theme == null) { Result.Theme = Level.Projects[Result.HeadIndex].classList[1]; } NewButton.HeadIndex = Result.HeadIndex; NewButton.TailIndex = Result.TailIndex; } else if(Level.Projects && Level.Projects.length > 0) { if(Distribution.ProjectsToPlace) { NewButton.Projects = Level.Projects[Result.HeadIndex].querySelectorAll(":scope > .cineraIndexProject"); Result.Theme = Level.Projects[Result.HeadIndex].classList[1]; } else { NewButton.Entries = Level.Projects[Result.HeadIndex].querySelectorAll(":scope > .cineraIndexEntries > div"); } } } return Result; } function ComputeItemDistribution(Level) { var Result = { ProjectsToPlace: Level.Projects ? Level.HeadIndex !== null && Level.TailIndex !== null ? Diff(Level.HeadIndex, Level.TailIndex) + 1 : Level.Projects.length : 0, ButtonsForProjects: 0, FullButtonProjectCount: 0, EntriesToPlace: Level.Entries ? Level.HeadIndex !== null && Level.TailIndex !== null ? Diff(Level.HeadIndex, Level.TailIndex) + 1 : Level.Entries.length : 0, ButtonsForEntries: 0, FullButtonEntryCount: 0, }; // NOTE(matt): Reserving the top row for projects if(Result.ProjectsToPlace > 0 && Result.EntriesToPlace > 0) { Result.ButtonsForProjects = Math.min(Nav.GridSize.X, Result.ProjectsToPlace); } Result.ButtonsForEntries = Nav.Buttons.length - Result.ButtonsForProjects; if(Result.EntriesToPlace < Result.ButtonsForEntries) { Result.ButtonsForProjects += (Result.ButtonsForEntries - Result.EntriesToPlace); Result.ButtonsForEntries = Nav.Buttons.length - Result.ButtonsForProjects; } Result.FullButtonProjectCount = ComputeFullButtonItemCount(Result.ProjectsToPlace, Result.ButtonsForProjects); Result.FullButtonEntryCount = ComputeFullButtonItemCount(Result.EntriesToPlace, Result.ButtonsForEntries); return Result; } function ResetTransform(Transform, IgnoreSorting) { Transform.Pos.X = 0; Transform.Pos.Y = 0; Transform.Scale.X = 1; Transform.Scale.Y = 1; Transform.Rotation.X = 0; Transform.Rotation.Y = 0; Transform.Rotation.Z = (Nav.SortChronological || IgnoreSorting) ? 0 : 180; Transform.Opacity = 1; Transform.ScrollX = 0; Transform.ZIndex = null; } function ResetButtonsContainerClone() { EmptyElement(Nav.Transition.ButtonsContainerCloneElement); ResetTransform(Nav.Transition.Transforms.ButtonsTransitionContainer.Current, true); ResetTransform(Nav.Transition.Transforms.ButtonsTransitionContainer.Initial, true); ApplyTransform(Nav.Transition.ButtonsTransitionContainerElement, Nav.Transition.Transforms.ButtonsTransitionContainer.Current); ResetTransform(Nav.Transition.Transforms.ButtonsContainer.Current); ResetTransform(Nav.Transition.Transforms.ButtonsContainer.Initial); ApplyTransform(Nav.ButtonsContainer, Nav.Transition.Transforms.ButtonsContainer.Current); ResetTransform(Nav.Transition.Transforms.ButtonsContainerClone.Current); ResetTransform(Nav.Transition.Transforms.ButtonsContainerClone.Initial); ApplyTransform(Nav.Transition.ButtonsContainerCloneElement, Nav.Transition.Transforms.ButtonsContainerClone.Current); Nav.Transition.ButtonsContainerCloneElement.style.gridTemplateColumns = Nav.ButtonsContainer.style.gridTemplateColumns; Nav.Transition.ButtonsContainerCloneElement.style.gridTemplateRows = Nav.ButtonsContainer.style.gridTemplateRows; Nav.Transition.ButtonsContainerCloneElement.style.paddingRight = null; Nav.Transition.ButtonsContainerCloneElement.style.paddingLeft = null; Nav.Transition.ButtonsContainerCloneElement.style.position = "absolute"; if(Nav.Transition.RelevantButtonElement) { ResetTransform(Nav.Transition.Transforms.RelevantButton.Current, true); ResetTransform(Nav.Transition.Transforms.RelevantButton.Initial, true); ApplyTransform(Nav.Transition.RelevantButtonElement, Nav.Transition.Transforms.RelevantButton.Current); Nav.Transition.RelevantButtonElement = null; } Nav.ButtonsContainer.style.zIndex = 1; Nav.ButtonsContainer.style.order = 1; Nav.Transition.ButtonsContainerCloneElement.style.order = 0; Nav.Transition.ButtonsContainerCloneElement.style.display = "none"; } function CloneButtonsContainer() { ResetButtonsContainerClone(); for(var i = 0; i < Nav.ButtonsContainer.children.length; ++i) { var ChildClone = Nav.ButtonsContainer.children[i].cloneNode(true); Nav.Transition.ButtonsContainerCloneElement.appendChild(ChildClone); } CopyTransform(Nav.Transition.Transforms.ButtonsContainerClone.Current, Nav.Transition.Transforms.ButtonsContainer.Current); ApplyTransform(Nav.Transition.ButtonsContainerCloneElement, Nav.Transition.Transforms.ButtonsContainerClone.Current); Nav.Transition.ButtonsContainerCloneElement.style.zIndex = 1; Nav.Transition.ButtonsContainerCloneElement.style.display = "grid"; } function GetIndexOfElement(ParentNodeList, Element) { var Result = null; for(var i = 0; i < ParentNodeList.length; ++i) { if(Element == ParentNodeList[i]) { Result = i; break; } } return Result; } function GetIndexOfButton(Button) { var Result = null; for(var i = 0; i < Nav.Buttons.length; ++i) { if(Button == Nav.Buttons[i]) { Result = i break; } } return Result; } function ApplyTransform(Element, Transform) { var TranslateStyle = "translate(" + Transform.Pos.X + "px, " + Transform.Pos.Y + "px)"; var ScaleStyle = "scale(" + Transform.Scale.X + "," + Transform.Scale.Y + ")"; var RotateX = "rotate3d(1, 0, 0, " + Transform.Rotation.X + "deg)"; var RotateY = "rotate3d(0, 1, 0, " + Transform.Rotation.Y + "deg)"; var RotateZ = "rotate3d(0, 0, 1, " + Transform.Rotation.Z + "deg)"; var RotateStyle = RotateX + " " + RotateY + " " + RotateZ; var TransformString = TranslateStyle + " " + ScaleStyle + " " + RotateStyle; Element.style.transform = TransformString; Element.style.opacity = Transform.Opacity; Element.style.zIndex = Transform.ZIndex; if(Transform.ScrollX !== null) { Element.scrollLeft = Transform.ScrollX; } } function CopyTransform(Dest, Src) { Dest.Pos.X = Src.Pos.X; Dest.Pos.Y = Src.Pos.Y; Dest.Scale.X = Src.Scale.X; Dest.Scale.Y = Src.Scale.Y; Dest.Rotation.X = Src.Rotation.X; Dest.Rotation.Y = Src.Rotation.Y; Dest.Rotation.Z = Src.Rotation.Z; Dest.Opacity = Src.Opacity; Dest.ScrollX = Src.ScrollX; Dest.ZIndex = Src.ZIndex; } function TransformsMatch(Current, Target) { var Result = true; if((Target.Pos.X != null && Target.Pos.X != Current.Pos.X) || (Target.Pos.Y != null && Target.Pos.Y != Current.Pos.Y) || (Target.Scale.X != null && Target.Scale.X != Current.Scale.X) || (Target.Scale.Y != null && Target.Scale.Y != Current.Scale.Y) || (Target.Rotation.X != null && Target.Rotation.X != Current.Rotation.X) || (Target.Rotation.Y != null && Target.Rotation.Y != Current.Rotation.Y) || (Target.Rotation.Z != null && Target.Rotation.Z != Current.Rotation.Z) || (Target.Opacity != null && Target.Opacity != Current.Opacity) || (Target.ScrollX != null && Target.ScrollX != Current.ScrollX) || (Target.ZIndex != null && Target.ZIndex != Current.ZIndex)) { Result = false; } return Result; } function TransformsComplete(TransformSet) { var Result = true; var ButtonsContainer = TransformSet.ButtonsContainer; var ButtonsContainerClone = TransformSet.ButtonsContainerClone; var ButtonsTransitionContainer = TransformSet.ButtonsTransitionContainer; var RelevantButton = TransformSet.RelevantButton; if((ButtonsContainer.TargetStages.length && !TransformsMatch(ButtonsContainer.Current, ButtonsContainer.TargetStages[0])) || (ButtonsContainerClone.TargetStages.length && !TransformsMatch(ButtonsContainerClone.Current, ButtonsContainerClone.TargetStages[0])) || (ButtonsTransitionContainer.TargetStages.length && !TransformsMatch(ButtonsTransitionContainer.Current, ButtonsTransitionContainer.TargetStages[0])) || (RelevantButton.TargetStages.length && !TransformsMatch(RelevantButton.Current, RelevantButton.TargetStages[0]))) { Result = false; } return Result; } function FinaliseTransforms(TransformsSet) { Nav.Transition.StartTime = undefined; CopyTransform(TransformsSet.ButtonsTransitionContainer.Initial, TransformsSet.ButtonsTransitionContainer.Current); CopyTransform(TransformsSet.ButtonsContainer.Initial, TransformsSet.ButtonsContainer.Current); CopyTransform(TransformsSet.ButtonsContainerClone.Initial, TransformsSet.ButtonsContainerClone.Current); CopyTransform(TransformsSet.RelevantButton.Initial, TransformsSet.RelevantButton.Current); ShiftStage(); } function LerpTransforms(TransformSet, t) { var Result = false; if(TransformSet.TargetStages.length) { var Initial = TransformSet.Initial; var Current = TransformSet.Current; var Target = TransformSet.TargetStages[0]; if(Target.Pos.X !== null) { Current.Pos.X = Lerp(Initial.Pos.X, t, Target.Pos.X); Result = true; } if(Target.Pos.Y !== null) { Current.Pos.Y = Lerp(Initial.Pos.Y, t, Target.Pos.Y); Result = true; } if(Target.Scale.X !== null) { Current.Scale.X = Lerp(Initial.Scale.X, t, Target.Scale.X); Result = true; } if(Target.Scale.Y !== null) { Current.Scale.Y = Lerp(Initial.Scale.Y, t, Target.Scale.Y); Result = true; } if(Target.Rotation.X !== null) { Current.Rotation.X = Lerp(Initial.Rotation.X, t, Target.Rotation.X); Result = true; } if(Target.Rotation.Y !== null) { Current.Rotation.Y = Lerp(Initial.Rotation.Y, t, Target.Rotation.Y); Result = true; } if(Target.Rotation.Z !== null) { Current.Rotation.Z = Lerp(Initial.Rotation.Z, t, Target.Rotation.Z); Result = true; } if(Target.Opacity !== null) { Current.Opacity = Lerp(Initial.Opacity, t, Target.Opacity); Result = true; } if(Target.ScrollX !== null) { Current.ScrollX = Lerp(Initial.ScrollX, t, Target.ScrollX); Result = true; } if(Target.ZIndex !== null) { Current.ZIndex = Lerp(Initial.ZIndex, t, Target.ZIndex); Result = true; } } return Result; } function ShiftStage() { if(Nav.Transition.StageDurations.length) { Nav.Transition.StageDurations.shift(); } if(Nav.Transition.Transforms.ButtonsTransitionContainer.TargetStages.length) { Nav.Transition.Transforms.ButtonsTransitionContainer.TargetStages.shift(); } if(Nav.Transition.Transforms.ButtonsContainer.TargetStages.length) { Nav.Transition.Transforms.ButtonsContainer.TargetStages.shift(); } if(Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.length) { Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.shift(); } if(Nav.Transition.Transforms.RelevantButton.TargetStages.length) { Nav.Transition.Transforms.RelevantButton.TargetStages.shift(); } } function MergeTransform(Dest, Src) { if(Src.Pos.X !== null) { Dest.Pos.X = Src.Pos.X; } if(Src.Pos.Y !== null) { Dest.Pos.Y = Src.Pos.Y; } if(Src.Scale.X !== null) { Dest.Scale.X = Src.Scale.X; } if(Src.Scale.Y !== null) { Dest.Scale.Y = Src.Scale.Y; } if(Src.Rotation.X !== null) { Dest.Rotation.X = Src.Rotation.X; } if(Src.Rotation.Y !== null) { Dest.Rotation.Y = Src.Rotation.Y; } if(Src.Rotation.Z !== null) { Dest.Rotation.Z = Src.Rotation.Z; } if(Src.Opacity !== null) { Dest.Opacity = Src.Opacity; } if(Src.ScrollX !== null) { Dest.ScrollX = Src.ScrollX; } if(Src.ZIndex !== null) { Dest.ZIndex = Src.ZIndex; } } function DoTransitionStage(Now) { if(Nav.Transition.StageDurations.length) { if(Nav.Transition.StartTime === undefined) { Nav.Transition.StartTime = Now; } var Elapsed = Now - Nav.Transition.StartTime; var Duration = Nav.Transition.StageDurations[0]; if(Duration === 0) { // Instant transform if(Nav.Transition.Transforms.ButtonsTransitionContainer.TargetStages.length) { MergeTransform(Nav.Transition.Transforms.ButtonsTransitionContainer.Current, Nav.Transition.Transforms.ButtonsTransitionContainer.TargetStages[0]); ApplyTransform(Nav.Transition.ButtonsTransitionContainerElement, Nav.Transition.Transforms.ButtonsTransitionContainer.Current); } if(Nav.Transition.Transforms.ButtonsContainer.TargetStages.length) { MergeTransform(Nav.Transition.Transforms.ButtonsContainer.Current, Nav.Transition.Transforms.ButtonsContainer.TargetStages[0]); ApplyTransform(Nav.ButtonsContainer, Nav.Transition.Transforms.ButtonsContainer.Current); } if(Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.length) { MergeTransform(Nav.Transition.Transforms.ButtonsContainerClone.Current, Nav.Transition.Transforms.ButtonsContainerClone.TargetStages[0]); ApplyTransform(Nav.Transition.ButtonsContainerCloneElement, Nav.Transition.Transforms.ButtonsContainerClone.Current); } if(Nav.Transition.Transforms.RelevantButton.TargetStages.length) { MergeTransform(Nav.Transition.Transforms.RelevantButton.Current, Nav.Transition.Transforms.RelevantButton.TargetStages[0]); ApplyTransform(Nav.Transition.RelevantButtonElement, Nav.Transition.Transforms.RelevantButton.Current); } } else { // Lerp var t = Clamp01(Elapsed / Duration); if(LerpTransforms(Nav.Transition.Transforms.ButtonsTransitionContainer, t)) { ApplyTransform(Nav.Transition.ButtonsTransitionContainerElement, Nav.Transition.Transforms.ButtonsTransitionContainer.Current); } if(LerpTransforms(Nav.Transition.Transforms.ButtonsContainer, t)) { ApplyTransform(Nav.ButtonsContainer, Nav.Transition.Transforms.ButtonsContainer.Current); } if(LerpTransforms(Nav.Transition.Transforms.ButtonsContainerClone, t)) { ApplyTransform(Nav.Transition.ButtonsContainerCloneElement, Nav.Transition.Transforms.ButtonsContainerClone.Current); } if(LerpTransforms(Nav.Transition.Transforms.RelevantButton, t)) { ApplyTransform(Nav.Transition.RelevantButtonElement, Nav.Transition.Transforms.RelevantButton.Current); } } if(TransformsComplete(Nav.Transition.Transforms)) { FinaliseTransforms(Nav.Transition.Transforms); } Nav.Transition.RequestedFrame = window.requestAnimationFrame(DoTransitionStage); } else { Nav.Transition.RequestedFrame = undefined; ResetButtonsContainerClone(); DequeueInteraction(); } } function CompressTransitionStages() { while(Nav.Transition.StageDurations.length > 1) { Nav.Transition.StageDurations.shift() } Nav.Transition.StageDurations[0] = 0; while(Nav.Transition.Transforms.ButtonsTransitionContainer.TargetStages.length > 1) { Nav.Transition.Transforms.ButtonsTransitionContainer.TargetStages.shift(); } while(Nav.Transition.Transforms.ButtonsContainer.TargetStages.length > 1) { Nav.Transition.Transforms.ButtonsContainer.TargetStages.shift(); } while(Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.length > 1) { Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.shift(); } while(Nav.Transition.Transforms.RelevantButton.TargetStages.length > 1) { Nav.Transition.Transforms.RelevantButton.TargetStages.shift(); } } function DoTransition() { if(Nav.Transition.Enabled == false) { CompressTransitionStages(); } CopyTransform(Nav.Transition.Transforms.ButtonsTransitionContainer.Initial, Nav.Transition.Transforms.ButtonsTransitionContainer.Current); CopyTransform(Nav.Transition.Transforms.ButtonsContainer.Initial, Nav.Transition.Transforms.ButtonsContainer.Current); CopyTransform(Nav.Transition.Transforms.ButtonsContainerClone.Initial, Nav.Transition.Transforms.ButtonsContainerClone.Current); CopyTransform(Nav.Transition.Transforms.RelevantButton.Initial, Nav.Transition.Transforms.RelevantButton.Current); Nav.Transition.StartTime = undefined; Nav.Transition.RequestedFrame = window.requestAnimationFrame(DoTransitionStage); } function NodesMatch(A, B) { var Result = false; var i = 0; if(A && B) { Result = true; for(; i < A.length && i < B.length && A[i] == B[i];) { ++i; } if(i != A.length || i != B.length) { Result = false; } } else if(!A && !B) { Result = true; } return Result; } function ButtonAndLevelMatch(Button, Level) { var Result = true; if(Button && Level) { var ButtonProjects = Button.Projects; var ButtonEntries = Button.Entries; if(Button.Projects && Button.Projects.length === undefined) { ButtonProjects = Button.Projects.querySelectorAll(":scope > .cineraIndexProject"); ButtonEntries = Button.Projects.querySelectorAll(":scope > .cineraIndexEntries > div"); if(ButtonProjects.length == 0) { ButtonProjects = null; } if(ButtonEntries.length == 0) { ButtonEntries = null; } } if(!NodesMatch(Level.Projects, ButtonProjects)) { Result = false; } if(!NodesMatch(Level.Entries, ButtonEntries)) { Result = false; } if(Button.HeadIndex != Level.HeadIndex) { Result = false; } if(Button.TailIndex != Level.TailIndex) { Result = false; } } return Result; } function NullTarget() { let Result = { Pos: { X: null, Y: null, }, Scale: { X: null, Y: null, }, Rotation: { X: null, Y: null, Z: null, }, Opacity: null, ScrollX: null, ZIndex: null, }; return Result; } function ComputeButtonGeometryRelativeToGrid(Button) { var Result = { Pos: { X: null, Y: null, }, Scale: { X: null, Y: null, }, }; var ButtonStyle = window.getComputedStyle(Button); var GridDimX = Nav.GridDim.X; var GridDimY = Nav.GridDim.Y; var ButtonDimX = parseInt(ButtonStyle.width); var ButtonDimY = parseInt(ButtonStyle.height); Result.Scale.X = ButtonDimX / GridDimX; Result.Scale.Y = ButtonDimY / GridDimY; var ButtonPosX = Button.offsetLeft; var ButtonPosY = Button.offsetTop; if(!Nav.SortChronological) { ButtonPosX = GridDimX - ButtonPosX - ButtonDimX; ButtonPosY = GridDimY - ButtonPosY - ButtonDimY; } var ButtonCentreX = ButtonDimX / 2 + ButtonPosX; var ButtonCentreY = ButtonDimY / 2 + ButtonPosY; var GridCentreX = GridDimX / 2; var GridCentreY = GridDimY / 2; Result.Pos.X = ButtonCentreX - GridCentreX; Result.Pos.Y = ButtonCentreY - GridCentreY; return Result; } function GetItemType(Level) { var Result = null; if(Level.Projects && Level.Projects.length !== undefined && Level.Projects.length > 0) { Result = item_type.PROJECT; } else if(Level.Entries && Level.Entries.length !== undefined && Level.Entries.length > 0) { Result = item_type.ENTRY; } return Result; } function GetTraversalLevelBundle() { var Result = { Generation: null, This: null, Parent: null, Type: null }; Result.Generation = Nav.TraversalStack.length; Result.This = Nav.TraversalStack[Result.Generation - 1]; Result.Parent = Nav.TraversalStack[Result.Generation - 2]; Result.Type = GetItemType(Result.This); return Result; } function FitText(Element, ItemEnd) { var Paragraph = Element.firstElementChild; var ParagraphStyle = window.getComputedStyle(Paragraph); var ElementStyle = window.getComputedStyle(Element); var Width = parseInt(ElementStyle.width); var Height = parseInt(ElementStyle.height); Element.style.alignItems = "flex-start"; // NOTE(matt): Allows IsOverflowed() to work on a flex-end Element var FontSize = parseInt(window.getComputedStyle(Element).fontSize); while(FontSize >= 10.2 && (IsOverflowed(Element) || parseInt(ParagraphStyle.width) > Width || parseInt(ParagraphStyle.height) > Height)) { FontSize -= 0.2; Element.style.fontSize = FontSize + "px"; } if(IsOverflowed(Element) || parseInt(ParagraphStyle.width) > Width || parseInt(ParagraphStyle.height) > Height) { Element.style.fontWeight = "normal"; } var IsHeadOrTailAndTooTallForElement = ItemEnd !== undefined && (Element.scrollHeight > Element.clientHeight || parseInt(ParagraphStyle.height) > Height); // NOTE(matt): Leave "flex-start" in place for tall items, to keep their beginning visible if(!IsHeadOrTailAndTooTallForElement) { Element.style.alignItems = null; } } function AppendItemToButton(Button, Text, ItemEnd) { var ButtonElementStyle = window.getComputedStyle(Button.Element); var VerticalBorderAllowance = parseInt(ButtonElementStyle.borderLeftWidth) + parseInt(ButtonElementStyle.borderRightWidth); var HorizontalBorderAllowance; var ItemElement = document.createElement("div"); ItemElement.classList.add("cineraText"); switch(ItemEnd) { case item_end.HEAD: { HorizonalBorderAllowance = parseInt(ButtonElementStyle.borderTopWidth); ItemElement.classList.add("head-item"); } break; case item_end.TAIL: { HorizonalBorderAllowance = parseInt(ButtonElementStyle.borderBottomWidth); ItemElement.classList.add("tail-item"); } break; } if(!Nav.SortChronological) { ItemElement.style.transform = "rotate3d(0, 0, 1, 180deg)"; } var ParagraphNode = document.createElement("p"); ParagraphNode.textContent = Text; ItemElement.appendChild(ParagraphNode); var Item = Button.Element.appendChild(ItemElement); // NOTE(matt): This enables Safari to apply height 50% to the head / tail items var ButtonWidth = parseInt(ButtonElementStyle.width); var ButtonHeight = parseInt(ButtonElementStyle.height); SetDim(Button.Element, ButtonWidth + "px", ButtonHeight + "px"); //// FitText(Item, ItemEnd); } function UpdateButtons(TransitionType, RelevantButton, PoppedLevel) { if(GridSizeIsSupported(Nav.GridSize)) { var LevelBundle = GetTraversalLevelBundle(); Nav.Controls.GridTraversal.Prev.children[0].textContent = "←"; Nav.Controls.GridTraversal.PrevAscends = false; Nav.Controls.GridTraversal.NextAscends = false; if(LevelBundle.Generation <= 1) { Nav.Controls.GridTraversal.Ascend.classList.add("nowhere"); Nav.Controls.GridTraversal.Prev.classList.add("nowhere"); Nav.Controls.GridTraversal.Next.classList.add("nowhere"); Nav.Controls.GridTraversal.Next.classList.remove("ascension"); } else { Nav.Controls.GridTraversal.Ascend.classList.remove("nowhere"); Nav.Controls.GridTraversal.Prev.classList.remove("nowhere"); Nav.Controls.GridTraversal.Next.classList.remove("nowhere"); if(!HasPrevSibling(LevelBundle.This)) { Nav.Controls.GridTraversal.Prev.classList.add("nowhere"); } else if(SiblingIsLeaf(siblings.PREV)) { Nav.Controls.GridTraversal.PrevAscends = true; Nav.Controls.GridTraversal.Prev.children[0].textContent = Nav.SortChronological ? "↰" : "↲"; } if(!HasNextSibling(LevelBundle.This)) { Nav.Controls.GridTraversal.Next.classList.add("nowhere"); Nav.Controls.GridTraversal.Next.classList.remove("ascension"); } else if(SiblingIsLeaf(siblings.NEXT)) { Nav.Controls.GridTraversal.NextAscends = true; Nav.Controls.GridTraversal.Next.classList.add("ascension"); } else { Nav.Controls.GridTraversal.Next.classList.remove("ascension"); } } var Distribution = ComputeItemDistribution(LevelBundle.This); // NOTE(matt): Centre-alignment. If people would prefer left-alignment, we do that here // // We're doing simple 1D centring here, so would need to do correct 2D centring var HalfEmptyButtonCount = (Nav.Buttons.length - (Distribution.ProjectsToPlace + Distribution.EntriesToPlace)) / 2; var EmptyPadding = 0; //EmptyPadding = Math.floor(HalfEmptyButtonCount); // NOTE(matt): Comment out to disable centring // var Prev = null; var ButtonInfo = { HeadIndex: null, TailIndex: null, ItemCount: null, Theme: null, }; var DoingEntries = false; if(TransitionType !== undefined) { CloneButtonsContainer(); } for(var ButtonIndex = 0; ButtonIndex < Nav.Buttons.length; ++ButtonIndex) { let This = Nav.Buttons[ButtonIndex]; EmptyAndResetButton(This); if(EmptyPadding > 0) { --EmptyPadding; } else { if(Distribution.ProjectsToPlace > 0 || Distribution.EntriesToPlace > 0) { if(Distribution.ProjectsToPlace > 0) { This.Projects = LevelBundle.This.Projects; if(Distribution.ProjectsToPlace == 1 || Distribution.ProjectsToPlace == Distribution.ButtonsForProjects - ButtonIndex) { Distribution.FullButtonProjectCount = 1; } ButtonInfo = SetButtonInfo(This, Prev, LevelBundle.This, Distribution); if(ButtonInfo.ItemCount == 1) { This.Projects = LevelBundle.This.Projects[ButtonInfo.HeadIndex]; } Distribution.ProjectsToPlace -= ButtonInfo.ItemCount; if(Distribution.FullButtonProjectCount == 1) { This.Element.classList.add("leaf"); var TextElement = document.createElement("p"); TextElement.classList.add("cineraText"); if(!Nav.SortChronological) { TextElement.style.transform = "rotate3d(0, 0, 1, 180deg)"; } var Text = LevelBundle.This.Projects[ButtonInfo.HeadIndex].querySelector(".cineraProjectTitle").innerText; var TextNode = document.createTextNode(Text); TextElement.appendChild(TextNode); This.Element.appendChild(TextElement); FitText(This.Element); } else { var HeadText = LevelBundle.This.Projects[ButtonInfo.HeadIndex].querySelector(".cineraProjectTitle").innerText; AppendItemToButton(This, HeadText, item_end.HEAD); var TailText = LevelBundle.This.Projects[ButtonInfo.TailIndex].querySelector(".cineraProjectTitle").innerText; AppendItemToButton(This, TailText, item_end.TAIL); } } else { This.Entries = LevelBundle.This.Entries; if(!DoingEntries) { Prev = null; DoingEntries = true; } if(Distribution.EntriesToPlace == 1 || Distribution.EntriesToPlace == Distribution.ButtonsForEntries - ButtonIndex) { Distribution.FullButtonEntryCount = 1; } ButtonInfo = SetButtonInfo(This, Prev, LevelBundle.This, Distribution); if(ButtonInfo.ItemCount == 1) { This.Entries = This.Entries[ButtonInfo.HeadIndex]; } Distribution.EntriesToPlace -= ButtonInfo.ItemCount; if(Distribution.FullButtonEntryCount == 1) { This.Element.classList.add("leaf"); var ButtonLink = document.createElement("a"); ButtonLink.classList.add("cineraText"); if(!Nav.SortChronological) { ButtonLink.style.transform = "rotate3d(0, 0, 1, 180deg)"; } var EntryAddress = LevelBundle.This.Entries[ButtonInfo.HeadIndex].lastElementChild.getAttribute("href"); ButtonLink.setAttribute("href", EntryAddress); var Text = LevelBundle.This.Entries[ButtonInfo.HeadIndex].innerText; var TextNode = document.createTextNode(Text); ButtonLink.appendChild(TextNode); This.Element.appendChild(ButtonLink); FitText(This.Element); } else { var HeadText = LevelBundle.This.Entries[ButtonInfo.HeadIndex].innerText; AppendItemToButton(This, HeadText, item_end.HEAD); var TailText = LevelBundle.This.Entries[ButtonInfo.TailIndex].innerText; AppendItemToButton(This, TailText, item_end.TAIL); } } This.Element.classList.add(ButtonInfo.Theme); Prev = ButtonInfo; } else { This.Element.classList.add(Nav.Nexus.classList[0]); } } if(PoppedLevel && !Nav.Transition.RelevantButtonElement && ButtonAndLevelMatch(This, PoppedLevel)) { Nav.Transition.RelevantButtonElement = This.Element; } } if(TransitionType !== undefined) { switch(TransitionType) { case transition_type.SIBLING_SHIFT_PREV: { // Init targets //// ButtonsTransitionContainer let TargetA0 = NullTarget(); var Padding = Nav.GridColumnGap; if(Nav.SortChronological) { TargetA0.ScrollX = 0; } else { TargetA0.ScrollX = Nav.GridDim.X + Padding; } Nav.Transition.StageDurations.push(320); Nav.Transition.Transforms.ButtonsTransitionContainer.TargetStages.push(TargetA0); // Prep Nav.Transition.ButtonsContainerCloneElement.style.position = "relative"; var ScrollX; if(Nav.SortChronological) { Nav.ButtonsContainer.style.order = 0; Nav.Transition.ButtonsContainerCloneElement.style.order = 1; ScrollX = Nav.GridDim.X + Padding; } else { Nav.ButtonsContainer.style.order = 1; Nav.Transition.ButtonsContainerCloneElement.style.order = 0; ScrollX = 0; } Nav.Transition.ButtonsContainerCloneElement.style.paddingLeft = Padding + "px"; Nav.Transition.Transforms.ButtonsTransitionContainer.Current.ScrollX = ScrollX; ApplyTransform(Nav.Transition.ButtonsTransitionContainerElement, Nav.Transition.Transforms.ButtonsTransitionContainer.Current); } break; case transition_type.SIBLING_SHIFT_NEXT: { // Init targets //// let TargetA0 = NullTarget(); var Padding = Nav.GridColumnGap; if(Nav.SortChronological) { TargetA0.ScrollX = Nav.GridDim.X + Padding; } else { TargetA0.ScrollX = 0; } Nav.Transition.StageDurations.push(320); Nav.Transition.Transforms.ButtonsTransitionContainer.TargetStages.push(TargetA0); // Prep Nav.Transition.ButtonsContainerCloneElement.style.position = "relative"; var ScrollX; if(Nav.SortChronological) { Nav.ButtonsContainer.style.order = 1; Nav.Transition.ButtonsContainerCloneElement.style.order = 0; ScrollX = 0; } else { Nav.ButtonsContainer.style.order = 0; Nav.Transition.ButtonsContainerCloneElement.style.order = 1; ScrollX = Nav.GridDim.X + Padding; } Nav.Transition.ButtonsContainerCloneElement.style.paddingRight = Padding + "px"; Nav.Transition.Transforms.ButtonsTransitionContainer.Current.ScrollX = ScrollX; ApplyTransform(Nav.Transition.ButtonsTransitionContainerElement, Nav.Transition.Transforms.ButtonsTransitionContainer.Current); } break; case transition_type.PROJECT_ENTRY: { // Init targets //// ButtonsContainer let TargetA0 = NullTarget(); let TargetA1 = NullTarget(); let TargetA2 = NullTarget(); let TargetA3 = NullTarget(); if(Nav.SortChronological) { TargetA0.Rotation.Y = 90; } else { TargetA0.Rotation.Y = -90; } TargetA1.ZIndex = 1; TargetA2.Rotation.Y = 0; TargetA3.Pos.X = 0; TargetA3.Pos.Y = 0; TargetA3.Scale.X = 1; TargetA3.Scale.Y = 1; //// RelevantButton let TargetB0 = NullTarget(); let TargetB1 = NullTarget(); let TargetB2 = NullTarget(); if(Nav.SortChronological) { TargetB0.Rotation.Y = -90; TargetB2.Rotation.Y = -180; } else { TargetB0.Rotation.Y = 90; TargetB2.Rotation.Y = 180; } //// ButtonsContainerClone let TargetC0 = NullTarget(); let TargetC1 = NullTarget(); TargetC1.ZIndex = 0; Nav.Transition.StageDurations.push(80); Nav.Transition.StageDurations.push(0); Nav.Transition.StageDurations.push(80); Nav.Transition.StageDurations.push(160); Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA0); Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA1); Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA2); Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA3); Nav.Transition.Transforms.RelevantButton.TargetStages.push(TargetB0); Nav.Transition.Transforms.RelevantButton.TargetStages.push(TargetB1); Nav.Transition.Transforms.RelevantButton.TargetStages.push(TargetB2); Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.push(TargetC0); Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.push(TargetC1); // Prep var RelevantButtonIndex = GetIndexOfButton(RelevantButton); Nav.Transition.RelevantButtonElement = Nav.Transition.ButtonsContainerCloneElement.children[RelevantButtonIndex]; var ButtonGeometry = ComputeButtonGeometryRelativeToGrid(Nav.Transition.RelevantButtonElement); Nav.Transition.Transforms.ButtonsContainer.Current.Pos.X = ButtonGeometry.Pos.X; Nav.Transition.Transforms.ButtonsContainer.Current.Pos.Y = ButtonGeometry.Pos.Y; Nav.Transition.Transforms.ButtonsContainer.Current.Scale.X = ButtonGeometry.Scale.X; Nav.Transition.Transforms.ButtonsContainer.Current.Scale.Y = ButtonGeometry.Scale.Y; if(Nav.SortChronological) { Nav.Transition.Transforms.ButtonsContainer.Current.Rotation.Y = 180; } else { Nav.Transition.Transforms.ButtonsContainer.Current.Rotation.Y = -180; } Nav.Transition.Transforms.ButtonsContainer.Current.ZIndex = 0; Nav.Transition.Transforms.ButtonsContainerClone.Current.ZIndex = 1; ApplyTransform(Nav.Transition.ButtonsContainerCloneElement, Nav.Transition.Transforms.ButtonsContainerClone.Current); ApplyTransform(Nav.ButtonsContainer, Nav.Transition.Transforms.ButtonsContainer.Current); } break; case transition_type.PROJECT_EXIT: { // Init targets //// ButtonsContainer let TargetA0 = NullTarget(); let TargetA1 = NullTarget(); let TargetA2 = NullTarget(); TargetA2.ZIndex = 1; //// RelevantButton let TargetB0 = NullTarget(); let TargetB1 = NullTarget(); let TargetB2 = NullTarget(); let TargetB3 = NullTarget(); if(Nav.SortChronological) { TargetB1.Rotation.Y = -90; } else { TargetB1.Rotation.Y = 90; } TargetB3.Rotation.Y = 0; //// ButtonsContainerClone let TargetC0 = NullTarget(); let TargetC1 = NullTarget(); let TargetC2 = NullTarget(); let TargetC3 = NullTarget(); var ButtonGeometry = ComputeButtonGeometryRelativeToGrid(Nav.Transition.RelevantButtonElement); TargetC0.Pos.X = ButtonGeometry.Pos.X; TargetC0.Pos.Y = ButtonGeometry.Pos.Y; TargetC0.Scale.X = ButtonGeometry.Scale.X; TargetC0.Scale.Y = ButtonGeometry.Scale.Y; if(Nav.SortChronological) { TargetC1.Rotation.Y = 90; TargetC3.Rotation.Y = 180; } else { TargetC1.Rotation.Y = -90; TargetC3.Rotation.Y = -180; } TargetC2.ZIndex = 0; Nav.Transition.StageDurations.push(160); Nav.Transition.StageDurations.push(80); Nav.Transition.StageDurations.push(0); Nav.Transition.StageDurations.push(80); Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA0); Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA1); Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA2); Nav.Transition.Transforms.RelevantButton.TargetStages.push(TargetB0); Nav.Transition.Transforms.RelevantButton.TargetStages.push(TargetB1); Nav.Transition.Transforms.RelevantButton.TargetStages.push(TargetB2); Nav.Transition.Transforms.RelevantButton.TargetStages.push(TargetB3); Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.push(TargetC0); Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.push(TargetC1); Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.push(TargetC2); Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.push(TargetC3); // Prep if(Nav.SortChronological) { Nav.Transition.Transforms.RelevantButton.Current.Rotation.Y = -180; } else { Nav.Transition.Transforms.RelevantButton.Current.Rotation.Y = 180; } ApplyTransform(Nav.Transition.RelevantButtonElement, Nav.Transition.Transforms.RelevantButton.Current); Nav.Transition.Transforms.ButtonsContainer.Current.ZIndex = 0; ApplyTransform(Nav.ButtonsContainer, Nav.Transition.Transforms.ButtonsContainer.Current); Nav.Transition.Transforms.ButtonsContainerClone.Current.ZIndex = 1; ApplyTransform(Nav.Transition.ButtonsContainerCloneElement, Nav.Transition.Transforms.ButtonsContainerClone.Current); } break; case transition_type.SUBDIVISION_DESCENT: { // Init targets //// ButtonsContainer let TargetA0 = NullTarget(); let TargetA1 = NullTarget(); TargetA0.Opacity = 1; TargetA1.Pos.X = 0; TargetA1.Pos.Y = 0; TargetA1.Scale.X = 1; TargetA1.Scale.Y = 1; Nav.Transition.StageDurations.push(160); Nav.Transition.StageDurations.push(160); Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA0); Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA1); // Prep var RelevantButtonIndex = GetIndexOfButton(RelevantButton); Nav.Transition.RelevantButtonElement = Nav.Transition.ButtonsContainerCloneElement.children[RelevantButtonIndex]; var ButtonGeometry = ComputeButtonGeometryRelativeToGrid(Nav.Transition.RelevantButtonElement); Nav.Transition.Transforms.ButtonsContainer.Current.Pos.X = ButtonGeometry.Pos.X; Nav.Transition.Transforms.ButtonsContainer.Current.Pos.Y = ButtonGeometry.Pos.Y; Nav.Transition.Transforms.ButtonsContainer.Current.Scale.X = ButtonGeometry.Scale.X; Nav.Transition.Transforms.ButtonsContainer.Current.Scale.Y = ButtonGeometry.Scale.Y; Nav.Transition.Transforms.ButtonsContainer.Current.Opacity = 0; Nav.Transition.Transforms.ButtonsContainer.Current.ZIndex = 1; Nav.Transition.Transforms.ButtonsContainerClone.Current.ZIndex = 0; ApplyTransform(Nav.Transition.ButtonsContainerCloneElement, Nav.Transition.Transforms.ButtonsContainerClone.Current); ApplyTransform(Nav.ButtonsContainer, Nav.Transition.Transforms.ButtonsContainer.Current); } break; case transition_type.SUBDIVISION_ASCENT: { // Init targets //// ButtonsContainer let TargetA0 = NullTarget(); let TargetA1 = NullTarget(); let TargetA2 = NullTarget(); TargetA2.ZIndex = 1; //// ButtonsContainerClone let TargetC0 = NullTarget(); let TargetC1 = NullTarget(); let TargetC2 = NullTarget(); var ButtonGeometry = ComputeButtonGeometryRelativeToGrid(Nav.Transition.RelevantButtonElement); TargetC0.Pos.X = ButtonGeometry.Pos.X; TargetC0.Pos.Y = ButtonGeometry.Pos.Y; TargetC0.Scale.X = ButtonGeometry.Scale.X; TargetC0.Scale.Y = ButtonGeometry.Scale.Y; TargetC1.Opacity = 0; TargetC2.ZIndex = 0; Nav.Transition.StageDurations.push(160); Nav.Transition.StageDurations.push(160); Nav.Transition.StageDurations.push(0); Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA0); Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA1); Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(TargetA2); Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.push(TargetC0); Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.push(TargetC1); Nav.Transition.Transforms.ButtonsContainerClone.TargetStages.push(TargetC2); // Prep Nav.Transition.Transforms.ButtonsContainer.Current.ZIndex = 0; Nav.Transition.Transforms.ButtonsContainerClone.Current.ZIndex = 1; ApplyTransform(Nav.Transition.ButtonsContainerCloneElement, Nav.Transition.Transforms.ButtonsContainerClone.Current); ApplyTransform(Nav.ButtonsContainer, Nav.Transition.Transforms.ButtonsContainer.Current); } break; } DoTransition(); } } } function PushButton(ButtonElement, Button) { var Level = { Projects: Button.Projects, Entries: Button.Entries, HeadIndex: Button.HeadIndex, TailIndex: Button.TailIndex, }; var TransitionType = undefined; if(Level.Projects !== null || Level.Entries !== null) { if(Level.Projects !== null) { if(Level.Projects.length === undefined) { var Entries = Level.Projects.querySelectorAll(":scope > .cineraIndexEntries > div"); var Projects = Level.Projects.querySelectorAll(":scope > .cineraIndexProject"); Level.Entries = Entries.length ? Entries : null; Level.Projects = Projects.length ? Projects : null; TransitionType = transition_type.PROJECT_ENTRY; } else { TransitionType = transition_type.SUBDIVISION_DESCENT; } Nav.TraversalStack.push(Level); UpdateButtons(TransitionType, Button); } else { if(Level.Entries.length === undefined) { var Address = ButtonElement.lastElementChild.getAttribute("href"); location = Address; } else { Nav.TraversalStack.push(Level); TransitionType = transition_type.SUBDIVISION_DESCENT; UpdateButtons(TransitionType, Button); } } } } function RangeContains(Range, Target) { return (Range.HeadIndex == null && Range.TailIndex == null) || (Range.HeadIndex <= Target && Range.TailIndex >= Target); } function LengthOf(Level) { var Result = 0; var Items = Level.Projects && Level.Projects.length > 0 ? Level.Projects : Level.Entries; if(Items) { if(Level.HeadIndex !== null && Level.TailIndex !== null) { Result = Diff(Level.HeadIndex, Level.TailIndex) + 1; } else if(Items.length !== undefined) { Result = Items.length; } else { Result = 1; } } return Result; } function DereferenceLevel(Generation) { var Level = Nav.TraversalStack[Generation - 1]; var Result = { Projects: Level.Projects, Entries: Level.Entries, HeadIndex: Level.HeadIndex, TailIndex: Level.TailIndex, }; return Result; } function GetSubdivisionFor(LevelBundle, Index, TargetGeneration) { var Result = { Projects: LevelBundle.This.Projects, Entries: LevelBundle.This.Entries, HeadIndex: null, TailIndex: null, }; var GenerationsToDistribute = 1; var Generation = TargetGeneration ? TargetGeneration : LevelBundle.Generation; var Parent = DereferenceLevel(Generation - GenerationsToDistribute); while(!RangeContains(Parent, Index)) { ++GenerationsToDistribute; Parent = DereferenceLevel(Generation - GenerationsToDistribute); } while(GenerationsToDistribute > 0) { var Distribution = ComputeItemDistribution(Parent); var ToPlace = null; var FullButtonItemCount = null; var ButtonCount = null; if(LevelBundle.Type == item_type.PROJECT) { ToPlace = Distribution.ProjectsToPlace; FullButtonItemCount = Distribution.FullButtonProjectCount; ButtonCount = Distribution.ButtonsForProjects; } else if(LevelBundle.Type = item_type.ENTRY) { ToPlace = Distribution.EntriesToPlace; FullButtonItemCount = Distribution.FullButtonEntryCount; ButtonCount = Distribution.ButtonsForEntries; } Result.HeadIndex = null; Result.TailIndex = null; for(var i = 0; i < ButtonCount; ++i) { if(ToPlace == ButtonCount - i) { FullButtonItemCount = 1; } Result.HeadIndex = Result.TailIndex !== null ? Result.TailIndex + 1 : Parent.HeadIndex !== null ? Parent.HeadIndex : 0; Result.TailIndex = Result.HeadIndex + Math.min(FullButtonItemCount, ToPlace) - 1; if(RangeContains(Result, Index)) { break; } ToPlace -= LengthOf(Result); } --GenerationsToDistribute; Parent.HeadIndex = Result.HeadIndex; Parent.TailIndex = Result.TailIndex; } return Result; } function SiblingIsLeaf(SiblingID) { var LevelBundle = GetTraversalLevelBundle(); return LengthOf(GetSubdivisionFor(LevelBundle, SiblingID == siblings.PREV ? LevelBundle.This.HeadIndex - 1 : LevelBundle.This.TailIndex + 1)) == 1; } function ShiftToSibling(SiblingID) { var LevelBundle = GetTraversalLevelBundle(); if((SiblingID == siblings.PREV && HasPrevSibling(LevelBundle.This)) || (SiblingID == siblings.NEXT && HasNextSibling(LevelBundle.This))) { var TransitionType = SiblingID == siblings.PREV ? transition_type.SIBLING_SHIFT_PREV : transition_type.SIBLING_SHIFT_NEXT; var CurrentItem = LevelBundle.Type == item_type.PROJECT ? LevelBundle.This.Projects : LevelBundle.This.Entries; var TargetIndex = SiblingID == siblings.PREV ? LevelBundle.This.HeadIndex - 1 : LevelBundle.This.TailIndex + 1; LevelBundle.Parent = Nav.TraversalStack[0]; var TransitionLevel; for(let i = 0; i < LevelBundle.Generation; ++i) { LevelBundle.This = Nav.TraversalStack[i]; if((LevelBundle.Type == item_type.PROJECT && CurrentItem == LevelBundle.Parent.Projects) || (LevelBundle.Type == item_type.ENTRY && CurrentItem == LevelBundle.Parent.Entries)) { if(!RangeContains(LevelBundle.This, TargetIndex)) { Nav.TraversalStack[i] = GetSubdivisionFor(LevelBundle, TargetIndex, i + 1); if(LengthOf(Nav.TraversalStack[i]) == 1) { TransitionType = transition_type.SUBDIVISION_ASCENT; TransitionLevel = LevelBundle.This; Nav.TraversalStack.pop(); } } } LevelBundle.Parent = Nav.TraversalStack[i]; } UpdateButtons(TransitionType, undefined, TransitionLevel); } else { DequeueInteraction(); } } function Ascend() { var LevelBundle = GetTraversalLevelBundle(); if(LevelBundle.Generation > 1) { var TransitionType = LevelBundle.This.HeadIndex !== null && LevelBundle.This.TailIndex !== null ? transition_type.SUBDIVISION_ASCENT : transition_type.PROJECT_EXIT; var PoppedLevel = Nav.TraversalStack.pop(); UpdateButtons(TransitionType, undefined, PoppedLevel); } } function ShiftToPrevSibling() { var LevelBundle = GetTraversalLevelBundle(); if(LevelBundle.Generation > 1) { if(Nav.Controls.GridTraversal.PrevAscends == true) { Ascend(); } } ShiftToSibling(siblings.PREV); } function ShiftToNextSibling() { ShiftToSibling(siblings.NEXT); } function InputIsFocused() { return document.activeElement == Search.QueryElement; } function ShouldFireGridEvents() { return Nav.ViewType == view_type.GRID && !InputIsFocused() && !IsQuery(); } function ModifyControlKeybinding(Event) { // TODO(matt): Settle on the final sets of bindings var Chron = Nav.SortChronological; var Key = Event.key; var IT = interaction_type.PUSH_BUTTON; var ID = { Element: null, Button: null, }; // NOTE(matt): InteractionData switch(Key) { case "?": if(!InputIsFocused()) { Nav.Controls.HelpDocumentation.classList.toggle("visible"); } break; case "t": if(!InputIsFocused()) { EnqueueInteraction(interaction_type.SORT); } break; case "y": if(!InputIsFocused()) { ToggleView(); } break; case "m": if(!InputIsFocused()) { ToggleAnimations(); } break; case "h": if(ShouldFireGridEvents()) { EnqueueInteraction(Chron ? interaction_type.SIBLING_SHIFT_PREV : interaction_type.SIBLING_SHIFT_NEXT); } break; case "k": if(ShouldFireGridEvents()) { EnqueueInteraction(interaction_type.ASCEND); } break; case "l": if(ShouldFireGridEvents()) { EnqueueInteraction(Chron ? interaction_type.SIBLING_SHIFT_NEXT : interaction_type.SIBLING_SHIFT_PREV); } break; } } function BindControlKeys() { document.addEventListener("keydown", ModifyControlKeybinding); } function UnbindControlKeys() { document.removeEventListener("keydown", ModifyControlKeybinding); } function RebindControlKeys() { UnbindControlKeys(); BindControlKeys(); } function KeyIsInGrid(GridSize, KeyPos) { return GridSize.X > KeyPos.X && GridSize.Y > KeyPos.Y; } function ComputeNaturalKeyIndex(GridSize, KeyPos) { return KeyPos.Y * GridSize.X + KeyPos.X; } function EnqueueGridInteraction(GridSize, KeyPos) { var Chron = Nav.SortChronological; var LastButtonIndex = Nav.Buttons.length - 1; var NaturalIndex = ComputeNaturalKeyIndex(GridSize, KeyPos); var IT = interaction_type.PUSH_BUTTON; var ID = { Element: null, Button: null, }; // NOTE(matt): InteractionData ID.Element = Nav.Buttons[Chron ? NaturalIndex : LastButtonIndex - NaturalIndex].Element; ID.Button = Nav.Buttons[Chron ? NaturalIndex : LastButtonIndex - NaturalIndex]; EnqueueInteraction(IT, ID); } function Get2DPosFromIndex(Layout, Index) { var Result = { X: null, Y: null, }; Result.X = Index % Layout.X; Result.Y = (Index - Result.X) / Layout.Y; return Result; } function ModifyGridKeybinding(Event) { var Key = Event.key; // TODO(matt): With this, we could probably easily add a setting for keyboard layout: e.g. Dvorak var PhysicalKeys = [ "1", "2", "3", "4", "q", "w", "e", "r", "a", "s", "d", "f", "z", "x", "c", "v" ]; var KeyLayout = { X: 4, Y: 4 }; for(var i = 0; i < PhysicalKeys.length; ++i) { if(Key == PhysicalKeys[i]) { var KeyPos = Get2DPosFromIndex(KeyLayout, i); if(KeyIsInGrid(Nav.GridSize, KeyPos) && ShouldFireGridEvents()) { EnqueueGridInteraction(Nav.GridSize, KeyPos); } } } } function BindGridKeys() { document.addEventListener("keydown", ModifyGridKeybinding); } function UnbindGridKeys() { document.removeEventListener("keydown", ModifyGridKeybinding); } function DoRotationStage(Now) { if(Nav.Transition.StageDurations.length) { if(Nav.Transition.StartTime === undefined) { Nav.Transition.StartTime = Now; } var Elapsed = Now - Nav.Transition.StartTime; var Duration = Nav.Transition.StageDurations[0]; if(Duration === 0) { if(Nav.Transition.Transforms.ButtonsContainer.TargetStages.length) { MergeTransform(Nav.Transition.Transforms.ButtonsContainer.Current, Nav.Transition.Transforms.ButtonsContainer.TargetStages[0]); ApplyTransform(Nav.ButtonsContainer, Nav.Transition.Transforms.ButtonsContainer.Current); } } else { var t = Clamp01(Elapsed / Duration); if(LerpTransforms(Nav.Transition.Transforms.ButtonsContainer, t)) { ApplyTransform(Nav.ButtonsContainer, Nav.Transition.Transforms.ButtonsContainer.Current); } } for(var i = 0; i < Nav.ButtonsContainer.children.length; ++i) { var This = Nav.ButtonsContainer.children[i]; var ThisTexts = This.querySelectorAll(".cineraText"); for(var j = 0; j < ThisTexts.length; ++j) { ThisTexts[j].style.transform = "rotate3d(0, 0, 1, " + -Nav.Transition.Transforms.ButtonsContainer.Current.Rotation.Z + "deg)"; } } if(TransformsComplete(Nav.Transition.Transforms)) { FinaliseTransforms(Nav.Transition.Transforms); } Nav.Transition.RequestedFrame = window.requestAnimationFrame(DoRotationStage); } else { Nav.Transition.RequestedFrame = undefined; ResetButtonsContainerClone(); DequeueInteraction(); Nav.Controls.Header.style.overflow = null; } } function RotateButtons(Initialising) { // TODO(matt): Consider pushing this through DoTransition(), aware that it'd need to transform ".cineraText" children CopyTransform(Nav.Transition.Transforms.ButtonsContainer.Initial, Nav.Transition.Transforms.ButtonsContainer.Current); Nav.Transition.StartTime = undefined; if(!Initialising && Nav.Transition.Enabled) { Nav.Transition.StageDurations.push(320); } else { Nav.Transition.StageDurations.push(0); } let Target = NullTarget(); if(Nav.SortChronological) { Target.Rotation.Z = 0; } else { Target.Rotation.Z = 180; } Nav.Transition.Transforms.ButtonsContainer.TargetStages.push(Target); CopyTransform(Nav.Transition.Transforms.ButtonsContainer.Initial, Nav.Transition.Transforms.ButtonsContainer.Current); Nav.Transition.RequestedFrame = window.requestAnimationFrame(DoRotationStage); } function AddAnimationClass() { Nav.Nexus.classList.add("anim"); } function Sort(Initialising) { ResetButtonsContainerClone(); if(Initialising && Nav.Transition.Enabled) { Nav.Nexus.classList.remove("anim"); } if(Nav.SortChronological) { Nav.Controls.Sort.textContent = "Sort: New to Old ⏷"; Nav.Nexus.classList.add("reversed"); for(var i = 0; i < Search.Projects.length; ++i) { if(Search.Projects[i].entriesContainer) { Search.Projects[i].entriesContainer.style.flexFlow = "column-reverse"; } } } else { Nav.Controls.Sort.textContent = "Sort: Old to New ⏶"; Nav.Nexus.classList.remove("reversed"); for(var i = 0; i < Search.Projects.length; ++i) { if(Search.Projects[i].entriesContainer) { Search.Projects[i].entriesContainer.style.flexFlow = "column"; } } } if(Initialising && Nav.Transition.Enabled) { setTimeout(AddAnimationClass, 320); } UnbindGridKeys(Nav.GridSize); Nav.SortChronological = !Nav.SortChronological; if(Nav.Controls.GridTraversal.PrevAscends) { if(Nav.SortChronological) { Nav.Controls.GridTraversal.Prev.children[0].textContent = "↰"; } else { Nav.Controls.GridTraversal.Prev.children[0].textContent = "↲"; } } if(MaintainingState()) { if(Nav.SortChronological) { ClearStateBit(state_bit.SORT_REVERSED); } else { SetStateBit(state_bit.SORT_REVERSED); } } RotateButtons(Initialising); BindGridKeys(); runSearch(true); } function DequeueInteraction() { if(Nav.InteractionQueue.length) { var I = Nav.InteractionQueue.shift(); switch(I.Type) { case interaction_type.PUSH_BUTTON: { PushButton(I.Data.Element, I.Data.Button); } break; case interaction_type.SIBLING_SHIFT_PREV: { ShiftToPrevSibling(); } break; case interaction_type.SIBLING_SHIFT_NEXT: { ShiftToNextSibling(); } break; case interaction_type.ASCEND: { Ascend(); } break; case interaction_type.SORT: { Sort(); } break; } } } function EnqueueInteraction(InteractionType, Data) { // TODO(matt): Maybe see about interpolating out of interrupted transitions? // // Interruptability: // Reversible Pairs: // SIBLING_SHIFT_PREV by SIBLING_SHIFT_NEXT // SIBLING_SHIFT_NEXT (when SHIFT) by SIBLING_SHIFT_PREV // SIBLING_SHIFT_NEXT (when ASCEND) by PUSH_BUTTON (of the corresponding button) // PUSH_BUTTON by ASCEND // ASCEND by PUSH_BUTTON (of the corresponding button) // SORT by SORT // // Mixtures: // SIBLING_SHIFT_PREV – SORT // var I = { Type: InteractionType, Data: Data, }; Nav.InteractionQueue.push(I); if(!Nav.Transition.RequestedFrame) { DequeueInteraction(); } } function UseOrientation(Orientation) { Nav.GridContainer.classList.remove("Portrait", "Landscape", "Left", "Right"); switch(Orientation) { case orientations.PORTRAIT: { Nav.Controls.GridTraversal.Header.insertBefore(Nav.Controls.GridTraversal.Ascend, Nav.Controls.GridTraversal.Next); Nav.GridContainer.classList.add("Portrait"); } break; case orientations.LANDSCAPE_LEFT: { Nav.Controls.GridTraversal.Header.parentElement.insertBefore(Nav.Controls.GridTraversal.Ascend, Nav.Controls.GridTraversal.Header); Nav.GridContainer.classList.add("Landscape", "Left"); } break; case orientations.LANDSCAPE_RIGHT: { Nav.Controls.GridTraversal.Header.parentElement.insertBefore(Nav.Controls.GridTraversal.Ascend, Nav.Controls.GridTraversal.Header); Nav.GridContainer.classList.add("Landscape", "Right"); } break; } } function InitNexus() { Nav.List.classList.add("hidden"); var ButtonsContainerPrototype = document.createElement("div"); ButtonsContainerPrototype.setAttribute("id", "cineraIndexGrid"); var ButtonsPrototype = document.createElement("div"); ButtonsPrototype.classList.add("cineraButtons"); var ButtonsClonePrototype = document.createElement("div"); ButtonsClonePrototype.classList.add("cineraButtons"); ButtonsContainerPrototype.appendChild(ButtonsClonePrototype); ButtonsContainerPrototype.appendChild(ButtonsPrototype); Nav.Transition.ButtonsTransitionContainerElement = Nav.GridContainer.appendChild(ButtonsContainerPrototype); Nav.Grid = Nav.Transition.ButtonsTransitionContainerElement; Nav.Transition.ButtonsContainerCloneElement = Nav.Transition.ButtonsTransitionContainerElement.querySelectorAll(".cineraButtons")[0]; Nav.ButtonsContainer = Nav.Transition.ButtonsTransitionContainerElement.querySelectorAll(".cineraButtons")[1]; Nav.GridColumnGap = parseInt(window.getComputedStyle(Nav.ButtonsContainer).gridColumnGap); Nav.GridRowGap = parseInt(window.getComputedStyle(Nav.ButtonsContainer).gridRowGap); // NOTE(matt): We ResetButtonsContainerClone() anyway, but without cycling this classList Safari seems to do transitions // based on the wrong size grid Nav.GridContainer.classList.add("hidden"); ResetButtonsContainerClone(); switch(Nav.ViewType) { case view_type.LIST: { Nav.List.classList.remove("hidden"); ScrollCondition = false; } break; case view_type.GRID: { Nav.Grid.classList.remove("hidden"); ScrollCondition = true; } break; } Nav.Nexus.classList.add("anim"); if(CineraProps.IsMobile) { CineraProps.Orientation = GetRealOrientation(orientations.LANDSCAPE_LEFT, CineraProps.IsMobile); UseOrientation(CineraProps.Orientation); } } function ComputePossibleCellsInDimension(AvailableSpaceInPixels, GridGap) { var Result = 0; var SpaceToFill = AvailableSpaceInPixels; if(SpaceToFill >= Nav.MinButtonDim) { SpaceToFill -= Nav.MinButtonDim; ++Result; } for(; SpaceToFill >= (Nav.MinButtonDim + GridGap); ) { SpaceToFill -= (Nav.MinButtonDim + GridGap); ++Result; } return Result; } function GridSizeMeetsMinimumSupported(GridSize) { return GridSize.X * GridSize.Y >= 2; } function GridSizeIsSupported(GridSize) { return (GridSize.X >= Nav.GridMinCellsPerDimension && GridSize.X <= Nav.GridMaxCellsPerDimension) && (GridSize.Y >= Nav.GridMinCellsPerDimension && GridSize.Y <= Nav.GridMaxCellsPerDimension) && GridSizeMeetsMinimumSupported(GridSize); } function ComputeOptimalGridSize() { var Result = { X: null, Y: null, }; var WindowDim = GetWindowDim(CineraProps.IsMobile); var DimReduction = { X: 0, Y: Nav.Controls.Header.offsetHeight, }; Nav.Transition.ButtonsTransitionContainerElement.style = null; Nav.ButtonsContainer.style = null; Nav.Controls.GridTraversal.Container.style = null; Nav.Controls.GridTraversal.Ascend.style = null; Nav.Controls.GridTraversal.Prev.style = null; Nav.Controls.GridTraversal.Next.style = null; // TODO(matt): Maybe structure it such that the grid is always not hidden at this point? var GridWasHidden = Nav.GridContainer.classList.contains("hidden"); if(GridWasHidden) { Nav.GridContainer.classList.remove("hidden"); } if(CineraProps.IsMobile && (CineraProps.Orientation == orientations.LANDSCAPE_LEFT || CineraProps.Orientation == orientations.LANDSCAPE_RIGHT)) { DimReduction.X += Nav.Controls.GridTraversal.Container.offsetWidth; } else { DimReduction.Y += Nav.Controls.GridTraversal.Container.offsetHeight; } var MaxWidth = MaxWidthOfElement(Nav.Transition.ButtonsTransitionContainerElement, WindowDim) - DimReduction.X; var MaxHeight = MaxHeightOfElement(Nav.Transition.ButtonsTransitionContainerElement, WindowDim) - DimReduction.Y; if(GridWasHidden) { Nav.GridContainer.classList.add("hidden"); } var BodyStyle = window.getComputedStyle(document.body); if(Nav.Nexus.parentNode == document.body) { // TODO(matt): Robustify this MaxWidth -= parseInt(BodyStyle.marginRight); MaxHeight -= parseInt(BodyStyle.marginBottom); } Result.X = ComputePossibleCellsInDimension(MaxWidth, Nav.GridColumnGap); Result.Y = ComputePossibleCellsInDimension(MaxHeight, Nav.GridRowGap); if(GridSizeMeetsMinimumSupported(Result)) { Result.X = Clamp(Nav.GridMinCellsPerDimension, Result.X, Nav.GridMaxCellsPerDimension); Result.Y = Clamp(Nav.GridMinCellsPerDimension, Result.Y, Nav.GridMaxCellsPerDimension); var ButtonDimBasedOnX = Math.floor((MaxWidth - Nav.GridColumnGap * (Result.X - 1)) / Result.X); var ButtonDimBasedOnY = Math.floor((MaxHeight - Nav.GridRowGap * (Result.Y - 1)) / Result.Y); Nav.ButtonDim = Math.min(ButtonDimBasedOnX, ButtonDimBasedOnY); Nav.ButtonDim -= Nav.ButtonDim % 2; // NOTE(matt): Even-length helps CSS keep the head & tail's correct size when rotated 180 degrees var GridTemplateColumnsStyle = "repeat(" + Result.X + ", minmax(" + Nav.ButtonDim + "px, " + Nav.ButtonDim + "px))"; Nav.ButtonsContainer.style.gridTemplateColumns = GridTemplateColumnsStyle; var GridTemplateRowsStyle = "repeat(" + Result.Y + ", " + Nav.ButtonDim + "px)"; Nav.ButtonsContainer.style.gridTemplateRows = GridTemplateRowsStyle; Nav.GridDim.X = Nav.ButtonDim * Result.X + Nav.GridColumnGap * (Result.X - 1); Nav.GridDim.Y = Nav.ButtonDim * Result.Y + Nav.GridRowGap * (Result.Y - 1); SetDim(Nav.Transition.ButtonsTransitionContainerElement, Nav.GridDim.X + "px", Nav.GridDim.Y + "px"); Nav.Controls.GridTraversal.Container.style.maxWidth = Nav.GridDim.X + "px"; Nav.Controls.GridTraversal.Container.style.maxHeight = Nav.GridDim.Y + "px"; var TraversalButtonCount = 3; if(Nav.Controls.GridTraversal.Container.scrollWidth > Nav.Controls.GridTraversal.Container.clientWidth) { var TraversalButtonDim = Nav.Controls.GridTraversal.Container.clientWidth / TraversalButtonCount; SetDim(Nav.Controls.GridTraversal.Ascend, TraversalButtonDim + "px", TraversalButtonDim + "px"); SetDim(Nav.Controls.GridTraversal.Prev, TraversalButtonDim + "px", TraversalButtonDim + "px"); SetDim(Nav.Controls.GridTraversal.Next, TraversalButtonDim + "px", TraversalButtonDim + "px"); } if(Nav.Controls.GridTraversal.Container.scrollHeight > Nav.Controls.GridTraversal.Container.clientHeight) { var TraversalButtonDim = Nav.Controls.GridTraversal.Container.clientHeight / TraversalButtonCount; SetDim(Nav.Controls.GridTraversal.Ascend, TraversalButtonDim + "px", TraversalButtonDim + "px"); SetDim(Nav.Controls.GridTraversal.Prev, TraversalButtonDim + "px", TraversalButtonDim + "px"); SetDim(Nav.Controls.GridTraversal.Next, TraversalButtonDim + "px", TraversalButtonDim + "px"); } } ResetButtonsContainerClone(); // NOTE(matt): This reapplies the sorting Z-rotation return Result; } function GetScrollToElement(NodeList, UpperIndex) { var Result = undefined; if(UpperIndex === null) { if(NodeList.length) { Result = NodeList[0].closest(".cineraIndexProject"); } else { Result = NodeList.closest(".cineraIndexProject"); } } else { Result = NodeList[UpperIndex]; } return Result; } function EmptyTraversalStack() { while(Nav.TraversalStack.length > 1) { Nav.TraversalStack.pop(); } } function GetContainingProjectOfLevel(Level) { var Result = null; if(Level.Projects !== null) { if(Level.Projects.length) { Result = Level.Projects[0].parentNode; } else { Result = Level.Projects.parentNode; } } else if(Level.Entries !== null) { if(Level.Entries.length) { Result = Level.Entries[0].closest(".cineraIndexProject"); } else { Result = Level.Entries.closest(".cineraIndexProject"); } } return Result; } function GetContainingProject(ProjectElement) { var Result = null; if(ProjectElement.parentNode.classList.contains("cineraIndexProject")) { Result = ProjectElement.parentNode; } return Result; } function PushProjectOntoStack(Stack, ContainingProject) { var Project = { Element: ContainingProject, Projects: ContainingProject.querySelectorAll(":scope > .cineraIndexProject"), Entries: ContainingProject.querySelectorAll(":scope > .cineraIndexEntries > div"), Index: null, }; if(Project.Projects.length == 0) { Project.Projects = null; } if(Project.Entries.length == 0) { Project.Entries = null; } var Siblings = ContainingProject.parentNode.querySelectorAll(":scope > .cineraIndexProject"); Project.Index = GetIndexOfElement(Siblings, ContainingProject); Stack.push(Project); } function BuildProjectsStack(TargetLevel) { let ProjectsStack = []; var ContainingProject = GetContainingProjectOfLevel(TargetLevel); PushProjectOntoStack(ProjectsStack, ContainingProject); ContainingProject = GetContainingProject(ProjectsStack[ProjectsStack.length - 1].Element); while(ContainingProject) { PushProjectOntoStack(ProjectsStack, ContainingProject); ContainingProject = GetContainingProject(ProjectsStack[ProjectsStack.length - 1].Element); } return ProjectsStack; } function EmptyTraversalStackIntoProjectsStack() { let ProjectsStack = []; while(Nav.TraversalStack.length > 1) { let ThisLevel = Nav.TraversalStack[Nav.TraversalStack.length - 1]; PushLevelProjectUniquely(ProjectsStack, ThisLevel); Nav.TraversalStack.pop(); } return ProjectsStack; } function DeriveTraversalStack(ProjectsStack, TargetLevel) { if(ProjectsStack && TargetLevel) { while(ProjectsStack.length > 0) { var ThisProject = ProjectsStack[ProjectsStack.length - 1]; var ButtonInfo = { HeadIndex: null, TailIndex: null, ItemCount: null, }; let PopulationData = { Distribution: null, ThisLevel: null, Prev: null, DoingEntries: false, }; PopulationData.ThisLevel = Nav.TraversalStack[Nav.TraversalStack.length - 1]; PopulationData.Distribution = ComputeItemDistribution(PopulationData.ThisLevel); for(var ButtonIndex = 0; ButtonIndex < Nav.Buttons.length; ++ButtonIndex) { let Button = { Projects: null, Entries: null, HeadIndex: null, TailIndex: null, }; if(PopulationData.Distribution.ProjectsToPlace > 0 || PopulationData.Distribution.EntriesToPlace > 0) { ButtonInfo = PopulateButton(PopulationData, Button, ButtonIndex); if(ButtonInfo.HeadIndex <= ThisProject.Index && ButtonInfo.TailIndex >= ThisProject.Index) { var FoundProject = PseudoPushButton(Button); if(FoundProject) { ProjectsStack.pop(); } break; } PopulationData.Prev = ButtonInfo; } } } if(TargetLevel.HeadIndex !== null && TargetLevel.TailIndex !== null) { var Descended = false; while(true) { let PopulationData = { Distribution: null, ThisLevel: null, Prev: null, DoingEntries: false, }; PopulationData.ThisLevel = Nav.TraversalStack[Nav.TraversalStack.length - 1]; PopulationData.Distribution = ComputeItemDistribution(PopulationData.ThisLevel); for(var ButtonIndex = 0; ButtonIndex < Nav.Buttons.length; ++ButtonIndex) { let Button = { Projects: null, Entries: null, HeadIndex: null, TailIndex: null, }; if(PopulationData.Distribution.ProjectsToPlace > 0 || PopulationData.Distribution.EntriesToPlace > 0) { ButtonInfo = PopulateButton(PopulationData, Button, ButtonIndex); if(ButtonInfo.HeadIndex <= TargetLevel.HeadIndex && ButtonInfo.TailIndex >= TargetLevel.TailIndex) { Descended = true; PseudoPushButton(Button); break; } PopulationData.Prev = ButtonInfo; } } if(!Descended) { break; } else { Descended = false; } } } } } function GetGenerationOf(Element) { var Result = 0; if(Element.classList.contains("cineraIndexProject")) { ++Result; var ContainingProject = GetContainingProject(Element); while(ContainingProject) { ++Result; ContainingProject = GetContainingProject(ContainingProject); } } return Result; } function ComputeTargetLevelForViewport() { var Result = { Projects: null, Entries: null, HeadIndex: null, TailIndex: null, }; var ViewportTop = window.scrollY; var ViewportBottom = ViewportTop + window.innerHeight; var ControlsHeight = Nav.Controls.Header.offsetHeight; ViewportTop += ControlsHeight; var Elements = []; for(var ProjectIndex = 0; ProjectIndex < Search.Projects.length; ++ProjectIndex) { var ThisProject = Search.Projects[ProjectIndex]; Elements.push(ThisProject.projectTitleElement); if(ThisProject.entriesContainer) { var Entries = ThisProject.entriesContainer.querySelectorAll(":scope > div"); if(Nav.SortChronological) { for(var EntryIndex = 0; EntryIndex < Entries.length; ++EntryIndex) { Elements.push(Entries[EntryIndex]); } } else { for(var EntryIndex = Entries.length - 1; EntryIndex >= 0; --EntryIndex) { Elements.push(Entries[EntryIndex]); } } } } var Upper = { Element: null, Parent: null, Type: null, }; var Lower = { Element: null, Parent: null, Type: null, }; for(var i = 0; i < Elements.length; ++i) { var ElementTop = getElementYOffsetFromPage(Elements[i]); if(!Upper.Element) { if(ElementTop >= ViewportTop) { Upper.Element = Elements[i]; Lower.Element = Upper.Element; } } else { var ElementBottom = ElementTop + Elements[i].scrollHeight; if(ElementBottom <= ViewportBottom) { Lower.Element = Elements[i]; } else { break; } } } Upper.Type = Upper.Element.classList.contains("cineraProjectTitle") ? item_type.PROJECT : item_type.ENTRY; Lower.Type = Lower.Element.classList.contains("cineraProjectTitle") ? item_type.PROJECT : item_type.ENTRY; Upper.Parent = Upper.Element.closest(".cineraIndexProject"); Lower.Parent = Lower.Element.closest(".cineraIndexProject"); if(Upper.Parent == Lower.Parent) { if(Upper.Type == Lower.Type) { switch(Upper.Type) { case item_type.PROJECT: { Result.Projects = Upper.Parent.querySelectorAll(":scope > .cineraIndexProject"); Result.HeadIndex = GetIndexOfElement(Result.Projects, Upper.Element); Result.TailIndex = GetIndexOfElement(Result.Projects, Lower.Element); } break; case item_type.ENTRY: { Result.Entries = Upper.Parent.querySelectorAll(":scope > .cineraIndexEntries > div"); Result.HeadIndex = GetIndexOfElement(Result.Entries, Upper.Element); Result.TailIndex = GetIndexOfElement(Result.Entries, Lower.Element); } break; } if(!Nav.SortChronological) { var Temp = Result.HeadIndex; Result.HeadIndex = Result.TailIndex; Result.TailIndex = Temp; } } else { Result.Projects = Upper.Parent.querySelectorAll(":scope > .cineraIndexProject"); if(Result.Projects.length == 0) { Result.Projects = null; } Result.Entries = Upper.Parent.querySelectorAll(":scope > .cineraIndexEntries > div"); if(Result.Entries.length == 0) { Result.Entries = null; } } } else { var UpperGeneration = GetGenerationOf(Upper.Parent); var LowerGeneration = GetGenerationOf(Lower.Parent); while(UpperGeneration > LowerGeneration) { Upper.Parent = GetContainingProject(Upper.Parent); --UpperGeneration; } while(LowerGeneration > UpperGeneration) { Lower.Parent = GetContainingProject(Lower.Parent); --LowerGeneration; } if(UpperGeneration == 0 && LowerGeneration == 0) { Upper.Parent = null; Lower.Parent = null; } while(Upper.Parent && Lower.Parent && Upper.Parent != Lower.Parent) { Upper.Parent = GetContainingProject(Upper.Parent); Lower.Parent = GetContainingProject(Lower.Parent); } if(Upper.Parent != null) { Result.Projects = Upper.Parent.querySelectorAll(":scope > .cineraIndexProject"); if(Result.Projects.length == 0) { Result.Projects = null; } Result.Entries = Upper.Parent.querySelectorAll(":scope > .cineraIndexEntries > div"); if(Result.Entries.length == 0) { Result.Entries = null; } } } while(Elements.length > 0) { Elements.pop(); } return Result; } function ScrollToWithOffset(Element, Offset) { var ScrollTop = getElementYOffsetFromPage(Element); ScrollTop -= Offset; window.scrollTo(0, ScrollTop); } function PickGridView() { if(GridSizeIsSupported(Nav.GridSize)) { Nav.Controls.View.textContent = "View: 𝑛-ary Grid"; Nav.ViewType = view_type.GRID; if(MaintainingState()) { ClearStateBit(state_bit.VIEW_LIST); SetStateBit(state_bit.VIEW_GRID); } if(!IsQuery()) { var TargetLevel = ComputeTargetLevelForViewport(); EmptyTraversalStack(); if(TargetLevel.Projects || TargetLevel.Entries) { var ProjectsStack = BuildProjectsStack(TargetLevel); DeriveTraversalStack(ProjectsStack, TargetLevel); } Nav.List.classList.add("hidden"); Nav.GridContainer.classList.remove("hidden"); UpdateButtons(); ScrollToWithOffset(Nav.Controls.GridTraversal.Header, Nav.Controls.Header.offsetHeight); } ScrollCondition = true; } else { // TODO(matt): Inform user that grid view is unavailable } } function PickListView() { Nav.Controls.View.textContent = "View: List"; Nav.ViewType = view_type.LIST; if(MaintainingState()) { ClearStateBit(state_bit.VIEW_GRID); SetStateBit(state_bit.VIEW_LIST); } if(!IsQuery()) { Nav.List.classList.remove("hidden"); Nav.GridContainer.classList.add("hidden"); var LevelBundle = GetTraversalLevelBundle(); if(LevelBundle.Generation > 1) { var Element; if(LevelBundle.This.Entries !== null) { Element = GetScrollToElement(LevelBundle.This.Entries, Nav.SortChronological ? LevelBundle.This.HeadIndex : LevelBundle.This.TailIndex); } else if(LevelBundle.This.Projects !== null) { Element = GetScrollToElement(LevelBundle.This.Projects, Nav.SortChronological ? LevelBundle.This.HeadIndex : LevelBundle.This.TailIndex); } ScrollToWithOffset(Element, Nav.Controls.Header.offsetHeight); } } ScrollCondition = false; } function ToggleView() { // NOTE(matt): While we only have two views, a toggle will suffice. clearTimeout(ScrollerFunction); ScrollTicking = false; if(Nav.ViewType == view_type.GRID) { PickListView(); } else { PickGridView(); } } function ToggleAnimations() { Nav.Transition.Enabled = !Nav.Transition.Enabled; if(Nav.Transition.Enabled) { Nav.Controls.Anim.textContent = "Animations: ✔"; Nav.Nexus.classList.add("anim"); ClearStateBit(state_bit.DISABLE_ANIMATIONS); } else { Nav.Controls.Anim.textContent = "Animations: ✘"; Nav.Nexus.classList.remove("anim"); SetStateBit(state_bit.DISABLE_ANIMATIONS); } } function ToggleSave() { if(MaintainingState()) { Nav.Controls.Save.textContent = "Save Settings: ✘"; Nav.State = 0; Nav.State |= state_bit.NO_SAVE; SaveState(); } else { Nav.Controls.Save.textContent = "Save Settings: ✔"; Nav.State ^= state_bit.NO_SAVE; if(!Nav.Transition.Enabled) { SetStateBit(state_bit.DISABLE_ANIMATIONS); } if(!Nav.SortChronological) { SetStateBit(state_bit.SORT_REVERSED); } if(Nav.ViewType == view_type.LIST) { SetStateBit(state_bit.VIEW_LIST); } else if(Nav.ViewType == view_type.GRID) { SetStateBit(state_bit.VIEW_GRID); } } } function BindMenuItem(Item) { // TODO(matt): Enable this to bind the "click" event, making it take a function and parameters Item.addEventListener("mouseover", function(ev) { this.classList.add("focused"); }); Item.addEventListener("mouseout", function(ev) { this.classList.remove("focused"); }); } function BindControls() { var SettingsMenu = Nav.Controls.Header.querySelector(".cineraMenu.ViewSettings"); var SettingsMenuContainer = SettingsMenu.querySelector(".cineraMenuContainer"); SettingsMenu.addEventListener("mouseenter", function(ev) { SettingsMenuContainer.classList.add("visible"); }); SettingsMenu.addEventListener("mouseleave", function(ev) { SettingsMenuContainer.classList.remove("visible"); }); SettingsMenu.addEventListener("click", function(ev) { SettingsMenuContainer.classList.toggle("visible"); }); BindMenuItem(Nav.Controls.Sort); Nav.Controls.Sort.addEventListener("click", function(ev) { ev.stopPropagation(); EnqueueInteraction(interaction_type.SORT); }); BindMenuItem(Nav.Controls.View); Nav.Controls.View.addEventListener("click", function(ev) { ev.stopPropagation(); ToggleView(); }); BindMenuItem(Nav.Controls.Anim); Nav.Controls.Anim.addEventListener("click", function(ev) { ev.stopPropagation(); ToggleAnimations(); }); BindMenuItem(Nav.Controls.Save); Nav.Controls.Save.addEventListener("click", function(ev) { ev.stopPropagation(); ToggleSave(); }); var Filter = Nav.Controls.Header.querySelector(".cineraMenu.IndexFilter"); if(Filter) { var FilterContainer = Filter.querySelector(".cineraMenuContainer"); // TODO(matt): Once we have multiple menus, use a menuState on this page // menuState.push(Filter); Filter.addEventListener("mouseenter", function(ev) { FilterContainer.classList.add("visible"); }); Filter.addEventListener("mouseleave", function(ev) { FilterContainer.classList.remove("visible"); }); Filter.addEventListener("click", function(ev) { FilterContainer.classList.toggle("visible"); }); var IndexFilterProjects = Filter.querySelectorAll(".cineraFilterProject"); for(var i = 0; i < IndexFilterProjects.length; ++i) { IndexFilterProjects[i].addEventListener("mouseover", function(ev) { ev.stopPropagation(); this.classList.add("focused"); focusSprite(this); }); IndexFilterProjects[i].addEventListener("mouseout", function(ev) { ev.stopPropagation(); this.classList.remove("focused"); unfocusSprite(this); }); IndexFilterProjects[i].addEventListener("click", function(ev) { ev.stopPropagation(); toggleEntriesOfProjectAndChildren(this); }); } } Search.QueryElement.addEventListener("input", function(ev) { history.replaceState(null, null, "#" + encodeURIComponent(Search.QueryElement.value)); runSearch(); }); Nav.Controls.GridTraversal.Prev.addEventListener("click", function() { EnqueueInteraction(interaction_type.SIBLING_SHIFT_PREV) }); Nav.Controls.GridTraversal.Ascend.addEventListener("click", function() { EnqueueInteraction(interaction_type.ASCEND) }); Nav.Controls.GridTraversal.Next.addEventListener("click", function() { EnqueueInteraction(interaction_type.SIBLING_SHIFT_NEXT) }); BindHelp(Nav.Controls.Help, Nav.Controls.HelpDocumentation); } function InitButtons() { if(GridSizeIsSupported(Nav.GridSize)) { var ButtonPrototype = document.createElement("div"); ButtonPrototype.classList.add("cineraButton", "subdivision"); for(var i = 0; i < Nav.GridSize.X * Nav.GridSize.Y; ++i) { Nav.ButtonsContainer.appendChild(ButtonPrototype.cloneNode()); } var Buttons = Nav.ButtonsContainer.querySelectorAll(".cineraButton.subdivision"); for(let j = 0; j < Buttons.length; ++j) { let Button = { Element: Buttons[j], Projects: [], Entries: [], HeadIndex: null, TailIndex: null, }; Buttons[j].addEventListener("click", function() { let InteractionData = { Element: this, Button: Button, }; EnqueueInteraction(interaction_type.PUSH_BUTTON, InteractionData); }); Nav.Buttons.push(Button); } } } function ReinitButtons() { for(; Nav.Buttons.length > 0;) { Nav.Buttons[0].Element.remove(); Nav.Buttons.shift(); } InitButtons(); } function PushLevelProjectUniquely(Stack, Level) { if(Level.HeadIndex == null && Level.TailIndex == null) { var Found = false; var ContainingProject = GetContainingProjectOfLevel(Level); for(var i = 0; i < Stack.length; ++i) { if(ContainingProject === Stack[i].Element) { Found = true; break; } } if(!Found) { PushProjectOntoStack(Stack, ContainingProject); } } } function ButtonContainsLevel(Button, Level) { var Result = false; if(NodesMatch(Button.Projects, Level.Projects) && NodesMatch(Button.Entries, Level.Entries) && Button.HeadIndex <= Level.HeadIndex && Button.TailIndex >= Level.TailIndex) { Result = true; } return Result; } function PopulateButton(PopulationData, Button, ButtonIndex) { var Result; if(PopulationData.Distribution.ProjectsToPlace > 0) { Button.Projects = PopulationData.ThisLevel.Projects; if(PopulationData.Distribution.ProjectsToPlace == 1 || PopulationData.Distribution.ProjectsToPlace == PopulationData.Distribution.ButtonsForProjects - ButtonIndex) { PopulationData.Distribution.FullButtonProjectCount = 1; } Result = SetButtonInfo(Button, PopulationData.Prev, PopulationData.ThisLevel, PopulationData.Distribution); if(Result.ItemCount == 1) { Button.Projects = PopulationData.ThisLevel.Projects[Result.HeadIndex]; } PopulationData.Distribution.ProjectsToPlace -= Result.ItemCount; } else { Button.Entries = PopulationData.ThisLevel.Entries; if(!PopulationData.DoingEntries) { PopulationData.Prev = null; PopulationData.DoingEntries = true; } if(PopulationData.Distribution.EntriesToPlace == 1 || PopulationData.Distribution.EntriesToPlace == PopulationData.Distribution.ButtonsForEntries - ButtonIndex) { PopulationData.Distribution.FullButtonEntryCount = 1; } Result = SetButtonInfo(Button, PopulationData.Prev, PopulationData.ThisLevel, PopulationData.Distribution); if(Result.ItemCount == 1) { Button.Entries = Button.Entries[Result.HeadIndex]; } PopulationData.Distribution.EntriesToPlace -= Result.ItemCount; } return Result; } function PseudoPushButton(Button) { var ButtonIsProjectOrEntry = false; var Level = { Projects: Button.Projects, Entries: Button.Entries, HeadIndex: Button.HeadIndex, TailIndex: Button.TailIndex, }; if(Level.Projects !== null || Level.Entries !== null) { if(Level.Projects !== null) { if(Level.Projects.length === undefined) { var Entries = Level.Projects.querySelectorAll(":scope > .cineraIndexEntries > div"); var Projects = Level.Projects.querySelectorAll(":scope > .cineraIndexProject"); Level.Entries = Entries.length ? Entries : null; Level.Projects = Projects.length ? Projects : null; ButtonIsProjectOrEntry = true; } Nav.TraversalStack.push(Level); } else { if(Level.Entries.length === undefined) { ButtonIsProjectOrEntry = true; } else { Nav.TraversalStack.push(Level); } } } return ButtonIsProjectOrEntry; } function ResizeFunction() { var OriginalScrollX = scrollX; var OriginalScrollY = scrollY; CineraProps.Orientation = GetRealOrientation(orientations.LANDSCAPE_LEFT, CineraProps.IsMobile); if(CineraProps.IsMobile) { UseOrientation(CineraProps.Orientation); } var NewGridSize = ComputeOptimalGridSize(); if(Nav.GridSize !== NewGridSize) { UnbindGridKeys(); Nav.GridSize = NewGridSize; ReinitButtons(); BindGridKeys(); SetHelpKeyAvailability(Nav.GridSize) if(GridSizeIsSupported(Nav.GridSize)) { var TargetLevel = Nav.TraversalStack[Nav.TraversalStack.length - 1]; var ProjectsStack = EmptyTraversalStackIntoProjectsStack(); DeriveTraversalStack(ProjectsStack, TargetLevel); } else if(Nav.ViewType == view_type.GRID) { PickListView(); // TODO(matt): Inform user that we've switched to the list view } scroll(OriginalScrollX, OriginalScrollY); } UpdateButtons(); } function InitResizeEventListener() { window.addEventListener("resize", function() { if(CineraProps.IsMobile) { window.setTimeout(ResizeFunction, 512); } else { ResizeFunction(); } }); } function InitOrientationChangeListener() { screen.orientation.onchange = function() { if(CineraProps.IsMobile) { window.setTimeout(ResizeFunction, 512); } else { ResizeFunction(); } }; } // // Presenting / Navigating (Laying out and traversing the grid, and sorting)