www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | LICENSE

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 }