ExamplesPlaygroundReference Source

coral-spectrum/coral-component-multifield/src/scripts/Multifield.js

  1. /**
  2. * Copyright 2019 Adobe. All rights reserved.
  3. * This file is licensed to you under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License. You may obtain a copy
  5. * of the License at http://www.apache.org/licenses/LICENSE-2.0
  6. *
  7. * Unless required by applicable law or agreed to in writing, software distributed under
  8. * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
  9. * OF ANY KIND, either express or implied. See the License for the specific language
  10. * governing permissions and limitations under the License.
  11. */
  12. import '../../../coral-component-textfield';
  13. import {BaseComponent} from '../../../coral-base-component';
  14. import MultifieldCollection from './MultifieldCollection';
  15. import {commons, i18n, validate, transform} from '../../../coral-utils';
  16. import {Decorator} from '../../../coral-decorator';
  17.  
  18. const CLASSNAME = '_coral-Multifield';
  19. const IS_DRAGGING_CLASS = 'is-dragging';
  20. const IS_AFTER_CLASS = 'is-after';
  21. const IS_BEFORE_CLASS = 'is-before';
  22. const TEMPLATE_SUPPORT = 'content' in document.createElement('template');
  23.  
  24. /**
  25. @class Coral.Multifield
  26. @classdesc A Multifield component that enables adding, reordering, and removing multiple instances of a component.
  27. Multifield partially supports the <code>template</code> element in IE 11. If adding/removing items in the template
  28. is required, <code>template.content</code> should be used.
  29. Child elements can be given a special attribute to enable functionality:
  30. - <code>[coral-multifield-add]</code>. Click to add an item.
  31. @htmltag coral-multifield
  32. @extends {HTMLElement}
  33. @extends {BaseComponent}
  34. */
  35. const Multifield = Decorator(class extends BaseComponent(HTMLElement) {
  36. /** @ignore */
  37. constructor() {
  38. super();
  39.  
  40. this.setAttribute('id', this.id || commons.getUID());
  41.  
  42. // Attach events
  43. const events = {
  44. 'coral-dragaction:dragstart coral-multifield-item': '_onDragStart',
  45. 'coral-dragaction:drag coral-multifield-item': '_onDrag',
  46. 'coral-dragaction:dragend coral-multifield-item': '_onDragEnd',
  47.  
  48. 'click [coral-multifield-add]': '_onAddItemClick',
  49. 'click ._coral-Multifield-remove': '_onRemoveItemClick',
  50. 'click [coral-multifield-move]': '_onClickDragHandle',
  51. 'key:up [coral-multifield-move]': '_onMoveItemUp',
  52. 'key:pageup [coral-multifield-move]': '_onMoveItemUp',
  53. 'key:down [coral-multifield-move]': '_onMoveItemDown',
  54. 'key:pagedown [coral-multifield-move]': '_onMoveItemDown',
  55. 'key:home [coral-multifield-move]': '_onMoveItemHome',
  56. 'key:end [coral-multifield-move]': '_onMoveItemEnd',
  57. 'key:esc [coral-multifield-move]': '_onMoveItemEsc',
  58. 'click [coral-multifield-up]': '_onUpClick',
  59. 'click [coral-multifield-down]': '_onDownClick',
  60. 'capture:blur [coral-multifield-move]': '_onBlurDragHandle',
  61. 'change coral-multifield-item-content > input': '_onInputChange'
  62. };
  63.  
  64. events[`global:key:escape #${this.id} > [coral-multifield-move]`] = '_onMoveItemEsc';
  65.  
  66. this._delegateEvents(events);
  67.  
  68. // Templates
  69. this._elements = {
  70. template: this.querySelector(`#${this.id} > template[coral-multifield-template]`) || document.createElement('template')
  71. };
  72. this._elements.template.setAttribute('coral-multifield-template', '');
  73.  
  74. // In case <template> is not supported
  75. this._handleTemplateSupport(this._elements.template);
  76.  
  77. // Template support: move nodes added to the <template> to its content fragment
  78. this._observer = new MutationObserver((mutations) => {
  79. mutations.forEach((mutation) => {
  80. for (let i = 0 ; i < mutation.addedNodes.length ; i++) {
  81. const addedNode = mutation.addedNodes[i];
  82. const template = this.template;
  83.  
  84. if (template.contains(addedNode) && template !== addedNode) {
  85. // Move the node to the template content
  86. template.content.appendChild(addedNode);
  87. // Update all items content with the template content
  88. this.items.getAll().forEach((item) => {
  89. this._renderTemplate(item);
  90. });
  91. this._updatePosInSet();
  92. }
  93. }
  94. });
  95. });
  96.  
  97. // Watch for changes to the template element
  98. this._observer.observe(this, {
  99. childList: true,
  100. subtree: true
  101. });
  102.  
  103. // Init the collection mutation observer
  104. this.items._startHandlingItems(true);
  105. }
  106.  
  107. /**
  108. The Collection Interface that allows interacting with the Coral.Multifield items that the component contains.
  109.  
  110. @type {MultifieldCollection}
  111. @readonly
  112. */
  113. get items() {
  114. // just init on demand
  115. if (!this._items) {
  116. this._items = new MultifieldCollection({
  117. host: this,
  118. itemTagName: 'coral-multifield-item',
  119. // allows multifields to be nested
  120. itemSelector: ':scope > coral-multifield-item',
  121. onlyHandleChildren: true,
  122. onItemAdded: this._onItemAdded,
  123. onItemRemoved: this._onItemRemoved
  124. });
  125. }
  126. return this._items;
  127. }
  128.  
  129. /**
  130. The Multifield template element. It will be used to render a new item once the element with the attribute
  131. <code>coral-multifield-add</code> is clicked. It supports the <code>template</code> tag. While specifying the
  132. template from markup, it should include the <code>coral-multifield-template</code> attribute.
  133. NOTE: On IE11, only <code>template.content</code> is supported to add/remove elements to the template.
  134.  
  135. @type {HTMLElement}
  136. @contentzone
  137. */
  138. get template() {
  139. return this._getContentZone(this._elements.template);
  140. }
  141.  
  142. set template(value) {
  143. this._setContentZone('template', value, {
  144. handle: 'template',
  145. tagName: 'template',
  146. insert: function (template) {
  147. this.appendChild(template);
  148. },
  149. set: function (content) {
  150. // Additionally add support for template
  151. this._handleTemplateSupport(content);
  152. }
  153. });
  154. }
  155.  
  156. /**
  157. Whether this multifield is readOnly or not. Indicating that the user cannot modify the value of the multifield fields.
  158. @type {Boolean}
  159. @default false
  160. @htmlattribute readonly
  161. @htmlattributereflected
  162. */
  163. get readOnly() {
  164. return this._readOnly || false;
  165. }
  166.  
  167. set readOnly(value) {
  168. value = transform.booleanAttr(value);
  169. this._readOnly = value;
  170. this._reflectAttribute('readonly', value);
  171.  
  172. this.items.getAll().forEach((item) => {
  173. item[value ? 'setAttribute' : 'removeAttribute']('_readonly', '');
  174. });
  175.  
  176. let addBtn = this.querySelector('[coral-multifield-add]');
  177. if (addBtn) {
  178. addBtn.disabled = value;
  179. }
  180.  
  181. }
  182.  
  183. /**
  184. Specifies the minimum number of items multifield should render.
  185. If component contains less items, remaining items will be added.
  186.  
  187. @type {Number}
  188. @default 0
  189. @htmlattribute min
  190. @htmlattributereflected
  191. */
  192. get min() {
  193. return this._min || 0;
  194. }
  195.  
  196. set min(value) {
  197. const self = this;
  198. value = transform.number(value);
  199.  
  200. if(value && validate.valueMustChange(self._min, value)) {
  201. self._min = value;
  202. self._reflectAttribute('min', value);
  203. self._validateMinItems();
  204. }
  205. }
  206.  
  207. static get _attributePropertyMap() {
  208. return commons.extend(super._attributePropertyMap, {
  209. reorderupdown: 'reorderUpDown',
  210. readonly: 'readOnly'
  211. });
  212. }
  213.  
  214. /**
  215. Whether this multifield require up and down buttons.
  216. @type {Boolean}
  217. @default false
  218. @htmlattribute reorderupdown
  219. @htmlattributereflected
  220. */
  221. get reorderUpDown() {
  222. return this._reorderUpDown || false;
  223. }
  224.  
  225. set reorderUpDown(value) {
  226. value = transform.booleanAttr(value);
  227. this._reorderUpDown = value;
  228. this._reflectAttribute('reorderupdown', value);
  229. }
  230.  
  231. /**
  232. * Validates minimum items required. will add items, if validation fails.
  233. * @param schedule schedule validation in next frame
  234. * @ignore
  235. */
  236. _validateMinItems(schedule) {
  237. // only validate when multifield is connected
  238. if(this._disconnected === false) {
  239. const self = this;
  240. const items = self.items;
  241. let currentLength = items.length;
  242. let currentMin = self.min;
  243. let deletable = true;
  244.  
  245. if(currentLength <= currentMin) {
  246. let itemsToBeAdded = currentMin - currentLength;
  247.  
  248. for(let i = 0; i < itemsToBeAdded; i++) {
  249. let item = document.createElement('coral-multifield-item');
  250. items.add(item);
  251. item._readOnly = this.readOnly;
  252. }
  253.  
  254. deletable = !deletable;
  255. }
  256.  
  257. if(!schedule) {
  258. window.cancelAnimationFrame(self._updateItemsDeletableId);
  259. delete self._updateItemsDeletableId;
  260. self._updateItemsDeletable(items.getAll(), deletable);
  261. } else if(!self._updateItemsDeletableId) {
  262. self._updateItemsDeletableId = window.requestAnimationFrame(() => {
  263. delete self._updateItemsDeletableId;
  264. self._updateItemsDeletable(items.getAll(), deletable);
  265. });
  266. }
  267. }
  268. }
  269.  
  270. /**
  271. * Change the deletable property of passed items to the specified deletable value
  272. * @ignore
  273. */
  274. _updateItemsDeletable(items, deletable) {
  275. deletable = transform.boolean(deletable);
  276. items = !Array.isArray(items) ? [items] : items;
  277.  
  278. items.forEach(function(item) {
  279. item._deletable = deletable;
  280. });
  281. }
  282.  
  283. /** @ignore */
  284. _handleTemplateSupport(template) {
  285. // @polyfill IE
  286. if (!TEMPLATE_SUPPORT && !template.content) {
  287. const frag = document.createDocumentFragment();
  288. while (template.firstChild) {
  289. frag.appendChild(template.firstChild);
  290. }
  291. template.content = frag;
  292. }
  293. }
  294.  
  295. /** @ignore */
  296. _onAddItemClick(event) {
  297. if (event.matchedTarget.closest('coral-multifield') === this) {
  298. this.items.add(document.createElement('coral-multifield-item'));
  299.  
  300. // Wait for MO to render item template
  301. window.requestAnimationFrame(() => {
  302. this.trigger('change');
  303.  
  304. this._trackEvent('click', 'add item button', event);
  305.  
  306. // Focus the newly created input if it can receive focus
  307. var addBtn = event.target;
  308. const items = this.items.getAll();
  309. const setsize = items.length;
  310. const itemToFocus = items[setsize - 1];
  311. const focusableItem = itemToFocus.querySelector(commons.TABBABLE_ELEMENT_SELECTOR);
  312.  
  313. if (focusableItem.hasAttribute('disabled')) {
  314. addBtn.focus();
  315. } else {
  316. focusableItem.focus();
  317. }
  318. });
  319. }
  320. }
  321.  
  322. /** @ignore */
  323. _onRemoveItemClick(event) {
  324. if (event.matchedTarget.closest('coral-multifield') === this) {
  325. const item = event.matchedTarget.closest('coral-multifield-item');
  326. if (item) {
  327. // manage focus when item is removed
  328. let itemToFocus;
  329. const items = this.items.getAll();
  330. const setsize = items.length;
  331. if (setsize > 1) {
  332. const itemIndex = items.indexOf(item);
  333. if (itemIndex === setsize - 1) {
  334. itemToFocus = items[itemIndex - 1];
  335. } else {
  336. itemToFocus = items[itemIndex + 1];
  337. }
  338. }
  339. item.remove();
  340. if (itemToFocus) {
  341. itemToFocus._elements.remove.focus();
  342. } else {
  343. itemToFocus = this.querySelector('[coral-multifield-add]');
  344. if (itemToFocus) {
  345. itemToFocus.focus();
  346. }
  347. }
  348. }
  349.  
  350. this.trigger('change');
  351.  
  352. this._trackEvent('click', 'remove item button', event);
  353. }
  354. }
  355.  
  356. /**
  357. * Toggles keyboard accessible dragging of the current multifield item.
  358. * @ignore
  359. */
  360. _toggleItemDragging(multiFieldItem, dragging = false) {
  361. if (multiFieldItem._dragging === dragging) {
  362. return;
  363. }
  364. multiFieldItem._dragging = dragging;
  365. if (dragging) {
  366. this._oldBefore = multiFieldItem.previousElementSibling;
  367. this._before = multiFieldItem.nextElementSibling;
  368. } else {
  369. this.trigger('coral-multifield:beforeitemorder', {
  370. item: multiFieldItem,
  371. oldBefore: this._oldBefore,
  372. before: this._before
  373. });
  374. this.trigger('coral-multifield:itemorder', {
  375. item: multiFieldItem,
  376. oldBefore: this._oldBefore,
  377. before: multiFieldItem.nextElementSibling
  378. });
  379. this.trigger('change');
  380. this._oldBefore = null;
  381. this._before = null;
  382. }
  383. }
  384.  
  385. /**
  386. * Clicking dragHandle toggles keyboard accessible dragging of the current multifield item.
  387. * @ignore
  388. */
  389. _onClickDragHandle(event) {
  390. event.preventDefault();
  391. event.stopPropagation();
  392. const multiFieldItem = event.matchedTarget.closest('coral-multifield-item');
  393. this._toggleItemDragging(multiFieldItem, !multiFieldItem._dragging);
  394. }
  395.  
  396. /**
  397. * When the drag handle blurs, cancel dragging, leaving item where it is.
  398. * @ignore
  399. */
  400. _onBlurDragHandle(event) {
  401. const dragHandle = event.matchedTarget;
  402. const multiFieldItem = dragHandle.closest('coral-multifield-item');
  403. commons.nextFrame(() => {
  404. if (document.activeElement !== dragHandle) {
  405. this._toggleItemDragging(multiFieldItem, false);
  406. }
  407. });
  408. }
  409.  
  410. /**
  411. * Moves multiField item selected for dragging up one index position in the multifield collection.
  412. * @ignore
  413. */
  414. _onMoveItemUp(event) {
  415. const dragHandle = event.matchedTarget;
  416. const dragElement = dragHandle.closest('coral-multifield-item');
  417. if (!dragElement._dragging) {
  418. return;
  419. }
  420. event.preventDefault();
  421. event.stopPropagation();
  422. const items = this.items.getAll();
  423. const dragElementIndex = items.indexOf(dragElement);
  424. if (dragElementIndex > 0) {
  425. this.insertBefore(dragElement, dragElement.previousElementSibling);
  426. }
  427. dragElement._dragging = true;
  428. dragHandle.focus();
  429. }
  430.  
  431. /**
  432. * Moves multiField item selected for dragging down one index position in the multifield collection.
  433. * @ignore
  434. */
  435. _onMoveItemDown(event) {
  436. const dragHandle = event.matchedTarget;
  437. const dragElement = dragHandle.closest('coral-multifield-item');
  438. if (!dragElement._dragging) {
  439. return;
  440. }
  441. event.preventDefault();
  442. event.stopPropagation();
  443. const items = this.items.getAll();
  444. const dragElementIndex = items.indexOf(dragElement);
  445. if (dragElementIndex < items.length - 1) {
  446. const nextElement = dragElement.nextElementSibling;
  447. this.insertBefore(dragElement, nextElement.nextElementSibling);
  448. }
  449. dragElement._dragging = true;
  450. dragHandle.focus();
  451. }
  452.  
  453. /**
  454. * Moves multiField item selected for dragging to start of multifield collection.
  455. * @ignore
  456. */
  457. _onMoveItemHome(event) {
  458. const dragHandle = event.matchedTarget;
  459. let dragElement = dragHandle.closest('coral-multifield-item');
  460. if (!dragElement._dragging) {
  461. return;
  462. }
  463. event.preventDefault();
  464. event.stopPropagation();
  465. const items = this.items.getAll();
  466. const dragElementIndex = items.indexOf(dragElement);
  467. if (dragElementIndex > 0) {
  468. this.insertBefore(dragElement, this.items.first());
  469. }
  470. dragElement._dragging = true;
  471. dragHandle.focus();
  472. }
  473.  
  474. /**
  475. * Moves multiField item selected for dragging to end of multifield collection.
  476. * @ignore
  477. */
  478. _onMoveItemEnd(event) {
  479. const dragHandle = event.matchedTarget;
  480. let dragElement = dragHandle.closest('coral-multifield-item');
  481. if (!dragElement._dragging) {
  482. return;
  483. }
  484. event.preventDefault();
  485. event.stopPropagation();
  486. const items = this.items.getAll();
  487. const dragElementIndex = items.indexOf(dragElement);
  488. if (dragElementIndex < items.length - 1) {
  489. this.insertBefore(dragElement, this.items.last().nextElementSibling);
  490. }
  491. dragElement._dragging = true;
  492. dragHandle.focus();
  493. }
  494.  
  495. /**
  496. * Cancels keyboard drag and drop operation, restoring item to its previous location.
  497. * @ignore
  498. */
  499. _onMoveItemEsc(event) {
  500. const dragHandle = event.matchedTarget;
  501. const multiFieldItem = dragHandle.closest('coral-multifield-item');
  502. if (multiFieldItem._dragging && this._oldBefore && this._before) {
  503. event.stopPropagation();
  504. this.insertBefore(multiFieldItem, this._before);
  505. dragHandle.focus();
  506. }
  507. this._toggleItemDragging(multiFieldItem, false);
  508. }
  509.  
  510. _onInputChange(event) {
  511. this._trackEvent('change', 'input', event);
  512. }
  513.  
  514. /** @ignore */
  515. _onDragStart(event) {
  516. if (event.target.closest('coral-multifield') === this) {
  517. document.body.classList.add('u-coral-closedHand');
  518.  
  519. const dragElement = event.detail.dragElement;
  520. const items = this.items.getAll();
  521. const dragElementIndex = items.indexOf(dragElement);
  522.  
  523. // Toggle dragging state on multifield item.
  524. dragElement._dragging = true;
  525. dragElement.classList.add(IS_DRAGGING_CLASS);
  526. items.forEach((item, i) => {
  527. if (i < dragElementIndex) {
  528. item.classList.add(IS_BEFORE_CLASS);
  529. } else if (i > dragElementIndex) {
  530. item.classList.add(IS_AFTER_CLASS);
  531. }
  532. });
  533. }
  534. }
  535.  
  536. /** @ignore */
  537. _onDrag(event) {
  538. if (event.target.closest('coral-multifield') === this) {
  539. const items = this.items.getAll();
  540. let marginBottom = 0;
  541.  
  542. if (items.length) {
  543. marginBottom = parseFloat(window.getComputedStyle(items[0]).marginBottom);
  544. }
  545.  
  546. items.forEach((item) => {
  547. if (!item.classList.contains(IS_DRAGGING_CLASS)) {
  548. const dragElement = event.detail.dragElement;
  549. const dragElementBoundingClientRect = dragElement.getBoundingClientRect();
  550. const itemBoundingClientRect = item.getBoundingClientRect();
  551. const dragElementOffsetTop = dragElementBoundingClientRect.top;
  552. const itemOffsetTop = itemBoundingClientRect.top;
  553.  
  554. const isAfter = dragElementOffsetTop < itemOffsetTop;
  555. const itemReorderedTop = `${dragElementBoundingClientRect.height + marginBottom}px`;
  556.  
  557. item.classList.toggle(IS_AFTER_CLASS, isAfter);
  558. item.classList.toggle(IS_BEFORE_CLASS, !isAfter);
  559.  
  560. if (item.classList.contains(IS_AFTER_CLASS)) {
  561. item.style.top = items.indexOf(item) < items.indexOf(dragElement) ? itemReorderedTop : '';
  562. }
  563.  
  564. if (item.classList.contains(IS_BEFORE_CLASS)) {
  565. const afterDragElement = items.indexOf(item) > items.indexOf(dragElement);
  566. item.style.top = afterDragElement ? `-${itemReorderedTop}` : '';
  567. }
  568. }
  569. });
  570. }
  571. }
  572.  
  573. /** @ignore */
  574. _onDragEnd(event) {
  575. if (event.target.closest('coral-multifield') === this) {
  576. document.body.classList.remove('u-coral-closedHand');
  577.  
  578. const dragElement = event.detail.dragElement;
  579. const items = this.items.getAll();
  580. const beforeArr = [];
  581. const afterArr = [];
  582.  
  583. items.forEach((item) => {
  584. if (item.classList.contains(IS_AFTER_CLASS)) {
  585. afterArr.push(item);
  586. } else if (item.classList.contains(IS_BEFORE_CLASS)) {
  587. beforeArr.push(item);
  588. }
  589.  
  590. item.classList.remove(IS_DRAGGING_CLASS, IS_AFTER_CLASS, IS_BEFORE_CLASS);
  591. item.style.top = '';
  592. item.style.position = '';
  593. });
  594.  
  595. const oldBefore = dragElement.previousElementSibling;
  596. const before = afterArr.shift();
  597. const after = beforeArr.pop();
  598. const beforeEvent = this.trigger('coral-multifield:beforeitemorder', {
  599. item: dragElement,
  600. oldBefore: oldBefore,
  601. before: before
  602. });
  603.  
  604. if (!beforeEvent.defaultPrevented) {
  605. if (before) {
  606. this.insertBefore(dragElement, before);
  607. }
  608. if (after) {
  609. this.insertBefore(dragElement, after.nextElementSibling);
  610. }
  611.  
  612. // Toggle dragging state on multifield item.
  613. dragElement._dragging = false;
  614.  
  615. this.trigger('coral-multifield:itemorder', {
  616. item: dragElement,
  617. oldBefore: oldBefore,
  618. before: before
  619. });
  620.  
  621. this.trigger('change');
  622.  
  623. dragElement._elements.move.focus();
  624. }
  625. }
  626. }
  627.  
  628. /** @ignore */
  629. _onUpClick(event) {
  630. const upHandle = event.matchedTarget;
  631. const shiftElement = upHandle.closest('coral-multifield-item');
  632. if(shiftElement.previousElementSibling.tagName === 'CORAL-MULTIFIELD-ITEM') {
  633. this.insertBefore(shiftElement, shiftElement.previousElementSibling);
  634. }
  635. }
  636.  
  637. /** @ignore */
  638. _onDownClick(event) {
  639. const upHandle = event.matchedTarget;
  640. const shiftElement = upHandle.closest('coral-multifield-item');
  641. if(shiftElement.nextElementSibling.tagName === 'CORAL-MULTIFIELD-ITEM') {
  642. this.insertBefore(shiftElement.nextElementSibling, shiftElement);
  643. }
  644. }
  645.  
  646. /** @private */
  647. _onItemAdded(item) {
  648. const self = this;
  649. // Update the item content with the template content
  650. if (item.parentNode === self) {
  651. self._renderTemplate(item);
  652. self._updatePosInSet();
  653. }
  654.  
  655. if(self.items.length === self.min + 1) {
  656. self._validateMinItems();
  657. }
  658.  
  659. // a11y
  660. self._handleRoleList();
  661. }
  662.  
  663. /** @private */
  664. _onItemRemoved() {
  665. const self = this;
  666. self._updatePosInSet();
  667.  
  668. // only validate when required
  669. if(self.items.length <= self.min) {
  670. self._validateMinItems();
  671. }
  672.  
  673. // a11y
  674. self._handleRoleList();
  675. }
  676.  
  677. /**
  678. * handle role list of the multifield based on number of items
  679. * @private
  680. */
  681. _handleRoleList() {
  682. const self = this;
  683. if (self.items.length > 0 && self.getAttribute('role') !== 'list') {
  684. self.setAttribute('role', 'list');
  685. } else if (self.items.length === 0 && self.getAttribute('role') === 'list') {
  686. self.removeAttribute('role');
  687. }
  688. }
  689.  
  690. /**
  691. * update aria-posinset and aria-setsize for each item in the collection
  692. * @private
  693. */
  694. _updatePosInSet() {
  695. const items = this.items.getAll();
  696. const setsize = items.length;
  697. items.forEach((item, i) => {
  698. item.setAttribute('aria-posinset', i + 1);
  699. item.setAttribute('aria-setsize', setsize);
  700. item.setAttribute('aria-label', i18n.get('({0} of {1})', i + 1, setsize));
  701. // so long as item content is not another multifield,
  702. // add aria-labelledby so that the item is labelled by its content and itself.
  703. if (!item.querySelector('coral-multifield')) {
  704. item.setAttribute('aria-labelledby', `${item.id}-content ${item.id}`);
  705. }
  706. });
  707. }
  708.  
  709. /** @private */
  710. _renderTemplate(item) {
  711. const content = item.content || item.querySelector('coral-multifield-item-content') || item;
  712.  
  713. // Insert the template if item content is empty
  714. if (!content.firstChild) {
  715. // @polyfill IE
  716. if (!TEMPLATE_SUPPORT) {
  717. // Before cloning, put the nested templates content back in the DOM
  718. const nestedTemplates = this.template.content.querySelectorAll('template[coral-multifield-template]');
  719.  
  720. Array.prototype.forEach.call(nestedTemplates, (template) => {
  721. while (template.content.firstChild) {
  722. template.appendChild(template.content.firstChild);
  723. }
  724. });
  725. }
  726.  
  727. // Clone the template and append it to the item content
  728. content.appendChild(document.importNode(this.template.content, true));
  729. }
  730. }
  731.  
  732. get _contentZones() {
  733. return {template: 'template'};
  734. }
  735.  
  736. /** @ignore */
  737. static get observedAttributes() {
  738. return super.observedAttributes.concat([
  739. 'min',
  740. 'readonly',
  741. 'reorderupdown'
  742. ]);
  743. }
  744.  
  745. /** @ignore */
  746. render() {
  747. super.render();
  748.  
  749. this.classList.add(CLASSNAME, 'coral-Well');
  750.  
  751. // a11y
  752. this._handleRoleList();
  753.  
  754. // a11y Add aria-label to the add button if exists to give context to screen reader users
  755. const coralMultifieldAddBtn = this.querySelector('[coral-multifield-add]');
  756. if (coralMultifieldAddBtn){
  757. coralMultifieldAddBtn.setAttribute("aria-label","Add");
  758. }
  759.  
  760. // Assign the content zones, moving them into place in the process
  761. this.template = this._elements.template;
  762.  
  763. // Prepare items content based on the given template
  764. this.items.getAll().forEach((item) => {
  765. this._renderTemplate(item);
  766. });
  767.  
  768. // update aria-posinset and aria-setsize for each item in the collection
  769. this._updatePosInSet();
  770.  
  771. this._validateMinItems(true);
  772. }
  773.  
  774. /**
  775. Triggered when the {@link Multifield} item are reordered.
  776.  
  777. @typedef {CustomEvent} coral-multifield:beforeitemorder
  778.  
  779. @property {MultifieldItem} detail.item
  780. The item to be ordered.
  781. @property {MultifieldItem} detail.oldBefore
  782. Ordered item next sibling before the swap. If <code>null</code>, the item was the last item.
  783. @property {MultifieldItem} detail.before
  784. Ordered item will be inserted before this sibling item. If <code>null</code>, the item is inserted at the end.
  785. */
  786.  
  787. /**
  788. Triggered when the {@link Multifield} item are reordered.
  789.  
  790. @typedef {CustomEvent} coral-multifield:itemorder
  791.  
  792. @property {MultifieldItem} detail.item
  793. The ordered item.
  794. @property {MultifieldItem} detail.oldBefore
  795. Ordered item next sibling before the swap. If <code>null</code>, the item was the last item.
  796. @property {MultifieldItem} detail.before
  797. Ordered item was inserted before this sibling item. If <code>null</code>, the item was inserted at the end.
  798. */
  799. });
  800.  
  801. export default Multifield;