CINXE.COM
HubSpot Community - Group Hubs for kennedyp - HubSpot Community
<!DOCTYPE html><html prefix="og: http://ogp.me/ns#" dir="ltr" lang="en" class="no-js"> <head> <title> HubSpot Community - Group Hubs for kennedyp - HubSpot Community </title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"> <meta content="Welcome to the HubSpot Community! Connect with peers, maximize your HubSpot knowledge, and learn how to grow better with HubSpot." name="description"/><meta content="width=device-width, initial-scale=1.0" name="viewport"/><meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/><link href="https://community.hubspot.com/t5/grouphubs/page/user-id/169781" rel="canonical"/> <link class="lia-link-navigation hidden live-links" title="New board topics in HubSpot Community" type="application/rss+xml" rel="alternate" id="link" href="/mjmao93648/rss/Community?interaction.style=forum"></link> <link class="lia-link-navigation hidden live-links" title="All board posts in HubSpot Community" type="application/rss+xml" rel="alternate" id="link_0" href="/mjmao93648/rss/Community?interaction.style=forum&feeds.replies=true"></link> <link class="lia-link-navigation hidden live-links" title="New blog blog posts in HubSpot Community" type="application/rss+xml" rel="alternate" id="link_1" href="/mjmao93648/rss/Community?interaction.style=blog"></link> <link class="lia-link-navigation hidden live-links" title="All blog posts in HubSpot Community" type="application/rss+xml" rel="alternate" id="link_2" href="/mjmao93648/rss/Community?interaction.style=blog&feeds.replies=true"></link> <link class="lia-link-navigation hidden live-links" title="New idea exchange ideas in HubSpot Community" type="application/rss+xml" rel="alternate" id="link_3" href="/mjmao93648/rss/Community?interaction.style=idea"></link> <link class="lia-link-navigation hidden live-links" title="All idea exchange posts in HubSpot Community" type="application/rss+xml" rel="alternate" id="link_4" href="/mjmao93648/rss/Community?interaction.style=idea&feeds.replies=true"></link> <link class="lia-link-navigation hidden live-links" title="New Products" type="application/rss+xml" rel="alternate" id="link_5" href="/mjmao93648/rss/Community?interaction.style=review"></link> <link class="lia-link-navigation hidden live-links" title="All Reviews and Comments" type="application/rss+xml" rel="alternate" id="link_6" href="/mjmao93648/rss/Community?interaction.style=review&feeds.replies=true"></link> <link class="lia-link-navigation hidden live-links" title="rss.livelink.threads-in-node@place:occasion" type="application/rss+xml" rel="alternate" id="link_7" href="/mjmao93648/rss/Community?interaction.style=occasion"></link> <link class="lia-link-navigation hidden live-links" title="rss.livelink.posts-in-node@place:occasion" type="application/rss+xml" rel="alternate" id="link_8" href="/mjmao93648/rss/Community?interaction.style=occasion&feeds.replies=true"></link> <link class="lia-link-navigation hidden live-links" title="New media posts in HubSpot Community" type="application/rss+xml" rel="alternate" id="link_9" href="/mjmao93648/rss/Community?interaction.style=media"></link> <link class="lia-link-navigation hidden live-links" title="All media posts in HubSpot Community" type="application/rss+xml" rel="alternate" id="link_10" href="/mjmao93648/rss/Community?interaction.style=media&feeds.replies=true"></link> <link href="/skins/6566941/cf3d09a7a299b32f8a1bbd2f844fb61f/hubspot.css" rel="stylesheet" type="text/css"/> <!-- Twitter Card metadata: For Japanese Blog Articles only --> <meta name="google-site-verification" content="JhdbLb5-5cPIvkeSYPUWX4n-wvBOsUlTzu7NjgCxLjQ" /> <script> window.$start = performance.now(); window.$stats = {}; // We need to define this BEFORE the lib is loaded so it initializes properly with this config window.__unocss = { theme: { breakpoints: { '@s': '(max-width: 479px)', '@m': '(max-width: 767px)', '@l': '(max-width: 1023px)', '@xl': '(min-width: 1200px)', }, px2rem: false, }, /** /* Within the RegEx rules below are named groups: /* - <d> stands for direction, think top, right, bottom, left, x, y /* - <g> stands for global, think values like auto|inherit|initial|revert|revert-layer|unset etc. /* - <m> stands for modifier, used for sub-properties like "-size" in "background-size" /* - <p> stands for property /* - <s> stands for selector where applicable /* - <u> stands for unit, default is px, no need to add that, for all other units, write them behind the value /* - <v> stands for value /*/ rules: [ // if you need a class to be !important simply prefix the class in HTMl with ! (it's automatic) // animation/transition utilities [/^a:(?<p>[a-z\-]+)?\/?(?<dur>[\d.ms]+)?\/?(?<e>[a-z\-]+)?\/?(?<del>[\d.ms]+)?$/, ([, p, dur, e, del]) => ( Object.entries({ 'transition-property': p || false, 'transition-duration': dur || '236ms', 'transition-timing-function': e || 'ease-in-out', 'transition-delay': del || false, }).reduce((rules, [prop, value]) => { if ( value ) { rules[prop] = /^[\d\.]+$/.test(value) ? `${value}ms` : value.replace(/(?<!var\()(?<v>--\S+)/, `var(${value})`); } return rules; }, {}) )], // border utilities // This one is a bit special, it's basically able to set a variety of properties if specified, the pattern is like // `b:<direction><color>/<width>/<style>/<radius>` if you want to omit certain values, you need the slash (/), // but with no value in between, so let's say you only want to set the border-style, you would do `b:///dashed`, // only the border-width: `t:/3`, only border-top-color: `b:tred`, all properties support CSS custom properties // and properties that have numeric values support also `calc()`, e.g. `b:t--color-red` and `t:blue/calc(100px/5)` // The order of properties is different than standard CSS which is `<width><style><color>` as in my experience // 90% of border styling are color, but we mostly write `1px solid <color>`, which is annoying, so this utility // defaults to `border-width: 1px` and `border-style: solid` which can of course easily be changed, but doing simply // `b:green` will result in a 1px solid green border in all directions, which cuts down on how much we have to type // if you don't like these defaults, it's quite simple to change it to whatever behaviour you like! // @limitations: This utility does NOT deal with any kind of border-image stuff, I can't even remember when I have // used this feature last, so not going to include it as a utility, if you really want to use it, write CSS... // @note: The strange looking negative lookaheads in the regex below are there to deal with 'reserved' word like // `transparent` or all kinds of named colors that would otherwise get chopped up by the direction indicators // workaround is simply using a hex color with 0 alpha, e.g. #0000... // TODO: Add in support for `outline`, don't want to make a separate util for it as it is relatively similar to // a border and mostly just annoying default focus behavior for lazy people who don't care about :focus... // TODO: Add support for border radius, probably best to take everything at the end (e.g. after <style>) and // split it by slashes, discard anything more than 4 and then apply them as border-radius would be applied /* TODO: think about how to implement fake directional borders (use box-shadow outlines for full ones) &r-fake { position: relative; &:after { color: red; content: ''; position: absolute; top: 0; bottom: 0; width: 1px; } } */ [/^b:(?<d>[bi][se]|r(?![ego])|t(?![r])|l(?![aei])|b(?![eilru])|y(?![e])|x|o(?![lr]))?\(?(?<v>(?:[a-z\-]+?\([\S]+?\)|[^\s\(\)]+?)+?)\)?$/, ([, d, v]) => ( ({ t: ['-top'], r: ['-right'], b: ['-bottom'], l: ['-left'], x: ['-right', '-left'], y: ['-top', '-bottom'], is: ['-inline-start'], ie: ['-inline-end'], bs: ['-block-start'], be: ['-block-end'], }[d] || ['']).reduce((rules, dir) => ( (v || '') .split(/\/?(?<v>[a-z\-]+\([\S]+?\)+(?![,\)_])|[^\s\/]+)\/?/g) .filter((x) => x.trim()) // handles shortcut values for inherit (`i`) and skip (`x`) // you can optionally specify border radius separated by _: `.b:red/1/solid/1px_2px_3px_4px` // but it has to be specified with the unit! slash-notation doesn't need the unit, will default to px // there's a special class `.b:none` to remove a border, but `.b:x/0` is shorter .map((x, i) => /^[\d\.]+$/.test(x) ? `${x}px` : x.replace(/(?<!var\()(--[^\s,/]+)/g, 'var($1)').replace(/_/g, ' ').replace(/^i$/g, 'inherit').replace(/^c$/g, 'currentColor').replace(/^s$/g, 'solid').replace(/^x$/g, '')) // we want always minimum 3 values, as we want to set defaults for width/style if not defined .reduce((arr, val, i, values) => (values.length < 3 ? [...values, ...Array(3-Math.min(values.length, 3)).fill(null)] : values), []) .reduce((ret, val, i, arr) => ({ ...ret, ...({ 0: !['', 'none'].includes(val) ? { [({o: 'outline'}[d] || `border${dir}`) + '-color']: val } : {}, 1: ![''].includes(val) ? { [({o: 'outline'}[d] || `border${dir}`) + '-width']: arr[0] === 'none' ? '0' : val || '1px' } : {}, 2: ![''].includes(val) ? { [({o: 'outline'}[d] || `border${dir}`) + '-style']: val || 'solid' } : {}, 3: { 'border-radius': val }, 4: { 'border-radius': `${ret['border-radius']} ${val}` }, 5: { 'border-radius': `${ret['border-radius']} ${val}` }, 6: { 'border-radius': `${ret['border-radius']} ${val}` }, }[i] || {}) }), {}) ), {}) )], /* OLD border version [/^(?<p>b):(?<d>[bi][se]|r(?![ego])|t(?![r])|l(?![aei])|b(?![eilru])|y(?![e])|x)?(?<g>inherit|initial|revert|revert-layer|unset)?(?<outline>o(?![lr]))?(?<color>[^\/\s:@]*)?\/?(?<width>[\d.]+|--[^\/\s:@]+|calc\(.*?\))?(?<u>[a-zA-Z%]+)?\/?(?<style>[^\/]*)?\/?(?<radius>[\S]+)?$/, ([, p, d, g, outline, color, width, u, style, radius]) => ( { undefined: [''], t: ['-top'], r: ['-right'], b: ['-bottom'], l: ['-left'], x: ['-right', '-left'], y: ['-top', '-bottom'], is: ['-inline-start'], ie: ['-inline-end'], bs: ['-block-start'], be: ['-block-end'], }[d].reduce((rules, dir) => { // global values like auto, initial, revert will be captured by the <u> group Object.entries({ [(outline ? 'outline' : `border${dir}`) + '-color']: color || g ? `${color?.replace(/(?<!var\()(?<v>--\S+)/, `var(${color})`) || ( g || '')}` : false, [(outline ? 'outline' : `border${dir}`) + '-width']: `${width?.replace(/(?<!var\()(?<v>--\S+)/, `var(${width})`) || '1'}${u || (!width?.startsWith('--') && !width?.includes('calc') ? 'px' : '')}`, [(outline ? 'outline' : `border${dir}`) + '-style']: `${style?.replace(/(?<!var\()(?<v>--\S+)/, `var(${style})`) || 'solid'}`, 'border-radius': radius ? radius.split(/\//g).map((v) => (/^[\d\.]+$/.test(v) ? `${v}px` : v.replace(/(?<!var\()(?<v>--\S+)/, `var(${v})`))).join(' ') : false, }).forEach(([prop, value]) => { if ( value ) { rules[prop] = g ? g : value; } }); return rules; }, {}) )], */ [/^b:(?<c>circle)|(?:rad\(?(?<v>(?:[a-z\-]+?\([\S]+?\)|[^\s\(\)]+?)+?)\)?)$/, ([, c, v]) => ( c ? { 'border-radius': '50%' } : (v || '') .split(/\/?(?<v>[a-z\-]+\([\S]+?\)+(?![,\)_])|[^\s\/]+)\/?/g) .filter((x) => x.trim()) .map((x, i) => /^[\d\.]+$/.test(x) ? `${x}px` : x.replace(/(?<!var\()(--[^\s,/]+)/g, 'var($1)').replace(/_/g, ' ').replace(/^i$/g, 'inherit').replace(/^c$/g, 'currentColor').replace(/^x$/g, '')) .reduce((ret, val, i) => ({ ...ret, ...({ 'border-radius': `${ret['border-radius'] || ''} ${val}` }) }), {}) )], // background: shorthand and advanced multi-value uncovered sub-property accessor // @note: if you encounter problems consider disabling this rule and swap it out // for the commented out one below that just takes in custom properties [/^bg:(?<m>-[a-z]+)?_?(?<v>['"0-9A-Za-z .,\/()\-_!%#]+)$/, ([, m, v]) => ({ [`background${m || ''}`]: v?.replace(/_/g, ' ')?.replace(/(?<!var\()(?<v>--\S+)/, `var(${v})`) })], // background[-prop]: variable/custom property interpolation, use like `bg:-image--var-name` // @note: swap with rule above if that one creates problems, it does this as well // [/^bg:(?<m>-[a-z]+)?_?(?<v>--.*|none)$/, ([, m, v]) => ({ // [`background${m || ''}`]: /^none$/.test(v) ? 'none' : `var(${v})` // })], // background-color: Use custom property syntax for actual colors like `bg:--c-green` [/^bg:(?<v>current|transparent)$/, ([, v]) => ({ 'background-color': v?.replace('current', 'currentColor') })], // background-position: we only support the 1 value version with utils /* TODO: think about implementing these modifiers from the SCSS version @include modifiers(( 'b': 'bottom', 'c': 'center', 'l': 'left', 'lb': 'left bottom', 'lt': 'left top', 'r': 'right', 'rb': 'right bottom', 'rt': 'right top', 't': 'top', ), $properties: 'background-position', $prefix: 'pos-', $separator: '-'); */ [/^bg:(?<v>center|top|right|bottom|left)$/, ([, v]) => ({ 'background-position': v })], // background-repeat: we only support the 1 value version with utils [/^bg:(?<v>repeat-x|repeat-y|repeat|space|no-repeat)$/, ([, v]) => ({ 'background-repeat': v })], // background-size: only word values suported with utils [/^bg:(?<v>auto|cover|contain)$/, ([, v]) => ({ 'background-size': v })], // box-shadow [/^bs:(?<p>oi|o)?\/?(?<v>[^\s]+)?$/, ([, p, v]) => ({ // `/([^\s\/]+\([\S]+?\))|\//g` => splits by slash except when they are between parenthesis like `calc(100px/2)` // `b`/`o` and `oi` are shortcuts for box-shadow outlines/borders (they don't affect the box-model), `o` creates // and outline outside of the box, `oi` one that does not go beyond the box-boundaries. This can be useful // for scenarios where you want a border in a certain state but not others and then have to set // `border-color: transparent` on those elements. But box-shadow outlines can't be direction controlled, // so they are only useful if the element should have a border on all sides 'box-shadow': ({ 'o': '0 0 0 ', 'oi': 'inset 0 0 0 ', }[p] || '') + v.split(/([^\s\/]+\([\S]+?\))|\//g).filter((x) => x).map((x) => (/^[\d\.]+$/.test(x) ? `${x}px` : x.replace(/(?<!var\()(--\S+)/, `var(${x})`))).join(' ') })], // cursor: just make sure you got the value right, unrestricted for brevity sake [/^c:(?<v>\S+)$/, ([, v]) => ({ 'cursor': v })], // display: There's a lot of different utilities summarized under the d: prefix, see preflights // still working on figuring out the best way to implement them as some might still require // browser prefixes, or target multiple properties etc. [/^(?<p>bg|d):(?<filter>blur|brightness|contrast|drop-shadow|grayscale|hue-rotate|invert|opacity|saturate|sepia)\(?(?<v>calc\([\S]+?\)|[^\s)]+)?\)?$/, ([, p, filter, v]) => ({ [`${p === 'bg' ? 'backdrop-' : ''}filter`]: `${filter}(${v?.startsWith('calc') ? v : v?.replace(/\//g, ' ') || ''})` })], ['d:b', { 'display': 'block' }], // `d:c(<name>/<type>)` || `d:c(<name>)` || `d:c` // https://developer.mozilla.org/en-US/docs/Web/CSS/container [/^d:c(?:\((?<n>[^\s\/]+)\/?(?<t>[^\s\/]+)?\))?$/, ([, n, t]) => ({ 'container': `${n || 'x'} / ${t || 'inline-size'}` })], ['d:i', { 'display': 'inline' }], ['d:ib', { 'display': 'inline-block' }], ['d:none', { 'display': 'none' }], ['d:hide', { 'visibility': 'hidden' }], ['d:show', { 'visibility': 'visible' }], ['d:invisible', { 'border': '0', 'clip': 'rect(1px, 1px, 1px, 1px)', 'height': '1px', 'outline': 'none', 'overflow': 'hidden', 'padding': '0', 'position': 'absolute', 'width': '1px', }], /* 1 */ ['d:f', { 'display': 'flex' }], ['d:fi', { 'display': 'flex inline' }], ['d:f-col', { 'flex-direction': 'column' }], ['d:f-row', { 'flex-direction': 'row' }], ['d:f-wrap', { 'flex-wrap': 'wrap' }], ['d:fc-items-center', { 'align-items': 'center' }], ['d:fc-justify-between', { 'justify-content': 'space-between' }], ['d:fc-justify-center', { 'justify-content': 'center' }], // If functions are used the value wrapping with () is mandatory! `d:f(col/1/0/calc(3px*100))` [/^d:f\(?(?<dir>col|row)?\/?(?<grow>[\d.]+)?\/?(?<shrink>[\d.]+)?\/?(?<basis>[\S]+)?\)+?$/, ([, dir, grow, shrink, basis]) => ( Object.entries({ 'flex-direction': { col: 'column', row: 'row' }[dir] || false, 'flex-grow': grow || false, 'flex-shrink': shrink || false, 'flex-basis': /^[\d\.]+$/.test(basis) ? `${basis}px` : (basis || '').replace(/(?<!var\()(?<v>--\S+)/, `var(${basis})`) || false, }).reduce((rules, [prop, value]) => { if ( value ) { rules[prop] = value; } return rules; }, {}) )], // order matters here, needs to be below the general `d:f` rule [/^d:f-(?<v>none|auto|initial)$/, ([, v]) => ({ 'flex': v })], // grid/flexbox gap, be careful though when using `d:g` it needs the gap defined within it's values // to properly calculate column width! so specify gap there if you use `d:g(<values>)`, it's fine to // use `d:gap` if you use `d:g` (without values) and define your template-columns yourself via CSS [/^d:gap(?<row>calc\([\S]+?\)|[^\s\/]+)?\/?(?<col>[^\s]+)?$/, ([, row, col]) => ( Object.entries({ 'row-gap': col ? row : false, 'column-gap': row ? col : false, 'gap': (row && !col) ? row : false, }).reduce((rules, [prop, value]) => { if ( value ) { rules[prop] = /^[\d\.]+$/.test(value) ? `${value}px` : value.replace(/(?<!var\()(?<v>--\S+)/, `var(${value})`); } return rules; }, {}) )], // flexbox/grid align and justify utils to be used like: // `.d:jself(center) or .d:j-self(center) or .d:js(center) or .d:aitems(start) or .d:a-items(start) or .d:ai(start)` [/^d:(?<d>a|j)-?(?<scope>c|i|s|t|content|items|self|tracks)[\(-](?<v>\S*?)\)?$/, ([, d, scope, v]) => ( { [`${{ a: 'align', j: 'justify' }[d]}-${{ c: 'content', i: 'items', s: 'self', t: 'tracks' }[scope] || scope}`]: v } )], // grid: format `d:g` to just set the display property, `d:g(<template-cols>/<gap/row-gap>?/<col-gap>?/<template-rows>?)` // TODO: Keep an eye on `grid-template-rows: masonry` support, it would be awesome, but is not supported as of 2023 // it's technically implemented as the 4th via `grid-template-rows`: That one is a bit different, can't do it with // optional gap values, so even if no gaps should be defined, it needs to be written like `d:g(12/0/0/masonry)` // there is a special value `equal` that will make all rows equal height to the tallest one: `d:g(12/0/0/equal)` // the 4th value is basically a free-for-all, you can go crazy with stuff like `repeat(auto,minmax(calc(100vh/3),1fr)))` // to define the grid-template-rows, I use it rarely, but it's there if needed. [/^d:(?<p>g|gi)(?:\((?<v>[^\s]+)?\))?$/, ([, p, v], ctx) => ( ({ 'g': [ ['display', 'grid'] ], 'gi': [ ['display', 'inline-grid'] ], }[p] || []).reduce((rules, [prop, value]) => { if ( value ) { //console.log('rule matcher args', p, v, ctx); // set the display property rules[prop] = value; // parse the grid config values and inject additional properties into the outer reduced array // depending on how many config values were provided, do not give unit to first value automatically // as it is the number of grid-columns and needs to be a unitless value for the internal calc(), it // can be a custom CSS property/variable or calc() resolving in a unitless number itself though, // meaning: `d:g(--var-cols/12px/calc(100px/4))` or `d:g(calc(48/4)/12px/calc(100px/4))` are fine. // Does it make sense to calc() within calc()? not sure... but it's possible ;). // If the first value has a unit, no automatic cols calculation will happen, but the value will // be used as a min-width of a grid-column letting the browser do the heavy lifting, this can be // very helpful not having to define any kind of @media queries for responsiveness! // `d:g(264px)` => `grid-template-columns: repeat(auto-fit, minmax(264px, 1fr));` // auto-fill vs auto-fit: https://css-tricks.com/auto-sizing-columns-css-grid-auto-fill-vs-auto-fit/ if ( v ) { v = v.split(/\/?(?<v>[a-z\-]+\([\S]+?\)(?![,\)])+|[^\s\/]+)\/?/g).filter(x => x).map((x, i) => /^[\d\.]+$/.test(x) && i !== 0 ? `${x}px` : x.replace(/(?<!var\()(?<v>--\S+)/, `var(${x})`)); // The last value can be 'fit' or 'fill' regardless of position to select auto-filling algo const f = /(fit|fill)/.test(v.at(-1)) ? v.pop() : 'fit'; const props = ({ 1: [['grid-template-columns', `repeat(auto-${f}, minmax(${/[\d\.]+[a-z%]+$/.test(v[0]) ? v[0] : `calc(100%/${v[0]})`}, 1fr))`]], 2: [['grid-template-columns', `repeat(auto-${f}, minmax(${/[\d\.]+[a-z%]+$/.test(v[0]) ? v[0] : `calc(100%/${v[0]} - ${v[1]})`}, 1fr))`], ['gap', v[1]]], 3: [['grid-template-columns', `repeat(auto-${f}, minmax(${/[\d\.]+[a-z%]+$/.test(v[0]) ? v[0] : `calc(100%/${v[0]} - ${v[2]})`}, 1fr))`], ['row-gap', v[1]], ['column-gap', v[2]]], 4: [['grid-template-columns', `repeat(auto-${f}, minmax(${/[\d\.]+[a-z%]+$/.test(v[0]) ? v[0] : `calc(100%/${v[0]} - ${v[2] || 0})`}, 1fr))`], ['row-gap', v[1]], ['column-gap', v[2]], ( v[3] === 'equal' ? ['grid-auto-rows', '1fr'] : ['grid-template-rows', ( v[3] ? v[3].replace(/_/g, ' ') : null )] )], }[Math.min(v.length, 4)] || []).forEach(([_p, _v]) => (rules[_p] = _v)); } } return rules; }, {}) )], [/^d:(?:g-?)?(?<p>col|row)\(?(?<v>\S+?)\)?$/, ([, p, v]) => ({ [`grid-${p === 'col' ? 'column' : 'row'}`]: v.replace(/_/g, ' ') })], // grid col/row span // regex captures everything after `d:span<v>` (or within parenthesis `d:span(<v>)`), syntax is: // variant a): `d:span<v[<col[<start>-<end?>]>/<row?[<start>-<end?>]>]>` // variant b): `d:span(<v[<col[<start>-<end?>]>/<row?[<start>-<end?>]>]>)`, // we deal with the value inside the matcher function and split it into it's components // TODO: Re-evaluate if the separate util is worth it as we can simply use `.d:g-col(<v>` and `.d:g-row(<v>)` [/^d:(?:g-?)?span\(?(?<v>(?:[a-z\-]+?\([\S]+?\)|[^\s\(\)]+?)+?)\)?$/, ([, v]) => ( (v || '') .split(/\/?(?<v>[a-z\-]+\([\S]+?\)+(?![,\)_])|[^\s\/]+)\/?/g) .filter(x => x.trim()) // don't need the auto-pixelator here, span values are unitless, but we support CSS vars //.map((x, i) => /^[\d\.]+$/.test(x) ? `${x}px` : x.replace(/(?<!var\()(--[^\s,/]+)/g, 'var($1)').replace(/_/g, ' ')) .map((x, i) => x.replace(/(?<!var\()(--[^\s,/]+)/g, 'var($1)')) .reduce((rules, value, i) => ({ ...rules, ...({ // if useing CSS vars, specify the full value, like this it's most flexible 0: { 'grid-column': value.includes('var(') ? value : value.split('-').map((x, i, a) => a.length === 1 ? `span ${x}` : x ).join(' / ') }, 1: { 'grid-row': value.includes('var(') ? value : value.split('-').map((x, i, a) => a.length === 1 ? `span ${x}` : x ).join(' / ') }, }[i] || {}) }), {}) )], ['d:noverflow', { 'overflow': 'hidden' }], // order (flexbox/grid), syntax: `d:order(?-?<v>)?` [/^d:order\(?(?<v>[-\d]+)\)?$/, ([, v]) => ({ 'order': v })], // display => overflow(-[x|y]) auto [/^d:scroll(?<m>-[xy])?$/, ([, m]) => ({ [`overflow${m || ''}`]: 'auto' })], ['d:scroll', { 'overflow': 'auto' }], ['d:scroll(x)', { 'overflow-x': 'auto' }], ['d:scroll(y)', { 'overflow-y': 'auto' }], // it's a bit tricky to yield those ::selectors, but this is how it can be done... // question is: should we just preflight those as raw CSS instead of doing constructs like this? // the strings ship with the bundle, one way or the other, but as preflights at least the utils // are readable... and after all, there's nothing dynamic about them, simply creating overhead here // futhermore it's questionable to do it like that as it will most likely result in more // characters/bytes shipped than if it was just pure CSS... [/^(?<sel>d:scroll-nobar)$/, ([, sel], context) => { return `${context.constructCSS({ 'scrollbar-width': 'none', /* Firefox */ '-ms-overflow-style': 'none', /* IE 10+ */ })}\n.${CSS.escape(sel)}::-webkit-scrollbar { width: 0; height: 0; }` /* WebKit */ }], // pointer-events [/^e:(?<v>\S+)$/, ([, v]) => ({ 'pointer-events': v })], // list-style: order of values is type | image | position, escape quotes with ^ [/^l:(?<v>.{3,}?)?(?:_(?<url>.+?))?(?:_(?<pos>outside|inside))?$/, ([, v, url, pos]) => ({ 'list-style': `${v?.replaceAll('^', '"')} ${url || ''} ${pos || ''}` })], // margin and padding: supports basics plus custom properties (variables), optional directions and global word values [/^(?<p>m|p):(?<d>r(?!e)|[ltbxy]|[bi][se])?(?<v>(?:(?:-(?!-))?[\d._]+)|--\S+|calc\(.*?\))?(?<u>[a-zA-Z%]+)?$/, ([, p, d, v, u]) => ( { undefined: [''], t: ['-top'], r: ['-right'], b: ['-bottom'], l: ['-left'], x: ['-right', '-left'], y: ['-top', '-bottom'], is: ['-inline-start'], ie: ['-inline-end'], bs: ['-block-start'], be: ['-block-end'], }[d].reduce((rules, dir) => { // global values like auto, initial, revert will be captured by the <u> group // TODO: not sure what the `.replaceAll('_', ' ')` is for, maybe a copy paste leftover from another rul? rules[`${{m: 'margin', p: 'padding'}[p]}${dir}`] = `${v?.replaceAll('_', ' ')?.replace(/(?<!var\()(?<v>--\S+)/, `var(${v})`) || ''}${u || (!v?.startsWith('--') && !v?.includes('calc') ? 'px' : '')}`; return rules; }, {}) )], // opacity // technically the spec allows for percantage values, but we convert anything to float, makes things easier [/^o:(?<v>[\d.%]+|--\S+|calc\(.*?\))?$/, ([, v]) => ({ 'opacity': isNaN(parseFloat(v)) ? (v?.replace(/(?<!var\()(?<v>--\S+)/, `var(${v})`) || '') : ( parseFloat(v) <= 1 ? parseFloat(v) : parseFloat(v)/100 ) })], // position utils [/^(?<p>pos):(?<d>r(?![e]|$)|[ltb])?(?<v>[\S]+)?$/, ([, p, d, v]) => ( { 'a': [ ['position', 'absolute'] ], 'f': [ ['position', 'fixed'], ['backface-visibility', 'hidden'] ], 'r': [ d ? ['right', null] : ['position', 'relative'] ], 's': [ ['position', 'sticky'] ], 'center': [ ['position', 'absolute'], ['top', '50%'], ['left', '50%'], ['transform', 'translate(-50%, -50%)'] ], 'center-x': [ ['position', 'relative'], ['left', '50%'], ['transform', 'translateX(-50%) perspective(1px)'] ], 'center-y': [ ['position', 'relative'], ['top', '50%'], ['transform', 'translateY(-50%) perspective(1px)'] ], 'reset': [ ['position', 'static'] ], // direction utils, set the value falsy so we can set it within reduce 't': [ ['top', null] ], 'b': [ ['bottom', null] ], 'l': [ ['left', null] ], undefined: [], }[d || v].reduce((rules, [prop, value]) => { if ( value ) { rules[prop] = value; } else if ( d && v ) { rules[prop] = /^[\d\.]+$/.test(v) ? `${v}px` : v.replace(/(?<!var\()(?<v>--\S+)/, `var(${v})`); } return rules; }, {}) )], /** /* FLUID MEDIA CONTENT UTILS (IMG, VIDEOS, IFRAMES etc.) /* 1. Element will be stretched to the full extend of the nearest realtively-positioned /* ancestor. /* 2. Element will be stretched to the entire viewport and follow the user's scrolling, /* good for modal windows and overlays /* 3. Add this class to the element that contains the fluid media content and do not forget /* to define the aspect ratio like `.scale:frame(16/9)`. /* 4. Add this class to the element that should scale in a specific ratio, useful for /* fluid videos, iframes (maps anybody?), embeds but also images. /* 5. Allows an image to be responsvie up to its container width but not exceeding /* it's native size. /*/ [/^scale:(?<v>[^\s(]+)\(?(?<ratio>[\d\/]+)?\)?$/, ([, v, ratio]) => ( { 'fit': [ ['bottom', '0'], ['left', '0'], ['margin', 'auto'], ['position', 'absolute'], ['right', '0'], ['top', '0'] ], /* 1 */ 'fullscreen': [ ['backface-visibility', 'hidden'], ['bottom', '0'], ['left', '0'], ['margin', 'auto'], ['position', 'fixed'], ['right', '0'], ['top', '0'] ], /* 2 */ 'frame': [ ['display', 'block'], ['position', 'relative'], ['aspect-ratio', (ratio ? `${ratio}` : null) ] ], /* 3 */ // The padding hack is old-school now, we now have native aspect ratio, but for reference // I'll leave those here anyways, the calculation goes as follows: `9 / 16 * 100% = 56.25%` // So if a custom aspect ratio is needed one could simply apply `p:tcalc(2/12*100%)` //'frame(16/9)': [ ['padding-top', '56.25%'] ], //'frame(3/2)': [ ['padding-top', '66.66666%'] ], //'frame(4/3)': [ ['padding-top', '75%'] ], 'content': [ ['bottom', '0'], ['left', '0'], ['margin', 'auto'], ['position', 'absolute'], ['right', '0'], ['top', '0'] ], /* 4 */ 'img': [ ['display', 'block'], ['height', 'auto'], ['max-width', '100%'] ], /* 5 */ undefined: [], }[v].reduce((rules, [prop, value]) => { if ( value ) { rules[prop] = value; } return rules; }, {}) )], /** /* TEXT UTILS /* /* 1. Enables font kerning in all browsers. /* see also: http://blog.typekit.com/2014/02/05/kerning-on-the-web/ /* 2. Ensure that the node has a maximum width after which truncation can occur. /* 3. Fix for IE 8/9 if 'word-wrap: break-word' is in effect on ancestor nodes. /* 4. A little helper to increase the font-weight slightly without having to rely /* on font-style. Especially useful to increase readability of very small type! /*/ // This one is a bit special, it's basically able to set a variety of properties if specified, the pattern is like // `t:<color>/<font-size>/<line-height>/<font-weight>/<font-style>/<font-family>` if you want to omit certain values, // you need the slash (/), but with no value in between, so let's say you only want to set the font family, // you would do `t://///Arial`, only the font-weight: `t:///700`, only the color: `t:red`, all properties // support CSS custom properties and properties that have numeric values support also `calc()`, e.g. `t:--color-red` // and `t:blue/calc(100px/5)/calc(100px/4)` // TODO: support letter-spacing here or in separate utility? // TODO: support other functions than calc(), clamp() in particular, but basically all CSS math functions [/^(?<p>t):(?<color>[^\/\s:@]*)?\/?(?<size>[\d.]+|--[^\/\s:@]+|calc\(.*?\))?(?<u>[a-zA-Z%]+)?\/?(?<lh>[\d.]+|--[^\/\s:@]+|calc\(.*?\))?(?<lhu>[a-zA-Z%]+)?\/?(?<weight>[\d.]+|--[^\/\s:@]+|calc\(.*?\))?\/?(?<style>[^\/\s:@]*)?\/?(?<family>[^\/\s:@]*)?$/, ([, p, color, size, u, lh, lhu, weight, style, family]) => ( Object.entries({ 'color': color ? `${color?.replace(/(?<!var\()(?<v>--\S+)/, `var(${color})`) || ''}` : false, 'font-size': size || size === '0' ? `${size?.replace(/(?<!var\()(?<v>--\S+)/, `var(${size})`) || ''}${u || (!size?.startsWith('--') && !size?.includes('calc') ? 'px' : '')}` : false, 'line-height': lh || lh === '0' ? `${lh?.replace(/(?<!var\()(?<v>--\S+)/, `var(${lh})`) || ''}${lhu || ''}` : false, 'font-weight': weight ? `${weight?.replace(/(?<!var\()(?<v>--\S+)/, `var(${weight})`) || ''}` : false, 'font-style': style ? `${style?.replace(/(?<!var\()(?<v>--\S+)/, `var(${style})`) || ''}` : false, 'font-family': family ? `${family?.replace(/(?<!var\()(?<v>--\S+)/, `var(${family})`) || ''}` : false, }).reduce((rules, [prop, value]) => { if ( value ) { rules[prop] = value; } return rules; }, {}) )], // font-family // This utility isn't really meant to define full font-stacks, you may try to use the general utility // like `t://///Arial` even though it's questionable to use like that. By default it just applies the global CSS // keywords for font-family, including such as ui-serif etc. which are not supported by browsers today // the power of this one is that it will also look for pre-defined custom CSS properties/variables at // :root level with the specific pattern of `---t-family-<keyword>`, if found it will use the variable // value. In this way font stacks can easily be defined via such variables. Besides the standrad spec // keywords, it also supports some custom short ones: sans => sans-serif, mono => monospace, ui => system-ui // TODO: Make variable names configurable via theme or at least the prefix and allow passing in the specific // `--t-family-<name>` variable via CSS class as well, like this we could reference full font stacks without // making things too ugly? How can we access theme within rules? [/^t:(?<v>serif|sans-serif|sans|monospace|mono|cursive|fantasy|system-ui|ui|ui-serif|ui-sans-serif|ui-monospace|ui-rounded|emoji|math|fangsong)$/, ([, v]) => ({ 'font-family': !!getComputedStyle(document.documentElement).getPropertyValue(`---t-family-${v}`) ? `var(---t-family-${v})` : v.replace(/^(sans|mono|ui)$/gi, x => ({'sans': 'sans-serif', 'mono': 'monospace', 'ui': 'system-ui'}[x])) })], // font-style // Shortcuts for what is doable with the 'general' utility above as well, the key word `normal` // is already used by the font-weight utility below, so `t:regular` is used to set `font-style: normal` // specifying the angle for `font-style: oblique 20deg` is supported as well with `t:oblique/20` // valid angle values are +-90deg, but most fonts don't support it anyways... [/^t:(?<v>italic|oblique|regular)\/?(?<deg>-?\d{1,2})?$/, ([, v, deg]) => ({ 'font-style': `${v.replace('regular', 'normal')}${deg ? ` ${deg}deg`: ''}` })], // font-weight // Those are shortcuts which can all be achieved by using the above 'general' utility as well, but they // are still included for convenience, lighter/bolder are relative-to-parent font-weights (see MDN) // this util has a feature where it looks for custom CSS properties (variables) defined at `:root` level // that match the specific pattern of `---t-weight-<word|weight>`, if found, it will use those instead of // the actual word or numeric weight, this allows to re-define what those words/weights mean in terms of // actual font-weight, e.g. if default for `font-weight: normal` is 400, if you set `---t-weight-normal: 300` // at `:root` level and use the utility `t:normal` on some element, it's font-weight will now be 300 // these custom CSS properties/variables are intentionally prefixed with 3! dashes (---) to hopefully avoid // any conflicts with 'regularly' defined custom CSS variables which accidentially have the same name! [/^t:(?<v>lighter|bolder|thin|normal|bold|heavy|\d00)$/, ([, v]) => ({ 'font-weight': !!getComputedStyle(document.documentElement).getPropertyValue(`---t-weight-${v}`) ? `var(---t-weight-${v})` : v.replace('thin', '100').replace('heavy', '900') })], // `t:boldest` applies a text-shadow which adds to the visual boldness of text regardless of font-weight applied ['t:boldest', { 'text-shadow': '0 0 0.3px currentColor' }], // text-align: we only support the 1 value version with utils, the string thing seems fringe anyways // additionally we convert left/right 'absolute' values to start/end which is text-direction aware // `justify-all` and `match-parent` are in the spec but seem very poorly supported! [/^t:(?<v>start|end|left|right|center|justify|justify-all|match-parent)$/, ([, v]) => ({ 'text-align': v.replace('left', 'start').replace('right', 'end') })], // text-decoration // We don't support multiple text-decoration-line values, if you really need that, specify it via multiple classes // text-decoration-skip is also not supported as browser support is 0, but text-decoration-skip-ink is supported // There is a shortcut `t:del` which translates to `t:line-through` automatically. // `blink` is deprecated and only Opera and Safari still support it...We don't, it's bullshit UX anyways... // A text-decoration-line value has to be defined, otherwise this util won't catch, the other additional properties // can be omitted, the format is `t:<decoration-line>/<style>/<color>/<thickness>/<skip-ink> [/^t:(?<line>none|underline|overline|line-through|del)\/?(?<style>solid|double|dotted|dashed|wavy|--[^\/\s:@]+)?\/?(?<color>[^\/\s:@]*)?\/?(?<width>[\d.]+|--[^\/\s:@]+|calc\(.*?\))?(?<u>[a-zA-Z%]+)?\/?(?<skip>none|auto|all|--[^\/\s:@]+)?$/, ([, line, style, color, width, u, skip]) => ( Object.entries({ 'text-decoration-line': line ? `${line?.replace(/(?<!var\()(?<v>--\S+)/, `var(${line})`).replace('del', 'line-through') || ''}` : false, 'text-decoration-style': style ? `${style?.replace(/(?<!var\()(?<v>--\S+)/, `var(${style})`) || ''}` : false, 'text-decoration-color': color ? `${color?.replace(/(?<!var\()(?<v>--\S+)/, `var(${color})`) || ''}` : false, 'text-decoration-thickness': width || width === '0' ? `${width?.replace(/(?<!var\()(?<v>--\S+)/, `var(${width})`) || ''}${u || (!width?.startsWith('--') && !width?.includes('calc') ? 'px' : '')}` : false, 'text-decoration-skip-ink': skip ? `${skip?.replace(/(?<!var\()(?<v>--\S+)/, `var(${skip})`) || ''}` : false, }).reduce((rules, [prop, value]) => { if ( value ) { rules[prop] = value; } return rules; }, {}) )], // text-tranform [/^t:(?<v>capitalize|caps|lowercase|lcase|uppercase|ucase)$/, ([, v]) => ({ 'text-transform': v.replace('caps', 'capitalize').replace('lcase', 'lowercase').replace('ucase', 'uppercase') })], ['t:break', { 'word-wrap': 'break-word' /* 4 */ }], ['t:nowrap', { 'white-space': 'nowrap' }], // TODO: not sure if this even does something visually, what is it's purpose? remove it? ['t:kern', { '-webkit-font-feature-settings': '"kern" 1', 'font-feature-settings': '"kern" 1', '-webkit-font-kerning': 'normal', 'font-kerning': 'normal', 'text-rendering': 'optimizeLegibility' }], // 1 ['t:truncate', { 'max-width': '100%' /* 2 */, 'overflow': 'hidden', 'text-overflow': 'ellipsis', 'white-space': 'nowrap', 'word-wrap': 'normal' /* 3 */ }], // transform // transform function values are very diverse, just write them as usual separated with commas just NO spaces! // You can use `calc()` or custom css properties `--some-var` if they are allowed to be used for function values // we can't cast non-pixel values to pixels either, because different functions have different inputs, like `scale()` // for example expects unitless values, so for the values, you do need to specify the unit if appropriate // Does NOT support specifying multiple transform functions in one call, write proper CSS for a use-case like that! // Does support transform-origin/box/style with the appropriate prefix `b|o|s` e.g. `.tr:o(center/50px)` // note that the classname consistently has to encapsulate the value in parenthesis, e.g. `.tr:rotate(120deg)`, but // also `.tr:o(--some-var/calc(100px/2))` => `transform-origin: var(--some-var) calc(100px/2);` // The order of the transform functions in the RegEx matters, so don't touch! [/^tr:(?<p>[bo]|s(?![ck]))?(?<fn>matrix3d|matrix|none|perspective|rotate3d|rotateX|rotateY|rotateZ|rotate|scale3d|scaleX|scaleY|scaleZ|scale|skewX|skewY|skew|translate3d|translateX|translateY|translateZ|translate)?\((?<v>[^\s]+)?\)$/, ([, p, fn, v]) => ({ [{ b: 'transform-box', o: 'transform-origin', s: 'transform-style', undefined: 'transform', }[p]]: p // `/\/(?![\S]+\))/g` => splits by slash except when they followed by `)` like `calc(100px/2)` ? v.split(/\/(?![\S]+\))/g).map((x) => (/^[\d\.]+$/.test(x) ? `${x}px` : x.replace(/(?<!var\()(--\S+)/, `var(${x})`))).join(' ') : fn.match(/none/) ? fn : `${fn}(${v?.replace(/(?<!var\()(?<v>--\S+)/g, `var(${v})`)})` })], // width & height [/^(?<p>w|h):(?<m>min|max|screen)?(?<v>[\d.]+|--\S+|calc\(.*?\))?(?<u>[a-zA-Z%]+)?$/, ([, p, m, v, u]) => ( { undefined: [''], min: ['min-'], max: ['max-'], screen: [(rules) => (rules[`${{w: 'width', h: 'height'}[p]}`] = `100v${p}`)] }[m].reduce((rules, mod) => { if ( mod instanceof Function ) { mod(rules); return rules; } // global values like auto, initial, revert will be captured by the <u> group // replacing _ with ' ' allows for escaping required spaces in calc +/- operations like calc(100% - 32px) rules[`${mod}${{w: 'width', h: 'height'}[p]}`] = `${v?.replace(/(?<!var\()(?<v>--\S+)/, `var(${v})`).replaceAll('_', ' ') || ''}${u || (!v?.startsWith('--') && !v?.includes('calc') ? 'px' : '')}`; return rules; }, {}) )], [/^z:(?<v>.+)$/, ([, v]) => ({ 'z-index': v.replace(/(?<!var\()(?<v>--\S+)/, `var(${v})`) })], // debug/dev tools, simply add the class `?` to an element and it will make itself very visible ;) // It might very often be the case that you need to overrule existing styles that conflict with the // dev tool class, so just prefix with `!` to make everything !important. [/^(\?)$/, (_, { constructCSS, generator }) => ( `@keyframes __imhere{0%{box-shadow:inset 0 0 0 2px red}100%{box-shadow:inset 0 0 0 6px yellow}}\n${constructCSS({ animation: '__imhere 0.5s ease-in-out alternate infinite' })}` )], // create a box around an element, good for highlighting stuff // arrows can be useful for screenshots, try it out with `?[]>`, `?[]^`, `?[]<`, `?[]>t` etc. [/^(?<sel>\?\[(?<inset>[0-9-]+)?\/?(?<width>\d+)?\](?:(?<arrow>\>|\<|\^)(?<top>t)?)?\/?(?<hue>\d+)?\/?(?<opacity>[\d.%]+)?)$/, ([, sel, inset, width, arrow, top, hue, opacity], ctx) => ( `${ctx.constructCSS({ overflow: 'visible !important', position: 'relative', })}${ arrow ? `\n.${CSS.escape(sel)}:before { color: hsl(${hue || 0} 100% 50%) !important; content: '↗' !important; font-size: 36px !important; font-weight: 300 !important; line-height: 1 !important; position: absolute; ${top ? `top: -${Math.abs(inset || 0)}px` : `bottom: -${Math.abs(inset || 0)}px`}; ${arrow === '<' ? `right: -${Math.abs(inset || 0)}px` : `left: ${ arrow === '^' ? '50%' : `-${Math.abs(inset || 0)}px` }`}; transform: translate(${arrow === '<' ? '150%' : `${arrow === '^' ? '-50%' : '-150%'}`}, ${ top ? '-125%' : '125%'}) ${arrow === '<' ? 'rotate(-90deg)' : arrow === '^' ? 'rotate(-45deg)' : ''} ${ top ? `scale(${arrow === '<' ? '-1,1' : arrow === '^' ? '-1' : '1,-1'})` : ''}; z-index: 9999 !important; }` : ''}\n.${CSS.escape(sel)}:after { box-shadow: inset 0 0 0 ${Math.abs(inset || 0)}px hsl(${hue || 0} 100% 50% / ${opacity || .1}) !important; content: '' !important; inset: ${inset || 0}px !important; margin: 0 !important; padding: 0 !important; outline: ${width || 1}px solid hsl(${hue || 0} 100% 50%) !important; position: absolute !important; transform: none !important; z-index: 9999 !important; }` )], // and the dev grid overlay [/^(?<sel>\?#\(?(?<s>\d+)?\/?(?<o>[\d.]+)?\/?(?<h>[\d]+)?(?<r>r)?\)?)$/, ([, sel, s, o, h, r], ctx) => ( `${ctx.constructCSS({ 'position': 'relative', })}\n.${CSS.escape(sel)}:before { background-image: linear-gradient(hsl(${ h ? h : '0'} 100% ${ h ? '50%' : '0%'} / ${ o ? o : '.12'}) 1px, transparent 1px), linear-gradient(90deg, hsl(${ h ? h : '0'} 100% ${ h ? '50%' : '0%'} / ${ o ? o : '.12'}) 1px, transparent 1px); /*background-position: -1px -1px, -1px -1px;*/ background-size: ${ s ? `${s}px ${s}px, ${s}px ${s}px` : '24px 24px, 24px 24px'}; box-shadow: inset 0 0 0 1px hsl(${ h ? h : '0'} 100% ${ h ? '50%' : '0%'} / ${ o ? o : '.12'}); content: ''; position: absolute; top: 0; bottom: 0; left: 0; right: 0; width: 100%; z-index: 10000; }\n.${CSS.escape(sel)} > [class*="${CSS.escape('b:')}"] { border-color: red !important; border-radius: 0 !important; }\n.${CSS.escape(sel)} > :not([class*="${CSS.escape('b:')}"]) { border-radius: 0 !important; box-shadow: inset 0 0 0 1px red !important; }${ r ? `\n.${CSS.escape(sel)}:after { background-image: linear-gradient(hsl(0 100% 0% / 1) 1px, transparent 1px), linear-gradient(90deg, hsl(0 100% 0% / 1) 1px, transparent 1px), linear-gradient(90deg, hsl(0 100% 0% / 1) 1px, transparent 1px), linear-gradient(-90deg, hsl(0 100% 0% / 1) 1px, transparent 1px); background-position: left bottom; background-repeat-y: no-repeat; background-size: ${ s ? `${s/2}px 6px, ${s/2}px 6px, ${s}px 11px, 100% 11px` : '6px 6px, 6px 6px, 12px 11px, 100% 11px'}; content: attr(data-width); display: block; font-size: 10px; line-height: 1; padding-bottom: 16px; position: absolute; top: 0; left: 0; text-align: center; transform: translateY(-125%); width: 100%; }` : '' }` )], ], shortcuts: [ // you could still have object style /*{ 'forum-nav-bar': '!bg:cyan', },*/ // dynamic shortcuts //[/^btn-(.*)$/, ([, c]) => `bg-${c}-400 text-${c}-100 py-2 px-4 rounded-lg`], ], variants: [ // Allows targeting child/sub elements of the element the util is applied to // Any valid combinator (or none) are supported, just add the child selector between pipes `|` // and add the util after, give that class to the wrapping element of whatever should be targeted // this helps a lot for use-cases where every child (imagine a ul>li structure) should get the // same styles, with regular utils, every li has to have all util classes, which is very // redundant and one of the major downsides of utility-based CSS approaches. With the help of this // variant this is a thing of the past as only one class has to be defined on the parent targeting // any and all decendent nodes with the appropriate selector. // `|>li|bg:red` => `.\|\>li\|bg\:red > li { background-color: red; }` { name: 'combinators', match: (matcher) => { const rx = /^\|(?<combinator>[>+~])?(?<selector>\S+)\|(?<util>\S+)$/; if ( !rx.test(matcher) ) { return matcher; } const { groups: { combinator, selector, util } } = matcher.match(rx); // the combinator is optional, but makes no sense to continue without selector or util if ( !selector || !util ) { return matcher; } //console.log('Found combinator match', `(selector) ${combinator} ${selector}`, util); return { matcher: util, selector: (s) => `${s} ${combinator || ''} ${selector}`, } }, multipass: false, //order: -1, }, // Targets basically every advanced CSS selector and pesudo content if they are prefixed with a colon `:` // this is very powerful as it allows targeting stuff like `:not(:last-child)` purely through CSS // classes, like `.b:b1/red::not(:last-child)` or `.t:bold::after` the double colon separator is needed // so we can actually do stuff like `:not(:last-child)`, which wouldn't work with a single colon (or put // differently: I'm too dumb to figure out the regex to do that!) // TODO: Figure out why those don't work in conjunction with combinator variants { name: 'pseudo', match: (matcher, ctx) => { const rx = /^[^:|]+:[^:]+(?<pseudoclass>:\S+)$/; //if ( !/:(:.+)$/.test(matcher) ) { return matcher; } if ( !rx.test(matcher) ) { return matcher; } const { groups: { pseudoclass } } = matcher.match(rx); // You can define any custom pseudo-classes and their selector interpolations here const custom = function(pc, s) { return { ':hocus': `${s}:hover, ${s}:focus`, ':hocus-within': `${s}:hover, ${s}:focus-within`, }[pc]; }; //console.log('Pseudoclass match found', matcher, pseudoclass); return { // slice pseudo-class and pass to the next variants and rules matcher: (pseudoclass ? matcher.slice(0, -(pseudoclass.length)) : matcher), selector: (s) => (pseudoclass ? custom(pseudoclass, s) || `${s}${pseudoclass}` : s), } }, // doesn't really work yet, probably my fault (regex?), not that important, specify two classes meanwhile multipass: false, //order: -1, }, // Converts `©(width>500px)` to `@container(width>500px)` to be dealt with by the atrule variant // => meh: I prefer the shortcut atrules like @c, @s, @l... /*{ name: '©rules', match: (matcher, ctx) => { if ( !matcher.includes('©') ) { return matcher; } return { matcher: matcher.replace(/©/g, '@container'), } }, multipass: false, },*/ // @rules { name: '@rules', match: (matcher, ctx) => { const rx = /^\S+:\S+(?<atrule>@[^:]+)/; if ( !rx.test(matcher) ) { return matcher; } const { groups: { atrule } } = matcher.match(rx); /** /* We want to support a variety of @rules, not all make sense to be specified via classes /* but the goal is to support: /* - pre-configured theme breakpoints which are mapped to @media, they are simply a string /* with the exact syntax of a regular CSS media query, this allows for complex breakpoints /* with logical operators etc. without creating a massive parsing overhead here. /* - on-the-fly evaluated breakpoints to be specified like CSS 4 range queries: /* `@(<|<=|=|>=|>)<number><unit>(width|height)(<|<=|=|>=|>)<number><unit>` supporting `width` and `height` /* There's a limit on how far I think it makes sense to go with supporting the offical spec /* it would get highly complex to implement all of it, so for now as we want to translate /* those expressions into the wider supported min-<prop> max-<prop> syntax, once browser support /* for range syntax is not that recent anymore, it should be easy to simply evaluate and forward /* complex range syntax queries with multiple conditions and operators. An implementation detail /* worth noting is the use of `=` (which usually makes 0 sense for media queries, who wants to /* specify an exact pixel value where styles apply?) to communicate values for props, so for example /* you'd specify the orientation feature (normally `@media (orientation: landscape)`) like /* `@orientation=landscape` or `@media(orientation=landscape)` or `@(orientation=landscape) (all valid) /* - @supports(display=grid) => https://css-tricks.com/how-supports-works/ /* - @layer(name), /* - @container((<|<=|=|>=|>)<number><unit>(width|height)(<|<=|=|>=|>)<number><unit>), /* => if none of those 3 keywords are found, @media, is assumed by default, as values can /* contain colons (:) we 'escape' those within the CSS class names with `=` as the colon /* is already used for pseudo classes this goes also for stuff like `@supports(selector(=last-child))` /* and is then translated to `@supports (selector(:last-child))` /*/ function transform(atrule) { //console.log('transform', atrule, ctx.theme.breakpoints[atrule], ctx.theme); // Check theme config for matching breakpoint (with or without the @) and return that early if ( ctx.theme.breakpoints[atrule] || ctx.theme.breakpoints[atrule.substring(1)] ) { return `@media ${(ctx.theme.breakpoints[atrule] || ctx.theme.breakpoints[atrule.substring(1)])}`; } let { groups: { at, rule } } = atrule.match(/^@(?<at>media|c|container|l|layer|s|supports)?(?<rule>\S+)$/); // Map @rule shortcuts like @c, @l, @s, those are different from theme breakpoints as they have a value! // => @l(<layer-name>), @s(display=grid), @c(width>618px) at = { c: 'container', l: 'layer', s: 'supports', }[at] || at; const operators = { '&&': () => ') and (', '||': () => '), (', '!': () => '), not all and (', '<': (p, inv) => (!inv ? `max-${p}: ` : `min-${p}: `), '<=': (p, inv) => (!inv ? `max-${p}: ` : `min-${p}: `), '=': () => ':', '>=': (p, inv) => (!inv ? `min-${p}: ` : `max-${p}: `), '>': (p, inv) => (!inv ? `min-${p}: ` : `max-${p}: `), }; let invert = false; let property = 'width'; // Unwarp @rule from parenthesis if it comes in wrapped rule = (rule.startsWith('(') ? rule.substring(1, rule.length-1) : rule); // Deal with simple values with no operators like @320px, prefix so it transforms to `max-width` // this allows something like `bg:red@320` to translate to `@media (max-width: 320px)` rule = rule.replace(/^(?<v>[\d.]+)(?<u>[a-z%]{1,4})?$/, (m, v, u) => `<${v}${u || 'px'}`); // Deal with range syntax, anything else won't be matched, transform into CSS3 conditions // don't check for proper units here, stop being lazy and write them... rule = rule.replace(/(?<left>[^<>=\s]+)?(?<oleft><=|>=|=|<|>)?(?<prop>width|height)(?<oright><=|>=|=|<|>)(?<right>[^<>=\s]+)/, (match, left, oleft, prop, oright, right) => { invert = !!oleft; property = prop; return `${oleft||''}${left||''}${oleft ? '&&' : ''}${oright}${right}`; }); // Use `.split()` with a RegEx and a capturing group to include the separators const parts = rule.split(/([^<>=&|!]+)?(&&|\|\||!|<=|>=|=|<|>)([^<>=&|!]+)/).filter(x => x); const result = parts.reduce((r, v, i, arr) => { if ( operators[v] ) { r.push(operators[v](property, invert)); // Only applies to range syntax, we have to invert the first operator, then reset the var invert = false; } else { // Resolve custom CSS properties/variables, we can't use them in @rules // a bit like https://github.com/WolfgangKluge/postcss-media-variables if ( v.startsWith('--') ) { v = getComputedStyle(document.documentElement).getPropertyValue(v).trim() || `${v}__var-undefined`; } // Cast unitless numeric values to pixels r.push((/^[\d\.]+$/.test(v) ? `${v}px` : v)); } // Add closing parenthesis if we reached the end (i == arr.length-1 && !(at || '').includes('layer')) && r.push(')'); return r; }, ['@', (at || 'media'), `${!(at || '').includes('layer') ? ' (' : ' '}`]).join(''); //console.log(result); return result; } return { // slice @rule and pass to the next variants and rules matcher: matcher.replace(atrule, ''), //selector: (s) => s, handle: (input, next) => { //console.log('input', input, 'next', next); return next({ ...input, parent: `${input.parent ? `${input.parent} $$ ` : ''}${transform(atrule)}`, }) }, } }, multipass: true, }, // Adds !important to a rules resulting CSS if the class is prefixed with !, e.g. `!t:red` { name: 'important', match: (matcher, ctx) => { if ( !matcher.startsWith('!') ) { return matcher; } return { matcher: matcher.slice(1), // body is an array of tuples like `[ [<prop>, <value>], [<prop>, <value>] ]` body: (body) => { body.forEach(([prop, val], i, arr) => { arr[i] = [prop, ( val ? val += ' !important' : val )]; }); return body; }, } }, multipass: false, }, ], preflights: [ { layer: 'recss', //getCSS: async () => (await fetch('my-style.css')).text(), // `:` (colon) needs double escaping when used in template literal! e.g. `<foo>\\:<bar>` getCSS: ({ theme }) => ` /** /* Little pseudo content helpers. /*/ [data-before]:before { content: attr(data-before); } [data-after]:after { content: attr(data-after); } [data-class]:after { content: attr(class); } /** /* Yes, invalid attribute, but no browser cares, useful for easily showing/hiding entire blocks /* based on FreeMarker/JS conditions. Use like <div class="..." if="{somebooleanexpression?c}">. /*/ [if="false"] { display: none !important; } /** /* Use for elements that should only be visible when handled by JavaScript, it's the JS code's /* responsibility to remove this class once it has done whatever it's doing. Useful for /* pre-rendering markup in FreeMarker and then progressively enhance it with JS. /*/ .js--only { display: none !important; } /** TODO: IMPLEMENT THESE DYNAMICALLY! **/ /** /* LAYOUT, DISPLAY & POSITIONING UTILS /* 1. Completely remove from the flow but leave available to screen readers. /* 2. Fix for Firefox bug: an image styled 'max-width:100%' within an /* inline-block will display at its default size, and not limit its width to /* 100% of an ancestral container. /* 3. The space content is one way to avoid an Opera bug when the /* 'contenteditable' attribute is included anywhere else in the document. /* Otherwise it causes space to appear at the top and bottom of the /* element. /* 4. The use of 'table' rather than 'block' is only necessary if using /* ':before' to contain the top-margins of child elements. /* 5. Make sure fixed elements are promoted into a new layer, for performance /* reasons. /* 6. Element will be absolutely centered inside the nearest relatively-positioned ancestor. /* 7. Element will be centered horizontally regardless of width. Setting 'transform: perspective(1px)' /* prevents element from being blurry if positioned on a "half-pixel", alternatively, setting /* 'transform-style: preserve-3d;' on the parent element has the same effect! /* 8. Element will be centered vertically regardless of height. Setting 'transform: perspective(1px)' /* prevents element from being blurry if positioned on a "half-pixel", alternatively, setting /* 'transform-style: preserve-3d;' on the parent element has the same effect! /* 9. Fix for Chrome 44 bug. https://code.google.com/p/chromium/issues/detail?id=506893 /* 10. Setting percentage height is rather rare, not worth the bloat of all utility classes /*/ .d\\:ib-fix { font-size: 0; line-height: 0; } .d\\:ib-fix > *, .d\\:ib-fix *:before, .d\\:ib-fix *:after { font-size: initial; line-height: initial; vertical-align: middle; } .d\\:f-col-r { -webkit-box-orient: vertical; -webkit-box-direction: reverse; -webkit-flex-direction: column-reverse; -ms-flex-direction: column-reverse; flex-direction: column-reverse; } .d\\:f-row-r { -webkit-box-orient: horizontal; -webkit-box-direction: reverse; -webkit-flex-direction: row-reverse; -ms-flex-direction: row-reverse; flex-direction: row-reverse; } .d\\:f-wrap-r { -webkit-flex-wrap: wrap-reverse; -ms-flex-wrap: wrap-reverse; flex-wrap: wrap-reverse; } .d\\:fc-items-start { -webkit-box-align: start; -webkit-align-items: flex-start; -ms-flex-align: start; align-items: flex-start; } .d\\:fc-items-end { -webkit-box-align: end; -webkit-align-items: flex-end; -ms-flex-align: end; align-items: flex-end; } .d\\:fc-items-baseline { -webkit-box-align: baseline; -webkit-align-items: baseline; -ms-flex-align: baseline; align-items: baseline; } .d\\:fc-items-stretch { -webkit-box-align: stretch; -webkit-align-items: stretch; -ms-flex-align: stretch; align-items: stretch; } .d\\:fc-justify-start { -webkit-box-pack: start; -webkit-justify-content: flex-start; -ms-flex-pack: start; justify-content: flex-start; } .d\\:fc-justify-end { -webkit-box-pack: end; -webkit-justify-content: flex-end; -ms-flex-pack: end; justify-content: flex-end; } .d\\:fc-justify-around { -webkit-justify-content: space-around; -ms-flex-pack: distribute; justify-content: space-around; } .d\\:fc-justify-evenly { -webkit-box-pack: space-evenly; -webkit-justify-content: space-evenly; -ms-flex-pack: space-evenly; justify-content: space-evenly; /* not supported in Edge! */ } .d\\:fc-content-start { -webkit-align-content: flex-start; -ms-flex-line-pack: start; align-content: flex-start; } .d\\:fc-content-end { -webkit-align-content: flex-end; -ms-flex-line-pack: end; align-content: flex-end; } .d\\:fc-content-center { -webkit-align-content: center; -ms-flex-line-pack: center; align-content: center; } .d\\:fc-content-between { -webkit-align-content: space-between; -ms-flex-line-pack: justify; align-content: space-between; } .d\\:fc-content-around { -webkit-align-content: space-around; -ms-flex-line-pack: distribute; align-content: space-around; } .d\\:fc-content-stretch { -webkit-align-content: stretch; -ms-flex-line-pack: stretch; align-content: stretch; } .d\\:fi-self-start { -webkit-align-self: flex-start; -ms-flex-item-align: start; align-self: flex-start; } .d\\:fi-self-end { -webkit-align-self: flex-end; -ms-flex-item-align: end; align-self: flex-end; } .d\\:fi-self-center { -webkit-align-self: center; -ms-flex-item-align: center; align-self: center; } .d\\:fi-self-baseline { -webkit-align-self: baseline; -ms-flex-item-align: baseline; align-self: baseline; } .d\\:fi-self-stretch { -webkit-align-self: stretch; -ms-flex-item-align: stretch; align-self: stretch; } .d\\:fi-grow { -webkit-box-flex: 1; -webkit-flex: 1 1 auto; -ms-flex: 1 1 auto; flex: 1 1 auto; min-width: 0; /* 9 */ min-height: 0; /* 9 */ } .d\\:table { display: table; } .d\\:table-collapse { border-collapse: collapse; } .d\\:table-separate { border-collapse: separate; } .d\\:table-spacing { border-spacing: 1px; } .d\\:td { display: table-cell; } .d\\:tr { display: table-row; } .d\\:left { float: left; } .d\\:right { float: right; } .d\\:clear:before, .d\\:clear:after { content: " "; /* 3 */ display: table; /* 4 */ } .d\\:clear:after { clear: both; } .d\\:valign-baseline { vertical-align: baseline; } .d\\:valign-top { vertical-align: top; } .d\\:valign-middle { vertical-align: middle; } .d\\:valign-bottom { vertical-align: bottom; } .d\\:valign-all-baseline > * { vertical-align: baseline; } .d\\:valign-all-top > * { vertical-align: top; } .d\\:valign-all-middle > * { vertical-align: middle; } .d\\:valign-all-bottom > * { vertical-align: bottom; } /** /* Skeleton loader styles /* @credits: https://css-tricks.com/a-bare-bones-approach-to-versatile-and-reusable-skeleton-loaders/ /* Multi-row skeleton loaders can be added by adding <br/> elements assuming that all /* child (content) elements get replaced with actual content once loading is done. /*/ .is--loading, .is--loading * { pointer-events: none; user-select: none; cursor: default; } .is--loading .sk\\:el { animation: 2s sk\\:progress linear infinite; background-size: 200% 100%; /* for loading animation */ background: var(--c-skeleton-bg); background: var(--c-skeleton-gradient); border-color: rgba(0,0,0,0) !important; border-radius: 2px; color: rgba(0,0,0,0) !important; } .is--loading .sk\\:el:is(.sk\\:static) { animation: none !important; background: var(--c-skeleton-bg); } @media (prefers-reduced-motion) { .is--loading .sk\\:el { animation: none !important; background: var(--c-skeleton-bg); } } /* Make sure all child elements are hidden, but preserve their dimensions and layout */ .is--loading .sk\\:el * { visibility: hidden; } /** * Make sure that an element has at least a whitespace character as a child so it displays * properly. This is useful when no text placeholder is present (element is empty). */ .is--loading .sk\\:el:empty:after, .is--loading .sk\\:el *:empty:after { content: ' '; /* Can't use 00a0 (octal escape sequences) in template literls! */ } @keyframes sk\\:progress { to { background-position-x: -200%; } } ` }, ], /*transformers: [ { name: 'my-transformer', enforce: 'pre', // enforce before other transformers idFilter(id) { // only transform .tsx and .jsx files //return id.match(/\.[tj]sx$/) //console.log('transformer: idFilter', id); return true; }, async transform(code, id, { uno }) { // code is a MagicString instance //console.log('transformer: transform()', code, id, uno); }, } ],*/ /* preprocess: (t) => { // for example prefix all classes with ! which makes them !important (bad idea!) if (t.includes('!')) return t; return `!${t}`; }, */ // convert pixels to rem for all utils postprocess: (util, ...args) => { if ( !window || !window.__unocss?.theme?.px2rem ) { return } const px = /(-?[\.\d]+)px/g; const reminpx = parseFloat(getComputedStyle(document.documentElement).fontSize); util.entries.forEach((i) => { const value = i[1]; if ( typeof value === 'string' && px.test(value) ) { i[1] = value.replace(px, (_, v) => `${v / reminpx}rem`); } }); }, /*extractors: [ // This is the default split extractor of UnoCSS, comment out what is below if you encounter // issues and try this one, it's very crude... but works with more or less false positives // depending on the markup your are working with { name: '@unocss/core/extractor-split', order: 0, extract({ code }) { const defaultSplitRE = /[\\:]?[\s'"`;{}]+/g; function splitCode(code) { return code.split(defaultSplitRE); }; console.log('split extractor code', typeof code, code.split(defaultSplitRE)); return splitCode(code) }, }, { name: 're:css', order: 0, extract({ code }) { const classes = []; for ([_, q, c] of code.matchAll(/(?:class\s*?=\s*?)(["'])((?:(?=(?:\\)*)\\.|.)*?)\1/gi)) { classes.push(...c.split(/\s+/g)); } console.log('re:css extractor classes', classes); return classes; }, } ],*/ // disable the default extractor //extractorDefault: false, // override the default extractor // This one WILL FAIL if you do not quote your class attribute values (as you should anyways)! // But this extractor is 5-10x faster than the default extractor extractorDefault: { name: 're:css', order: 0, extract({ code }) { //const start = performance.now(); const classes = []; for (match of code.matchAll(/(?:class\s*?=\s*?)(["'])((?:(?=(?:\\)*)\\.|.)*?)\1/gi)) { // we use the default splitter RegEx, but only to split class attribute values, nothing else // it properly deals with inline riot `<template>` tags that contain unparsed expressions // `{ <expression> }` which cause problems with a 'simple' whitespace splitter... classes.push(...match[2].split(/[\\:]?[\s'"`;{}]+/g)); } //console.log('re:css extractor classes', classes); //console.log(`custom extractor done in ${performance.now()-start}ms`); return classes; }, }, // This is actually the default extractor, takes 60ms+ for a large HTML document /*extractorDefault: { name: '@unocss/core/extractor-split', order: 0, extract({ code }) { //const start = performance.now(); const defaultSplitRE = /[\\:]?[\s'"`;{}]+/g; function splitCode(code) { return code.split(defaultSplitRE); }; const tokens = splitCode(code); //console.log(`default extractor done in ${performance.now()-start}ms`); return tokens; }, },*/ runtime: { inject: (styleElement) => document.head.append(styleElement), observer: { target: () => document.querySelector('.lia-page'), attributeFilter: ['class'], }, //inspect: (el) => { console.log(el); if ( /\S+:\S+/gi.test(el.getAttribute('class') || '') ) { console.log('uno inspect', el, el.classList, el.classList.matchAll(/\S+:\S+/gi)); } return true; }, ready: (ctx) => { //console.log('uno ready?', `${performance.now()-window.$start}ms`, ctx); // we can't pass the inspect callback directly via runtime config? why? ctx.inspect((el) => { if ( /\S+:\S+/gi.test(el.getAttribute('class') || '') ) { [...el.classList].filter((x) => /\S+:\S+/i.test(x)).forEach((v) => { window.$stats[v] = (window.$stats[v] || 0) + 1; }); } // need to return true from inspector callback return true; }); // need to return true from ready return true; }, //configResolved: (config, defaults) => { console.log('uno config resolved, modify it?', `${performance.now()-window.$start}ms`, config, defaults); }, }, }; </script> <script src="https://community.hubspot.com/html/@BAA2A9A38B8DF4DC5426B3D61241E9AB/assets/_core.libs.min.js" type="text/javascript"></script> <script src="https://community.hubspot.com/html/@B5A089E4D43239236FCE70D92814096D/assets/_core.global.min.js" type="text/javascript"></script> <script src="https://community.hubspot.com/html/@F771820AC374A1A1436A3D822BE61093/assets/_cmp.min.js" type="text/javascript"></script> <link rel="icon" href="https://community.hubspot.com/html/@46292D292824DF071B6641C2DA6FDD8E/assets/favicon.png"> <!--[if IE]><link rel="shortcut icon" href="https://community.hubspot.com/html/@46292D292824DF071B6641C2DA6FDD8E/assets/favicon.png"><![endif]--> <meta class="swiftype" name="doc-type" data-type="string" content="Community"> <script data-external-hs-domain="true" data-gtm-id="GTM-M3KWR2J" src="https://www.hubspot.com/wt-assets/static-files/compliance/index.js" defer nonce></script> <script type="text/javascript" src="/t5/scripts/A8A4D60844A7A24245ECDC960EA81DEE/lia-scripts-head-min.js"></script><script language="javascript" type="text/javascript"> <!-- LITHIUM.PrefetchData = {"Components":{"grouphubs.widget.grid":{"instances":[{"nodeScope":{"result":{"data":{"size":1,"list_item_type":"node","type":"nodes","items":[{"id":"community:mjmao93648","type":"node","title":"HubSpot Community","user_context":{"type":"node_user_context","can_create_group_hub":false}}]},"successful":true}},"hiddenGroup":{"result":{"data":{"size":0,"list_item_type":"node","type":"nodes","items":[]},"successful":true}},"userScope":{"result":{"data":{"size":1,"list_item_type":"user","type":"users","items":[{"id":"169781","type":"user","login":"kennedyp"}]},"successful":true}},"groupHubs":{"call":{"query":{"nodes":{"meta":{"defer":false,"quilts":[{"get":{"path":"/ui/quilts/GroupHubCard"},"id":"GroupHubCard","type":"quilt"}]},"limit":20,"paging":"true","fields":["id","node_type","title","description","view_href","creation_date_friendly","avatar","user_context.can_update_node","user_context.is_member","membership_type","memberships.count(*)","parent","topics.count(*)","meta"],"constraints":[{"user.id":"169781"},{"node_type":"grouphub"}],"sorts":["memberships.count(*) desc"]}}},"result":{"data":{"size":3,"list_item_type":"node","type":"nodes","items":[{"parent":{"id":"category:study-groups","href":"/nodes/category:study-groups","type":"node"},"topics":{"count":155},"description":"A place for inbound professionals to share ideas, learn, network, and be inspired.","avatar":{"medium_href":"/t5/image/serverpage/image-id/45260i54F3605EC818A699/image-size/medium?v=v2&px=400","small_href":"/t5/image/serverpage/image-id/45260i54F3605EC818A699/image-size/small?v=v2&px=200","large_href":"/t5/image/serverpage/image-id/45260i54F3605EC818A699/image-size/large?v=v2&px=999","tiny_href":"/t5/image/serverpage/image-id/45260i54F3605EC818A699/image-size/tiny?v=v2&px=100","type":"avatar"},"type":"node","title":"Inbound","memberships":{"count":12126},"node_type":"grouphub","meta":{"quilts":[{"layout":"one-column","instance":0,"id":"GroupHubCard","type":"rendered_quilt","rows":[{"columns":[{"components":[],"width":24,"id":"common-header","type":"rendered_quilt_column"}],"id":"header","type":"rendered_quilt_row"},{"columns":[{"components":[{"path":"limuirs/components/memberships/MembershipType","instance":0,"engine":"limuirs","id":"memberships.widget.type","type":"rendered_quilt_component","params":{}},{"path":"limuirs/components/nodes/NodeAvatar","instance":0,"engine":"limuirs","id":"nodes.widget.avatar-limuirs","type":"rendered_quilt_component","params":{"useLink":false}},{"path":"limuirs/components/nodes/NodeTitle","instance":0,"engine":"limuirs","id":"nodes.widget.title","type":"rendered_quilt_component","params":{"useLink":false,"useMembershipTypeIcon":false}},{"path":"limuirs/components/nodes/NodeDescription","instance":0,"engine":"limuirs","id":"nodes.widget.description","type":"rendered_quilt_component","params":{}},{"path":"limuirs/components/memberships/MembershipCount","instance":0,"engine":"limuirs","id":"memberships.widget.count","type":"rendered_quilt_component","params":{}},{"path":"limuirs/components/nodes/NodeTopicCount","instance":0,"engine":"limuirs","id":"nodes.widget.topic-count","type":"rendered_quilt_component","params":{}}],"width":24,"id":"main-content","type":"rendered_quilt_column"}],"id":"main","type":"rendered_quilt_row"},{"columns":[{"components":[],"width":24,"id":"common-footer","type":"rendered_quilt_column"}],"id":"footer","type":"rendered_quilt_row"}]}],"type":"call_metadata"},"creation_date_friendly":"Jun 28, 2021","id":"grouphub:study-group-inbound","view_href":"/t5/Inbound/gh-p/study-group-inbound","user_context":{"can_update_node":false,"is_member":false,"type":"node_user_context"},"membership_type":"open"},{"parent":{"id":"category:hangouts","href":"/nodes/category:hangouts","type":"node"},"topics":{"count":36},"description":"Women in Tech HubSpot User Group","avatar":{"medium_href":"/t5/image/serverpage/image-id/35492i55896808D811C855/image-size/medium?v=v2&px=400","small_href":"/t5/image/serverpage/image-id/35492i55896808D811C855/image-size/small?v=v2&px=200","large_href":"/t5/image/serverpage/image-id/35492i55896808D811C855/image-size/large?v=v2&px=999","tiny_href":"/t5/image/serverpage/image-id/35492i55896808D811C855/image-size/tiny?v=v2&px=100","type":"avatar"},"type":"node","title":"Women in Tech","memberships":{"count":734},"node_type":"grouphub","meta":{"quilts":[{"layout":"one-column","instance":1,"id":"GroupHubCard","type":"rendered_quilt","rows":[{"columns":[{"components":[],"width":24,"id":"common-header","type":"rendered_quilt_column"}],"id":"header","type":"rendered_quilt_row"},{"columns":[{"components":[{"path":"limuirs/components/memberships/MembershipType","instance":0,"engine":"limuirs","id":"memberships.widget.type","type":"rendered_quilt_component","params":{}},{"path":"limuirs/components/nodes/NodeAvatar","instance":0,"engine":"limuirs","id":"nodes.widget.avatar-limuirs","type":"rendered_quilt_component","params":{"useLink":false}},{"path":"limuirs/components/nodes/NodeTitle","instance":0,"engine":"limuirs","id":"nodes.widget.title","type":"rendered_quilt_component","params":{"useLink":false,"useMembershipTypeIcon":false}},{"path":"limuirs/components/nodes/NodeDescription","instance":0,"engine":"limuirs","id":"nodes.widget.description","type":"rendered_quilt_component","params":{}},{"path":"limuirs/components/memberships/MembershipCount","instance":0,"engine":"limuirs","id":"memberships.widget.count","type":"rendered_quilt_component","params":{}},{"path":"limuirs/components/nodes/NodeTopicCount","instance":0,"engine":"limuirs","id":"nodes.widget.topic-count","type":"rendered_quilt_component","params":{}}],"width":24,"id":"main-content","type":"rendered_quilt_column"}],"id":"main","type":"rendered_quilt_row"},{"columns":[{"components":[],"width":24,"id":"common-footer","type":"rendered_quilt_column"}],"id":"footer","type":"rendered_quilt_row"}]}],"type":"call_metadata"},"creation_date_friendly":"Dec 16, 2020","id":"grouphub:Women_In_Tech","view_href":"/t5/Women-in-Tech/gh-p/Women_In_Tech","user_context":{"can_update_node":false,"is_member":false,"type":"node_user_context"},"membership_type":"open"},{"parent":{"id":"category:hangouts","href":"/nodes/category:hangouts","type":"node"},"topics":{"count":40},"description":"A place for life long learners to connect, engage, learn, and be inspired.","avatar":{"medium_href":"/t5/image/serverpage/image-id/66276i43039547FC51BCB3/image-size/medium?v=v2&px=400","small_href":"/t5/image/serverpage/image-id/66276i43039547FC51BCB3/image-size/small?v=v2&px=200","large_href":"/t5/image/serverpage/image-id/66276i43039547FC51BCB3/image-size/large?v=v2&px=999","tiny_href":"/t5/image/serverpage/image-id/66276i43039547FC51BCB3/image-size/tiny?v=v2&px=100","type":"avatar"},"type":"node","title":"StudentSpot","memberships":{"count":274},"node_type":"grouphub","meta":{"quilts":[{"layout":"one-column","instance":2,"id":"GroupHubCard","type":"rendered_quilt","rows":[{"columns":[{"components":[],"width":24,"id":"common-header","type":"rendered_quilt_column"}],"id":"header","type":"rendered_quilt_row"},{"columns":[{"components":[{"path":"limuirs/components/memberships/MembershipType","instance":0,"engine":"limuirs","id":"memberships.widget.type","type":"rendered_quilt_component","params":{}},{"path":"limuirs/components/nodes/NodeAvatar","instance":0,"engine":"limuirs","id":"nodes.widget.avatar-limuirs","type":"rendered_quilt_component","params":{"useLink":false}},{"path":"limuirs/components/nodes/NodeTitle","instance":0,"engine":"limuirs","id":"nodes.widget.title","type":"rendered_quilt_component","params":{"useLink":false,"useMembershipTypeIcon":false}},{"path":"limuirs/components/nodes/NodeDescription","instance":0,"engine":"limuirs","id":"nodes.widget.description","type":"rendered_quilt_component","params":{}},{"path":"limuirs/components/memberships/MembershipCount","instance":0,"engine":"limuirs","id":"memberships.widget.count","type":"rendered_quilt_component","params":{}},{"path":"limuirs/components/nodes/NodeTopicCount","instance":0,"engine":"limuirs","id":"nodes.widget.topic-count","type":"rendered_quilt_component","params":{}}],"width":24,"id":"main-content","type":"rendered_quilt_column"}],"id":"main","type":"rendered_quilt_row"},{"columns":[{"components":[],"width":24,"id":"common-footer","type":"rendered_quilt_column"}],"id":"footer","type":"rendered_quilt_row"}]}],"type":"call_metadata"},"creation_date_friendly":"May 13, 2022","id":"grouphub:studentspot","view_href":"/t5/StudentSpot/gh-p/studentspot","user_context":{"can_update_node":false,"is_member":false,"type":"node_user_context"},"membership_type":"open"}]},"successful":true}},"closedGroup":{"result":{"data":{"next_cursor":"MjQuOHwyLjB8aXwxfDMyOjF8aW50LDQyMCw0MjA","size":1,"list_item_type":"node","type":"nodes","items":[{"id":"grouphub:Community_Champions","type":"node"}]},"successful":true}},"openGroup":{"result":{"data":{"next_cursor":"MjQuOHwyLjB8aXwxfDMyOjF8aW50LDQyNSw0MjU","size":1,"list_item_type":"node","type":"nodes","items":[{"id":"grouphub:Study_Group_Marketing","type":"node"}]},"successful":true}}}]}},"commonResults":{}}; ;(function(){var en = function(n, ord ) { var s = String(n).split('.'), v0 = !s[1], t0 = Number(s[0]) == n, n10 = t0 && s[0].slice(-1), n100 = t0 && s[0].slice(-2); if (ord) return (n10 == 1 && n100 != 11) ? 'one' : (n10 == 2 && n100 != 12) ? 'two' : (n10 == 3 && n100 != 13) ? 'few' : 'other'; return (n == 1 && v0) ? 'one' : 'other'; }; var number = function (value, name, offset) { if (!offset) return value; if (isNaN(value)) throw new Error("Can't apply offset:" + offset + ' to argument `' + name + '` with non-numerical value ' + JSON.stringify(value) + '.'); return value - offset; }; var plural = function (value, offset, lcfunc, data, isOrdinal) { if ({}.hasOwnProperty.call(data, value)) return data[value]; if (offset) value -= offset; var key = lcfunc(value, isOrdinal); return key in data ? data[key] : data.other; }; var fmt = { prop: function (value, lc, param) { return value[param]; } }; LITHIUM.TextData = { li: { common: { Pagination: { next: function(d) { return "Next"; }, previous: function(d) { return "Previous"; }, ellipsis: function(d) { return "..."; }, leftArrow: function(d) { return "«"; }, rightArrow: function(d) { return "»"; } }, Menu: { linkTitle: function(d) { return "Show option menu"; }, ariaLabel: function(d) { return "Show option menu"; } } }, grouphubs: { GroupHubsGrid: { groupFilter: { all: function(d) { return "All group types"; }, hidden: function(d) { return "Hidden"; }, closed: function(d) { return "Closed"; }, label: function(d) { return "Filter by type"; }, open: function(d) { return "Open"; } }, search: { placeholder: function(d) { return "Filter by name"; }, title: function(d) { return "Filter by name"; } }, memberFilter: { all: function(d) { return "Member and Nonmember"; }, owner: function(d) { return "Owner"; }, member: function(d) { return "Member"; }, nonMember: function(d) { return "Nonmember"; }, label: function(d) { return "Membership status"; } }, sort: { creationDateDesc: function(d) { return "Date created (newest)"; }, alpha: function(d) { return "Name"; }, lastUpdate: function(d) { return "Latest activity"; }, memberCount: function(d) { return "Member count"; }, creationDateAsc: function(d) { return "Date created (oldest)"; }, postCount: function(d) { return "Post count"; }, label: function(d) { return "Sort by"; } }, title: { node: { "default": function(d) { return "Group Hubs"; }, top: function(d) { return "Group Hubs"; }, leaf: function(d) { return "Group Hubs in " + fmt.prop(d.node, "en", (" title").trim()); } }, user: { them: function(d) { return "Group Hubs for " + fmt.prop(d.user, "en", (" login").trim()); }, my: function(d) { return "My Group Hubs"; } } }, empty: function(d) { return "No group hubs to display!"; } }, CreateGroupHubButton: { title: function(d) { return "Create Group Hub"; } } }, nodes: { NodeTitle: { title: function(d) { return d.title; } }, NodeDescription: { description: function(d) { return d.description; } }, NodeAvatar: { alt: function(d) { return d.title; } }, NodeTopicCount: { count: function(d) { return d.count; } } }, memberships: { MembershipType: { short: { closed_hidden: function(d) { return "Hidden"; }, closed: function(d) { return "Closed"; }, open: function(d) { return ""; } }, long: { closed_hidden: function(d) { return "Hidden group"; }, closed: function(d) { return "Closed group"; }, open: function(d) { return "Open group"; } } }, MembershipCount: { iconCount: function(d) { return d.count; }, count: function(d) { return plural(d.count, 0, en, { one: "1 member", other: number(d.count, "count") + " members" }); } } } } };LITHIUM.Limuirs = LITHIUM.Limuirs || {}; LITHIUM.Limuirs.logLevel = "error"; LITHIUM.Limuirs.getChunkURL = function(){ return "https:\u002F\u002Flimuirs-assets.lithium.com\u002Fassets\u002F"}; LITHIUM.Limuirs.preloadPaths = ["0\u002Flimuirs\u002Fcomponents\u002Fmemberships\u002FMembershipType","0\u002Flimuirs\u002Fcomponents\u002Fnodes\u002FNodeAvatar","0\u002Flimuirs\u002Fcomponents\u002Fnodes\u002FNodeTitle","0\u002Flimuirs\u002Fcomponents\u002Fnodes\u002FNodeDescription","0\u002Flimuirs\u002Fcomponents\u002Fmemberships\u002FMembershipCount","0\u002Flimuirs\u002Fcomponents\u002Fnodes\u002FNodeTopicCount","1\u002Flimuirs\u002Fcomponents\u002Fmemberships\u002FMembershipType","1\u002Flimuirs\u002Fcomponents\u002Fnodes\u002FNodeAvatar","1\u002Flimuirs\u002Fcomponents\u002Fnodes\u002FNodeTitle","1\u002Flimuirs\u002Fcomponents\u002Fnodes\u002FNodeDescription","1\u002Flimuirs\u002Fcomponents\u002Fmemberships\u002FMembershipCount","1\u002Flimuirs\u002Fcomponents\u002Fnodes\u002FNodeTopicCount"];;LITHIUM.release = "24.8"})(); LITHIUM.DEBUG = false; LITHIUM.CommunityJsonObject = { "Validation" : { "image.description" : { "min" : 0, "max" : 1000, "isoneof" : [ ], "type" : "string" }, "tkb.toc_maximum_heading_level" : { "min" : 1, "max" : 6, "isoneof" : [ ], "type" : "integer" }, "tkb.toc_heading_list_style" : { "min" : 0, "max" : 50, "isoneof" : [ "disc", "circle", "square", "none" ], "type" : "string" }, "blog.toc_maximum_heading_level" : { "min" : 1, "max" : 6, "isoneof" : [ ], "type" : "integer" }, "tkb.toc_heading_indent" : { "min" : 5, "max" : 50, "isoneof" : [ ], "type" : "integer" }, "blog.toc_heading_indent" : { "min" : 5, "max" : 50, "isoneof" : [ ], "type" : "integer" }, "blog.toc_heading_list_style" : { "min" : 0, "max" : 50, "isoneof" : [ "disc", "circle", "square", "none" ], "type" : "string" } }, "User" : { "settings" : { "imageupload.legal_file_extensions" : "*.jpg;*.JPG;*.jpeg;*.JPEG;*.gif;*.GIF;*.png;*.PNG", "config.enable_avatar" : true, "integratedprofile.show_klout_score" : true, "layout.sort_view_by_last_post_date" : false, "layout.friendly_dates_enabled" : true, "profileplus.allow.anonymous.scorebox" : false, "tkb.message_sort_default" : "topicPublishDate", "layout.format_pattern_date" : "MMM d, yyyy", "config.require_search_before_post" : "off", "isUserLinked" : false, "integratedprofile.cta_add_topics_dismissal_timestamp" : -1, "layout.message_body_image_max_size" : 1000, "profileplus.everyone" : false, "integratedprofile.cta_connect_wide_dismissal_timestamp" : -1, "blog.toc_maximum_heading_level" : "", "integratedprofile.hide_social_networks" : false, "blog.toc_heading_indent" : "", "contest.entries_per_page_num" : 20, "layout.messages_per_page_linear" : 12, "integratedprofile.cta_manage_topics_dismissal_timestamp" : -1, "profile.shared_profile_test_group" : false, "integratedprofile.cta_personalized_feed_dismissal_timestamp" : -1, "integratedprofile.curated_feed_size" : 10, "contest.one_kudo_per_contest" : false, "integratedprofile.enable_social_networks" : false, "integratedprofile.my_interests_dismissal_timestamp" : -1, "profile.language" : "en", "layout.friendly_dates_max_age_days" : 31, "layout.threading_order" : "thread_descending", "blog.toc_heading_list_style" : "disc", "useRecService" : false, "layout.module_welcome" : "", "imageupload.max_uploaded_images_per_upload" : 100, "imageupload.max_uploaded_images_per_user" : 6000, "integratedprofile.connect_mode" : "", "tkb.toc_maximum_heading_level" : "", "tkb.toc_heading_list_style" : "disc", "sharedprofile.show_hovercard_score" : true, "config.search_before_post_scope" : "container", "tkb.toc_heading_indent" : "", "p13n.cta.recommendations_feed_dismissal_timestamp" : -1, "imageupload.max_file_size" : 3072, "layout.show_batch_checkboxes" : false, "integratedprofile.cta_connect_slim_dismissal_timestamp" : -1 }, "isAnonymous" : true, "policies" : { "image-upload.process-and-remove-exif-metadata" : false }, "registered" : false, "emailRef" : "", "id" : -1, "login" : "Anonymous" }, "Server" : { "communityPrefix" : "/mjmao93648", "nodeChangeTimeStamp" : 1732500584852, "tapestryPrefix" : "/t5", "deviceMode" : "DESKTOP", "responsiveDeviceMode" : "DESKTOP", "membershipChangeTimeStamp" : "0", "version" : "24.8", "branch" : "24.8-release", "showTextKeys" : false }, "Config" : { "phase" : "prod", "integratedprofile.cta.reprompt.delay" : 30, "profileplus.tracking" : { "profileplus.tracking.enable" : false, "profileplus.tracking.click.enable" : false, "profileplus.tracking.impression.enable" : false }, "app.revision" : "2410251442-s96644fcabc-b95", "navigation.manager.community.structure.limit" : "1000" }, "Activity" : { "Results" : [ { "name" : "UserUpdated", "user" : { "uid" : -1, "login" : "Anonymous" } } ] }, "NodeContainer" : { "viewHref" : "https://community.hubspot.com/t5/Top/ct-p/top", "description" : "", "id" : "top", "shortTitle" : "Top", "title" : "Top", "nodeType" : "category" }, "Page" : { "skins" : [ "hubspot", "responsive_peak" ], "authUrls" : { "loginUrl" : "https://app.hubspot.com/khoros/integration/jwt/authenticate?referer=https%3A%2F%2Fcommunity.hubspot.com%2Ft5%2Fgrouphubs%2Fpage%2Fuser-id%2F169781", "loginUrlNotRegistered" : "https://app.hubspot.com/khoros/integration/jwt/authenticate?redirectreason=notregistered&referer=https%3A%2F%2Fcommunity.hubspot.com%2Ft5%2Fgrouphubs%2Fpage%2Fuser-id%2F169781", "loginUrlNotRegisteredDestTpl" : "https://app.hubspot.com/khoros/integration/jwt/authenticate?redirectreason=notregistered&referer=%7B%7BdestUrl%7D%7D" }, "name" : "GroupHubsPage", "rtl" : false, "object" : { } }, "WebTracking" : { "Activities" : { }, "path" : "Community:HubSpot Community" }, "Feedback" : { "targeted" : { } }, "Seo" : { "markerEscaping" : { "pathElement" : { "prefix" : "@", "match" : "^[0-9][0-9]$" }, "enabled" : false } }, "TopLevelNode" : { "viewHref" : "https://community.hubspot.com/", "description" : "Welcome to the HubSpot Community! Connect with peers, maximize your HubSpot knowledge, and learn how to grow better with HubSpot.", "id" : "mjmao93648", "shortTitle" : "HubSpot Community", "title" : "HubSpot Community", "nodeType" : "Community" }, "Community" : { "viewHref" : "https://community.hubspot.com/", "integratedprofile.lang_code" : "en", "integratedprofile.country_code" : "US", "id" : "mjmao93648", "shortTitle" : "HubSpot Community", "title" : "HubSpot Community" }, "CoreNode" : { "viewHref" : "https://community.hubspot.com/", "settings" : { }, "description" : "Welcome to the HubSpot Community! Connect with peers, maximize your HubSpot knowledge, and learn how to grow better with HubSpot.", "id" : "mjmao93648", "shortTitle" : "HubSpot Community", "title" : "HubSpot Community", "nodeType" : "Community", "ancestors" : [ ] } }; LITHIUM.Components.RENDER_URL = "/t5/util/componentrenderpage/component-id/#{component-id}?render_behavior=raw"; LITHIUM.Components.ORIGINAL_PAGE_NAME = 'grouphubs/Page'; LITHIUM.Components.ORIGINAL_PAGE_ID = 'GroupHubsPage'; LITHIUM.Components.ORIGINAL_PAGE_CONTEXT = '9SWbiOWaCtox4iv-2ZjM0ak0ikmo11FjyDl2oebrziaJYh7gDuksOxsfyQZZZWQf1ky1n7bJjodC6e3EZlVcGVD8KbTfl4wZuYr-i1z32H4MCNhMuaiMfdbzZu4TCPkLRLvC5RBanHmYMqPNqhB2g5RzmbIK6tXXDt7ZP3Wyfvxcpg1JAicOvKwia5IAJmPHYVahZtEOC9VEHl4YA4_SiyIovh_mE1889C4JyNyhP686Y6s9PxH7N1xSeiM4-GaM4W2X42rcSyLL3DP5RAXSCyU-9ljqZuQQ6wog_n-pN4gQhOKK4y4iaO06H7wLypq2'; LITHIUM.Css = { "BASE_DEFERRED_IMAGE" : "lia-deferred-image", "BASE_BUTTON" : "lia-button", "BASE_SPOILER_CONTAINER" : "lia-spoiler-container", "BASE_TABS_INACTIVE" : "lia-tabs-inactive", "BASE_TABS_ACTIVE" : "lia-tabs-active", "BASE_AJAX_REMOVE_HIGHLIGHT" : "lia-ajax-remove-highlight", "BASE_FEEDBACK_SCROLL_TO" : "lia-feedback-scroll-to", "BASE_FORM_FIELD_VALIDATING" : "lia-form-field-validating", "BASE_FORM_ERROR_TEXT" : "lia-form-error-text", "BASE_FEEDBACK_INLINE_ALERT" : "lia-panel-feedback-inline-alert", "BASE_BUTTON_OVERLAY" : "lia-button-overlay", "BASE_TABS_STANDARD" : "lia-tabs-standard", "BASE_AJAX_INDETERMINATE_LOADER_BAR" : "lia-ajax-indeterminate-loader-bar", "BASE_AJAX_SUCCESS_HIGHLIGHT" : "lia-ajax-success-highlight", "BASE_CONTENT" : "lia-content", "BASE_JS_HIDDEN" : "lia-js-hidden", "BASE_AJAX_LOADER_CONTENT_OVERLAY" : "lia-ajax-loader-content-overlay", "BASE_FORM_FIELD_SUCCESS" : "lia-form-field-success", "BASE_FORM_WARNING_TEXT" : "lia-form-warning-text", "BASE_FORM_FIELDSET_CONTENT_WRAPPER" : "lia-form-fieldset-content-wrapper", "BASE_AJAX_LOADER_OVERLAY_TYPE" : "lia-ajax-overlay-loader", "BASE_FORM_FIELD_ERROR" : "lia-form-field-error", "BASE_SPOILER_CONTENT" : "lia-spoiler-content", "BASE_FORM_SUBMITTING" : "lia-form-submitting", "BASE_EFFECT_HIGHLIGHT_START" : "lia-effect-highlight-start", "BASE_FORM_FIELD_ERROR_NO_FOCUS" : "lia-form-field-error-no-focus", "BASE_EFFECT_HIGHLIGHT_END" : "lia-effect-highlight-end", "BASE_SPOILER_LINK" : "lia-spoiler-link", "FACEBOOK_LOGOUT" : "lia-component-users-action-logout", "BASE_DISABLED" : "lia-link-disabled", "FACEBOOK_SWITCH_USER" : "lia-component-admin-action-switch-user", "BASE_FORM_FIELD_WARNING" : "lia-form-field-warning", "BASE_AJAX_LOADER_FEEDBACK" : "lia-ajax-loader-feedback", "BASE_AJAX_LOADER_OVERLAY" : "lia-ajax-loader-overlay", "BASE_LAZY_LOAD" : "lia-lazy-load" }; LITHIUM.noConflict = true; LITHIUM.useCheckOnline = false; LITHIUM.RenderedScripts = [ "ActiveCast3.js", "jquery.ui.draggable.js", "SearchAutoCompleteToggle.js", "jquery.js", "LiModernizr.js", "Video.js", "DeferredImages.js", "ElementMethods.js", "limuirs-24_7-main.138f37e85bead07e28fd.js", "Components.js", "jquery.ui.widget.js", "jquery.clone-position-1.0.js", "jquery.iframe-shim-1.0.js", "Text.js", "Link.js", "jquery.ui.mouse.js", "PolyfillsOld.js", "jquery.fileupload.js", "Throttle.js", "Loader.js", "jquery.ui.dialog.js", "Globals.js", "Sandbox.js", "jquery.tmpl-1.1.1.js", "jquery.iframe-transport.js", "jquery.tools.tooltip-1.2.6.js", "jquery.hoverIntent-r6.js", "PartialRenderProxy.js", "jquery.effects.core.js", "Tooltip.js", "json2.js", "jquery.json-2.6.0.js", "PolyfillsAll.js", "Auth.js", "jquery.position-toggle-1.0.js", "jquery.function-utils-1.0.js", "ElementQueries.js", "jquery.viewport-1.0.js", "prism.js", "jquery.lithium-selector-extensions.js", "NoConflict.js", "jquery.blockui.js", "SpoilerToggle.js", "jquery.css-data-1.0.js", "jquery.effects.slide.js", "limuirs-24_7-vendors~main.5ef86aa8c72fe4cbb8d6.js", "jquery.placeholder-2.0.7.js", "jquery.appear-1.1.1.js", "jquery.ui.position.js", "jquery.ui.core.js", "Cache.js", "AjaxFeedback.js", "InformationBox.js", "ForceLithiumJQuery.js", "HelpIcon.js", "jquery.delayToggle-1.0.js", "Events.js", "AjaxSupport.js", "SearchForm.js", "Forms.js", "jquery.ajax-cache-response-1.0.js", "DataHandler.js", "jquery.autocomplete.js", "Namespace.js", "Lithium.js", "ResizeSensor.js", "Placeholder.js", "jquery.scrollTo.js", "AutoComplete.js", "jquery.ui.resizable.js" ];// --> </script><script type="text/javascript" src="/t5/scripts/D60EB96AE5FF670ED274F16ABB044ABD/lia-scripts-head-min.js"></script></head> <body class="lia-user-status-anonymous GroupHubsPage lia-body" id="lia-body"> <div id="C04-182-4" class="ServiceNodeInfoHeader"> </div> <div class="lia-page"> <center> <noscript class=" " id="hubspot" data-page="GroupHubsPage" data-style="none" data-rootid="mjmao93648" data-roottype="community" data-topid="mjmao93648" data-nodeid="mjmao93648" data-nodetype="community" data-nodelang="en" data-userlang="en" data-skin="hubspot"> <p>JavaScript must be installed and enabled to use these boards.<p> Your browser appears to have JavaScript disabled or does not support JavaScript. Please refer to your browser's help file to determine how to enable JavaScript.</p> </noscript> <svg aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden;" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <defs> <symbol id="i:hubspot" viewBox="0 0 32 32"> <path d="M23.7 10.92V7.36a2.74 2.74 0 0 0 1.59-2.48V4.8a2.75 2.75 0 0 0-2.74-2.74h-.08a2.75 2.75 0 0 0-2.74 2.74v.08c0 1.1.64 2.03 1.56 2.47h.02v3.57a7.7 7.7 0 0 0-3.71 1.64l.01-.01-9.78-7.62a3.12 3.12 0 1 0-1.45 1.9h-.01l9.62 7.49a7.75 7.75 0 0 0 .14 8.81l-.02-.03-2.93 2.94a2.54 2.54 0 1 0 1.81 2.43c0-.27-.04-.52-.12-.75v.01l2.9-2.9a7.8 7.8 0 1 0 5.98-13.91h-.04zm-1.2 11.72a3.55 3.55 0 1 1 .01 0z" /> </symbol> <symbol id="i:link" viewBox="0 0 20 20"> <path d="M7.859 14.691l-0.81 0.805c-0.701 0.695-1.843 0.695-2.545 0-0.336-0.334-0.521-0.779-0.521-1.252s0.186-0.916 0.521-1.252l2.98-2.955c0.617-0.613 1.779-1.515 2.626-0.675 0.389 0.386 1.016 0.384 1.403-0.005 0.385-0.389 0.383-1.017-0.006-1.402-1.438-1.428-3.566-1.164-5.419 0.675l-2.98 2.956c-0.715 0.709-1.108 1.654-1.108 2.658 0 1.006 0.394 1.949 1.108 2.658 0.736 0.73 1.702 1.096 2.669 1.096s1.934-0.365 2.669-1.096l0.811-0.805c0.389-0.385 0.391-1.012 0.005-1.4s-1.014-0.39-1.403-0.006zM16.891 3.207c-1.547-1.534-3.709-1.617-5.139-0.197l-1.009 1.002c-0.389 0.386-0.392 1.013-0.006 1.401 0.386 0.389 1.013 0.391 1.402 0.005l1.010-1.001c0.74-0.736 1.711-0.431 2.346 0.197 0.336 0.335 0.522 0.779 0.522 1.252s-0.186 0.917-0.522 1.251l-3.18 3.154c-1.454 1.441-2.136 0.766-2.427 0.477-0.389-0.386-1.016-0.383-1.401 0.005s-0.384 1.017 0.005 1.401c0.668 0.662 1.43 0.99 2.228 0.99 0.977 0 2.010-0.492 2.993-1.467l3.18-3.153c0.712-0.71 1.107-1.654 1.107-2.658s-0.395-1.949-1.109-2.659z"></path> </symbol> <symbol id="i:linkedin" viewBox="0 0 20 20"> <path d="M10 0.4c-5.302 0-9.6 4.298-9.6 9.6s4.298 9.6 9.6 9.6 9.6-4.298 9.6-9.6-4.298-9.6-9.6-9.6zM7.65 13.979h-1.944v-6.256h1.944v6.256zM6.666 6.955c-0.614 0-1.011-0.435-1.011-0.973 0-0.549 0.409-0.971 1.036-0.971s1.011 0.422 1.023 0.971c0 0.538-0.396 0.973-1.048 0.973zM14.75 13.979h-1.944v-3.467c0-0.807-0.282-1.355-0.985-1.355-0.537 0-0.856 0.371-0.997 0.728-0.052 0.127-0.065 0.307-0.065 0.486v3.607h-1.945v-4.26c0-0.781-0.025-1.434-0.051-1.996h1.689l0.089 0.869h0.039c0.256-0.408 0.883-1.010 1.932-1.010 1.279 0 2.238 0.857 2.238 2.699v3.699z"></path> </symbol> <symbol id="i:xcom" viewBox="0 0 32 32"> <path d="M24.325 3h4.411l-9.636 11.013 11.336 14.987h-8.876l-6.952-9.089-7.955 9.089h-4.413l10.307-11.78-10.875-14.22h9.101l6.284 8.308zM22.777 26.36h2.444l-15.776-20.859h-2.623z"></path> </symbol> <symbol id="i:connectcom" viewBox="0 0 24 24"> <path fill="#192733" style="fill: var(--color1, #192733)" d="M17.585 5.753c-1.941 0-3.675 0.886-4.822 2.273l0.005 0.005c-0.025 0.030-0.052 0.060-0.075 0.092l-3.988 5.512c-0.608 0.75-1.335 1.171-2.291 1.171-1.549 0-2.806-1.258-2.806-2.806s1.258-2.806 2.806-2.806c1.082 0 1.953 0.603 2.519 1.492l2.261-2.712c-1.146-1.36-2.861-2.221-4.78-2.221-3.452 0.003-6.248 2.799-6.248 6.248s2.797 6.248 6.248 6.248c1.941 0 3.675-0.886 4.822-2.273l-0.005-0.005c0.067-0.067 0.132-0.136 0.189-0.216l4.023-5.559c0.579-0.642 1.263-1 2.142-1 1.549 0 2.806 1.258 2.806 2.806s-1.258 2.806-2.806 2.806c-1.082 0-1.953-0.603-2.519-1.492l-2.261 2.712c1.146 1.36 2.861 2.221 4.78 2.221 3.452 0 6.248-2.797 6.248-6.248s-2.797-6.248-6.248-6.248z"></path> <path fill="#ff5c35" style="fill: var(--color2, #ff5c35)" d="M17.585 10.15c-0.747 0-1.389 0.446-1.682 1.085h-7.737c-0.293-0.64-0.935-1.085-1.682-1.085-1.020 0-1.849 0.829-1.849 1.849s0.829 1.849 1.849 1.849c0.747 0 1.389-0.446 1.682-1.085h7.737c0.293 0.64 0.935 1.085 1.682 1.085 1.020 0 1.849-0.829 1.849-1.849s-0.829-1.849-1.849-1.849z"></path> </symbol> <symbol id="i:globe" viewBox="0 0 24 24"> <path d="M16.951 11c-0.214-2.69-1.102-5.353-2.674-7.71 1.57 0.409 2.973 1.232 4.087 2.346 1.408 1.408 2.351 3.278 2.581 5.364zM14.279 20.709c1.483-2.226 2.437-4.853 2.669-7.709h3.997c-0.23 2.086-1.173 3.956-2.581 5.364-1.113 1.113-2.516 1.936-4.085 2.345zM7.049 13c0.214 2.69 1.102 5.353 2.674 7.71-1.57-0.409-2.973-1.232-4.087-2.346-1.408-1.408-2.351-3.278-2.581-5.364zM9.721 3.291c-1.482 2.226-2.436 4.853-2.669 7.709h-3.997c0.23-2.086 1.173-3.956 2.581-5.364 1.114-1.113 2.516-1.936 4.085-2.345zM12.004 1c0 0 0 0 0 0-3.044 0.001-5.794 1.233-7.782 3.222-1.99 1.989-3.222 4.741-3.222 7.778s1.232 5.789 3.222 7.778c1.988 1.989 4.738 3.221 7.774 3.222 0 0 0 0 0 0 3.044-0.001 5.793-1.233 7.782-3.222 1.99-1.989 3.222-4.741 3.222-7.778s-1.232-5.789-3.222-7.778c-1.988-1.989-4.738-3.221-7.774-3.222zM14.946 13c-0.252 2.788-1.316 5.36-2.945 7.451-1.729-2.221-2.706-4.818-2.945-7.451zM11.999 3.549c1.729 2.221 2.706 4.818 2.945 7.451h-5.89c0.252-2.788 1.316-5.36 2.945-7.451z"></path> </symbol> </defs> </svg> <style class="core-cmp-icons/core" type="text/css"> .i { display: inline-block; fill: currentColor; height: 1rem; width: 1rem; stroke-width: 0; stroke: currentColor; } /* Single-colored icons can be modified like so but this usually happens directly where they are used: */ /* .i\:<name> { font-size: 32px; color: red; } */ </style> <style class="cmp-global-styles/core" type="text/css"> /** /* TODO: Potentially move to skin or leave here? Might be easier to find changes done by us this way? /*/ /* Center category banner card icons (often the HTMl is CC23 based), for example Advocacy */ #lia-body .custom-home-banner-section__cards .card-item__icon { align-self: center; } /* Fix unstyled layout for NotifyModeratorPage quilt */ .lia-quilt-notify-moderator-page > .lia-quilt-row-header .lia-page-header, .lia-quilt-notify-moderator-page > .lia-quilt-row-main { margin: 0 auto; max-width: 1236px; padding-left: 15px; padding-right: 15px; } /* KBCOM-2818: Add node description the hacky way */ .CategoryPage .custom-v2-banner__wrapper .page-title-wrapper:after, .ForumPage .custom-v2-banner__wrapper .page-title-wrapper:after, .GroupHubPage .custom-v2-banner__wrapper .page-title-wrapper:after { content: "Welcome to the HubSpot Community! Connect with peers, maximize your HubSpot knowledge, and learn how to grow better with HubSpot."; display: block; margin-bottom: -24px; } /* KBCOM-2802: Fix BlogDashboardPage filter alignment issue */ .BlogDashboardPage .dashboard-wrapper { margin: 0 auto; max-width: 1236px; } .BlogDashboardPage .dashboard-wrapper .lia-node-selector-dropdown { left: auto !important; right: 0; } .BlogDashboardPage .dashboard-wrapper .lia-component-blog-dashboard-tabs ul.lia-tabs-standard, .BlogDashboardPage .dashboard-wrapper .lia-component-blog-widget-dashboard-tabs ul.lia-tabs-standard { padding-left: 0 !important; } /* KBCOM-2831: Remove advanced search options toggle on SearchPage */ .SearchPage .lia-advanced-search-toggle { display: none; } /* re-css cloaking class to hide components relying on dynamically generated classes to only show when they are ready */ [un-cloak] { display: none; } /** /* Custom styles for core feedback elements. Those are usually only used to provide info and hints /* to priviledged roles when issues occur within custom components, so they are not part of the /* regular community theme. /*/ .admininfo { --b-radius: 3px; --H: 0; --S: 0%; --L: 41%; --A: 1; background: hsla(var(--H), var(--S), var(--L), var(--A)); border-radius: var(--b-radius); color: white; display: flex; align-items: center; gap: 24px; /*filter: grayscale(1);*/ font-size: 12px; line-height: 1.25; margin: 24px 0; padding: 24px; transition: all 236ms ease; } .admininfo .checkmark { display: none; } .admininfo__title { display: block; font-weight: bold; font-size: 125%; } .admininfo code { background: white; border-radius: var(--b-radius); color: hsla(var(--H), var(--S), var(--L), 1); display: inline-block; font-size: 90%; font-weight: bold; padding: 2px 4px; } .admininfo a { color: white; display: inline-block; position: relative; } .admininfo a:hover, .admininfo a:focus { text-decoration: none; } .admininfo a:before, .admininfo a:after { border-bottom: 1px dotted white; content: ''; position: absolute; left: 0; right: 0; bottom: -1px; transition: all 382ms ease; } .admininfo a:after { border-bottom: 1px solid white; max-width: 0; width: 0; right: auto; } .admininfo a:hover:after, .admininfo a:focus:after { max-width: 100%; width: 1200px; } .admininfo a:active:after { transform: scaleX(1.0618); } .admininfo.is--error { --S: 100%; --L: 41%; background: hsla(var(--H), var(--S), var(--L), var(--A)); filter: grayscale(0); } .admininfo.is--error .checkmark { display: block; } .admininfo.is--error code { background: #ff4444; color: white; } .admininfo.is--success { --H: 135; --S: 100%; background: hsla(var(--H), var(--S), var(--L), var(--A)); filter: grayscale(0); } .admininfo.is--success code { background: #00c851; color: white; } .checkmark { flex-shrink: 0; /* prevents shrinking under 48px! */ min-width: 48px; width: 48px; height: 48px; border-radius: 50%; display: block; stroke-width: 3px; stroke: white; stroke-miterlimit: 10; } .checkmark_circle_error { stroke-dasharray: 166; stroke-dashoffset: 166; stroke-width: 5px; stroke-miterlimit: 10; stroke: #ff4444; animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards; } .checkmark.error { box-shadow: inset 0px 0px 0px #ff4444; animation: fillerror 0.4s ease-in-out 0.4s forwards, scale 0.3s ease-in-out 0.9s both; } .checkmark_check { transform-origin: 50% 50%; stroke-dasharray: 48; stroke-dashoffset: 48; animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.9s forwards; } .progress { position: absolute; top: 5%; left: 5%; stroke: black; transform: rotate(-90deg); } .progress.progress--thin { left: auto; right: 5%; } .progress circle { stroke-dasharray: 130; stroke-dashoffset: 130; animation: dash 1.5s infinite; } @keyframes dash { 50% { stroke-dashoffset: 0; } 100% { stroke-dashoffset: -130; } } @keyframes stroke { 100% { stroke-dashoffset: 0; } } @keyframes scale { 0%, 100% { transform: none; } 50% { transform: scale3d(1.1, 1.1, 1); } } @keyframes fillerror { 100% { box-shadow: inset 0px 0px 0px 75px #ff4444; } } </style><style class="cmp-global-search-external/core" type="text/css"> @media (min-width: 799px) { .lia-search-input-message + .lia-autocomplete-container { width: 200% !important; max-width: 768px !important; } } .lia-search-input-message + .lia-autocomplete-container .lia-autocomplete-content { display: flex; margin: 0; } @media (max-width: 799px) { .mobile-header form.SerachForm [name="messageSearchField"] + .lia-autocomplete-container > ul:first-of-type { max-height: 210px !important; } /*.mobile-header form.SerachForm [name="messageSearchField"] + .lia-autocomplete-container .collapse-results.fa-chevron-down + ul { max-height: 36px !important; overflow: hidden !important; }*/ .lia-search-input-message + .lia-autocomplete-container .lia-autocomplete-content { flex-direction: column; } } .lia-search-input-message + .lia-autocomplete-container .lia-autocomplete-content>ul>li { padding: 8px 15px 8px 15px; } /* default tag styles */ .SearchPage .search-external-link:after, .is--tag { background-color: var(--color-calypso-light); border: 1px solid transparent; border-radius: 2px; color: var(--color-link-hover); display: inline-block; font-size: 12px; font-weight: 600; line-height: 22px; padding: 0 8px; position: relative; vertical-align: baseline; } .SearchPage .search-external-link:after { line-height: 16px; margin-left: 6px; } /* Override styles form the native Khoros skin... */ .SearchPage .lia-tabs-standard .lia-tabs:first-child { padding-left: 0; } .SearchPage .lia-tabs-standard-wrapper>.lia-tabs-standard { padding: 0 15px !important; /* This one is especially stubborn for some reason I can't explain! */ } </style> <script> // glowingblue: really? xhr.open()? when 5 lines up you were aware you have jQuery? oh boy... function followunfollow(id,value,currentUser,top){ const xhttp = new XMLHttpRequest(); xhttp.onload = function() { if (value=='Unfollow') { document.getElementById("tunfollow-"+id+"").style.display = 'none'; document.getElementById("tfollow-"+id+"").style.display = 'flex'; } if (value=='follow') { document.getElementById("tunfollow-"+id+"").style.display = 'flex'; document.getElementById("tfollow-"+id+"").style.display = 'none'; } } xhttp.open("GET", "/plugins/custom/hubspot/hubspot/follow-unfollow-hover-card-button?id="+id+"&val="+value+"¤tUser="+currentUser+"",true);xhttp.send(); } </script> <div class="MinimumWidthContainer"> <div class="min-width-wrapper"> <div class="min-width"> <div class="lia-content"> <div class="lia-browser-support-alert"> <div class="lia-browser-support-alert-text"> We no longer support Internet Explorer v10 and older, or you have compatibility view enabled. Disable Compatibility view, upgrade to a newer version, or use a different browser. </div> <div class="lia-browser-support-alert-close"> <a class="lia-link-navigation lia-link-ticket-post-action" data-lia-action-token="L_s67oWPzgNgvj6RodzTgsGC9W0l62cjBzKqVxE99WM." rel="nofollow" id="dismissAlert" href="https://community.hubspot.com/t5/grouphubs/page.liabase.basebody.browsersupportalert.dismissalert:dismissalert?t:ac=user-id/169781"><span class="lia-img-close-small lia-fa-close lia-fa-small lia-fa" title="Dismiss this alert" alt="Dismiss this alert" aria-label="Dismiss this alert" role="img" id="display"></span></a> </div> </div> <div class="lia-quilt lia-quilt-group-hubs-page lia-quilt-layout-two-column-side-main lia-top-quilt"> <div class="lia-quilt-row lia-quilt-row-header"> <div class="lia-quilt-column lia-quilt-column-24 lia-quilt-column-single lia-quilt-column-common-header"> <div class="lia-quilt-column-alley lia-quilt-column-alley-single"> <div class="lia-quilt lia-quilt-header lia-quilt-layout-one-column lia-component-quilt-header"> <div class="lia-quilt-row lia-quilt-row-header"> <div class="lia-quilt-column lia-quilt-column-24 lia-quilt-column-single lia-quilt-column-common-header"> <div class="lia-quilt-column-alley lia-quilt-column-alley-single"> <!-- hs.custom.responsive-header --> <style> #lia-body .mobile-header .navbar .menu .menu-item.active.has-collapsible .menu-child{ display: flex; flex-direction: column; } .jp-resources-list{ display:flex; flex-direction:column; } .jp-resources-list>li.menu-child-item.jp-class-HubSpot.Community.Blog{ order:1; } .header-dropdown-menu .lia-header-nav-component-widget .private-notes-link:before { content: "Message"; } #lia-body .mobile-header .navbar .menu-wrapper.offcanvas::before {width: 0px;} .profile-menu-dropdown{display: none !important;} .pagination-recent-post.pagination a#jp-previous:after { content: "Prev"; } .pagination-recent-post.pagination a#jp-next:before { content: "Next"; } #lia-body .MessageView.lia-message-view-idea-message-item .lia-quilt-idea-message-item .lia-message-footer-action .lia-link-navigation.lia-message-comment-post:after { content: "0 Comment"; } </style> <div class="header mobile-header"> <nav class="navbar"> <span class="open-menu"> <img src="https://community.hubspot.com/html/@3A38E73C772F7CCD402C2EC02A244F14/assets/Hamburger-Nav.svg"> </span> <span class="hubspot-mobile-logo-wrapper"> <a href="/"> <img src="https://community.hubspot.com/html/@813D252A70F0A7024C8EA3BB1B8B9CFD/assets/sticky-logo.png"> </a> </span> <div class="menu-wrapper"> <div class="menu-block"> <span class="close-menu"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20"> <path fill="#252a32" fill-rule="evenodd" d="M17.778.808l1.414 1.414L11.414 10l7.778 7.778-1.414 1.414L10 11.414l-7.778 7.778-1.414-1.414L8.586 10 .808 2.222 2.222.808 10 8.586 17.778.808z" /> </svg> </span> </div> <ul class="menu"> <li class="menu-item has-collapsible"> <a><span></span>Discussions</a> <ul class="menu-child"> <li class="menu-child-item"> <a href="/t5/CRM-Sales-Hub/ct-p/sales" class="nav-dropdown-link nav-discussions-crm"> CRM & Sales </a> </li> <li class="menu-child-item"> <a href="/t5/Marketing-Hub/ct-p/marketing" class="nav-dropdown-link nav-discussions-mktg"> Marketing & Content </a> </li> <li class="menu-child-item"> <a href="/t5/Service-Hub/ct-p/service_hub" class="nav-dropdown-link nav-discussions-svc"> Customer Success & Service </a> </li> <li class="menu-child-item"> <a href="/t5/Operations/ct-p/Operations" class="nav-dropdown-link nav-discussions-ops"> RevOps & Operations </a> </li> <li class="menu-child-item"> <a href="/t5/Commerce/ct-p/commerce" class="nav-dropdown-link nav-discussions-commerce "> Commerce </a> </li> <li class="menu-child-item"> <a href="/t5/HubSpot-Developers/ct-p/developers" class="nav-dropdown-link nav-discussions-developers "> Developers </a> </li> <li class="menu-child-item"> <a href="https://community.hubspot.com/t5/Getting-Started-on-the-Community/How-to-join-the-Solutions-Partner-Program/ba-p/400205" class="nav-dropdown-link nav-partners "> Partners </a> </li> <li class="menu-child-item"> <a href="/t5/HubSpot-Ideas/idb-p/HubSpot_Ideas" class="nav-dropdown-link nav-discussions-ideas"> Ideas </a> </li> </ul> </li> <li class="menu-item has-collapsible"> <a><span></span>Academy</a> <ul class="menu-child"> <li class="nav-dropdown-item"> <a href="https://academy.hubspot.com/courses" class="nav-dropdown-link nav-external-link" target="_blank"> Courses </a> </li> <li class="nav-dropdown-item"> <a href="https://academy.hubspot.com/certification-overview" class="nav-dropdown-link nav-external-link" target="_blank"> Certifications </a> </li> <li class="nav-dropdown-item"> <a href="https://www.hubspot.com/academy/bootcamps/home" class="nav-dropdown-link nav-external-link" target="_blank"> Bootcamps </a> </li> <li class="nav-dropdown-item"> <a href="https://academy.hubspot.com/learning-paths" class="nav-dropdown-link nav-external-link " target="_blank"> Learning Paths </a> </li> <li class="nav-dropdown-item"> <a href="https://community.hubspot.com/t5/HubSpot-Academy-Support/bd-p/certifications_help" class="nav-dropdown-link"> Academy Support </a> </li> <li class="nav-dropdown-item"> <a href="https://community.hubspot.com/t5/Study-Groups/ct-p/study-groups" class="nav-dropdown-link"> Study Groups </a> </li> </ul> </li> <li class="menu-item has-collapsible"> <a><span></span>Resources</a> <ul class="menu-child jp-resources-list"> <li class="menu-child-item "> <a href="/t5/Getting-Started/ct-p/getting_started" class="nav-dropdown-link nav-discussions-gs"> Getting Started </a> </li> <li class="nav-dropdown-item"> <a href="https://help.hubspot.com/" class="nav-dropdown-link nav-external-link" target="_blank"> Help Center </a> </li> <li class="nav-dropdown-item"> <a href="https://knowledge.hubspot.com/" class="nav-dropdown-link nav-external-link" target="_blank"> Knowledge Base </a> </li> <li class="nav-dropdown-item"> <a href="https://developers.hubspot.com/docs/api/overview" class="nav-dropdown-link nav-external-link" target="_blank"> API Documentation </a> </li> <li class="nav-dropdown-item"> <a href="https://developers.hubspot.com/docs/cms" class="nav-dropdown-link nav-external-link" target="_blank"> CMS Documentation </a> </li> <li class="menu-child-item "> <a href="/t5/News-Networking-Events/ct-p/communityboard" class="nav-dropdown-link nav-news"> News </a> </li> <li class="menu-child-item "> <a href="/t5/Resources/ct-p/resources?node_id=webinars&order_by=last_updated" class="nav-dropdown-link nav-resource-blog-Webinars "> Webinars </a> </li> <li class="menu-child-item "> <a href="/t5/Resources/ct-p/resources?node_id=releases-updates" class="nav-dropdown-link nav-resource-blog-Releases and Updates "> Releases and Updates </a> </li> <li class="menu-child-item "> <a href="/t5/Resources/ct-p/resources?node_id=hubspot-community-blog" class="nav-dropdown-link nav-resource-blog-HubSpot Community Blog "> Community Blog </a> </li> <li class="menu-child-item "> <a href="/t5/Resources/ct-p/resources?node_id=sales-hub-community-perspectives" class="nav-dropdown-link nav-resource-blog-Sales Hub Community Perspectives "> Sales Hub Community Perspectives </a> </li> <li class="menu-child-item "> <a href="/t5/Resources/ct-p/resources?node_id=workflows_library" class="nav-dropdown-link nav-resource-blog-Workflows Library "> Workflows Library </a> </li> <li class="menu-child-item "> <a href="/t5/Resources/ct-p/resources?node_id=ai-library" class="nav-dropdown-link nav-resource-blog-Breeze Library "> Breeze Library </a> </li> </ul> </li> <li class="menu-item has-collapsible"> <a href="#"><span></span>Events</a> <ul class="menu-child"> <li class="nav-dropdown-item"> <a href="https://community.hubspot.com/t5/Ask-Me-Anything-and-Panel/bd-p/ama_discussions" class="nav-dropdown-link "> AMA </a> </li> <li class="nav-dropdown-item"> <a href="https://community.hubspot.com/t5/Community-Led-Events/bd-p/adapt" class="nav-dropdown-link "> Community Led Events </a> </li> <li class="nav-dropdown-item"> <a href="https://www.hubspot.com/resources/webinar" class="nav-dropdown-link nav-external-link " target="_blank"> Webinars </a> </li> <li class="nav-dropdown-item"> <a href="https://www.hubspot.com/hubspot-user-groups" class="nav-dropdown-link nav-external-link " target="_blank"> HUGS </a> </li> </ul> </li> <li class="menu-item has-collapsible"> <a><span></span>Advocacy</a> <ul class="menu-child"> <li class="menu-child-item"> <a href='/t5/Advocacy/ct-p/advocacy' class="nav-dropdown-link nav-hubFans-program "> Community Champions Program </a> </li> <li class="menu-child-item"> <a href="/t5/Advocates-Blog/bg-p/advocates-blog" class="nav-dropdown-link nav-adovcates-blog "> Champions Blog </a> </li> </ul> </li> </ul> <div class="lang-picker-wrapper"> <div class="lang-picker-container"> <a id="current-language" class="current-language"> <span class="lang-picker-globe-icon"></span> English </a> <div id="lang-picker-global" class="nav-popover lang-picker"> <div class="nav-popover-arrow" style="border-top-color: transparent; border-left-color: transparent; width: 20px; height: 20px; transform: rotate(-135deg); top: -10px; right: calc(50% - 48px);"></div> <ul id="lang-picker-dropdown" class="nav-dropdown-list"> <a href="https://community.hubspot.com/t5/grouphubs/page/user-id/169781?profile.language=en" class="nav-dropdown-link" data-lang="en"> <li class="nav-dropdown-item"> English </li> </a> <a href="https://community.hubspot.com/t5/grouphubs/page/user-id/169781?profile.language=es" class="nav-dropdown-link" data-lang="es"> <li class="nav-dropdown-item"> Español </li> </a> <a href="https://community.hubspot.com/t5/grouphubs/page/user-id/169781?profile.language=pt-br" class="nav-dropdown-link" data-lang="pt-br"> <li class="nav-dropdown-item"> Português </li> </a> <a href="https://community.hubspot.com/t5/grouphubs/page/user-id/169781?profile.language=fr" class="nav-dropdown-link" data-lang="fr"> <li class="nav-dropdown-item"> Français </li> </a> <a href="https://community.hubspot.com/t5/grouphubs/page/user-id/169781?profile.language=de" class="nav-dropdown-link" data-lang="de"> <li class="nav-dropdown-item"> Deutsch </li> </a> <a href="https://community.hubspot.com/t5/grouphubs/page/user-id/169781?profile.language=ja" class="nav-dropdown-link" data-lang="ja"> <li class="nav-dropdown-item"> 日本語 </li> </a> </ul> </div> </div></div> </div> <div class="header-right-col"> <div class="search_icon" onclick="myFunction()"> <img src="/html/assets/search-icon-grey.svg?version=preview" alt=""> </div> <div class="wrapper-search" style="display:none"> <div id='lia-searchformV32_23fd878fe08c79' class='SearchForm lia-search-form-wrapper lia-mode-default lia-component-common-widget-search-form'> <div class='lia-inline-ajax-feedback'> <div class='AjaxFeedback' id='ajaxfeedback_23fd878fe08c79'></div> </div> <div id='searchautocompletetoggle_23fd878fe08c79'> <div class='lia-inline-ajax-feedback'> <div class='AjaxFeedback' id='ajaxfeedback_23fd878fe08c79_0'></div> </div> <form enctype='multipart/form-data' class='lia-form lia-form-inline SearchForm' action='https://community.hubspot.com/t5/grouphubs/page.searchformv32.form.form' method='post' id='form_23fd878fe08c79' name='form_23fd878fe08c79'><div class='t-invisible'><input value='user-id/169781' name='t:ac' type='hidden'></input><input value='search/contributions/page' name='t:cp' type='hidden'></input><input value='Uhsc-pG1biKZc30LR-2xJHQPhgKNUGiQkRD5GfqvEb8sh7TcUd--BkqTgVKQ2CPrd5VXdEMU_w2PRz8aYKqzkBq1WzYaF1QciGlA1MLRCe-XgdJ95iL-Ju11W6IXdXoUIsD_DKE2Ge8iQF17Iue5pIA8XyhJo8BtjU79YuuWB-WtqLOv-cWMgSFxKvGakP1yCqGNPqYX1moEBvVwGlN9HLrPq0XsJLVEtW4ZvfHgxzljl8XVfNGUvOCrBdBrclyoim9V3YYHx0ztRFBmsjQXQ8y_Z2Pizrs7YomkZ2_APV4OkU6BqUslnL3m35mzZIj3SKbDo1uiPeHwJPTyh_hZcFfHlVl-p39j_hWNI0redqg443oJRT9PBjCnPuZUxoj_B6tjmMW03G5GzJLZPttNjd3dq-EPH_AHLhiSueXNDe5xUBdqRZ1P1f1QaBJVFWWEPVFdvY0EgLbE3qUcxw3o1yR0Sn_7B6ndsvtBv71sT7jtsyFdLw9g0lLEkagM5CqrSmb_vfGv6uGEkK0ygBbwcaWySg1I-sxSfk2_yQDXkFgC8ZezjIGq1W_jFWd3ukQX5PU0eIBtYKtZ6Gug9tydFf1-mM2WJlcgrS4LC8AFevvUCS49wNt-9psCunD5pbHD3aSC0N0pRZeGfSdGjWgh1KHeGx9yvcirwqAYe4HxGPsRGyhmm4jg-ZKiGeRwxEcnOySEVpy4LKzutwJpx20KUR3LkzfGexieL0H-3ae5llAoQY_FDH7xCFA61U7cf6SZelW3P8HSUbI6sHRSwjv-czrVmPlCOqIsKEO4CyDM11G2om_5R9Q8yvG60icfopT8Y5C5RtqLdIrNFiXY8pyRh_5xkES1GDIoSJn7_NBCLLL0sZ8rNQ7vBKzC9uA5cna-NRvAKgyBULkNJqifPTmjtbwPPw7lmABfGXkSSX9JR1preLNYJ1Bu58BNCbSqb2yj61NLAuoixDalpLstJbcXhfALwWiJeRcqWMK2HfH7Ykic1CP0WPXZDw1qkyyhBIav2-2eil64-6gR59WnuwKkUpfR2qwFDZq-g3qQgB6qa915CNHdm0rE3DIZsGOEbCG-6PLAHu_QAsAnNaZwZI3ANvIlcBSwlN03xKSRf-ncitRAGXpDuy9Dcl8uIynqyGGenYZafsaVRUXcS5FhoNaxNDODkNpLZ0QGJznTXkIf58Q.' name='lia-form-context' type='hidden'></input><input value='GroupHubsPage:user-id/169781:searchformv32.form:' name='liaFormContentKey' type='hidden'></input><input value='yDZKQFNI0U/sd9X4pbUemqvQQZw=:H4sIAAAAAAAAALWSzUrDQBSFr4Wuigiib6DbiajdqAhFUISqweBaZibTNJpk4sxNEzc+ik8gvkQX7nwHH8CtKxfmzxJbwaTUVZhzw/nOmblP79COD+FAC6r40OAyQOWyCF0ZaMOkjtgrJgOp/NHONtER810sP9nfIkGtoCuVQ2hI+VAQpKHQqO67hEslPJcRRrUgPZaKlOOxKzx7wxIYhZtX487b+stnC5b60MnZ0junvkBY7d/QETU8GjiGlUYKnP0kRFguwEcFeAHBe02Dm0pyobWV+Wid0sbP9u7g4/G1BZCE8QWc1U3kpzapWoqZ+S+SvoMHgPQ+ypGVj/IoC2dlqHZ8CWZdV7xljUqszZa43voPYNHkFE7qGkdaqKrl1Pm7wEqmV59gcYjGkQOJP25h6jyJnOlzRv4DUURusIWhknbEsWo5K002vhzNufG1WHmDLwdzh8gDBQAA' name='t:formdata' type='hidden'></input></div> <div class='lia-inline-ajax-feedback'> <div class='AjaxFeedback' id='feedback_23fd878fe08c79'></div> </div> <input value='eqoJChg949gOAdJEdzmZpFs0pDwUNWPok1OPWvEEAaU.' name='lia-action-token' type='hidden'></input> <input value='form_23fd878fe08c79' id='form_UIDform_23fd878fe08c79' name='form_UID' type='hidden'></input> <input value='' id='form_instance_keyform_23fd878fe08c79' name='form_instance_key' type='hidden'></input> <span class='lia-search-input-wrapper'> <span class='lia-search-input-field'> <span class='lia-button-wrapper lia-button-wrapper-secondary lia-button-wrapper-searchForm-action'><input value='searchForm' name='submitContextX' type='hidden'></input><input class='lia-button lia-button-secondary lia-button-searchForm-action' value='Search' id='submitContext_23fd878fe08c79' name='submitContext' type='submit'></input></span> <input placeholder='Search the Community' aria-label='Search' title='Search' class='lia-form-type-text lia-autocomplete-input search-input lia-search-input-message' value='' id='messageSearchField_23fd878fe08c79_0' name='messageSearchField' type='text'></input> <input placeholder='Search the Community' aria-label='Search' title='Search' class='lia-form-type-text lia-autocomplete-input search-input lia-search-input-tkb-article lia-js-hidden' value='' id='messageSearchField_23fd878fe08c79_1' name='messageSearchField_0' type='text'></input> <input ng-non-bindable='' title='Enter a user name or rank' class='lia-form-type-text UserSearchField lia-search-input-user search-input lia-js-hidden lia-autocomplete-input' aria-label='Enter a user name or rank' value='' id='userSearchField_23fd878fe08c79' name='userSearchField' type='text'></input> <input placeholder='Enter a keyword to search within the private messages' title='Enter a search word' class='lia-form-type-text NoteSearchField lia-search-input-note search-input lia-js-hidden lia-autocomplete-input' aria-label='Enter a search word' value='' id='noteSearchField_23fd878fe08c79_0' name='noteSearchField' type='text'></input> <input title='Enter a search word' class='lia-form-type-text ProductSearchField lia-search-input-product search-input lia-js-hidden lia-autocomplete-input' aria-label='Enter a search word' value='' id='productSearchField_23fd878fe08c79' name='productSearchField' type='text'></input> <input class='lia-as-search-action-id' name='as-search-action-id' type='hidden'></input> </span> </span> <span class='lia-cancel-search'>cancel</span> </form> <div class='search-autocomplete-toggle-link lia-js-hidden'> <span> <a class='lia-link-navigation auto-complete-toggle-on lia-link-ticket-post-action lia-component-search-action-enable-auto-complete' data-lia-action-token='d4clgVRWN-Bu4ZwyAIWM8wocVZXMhvseyiFKfaP_Uxs.' rel='nofollow' id='enableAutoComplete_23fd878fe08c79' href='https://community.hubspot.com/t5/grouphubs/page.enableautocomplete:enableautocomplete?t:ac=user-id/169781&t:cp=action/contributions/searchactions'>Turn on suggestions</a> <span class='HelpIcon'> <a class='lia-link-navigation help-icon lia-tooltip-trigger' role='button' aria-label='Help Icon' id='link_23fd878fe08c79' href='#'><span class='lia-img-icon-help lia-fa-icon lia-fa-help lia-fa' alt='Auto-suggest helps you quickly narrow down your search results by suggesting possible matches as you type.' aria-label='Help Icon' role='img' id='display_23fd878fe08c79'></span></a><div role='alertdialog' class='lia-content lia-tooltip-pos-bottom-left lia-panel-tooltip-wrapper' id='link_23fd878fe08c79_0-tooltip-element'><div class='lia-tooltip-arrow'></div><div class='lia-panel-tooltip'><div class='content'>Auto-suggest helps you quickly narrow down your search results by suggesting possible matches as you type.</div></div></div> </span> </span> </div> </div> <div class='spell-check-showing-result'> Showing results for <span class='lia-link-navigation show-results-for-link lia-link-disabled' aria-disabled='true' id='showingResult_23fd878fe08c79'></span> </div> <div> <span class='spell-check-search-instead'> Search instead for <a class='lia-link-navigation search-instead-for-link' rel='nofollow' id='searchInstead_23fd878fe08c79' href='#'></a> </span> </div> <div class='spell-check-do-you-mean lia-component-search-widget-spellcheck'> Did you mean: <a class='lia-link-navigation do-you-mean-link' rel='nofollow' id='doYouMean_23fd878fe08c79' href='#'></a> </div> </div> </div> <script> function myFunction() { var x = document.getElementsByClassName("wrapper-search")[0]; if (x.style.display === "none") { x.style.display = "block"; } else { x.style.display = "none"; } } </script><div class="search-icon-plus-top"> <button class="lia-button search-toggle-action-icon-plus"><img src='https://community.hubspot.com/html/@5320E40129AA1377479EABCA2009B53A/assets/Start-dicsucssion.svg' alt=""><i class="lia-fa lia-fa-caret-down"></i></button> <div class="plus-bar-main-content" style="display: none;"> <ul id="plus-bar-top-main"> <li class="plus-bar"> <a href="https://app.hubspot.com/khoros/integration/jwt/authenticate?referer=https%3A%2F%2Fcommunity.hubspot.com%2Ft5%2Fforums%2Fpostpage%2Fcategory-id%2Fhubspot_community_en%2Fchoose-node%2Ftrue" class="white-btn transpaent"><i><img src="https://community.hubspot.com/html/@38F5B2AB35958F39F47C2BFFE5486135/assets/Edit.svg"></i> Create post</a> </li> <li class="plus-bar"><a href="https://app.hubspot.com/khoros/integration/jwt/authenticate?referer=https%3A%2F%2Fcommunity.hubspot.com%2Ft5%2Fforums%2Fpostpage%2Fboard-id%2FHubSpot_Ideas"><i><img src="https://community.hubspot.com/html/@C71A42AEB76B8A3A82335DA9F5B9C717/assets/lightbulb.svg"></i> Submit Idea</a></li> </ul> </div> </div> <div class="login-container"> <a class='lia-link-navigation login-link lia-authentication-link lia-component-users-action-login' rel='nofollow' id='loginPageV2_23fd8792dd8af7' href='https://app.hubspot.com/khoros/integration/jwt/authenticate?referer=https%3A%2F%2Fcommunity.hubspot.com%2Ft5%2Fgrouphubs%2Fpage%2Fuser-id%2F169781'>Log in</a> </div> </div> </nav> </div> <script> const openMenu = document.querySelector(".open-menu"); const closeMenu = document.querySelector(".close-menu"); const menuWrapper = document.querySelector(".menu-wrapper"); const hasCollapsible = document.querySelectorAll(".has-collapsible"); // Sidenav Toggle openMenu.addEventListener("click", function () { menuWrapper.classList.add("offcanvas"); }); closeMenu.addEventListener("click", function () { menuWrapper.classList.remove("offcanvas"); }); // Collapsible Menu hasCollapsible.forEach(function (collapsible) { collapsible.addEventListener("click", function () { collapsible.classList.toggle("active"); // Close Other Collapsible hasCollapsible.forEach(function (otherCollapsible) { if (otherCollapsible !== collapsible) { otherCollapsible.classList.remove("active"); } }); }); }); </script> </div> </div> </div><div class="lia-quilt-row lia-quilt-row-main"> <div class="lia-quilt-column lia-quilt-column-24 lia-quilt-column-single lia-quilt-column-main-content"> <div class="lia-quilt-column-alley lia-quilt-column-alley-single"> <script> // Inline click listeners function getHubSpotClickListener(btnId) { if(btnId == "get-hubspot-free-v2"){ if (document.getElementById('get-hubspot-v2')) { document.getElementById('get-hubspot-v2').classList.toggle("show"); } } } function langPickerClickListener() { if (document.getElementById('lang-picker-global')) { document.getElementById('lang-picker-global').classList.toggle("show"); } } </script> <style> /* Text customization for CSS generated content */ .lia-list-row-thread-solved:after { content: "Solved"!important; text-transform: uppercase; } .SearchPage .lia-replies-toggle-link:before { content: "Replies"!important; } label.lia-form-label:after { // content: "(required)"; margin-left: 4px; } .lia-form-board-entry label.lia-form-label:after, .lia-form-subject-entry label.lia-form-label:after, .lia-form-body-entry label.lia-form-label:after, .lia-form-labels-entry label.lia-form-label:after { content: "(required)"; } .lia-form-login-entry .lia-form-input-wrapper:before { content: "Username*"!important; } .lia-form-profile-first-name-entry:before { content: "First name*"!important; } .lia-form-profile-last-name-entry:before { content: "Last name*"!important; } .lia-note-unread:after { content: "Unread"; text-transform: uppercase; } .EditPage label.lia-form-label:after, .EditPage .lia-form-label.lia-fieldset-title:after, .EditPage .lia-component-tkb-article-editor-form .lia-form-label.lia-form-compare-title:after, .lia-component-tkb-article-editor-form .EditPage .lia-form-label.lia-form-compare-title:after, .EditPage .lia-component-tkb-article-editor-form .lia-form-label.lia-revision-info-title:after, .lia-component-tkb-article-editor-form .EditPage .lia-form-label.lia-revision-info-title:after, .EditPage .lia-component-tkb-article-editor-form .lia-form-label.lia-related-messages-title:after, .lia-component-tkb-article-editor-form .EditPage .lia-form-label.lia-related-messages-title:after, .ReplyPage label.lia-form-label:after, .ReplyPage .lia-form-label.lia-fieldset-title:after, .ReplyPage .lia-component-tkb-article-editor-form .lia-form-label.lia-form-compare-title:after, .lia-component-tkb-article-editor-form .ReplyPage .lia-form-label.lia-form-compare-title:after, .ReplyPage .lia-component-tkb-article-editor-form .lia-form-label.lia-revision-info-title:after, .lia-component-tkb-article-editor-form .ReplyPage .lia-form-label.lia-revision-info-title:after, .ReplyPage .lia-component-tkb-article-editor-form .lia-form-label.lia-related-messages-title:after, .lia-component-tkb-article-editor-form .ReplyPage .lia-form-label.lia-related-messages-title:after, .PostPage label.lia-form-label:after, .PostPage .lia-form-label.lia-fieldset-title:after, .PostPage .lia-component-tkb-article-editor-form .lia-form-label.lia-form-compare-title:after, .lia-component-tkb-article-editor-form .PostPage .lia-form-label.lia-form-compare-title:after, .PostPage .lia-component-tkb-article-editor-form .lia-form-label.lia-revision-info-title:after, .lia-component-tkb-article-editor-form .PostPage .lia-form-label.lia-revision-info-title:after, .PostPage .lia-component-tkb-article-editor-form .lia-form-label.lia-related-messages-title:after, .lia-component-tkb-article-editor-form .PostPage .lia-form-label.lia-related-messages-title:after, .MyProfilePage label.lia-form-label:after, .MyProfilePage .lia-form-label.lia-fieldset-title:after, .MyProfilePage .lia-component-tkb-article-editor-form .lia-form-label.lia-form-compare-title:after, .lia-component-tkb-article-editor-form .MyProfilePage .lia-form-label.lia-form-compare-title:after, .MyProfilePage .lia-component-tkb-article-editor-form .lia-form-label.lia-revision-info-title:after, .lia-component-tkb-article-editor-form .MyProfilePage .lia-form-label.lia-revision-info-title:after, .MyProfilePage .lia-component-tkb-article-editor-form .lia-form-label.lia-related-messages-title:after, .lia-component-tkb-article-editor-form .MyProfilePage .lia-form-label.lia-related-messages-title:after, .KudosMessagePage label.lia-form-label:after, .KudosMessagePage .lia-form-label.lia-fieldset-title:after, .KudosMessagePage .lia-component-tkb-article-editor-form .lia-form-label.lia-form-compare-title:after, .lia-component-tkb-article-editor-form .KudosMessagePage .lia-form-label.lia-form-compare-title:after, .KudosMessagePage .lia-component-tkb-article-editor-form .lia-form-label.lia-revision-info-title:after, .lia-component-tkb-article-editor-form .KudosMessagePage .lia-form-label.lia-revision-info-title:after, .KudosMessagePage .lia-component-tkb-article-editor-form .lia-form-label.lia-related-messages-title:after, .lia-component-tkb-article-editor-form .KudosMessagePage .lia-form-label.lia-related-messages-title:after { // content: "(required)"; } .ForumPage span.in-english:after, .CategoryPage span.in-english:after { content: "EN"; } a.in-english:after { content: "EN"; } .nav-menu a.in-english:after { content: "EN"; } .header-search-wrapper{margin-right:5px;margin-top: 33px;} #lia-body .nav-wrapper .header-search-wrapper .lia-search-form-wrapper{position: relative;left: 0px;border:none;box-shadow:none;max-width: 460px;width: 100%;} #lia-body .nav-wrapper .header-search-wrapper .lia-button-searchForm-action{top:8px !important;position: absolute;right: 6px;min-width: auto !important; background-color: transparent !important;border: none !important;max-height: 18px !important; background-size:contain !important;background-image: url(/html/assets/search-icon.svg)} #lia-body .nav-wrapper .header-search-wrapper .search-input::-webkit-input-placeholder{font-size: 16px !important;} #lia-body .nav-wrapper .header-search-wrapper .search-input{height: 32px !important;padding: 10px !important;padding-right: 50px !important;border: 1px solid #CBD6E2 !important;box-shadow: none !important;background-color: white !important;font-size: 16px !important; border-radius: 3px !important;color: #33475B !important;line-height:24px !important;} #lia-body .header-search-wrapper .lia-search-input-wrapper {width: 460px !important;} #lia-body .nav-wrapper .header-search-wrapper .lia-search-input-wrapper input:focus {box-shadow: 0 0 4px 1px rgb(255 255 255 / 30%), 0 0 0 1px #fff!important;} .header-search-wrapper .lia-search-granularity-wrapper .lia-search-form-granularity{display:none !important;} .header-search-wrapper .lia-search-granularity-wrapper:before{display:none !important;} @media only screen and (max-width: 992px) { #lia-body .nav-wrapper .header-search-wrapper .lia-search-form-wrapper{z-index: 1;} } @media only screen and (max-width: 767px) { .user-nav-bar{ position: relative; height: 100%; } #lia-body .header-search-wrapper .lia-search-input-wrapper{ width: 100% !important; max-width: none;} #lia-body .nav-wrapper .header-search-wrapper .lia-search-form-wrapper{padding: 10px 24px !important; background: #fff;border: 1px solid #eee2e2;margin-left: 4px;max-width:none !important;width: 100%;} #lia-body .nav-wrapper .header-search-wrapper .lia-button-searchForm-action {right:20px;top:17px !important;} .header-search-wrapper{display:none; position: absolute; top: 121px; width: 100% !important;} #lia-body .nav-wrapper .header-search-wrapper .lia-search-input-message{ margin-right:-7px;} #lia-body .nav-wrapper .header-search-wrapper .lia-search-form-wrapper::before{position: absolute; top: -6px;content: "";right: 67px;width: 10px;height: 10px;background-color: white;transform: rotate(45deg);border-top: 1px solid #eee2e2; border-left: 1px solid #eee2e2;} } </style> <section class="community-header-nav v2"> <div class="nav-wrapper"> <div class="user-nav-bar"> <nav class="nav-menu template-centered template-section"> <div class="lang-picker-wrapper"> <div class="lang-picker-container"> <a id="current-language" class="current-language"> <span class="lang-picker-globe-icon"></span> English </a> <div id="lang-picker-global" class="nav-popover lang-picker"> <div class="nav-popover-arrow" style="border-top-color: transparent; border-left-color: transparent; width: 20px; height: 20px; transform: rotate(-135deg); top: -10px; right: calc(50% - 48px);"></div> <ul id="lang-picker-dropdown" class="nav-dropdown-list"> <a href="https://community.hubspot.com/t5/grouphubs/page/user-id/169781?profile.language=en" class="nav-dropdown-link" data-lang="en"> <li class="nav-dropdown-item"> English </li> </a> <a href="https://community.hubspot.com/t5/grouphubs/page/user-id/169781?profile.language=es" class="nav-dropdown-link" data-lang="es"> <li class="nav-dropdown-item"> Español </li> </a> <a href="https://community.hubspot.com/t5/grouphubs/page/user-id/169781?profile.language=pt-br" class="nav-dropdown-link" data-lang="pt-br"> <li class="nav-dropdown-item"> Português </li> </a> <a href="https://community.hubspot.com/t5/grouphubs/page/user-id/169781?profile.language=fr" class="nav-dropdown-link" data-lang="fr"> <li class="nav-dropdown-item"> Français </li> </a> <a href="https://community.hubspot.com/t5/grouphubs/page/user-id/169781?profile.language=de" class="nav-dropdown-link" data-lang="de"> <li class="nav-dropdown-item"> Deutsch </li> </a> <a href="https://community.hubspot.com/t5/grouphubs/page/user-id/169781?profile.language=ja" class="nav-dropdown-link" data-lang="ja"> <li class="nav-dropdown-item"> 日本語 </li> </a> </ul> </div> </div></div> <div class="header-search-wrapper"> <div id='lia-searchformV32_23fd879321d040' class='SearchForm lia-search-form-wrapper lia-mode-default lia-component-common-widget-search-form'> <div class='lia-inline-ajax-feedback'> <div class='AjaxFeedback' id='ajaxfeedback_23fd879321d040'></div> </div> <div id='searchautocompletetoggle_23fd879321d040'> <div class='lia-inline-ajax-feedback'> <div class='AjaxFeedback' id='ajaxfeedback_23fd879321d040_0'></div> </div> <form enctype='multipart/form-data' class='lia-form lia-form-inline SearchForm' action='https://community.hubspot.com/t5/grouphubs/page.searchformv32.form.form' method='post' id='form_23fd879321d040' name='form_23fd879321d040'><div class='t-invisible'><input value='user-id/169781' name='t:ac' type='hidden'></input><input value='search/contributions/page' name='t:cp' type='hidden'></input><input value='xtnqIU9Z02vqrhUw8h8pKxayRah9gCmlo8f-6JnSJpZ29VOakOsi17xn2rE2csC2uDJiuOT2jZHahK46AEn_NFQ490XhWIQxH8Bw-t204BQZRZf6oizNRrgRaUZNKqL_ca3moqqDKoX1-zNzWLaFVckhI070AxL65niNi8evHHyzlS1dc_iFsR6iQmIS5JIXgoyraCQLI3922Nl8w5sEmoDgqhRhAg-jLuYyeRmB6T3ZCJ6LqPFFcnVDeV_AWQliLArK5bRXa3gbyqNrwH4-fgU9fNKSlIr8TODLEsui5VZqh534YwCQdUXiHz0k9pcB3Od3M0c8Oz-ymhABhXYfn-XKaAdOXACFf-2EdcNUNR_qD2Kq2Ng8LpC77BrdBmVkMw6qYyBJ1Ojt2YXDKPD_-lckd9DpAZddJwWgej-AzvB6fpvCxjYBjgrQ5bWKUIEvaR4O76w1ffnu7krbcBUW5fyxxV-_j1sI41BJtgvn0qpeudf6_x8GWv3Vcnonj8IUt5tl5-h9uueW12XZw1aimx-SzuHsC3dQNeHP4SId3OO7DLueMb2jBpDTgUZpeUmr4EmW7oQn1RTboXK5onXJX6GSRfNp0mcNOow2ECHYI_vRUD6PzrF0LbM17Ffy9ZyAYhN1XwuvGS2EzDFgWt-Ntw7oyjecqY2GTRWVoPL4fC46XC1RCQMtPMLX0MrZsyhZRVeD2KrUuImMu-r47q-kbF83p55NQ9dPAubZXDr67i_BvqJh7LEsi1BCWDFtrGp1PNLYtPlls9WlKiaLKY9OcCTHFiFxb69KcWJ47qE3h4KjoebM3GRHgZPLz8jcFmpQcxbE7jEfPe3fr0XMyyFj_J4jrhZVDgQC7Gwu46lPaAhhl0b9D7kwZldqkyMUKxrHdt7rq_jODmOH26VuSp0JJYBmekkwk2RDA29hSrLG0GlbF1MRXj4ys4Ym5m2PysVPskyJG7ZyVTbp9zxETspMNZDeLVhaVJ6TMIH-q1AldEt4DrwFf05ubk-fPyWzA8BpzGQFS-3xOr4Mwm78MIU5uZQXkoY6CtDl1fWDbVa1w0k09LRn6ZALppRcyjHcE76liJI2hHhZCG41jROb_6xBgWA55kvSj2BeqAnjkmwZoK7dLFNw1Cwm8if2nHkSXKCndQ6hPGRpFWklSGm8CgllD8bsVMJqifGIXeD_BecYQfiu_BiTkHo83pgZEx8JSHaUDl7xXYtC0AUUEN1UH-FUd1i_Y6jBfJuhoDkpoDVtZR0.' name='lia-form-context' type='hidden'></input><input value='GroupHubsPage:user-id/169781:searchformv32.form:' name='liaFormContentKey' type='hidden'></input><input value='5DI9GWMef1Esyz275vuiiOExwpQ=:H4sIAAAAAAAAALVSTU7CQBR+krAixkj0BrptjcpCMSbERGKCSmxcm+kwlGrbqTOvFDYexRMYL8HCnXfwAG5dubDtFKxgYgu4mrzvm3w/M+/pHcphHQ4kI4L2dMo9FLYZoM09qbeJxQ4V0+XC7e/tamqyBPEChwgbh1JAjQtLIz6hPaYh8ZlEMaxplAvm2KZmEsm0hhmBhOKpzZzOlsEw8LevR5W3zZfPEqy0oJIYc+eCuAyh2rolfaI7xLN0I8rjWfWBj7CuzJvf5osmbxRN3hacMimNwHRtKSOr0XNnv/vx+FoCGPjhMRzljhNLYHrEt9kA5T08ACCsKvREoYuqxqLl8BLO84q4UcMITcG49y/QOGs1pYyESl5p6V6qwRW086rinVmoxMZsiZud/zBUTc6gmVc4kExkJafmcYG1GM9+wfIsCkf2OP54hal5EjnG54z8h0XhjfcF7wQUs5Kz0GTjU2rOjc/llTT4Au07pDOcBQAA' name='t:formdata' type='hidden'></input></div> <div class='lia-inline-ajax-feedback'> <div class='AjaxFeedback' id='feedback_23fd879321d040'></div> </div> <input value='YoI2hS30uguCCgyoBI5370v1qNHVXJ0EhjmkHsnioAw.' name='lia-action-token' type='hidden'></input> <input value='form_23fd879321d040' id='form_UIDform_23fd879321d040' name='form_UID' type='hidden'></input> <input value='' id='form_instance_keyform_23fd879321d040' name='form_instance_key' type='hidden'></input> <span class='lia-search-granularity-wrapper'> <select title='Search Granularity' class='lia-search-form-granularity search-granularity' aria-label='Search Granularity' id='searchGranularity_23fd879321d040' name='searchGranularity'><option title='All Results' selected='selected' value='mjmao93648|community'>All Results</option><option title='kennedyp' value='169781|authorMessages'>kennedyp</option><option title='Users' value='user|user'>Users</option></select> </span> <span class='lia-search-input-wrapper'> <span class='lia-search-input-field'> <span class='lia-button-wrapper lia-button-wrapper-secondary lia-button-wrapper-searchForm-action'><input value='searchForm' name='submitContextX' type='hidden'></input><input class='lia-button lia-button-secondary lia-button-searchForm-action' value='Search' id='submitContext_23fd879321d040' name='submitContext' type='submit'></input></span> <input placeholder='Search the Community' aria-label='Search' title='Search' class='lia-form-type-text lia-autocomplete-input search-input lia-search-input-message' value='' id='messageSearchField_23fd879321d040_0' name='messageSearchField' type='text'></input> <input placeholder='Search the Community' aria-label='Search' title='Search' class='lia-form-type-text lia-autocomplete-input search-input lia-search-input-tkb-article lia-js-hidden' value='' id='messageSearchField_23fd879321d040_1' name='messageSearchField_0' type='text'></input> <input ng-non-bindable='' title='Enter a user name or rank' class='lia-form-type-text UserSearchField lia-search-input-user search-input lia-js-hidden lia-autocomplete-input' aria-label='Enter a user name or rank' value='' id='userSearchField_23fd879321d040' name='userSearchField' type='text'></input> <input placeholder='Enter a keyword to search within the private messages' title='Enter a search word' class='lia-form-type-text NoteSearchField lia-search-input-note search-input lia-js-hidden lia-autocomplete-input' aria-label='Enter a search word' value='' id='noteSearchField_23fd879321d040_0' name='noteSearchField' type='text'></input> <input title='Enter a search word' class='lia-form-type-text ProductSearchField lia-search-input-product search-input lia-js-hidden lia-autocomplete-input' aria-label='Enter a search word' value='' id='productSearchField_23fd879321d040' name='productSearchField' type='text'></input> <input class='lia-as-search-action-id' name='as-search-action-id' type='hidden'></input> </span> </span> <span class='lia-cancel-search'>cancel</span> </form> <div class='search-autocomplete-toggle-link lia-js-hidden'> <span> <a class='lia-link-navigation auto-complete-toggle-on lia-link-ticket-post-action lia-component-search-action-enable-auto-complete' data-lia-action-token='EgHfWfby5jLJvG47zYliGM9HnUTDKpljH3jbQLXvtgU.' rel='nofollow' id='enableAutoComplete_23fd879321d040' href='https://community.hubspot.com/t5/grouphubs/page.enableautocomplete:enableautocomplete?t:ac=user-id/169781&t:cp=action/contributions/searchactions'>Turn on suggestions</a> <span class='HelpIcon'> <a class='lia-link-navigation help-icon lia-tooltip-trigger' role='button' aria-label='Help Icon' id='link_23fd879321d040' href='#'><span class='lia-img-icon-help lia-fa-icon lia-fa-help lia-fa' alt='Auto-suggest helps you quickly narrow down your search results by suggesting possible matches as you type.' aria-label='Help Icon' role='img' id='display_23fd879321d040'></span></a><div role='alertdialog' class='lia-content lia-tooltip-pos-bottom-left lia-panel-tooltip-wrapper' id='link_23fd879321d040_0-tooltip-element'><div class='lia-tooltip-arrow'></div><div class='lia-panel-tooltip'><div class='content'>Auto-suggest helps you quickly narrow down your search results by suggesting possible matches as you type.</div></div></div> </span> </span> </div> </div> <div class='spell-check-showing-result'> Showing results for <span class='lia-link-navigation show-results-for-link lia-link-disabled' aria-disabled='true' id='showingResult_23fd879321d040'></span> </div> <div> <span class='spell-check-search-instead'> Search instead for <a class='lia-link-navigation search-instead-for-link' rel='nofollow' id='searchInstead_23fd879321d040' href='#'></a> </span> </div> <div class='spell-check-do-you-mean lia-component-search-widget-spellcheck'> Did you mean: <a class='lia-link-navigation do-you-mean-link' rel='nofollow' id='doYouMean_23fd879321d040' href='#'></a> </div> </div> </div> <div class="user-nav-options"> <!-- community-header-nav-upper-v2 --> <style>.get-hubspot .nav-popover-arrow {border-top-color: transparent; border-left-color: transparent; width: 20px; height: 20px; transform: rotate(-135deg); top: -10px; display: block;}</style> <div class="search-icon-plus-top"> <button class="lia-button search-toggle-action-icon-plus"><img src='https://community.hubspot.com/html/@5320E40129AA1377479EABCA2009B53A/assets/Start-dicsucssion.svg' alt=""><i class="lia-fa lia-fa-caret-down"></i></button> <div class="plus-bar-main-content" style="display: none;"> <ul id="plus-bar-top-main"> <li class="plus-bar"> <a href="https://app.hubspot.com/khoros/integration/jwt/authenticate?referer=https%3A%2F%2Fcommunity.hubspot.com%2Ft5%2Fforums%2Fpostpage%2Fcategory-id%2Fhubspot_community_en%2Fchoose-node%2Ftrue" class="white-btn transpaent"><i><img src="https://community.hubspot.com/html/@38F5B2AB35958F39F47C2BFFE5486135/assets/Edit.svg"></i> Create post</a> </li> <li class="plus-bar"><a href="https://app.hubspot.com/khoros/integration/jwt/authenticate?referer=https%3A%2F%2Fcommunity.hubspot.com%2Ft5%2Fforums%2Fpostpage%2Fboard-id%2FHubSpot_Ideas"><i><img src="https://community.hubspot.com/html/@C71A42AEB76B8A3A82335DA9F5B9C717/assets/lightbulb.svg"></i> Submit Idea</a></li> </ul> </div> </div> <div class="login-container"> <a class='lia-link-navigation login-link lia-authentication-link lia-component-users-action-login' rel='nofollow' id='loginPageV2_23fd87952c65e4' href='https://app.hubspot.com/khoros/integration/jwt/authenticate?referer=https%3A%2F%2Fcommunity.hubspot.com%2Ft5%2Fgrouphubs%2Fpage%2Fuser-id%2F169781'>Log in</a> <a id="get-hubspot-free" class="btn btn-sm button-tertiary get-hubspot">Get HubSpot free</a> <div id="get-hubspot" class="get-hubspot"> <div class="nav-popover-arrow"></div> <ul id="get-hs-dropdown" class="nav-dropdown-list"> <li class="nav-dropdown-item nav-crm"> <a href="https://www.hubspot.com/products/get-started" class="nav-dropdown-link nav-crm"> Get a HubSpot CRM account </a> </li> <li class="nav-dropdown-item nav-dev"> <a href="https://developers.hubspot.com/" class="nav-dropdown-link nav-dev"> Get a HubSpot developer account </a> </li> </ul> </div> </div> </div> </nav> </div> <div class="forum-nav-bar"> <nav class="nav-menu template-centered template-section"> <a href="/" id="nav-logo" class="en nav-wordmark"> <img src='https://community.hubspot.com/html/@60C06466C7735C4373198758B1428669/assets/Community-logo-new.svg' alt="hubspot" class="nav-wordmark"> <span class="page-title nav-wordmark">Community</span> </a> <a href="/" id="nav-logo-v2" class="en nav-wordmark v2"> <img src='https://community.hubspot.com/html/@813D252A70F0A7024C8EA3BB1B8B9CFD/assets/sticky-logo.png' alt="hubspot" class="nav-wordmark"> </a> <style> .jp-resources-list{ display:flex; flex-direction:column; } .jp-resources-list>li.nav-dropdown-item.jp-class-HubSpot.Community.Blog{ order:1; } .nav-external-link::after { background: none; content: "\f08e"; font-family: "FontAwesome"; margin-left: 5px; font-size: 14px; } </style> <div class="nav-group-wrapper"> <div class="nav-menu"> <ul class="nav-group-primary"> <li class="nav-group-item nav-group-item-has-dropdown"> <div class="nav-link-wrapper"> <a class="nav-link forum"> <span class="nav-link-label"> Discussions </span> <span class="dropdown-caret"></span> </a> <div class="nav-popover-arrow"></div> <div class="nav-popover forum"> <ul class="nav-dropdown-list"> <li class="nav-dropdown-item"> <a href="/t5/CRM-Sales-Hub/ct-p/sales" class="nav-dropdown-link nav-discussions-crm"> CRM & Sales </a> </li> <li class="nav-dropdown-item"> <a href="/t5/Marketing-Hub/ct-p/marketing" class="nav-dropdown-link nav-discussions-mktg"> Marketing & Content </a> </li> <li class="nav-dropdown-item"> <a href="/t5/Service-Hub/ct-p/service_hub" class="nav-dropdown-link nav-discussions-svc"> Customer Success & Service </a> </li> <li class="nav-dropdown-item"> <a href="/t5/Operations/ct-p/Operations" class="nav-dropdown-link nav-discussions-ops"> RevOps & Operations </a> </li> <li class="nav-dropdown-item"> <a href="/t5/Commerce/ct-p/commerce" class="nav-dropdown-link nav-discussions-commerce "> Commerce </a> </li> <li class="nav-dropdown-item"> <a href="/t5/HubSpot-Developers/ct-p/developers" class="nav-dropdown-link nav-discussions-developers "> Developers </a> </li> <li class="nav-dropdown-item"> <a href="https://community.hubspot.com/t5/Getting-Started-on-the-Community/How-to-join-the-Solutions-Partner-Program/ba-p/400205" class="nav-dropdown-link nav-partners "> Partners </a> </li> <li class="nav-dropdown-item"> <a href="/t5/HubSpot-Ideas/idb-p/HubSpot_Ideas" class="nav-dropdown-link nav-discussions-ideas"> Ideas </a> </li> </ul> </div> </div> </li> <li class="nav-group-item nav-group-item-has-dropdown secondInList"> <div class="nav-link-wrapper"> <a class="nav-link forum"> <span class="nav-link-label"> Academy </span> <span class="dropdown-caret"></span> </a> <div class="nav-popover-arrow"></div> <div class="nav-popover forum"> <ul class="nav-dropdown-list"> <li class="nav-dropdown-item"> <a href="https://academy.hubspot.com/courses" class="nav-dropdown-link nav-external-link" target="_blank"> Courses </a> </li> <li class="nav-dropdown-item"> <a href="https://academy.hubspot.com/certification-overview" class="nav-dropdown-link nav-external-link" target="_blank"> Certifications </a> </li> <li class="nav-dropdown-item"> <a href="https://www.hubspot.com/academy/bootcamps/home" class="nav-dropdown-link nav-external-link" target="_blank"> Bootcamps </a> </li> <li class="nav-dropdown-item"> <a href="https://academy.hubspot.com/learning-paths" class="nav-dropdown-link nav-external-link" target="_blank"> Learning Paths </a> </li> <li class="nav-dropdown-item"> <a href="https://community.hubspot.com/t5/HubSpot-Academy-Support/bd-p/certifications_help" class="nav-dropdown-link "> Academy Support </a> </li> <li class="nav-dropdown-item"> <a href="https://community.hubspot.com/t5/Study-Groups/ct-p/study-groups" class="nav-dropdown-link"> Study Groups </a> </li> </ul> </div> </div> </li> <li class="nav-group-item"> <div class="nav-link-wrapper"> <a class="nav-link nav-resources"> <span class="nav-link-label"> Resources </span> <span class="dropdown-caret"></span> </a> <div class="nav-popover-arrow"></div> <div class="nav-popover forum"> <ul class="nav-dropdown-list jp-resources-list"> <li class="nav-dropdown-item "> <a href="/t5/Getting-Started/ct-p/getting_started" class="nav-dropdown-link nav-discussions-gs"> Getting Started </a> </li> <li class="nav-dropdown-item"> <a href="https://help.hubspot.com/" class="nav-dropdown-link nav-external-link" target="_blank"> Help Center </a> </li> <li class="nav-dropdown-item"> <a href="https://knowledge.hubspot.com/" class="nav-dropdown-link nav-external-link" target="_blank"> Knowledge Base </a> </li> <li class="nav-dropdown-item"> <a href="https://developers.hubspot.com/docs/api/overview" class="nav-dropdown-link nav-external-link" target="_blank"> API Documentation </a> </li> <li class="nav-dropdown-item"> <a href="https://developers.hubspot.com/docs/cms" class="nav-dropdown-link nav-external-link" target="_blank"> CMS Documentation </a> </li> <li class="nav-dropdown-item "> <a href="/t5/News-Networking-Events/ct-p/communityboard" class="nav-dropdown-link nav-news"> News </a> </li> <li class="nav-dropdown-item "> <a href="/t5/Resources/ct-p/resources?node_id=webinars&order_by=last_updated" class="nav-dropdown-link nav-resource-blog-Webinars "> Webinars </a> </li> <li class="nav-dropdown-item "> <a href="/t5/Resources/ct-p/resources?node_id=releases-updates" class="nav-dropdown-link nav-resource-blog-Releases and Updates "> Releases and Updates </a> </li> <li class="nav-dropdown-item "> <a href="/t5/Resources/ct-p/resources?node_id=hubspot-community-blog" class="nav-dropdown-link nav-resource-blog-HubSpot Community Blog "> Community Blog </a> </li> <li class="nav-dropdown-item "> <a href="/t5/Resources/ct-p/resources?node_id=sales-hub-community-perspectives" class="nav-dropdown-link nav-resource-blog-Sales Hub Community Perspectives "> Sales Hub Community Perspectives </a> </li> <li class="nav-dropdown-item "> <a href="/t5/Resources/ct-p/resources?node_id=workflows_library" class="nav-dropdown-link nav-resource-blog-Workflows Library "> Workflows Library </a> </li> <li class="nav-dropdown-item "> <a href="/t5/Resources/ct-p/resources?node_id=ai-library" class="nav-dropdown-link nav-resource-blog-Breeze Library "> Breeze Library </a> </li> </ul> </div> </div> </li> <li class="nav-group-item nav-group-item-has-dropdown"> <div class="nav-link-wrapper"> <a class="nav-link forum"> <span class="nav-link-label"> Events </span> <span class="dropdown-caret"></span> </a> <div class="nav-popover-arrow"></div> <div class="nav-popover forum"> <ul class="nav-dropdown-list"> <li class="nav-dropdown-item"> <a href="https://community.hubspot.com/t5/Ask-Me-Anything-and-Panel/bd-p/ama_discussions" class="nav-dropdown-link "> AMA </a> </li> <li class="nav-dropdown-item"> <a href="https://community.hubspot.com/t5/Community-Led-Events/bd-p/adapt" class="nav-dropdown-link "> Community Led Events </a> </li> <li class="nav-dropdown-item"> <a href="https://www.hubspot.com/resources/webinar" class="nav-dropdown-link nav-external-link" target="_blank"> Webinars </a> </li> <li class="nav-dropdown-item"> <a href="https://www.hubspot.com/hubspot-user-groups" class="nav-dropdown-link nav-external-link" target="_blank"> HUGS </a> </li> </ul> </div> </div> </li> <li class="nav-group-item nav-group-item-has-dropdown"> <div class="nav-link-wrapper"> <a class="nav-link forum"> <span class="nav-link-label"> Advocacy </span> <span class="dropdown-caret"></span> </a> <div class="nav-popover-arrow"></div> <div class="nav-popover forum"> <ul class="nav-dropdown-list"> <li class="nav-dropdown-item"> <a href='/t5/Advocacy/ct-p/advocacy' class="nav-dropdown-link nav-hubFans-program "> Community Champions Program </a> </li> <li class="nav-dropdown-item"> <a href="/t5/Advocates-Blog/bg-p/advocates-blog" class="nav-dropdown-link nav-adovcates-blog "> Champions Blog </a> </li> </ul> </div> </div> </li> </ul> <div class="login-container v2"> <style> #lia-body .community-header-nav.v2 .forum-nav-bar.ch-sticky .nav-menu > ul.nav-group-primary { transform: none !important; } #lia-body .community-header-nav.v2 .forum-nav-bar.ch-sticky .login-container.v2 { display: flex; align-items: center; gap: 12px; padding-left: 0; width: 116px; } #lia-body.lia-user-status-anonymous .community-header-nav.v2 .forum-nav-bar.ch-sticky .login-container.v2 { gap: 0; width: 24px; } .ch-sticky .nav-menu { gap: 36px; } .custom-search-focus { background-image: url('https://community.hubspot.com/html/@C11BAC294B66222FECF4AA35890ACE71/assets/search-icon.svg'); background-repeat: no-repeat; background-size: contain; cursor: pointer; margin: 0; width: 24px; height: 24px; } </style> <label class="custom-search-focus"></label> <div class="nav-link-wrapper custom-user-menu-v2"> <div class="nav-popover-arrow"></div> <div class="nav-popover profile first"> <div class="header-dropdown-menu"> <div class="user-heading">Anonymous</div> <ul class="header-tab-nav"> <li><span id="profile" class="active">Profile</span></li> </ul> <div class="header-tab-nav-content"> <div id="profile-list-wrapper"> <div class="nav-link-wrapper"> <a href="https://app.hubspot.com/l/reports-dashboard/" class="text-link my-account nav-hubspot-account" target="_blank"> Go to my HubSpot Account </a> </div> <ul class="nav-dropdown-list"> <li class="nav-dropdown-item"> <a href="/t5/user/viewprofilepage/user-id/-1" class="nav-dropdown-link nav-account-profile-2"> My Profile </a> </li> <li class="nav-dropdown-item"> <a href="/t5/user/myprofilepage/tab/personal-profile" class="nav-dropdown-link nav-account-user-settings"> Settings </a> </li> <li class="nav-dropdown-item"> </li> <li class="nav-dropdown-item"> <a href="https://community.hubspot.com/t5/community/page.logoutpage?t:cp=authentication/contributions/unticketedauthenticationactions&dest_url=https%3A%2F%2Fcommunity.hubspot.com%2Ft5%2Fgrouphubs%2Fpage%2Fuser-id%2F169781&lia-action-token=4uwfsHm4KqV6Hp0NfWaXl0vzp2vt0WtEKT6MAQfQ58g.&lia-action-token-id=logoff" class="nav-dropdown-link nav-account-sign-out"> Sign out </a> </li> </ul> </div> <div id="admin-list-wrapper" style="display: none;"> <div class="admin-menu-list"> </div> </div> </div> </div> </div> </div> </div> </div> </div> </nav> </div> </div> </section> </div> </div> </div><div class="lia-quilt-row lia-quilt-row-footer"> <div class="lia-quilt-column lia-quilt-column-24 lia-quilt-column-single lia-quilt-column-common-footer lia-mark-empty"> </div> </div> </div> </div> </div> </div><div class="lia-quilt-row lia-quilt-row-main"> <div class="lia-quilt-column lia-quilt-column-07 lia-quilt-column-left lia-quilt-column-side-content lia-mark-empty"> </div><div class="lia-quilt-column lia-quilt-column-17 lia-quilt-column-right lia-quilt-column-main-content"> <div class="lia-quilt-column-alley lia-quilt-column-alley-right"> <div aria-label="breadcrumbs" role="navigation" class="BreadCrumb crumb-line lia-breadcrumb lia-component-common-widget-breadcrumb"> <ul role="list" class="lia-list-standard-inline"> <li class="lia-breadcrumb-node crumb"> <a href="/" class="crumb-community lia-breadcrumb-community lia-breadcrumb-forum community-home">HubSpot Community</a> </li> <li class="lia-breadcrumb-node crumb final-crumb"> <span>Group Hubs for kennedyp</span> </li> </ul> </div> <style> .lia-quilt-category-page-group-listing .lia-limuirs-comp.lia-component-grouphubs-widget-grid .lia-form-group-list-filters:before { } .lia-groups-list .lia-menu-navigation-wrapper.lia-menu-action.lia-menu-overflow-right.lia-menu-allow-selected:nth-child(3) .dropdown-default-item:before { font-size: 14px; color: #000; font-family: "Avenir Next W02"; line-height: 20px; content: "Filter by"; } @media (max-width: 1128px){ #lia-body .lia-quilt-category-page-template-groups .lia-limuirs-comp.lia-component-grouphubs-widget-grid .lia-form-inline.lia-form-group-list-filters .lia-menu-navigation-wrapper{ order: 2; width: auto; margin-left: 0; display: inline-block; } .lia-groups-list .lia-menu-navigation-wrapper.lia-menu-action.lia-menu-overflow-right.lia-menu-allow-selected:nth-child(3) .dropdown-default-item:before { display:none; } } @media (max-width: 1024px){ #lia-body .lia-quilt-category-page-template-groups .lia-limuirs-comp.lia-component-grouphubs-widget-grid .lia-form-inline.lia-form-group-list-filters .lia-menu-navigation-wrapper{ order: 2; width: auto; margin-left: 0; display: inline-block; } .lia-groups-list .lia-menu-navigation-wrapper.lia-menu-action.lia-menu-overflow-right.lia-menu-allow-selected:nth-child(3) .dropdown-default-item:before { display:none; } } @media (max-width: 767px){ #lia-body .lia-quilt-category-page-template-groups .lia-limuirs-comp.lia-component-grouphubs-widget-grid .lia-form-inline.lia-form-group-list-filters .lia-menu-navigation-wrapper{ order: 2; width: auto; margin-left: 0; display: inline-block; } .lia-groups-list .lia-menu-navigation-wrapper.lia-menu-action.lia-menu-overflow-right.lia-menu-allow-selected:nth-child(3) .dropdown-default-item:before { display:none; } } @media (max-width: 339px){ #lia-body .lia-quilt-category-page-template-groups .lia-menu-navigation-wrapper.lia-menu-action.lia-menu-overflow-right.dropdownHover.lia-menu-allow-selected .lia-menu-navigation .dropdown-default-item .dropdown-positioning .dropdown-positioning-static .lia-menu-dropdown-items{ left: 45px; } } .lia-groups-list .lia-menu-navigation-wrapper.lia-menu-action.lia-menu-overflow-right.lia-menu-allow-selected:nth-child(2) .dropdown-default-item:before { content: "Order by"; } </style> <li:grouphubs-group-hubs-grid class="lia-limuirs-comp lia-component-grouphubs-widget-grid" data-lia-limuirs-comp="{"mode":"DEFAULT","componentId":"grouphubs.widget.grid","showTitle":false,"path":"limuirs\u002Fcomponents\u002Fgrouphubs\u002FGroupHubsGrid","alias":"grouphubs.widget.grid","instance":0,"fqPath":"0\u002Flimuirs\u002Fcomponents\u002Fgrouphubs\u002FGroupHubsGrid"}"><div class="lia-groups-list"><form class="lia-form lia-form-inline lia-form-group-list-filters"><input type="text" placeholder="Filter by name" title="Filter by name" class="lia-form-type-text lia-form-input-inline lia-form-filter-by-name-input" value=""><div class="lia-menu-navigation-wrapper lia-menu-action lia-menu-overflow-right lia-menu-allow-selected"><div class="lia-menu-navigation"><div class="dropdown-default-item"><a href="javascript:void(0);" title="Member count" class="lia-js-menu-opener default-menu-option lia-button-navigation" aria-expanded="false" aria-label="Show option menu" role="button" aria-haspopup="true">Member count</a><div class="dropdown-positioning"><div class="dropdown-positioning-static"><ul class="lia-menu-dropdown-items" role="listbox"><li><a href="javascript:void(0)" class="lia-menu-label lia-link-disabled">Sort by</a></li><li><a href="javascript:void(0)" class="lia-menu-item">Date created (oldest)</a></li><li><a href="javascript:void(0)" class="lia-menu-item">Date created (newest)</a></li><li><a href="javascript:void(0)" class="lia-menu-item">Latest activity</a></li><li><a href="javascript:void(0)" class="lia-link-disabled lia-item-selected lia-menu-close-bypass lia-menu-item">Member count</a></li><li><a href="javascript:void(0)" class="lia-menu-item">Name</a></li><li><a href="javascript:void(0)" class="lia-menu-item">Post count</a></li></ul></div></div></div></div></div><div class="lia-menu-navigation-wrapper lia-menu-action lia-menu-overflow-right lia-menu-allow-selected"><div class="lia-menu-navigation"><div class="dropdown-default-item"><a href="javascript:void(0);" title="All group types" class="lia-js-menu-opener default-menu-option lia-button-navigation" aria-expanded="false" aria-label="Show option menu" role="button" aria-haspopup="true">All group types</a><div class="dropdown-positioning"><div class="dropdown-positioning-static"><ul class="lia-menu-dropdown-items" role="listbox"><li><a href="javascript:void(0)" class="lia-menu-label lia-link-disabled">Filter by type</a></li><li><a href="javascript:void(0)" class="lia-link-disabled lia-item-selected lia-menu-close-bypass lia-menu-item">All group types</a></li><li><a href="javascript:void(0)" class="lia-menu-item">Open</a></li><li><a href="javascript:void(0)" class="lia-menu-item">Closed</a></li></ul></div></div></div></div></div></form><ul class="lia-cards"><li class="lia-card-item"><div class="lia-group-hub-card lia-card" data-lia-group-uid="grouphub:study-group-inbound"><a href="/t5/Inbound/gh-p/study-group-inbound" class="lia-link-navigation"><div class="lia-quilt lia-quilt-group-hub-card lia-quilt-layout-one-column"><div class="lia-quilt-row lia-quilt-row-header"><div class="lia-quilt-column lia-quilt-column-24 lia-quilt-column-single lia-quilt-column-common-header"></div></div><div class="lia-quilt-row lia-quilt-row-main"><div class="lia-quilt-column lia-quilt-column-24 lia-quilt-column-single lia-quilt-column-main-content"><div class="lia-membership-type lia-membership-type-open"></div><img class="lia-node-avatar" src="/t5/image/serverpage/image-id/45260i54F3605EC818A699/image-size/tiny/crop-image/true?v=v2&px=100" alt="Inbound"><h4 class="lia-node-title">Inbound<!-- --> </h4><div class="lia-node-description">A place for inbound professionals to share ideas, learn, network, and be inspired.</div><div class="lia-membership-count"><i class="lia-fa lia-fa-user "></i>12126</div><div class="lia-node-topic-count"><i class="lia-fa lia-fa-comment-o"></i>155</div></div></div><div class="lia-quilt-row lia-quilt-row-footer"><div class="lia-quilt-column lia-quilt-column-24 lia-quilt-column-single lia-quilt-column-common-footer"></div></div></div></a></div></li><li class="lia-card-item"><div class="lia-group-hub-card lia-card" data-lia-group-uid="grouphub:Women_In_Tech"><a href="/t5/Women-in-Tech/gh-p/Women_In_Tech" class="lia-link-navigation"><div class="lia-quilt lia-quilt-group-hub-card lia-quilt-layout-one-column"><div class="lia-quilt-row lia-quilt-row-header"><div class="lia-quilt-column lia-quilt-column-24 lia-quilt-column-single lia-quilt-column-common-header"></div></div><div class="lia-quilt-row lia-quilt-row-main"><div class="lia-quilt-column lia-quilt-column-24 lia-quilt-column-single lia-quilt-column-main-content"><div class="lia-membership-type lia-membership-type-open"></div><img class="lia-node-avatar" src="/t5/image/serverpage/image-id/35492i55896808D811C855/image-size/tiny/crop-image/true?v=v2&px=100" alt="Women in Tech"><h4 class="lia-node-title">Women in Tech<!-- --> </h4><div class="lia-node-description">Women in Tech HubSpot User Group</div><div class="lia-membership-count"><i class="lia-fa lia-fa-user "></i>734</div><div class="lia-node-topic-count"><i class="lia-fa lia-fa-comment-o"></i>36</div></div></div><div class="lia-quilt-row lia-quilt-row-footer"><div class="lia-quilt-column lia-quilt-column-24 lia-quilt-column-single lia-quilt-column-common-footer"></div></div></div></a></div></li><li class="lia-card-item"><div class="lia-group-hub-card lia-card" data-lia-group-uid="grouphub:studentspot"><a href="/t5/StudentSpot/gh-p/studentspot" class="lia-link-navigation"><div class="lia-quilt lia-quilt-group-hub-card lia-quilt-layout-one-column"><div class="lia-quilt-row lia-quilt-row-header"><div class="lia-quilt-column lia-quilt-column-24 lia-quilt-column-single lia-quilt-column-common-header"></div></div><div class="lia-quilt-row lia-quilt-row-main"><div class="lia-quilt-column lia-quilt-column-24 lia-quilt-column-single lia-quilt-column-main-content"><div class="lia-membership-type lia-membership-type-open"></div><img class="lia-node-avatar" src="/t5/image/serverpage/image-id/66276i43039547FC51BCB3/image-size/tiny/crop-image/true?v=v2&px=100" alt="StudentSpot"><h4 class="lia-node-title">StudentSpot<!-- --> </h4><div class="lia-node-description">A place for life long learners to connect, engage, learn, and be inspired.</div><div class="lia-membership-count"><i class="lia-fa lia-fa-user "></i>274</div><div class="lia-node-topic-count"><i class="lia-fa lia-fa-comment-o"></i>40</div></div></div><div class="lia-quilt-row lia-quilt-row-footer"><div class="lia-quilt-column lia-quilt-column-24 lia-quilt-column-single lia-quilt-column-common-footer"></div></div></div></a></div></li></ul></div></li:grouphubs-group-hubs-grid> </div> </div> </div><div class="lia-quilt-row lia-quilt-row-footer"> <div class="lia-quilt-column lia-quilt-column-24 lia-quilt-column-single lia-quilt-column-common-footer"> <div class="lia-quilt-column-alley lia-quilt-column-alley-single"> <div class="lia-quilt lia-quilt-footer lia-quilt-layout-one-column lia-component-quilt-footer"> <div class="lia-quilt-row lia-quilt-row-header"> <div class="lia-quilt-column lia-quilt-column-24 lia-quilt-column-single lia-quilt-column-common-header lia-mark-empty"> </div> </div><div class="lia-quilt-row lia-quilt-row-main"> <div class="lia-quilt-column lia-quilt-column-24 lia-quilt-column-single lia-quilt-column-main-content"> <div class="lia-quilt-column-alley lia-quilt-column-alley-single"> <!-- FOOTER START --> <footer class="community-footer"> <div class="community-footer-wrapper template-centered"> <div class="footer-responsive"> <div class="contain-top"> <div class="col"> <h5>HubSpot</h5> <ul> <li><a href="https://www.hubspot.com/">Home</a></li> <li><a href="https://help.hubspot.com/">Help</a></li> <li><a href="https://academy.hubspot.com/">Academy</a></li> <li><a href="https://knowledge.hubspot.com/">Knowledge Base</a></li> <li><a href="https://ecosystem.hubspot.com/marketplace/solutions">Solutions Directory</a></li> <li><a href="https://blog.hubspot.com/">Blog</a></li> </ul> </div> <div class="col"> <h5>Get involved</h5> <ul> <li><a href= "https://offers.hubspot.com/community-champions" class="">Community Champions</a> </li> <li><a href= "https://www.hubspot.com/hubspot-user-groups" class="">HubSpot User Groups</a></li> <li><a href= "https://www.hubspot.com/partners/solutions" class="">Solutions Partner Program</a></li> <li><a href= "https://www.hubspot.com/community-newsletter" class="">Community Newsletter</a></li> </ul> </div> <div class="col"> <h5>Community</h5> <ul> <li><a href= "https://community.hubspot.com/t5/CRM-Sales-Hub/ct-p/sales">CRM & Sales</a></li> <li><a href= "https://community.hubspot.com/t5/Marketing-Hub/ct-p/marketing">Marketing</a></li> <li><a href= "https://community.hubspot.com/t5/Service-Hub/ct-p/service_hub">Service</a></li> <li><a href= "https://community.hubspot.com/t5/RevOps-Operations/ct-p/Operations">RevOps & Operations</a></li> <li><a href= "https://community.hubspot.com/t5/HubSpot-Developers/ct-p/developers" class="">Developers</a></li> <li><a href= "https://community.hubspot.com/t5/Getting-Started-on-the-Community/How-to-join-the-Solutions-Partner-Program/ba-p/400205" class="">Partners</a></li> </ul> </div> <div class="col"> <ul> <li><a href= "https://community.hubspot.com/t5/Academy/ct-p/academy">Academy</a></li> <li><a href= "https://community.hubspot.com/t5/Groups/ct-p/groups">Groups</a></li> <li><a href= "/t5/Advocacy/ct-p/advocacy" class="">Advocacy</a></li> <li><a href= "/t5/HubSpot-Ideas/idb-p/HubSpot_Ideas" class="">Ideas</a></li> </ul> </div> </div> <div class="contain-top-two"> <hr class="seperator"> </div> </div> <div class="footer-main-two"> <div class="footer-copywrite-container"> <a href="/" id="footer-logo" class="footer-wordmark"> <img src="https://community.hubspot.com/html/@B5A74D0D426EC31D4B4C76F0526FF1E5/assets/HS_Logo_Wordmark-White.svg" alt="hubspot"> </a> <span id="copywrite">Copyright © 2024 HubSpot, Inc.</span> </div> <div class="footer-links-container"> <ul class="footer-link"> <li> <a href="https://legal.hubspot.com/privacy-policy" class="footer-terms">Privacy Policy</a> </li> <li> <a href="https://legal.hubspot.com/community-tou" class="footer-privacy">Community Terms of Use</a> </li> <li> <a href="https://community.hubspot.com/t5/Getting-Started-on-the-Community/HubSpot-Community-Guidelines/ba-p/384050" class="footer-guidelines">Community Guidelines</a> </li> <li> <a href="https://status.hubspot.com/" class="footer-status">Status</a> </li> <li> <a href="https://legal.hubspot.com/digital-services-act" class="footer-dsa">DSA Statement</a> </li> <li class="hs-footer-cookie-settings footer-cookie-settings" hidden> <a href=""></a> </li> </ul> </div> </div> </div> </footer> </div> </div> </div><div class="lia-quilt-row lia-quilt-row-footer"> <div class="lia-quilt-column lia-quilt-column-24 lia-quilt-column-single lia-quilt-column-common-footer lia-mark-empty"> </div> </div> </div> </div> </div> </div> </div> </div> </div> </div> </div> <script> document.cookie = "Crowdvocate_user_ck=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; //console.log('deleting cookie for logged out case'); //console.log('is cookie set? '+ getCookie('Crowdvocate_user_ck')); </script> <!-- Start HubSpot SDK Script --> <script>window.acsdk = {cid: 'WSUMXJFRDOXTWNTJ8YKRUEQ6GHNP1UAUP9EA', params: {popupDelay: 10, position: [0, 20, 50, 0, 0]}};</script> <script>(function(){var w=window;var ac=w.Crowdvocate;if(typeof ac==="function"){ac('init',w.acsdk);}else{var a=function(){a.c(arguments)};a.q=[];a.c=function(args){a.q.push(args)};w.Crowdvocate=a;} var s=document.createElement('script');s.type='text/javascript';s.async=true;s.src='https://d29zub39v1xeg4.cloudfront.net/api/v1/sdk.js';var x=document.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);})();</script> <!-- End of HubSpot SDK Script --> </center> </div> <script type="text/javascript"> new Image().src = ["/","b","e","a","c","o","n","/","1","2","5","4","9","0","3","2","6","4","9","0","_","1","7","3","2","5","0","0","7","9","1","0","7","4",".","g","i","f"].join(""); </script> <script type="text/javascript" src="/t5/scripts/C1D0FDEB5D557CE5FA1EFA105E95A13F/lia-scripts-common-min.js"></script><script type="text/javascript" src="https://limuirs-assets.lithium.com/assets/limuirs-24_7-vendors~main.5ef86aa8c72fe4cbb8d6.js"></script><script type="text/javascript" src="https://limuirs-assets.lithium.com/assets/limuirs-24_7-main.138f37e85bead07e28fd.js"></script><script type="text/javascript" src="/t5/scripts/C591454ADEF77ABB5B131FC908C96817/lia-scripts-body-min.js"></script><script language="javascript" type="text/javascript"> <!-- LITHIUM.Sandbox.restore(); LITHIUM.jQuery.fn.cssData.defaults = {"dataPrefix":"lia-js-data","pairDelimeter":"-","prefixDelimeter":"-"}; (function($){ jQuery(document).on('click', '#hs-eu-confirmation-button', function() { location.reload(); }); })(LITHIUM.jQuery); LITHIUM.CommunityJsonObject.User.policies['forums.action.message-view.batch-messages.allow'] = false; // <script> // $ prefix because 'core' is kinda prone to conflict with other stuff that might define a variable 'core' // If there are issues with markup_output (on communities that have auto-escaping enabled!) you can add // ?no_esc behind the expression, but can't have that conditionally there because FreeMarker will just throw // an exception just because the built in is in the code if auto-escaping is disabled... var $core = { config: {"initialized" : true,"debug" : false,"devmode" : false,"env" : "prod","context" : "component","versions" : {"lithium" : 24.8,"freemarker" : "2.3.26-incubating","core.component" : 23.8,"core.users" : 23.5,"core" : 23.4},"file" : "core.cmp.noscript","output" : "undefined","locale" : {"charset" : "UTF-8","timezone" : "US/Eastern","format" : {"date" : "MMM d, yyyy","time" : "h:mm a","full" : "MMM d, yyyy h:mm a","relative" : true,"cutoff" : 31}},"lang" : "en","langs" : ["en","de","es","fr","ja","pt-br"],"node" : {"id" : "mjmao93648","uid" : 1,"lang" : "en","type" : "community","style" : "none","url" : "https://community.hubspot.com/","quilt" : "GroupHubsPage","skin" : "hubspot","path" : "\/","top" : "\/categories/id/mjmao93648","settings_key" : "config_node"},"user" : {"admin" : false,"mod" : false,"auth" : false,"device" : "desktop","lang" : "en","registered" : false,"roles" : [],"show_text_keys" : false,"settings_key" : "config_user","settings" : {}},"cache" : {"cached" : false,"type" : "","map" : {},"config" : {"usercache" : {"inactiveTime" : 1200000,"maximumTime" : 7200000,"maxSize" : 10000},"appcache" : {"inactiveTime" : 1200000,"maximumTime" : 7200000,"maxSize" : 10000}}},"modules" : {"api" : false,"community" : false,"component" : true,"nodes" : false,"templates" : false,"users" : true},"ids" : {"lang" : {"en" : "hubspot_community_en","de" : "hubspot_community_de","es" : "hubspot_community_es","fr" : "hubspot_community_fr","ja" : "hubspot_community_jp","pt" : "hubspot_community_pt"}},"request" : {"get" : "","uri" : "https://community.hubspot.com/t5/grouphubs/page/user-id/169781","type" : "GET","https" : true,"options" : {},"context" : "","endpoint" : "https://community.hubspot.com/mjmao93648/plugins/custom/hubspot/hubspot/controller"},"rest_default_version" : 1,"rest_base_url_v1" : "https://community.hubspot.com/restapi/v1","rest_base_url_v2" : "https://community.hubspot.com/api/2.0/search?q=","stats" : {"requests" : 0,"total" : 0,"calls" : []},"logs" : []}, properties: {}, // Add a reference to the global (native) JS object for the community, can be useful lithium: LITHIUM?.CommunityJsonObject, version: LITHIUM?.CommunityJsonObject?.Config?.['app.revision'] || null }; // </script> // <script> just for inline syntax-highlighting... ;(function($){ var tools = { admin: { /** /* By default admin/studio pages all have the same page title "Community Settings" which is extermely unhelpful /* when having 10 admin/studio tabs open and then having to cycle through them just to find the right one. /* This fix makes admin/studio tabs more distinguishable. /*/ fixStudioTitle: function() { var title = [ $('.lia-bizapps-tab-studio-tab-group .lia-tabs-active').text(), $('.lia-bizapps-tab-community-tab-group .lia-tabs-active').text(), $('.lia-bizapps-page-title-community').text(), ]; $('title').text(title.join(' - ')); }, /** /* In multi-language communities it can be difficult to distinguish nodes if they all have the same title but another language. /* Usually we add a language suffix to the node ID but that is not easily visible in the community structure. Thea idea of this /* enhancement is to extract that language suffix from the node id and add it to the node title within the community structure. /*/ structureAddLanguageFromID: function(validLanguages = ['de', 'fr', 'it', 'jp', 'en', 'es', 'pt']) { $('.lia-component-admin-widget-node-editor-tree .lia-list-tree-toggle-node').each(function() { var $listitem = $(this); var $title = $listitem.find('span.lia-node-display-node-title:first'); var id = $listitem.find('.manage-node-link').first().prop('href').split('_').pop().trim().toLowerCase(); // console.log(id); if ( validLanguages.includes(id) ) { $title.text($title.text() + ' (' + id.toUpperCase() + ')'); } }); }, }, dom: function() { /** /* Just a little clutter saver for components. We should specify both aria-label and title /* attributes, but doing so can lead to very messy markup. As aria-label is more important /* devs have the option of simply adding an empty title attribute as well to elements which /* have aria-label already. This little tool will look for those and simply copy the aria-label /* text over to the title attribute so mouse-users can also get the hints as tooltips. /*/ $('[aria-label][title=""]').each(function() { $(this).attr('title', $(this).attr('aria-label')); }); /** /* A 'big-target' implementation I came up with using a `data-target` attribute on the actual link. /*/ (function(attr = 'data-target') { document.querySelectorAll(`a[${attr}]`).forEach(link => { //console.log('handling big target', link); let trigger = link.parentNode; const selector = link?.getAttribute(attr); while (trigger && trigger !== document) { if ( (!selector || trigger.matches(selector)) && !trigger.matches('[data-edit], [data-bind]') ) { trigger.style.cursor = 'pointer'; trigger.querySelectorAll('a').forEach(link => link.addEventListener('click', (e) => e.stopPropagation())); trigger.addEventListener('click', (e) => (!window.getSelection().toString() && link.click())); break; } trigger = trigger.parentNode; } }); })(); }, installKV: function() { /** /* Creates a Deno KV-like key-value store based on the localStorage with optional /* (custom) versioning and migration support. /* This has passed basic testing, although not how well it aligns with what native Deno KV does itself! /* The API is the same minus atomic operations and some options like consistency level, Kv64 etc. /* that don't really make sense or are insanely complex or impossible to do with a synchronous /* API like localStorage is. The KV's methods are all fake async (because Deno KV's api is async) /* so code written and used in the browser with this implementation should (hopefully) work /* with Deno KV on the server side as well (muuuch more testing needed to confirm that)! /* Aside from `open()` and `close()` this implementation should cover the entire Deno KV API. /* /* There are of course differences in behavior you should be aware of if you build any logic around it: /* - The most important limitation with this mock is that if you use non-serializable values in /* in your keys (your values too!), it will not behave like you expect! Stick to types that can be /* serialized to JSON... localStorage can only handle strings, that's the reason. /* - With Deno KV you can pass an `expireIn` option, but the values you receive from `get()` /* will never return that expiration date. This implementation returns that information /* when querying a key, e.g. besides value and versionstamp you get back `expires` /* if a TTL was provided when setting the value, it's going to be an ISO timestamp otherwise `null`. /* - `list()` is an AsyncIterator like it is with Deno KV, but it does NOT support cursor, consistency /* or batchSize options, you can pass them to the method, but nothing will happen. /* Deno KV `list()` requires a selector, this implementation does not, it will simply list all /* KV entries if you don't provide a selector. /* Furthermore this `list()` implementation supports an optional 3rd argument which is a function /* passed to `Array.filter()` that allows filtering the returned entries further after selector(s) /* and options have been applied. /* - This implementation does support some features Deno KV does not have: /* 1. You can optionally provide a `version` function that will replace how the versionstamp is created /* and thus provide your own versioning implementation. /* 2. You can also provide a `migrate` function that will be applied if an outdated versionstamp /* is encountered, you can for example extend the old value with some new ones keeping what was stored. /* 3. There is a `toJSON()` method you can use to serialize either the entire KV store or a portion of it. /* It supports the same arguments as `list()` (because it internally calls it) and has options to /* serialize prettified JSON or streamable (individual store entries separated by newlines) JSON. /* /* @param {Function} [version] - Optional function to generate a versionstamp for stored values. /* @param {Function} [migrate] - Optional function to migrate outdated values. It is called with the key and outdated value and should return the migrated value. /* @param {String} [prefix='kv:'] - Prefix to be used for keys in localStorage, helping in namespacing and avoiding key conflicts. /* /* @returns {Object} - An object providing the mocked Deno KV API methods to interact with the store. /*/ Object.defineProperties(window, { $kv: { value: function(version, migrate, prefix = 'kv:') { const $hash = typeof version === 'function' ? version : ( $hash || ((v) => 1) ); const _queueListeners = new Set(); const _key = { inrange: (key, start, end) => { const orderedKey = _key.order(key); const orderedStart = start ? _key.order(start) : null; const orderedEnd = end ? _key.order(end) : null; if (orderedStart && orderedKey < orderedStart) { return false; } if (orderedEnd && orderedKey >= orderedEnd) { return false; } return true; }, order: (key) => key.slice().sort((a, b) => { const order = ['Uint8Array', 'string', 'number', 'bigint', 'boolean']; const typeA = typeof a, typeB = typeof b; return typeA === typeB ? (typeA === 'number' ? a - b : String(a).localeCompare(String(b))) : order.indexOf(typeA) - order.indexOf(typeB); }), // Check how Deno KV actually does it here: // https://github.com/denoland/deno/blob/main/ext/kv/codec.rs serialize: (key) => prefix + JSON.stringify(key), deserialize: (serializedKey) => JSON.parse(serializedKey.substring(prefix.length)) }; return { /** /* Retrieves a value from the store by its key(s). /* /* @param {Array} key - The keys array to look up the value. /* /* @returns {Promise<Object>} - An object containing the key, its associated value, versionstamp and expiry. /*/ get: async (key) => { const serializedKey = _key.serialize(key); const data = JSON.parse(localStorage.getItem(serializedKey)); if ( !data || (data.expires && new Date(data.expires) < new Date()) ) { data && data.expires && localStorage.removeItem(serializedKey); return { key, value: null, versionstamp: null }; } if ( data.versionstamp !== $hash(data.value) ) { if (typeof migrate === 'function') { data.value = await migrate(key, data.value); data.versionstamp = $hash(data.value); localStorage.setItem(serializedKey, JSON.stringify(data)); } else { console.warn('Outdated value found but no migration function defined!', key, data); } } return { key, ...data }; }, /** /* Retrieves multiple values from the store based on the provided keys. /* /* @param {Array<Array>} keys - An array of key arrays to look up the values. /* /* @returns {Promise<Array<Object>>} - An array of objects. /*/ getMany: async function(keys) { return Promise.all(keys.map(key => this.get(key))) }, /** /* Stores a value in the store with the given key. /* /* @param {Array} key - The key array to associate with the value. /* @param {*} value - The value to be stored. /* @param {Object} [options] - Optional parameters for storing the value. `expireIn` sets the expiration time in milliseconds. /* /* @returns {Promise<Object>} - An object indicating the success and the versionstamp of the stored value. /*/ set: async (key, value, options = {}) => { const data = { value, versionstamp: $hash(value), expires: options.expireIn ? new Date(Date.now() + options.expireIn).toISOString() : null }; localStorage.setItem(_key.serialize(key), JSON.stringify(data)); return { ok: true, versionstamp: data.versionstamp }; }, /** /* Removes a value from the store by its key. /* /* @param {Array} key - The key array of the value to be removed. /*/ delete: async (key) => localStorage.removeItem(_key.serialize(key)), /** /* Iterates over values in the store based on the provided selector prefix, range, or limited by options. /* This method provides an AsyncIterator to be used with `for await...of` loops. /* This `list()` implementation supports an additional third argument which is NOT STANDARD for Deno KV. /* It's a function that allows for further filtering the results before yielding them within the iterator. /* /* The selector can either be a prefix selector or a range selector: /* - A prefix selector selects all keys that start with the given prefix (optionally starting at a given key). /* - A range selector selects all keys that are lexicographically between the given start and end keys. /* /* @param {Object} [selector] - The selection criteria. NOT STANDARD: Can be undefined here, not with Deno KV! /* @property {Array} [selector.prefix] - Defines the prefix for filtering the results. /* @property {Array} [selector.start] - The starting key for range selection. /* @property {Array} [selector.end] - The ending key for range selection. /* @param {Object} [options] - Optional parameters for the listing. /* @property {number} [options.limit] - Limits the number of results. /* @property {boolean} [options.reverse] - If true, reverses the order of results. /* @param {Function} [fn] - NON STANDARD: Optional filter function to filter entries before yielding. /* /* @returns {AsyncIterator} - An AsyncIterator yielding store entries. /*/ list: async function*(selector, options = {}, fn) { const keys = options?.reverse ? Object.keys(localStorage).reverse() : Object.keys(localStorage); for (const serializedKey of keys) { // Limit results based on `options.limit` if ( options?.limit !== undefined && options.limit <= 0 ) { break; } if ( !serializedKey.startsWith(prefix) ) { continue; }; const deserializedKey = _key.deserialize(serializedKey); if ( selector?.prefix && !deserializedKey.slice(0, selector.prefix.length).every((part, index) => part === selector.prefix[index]) ) { continue; } if ( selector?.start && !_key.inrange(deserializedKey, selector?.start, selector?.end) ) { continue; } const entry = await this.get(deserializedKey); if ( entry.value !== null ) { if ( !fn || (typeof fn === 'function' && fn(entry)) ) { if ( options?.limit !== undefined ) { options.limit--; } yield entry; } } } }, /** /* Adds a value into a mock database queue to be delivered to queue listeners. /* This method simulates the behavior of the Deno KV's enqueue(). /* `keysIfUndelivered` option is not supported, you can pass it, but won't have an effect. /* /* @param {*} value - The value to be enqueued. /* @param {Object} [options] - Optional settings for the enqueue operation. /* @param {number} [options.delay] - Delays the delivery of the value by the specified number of milliseconds. /* @returns {Promise<Object>} - An object indicating the success of the enqueue operation. /*/ enqueue: async (value, options) => { for (const fn of _queueListeners) { options?.delay && await (new Promise(res => setTimeout(res, options.delay))); await fn(value); }; }, /** /* Listens for queue values to be delivered from the mock database queue. /* This method simulates the behavior of the Deno KV's listenQueue(). /* /* @param {Function} handler - A callback function that gets when a new value is dequeued. /* /* @returns {Promise<void>} /*/ listenQueue: async (handler) => { if ( !_queueListeners.has(handler) ) { _queueListeners.add(handler) } }, /** /* Provides a mock for Deno Kv's AtomicOperation API with the same chainable methods, but /* they do nothing...you can provide a function to each of the mock methods that receives /* `this`, just return it again, otherwise you break the chaining! Something like this: /* /* @example /* ``` /* (await $core.kv().atomic()).check((t) => (console.log('check'), t)).min((t) => (console.log('min'), t)).commit(); /* // you can pass an arbitrary number of additional args to those mock functions, like /* (await $core.kv().atomic()).check((t, arg1, arg2) => (console.log('check', arg1, arg2), t), 'foo', 'var') /* ``` /* /* @returns {Promise<*>} - A mocked API of Deno KV's atomic(). /*/ atomic: async () => { console.warn('Atomic ops are not supported!'); return [ 'check', 'commit', 'delete', 'enqueue', 'max', 'min', 'mutate', 'set', 'sum' ].reduce((r, m) => (r[m] = function(fn, ...args) { return typeof fn === 'function' ? fn(this, ...args) : this }, r), Object.create(null)); }, /** /* NON STANDARD: Serializes the store to a JSON string. /* The methods first 3 arguments match the ones from list() (see there for details). /* /* @param {Object} selector - The selection criteria for list(). /* @param {Object} [options] - Optional parameters for list(). /* @param {Function} [fn] - Optional filter function list(). /* @param {Number|String} [pretty] - JSON.stringify() space param to prettify output. Defatuls to tab indent. /* @param {Boolean} [streamable] - If the output should be stringified entries separated by newlilnes. /* /* @returns {String} - A JSON string representation of the KV store. /*/ toJSON: async function(selector, options, fn, pretty = '\t', streamable = false) { let data = []; for await (const entry of this.list(selector, options, fn)) { data.push(entry); } data = data.reduce((r, entry) => { if ( streamable ) { r.push(JSON.stringify({ key: entry.key, value: entry.value })); return r; } return r[JSON.stringify(entry.key)] = entry.value, r; }, streamable ? [] : {}); return streamable ? data.join('\n') : JSON.stringify(data, null, pretty); }, }; }, configurable: false, // Cannot be deleted enumerable: false, // Will not show up in loops writable: false, // Cannot be overwritten } }); }, logMeIn: function() { /** /* Enable 'magic' redirect to login page when "logmein" is typed into the void =) /* just for convenience and speed, only useful on stage if SSO is activated for the community /*/ var keycodes = { logmein: [76, 79, 71, 77, 69, 73, 78], listudio: [], liadmin: [] }; var neededkeys = [76, 79, 71, 77, 69, 73, 78]; var watching = false; var count = 0; $(document).keydown(function(e) { var key = e.keyCode; // console.log(key); // Set start to true only if the first key in the sequence is pressed if ( !watching ) { if ( key == neededkeys[0] ) { watching = true; } } // If watching, pay attention to key presses, looking for right sequence. if ( watching ) { // console.log('watching: ' + key); if ( neededkeys[count] == key ) { // We're good so far. count++; } else { // Oops, not the right sequence, lets restart from the top. watching = false; count = 0; return; } if ( count == neededkeys.length ) { // We made it! Execute whatever should happen when entering the right sequence window.location.replace('/t5/user/userloginpage'); // Reset the conditions so that someone can do it all again. watching = false; count = 0; return; } } else { // Oops. watching = false; count = 0; return; } }); }, scssCompile: function() { /** /* Handle on-the-fly SCSS compilation if any text/scss inline style tags are found /* This primarily useful for development, no style/scss tags should remain when on /* production as they would still be compiled. /*/ if ( $('style[type$="scss"]').length ) { $('style[type$="scss"]').each(function() { var $el = $(this); var scss = $el.text(); $core?.config?.user?.admin && console.log('Found inline SCSS style tag, compiling to CSS...', $el); $.ajax({ type: 'POST', url: 'https://www.sassmeister.com/app/lib/compile', data: { input: scss, compiler: 'lib', syntax: 'scss', original_syntax: 'scss', output_style: 'nested' }, contentType: 'multipart/form-data', dataType: 'json' }) .done(function(response) { $core?.config?.user?.admin && console.log('core.cmp.tools: SCSS successfully compiled, injecting usable CSS...'); // inject compiled CSS into irignal source tag $el.text(response.css).attr('type', 'text/css'); }) .fail(function(err) { console.log('fail', err); }); }); } }, redirect: function() { /** /* Handles redirects specified in `cmp.global.scripts` (or elsewhere) and added to /* the `$core.redirects` object. /*/ if ( $core?.redirects ) { Object.entries($core.redirects).some(function([source, target]) { if ( window.location.pathname === source ) { $core?.config?.user?.admin && console.log('core.cmp.tools: redirect match found, redirecting...'); window.location.href = target; // Stop the .some() iteration by returning true return true; } }); } }, fixAmp: function() { /** /* Handle & ampersand bug in SEO field of ArticleEditorPage (TKB, more?) /* & gets replace with & but each time the article is saved again, the & of the & /* gets encoded again, resulting in repeated and invalid encoding, e.g. &amp;amp;amp;amp; /*/ $('.lia-form-message-seo-description-input, .lia-form-message-seo-title-input').each(function() { $(this).val($(this).val().replaceAll(/amp;/gm, '')); }); }, fixTOC: function() { /** /* Adds 'is--toc' class to `ul`-tag of BlogArticle TOC (table of contents, an Angular component by Khoros) /* as it natively does not have any identifier, making it tricky to target with CSS /* WebKit browsers remove list semantics when list-style-type is none, we fix that with `role=list` /*/ $('.BlogArticlePage a[href*="#toc-hId"]').first().parents('ul').addClass('is--toc').attr('role', 'list'); }, inview: function() { // Make sure nothing explodes in older browsers that do not support IntersectionObserver if ( 'IntersectionObserver' in window && 'IntersectionObserverEntry' in window && 'intersectionRatio' in window.IntersectionObserverEntry.prototype ) { // Minimal polyfill for Edge 15's lack of `isIntersecting` // See: https://github.com/w3c/IntersectionObserver/issues/211 if ( !('isIntersecting' in window.IntersectionObserverEntry.prototype) ) { Object.defineProperty(window.IntersectionObserverEntry.prototype, 'isIntersecting', { get: function () { return this.intersectionRatio > 0; } }); } // Set up the intersection observer const observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { // If the element is fully in view, trigger custom event with jQuery if (entry.isIntersecting && entry.intersectionRatio === 1) { entry.target.dataset.inview = 'true'; $(document).trigger('inview', [entry.target]); } else if ( entry.target.dataset.inview !== 'false' ) { entry.target.dataset.inview = 'false'; } }); }, { threshold: 1 }); // Observe each element with attribute `data-inview` $('[data-inview]').each(function() { observer.observe(this); }); } }, XHRmiddleware: function() { // Middleware storage const middlewares = []; // Override the XMLHttpRequest constructor (breaks Khoros for some reason...) /* const xhrRequest = XMLHttpRequest; XMLHttpRequest = function() { const xhr = new xhrRequest(); return new Proxy(xhr, { get: function(target, prop) { // You can intercept specific properties here and provide custom values if ( prop === 'responseText' ) { let value = target[prop]; middlewares.forEach((fn) => { if ( !fn.on || fn.on.includes('response') { // make sure in case the dev forgets to return from the fn() // we still return a value, it's gonna be the unmodified one // but better than nothing... value = fn(target, 'response', value) || value; } }); return value; } // For all other properties, return the original value return target[prop]; }, set: function(target, prop, value) { target[prop] = value; return true; } }); }; XMLHttpRequest.prototype = xhrRequest.prototype; */ // Save original XHR methods const xhrSend = XMLHttpRequest.prototype.send; const xhrOpen = XMLHttpRequest.prototype.open; // Define the .$use() method using Object.defineProperty Object.defineProperty(XMLHttpRequest, '$use', { value: function(fn) { if ( typeof fn === 'function' ) { middlewares.push(fn); } }, writable: false, // Cannot be overwritten enumerable: false, // Will not show up in loops configurable: false, // Cannot be deleted }); // Override the .open() method XMLHttpRequest.prototype.open = function(method, url, async, user, password) { middlewares.forEach((fn) => { !fn.on || fn.on.includes('open') ? fn(this, 'open', method, url, async, user, password) : null; }); xhrOpen.apply(this, arguments); }; // Override the .send() method XMLHttpRequest.prototype.send = function(body) { const xhr = this; middlewares.forEach((fn) => { !fn.on || fn.on.includes('send') ? fn(xhr, 'send', body) : null; }); // Attach an event listener for the 'readystatechange' event xhr.addEventListener('readystatechange', function() { if ( xhr.readyState === XMLHttpRequest.DONE ) { middlewares.forEach((fn) => { !fn.on || fn.on.includes('done') ? fn(xhr, 'done') : null; }); } }); xhrSend.apply(this, arguments); }; // Example middleware /* XMLHttpRequest.$use((xhr, event, ...args) => { if ( event === 'open' ) { // args is going to be [method, url, async, user, password] if defined console.log('Request created:', xhr, ...args); } else if ( event === 'send' ) { // args is going to be [body] the optional payload/body of the request console.log('Request payload is about to be sent:', args[0]); } else if ( event === 'response' ) { // args is going to be [responseText]! console.log('Can modify response text!, xhr); // IMPORTANT: Return something from here, // otherwise response is gonna be returned umodified to the caller! return xhr; } else if ( event === 'done' ) { console.log('Response received:', xhr.responseText); } }); */ }, }; // Install tools before extending core in case we need access to globally defined tools! Object.entries(tools).map(function([name, fn]) { if ( typeof fn === 'function' ) { $core?.config?.user?.admin && console.info(`core.cmp.tools: Running tools.${name}()`); fn(); } }); // Extend $core base variable with additional capabilities if ( $core ) { /** /* Helper method to automatically proxy requests to external URL's through the proxy endpoint /* when they can't be requested directly via JS due to CORS restrictions. /*/ $core.compile = async function(mount) { console.time("compiling"); // start benchmark // Handles inline tag definitions and script-linked tags with `[type="riot/tag"]`. // Why not go with the native riot.compile()? a) we want to check first if we actually // need to fetch the tag file or not (if it wasn't modified) and b) we want to compile // tags that are defined inline as well and cache the compiled result of both for faster // loading during prototyping. const usedb = true; const db = localStorage; const sources = Array.prototype.slice.call(document.querySelectorAll('script[type*="riot"]')).concat(Array.prototype.slice.call(document.querySelectorAll('template[type*="riot"]'))); const tags = await Promise.all( sources.map(async (el, i, arr) => { //console.log('compiling from:', el.hasAttribute('data-src') ? 'File' : 'Inline Template'); let cached; let hash; let tag; let response; if (el.hasAttribute('data-src')) { try { if ( usedb ) { // first we want to check the last modified header! // if the dev server is not running this will fail and enter catch block response = await fetch(el.getAttribute('data-src'), { // credentials: 'include', method: 'HEAD', }); // typecast to string for localStorage (keys are strings!) hash = `riot:${await $hash(el.getAttribute('data-src') + new Date(response.headers.get('last-modified')).getTime())}`; cached = db.getItem(hash); } if ( !cached ) { console.log('compile(): No cached version found, fetching source!'); response = await fetch(el.getAttribute('data-src'), { // credentials: 'include', method: 'GET', }); data = await response.json(); response = { headers: [...response.headers].reduce((acc, header) => { return { ...acc, [header[0]]: header[1] }; }, {}), status: response.status, data: data, }; //console.log('response', response); // add attribute data-scoped="false" to the include script tag to turn off CSS scoping //tag = riot.compileFromString(response.data, { scopedCss: !['false', '0', 0].includes(el.getAttribute('data-scoped')) }).code; // TODO: Dev server should return compiled code exactly as riot.deno.dev does tag = response.data?.code; //console.log('tag', tag); if ( tag && usedb ) { db.setItem(hash, tag); } console.log(`compile(): No cached tag found, file-tag compiled ${usedb ? `and cached with hash ${hash}`: ''}`); } else { tag = cached; console.log(`compile(): Found cached version of file-tag!`); } } catch (ex) { // We do not get details for net errors (e.g. if the server is down), so we try to // isolate those because they don't have a message (well, which ones do? custom ones?) if ( !ex.data?.message ) { console.warn(`compile(): fetching ${el.getAttribute('data-src')} failed: Dev server is not reachable...`); } else { console.error(ex); } } } // if `tag` is still undefined, we can assume the dev server wasn't running, // so we compile the content locally if ( !tag ) { // `.innerHTML` returns the 'fixed' (browser interpreted) HTML, but it will encode // `&` to `&`, also within riot expressions, which of course messes up the compiler // so once we have the tag html, we need to get those encoded ampersands back to normal tag = el.innerHTML.replace(/&/g, '&').trim(); if ( !tag ) { console.warn(`compile(): Tried to get content from inline tag, but was empty, did you forget to paste the component code in?`, el); return; } hash = `riot:${await $hash(tag)}`; cached = db.getItem(hash); if ( !cached ) { // add attribute data-scoped="false" to the template tag to turn off CSS scoping //tag = riot.compileFromString(tag, { scopedCss: !['false', '0', 0].includes(el.getAttribute('data-scoped')) }).code; try { response = await (await fetch('https://riot.deno.dev', { method: 'POST', body: JSON.stringify({ markup: encodeURIComponent(tag), versionstamp: await $hash(tag), key: await $hash(window.location.origin), }), headers: { 'content-type': 'application/json', }, })).json(); if ( response.code ) { tag = response.code; } else { console.error('compile(): Faulty response', response); if ( response.payload ) { console.warn('compile(): Creating downloadable file from failed component markup!'); const file = new File([response.payload.markup], 'failed.tag.html', { type: 'text/plain', }); const fr = new FileReader(); fr.onload = function(e) { const link = `<span class="compile-error">Download uncompilable markup: <a href="${URL.createObjectURL(file)}" download="${file.name}">${file.name}</a></span>`; $('body').append(link); } fr.readAsText(file); } throw new Error('compile(): response.code could not be found!', { cause: 'fetch()', message: response }); } } catch(ex) { console.error(ex); tag = null; } if ( tag && usedb ) { db.setItem(hash, tag); } console.log(`compile(): No cached tag found, inline-tag compiled ${usedb ? `and cached with hash ${hash}` : ''}`); } else { tag = cached; console.log(`compile(): Found cached version of inline-tag!`); } } try { const { groups: { name = null } } = tag.match(/^riot\.register\(['"](?<name>[^\s'"]+)/) || { groups: {} }; console.log('compile(): tag name = ', name); //riot.inject(tag, name, `./${name}.html`); // yeahyeah, eval is evil, but we are the author of the code, so nothing to worry... } catch (ex) { console.error('compile(): Something went wrong with tag name extraction', ex); } try { eval(tag); } catch(ex) { console.error(`compile(): Something went wrong with tag evaluation for '${name}'`, ex); } return { name: name, hash: hash, cached: cached !== null, code: tag }; }) ); console.log('compile(): result', tags); console.timeEnd('compiling'); // end benchmark // optionally mount tag(s) via the compile() method if ( typeof mount === 'string' ) { console.time('mounting'); riot.mount(mount); console.timeEnd('mounting'); } else if ( typeof mount === 'boolean' ) { // auto-mount all top level components, but not the nested ones, they will be handled by the parent riot.mount('[is]:not([is] [is])'); } }; /** /* Helper method to automatically proxy requests to external URL's through the proxy endpoint /* when they can't be requested directly via JS due to CORS restrictions. /* /* @usage `$core.fetch('<url>', {<fetch.options>});` /* /* @param {string} url - The URL to fetch via proxy. /* @param {object} options - An optional fetch options object. /* /* @returns {any} - The proxied response data. /*/ $core.fetch = function(url = '', options = {}, cache) { return fetch(`${$core.config.request.endpoint}?get=proxy&url=${encodeURIComponent(url)}${cache ? '&cache=' + cache : ''}`, options); }; $core.fmt = { /** /* Adapted from https://github.com/lukeed/tinydate/blob/master/src/index.js /* /* @usage `$core.fmt.date('Current time: [{HH}:{mm}:{ss}]')(new Date())` /* /* @param str - Output string with placeholders /* @param custom - Custom formatter functions for placeholders (optional) /* /* @return - Returns a rendering function that will optionally accept a date value as its only argument. /*/ date: function(str, custom) { const RGX = /([^{]*?)\w(?=\})/g; const MAP = { YYYY: 'getFullYear', YY: 'getYear', MM: function (d) { return d.getMonth() + 1; }, DD: 'getDate', HH: 'getHours', mm: 'getMinutes', ss: 'getSeconds', fff: 'getMilliseconds' }; let parts=[], offset=0; str.replace(RGX, function(key, _, idx) { // save preceding string parts.push(str.substring(offset, idx - 1)); offset = idx += key.length + 1; // save function parts.push(custom && custom[key] || function(d) { return ('00' + (typeof MAP[key] === 'string' ? d[MAP[key]]() : MAP[key](d))).slice(-key.length); }); }); if ( offset !== str.length ) { parts.push(str.substring(offset)); } return function(arg) { var out='', i=0, d=arg||new Date(); for (; i<parts.length; i++) { out += (typeof parts[i]==='string') ? parts[i] : parts[i](d); } return out; }; }, }; /** /* Map (now) global $hash function to $core namespace for backwards compatibility. /*/ $core.hash = $hash; /** /* Dynamically imports and appends scripts to the DOM. /* Offers extended functionality such as manual deferral, error handling, and initialization tasks. /* /* @usage /* ``` /* $import('path/to/script.js').then(() => { /* console.log('All scripts loaded!'); /* }).catch(error => { /* console.error('Error loading script:', error); /* }); /* ``` /* /* @param {String|Array|HTMLElement|NodeList} input - Path(s) to the script(s) to be imported, or DOM nodes. /* @param {Object} [options] - Optional configuration object. /* @param {Function} [options.on] - Function to manually handle the script injection. /* @param {Function} [options.init] - Function to run initial tasks, e.g. for setup purposes. /* @param {HTMLElement} [options.target=document.body] - DOM element to which the script will be appended. /* @param {Object} [options.attributes={}] - Additional attributes to set on the script element. /* /* @returns {Promise} - Resolves when all scripts are loaded; rejects on any error. /*/ $core.import = function(scripts, { on, init, target = document.body, attributes = {} } = {}) { // Ensure scripts is always an array and not an already loaded script scripts = [].concat(scripts).filter(s => !(s?.src || document.querySelector(`script[src="${s}"]`))); typeof init === 'function' && init(scripts); return Promise.all(scripts.map(script => new Promise((resolve, reject) => { let el = script instanceof HTMLElement ? script : document.createElement('script'); if ( el.tagName === 'SCRIPT' ) { if ( el.querySelector(':is(script[data-src])') ) { el.src = el.getAttribute('data-src'); } else { Object.entries({ ...attributes, src: script }).forEach(([attr, val]) => el.setAttribute(attr, val)); } } else { return reject({ message: `Invalid input!`, data: script }); } el.addEventListener('load', resolve(el, script)); el.addEventListener('error', (e) => reject({ message: `Failed to load script: ${e.target.src}`, event: e })); // If an `on` function is provided, pass the script element to it for manual deferring typeof on === 'function' ? on(el, script) : target.appendChild(el); }))); }; /** /* Map global KV localStorage wrapper to $core for convenience. /*/ $core.kv = $kv; /** /* Proximity sensor helper method. Triggers a callback function when the mouse is within /* a certain distance of the given element. With the optional `check` flag set to `true` the method /* will check if the target element is reachable by the user, e.g. not hidden or obstructed by /* other elements. These checks are off by default, as they will be triggered with every tracked /* mousemove event which can potentially cause performance issues. If the target element is initially /* hidden, consider binding `$near()` AFTER the element has become visible! /* /* @usage /* ``` /* $near('.my-button', 50, (el, threshold) => { /* console.log(`Mouse is within ${threshold}px of ${el}`); /* }); /* ``` /* /* @param {Element|String} el - The target DOM element or a selector string to identify the element. /* @param {Number} threshold - The proximity threshold (in pixels) at which the callback will be invoked. /* @param {Function} fn - The callback function to be invoked when the mouse is within the defined distance of the target. /* @param {Boolean} [once=false] - If true, the callback will be triggered only once. /* @param {Boolean} [check=false] - If true, performs enhanced checks on the element that it's visible and not obstructed. /* /* @returns {void} /*/ $core.near = function(el, threshold, fn, { once = false, check = false } = {}) { el = typeof el === 'string' ? document.querySelector(el) : el; if ( !el || typeof fn !== 'function' || typeof threshold !== 'number' ) { return; } // make sure element exists let run = false; let within = false; // flag to track if the mouse is inside the proximity zone const proximity = function(target, x, y) { const { left, right, top, bottom } = target.getBoundingClientRect(); if ( check ) { const style = window.getComputedStyle(target); if ( Object.entries({ display: 'none', visibility: 'hidden', opacity: '0' }).some(([p, v]) => style[p] === v) ) { return false; } } return x > left - threshold && x < right + threshold && y > top - threshold && y < bottom + threshold; }; const handler = (e) => { if (run) { return; } run = true; window.requestAnimationFrame(() => { const near = proximity(el, e.clientX, e.clientY); if ( near && !within ) { within = true; fn(el, threshold); once && window.removeEventListener('mousemove', handler); } else if ( !near && within ) { within = false; } run = false; }); }; window.addEventListener('mousemove', handler); // return a function that removes the event listener when called return () => window.removeEventListener('mousemove', handler); }; /** /* This is the velocity parser implemented in core ported over to javascript, but it's only /* half useful, as it can't interpolate native strings, so this is only used to interpret /* non-interpolated velocity expressions. /*/ $core.parseVelocity = function(key = '', value = '', args = []) { // Cast all placeholder values to strings, otherwise the parser can't deal with the args args = args.map(String); const scopes_map = { component: $core.config.file, device: $core.config.user.device, page: $core.config.node.quilt, place: $core.config.node.type, lang: $core.config.user.lang, }; const scopes_matches = [...key.matchAll(/@([^@\r\n\t\f\v ]+)/gm)] || []; const scopes = scopes_matches.reduce((obj, match) => { const scope = match[1].split(':')[0]; const value = match[1].split(':')[1]; if ( scopes_map[scope] ) { obj[scope] = (scopes_map[scope] == value); } return obj; }, {}); const choice = (expr = '', args = []) => { const value = args[parseInt(expr.split(',').shift())]; const choices = expr.split('choice,').pop().trim(); return choices.match(`(${value})#(.*?)\\|`)?.[2] || choices.split('|').pop().replace(/\d+</, '') }; const rx = /\$\{(?<i>.+?)\}|\{(?<p>[0-5])\}|\{(?<e>[^\$]+?)\}/gm; let result = value; let cnt = 1; // Enter recursion to resolve nested velocity variables/placeholders/expressions // Have a safety max recursion depth to avoid unintentional memory leaks / endless loops while ( result.match(rx) ) { result = $core.parseVelocity(key, value.replace(rx, (match, i, p, e) => (i ? $core.str(i, null, ...args) : ( p ? args[parseInt(p)] || '' : choice(e, args) ))), args).value; if ( cnt >= 10 ) { console.warn('$core.parseVelocity(): Recursion depth of 10 exceeded!', result, result.match(rx), !!result.match(rx)); break; } cnt++; } return { key: key, value: result, scopes: scopes }; }; $core.serializeForm = function(form, json = false, filter, reducer) { // The shortest way of getting an object from FormData is // `Object.fromEntries((new FormData(form)).entries())` // but it will not handle select multiple inputs as the entries will have the same key // and therefore only the last selected option is returned as a value, to aggregate such array-like fields // properly, we have to manually loop over the fields after destructuring them into tuples (array of arrays) // this can also handle any type of field as an array of values by specifying the name with `[]` at the end // and furthermore it is also possible to directly aggregate form inputs into objects by specifying the name // attribute like so `name='object\{key}'`. NOTE: I'm not sure if the backslash escape is needed outside of // frameworks that interpolate expressions with `{`. Check the admin component for a practial example. const data = [...(new FormData(form)).entries()].reduce(reducer ? reducer : (obj, [name, value]) => { value = name.startsWith('(bool)') ? ({ '0': false, 'false': false, '1': true, 'true': true})[value.trim()] : value; const key = name.replace(/{(.+)}/gm, '').replace('(bool)', ''); value = obj[key] && key.endsWith('[]') ? [...obj[key], value] : ( key.endsWith('[]') ? [value] : ( /{(.+)}/gm.test(name) ? { ...obj[key], [[...name.matchAll(/{(?<k>.+)}/gm)][0]?.groups?.k]: value } : value ) ); if ( typeof filter === 'function' && filter(key, value) ) { obj[key] = value; } else { obj[key] = value; } return obj; }, {}); // Return a JSON string if requested return json ? JSON.stringify(data) : data; }; $core.obj = { /** /* Filters properties defined in props from the input object (non-destructive). /* /* @param {object} obj - The input object to operate on. /* @param {array} props - The properties to skip from the input object if present. /* /* @returns {object} - A new object without the properties defined in props. /*/ skip: function(obj = {}, props = []) { return Object.entries(obj).reduce((acc, [prop, value]) => { return props.includes(prop) ? acc : ((acc[prop] = value), acc); }, {}); }, /** /* Include only properties defined in props from the input object (non-destructive). /* Basically the opposite of _.skip(). /* /* @param {object} obj - The input object to operate on. /* @param {array} props - The properties to include from the input object if present. /* /* @returns {object} - A new object with only the properties defined in props. /*/ only: function(obj = {}, props = []) { return Object.entries(obj).reduce((acc, [prop, value]) => { return !props.includes(prop) ? acc : ((acc[prop] = value), acc); }, {}); }, }; $core.str = function(_key = null, _default = null, ...placeholders) { if ( !_key ) { return _default; } if ( _key && !_default ) { _default = _key; } // Try a direct lookup first, should work in most cases, except when there are @scopes // that weren't specified in the key let key = _key; let value = $core.properties[`${key}@component:${$core.config.file}`]; if ( !value ) { // Try to find a matching key by checking all properties //console.log(`$core.str(): could not find matching string for key ${key}: ${key}@component:${$core.config.file}, trying all properties:`, Object.entries($core.properties).find(([k, v]) => (k.includes(`@component:${$core.config.file}`) && k.split('@component').pop().includes(key)))); [ key, value ] = Object.entries($core.properties).find(([k, v]) => (k.includes(`@component:${$core.config.file}`) && k.split('@component').pop().includes(key))) || []; // console.log(`$core.str(): Could not find string by direct key (${_key}) lookup, find result:`, key, value); } /*else { console.log(`$core.str(): found matching string for key ${key}: ${key}@component:${$core.config.file}`); }*/ if ( value ) { return $core.parseVelocity(`${key}@component:${$core.config.file}`, value, placeholders).value; } else { return $core.parseVelocity(`${_key}@component:${$core.config.file}`, _default, placeholders).value; } }; /** /* Helper method facilitating watching for a DOM element for mutations and run a callback on them. /* /* @usage /* ``` /* $core.watch('<selector>', (mutation) => { /* console.log('I am here!', mutation); /* }, <optional:options>, <optional.immediate>); /* ``` /* /* @param {string} selector - The CSS selector to watch for mutations. /* @param {function} fn - The callback function to call when the element is mutated. /* @param {object} options - An optional options object for `.observe(<target>, <option>)` /* @param {boolean} immediate - If the callback function should be executed immediately once if the element is present. /*/ $core.watch = (selector, fn, options = {}, immediate = false) => { // Make sure nothing explodes in older browsers that do not support MutationObserver if ( window.MutationObserver ) { // Look for watched selector matching elements already present in the DOM, // and execute the callback on them immediately. if ( immediate && document.querySelector(selector) ) { [document.querySelector(selector)].forEach(fn); } options = { attributes: true, // can't have it set by default, otherwise not all attributes are monitored //attributeFilter: ['style', 'class'], attributeOldValue: false, characterData: false, characterDataOldValue: false, childList: false, subtree: false, ...(options || {}) } // One might have to do `const target = el.target as HTMLElement;` // within the callback to get an actual HTMLElement with its expected methods return (new MutationObserver(mutations => mutations.forEach(fn))) .observe(document.querySelector(selector), options); } }; /** /* Helper method facilitating waiting for a DOM element to appear and execute a callback. /* /* @usage `$core.when('<selector>', (el) => { console.log('I am here!', el); }, <optional:targetNode>);` /* /* @param {string} selector - The CSS selector to wait for. /* @param {function} fn - The callback function to call when the element appears. /* @param {HTMLElement} watch - The node to watch for mutations within. /*/ $core.when = (selector, fn, watch = document.body, existing = false) => { // Check if we even have a valid node to watch, otherwise MutationObserver will throw! watch = typeof watch === 'string' ? document.querySelector(watch) : watch; if ( !(watch instanceof Node) ) { console.warn(`$core.when(): 'watch' param wasn't a Node, aborting!`, watch); return; } // Make sure nothing explodes in older browsers that do not support MutationObserver if ( window.MutationObserver ) { // Look for watched selector matching elements already present in the DOM, // and execute the callback on them immediately. if ( document.querySelectorAll(selector).length ) { document.querySelectorAll(selector).forEach(fn); } return (new MutationObserver(mutations => [...mutations] .flatMap((mutation) => [...mutation.addedNodes]) .filter((node) => node.matches && node.matches(selector)) .forEach(fn))) .observe(watch, { childList: true, subtree: true }); } }; } // Initialize Tools // Make sure jQuery is available in some form which is not always the case in admin/studio and // several tools rely on it. if ( $ ) { // Tools only useful in admin/studio if ( $('body').is('.BizAppsPage') ) { Object.entries(tools.admin).map(function([name, fn]) { if ( typeof fn === 'function' ) { $core?.config?.user?.admin && console.info(`core.cmp.tools: Running tools.admin.${name}()`); fn(); } else { $core?.config?.user?.admin && console.warn(`core.cmp.tools: fn was not a function!`, fn); } }); } } // Bootstrap riot with global stuff if present // TODO: Maybe this should be its own component, separated from general purpose tools code! if ( window.riot ) { /** /* Stateless minimal router. /* Being stateless is a feature! The real "state" (i.e., the current route, history, etc.) /* is managed by the browser itself through the History API and the current URL. /* The router's job is to react to changes in that state and inform the rest of the app /* (via events or other mechanisms) about those changes. /*/ riot.$router = function(base = '/', options = {}) { const routes = []; base = '/' + base.replace(/^\/|\/$/g, ''); function parseRoute(route) { const { k, r } = route.split('/').reduce(({ k, r }, segment, i) => { switch (segment[0]) { case '*': return { k: k.concat('*'), r: r + '/(?<wild>.*)' }; case ':': const { key, con, ext, opt } = segment.match(/^:(?<key>[^\s(.?]+)(?:\((?<con>[\S]+?)\)(?![\)]))?(?<opt>\?)?\.?\(?(?<ext>[a-z0-9|]+)?\)?$/i)?.groups || {}; const p = `(?<${key}>${(con || '[^/]+?')}${ext ? `\\.(?<ext>${ext})` : ''})`; return key ? { k: k.concat(key), r: r + (opt ? `(?:/${p})?` : `/${p}`), } : { k, r }; default: console.log('parse index', i, Boolean(i)); return segment ? { k, r: r + `/(?<type${i > 1 ? i : ''}>${segment})` } : { k, r }; } }, { k: [], r: '' }); return { keys: k, pattern: new RegExp('^' + r + '/?$','i'), }; } function on(route, handler) { const { keys, pattern } = parseRoute(route); routes.push({ keys, pattern, handler }); } function navigate(path, replace) { const url = new URL(path, location.origin); const { keys, pattern } = parse(url.pathname); const match = routes.find(r => pattern.test(r.path)); if (match) { const match = match.pattern.exec(path); history[replace ? 'replaceState' : 'pushState'](null, null, path); $trigger('route', { url, match, keys, pattern }); } else if (options['404']) { options['404'](path); } } document.addEventListener('click', e => { const href = e.target.closest('a') && e.target.getAttribute('href'); const skip = [ !href, href.startsWith('#'), href.startsWith('javascript:'), !href.startsWith(base), e.defaultPrevented, e.button !== 0, e.metaKey, e.ctrlKey, e.shiftKey, e.altKey, ...(options.skipConditions || []) ]; if ( skip.some((con) => (typeof con === 'function' ? con(e) : Boolean(con))) ) { return; } e.preventDefault(); navigate(href); }); return { on, navigate }; }; riot.install((cmp) => { /** /* Override native riot `tag.$` (we don't touch `tag.$$`) with a much more powerful /* jQuery like API (it's not complete of course, but very mighty for 3.6KB code)! /* Of course we could also just map an already present jQuery instance to `tag.$`! /* This doesn't work, riot component internals are frozen! /*/ /*delete cmp.$; cmp['$'] = (selector) => { console.log('overwritten tag.$', selector); return cmp.$(selector); }; */ /** /* Install global event bus proxy methods. We do not want these methods scoped to every /* component individually but for them to be the same for all components so they can talk /* to each other on a global scope. /* /* - `on` will automatically bind `this` within the event listener function to the component. /*/ // This auto binding magic creates trouble, bind the component to the handler yourself if needed! //cmp['on'] = (e, fn, once) => $on(e, fn.bind(cmp), once); cmp['on'] = $on; cmp['off'] = $off; cmp['trigger'] = $trigger; /** /* Look for 'magic' listener methods defined within the component and automatically /* create a listener for them. These methods need to be named in a particular way: /* `$on<event.name.capitalized>` (note the $ prefix!), e.g. for an event type 'results' /* the component method needs to be defined as `$onResults: (e, results) => {}`, /* This approach is eliminating the need to define an explicit event listener somewhere /* within a regular lifecycle method with `tag.on('results', (e, results) => {})`... /* Using this automatic approach eliminates the need to remove any explicitely defined /* event listeners when a component is unmounted, as this is done automatically through /* the proxied lifecycle methods below. /* /* @note Be aware that if you need `this` bound to the component within your listeners /* (regardless if automatic or explicitely defined) you need to use the `function` way /* e.g. `$onResults: funciton(e, results) {}`! /* /* As this will create a listener for all events for every component, it's not enabled /* by default, only when the component has a property `events: true`. This way 'dumb' /* components do not get useless listeners registered which have to be processed! /*/ // We can either override riot.$trigger or 'properly' use the event bus, I'm not sure what // is actually better, both approaches seem to work just fine, when overriding obviously // there are no listeners stored and thus don't need to be removed when unmounting, resulting // in slightly less bootstrap code, but other than that it somehow feels wrong to me... /* if ( cmp.events ) { const $trigger = riot.$trigger; riot.$trigger = async (e, ...args) => { const $ = `$on${e.replace(/\b\w/, (c) => c.toUpperCase())}`; if ( cmp[$] ) { console.log(`automatically triggering ${$} listener for ${cmp.name}`); await cmp[$](e, ...args); } await $trigger(e, ...args); }; } */ let off = null; if ( cmp.events ) { // `on` will return a function to remove that listener, we store it for auto-cleanup off = cmp.on('*', async function(e, ...args) { const $ = `$on${e.replace(/\b\w/, (c) => c.toUpperCase())}`; if ( cmp[$] ) { await cmp[$](e, ...args); } }); } /** /* Proxy component lifecycle methods to give some debug info in debug mode /* allows us to define debugging stuff once here instead of having to repeat /* the same lengthy logging code in every single component increasing bundle /* size for nothing... we can reference component.name safely as it is part of the /* default component implementation along with .css and .template even though those /* properties are not enumerable (e.g. shown when console.log(component)) /* to proxy lifecycle methods simply set the .proxy property within the component to true /* if undefined or false nothing will be done here (e.g. the proxy is opt-in) /*/ ['onBeforeMount', 'onMounted', 'onBeforeUnmount'].forEach((method) => { // make a reference to the original lifecycle method and bind the component to it const org = cmp[method].bind(cmp); // add proxy method calling the original after it has done global stuff cmp[method] = (props, state) => { org(cmp.props, cmp.state); // automatically removes auto-event listeners when the component is unmounted if ( method === 'onBeforeUnmount' && off ) { off(); } cmp.debug && console.log(`${cmp.name}.${method}()`, cmp.props, cmp.state, method === 'onBeforeMount' ? cmp : null); }; }); //console.log('riot.install() done!', cmp); }); } if ( $core?.config?.env?.includes('stage') && $core?.config?.user?.roles?.includes('GlowingBlue') && document.querySelectorAll('[type*="riot"]').length && window.riot ) { // Live compiling riot components in dev mode console.log('core.cmp.tools: Compiling and registering riot components'); (async () => { await $core.compile(); // Useful for profiling component performance if ( window.performance ) { window.start = performance.now(); } if ( $trigger ) { await $trigger('compiled'); } })(); } else { // directly trigger on prod as components are pre-compiled! (async () => { if ( $trigger ) { // Wait for next event loop, if not it can cause strange non-mounting issues // due to no work having to be done (e.g. compile event basically happening immediately) // and the order in which @liaAddScript adds the JS from various places within the codebase await $wait(0); await $trigger('compiled'); } })(); } //document.addEventListener('DOMContentLoaded', function() {}); })(LITHIUM?.jQuery || jQuery); // Pull in global jQuery reference // </script> // <script> just for inline syntax-highlighting... ;(function($){ // This is very much work in progress, but a synk object should generally look something like: /* { type: <event-type-synk-understands>, event: { data: {}, source: { node: {}, page: {}, user: {}, }, verified: <bool>, } } */ // Store objectified request payloads for later use let payload = {}; // Define which particular routes we want to forward to synk. Those can be from forms, // links etc. // The key is the form action or link (partial) to check for when a request comes in // the value is an object with an `only`array for filtering the payload and a `type` // function to dynamically determine the type of event that is witnessed. // TODO: Look into those `t:cp=solutions/contributions/acceptedsolutionsactions` // identifiers that most actions seem to have, maybe they are an easier to detect way // of what to track on different pages, then the changing URL's which force us to use // URL partials... => unfortunately not, the identifier stays the same, the action is // still burried in the URL, like `markmessageasacceptedsolutionsecondarybutton` (WTF?): // t5/forums/v5/forumtopicpage.markmessageasacceptedsolutionsecondarybutton/message-uid/1844 const synk = { // AJAX: inline reply form submit 'inlinemessagereplyeditor.form.form.form.form': { only: [ 'attachment-key', 'liaFormContentKey', 'mediaSnippetUrl', 'multipleUpload', 'parentMessageRef', 't:ac', 'tags_', 'tinyMceEditor', ], type: (xhr) => 'post-reply', }, // AJAX: Kudos button 'kudosbuttonv2.kudoentity': { only: ['triggerEvent', 'parameterOverrides'], type: (xhr) => (xhr.responseURL.includes('revoke-kudos/true') ? 'dislike' : 'like'), }, // not AJAX: Report content to moderator 'notifymoderatorform.form.form.form': { type: (xhr) => 'mod-check', }, // subscribe: /t5/forums/v5/forumtopicpage.__addmessageuseremailsubscription__/message-uid/1841?t:cp=subscriptions/contributions/messageactions // post-mute: /t5/forums/v5/forumtopicpage.__addmessageusermute__/message-uid/1924?t:cp=subscriptions/contributions/messageactions // bookmark: /t5/forums/v5/forumtopicpage.__addmessageuserbookmark__/message-uid/1924?t:cp=subscriptions/contributions/messageactions // post-edit: /t5/forums/v5/forumtopicpage.__editmessageinline:editmessage__/message-uid/1924?t:cp=boards/contributions/messageactions // post-delete: /t5/forums/v5/forumtopicpage.__deletemessage:deletemessage__/message-uid/1924?t:cp=boards/contributions/messageactions // post-move: /t5/forums/v5/forumtopicpage.__movemessage:movemessage__/message-uid/1924?t:cp=boards/contributions/messageactions // post-solved: /t5/forums/v5/forumtopicpage.__markmessageasacceptedsolutionsecondarybutton__/message-uid/1844?t:cp=solutions/contributions/acceptedsolutionsactions // post-unsolved: /t5/forums/v5/forumtopicpage.__unmarkmessageasacceptedsolution__/message-uid/1844?t:cp=solutions/contributions/acceptedsolutionsactions }; $on('xhr', (e, state, xhr, ...args) => { //console.log('$onXHR', e, xhr, args); if ( state === 'open' ) { $core.config.devmode === 'xhr' && console.log('XHRmiddlaware: Request created', args, xhr); } else if ( state === 'send' ) { $core.config.devmode === 'xhr' && console.log('XHRmiddlaware: Request payload is about to be sent', Object.fromEntries([...(new URLSearchParams(args[0]))])); payload = args[0] ? Object.fromEntries([...(new URLSearchParams(args[0]))]) : {}; } else if ( state === 'response' ) { $core.config.devmode === 'xhr' && console.log('XHRmiddlaware: Can modify response text!', args, xhr); // important to return modified response from here return args[0]; } else if ( state === 'done' ) { if ( !Object.entries(synk).some(([urlpartial, obj]) => xhr.responseURL.includes(urlpartial)) ) { $core.config.devmode === 'xhr' && console.log('XHRmiddlaware: skipping response processing for', xhr.responseURL); return; } $core.config.devmode === 'xhr' && console.log('XHRmiddlaware: Response received', xhr.responseText); try { // Do some basic sanity checks before attempting to parse JSON... // Usually Khoros XHR responses are a huge object that is then processed and // injected into the current page via some very convoluted logic, most of the data // is irrelevant for us and is contained within a response's `components` property let data = xhr.responseText.trim().startsWith('{') ? $core.obj.skip(JSON.parse(xhr.responseText)?.response, ['components']) : xhr.responseText; if ( typeof data !== 'string' ) { const { only, type } = (Object.entries(synk).find(([urlpartial, obj]) => xhr.responseURL.includes(urlpartial))[1] || {}); data = { type: type(xhr) || 'undefined', event: { url: xhr.responseURL, timestamp: (new Date()).toISOString(), // the result of the event data: data, // the initiator of the event source: { payload: $core.obj.only(payload, (only || [])), user: $core.obj.skip(LITHIUM.CommunityJsonObject.User, ['settings', 'policies', 'emailRef']), node: LITHIUM.CommunityJsonObject.CoreNode, page: { ...($core.obj.only(LITHIUM.CommunityJsonObject.Page, ['object'])?.object || {}), }, } } }; $trigger('synk', data); //console.log('Forwarded data:', data); } else { $core.config.devmode === 'xhr' && console.warn('XHRmiddlaware: Respsone was a string, skipping sync!'); } } catch(ex) { console.error(ex); } } }); // The above handles (old school) AJAX requests, but we also need to deal with actions/events // that occur the even old-schooler way through regular link clicks that reload the page. // To do that we attach a global link listener. I believe to catch Khoros related events we // can safely filter links by `data-lia-action-token` as all the relevant action links seem // to have such an attribute! // TODO: Make sure we do not somehow also track links that are handled by AJAX requests and // thus would be double-synk'ed... document.querySelectorAll('a[data-lia-action-token]').forEach((el) => { el.addEventListener('click', (e) => { if ( e.target.getAttribute('href') ) { const url = new URL(e.target.getAttribute('href')); const params = Object.fromEntries([...url.searchParams]); $core.config.devmode === 'xhr' && console.log('XHRmiddlaware: Action link params', url, params, e.target.getAttribute('href')); } else { $core.config.devmode === 'xhr' && console.warn('XHRmiddlaware: Action link did not have a href attribute?', e.target, e); } if ( $core.config.user.roles.includes('GlowingBlue') ) { $core.config.devmode === 'xhr' && console.log('XHRmiddlaware: Preventing action from glowingblue user'); //e.preventDefault(); } // We would then build a synk object and trigger a 'synk' event through the global // event bus... // TODO: We also need to think about what happens if an action fails, maybe storing // potential sync objects in localStorage with an attribute of `verified: false` // would be a good idea, for AJAX requests we can more or less reliably track // if an action was successful, as the returned objects contain a property `state` // (NOT `status`, that one is always 'success'!) that will indicate any errors... }); }); // There are also forms that are not handled via AJAX! It seems this listener does not // conflict with the AJAX ones, as those are implemented by Khoros earlier on, so this // listener is never called for AJAX handled forms, which is good because then we don't // have to deal with duplicate synk events... document.querySelectorAll('input[name="lia-action-token"]').forEach((el) => { $(el).parents('form')[0].addEventListener('submit', (e) => { if ( $core.config.user.roles.includes('GlowingBlue') ) { $core.config.devmode === 'xhr' && console.log('XHRmiddlaware: Preventing form submission from glowingblue user'); //e.preventDefault(); $core.config.devmode === 'xhr' && console.log('XHRmiddlaware: Serialized form', $core.serializeForm(e.target)); } else { //return true; } }); }); $on('synk', (e, data) => { console.log('$onSynk', e, data); // TODO: make sure there is no issue with aborted requests due to page reloading // if it is, we might have to store the synk objects in local storage before sending // out the request and then when we get a successful response, delete them and on page // load check if we have any leftover items to synk and process those again... // TODO: Push requests through proxy (not sure if it already handles POST requests!) // So we do not leak any user IP information to a third party which Deno deploy is... fetch('https://synk.deno.dev', {method:'POST', body: JSON.stringify(data)}); }); // Simply trigger a custom xhr event and handle whatever logic in the event listener! XMLHttpRequest.$use((xhr, state, ...args) => { $trigger('xhr', state, xhr, ...args); }); })(LITHIUM.jQuery); // Pull in global jQuery reference // </script> // <script> just for inline syntax-highlighting... ;(function($){ /** /* Deals with any redirects that need to be handled in JavaScript for some reason, like 404 Pages /* as we don't get the request in page.init and therefore can't do a redirect form there. /* /* You only need to specify the project specific redirects by adding them to the global `$core` /* object. The actual redirect logic will be dealt with from `core.cmp.tools` via `tools.redirect()`, /* just make sure you don't change the property name as the tool method looks for `$core.redirects`. /* /* @usage: Just define key/value paris where key = url.pathname and value the url to redirect to /*/ $core.redirects = { //'/some/path/to/redirect': 'https://www.community.tld/some/url/to/target/redirect/to', }; /** /* @issue KBCOM-2655 /* /* Immediately makes the inline 'Reply' button disabled on click instead of waiting for TinyMCE /* to be initialized as OOB does. /*/ $('.lia-component-messages-widget-reply-inline-button .lia-button.lia-action-reply').each(function() { $(this).on('click', function(e) { $(this).attr('disabled', true) }); }); })(LITHIUM.jQuery); // Pull in global jQuery reference // </script> // <script> ;(function() { if ( $core ) { $core.config.file = 'cmp.global.search-external'; $core.properties = { ...($core.properties || {}), ...{"general.in@component:cmp.global.search-external" : "in","general.of@component:cmp.global.search-external" : "of","general.from-community@component:cmp.global.search-external" : "From the Community","title@component:cmp.global.search-external" : "Additional Resources","filter.title.resources@component:cmp.global.search-external" : "Included Resources","filter.title.languages@component:cmp.global.search-external" : "Languages","filter.knowledge@component:cmp.global.search-external" : "Knowledge Base","filter.academy@component:cmp.global.search-external" : "Academy","filter.cms@component:cmp.global.search-external" : "CMS Documentation","filter.api@component:cmp.global.search-external" : "API Documentation","filter.customer@component:cmp.global.search-external" : "Customer Blog","filter.language.en@component:cmp.global.search-external" : "en","filter.language.de@component:cmp.global.search-external" : "de","filter.language.es@component:cmp.global.search-external" : "es","filter.language.fr@component:cmp.global.search-external" : "fr","filter.language.pt@component:cmp.global.search-external" : "pt","filter.language.ja@component:cmp.global.search-external" : "ja","result@component:cmp.global.search-external" : "result","results@component:cmp.global.search-external" : "results","results.in@component:cmp.global.search-external" : "{0} {0,choice,0#${results}|1#${result}|1<${results}} ${general.in} {1}","results.none.title@component:cmp.global.search-external" : "No results for \"{0}\"","results.none.text@component:cmp.global.search-external" : "Try a different search term or use the filter on the left to search other resources.","paging.prev@component:cmp.global.search-external" : "Prev","paging.next@component:cmp.global.search-external" : "Next"}, }; } })(); // </script> // <script> just for inline syntax-highlighting... ;(function($){ const params = (new URL(window.location.href)).searchParams; // Allows testing production behavior on stage by switching env temporarily via URL param if ( $core.config.env.includes('stage') && params.get('test') ) { $core.config.env = 'prod'; console.warn('Production test mode enabled', $core.config); } // The most primitive in-memory cache you can imagine // it will hold HubSpot search API results as long as the page is not reloaded, this // speeds things up drastically when using typeahead (e.g. reacting to every key stroke) $core.cache = new Map(); const title = $core.str('title', 'Additional Resources'); /** /* Searchbar autosuggest integration. /* We can't really use riot here as we are hacking into the existing Khoros auto-suggest dropdown /* Furthermore the SearchForm is added twice to every page due to how the mobile header was done. /* It's entirely separate from the desktop header and therefore needs to be targetet properly when /* it's visible from 1024px down. /*/ const search_form = window.innerWidth <= 1024 ? '.mobile-header form.SearchForm' : '.community-header-nav .SearchForm'; $core.when(`${search_form} [name="messageSearchField"] + .lia-autocomplete-container .lia-autocomplete-content`, function(el) { $(`${search_form} [name="messageSearchField"]`).each(function() { var $input = $(this); var $results = $input.find('+ .lia-autocomplete-container .lia-autocomplete-content'); /** /* The autosuggest-dropdown closes on a click anywhere besides it's own results, /* not too crazy of an issue, but it annoys me and I can't fix it, don't know what /* triggers which event, tried to find out but without success... /*/ /*if ( window.innerWidth <= 1024 ) { $results .prepend(`<i class="collapse-results lia-autocomplete-no-event-item lia-fa fa-chevron-up p:x15 p:y11 pos:a pos:r0 pos:t0"></i>`) .find('.collapse-results') .on('click', function(e) { e.stopPropagation(); console.log($(this).siblings('ul:first'), $(this)); $(this).toggleClass('fa-chevron-up fa-chevron-down'); }); }*/ if ( !$results.find('ul.custom-external-results').length ) { $results.append(`<ul class="custom-external-results d:f(row/0/1/100%) h:max300 d:scroll(y) t:/12//400 d:b:before p:y8:before p:l15:before pos:r t:ucase:before" aria-label="${title}" data-before="${title}"></ul>`); } var $container = $results.find('ul.custom-external-results'); $input.on('input', async function(e) { // TODO: find out what is the URL param to limit `limit=` does not work... // => well, it seems the autocomplete endpoint of the HubSpot search API does not // support any kind of configuration at all. If it's needed, we might have to switch // to the slower 'full' API endpoint var url = `https://wtcfns.hubspot.com/wt-api/search/autocomplete?queryString=${$input.val()}&language=${$core.config.user.lang}&limit=5`; //console.log($input.val()); if ( !$core.cache.has(url) ) { $core.cache.set(url, (await (await $core.fetch(url)).json())); } //console.log($core.cache.get(url)); // HubSpot search API will return `{'message': 'Missing search key'}` when the `queryString` is empty // in that case, markup will be `undefined`, which we have to catch, otherwise it's going to be // an empty array if there are really no results for a search term. var markup = $core.cache.get(url).data?.searchResults?.results?.reduce((r, v) => { // As limit doesn't work on the autocomplete endpoint we have to limit the auto-suggest results like this //if ( r.length < 3) { r.push(` <li class="lia-autocomplete-node-item lia-autocomplete-custom-item"> <a class="lia-link-navigation board-icon" tabindex="-1" href="#"> <span class="custom-img-icon-help lia-fa-icon lia-fa-question lia-fa bg:--color-lorax t:--color-olaf" title="${v.resource}" aria-label="${v.resource}" role="img"></span> </a> <a class="lia-link-navigation lia-js-autocomplete-list-item-link lia-autocomplete-message-list-item-link" tabindex="-1" href="${v.url}" target="_blank"> ${v.title.replace('hs-search-highlight hs-highlight-title', 'lia-search-match-lithium')} </a> <div class="lia-autocomplete-suggestion-additional-details lia-component-nodes-widget-auto-complete-node-list-item"> <span class="lia-autocomplete-suggestion-board-title t:caps">${v.resource}</span> </div> </li> `); //} return r; }, []); if ( markup === []._ ) { return; } if ( markup.length ) { $container.html(markup.join('\n')); } else { $container.html(`<div class="pos:center t:center t:/14">${$core.str('results.none.title', null, $input.val())}</div>`); } }); }); }, document.querySelector(`${search_form}`)); /** /* Global SearchPage integration of external search. /* Adds a new tab to the search sections area and additionally a filter-like fake dropdown /* That additional filter was removed again via KBCOM-2830! /* that triggers a click on the new tab, it's just for more visual exposure as we worry the /* new tab might be too unassuming and might be overlooked. /*/ if ( $core.config.node.quilt.includes('SearchPage') ) { let results = null; const getResults = async function(query, resources = ['knowledge', 'academy', /*'customer', 'api', 'cms'*/], language = ($core?.config?.user?.lang || 'en'), page = 1, limit = 10, offset = 0, padding = 2) { offset = (page-1) * limit; // HubSpot search API resources are targeted with `contentKey: api, cms, knowledge, academy, customer` const url = `https://wtcfns.hubspot.com/wt-api/search?queryString=${query}&limit=${limit}&offset=${offset}&page=${page}&language=${language}&contentKey=${resources.join(',')}`; //console.log(url); if ( !$core.cache.has(url) ) { $core.cache.set(url, (await (await $core.fetch(url, null, 'appcache')).json())); } // once received, the HubSpot search API results are agumented with custom stuff // that helps rendering things like pagination etc. let results = Object.entries(($core.cache.get(url)?.data?.searchResults || {})).reduce((r, [k, v]) => ({ ...r, [k]: v }), { active: resources, lang: language, query: query, url: url }); // calculate the total amount of pages first and set it to minimum 1 const pages = Array.from({length: Math.max(Math.ceil(results.total/limit), 1)}, (el, i) => i+1); // calculate collection object const collection = { limit: limit, offset: offset, total: results.total, paging: { page: page, pages: pages.length, // TODO: there are still some issues with this, it does work for page // 1, but for the last page, only 3 (instead of 5) pages are returned display: pages.length ? (() => { const num = (padding * 2) + 1; const i = pages.indexOf(page); const from = Math.max(i - Math.floor(num / 2), 0); const to = Math.min(from + num - 1, pages.length - 1); return pages.slice(from, to + 1); })() : [], // number of page-links left and right of current page padding: padding, // rendering helpers is_first: page === 1, is_last: page == pages.length } }; results = { ...results, collection: collection }; //console.log('response', res); //console.log('results', results); // Trigger a custom jQuery event globally that we can hook into from any other code $(document).trigger('results', results); // We can trigger the custom global riot event-bus from outside of components as it // is attached to the global riot object! if ( $trigger ) { $trigger('results', results); } return results; }; const injectSearchExternal = function() { // Inject our custom tab const $tab = $('.lia-search-tab-bar .lia-component-search-tabs .lia-tabs-standard').append(` <li role="presentation" class="search-external-tab lia-tabs lia-tabs-inactive is--custom"> <span><a class="search-external-link lia-link-navigation tab-link" role="tab" aria-selected="false" tabindex="0" href="${window.location.href}">${title}</a></span> </li> `).find('.search-external-tab'); // Removed via KBCOM-2830 /* const $filter = $(` <div class="lia-form-fieldset-wrapper lia-component-search-widget-external is--custom"> <a href="${window.location.href}" class="lia-common-dropdown-toggle" role="button">${title}</a> </div> `).insertBefore('.lia-component-quilt-search-page-thread-filters .lia-component-search-widget-location-filter'); */ // Handle clicks on our new tab: // The idea is to basically wipe the existing content from the page and mount the // custom search component instead. Any click on the regular tabs will behave like always and // trigger a page reload which will show the original content again $tab.find('.search-external-link').on('click', function(e) { e.preventDefault(); // Handle active state of tabs $(this).parents('.search-external-tab').addClass('lia-tabs-active').siblings('.lia-tabs-active').removeClass('lia-tabs-active'); // Then we clean out some of the content of the current page and make it look like a tab switch $('.lia-search-tab-bar .lia-component-search-widget-advanced-search-toggle, .lia-search-tab-bar .lia-component-search-actions, .lia-search-results .search-result-sorting').remove(); $('.lia-search-results .search-result-count').remove(); // Once cleaned up we inject the base tag and mount our custom component $('.lia-quilt-column-main-content .lia-quilt-row-main').empty().append('<div is="search-external" class="p:x15" data-cmp="cmp.global.search-external"></div>'); riot.mount('[is]:not([is] [is])', { title: title, results: results, params: params, getResults: getResults }); }); // The fake injected filter simply triggers the tab, it's meant to provide greater exposure (visually) // $filter.on('click', function(e) { // e.preventDefault(); // $tab.find('.search-external-link').trigger('click'); // }); }; // Pre-fetch results, why wait as we already know the query here, this also allows to inject a variety of // dynamic information into the native search results content and agument it with external results data // this also deals with the fact that the search page is an Angular component that reloads dynamically // when doing certain things... (not tab switching though) $core.when('.lia-message-search-container', async (el) => { if ( !document.querySelector('.lia-tabs.external-tab') ) { //console.log('Injecting external search!'); injectSearchExternal(); results = await getResults(params.get('q')); } }); // Attach custom event listener here to deal with non-component DOM updates as I don't want to handle those // within the custom component but also be updated if something changes there... $(document).on('results', function(e, results) { //console.log('onResults', e, results); if ( $('.lia-search-results .search-result-count').text().trim().length ) { $('.lia-search-results .search-result-count').attr('data-after', $core.str('general.from-community')); } // Update the tab tag $('.search-external-link').attr('data-after', (results?.total || 0)); }); } // Re-set production test mode if ( $core.config.env.match('stage') && params.get('test') ) { $core.config.env = 'stage'; } })(LITHIUM.jQuery); // Pull in global jQuery reference // </script> (function($) { //START END-USER CONFIGURATION //------------------------------ //selectors for hover card triggers var allHoverCardTriggers = '.author-name-link,.friend-list .friend a,.username a,.avatar,.user-avatar,.author-img, .authors a, .messageauthorusername a, a.lia-user-name-link, .js-latest-post-by-from a, .user-online-list li a, a.UserAvatar, .customUsersOnline a, #authors a,.dashboard-followers a.user-name, .dashboard-following a.user-name,.author-login-wrapper a, .hb-leaderboard a, .author-img-floated'; // Forward calling page's URL params to endpoint URL as well, helps with testing! var params = (new URL(location.href)).searchParams; var userApiUrl = '/plugins/custom/hubspot/hubspot/hovercardendpoint?' + ((params.set('user_id', '') == []._) && params.toString()); if($('.hover-card-container').length<1){ $('body').append('<div class="hover-card-container"></div>'); } var cardWrapper = $('.hover-card-container'); var error = false; var thisUserID = ''; var thisUserLogin = ''; var userLink =''; var cardTimer; var leaveTimer; function mouseenter(Elem) { var thisEl = Elem; cardTimer = setTimeout(function(){ var docWidth = $(document).width(); var rightSide = false; var userLink = thisEl.attr('href'); if($('.ViewProfilePage').length && $('img.lia-user-avatar-profile',thisEl).length){thisUserID = '';} else if(thisEl.attr('href')=='#' || thisEl.attr('href')=='' || !userLink.match('viewprofilepage')){ return false;} else{ var thisLen = (userLink).split('/'); thisUserID = (thisLen)[thisLen.length-1]; } var thisCard = $('.profileCard[data-user='+thisUserID+']',cardWrapper); var cardId = 'userProfileCard-'+ thisUserID; var addAttr = thisEl.attr('aria-describedby',cardId); var thisElTopOffset = Math.round(thisEl.offset().top+(thisEl.height()/2)+30); var thisElbottomoffset = "auto"; var className = ""; var winHeight = $(window).height(); var elOffset = thisEl.offset(); var scrollTop = $(window).scrollTop(); var elementOffset = thisEl.offset().top; var distanceTop = (elementOffset - scrollTop); var distanceBottom = (winHeight + scrollTop) - (elOffset.top + thisEl.outerHeight(true)); var distanceLeft = Math.round(thisEl.offset().left); var bodyHight = $('body').height(); var topParam = ''; var bottomparam = ''; var position = ''; var className = 'topArrow'; cardId if(distanceBottom < 300 ){ if(distanceLeft < 59){ thisCard.removeClass('bottomArrow'); var className = 'leftArrow'; var distanceLeft = (distanceLeft)+(39); var thisElTopOffset = (thisElTopOffset)-(150); }else{ var thisElTopOffset = (thisElTopOffset)-(301); var className = 'bottomArrow'; thisCard.removeClass('topArrow'); thisCard.removeClass('leftArrow'); var distanceLeft = (distanceLeft)-(45); } } else{ if(distanceLeft < 59){ thisCard.removeClass('topArrow'); var className = 'leftArrow'; var distanceLeft = (distanceLeft)+(39); var thisElTopOffset = (thisElTopOffset)-(150); }else{ thisCard.removeClass('leftArrow'); thisCard.removeClass('bottomArrow').addClass('topArrow'); var distanceLeft = (distanceLeft)-(45); } } if(thisCard.length && $('.profileCard[data-user='+thisUserID+'] .preloader',cardWrapper).length<1){ $('.profileCard',cardWrapper).hide(); thisCard.addClass(className); rightSide?thisCard.addClass('rightArrow'):thisCard.removeClass('rightArrow'); thisCard.delay(0).css({'top':(thisElTopOffset),'left':distanceLeft,'bottom':thisElbottomoffset}).fadeIn(); } else { var ajaxReturn = ''; //just in case thisCard.remove(); //hover card wrapper markup var rightArrowClass = rightSide?'rightArrow':''; if(thisElTopOffset != "auto"){ topParam = 'px'; } if(thisElbottomoffset != "auto"){ bottomparam = 'px'; } var profileCardHtml = '<div id="'+cardId+'" role="tooltip" class="AllCard profileCard '+rightArrowClass+' '+className+'"style="display:block;top:'+thisElTopOffset+topParam+';left:'+distanceLeft+'px;bottom:'+thisElbottomoffset+bottomparam+';" data-user="'+thisUserID+'"></div>'; $.when( //get the background $.ajax({ type: 'GET', url: userApiUrl+thisUserID, dataType: 'html', success: function(data) { $('.profileCard',cardWrapper).hide(); ajaxReturn = data; } }) ) .done(function(){ cardWrapper.append(profileCardHtml); $('.profileCard[data-user='+thisUserID+']',cardWrapper).eq(0).empty().html(ajaxReturn); if($('.profileCard[data-user='+thisUserID+'] .preloader',cardWrapper).length){ $('.profileCard[data-user='+thisUserID+'] .preloader',cardWrapper).parents('div.profileCard').remove(); } }) .fail(function(){ //uh oh - bail out! $('.profileCard',cardWrapper).hide(); }); } }, 360); } function mouseleave(e) { clearTimeout(cardTimer); // glowingblue: When the user leaves the hovercard trigger, wait because the leaving could be // to interact with the hovercard, if we don't wait it will just disappear...because // we left the trigger, right...so we'll have another handler that check if the mouse is // over the hovercard and if so clears this timer, so the card doesn't close here leaveTimer = setTimeout(function() { if ($('.profileCard[data-user="'+thisUserID+'"]',cardWrapper).length) { $('.profileCard[data-user="'+thisUserID+'"]',cardWrapper).fadeOut('fast'); } else { $(".profileCard").fadeOut('fast'); } }, 2400); } $(document).on("mouseenter focusin", allHoverCardTriggers, function(event) { if(!($(this).parents().hasClass('custom-header'))&& !($(this).parents().hasClass('green-wrap'))){ (leaveTimer !== []._) && clearTimeout(leaveTimer); mouseenter($(this)); event.stopPropagation(); } }); $(document).on("mouseleave focusout", allHoverCardTriggers, function(event) { (leaveTimer !== []._) && clearTimeout(leaveTimer); mouseleave(event); event.stopPropagation(); }); // glowingblue: Add handlers for when the users interacts with the hovercard, no closing! $('.hover-card-container').on('mouseenter', function(e) { (leaveTimer !== []._) && clearTimeout(leaveTimer); }); $('.hover-card-container').on('mouseleave', function(e) { (leaveTimer !== []._) && clearTimeout(leaveTimer); if ( $(e.target).is('.profileCard[style*="block"]') ) { leaveTimer = setTimeout(function() { $(e.target).fadeOut('fast'); }, 2400); } }); // glowingblue: add one global root level click handler to also close any visible hovercards // if the user taps/clicks outside the hovercard $(document).on('mousedown', function(e) { if ( !$(e.target).parents('.hover-card-container').length ) { (leaveTimer != []._) && clearTimeout(leaveTimer); $('.hover-card-container .profileCard[style*="block"]').each(function() { $(this).fadeOut('fast'); }); } }); })(LITHIUM.jQuery); (function($) { <!-- Expire all cookies --> document.cookie = "advocacyToken=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; document.cookie = "Crowdvocate_jwt_token=; domain=.hubspot.com; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; document.cookie = "Crowdvocate_user_ck=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; })(LITHIUM.jQuery); (function($) { document.addEventListener('gdpr.allow', function() { if (document.querySelector('.lia-cookie-banner-alert-accept a')) { document.querySelector('.lia-cookie-banner-alert-accept a').click(); } }); })(LITHIUM.jQuery); LITHIUM.Link({"linkSelector":"a.lia-link-ticket-post-action"}); ;(function($){ var langMap = { 'en':'hubspot_community_en', 'es':'hubspot_community_es', 'fr':'hubspot_community_fr', 'ja':'hubspot_community_jp', 'pt-br':'hubspot_community_pt', 'de':'hubspot_community_de' } var nodeType = "community"; var langScope = langMap['en']; var isSearchPage = jQuery('body').hasClass('SearchPage'); var isIdeasLandingPage = jQuery('body').hasClass('ideaslandingpage'); if (nodeType === "community" && !isSearchPage && !isIdeasLandingPage) { var inputFormFilter = '<input name="filter" value="location" type="hidden">'; var inputFormLocation = '<input name="location" value="category:' + langScope + '" type="hidden">'; $('form.SearchForm').append(inputFormFilter).append(inputFormLocation); } else if (nodeType === "community" && isIdeasLandingPage) { var searchUrl = "/t5/forums/searchpage/tab/message?filter=location&location=idea-board:HubSpot_Ideas&collapse_discussion=true"; var query = jQuery('.SearchForm .lia-search-input-message').val(); jQuery(document).on('submit', 'form.SearchForm', function(e) { e.preventDefault(); var newQ = "&q=" + document.querySelector('.SearchForm .lia-search-input-wrapper input.search-input').value; window.location = window.location.origin + searchUrl + newQ; }) } })(LITHIUM.jQuery) LITHIUM.InformationBox({"updateFeedbackEvent":"LITHIUM:updateAjaxFeedback","componentSelector":"#informationbox_23fd878fe08c79","feedbackSelector":".InfoMessage"}); LITHIUM.InformationBox({"updateFeedbackEvent":"LITHIUM:updateAjaxFeedback","componentSelector":"#informationbox_23fd878fe08c79_0","feedbackSelector":".InfoMessage"}); LITHIUM.InformationBox({"updateFeedbackEvent":"LITHIUM:updateAjaxFeedback","componentSelector":"#informationbox_23fd878fe08c79_1","feedbackSelector":".InfoMessage"}); LITHIUM.InformationBox({"updateFeedbackEvent":"LITHIUM:updateAjaxFeedback","componentSelector":"#informationbox_23fd878fe08c79_2","feedbackSelector":".InfoMessage"}); LITHIUM.AjaxFeedback(".lia-inline-ajax-feedback", "LITHIUM:hideAjaxFeedback", ".lia-inline-ajax-feedback-persist"); LITHIUM.Placeholder(); LITHIUM.AutoComplete({"options":{"triggerTextLength":0,"updateInputOnSelect":true,"loadingText":"Searching...","emptyText":"No Matches","successText":"Results:","defaultText":"Enter a search word","disabled":false,"footerContent":[{"scripts":"\n\n;(function($){LITHIUM.Link=function(params){var $doc=$(document);function handler(event){var $link=$(this);var token=$link.data('lia-action-token');if($link.data('lia-ajax')!==true&&token!==undefined){if(event.isPropagationStopped()===false&&event.isImmediatePropagationStopped()===false&&event.isDefaultPrevented()===false){event.stop();var $form=$('<form>',{method:'POST',action:$link.attr('href'),enctype:'multipart/form-data'});var $ticket=$('<input>',{type:'hidden',name:'lia-action-token',value:token});$form.append($ticket);$(document.body).append($form);$form.submit();$doc.trigger('click');}}}\nif($doc.data('lia-link-action-handler')===undefined){$doc.data('lia-link-action-handler',true);$doc.on('click.link-action',params.linkSelector,handler);$.fn.on=$.wrap($.fn.on,function(proceed){var ret=proceed.apply(this,$.makeArray(arguments).slice(1));if(this.is(document)){$doc.off('click.link-action',params.linkSelector,handler);proceed.call(this,'click.link-action',params.linkSelector,handler);}\nreturn ret;});}}})(LITHIUM.jQuery);\r\n\nLITHIUM.Link({\n \"linkSelector\" : \"a.lia-link-ticket-post-action\"\n});LITHIUM.AjaxSupport.defaultAjaxFeedbackHtml = \"<div class=\\\"lia-inline-ajax-feedback lia-component-common-widget-ajax-feedback\\\">\\n\\t\\t\\t<div class=\\\"AjaxFeedback\\\" id=\\\"ajaxFeedback_23fd879059b0ec\\\"><\\/div>\\n\\t\\t\\t\\n\\t\\n\\n\\t\\n\\n\\t\\t<\\/div>\";LITHIUM.AjaxSupport.defaultAjaxErrorHtml = \"<span id=\\\"feedback-errorfeedback_23fd87907456e1\\\"> <\\/span>\\n\\n\\t\\n\\t\\t<div class=\\\"InfoMessage lia-panel-feedback-inline-alert lia-component-common-widget-feedback\\\" id=\\\"feedback_23fd87907456e1\\\">\\n\\t\\t\\t<div role=\\\"alert\\\" class=\\\"lia-text\\\">\\n\\t\\t\\t\\t\\n\\n\\t\\t\\t\\t\\n\\t\\t\\t\\t\\t<p ng-non-bindable=\\\"\\\" tabindex=\\\"0\\\">\\n\\t\\t\\t\\t\\t\\tSorry, unable to complete the action you requested.\\n\\t\\t\\t\\t\\t<\\/p>\\n\\t\\t\\t\\t\\n\\n\\t\\t\\t\\t\\n\\n\\t\\t\\t\\t\\n\\n\\t\\t\\t\\t\\n\\t\\t\\t<\\/div>\\n\\n\\t\\t\\t\\n\\t\\t<\\/div>\";LITHIUM.AjaxSupport.fromLink('#disableAutoComplete_23fd87904957e9', 'disableAutoComplete', '#ajaxfeedback_23fd878fe08c79_0', 'LITHIUM:ajaxError', {}, 'u9DVrLjxEB2b-bOcAKxryCrz_27B_vSDyqzhBL9TlfA.', 'ajax');","content":"<a class=\"lia-link-navigation lia-autocomplete-toggle-off lia-link-ticket-post-action lia-component-search-action-disable-auto-complete\" data-lia-action-token=\"e_IGI7OwH0wKQgmam_WNYuAxqoO6Fl_dcRaTodpk7C0.\" rel=\"nofollow\" id=\"disableAutoComplete_23fd87904957e9\" href=\"https://community.hubspot.com/t5/grouphubs/page.disableautocomplete:disableautocomplete?t:ac=user-id/169781&t:cp=action/contributions/searchactions\">Turn off suggestions<\/a>"}],"prefixTriggerTextLength":3},"inputSelector":"#messageSearchField_23fd878fe08c79_0","redirectToItemLink":false,"url":"https://community.hubspot.com/t5/grouphubs/page.searchformv32.messagesearchfield.messagesearchfield:autocomplete?t:ac=user-id/169781&t:cp=search/contributions/page","resizeImageEvent":"LITHIUM:renderImages"}); LITHIUM.AutoComplete({"options":{"triggerTextLength":0,"updateInputOnSelect":true,"loadingText":"Searching...","emptyText":"No Matches","successText":"Results:","defaultText":"Enter a search word","disabled":false,"footerContent":[{"scripts":"\n\n;(function($){LITHIUM.Link=function(params){var $doc=$(document);function handler(event){var $link=$(this);var token=$link.data('lia-action-token');if($link.data('lia-ajax')!==true&&token!==undefined){if(event.isPropagationStopped()===false&&event.isImmediatePropagationStopped()===false&&event.isDefaultPrevented()===false){event.stop();var $form=$('<form>',{method:'POST',action:$link.attr('href'),enctype:'multipart/form-data'});var $ticket=$('<input>',{type:'hidden',name:'lia-action-token',value:token});$form.append($ticket);$(document.body).append($form);$form.submit();$doc.trigger('click');}}}\nif($doc.data('lia-link-action-handler')===undefined){$doc.data('lia-link-action-handler',true);$doc.on('click.link-action',params.linkSelector,handler);$.fn.on=$.wrap($.fn.on,function(proceed){var ret=proceed.apply(this,$.makeArray(arguments).slice(1));if(this.is(document)){$doc.off('click.link-action',params.linkSelector,handler);proceed.call(this,'click.link-action',params.linkSelector,handler);}\nreturn ret;});}}})(LITHIUM.jQuery);\r\n\nLITHIUM.Link({\n \"linkSelector\" : \"a.lia-link-ticket-post-action\"\n});LITHIUM.AjaxSupport.fromLink('#disableAutoComplete_23fd8790ded64d', 'disableAutoComplete', '#ajaxfeedback_23fd878fe08c79_0', 'LITHIUM:ajaxError', {}, 'eFTkSkbbzk1ACXFLArBlqjptCRI2z1VINvV5GMeb8-Y.', 'ajax');","content":"<a class=\"lia-link-navigation lia-autocomplete-toggle-off lia-link-ticket-post-action lia-component-search-action-disable-auto-complete\" data-lia-action-token=\"W6R2gSEcUeKG88iIQOmeWz9apkDdHW7HpqThLk0USsY.\" rel=\"nofollow\" id=\"disableAutoComplete_23fd8790ded64d\" href=\"https://community.hubspot.com/t5/grouphubs/page.disableautocomplete:disableautocomplete?t:ac=user-id/169781&t:cp=action/contributions/searchactions\">Turn off suggestions<\/a>"}],"prefixTriggerTextLength":3},"inputSelector":"#messageSearchField_23fd878fe08c79_1","redirectToItemLink":false,"url":"https://community.hubspot.com/t5/grouphubs/page.searchformv32.tkbmessagesearchfield.messagesearchfield:autocomplete?t:ac=user-id/169781&t:cp=search/contributions/page","resizeImageEvent":"LITHIUM:renderImages"}); LITHIUM.AutoComplete({"options":{"triggerTextLength":0,"updateInputOnSelect":true,"loadingText":"Searching for users...","emptyText":"No Matches","successText":"Users found:","defaultText":"Enter a user name or rank","disabled":false,"footerContent":[{"scripts":"\n\n;(function($){LITHIUM.Link=function(params){var $doc=$(document);function handler(event){var $link=$(this);var token=$link.data('lia-action-token');if($link.data('lia-ajax')!==true&&token!==undefined){if(event.isPropagationStopped()===false&&event.isImmediatePropagationStopped()===false&&event.isDefaultPrevented()===false){event.stop();var $form=$('<form>',{method:'POST',action:$link.attr('href'),enctype:'multipart/form-data'});var $ticket=$('<input>',{type:'hidden',name:'lia-action-token',value:token});$form.append($ticket);$(document.body).append($form);$form.submit();$doc.trigger('click');}}}\nif($doc.data('lia-link-action-handler')===undefined){$doc.data('lia-link-action-handler',true);$doc.on('click.link-action',params.linkSelector,handler);$.fn.on=$.wrap($.fn.on,function(proceed){var ret=proceed.apply(this,$.makeArray(arguments).slice(1));if(this.is(document)){$doc.off('click.link-action',params.linkSelector,handler);proceed.call(this,'click.link-action',params.linkSelector,handler);}\nreturn ret;});}}})(LITHIUM.jQuery);\r\n\nLITHIUM.Link({\n \"linkSelector\" : \"a.lia-link-ticket-post-action\"\n});LITHIUM.AjaxSupport.fromLink('#disableAutoComplete_23fd8791891a7e', 'disableAutoComplete', '#ajaxfeedback_23fd878fe08c79_0', 'LITHIUM:ajaxError', {}, '5erpFBxvxQcepJgCh0njTpXk1UId37eiChdYGdweUNU.', 'ajax');","content":"<a class=\"lia-link-navigation lia-autocomplete-toggle-off lia-link-ticket-post-action lia-component-search-action-disable-auto-complete\" data-lia-action-token=\"KXOpyDwS6UNBoKYo96MKiOpqJ5fe1FRJ7kvUEp0bM0I.\" rel=\"nofollow\" id=\"disableAutoComplete_23fd8791891a7e\" href=\"https://community.hubspot.com/t5/grouphubs/page.disableautocomplete:disableautocomplete?t:ac=user-id/169781&t:cp=action/contributions/searchactions\">Turn off suggestions<\/a>"}],"prefixTriggerTextLength":0},"inputSelector":"#userSearchField_23fd878fe08c79","redirectToItemLink":false,"url":"https://community.hubspot.com/t5/grouphubs/page.searchformv32.usersearchfield.usersearchfield:autocomplete?t:ac=user-id/169781&t:cp=search/contributions/page","resizeImageEvent":"LITHIUM:renderImages"}); LITHIUM.AjaxSupport({"ajaxOptionsParam":{"event":"LITHIUM:userExistsQuery","parameters":{"javascript.ignore_combine_and_minify":"true"}},"tokenId":"ajax","elementSelector":"#userSearchField_23fd878fe08c79","action":"userExistsQuery","feedbackSelector":"#ajaxfeedback_23fd878fe08c79_0","url":"https://community.hubspot.com/t5/grouphubs/page.searchformv32.usersearchfield:userexistsquery?t:ac=user-id/169781&t:cp=search/contributions/page","ajaxErrorEventName":"LITHIUM:ajaxError","token":"lgEER8XnJnPb00eXMOexo_TxVWyEyb9tiSu18_JmaYA."}); LITHIUM.AutoComplete({"options":{"triggerTextLength":0,"updateInputOnSelect":true,"loadingText":"Searching...","emptyText":"No Matches","successText":"Results:","defaultText":"Enter a search word","disabled":false,"footerContent":[{"scripts":"\n\n;(function($){LITHIUM.Link=function(params){var $doc=$(document);function handler(event){var $link=$(this);var token=$link.data('lia-action-token');if($link.data('lia-ajax')!==true&&token!==undefined){if(event.isPropagationStopped()===false&&event.isImmediatePropagationStopped()===false&&event.isDefaultPrevented()===false){event.stop();var $form=$('<form>',{method:'POST',action:$link.attr('href'),enctype:'multipart/form-data'});var $ticket=$('<input>',{type:'hidden',name:'lia-action-token',value:token});$form.append($ticket);$(document.body).append($form);$form.submit();$doc.trigger('click');}}}\nif($doc.data('lia-link-action-handler')===undefined){$doc.data('lia-link-action-handler',true);$doc.on('click.link-action',params.linkSelector,handler);$.fn.on=$.wrap($.fn.on,function(proceed){var ret=proceed.apply(this,$.makeArray(arguments).slice(1));if(this.is(document)){$doc.off('click.link-action',params.linkSelector,handler);proceed.call(this,'click.link-action',params.linkSelector,handler);}\nreturn ret;});}}})(LITHIUM.jQuery);\r\n\nLITHIUM.Link({\n \"linkSelector\" : \"a.lia-link-ticket-post-action\"\n});LITHIUM.AjaxSupport.fromLink('#disableAutoComplete_23fd8791d7eebd', 'disableAutoComplete', '#ajaxfeedback_23fd878fe08c79_0', 'LITHIUM:ajaxError', {}, 'Y7tTuFV7LUpBHoshukKA-9PShBL4p70QA4NaoOCAvsE.', 'ajax');","content":"<a class=\"lia-link-navigation lia-autocomplete-toggle-off lia-link-ticket-post-action lia-component-search-action-disable-auto-complete\" data-lia-action-token=\"80G7bS66tWD_zhCx7DLtpRxqx1knUh7XMPE6CjFG2PI.\" rel=\"nofollow\" id=\"disableAutoComplete_23fd8791d7eebd\" href=\"https://community.hubspot.com/t5/grouphubs/page.disableautocomplete:disableautocomplete?t:ac=user-id/169781&t:cp=action/contributions/searchactions\">Turn off suggestions<\/a>"}],"prefixTriggerTextLength":0},"inputSelector":"#noteSearchField_23fd878fe08c79_0","redirectToItemLink":false,"url":"https://community.hubspot.com/t5/grouphubs/page.searchformv32.notesearchfield.notesearchfield:autocomplete?t:ac=user-id/169781&t:cp=search/contributions/page","resizeImageEvent":"LITHIUM:renderImages"}); LITHIUM.AutoComplete({"options":{"triggerTextLength":0,"updateInputOnSelect":true,"loadingText":"Searching...","emptyText":"No Matches","successText":"Results:","defaultText":"Enter a search word","disabled":false,"footerContent":[{"scripts":"\n\n;(function($){LITHIUM.Link=function(params){var $doc=$(document);function handler(event){var $link=$(this);var token=$link.data('lia-action-token');if($link.data('lia-ajax')!==true&&token!==undefined){if(event.isPropagationStopped()===false&&event.isImmediatePropagationStopped()===false&&event.isDefaultPrevented()===false){event.stop();var $form=$('<form>',{method:'POST',action:$link.attr('href'),enctype:'multipart/form-data'});var $ticket=$('<input>',{type:'hidden',name:'lia-action-token',value:token});$form.append($ticket);$(document.body).append($form);$form.submit();$doc.trigger('click');}}}\nif($doc.data('lia-link-action-handler')===undefined){$doc.data('lia-link-action-handler',true);$doc.on('click.link-action',params.linkSelector,handler);$.fn.on=$.wrap($.fn.on,function(proceed){var ret=proceed.apply(this,$.makeArray(arguments).slice(1));if(this.is(document)){$doc.off('click.link-action',params.linkSelector,handler);proceed.call(this,'click.link-action',params.linkSelector,handler);}\nreturn ret;});}}})(LITHIUM.jQuery);\r\n\nLITHIUM.Link({\n \"linkSelector\" : \"a.lia-link-ticket-post-action\"\n});LITHIUM.AjaxSupport.fromLink('#disableAutoComplete_23fd87924272a7', 'disableAutoComplete', '#ajaxfeedback_23fd878fe08c79_0', 'LITHIUM:ajaxError', {}, 'pjkl4joRmfiFhSbOBTybq9M-kR02s-g0mtBIlEnCGeE.', 'ajax');","content":"<a class=\"lia-link-navigation lia-autocomplete-toggle-off lia-link-ticket-post-action lia-component-search-action-disable-auto-complete\" data-lia-action-token=\"W3GRoKeIoPMjLvnM6Z7wHbSsceUTrL8T6EvCiKNLD3w.\" rel=\"nofollow\" id=\"disableAutoComplete_23fd87924272a7\" href=\"https://community.hubspot.com/t5/grouphubs/page.disableautocomplete:disableautocomplete?t:ac=user-id/169781&t:cp=action/contributions/searchactions\">Turn off suggestions<\/a>"}],"prefixTriggerTextLength":0},"inputSelector":"#productSearchField_23fd878fe08c79","redirectToItemLink":false,"url":"https://community.hubspot.com/t5/grouphubs/page.searchformv32.productsearchfield.productsearchfield:autocomplete?t:ac=user-id/169781&t:cp=search/contributions/page","resizeImageEvent":"LITHIUM:renderImages"}); LITHIUM.AjaxSupport.fromLink('#enableAutoComplete_23fd878fe08c79', 'enableAutoComplete', '#ajaxfeedback_23fd878fe08c79_0', 'LITHIUM:ajaxError', {}, 'WZZticmbzb3n0deExTuDRlT_GPLWJDOVIdTtGgOcBIs.', 'ajax'); LITHIUM.Tooltip({"bodySelector":"body#lia-body","delay":30,"enableOnClickForTrigger":false,"predelay":10,"triggerSelector":"#link_23fd878fe08c79","tooltipContentSelector":"#link_23fd878fe08c79_0-tooltip-element .content","position":["bottom","left"],"tooltipElementSelector":"#link_23fd878fe08c79_0-tooltip-element","events":{"def":"focus mouseover keydown,blur mouseout keydown"},"hideOnLeave":true}); LITHIUM.HelpIcon({"selectors":{"helpIconSelector":".help-icon .lia-img-icon-help"}}); LITHIUM.SearchAutoCompleteToggle({"containerSelector":"#searchautocompletetoggle_23fd878fe08c79","enableAutoCompleteSelector":".search-autocomplete-toggle-link","enableAutocompleteSuccessEvent":"LITHIUM:ajaxSuccess:enableAutoComplete","disableAutoCompleteSelector":".lia-autocomplete-toggle-off","disableAutocompleteSuccessEvent":"LITHIUM:ajaxSuccess:disableAutoComplete","autoCompleteSelector":".lia-autocomplete-input"}); LITHIUM.SearchForm({"asSearchActionIdSelector":".lia-as-search-action-id","useAutoComplete":true,"selectSelector":".lia-search-form-granularity","useClearSearchButton":false,"buttonSelector":".lia-button-searchForm-action","asSearchActionIdParamName":"as-search-action-id","formSelector":"#lia-searchformV32_23fd878fe08c79","asSearchActionIdHeaderKey":"X-LI-AS-Search-Action-Id","inputSelector":"#messageSearchField_23fd878fe08c79_0:not(.lia-js-hidden)","clearSearchButtonSelector":null}); LITHIUM.Form.resetFieldForFocusFound(); (function($) { document.querySelector('a.login-link').classList.add('homepage-nav-login'); })(LITHIUM.jQuery); ;(function($){ $(document).ready(function() { $(".custom-user-menu-v2 .nav-link").click(function(e) { e.preventDefault(); $(".nav-popover.profile").toggleClass('show'); }); $(".search-toggle-action-icon-plus").on("click",function(e){ e.preventDefault(); $(this).parent().find(".plus-bar-main-content").toggle(); }); //User Avatar $('.header-tab-nav li span').click(function() { $('.header-tab-nav li span').removeClass("active"); if(this.id == 'profile'){ $('span#profile').addClass("active"); $('.header-tab-nav-content > div#profile-list-wrapper').show(); $('.header-tab-nav-content > div#admin-list-wrapper').hide(); $('.header-tab-nav-content > div#profile-list-wrapper').removeClass('profile-menu-dropdown'); } if(this.id == 'admin'){ $('span#admin').addClass("active"); $('.header-tab-nav-content > div#profile-list-wrapper').hide(); $('.header-tab-nav-content > div#admin-list-wrapper').show(); $('.header-tab-nav-content > div#profile-list-wrapper').addClass('profile-menu-dropdown'); } var indexer = $(this).index(); //gets the current index of (this) which is #header-tab-nav li $('.header-tab-nav-content > div:eq(' + indexer + ')').fadeIn(); //uses whatever index the link has to open the corresponding box }); $(this).mouseup(function (e){ var customButton = $('.nav-popover.profile'); if(!$('.custom-menu-caret').is(e.target) && $('.custom-menu-caret').has(e.target).length === 0){ if(!customButton.is(e.target) && customButton.has(e.target).length === 0){ if (!$('.custom-user-menu-v2 > .nav-link').is(e.target) && $('.custom-user-menu-v2 > .nav-link').has(e.target).length === 0) { customButton.removeClass('show'); } } } var menuWrapper = $('.menu-wrapper'); if(!menuWrapper.is(e.target) && menuWrapper.has(e.target).length === 0){ if (!$('.menu').is(e.target) && $('.menu').has(e.target).length === 0) { menuWrapper.removeClass('offcanvas'); } } var container = $(".plus-bar-main-content"); var customButton = $(".search-toggle-action-icon-plus"); if (!customButton.is(e.target) && customButton.has(e.target).length === 0) { container.hide(); } if(!$('.lang-picker-wrapper').is(e.target) && $('.lang-picker-wrapper').has(e.target).length === 0){ if (!$('.lang-picker').is(e.target) && $('.lang-picker').has(e.target).length === 0) { $('.lang-picker').removeClass('show'); } } }); //SCROLL JS $(window).scroll(function(e) { e.preventDefault(); if($('.nav-popover.profile').hasClass("show")){ if ($(this).scrollTop() > 0) { $('.nav-popover.profile').removeClass("show"); } else { $('.nav-popover.profile').addClass("show"); } } if($('.nav-popover.get-hubspot').hasClass("show")){ if ($(this).scrollTop() > 0) { $('.nav-popover.get-hubspot').removeClass("show"); } else { $('.nav-popover.get-hubspot').addClass("show"); } } if ($(this).scrollTop() > 0) { $('.search-input.lia-search-input-message').blur(); $('.plus-bar-main-content').hide(); } }); }); jQuery('.lang-picker-wrapper').click(function(){ jQuery(".lang-picker").toggleClass('show'); }); jQuery('.lia-cat-sub-editor-modal .lia-ui-modal-footer .lia-button-Submit-action').live('click',function(){ setTimeout( function() { location.reload(true); },1000); }); })(LITHIUM.jQuery); ;(function($){ var langMap = { 'en':'hubspot_community_en', 'es':'hubspot_community_es', 'fr':'hubspot_community_fr', 'ja':'hubspot_community_jp', 'pt-br':'hubspot_community_pt', 'de':'hubspot_community_de' } var nodeType = "community"; var langScope = langMap['en']; var isSearchPage = jQuery('body').hasClass('SearchPage'); var isIdeasLandingPage = jQuery('body').hasClass('ideaslandingpage'); if (nodeType === "community" && !isSearchPage && !isIdeasLandingPage) { var inputFormFilter = '<input name="filter" value="location" type="hidden">'; var inputFormLocation = '<input name="location" value="category:' + langScope + '" type="hidden">'; $('form.SearchForm').append(inputFormFilter).append(inputFormLocation); } else if (nodeType === "community" && isIdeasLandingPage) { var searchUrl = "/t5/forums/searchpage/tab/message?filter=location&location=idea-board:HubSpot_Ideas&collapse_discussion=true"; var query = jQuery('.SearchForm .lia-search-input-message').val(); jQuery(document).on('submit', 'form.SearchForm', function(e) { e.preventDefault(); var newQ = "&q=" + document.querySelector('.SearchForm .lia-search-input-wrapper input.search-input').value; window.location = window.location.origin + searchUrl + newQ; }) } })(LITHIUM.jQuery) LITHIUM.InformationBox({"updateFeedbackEvent":"LITHIUM:updateAjaxFeedback","componentSelector":"#informationbox_23fd879321d040","feedbackSelector":".InfoMessage"}); LITHIUM.InformationBox({"updateFeedbackEvent":"LITHIUM:updateAjaxFeedback","componentSelector":"#informationbox_23fd879321d040_0","feedbackSelector":".InfoMessage"}); LITHIUM.InformationBox({"updateFeedbackEvent":"LITHIUM:updateAjaxFeedback","componentSelector":"#informationbox_23fd879321d040_1","feedbackSelector":".InfoMessage"}); LITHIUM.InformationBox({"updateFeedbackEvent":"LITHIUM:updateAjaxFeedback","componentSelector":"#informationbox_23fd879321d040_2","feedbackSelector":".InfoMessage"}); LITHIUM.AutoComplete({"options":{"triggerTextLength":0,"updateInputOnSelect":true,"loadingText":"Searching...","emptyText":"No Matches","successText":"Results:","defaultText":"Enter a search word","disabled":false,"footerContent":[{"scripts":"\n\n;(function($){LITHIUM.Link=function(params){var $doc=$(document);function handler(event){var $link=$(this);var token=$link.data('lia-action-token');if($link.data('lia-ajax')!==true&&token!==undefined){if(event.isPropagationStopped()===false&&event.isImmediatePropagationStopped()===false&&event.isDefaultPrevented()===false){event.stop();var $form=$('<form>',{method:'POST',action:$link.attr('href'),enctype:'multipart/form-data'});var $ticket=$('<input>',{type:'hidden',name:'lia-action-token',value:token});$form.append($ticket);$(document.body).append($form);$form.submit();$doc.trigger('click');}}}\nif($doc.data('lia-link-action-handler')===undefined){$doc.data('lia-link-action-handler',true);$doc.on('click.link-action',params.linkSelector,handler);$.fn.on=$.wrap($.fn.on,function(proceed){var ret=proceed.apply(this,$.makeArray(arguments).slice(1));if(this.is(document)){$doc.off('click.link-action',params.linkSelector,handler);proceed.call(this,'click.link-action',params.linkSelector,handler);}\nreturn ret;});}}})(LITHIUM.jQuery);\r\n\nLITHIUM.Link({\n \"linkSelector\" : \"a.lia-link-ticket-post-action\"\n});LITHIUM.AjaxSupport.fromLink('#disableAutoComplete_23fd87939c1718', 'disableAutoComplete', '#ajaxfeedback_23fd879321d040_0', 'LITHIUM:ajaxError', {}, '1QCrJp2J3sV1VwYV49UNoYhQev54aymD2aKl8IvJDBc.', 'ajax');","content":"<a class=\"lia-link-navigation lia-autocomplete-toggle-off lia-link-ticket-post-action lia-component-search-action-disable-auto-complete\" data-lia-action-token=\"3fM4Sep22LWEm1VzkZFC7YVoLyzE2SAKKGPIJq96CKc.\" rel=\"nofollow\" id=\"disableAutoComplete_23fd87939c1718\" href=\"https://community.hubspot.com/t5/grouphubs/page.disableautocomplete:disableautocomplete?t:ac=user-id/169781&t:cp=action/contributions/searchactions\">Turn off suggestions<\/a>"}],"prefixTriggerTextLength":3},"inputSelector":"#messageSearchField_23fd879321d040_0","redirectToItemLink":false,"url":"https://community.hubspot.com/t5/grouphubs/page.searchformv32.messagesearchfield.messagesearchfield:autocomplete?t:ac=user-id/169781&t:cp=search/contributions/page","resizeImageEvent":"LITHIUM:renderImages"}); LITHIUM.AutoComplete({"options":{"triggerTextLength":0,"updateInputOnSelect":true,"loadingText":"Searching...","emptyText":"No Matches","successText":"Results:","defaultText":"Enter a search word","disabled":false,"footerContent":[{"scripts":"\n\n;(function($){LITHIUM.Link=function(params){var $doc=$(document);function handler(event){var $link=$(this);var token=$link.data('lia-action-token');if($link.data('lia-ajax')!==true&&token!==undefined){if(event.isPropagationStopped()===false&&event.isImmediatePropagationStopped()===false&&event.isDefaultPrevented()===false){event.stop();var $form=$('<form>',{method:'POST',action:$link.attr('href'),enctype:'multipart/form-data'});var $ticket=$('<input>',{type:'hidden',name:'lia-action-token',value:token});$form.append($ticket);$(document.body).append($form);$form.submit();$doc.trigger('click');}}}\nif($doc.data('lia-link-action-handler')===undefined){$doc.data('lia-link-action-handler',true);$doc.on('click.link-action',params.linkSelector,handler);$.fn.on=$.wrap($.fn.on,function(proceed){var ret=proceed.apply(this,$.makeArray(arguments).slice(1));if(this.is(document)){$doc.off('click.link-action',params.linkSelector,handler);proceed.call(this,'click.link-action',params.linkSelector,handler);}\nreturn ret;});}}})(LITHIUM.jQuery);\r\n\nLITHIUM.Link({\n \"linkSelector\" : \"a.lia-link-ticket-post-action\"\n});LITHIUM.AjaxSupport.fromLink('#disableAutoComplete_23fd8793cd745c', 'disableAutoComplete', '#ajaxfeedback_23fd879321d040_0', 'LITHIUM:ajaxError', {}, 'fud9-3vdQxizOmwAt704tcvjwKTIDE3l2Bfghz1RPbk.', 'ajax');","content":"<a class=\"lia-link-navigation lia-autocomplete-toggle-off lia-link-ticket-post-action lia-component-search-action-disable-auto-complete\" data-lia-action-token=\"HCiFVsmabrDnOCzpT-OblbUhxDR58rEHLf83VWkccYQ.\" rel=\"nofollow\" id=\"disableAutoComplete_23fd8793cd745c\" href=\"https://community.hubspot.com/t5/grouphubs/page.disableautocomplete:disableautocomplete?t:ac=user-id/169781&t:cp=action/contributions/searchactions\">Turn off suggestions<\/a>"}],"prefixTriggerTextLength":3},"inputSelector":"#messageSearchField_23fd879321d040_1","redirectToItemLink":false,"url":"https://community.hubspot.com/t5/grouphubs/page.searchformv32.tkbmessagesearchfield.messagesearchfield:autocomplete?t:ac=user-id/169781&t:cp=search/contributions/page","resizeImageEvent":"LITHIUM:renderImages"}); LITHIUM.AutoComplete({"options":{"triggerTextLength":0,"updateInputOnSelect":true,"loadingText":"Searching for users...","emptyText":"No Matches","successText":"Users found:","defaultText":"Enter a user name or rank","disabled":false,"footerContent":[{"scripts":"\n\n;(function($){LITHIUM.Link=function(params){var $doc=$(document);function handler(event){var $link=$(this);var token=$link.data('lia-action-token');if($link.data('lia-ajax')!==true&&token!==undefined){if(event.isPropagationStopped()===false&&event.isImmediatePropagationStopped()===false&&event.isDefaultPrevented()===false){event.stop();var $form=$('<form>',{method:'POST',action:$link.attr('href'),enctype:'multipart/form-data'});var $ticket=$('<input>',{type:'hidden',name:'lia-action-token',value:token});$form.append($ticket);$(document.body).append($form);$form.submit();$doc.trigger('click');}}}\nif($doc.data('lia-link-action-handler')===undefined){$doc.data('lia-link-action-handler',true);$doc.on('click.link-action',params.linkSelector,handler);$.fn.on=$.wrap($.fn.on,function(proceed){var ret=proceed.apply(this,$.makeArray(arguments).slice(1));if(this.is(document)){$doc.off('click.link-action',params.linkSelector,handler);proceed.call(this,'click.link-action',params.linkSelector,handler);}\nreturn ret;});}}})(LITHIUM.jQuery);\r\n\nLITHIUM.Link({\n \"linkSelector\" : \"a.lia-link-ticket-post-action\"\n});LITHIUM.AjaxSupport.fromLink('#disableAutoComplete_23fd879422ad80', 'disableAutoComplete', '#ajaxfeedback_23fd879321d040_0', 'LITHIUM:ajaxError', {}, 'iKbBlWe4MuC5Dq3CnotyiJ7_IaRA7LJhUT8nhVqG1Gs.', 'ajax');","content":"<a class=\"lia-link-navigation lia-autocomplete-toggle-off lia-link-ticket-post-action lia-component-search-action-disable-auto-complete\" data-lia-action-token=\"ezzIsWncuz98A_LFqxnnriKkgf9jB3RmhQabvGTSM1M.\" rel=\"nofollow\" id=\"disableAutoComplete_23fd879422ad80\" href=\"https://community.hubspot.com/t5/grouphubs/page.disableautocomplete:disableautocomplete?t:ac=user-id/169781&t:cp=action/contributions/searchactions\">Turn off suggestions<\/a>"}],"prefixTriggerTextLength":0},"inputSelector":"#userSearchField_23fd879321d040","redirectToItemLink":false,"url":"https://community.hubspot.com/t5/grouphubs/page.searchformv32.usersearchfield.usersearchfield:autocomplete?t:ac=user-id/169781&t:cp=search/contributions/page","resizeImageEvent":"LITHIUM:renderImages"}); LITHIUM.AjaxSupport({"ajaxOptionsParam":{"event":"LITHIUM:userExistsQuery","parameters":{"javascript.ignore_combine_and_minify":"true"}},"tokenId":"ajax","elementSelector":"#userSearchField_23fd879321d040","action":"userExistsQuery","feedbackSelector":"#ajaxfeedback_23fd879321d040_0","url":"https://community.hubspot.com/t5/grouphubs/page.searchformv32.usersearchfield:userexistsquery?t:ac=user-id/169781&t:cp=search/contributions/page","ajaxErrorEventName":"LITHIUM:ajaxError","token":"wcq1WLjyYjnmmVelOyvYzado_PrkXlF97YwLtNRzaxU."}); LITHIUM.AutoComplete({"options":{"triggerTextLength":0,"updateInputOnSelect":true,"loadingText":"Searching...","emptyText":"No Matches","successText":"Results:","defaultText":"Enter a search word","disabled":false,"footerContent":[{"scripts":"\n\n;(function($){LITHIUM.Link=function(params){var $doc=$(document);function handler(event){var $link=$(this);var token=$link.data('lia-action-token');if($link.data('lia-ajax')!==true&&token!==undefined){if(event.isPropagationStopped()===false&&event.isImmediatePropagationStopped()===false&&event.isDefaultPrevented()===false){event.stop();var $form=$('<form>',{method:'POST',action:$link.attr('href'),enctype:'multipart/form-data'});var $ticket=$('<input>',{type:'hidden',name:'lia-action-token',value:token});$form.append($ticket);$(document.body).append($form);$form.submit();$doc.trigger('click');}}}\nif($doc.data('lia-link-action-handler')===undefined){$doc.data('lia-link-action-handler',true);$doc.on('click.link-action',params.linkSelector,handler);$.fn.on=$.wrap($.fn.on,function(proceed){var ret=proceed.apply(this,$.makeArray(arguments).slice(1));if(this.is(document)){$doc.off('click.link-action',params.linkSelector,handler);proceed.call(this,'click.link-action',params.linkSelector,handler);}\nreturn ret;});}}})(LITHIUM.jQuery);\r\n\nLITHIUM.Link({\n \"linkSelector\" : \"a.lia-link-ticket-post-action\"\n});LITHIUM.AjaxSupport.fromLink('#disableAutoComplete_23fd87944ddb4f', 'disableAutoComplete', '#ajaxfeedback_23fd879321d040_0', 'LITHIUM:ajaxError', {}, 'CWeJ2pWaBX4z7U1ExaWrJ0RwgGfzY7JklrO4wxX5DyA.', 'ajax');","content":"<a class=\"lia-link-navigation lia-autocomplete-toggle-off lia-link-ticket-post-action lia-component-search-action-disable-auto-complete\" data-lia-action-token=\"s3GPVQHoaJzMMWaWaXvszwxDyYVAhJFk5URwdb6vwyw.\" rel=\"nofollow\" id=\"disableAutoComplete_23fd87944ddb4f\" href=\"https://community.hubspot.com/t5/grouphubs/page.disableautocomplete:disableautocomplete?t:ac=user-id/169781&t:cp=action/contributions/searchactions\">Turn off suggestions<\/a>"}],"prefixTriggerTextLength":0},"inputSelector":"#noteSearchField_23fd879321d040_0","redirectToItemLink":false,"url":"https://community.hubspot.com/t5/grouphubs/page.searchformv32.notesearchfield.notesearchfield:autocomplete?t:ac=user-id/169781&t:cp=search/contributions/page","resizeImageEvent":"LITHIUM:renderImages"}); LITHIUM.AutoComplete({"options":{"triggerTextLength":0,"updateInputOnSelect":true,"loadingText":"Searching...","emptyText":"No Matches","successText":"Results:","defaultText":"Enter a search word","disabled":false,"footerContent":[{"scripts":"\n\n;(function($){LITHIUM.Link=function(params){var $doc=$(document);function handler(event){var $link=$(this);var token=$link.data('lia-action-token');if($link.data('lia-ajax')!==true&&token!==undefined){if(event.isPropagationStopped()===false&&event.isImmediatePropagationStopped()===false&&event.isDefaultPrevented()===false){event.stop();var $form=$('<form>',{method:'POST',action:$link.attr('href'),enctype:'multipart/form-data'});var $ticket=$('<input>',{type:'hidden',name:'lia-action-token',value:token});$form.append($ticket);$(document.body).append($form);$form.submit();$doc.trigger('click');}}}\nif($doc.data('lia-link-action-handler')===undefined){$doc.data('lia-link-action-handler',true);$doc.on('click.link-action',params.linkSelector,handler);$.fn.on=$.wrap($.fn.on,function(proceed){var ret=proceed.apply(this,$.makeArray(arguments).slice(1));if(this.is(document)){$doc.off('click.link-action',params.linkSelector,handler);proceed.call(this,'click.link-action',params.linkSelector,handler);}\nreturn ret;});}}})(LITHIUM.jQuery);\r\n\nLITHIUM.Link({\n \"linkSelector\" : \"a.lia-link-ticket-post-action\"\n});LITHIUM.AjaxSupport.fromLink('#disableAutoComplete_23fd8794a0aee3', 'disableAutoComplete', '#ajaxfeedback_23fd879321d040_0', 'LITHIUM:ajaxError', {}, '3-SYcQ_tMEgCkyW1tlFe1B9BA8FyelVyKj-OHUqsqnA.', 'ajax');","content":"<a class=\"lia-link-navigation lia-autocomplete-toggle-off lia-link-ticket-post-action lia-component-search-action-disable-auto-complete\" data-lia-action-token=\"2kFQQKN8m5OqHiv_0__lHdSJMEzT2M0GuqTtiq7QvXU.\" rel=\"nofollow\" id=\"disableAutoComplete_23fd8794a0aee3\" href=\"https://community.hubspot.com/t5/grouphubs/page.disableautocomplete:disableautocomplete?t:ac=user-id/169781&t:cp=action/contributions/searchactions\">Turn off suggestions<\/a>"}],"prefixTriggerTextLength":0},"inputSelector":"#productSearchField_23fd879321d040","redirectToItemLink":false,"url":"https://community.hubspot.com/t5/grouphubs/page.searchformv32.productsearchfield.productsearchfield:autocomplete?t:ac=user-id/169781&t:cp=search/contributions/page","resizeImageEvent":"LITHIUM:renderImages"}); LITHIUM.AjaxSupport.fromLink('#enableAutoComplete_23fd879321d040', 'enableAutoComplete', '#ajaxfeedback_23fd879321d040_0', 'LITHIUM:ajaxError', {}, '7gvrnyXFBWZewNef6qfHLWlA3ewZVdmWNtBLM2wpRhE.', 'ajax'); LITHIUM.Tooltip({"bodySelector":"body#lia-body","delay":30,"enableOnClickForTrigger":false,"predelay":10,"triggerSelector":"#link_23fd879321d040","tooltipContentSelector":"#link_23fd879321d040_0-tooltip-element .content","position":["bottom","left"],"tooltipElementSelector":"#link_23fd879321d040_0-tooltip-element","events":{"def":"focus mouseover keydown,blur mouseout keydown"},"hideOnLeave":true}); LITHIUM.HelpIcon({"selectors":{"helpIconSelector":".help-icon .lia-img-icon-help"}}); LITHIUM.SearchAutoCompleteToggle({"containerSelector":"#searchautocompletetoggle_23fd879321d040","enableAutoCompleteSelector":".search-autocomplete-toggle-link","enableAutocompleteSuccessEvent":"LITHIUM:ajaxSuccess:enableAutoComplete","disableAutoCompleteSelector":".lia-autocomplete-toggle-off","disableAutocompleteSuccessEvent":"LITHIUM:ajaxSuccess:disableAutoComplete","autoCompleteSelector":".lia-autocomplete-input"}); LITHIUM.SearchForm({"asSearchActionIdSelector":".lia-as-search-action-id","useAutoComplete":true,"selectSelector":".lia-search-form-granularity","useClearSearchButton":false,"buttonSelector":".lia-button-searchForm-action","asSearchActionIdParamName":"as-search-action-id","formSelector":"#lia-searchformV32_23fd879321d040","nodesModel":{"169781|authorMessages":{"title":"Search posts by kennedyp","inputSelector":".lia-search-input-message"},"user|user":{"title":"Users","inputSelector":".lia-search-input-user"},"mjmao93648|community":{"title":"Search Community: HubSpot Community","inputSelector":".lia-search-input-message"}},"asSearchActionIdHeaderKey":"X-LI-AS-Search-Action-Id","inputSelector":"#messageSearchField_23fd879321d040_0:not(.lia-js-hidden)","clearSearchButtonSelector":null}); (function($) { document.querySelector('a.login-link').classList.add('homepage-nav-login'); })(LITHIUM.jQuery); (function($) { if ( $('.lia-notification-feed-page-link').length ) { $('.lia-notification-feed-page-link').addClass('nav-notifs'); } if ( $('.private-notes-link').length ) { $('.private-notes-link').addClass('nav-mail'); } })(LITHIUM.jQuery); ;(function($){ $('.custom-search-focus').on('click', function() { $('.lia-search-input-message').focus(); }); })(LITHIUM.jQuery); // Pull in global jQuery reference if (document.querySelectorAll('.lia-component-admin-widget-moderation-manager')[0]) { document.querySelectorAll('.lia-component-admin-widget-moderation-manager')[0].href = "/t5/bizapps/page/tab/community%3Amoderation?filter=includeForums&sort_by=-topicPostDate&include_forums=true&collapse_discussion=true" } ;(function($) { $("#get-hubspot-free").click(function(){ $("#get-hubspot").toggleClass("show"); }); // Closes dropdown boxes when clicking outside of the box // click listener applied inline, function in script tag window.onclick = function(e) { if (e.target?.matches && !e.target?.matches('#get-hubspot-free')) { if (document.getElementById("get-hubspot")) { if (document.getElementById("get-hubspot").classList.contains('show')) { document.getElementById("get-hubspot").classList.remove('show'); } } } if (e.target?.matches && !e.target?.matches('#current-language')) { if (document.getElementById("lang-picker-global")) { if (document.getElementById("lang-picker-global").classList.contains('show')) { document.getElementById("lang-picker-global").classList.remove('show'); } } } }; $(window).scroll(function(){ if ($(this).scrollTop() > 65) { $('.forum-nav-bar').addClass('ch-sticky'); $('.community-header-nav').addClass('ch-space'); } else { $('.forum-nav-bar').removeClass('ch-sticky'); $('.community-header-nav').removeClass('ch-space');; } }); $('span.custom-menu-caret').on('click',function(){ $(this).siblings('.nav-popover.profile').toggleClass('show'); }); })(LITHIUM.jQuery); ;(function($){ function addUneven() { $(".lia-groups-list i.lia-fa").each(function() { $(this).addClass("spacesword"); }) var groupsList = $('.lia-groups-list .lia-cards'); if (groupsList[0]) { if (!Number.isInteger(groupsList[0].children.length / 3)) { groupsList.addClass('uneven'); } } var myGroupsList = $('.my-groups-container ul'); if (myGroupsList[0]) { if (myGroupsList[0].children.length < 3 && myGroupsList[0].children.length > 1) { myGroupsList.addClass('uneven'); } } }; $(document).ready(function() { addUneven(); }); var dropDown = $('.dropdown-default-item .lia-menu-dropdown-items li'); $(dropDown).each(function() { $(this).on("click", function(){ count = 0; var x = setInterval(function(){ addUneven(); if(count > 80) clearInterval(x); count++; }, 100); }); }); $('.lia-paging-page-previous a, .lia-paging-page-next a').on("click", function() { count2 = 0; var y = setInterval(function(){ addUneven(); if(count2 > 80) clearInterval(y); count2++; }, 100); }); $('.lia-groups-list form input.lia-form-filter-by-name-input').on("keyup change", function() { count3 = 0; var z = setInterval(function(){ addUneven(); if(count3 > 80) clearInterval(z); count3++; }, 100); }); })(LITHIUM.jQuery); ;(function ($) { if ($(window).width() <= 991) { $(".community-footer .col:nth-child(3)").on('click',function(){ if ($(this).hasClass('active')) { $(this).removeClass('active'); $(this).children("ul").hide(); $(this).children("h5").removeClass("addedClass"); } else { $(".community-footer .col").removeClass('active'); $('.community-footer .col ul').hide(); $(this).addClass('active'); $(this).children("ul").show(); $('.community-footer .col').children("h5").removeClass("addedClass"); $(this).children("h5").addClass("addedClass"); } if ( $(".community-footer .col:nth-child(4) ul").hasClass('custom-footer-res')) { $(".community-footer .col:nth-child(4) ul").removeClass('custom-footer-res'); } else { $(".community-footer .col:nth-child(4) ul").addClass('custom-footer-res'); } }) $('.community-footer .col:nth-child(1)').on("click", function () { if ($(this).hasClass('active')) { $(this).removeClass('active'); $(this).children("ul").hide(); $(this).children("h5").removeClass("addedClass"); } else { $(".community-footer .col").removeClass('active'); $('.community-footer .col ul').hide(); $(this).addClass('active'); $(this).children("ul").show(); $('.community-footer .col').children("h5").removeClass("addedClass"); $(this).children("h5").addClass("addedClass"); } if ( $(".community-footer .col:nth-child(4) ul").hasClass('custom-footer-res')) { $(".community-footer .col:nth-child(4) ul").removeClass('custom-footer-res'); } }) $('.community-footer .col:nth-child(2)').on("click", function () { if ($(this).hasClass('active')) { $(this).removeClass('active'); $(this).children("ul").hide(); $(this).children("h5").removeClass("addedClass"); } else { $(".community-footer .col").removeClass('active'); $('.community-footer .col ul').hide(); $(this).addClass('active'); $(this).children("ul").show(); $('.community-footer .col').children("h5").removeClass("addedClass"); $(this).children("h5").addClass("addedClass"); } if ( $(".community-footer .col:nth-child(4) ul").hasClass('custom-footer-res')) { $(".community-footer .col:nth-child(4) ul").removeClass('custom-footer-res'); } }) } })(LITHIUM.jQuery); LITHIUM.PartialRenderProxy({"limuirsComponentRenderedEvent":"LITHIUM:limuirsComponentRendered","relayEvent":"LITHIUM:partialRenderProxyRelay","listenerEvent":"LITHIUM:partialRenderProxy"}); LITHIUM.AjaxSupport({"ajaxOptionsParam":{"event":"LITHIUM:partialRenderProxyRelay","parameters":{"javascript.ignore_combine_and_minify":"true"}},"tokenId":"ajax","elementSelector":document,"action":"partialRenderProxyRelay","feedbackSelector":false,"url":"https://community.hubspot.com/t5/grouphubs/page.liabase.basebody.partialrenderproxy:partialrenderproxyrelay?t:ac=user-id/169781","ajaxErrorEventName":"LITHIUM:ajaxError","token":"i2RPFPNLuht-V6ncd2gxOyqmgZ1JzKHw92pxL9VY1nU."}); LITHIUM.Auth.API_URL = "/t5/util/authcheckpage"; LITHIUM.Auth.LOGIN_URL_TMPL = "https://app.hubspot.com/khoros/integration/jwt/authenticate?referer=https%3A%2F%2FREPLACE_TEXT"; LITHIUM.Auth.KEEP_ALIVE_URL = "/t5/status/blankpage?keepalive"; LITHIUM.Auth.KEEP_ALIVE_TIME = 300000; LITHIUM.Auth.CHECK_SESSION_TOKEN = 'G3M7MS5VVvsJCDdyExPvVgxpxehwl3jbOaGRHw4qRws.'; LITHIUM.AjaxSupport.useTickets = false; LITHIUM.Loader.runJsAttached(); // --> </script></body> </html>