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