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
JavaScript
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)
}
})