Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7,252 changes: 359 additions & 6,893 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"source": "src/index.js",
"main": "dist/checkboxland.js",
"types": "dist/checkboxland.d.ts",
"exports": "./dist/checkboxland.modern.js",
"exports": "./dist/checkboxland.js",
"files": [
"package.json",
"README.md",
Expand All @@ -25,12 +25,11 @@
"homepage": "https://github.com/bryanbraun/checkboxland#readme",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 0",
"build": "microbundle --format modern --target web --tsconfig tsconfig.json",
"build": "tsc",
"dev": "http-server"
},
"devDependencies": {
"http-server": "^14.1.0",
"microbundle": "^0.15.1",
"typescript": "^5.8.2"
"typescript": "^5.9.3"
}
}
112 changes: 100 additions & 12 deletions src/checkboxland.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,41 @@
/**
* @typedef {Object} Plugin
* @property {string} name - The name of the plugin. This will be the name of the method added to the Checkboxland class.
* @property {function} exec - The function that will be executed when the plugin method is called. This function should use "this" to refer to the Checkboxland instance.
* @property {function} [cleanUp] - An optional function that will be executed when the plugin is cleaned up. This is useful for plugins that set up intervals, event listeners, or other resources that need to be cleaned up to prevent memory leaks.
*/

export class Checkboxland {

/**
* @param {{ fillValue?: number, dimensions?: string, selector?: string }} props
*/
constructor(props = {}) {
if (typeof props.fillValue !== 'undefined') _checkForValidValue(props.fillValue);

this.dimensions = _textDimensionsToArray(props.dimensions || '8x8');
/**
* @public
* @type {HTMLElement}
*/
this.displayEl = _getValidHTMLContainer(props.selector);
this.#data = this.getEmptyMatrix({ fillValue: props.fillValue || 0 });

_createInitialCheckboxDisplay(this.displayEl, this.#data);
}

#data; // Private property for storing the checkbox data matrix

/**
* Private property for storing the checkbox data matrix
* @type {number[][]}
*/
#data;

/**
* Gets the value of a specific checkbox in the matrix.
* @param {number} x - The x-coordinate of the checkbox.
* @param {number} y - The y-coordinate of the checkbox.
* @returns {number} The value of the checkbox (0, 1, or 2).
*/
getCheckboxValue(x, y) {
const isWithinDisplay = (x >= 0 && y >= 0 && x < this.dimensions[0] && y < this.dimensions[1]);

Expand All @@ -22,6 +46,13 @@ export class Checkboxland {
return this.#data[y][x];
}

/**
* Sets the value of a specific checkbox in the matrix.
* @param {number} x - The x-coordinate of the checkbox.
* @param {number} y - The y-coordinate of the checkbox.
* @param {number} newValue - The new value to set (0, 1, or 2).
* @returns {void}
*/
setCheckboxValue(x, y, newValue) {
const isWithinDisplay = (x >= 0 && y >= 0 && x < this.dimensions[0] && y < this.dimensions[1]);

Expand All @@ -32,7 +63,7 @@ export class Checkboxland {
this.#data[y][x] = newValue;

// We can assume the checkboxEl exists because it's within the display.
const checkboxEl = this.displayEl.children[y].children[x];
const checkboxEl = /** @type {HTMLInputElement} */ (this.displayEl.children[y].children[x]);

// Handle indeterminate newValues
if (newValue === 2) {
Expand Down Expand Up @@ -62,6 +93,11 @@ export class Checkboxland {
return clonedData;
}

/**
* Sets the data for the checkbox matrix.
* @param {number[][]} data - The new data to set.
* @param {{ x?: number, y?: number, fillValue?: number }} options - Options for setting the data.
*/
setData(data, options = {}) {
const { x = 0, y = 0, fillValue } = options;
const isFillValueProvided = (typeof fillValue !== 'undefined');
Expand All @@ -80,7 +116,7 @@ export class Checkboxland {

if (isOutsideOfProvidedData && !isFillValueProvided) continue;

let valueToSet = isOutsideOfProvidedData ? fillValue : data[rowIndex - y][colIndex - x];
let valueToSet = /** @type {number} */ (isOutsideOfProvidedData ? fillValue : data[rowIndex - y][colIndex - x]);

this.setCheckboxValue(colIndex, rowIndex, valueToSet);
}
Expand All @@ -92,10 +128,17 @@ export class Checkboxland {
this.setData(emptyMatrix);
}

// This kind of method makes more sense as a plugin but I needed to
// use it in the core library anyways so I decided to expose it here.
/**
* This kind of method makes more sense as a plugin but I needed to
* use it in the core library anyways so I decided to expose it here.
* @param {{ fillValue?: number, width?: number, height?: number }} options
* @returns {number[][]}
*/
getEmptyMatrix(options = {}) {
const { fillValue = 0, width = this.dimensions[0], height = this.dimensions[1] } = options;
/**
* @type {number[][]}
*/
const matrix = [];

for (let i = 0; i < height; i++) {
Expand All @@ -108,7 +151,15 @@ export class Checkboxland {
return matrix;
}

static extend(pluginObj = {}) {
/**
* Extends the checkboxland class with a new plugin.
* @param {Plugin} pluginObj
*/
static extend(pluginObj) {
if (!pluginObj || typeof pluginObj !== 'object') {
throw new Error('You must provide a plugin object to extend checkboxland.');
}

const { name, exec, cleanUp } = pluginObj;

if (!name || !exec) {
Expand All @@ -124,40 +175,72 @@ export class Checkboxland {
}

if (cleanUp) {
// @ts-ignore
exec.cleanUp = cleanUp;
}

// @ts-ignore
this.prototype[name] = exec;
}
}


// Private helper functions

/**
* Checks if the provided value is a valid checkbox value (0, 1, or 2).
* @param {number} value
* @returns {void}
* @throws Will throw an error if the value is not valid.
*/
function _checkForValidValue(value) {
if (value === 0 || value === 1 || value === 2) return;

throw new Error(`${value} is not a valid checkbox value.`);
}

/**
* Checks if the provided matrix is a valid checkbox matrix. A valid matrix is a 2D array (array of arrays).
* @param {number[][]} matrix
* @returns {void}
* @throws Will throw an error if the matrix is not valid.
*/
function _checkForValidMatrix(matrix) {
if (Array.isArray(matrix) && Array.isArray(matrix[0])) return;

throw new Error(`${matrix} is not a valid matrix.`);
}

/**
* Gets a valid HTML container element from the provided selector. The selector can be either a string (CSS selector) or an actual HTML element. If the selector is invalid, an error is thrown.
* @param {string | HTMLElement} selector
* @returns {HTMLElement}
*/
function _getValidHTMLContainer(selector = '#checkboxland') {
if (selector instanceof Element) {
if (selector instanceof HTMLElement) {
return selector;
}

if (typeof selector === 'string') {
return document.querySelector(selector);
const el = document.querySelector(selector);

// querySelector can return any Element,
// and we need a valid styleable HTMLElement.
if (!el || !(el instanceof HTMLElement)) {
throw new Error(`No element found for the selector "${selector}".`);
}

return el;
}

throw new Error(`Checkboxland selector is invalid.`);
}

/**
* Converts a text representation of dimensions (e.g., "100x200") into an array of numbers. The text must be in the format "widthxheight". If the format is invalid, an error is thrown.
* @param {string} textDimensions - The text representation of dimensions.
* @returns {number[]}
*/
function _textDimensionsToArray(textDimensions) {
const errorMessage = 'The dimensions you provided are invalid.';

Expand All @@ -171,24 +254,29 @@ function _textDimensionsToArray(textDimensions) {
return textDimensions.split('x').map(val => Number(val));
}

/**
* Creates the initial checkbox display.
* @param {HTMLElement} displayEl
* @param {number[][]} data
*/
function _createInitialCheckboxDisplay(displayEl, data) {
displayEl.innerHTML = '';
displayEl.style.overflowX = 'auto';

data.forEach(rowData => {
const rowEl = document.createElement('div');
rowEl.style.lineHeight = 0.75;
rowEl.style.lineHeight = '0.75';
rowEl.style.whiteSpace = 'nowrap';

rowData.forEach(cellData => {
const checkboxEl = document.createElement('input');
const indeterminateVal = cellData === 2 ? true : false;
const checkedVal = indeterminateVal ? false : Boolean(cellData);

checkboxEl.style.margin = 0;
checkboxEl.style.margin = '0';
checkboxEl.style.verticalAlign = 'top';
checkboxEl.type = 'checkbox';
checkboxEl.tabIndex = '-1';
checkboxEl.tabIndex = -1;
checkboxEl.checked = checkedVal;
checkboxEl.indeterminate = indeterminateVal;

Expand Down
31 changes: 30 additions & 1 deletion src/plugins/dataUtils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
/**
* @typedef {Object} Options
* @property {number} [all]
* @property {number} [top]
* @property {number} [right]
* @property {number} [bottom]
* @property {number} [left]
*/

/**
* @param {'invert' | 'pad'} actionName
* @param {number[][]} matrix
* @param {Options} options
* @returns
*/
function dataUtils(actionName, matrix, options) {
const actions = {
invert,
Expand All @@ -7,6 +22,11 @@ function dataUtils(actionName, matrix, options) {
return actions[actionName](matrix, options);
}

/**
*
* @param {number[][]} matrix
* @returns
*/
function invert(matrix) {
return matrix.map((row) => {
return row.map((value) => {
Expand All @@ -15,6 +35,12 @@ function invert(matrix) {
});
}

/**
*
* @param {number[][]} matrix
* @param {Options} options
* @returns
*/
function pad(matrix, options = {}) {
const isPaddingAllSidesEqually = Number.isInteger(options.all);

Expand All @@ -39,7 +65,7 @@ function pad(matrix, options = {}) {

// Set up to add top and bottom padding.
const newRowLength = newMatrix[0].length;
const buildPaddingRows = (numberOfRows, rowLength) => {
const buildPaddingRows = (/** @type {number} */ numberOfRows, /** @type {number} */ rowLength) => {
const paddingRows = [];
for (let i = 0; i < numberOfRows; i++) {
paddingRows.push(Array(rowLength).fill(0));
Expand All @@ -57,6 +83,9 @@ function pad(matrix, options = {}) {
return newMatrix
}

/**
* @type {import('../checkboxland.js').Plugin}
*/
export default {
name: 'dataUtils',
exec: dataUtils
Expand Down
16 changes: 16 additions & 0 deletions src/plugins/marquee.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
/**
* @typedef {Object} PluginMarquee
* @property {function(number[][], { interval?: number, repeat?: boolean, fillValue?: number, callback?: () => void }): void} marquee - Animates new data entering the grid from the right, pushing existing data to the left.
*/

/**
* @type {number}
*/
let intervalId;

/**
* @this {import('../checkboxland.js').Checkboxland}
* @param {number[][]} newData
* @param {{ interval?: number, repeat?: boolean, fillValue?: number, callback?: () => void }} options
*/
function marquee(newData, options = {}) {
const { interval = 200, repeat = false, fillValue = 0, callback = () => {} } = options;

Expand Down Expand Up @@ -51,6 +64,9 @@ function cleanUp() {
clearInterval(intervalId);
}

/**
* @type {import('../checkboxland.js').Plugin}
*/
export default {
name: 'marquee',
exec: marquee,
Expand Down
Loading