table.js (22841B)
1 /* License CC0 */ 2 var table_js = function(customization) { 3 function make_cell(contents_addr) { 4 return { l: true, u: true, r: true, d: true, contents_addr: contents_addr }; 5 } 6 function make_model(h, w) { 7 var init = customization.init; 8 var m = []; 9 var contents = []; 10 for (var y = 0; y < h; y++) { 11 m[y] = []; 12 for (var x = 0; x < w; x++) { 13 m[y][x] = make_cell(contents.length); 14 contents.push(init({y:y, x:x})); 15 } 16 } 17 var focus = {y:0,x:0}; 18 return { m, contents, focus }; 19 } 20 function height(mod) { 21 return mod.m.length; 22 } 23 function width(mod) { 24 return mod.m[0].length; 25 } 26 function copy(mod, transform) { 27 var transform = transform || { 28 h: (hw => hw.h), 29 w: (hw => hw.w), 30 x: (yxhw => yxhw.x), 31 y: (yxhw => yxhw.y), 32 l: (c => c.l), 33 u: (c => c.u), 34 r: (c => c.r), 35 d: (c => c.d), 36 }; 37 var h = height(mod) 38 var w = width(mod) 39 var c = make_model(transform.h({h,w}), transform.w({h,w})); 40 var contents_redirect = {}; 41 c.contents = []; 42 for (var y = 0; y < height(mod); y++) { 43 for (var x = 0; x < width(mod); x++) { 44 var cell = mod.m[y][x]; 45 46 // lazily copy the mod.contents to c.contents 47 if (typeof(contents_redirect[cell.contents_addr]) == 'undefined') { 48 contents_redirect[cell.contents_addr] = c.contents.length; 49 var copied_contents = customization.deep_copy_content(mod.contents[cell.contents_addr]); 50 c.contents.push(copied_contents); 51 } 52 53 c.m[transform.y({y,x,h,w})][transform.x({y,x,h,w})] = { 54 l: transform.l(cell), 55 u: transform.u(cell), 56 r: transform.r(cell), 57 d: transform.d(cell), 58 contents_addr: contents_redirect[cell.contents_addr] 59 }; 60 } 61 } 62 var focushw = {y: mod.focus.y, x: mod.focus.x, h, w}; 63 c.focus = fuse_corner(c, transform.y(focushw), transform.x(focushw)); 64 return c; 65 } 66 67 function update_focus(mod, y, x) { 68 var mod = copy(mod, false); 69 mod.focus = { y, x } 70 return mod; 71 } 72 73 function delete_column(mod, col, skip_recursive_delete) { 74 var mod = mod; 75 if (width(mod) == 1) { 76 return make_model(1, 1); 77 } else { 78 for (var y = 0; y < height(mod); y++) { 79 if (width(mod) == 1) { 80 // can't happen, already tested above. 81 } else if (col == 0) { 82 mod.m[y][col+1].l = true; 83 } else if (col == width(mod) - 1) { 84 mod.m[y][col-1].r = true; 85 } else { 86 if (mod.m[y][col-1].r || mod.m[y][col+1].l) { 87 mod.m[y][col-1].r = true; 88 mod.m[y][col+1].l = true; 89 } 90 } 91 } 92 for (var y = 0; y < height(mod); y++) { 93 mod.m[y].splice(col,1); 94 } 95 if (mod.focus.x >= col && mod.focus.x >= 1) { 96 mod.focus.x = mod.focus.x - 1; 97 } 98 99 if (! skip_recursive_delete) { 100 mod = delete_useless_rows(mod); 101 } 102 103 return mod; 104 } 105 } 106 107 function delete_useless_rows(mod) { 108 var mod = mod; 109 for (var y = height(mod) - 1; y >= 1; y--) { 110 var keep_line = false; 111 for (var x = 0; x < width(mod); x++) { 112 if (mod.m[y][x].u) { 113 keep_line = true; 114 break; 115 } 116 } 117 if (! keep_line) { 118 mod = delete_row(mod, y, true); 119 } 120 } 121 return mod; 122 } 123 124 function delete_useless_rows_and_columns(mod) { 125 return transpose(delete_useless_rows(transpose(delete_useless_rows(mod)))); 126 } 127 128 function sanitize(mod) { 129 var w = width(mod); 130 var h = height(mod); 131 for (var y = 0; y < h; y++) { 132 mod.m[y][0].l = true; 133 mod.m[y][w-1].r = true; 134 for (var x = 0; x < w - 1; x++) { // skip rightmost cell 135 var border = mod.m[y][x].r || mod.m[y][x+1].l; 136 mod.m[y][x].r = border; 137 mod.m[y][x+1].l = border; 138 } 139 } 140 for (var x = 0; x < w; x++) { 141 mod.m[0][x].u = true; 142 mod.m[h-1][x].d = true; 143 for (var y = 0; y < h - 1; y++) { // skip bottommost cell 144 var border = mod.m[y][x].d || mod.m[y+1][x].u; 145 mod.m[y][x].d = border; 146 mod.m[y+1][x].u = border; 147 } 148 } 149 mod.focus = fuse_corner(mod, mod.focus.y, mod.focus.x); 150 } 151 152 function serialize(mod) { 153 return JSON.stringify({m: mod.m, contents: mod.contents, focus: mod.focus}); 154 } 155 156 function deserialize(json) { 157 var mc = JSON.parse(json); 158 return sanitize({ m: mc.m, contents: mc.contents, focus: mc.focus }); 159 } 160 161 function transpose(mod) { 162 return copy(mod, { 163 h: (hw => hw.w), 164 w: (hw => hw.h), 165 x: (yxhw => yxhw.y), 166 y: (yxhw => yxhw.x), 167 l: (c => c.u), 168 u: (c => c.l), 169 r: (c => c.d), 170 d: (c => c.r), 171 }); 172 } 173 174 function mirror_vertically(mod) { 175 return copy(mod, { 176 h: (hw => hw.h), 177 w: (hw => hw.w), 178 x: (yxhw => yxhw.x), 179 y: (yxhw => yxhw.h - yxhw.y - 1), 180 l: (c => c.l), 181 u: (c => c.d), 182 r: (c => c.r), 183 d: (c => c.u), 184 }); 185 } 186 187 function mirror_horizontally(mod) { 188 return copy(mod, { 189 h: (hw => hw.h), 190 w: (hw => hw.w), 191 x: (yxhw => yxhw.w - yxhw.x - 1), 192 y: (yxhw => yxhw.y), 193 l: (c => c.r), 194 u: (c => c.u), 195 r: (c => c.l), 196 d: (c => c.d), 197 }); 198 } 199 200 function delete_row(mod, row, skip_recursive_delete) { 201 return transpose(delete_column(transpose(mod), row, skip_recursive_delete)); 202 } 203 204 function insert_column(mod, col, init) { 205 for (var y = 0; y < height(mod); y++) { 206 mod.m[y].splice(col,0,make_cell(mod.contents.length)); 207 mod.contents.push(init({y,x:col})); 208 209 if (col == 0) { 210 // nothing to do. 211 } else if (col == width(mod)) { 212 // nothing to do. 213 } else { 214 if (mod.m[y][col-1].r || mod.m[y][col+1].l) { 215 // nothing to do. 216 } else { 217 // inserted in the middle of a fusion 218 // no horizontal edges 219 mod.m[y][col].r = false; 220 mod.m[y][col].l = false; 221 // same vertical edges as neighbours 222 console.assert(mod.m[y][col-1].u == mod.m[y][col+1].u) 223 console.assert(mod.m[y][col-1].d == mod.m[y][col+1].d) 224 mod.m[y][col].u = mod.m[y][col-1].u; 225 mod.m[y][col].d = mod.m[y][col-1].d; 226 } 227 } 228 } 229 if (mod.focus.x >= col) { 230 mod.focus.x++; 231 } 232 return mod; 233 } 234 235 function insert_row(mod, row, init) { 236 return transpose(insert_column(transpose(mod), row, yx => init({y:yx.x, x:yx.y}))); 237 } 238 239 function fuse_corner(mod, y, x) { 240 while (!mod.m[y][x].l) { x--; } 241 while (!mod.m[y][x].u) { y--; } 242 return {y,x}; 243 } 244 245 function fuse_width(mod, y, x) { 246 var rightmost; 247 for (rightmost = x; !mod.m[y][rightmost].r; rightmost++) { } 248 return rightmost - x + 1; 249 } 250 251 function fuse_height(mod, y, x) { 252 var bottommost; 253 for (bottommost = y; !mod.m[bottommost][x].d; bottommost++) { } 254 return bottommost - y + 1; 255 } 256 257 function check_fuse_right(mod, y, x) { 258 if ((!mod.m[y][x].l) || (!mod.m[y][x].u)) { 259 //console.log ("["+y+"]["+x+"] is not the top-left corner of a fuse"); 260 return false; 261 } 262 263 var rightmost = x + fuse_width(mod, y, x) - 1; 264 var bottommost = y + fuse_height(mod, y, x) - 1; 265 266 if (rightmost >= width(mod) - 1) { 267 //console.log ("the fuse starting at [y][x] is touching the right edge, can't fuse to the right."); 268 return false; 269 } 270 271 var first_x_rhs = rightmost + 1; 272 var first_rhs_width = fuse_width(mod, y, first_x_rhs); 273 var rightmost_rhs = first_x_rhs + first_rhs_width - 1; 274 275 if (!mod.m[y][first_x_rhs].u) { 276 //console.log ("[y][first_x_rhs] is not the top-left corner of a fuse"); 277 return false; 278 } 279 280 var y_rhs; 281 var x_rhs; 282 for (y_rhs = y; y_rhs <= bottommost; y_rhs += fuse_height(mod, y_rhs, first_x_rhs)) { 283 if (fuse_width(mod, y_rhs, first_x_rhs) != first_rhs_width) { 284 //console.log("the fuse starting at [y_rhs][first_x_rhs] is not " 285 // + "the same width as the fuse starting at [y][first_x_rhs]," 286 // + "can't collapse these into a single column.") 287 return false; 288 } 289 } 290 if (y_rhs != bottommost + 1) { 291 //console.log("the last fuse in the rhs is too high, it ends below" 292 // + "the bottommost element of the fuse starting at [y][x]."); 293 return false; 294 } 295 296 return { }; 297 } 298 299 function fuse_right_(mod, dir, y, x) { 300 if (! check_fuse_right(mod, y, x)) { 301 return mod; 302 }; 303 304 var rightmost = x + fuse_width(mod, y, x) - 1; 305 var bottommost = y + fuse_height(mod, y, x) - 1; 306 var first_x_rhs = x + fuse_width(mod, y, x); 307 var first_rhs_width = fuse_width(mod, y, first_x_rhs); 308 var rightmost_rhs = first_x_rhs + first_rhs_width - 1; 309 310 var new_contents_addr = mod.m[y][x].contents_addr; 311 var discarded_cell_contents = []; 312 313 // loop over the fuses in the right-hand-side 314 for (var y_rhs = y; y_rhs <= bottommost; y_rhs += fuse_height(mod, y_rhs, first_x_rhs)) { 315 var cell = mod.m[y_rhs][first_x_rhs]; 316 discarded_cell_contents.push(mod.contents[cell.contents_addr]); // copy old contents 317 mod.contents[cell.contents_addr] = null; // erase old contents 318 } 319 320 // loop over the cells in the right-hand-site and add them to the main fuse 321 for (var y_rhs = y; y_rhs <= bottommost; y_rhs++) { 322 // we also update the rihghtmost column of the main fuse. 323 for (x_rhs = rightmost; x_rhs <= rightmost_rhs; x_rhs++) { 324 var cell = mod.m[y_rhs][x_rhs]; 325 326 // set contents_addr 327 cell.contents_addr = new_contents_addr; // point to new contents 328 329 // set borders 330 cell.l = (x_rhs == x); 331 cell.u = (y_rhs == y); 332 cell.r = (x_rhs == rightmost_rhs); 333 cell.d = (y_rhs == bottommost); 334 } 335 } 336 337 mod.contents[new_contents_addr] = customization.merge_contents(dir, mod.contents[new_contents_addr], discarded_cell_contents); 338 339 mod.focus = fuse_corner(mod, mod.focus.y, mod.focus.x); 340 341 mod = delete_useless_rows_and_columns(mod); 342 343 return mod; 344 } 345 346 function fuse_right(mod, x, y) { 347 return fuse_right_(mod, "r", x, y); 348 } 349 350 function fuse_down(mod, y, x) { 351 return transpose(fuse_right_(transpose(mod), "d", x, y)); 352 } 353 354 function check_fuse_down(mod, y, x) { 355 return check_fuse_right(transpose(mod), x, y); 356 } 357 358 function check_fuse_left(mod, y, x) { 359 return check_fuse_right(mirror_horizontally(mod), y, width(mod) - x - fuse_width(mod, y, x)); 360 } 361 362 function check_fuse_up(mod, y, x) { 363 return check_fuse_left(transpose(mod), x, y); 364 } 365 366 function check_fuse(mod, y, x) { 367 return { 368 l: check_fuse_left(mod, y, x), 369 u: check_fuse_up(mod, y, x), 370 r:check_fuse_right(mod, y, x), 371 d:check_fuse_down(mod, y, x) 372 }; 373 } 374 375 function fuse_left_(mod, dir, y, x) { 376 return mirror_horizontally(fuse_right_(mirror_horizontally(mod), dir, y, width(mod) - x - fuse_width(mod, y, x))); 377 } 378 379 function fuse_left(mod, y, x) { 380 return fuse_left_(mod, "l", y, x); 381 } 382 383 function fuse_up(mod, y, x) { 384 return transpose(fuse_left_(transpose(mod), "u", x, y)); 385 } 386 387 function to_html(state) { 388 var content_to_html = customization.content_to_html; 389 var id_prefix = customization.id_prefix; 390 var mod = get_current_mod(state); 391 392 var c = function(tag) { return document.createElement(tag); }; 393 var appendChild = function(elem, tag, className) { 394 var child = c(tag); 395 elem.appendChild(child); 396 child.className = className; 397 return child; 398 } 399 400 var make_button = function (class_, callback, label, tooltip) { 401 var a = c('a'); 402 a.className = class_; 403 a.setAttribute('title', tooltip); 404 a.addEventListener('click', function (e) { callback(e); e.preventDefault(); return void(0); }); 405 a.innerHTML = label; 406 return a; 407 } 408 var insert_column_button = function(index) { 409 return make_button('column-button insert-button', function(e) { update_state(state, insert_column, [index, customization.init_new_column]) }, '+', 'Insert column'); 410 } 411 var insert_row_button = function (index) { 412 return make_button('row-button insert-button', function(e) { update_state(state, insert_row, [index, customization.init_new_row]) }, '+', 'Insert row'); 413 } 414 var delete_column_button = function (index) { 415 return make_button('column-button delete-button', function(e) { update_state(state, delete_column, [index, false]) }, 'ⓧ', 'Delete column'); 416 } 417 var delete_row_button = function (index) { 418 return make_button('row-button delete-button', function(e) { update_state(state, delete_row, [index, false]) }, 'ⓧ', 'Delete row'); 419 } 420 var fuse_button = function(label, direction, fuse_f) { 421 return make_button('', function (e) { 422 update_state(state, function (mod) { return fuse_f(mod, mod.focus.y, mod.focus.x); }, []); 423 }, label, 'Fuse with the cells ' + direction); 424 }; 425 426 var div = c('div'); 427 428 var make_all_fuse_buttons = function() { 429 var div_fuse = c('div'); 430 div_fuse.setAttribute('id', 'all-fuse-buttons'); 431 var make_fuse_button = function(dir, label, direction, fuse_f) { 432 var d = appendChild(div_fuse, 'div', 'fuse-button'); 433 d.setAttribute('id', ''+id_prefix+'-fuse-' + dir); 434 d.appendChild(fuse_button(label, direction, fuse_f)); 435 } 436 make_fuse_button('l', '←', 'to the left', fuse_left); 437 make_fuse_button('u', '↑', 'above', fuse_up); 438 make_fuse_button('r', '→', 'to the right', fuse_right); 439 make_fuse_button('d', '↓', 'below', fuse_down); 440 return div_fuse; 441 } 442 443 div.appendChild(make_all_fuse_buttons()); 444 445 var table = appendChild(div, 'table', ''); 446 447 // colgroup 448 var colgroup = appendChild(table, 'colgroup', ''); 449 appendChild(colgroup, 'col', 'insert-delete-row-col'); 450 for (var x = 0; x < width(mod); x++) { 451 appendChild(colgroup, 'col', 'user-column-col'); 452 } 453 454 // thead 455 var thead = appendChild(table, 'thead', ''); 456 var first_tr = appendChild(thead, 'tr', ''); 457 var first_first_th = appendChild(first_tr, 'th', 'insert-delete-column insert-delete-row'); 458 first_first_th.appendChild(insert_column_button(0)); 459 first_first_th.appendChild(insert_row_button(0)); 460 for (var x = 0; x < width(mod); x++) { 461 var th = appendChild(first_tr, 'th', 'insert-delete-column'); 462 th.appendChild(delete_column_button(x)); 463 th.appendChild(insert_column_button(x+1)) 464 } 465 466 // tbody 467 var tbody = appendChild(table, 'tbody', ''); 468 for (var y = 0; y < height(mod); y++) { 469 var tr = appendChild(tbody, 'tr', ''); 470 first_th = appendChild(tr, 'th', 'insert-delete-row'); 471 first_th.appendChild(delete_row_button(y)); 472 first_th.appendChild(insert_row_button(y+1)); 473 for (var x = 0; x < width(mod); x++) { 474 var cell = mod.m[y][x]; 475 if (cell.l && cell.u) { 476 var td = appendChild(tr, 'td', ''); 477 td.setAttribute('id', '' + id_prefix + '-' + y + '-' + x); 478 td.setAttribute('rowspan', fuse_height(mod, y, x)); 479 td.setAttribute('colspan', fuse_width(mod, y, x)); 480 td.appendChild(content_to_html(mod.contents[cell.contents_addr], {y,x})); 481 482 var f = function (closure_y, closure_x) { 483 return function (e) { 484 var old = document.getElementById('all-fuse-buttons'); 485 old.parentElement.removeChild(old); 486 var td = document.getElementById(id_prefix + '-' + closure_y + '-' + closure_x); 487 td.appendChild(make_all_fuse_buttons()); 488 update_state(state, update_focus, [closure_y, closure_x], true, true); 489 return true; 490 } 491 } 492 // focus 493 td.addEventListener('focusin', f(y, x)); 494 } 495 } 496 } 497 498 return div; 499 } 500 501 function getOffset(elt) { 502 if (elt) { 503 var o = getOffset(elt.offsetParent); 504 return { left: elt.offsetLeft + o.left, top: elt.offsetTop + o.top }; 505 } else { 506 return { left: 0, top: 0 }; 507 } 508 } 509 510 function reload_values(mod) { 511 var id_prefix = customization.id_prefix; 512 var html_to_content = customization.html_to_content; 513 var already_reloaded = {}; 514 for (var y = 0; y < height(mod); y++) { 515 for (var x = 0; x < width(mod); x++) { 516 if (mod.m[y][x].l && mod.m[y][x].u) { 517 var addr = mod.m[y][x].contents_addr; 518 if (! already_reloaded[addr]) { 519 already_reloaded[addr] = true; 520 mod.contents[addr] = html_to_content(document.getElementById(id_prefix + '-' + y + '-' + x)); 521 } 522 } 523 } 524 } 525 return mod; 526 } 527 528 function get_current_mod(state) { 529 return state.stack[state.current]; 530 } 531 532 function set_current_mod(state, mod) { 533 state.stack.splice(state.current + 1, state.stack.length - state.current - 1); 534 state.current++; 535 return state.stack[state.current] = mod; 536 } 537 538 function undo(state) { 539 state.current--; 540 } 541 542 function redo(state) { 543 state.current++; 544 if (state.current >= state.stack.length) { 545 state.current = state.stack.length - 1; 546 } 547 } 548 549 function create_state_from_mod(initial_mod) { 550 return { current: 0, stack: [initial_mod] }; 551 } 552 553 function create_state(height, width) { 554 return create_state_from_mod(make_model(height, width)); 555 } 556 557 function update_state(state, f, args, skip_redraw, skip_history, skip_reload) { 558 var id_prefix = customization.id_prefix; 559 560 var mod = copy(get_current_mod(state), false); 561 var matrix_reloaded = skip_reload ? mod : reload_values(mod); 562 args.splice(0, 0, matrix_reloaded); 563 var new_mod = f.apply(null, args); 564 set_current_mod(state, new_mod); 565 566 if (!skip_redraw) { 567 document.getElementById(id_prefix).innerHTML = ''; 568 document.getElementById(id_prefix).appendChild(to_html(state)); 569 document.getElementById(id_prefix + '-' + new_mod.focus.y + '-' + new_mod.focus.x).getElementsByTagName('textarea')[0].focus(); 570 for (var y = 0; y < height(new_mod); y++) { 571 for (var x = 0; x < width(new_mod); x++) { 572 var el = document.getElementById(id_prefix + '-' + y + '-' + x); 573 if (el) { 574 customization.postprocess(el); 575 } 576 } 577 } 578 } 579 // draw merge arrows: 580 var td = document.getElementById(id_prefix + '-' + new_mod.focus.y + '-' + new_mod.focus.x); 581 var tdw = td.offsetWidth; 582 var tdh = td.offsetHeight; 583 var check = check_fuse(new_mod, new_mod.focus.y, new_mod.focus.x); 584 var o = getOffset(td); 585 var pos = function(dir, left, top) { 586 var elt = document.getElementById(id_prefix + '-fuse-'+dir); 587 elt.style.display = check[dir] ? 'inherit' : 'none'; 588 var elto = getOffset(elt.offsetParent); 589 elt.style.left = left - elto.left - elt.offsetWidth/2; 590 elt.style.top = top - elto.top - elt.offsetHeight/2; 591 } 592 pos('l', o.left, o.top + td.offsetHeight/2); 593 pos('u', o.left + td.offsetWidth/2, o.top); 594 pos('r', o.left + td.offsetWidth, o.top + td.offsetHeight/2); 595 pos('d', o.left + td.offsetWidth/2, o.top + td.offsetHeight); 596 return true; 597 } 598 599 function cell_contents(mod, yx, updater) { 600 var addr = mod.m[yx.y][yx.x].contents_addr; 601 if (updater) { 602 mod.contents[addr] = updater(mod.contents[addr]); 603 return mod; 604 } else { 605 return mod.contents[addr]; 606 } 607 } 608 609 function focus(mod, yx) { 610 if (yx) { 611 mod.focus.y = yx.y; 612 mod.focus.y = yx.x; 613 return mod; 614 } else { 615 return mod.focus; 616 } 617 } 618 619 return { 620 // protected 621 make_model, 622 delete_column, 623 delete_row, 624 insert_column, 625 insert_row, 626 fuse_left, 627 fuse_up, 628 fuse_right, 629 fuse_down, 630 create_state_from_mod, 631 // public 632 create_state, 633 serialize, 634 deserialize, 635 update_state, 636 undo, 637 redo, 638 cell_contents, 639 focus, 640 } 641 }