Skip to content

Commit

Permalink
EMMR-111 Add transcription marginalia CKE5 plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
camilocodes committed Nov 24, 2023
1 parent ba30b42 commit 886e8a8
Show file tree
Hide file tree
Showing 11 changed files with 355 additions and 1 deletion.
3 changes: 3 additions & 0 deletions custom/modules/emmr_transcribe/css/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@
.ckeditor5-toolbar-button-transReplace {
background-image: url(../icons/transReplace.svg);
}
.ckeditor5-toolbar-button-transMargin {
background-image: url(../icons/transMargin.svg);
}
5 changes: 4 additions & 1 deletion custom/modules/emmr_transcribe/emmr_transcribe.ckeditor5.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ emmr_transcribe_plugins:
plugins:
- transInsert.TransInsert
- transReplace.TransReplace
- transMargin.TransMargin
# *Additional configuration properties*
# config: data sent to the constructor of any CKEditor 5 plugin
# editorPluginName:
Expand All @@ -33,6 +34,8 @@ emmr_transcribe_plugins:
label: Transcription Insert
transReplace:
label: Transcription Replace
transMargin:
label: Transcription Marginalia
# If the plugin does not provide elements, set this as
# `elements: false`
elements:
Expand All @@ -46,8 +49,8 @@ emmr_transcribe_plugins:
- <span class="trxn-caret">
- <span class="trxn-text">
- <span class="trxn-retext">
- <span class="trxn-margin">
- <span class="trxn-number">
- <s>
- <s class="trxn-replaced">
# *Additional configuration properties*
# conditions: for setting additional criteria that must be met for the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ transcribe:
js:
js/build/transInsert.js: { preprocess: false, minified: false }
js/build/transReplace.js: { preprocess: false, minified: false }
js/build/transMargin.js: { preprocess: false, minified: false }
js/emmr_ckeditor_marginalia.js: { }
dependencies:
- core/ckeditor5
Expand Down
1 change: 1 addition & 0 deletions custom/modules/emmr_transcribe/icons/transMargin.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions custom/modules/emmr_transcribe/js/build/transMargin.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
This plugin is largely based on CKEditor 5's [block plugin widget tutorial](https://ckeditor.com/docs/ckeditor5/latest/framework/guides/tutorials/implementing-a-block-widget.html),
but with added documentation to facilitate better understanding of CKEditor 5
plugin development and other minor changes.

Within `/src` are the multiple files that will be used by the build process to
become a CKEditor 5 plugin in `/build`. Technically, everything in these files
could be in a single `index.js` - the only file the MUST be present is
`/src/index.js`. However, splitting the plugin into concern-specific files has
maintainability benefits.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* @file The build process always expects an index.js file. Anything exported
* here will be recognized by CKEditor 5 as an available plugin. Multiple
* plugins can be exported in this one file.
*
* I.e. this file's purpose is to make plugin(s) discoverable.
*/
// cSpell:ignore transmargin

import TransMargin from './transmargin';

