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>
|
||||
{{#if authorized}}
|
||||
<nav aria-label="Breadcrumb">
|
||||
<YieldSlot @name="breadcrumbs">{{yield}}</YieldSlot>
|
||||
<YieldSlot @name="breadcrumbs">
|
||||
{{document-attrs class="with-breadcrumbs"}}
|
||||
{{yield}}
|
||||
</YieldSlot>
|
||||
</nav>
|
||||
{{/if}}
|
||||
<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);
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
/* units */
|
||||
%app-view {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
%app-view-title {
|
||||
padding-bottom: 0.2em;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
@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 */
|
||||
html[data-route$='create'] .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;
|
||||
}
|
||||
|
||||
@media #{$--lt-spacious-page-header} {
|
||||
html[data-route$='.index']:not([data-route^='dc.kv']) .app-view {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
@media #{$--lt-spacious-page-header} {
|
||||
.actions button.copy-btn {
|
||||
margin-top: -56px;
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
<HeadLayout />
|
||||
{{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')}}
|
||||
<HashicorpConsul
|
||||
id="wrapper"
|
||||
|
|
|
@ -4,16 +4,16 @@
|
|||
) as |filters|}}
|
||||
{{#let (or sortBy "Kind:asc") as |sort|}}
|
||||
<AppView>
|
||||
{{#if (not-eq parent.Key '/') }}
|
||||
<BlockSlot @name="breadcrumbs">
|
||||
<ol>
|
||||
{{#if (not-eq parent.Key '/') }}
|
||||
<li><a href={{href-to 'dc.kv'}}>Key / Values</a></li>
|
||||
{{/if}}
|
||||
{{#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>
|
||||
{{/each}}
|
||||
</ol>
|
||||
</BlockSlot>
|
||||
{{/if}}
|
||||
<BlockSlot @name="header">
|
||||
<h1>
|
||||
{{#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