diff --git a/js/ui/keyboard.js b/js/ui/keyboard.js deleted file mode 100644 index 62403d2..0000000 --- a/js/ui/keyboard.js +++ /dev/null @@ -1,311 +0,0 @@ -/* Copyright 2010-2019 Will Scullin - * - * Permission to use, copy, modify, distribute, and sell this software and its - * documentation for any purpose is hereby granted without fee, provided that - * the above copyright notice appear in all copies and that both that - * copyright notice and this permission notice appear in supporting - * documentation. No representations are made about the suitability of this - * software for any purpose. It is provided "as is" without express or - * implied warranty. - */ - -import { debug, toHex } from '../util'; - -// keycode: [plain, cntl, shift] - -var keymap = { - // Most of these won't happen - 0x00: [0x00, 0x00, 0x00], // - 0x01: [0x01, 0x01, 0x01], // - 0x02: [0x02, 0x02, 0x02], // - 0x03: [0x03, 0x03, 0x03], // - 0x04: [0x04, 0x04, 0x04], // - 0x05: [0x05, 0x05, 0x05], // - 0x06: [0x06, 0x06, 0x06], // - 0x07: [0x07, 0x07, 0x07], // - 0x08: [0x5f, 0x5f, 0x5f], // BS - 0x09: [0x09, 0x09, 0x09], // TAB - 0x0A: [0x0A, 0x0A, 0x0A], // - 0x0B: [0x0B, 0x0B, 0x0B], // - 0x0C: [0x0C, 0x0C, 0x0C], // - 0x0D: [0x0D, 0x0D, 0x0D], // CR - 0x0E: [0x0E, 0x0E, 0x0E], // - 0x0F: [0x0F, 0x0F, 0x0F], // - - 0x10: [0xff, 0xff, 0xff], // SHIFT - 0x11: [0xff, 0xff, 0xff], // CTRL - 0x12: [0xff, 0xff, 0xff], // OPTION - 0x13: [0x13, 0x13, 0x13], // - 0x14: [0x14, 0x14, 0x14], // - 0x15: [0x15, 0x15, 0x15], // - 0x16: [0x16, 0x16, 0x16], // - 0x17: [0x17, 0x17, 0x18], // - 0x18: [0x18, 0x18, 0x18], // - 0x19: [0x19, 0x19, 0x19], // - 0x1A: [0x1A, 0x1A, 0x1A], // - 0x1B: [0x1B, 0x1B, 0x1B], // ESC - 0x1C: [0x1C, 0x1C, 0x1C], // - 0x1D: [0x1D, 0x1D, 0x1D], // - 0x1E: [0x1E, 0x1E, 0x1E], // - 0x1F: [0x1F, 0x1F, 0x1F], // - - // Most of these besides space won't happen - 0x20: [0x20, 0x20, 0x20], // - 0x21: [0x21, 0x21, 0x21], // - 0x22: [0x22, 0x22, 0x22], // - 0x23: [0x23, 0x23, 0x23], // - 0x24: [0x24, 0x24, 0x24], // - 0x25: [0x5f, 0x5f, 0x5f], // <- left - 0x26: [0x0B, 0x0B, 0x0B], // ^ up - 0x27: [0x15, 0x15, 0x15], // -> right - 0x28: [0x0A, 0x0A, 0x0A], // v down - 0x29: [0x29, 0x29, 0x29], // ) - 0x2A: [0x2A, 0x2A, 0x2A], // * - 0x2B: [0x2B, 0x2B, 0x2B], // + - 0x2C: [0x2C, 0x2C, 0x3C], // , - < - 0x2D: [0x2D, 0x2D, 0x5F], // - - _ - 0x2E: [0x2E, 0x2E, 0x3E], // . - > - 0x2F: [0x2F, 0x2F, 0x3F], // / - ? - - 0x30: [0x30, 0x30, 0x29], // 0 - ) - 0x31: [0x31, 0x31, 0x21], // 1 - ! - 0x32: [0x32, 0x00, 0x40], // 2 - @ - 0x33: [0x33, 0x33, 0x23], // 3 - # - 0x34: [0x34, 0x34, 0x24], // 4 - $ - 0x35: [0x35, 0x35, 0x25], // 5 - % - 0x36: [0x36, 0x36, 0x5E], // 6 - ^ - 0x37: [0x37, 0x37, 0x26], // 7 - & - 0x38: [0x38, 0x38, 0x2A], // 8 - * - 0x39: [0x39, 0x39, 0x28], // 9 - ( - 0x3A: [0x3A, 0x3A, 0x3A], // : - 0x3B: [0x3B, 0x3B, 0x3A], // ; - : - 0x3C: [0x3C, 0x3C, 0x3C], // < - 0x3D: [0x3D, 0x3D, 0x2B], // = - + - 0x3E: [0x3E, 0x3E, 0x3E], // > - 0x3F: [0x3F, 0x3F, 0x3F], // ? - - // Alpha and control - 0x40: [0x40, 0x00, 0x40], // @ - 0x41: [0x41, 0x01, 0x41], // A - 0x42: [0x42, 0x02, 0x42], // B - 0x43: [0x43, 0x03, 0x43], // C - BRK - 0x44: [0x44, 0x04, 0x44], // D - 0x45: [0x45, 0x05, 0x45], // E - 0x46: [0x46, 0x06, 0x46], // F - 0x47: [0x47, 0x07, 0x47], // G - BELL - 0x48: [0x48, 0x08, 0x48], // H - 0x49: [0x49, 0x09, 0x49], // I - TAB - 0x4A: [0x4A, 0x0A, 0x4A], // J - NL - 0x4B: [0x4B, 0x0B, 0x4B], // K - VT - 0x4C: [0x4C, 0x0C, 0x4C], // L - 0x4D: [0x4D, 0x0D, 0x4D], // M - CR - 0x4E: [0x4E, 0x0E, 0x4E], // N - 0x4F: [0x4F, 0x0F, 0x4F], // O - - 0x50: [0x50, 0x10, 0x50], // P - 0x51: [0x51, 0x11, 0x51], // Q - 0x52: [0x52, 0x12, 0x52], // R - 0x53: [0x53, 0x13, 0x53], // S - 0x54: [0x54, 0x14, 0x54], // T - 0x55: [0x55, 0x15, 0x55], // U - 0x56: [0x56, 0x16, 0x56], // V - 0x57: [0x57, 0x17, 0x57], // W - 0x58: [0x58, 0x18, 0x58], // X - 0x59: [0x59, 0x19, 0x59], // Y - 0x5A: [0x5A, 0x1A, 0x5A], // Z - // 0x5B: [0x5B, 0x1B, 0x5B], // [ - ESC - // 0x5C: [0x5C, 0x1C, 0x5C], // \ - // 0x5D: [0x5D, 0x1D, 0x5D], // ] - 0x5E: [0x5E, 0x1E, 0x5E], // ^ - 0x5F: [0x5F, 0x1F, 0x5F], // _ - - // Stray keys - 0xBA: [0x3B, 0x3B, 0x3A], // ; - : - 0xBB: [0x3D, 0x3D, 0x2B], // = - + - 0xBC: [0x2C, 0x2C, 0x3C], // , - < - 0xBD: [0x2D, 0x2D, 0x5F], // - - _ - 0xBE: [0x2E, 0x2E, 0x3E], // . - > - 0xBF: [0x2F, 0x2F, 0x3F], // / - ? - 0xDB: [0x5B, 0x5B, 0x5B], // [ - 0xDC: [0x5C, 0x5C, 0x5C], // \ - 0xDD: [0x5D, 0x5D, 0x5D], // ] - 0xDE: [0x27, 0x27, 0x22], // ' - " - - 0xFF: [0xFF, 0xFF, 0xFF] // No comma line -}; - -export function mapKeyEvent(evt) { - var code = evt.keyCode; - - if (code in keymap) { - return keymap[code][evt.shiftKey ? 2 : (evt.ctrlKey ? 1 : 0)]; - } - - debug('Unhandled key = ' + toHex(code)); - return 0xFF; -} - -export function KeyBoard(id, cpu, io, text) { - var keys = - [[['1','2','3','4','5','6','7','8','9','0',':','-','RESET'], - ['ESC','Q','W','E','R','T','Y','U','I','O','P','FEED','RETURN'], - ['CTRL','A','S','D','F','G','H','J','K','L',';','OUT','CLS'], - ['SHIFT','Z','X','C','V','B','N','M',',','.','/','SHIFT'], - [' ']], - [['!','"','#','$','%','&','\'','(',')','0','*','=','RESET'], - ['ESC','Q','W','E','R','T','Y','U','I','O','@','LINE','RETURN'], - ['CTRL','A','S','D','F','BELL','H','J','K','L','+','RUB','CLS'], - ['SHIFT','Z','X','C','V','B','^','M','<','>','?','SHIFT'], - [' ']]]; - - var shifted = false; - var controlled = false; - var kb = document.querySelector(id); - - return { - shiftKey: function keyboard_shiftKey(down) { - shifted = down; - kb.querySelectorAll('.key-SHIFT').forEach(function(el) { - if (down) { - el.classList.add('active'); - } else { - el.classList.remove('active'); - } - }); - }, - - controlKey: function keyboard_controlKey(down) { - controlled = down; - kb.querySelectorAll('.key-CTRL').forEach(function(el) { - if (down) { - el.classList.add('active'); - } else { - el.classList.remove('active'); - } - }); - }, - - create: function keyboard_create() { - var x, y, row, key, key1, key2, label, label1, label2; - - function buildLabel(k) { - var span = document.createElement('span'); - span.innerHTML = k; - if (k.length > 1 && k.substr(0,1) != '&') { - span.classList.add('small'); - } - return span; - } - - function _mouseup(event) { - event.currentTarget.classList.remove('pressed'); - } - - function _mousedown(event) { - event.currentTarget.classList.add('pressed'); - var key = event.currentTarget.dataSet[shifted ? 'key2' : 'key1']; - switch (key) { - case 'BELL': - key = 'G'; - break; - case 'RETURN': - key = '\r'; - break; - case 'LINE': - case 'FEED': - key = '\n'; - break; - case 'RUB': - case 'OUT': - key = '_'; // 0x5f - break; - case ' ': - key = ' '; - break; - case 'ESC': - key = '\0x1b'; - break; - default: - break; - } - - if (key.length > 1) { - switch (key) { - case 'SHIFT': - shifted = !shifted; - kb.querySelectorAll('.key-SHIFT').forEach(function(el) { - el.classList.toggle('active'); - }); - break; - case 'CTRL': - controlled = !controlled; - kb.querySelectorAll('.key-CTRL').forEach(function(el) { - el.classList.toggle('active'); - }); - break; - case 'RESET': - cpu.reset(); - break; - case 'CLS': - text.clear(); - break; - default: - break; - } - } else { - if (controlled && key >= '@' && key <= '_') { - io.keyDown(key.charCodeAt(0) - 0x40); - } else { - io.keyDown(key.charCodeAt(0)); - } - } - } - - for (y = 0; y < 5; y++) { - row = document.createElement('div'); - row.classList.add('row', 'row' + y); - kb.append(row); - for (x = 0; x < keys[0][y].length; x++) { - key1 = keys[0][y][x]; - key2 = keys[1][y][x]; - - label = document.createElement('div'); - label1 = buildLabel(key1); - label2 = buildLabel(key2); - - key = document.createElement('div'); - key.classList.add('key', 'key-' + key1.replace(/[&;]/g,'')); - - if (key1.length > 1) { - if (key1 != key2) { - key.classList.add('vcenter2'); - } else { - key.classList.add('vcenter'); - } - } - - if (key1 != key2) { - key.classList.add('key-' + key2.replace(/[&;]/g,'')); - label.append(label2); - label.append(document.createElement('br')); - } - label.append(label1); - key.append(label); - key.dataSet = {'key1': key1, 'key2': key2}; - - if (window.ontouchstart === undefined) { - key.addEventListener('mousedown', _mousedown); - key.addEventListener('mouseup', _mouseup); - key.addEventListener('mouseout', _mouseup); - } else { - key.addEventListener('touchstart', _mousedown); - key.addEventListener('touchend', _mouseup); - key.addEventListener('touchleave', _mouseup); - } - - row.append(key); - } - } - } - }; -} diff --git a/js/ui/keyboard.ts b/js/ui/keyboard.ts new file mode 100644 index 0000000..699e0ce --- /dev/null +++ b/js/ui/keyboard.ts @@ -0,0 +1,341 @@ +/* Copyright 2010-2019 Will Scullin + * + * Permission to use, copy, modify, distribute, and sell this software and its + * documentation for any purpose is hereby granted without fee, provided that + * the above copyright notice appear in all copies and that both that + * copyright notice and this permission notice appear in supporting + * documentation. No representations are made about the suitability of this + * software for any purpose. It is provided "as is" without express or + * implied warranty. + */ + +import CPU6502 from "js/cpu6502"; +import { debug, toHex } from "../util"; +import Apple1IO from "js/apple1io"; +import { TextPage } from "js/canvas1"; +import { byte } from "js/types"; + +// keycode: [plain, cntl, shift] + +const keymap: Record = { + // Most of these won't happen + 0x00: [0x00, 0x00, 0x00], // + 0x01: [0x01, 0x01, 0x01], // + 0x02: [0x02, 0x02, 0x02], // + 0x03: [0x03, 0x03, 0x03], // + 0x04: [0x04, 0x04, 0x04], // + 0x05: [0x05, 0x05, 0x05], // + 0x06: [0x06, 0x06, 0x06], // + 0x07: [0x07, 0x07, 0x07], // + 0x08: [0x5f, 0x5f, 0x5f], // BS + 0x09: [0x09, 0x09, 0x09], // TAB + 0x0a: [0x0a, 0x0a, 0x0a], // + 0x0b: [0x0b, 0x0b, 0x0b], // + 0x0c: [0x0c, 0x0c, 0x0c], // + 0x0d: [0x0d, 0x0d, 0x0d], // CR + 0x0e: [0x0e, 0x0e, 0x0e], // + 0x0f: [0x0f, 0x0f, 0x0f], // + + 0x10: [0xff, 0xff, 0xff], // SHIFT + 0x11: [0xff, 0xff, 0xff], // CTRL + 0x12: [0xff, 0xff, 0xff], // OPTION + 0x13: [0x13, 0x13, 0x13], // + 0x14: [0x14, 0x14, 0x14], // + 0x15: [0x15, 0x15, 0x15], // + 0x16: [0x16, 0x16, 0x16], // + 0x17: [0x17, 0x17, 0x18], // + 0x18: [0x18, 0x18, 0x18], // + 0x19: [0x19, 0x19, 0x19], // + 0x1a: [0x1a, 0x1a, 0x1a], // + 0x1b: [0x1b, 0x1b, 0x1b], // ESC + 0x1c: [0x1c, 0x1c, 0x1c], // + 0x1d: [0x1d, 0x1d, 0x1d], // + 0x1e: [0x1e, 0x1e, 0x1e], // + 0x1f: [0x1f, 0x1f, 0x1f], // + + // Most of these besides space won't happen + 0x20: [0x20, 0x20, 0x20], // + 0x21: [0x21, 0x21, 0x21], // + 0x22: [0x22, 0x22, 0x22], // + 0x23: [0x23, 0x23, 0x23], // + 0x24: [0x24, 0x24, 0x24], // + 0x25: [0x5f, 0x5f, 0x5f], // <- left + 0x26: [0x0b, 0x0b, 0x0b], // ^ up + 0x27: [0x15, 0x15, 0x15], // -> right + 0x28: [0x0a, 0x0a, 0x0a], // v down + 0x29: [0x29, 0x29, 0x29], // ) + 0x2a: [0x2a, 0x2a, 0x2a], // * + 0x2b: [0x2b, 0x2b, 0x2b], // + + 0x2c: [0x2c, 0x2c, 0x3c], // , - < + 0x2d: [0x2d, 0x2d, 0x5f], // - - _ + 0x2e: [0x2e, 0x2e, 0x3e], // . - > + 0x2f: [0x2f, 0x2f, 0x3f], // / - ? + + 0x30: [0x30, 0x30, 0x29], // 0 - ) + 0x31: [0x31, 0x31, 0x21], // 1 - ! + 0x32: [0x32, 0x00, 0x40], // 2 - @ + 0x33: [0x33, 0x33, 0x23], // 3 - # + 0x34: [0x34, 0x34, 0x24], // 4 - $ + 0x35: [0x35, 0x35, 0x25], // 5 - % + 0x36: [0x36, 0x36, 0x5e], // 6 - ^ + 0x37: [0x37, 0x37, 0x26], // 7 - & + 0x38: [0x38, 0x38, 0x2a], // 8 - * + 0x39: [0x39, 0x39, 0x28], // 9 - ( + 0x3a: [0x3a, 0x3a, 0x3a], // : + 0x3b: [0x3b, 0x3b, 0x3a], // ; - : + 0x3c: [0x3c, 0x3c, 0x3c], // < + 0x3d: [0x3d, 0x3d, 0x2b], // = - + + 0x3e: [0x3e, 0x3e, 0x3e], // > + 0x3f: [0x3f, 0x3f, 0x3f], // ? + + // Alpha and control + 0x40: [0x40, 0x00, 0x40], // @ + 0x41: [0x41, 0x01, 0x41], // A + 0x42: [0x42, 0x02, 0x42], // B + 0x43: [0x43, 0x03, 0x43], // C - BRK + 0x44: [0x44, 0x04, 0x44], // D + 0x45: [0x45, 0x05, 0x45], // E + 0x46: [0x46, 0x06, 0x46], // F + 0x47: [0x47, 0x07, 0x47], // G - BELL + 0x48: [0x48, 0x08, 0x48], // H + 0x49: [0x49, 0x09, 0x49], // I - TAB + 0x4a: [0x4a, 0x0a, 0x4a], // J - NL + 0x4b: [0x4b, 0x0b, 0x4b], // K - VT + 0x4c: [0x4c, 0x0c, 0x4c], // L + 0x4d: [0x4d, 0x0d, 0x4d], // M - CR + 0x4e: [0x4e, 0x0e, 0x4e], // N + 0x4f: [0x4f, 0x0f, 0x4f], // O + + 0x50: [0x50, 0x10, 0x50], // P + 0x51: [0x51, 0x11, 0x51], // Q + 0x52: [0x52, 0x12, 0x52], // R + 0x53: [0x53, 0x13, 0x53], // S + 0x54: [0x54, 0x14, 0x54], // T + 0x55: [0x55, 0x15, 0x55], // U + 0x56: [0x56, 0x16, 0x56], // V + 0x57: [0x57, 0x17, 0x57], // W + 0x58: [0x58, 0x18, 0x58], // X + 0x59: [0x59, 0x19, 0x59], // Y + 0x5a: [0x5a, 0x1a, 0x5a], // Z + // 0x5B: [0x5B, 0x1B, 0x5B], // [ - ESC + // 0x5C: [0x5C, 0x1C, 0x5C], // \ + // 0x5D: [0x5D, 0x1D, 0x5D], // ] + 0x5e: [0x5e, 0x1e, 0x5e], // ^ + 0x5f: [0x5f, 0x1f, 0x5f], // _ + + // Stray keys + 0xba: [0x3b, 0x3b, 0x3a], // ; - : + 0xbb: [0x3d, 0x3d, 0x2b], // = - + + 0xbc: [0x2c, 0x2c, 0x3c], // , - < + 0xbd: [0x2d, 0x2d, 0x5f], // - - _ + 0xbe: [0x2e, 0x2e, 0x3e], // . - > + 0xbf: [0x2f, 0x2f, 0x3f], // / - ? + 0xdb: [0x5b, 0x5b, 0x5b], // [ + 0xdc: [0x5c, 0x5c, 0x5c], // \ + 0xdd: [0x5d, 0x5d, 0x5d], // ] + 0xde: [0x27, 0x27, 0x22], // ' - " + + 0xff: [0xff, 0xff, 0xff], // No comma line +} as const; + +const keys = [ + [ + ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0", ":", "-", "RESET"], + ["ESC", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "FEED", "RETURN"], + ["CTRL", "A", "S", "D", "F", "G", "H", "J", "K", "L", ";", "OUT", "CLS"], + ["SHIFT", "Z", "X", "C", "V", "B", "N", "M", ",", ".", "/", "SHIFT"], + [" "], + ], + [ + ["!", '"', "#", "$", "%", "&", "'", "(", ")", "0", "*", "=", "RESET"], + ["ESC", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "@", "LINE", "RETURN"], + ["CTRL", "A", "S", "D", "F", "BELL", "H", "J", "K", "L", "+", "RUB", "CLS"], + ["SHIFT", "Z", "X", "C", "V", "B", "^", "M", "<", ">", "?", "SHIFT"], + [" "], + ], +] as const; + +export function mapKeyEvent(evt: KeyboardEvent) { + var code = evt.keyCode; + + if (code in keymap) { + return keymap[code][evt.shiftKey ? 2 : evt.ctrlKey ? 1 : 0]; + } + + debug("Unhandled key = " + toHex(code)); + return 0xff; +} + +export class KeyBoard { + shifted = false; + controlled = false; + kb: HTMLDivElement; + + constructor( + id: string, + private cpu: CPU6502, + private io: Apple1IO, + private text: TextPage + ) { + this.kb = document.querySelector(id)!; + } + + shiftKey(down: boolean) { + this.shifted = down; + this.kb.querySelectorAll(".key-SHIFT").forEach(function (el) { + if (down) { + el.classList.add("active"); + } else { + el.classList.remove("active"); + } + }); + } + + controlKey(down: boolean) { + this.controlled = down; + this.kb.querySelectorAll(".key-CTRL").forEach(function (el) { + if (down) { + el.classList.add("active"); + } else { + el.classList.remove("active"); + } + }); + } + + create() { + var x, y, row, key, key1, key2, label, label1, label2; + + function buildLabel(k: string) { + var span = document.createElement("span"); + span.innerHTML = k; + if (k.length > 1 && !k.startsWith("&")) { + span.classList.add("small"); + } + return span; + } + + const _mouseup = ( + event: MouseEvent & { + currentTarget: HTMLElement; + } + ) => { + event.currentTarget.classList.remove("pressed"); + }; + + const _mousedown = ( + event: MouseEvent & { + currentTarget: HTMLElement; + } + ) => { + event.currentTarget.classList.add("pressed"); + var key = event.currentTarget.dataset[this.shifted ? "key2" : "key1"]; + if (!key) { + return; + } + switch (key) { + case "BELL": + key = "G"; + break; + case "RETURN": + key = "\r"; + break; + case "LINE": + case "FEED": + key = "\n"; + break; + case "RUB": + case "OUT": + key = "_"; // 0x5f + break; + case " ": + key = " "; + break; + case "ESC": + key = "\0x1b"; + break; + default: + break; + } + + if (key.length > 1) { + switch (key) { + case "SHIFT": + this.shifted = !this.shifted; + this.kb + .querySelectorAll(".key-SHIFT") + .forEach(function (el: HTMLElement) { + el.classList.toggle("active"); + }); + break; + case "CTRL": + this.controlled = !this.controlled; + this.kb.querySelectorAll(".key-CTRL").forEach(function (el) { + el.classList.toggle("active"); + }); + break; + case "RESET": + this.cpu.reset(); + break; + case "CLS": + this.text.clear(); + break; + default: + break; + } + } else { + if (this.controlled && key >= "@" && key <= "_") { + this.io.keyDown(key.charCodeAt(0) - 0x40); + } else { + this.io.keyDown(key.charCodeAt(0)); + } + } + }; + + for (y = 0; y < 5; y++) { + row = document.createElement("div"); + row.classList.add("row", "row" + y); + this.kb.append(row); + for (x = 0; x < keys[0][y].length; x++) { + key1 = keys[0][y][x]; + key2 = keys[1][y][x]; + + label = document.createElement("div"); + label1 = buildLabel(key1); + label2 = buildLabel(key2); + + key = document.createElement("div"); + key.classList.add("key", "key-" + key1.replace(/[&;]/g, "")); + + if (key1.length > 1) { + if (key1 != key2) { + key.classList.add("vcenter2"); + } else { + key.classList.add("vcenter"); + } + } + + if (key1 != key2) { + key.classList.add("key-" + key2.replace(/[&;]/g, "")); + label.append(label2); + label.append(document.createElement("br")); + } + label.append(label1); + key.append(label); + key.dataset["key1"] = key1; + key.dataset["key2"] = key2; + + if (window.ontouchstart === undefined) { + key.addEventListener("mousedown", _mousedown); + key.addEventListener("mouseup", _mouseup); + key.addEventListener("mouseout", _mouseup); + } else { + key.addEventListener("touchstart", _mousedown); + key.addEventListener("touchend", _mouseup); + key.addEventListener("touchleave", _mouseup); + } + + row.append(key); + } + } + } +}