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