Home Reference Source

cables_dev/cables_ui/src/ui/elements/tabpanel/tabpanel.js

  1. import { Events, Logger, ele, TalkerAPI } from "cables-shared-client";
  2. import { utils } from "cables";
  3. import { getHandleBarHtml } from "../../utils/handlebars.js";
  4. import { notify, notifyError } from "../notification.js";
  5. import { gui } from "../../gui.js";
  6. import { platform } from "../../platform.js";
  7. import { contextMenu } from "../contextmenu.js";
  8. import { editorSession } from "./editor_session.js";
  9. import { userSettings } from "../../components/usersettings.js";
  10. import Tab from "./tab.js";
  11. import { DomEvents } from "../../theme.js";
  12. /**
  13. * @typedef TabPanelOptions
  14. * @property {String} [name]
  15. * @property {boolean} [closable]
  16. * @property {boolean} [noUserSetting] - does not store last opened tab in userSettings
  17. */
  18. /** Tab panel that contains tabs. */
  19. export default class TabPanel extends Events
  20. {
  21. static EVENT_RESIZE = "resize";
  22. static EVENT_TAB_REMOVED = "onTabRemoved";
  23. static EVENT_TAB_ADDED = "onTabAdded";
  24. /** @type {TabPanelOptions} */
  25. #options;
  26. #eleId;
  27. /** @type {Tab[]} */
  28. tabs = [];
  29. /**
  30. * @param {String} eleId
  31. * @param {TabPanelOptions} options
  32. */
  33. constructor(eleId, options = {})
  34. {
  35. super();
  36. this._log = new Logger("TabPanel " + eleId);
  37. this.#options = options;
  38. this.id = utils.uuid();
  39. this.#eleId = eleId;
  40. this._eleContentContainer = null;
  41. this._eleTabPanel = null;
  42. this.showTabListButton = false;
  43. this._dynCmds = [];
  44. this._eleTabPanel = document.createElement("div");
  45. this._eleTabPanel.classList.add("tabpanel");
  46. this._eleTabPanel.innerHTML = "";
  47. const el = ele.byId(this.#eleId);
  48. if (!el)
  49. {
  50. this._log.error("could not find ele " + this.#eleId);
  51. return;
  52. }
  53. el.appendChild(this._eleTabPanel);
  54. this._eleContentContainer = document.createElement("div");
  55. this._eleContentContainer.classList.add("contentcontainer");
  56. this._eleContentContainer.innerHTML = "";
  57. el.appendChild(this._eleContentContainer);
  58. this.on(TabPanel.EVENT_RESIZE, () =>
  59. {
  60. for (let i = 0; i < this.tabs.length; i++) this.tabs[i].emitEvent(Tab.EVENT_RESIZE);
  61. });
  62. }
  63. /**
  64. * @param {string} title
  65. * @returns {string}
  66. */
  67. getUniqueTitle(title)
  68. {
  69. const existingTab = this.getTabByTitle(title);
  70. let count = 0;
  71. while (existingTab)
  72. {
  73. count++;
  74. if (!this.getTabByTitle(title + " (" + count + ")")) break;
  75. }
  76. if (count > 0)
  77. title = title + " (" + count + ")";
  78. return title;
  79. }
  80. updateHtml()
  81. {
  82. let html = "";
  83. html += getHandleBarHtml("tabpanel_bar", { "id": this.id, "tabs": this.tabs });
  84. this._eleTabPanel.innerHTML = html;
  85. const editortabList = document.getElementById("editortabList" + this.id);
  86. if (!editortabList)
  87. {
  88. this._log.warn("no editortabList?!?");
  89. return;
  90. }
  91. if (!this.showTabListButton)
  92. {
  93. editortabList.style.display = "none";
  94. editortabList.parentElement.style["padding-left"] = "0";
  95. }
  96. else
  97. {
  98. editortabList.parentElement.style["padding-left"] = "34px";
  99. editortabList.style.display = "block";
  100. editortabList.addEventListener(
  101. DomEvents.POINTER_DOWN,
  102. (e) =>
  103. {
  104. const items = [];
  105. for (let i = 0; i < this.tabs.length; i++)
  106. {
  107. const tab = this.tabs[i];
  108. items.push({
  109. "title": tab.options.name,
  110. "func": () => { this.activateTab(tab.id); }
  111. });
  112. }
  113. contextMenu.show(
  114. {
  115. "items": items
  116. }, e.target);
  117. }
  118. );
  119. }
  120. for (let i = 0; i < this._dynCmds.length; i++) gui.cmdPalette.removeDynamic(this._dynCmds[i]);
  121. for (let i = 0; i < this.tabs.length; i++)
  122. {
  123. if (window.gui && this.#eleId == "maintabs")
  124. {
  125. const t = this.tabs[i];
  126. const cmd = gui.cmdPalette.addDynamic("tab", "Tab " + t.title, () =>
  127. {
  128. gui.maintabPanel.show(true);
  129. this.activateTab(t.id, true);
  130. }, t.icon || "edit");
  131. this._dynCmds.push(cmd);
  132. }
  133. // ----------------
  134. ele.clickable(ele.byId("editortab" + this.tabs[i].id), (e) =>
  135. {
  136. if (e.target.dataset.id) this.activateTab(e.target.dataset.id, true);
  137. });
  138. if (this.tabs[i].options.closable)
  139. {
  140. document.getElementById("editortab" + this.tabs[i].id).addEventListener(
  141. "pointerdown",
  142. function (e)
  143. {
  144. if (e.button == 1) if (e.target.dataset.id) this.closeTab(e.target.dataset.id);
  145. }.bind(this)
  146. );
  147. }
  148. if (document.getElementById("closetab" + this.tabs[i].id))
  149. {
  150. document.getElementById("closetab" + this.tabs[i].id).addEventListener(
  151. "pointerdown",
  152. function (e)
  153. {
  154. this.closeTab(e.target.dataset.id);
  155. }.bind(this)
  156. );
  157. }
  158. }
  159. this.scrollToActiveTab();
  160. }
  161. /**
  162. * @param {string} name
  163. */
  164. activateTabByName(name)
  165. {
  166. name = name || "";
  167. let found = false;
  168. let tab = null;
  169. for (let i = 0; i < this.tabs.length; i++)
  170. {
  171. if (this.tabs[i].title.toLowerCase() === name.toLowerCase() ||
  172. (this.tabs[i].options.name || "").toLowerCase() === name.toLowerCase())
  173. {
  174. tab = this.tabs[i];
  175. this.activateTab(tab.id);
  176. found = true;
  177. }
  178. else this.tabs[i].deactivate();
  179. }
  180. if (!found) this._log.log("[activateTabByName] could not find tab", name);
  181. this.updateHtml();
  182. return tab;
  183. }
  184. scrollToActiveTab()
  185. {
  186. const tab = this.getActiveTab();
  187. const w = this._eleTabPanel.clientWidth;
  188. if (!tab) return;
  189. let left = document.getElementById("editortab" + tab.id).offsetLeft;
  190. left += document.getElementById("editortab" + tab.id).clientWidth;
  191. left += 25;
  192. const tabContainer = document.querySelector("#maintabs .tabs");
  193. if (tabContainer && left > w) tabContainer.scrollLeft = left;
  194. }
  195. /**
  196. * @param {string} id
  197. */
  198. activateTab(id)
  199. {
  200. let found = null;
  201. for (let i = 0; i < this.tabs.length; i++)
  202. {
  203. if (this.tabs[i].id === id)
  204. {
  205. found = this.tabs[i];
  206. this.emitEvent("onTabActivated", this.tabs[i]);
  207. this.tabs[i].activate();
  208. }
  209. }
  210. if (found)
  211. for (let i = 0; i < this.tabs.length; i++)
  212. if (this.tabs[i].id != id)
  213. this.tabs[i].deactivate();
  214. this.updateHtml();
  215. if (editorSession && editorSession.loaded() && gui.finishedLoading()) this.saveCurrentTabUsersettings();
  216. return found;
  217. }
  218. loadCurrentTabUsersettings()
  219. {
  220. if (this.#options.noUserSetting) return;
  221. let found = false;
  222. for (let i = 0; i < this.tabs.length; i++)
  223. {
  224. if (userSettings.get("tabsLastTitle_" + this.#eleId) == this.tabs[i].title)
  225. {
  226. this.activateTab(this.tabs[i].id);
  227. found = true;
  228. break;
  229. }
  230. }
  231. }
  232. saveCurrentTabUsersettings()
  233. {
  234. if (this.#options.noUserSetting) return;
  235. const activeTab = this.getActiveTab();
  236. if (!activeTab) return;
  237. userSettings.set("tabsLastTitle_" + this.#eleId, activeTab.title);
  238. }
  239. /**
  240. * @param {string} dataId
  241. */
  242. getTabByDataId(dataId)
  243. {
  244. for (let i = 0; i < this.tabs.length; i++) if (this.tabs[i].dataId == dataId) return this.tabs[i];
  245. }
  246. /**
  247. * @param {string} title
  248. * @param {boolean} [noexact]
  249. */
  250. getTabByTitle(title, noexact = false)
  251. {
  252. if (noexact)
  253. {
  254. for (let i = 0; i < this.tabs.length; i++) if (this.tabs[i].title.includes(title)) return this.tabs[i];
  255. }
  256. else
  257. {
  258. for (let i = 0; i < this.tabs.length; i++) if (this.tabs[i].title == title) return this.tabs[i];
  259. }
  260. }
  261. /**
  262. * @param {string} id
  263. */
  264. getTabById(id)
  265. {
  266. for (let i = 0; i < this.tabs.length; i++) if (this.tabs[i].id == id) return this.tabs[i];
  267. }
  268. closeAllTabs()
  269. {
  270. while (this.tabs.length) this.closeTab(this.tabs[0].id);
  271. }
  272. /**
  273. * @param {string} id
  274. */
  275. closeTab(id)
  276. {
  277. let tab = null;
  278. let idx = 0;
  279. for (let i = 0; i < this.tabs.length; i++)
  280. {
  281. if (this.tabs[i].id == id)
  282. {
  283. tab = this.tabs[i];
  284. // tab.emitEvent("close");
  285. this.tabs.splice(i, 1);
  286. idx = i;
  287. break;
  288. }
  289. }
  290. if (!tab) return;
  291. this.emitEvent(TabPanel.EVENT_TAB_REMOVED, tab);
  292. tab.remove();
  293. if (idx > this.tabs.length - 1) idx = this.tabs.length - 1;
  294. if (this.tabs[idx]) this.activateTab(this.tabs[idx].id);
  295. this.updateHtml();
  296. }
  297. /**
  298. * @param {string} id
  299. * @param {boolean} changed
  300. */
  301. setChanged(id, changed)
  302. {
  303. if (this.getTabById(id)) this.getTabById(id).options.wasChanged = changed;
  304. this.updateHtml();
  305. }
  306. /**
  307. * @param {number} num
  308. */
  309. setTabNum(num)
  310. {
  311. const tab = this.tabs[Math.min(this.tabs.length, num)];
  312. this.activateTab(tab.id);
  313. }
  314. /**
  315. * @returns {number}
  316. */
  317. getNumTabs()
  318. {
  319. return this.tabs.length;
  320. }
  321. /**
  322. * @returns {Tab}
  323. */
  324. cycleActiveTab()
  325. {
  326. if (this.tabs.length <= 1) return;
  327. for (let i = 1; i < this.tabs.length; i++)
  328. if (this.tabs[i - 1].active)
  329. return this.activateTab(this.tabs[i].id);
  330. return this.activateTab(this.tabs[0].id);
  331. }
  332. /**
  333. * @returns {Tab}
  334. */
  335. getActiveTab()
  336. {
  337. for (let i = 0; i < this.tabs.length; i++) if (this.tabs[i].active) return this.tabs[i];
  338. }
  339. updateSize()
  340. {
  341. for (let i = 0; i < this.tabs.length; i++) this.tabs[i].updateSize();
  342. }
  343. getSaveButton()
  344. {
  345. const t = this.getActiveTab();
  346. if (!t) return;
  347. const b = t.getSaveButton();
  348. if (b) return b;
  349. }
  350. /**
  351. * @param {Tab} tab
  352. * @param {boolean} [activate]
  353. * @returns {Tab}
  354. */
  355. addTab(tab, activate)
  356. {
  357. if (tab.options.singleton)
  358. {
  359. const t = this.getTabByTitle(tab.title);
  360. if (t)
  361. {
  362. this.activateTab(t.id);
  363. this.emitEvent(TabPanel.EVENT_TAB_ADDED, t, true);
  364. if (activate) this.activateTab(t.id);
  365. return t;
  366. }
  367. }
  368. tab.initHtml(this._eleContentContainer);
  369. this.tabs.push(tab);
  370. if (activate) this.activateTab(tab.id);
  371. this.updateHtml();
  372. this.emitEvent(TabPanel.EVENT_TAB_ADDED, tab, false);
  373. return tab;
  374. }
  375. /**
  376. * @param {String} title
  377. * @param {String} url
  378. * @param {Object} options
  379. * @param {boolean} userInteraction
  380. * @returns {Tab}
  381. */
  382. addIframeTab(title, url, options, userInteraction)
  383. {
  384. const iframeTab = this.addTab(new CABLES.UI.Tab(title, options));
  385. const id = utils.uuid();
  386. const src = new URL(url);
  387. src.searchParams.append("frameid", id);
  388. let html = "<div class=\"loading\" id=\"loading" + id + "\" style=\"position:absolute;left:45%;top:34%\"></div>";
  389. html += "<iframe id=\"iframe" + id + "\" ";
  390. html += " src=\"" + src + "\"";
  391. html += " allow=\"clipboard-write\" style=\"border:none;width:100%;height:100%\"";
  392. html += " onload=\"ele.byId('loading" + id + "').style.display='none'; if(ele.byQuery('#refresh" + id + " .icon-refresh')) { ele.byQuery('#refresh" + id + " .icon-refresh').classList.remove('rotate');}\"";
  393. html += "></iframe";
  394. iframeTab.contentEle.innerHTML = html;
  395. iframeTab.contentEle.style.padding = "0px";
  396. let buttons = "";
  397. let uri = url;
  398. if (options.gotoUrl)uri = options.gotoUrl;
  399. buttons += "<a class=\"button-small \" href=\"" + uri + "\" target=\"_blank\"><span class=\"icon nomargin icon-external\"></span></a>&nbsp;";
  400. buttons += "<a class=\"button-small \" id=\"refresh" + id + "\"><span class=\"icon nomargin icon-refresh\"></span></a>";
  401. iframeTab.toolbarEle.innerHTML = buttons;
  402. const iframe = ele.byId("iframe" + id);
  403. if (iframe) iframe.dataset.currentTab = "";
  404. const hashUpdate = (e) =>
  405. {
  406. if (e.data?.hash && e.data?.frameid && e.data.frameid === id)
  407. {
  408. if (iframe) iframe.dataset.currentTab = e.data.hash.replace("#", "");
  409. }
  410. };
  411. window.addEventListener("message", hashUpdate);
  412. iframeTab.on(Tab.EVENT_CLOSE, () =>
  413. {
  414. window.removeEventListener("message", hashUpdate);
  415. });
  416. const refreshButton = ele.byId("refresh" + id);
  417. ele.clickable(refreshButton, () =>
  418. {
  419. if (iframe)
  420. {
  421. const icon = ele.byQuery("#refresh" + id + " .icon-refresh");
  422. if (icon) icon.classList.add("rotate");
  423. const newsrc = new URL(iframe.src);
  424. newsrc.searchParams.set("frameid", id);
  425. newsrc.searchParams.set("nc", String(Date.now()));
  426. if (iframe.dataset.currentTab) newsrc.hash = iframe.dataset.currentTab;
  427. iframe.src = newsrc.toString();
  428. }
  429. });
  430. const frame = document.getElementById("iframe" + id);
  431. const talkerAPI = new CABLESUILOADER.TalkerAPI(frame.contentWindow);
  432. talkerAPI.on(TalkerAPI.CMD_UI_SET_SAVED_STATE, (opts) =>
  433. {
  434. if (opts.state)
  435. {
  436. gui.savedState.setSaved("talkerAPI", opts.subpatch);
  437. }
  438. else
  439. {
  440. gui.savedState.setUnSaved("talkerAPI", opts.subpatch);
  441. }
  442. });
  443. talkerAPI.on(TalkerAPI.CMD_UI_SETTING_MANUAL_SCREENSHOT, (opts, _next) =>
  444. {
  445. platform.setManualScreenshot(opts.manualScreenshot);
  446. });
  447. talkerAPI.on(TalkerAPI.CMD_UI_SAVE_SCREENSHOT, (_opts, _next) =>
  448. {
  449. gui.patchView.store.saveScreenshot(true, () =>
  450. {
  451. talkerAPI.send(TalkerAPI.EVENT_SCREENSHOT_SAVED);
  452. });
  453. });
  454. talkerAPI.on(TalkerAPI.CMD_UI_NOTIFY, (opts, next) =>
  455. {
  456. notify(opts.msg, opts.text, opts.options);
  457. });
  458. talkerAPI.on(TalkerAPI.CMD_UI_NOTIFY_ERROR, (opts, next) =>
  459. {
  460. notifyError(opts.msg, opts.text, opts.options);
  461. });
  462. talkerAPI.on(TalkerAPI.CMD_UI_UPDATE_PATCH_NAME, (opts, next) =>
  463. {
  464. gui.setProjectName(opts.name);
  465. gui.patchParamPanel.show(true);
  466. // send this back to have the title of the patch-editor iframe updated
  467. platform.talkerAPI.send(TalkerAPI.CMD_UPDATE_PATCH_NAME, opts);
  468. });
  469. talkerAPI.on(TalkerAPI.CMD_UI_OPS_DELETED, (opts, next) =>
  470. {
  471. const opdocs = gui.opDocs.getAll();
  472. const deletedOps = opts.ops || [];
  473. for (let i = 0; i < deletedOps.length; i++)
  474. {
  475. const deletedOp = deletedOps[i];
  476. const opDocToDelete = opdocs.findIndex((opDoc) => { return opDoc.id === deletedOp.id; });
  477. if (opDocToDelete) opdocs.splice(opDocToDelete, 1);
  478. gui.opSelect().reload();
  479. }
  480. let plural = deletedOps.length > 1 ? "s" : "";
  481. if (deletedOps.length > 0) notify("deleted " + deletedOps.length + " op" + plural);
  482. this.closeTab(iframeTab.id);
  483. });
  484. this.activateTab(iframeTab.id);
  485. gui.maintabPanel.show(userInteraction);
  486. return iframeTab;
  487. }
  488. }