ccm.spreadsheet-1.0.0.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756
  1. (function () {
  2. let component = {
  3. name: "spreadsheet",
  4. version: [1,0,0],
  5. ccm: "https://ccmjs.github.io/ccm/versions/ccm-20.0.0.js",
  6. config: {
  7. css: ["ccm.load", { url: "https://git.cyber-everything.de/Alexander/alysek-components/raw/master/spreadsheet/resources/css/spreadsheet.css", type: "css" }],
  8. mathjs: ["ccm.load", { url: "https://unpkg.com/mathjs@6.3.0/dist/math.min.js", type: "js"}],
  9. rxCore: ["ccm.load", { url: "https://dev.jspm.io/rxjs@6/_esm2015/index", type: "module" }],
  10. rxOps: ["ccm.load", { url: "https://dev.jspm.io/rxjs@6/_esm2015/operators", type: "module" }]
  11. },
  12. Instance: function () {
  13. const self = this;
  14. this.init = async () => {
  15. console.log(math);
  16. console.log(this.rxCore);
  17. console.log(this.rxOps);
  18. };
  19. this.start = async () => {
  20. const spreadsheetController = new SpreadsheetController(self, this.element);
  21. };
  22. }
  23. };
  24. const delimiter = '|';
  25. const rangeOperator = ':';
  26. const cellKeyRegex = /([A-Za-z]+)([0-9]+)/;
  27. const getColumnIdFromIndex = function(index) {
  28. if(index === 0) return '0';
  29. let rest, columnId = '';
  30. while(index > 0) {
  31. rest = (index - 1) % 26;
  32. columnId = String.fromCharCode(65 + rest) + columnId;
  33. index = (index - rest - 1) / 26;
  34. }
  35. return columnId;
  36. };
  37. const getIndexFromColumnId = function(columnId) {
  38. return Array.from(columnId).reduce( (index, char, pos, array) => { return index + ((char.charCodeAt(0) - 64) * Math.pow(26, array.length - pos - 1)); }, 0 );
  39. };
  40. const getCoordObjectFromKey = function(key) {
  41. const keyPairs = key.split(delimiter);
  42. return { x: parseInt(keyPairs[0]), y: parseInt(keyPairs[1]) };
  43. };
  44. const compareCellId = function(cellA, cellB) {
  45. if(cellA.coords.x < cellB.coords.x || cellA.coords.y < cellB.coords.y) {
  46. return -1;//A < B
  47. } else if(cellA.coords.x > cellB.coords.x || cellA.coords.y > cellB.coords.y) {
  48. return 1;//A > B
  49. } else {
  50. return 0;//A = B
  51. }
  52. };
  53. const cellIdToCellCoords = function(cellId) {
  54. const groups = cellId.match(cellKeyRegex);
  55. const x = getIndexFromColumnId(groups[1]);
  56. const y = parseInt(groups[2]);
  57. return {x: x, y: y};
  58. };
  59. const sum = function(cells) {
  60. return '=' + cells.map( cell => cell.id).join('+');
  61. };
  62. const product = function(cells) {
  63. return '=' + cells.map( cell => cell.id).join('*');
  64. };
  65. const average = function(cells) {
  66. return '=(' + cells.map( cell => cell.id).join('+') + ')/' + cells.length;
  67. };
  68. class SpreadsheetController {
  69. constructor(self, htmlRootElement) {
  70. this.self = self;
  71. this.rxCore = self.rxCore;
  72. this.rxOps = self.rxOps;
  73. this.cellStorage = new CellStorage(this);
  74. this.rootEle = htmlRootElement;
  75. this.maxRowCount = 100 + 1;
  76. this.maxColumnCount = 50 + 1;
  77. this.cellRangeSelectionController = new CellRangeSelectionController(this);
  78. this.formulaController = new FormulaController(this);
  79. this.spreadsheetView = new SpreadsheetView(this, htmlRootElement);
  80. this.formulaSubject = new this.rxCore.Subject();
  81. this.actionSubject = new this.rxCore.Subject();
  82. //this.navigationObservable = new this.rxCore.Observable();
  83. this.evaluationSubject = new this.rxCore.Subject();
  84. this.formulaController.initialize();
  85. this.cellRangeSelectionController.initialize(this.cellStorage.getCells({start: 'A1', end: 'A1'})[0]);
  86. this.initializeActionSubject();
  87. this.initializeEvaluationSubject();
  88. }
  89. initializeActionSubject() {
  90. const sumStream = this.rxCore.fromEvent(this.spreadsheetView.getSumBtnDiv(), 'click').pipe(
  91. this.rxOps.map(clickEvent => sum)
  92. );
  93. const productStream = this.rxCore.fromEvent(this.spreadsheetView.getProductBtnDiv(), 'click').pipe(
  94. this.rxOps.map(clickEvent => product)
  95. );
  96. const averageStream = this.rxCore.fromEvent(this.spreadsheetView.getAverageBtnDiv(), 'click').pipe(
  97. this.rxOps.map(clickEvent => average)
  98. );
  99. const actionStream = this.rxCore.merge(sumStream, productStream, averageStream);
  100. const actionAndTargetStream = this.actionSubject.pipe(
  101. this.rxOps.observeOn(this.rxCore.asyncScheduler),
  102. this.rxOps.withLatestFrom(this.cellRangeSelectionController.selectionSubject),
  103. this.rxOps.map( array => { array.push(this.getResultCell(array[1])); return array; }),
  104. this.rxOps.filter(array => array.length > 2 && array[2] !== undefined && array[2] !== null),
  105. this.rxOps.map( array => {
  106. const callback = array[0];
  107. const cells = array[1];
  108. const resultCell = array[2];
  109. const inputString = callback(cells);
  110. resultCell.handleInput({input: inputString});
  111. return resultCell;
  112. })
  113. );
  114. actionStream.subscribe(actionAndTargetStream);
  115. actionAndTargetStream.subscribe({
  116. next: cell => this.evaluationSubject.next(cell),
  117. error: () => console.log('Error in actionAndTarget Stream')
  118. });
  119. }
  120. getResultCell(cells) {
  121. if(cells.length > 1) {
  122. return this.cellStorage.getResultCell(cells[0], cells[cells.length-1]);
  123. } else {
  124. return this.cellStorage.getResultCell(cells[cells.length-1]);
  125. }
  126. }
  127. initializeEvaluationSubject() {
  128. this.evaluationSubject.pipe(
  129. this.rxOps.filter(cell => cell.isFormula && !cell.isError),
  130. this.rxOps.observeOn(this.rxCore.asyncScheduler)
  131. ).subscribe({
  132. next: cell => cell.evaluate(),
  133. error: () => console.log(`Error in evaluation observer!`)
  134. });
  135. }
  136. }
  137. class CellRangeSelectionController {
  138. constructor(spreadsheetController) {
  139. this.controller = spreadsheetController;
  140. this.rxCore = this.controller.rxCore;
  141. this.rxOps = this.controller.rxOps;
  142. this.selectionSubject = null;
  143. this.cellRangeDiv = null;
  144. this.cellStart = null;
  145. this.cellEnd = null;
  146. this.initializeSelectionSubject();
  147. }
  148. initializeSelectionSubject() {
  149. this.selectionSubject = new this.rxCore.Subject().pipe(
  150. this.rxOps.map( range => {
  151. return this.controller.cellStorage.getCells(range);
  152. })
  153. );
  154. }
  155. initialize(initiallySelectedCell) {
  156. this.cellRangeDiv = this.controller.spreadsheetView.getSelectedCellRangeDiv();
  157. this.handleInput({shiftKey: false}, initiallySelectedCell);
  158. }
  159. handleInput(clickEvent, targetCell) {
  160. if(clickEvent.shiftKey) {
  161. this.cellEnd = targetCell;
  162. } else {
  163. this.cellStart = targetCell;
  164. this.cellEnd = null;
  165. }
  166. if(this.cellEnd !== null) {
  167. if(compareCellId(this.cellStart, this.cellEnd) === 0) {
  168. this.cellEnd = null;
  169. } else if(compareCellId(this.cellStart, this.cellEnd) === 1) {
  170. const cellTmp = this.cellEnd;
  171. this.cellEnd = this.cellStart;
  172. this.cellStart = cellTmp;
  173. }
  174. }
  175. this.handleOutput();
  176. }
  177. handleOutput() {
  178. if(this.cellEnd === null) {
  179. this.cellRangeDiv.innerText = this.cellStart.id;
  180. this.selectionSubject.next({start: this.cellStart.id, end: this.cellStart.id});
  181. } else {
  182. this.cellRangeDiv.innerText = this.cellStart.id + rangeOperator + this.cellEnd.id;
  183. this.selectionSubject.next({start: this.cellStart.id, end: this.cellEnd.id});
  184. }
  185. }
  186. }
  187. class FormulaController {
  188. constructor(spreadsheetController) {
  189. this.controller = spreadsheetController;
  190. this.rxCore = this.controller.rxCore;
  191. this.rxOps = this.controller.rxOps;
  192. this.formulaDiv = null;
  193. this.selectedCell = null;
  194. }
  195. initialize() {
  196. this.formulaDiv = this.controller.spreadsheetView.getFormulaDiv();
  197. this.controller.cellRangeSelectionController.selectionSubject.subscribe({
  198. next: selection => {
  199. this.selectedCell = selection[0];
  200. this.formulaDiv.innerText = this.selectedCell.getFormulaOutput();
  201. },
  202. error: () => console.log(`Error in CellRangeSelectionQueue`)
  203. });
  204. this.setupEvaluationTrigger();
  205. this.setupInputHandler();
  206. }
  207. setupEvaluationTrigger() {
  208. const enterKeydownEvent = this.rxCore.fromEvent(this.formulaDiv, 'keydown').pipe(
  209. this.rxOps.filter(event => event.keyCode === 13),
  210. this.rxOps.tap(event => event.preventDefault())
  211. );
  212. const deselectEvent = this.rxCore.fromEvent(this.formulaDiv, 'blur');
  213. const evaluationTriggerEvents = this.rxCore.merge(enterKeydownEvent, deselectEvent).pipe(
  214. this.rxOps.debounce( () => this.rxCore.timer(50) )
  215. );
  216. const evaluationSubscription = evaluationTriggerEvents.subscribe({
  217. next: (event) => {
  218. if(this.selectedCell !== null && this.selectedCell.isFormula)
  219. this.controller.evaluationSubject.next(this.selectedCell);
  220. },
  221. error: () => console.log(`Error in FormulaEvaluationTrigger`)
  222. });
  223. }
  224. setupInputHandler() {
  225. this.rxCore.fromEvent(this.formulaDiv, 'input').subscribe(
  226. event => {
  227. if(this.selectedCell !== null)
  228. this.selectedCell.inputSubject.next({target: this.formulaDiv, event: event});
  229. }
  230. );
  231. }
  232. setFormulaText(text) {
  233. this.formulaDiv.innerText = text;
  234. }
  235. }
  236. class SpreadsheetView {
  237. constructor(spreadsheetController, htmlRootElement) {
  238. this.controller = spreadsheetController;
  239. this.rxCore = this.controller.rxCore;
  240. this.rxOps = this.controller.rxOps;
  241. this.rootEle = htmlRootElement;
  242. this.tableDiv = null;
  243. this.selectedCellRangeDiv = null;
  244. this.formulaDiv = null;
  245. this.sumBtnDiv = null;
  246. this.productBtnDiv = null;
  247. this.averageBtnDiv = null;
  248. this.createControlBar();
  249. this.createSpreadsheet();
  250. }
  251. getSelectedCellRangeDiv() {
  252. return this.selectedCellRangeDiv;
  253. }
  254. getFormulaDiv() {
  255. return this.formulaDiv;
  256. }
  257. getSumBtnDiv() {
  258. return this.sumBtnDiv;
  259. }
  260. getProductBtnDiv() {
  261. return this.productBtnDiv;
  262. }
  263. getAverageBtnDiv() {
  264. return this.averageBtnDiv;
  265. }
  266. createControlBar() {
  267. const controlBarContainerDiv = document.createElement('div');
  268. controlBarContainerDiv.className = "controlBarContainer";
  269. const actionBarContainerDiv = this.createActionBar();
  270. controlBarContainerDiv.appendChild(actionBarContainerDiv);
  271. const formulaBarContainerDiv = this.createFormulaBar();
  272. controlBarContainerDiv.appendChild(formulaBarContainerDiv);
  273. this.rootEle.appendChild(controlBarContainerDiv);
  274. }
  275. createActionBar() {
  276. const actionBarDiv = document.createElement('div');
  277. actionBarDiv.className = "barContainer";
  278. this.sumBtnDiv = this.createActionButton('Summe');
  279. this.productBtnDiv = this.createActionButton('Produkt');
  280. this.averageBtnDiv = this.createActionButton('Mittelwert');
  281. actionBarDiv.appendChild(this.sumBtnDiv);
  282. actionBarDiv.appendChild(this.productBtnDiv);
  283. actionBarDiv.appendChild(this.averageBtnDiv);
  284. return actionBarDiv;
  285. }
  286. createActionButton(actionName) {
  287. const actionButton = document.createElement('div');
  288. actionButton.className = "content static action";
  289. actionButton.innerText = actionName;
  290. return actionButton;
  291. }
  292. createFormulaBar() {
  293. const formulaBarDiv = document.createElement('div');
  294. formulaBarDiv.className = "barContainer";
  295. this.selectedCellRangeDiv = document.createElement('div');
  296. this.selectedCellRangeDiv.id = "selectedCellRange";
  297. this.selectedCellRangeDiv.className = "content static";
  298. this.selectedCellRangeDiv.innerText = 'A1';
  299. const functionSignDiv = document.createElement('div');
  300. functionSignDiv.id = "formulaSign";
  301. functionSignDiv.className = "content static";
  302. functionSignDiv.style.fontStyle = "oblique";
  303. functionSignDiv.style.color = "grey";
  304. functionSignDiv.innerText = "fx";
  305. this.formulaDiv = document.createElement('div');
  306. this.formulaDiv.id = "formula";
  307. this.formulaDiv.className = "content dynamic";
  308. this.formulaDiv.contentEditable = true;
  309. formulaBarDiv.appendChild(this.selectedCellRangeDiv);
  310. formulaBarDiv.appendChild(functionSignDiv);
  311. formulaBarDiv.appendChild(this.formulaDiv);
  312. return formulaBarDiv;
  313. }
  314. createSpreadsheet() {
  315. const tableContainerDiv = document.createElement('div');
  316. tableContainerDiv.id = "tableContainer";
  317. this.rxCore.range(0, this.controller.maxRowCount).pipe(
  318. this.rxOps.map( rowIndex => {
  319. let tmpRowDiv = null;
  320. this.rxCore.range(0, this.controller.maxColumnCount).pipe(
  321. this.rxOps.map( columnIndex => {
  322. return this.createCellDiv(columnIndex, rowIndex);
  323. }),
  324. this.rxOps.reduce( (tableRowDiv, cellDiv) => {
  325. tableRowDiv.appendChild(cellDiv);
  326. return tableRowDiv;
  327. }, this.createTableRowDiv())
  328. ).subscribe( value => { tmpRowDiv = value; });
  329. return tmpRowDiv;
  330. }),
  331. this.rxOps.reduce( (tableDiv, tableRowDiv) => {
  332. tableDiv.appendChild(tableRowDiv);
  333. return tableDiv;
  334. }, this.createTableDiv())
  335. ).subscribe(value => { this.tableDiv = value; });
  336. const cellDivs = Array.from(this.tableDiv.childNodes).reduce( (flat, row) => {
  337. return flat.concat(...row.childNodes);
  338. }, [] );
  339. cellDivs.filter(this.isDataCell).map( dataCell => {
  340. dataCell.contentEditable = true;
  341. this.controller.cellStorage.createCell(dataCell.getAttribute('key'), dataCell);
  342. });
  343. cellDivs.filter(this.isRowIndexCellDiv).map( rowIndexCell => {
  344. rowIndexCell.innerText = rowIndexCell.getAttribute('key').split(delimiter)[1];
  345. rowIndexCell.style = "resize: vertical;";
  346. rowIndexCell.className += " indexCell";
  347. });
  348. cellDivs.filter(this.isColumnIndexCellDiv).map( columnIndexCell => {
  349. columnIndexCell.innerText = getColumnIdFromIndex(
  350. parseInt(
  351. columnIndexCell.getAttribute('key').split(delimiter)[0]
  352. )
  353. );
  354. columnIndexCell.style = "resize: horizontal;";
  355. columnIndexCell.className += " indexCell";
  356. });
  357. const topLeftCellDiv = this.tableDiv.firstChild.firstChild;
  358. topLeftCellDiv.innerText = "";
  359. topLeftCellDiv.style = "";
  360. topLeftCellDiv.style.backgroundColor = "grey";
  361. tableContainerDiv.appendChild(this.tableDiv);
  362. this.rootEle.appendChild(tableContainerDiv);
  363. }
  364. isDataCell(cellDiv) {
  365. const keyValueArray = cellDiv.getAttribute('key').split(delimiter);
  366. return parseInt(keyValueArray[0]) > 0 && parseInt(keyValueArray[1]) > 0;
  367. }
  368. isRowIndexCellDiv(cellDiv) {
  369. return cellDiv.getAttribute('key').split(delimiter)[0] === '0';
  370. }
  371. isColumnIndexCellDiv(cellDiv) {
  372. return cellDiv.getAttribute('key').split(delimiter)[1] === '0';
  373. }
  374. createCellDiv(columnIndex, rowIndex) {
  375. const cellDiv = document.createElement('div');
  376. cellDiv.className = 'TableCell';
  377. const cellKey = columnIndex + delimiter + rowIndex;
  378. cellDiv.setAttribute('key', cellKey);
  379. return cellDiv;
  380. }
  381. createTableRowDiv() {
  382. const tableRowDiv = document.createElement('div');
  383. tableRowDiv.className = 'TableRow';
  384. return tableRowDiv;
  385. }
  386. createTableDiv() {
  387. const tableDiv = document.createElement('div');
  388. tableDiv.className = 'Table';
  389. return tableDiv;
  390. }
  391. }
  392. class CellStorage {
  393. constructor(controller) {
  394. this.controller = controller;
  395. this.cells = {};
  396. }
  397. createCell(key, div) {
  398. return this.cells[key] = new Cell(this.controller, key, div);
  399. }
  400. getCells(range) {
  401. if(range.start === range.end) {
  402. const cellCoords = cellIdToCellCoords(range.start);
  403. return [this.cells[cellCoords.x + delimiter + cellCoords.y]];
  404. } else {
  405. const startCoords = cellIdToCellCoords(range.start);
  406. const endCoords = cellIdToCellCoords(range.end);
  407. if(startCoords.y > endCoords.y) {
  408. const yTmp = startCoords.y;
  409. startCoords.y = endCoords.y;
  410. endCoords.y = yTmp;
  411. }
  412. if(startCoords.x > endCoords.x) {
  413. const xTmp = startCoords.x;
  414. startCoords.x = endCoords.x;
  415. endCoords.x = xTmp;
  416. }
  417. return Object.values(this.cells).filter((cell) => {
  418. return cell.coords.x >= startCoords.x &&
  419. cell.coords.y >= startCoords.y &&
  420. cell.coords.x <= endCoords.x &&
  421. cell.coords.y <= endCoords.y;
  422. });
  423. }
  424. }
  425. getResultCell(startCell, endCell) {
  426. if(endCell !== undefined && endCell !== null) {
  427. let offsetX = 0;
  428. let offsetY = 0;
  429. if(endCell.coords.y === startCell.coords.y) {
  430. offsetX += 1;
  431. } else {
  432. offsetY += 1;
  433. }
  434. const columnLetter = getColumnIdFromIndex(endCell.coords.x + offsetX);
  435. const rowNumber = endCell.coords.y + offsetY;
  436. return this.getCells({start: columnLetter + rowNumber, end: columnLetter + rowNumber})[0];
  437. } else {
  438. const columnLetter = getColumnIdFromIndex(startCell.coords.x);
  439. const rowNumber = startCell.coords.y + 1;
  440. return this.getCells({start: columnLetter + rowNumber, end: columnLetter + rowNumber})[0];
  441. }
  442. }
  443. }
  444. class Cell {
  445. cellRegex = /([A-Z|a-z]+[0-9]+)/mg;
  446. cellKeyRegex = /([A-Za-z]+)([0-9]+)/mg;
  447. newlineRegex = /[\r\n]+/g;
  448. constructor(controller, key, tableCellDiv) {
  449. this.controller = controller;
  450. this.rxCore = controller.rxCore;
  451. this.rxOps = controller.rxOps;
  452. this.coords = getCoordObjectFromKey(key);
  453. this.id = "" + getColumnIdFromIndex(this.coords.x) + this.coords.y;
  454. this.subMgr = new SubscriptionManager(this);
  455. this.tableCellDiv = tableCellDiv;
  456. this.value = "";
  457. this.formula = "";
  458. this.error = "";
  459. this.isFormula = false;
  460. this.isError = false;
  461. this.inputSubject = new this.rxCore.Subject();
  462. this.cellSubject = new this.rxCore.Subject();
  463. this.setupEvaluationTrigger();
  464. this.setupOnclickHandler();
  465. this.setupInputSubject();
  466. this.setupInputHandler();
  467. }
  468. setupOnclickHandler() {
  469. this.rxCore.fromEvent(this.tableCellDiv, 'click').subscribe(
  470. event => this.controller.cellRangeSelectionController.handleInput(event, this)
  471. );
  472. }
  473. setupEvaluationTrigger() {
  474. const enterKeydownEvent = this.rxCore.fromEvent(this.tableCellDiv, 'keydown').pipe(
  475. this.rxOps.filter(event => event.keyCode === 13 && event.shiftKey === false),
  476. this.rxOps.tap(event => event.preventDefault())
  477. );
  478. const deselectEvent = this.rxCore.fromEvent(this.tableCellDiv, 'blur');
  479. const evaluationTriggerEvents = this.rxCore.merge(enterKeydownEvent, deselectEvent).pipe(
  480. this.rxOps.debounce( () => this.rxCore.timer(50) )
  481. );
  482. const evaluationSubscription = evaluationTriggerEvents.subscribe({
  483. next: (event) => {
  484. this.controller.evaluationSubject.next(this);
  485. },
  486. error: () => console.log(`Error in EvaluationTrigger(${this.id})`)
  487. });
  488. }
  489. setupInputHandler() {
  490. this.rxCore.fromEvent(this.tableCellDiv, 'input').subscribe(
  491. event => this.inputSubject.next({ src: this.tableCellDiv, event: event })
  492. );
  493. }
  494. setupInputSubject() {
  495. const inputSubjectDebounce = this.inputSubject.pipe(
  496. this.rxOps.debounce( () => this.rxCore.timer(100) ),
  497. this.rxOps.map(this.prepareInput)
  498. );
  499. const cellDivInput = inputSubjectDebounce.pipe(
  500. this.rxOps.filter( (object) => object.src === this.tableCellDiv )
  501. );
  502. cellDivInput.subscribe({
  503. next: (object) => {
  504. this.handleInput(object);
  505. this.controller.formulaController.setFormulaText(this.getFormulaOutput());
  506. },
  507. error: () => console.log(`Error in InputSubject(${this.id})`)
  508. });
  509. const formulaDivInput = inputSubjectDebounce.pipe(
  510. this.rxOps.filter( (object) => object.src !== this.tableCellDiv )
  511. );
  512. formulaDivInput.subscribe({
  513. next: (object) => {
  514. this.handleInput(object);
  515. this.tableCellDiv.innerText = this.getFormulaOutput();
  516. },
  517. error: () => console.log(`Error in InputSubject(${this.id})`)
  518. });
  519. }
  520. prepareInput(object) {
  521. const input = object.event.path[0].innerText.replace(this.newlineRegex,'');
  522. return { src: object.src, input: input };
  523. }
  524. handleInput(object) {
  525. if(object.input.startsWith("=")) {
  526. this.formula = object.input.toUpperCase();
  527. this.isFormula = true;
  528. this.handleSubscriptions();
  529. } else {
  530. this.value = object.input;
  531. this.isFormula = false;
  532. this.notify();
  533. }
  534. }
  535. handleSubscriptions() {
  536. this.subMgr.cancelAll();
  537. if(this.cellRegex.test(this.formula)) {
  538. this.formula.substring(1).match(this.cellRegex).some( cellId => {
  539. const targetCell = this.controller.cellStorage.getCells({start: cellId, end: cellId})[0];
  540. if(targetCell === undefined) {//in case of out of bounds
  541. this.isError = true;
  542. this.error = `OutOfBounds for ${cellId}`;
  543. return this.isError;
  544. }
  545. const hasNoCycle = this.subMgr.hasNoDepWith(targetCell);
  546. if(hasNoCycle) {
  547. this.isError = false;
  548. this.subMgr.subscribe(targetCell);
  549. } else {
  550. this.isError = true;
  551. this.error = `Cycle between ${this.id} and ${targetCell.id}`;
  552. console.log(`Error: Cycle detected between Cell(${this.id}) and Cell(${targetCell.id})`);
  553. }
  554. return this.isError;
  555. });
  556. }
  557. }
  558. getFormulaOutput() {
  559. return this.isFormula ? this.formula.replace(this.newlineRegex,'') : ('' + this.value);
  560. }
  561. evaluate() {
  562. const evaluationString = this.formula.substring(1)
  563. .replace(this.newlineRegex,'')
  564. .replace(this.cellRegex, (cellId, $1) => {
  565. return this.subMgr.getValue(cellId);
  566. }
  567. );
  568. let result = 0;
  569. try {
  570. result = math.evaluate(evaluationString);
  571. result = result === undefined ? 0 : math.round(result, 3);
  572. } catch(error) {
  573. result = 0;
  574. }
  575. this.value = result;
  576. this.tableCellDiv.innerText = result;
  577. this.notify();
  578. }
  579. notify() {
  580. this.cellSubject.next(this);
  581. }
  582. }
  583. class SubscriptionManager {
  584. constructor(owner) {
  585. this.owner = owner;
  586. this.rxCore = owner.rxCore;
  587. this.rxOps = owner.rxOps;
  588. this.subs = {};
  589. }
  590. hasNoDepWith(cell) {
  591. if(cell.id === this.owner.id)
  592. return false;
  593. let noDep = false;
  594. this.getDependencies(cell).pipe(
  595. this.rxOps.find(value => value === this.owner.id)
  596. ).subscribe(value => value === undefined ? noDep = true : noDep = false);
  597. return noDep;
  598. }
  599. getDependencies(cell) {
  600. if(Object.entries(cell.subMgr.subs).length > 0) {
  601. let result = null;
  602. this.rxCore.from(Object.values(cell.subMgr.subs)).pipe(
  603. this.rxOps.reduce( (acc, sub) => {
  604. return acc.pipe(
  605. this.rxOps.concat(this.rxCore.of(sub.cell.id), this.getDependencies(sub.cell))
  606. );
  607. }, this.rxCore.empty())
  608. ).subscribe(value => result = value);
  609. return result;
  610. } else /*(Object.entries(cell.subMgr.subs).length === 0) */{
  611. return this.rxCore.empty();
  612. }
  613. }
  614. subscribe(cell) {
  615. if(!(cell.id in this.subs)) {
  616. //console.log(`Cell(${this.owner.id}) subscribing to cell(${cell.id})`);
  617. const subscription = cell.cellSubject.subscribe({
  618. next: (value) => { this.owner.controller.evaluationSubject.next(this.owner); },
  619. error: () => console.log(`Error in SubscriptionManager of cell(${this.owner.id}) subscribed to cell(${cell.id})`)
  620. });
  621. this.subs[cell.id] = { cell: cell, sub: subscription };
  622. } else {
  623. console.log(`Warning: Tried duplicate subscription to cell(${cell.id})`);
  624. }
  625. }
  626. cancel(cell) {
  627. if(cell.id in this.subs) {
  628. const subscription = this.subs[cell.id].sub;
  629. subscription.unsubscribe();
  630. delete this.subs[cell.id];
  631. } else {
  632. console.log(`Error: Owner(${this.owner.id}) is not subscribed to cell(${cell.id})`);
  633. }
  634. }
  635. cancelAll() {
  636. Object.keys(this.subs).forEach( (id, value) => {
  637. //console.log(`Cell(${this.owner.id}) cancelling subscription to cell(${id})`);
  638. this.cancel(this.subs[id].cell);
  639. });
  640. }
  641. getValue(cellId) {
  642. if(cellId in this.subs) {
  643. return this.subs[cellId].cell.value;
  644. } else {
  645. console.log(`Error: Requesting value of Cell(${cellId}). No subscription to that cell!)`);
  646. return '0';
  647. }
  648. }
  649. }
  650. //Magic ccm code line.
  651. let b="ccm."+component.name+(component.version?"-"+component.version.join("."):"")+".js";if(window.ccm&&null===window.ccm.files[b])return window.ccm.files[b]=component;(b=window.ccm&&window.ccm.components[component.name])&&b.ccm&&(component.ccm=b.ccm);"string"===typeof component.ccm&&(component.ccm={url:component.ccm});let c=(component.ccm.url.match(/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)/)||["latest"])[0];if(window.ccm&&window.ccm[c])window.ccm[c].component(component);else{var a=document.createElement("script");document.head.appendChild(a);component.ccm.integrity&&a.setAttribute("integrity",component.ccm.integrity);component.ccm.crossorigin&&a.setAttribute("crossorigin",component.ccm.crossorigin);a.onload=function(){window.ccm[c].component(component);document.head.removeChild(a)};a.src=component.ccm.url}
  652. })();