ExamplesPlaygroundReference Source

coral-spectrum/coral-component-wizardview/src/scripts/WizardView.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 {BaseComponent} from '../../../coral-base-component';
  14. import {Collection} from '../../../coral-collection';
  15. import '../../../coral-component-steplist';
  16. import '../../../coral-component-panelstack';
  17. import {commons} from '../../../coral-utils';
  18. import {Decorator} from '../../../coral-decorator';
  19.  
  20. const CLASSNAME = '_coral-WizardView';
  21.  
  22. /**
  23. @class Coral.WizardView
  24. @classdesc A WizardView component is the wrapping container used to create the typical Wizard pattern. This is intended
  25. to be used with a {@link StepList} and a {@link PanelStack}.
  26. @htmltag coral-wizardview
  27. @extends {HTMLElement}
  28. @extends {BaseComponent}
  29. */
  30. const WizardView = Decorator(class extends BaseComponent(HTMLElement) {
  31. /** @ignore */
  32. constructor() {
  33. super();
  34.  
  35. this._delegateEvents({
  36. 'capture:click coral-steplist[coral-wizardview-steplist] > coral-step': '_onStepClick',
  37. 'coral-steplist:change coral-steplist[coral-wizardview-steplist]': '_onStepListChange',
  38. 'click [coral-wizardview-previous]': '_onPreviousClick',
  39. 'click [coral-wizardview-next]': '_onNextClick'
  40. });
  41.  
  42. // Init the collection mutation observer
  43. this.stepLists._startHandlingItems(true);
  44. this.panelStacks._startHandlingItems(true);
  45.  
  46. // Disable tracking for specific elements that are attached to the component.
  47. this._observer = new MutationObserver((mutations) => {
  48. mutations.forEach((mutation) => {
  49. // Sync added nodes
  50. for (let i = 0 ; i < mutation.addedNodes.length ; i++) {
  51. const addedNode = mutation.addedNodes[i];
  52.  
  53. if (addedNode.setAttribute &&
  54. (
  55. addedNode.hasAttribute('coral-wizardview-next') ||
  56. addedNode.hasAttribute('coral-wizardview-previous') ||
  57. addedNode.hasAttribute('coral-wizardview-steplist') ||
  58. addedNode.hasAttribute('coral-wizardview-panelstack')
  59. )) {
  60. addedNode.setAttribute('tracking', 'off');
  61. }
  62. }
  63. });
  64. });
  65.  
  66. this._observer.observe(this, {
  67. childList: true,
  68. subtree: true
  69. });
  70. }
  71.  
  72. /**
  73. The set of controlled PanelStacks. Each PanelStack must have the <code>coral-wizardview-panelstack</code> attribute.
  74.  
  75. @type {Collection}
  76. @readonly
  77. */
  78. get panelStacks() {
  79. // Construct the collection on first request:
  80. if (!this._panelStacks) {
  81. this._panelStacks = new Collection({
  82. host: this,
  83. itemTagName: 'coral-panelstack',
  84. // allows panelstack to be nested
  85. itemSelector: ':scope > coral-panelstack[coral-wizardview-panelstack]',
  86. onlyHandleChildren: true,
  87. onItemAdded: this._onItemAdded
  88. });
  89. }
  90.  
  91. return this._panelStacks;
  92. }
  93.  
  94. /**
  95. The set of controlling StepLists. Each StepList must have the <code>coral-wizardview-steplist</code> attribute.
  96.  
  97. @type {Collection}
  98. @readonly
  99. */
  100. get stepLists() {
  101. // Construct the collection on first request:
  102. if (!this._stepLists) {
  103. this._stepLists = new Collection({
  104. host: this,
  105. itemTagName: 'coral-steplist',
  106. // allows steplist to be nested
  107. itemSelector: ':scope > coral-steplist[coral-wizardview-steplist]',
  108. onlyHandleChildren: true,
  109. onItemAdded: this._onItemAdded
  110. });
  111. }
  112.  
  113. return this._stepLists;
  114. }
  115.  
  116. /**
  117. Called by the Collection when an item is added
  118.  
  119. @private
  120. */
  121. _onItemAdded(item) {
  122. this._selectItemByIndex(item, this._getSelectedIndex());
  123. }
  124.  
  125. _onStepClick(event) {
  126. this._trackEvent('click', 'coral-wizardview-steplist-step', event, event.matchedTarget);
  127. }
  128.  
  129. /**
  130. Handles the next button click.
  131.  
  132. @private
  133. */
  134. _onNextClick(event) {
  135. // we stop propagation in case the wizard views are nested
  136. event.stopPropagation();
  137.  
  138. this.next();
  139.  
  140. const stepList = this.stepLists.first();
  141. const step = stepList.items.getAll()[this._getSelectedIndex()];
  142. this._trackEvent('click', 'coral-wizardview-next', event, step);
  143. }
  144.  
  145. /**
  146. Handles the previous button click.
  147.  
  148. @private
  149. */
  150. _onPreviousClick(event) {
  151. // we stop propagation in case the wizard views are nested
  152. event.stopPropagation();
  153.  
  154. this.previous();
  155.  
  156. const stepList = this.stepLists.first();
  157. const step = stepList.items.getAll()[this._getSelectedIndex()];
  158. this._trackEvent('click', 'coral-wizardview-previous', event, step);
  159. }
  160.  
  161. /**
  162. Detects a change in the StepList and triggers an event.
  163.  
  164. @private
  165. */
  166. _onStepListChange(event) {
  167. // Stop propagation of the events to support nested panels
  168. event.stopPropagation();
  169.  
  170. // Get the step number
  171. const index = event.target.items.getAll().indexOf(event.detail.selection);
  172.  
  173. // Sync the other StepLists
  174. this._selectStep(index);
  175.  
  176. this.trigger('coral-wizardview:change', {
  177. selection: event.detail.selection,
  178. oldSelection: event.detail.oldSelection
  179. });
  180.  
  181. this._trackEvent('change', 'coral-wizardview', event);
  182. }
  183.  
  184. /** @private */
  185. _getSelectedIndex() {
  186. const stepList = this.stepLists.first();
  187. if (!stepList) {
  188. return -1;
  189. }
  190.  
  191. let stepIndex = -1;
  192. if (stepList.items) {
  193. stepIndex = stepList.items.getAll().indexOf(stepList.selectedItem);
  194. } else {
  195. // Manually get the selected step
  196. const steps = stepList.querySelectorAll('coral-step');
  197.  
  198. // Find the last selected step
  199. for (let i = steps.length - 1 ; i >= 0 ; i--) {
  200. if (steps[i].hasAttribute('selected')) {
  201. stepIndex = i;
  202. break;
  203. }
  204. }
  205. }
  206.  
  207. return stepIndex;
  208. }
  209.  
  210. /**
  211. Select the step according to the provided index.
  212.  
  213. @param {*} component
  214. The StepList or PanelStack to select the step on.
  215. @param {Number} index
  216. The index of the step that should be selected.
  217.  
  218. @private
  219. */
  220. _selectItemByIndex(component, index) {
  221. let item = null;
  222.  
  223. // we need to set an id to be able to find direct children
  224. component.id = component.id || commons.getUID();
  225.  
  226. // if collection api is available we use it to find the correct item
  227. if (component.items) {
  228. // Get the corresponding item
  229. item = component.items.getAll()[index];
  230. }
  231. // Resort to querying manually on immediately children
  232. else if (component.tagName === 'CORAL-STEPLIST') {
  233. // @polyfill IE - we use id since :scope is not supported
  234. item = component.querySelectorAll(`#${component.id} > coral-step`)[index];
  235. } else if (component.tagName === 'CORAL-PANELSTACK') {
  236. // @polyfill IE - we use id since :scope is not supported
  237. item = component.querySelectorAll(`#${component.id} > coral-panel`)[index];
  238. }
  239.  
  240. if (item) {
  241. // we only select if not select to avoid mutations
  242. if (!item.hasAttribute('selected')) {
  243. item.setAttribute('selected', '');
  244. }
  245. }
  246. // if we did not find an item to select, it means that the "index" is not available in the component, therefore we
  247. // need to deselect all items
  248. else {
  249. // we use the component id to be able to find direct children
  250. if (component.tagName === 'CORAL-STEPLIST') {
  251. // @polyfill IE - we use id since :scope is not supported
  252. item = component.querySelector(`#${component.id} > coral-step[selected]`);
  253. } else if (component.tagName === 'CORAL-PANELSTACK') {
  254. // @polyfill IE - we use id since :scope is not supported
  255. item = component.querySelector(`#${component.id} > coral-panel[selected]`);
  256. }
  257.  
  258. if (item) {
  259. item.removeAttribute('selected');
  260. }
  261. }
  262. }
  263.  
  264. /** @private */
  265. _selectStep(index) {
  266. // we apply the selection to all available steplists
  267. this.stepLists.getAll().forEach((stepList) => {
  268. this._selectItemByIndex(stepList, index);
  269. });
  270.  
  271. // we apply the selection to all available panelstacks
  272. this.panelStacks.getAll().forEach((panelStack) => {
  273. this._selectItemByIndex(panelStack, index);
  274. });
  275. }
  276.  
  277. /**
  278. Sets the correct selected item in every PanelStack.
  279.  
  280. @private
  281. */
  282. _syncPanelStackSelection(defaultIndex) {
  283. // Find out which step we're on by checking the first StepList
  284. let index = this._getSelectedIndex();
  285.  
  286. if (index === -1) {
  287. if (typeof defaultIndex !== 'undefined') {
  288. index = defaultIndex;
  289. } else {
  290. // No panel selected
  291. return;
  292. }
  293. }
  294.  
  295. this.panelStacks.getAll().forEach((panelStack) => {
  296. this._selectItemByIndex(panelStack, index);
  297. });
  298. }
  299.  
  300. /**
  301. Selects the correct step in every StepList.
  302.  
  303. @private
  304. */
  305. _syncStepListSelection(defaultIndex) {
  306. // Find out which step we're on by checking the first StepList
  307. let index = this._getSelectedIndex();
  308.  
  309. if (index === -1) {
  310. if (typeof defaultIndex !== 'undefined') {
  311. index = defaultIndex;
  312. } else {
  313. // No step selected
  314. return;
  315. }
  316. }
  317.  
  318. this.stepLists.getAll().forEach((stepList) => {
  319. this._selectItemByIndex(stepList, index);
  320. });
  321. }
  322.  
  323. /**
  324. Shows the next step. If the WizardView is already in the last step nothing will happen.
  325.  
  326. @emits {coral-wizardview:change}
  327. */
  328. next() {
  329. const stepList = this.stepLists.first();
  330. if (!stepList) {
  331. return;
  332. }
  333.  
  334. // Change to the next step
  335. stepList.next();
  336.  
  337. // Select the step everywhere
  338. this._selectStep(stepList.items.getAll().indexOf(stepList.selectedItem));
  339. }
  340.  
  341. /**
  342. Shows the previous step. If the WizardView is already in the first step nothing will happen.
  343.  
  344. @emits {coral-wizardview:change}
  345. */
  346. previous() {
  347. const stepList = this.stepLists.first();
  348. if (!stepList) {
  349. return;
  350. }
  351.  
  352. // Change to the previous step
  353. stepList.previous();
  354.  
  355. // Select the step everywhere
  356. this._selectStep(stepList.items.getAll().indexOf(stepList.selectedItem));
  357. }
  358.  
  359. /** @ignore */
  360. render() {
  361. super.render();
  362.  
  363. this.classList.add(CLASSNAME);
  364.  
  365. this._syncStepListSelection(0);
  366. this._syncPanelStackSelection(0);
  367.  
  368. // Disable tracking for specific elements that are attached to the component.
  369. const selector = '[coral-wizardview-next],[coral-wizardview-previous],[coral-wizardview-steplist],[coral-wizardview-panelstack]';
  370. const items = this.querySelectorAll(selector);
  371. for (let i = 0 ; i < items.length ; i++) {
  372. items[i].setAttribute('tracking', 'off');
  373. }
  374. }
  375.  
  376. /**
  377. Triggered when the {@link WizardView} selected step list item has changed.
  378.  
  379. @typedef {CustomEvent} coral-wizardview:change
  380.  
  381. @property {Step} event.detail.selection
  382. The new selected step list item.
  383. @property {Step} event.detail.oldSelection
  384. The prior selected step list item.
  385. */
  386. });
  387.  
  388. export default WizardView;