UNPKG

@zywave/zywave-api-toolkit-bundle

Version:

1,132 lines (1,120 loc) 86.5 kB
import { d as desc, _ as _$LH, n as noChange, a as nothing, c as css, p as property, q as query, Z as ZywaveBaseElement, h as html, i as ifDefined } from './internals/_api-proxy-f3bf2df5.js'; import { A as AnalyticsTracker } from './internals/_analytics-a9e9065c.js'; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ // Note, in the future, we may extend this decorator to support the use case // where the queried element may need to do work to become ready to interact // with (e.g. load some implementation code). If so, we might elect to // add a second argument defining a function that can be run to make the // queried element loaded/updated/ready. /** * A property decorator that converts a class property into a getter that * returns a promise that resolves to the result of a querySelector on the * element's renderRoot done after the element's `updateComplete` promise * resolves. When the queried property may change with element state, this * decorator can be used instead of requiring users to await the * `updateComplete` before accessing the property. * * @param selector A DOMString containing one or more selectors to match. * * See: https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector * * ```ts * class MyElement { * @queryAsync('#first') * first: Promise<HTMLDivElement>; * * render() { * return html` * <div id="first"></div> * <div id="second"></div> * `; * } * } * * // external usage * async doSomethingWithFirst() { * (await aMyElement.first).doSomething(); * } * ``` * @category Decorator */ function queryAsync(selector) { return (obj, name) => { return desc(obj, name, { async get() { await this.updateComplete; return this.renderRoot?.querySelector(selector) ?? null; } }); }; } /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ const PartType = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 }; /** * Creates a user-facing directive function from a Directive class. This * function has the same parameters as the directive's render() method. */ const directive = c => (...values) => ({ // This property needs to remain unminified. ['_$litDirective$']: c, values }); /** * Base class for creating custom directives. Users should extend this class, * implement `render` and/or `update`, and then pass their subclass to * `directive`. */ class Directive { constructor(_partInfo) {} // See comment in Disconnectable interface for why this is a getter get _$isConnected() { return this._$parent._$isConnected; } /** @internal */ _$initialize(part, parent, attributeIndex) { this.__part = part; this._$parent = parent; this.__attributeIndex = attributeIndex; } /** @internal */ _$resolve(part, props) { return this.update(part, props); } update(_part, props) { return this.render(...props); } } /** * @license * Copyright 2020 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ const { _ChildPart: ChildPart } = _$LH; const wrap = window.ShadyDOM?.inUse && window.ShadyDOM?.noPatch === true ? window.ShadyDOM.wrap : node => node; const createMarker = () => document.createComment(''); /** * Inserts a ChildPart into the given container ChildPart's DOM, either at the * end of the container ChildPart, or before the optional `refPart`. * * This does not add the part to the containerPart's committed value. That must * be done by callers. * * @param containerPart Part within which to add the new ChildPart * @param refPart Part before which to add the new ChildPart; when omitted the * part added to the end of the `containerPart` * @param part Part to insert, or undefined to create a new part */ const insertPart = (containerPart, refPart, part) => { const container = wrap(containerPart._$startNode).parentNode; const refNode = refPart === undefined ? containerPart._$endNode : refPart._$startNode; if (part === undefined) { const startNode = wrap(container).insertBefore(createMarker(), refNode); const endNode = wrap(container).insertBefore(createMarker(), refNode); part = new ChildPart(startNode, endNode, containerPart, containerPart.options); } else { const endNode = wrap(part._$endNode).nextSibling; const oldParent = part._$parent; const parentChanged = oldParent !== containerPart; if (parentChanged) { part._$reparentDisconnectables?.(containerPart); // Note that although `_$reparentDisconnectables` updates the part's // `_$parent` reference after unlinking from its current parent, that // method only exists if Disconnectables are present, so we need to // unconditionally set it here part._$parent = containerPart; // Since the _$isConnected getter is somewhat costly, only // read it once we know the subtree has directives that need // to be notified let newConnectionState; if (part._$notifyConnectionChanged !== undefined && (newConnectionState = containerPart._$isConnected) !== oldParent._$isConnected) { part._$notifyConnectionChanged(newConnectionState); } } if (endNode !== refNode || parentChanged) { let start = part._$startNode; while (start !== endNode) { const n = wrap(start).nextSibling; wrap(container).insertBefore(start, refNode); start = n; } } } return part; }; /** * Sets the value of a Part. * * Note that this should only be used to set/update the value of user-created * parts (i.e. those created using `insertPart`); it should not be used * by directives to set the value of the directive's container part. Directives * should return a value from `update`/`render` to update their part state. * * For directives that require setting their part value asynchronously, they * should extend `AsyncDirective` and call `this.setValue()`. * * @param part Part to set * @param value Value to set * @param index For `AttributePart`s, the index to set * @param directiveParent Used internally; should not be set by user */ const setChildPartValue = (part, value, directiveParent = part) => { part._$setValue(value, directiveParent); return part; }; // A sentinel value that can never appear as a part value except when set by // live(). Used to force a dirty-check to fail and cause a re-render. const RESET_VALUE = {}; /** * Sets the committed value of a ChildPart directly without triggering the * commit stage of the part. * * This is useful in cases where a directive needs to update the part such * that the next update detects a value change or not. When value is omitted, * the next update will be guaranteed to be detected as a change. * * @param part * @param value */ const setCommittedValue = (part, value = RESET_VALUE) => part._$committedValue = value; /** * Returns the committed value of a ChildPart. * * The committed value is used for change detection and efficient updates of * the part. It can differ from the value set by the template or directive in * cases where the template value is transformed before being committed. * * - `TemplateResult`s are committed as a `TemplateInstance` * - Iterables are committed as `Array<ChildPart>` * - All other types are committed as the template value or value returned or * set by a directive. * * @param part */ const getCommittedValue = part => part._$committedValue; /** * Removes a ChildPart from the DOM, including any of its content and markers. * * Note: The only difference between this and clearPart() is that this also * removes the part's start node. This means that the ChildPart must own its * start node, ie it must be a marker node specifically for this part and not an * anchor from surrounding content. * * @param part The Part to remove */ const removePart = part => { part._$clear(); part._$startNode.remove(); }; /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ // Helper for generating a map of array item to its index over a subset // of an array (used to lazily generate `newKeyToIndexMap` and // `oldKeyToIndexMap`) const generateMap = (list, start, end) => { const map = new Map(); for (let i = start; i <= end; i++) { map.set(list[i], i); } return map; }; class RepeatDirective extends Directive { constructor(partInfo) { super(partInfo); if (partInfo.type !== PartType.CHILD) { throw new Error('repeat() can only be used in text expressions'); } } _getValuesAndKeys(items, keyFnOrTemplate, template) { let keyFn; if (template === undefined) { template = keyFnOrTemplate; } else if (keyFnOrTemplate !== undefined) { keyFn = keyFnOrTemplate; } const keys = []; const values = []; let index = 0; for (const item of items) { keys[index] = keyFn ? keyFn(item, index) : index; values[index] = template(item, index); index++; } return { values, keys }; } render(items, keyFnOrTemplate, template) { return this._getValuesAndKeys(items, keyFnOrTemplate, template).values; } update(containerPart, [items, keyFnOrTemplate, template]) { // Old part & key lists are retrieved from the last update (which may // be primed by hydration) const oldParts = getCommittedValue(containerPart); const { values: newValues, keys: newKeys } = this._getValuesAndKeys(items, keyFnOrTemplate, template); // We check that oldParts, the committed value, is an Array as an // indicator that the previous value came from a repeat() call. If // oldParts is not an Array then this is the first render and we return // an array for lit-html's array handling to render, and remember the // keys. if (!Array.isArray(oldParts)) { this._itemKeys = newKeys; return newValues; } // In SSR hydration it's possible for oldParts to be an array but for us // to not have item keys because the update() hasn't run yet. We set the // keys to an empty array. This will cause all oldKey/newKey comparisons // to fail and execution to fall to the last nested brach below which // reuses the oldPart. const oldKeys = this._itemKeys ??= []; // New part list will be built up as we go (either reused from // old parts or created for new keys in this update). This is // saved in the above cache at the end of the update. const newParts = []; // Maps from key to index for current and previous update; these // are generated lazily only when needed as a performance // optimization, since they are only required for multiple // non-contiguous changes in the list, which are less common. let newKeyToIndexMap; let oldKeyToIndexMap; // Head and tail pointers to old parts and new values let oldHead = 0; let oldTail = oldParts.length - 1; let newHead = 0; let newTail = newValues.length - 1; // Overview of O(n) reconciliation algorithm (general approach // based on ideas found in ivi, vue, snabbdom, etc.): // // * We start with the list of old parts and new values (and // arrays of their respective keys), head/tail pointers into // each, and we build up the new list of parts by updating // (and when needed, moving) old parts or creating new ones. // The initial scenario might look like this (for brevity of // the diagrams, the numbers in the array reflect keys // associated with the old parts or new values, although keys // and parts/values are actually stored in parallel arrays // indexed using the same head/tail pointers): // // oldHead v v oldTail // oldKeys: [0, 1, 2, 3, 4, 5, 6] // newParts: [ , , , , , , ] // newKeys: [0, 2, 1, 4, 3, 7, 6] <- reflects the user's new // item order // newHead ^ ^ newTail // // * Iterate old & new lists from both sides, updating, // swapping, or removing parts at the head/tail locations // until neither head nor tail can move. // // * Example below: keys at head pointers match, so update old // part 0 in-place (no need to move it) and record part 0 in // the `newParts` list. The last thing we do is advance the // `oldHead` and `newHead` pointers (will be reflected in the // next diagram). // // oldHead v v oldTail // oldKeys: [0, 1, 2, 3, 4, 5, 6] // newParts: [0, , , , , , ] <- heads matched: update 0 // newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldHead // & newHead // newHead ^ ^ newTail // // * Example below: head pointers don't match, but tail // pointers do, so update part 6 in place (no need to move // it), and record part 6 in the `newParts` list. Last, // advance the `oldTail` and `oldHead` pointers. // // oldHead v v oldTail // oldKeys: [0, 1, 2, 3, 4, 5, 6] // newParts: [0, , , , , , 6] <- tails matched: update 6 // newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldTail // & newTail // newHead ^ ^ newTail // // * If neither head nor tail match; next check if one of the // old head/tail items was removed. We first need to generate // the reverse map of new keys to index (`newKeyToIndexMap`), // which is done once lazily as a performance optimization, // since we only hit this case if multiple non-contiguous // changes were made. Note that for contiguous removal // anywhere in the list, the head and tails would advance // from either end and pass each other before we get to this // case and removals would be handled in the final while loop // without needing to generate the map. // // * Example below: The key at `oldTail` was removed (no longer // in the `newKeyToIndexMap`), so remove that part from the // DOM and advance just the `oldTail` pointer. // // oldHead v v oldTail // oldKeys: [0, 1, 2, 3, 4, 5, 6] // newParts: [0, , , , , , 6] <- 5 not in new map: remove // newKeys: [0, 2, 1, 4, 3, 7, 6] 5 and advance oldTail // newHead ^ ^ newTail // // * Once head and tail cannot move, any mismatches are due to // either new or moved items; if a new key is in the previous // "old key to old index" map, move the old part to the new // location, otherwise create and insert a new part. Note // that when moving an old part we null its position in the // oldParts array if it lies between the head and tail so we // know to skip it when the pointers get there. // // * Example below: neither head nor tail match, and neither // were removed; so find the `newHead` key in the // `oldKeyToIndexMap`, and move that old part's DOM into the // next head position (before `oldParts[oldHead]`). Last, // null the part in the `oldPart` array since it was // somewhere in the remaining oldParts still to be scanned // (between the head and tail pointers) so that we know to // skip that old part on future iterations. // // oldHead v v oldTail // oldKeys: [0, 1, -, 3, 4, 5, 6] // newParts: [0, 2, , , , , 6] <- stuck: update & move 2 // newKeys: [0, 2, 1, 4, 3, 7, 6] into place and advance // newHead // newHead ^ ^ newTail // // * Note that for moves/insertions like the one above, a part // inserted at the head pointer is inserted before the // current `oldParts[oldHead]`, and a part inserted at the // tail pointer is inserted before `newParts[newTail+1]`. The // seeming asymmetry lies in the fact that new parts are // moved into place outside in, so to the right of the head // pointer are old parts, and to the right of the tail // pointer are new parts. // // * We always restart back from the top of the algorithm, // allowing matching and simple updates in place to // continue... // // * Example below: the head pointers once again match, so // simply update part 1 and record it in the `newParts` // array. Last, advance both head pointers. // // oldHead v v oldTail // oldKeys: [0, 1, -, 3, 4, 5, 6] // newParts: [0, 2, 1, , , , 6] <- heads matched: update 1 // newKeys: [0, 2, 1, 4, 3, 7, 6] and advance both oldHead // & newHead // newHead ^ ^ newTail // // * As mentioned above, items that were moved as a result of // being stuck (the final else clause in the code below) are // marked with null, so we always advance old pointers over // these so we're comparing the next actual old value on // either end. // // * Example below: `oldHead` is null (already placed in // newParts), so advance `oldHead`. // // oldHead v v oldTail // oldKeys: [0, 1, -, 3, 4, 5, 6] <- old head already used: // newParts: [0, 2, 1, , , , 6] advance oldHead // newKeys: [0, 2, 1, 4, 3, 7, 6] // newHead ^ ^ newTail // // * Note it's not critical to mark old parts as null when they // are moved from head to tail or tail to head, since they // will be outside the pointer range and never visited again. // // * Example below: Here the old tail key matches the new head // key, so the part at the `oldTail` position and move its // DOM to the new head position (before `oldParts[oldHead]`). // Last, advance `oldTail` and `newHead` pointers. // // oldHead v v oldTail // oldKeys: [0, 1, -, 3, 4, 5, 6] // newParts: [0, 2, 1, 4, , , 6] <- old tail matches new // newKeys: [0, 2, 1, 4, 3, 7, 6] head: update & move 4, // advance oldTail & newHead // newHead ^ ^ newTail // // * Example below: Old and new head keys match, so update the // old head part in place, and advance the `oldHead` and // `newHead` pointers. // // oldHead v oldTail // oldKeys: [0, 1, -, 3, 4, 5, 6] // newParts: [0, 2, 1, 4, 3, ,6] <- heads match: update 3 // newKeys: [0, 2, 1, 4, 3, 7, 6] and advance oldHead & // newHead // newHead ^ ^ newTail // // * Once the new or old pointers move past each other then all // we have left is additions (if old list exhausted) or // removals (if new list exhausted). Those are handled in the // final while loops at the end. // // * Example below: `oldHead` exceeded `oldTail`, so we're done // with the main loop. Create the remaining part and insert // it at the new head position, and the update is complete. // // (oldHead > oldTail) // oldKeys: [0, 1, -, 3, 4, 5, 6] // newParts: [0, 2, 1, 4, 3, 7 ,6] <- create and insert 7 // newKeys: [0, 2, 1, 4, 3, 7, 6] // newHead ^ newTail // // * Note that the order of the if/else clauses is not // important to the algorithm, as long as the null checks // come first (to ensure we're always working on valid old // parts) and that the final else clause comes last (since // that's where the expensive moves occur). The order of // remaining clauses is just a simple guess at which cases // will be most common. // // * Note, we could calculate the longest // increasing subsequence (LIS) of old items in new position, // and only move those not in the LIS set. However that costs // O(nlogn) time and adds a bit more code, and only helps // make rare types of mutations require fewer moves. The // above handles removes, adds, reversal, swaps, and single // moves of contiguous items in linear time, in the minimum // number of moves. As the number of multiple moves where LIS // might help approaches a random shuffle, the LIS // optimization becomes less helpful, so it seems not worth // the code at this point. Could reconsider if a compelling // case arises. while (oldHead <= oldTail && newHead <= newTail) { if (oldParts[oldHead] === null) { // `null` means old part at head has already been used // below; skip oldHead++; } else if (oldParts[oldTail] === null) { // `null` means old part at tail has already been used // below; skip oldTail--; } else if (oldKeys[oldHead] === newKeys[newHead]) { // Old head matches new head; update in place newParts[newHead] = setChildPartValue(oldParts[oldHead], newValues[newHead]); oldHead++; newHead++; } else if (oldKeys[oldTail] === newKeys[newTail]) { // Old tail matches new tail; update in place newParts[newTail] = setChildPartValue(oldParts[oldTail], newValues[newTail]); oldTail--; newTail--; } else if (oldKeys[oldHead] === newKeys[newTail]) { // Old head matches new tail; update and move to new tail newParts[newTail] = setChildPartValue(oldParts[oldHead], newValues[newTail]); insertPart(containerPart, newParts[newTail + 1], oldParts[oldHead]); oldHead++; newTail--; } else if (oldKeys[oldTail] === newKeys[newHead]) { // Old tail matches new head; update and move to new head newParts[newHead] = setChildPartValue(oldParts[oldTail], newValues[newHead]); insertPart(containerPart, oldParts[oldHead], oldParts[oldTail]); oldTail--; newHead++; } else { if (newKeyToIndexMap === undefined) { // Lazily generate key-to-index maps, used for removals & // moves below newKeyToIndexMap = generateMap(newKeys, newHead, newTail); oldKeyToIndexMap = generateMap(oldKeys, oldHead, oldTail); } if (!newKeyToIndexMap.has(oldKeys[oldHead])) { // Old head is no longer in new list; remove removePart(oldParts[oldHead]); oldHead++; } else if (!newKeyToIndexMap.has(oldKeys[oldTail])) { // Old tail is no longer in new list; remove removePart(oldParts[oldTail]); oldTail--; } else { // Any mismatches at this point are due to additions or // moves; see if we have an old part we can reuse and move // into place const oldIndex = oldKeyToIndexMap.get(newKeys[newHead]); const oldPart = oldIndex !== undefined ? oldParts[oldIndex] : null; if (oldPart === null) { // No old part for this value; create a new one and // insert it const newPart = insertPart(containerPart, oldParts[oldHead]); setChildPartValue(newPart, newValues[newHead]); newParts[newHead] = newPart; } else { // Reuse old part newParts[newHead] = setChildPartValue(oldPart, newValues[newHead]); insertPart(containerPart, oldParts[oldHead], oldPart); // This marks the old part as having been used, so that // it will be skipped in the first two checks above oldParts[oldIndex] = null; } newHead++; } } } // Add parts for any remaining new values while (newHead <= newTail) { // For all remaining additions, we insert before last new // tail, since old pointers are no longer valid const newPart = insertPart(containerPart, newParts[newTail + 1]); setChildPartValue(newPart, newValues[newHead]); newParts[newHead++] = newPart; } // Remove any remaining unused old parts while (oldHead <= oldTail) { const oldPart = oldParts[oldHead++]; if (oldPart !== null) { removePart(oldPart); } } // Save order of new parts for next round this._itemKeys = newKeys; // Directly set part value, bypassing it's dirty-checking setCommittedValue(containerPart, newParts); return noChange; } } /** * A directive that repeats a series of values (usually `TemplateResults`) * generated from an iterable, and updates those items efficiently when the * iterable changes based on user-provided `keys` associated with each item. * * Note that if a `keyFn` is provided, strict key-to-DOM mapping is maintained, * meaning previous DOM for a given key is moved into the new position if * needed, and DOM will never be reused with values for different keys (new DOM * will always be created for new keys). This is generally the most efficient * way to use `repeat` since it performs minimum unnecessary work for insertions * and removals. * * The `keyFn` takes two parameters, the item and its index, and returns a unique key value. * * ```js * html` * <ol> * ${repeat(this.items, (item) => item.id, (item, index) => { * return html`<li>${index}: ${item.name}</li>`; * })} * </ol> * ` * ``` * * **Important**: If providing a `keyFn`, keys *must* be unique for all items in a * given call to `repeat`. The behavior when two or more items have the same key * is undefined. * * If no `keyFn` is provided, this directive will perform similar to mapping * items to values, and DOM will be reused against potentially different items. */ const repeat = directive(RepeatDirective); const resources = { userHeader: "Account Information", manageUserLink: "Manage account", logoutUserLink: "Log out", profileHeader: "Profile Information", manageProfileLink: "Edit profile", switchProfileLink: "Switch profile", impersonatingVerb: "acting as", notificationsHeader: "Notifications", notificationsLabel: "Unread notifications", noNotificationsLabel: "No unread notifications", notificationsLink: "View all", copyright: "© 1995 - {0} Zywave, Inc. All rights reserved.", tosLink: "Terms and Conditions", privacyLink: "Privacy Statement", dmcaLink: "DMCA", cookiesLink: "Cookie Usage", contactLinkTitle: "Contact", contactLink: "https://support.zywave.com/s/contactsupport", loginUserText: "Log in", featureFlagsHeader: "Early feature access" }; /** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ class ClassMapDirective extends Directive { constructor(partInfo) { super(partInfo); if (partInfo.type !== PartType.ATTRIBUTE || partInfo.name !== 'class' || partInfo.strings?.length > 2) { throw new Error('`classMap()` can only be used in the `class` attribute ' + 'and must be the only part in the attribute.'); } } render(classInfo) { // Add spaces to ensure separation from static classes return ' ' + Object.keys(classInfo).filter(key => classInfo[key]).join(' ') + ' '; } update(part, [classInfo]) { // Remember dynamic classes on the first render if (this._previousClasses === undefined) { this._previousClasses = new Set(); if (part.strings !== undefined) { this._staticClasses = new Set(part.strings.join(' ').split(/\s/).filter(s => s !== '')); } for (const name in classInfo) { if (classInfo[name] && !this._staticClasses?.has(name)) { this._previousClasses.add(name); } } return this.render(classInfo); } const classList = part.element.classList; // Remove old classes that no longer apply for (const name of this._previousClasses) { if (!(name in classInfo)) { classList.remove(name); this._previousClasses.delete(name); } } // Add or remove classes based on their classMap value for (const name in classInfo) { // We explicitly want a loose truthy check of `value` because it seems // more convenient that '' and 0 are skipped. const value = !!classInfo[name]; if (value !== this._previousClasses.has(name) && !this._staticClasses?.has(name)) { if (value) { classList.add(name); this._previousClasses.add(name); } else { classList.remove(name); this._previousClasses.delete(name); } } } return noChange; } } /** * A directive that applies dynamic CSS classes. * * This must be used in the `class` attribute and must be the only part used in * the attribute. It takes each property in the `classInfo` argument and adds * the property name to the element's `classList` if the property value is * truthy; if the property value is falsy, the property name is removed from * the element's `class`. * * For example `{foo: bar}` applies the class `foo` if the value of `bar` is * truthy. * * @param classInfo */ const classMap = directive(ClassMapDirective); /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ const HTML_RESULT = 1; class UnsafeHTMLDirective extends Directive { constructor(partInfo) { super(partInfo); this._value = nothing; if (partInfo.type !== PartType.CHILD) { throw new Error(`${this.constructor.directiveName}() can only be used in child bindings`); } } render(value) { if (value === nothing || value == null) { this._templateResult = undefined; return this._value = value; } if (value === noChange) { return value; } if (typeof value != 'string') { throw new Error(`${this.constructor.directiveName}() called with a non-string value`); } if (value === this._value) { return this._templateResult; } this._value = value; const strings = [value]; // eslint-disable-next-line @typescript-eslint/no-explicit-any strings.raw = strings; // WARNING: impersonating a TemplateResult like this is extremely // dangerous. Third-party directives should not do this. return this._templateResult = { // Cast to a known set of integers that satisfy ResultType so that we // don't have to export ResultType and possibly encourage this pattern. // This property needs to remain unminified. ['_$litType$']: this.constructor.resultType, strings, values: [] }; } } UnsafeHTMLDirective.directiveName = 'unsafeHTML'; UnsafeHTMLDirective.resultType = HTML_RESULT; /** * Renders the result as HTML, rather than text. * * The values `undefined`, `null`, and `nothing`, will all result in no content * (empty string) being rendered. * * Note, this is unsafe to use with any user-provided input that hasn't been * sanitized or escaped, as it may lead to cross-site-scripting * vulnerabilities. */ const unsafeHTML = directive(UnsafeHTMLDirective); const style$2 = css`:host{display:contents}:host *:not(:defined){display:none}.theme-logo{width:auto;height:100%}zui-shell{--zui-shell-primary-theme: var(--zywave-shell-primary-color, var(--zui-blue-500))}.legalese{display:block}.notify-parent{position:fixed;top:var(--zui-shell-topbar-global-height);left:0;width:calc(100vw - 1.25rem);visibility:hidden;margin-top:1.25rem}.profile-types-list,.profile-type-identifier{list-style:none;padding:0}*:not(:defined){display:none;width:0;height:0;visibility:hidden}.feature-flags-header{padding:.625rem .9375rem;background:var(--zui-gray-50);font-weight:700}slot[name=experimentalassistant]{display:none}@media(min-width: 45em){slot[name=experimentalassistant]{display:contents}}::slotted([slot=experimentalassistant]){min-width:0}`; function sleep(timeoutMs, reject = false) { return new Promise((resolve, rej) => { setTimeout(() => reject ? rej(new Error("Sleep rejected")) : resolve(), timeoutMs); }); } /** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ const important = 'important'; // The leading space is important const importantFlag = ' !' + important; // How many characters to remove from a value, as a negative number const flagTrim = 0 - importantFlag.length; class StyleMapDirective extends Directive { constructor(partInfo) { super(partInfo); if (partInfo.type !== PartType.ATTRIBUTE || partInfo.name !== 'style' || partInfo.strings?.length > 2) { throw new Error('The `styleMap` directive must be used in the `style` attribute ' + 'and must be the only part in the attribute.'); } } render(styleInfo) { return Object.keys(styleInfo).reduce((style, prop) => { const value = styleInfo[prop]; if (value == null) { return style; } // Convert property names from camel-case to dash-case, i.e.: // `backgroundColor` -> `background-color` // Vendor-prefixed names need an extra `-` appended to front: // `webkitAppearance` -> `-webkit-appearance` // Exception is any property name containing a dash, including // custom properties; we assume these are already dash-cased i.e.: // `--my-button-color` --> `--my-button-color` prop = prop.includes('-') ? prop : prop.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g, '-$&').toLowerCase(); return style + `${prop}:${value};`; }, ''); } update(part, [styleInfo]) { const { style } = part.element; if (this._previousStyleProperties === undefined) { this._previousStyleProperties = new Set(Object.keys(styleInfo)); return this.render(styleInfo); } // Remove old properties that no longer exist in styleInfo for (const name of this._previousStyleProperties) { // If the name isn't in styleInfo or it's null/undefined if (styleInfo[name] == null) { this._previousStyleProperties.delete(name); if (name.includes('-')) { style.removeProperty(name); } else { // eslint-disable-next-line @typescript-eslint/no-explicit-any style[name] = null; } } } // Add or update properties for (const name in styleInfo) { const value = styleInfo[name]; if (value != null) { this._previousStyleProperties.add(name); const isImportant = typeof value === 'string' && value.endsWith(importantFlag); if (name.includes('-') || isImportant) { style.setProperty(name, isImportant ? value.slice(0, flagTrim) : value, isImportant ? important : ''); } else { // eslint-disable-next-line @typescript-eslint/no-explicit-any style[name] = value; } } } return noChange; } } /** * A directive that applies CSS properties to an element. * * `styleMap` can only be used in the `style` attribute and must be the only * expression in the attribute. It takes the property names in the * {@link StyleInfo styleInfo} object and adds the properties to the inline * style of the element. * * Property names with dashes (`-`) are assumed to be valid CSS * property names and set on the element's style object using `setProperty()`. * Names without dashes are assumed to be camelCased JavaScript property names * and set on the element's style object using property assignment, allowing the * style object to translate JavaScript-style names to CSS property names. * * For example `styleMap({backgroundColor: 'red', 'border-top': '5px', '--size': * '0'})` sets the `background-color`, `border-top` and `--size` properties. * * @param styleInfo * @see {@link https://lit.dev/docs/templates/directives/#stylemap styleMap code samples on Lit.dev} */ const styleMap = directive(StyleMapDirective); const style$1 = css`@supports(scrollbar-width: thin){.options-list{scrollbar-color:var(--zui-gray-400) var(--zui-gray-50);scrollbar-width:thin}}@supports not (scrollbar-width: thin){.options-list::-webkit-scrollbar{width:7px;height:7px;background-color:var(--zui-gray-50)}.options-list::-webkit-scrollbar-thumb{background-color:var(--zui-gray-400);border-radius:10px}}:host{display:block}.wrapper{position:relative;width:100%}@media(min-width: 45em){.wrapper{width:auto}}.input-wrapper{position:relative;background:#fff;border-radius:.25rem}.input-wrapper zui-icon{--zui-icon-size: 1.0625rem;position:absolute;top:calc(50% - 0.53125rem);left:.8125rem;z-index:1;fill:var(--zui-gray-400)}.input-wrapper input{position:relative;display:inline-block;width:100%;min-width:0;min-height:2.625rem;z-index:2;vertical-align:middle;padding:0 .625rem 0 2.5rem;background-color:rgba(0,0,0,0);border:.0625rem solid var(--zui-gray-200);border-radius:.25rem;font:inherit;color:inherit;transition:border 100ms ease-in-out,box-shadow 100ms ease-in-out,width 250ms ease-in-out;box-sizing:border-box;appearance:textfield}@media(min-width: 45em){.input-wrapper input{min-width:69ch}}.input-wrapper input::placeholder{color:var(--zui-gray-300)}.input-wrapper input::-webkit-input-placeholder{color:var(--zui-gray-300)}.input-wrapper input::-moz-placeholder{opacity:1;color:var(--zui-gray-300)}.input-wrapper input:-moz-placeholder{opacity:1;color:var(--zui-gray-300)}.input-wrapper input:-ms-input-placeholder{color:var(--zui-gray-300)}.input-wrapper input::-ms-clear{display:none;width:0;height:0}.input-wrapper input::-ms-reveal{display:none;width:0;height:0}.input-wrapper input:not(output):-moz-ui-invalid{box-shadow:none}.input-wrapper input::-webkit-search-decoration,.input-wrapper input::-webkit-search-cancel-button,.input-wrapper input::-webkit-search-results-button,.input-wrapper input::-webkit-search-results-decoration{display:none}.input-wrapper input:hover{border-color:var(--zui-gray-300)}.input-wrapper input:focus{border-color:var(--zui-blue-400);box-shadow:0 0 0 .0625rem var(--zui-blue-400);outline:none}.input-wrapper input[disabled]{background-color:var(--zui-gray-100);cursor:not-allowed;color:var(--zui-gray)}.input-wrapper input[disabled]:hover{border:.0625rem solid var(--zui-gray-200)}.input-wrapper input[readonly]{background:rgba(0,0,0,0);border:0;outline:none;color:var(--zui-gray);pointer-events:none}.options-list{width:calc(100% - 2.5rem);max-width:69ch;max-height:80vh;z-index:1;overflow-y:auto;padding:1.5625rem 0 .9375rem;background-color:#fff;border:0;border-radius:.25rem;box-shadow:0 .0625rem .1875rem .0625rem rgba(0,0,0,.16);color:var(--zui-gray-700);transition:max-height .2s,padding .2s,box-shadow .2s,transform .3s;scroll-behavior:smooth}@media(min-width: 45em){.options-list{width:69ch;max-width:none}}.options-list:popover-open{position:fixed;inset:unset}.options-list .options-group{display:block}.options-list .group-label{display:flex;justify-content:space-between;align-items:center;padding:1.25rem 1.5625rem .75rem;font-size:.75rem;font-weight:700;text-transform:uppercase}.options-list .group-label:first-child{padding-top:0}.options-list .option{display:flex;height:2.6rem;align-items:center;padding:.5rem 1.5625rem;background-color:rgba(0,0,0,0);cursor:pointer;color:var(--zui-gray-600);transition:background-color 100ms ease-in-out;text-decoration:none}.options-list .option:hover{background-color:var(--zui-gray-50)}.options-list .option:focus{background-color:var(--zui-gray-50);outline:none}.options-list .option zui-icon{--zui-icon-size: 1.25rem;align-self:flex-start;margin-top:.5ch;margin-right:.9375rem}.options-list .description{width:calc(100% - 1.875rem);align-self:stretch}.options-list .first-line{font-weight:700}.options-list .second-line{font-size:.75rem;color:var(--zui-gray-500)}.options-list .first-line,.options-list .second-line{display:block;width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.options-list .no-results{padding:.5rem 1.5625rem;font-style:italic;color:var(--zui-gray-800)}.see-more{display:block;padding:0 1.5625rem}.see-more a{display:inline-block;vertical-align:baseline;margin:0;padding:.5rem 0 .625rem;font-size:.75rem;font-weight:600;cursor:pointer;color:var(--zui-blue);text-decoration:none}.see-more a:hover{color:var(--zui-blue-400)}.see-more a:focus{outline:.0625rem solid var(--zui-blue);outline-offset:.25rem;text-decoration:none}.see-more a:active{outline:none;color:var(--zui-blue-600);text-decoration:underline}.see-more+.group-label,.no-results+.group-label{margin-top:.625rem;border-top:1px solid var(--zui-gray-100)}.skeleton{position:relative;display:inline-block;height:1.6em;overflow:hidden;background-color:var(--zui-gray-100);border-radius:.1875rem}.skeleton::after{position:absolute;top:0;right:0;bottom:0;left:0;content:"";background-image:linear-gradient(90deg, rgba(255, 255, 255, 0) 0, rgba(255, 255, 255, 0.2) 20%, rgba(255, 255, 255, 0.5) 60%, rgba(255, 255, 255, 0));animation:shimmer 2s infinite;transform:translateX(-100%)}@keyframes shimmer{100%{transform:translateX(100%)}}zui-icon.skeleton{width:1.25rem;height:1.25rem;border-radius:50%}.skeleton.group-label{width:10ch;margin:0 1.5625rem .75rem;padding:0}.skeleton.first-line,.skeleton.second-line{height:1.6em}.skeleton.first-line{margin-bottom:.15625rem}.skeleton.second-line{width:70%;margin-top:.15625rem}.skeleton.see-more{width:8ch;margin-left:1.5625rem;padding:0}`; var __decorate$2 = undefined && undefined.__decorate || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; /** * @element 'zywave-shell-omnisearch' * * @slot - The default slot */ class ZywaveShellOmnisearchElement extends ZywaveBaseElement { #searchResult; #value; #timeoutId; #polyfillReady; #controller; #isActive; #isLoading; #initialListTopValue; #handleTopbarHeightChange; get value() { return this.#value; } set value(val) { const oldVal = this.#value; this.#value = val; this.requestUpdate("value", oldVal); } static get styles() { return [style$1]; } get #query() { return this._input?.value; } constructor() { super(); this.#value = ""; this.#isActive = false; this.#isLoading = false; /** * Default placeholder (ghost) text */ this.placeholder = "Search..."; // temporary default this.#handleTopbarHeightChange = e => { this.#onTopbarHeightChange(e); }; } connectedCallback() { super.connectedCallback(); window.addEventListener("topbarheightchange", this.#handleTopbarHeightChange); } disconnectedCallback() { super.disconnectedCallback(); window.removeEventListener("topbarheightchange", this.#handleTopbarHeightChange); } async firstUpdated(changedProps) { super.firstUpdated(changedProps); this.#polyfillReady = window.zywave?.zapi?._popover?.requiresPolyfill ? window.zywave.zapi._popover.loadPolyfill() : Promise.resolve(); await this.#polyfillReady; // one final update to get the input/list in sync this.requestUpdate(); this.#initialListTopValue = this._input.getBoundingClientRect().top; } render() { return html` ${this.#renderPolyfillStyles()} <div class="wrapper">${this.#renderInput()} ${this.#renderOptions()}</div> <slot></slot> `; } #renderPolyfillStyles() { return window.zywave?.zapi?._popover?.requiresPolyfill ? html`<link href="${window.zywave.zapi._popover.cssUri}" rel="stylesheet" />` : nothing; } #renderInput() { return html` <div class="input-wrapper"> <zui-icon icon="zui-search"></zui-icon> <input id="search" aria-label="Search" type="search" .value="${this.value || ""}" placeholder="${this.placeholder ?? ""}" @input="${() => this.#onSearchInputUpdate()}" @keyup="${e => this.#onInputKeyUp(e)}" popovertarget="list" autocomplete="nope" /> </div> `; } #renderOptions() { if (!this._input) { return nothing; } const inputPosition = this._input.getBoundingClientRect(); const inlineStyles = {}; if (window.zywave?.zapi?._popover?.requiresPolyfill) { inlineStyles.position = "fixed"; inlineStyles.inset = "unset"; } inlineStyles.top = `${inputPosition.height}px`; // if for whatever reason the input position changes, // such as toggling mobile search into and out of view, // we need to update the top value to properly position the list const newTopValue = this._input.getBoundingClientRect().top; if (this.#initialListTopValue !== newTopValue) { this.#initialListTopValue = newTopValue; } if (!window.zywave?.zapi?._popover?.requiresPolyfill) { inlineStyles.top = `${this.#initialListTopValue + inputPosition.height}px`; inlineStyles.left = `${inputPosition.left}px`; } // we make a deep clone in case the user resumes querying while loading results const results = structuredClone(this.#searchResult ?? []); const renderResultItem = item => { return html`<a href="${item.href}" class="option" @click="${() => this.#onResultItemClick(item)}" ><zui-icon icon="${item.icon ?? "zui-rocket"}" class="small"></zui-icon> <div class="description"> <div class="first-line" title="${item.title}">${item.title}</div> <div class="second-line" title="${item.subtitle}">${item.subtitle}</div> </div></a ></a>`; }; const renderResults = list => { return html`<div class="group-label">${list.entityName}</div> ${list.items.length > 0 ? repeat(list.items, renderResultItem) : html`<div class="no-results">No results found.</div>`} ${list.items.length > 0 ? html`<div class="see-more"> <a href="${list.items[0].searchPageUri}" @click="${() => this.#onSeeMoreClick(list)}">See all results</a> </div>` : nothing}`; }; return html` <div id="list" class="options-list" style="${styleMap(inlineStyles)}" popover @toggle="${this.#onPopoverToggle}"> ${this.#isLoading ? this.#renderSkeleton() : repeat(results, renderResults)} <slot></slot> </div> `; } #multiplySkeletonOptions() { const skeletonOption = () => { return html`<div class="option"> <zui-icon icon="" class="small skeleton"></zui-icon> <div class="description"> <div class="first-line skeleton">&nbsp;</div> <div class="second-line skeleton">&nbsp;</div> </div> </div>`; }; return html`${repeat([1, 2, 3, 4, 5], skeletonOption)}`; } #renderSkeleton() { return html`<div class="group-label skeleton">&nbsp;</div> ${this.#multiplySkeletonOptions()}`; } async #loadSearchResults(query, signal) { const start = performance.now(); const url = new URL("shell/v2.0/search", this.apiUrl); if (query) { url.searchParams.set("query", query); } url.searchParams.set("host", window.location.hostname); const resp = await this._apiClient.fetch(url, { signal }); if (resp instanceof Response && resp.status === 200 && !signal.aborted) { const end = performance.now(); this.#searchResult = await resp.json(); const eventDetails = { duration: end - start, query }; for (const entitySearchResult of this.#searchResult) { eventDetails[`${entitySearchResult.entityName} - Count`] = entitySearchResult.items?.length ?? 0; eventDetails[`${entitySearchResult.entityName} - Timed Out`] = entitySearchResult.timedOut; eventDetails[`${entitySearchResult.entityName} - Elapsed Milliseconds`] = entitySearchResult.elapsedMs; } this.dispatchEvent(new CustomEvent("search", { detail: eventDetails, bubbles: false })); this.requestUpdate(); } } #onSearchInputUpdate() { this.#showPopover(); this.#isLoading = true; this.requestUpdate(); if (this.#timeoutId) { window.clearTimeout(this.#timeoutId); this.#controller?.abort(); } this.#controller = new AbortController(); const signal = this.#controller.signal; const task = async () => { if (!signal.aborted) { await this.#loadSearchResults(this.#query, this.#controller.signal); this.#isLoading = false; this.requestUpdate(); } }; this.#timeoutId = window.setTimeout(() => { task(); }, 300); } #onPopoverToggle(event) { this.#isActive = event.oldState === "closed" && event.newState === "open"; if (!this.#isActive) { this.#onDismiss(); } } #showPopover() { const show = () => { this._list?.showPopover?.(); this._input?.focus(); this.#isActive = true; }; if (this.#polyfillReady) { this.#polyfillReady?.then(show); } else { show(); } } #onResultItemClick(item) { const itemDetails = { entityName: item.entityName, title: item.title, subtitle: item.subtitle, href: item.href, query: this.#query }; this.dispatchEvent(new CustomEvent("clickresult", { detail: itemDetails, bubbles: false })); } #onSeeMoreClick(list) { const listDetails = { entityName: list.entityName, searchPageUri: list.items[0].searchPageUri, query: this.#query }; this.dispatchEvent(new CustomEvent("clickse