u2ui 1.5.13 → 1.5.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.
@@ -8,7 +8,7 @@ import {editingHost, isPlainTextHost} from '../selection/ownership/ownership.js'
8
8
  import {Point} from '../selection/point/point.js';
9
9
  import {SelectionSnapshot} from '../selection/snapshot.js';
10
10
  import {defaultUnstyle} from '../unstyle/unstyle.js';
11
- import {sanitizePolicy} from '../sanitize/policy.js';
11
+ import {policyFor, sanitizePolicy} from '../sanitize/policy.js';
12
12
 
13
13
  const TRIGGERS = new Set(['input', 'paste', 'drop', 'command']);
14
14
  const PASTE = new Set(['insertFromPaste', 'insertFromPasteAsQuotation']);
@@ -109,11 +109,13 @@ export class InputPipeline {
109
109
  // A native paste is the one import the browser inserts itself, so
110
110
  // this is where the policy reaches it: elements first, because
111
111
  // unwrapping moves nodes, then attributes.
112
- if (this.#sanitize && settings.importSanitize === 'policy' && imported?.roots.length) {
112
+ // The host may declare its own attributes and protocols; everything else stays as configured.
113
+ const sanitize = this.#sanitize && policyFor(settings, this.#sanitize);
114
+ if (sanitize && settings.importSanitize === 'policy' && imported?.roots.length) {
113
115
  // Attributes first: removing them moves nothing, while narrowing
114
116
  // dissolves wrappers and would leave nothing left to clean.
115
117
  for (const root of imported.roots) {
116
- this.#sanitize.clean(root, {
118
+ sanitize.clean(root, {
117
119
  preserve: imported.preserve,
118
120
  base: this.#root.ownerDocument.baseURI,
119
121
  classes: settings.classes.length ? settings.classes : null,
@@ -121,7 +123,7 @@ export class InputPipeline {
121
123
  }
122
124
  const map = new PointMap(points);
123
125
  for (const root of imported.roots) {
124
- this.#sanitize.narrow(root, {
126
+ sanitize.narrow(root, {
125
127
  map,
126
128
  preserve: imported.preserve,
127
129
  elements: importable(settings),
@@ -8,17 +8,21 @@ repairs editable document shape.
8
8
 
9
9
  ## Policy
10
10
 
11
- `SanitizePolicy` is an immutable allowlist. Its options are:
11
+ `SanitizePolicy` is an immutable allowlist. It speaks the vocabulary of the web
12
+ Sanitizer API, so one set of terms carries through browser, editor, and whatever
13
+ an application filters with; where the API has no answer, the policy extends it
14
+ rather than renaming what it has. Its options are:
12
15
 
13
16
  - `elements`: element names the security policy can accept;
14
17
  - `attributes`: global (`*`) and per-element attribute names;
15
- - `protocols`: per-element rules for URL attributes;
16
- - `drop`: element names that `narrow()` removes with their content, before it
18
+ - `protocols`: per-element rules for URL attributes — the one extension, because
19
+ `clean()` also runs on nodes a browser inserted, where no sink checked them;
20
+ - `removeElements`: element names that `narrow()` removes with their content, before it
17
21
  asks `skip` — no later stage may be left holding them;
18
22
  - `comments` and `dataAttributes`: explicit booleans, both false by default.
19
23
 
20
24
  An element outside `elements` keeps its content — dropping it would lose text the
21
- author wrote. `drop` names the exception: elements a browser never renders the
25
+ author wrote. `removeElements` names the exception: elements a browser never renders the
22
26
  children of, where unwrapping would turn a stylesheet, a script, or document
23
27
  metadata into visible text. It defaults to `base head link meta noscript script
24
28
  style template title` and, like every default, an application may replace it.
@@ -27,11 +31,23 @@ The supplied `sanitizePolicy` accepts the `document` element preset, ordinary
27
31
  editor metadata, and conservative web URLs. An image may carry a `data:` URL,
28
32
  because an image executes nothing — not even an SVG one, which browsers draw in
29
33
  a script-free context; in a navigating attribute the same protocol is a page, so
30
- links and citations keep it out. Inline styles, event attributes, comments, and
31
- data attributes are not enabled by default.
34
+ links and citations keep it out. There is no allowlist of css properties, for the
35
+ same reason the Sanitizer API has none: `style` is an attribute like any other,
36
+ and taking presentation away is Unstyle's job.
37
+ Event attributes, comments, and data attributes
38
+ are not enabled by default. Inline `style` is: it executes nothing, and where
39
+ foreign presentation is unwanted the Unstyle policy already removes it from what
40
+ an import brought — a policy that must not carry it at all narrows `attributes`.
32
41
  Relative URLs use the explicit `relative` protocol token. Known URL attributes
33
42
  without a matching protocol rule are removed rather than accepted implicitly.
34
43
 
44
+ `policy.with(options)` copies a policy with single axes replaced, and
45
+ `policyFor(config, base)` is what a host gets: its own `--u2-rte-elements`,
46
+ `--u2-rte-attributes` and `--u2-rte-protocols` over the policy the application
47
+ chose — each axis declared is that axis, not a narrowing of it — with equal
48
+ declarations sharing one policy instead of building it again. Nothing declared
49
+ returns the base itself.
50
+
35
51
  ```js
36
52
  const policy = new SanitizePolicy({
37
53
  elements: ['p', 'a', 'strong'],
@@ -72,6 +88,8 @@ rich paste/drop events and direct HTML imports.
72
88
  - Attribute and URL policy remains separate from structural normalization.
73
89
  - Sanitizing creates no editor state, listeners, or markers.
74
90
  - Policies do not mutate after construction.
91
+ - A configuration never carries `elements` and `removeElements` together: the api
92
+ rejects that pair, and `setHTML()` then yields nothing.
75
93
 
76
94
  ## Where the policy is applied
77
95
 
@@ -83,8 +101,8 @@ Three paths bring external HTML in, and all three now meet the same policy:
83
101
  there and no fallback parser is needed, but the result still has to obey the
84
102
  same attribute policy — so the input pipeline applies `clean()` to the nodes
85
103
  that arrived. Until it did, the most common import was the only one without an
86
- allowlist, and layout ids, tracking `data-` attributes and inline styles came
87
- straight through.
104
+ allowlist, and layout ids and tracking `data-` attributes came straight through.
105
+ Inline styles are not that kind of problem: Unstyle takes them off an import.
88
106
 
89
107
  `clean(root, {preserve})` spares elements that were already there, so a paste
90
108
  never re-cleans the document it landed in.
@@ -39,5 +39,7 @@ function config(policy, elements) {
39
39
  dataAttributes: policy.dataAttributes,
40
40
  };
41
41
  if (replaced.length) result.replaceWithChildrenElements = replaced;
42
+ // No `removeElements` beside an `elements` allowlist: the api rejects a config carrying both, and
43
+ // an allowlist already excludes them — what they must not leave behind is `narrow()`'s job.
42
44
  return result;
43
45
  }
@@ -4,8 +4,11 @@ const NAME = /^[a-z][a-z\d-]*$/;
4
4
  const ATTRIBUTE = /^[a-z][a-z\d_.:-]*$/;
5
5
  const PROTOCOL = /^[a-z][a-z\d+.-]*$/;
6
6
  const URL_ATTRIBUTES = new Set(['action', 'cite', 'formaction', 'href', 'poster', 'src', 'xlink:href']);
7
+ // `style` is presentation, not execution: inline css runs no script, and where foreign styling is
8
+ // unwanted the Unstyle policy already removes it from what an import brought — a question of taste
9
+ // with a stage of its own. An application that must not carry it at all narrows this policy.
7
10
  const DEFAULT_ATTRIBUTES = Object.freeze({
8
- '*': Object.freeze(['class', 'dir', 'lang', 'title']),
11
+ '*': Object.freeze(['class', 'dir', 'lang', 'style', 'title']),
9
12
  a: Object.freeze(['href', 'rel', 'target']),
10
13
  blockquote: Object.freeze(['cite']),
11
14
  img: Object.freeze(['alt', 'height', 'loading', 'src', 'width']),
@@ -30,7 +33,7 @@ const DEFAULT_PROTOCOLS = Object.freeze({
30
33
  // Elements a browser never renders the children of. Unwrapping one turns source — a stylesheet, a
31
34
  // script, document metadata — into visible text, so these go with their content. Every other unknown
32
35
  // element keeps its content: dropping it would lose text the author wrote.
33
- const DEFAULT_DROP = Object.freeze([
36
+ const DEFAULT_REMOVE = Object.freeze([
34
37
  'base', 'head', 'link', 'meta', 'noscript', 'script', 'style', 'template', 'title',
35
38
  ]);
36
39
 
@@ -38,7 +41,7 @@ export const sanitizeDefaults = Object.freeze({
38
41
  elements: elementPresets.document,
39
42
  attributes: DEFAULT_ATTRIBUTES,
40
43
  protocols: DEFAULT_PROTOCOLS,
41
- drop: DEFAULT_DROP,
44
+ removeElements: DEFAULT_REMOVE,
42
45
  comments: false,
43
46
  dataAttributes: false,
44
47
  });
@@ -49,7 +52,7 @@ export class SanitizePolicy {
49
52
 
50
53
  constructor(options = {}) {
51
54
  this.elements = names(options.elements ?? sanitizeDefaults.elements, NAME, 'element');
52
- this.drop = names(options.drop ?? sanitizeDefaults.drop, NAME, 'element');
55
+ this.removeElements = names(options.removeElements ?? sanitizeDefaults.removeElements, NAME, 'element');
53
56
  this.#attributes = groups(options.attributes ?? sanitizeDefaults.attributes, ATTRIBUTE, 'attribute');
54
57
  this.#protocols = protocolGroups(options.protocols ?? sanitizeDefaults.protocols);
55
58
  this.attributeNames = Object.freeze(unique(Object.values(this.#attributes).flat()));
@@ -58,6 +61,14 @@ export class SanitizePolicy {
58
61
  Object.freeze(this);
59
62
  }
60
63
 
64
+ /** A copy with some axes replaced: what a host declares, on top of what the application chose. */
65
+ with(options) {
66
+ return new SanitizePolicy({
67
+ elements: this.elements, removeElements: this.removeElements, attributes: this.#attributes,
68
+ protocols: this.#protocols, comments: this.comments, dataAttributes: this.dataAttributes, ...options,
69
+ });
70
+ }
71
+
61
72
  allowsAttribute(element, attribute) {
62
73
  const name = element.localName.toLowerCase();
63
74
  const attributeName = attribute.toLowerCase();
@@ -78,15 +89,15 @@ export class SanitizePolicy {
78
89
  }
79
90
 
80
91
  // Reduces a subtree to the elements this policy allows, keeping the content
81
- // of the rest — except the `drop` elements, which go with theirs. Parsed input
92
+ // of the rest — except the `removeElements` ones, which go with theirs. Parsed input
82
93
  // gets this from the safe sink; markup the browser inserted itself has to be
83
94
  // narrowed afterwards.
84
95
  //
85
96
  // `skip` leaves an element to a later stage. Structural repair already
86
97
  // removes what the content model rejects, and it does so knowing that
87
98
  // dissolving a block into inline content needs a line break to survive.
88
- // A `drop` element is not that kind of question — no later stage may keep
89
- // its content — so it goes before `skip` is asked.
99
+ // A removed element is not that kind of question — no later stage may keep its
100
+ // content — so it goes before `skip` is asked.
90
101
  //
91
102
  // `alias` names an equivalent for an element that is not allowed. Dropping
92
103
  // `<b>` would lose the emphasis it carries, so a strict list can still keep
@@ -99,7 +110,7 @@ export class SanitizePolicy {
99
110
  const changed = [];
100
111
  for (const element of descendants(root).reverse()) {
101
112
  if (preserve?.has(element) || !element.parentNode) continue;
102
- if (this.drop.includes(element.localName)) {
113
+ if (this.removeElements.includes(element.localName)) {
103
114
  if (map) map.remove(element);
104
115
  else element.remove();
105
116
  changed.push(element);
@@ -147,6 +158,25 @@ export class SanitizePolicy {
147
158
 
148
159
  export const sanitizePolicy = new SanitizePolicy();
149
160
 
161
+ // The policy a host works with: what it declares is what it may carry, and the rest stays as the
162
+ // application configured it. Declaring is rare and reading is not, so equal declarations share one
163
+ // policy instead of building it again.
164
+ const declared = new WeakMap();
165
+
166
+ export function policyFor({elements = null, attributes = null, protocols = null} = {}, base = sanitizePolicy) {
167
+ if (!elements && !attributes && !protocols) return base;
168
+ const key = JSON.stringify([elements, attributes, protocols]);
169
+ let known = declared.get(base);
170
+ if (!known) declared.set(base, known = new Map());
171
+ let policy = known.get(key);
172
+ if (!policy) {
173
+ known.set(key, policy = base.with({
174
+ ...(elements && {elements}), ...(attributes && {attributes}), ...(protocols && {protocols}),
175
+ }));
176
+ }
177
+ return policy;
178
+ }
179
+
150
180
  function names(values, pattern, label) {
151
181
  if (!values || typeof values[Symbol.iterator] !== 'function' || typeof values === 'string') {
152
182
  throw new TypeError(`${label} names must be an iterable`);
@@ -1,6 +1,7 @@
1
1
  import {htmlModel} from '../model/html/html-model.js';
2
2
  import {narrow} from '../command/edit.js';
3
3
  import {NativeSanitizer} from '../sanitize/native.js';
4
+ import {policyFor} from '../sanitize/policy.js';
4
5
  import {SelectionSnapshot} from '../selection/snapshot.js';
5
6
  import {replaceContent} from '../surface/content.js';
6
7
 
@@ -21,7 +22,9 @@ export class Source {
21
22
  #sanitizer;
22
23
  #indent;
23
24
 
24
- constructor(surface, {sanitizer = new NativeSanitizer(), indent = ' '} = {}) {
25
+ constructor(surface, {sanitizer = null, indent = ' '} = {}) {
26
+ // Without one supplied, the host's own declarations decide what may be written back.
27
+ sanitizer ??= new NativeSanitizer(policyFor(surface?.config));
25
28
  if (surface?.element?.nodeType !== Node.ELEMENT_NODE || typeof surface?.transact !== 'function') {
26
29
  throw new TypeError('A source view requires an editor surface');
27
30
  }
@@ -7,6 +7,7 @@ export class Surface extends EventTarget {
7
7
  #element;
8
8
  #options;
9
9
  #selection = null;
10
+ #settings = null;
10
11
  #transaction = null;
11
12
  #active = false;
12
13
  #connected = true;
@@ -21,7 +22,15 @@ export class Surface extends EventTarget {
21
22
  get core() { return this.#core; }
22
23
  get element() { return this.#element; }
23
24
  get options() { return this.#options; }
24
- get config() { return config(this.#element); }
25
+ // One reading per burst of work: a refresh asks dozens of times, and a computed style is the
26
+ // expensive part of a path every keystroke goes down. The next task reads again, and anything
27
+ // that changes a host property in this one says so with `invalidate()` — as `refresh()` does.
28
+ get config() { return this.#settings ??= config(this.#element); }
29
+
30
+ invalidate() {
31
+ this.#settings = null;
32
+ return this;
33
+ }
25
34
  get selection() { return this.#selection; }
26
35
  get transaction() { return this.#transaction; }
27
36
  get active() { return this.#active; }
@@ -55,6 +55,11 @@ item, boolean and `'mixed'` command states become `aria-pressed`, so toggle
55
55
  buttons need no separate adapter. Action buttons omit `data-state` and never
56
56
  pretend to be toggles merely because their command can report state.
57
57
  `data-control` may give a button a CSS-facing name different from its command,
58
+ A declared `--u2-rte-toolbar` names the controls in the order they are shown; the
59
+ toolbar arranges its markup to match, and anything it does not name keeps the
60
+ order it was built in, after the rest. Nothing moves while the declaration stays
61
+ the same.
62
+
58
63
  just as it does for a value control. This lets `--u2-rte-toolbar: breaks` expose
59
64
  a button whose command is `showBreaks`.
60
65
 
@@ -4,11 +4,29 @@
4
4
  // surface takes the focus first, so what it captures is a selection it owns.
5
5
  //
6
6
  // An element that is no longer there leaves the surface with what it had.
7
+ // The same hand-back where there is no element left: emptying an address takes the link away, and
8
+ // what follows it is the end of the text it used to be.
9
+ export function caretAfterRange(surface, range) {
10
+ surface.element.focus();
11
+ if (!range || !surface.element.contains(range.endContainer)) return surface.restore();
12
+ const selection = surface.core.selection;
13
+ const collapsed = range.cloneRange();
14
+ collapsed.collapse(false);
15
+ selection.removeAllRanges();
16
+ selection.addRange(collapsed);
17
+ return !!surface.capture();
18
+ }
19
+
7
20
  export function caretAfter(surface, element) {
8
21
  surface.element.focus();
9
22
  if (!element?.isConnected) return surface.restore();
10
23
  const range = surface.element.ownerDocument.createRange();
11
- range.setStartAfter(element);
24
+ // The start of what follows, rather than the boundary behind the element: engines resolve a
25
+ // caret at that boundary towards the element it sits behind, and typing would go on inside the
26
+ // link that was just left. Naming the text node instead says the same place without the pull.
27
+ const next = element.nextSibling;
28
+ if (next?.nodeType === Node.TEXT_NODE) range.setStart(next, 0);
29
+ else range.setStartAfter(element);
12
30
  range.collapse(true);
13
31
  const selection = surface.core.selection;
14
32
  selection.removeAllRanges();
@@ -9,6 +9,7 @@ export class Toolbar {
9
9
  #surface = null;
10
10
  #commands = null;
11
11
  #connected = true;
12
+ #ordered = null;
12
13
 
13
14
  constructor(core, element, {commands, place = null} = {}) {
14
15
  if (!core?.root || typeof core?.addEventListener !== 'function') {
@@ -60,6 +61,7 @@ export class Toolbar {
60
61
  refresh() {
61
62
  if (!this.#connected) return false;
62
63
  const surface = this.#surface;
64
+ surface?.invalidate(); // whatever a host declared may have changed since the last one
63
65
  const commands = surface?.connected ? this.#resolve(surface) : null;
64
66
  if (commands != null && !registry(commands)) throw new TypeError('Toolbar commands must resolve to a command registry');
65
67
  this.#commands = commands || null;
@@ -70,6 +72,7 @@ export class Toolbar {
70
72
  const style = commands && getComputedStyle(surface.element);
71
73
  const active = !!commands && surface.config.ui === 'roaming' && visibleForSelection(surface, style);
72
74
  const names = active ? configured(style) : null;
75
+ this.#order(names);
73
76
  // A host may prefer a toolbar that only ever shows what it can do, at
74
77
  // the cost of a shape that moves with the caret.
75
78
  const hiding = active && setting(style, 'toolbar-unavailable') === 'hide';
@@ -78,7 +81,7 @@ export class Toolbar {
78
81
  for (const item of this.#items()) {
79
82
  const name = item.dataset.command.trim();
80
83
  const control = item.dataset.control?.trim() || name;
81
- const offered = active && commands.has(name) && (!names || names.has(control));
84
+ const offered = active && commands.has(name) && (!names || names.includes(control));
82
85
  const disabled = !offered || !commands.enabled(name, detail);
83
86
  item.hidden = !offered || hiding && disabled;
84
87
  if (item.hidden) {
@@ -88,10 +91,35 @@ export class Toolbar {
88
91
  visible++;
89
92
  state(item, item.hasAttribute('data-state') ? commands.state(name, detail) : null, disabled);
90
93
  }
94
+ // A menu button offers what its entries do: present when the command is, available while at
95
+ // least one entry is, and each entry marked with the value its set currently carries.
96
+ for (const button of this.#element.querySelectorAll('button[data-command-menu]')) {
97
+ const command = button.dataset.commandMenu.trim();
98
+ const control = button.dataset.control?.trim() || command;
99
+ const show = active && commands.has(command) && (!names || names.includes(control));
100
+ const panel = this.#element.querySelector(`[data-menu="${command}"]`);
101
+ let usable = 0;
102
+ for (const entry of panel?.querySelectorAll('button[data-value]') ?? []) {
103
+ const value = entry.dataset.value;
104
+ const on = show && commands.state(command, {...detail, value}) === value;
105
+ // What is already set stays in the list, marked: an entry the selection carries has
106
+ // nothing left to apply, and hiding it would take the answer away with the question.
107
+ const enabled = on || show && commands.enabled(command, {...detail, value});
108
+ entry.disabled = !enabled;
109
+ entry.hidden = !enabled;
110
+ entry.setAttribute('aria-pressed', String(on));
111
+ if (enabled) usable++;
112
+ }
113
+ const offered = show && !!panel?.querySelector('button[data-value]');
114
+ button.hidden = !offered || hiding && !usable;
115
+ state(button, null, !usable);
116
+ if (button.hidden && panel) panel.hidden = true;
117
+ else visible++;
118
+ }
91
119
  for (const select of this.#values()) {
92
120
  const command = select.dataset.commandValue.trim();
93
121
  const control = select.dataset.control?.trim() || command;
94
- const show = active && commands.has(command) && (!names || names.has(control));
122
+ const show = active && commands.has(command) && (!names || names.includes(control));
95
123
  let choices = 0;
96
124
  for (const option of select.options) {
97
125
  if (!option.value) continue;
@@ -167,6 +195,21 @@ export class Toolbar {
167
195
  this.refresh();
168
196
  }
169
197
 
198
+ // The declaration is the order, not only the choice: a host says what its toolbar looks like,
199
+ // and what it does not name keeps the order its modules were registered in, behind the rest.
200
+ // Only a changed declaration moves anything — this runs on every refresh.
201
+ #order(names) {
202
+ const key = names?.join(' ') ?? '';
203
+ if (this.#ordered === key) return;
204
+ this.#ordered = key;
205
+ if (!names) return;
206
+ for (const name of [...names].reverse()) {
207
+ const control = this.#element.querySelector(`[data-control="${name}"]`);
208
+ const panel = control?.nextElementSibling?.hasAttribute('data-menu') ? control.nextElementSibling : null;
209
+ if (control) this.#element.prepend(control, ...(panel ? [panel] : []));
210
+ }
211
+ }
212
+
170
213
  #items() {
171
214
  return this.#element.querySelectorAll('[data-command]');
172
215
  }
@@ -198,11 +241,23 @@ export class Toolbar {
198
241
  // Fields keep their own pointer behaviour so they can be opened and typed in.
199
242
  #pointerDown = event => {
200
243
  const target = event.composedPath()[0];
201
- if (target?.closest?.('select, input, textarea')) return;
244
+ // Fields keep their own pointer behaviour so they can be opened and typed in — but only
245
+ // while they can take the focus. A disabled one would send it nowhere, and a session ends
246
+ // where its focus went.
247
+ const field = target?.closest?.('select, input, textarea');
248
+ if (field && !field.disabled) return;
202
249
  event.preventDefault();
203
250
  };
204
251
 
205
252
  #click = event => {
253
+ // A menu entry sets a value the way a select does; the menu stays open, so a second set can
254
+ // be picked without opening it again.
255
+ const entry = event.target?.closest?.('button[data-command-value][data-value]');
256
+ if (entry && this.#element.contains(entry) && !entry.disabled) {
257
+ event.preventDefault();
258
+ this.#run(entry.dataset.commandValue.trim(), {value: entry.dataset.value});
259
+ return;
260
+ }
206
261
  const item = this.#item(event.target);
207
262
  if (!item) return;
208
263
  event.preventDefault();
@@ -235,7 +290,7 @@ function setting(style, name) {
235
290
 
236
291
  function configured(style) {
237
292
  const value = setting(style, 'toolbar');
238
- return value ? new Set(value.split(/[\s,]+/).filter(Boolean)) : null;
293
+ return value ? [...new Set(value.split(/[\s,]+/).filter(Boolean))] : null;
239
294
  }
240
295
 
241
296
  function visibleForSelection(surface, style) {
@@ -250,9 +305,11 @@ function state(item, value, disabled) {
250
305
  else item.removeAttribute('aria-pressed');
251
306
  }
252
307
 
308
+ // Hidden before it leaves the top layer, shown after it enters: the two attributes hide the same
309
+ // element, and `togglePopover(force)` says which state is wanted rather than asking for the current.
253
310
  function display(element, visible) {
254
- const popover = element.hasAttribute('popover') && typeof element.showPopover === 'function';
255
- if (!visible && popover && element.matches(':popover-open')) element.hidePopover();
311
+ const popover = element.hasAttribute('popover') && typeof element.togglePopover === 'function';
312
+ if (!visible && popover) element.togglePopover(false);
256
313
  element.hidden = !visible;
257
- if (visible && popover && !element.matches(':popover-open')) element.showPopover();
314
+ if (visible && popover) element.togglePopover(true);
258
315
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u2ui",
3
- "version": "1.5.13",
3
+ "version": "1.5.15",
4
4
  "description": "HTML Advanced: More Capabilities, Same Feel, Same Simplicity",
5
5
  "keywords": ["web-components","pattern-library","component-library","ui-toolkit","design-system"],
6
6
  "author": "Tobias Buschor",