ui: document-attrs helper (#9336)
This commit adds a {{document-attrs}} helper, specifically for adding attributes to the root documentElement, which in our case is always <html>
This commit is contained in:
parent
3be60d9e4e
commit
7dc1a91edf
|
@ -52,7 +52,10 @@
|
||||||
<div>
|
<div>
|
||||||
{{#if authorized}}
|
{{#if authorized}}
|
||||||
<nav aria-label="Breadcrumb">
|
<nav aria-label="Breadcrumb">
|
||||||
<YieldSlot @name="breadcrumbs">{{yield}}</YieldSlot>
|
<YieldSlot @name="breadcrumbs">
|
||||||
|
{{document-attrs class="with-breadcrumbs"}}
|
||||||
|
{{yield}}
|
||||||
|
</YieldSlot>
|
||||||
</nav>
|
</nav>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
import Helper from '@ember/component/helper';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { runInDebug } from '@ember/debug';
|
||||||
|
import MultiMap from 'mnemonist/multi-map';
|
||||||
|
|
||||||
|
// keep a record or attrs
|
||||||
|
const attrs = new Map();
|
||||||
|
|
||||||
|
// keep a record of hashes privately
|
||||||
|
const wm = new WeakMap();
|
||||||
|
|
||||||
|
export default class DocumentAttrsHelper extends Helper {
|
||||||
|
@service('-document') document;
|
||||||
|
|
||||||
|
compute(params, hash) {
|
||||||
|
this.synchronize(this.document.documentElement, hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
willDestroy() {
|
||||||
|
this.synchronize(this.document.documentElement);
|
||||||
|
wm.delete(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronize(root, hash) {
|
||||||
|
const prev = wm.get(this);
|
||||||
|
if (prev) {
|
||||||
|
// if this helper was already setting a property then remove them from
|
||||||
|
// our book keeping
|
||||||
|
Object.entries(prev).forEach(([key, value]) => {
|
||||||
|
let map = attrs.get(key);
|
||||||
|
|
||||||
|
if (typeof map !== 'undefined') {
|
||||||
|
[...new Set(value.split(' '))].map(val => map.remove(val, this));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (hash) {
|
||||||
|
// if we are setting more properties add them to our book keeping
|
||||||
|
wm.set(this, hash);
|
||||||
|
[...Object.entries(hash)].forEach(([key, value]) => {
|
||||||
|
let values = attrs.get(key);
|
||||||
|
if (typeof values === 'undefined') {
|
||||||
|
values = new MultiMap(Set);
|
||||||
|
attrs.set(key, values);
|
||||||
|
}
|
||||||
|
[...new Set(value.split(' '))].map(val => {
|
||||||
|
if (values.count(val) === 0) {
|
||||||
|
values.set(val, null);
|
||||||
|
}
|
||||||
|
values.set(val, this);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
[...attrs.entries()].forEach(([attr, values]) => {
|
||||||
|
let type = 'attr';
|
||||||
|
if (attr === 'class') {
|
||||||
|
type = attr;
|
||||||
|
} else if (attr.startsWith('data-')) {
|
||||||
|
type = 'data';
|
||||||
|
}
|
||||||
|
// go through our list of properties and synchronize the DOM
|
||||||
|
// properties with our properties
|
||||||
|
[...values.keys()].forEach(value => {
|
||||||
|
if (values.count(value) === 1) {
|
||||||
|
switch (type) {
|
||||||
|
case 'class':
|
||||||
|
root.classList.remove(value);
|
||||||
|
break;
|
||||||
|
case 'data':
|
||||||
|
default:
|
||||||
|
runInDebug(() => {
|
||||||
|
throw new Error(`${type} is not implemented yet`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
values.delete(value);
|
||||||
|
// remove the property if it has no values
|
||||||
|
if (values.size === 0) {
|
||||||
|
attrs.delete(attr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (type) {
|
||||||
|
case 'class':
|
||||||
|
root.classList.add(value);
|
||||||
|
break;
|
||||||
|
case 'data':
|
||||||
|
default:
|
||||||
|
runInDebug(() => {
|
||||||
|
throw new Error(`${type} is not implemented yet`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
}
|
|
@ -101,22 +101,6 @@ export function initialize(container) {
|
||||||
}
|
}
|
||||||
register(container, route, item);
|
register(container, route, item);
|
||||||
});
|
});
|
||||||
|
|
||||||
// tell the view we have nspaces enabled
|
|
||||||
container
|
|
||||||
.lookup('service:dom')
|
|
||||||
.root()
|
|
||||||
.classList.add('has-nspaces');
|
|
||||||
}
|
|
||||||
// TODO: This needs to live in its own initializer, either:
|
|
||||||
// 1. Make it be about adding classes to the root dom node
|
|
||||||
// 2. Make it be about config and things to do on initialization re: config
|
|
||||||
// If we go with 1 then we need to move both this and the above nspaces class
|
|
||||||
if (env('CONSUL_ACLS_ENABLED')) {
|
|
||||||
container
|
|
||||||
.lookup('service:dom')
|
|
||||||
.root()
|
|
||||||
.classList.add('has-acls');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,6 @@
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
/* units */
|
/* units */
|
||||||
%app-view {
|
|
||||||
margin-top: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
%app-view-title {
|
%app-view-title {
|
||||||
padding-bottom: 0.2em;
|
padding-bottom: 0.2em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
@import 'layouts/index';
|
@import 'layouts/index';
|
||||||
|
|
||||||
|
.app-view {
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
@media #{$--lt-spacious-page-header} {
|
||||||
|
html:not(.with-breadcrumbs) .app-view {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* all forms have a margin below the header */
|
/* all forms have a margin below the header */
|
||||||
html[data-route$='create'] .app-view > header + div > *:first-child,
|
html[data-route$='create'] .app-view > header + div > *:first-child,
|
||||||
html[data-route$='edit'] .app-view > header + div > *:first-child {
|
html[data-route$='edit'] .app-view > header + div > *:first-child {
|
||||||
|
@ -66,11 +75,6 @@ html[data-route$='edit'] main {
|
||||||
@extend %content-container-restricted;
|
@extend %content-container-restricted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media #{$--lt-spacious-page-header} {
|
|
||||||
html[data-route$='.index']:not([data-route^='dc.kv']) .app-view {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media #{$--lt-spacious-page-header} {
|
@media #{$--lt-spacious-page-header} {
|
||||||
.actions button.copy-btn {
|
.actions button.copy-btn {
|
||||||
margin-top: -56px;
|
margin-top: -56px;
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
<HeadLayout />
|
<HeadLayout />
|
||||||
{{page-title 'Consul' separator=' - '}}
|
{{page-title 'Consul' separator=' - '}}
|
||||||
|
{{#if (env 'CONSUL_ACLS_ENABLED')}}
|
||||||
|
{{document-attrs class="has-acls"}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
|
||||||
|
{{document-attrs class="has-nspaces"}}
|
||||||
|
{{/if}}
|
||||||
{{#if (not-eq router.currentRouteName 'application')}}
|
{{#if (not-eq router.currentRouteName 'application')}}
|
||||||
<HashicorpConsul
|
<HashicorpConsul
|
||||||
id="wrapper"
|
id="wrapper"
|
||||||
|
|
|
@ -4,16 +4,16 @@
|
||||||
) as |filters|}}
|
) as |filters|}}
|
||||||
{{#let (or sortBy "Kind:asc") as |sort|}}
|
{{#let (or sortBy "Kind:asc") as |sort|}}
|
||||||
<AppView>
|
<AppView>
|
||||||
|
{{#if (not-eq parent.Key '/') }}
|
||||||
<BlockSlot @name="breadcrumbs">
|
<BlockSlot @name="breadcrumbs">
|
||||||
<ol>
|
<ol>
|
||||||
{{#if (not-eq parent.Key '/') }}
|
|
||||||
<li><a href={{href-to 'dc.kv'}}>Key / Values</a></li>
|
<li><a href={{href-to 'dc.kv'}}>Key / Values</a></li>
|
||||||
{{/if}}
|
|
||||||
{{#each (slice 0 -2 (split parent.Key '/')) as |breadcrumb index|}}
|
{{#each (slice 0 -2 (split parent.Key '/')) as |breadcrumb index|}}
|
||||||
<li><a href={{href-to 'dc.kv.folder' (join '/' (append (slice 0 (add index 1) (split parent.Key '/')) ''))}}>{{breadcrumb}}</a></li>
|
<li><a href={{href-to 'dc.kv.folder' (join '/' (append (slice 0 (add index 1) (split parent.Key '/')) ''))}}>{{breadcrumb}}</a></li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ol>
|
</ol>
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
|
{{/if}}
|
||||||
<BlockSlot @name="header">
|
<BlockSlot @name="header">
|
||||||
<h1>
|
<h1>
|
||||||
{{#if (eq parent.Key '/')}}
|
{{#if (eq parent.Key '/')}}
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { module, test } from 'qunit';
|
||||||
|
import Helper from 'consul-ui/helpers/document-attrs';
|
||||||
|
|
||||||
|
const root = {
|
||||||
|
classList: {
|
||||||
|
add: () => {},
|
||||||
|
remove: () => {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
module('Unit | Helper | document-attrs', function() {
|
||||||
|
test('synchronize adds and removes values correctly', function(assert) {
|
||||||
|
let attrs, actual;
|
||||||
|
// add first helper
|
||||||
|
const a = new Helper();
|
||||||
|
attrs = a.synchronize(root, {
|
||||||
|
class: 'a b a a a a',
|
||||||
|
});
|
||||||
|
actual = [...attrs.get('class').keys()];
|
||||||
|
assert.deepEqual(actual, ['a', 'b'], 'keys are adding correctly');
|
||||||
|
const b = new Helper();
|
||||||
|
// add second helper
|
||||||
|
attrs = b.synchronize(root, {
|
||||||
|
class: 'z a a a a',
|
||||||
|
});
|
||||||
|
actual = [...attrs.get('class').keys()];
|
||||||
|
assert.deepEqual(actual, ['a', 'b', 'z'], 'more keys are added correctly');
|
||||||
|
// remove second helper
|
||||||
|
b.synchronize(root);
|
||||||
|
actual = [...attrs.get('class').keys()];
|
||||||
|
assert.deepEqual(actual, ['a', 'b'], 'keys are removed, leaving keys that need to remain');
|
||||||
|
// remove first helper
|
||||||
|
a.synchronize(root);
|
||||||
|
assert.ok(
|
||||||
|
typeof attrs.get('class') === 'undefined',
|
||||||
|
'property is completely removed once its empty'
|
||||||
|
);
|
||||||
|
assert.throws(() => {
|
||||||
|
a.synchronize(root, { data: 'a' });
|
||||||
|
}, `throws an error if the attrs isn't class`);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue