ExamplesPlaygroundReference Source

coral-spectrum/coral-collection/src/scripts/SelectableCollection.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.  
  13. import {commons} from '../../../coral-utils';
  14. import Collection from './Collection';
  15. import listToArray from './listToArray';
  16.  
  17. /**
  18. Collection capable of handling non-nested items with a selected attribute. It is useful to manage the
  19. internal state of selection. It currently does not support options.filter for the selection related functions.
  20. */
  21. class SelectableCollection extends Collection {
  22. constructor(options) {
  23. super(options);
  24.  
  25. if (this._filter) {
  26. commons._log('warn', 'Coral.SelectableCollection does not support the options.filter');
  27. }
  28.  
  29. // disabled items will not be a selection candicate although hidden items might
  30. this._selectableItemSelector = this._allItemsSelector.split(',').map(selector => `${selector}:not([disabled])`).join(',');
  31. this._selectedItemSelector = this._allItemsSelector.split(',').map(selector => `${selector}[selected]`).join(',');
  32. this._deselectAllExceptSelector = this._selectedItemSelector;
  33. }
  34.  
  35. /**
  36. Returns the selectable items. Items that are disabled quality for selection. On the other hand, hidden items
  37. can be selected as this is the default behavior in HTML. Please note that an already selected item could be
  38. returned, since the selection could be toggled.
  39.  
  40. @returns {Array.<HTMLElement>}
  41. an array of items whose selection could be toggled.
  42.  
  43. @protected
  44. */
  45. _getSelectableItems() {
  46. return Array.from(this._container.querySelectorAll(this._selectableItemSelector));
  47. }
  48.  
  49. /**
  50. Returns the first selectable item. Items that are disabled quality for selection. On the other hand, hidden items
  51. can be selected as this is the default behavior in HTML. Please note that an already selected item could be
  52. returned, since the selection could be toggled.
  53.  
  54. @returns {HTMLElement}
  55. an item whose selection could be toggled.
  56.  
  57. @protected
  58. */
  59. _getFirstSelectable() {
  60. return this._container.querySelector(this._selectableItemSelector) || null;
  61. }
  62.  
  63. /**
  64. Returns the last selectable item. Items that are disabled quality for selection. On the other hand, hidden items
  65. can be selected as this is the default behavior in HTML. Please note that an already selected item could be
  66. returned, since the selection could be toggled.
  67.  
  68. @returns {HTMLElement}
  69. an item whose selection could be toggled.
  70.  
  71. @protected
  72. */
  73. _getLastSelectable() {
  74. const items = this._container.querySelectorAll(this._selectableItemSelector);
  75. return items[items.length - 1] || null;
  76. }
  77.  
  78. /**
  79. Returns the previous selectable item.
  80.  
  81. @param {HTMLElement} item
  82. The reference item.
  83.  
  84. @returns {HTMLElement}
  85. an item whose selection could be toggled.
  86.  
  87. @protected
  88. */
  89. _getPreviousSelectable(item) {
  90. const items = this.getAll();
  91. let index = items.indexOf(item);
  92. let sibling = index > 0 ? items[index - 1] : null;
  93.  
  94. while (sibling) {
  95. if (sibling.matches(this._selectableItemSelector)) {
  96. break;
  97. } else {
  98. index--;
  99. sibling = index > 0 ? items[index - 1] : null;
  100. }
  101. }
  102.  
  103. // in case the item is not specified, or it is not inside the collection, we need to return the first selectable
  104. return sibling || (item.matches(this._selectableItemSelector) ? item : this._getFirstSelectable());
  105. }
  106.  
  107. /**
  108. Returns the next selectable item.
  109.  
  110. @param {HTMLElement} item
  111. The reference item.
  112.  
  113. @returns {HTMLElement}
  114. an item whose selection could be toggled.
  115.  
  116. @protected
  117. */
  118. _getNextSelectable(item) {
  119. const items = this.getAll();
  120. let index = items.indexOf(item);
  121. let sibling = index < items.length - 1 ? items[index + 1] : null;
  122.  
  123. while (sibling) {
  124. if (sibling.matches(this._selectableItemSelector)) {
  125. break;
  126. } else {
  127. index++;
  128. sibling = index < items.length - 1 ? items[index + 1] : null;
  129. }
  130. }
  131.  
  132. return sibling || item;
  133. }
  134.  
  135. /**
  136. Returns the first item that is selected in the Collection. It allows to configure the attribute used for selection
  137. so that components that use 'selected' and 'active' can share the same implementation.
  138.  
  139. @param {String} [selectedAttribute=selected]
  140. the attribute that will be used to check for selection.
  141.  
  142. @returns HTMLElement the first selected item.
  143.  
  144. @protected
  145. */
  146. _getFirstSelected(selectedAttribute) {
  147. let selector = this._selectedItemSelector;
  148.  
  149. if (typeof selectedAttribute === 'string') {
  150. selector = selector.replace('[selected]', `[${selectedAttribute}]`);
  151. }
  152.  
  153. return this._container.querySelector(selector) || null;
  154. }
  155.  
  156. /**
  157. Returns the last item that is selected in the Collection. It allows to configure the attribute used for selection
  158. so that components that use 'selected' and 'active' can share the same implementation.
  159.  
  160. @param {String} [selectedAttribute=selected]
  161. the attribute that will be used to check for selection.
  162.  
  163. @returns HTMLElment the last selected item.
  164.  
  165. @protected
  166. */
  167. _getLastSelected(selectedAttribute) {
  168. let selector = this._selectedItemSelector;
  169.  
  170. if (typeof selectedAttribute === 'string') {
  171. selector = selector.replace('[selected]', `[${selectedAttribute}]`);
  172. }
  173.  
  174. // last-of-type did not work so we need to query all
  175. const items = this._container.querySelectorAll(selector);
  176. return items[items.length - 1] || null;
  177. }
  178.  
  179. /**
  180. Returns an array that contains all the items that are selected.
  181.  
  182. @param {String} [selectedAttribute=selected]
  183. the attribute that will be used to check for selection.
  184.  
  185. @protected
  186.  
  187. @returns Array.<HTMLElement> an array with all the selected items.
  188. */
  189. _getAllSelected(selectedAttribute) {
  190. let selector = this._selectedItemSelector;
  191.  
  192. if (typeof selectedAttribute === 'string') {
  193. selector = selector.replace('[selected]', `[${selectedAttribute}]`);
  194. }
  195. return Array.from(this._container.querySelectorAll(selector));
  196. }
  197.  
  198. /**
  199. Deselects all the items except the first selected item in the Collection. By default the <code>selected</code>
  200. attribute will be removed. The attribute to remove is configurable via the <code>selectedAttribute</code> parameter.
  201. The selected attribute will be removed no matter if the item is <code>disabled</code> or <code>hidden</code>.
  202.  
  203. @param {String} [selectedAttribute=selected]
  204. the attribute that will be used to check for selection. This attribute will be removed from the matching elements.
  205.  
  206. @protected
  207. */
  208. _deselectAllExceptFirst(selectedAttribute) {
  209. let selector = this._deselectAllExceptSelector;
  210. const attributeToRemove = selectedAttribute || 'selected';
  211.  
  212. if (typeof selectedAttribute === 'string') {
  213. selector = selector.replace('[selected]', `[${selectedAttribute}]`);
  214. }
  215.  
  216. // we select all the selected attributes except the last one
  217. const items = this._container.querySelectorAll(selector);
  218. const itemsCount = items.length;
  219.  
  220. // ignores the first item of the list, everything else is deselected
  221. for (let i = 1 ; i < itemsCount ; i++) {
  222. // we use remoteAttribute since we do not know if the element is upgraded
  223. items[i].removeAttribute(attributeToRemove);
  224. }
  225. }
  226.  
  227. /**
  228. Deselects all the items except the last selected item in the Collecton. By default the <code>selected</code>
  229. attribute will be removed. The attribute to remove is configurable via the <code>selectedAttribute</code> parameter.
  230.  
  231. @param {String} [selectedAttribute=selected]
  232. the attribute that will be used to check for selection. This attribute will be removed from the matching elements.
  233.  
  234. @protected
  235. */
  236. _deselectAllExceptLast(selectedAttribute) {
  237. let selector = this._deselectAllExceptSelector;
  238. const attributeToRemove = selectedAttribute || 'selected';
  239.  
  240. if (typeof selectedAttribute === 'string') {
  241. selector = selector.replace('[selected]', `[${selectedAttribute}]`);
  242. }
  243.  
  244. // we query for all matching items with the given attribute
  245. const items = this._container.querySelectorAll(selector);
  246. // we ignore the last item
  247. const itemsCount = items.length - 1;
  248.  
  249. for (let i = 0 ; i < itemsCount ; i++) {
  250. // we use remoteAttribute since we do not know if the element is upgraded
  251. items[i].removeAttribute(attributeToRemove);
  252. }
  253. }
  254.  
  255. /**
  256. Deselects all the items except the given item. The provided attribute will be remove from all matching items. By
  257. default the <code>selected</code> attribute will be removed. The attribute to remove is configurable via the
  258. <code>selectedAttribute</code> parameter.
  259.  
  260. @name Coral.SelectableCollection#_deselectAllExcept
  261. @function
  262.  
  263. @param {HTMLElement} [itemOrSelectedAttribute]
  264. The item to keep selected. If the item is not provided, all elements will be deselected.
  265.  
  266. @param {String} [selectedAttribute=selected]
  267. the attribute that will be used to check for selection. This attribute will be removed from the matching elements.
  268.  
  269. @protected
  270. */
  271. _deselectAllExcept(itemOrSelectedAttribute, selectedAttribute) {
  272. // if no selectedAttribute we use the unmodified selector as default
  273. let selector = this._deselectAllExceptSelector;
  274.  
  275. let item;
  276. let attributeToRemove;
  277. // an item was not provided so we use it as 'selectedAttribute'
  278. if (typeof itemOrSelectedAttribute === 'string') {
  279. item = null;
  280. attributeToRemove = itemOrSelectedAttribute || 'selected';
  281. selector = selector.replace('[selected]', `[${attributeToRemove}]`);
  282. } else {
  283. item = itemOrSelectedAttribute;
  284. attributeToRemove = selectedAttribute || 'selected';
  285.  
  286. if (typeof selectedAttribute === 'string') {
  287. selector = selector.replace('[selected]', `[${attributeToRemove}]`);
  288. }
  289. }
  290.  
  291. // we query for all matching items with the given attribute
  292. const items = this._container.querySelectorAll(selector);
  293. const itemsCount = items.length;
  294.  
  295. for (let i = 0 ; i < itemsCount ; i++) {
  296. // we use remoteAttribute since we do not know if the element is upgraded
  297. if (item !== items[i]) {
  298. items[i].removeAttribute(attributeToRemove);
  299. }
  300. }
  301. }
  302. }
  303.  
  304. export default SelectableCollection;