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:
John Cowen 2020-12-09 09:22:01 +00:00 committed by GitHub
parent babd22c905
commit e0a4646768
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 158 additions and 28 deletions

View File

@ -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">

View File

@ -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;
}
}

View File

@ -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');
} }
} }

View File

@ -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;
} }

View File

@ -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;

View File

@ -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"

View File

@ -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 '/')}}

View File

@ -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`);
});
});