UNPKG

globular-mvc

Version:

Generic template to create web-application that made use of globular as backend and materialize as css (wrap in web-component's)

1,314 lines (1,081 loc) 138 kB
import '@polymer/iron-icons/iron-icons.js'; import '@polymer/iron-icons/av-icons' import '@polymer/paper-icon-button/paper-icon-button.js'; import { Application } from '../Application'; import { CreateVideoRequest, DeleteVideoRequest, GetFileAudiosRequest, GetFileTitlesRequest, GetTitleByIdRequest, GetTitleFilesRequest, SearchTitlesRequest } from 'globular-web-client/title/title_pb'; import { generatePeerToken, getUrl, Model } from '../Model'; import * as getUuid from 'uuid-by-string' import { BlogPostInfo, InformationsManager, searchEpisodes } from './Informations'; import { playVideo, playVideos } from './Video'; import { ApplicationView } from '../ApplicationView'; import * as getUuidByString from 'uuid-by-string'; import { SearchBlogPostsRequest } from 'globular-web-client/blog/blog_pb'; import { SearchDocumentsRequest } from 'globular-web-client/search/search_pb'; import * as JwtDecode from 'jwt-decode'; import { base64toBlob, formatBoolean, getCoords, randomUUID } from './utility'; import { playAudio, playAudios } from './Audio'; import { setAudio, setVideo } from './Playlist'; // Maximum number of results displayed... var MAX_DISPLAY_RESULTS = 20; // the maximum results display per page per globule... var MAX_RESULTS = 5000; // The total number of result search... window.onerror = function (msg, url, line) { alert("Message : " + msg); alert("url : " + url); alert("Line number : " + line); } // keep values in memorie to speedup... var titles = {} // Keep in memory to speed up.... var images = {} function toDataURL(url, callback) { if (images[url] != null) { callback(images[url]) return } var xhr = new XMLHttpRequest(); xhr.onload = function () { var reader = new FileReader(); reader.onloadend = function () { images[url] = reader.result callback(reader.result); } reader.readAsDataURL(xhr.response); }; xhr.open('GET', url); xhr.responseType = 'blob'; xhr.send(); } export function getCoverDataUrl(callback, videoId, videoUrl, videoPath, globule) { if (!globule) { globule = Application.globular } generatePeerToken(globule, token => { // set the url for the image. let url = getUrl(globule) // set the api call url += "/get_video_cover_data_url" // Set url query parameter. url += "?domain=" + Model.domain url += "&application=" + Model.application url += "&token=" + token url += "&id=" + videoId url += "&url=" + videoUrl url += "&path=" + videoPath if (images[url] != null) { callback(images[url]) return } var xhr = new XMLHttpRequest(); xhr.timeout = 1500 xhr.open('GET', url, true); xhr.setRequestHeader("token", token); xhr.setRequestHeader("application", Model.application); xhr.setRequestHeader("domain", Model.domain); // Set responseType to 'arraybuffer', we want raw binary data buffer xhr.responseType = 'text'; xhr.onload = (rsp) => { if (rsp.currentTarget.status == 200) { images[url] = rsp.currentTarget.response callback(rsp.currentTarget.response) } else { console.log("fail to create thumbnail ", videoId, videoUrl, videoPath) } }; xhr.send(); }) } // That function will be use to asscociate file with imdb information. export function getImdbInfo(id, callback, errorcallback, globule) { generatePeerToken(globule, token => { if (titles[id]) { if (titles[id].ID) { callback(titles[id]) } else { titles[id].callbacks.push(callback) } return } titles[id] = {} titles[id].callbacks = [] titles[id].callbacks.push(callback) let url = getUrl(globule) url += "/imdb_title?id=" + id url += "&token=" + token; var xmlhttp = new XMLHttpRequest(); xmlhttp.timeout = 10 * 1000 xmlhttp.onreadystatechange = function () { if (this.readyState == 4 && (this.status == 201 || this.status == 200)) { var obj = JSON.parse(this.responseText); while (titles[obj.ID].callbacks.length > 0) { let callback = titles[obj.ID].callbacks.pop() callback(obj) } titles[obj.ID] = obj // Now I will } else if (this.readyState == 4) { errorcallback("fail to get info from query " + url + " status " + this.status) } }; xmlhttp.open("GET", url, true); xmlhttp.setRequestHeader("domain", globule.domain); xmlhttp.send(); }) } function playTitleListener(player, title, indexPath) { if (!title) { return } searchEpisodes(title.getSerie(), indexPath, (episodes) => { let globule = title.globule let index = -1; episodes.forEach((e, i) => { if (e.getId() == title.getId()) { index = i; } }); index += 1 let nextEpisode = episodes[index] let video = document.getElementsByTagName('video')[0]; video.onended = () => { // exit full screen... if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.msExitFullscreen) { document.msExitFullscreen(); } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } document.getElementsByTagName('globular-video-player')[0].close(); if (localStorage.getItem(title.getId())) { localStorage.removeItem(title.getId()) } if (index == episodes.length) { return } // So here I will ask to display the next episode... let toast = ApplicationView.displayMessage(` <style> #play-next-dialog{ display: flex; flex-direction: column; } </style> <div id="play-next-dialog"> <div>Play the next episode?</div> <h3 style="font-size: 1.17em; font-weight: bold;">${nextEpisode.getName()}</h3> <div>Season ${nextEpisode.getSeason()} Episode ${nextEpisode.getEpisode()}</div> <img style="max-width: 250px; align-self: center;" src="${nextEpisode.getPoster().getContenturl()}"></img> <p style="max-width: 400px;">${nextEpisode.getDescription()}</p> <div style="display: flex; justify-content: flex-end;"> <paper-button id="imdb-lnk-ok-button">Play</paper-button> <paper-button id="imdb-lnk-cancel-button">Close</paper-button> </div> </div> `) toast.el.style.backgroundColor = "var(--palette-background-default)" toast.el.style.color = "var(--palette-text-primary)" let cancelBtn = toast.el.querySelector("#imdb-lnk-cancel-button") cancelBtn.onclick = () => { toast.dismiss(); } let okBtn = toast.el.querySelector("#imdb-lnk-ok-button") okBtn.onclick = () => { let rqst = new GetTitleFilesRequest rqst.setTitleid(nextEpisode.getId()) rqst.setIndexpath(indexPath) generatePeerToken(globule, token => { globule.titleService.getTitleFiles(rqst, { application: Application.application, domain: Application.domain, token: token }) .then(rsp => { if (rsp.getFilepathsList().length > 0) { let path = rsp.getFilepathsList().pop() playVideo(path, (player, nextEpisode) => { playTitleListener(player, nextEpisode, indexPath, globule) }, null, null, globule) } }) toast.dismiss(); }) } }; }) if (!player.media) { return } var type = player.media.tagName.toLowerCase(), toggle = document.querySelector("[data-plyr='fullscreen']"); if (type === "video" && toggle) { toggle.addEventListener("click", player.toggleFullscreen, false); } toggle.click() } // Search over multiple peers... function search(query, contexts_, offset) { // Connections can contain many time the same address.... let globules = Model.getGlobules() if (offset == undefined) { offset = 0; } let index = 0 let search_in_globule = (globules) => { let contexts = [...contexts_]; if (index < globules.length) { let g = globules[index] index++ // Search recursively... let search_in_context_ = (contexts) => { let context = contexts.pop() if (context) { let indexPath = g.config.DataPath + "/search/" + context if (context == "blogPosts") { if (contexts.length == 0) { searchBlogPosts(g, query, contexts_, indexPath, offset, MAX_RESULTS, () => { search_in_globule(globules) }) } else { searchBlogPosts(g, query, contexts_, indexPath, offset, MAX_RESULTS, () => { search_in_context_(contexts) }) } } else if (context == "webPages") { if (contexts.length == 0) { searchWebpageContent(g, query, contexts_, offset, MAX_RESULTS, () => { search_in_globule(globules) }) } else { searchWebpageContent(g, query, contexts_, offset, MAX_RESULTS, () => { search_in_context_(contexts) }) } } else { if (contexts.length == 0) { searchTitles(g, query, contexts_, indexPath, offset, MAX_RESULTS, () => { search_in_globule(globules) }) } else { searchTitles(g, query, contexts_, indexPath, offset, MAX_RESULTS, () => { search_in_context_(contexts) }) } } } } // search contexts search_in_context_(contexts) } } // start the search search_in_globule(globules) } /** * Search titles... */ function searchTitles(globule, query, contexts, indexPath, offset, max, callback, fields) { // This is a simple test... let rqst = new SearchTitlesRequest rqst.setIndexpath(indexPath) rqst.setQuery(query) rqst.setOffset(offset) rqst.setSize(max) if (fields) { rqst.setFieldsList(fields) } let hits = [] generatePeerToken(globule, token => { let stream = globule.titleService.searchTitles(rqst, { application: Application.application, domain: Application.domain, token: token }) stream.on("data", (rsp) => { if (rsp.hasSummary() && !fields) { Model.eventHub.publish("_display_search_results_", {}, true) Model.eventHub.publish("__new_search_event__", { query: query, summary: rsp.getSummary(), contexts: contexts, offset: offset }, true) } else if (rsp.hasFacets() && !fields) { let uuid = "_" + getUuid(query) Model.eventHub.publish(`${uuid}_search_facets_event__`, { facets: rsp.getFacets() }, true) } else if (rsp.hasHit()) { let hit = rsp.getHit() hit.globule = globule // keep track where the hit was found... if (hit.hasAudio()) { hit.getAudio().globule = globule } else if (hit.hasTitle()) { hit.getTitle().globule = globule } else if (hit.hasVideo()) { hit.getVideo().globule = globule } else if (hit.hasBlog()) { hit.getBlog().globule = globule } if (!fields) { // display the value in the console... hit.getSnippetsList().forEach(val => { let uuid = "_" + getUuid(query) Model.eventHub.publish(`${uuid}_search_hit_event__`, { hit: hit, context: indexPath.substring(indexPath.lastIndexOf("/") + 1) }, true) }) } else { console.log(globule, hit) // keep it hits.push(hit) } } }); stream.on("status", (status) => { if (status.code != 0) { // In case of error I will return an empty array console.log(status.details) } callback(hits) }); }) } function searchBlogPosts(globule, query, contexts, indexPath, offset, max, callback) { // This is a simple test... let rqst = new SearchBlogPostsRequest rqst.setIndexpath(indexPath) rqst.setQuery(query) rqst.setOffset(offset) rqst.setSize(max) generatePeerToken(globule, token => { let stream = globule.blogService.searchBlogPosts(rqst, { application: Application.application, domain: Application.domain, token: token }) stream.on("data", (rsp) => { if (rsp.hasSummary()) { Model.eventHub.publish("_display_search_results_", {}, true) Model.eventHub.publish("__new_search_event__", { query: query, summary: rsp.getSummary(), contexts: contexts, offset: offset }, true) } else if (rsp.hasFacets()) { let uuid = "_" + getUuid(query) Model.eventHub.publish(`${uuid}_search_facets_event__`, { facets: rsp.getFacets() }, true) } else if (rsp.hasHit()) { let hit = rsp.getHit() hit.globule = globule // keep track where the hit was found... // display the value in the console... hit.getSnippetsList().forEach(val => { let uuid = "_" + getUuid(query) Model.eventHub.publish(`${uuid}_search_hit_event__`, { hit: hit, context: indexPath.substring(indexPath.lastIndexOf("/") + 1) }, true) }) } }); stream.on("status", (status) => { if (status.code != 0) { // In case of error I will return an empty array console.log(status.details) } callback() }); }) } /** * Search document... * @param {*} query */ function searchWebpageContent(globule, query, contexts, offset, max, callback) { // Search over web-content. let rqst = new SearchDocumentsRequest rqst.setPathsList([globule.config.DataPath + "/search/applications/" + Model.application]) rqst.setLanguage("en") rqst.setFieldsList(["Text"]) rqst.setOffset(offset) rqst.setPagesize(max) rqst.setQuery(query) let startTime = performance.now() generatePeerToken(globule, token => { let stream = globule.searchService.searchDocuments(rqst, { token: token, application: Model.application, domain: Model.application }) let results = [] stream.on("data", (rsp) => { results = results.concat(rsp.getResults().getResultsList()) }); stream.on("status", (status) => { if (status.code != 0) { // In case of error I will return an empty array console.log("fail to retreive ", query, status.details) } else { let took = performance.now() - startTime Model.eventHub.publish("__new_search_event__", { query: query, summary: { getTotal: () => { return results.length }, getTook: () => { return took } }, contexts: contexts, offset: offset }, true) Model.eventHub.publish("display_webpage_search_result_" + query, results, true) } callback() }); }) } /** * Search Box */ export class SearchBar extends HTMLElement { // attributes. // Create the applicaiton view. constructor() { super() // Set the shadow dom. this.attachShadow({ mode: 'open' }); this.titlesCheckbox = null this.moviesCheckbox = null this.tvSeriesCheckbox = null this.tvEpisodesCheckbox = null this.videosCheckbox = null this.youtubeCheckbox = null this.adultCheckbox = null } // The connection callback. connectedCallback() { // Innitialisation of the layout. this.shadowRoot.innerHTML = ` <style> input { width: 100%; border: none; margin-right: 11px; background: transparent; color: var(--palette-text-accent); box-sizing: border-box; font-size: 1.2rem; } ::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */ color: var(--palette-text-accent); opacity: 1; /* Firefox */ } iron-icon{ padding-left: 11px; padding-right: 11px; } input:focus{ outline: none; } #context-search-selector{ display: none; flex-direction: column; position: absolute; top: 55px; right: 0px; left: 0px; border-radius: 5px; background-color: var(--palette-background-paper); color:var(--palette-text-primary); min-width: 340px; } #search-bar { min-width: 280px; display: flex; align-items: center; border-radius: 22px; box-sizing: border-box; font-size: 16px; height: var(--searchbox-height); opacity: 1; transition: none; background: transparent; color: var(--palette-text-accent); border: 1px solid var(--palette-divider); position: relative; } @media (max-width: 500px) { #context-search-selector { position: fixed; left: 5px; top: 75px; right: 5px; } } paper-card{ background-color: var(--palette-background-paper); color: var(--palette-text-primary); } paper-checkbox { margin-left: 16px; margin-bottom: 8px; margin-top: 8px; } .context-filter{ display: flex; font-size: .85rem; margin: 0px 18px 5px 18px; border-bottom: 1px solid var(--palette-action-disabled); } .context-filter paper-checkbox { } </style> <div id="search-bar"> <iron-icon icon="search" style="--iron-icon-fill-color: var(--palette-text-accent);" ></iron-icon> <input id='search_input' placeholder="Search"></input> <paper-icon-button id="change-search-context" icon="icons:expand-more" style="--iron-icon-fill-color: var(--palette-text-accent); margin-right: 2px; height: 36px;" ></paper-icon-button> <paper-card id="context-search-selector"> <!--paper-checkbox class="context" checked name="webPages" id="context-search-selector-webpages">Webpages</paper-checkbox--> <paper-checkbox class="context" checked name="blogPosts" id="context-search-selector-blog-posts">Blog Posts</paper-checkbox> <div style="display: flex; flex-direction: column"> <paper-checkbox class="context" checked name="titles" id="context-search-selector-titles">Titles</paper-checkbox> <div class="context-filter"> <paper-checkbox checked name="movies" id="context-search-selector-movies">Movies</paper-checkbox> <paper-checkbox checked name="movies" id="context-search-selector-tv-series">TV-Series</paper-checkbox> <paper-checkbox checked name="movies" id="context-search-selector-tv-episodes">TV-Episodes</paper-checkbox> </div> </div> <div style="display: flex; flex-direction: column"> <paper-checkbox class="context" checked name="videos" id="context-search-selector-videos">Videos</paper-checkbox> <div class="context-filter"> <paper-checkbox checked name="youtube" id="context-search-selector-youtube">Youtube</paper-checkbox> <paper-checkbox name="adult" id="context-search-selector-adult">Adult</paper-checkbox> </div> </div> <paper-checkbox class="context" checked name="audios" id="context-search-selector-audios">Audios</paper-checkbox> </paper-card> </div> ` // give the focus to the input. let searchInput = this.shadowRoot.getElementById("search_input") let div = this.shadowRoot.getElementById("search-bar") let changeSearchContextBtn = this.shadowRoot.getElementById("change-search-context") let contextSearchSelector = this.shadowRoot.getElementById("context-search-selector") this.titlesCheckbox = this.shadowRoot.querySelector("#context-search-selector-titles") this.moviesCheckbox = this.shadowRoot.querySelector("#context-search-selector-movies") this.tvSeriesCheckbox = this.shadowRoot.querySelector("#context-search-selector-tv-series") this.tvEpisodesCheckbox = this.shadowRoot.querySelector("#context-search-selector-tv-episodes") this.titlesCheckbox.onchange = () => { if (this.titlesCheckbox.checked) { this.moviesCheckbox.removeAttribute("disabled") this.tvSeriesCheckbox.removeAttribute("disabled") this.tvEpisodesCheckbox.removeAttribute("disabled") } else { this.moviesCheckbox.setAttribute("disabled", "") this.tvSeriesCheckbox.setAttribute("disabled", "") this.tvEpisodesCheckbox.setAttribute("disabled", "") } } this.videosCheckbox = this.shadowRoot.querySelector("#context-search-selector-videos") this.youtubeCheckbox = this.shadowRoot.querySelector("#context-search-selector-youtube") this.adultCheckbox = this.shadowRoot.querySelector("#context-search-selector-adult") this.videosCheckbox.onchange = () => { if (this.videosCheckbox.checked) { this.youtubeCheckbox.removeAttribute("disabled") this.adultCheckbox.removeAttribute("disabled") } else { this.youtubeCheckbox.setAttribute("disabled", "") this.adultCheckbox.setAttribute("disabled", "") } } searchInput.onblur = () => { div.style.boxShadow = "" } searchInput.onkeydown = (evt) => { if (evt.key == "Enter") { let contexts = [] let checkboxs = this.shadowRoot.querySelectorAll(".context") for (var i = 0; i < checkboxs.length; i++) { let c = checkboxs[i] if (c.checked) { if (!contexts.includes(c.name)) contexts.push(c.name) } } if (contexts.length > 0) { let query = searchInput.value // remove unwanted results... if (!this.adultCheckbox.checked) { query += " -adult" } if (!this.youtubeCheckbox.checked) { query += " -youtube" } if (!this.moviesCheckbox.checked) { query += " -Movie" } if (!this.tvEpisodesCheckbox.checked) { query += " -TVEpisode" } if (!this.tvSeriesCheckbox.checked) { query += " -TVSerie" } search(query, contexts, 0) searchInput.value = "" Model.eventHub.publish("_display_search_results_", {}, true) } else { ApplicationView.displayMessage("You must selected a search context, Blog, Video or Title...", 3000) contextSearchSelector.style.display = "flex" } } else if (evt.key == "Escape") { Model.eventHub.publish("_hide_search_results_", {}, true) } } searchInput.onfocus = (evt) => { evt.stopPropagation(); div.style.boxShadow = "var(--dark-mode-shadow)" contextSearchSelector.style.display = "none" Model.eventHub.publish("_display_search_results_", {}, true) let pages = document.querySelectorAll("globular-search-results-page") for (var i = 0; i < pages.length; i++) { let page = pages[i] if (page.style.display != "none") { page.facetFilter.style.display = "" } } // I will remove all highligted text.. let highlighted = document.getElementsByClassName("highlighted") for (var i = 0; i < highlighted.length; i++) { if (highlighted[i].lowlight) highlighted[i].lowlight(); } } // Change the search context, this will search over other indexations... changeSearchContextBtn.onclick = () => { if (contextSearchSelector.style.display != "flex") { contextSearchSelector.style.display = "flex" } else { contextSearchSelector.style.display = "none" } } } } customElements.define('globular-search-bar', SearchBar) /** * Search Results */ export class SearchResults extends HTMLElement { // attributes. // Create the applicaiton view. constructor() { super() // Set the shadow dom. this.attachShadow({ mode: 'open' }); // Innitialisation of the layout. this.shadowRoot.innerHTML = ` <style> :host{ padding: 10px; } #container{ min-height: calc(100vh - 85px); flex-direction: column; background-color: var(--palette-background-paper); color: var(--palette-text-primary); width: calc(100vw - 20px); } .header { display: flex; width: 100%; } paper-tabs { flex-grow: 1; /* custom CSS property */ --paper-tabs-selection-bar-color: var(--palette-primary-main); color: var(--palette-text-primary); --paper-tab-ink: var(--palette-action-disabled); } #close-all-btn { width: 30px; height: 30px; padding: 3px; } paper-card{ background-color: var(--palette-background-paper); color: var(--palette-text-primary); } paper-tab { display: inlinel; } paper-tab span { font-size: 1.1rem; flex-grow: 1; } </style> <paper-card id="container"> <div class="header"> <paper-tabs id="search-results" scrollable> </paper-tabs> <paper-icon-button id="close-all-btn" icon="icons:close"></paper-icon-button> </div> <h2 id="empty-search-msg" style="text-align: center; color: var(--palette-divider);">No search results to display...</h2> <slot></slot> </paper-card> ` this.tabs = this.shadowRoot.querySelector("#search-results") Model.eventHub.subscribe("_hide_search_results_", uuid => { }, evt => { if (this.parentNode) { this.parentNode.removeChild(this) } }, true) this.closeAllBtn = this.shadowRoot.querySelector("#close-all-btn") this.closeAllBtn.onclick = () => { Model.eventHub.publish("_hide_search_results_", {}, true) // Hide the search results... /*let facetFilters = ApplicationView.layout.sideMenu().getElementsByTagName("globular-facet-search-filter") for (var i = 0; i < facetFilters.length; i++) { let f = facetFilters[i] f.parentNode.removeChild(f) }*/ } // So here I will create a new Search Result page if none exist... Model.eventHub.subscribe("__new_search_event__", uuid => { }, evt => { this.shadowRoot.querySelector("#container").style.display = "flex" let uuid = "_" + getUuid(evt.query) let tab = this.tabs.querySelector(`#${uuid}-tab`) let query = evt.query.replaceAll(" -adult", "").replaceAll(" -youtube", "").replaceAll(" -TVEpisode", "").replaceAll(" -TVSerie", "").replaceAll(" -Movie", "") if (tab == null) { let html = ` <paper-tab id="${uuid}-tab"> <span>${query} (<span id="${uuid}-total-span" style="font-size: 1rem;"></span>)</span> <paper-icon-button id="${uuid}-close-btn" icon="icons:close"></paper-icon-button> </paper-tab> ` let range = document.createRange() this.tabs.appendChild(range.createContextualFragment(html)) tab = this.tabs.querySelector(`#${uuid}-tab`) tab.totalSpan = tab.querySelector(`#${uuid}-total-span`) tab.onclick = () => { let page = this.querySelector(`#${uuid}-results-page`) if (page == undefined) { return } let index = 0 for (var i = 0; i < this.children.length; i++) { this.children[i].style.display = "none"; if (this.children[i].id == `${uuid}-results-page`) { index = i } } this.tabs.selected = index; page.style.display = "" // display the filters... page.facetFilter.style.display = "" } let closeBtn = this.tabs.querySelector(`#${uuid}-close-btn`) closeBtn.onclick = (evt_) => { evt_.stopPropagation() this.deletePageResults(uuid) if (this.children.length == 0) { this.shadowRoot.querySelector("#empty-search-msg").style.display = "block"; } } this.tabs.selected = this.children.length; } else { tab.click() } // Create a new page... let resultsPage = this.querySelector(`#${uuid}-results-page`) if (resultsPage == null) { resultsPage = new SearchResultsPage(uuid, evt.summary, evt.contexts, tab) for (var i = 0; i < this.children.length; i++) { this.children[i].style.display = "none"; } this.appendChild(resultsPage) this.shadowRoot.querySelector("#empty-search-msg").style.display = "none"; // ApplicationView.layout.sideMenu().appendChild(resultsPage.facetFilter) } else if (evt.summary) { tab.totalSpan.innerHTML = resultsPage.getTotal() + "" } }, true) } connectedCallback() { } isEmpty() { return this.tabs.querySelectorAll("paper-tab").length == 0 } deletePageResults(uuid) { var index = 0; for (var i = 0; i < this.children.length; i++) { if (this.children[i].id == `${uuid}-results-page`) { index = i; break } } let page = this.querySelector(`#${uuid}-results-page`) page.parentNode.removeChild(page) // page.facetFilter.parentNode.removeChild(page.facetFilter) //ApplicationView.layout.sideMenu().removeChild(page.facetFilter) let tab = this.tabs.querySelector(`#${uuid}-tab`) tab.parentNode.removeChild(tab) let nextPage = null; if (index > this.children.length - 1) { nextPage = this.children[this.children.length - 1] } else { nextPage = this.children[index] } if (nextPage != null) { let tab_uuid = nextPage.id.replace("-results-page", "-tab") this.tabs.querySelector(`#${tab_uuid}`).click() } // close the results view if (this.tabs.querySelectorAll("paper-tab").length == 0) { Model.eventHub.publish("_hide_search_results_", {}, true) } } } customElements.define('globular-search-results', SearchResults) /** * Search Results */ export class SearchResultsPage extends HTMLElement { // attributes. // Create the applicaiton view. constructor(uuid, summary, contexts, tab) { super() // Set the shadow dom. this.attachShadow({ mode: 'open' }); this.id = `${uuid}-results-page` this.offset = 0; this.count = 0; this.query = summary.getQuery(); this.contexts = contexts; this.tab = tab; // Innitialisation of the layout. this.shadowRoot.innerHTML = ` <style> #container { display: flex; } #summary { display: flex; } #summary span { flex-grow: 1; } .disable{ --iron-icon-fill-color: var(--palette-action-disabled); } #webpage-search-results{ display: flex; flex-direction: column; } #summary span{ font-size: 1rem; } #results{ display: flex; flex-direction: column; overflow-y: auto; overflow-x: hidden; } #results-actions{ display: none; justify-content: flex-end; position: fixed; bottom: 0px; left: 0px; right: 0px; margin: 10px; align-items: center; } #results-actions-btns{ display:flex; background: var(--palette-background-default); border-radius: 20px; align-items: center; border: 1px solid var(--palette-divider); margin-right: 5px; } ::-webkit-scrollbar { width: 5px; height: 5px; } ::-webkit-scrollbar-track { background: var(--palette-background-default); } ::-webkit-scrollbar-thumb { background: var(--palette-divider); } .header{ align-items: center; } #facets{ margin-right: 5px; min-width: 225xp; } #content{ margin-left: 270px; } @media (max-width: 600px) { #content{ margin-left: 0px; } #container { flex-direction: column; } #facets{ max-height: 200px; } .header{ flex-direction: column; align-items: flex-start; } #summary{ align-self: flex-end; } globular-search-results-page-contexts-selector { font-size: 1rem; } #results{ padding-bottom: 100px; } @media (max-width: 650px) { slot { justify-content: center; } } } </style> <div id="container"> <div id="facets"> <slot name="facets"></slot> </div> <div id="content" style="display: flex; flex-direction: column; width: 100%;"> <div class="header" style="display: flex;"> <div style="display: flex; flex-wrap: wrap; flex-grow: 1; align-items: center;"> <globular-search-results-page-contexts-selector ></globular-search-results-page-contexts-selector> <globular-search-results-pages-navigator></globular-search-results-pages-navigator> </div> <div id="summary"> <paper-icon-button id="search-result-icon-view-btn" style="" icon="icons:view-module"></paper-icon-button> <paper-icon-button class="disable" id="search-result-lst-view-btn" icon="icons:view-list"></paper-icon-button> </div> </div> <div id="results" style=""> <div id="mosaic-view" style="display: block;"> <slot name="mosaic_blogPosts" style="display: flex; flex-wrap: wrap;"></slot> <slot name="mosaic_videos" style="display: flex; flex-wrap: wrap;"></slot> <slot name="mosaic_titles" style="display: flex; flex-wrap: wrap;"></slot> <slot name="mosaic_audios" style="display: flex; flex-wrap: wrap;"></slot> </div> <div id="list-view" style="display: none;"> <slot name="list_blogPosts" style="display: flex; flex-wrap: wrap;"> </slot> <slot name="list_videos" style="display: flex; flex-wrap: wrap;"> </slot> <slot name="list_titles" style="display: flex; flex-wrap: wrap;"> </slot> <slot name="list_audios" style="display: flex; flex-wrap: wrap;"> </slot> </div> <div id="results-actions"> <div id="results-actions-btns" style=""> <paper-icon-button id="previous-results-btn" icon="icons:chevron-left" style="visibility: hidden;"></paper-icon-button> <span id="results-index"></span> <paper-icon-button id="next-results-btn" icon="icons:chevron-right"></paper-icon-button> </div> </div> </div> </div> </div> ` // the next and previous results buttons. this.nextResultsBtn = this.shadowRoot.querySelector("#next-results-btn") this.previousResultsBtn = this.shadowRoot.querySelector("#previous-results-btn") this.currentPageIndex = this.shadowRoot.querySelector("#results-index") this.currentActionsBtns = this.shadowRoot.querySelector("#results-actions-btns") // display hint about more results can be displayed. let resultsDiv = this.shadowRoot.querySelector("#results") resultsDiv.onscroll = () => { if (resultsDiv.scrollTop == 0) { resultsDiv.style.boxShadow = "" resultsDiv.style.borderTop = "" } else { resultsDiv.style.boxShadow = "inset 0px 5px 6px -3px rgb(0 0 0 / 40%)" resultsDiv.style.borderTop = "1px solid var(--palette-divider)" } } this.navigator = this.shadowRoot.querySelector("globular-search-results-pages-navigator") this.navigator.setSearchResultsPage(this) // connect the previous and next page buttons this.nextResultsBtn.onclick = () => { this.navigator.setIndex(this.offset + 1) } this.previousResultsBtn.onclick = () => { this.navigator.setIndex(this.offset - 1) } this.contextsSelector = this.shadowRoot.querySelector("globular-search-results-page-contexts-selector") this.contextsSelector.setSearchResultsPage(this) this.contextsSelector.setContexts(contexts) // left or right side filter... this.facetFilter = new FacetSearchFilter(this) this.facetFilter.slot = "facets" this.appendChild(this.facetFilter) // Get the tow button... this.searchReusltLstViewBtn = this.shadowRoot.querySelector("#search-result-lst-view-btn") this.searchReusltIconViewBtn = this.shadowRoot.querySelector("#search-result-icon-view-btn") this.viewType = "icon" this.hits = {} // keep the current list in memory... this.hits_by_context = {} this.hits_by_className = {} this.searchReusltLstViewBtn.onclick = () => { this.searchReusltLstViewBtn.classList.remove("disable") this.searchReusltIconViewBtn.classList.add("disable") this.viewType = "lst" this.shadowRoot.querySelector("#list-view").style.display = "block" this.shadowRoot.querySelector("#mosaic-view").style.display = "none" } this.searchReusltIconViewBtn.onclick = () => { this.searchReusltLstViewBtn.classList.add("disable") this.searchReusltIconViewBtn.classList.remove("disable") this.viewType = "mosaic" this.shadowRoot.querySelector("#list-view").style.display = "none" this.shadowRoot.querySelector("#mosaic-view").style.display = "block" } // Display facets Model.eventHub.subscribe(`${uuid}_search_facets_event__`, listner_uuid => { }, evt => { this.facetFilter.setFacets(evt.facets) }, true) // Append it to the results. Model.eventHub.subscribe(`${uuid}_search_hit_event__`, listner_uuid => { }, evt => { Model.eventHub.publish("_display_search_results_", {}, true) if (this.hits_by_context[evt.context] == null) { this.hits_by_context[evt.context] = [] } // get the uuid from the hit content object. let getHitUuid = (hit) => { if (hit.hasTitle || hit.hasVideo || hit.hasAudio) { if (hit.hasTitle()) { return getUuidByString(hit.getTitle().getName()) } else if (hit.hasVideo()) { return getUuidByString(hit.getVideo().getId()) } else if (hit.hasAudio()) { return getUuidByString(hit.getAudio().getId()) } } else { return hit.getBlog().getUuid() } } let hit = evt.hit let uuid = getHitUuid(hit) if (this.hits[uuid] == undefined) { this.hits[uuid] = hit this.hits_by_context[evt.context].push(hit) hit.hidden = false; hit.enable = true; if (hit.hasTitle || hit.hasVideo || hit.hasAudio) { // here I will keep track of hit classes... if (hit.hasTitle()) { hit.getTitle().getGenresList().forEach(g => { let className = getUuidByString(g.toLowerCase()) if (this.hits_by_className[className] == undefined) { this.hits_by_className[className] = [] } if (this.hits_by_className[className].indexOf(uuid) == -1) { this.hits_by_className[className].push(uuid) } }) // now the term.. let className = getUuidByString("high") if (hit.getTitle().getRating() < 3.5) { className = getUuidByString("low") } else if (hit.getTitle().getRating() < 7.0) { className = getUuidByString("medium") } if (this.hits_by_className[className] == undefined) { this.hits_by_className[className] = [] } if (this.hits_by_className[className].indexOf(uuid) == -1) { this.hits_by_className[className].push(uuid) } } else if (hit.hasVideo()) { hit.getVideo().getGenresList().forEach(g => { let className = getUuidByString(g.toLowerCase()) if (this.hits_by_className[className] == undefined) { this.hits_by_className[className] = [] } if (this.hits_by_className[className].indexOf(uuid) == -1) { this.hits_by_className[className].push(uuid) } }) hit.getVideo().getTagsList().forEach(g => { let className = getUuidByString(g.toLowerCase()) if (this.hits_by_className[className] == undefined) { this.hits_by_className[className] = [] } if (this.hits_by_className[className].indexOf(uuid) == -1) { this.hits_by_className[className].push(uuid) } })