@fleetbase/ember-ui 0.3.13 → 0.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- <div class="fleetbase-power-select {{@wrapperClass}}">
1
+ <div class="fleetbase-power-select {{@wrapperClass}}" {{did-update this.handleChange @value}}>
2
2
  <PowerSelect
3
- @renderInPlace={{true}}
3
+ @renderInPlace={{this.renderInPlace}}
4
4
  @searchEnabled={{true}}
5
5
  @selected={{this.selected}}
6
6
  @searchField="name"
@@ -17,5 +17,4 @@
17
17
  <span class="mr-1">{{country.emoji}}</span>
18
18
  <span>{{country.name}}</span>
19
19
  </PowerSelect>
20
- <Input @type="hidden" class="hidden" @value={{@value}} aria-label="Country Value" data-county={{@value}} {{did-insert this.listenForInputChanges}} />
21
20
  </div>
@@ -4,7 +4,6 @@ import { tracked } from '@glimmer/tracking';
4
4
  import { action } from '@ember/object';
5
5
  import { guidFor } from '@ember/object/internals';
6
6
  import { task } from 'ember-concurrency';
7
- import { later } from '@ember/runloop';
8
7
 
9
8
  export default class CountrySelectComponent extends Component {
10
9
  @service fetch;
@@ -14,6 +13,10 @@ export default class CountrySelectComponent extends Component {
14
13
  @tracked value;
15
14
  @tracked id = guidFor(this);
16
15
 
16
+ get renderInPlace() {
17
+ return this.args.renderInPlace ?? true;
18
+ }
19
+
17
20
  constructor(owner, { value = null, disabled = false }) {
18
21
  super(...arguments);
19
22
  this.disabled = disabled;
@@ -23,10 +26,12 @@ export default class CountrySelectComponent extends Component {
23
26
 
24
27
  @task *fetchCountries(value = null) {
25
28
  try {
26
- this.countries = yield this.fetch.get('lookup/countries', { columns: ['name', 'cca2', 'flag', 'emoji'] });
27
- if (value) {
28
- this.selected = this.findCountry(value);
29
- }
29
+ this.countries = yield this.fetch.get(
30
+ 'lookup/countries',
31
+ { columns: ['name', 'cca2', 'flag', 'emoji'] },
32
+ { fromCache: true, expirationInterval: 1, expirationIntervalUnit: 'week' }
33
+ );
34
+ this.selected = this.findCountry(value);
30
35
  } catch (error) {
31
36
  this.countries = [];
32
37
  }
@@ -40,15 +45,8 @@ export default class CountrySelectComponent extends Component {
40
45
  }
41
46
  }
42
47
 
43
- @action listenForInputChanges(element) {
44
- later(() => {
45
- const { value } = element;
46
-
47
- if (this.value !== value) {
48
- this.value = value;
49
- this.changed(value);
50
- }
51
- }, 100);
48
+ @action handleChange(el, [value]) {
49
+ this.selected = this.findCountry(value);
52
50
  }
53
51
 
54
52
  @action selectCountry(country) {
@@ -8,6 +8,7 @@
8
8
  @fullHeight={{true}}
9
9
  @isResizable={{this.isResizable}}
10
10
  @width={{this.width}}
11
+ ...attributes
11
12
  >
12
13
  {{#if @headerComponent}}
13
14
  {{component
@@ -16,10 +16,10 @@
16
16
  </dd.Trigger>
17
17
  <dd.Content class="{{@contentClass}} locale-selector-tray-content {{if (media 'isMobile') 'is-mobile'}}">
18
18
  <div class="next-dd-menu {{@dropdownMenuClass}} {{if dd.isOpen 'is-open'}}">
19
- {{#if this.loadAvailableCountries.isRunning}}
19
+ {{#if this.language.loadAvailableCountries.isRunning}}
20
20
  <Spinner />
21
21
  {{else}}
22
- {{#each-in this.availableLocales as |key country|}}
22
+ {{#each-in this.language.availableLocales as |key country|}}
23
23
  <div class="px-1">
24
24
  <a href="javascript:;" class="next-dd-item" {{on "click" (fn this.changeLocale key)}}>
25
25
  <div class="flex flex-row items-center justify-between w-full">
@@ -2,7 +2,6 @@ import Component from '@glimmer/component';
2
2
  import { tracked } from '@glimmer/tracking';
3
3
  import { inject as service } from '@ember/service';
4
4
  import { action } from '@ember/object';
5
- import { debug } from '@ember/debug';
6
5
  import { task } from 'ember-concurrency';
7
6
  import calculatePosition from 'ember-basic-dropdown/utils/calculate-position';
8
7
 
@@ -10,26 +9,8 @@ export default class LocaleSelectorTrayComponent extends Component {
10
9
  @service intl;
11
10
  @service fetch;
12
11
  @service media;
13
-
14
- /**
15
- * Tracks all the available locales.
16
- *
17
- * @memberof LocaleSelectorComponent
18
- */
12
+ @service language;
19
13
  @tracked locales = [];
20
-
21
- /**
22
- * All available countries data.
23
- *
24
- * @memberof LocaleSelectorComponent
25
- */
26
- @tracked countries = [];
27
-
28
- /**
29
- * The current locale in use.
30
- *
31
- * @memberof LocaleSelectorComponent
32
- */
33
14
  @tracked currentLocale;
34
15
 
35
16
  /**
@@ -41,7 +22,6 @@ export default class LocaleSelectorTrayComponent extends Component {
41
22
 
42
23
  this.locales = this.intl.locales;
43
24
  this.currentLocale = this.intl.primaryLocale;
44
- this.loadAvailableCountries.perform();
45
25
 
46
26
  // Check for locale change
47
27
  this.intl.onLocaleChanged(() => {
@@ -85,28 +65,6 @@ export default class LocaleSelectorTrayComponent extends Component {
85
65
  this.saveUserLocale.perform(selectedLocale);
86
66
  }
87
67
 
88
- /**
89
- * Loads available countries asynchronously.
90
- * @returns {void}
91
- * @memberof LocaleSelectorComponent
92
- * @method loadAvailableCountries
93
- * @instance
94
- * @task
95
- * @generator
96
- */
97
- @task *loadAvailableCountries() {
98
- try {
99
- this.countries = yield this.fetch.get(
100
- 'lookup/countries',
101
- { columns: ['name', 'cca2', 'flag', 'emoji', 'languages'] },
102
- { fromCache: true, expirationInterval: 1, expirationIntervalUnit: 'week' }
103
- );
104
- this.availableLocales = this._createAvailableLocaleMap();
105
- } catch (error) {
106
- debug(`Locale Error: ${error.message}`);
107
- }
108
- }
109
-
110
68
  /**
111
69
  * Saves the user's selected locale to the server.
112
70
  * @param {string} locale - The user's selected locale.
@@ -120,45 +78,4 @@ export default class LocaleSelectorTrayComponent extends Component {
120
78
  @task *saveUserLocale(locale) {
121
79
  yield this.fetch.post('users/locale', { locale });
122
80
  }
123
-
124
- /**
125
- * Creates a map of available locales.
126
- * @private
127
- * @returns {Object} - The map of available locales.
128
- * @memberof LocaleSelectorComponent
129
- * @method _createAvailableLocaleMap
130
- * @instance
131
- */
132
- _createAvailableLocaleMap() {
133
- const localeMap = {};
134
-
135
- for (let i = 0; i < this.locales.length; i++) {
136
- const locale = this.locales.objectAt(i);
137
-
138
- localeMap[locale] = this._findCountryDataForLocale(locale);
139
- }
140
-
141
- return localeMap;
142
- }
143
-
144
- /**
145
- * Finds country data for a given locale.
146
- * @private
147
- * @param {string} locale - The locale to find country data for.
148
- * @returns {Object|null} - The country data or null if not found.
149
- * @memberof LocaleSelectorComponent
150
- * @method _findCountryDataForLocale
151
- * @instance
152
- */
153
- _findCountryDataForLocale(locale) {
154
- const localeCountry = locale.split('-')[1];
155
- const country = this.countries.find((country) => country.cca2.toLowerCase() === localeCountry);
156
-
157
- if (country) {
158
- // get the language
159
- country.language = Object.values(country.languages)[0];
160
- }
161
-
162
- return country;
163
- }
164
81
  }
@@ -28,6 +28,7 @@ export default class OverlayComponent extends Component {
28
28
  isOpen: this.isOpen,
29
29
  isMinimized: this.isMinimized,
30
30
  isMaximized: this.isMaximized,
31
+ overlayNode: this.overlayNode,
31
32
  };
32
33
 
33
34
  @action setupComponent(element) {
@@ -26,23 +26,25 @@
26
26
  {{#if (has-block)}}
27
27
  {{yield @resource}}
28
28
  {{else}}
29
- <div class="text-sm {{@titleClass}}">{{n-a @title this.resourceName}}</div>
29
+ <div class="text-sm {{@titleClass}}">{{or @title this.resourceName @titleFallback "-"}}</div>
30
30
  {{#if @subtitle}}
31
31
  <div class="text-xs text-gray-400 dark:text-gray-500 {{@subtitleClass}}">{{n-a @subtitle}}</div>
32
32
  {{/if}}
33
33
  {{yield @resource}}
34
34
  {{/if}}
35
35
  </div>
36
- {{#if (has-block "tooltip")}}
37
- <Attach::Tooltip @class="clean" @animation="scale" @placement={{or @tooltipPosition "top"}}>
38
- <InputInfo>
39
- {{yield @resource to="tooltip"}}
40
- </InputInfo>
41
- </Attach::Tooltip>
42
- {{else if @tooltipComponent}}
43
- <Attach::Tooltip @class="clean" @animation="scale" @placement={{or @tooltipPosition "top"}}>
44
- {{component @tooltipComponent}}
45
- </Attach::Tooltip>
46
- {{/if}}
36
+ {{#unless @noTooltip}}
37
+ {{#if (has-block "tooltip")}}
38
+ <Attach::Tooltip @class="clean" @animation="scale" @placement={{or @tooltipPosition "top"}}>
39
+ <InputInfo>
40
+ {{yield @resource to="tooltip"}}
41
+ </InputInfo>
42
+ </Attach::Tooltip>
43
+ {{else if @tooltipComponent}}
44
+ <Attach::Tooltip @class="clean" @animation="scale" @placement={{or @tooltipPosition "top"}}>
45
+ {{component @tooltipComponent}}
46
+ </Attach::Tooltip>
47
+ {{/if}}
48
+ {{/unless}}
47
49
  </a>
48
50
  </div>
@@ -6,7 +6,7 @@ export default class PillComponent extends Component {
6
6
  /* eslint-disable ember/no-get */
7
7
  get resourceName() {
8
8
  const record = this.args.resource;
9
- if (!record) return 'resource';
9
+ if (!record) return null;
10
10
 
11
11
  return (
12
12
  get(record, this.args.namePath ?? 'name') ??
@@ -2,6 +2,7 @@ import { getOwner } from '@ember/application';
2
2
  import { DEBUG } from '@glimmer/env';
3
3
  import { warn } from '@ember/debug';
4
4
  import { schedule } from '@ember/runloop';
5
+ import { isArray } from '@ember/array';
5
6
  import { all } from 'rsvp';
6
7
  import requirejs from 'require';
7
8
 
@@ -166,3 +167,92 @@ export function waitForInsertedAndSized(getElOrEl, { timeoutMs = 4000 } = {}) {
166
167
  }
167
168
  });
168
169
  }
170
+
171
+ /**
172
+ * Create a DOM element with declarative options.
173
+ *
174
+ * @param {string} tag
175
+ * @param {Object} [options]
176
+ * @param {string|string[]|Node|Node[]} [children]
177
+ * @returns {HTMLElement}
178
+ */
179
+ export function createElement(tag, options = {}, children = null) {
180
+ const el = document.createElement(tag);
181
+
182
+ // ---------- Classes ----------
183
+ if (options.classNames) {
184
+ const classes = isArray(options.classNames) ? options.classNames : options.classNames.split(' ');
185
+ el.classList.add(...classes.filter(Boolean));
186
+ }
187
+
188
+ // ---------- Styles ----------
189
+ if (options.styles && typeof options.styles === 'object') {
190
+ Object.assign(el.style, options.styles);
191
+ }
192
+
193
+ // ---------- Attributes ----------
194
+ if (options.attrs && typeof options.attrs === 'object') {
195
+ for (const [key, value] of Object.entries(options.attrs)) {
196
+ if (value !== false && value != null) {
197
+ el.setAttribute(key, value === true ? '' : value);
198
+ }
199
+ }
200
+ }
201
+
202
+ // ---------- Dataset ----------
203
+ if (options.dataset && typeof options.dataset === 'object') {
204
+ for (const [key, value] of Object.entries(options.dataset)) {
205
+ el.dataset[key] = value;
206
+ }
207
+ }
208
+
209
+ // ---------- Event listeners ----------
210
+ if (options.on && typeof options.on === 'object') {
211
+ for (const [event, handler] of Object.entries(options.on)) {
212
+ if (typeof handler === 'function') {
213
+ el.addEventListener(event, handler);
214
+ }
215
+ }
216
+ }
217
+
218
+ // ---------- Text / HTML (exclusive) ----------
219
+ const hasText = options.text != null || options.innerText != null;
220
+ const hasHtml = options.html != null || options.innerHTML != null;
221
+
222
+ if (hasText && hasHtml) {
223
+ throw new Error('createElement: use either text OR html, not both.');
224
+ }
225
+
226
+ if (hasText) {
227
+ el.textContent = options.text ?? options.innerText;
228
+ } else if (hasHtml) {
229
+ el.innerHTML = options.html ?? options.innerHTML;
230
+ } else {
231
+ // ---------- Children ----------
232
+ const append = (child) => {
233
+ if (child == null) return;
234
+ if (Array.isArray(child)) return child.forEach(append);
235
+ if (child instanceof Node) el.appendChild(child);
236
+ else el.appendChild(document.createTextNode(String(child)));
237
+ };
238
+
239
+ append(children);
240
+ }
241
+
242
+ // ---------- Mount ----------
243
+ if (options.mount) {
244
+ let mountTarget = options.mount;
245
+
246
+ if (typeof mountTarget === 'string') {
247
+ mountTarget = document.querySelector(mountTarget);
248
+ }
249
+
250
+ if (mountTarget instanceof Element) {
251
+ mountTarget.appendChild(el);
252
+ } else {
253
+ console.warn('createElement: mount target not found', options.mount);
254
+ }
255
+ }
256
+
257
+ return el;
258
+ }
@@ -0,0 +1,81 @@
1
+ import { computePosition, offset, flip, shift } from '@floating-ui/dom';
2
+ import { isArray } from '@ember/array';
3
+ import { createElement } from './dom';
4
+
5
+ export class Tooltip {
6
+ mountEl;
7
+ tooltipEl;
8
+ options;
9
+ cleanupFns = [];
10
+
11
+ constructor(mountEl, options = {}) {
12
+ this.mountEl = mountEl;
13
+ this.options = options;
14
+ this.#setup();
15
+ }
16
+
17
+ #setup() {
18
+ const { text, classNames = [], placement = 'top', offset: offsetValue = 5 } = this.options;
19
+
20
+ const classes = isArray(classNames) ? classNames : String(classNames).split(' ');
21
+
22
+ this.tooltipEl = createElement('div', {
23
+ classNames: ['ui-input-info', 'text-xs', ...classes],
24
+ text,
25
+ attrs: {
26
+ role: 'tooltip',
27
+ },
28
+ styles: {
29
+ position: 'absolute',
30
+ width: 'max-content',
31
+ zIndex: 777,
32
+ opacity: 0,
33
+ pointerEvents: 'none',
34
+ transition: 'opacity 0.15s ease',
35
+ },
36
+ mount: document.body,
37
+ });
38
+
39
+ const show = async () => {
40
+ const { x, y } = await computePosition(this.mountEl, this.tooltipEl, {
41
+ placement,
42
+ middleware: [offset(offsetValue), flip(), shift({ padding: 8 })],
43
+ });
44
+
45
+ Object.assign(this.tooltipEl.style, {
46
+ left: `${x}px`,
47
+ top: `${y}px`,
48
+ opacity: 1,
49
+ });
50
+ };
51
+
52
+ const hide = () => {
53
+ this.tooltipEl.style.opacity = 0;
54
+ };
55
+
56
+ this.mountEl.addEventListener('mouseenter', show);
57
+ this.mountEl.addEventListener('mouseleave', hide);
58
+ this.mountEl.addEventListener('focus', show);
59
+ this.mountEl.addEventListener('blur', hide);
60
+
61
+ // cleanup tracking
62
+ this.cleanupFns.push(() => {
63
+ this.mountEl.removeEventListener('mouseenter', show);
64
+ this.mountEl.removeEventListener('mouseleave', hide);
65
+ this.mountEl.removeEventListener('focus', show);
66
+ this.mountEl.removeEventListener('blur', hide);
67
+ this.tooltipEl.remove();
68
+ });
69
+ }
70
+
71
+ destroy() {
72
+ this.cleanupFns.forEach((fn) => fn());
73
+ this.cleanupFns = [];
74
+ }
75
+ }
76
+
77
+ export default {
78
+ createTooltip() {
79
+ return new Tooltip(...arguments);
80
+ },
81
+ };
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/ember-ui/utils/floating';
package/index.js CHANGED
@@ -75,11 +75,23 @@ module.exports = {
75
75
  included: function (app) {
76
76
  this._super.included.apply(this, arguments);
77
77
 
78
+ // Check if we're being included by an engine
79
+ // If so, skip setting postcssOptions to prevent engines from trying to compile our styles
80
+ let parent = this.parent;
81
+ while (parent) {
82
+ const isEngine = parent.lazyLoading === true || (parent.lazyLoading && parent.lazyLoading.enabled === true);
83
+ if (isEngine) {
84
+ // We're in an engine - don't set postcssOptions
85
+ // The engine will inherit from the host app
86
+ return;
87
+ }
88
+ parent = parent.parent;
89
+ }
90
+
78
91
  // Get Application Host (skips engines, finds root app)
79
92
  app = this.findApplicationHost(app);
80
93
 
81
94
  // PostCSS Options - only applied to the root application
82
- // Engines are excluded by findApplicationHost, so they won't get these options
83
95
  app.options = app.options || {};
84
96
  app.options.postcssOptions = postcssOptions;
85
97
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/ember-ui",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "description": "Fleetbase UI provides all the interface components, helpers, services and utilities for building a Fleetbase extension into the Console.",
5
5
  "keywords": [
6
6
  "fleetbase-ui",