Interactive to-do list functionality with checkbox interactions, state management, and visual feedback for task completion tracking.
Main plugin that provides complete to-do list functionality.
/**
* The to-do list feature.
* This is a "glue" plugin that loads the to-do list editing feature and to-do list UI feature.
*/
class TodoList extends Plugin {
/** @inheritDoc */
static get requires(): readonly [typeof TodoListEditing, typeof TodoListUI];
/** @inheritDoc */
static get pluginName(): "TodoList";
/** @inheritDoc */
static override get isOfficialPlugin(): true;
}Core editing functionality for to-do lists including checkbox state management and interactions.
/**
* The editing part of the to-do list feature. It handles creating, editing and removing to-do lists and to-do list items.
*/
class TodoListEditing extends Plugin {
/** @inheritDoc */
static get pluginName(): "TodoListEditing";
/** @inheritDoc */
static get requires(): readonly [typeof ListEditing, typeof TodoCheckboxChangeObserver];
}User interface for to-do lists including toolbar buttons and visual elements.
/**
* The to-do list UI feature. It introduces the `'todoList'` button that allows to convert paragraphs
* to and from to-do list items and to toggle the checked state of the to-do list items.
*/
class TodoListUI extends Plugin {
/** @inheritDoc */
static get pluginName(): "TodoListUI";
/** @inheritDoc */
static get requires(): readonly [typeof TodoListEditing];
}Command for toggling the checked state of to-do list items.
/**
* The check to-do list command.
* This command is used by the to-do list feature to check and uncheck to-do list items.
*/
class CheckTodoListCommand extends Command {
/**
* @inheritDoc
*/
constructor(editor: Editor);
/**
* Executes the check to-do list command.
* @param options Command execution options
* @param options.forceValue Force the checked state to a specific value. If not provided, toggles the current state
*/
execute(options?: { forceValue?: boolean }): void;
/**
* The current value of the command (whether the current to-do item is checked).
*/
value: boolean | null;
/**
* Checks whether the command can be enabled in the current context.
*/
isEnabled: boolean;
}Event fired when a to-do checkbox is clicked in the editing view.
import { type ViewDocumentDomEventData } from "ckeditor5/src/engine.js";
/**
* Event fired when a to-do checkbox is clicked in the editing view.
*/
export type ViewDocumentTodoCheckboxChangeEvent = {
name: 'todoCheckboxChange';
args: [ data: ViewDocumentDomEventData<Event> ];
};Observer that monitors checkbox interactions in the editing view.
/**
* Observer for the to-do checkbox change event.
* It observes changes to the to-do list checkboxes in the editing view and fires the `todoCheckboxChange` event.
*/
class TodoCheckboxChangeObserver extends Observer {
/**
* @inheritDoc
*/
constructor(view: View);
/**
* @inheritDoc
*/
observe(domElement: Element): void;
/**
* @inheritDoc
*/
stopObserving(domElement: Element): void;
}import { ClassicEditor } from "@ckeditor/ckeditor5-editor-classic";
import { List, TodoList } from "@ckeditor/ckeditor5-list";
ClassicEditor
.create(document.querySelector('#editor'), {
plugins: [List, TodoList],
toolbar: ['numberedList', 'bulletedList', 'todoList']
})
.then(editor => {
console.log('Editor with TodoList initialized');
});// Get the to-do list command
const todoListCommand = editor.commands.get('todoList');
const checkTodoCommand = editor.commands.get('checkTodoList');
// Create a to-do list
if (todoListCommand.isEnabled) {
todoListCommand.execute();
}
// Toggle checkbox state
if (checkTodoCommand.isEnabled) {
checkTodoCommand.execute(); // Toggle current state
// Or force a specific state
checkTodoCommand.execute({ forceValue: true }); // Check
checkTodoCommand.execute({ forceValue: false }); // Uncheck
}
// Check current state
console.log('Current to-do item checked:', checkTodoCommand.value);
console.log('In to-do list:', todoListCommand.value);// Listen to checkbox change events
editor.editing.view.document.on('todoCheckboxChange', (evt, data) => {
console.log('Checkbox clicked:', data.target);
console.log('New checked state:', data.checked);
// Perform custom actions based on checkbox changes
if (data.checked) {
console.log('Task completed!');
// Could trigger animations, sounds, analytics, etc.
} else {
console.log('Task unchecked');
}
});
// Listen to command state changes
const checkCommand = editor.commands.get('checkTodoList');
checkCommand.on('change:value', () => {
console.log('Todo item checked state changed:', checkCommand.value);
});
checkCommand.on('change:isEnabled', () => {
console.log('Check command enabled:', checkCommand.isEnabled);
});import { Plugin } from 'ckeditor5/src/core';
class TodoListExtensionPlugin extends Plugin {
static get requires() {
return [TodoList];
}
init() {
const editor = this.editor;
// Add custom behavior when tasks are completed
editor.editing.view.document.on('todoCheckboxChange', (evt, data) => {
if (data.checked) {
this.onTaskCompleted(data.target);
} else {
this.onTaskUnchecked(data.target);
}
});
// Add custom command for bulk operations
editor.commands.add('checkAllTodos', new CheckAllTodosCommand(editor));
editor.commands.add('uncheckAllTodos', new UncheckAllTodosCommand(editor));
}
onTaskCompleted(element) {
// Add completion timestamp or visual effects
console.log('Task completed at:', new Date().toISOString());
// Could add strikethrough style or fade effect
element.style.opacity = '0.6';
}
onTaskUnchecked(element) {
// Remove completion effects
element.style.opacity = '';
}
}
class CheckAllTodosCommand extends Command {
execute() {
const model = this.editor.model;
const todoItems = this.getAllTodoItems();
model.change(writer => {
todoItems.forEach(item => {
writer.setAttribute('todoListChecked', true, item);
});
});
}
getAllTodoItems() {
const model = this.editor.model;
const root = model.document.getRoot();
const todoItems = [];
model.createRangeIn(root).getWalker().forEach(({ item }) => {
if (item.is('element') && item.hasAttribute('listType') &&
item.getAttribute('listType') === 'todo') {
todoItems.push(item);
}
});
return todoItems;
}
}class TodoProgressPlugin extends Plugin {
static get requires() {
return [TodoList];
}
init() {
const editor = this.editor;
// Track progress changes
editor.model.document.on('change:data', () => {
this.updateProgress();
});
// Initial progress calculation
editor.on('ready', () => {
this.updateProgress();
});
}
updateProgress() {
const progress = this.calculateProgress();
this.displayProgress(progress);
}
calculateProgress() {
const model = this.editor.model;
const root = model.document.getRoot();
let totalTodos = 0;
let checkedTodos = 0;
model.createRangeIn(root).getWalker().forEach(({ item }) => {
if (item.is('element') && item.hasAttribute('listType') &&
item.getAttribute('listType') === 'todo') {
totalTodos++;
if (item.hasAttribute('todoListChecked') &&
item.getAttribute('todoListChecked')) {
checkedTodos++;
}
}
});
return {
total: totalTodos,
completed: checkedTodos,
percentage: totalTodos > 0 ? Math.round((checkedTodos / totalTodos) * 100) : 0
};
}
displayProgress(progress) {
const progressElement = document.querySelector('#todo-progress');
if (progressElement) {
progressElement.innerHTML = `
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress.percentage}%"></div>
</div>
<div class="progress-text">
${progress.completed}/${progress.total} tasks completed (${progress.percentage}%)
</div>
`;
}
}
}// Custom configuration with keyboard shortcuts and styling
ClassicEditor
.create(document.querySelector('#editor'), {
plugins: [List, TodoList],
toolbar: ['numberedList', 'bulletedList', 'todoList'],
// Custom keystrokes for todo operations
keystrokes: [
// Ctrl+Shift+T to toggle todo list
['Ctrl+Shift+T', 'todoList'],
// Ctrl+Shift+C to toggle checkbox
['Ctrl+Shift+C', 'checkTodoList']
]
})
.then(editor => {
// Add custom styling for completed todos
const editingView = editor.editing.view;
editingView.change(writer => {
writer.setStyle('color', '#666',
editingView.document.getRoot().getChild(0));
});
console.log('Advanced TodoList setup complete');
});class TodoPersistencePlugin extends Plugin {
static get requires() {
return [TodoList];
}
init() {
const editor = this.editor;
// Save state on changes
editor.model.document.on('change:data', () => {
this.saveState();
});
// Restore state on load
editor.on('ready', () => {
this.restoreState();
});
}
saveState() {
const todoStates = this.extractTodoStates();
localStorage.setItem('todoStates', JSON.stringify(todoStates));
}
restoreState() {
const savedStates = localStorage.getItem('todoStates');
if (savedStates) {
const todoStates = JSON.parse(savedStates);
this.applyTodoStates(todoStates);
}
}
extractTodoStates() {
const model = this.editor.model;
const root = model.document.getRoot();
const states = [];
model.createRangeIn(root).getWalker().forEach(({ item, type }) => {
if (type === 'elementStart' &&
item.hasAttribute('listType') &&
item.getAttribute('listType') === 'todo') {
states.push({
id: item.getAttribute('listItemId'),
checked: item.hasAttribute('todoListChecked') &&
item.getAttribute('todoListChecked')
});
}
});
return states;
}
applyTodoStates(states) {
const model = this.editor.model;
model.change(writer => {
states.forEach(state => {
const item = this.findTodoItemById(state.id);
if (item) {
writer.setAttribute('todoListChecked', state.checked, item);
}
});
});
}
findTodoItemById(id) {
const model = this.editor.model;
const root = model.document.getRoot();
for (const { item } of model.createRangeIn(root).getWalker()) {
if (item.is('element') &&
item.getAttribute('listItemId') === id) {
return item;
}
}
return null;
}
}