diff --git a/nx/public/plugins/tags.html b/nx/public/plugins/tags.html new file mode 100644 index 00000000..a61521e9 --- /dev/null +++ b/nx/public/plugins/tags.html @@ -0,0 +1,20 @@ + + + + DA App SDK Sample + + + + + + + + + +
+ + \ No newline at end of file diff --git a/nx/public/plugins/tags/tag-browser.css b/nx/public/plugins/tags/tag-browser.css new file mode 100644 index 00000000..55f93713 --- /dev/null +++ b/nx/public/plugins/tags/tag-browser.css @@ -0,0 +1,112 @@ +:host { + font-family: 'Adobe Clean', adobe-clean, sans-serif; + } + + .tag-browser { + display: block; + position: relative; + overflow: hidden; + width: 100%; + height: 100vh; + } + + .tag-browser .search-details { + height: 36px; + display: flex; + align-items: center; + justify-content: space-between; + line-height: 36px; + font-size: 16px; + padding: 0 12px; + } + + .tag-browser .tag-search input[type="text"] { + flex: 1; + height: 32px; + padding: 0 8px; + font-size: 16px; + font-family: 'Adobe Clean', adobe-clean, sans-serif; + border: 1px solid #d1d1d1; + border-radius: 2px; + box-sizing: border-box; + } + + .tag-browser .tag-groups { + display: flex; + overflow: auto; + position: absolute; + width: 100%; + height: calc(100% - 36px); + } + + .tag-browser ul { + margin: 0; + padding: 0; + list-style: none; + } + + .tag-browser .tag-group-column { + flex-shrink: 0; + width: 100%; + height: 100%; + overflow: auto; + max-width: 280px; + padding: 0 12px; + box-sizing: border-box; + } + + .tag-browser .tag-group-column:first-child { + padding: 0 12px; + } + + .tag-browser .tag-group .tag-details { + cursor: pointer; + display: flex; + justify-content: space-between; + line-height: 36px; + } + + .tag-browser .tag-group .tag-title { + flex: 1; + padding: 0 6px; + font-size: 16px; + font-family: 'Adobe Clean', adobe-clean, sans-serif; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + background: none; + border: none; + text-align: left; + cursor: pointer; + } + + .tag-browser .tag-search button, + .tag-browser .tag-details button { + cursor: pointer; + height: 32px; + width: 32px; + margin: 2px; + padding: 0; + border: none; + display: block; + border-radius: 2px; + background-color: #efefef; + } + + .tag-browser .tag-group .tag-title:hover { + background-color: #efefef; + } + + .tag-browser .tag-search button:hover, + .tag-browser .tag-group button:hover, + .tag-browser .tag-group .tag-title.active { + background-color: #e9e9e9; + } + + .tag-browser .tag-search::after, + .tag-browser .tag-group::after { + content: ""; + display: block; + height: 1px; + background: #d1d1d1; + } \ No newline at end of file diff --git a/nx/public/plugins/tags/tag-browser.js b/nx/public/plugins/tags/tag-browser.js new file mode 100644 index 00000000..6a6de953 --- /dev/null +++ b/nx/public/plugins/tags/tag-browser.js @@ -0,0 +1,164 @@ +/* eslint-disable no-underscore-dangle, import/no-unresolved */ +import { LitElement, html, nothing } from 'https://da.live/nx/deps/lit/lit-core.min.js'; +import getStyle from 'https://da.live/nx/utils/styles.js'; + +const style = await getStyle(import.meta.url); + +class DaTagBrowser extends LitElement { + static properties = { + rootTags: { type: Array }, + actions: { type: Object }, + getTags: { type: Function }, + tagValue: { type: String }, + _tags: { state: true }, + _activeTag: { state: true }, + _searchQuery: { state: true }, + _secondaryTags: { state: true }, + }; + + constructor() { + super(); + this._tags = []; + this._activeTag = {}; + this._searchQuery = ''; + this._secondaryTags = false; + } + + getTagSegments() { + return (this._activeTag.activeTag ? this._activeTag.activeTag.split('/') : []).concat(this._activeTag.name); + } + + getTagValue() { + if (this.tagValue === 'title') return this._activeTag.title; + const tagSegments = this.getTagSegments(); + return tagSegments.join(tagSegments.length > 2 ? '/' : ':').replace('/', ':'); + } + + handleBlur() { + this._secondaryTags = false; + } + + connectedCallback() { + super.connectedCallback(); + this.shadowRoot.adoptedStyleSheets = [style]; + this.addEventListener('blur', this.handleBlur, true); + } + + disconnectedCallback() { + this.removeEventListener('blur', this.handleBlur, true); + super.disconnectedCallback(); + } + + updated(changedProperties) { + if (changedProperties.has('rootTags')) { + this._tags = [this.rootTags]; + this._activeTag = {}; + } + + if (changedProperties.has('_tags')) { + setTimeout(() => { + const groups = this.renderRoot.querySelector('.tag-groups'); + if (!groups) return; + const firstTag = groups.lastElementChild?.querySelector('.tag-title'); + firstTag?.focus(); + groups.scrollTo({ left: groups.scrollWidth, behavior: 'smooth' }); + }, 100); + } + } + + async handleTagClick(tag, idx) { + this._activeTag = tag; + if (!this.getTags) return; + const newTags = await this.getTags(tag); + if (!newTags || newTags.length === 0) return; + this._tags = [...this._tags.toSpliced(idx + 1), newTags]; + } + + handleTagInsert(tag) { + this._activeTag = tag; + const tagValue = this._secondaryTags ? `, ${this.getTagValue()}` : this.getTagValue(); + this.actions.sendText(tagValue); + this._secondaryTags = true; + } + + handleBackClick() { + if (this._tags.length === 0) return; + this._tags = this._tags.slice(0, -1); + this._activeTag = this._tags[this._tags.length - 1] + .find((tag) => this._activeTag.activeTag.includes(tag.name)) || {}; + } + + handleSearchInput(event) { + this._searchQuery = event.target.value.toLowerCase(); + } + + filterTags(tags) { + if (!this._searchQuery) return tags; + return tags.filter((tag) => tag.title.toLowerCase().includes(this._searchQuery)); + } + + renderSearchBar() { + return html` + + `; + } + + renderTag(tag, idx) { + const active = this.getTagSegments()[idx] === tag.name; + return html` +
  • +
    + + +
    +
  • + `; + } + + renderTagGroup(group, idx) { + const filteredGroup = this.filterTags(group); + return html` + + `; + } + + render() { + if (this._tags.length === 0) return nothing; + return html` +
    + ${this.renderSearchBar()} + +
    + `; + } +} + +customElements.define('da-tag-browser', DaTagBrowser); diff --git a/nx/public/plugins/tags/tags.js b/nx/public/plugins/tags/tags.js new file mode 100644 index 00000000..97dccf2a --- /dev/null +++ b/nx/public/plugins/tags/tags.js @@ -0,0 +1,130 @@ +/* eslint-disable import/no-unresolved */ +import DA_SDK from 'https://da.live/nx/utils/sdk.js'; +import { DA_ORIGIN } from 'https://da.live/nx/public/utils/constants.js'; +import './tag-browser.js'; + +const ROOT_TAG_PATH = '/content/cq:tags'; +const UI_TAG_PATH = '/ui#/aem/aem/tags'; +const TAG_EXT = '.1.json'; + +async function getAemRepo(project, opts) { + const configUrl = `${DA_ORIGIN}/config/${project.org}/${project.repo}`; + const resp = await fetch(configUrl, opts); + if (!resp.ok) return null; + const json = await resp.json(); + + // Get all config values that might affect behavior + const configs = json.data.data.reduce((acc, entry) => { + acc[entry.key] = entry.value; + return acc; + }, {}); + + // If configs aren't set at all, use defaults + const requiresAuth = configs['aem.tags.requiresAuth'] === undefined ? true : configs['aem.tags.requiresAuth'] !== 'false'; + const tagExt = configs['aem.tags.extension'] || TAG_EXT; + + return { + aemRepo: configs['aem.repositoryId'], + namespaces: configs['aem.tags.namespaces'], + requiresAuth, + tagExt, + }; +} + +async function getTags(path, opts, aemConfig) { + const activeTag = path.split('cq:tags').pop().replace(aemConfig.tagExt, '').slice(1); + const resp = await fetch(path, opts); + if (!resp.ok) return null; + const json = await resp.json(); + const tags = Object.keys(json).reduce((acc, key) => { + if (json[key]['jcr:primaryType'] === 'cq:Tag') { + acc.push({ + path: `${path.replace(aemConfig.tagExt, '')}/${key}${aemConfig.tagExt}`, + activeTag, + name: key, + title: json[key]['jcr:title'] || key, + details: json[key], + }); + } + return acc; + }, []); + + return tags; +} + +const getRootTags = async (namespaces, aemConfig, opts) => { + const createTagUrl = (namespace = '') => `https://${aemConfig.aemRepo}${ROOT_TAG_PATH}${namespace ? `/${namespace}` : ''}${aemConfig.tagExt}`; + + if (namespaces.length === 0) { + return getTags(createTagUrl(), opts, aemConfig).catch(() => null); + } + + if (namespaces.length === 1) { + const namespace = namespaces[0].toLowerCase().replaceAll(' ', '-'); + return getTags(createTagUrl(namespace), opts, aemConfig).catch(() => null); + } + + return namespaces.map((title) => { + const namespace = title.toLowerCase().replaceAll(' ', '-'); + return { + path: createTagUrl(namespace), + name: namespace, + title, + activeTag: '', + details: {}, + }; + }); +}; + +function showError(message, link = null) { + const mainElement = document.body.querySelector('main'); + const errorMessage = document.createElement('p'); + errorMessage.textContent = message; + + if (link) { + const linkEl = document.createElement('a'); + linkEl.textContent = 'View Here'; + linkEl.href = link; + linkEl.target = '_blank'; + errorMessage.append(linkEl); + } + + const reloadButton = document.createElement('button'); + reloadButton.textContent = 'Reload'; + reloadButton.addEventListener('click', () => window.location.reload()); + + mainElement.append(errorMessage, reloadButton); +} + +(async function init() { + const { context, actions, token } = await DA_SDK.catch(() => null); + if (!context || !actions || !token) { + showError('Please log in to view tags.'); + return; + } + + const opts = { headers: { Authorization: `Bearer ${token}` } }; + const aemConfig = await getAemRepo(context, opts).catch(() => null); + if (!aemConfig || !aemConfig.aemRepo) { + showError('Failed to retrieve config. ', `https://da.live/config#/${context.org}/${context.repo}/`); + return; + } + + // Only use auth for tags if requiresAuth is true + const tagOpts = aemConfig.requiresAuth ? opts : {}; + const namespaces = aemConfig?.namespaces.split(',').map((namespace) => namespace.trim()) || []; + const rootTags = await getRootTags(namespaces, aemConfig, tagOpts); + + if (!rootTags || rootTags.length === 0) { + showError('Could not load tags. ', `https://${aemConfig.aemRepo}${UI_TAG_PATH}`); + return; + } + + const daTagBrowser = document.createElement('da-tag-browser'); + daTagBrowser.tabIndex = 0; + daTagBrowser.rootTags = rootTags; + daTagBrowser.getTags = async (tag) => getTags(tag.path, tagOpts, aemConfig); + daTagBrowser.tagValue = aemConfig.namespaces ? 'title' : 'path'; + daTagBrowser.actions = actions; + document.body.querySelector('main').append(daTagBrowser); +}());