popup.js 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082
  1. /* *
  2. *
  3. * Popup generator for Stock tools
  4. *
  5. * (c) 2009-2017 Sebastian Bochan
  6. *
  7. * License: www.highcharts.com/license
  8. *
  9. * */
  10. 'use strict';
  11. import H from '../parts/Globals.js';
  12. import U from '../parts/Utilities.js';
  13. var defined = U.defined,
  14. isArray = U.isArray,
  15. isObject = U.isObject,
  16. isString = U.isString,
  17. objectEach = U.objectEach,
  18. pick = U.pick,
  19. wrap = U.wrap;
  20. var addEvent = H.addEvent,
  21. createElement = H.createElement,
  22. indexFilter = /\d/g,
  23. PREFIX = 'highcharts-',
  24. DIV = 'div',
  25. INPUT = 'input',
  26. LABEL = 'label',
  27. BUTTON = 'button',
  28. SELECT = 'select',
  29. OPTION = 'option',
  30. SPAN = 'span',
  31. UL = 'ul',
  32. LI = 'li',
  33. H3 = 'h3';
  34. // onContainerMouseDown blocks internal popup events, due to e.preventDefault.
  35. // Related issue #4606
  36. wrap(H.Pointer.prototype, 'onContainerMouseDown', function (proceed, e) {
  37. var popupClass = e.target && e.target.className;
  38. // elements is not in popup
  39. if (!(isString(popupClass) &&
  40. popupClass.indexOf(PREFIX + 'popup-field') >= 0)
  41. ) {
  42. proceed.apply(this, Array.prototype.slice.call(arguments, 1));
  43. }
  44. });
  45. H.Popup = function (parentDiv, iconsURL) {
  46. this.init(parentDiv, iconsURL);
  47. };
  48. H.Popup.prototype = {
  49. /**
  50. * Initialize the popup. Create base div and add close button.
  51. * @private
  52. * @param {HTMLDOMElement} - container where popup should be placed
  53. * @param {Object} - user options
  54. * @return {HTMLDOMElement} - return created popup's div
  55. */
  56. init: function (parentDiv, iconsURL) {
  57. // create popup div
  58. this.container = createElement(DIV, {
  59. className: PREFIX + 'popup'
  60. }, null, parentDiv);
  61. this.lang = this.getLangpack();
  62. this.iconsURL = iconsURL;
  63. // add close button
  64. this.addCloseBtn();
  65. },
  66. /**
  67. * Create HTML element and attach click event (close popup).
  68. * @private
  69. */
  70. addCloseBtn: function () {
  71. var _self = this,
  72. closeBtn;
  73. // create close popup btn
  74. closeBtn = createElement(DIV, {
  75. className: PREFIX + 'popup-close'
  76. }, null, this.container);
  77. closeBtn.style['background-image'] = 'url(' +
  78. this.iconsURL + 'close.svg)';
  79. ['click', 'touchstart'].forEach(function (eventName) {
  80. addEvent(closeBtn, eventName, function () {
  81. _self.closePopup();
  82. });
  83. });
  84. },
  85. /**
  86. * Create two columns (divs) in HTML.
  87. * @private
  88. * @param {HTMLDOMElement} - container of columns
  89. * @return {Object} - reference to two HTML columns
  90. */
  91. addColsContainer: function (container) {
  92. var rhsCol,
  93. lhsCol;
  94. // left column
  95. lhsCol = createElement(DIV, {
  96. className: PREFIX + 'popup-lhs-col'
  97. }, null, container);
  98. // right column
  99. rhsCol = createElement(DIV, {
  100. className: PREFIX + 'popup-rhs-col'
  101. }, null, container);
  102. // wrapper content
  103. createElement(DIV, {
  104. className: PREFIX + 'popup-rhs-col-wrapper'
  105. }, null, rhsCol);
  106. return {
  107. lhsCol: lhsCol,
  108. rhsCol: rhsCol
  109. };
  110. },
  111. /**
  112. * Create input with label.
  113. * @private
  114. * @param {String} - chain of fields i.e params.styles.fontSize
  115. * @param {String} - indicator type
  116. * @param {HTMLDOMElement} - container where elements should be added
  117. * @param {String} - dafault value of input i.e period value is 14,
  118. * extracted from defaultOptions (ADD mode) or series options (EDIT mode)
  119. */
  120. addInput: function (option, type, parentDiv, value) {
  121. var optionParamList = option.split('.'),
  122. optionName = optionParamList[optionParamList.length - 1],
  123. lang = this.lang,
  124. inputName = PREFIX + type + '-' + optionName;
  125. if (!inputName.match(indexFilter)) {
  126. // add label
  127. createElement(
  128. LABEL, {
  129. innerHTML: lang[optionName] || optionName,
  130. htmlFor: inputName
  131. },
  132. null,
  133. parentDiv
  134. );
  135. }
  136. // add input
  137. createElement(
  138. INPUT,
  139. {
  140. name: inputName,
  141. value: value[0],
  142. type: value[1],
  143. className: PREFIX + 'popup-field'
  144. },
  145. null,
  146. parentDiv
  147. ).setAttribute(PREFIX + 'data-name', option);
  148. },
  149. /**
  150. * Create button.
  151. * @private
  152. * @param {HTMLDOMElement} - container where elements should be added
  153. * @param {String} - text placed as button label
  154. * @param {String} - add | edit | remove
  155. * @param {Function} - on click callback
  156. * @param {HTMLDOMElement} - container where inputs are generated
  157. * @return {HTMLDOMElement} - html button
  158. */
  159. addButton: function (parentDiv, label, type, callback, fieldsDiv) {
  160. var _self = this,
  161. closePopup = this.closePopup,
  162. getFields = this.getFields,
  163. button;
  164. button = createElement(BUTTON, {
  165. innerHTML: label
  166. }, null, parentDiv);
  167. ['click', 'touchstart'].forEach(function (eventName) {
  168. addEvent(button, eventName, function () {
  169. closePopup.call(_self);
  170. return callback(
  171. getFields(fieldsDiv, type)
  172. );
  173. });
  174. });
  175. return button;
  176. },
  177. /**
  178. * Get values from all inputs and create JSON.
  179. * @private
  180. * @param {HTMLDOMElement} - container where inputs are created
  181. * @param {String} - add | edit | remove
  182. * @return {Object} - fields
  183. */
  184. getFields: function (parentDiv, type) {
  185. var inputList = parentDiv.querySelectorAll('input'),
  186. optionSeries = '#' + PREFIX + 'select-series > option:checked',
  187. optionVolume = '#' + PREFIX + 'select-volume > option:checked',
  188. linkedTo = parentDiv.querySelectorAll(optionSeries)[0],
  189. volumeTo = parentDiv.querySelectorAll(optionVolume)[0],
  190. seriesId,
  191. param,
  192. fieldsOutput;
  193. fieldsOutput = {
  194. actionType: type,
  195. linkedTo: linkedTo && linkedTo.getAttribute('value'),
  196. fields: { }
  197. };
  198. [].forEach.call(inputList, function (input) {
  199. param = input.getAttribute(PREFIX + 'data-name');
  200. seriesId = input.getAttribute(PREFIX + 'data-series-id');
  201. // params
  202. if (seriesId) {
  203. fieldsOutput.seriesId = input.value;
  204. } else if (param) {
  205. fieldsOutput.fields[param] = input.value;
  206. } else {
  207. // type like sma / ema
  208. fieldsOutput.type = input.value;
  209. }
  210. });
  211. if (volumeTo) {
  212. fieldsOutput.fields['params.volumeSeriesID'] = volumeTo
  213. .getAttribute('value');
  214. }
  215. return fieldsOutput;
  216. },
  217. /**
  218. * Reset content of the current popup and show.
  219. * @private
  220. * @param {Chart} - chart
  221. * @param {Function} - on click callback
  222. * @return {Object} - fields
  223. */
  224. showPopup: function () {
  225. var popupDiv = this.container,
  226. toolbarClass = PREFIX + 'annotation-toolbar',
  227. popupCloseBtn = popupDiv
  228. .querySelectorAll('.' + PREFIX + 'popup-close')[0];
  229. // reset content
  230. popupDiv.innerHTML = '';
  231. // reset toolbar styles if exists
  232. if (popupDiv.className.indexOf(toolbarClass) >= 0) {
  233. popupDiv.classList.remove(toolbarClass);
  234. // reset toolbar inline styles
  235. popupDiv.removeAttribute('style');
  236. }
  237. // add close button
  238. popupDiv.appendChild(popupCloseBtn);
  239. popupDiv.style.display = 'block';
  240. },
  241. /**
  242. * Hide popup.
  243. * @private
  244. */
  245. closePopup: function () {
  246. this.popup.container.style.display = 'none';
  247. },
  248. /**
  249. * Create content and show popup.
  250. * @private
  251. * @param {String} - type of popup i.e indicators
  252. * @param {Chart} - chart
  253. * @param {Object} - options
  254. * @param {Function} - on click callback
  255. */
  256. showForm: function (type, chart, options, callback) {
  257. this.popup = chart.navigationBindings.popup;
  258. // show blank popup
  259. this.showPopup();
  260. // indicator form
  261. if (type === 'indicators') {
  262. this.indicators.addForm.call(this, chart, options, callback);
  263. }
  264. // annotation small toolbar
  265. if (type === 'annotation-toolbar') {
  266. this.annotations.addToolbar.call(this, chart, options, callback);
  267. }
  268. // annotation edit form
  269. if (type === 'annotation-edit') {
  270. this.annotations.addForm.call(this, chart, options, callback);
  271. }
  272. // flags form - add / edit
  273. if (type === 'flag') {
  274. this.annotations.addForm.call(this, chart, options, callback, true);
  275. }
  276. },
  277. /**
  278. * Return lang definitions for popup.
  279. * @private
  280. * @return {Object} - elements translations.
  281. */
  282. getLangpack: function () {
  283. return H.getOptions().lang.navigation.popup;
  284. },
  285. annotations: {
  286. /**
  287. * Create annotation simple form. It contains two buttons
  288. * (edit / remove) and text label.
  289. * @private
  290. * @param {Chart} - chart
  291. * @param {Object} - options
  292. * @param {Function} - on click callback
  293. */
  294. addToolbar: function (chart, options, callback) {
  295. var _self = this,
  296. lang = this.lang,
  297. popupDiv = this.popup.container,
  298. showForm = this.showForm,
  299. toolbarClass = PREFIX + 'annotation-toolbar',
  300. button;
  301. // set small size
  302. if (popupDiv.className.indexOf(toolbarClass) === -1) {
  303. popupDiv.className += ' ' + toolbarClass;
  304. }
  305. // set position
  306. popupDiv.style.top = chart.plotTop + 10 + 'px';
  307. // create label
  308. createElement(SPAN, {
  309. innerHTML: pick(
  310. // Advanced annotations:
  311. lang[options.langKey] || options.langKey,
  312. // Basic shapes:
  313. options.shapes && options.shapes[0].type
  314. )
  315. }, null, popupDiv);
  316. // add buttons
  317. button = this.addButton(
  318. popupDiv,
  319. lang.removeButton || 'remove',
  320. 'remove',
  321. callback,
  322. popupDiv
  323. );
  324. button.className += ' ' + PREFIX + 'annotation-remove-button';
  325. button.style['background-image'] = 'url(' +
  326. this.iconsURL + 'destroy.svg)';
  327. button = this.addButton(
  328. popupDiv,
  329. lang.editButton || 'edit',
  330. 'edit',
  331. function () {
  332. showForm.call(
  333. _self,
  334. 'annotation-edit',
  335. chart,
  336. options,
  337. callback
  338. );
  339. },
  340. popupDiv
  341. );
  342. button.className += ' ' + PREFIX + 'annotation-edit-button';
  343. button.style['background-image'] = 'url(' +
  344. this.iconsURL + 'edit.svg)';
  345. },
  346. /**
  347. * Create annotation simple form.
  348. * It contains fields with param names.
  349. * @private
  350. * @param {Chart} - chart
  351. * @param {Object} - options
  352. * @param {Function} - on click callback
  353. * @param {Boolean} - if it is a form declared for init annotation
  354. */
  355. addForm: function (chart, options, callback, isInit) {
  356. var popupDiv = this.popup.container,
  357. lang = this.lang,
  358. bottomRow,
  359. lhsCol;
  360. // create title of annotations
  361. lhsCol = createElement('h2', {
  362. innerHTML: lang[options.langKey] || options.langKey,
  363. className: PREFIX + 'popup-main-title'
  364. }, null, popupDiv);
  365. // left column
  366. lhsCol = createElement(DIV, {
  367. className: PREFIX + 'popup-lhs-col ' + PREFIX + 'popup-lhs-full'
  368. }, null, popupDiv);
  369. bottomRow = createElement(DIV, {
  370. className: PREFIX + 'popup-bottom-row'
  371. }, null, popupDiv);
  372. this.annotations.addFormFields.call(
  373. this,
  374. lhsCol,
  375. chart,
  376. '',
  377. options,
  378. [],
  379. true
  380. );
  381. this.addButton(
  382. bottomRow,
  383. isInit ?
  384. (lang.addButton || 'add') :
  385. (lang.saveButton || 'save'),
  386. isInit ? 'add' : 'save',
  387. callback,
  388. popupDiv
  389. );
  390. },
  391. /**
  392. * Create annotation's form fields.
  393. * @private
  394. * @param {HTMLDOMElement} - div where inputs are placed
  395. * @param {Chart} - chart
  396. * @param {String} - name of parent to create chain of names
  397. * @param {Object} - options
  398. * @param {Array} - storage - array where all items are stored
  399. * @param {Boolean} - isRoot - recursive flag for root
  400. */
  401. addFormFields: function (
  402. parentDiv,
  403. chart,
  404. parentNode,
  405. options,
  406. storage,
  407. isRoot
  408. ) {
  409. var _self = this,
  410. addFormFields = this.annotations.addFormFields,
  411. addInput = this.addInput,
  412. lang = this.lang,
  413. parentFullName,
  414. titleName;
  415. objectEach(options, function (value, option) {
  416. // create name like params.styles.fontSize
  417. parentFullName = parentNode !== '' ?
  418. parentNode + '.' + option : option;
  419. if (isObject(value)) {
  420. if (
  421. // value is object of options
  422. !isArray(value) ||
  423. // array of objects with params. i.e labels in Fibonacci
  424. (isArray(value) && isObject(value[0]))
  425. ) {
  426. titleName = lang[option] || option;
  427. if (!titleName.match(indexFilter)) {
  428. storage.push([
  429. true,
  430. titleName,
  431. parentDiv
  432. ]);
  433. }
  434. addFormFields.call(
  435. _self,
  436. parentDiv,
  437. chart,
  438. parentFullName,
  439. value,
  440. storage,
  441. false
  442. );
  443. } else {
  444. storage.push([
  445. _self,
  446. parentFullName,
  447. 'annotation',
  448. parentDiv,
  449. value
  450. ]);
  451. }
  452. }
  453. });
  454. if (isRoot) {
  455. storage = storage.sort(function (a) {
  456. return a[1].match(/format/g) ? -1 : 1;
  457. });
  458. storage.forEach(function (genInput) {
  459. if (genInput[0] === true) {
  460. createElement(SPAN, {
  461. className: PREFIX + 'annotation-title',
  462. innerHTML: genInput[1]
  463. }, null, genInput[2]);
  464. } else {
  465. addInput.apply(genInput[0], genInput.splice(1));
  466. }
  467. });
  468. }
  469. }
  470. },
  471. indicators: {
  472. /**
  473. * Create indicator's form. It contains two tabs (ADD and EDIT) with
  474. * content.
  475. * @private
  476. * @param {Chart} - chart
  477. * @param {Object} - options
  478. * @param {Function} - on click callback
  479. */
  480. addForm: function (chart, options, callback) {
  481. var tabsContainers,
  482. indicators = this.indicators,
  483. lang = this.lang,
  484. buttonParentDiv;
  485. // add tabs
  486. this.tabs.init.call(this, chart);
  487. // get all tabs content divs
  488. tabsContainers = this.popup.container
  489. .querySelectorAll('.' + PREFIX + 'tab-item-content');
  490. // ADD tab
  491. this.addColsContainer(tabsContainers[0]);
  492. indicators.addIndicatorList.call(
  493. this,
  494. chart,
  495. tabsContainers[0],
  496. 'add'
  497. );
  498. buttonParentDiv = tabsContainers[0]
  499. .querySelectorAll('.' + PREFIX + 'popup-rhs-col')[0];
  500. this.addButton(
  501. buttonParentDiv,
  502. lang.addButton || 'add',
  503. 'add',
  504. callback,
  505. buttonParentDiv
  506. );
  507. // EDIT tab
  508. this.addColsContainer(tabsContainers[1]);
  509. indicators.addIndicatorList.call(
  510. this,
  511. chart,
  512. tabsContainers[1],
  513. 'edit'
  514. );
  515. buttonParentDiv = tabsContainers[1]
  516. .querySelectorAll('.' + PREFIX + 'popup-rhs-col')[0];
  517. this.addButton(
  518. buttonParentDiv,
  519. lang.saveButton || 'save',
  520. 'edit',
  521. callback,
  522. buttonParentDiv
  523. );
  524. this.addButton(
  525. buttonParentDiv,
  526. lang.removeButton || 'remove',
  527. 'remove',
  528. callback,
  529. buttonParentDiv
  530. );
  531. },
  532. /**
  533. * Create HTML list of all indicators (ADD mode) or added indicators
  534. * (EDIT mode).
  535. * @private
  536. * @param {Chart} - chart
  537. * @param {HTMLDOMElement} - container where list is added
  538. * @param {String} - 'edit' or 'add' mode
  539. */
  540. addIndicatorList: function (chart, parentDiv, listType) {
  541. var _self = this,
  542. lhsCol = parentDiv
  543. .querySelectorAll('.' + PREFIX + 'popup-lhs-col')[0],
  544. rhsCol = parentDiv
  545. .querySelectorAll('.' + PREFIX + 'popup-rhs-col')[0],
  546. isEdit = listType === 'edit',
  547. series = isEdit ? chart.series : // EDIT mode
  548. chart.options.plotOptions, // ADD mode
  549. addFormFields = this.indicators.addFormFields,
  550. rhsColWrapper,
  551. indicatorList,
  552. item;
  553. // create wrapper for list
  554. indicatorList = createElement(UL, {
  555. className: PREFIX + 'indicator-list'
  556. }, null, lhsCol);
  557. rhsColWrapper = rhsCol
  558. .querySelectorAll('.' + PREFIX + 'popup-rhs-col-wrapper')[0];
  559. objectEach(series, function (serie, value) {
  560. var seriesOptions = serie.options;
  561. if (
  562. serie.params ||
  563. seriesOptions && seriesOptions.params
  564. ) {
  565. var indicatorNameType = _self.indicators
  566. .getNameType(serie, value),
  567. indicatorType = indicatorNameType.type;
  568. item = createElement(LI, {
  569. className: PREFIX + 'indicator-list',
  570. innerHTML: indicatorNameType.name
  571. }, null, indicatorList);
  572. ['click', 'touchstart'].forEach(function (eventName) {
  573. addEvent(item, eventName, function () {
  574. addFormFields.call(
  575. _self,
  576. chart,
  577. isEdit ? serie : series[indicatorType],
  578. indicatorNameType.type,
  579. rhsColWrapper
  580. );
  581. // add hidden input with series.id
  582. if (isEdit && serie.options) {
  583. createElement(INPUT, {
  584. type: 'hidden',
  585. name: PREFIX + 'id-' + indicatorType,
  586. value: serie.options.id
  587. }, null, rhsColWrapper)
  588. .setAttribute(
  589. PREFIX + 'data-series-id',
  590. serie.options.id
  591. );
  592. }
  593. });
  594. });
  595. }
  596. });
  597. // select first item from the list
  598. if (indicatorList.childNodes.length > 0) {
  599. indicatorList.childNodes[0].click();
  600. }
  601. },
  602. /**
  603. * Extract full name and type of requested indicator.
  604. * @private
  605. * @param {Series} - series which name is needed.
  606. * (EDIT mode - defaultOptions.series, ADD mode - indicator series).
  607. * @param {String} - indicator type like: sma, ema, etc.
  608. * @return {Object} - series name and type like: sma, ema, etc.
  609. */
  610. getNameType: function (series, type) {
  611. var options = series.options,
  612. seriesTypes = H.seriesTypes,
  613. // add mode
  614. seriesName = seriesTypes[type] &&
  615. seriesTypes[type].prototype.nameBase || type.toUpperCase(),
  616. seriesType = type;
  617. // edit
  618. if (options && options.type) {
  619. seriesType = series.options.type;
  620. seriesName = series.name;
  621. }
  622. return {
  623. name: seriesName,
  624. type: seriesType
  625. };
  626. },
  627. /**
  628. * List all series with unique ID. Its mandatory for indicators to set
  629. * correct linking.
  630. * @private
  631. * @param {String} - indicator type like: sma, ema, etc.
  632. * @param {String} - type of select i.e series or volume.
  633. * @param {Chart} - chart
  634. * @param {HTMLDOMElement} - element where created HTML list is added
  635. * @param {String} selectedOption
  636. * optional param for default value in dropdown
  637. */
  638. listAllSeries: function (
  639. type,
  640. optionName,
  641. chart,
  642. parentDiv,
  643. selectedOption
  644. ) {
  645. var selectName = PREFIX + optionName + '-type-' + type,
  646. lang = this.lang,
  647. selectBox,
  648. seriesOptions;
  649. createElement(
  650. LABEL, {
  651. innerHTML: lang[optionName] || optionName,
  652. htmlFor: selectName
  653. },
  654. null,
  655. parentDiv
  656. );
  657. // select type
  658. selectBox = createElement(
  659. SELECT,
  660. {
  661. name: selectName,
  662. className: PREFIX + 'popup-field'
  663. },
  664. null,
  665. parentDiv
  666. );
  667. selectBox.setAttribute('id', PREFIX + 'select-' + optionName);
  668. // list all series which have id - mandatory for creating indicator
  669. chart.series.forEach(function (serie) {
  670. seriesOptions = serie.options;
  671. if (
  672. !seriesOptions.params &&
  673. seriesOptions.id &&
  674. seriesOptions.id !== PREFIX + 'navigator-series'
  675. ) {
  676. createElement(
  677. OPTION,
  678. {
  679. innerHTML: seriesOptions.name || seriesOptions.id,
  680. value: seriesOptions.id
  681. },
  682. null,
  683. selectBox
  684. );
  685. }
  686. });
  687. if (defined(selectedOption)) {
  688. selectBox.value = selectedOption;
  689. }
  690. },
  691. /**
  692. * Create typical inputs for chosen indicator. Fields are extracted from
  693. * defaultOptions (ADD mode) or current indicator (ADD mode). Two extra
  694. * fields are added:
  695. * - hidden input - contains indicator type (required for callback)
  696. * - select - list of series which can be linked with indicator
  697. * @private
  698. * @param {Chart} - chart
  699. * @param {Series} - indicator
  700. * @param {String} - indicator type like: sma, ema, etc.
  701. * @param {HTMLDOMElement} - element where created HTML list is added
  702. */
  703. addFormFields: function (chart, series, seriesType, rhsColWrapper) {
  704. var fields = series.params || series.options.params,
  705. getNameType = this.indicators.getNameType;
  706. // reset current content
  707. rhsColWrapper.innerHTML = '';
  708. // create title (indicator name in the right column)
  709. createElement(
  710. H3,
  711. {
  712. className: PREFIX + 'indicator-title',
  713. innerHTML: getNameType(series, seriesType).name
  714. },
  715. null,
  716. rhsColWrapper
  717. );
  718. // input type
  719. createElement(
  720. INPUT,
  721. {
  722. type: 'hidden',
  723. name: PREFIX + 'type-' + seriesType,
  724. value: seriesType
  725. },
  726. null,
  727. rhsColWrapper
  728. );
  729. // list all series with id
  730. this.indicators.listAllSeries.call(
  731. this,
  732. seriesType,
  733. 'series',
  734. chart,
  735. rhsColWrapper,
  736. series.linkedParent && fields.volumeSeriesID
  737. );
  738. if (fields.volumeSeriesID) {
  739. this.indicators.listAllSeries.call(
  740. this,
  741. seriesType,
  742. 'volume',
  743. chart,
  744. rhsColWrapper,
  745. series.linkedParent && series.linkedParent.options.id
  746. );
  747. }
  748. // add param fields
  749. this.indicators.addParamInputs.call(
  750. this,
  751. chart,
  752. 'params',
  753. fields,
  754. seriesType,
  755. rhsColWrapper
  756. );
  757. },
  758. /**
  759. * Recurent function which lists all fields, from params object and
  760. * create them as inputs. Each input has unique `data-name` attribute,
  761. * which keeps chain of fields i.e params.styles.fontSize.
  762. * @private
  763. * @param {Chart} - chart
  764. * @param {String} - name of parent to create chain of names
  765. * @param {Series} - fields - params which are based for input create
  766. * @param {String} - indicator type like: sma, ema, etc.
  767. * @param {HTMLDOMElement} - element where created HTML list is added
  768. */
  769. addParamInputs: function (chart, parentNode, fields, type, parentDiv) {
  770. var _self = this,
  771. addParamInputs = this.indicators.addParamInputs,
  772. addInput = this.addInput,
  773. parentFullName;
  774. objectEach(fields, function (value, fieldName) {
  775. // create name like params.styles.fontSize
  776. parentFullName = parentNode + '.' + fieldName;
  777. if (isObject(value)) {
  778. addParamInputs.call(
  779. _self,
  780. chart,
  781. parentFullName,
  782. value,
  783. type,
  784. parentDiv
  785. );
  786. } else if (
  787. // skip volume field which is created by addFormFields
  788. parentFullName !== 'params.volumeSeriesID'
  789. ) {
  790. addInput.call(
  791. _self,
  792. parentFullName,
  793. type,
  794. parentDiv,
  795. [value, 'text'] // all inputs are text type
  796. );
  797. }
  798. });
  799. },
  800. /**
  801. * Get amount of indicators added to chart.
  802. * @private
  803. * @return {Number} - Amount of indicators
  804. */
  805. getAmount: function () {
  806. var series = this.series,
  807. counter = 0;
  808. objectEach(series, function (serie) {
  809. var seriesOptions = serie.options;
  810. if (
  811. serie.params ||
  812. seriesOptions && seriesOptions.params
  813. ) {
  814. counter++;
  815. }
  816. });
  817. return counter;
  818. }
  819. },
  820. tabs: {
  821. /**
  822. * Init tabs. Create tab menu items, tabs containers
  823. * @private
  824. * @param {Chart} - reference to current chart
  825. */
  826. init: function (chart) {
  827. var tabs = this.tabs,
  828. indicatorsCount = this.indicators.getAmount.call(chart),
  829. firstTab; // run by default
  830. // create menu items
  831. firstTab = tabs.addMenuItem.call(this, 'add');
  832. tabs.addMenuItem.call(this, 'edit', indicatorsCount);
  833. // create tabs containers
  834. tabs.addContentItem.call(this, 'add');
  835. tabs.addContentItem.call(this, 'edit');
  836. tabs.switchTabs.call(this, indicatorsCount);
  837. // activate first tab
  838. tabs.selectTab.call(this, firstTab, 0);
  839. },
  840. /**
  841. * Create tab menu item
  842. * @private
  843. * @param {String} - `add` or `edit`
  844. * @param {Number} - Disable tab when 0
  845. * @return {HTMLDOMElement} - created HTML tab-menu element
  846. */
  847. addMenuItem: function (tabName, disableTab) {
  848. var popupDiv = this.popup.container,
  849. className = PREFIX + 'tab-item',
  850. lang = this.lang,
  851. menuItem;
  852. if (disableTab === 0) {
  853. className += ' ' + PREFIX + 'tab-disabled';
  854. }
  855. // tab 1
  856. menuItem = createElement(
  857. SPAN,
  858. {
  859. innerHTML: lang[tabName + 'Button'] || tabName,
  860. className: className
  861. },
  862. null,
  863. popupDiv
  864. );
  865. menuItem.setAttribute(PREFIX + 'data-tab-type', tabName);
  866. return menuItem;
  867. },
  868. /**
  869. * Create tab content
  870. * @private
  871. * @return {HTMLDOMElement} - created HTML tab-content element
  872. */
  873. addContentItem: function () {
  874. var popupDiv = this.popup.container;
  875. return createElement(
  876. DIV,
  877. {
  878. className: PREFIX + 'tab-item-content'
  879. },
  880. null,
  881. popupDiv
  882. );
  883. },
  884. /**
  885. * Add click event to each tab
  886. * @private
  887. * @param {Number} - Disable tab when 0
  888. */
  889. switchTabs: function (disableTab) {
  890. var _self = this,
  891. popupDiv = this.popup.container,
  892. tabs = popupDiv.querySelectorAll('.' + PREFIX + 'tab-item'),
  893. dataParam;
  894. tabs.forEach(function (tab, i) {
  895. dataParam = tab.getAttribute(PREFIX + 'data-tab-type');
  896. if (dataParam === 'edit' && disableTab === 0) {
  897. return;
  898. }
  899. ['click', 'touchstart'].forEach(function (eventName) {
  900. addEvent(tab, eventName, function () {
  901. // reset class on other elements
  902. _self.tabs.deselectAll.call(_self);
  903. _self.tabs.selectTab.call(_self, this, i);
  904. });
  905. });
  906. });
  907. },
  908. /**
  909. * Set tab as visible
  910. * @private
  911. * @param {HTMLDOMElement} - current tab
  912. * @param {Number} - Index of tab in menu
  913. */
  914. selectTab: function (tab, index) {
  915. var allTabs = this.popup.container
  916. .querySelectorAll('.' + PREFIX + 'tab-item-content');
  917. tab.className += ' ' + PREFIX + 'tab-item-active';
  918. allTabs[index].className += ' ' + PREFIX + 'tab-item-show';
  919. },
  920. /**
  921. * Set all tabs as invisible.
  922. * @private
  923. */
  924. deselectAll: function () {
  925. var popupDiv = this.popup.container,
  926. tabs = popupDiv
  927. .querySelectorAll('.' + PREFIX + 'tab-item'),
  928. tabsContent = popupDiv
  929. .querySelectorAll('.' + PREFIX + 'tab-item-content'),
  930. i;
  931. for (i = 0; i < tabs.length; i++) {
  932. tabs[i].classList.remove(PREFIX + 'tab-item-active');
  933. tabsContent[i].classList.remove(PREFIX + 'tab-item-show');
  934. }
  935. }
  936. }
  937. };
  938. addEvent(H.NavigationBindings, 'showPopup', function (config) {
  939. if (!this.popup) {
  940. // Add popup to main container
  941. this.popup = new H.Popup(
  942. this.chart.container, (
  943. this.chart.options.navigation.iconsURL ||
  944. (
  945. this.chart.options.stockTools &&
  946. this.chart.options.stockTools.gui.iconsURL
  947. ) ||
  948. 'https://code.highcharts.com/8.0.0/gfx/stock-icons/'
  949. )
  950. );
  951. }
  952. this.popup.showForm(
  953. config.formType,
  954. this.chart,
  955. config.options,
  956. config.onSubmit
  957. );
  958. });
  959. addEvent(H.NavigationBindings, 'closePopup', function () {
  960. if (this.popup) {
  961. this.popup.closePopup();
  962. }
  963. });