export default {
TransMargin,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* @file defines InsertTransMarginCommand, which is executed when the Transcription Marginalia
* toolbar button is pressed.
*/
// cSpell:ignore transmarginediting

import { Command } from 'ckeditor5/src/core';

export default class InsertTransMarginCommand extends Command {
execute() {
const { model } = this.editor;

model.change((writer) => {
// Insert <trxnmar>*</trxnmar> at the current selection position
// in a way that will result in creating a valid model structure.
model.insertContent(createTransMargin(writer));
});
}

refresh() {
const { model } = this.editor;
const { selection } = model.document;

// Determine if the cursor (selection) is in a position where adding a
// transMargin is permitted. This is based on the schema of the model(s)
// currently containing the cursor.
const allowedIn = model.schema.findAllowedParent(
selection.getFirstPosition(),
'transMargin',
);

// If the cursor is not in a location where a transMargin can be added, return
// null so the addition doesn't happen.
this.isEnabled = allowedIn !== null;
}
}

function createTransMargin(writer) {
// Create instances of the three elements registered with the editor in
// transmarginediting.js.
const transMargin = writer.createElement('transMargin');
const transMarginNumber = writer.createElement('transMarginNumber');
writer.appendText('X', {}, transMarginNumber);
const transMarginText = writer.createElement('transMarginText');
let marginaliaText = prompt("Enter transcription marginalia text");
writer.appendText(marginaliaText, {}, transMarginText);

// Append the title and description elements to the transMargin, which matches
// the parent/child relationship as defined in their schemas.
writer.append(transMarginNumber, transMargin);
writer.append(transMarginText, transMargin);

// Return the element to be added to the editor.
return transMargin;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* @file This is what CKEditor refers to as a master (glue) plugin. Its role is
* just to load the “editing” and “UI” components of this Plugin. Those
* components could be included in this file, but
*
* I.e, this file's purpose is to integrate all the separate parts of the plugin
* before it's made discoverable via index.js.
*/
// cSpell:ignore transmarginediting transmarginui

// The contents of TransMarginUI and TransMargin editing could be included in this
// file, but it is recommended to separate these concerns in different files.
import TransMarginEditing from './transmarginediting';
import TransMarginUI from './transmarginui';
import { Plugin } from 'ckeditor5/src/core';

export default class TransMargin extends Plugin {
// Note that TransMarginEditing and TransMarginUI also extend `Plugin`, but these
// are not seen as individual plugins by CKEditor 5. CKEditor 5 will only
// discover the plugins explicitly exported in index.js.
static get requires() {
return [TransMarginEditing, TransMarginUI];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import { Plugin } from 'ckeditor5/src/core';
import { toWidget, toWidgetEditable } from 'ckeditor5/src/widget';
import { Widget } from 'ckeditor5/src/widget';
import InsertTransMarginCommand from './inserttransmargincommand';

// cSpell:ignore transmargin inserttransmargincommand

/**
* CKEditor 5 plugins do not work directly with the DOM. They are defined as
* plugin-specific data models that are then converted to markup that
* is inserted in the DOM.
*
* CKEditor 5 internally interacts with transMargin as this model:
* <transMargin>
* <transMarginNumber></transMarginNumber>
* <transMarginText></transMarginText>
* </transMargin>
*
* Which is converted for the browser/user as this markup
* <trxnmar>
* <span class="trxn-number"></span>
* <span class="trxn-text"></span>
* </trxnmar>
*
* This file has the logic for defining the transMargin model, and for how it is
* converted to standard DOM markup.
*/
export default class TransMarginEditing extends Plugin {
static get requires() {
return [Widget];
}

init() {
this._defineSchema();
this._defineConverters();
this.editor.commands.add(
'insertTransMargin',
new InsertTransMarginCommand(this.editor),
);
}

/*
* This registers the structure that will be seen by CKEditor 5 as
* <transMargin>
* <transMarginNumber></transMarginNumber>
* <transMarginText></transMarginText>
* </transMargin>
*
* The logic in _defineConverters() will determine how this is converted to
* markup.
*/
_defineSchema() {
// Schemas are registered via the central `editor` object.
const schema = this.editor.model.schema;

schema.register('transMargin', {
// Behaves like a self-contained object (e.g. an image).
isObject: true,
// Allow in places where other blocks are allowed (e.g. directly in the root).
allowWhere: '$block',
});

schema.register('transMarginNumber', {
// This creates a boundary for external actions such as clicking and
// and keypress. For example, when the cursor is inside this box, the
// keyboard shortcut for "select all" will be limited to the contents of
// the box.
isLimit: true,
// This is only to be used within transMargin.
allowIn: 'transMargin',
// Allow content that is allowed in blocks (e.g. text with attributes).
allowContentOf: '$block',
});

schema.register('transMarginText', {
isLimit: true,
allowIn: 'transMargin',
allowContentOf: '$block',
});

schema.addChildCheck((context, childDefinition) => {
// Disallow transMargin inside self or any children.
if (
context.startsWith('trans') &&
childDefinition.name === 'transMargin'
) {
return false;
}
});
}

/**
* Converters determine how CKEditor 5 models are converted into markup and
* vice-versa.
*/
_defineConverters() {
// Converters are registered via the central editor object.
const { conversion } = this.editor;

// Upcast Converters: determine how existing HTML is interpreted by the
// editor. These trigger when an editor instance loads.
//
// If <trxnmar> is present in the existing markup
// processed by CKEditor, then CKEditor recognizes and loads it as a
// <transMargin> model.
conversion.for('upcast').elementToElement({
model: 'transMargin',
view: {
name: 'trxnmar',
},
});

// If <span class="trxn-number"> is present in the existing markup
// processed by CKEditor, then CKEditor recognizes and loads it as a
// <transMarginNumber> model, provided it is a child element of <transMargin>,
// as required by the schema.
conversion.for('upcast').elementToElement({
model: 'transMarginNumber',
view: {
name: 'span',
classes: 'trxn-number',
},
});

// If <span class="trxn-text"> is present in the existing markup
// processed by CKEditor, then CKEditor recognizes and loads it as a
// <transMarginText> model, provided it is a child element of
// <transMargin>, as required by the schema.
conversion.for('upcast').elementToElement({
model: 'transMarginText',
view: {
name: 'span',
classes: 'trxn-text',
},
});

// Data Downcast Converters: converts stored model data into HTML.
// These trigger when content is saved.
//
// Instances of <transMargin> are saved as
// <trxnmar>{{inner content}}</trxnmar>.
conversion.for('dataDowncast').elementToElement({
model: 'transMargin',
view: {
name: 'trxnmar',
},
});

// Instances of <transMarginNumber> are saved as
// <span class="trxn-number">{{inner content}}</span>.
conversion.for('dataDowncast').elementToElement({
model: 'transMarginNumber',
view: {
name: 'span',
classes: 'trxn-number',
},
});

// Instances of <transMarginText> are saved as
// <span class="trxn-text">{{inner content}}</span>.
conversion.for('dataDowncast').elementToElement({
model: 'transMarginText',
view: {
name: 'span',
classes: 'trxn-text',
},
});

// Editing Downcast Converters. These render the content to the user for
// editing, i.e. this determines what gets seen in the editor. These trigger
// after the Data Upcast Converters, and are re-triggered any time there
// are changes to any of the models' properties.
//
// Convert the <transMargin> model into a container widget in the editor UI.
conversion.for('editingDowncast').elementToElement({
model: 'transMargin',
view: (modelElement, { writer: viewWriter }) => {
const trxnmar = viewWriter.createContainerElement('trxnmar', {});

return toWidget(trxnmar, viewWriter, { label: 'Transcription marginalia widget' });
},
});

// Convert the <transMarginNumber> model into an editable <span> widget.
conversion.for('editingDowncast').elementToElement({
model: 'transMarginNumber',
view: (modelElement, { writer: viewWriter }) => {
const span = viewWriter.createContainerElement('span',
{
class: 'trxn-number'
});
return toWidget(span, viewWriter);
},
});

// Convert the <transMarginText> model into an editable <span> widget.
conversion.for('editingDowncast').elementToElement({
model: 'transMarginText',
view: (modelElement, { writer: viewWriter }) => {
const span = viewWriter.createEditableElement('span', {
class: 'trxn-text',
});
return toWidgetEditable(span, viewWriter);
},
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* @file registers the transMargin toolbar button and binds functionality to it.
*/

import { Plugin } from 'ckeditor5/src/core';
import { ButtonView } from 'ckeditor5/src/ui';
import icon from '../../../../icons/transMargin.svg';

export default class TransMarginUI extends Plugin {
init() {
const editor = this.editor;

// This will register the transMargin toolbar button.
editor.ui.componentFactory.add('transMargin', (locale) => {
const command = editor.commands.get('insertTransMargin');
const buttonView = new ButtonView(locale);

// Create the toolbar button.
buttonView.set({
label: editor.t('Transcription Marginalia'),
icon,
tooltip: true,
});

// Bind the state of the button to the command.
buttonView.bind('isOn', 'isEnabled').to(command, 'value', 'isEnabled');

// Execute the command when the button is clicked (executed).
this.listenTo(buttonView, 'execute', () =>
editor.execute('insertTransMargin'),
);

return buttonView;
});
}
}

0 comments on commit 886e8a8

Please sign in to comment.