bibcite_footnotes-8.x-1.0-beta3/js/ckeditor5_plugins/bibciteFootnotes/src/bibciteFootnotes.js
js/ckeditor5_plugins/bibciteFootnotes/src/bibciteFootnotes.js
import { Plugin } from 'ckeditor5/src/core';
import { ButtonView, ContextualBalloon } from 'ckeditor5/src/ui';
import { View } from 'ckeditor5/src/ui';
import { toWidget } from 'ckeditor5/src/widget';
import { Widget } from 'ckeditor5/src/widget';
export default class BibciteFootnotes extends Plugin {
_currentElement = null;
_references = [];
static get requires() {
return [ContextualBalloon, Widget];
}
init() {
const editor = this.editor;
const t = editor.t;
this._balloon = editor.plugins.get(ContextualBalloon);
// Initialize references immediately
this._loadReferences();
// Register the schema
editor.model.schema.register('bibciteFootnote', {
inheritAllFrom: '$inlineObject',
allowAttributes: ['entityId', 'pageRange']
});
this._setupSimpleConversion();
// Add Alt+C keyboard shortcut
editor.keystrokes.set('Alt+C', (data, cancel) => {
const selection = editor.model.document.selection;
const selectedElement = selection.getSelectedElement();
if (selectedElement && selectedElement.is('element', 'bibciteFootnote')) {
this.showUI(selectedElement);
} else {
this.showUI();
}
cancel();
});
// UI component factory
editor.ui.componentFactory.add('bibciteFootnotes', locale => {
const button = new ButtonView(locale);
button.set({
label: t('Cite'),
withText: true,
tooltip: t('Insert citation (Alt+C)'),
});
button.on('execute', () => {
this.showUI();
});
return button;
});
// Click and keyboard handlers
this.listenTo(editor.editing.view.document, 'click', (evt, data) => {
const modelElement = editor.editing.mapper.toModelElement(data.target);
if (modelElement?.is('element', 'bibciteFootnote')) {
this.showUI(modelElement);
}
});
this.listenTo(editor.editing.view.document, 'keydown', (evt, data) => {
const modelElement = editor.editing.mapper.toModelElement(data.target);
if (modelElement?.is('element', 'bibciteFootnote')) {
if (data.keyCode === 13 || data.keyCode === 32) {
data.preventDefault();
this.showUI(modelElement);
}
}
});
editor.accessibility.addKeystrokeInfos({
keystrokes: [
{
label: t('Insert citation'),
keystroke: "Alt+c",
}
]
});
}
_loadReferences() {
try {
this._references = window.parent.drupalSettings?.bibcite_footnotes?.references || [];
} catch (e) {
console.error('Could not access Drupal settings:', e);
}
}
_setupSimpleConversion() {
const editor = this.editor;
const truncateText = (text, maxLength = 20) => {
if (!text || text.length <= maxLength) return text;
const commaIndex = text.substring(0, maxLength).lastIndexOf(',');
if (commaIndex > 0 && commaIndex <= maxLength) {
return text.substring(0, commaIndex) + '…';
}
const spaceIndex = text.substring(0, maxLength).lastIndexOf(' ');
if (spaceIndex > 0 && spaceIndex <= maxLength) {
return text.substring(0, spaceIndex) + '…';
}
return text.substring(0, maxLength - 1) + '…';
};
// Create a mapping of entity IDs to reference titles
const referenceMap = {};
this._references.forEach(ref => {
referenceMap[ref[1]] = ref[0];
});
// Editing downcast
editor.conversion.for('editingDowncast').elementToElement({
model: 'bibciteFootnote',
view: (modelElement, { writer }) => {
const entityId = modelElement.getAttribute('entityId');
const pageRange = modelElement.getAttribute('pageRange');
const fullTitle = referenceMap[entityId] || `Citation ${entityId}`;
const truncatedTitle = truncateText(fullTitle);
const labelText = `Citation: ${fullTitle}${pageRange ? ', pages ' + pageRange : ''}`;
const visibleText = pageRange ?
`[${truncatedTitle}, pp. ${pageRange}]` :
`[${truncatedTitle}]`;
const span = writer.createContainerElement(
'span',
{
class: 'bibcite-footnote-placeholder',
role: 'button',
'aria-haspopup': 'dialog',
'aria-label': labelText,
tabindex: '0'
},
writer.createText(visibleText)
);
return toWidget(span, writer, { label: labelText });
}
});
// Data downcast for output
editor.conversion.for('dataDowncast').elementToElement({
model: 'bibciteFootnote',
view: (modelElement, { writer }) => {
const entityId = modelElement.getAttribute('entityId');
const pageRange = modelElement.getAttribute('pageRange') || '';
return writer.createContainerElement('bibcite-footnote', {
'data-entity-id': entityId,
'data-page-range': pageRange
});
}
});
// Upcast conversion
editor.conversion.for('upcast').elementToElement({
view: {
name: 'bibcite-footnote',
attributes: ['data-entity-id']
},
model: (viewElement, { writer }) => {
return writer.createElement('bibciteFootnote', {
entityId: viewElement.getAttribute('data-entity-id'),
pageRange: viewElement.getAttribute('data-page-range') || ''
});
}
});
}
_createFormView() {
const editor = this.editor;
const t = editor.t;
const formView = new View(editor.locale);
formView.saveButtonView = new ButtonView(editor.locale);
formView.saveButtonView.set({
label: t('Save'),
withText: true,
class: 'ck-button-save'
});
formView.saveButtonView.on('execute', () => {
this._handleFormSubmit();
});
formView.cancelButtonView = new ButtonView(editor.locale);
formView.cancelButtonView.set({
label: t('Cancel'),
withText: true,
class: 'ck-button-cancel'
});
formView.cancelButtonView.on('execute', () => {
this._hideUI();
});
// Create a simple template that we can update dynamically
formView.setTemplate({
tag: 'div',
attributes: {
class: ['ck', 'ck-bibcite-form'],
tabindex: '-1',
},
children: [
{
tag: 'div',
attributes: { class: ['ck', 'ck-bibcite-form__row'] },
children: [
{
tag: 'label',
attributes: { for: 'work-select', class: ['ck', 'ck-label'] },
children: [t('Work to cite')]
},
{
tag: 'select',
attributes: {
class: ['ck', 'ck-input', 'ck-bibcite-work-select'],
id: 'work-select',
},
children: [] // Will be populated dynamically
}
]
},
{
tag: 'div',
attributes: { class: ['ck', 'ck-bibcite-form__row'] },
children: [
{
tag: 'label',
attributes: { for: 'page-input', class: ['ck', 'ck-label'] },
children: [t('Page(s)')]
},
{
tag: 'input',
attributes: {
class: ['ck', 'ck-input', 'ck-bibcite-page-input'],
type: 'text',
id: 'page-input',
placeholder: t('e.g., 42-45'),
}
}
]
},
{
tag: 'div',
attributes: { class: ['ck', 'ck-bibcite-form__actions'] },
children: [
formView.cancelButtonView,
formView.saveButtonView
]
}
]
});
// Add keydown event listener for Escape and Enter keys
formView.on('render', () => {
if (formView.element) {
formView.element.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
event.preventDefault();
event.stopPropagation();
this._hideUI();
} else if (event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
this._handleFormSubmit();
}
});
}
});
return formView;
}
_updateFormViewReferences() {
if (!this.formView || !this.formView.element) {
return;
}
const workSelect = this.formView.element.querySelector('.ck-bibcite-work-select');
if (!workSelect) {
return;
}
// Store current selection
const currentSelection = workSelect.value;
// Clear all options
workSelect.innerHTML = '';
// Add default option
const defaultOption = document.createElement('option');
defaultOption.value = '';
defaultOption.textContent = this.editor.t('-- Select --');
workSelect.appendChild(defaultOption);
// Add current references
this._references.forEach(ref => {
const option = document.createElement('option');
option.value = ref[1];
option.textContent = ref[0];
workSelect.appendChild(option);
});
// Restore selection if possible
if (currentSelection && Array.from(workSelect.options).some(opt => opt.value === currentSelection)) {
workSelect.value = currentSelection;
} else if (this._currentElement) {
const entityId = this._currentElement.getAttribute('entityId');
if (entityId && Array.from(workSelect.options).some(opt => opt.value === entityId)) {
workSelect.value = entityId;
}
}
console.log('Dropdown updated with', this._references.length, 'references');
}
showUI(modelElement = null) {
this._currentElement = modelElement;
// Always reload references and update the form when showing UI
this._loadReferences();
// Create form view if it doesn't exist
if (!this.formView) {
this.formView = this._createFormView();
}
// Update the form view with current references
if (!this._balloon.hasView(this.formView)) {
this._balloon.add({
view: this.formView,
position: this._getBalloonPositionData()
});
// Wait for the form to render, then populate the dropdown
setTimeout(() => {
this._updateFormViewReferences();
this._focusForm();
}, 50);
} else {
// Form is already visible, just update it
this._updateFormViewReferences();
this._focusForm();
}
}
_focusForm() {
if (this.formView.element) {
const workSelect = this.formView.element.querySelector('.ck-bibcite-work-select');
const pageInput = this.formView.element.querySelector('.ck-bibcite-page-input');
if (this._currentElement) {
const entityId = this._currentElement.getAttribute('entityId');
if (workSelect) {
workSelect.value = entityId || '';
}
if (pageInput) {
pageInput.value = this._currentElement.getAttribute('pageRange') || '';
}
} else {
if (workSelect) {
workSelect.selectedIndex = 0;
}
if (pageInput) {
pageInput.value = '';
}
}
if (workSelect) {
workSelect.focus();
}
}
}
_handleFormSubmit() {
const editor = this.editor;
const formElement = this.formView.element;
const workSelect = formElement.querySelector('.ck-bibcite-work-select');
const pageInput = formElement.querySelector('.ck-bibcite-page-input');
if (!workSelect) return;
const entityId = workSelect.value;
const pages = pageInput.value.trim();
if (entityId) {
editor.model.change(writer => {
if (this._currentElement) {
writer.setAttribute('entityId', entityId, this._currentElement);
writer.setAttribute('pageRange', pages, this._currentElement);
} else {
const selection = editor.model.document.selection;
const position = selection.getFirstRange().start;
const citationElement = writer.createElement('bibciteFootnote', { entityId, pageRange: pages });
writer.insert(citationElement, position);
}
});
}
this._hideUI();
}
_hideUI() {
if (this.formView && this._balloon.hasView(this.formView)) {
this._balloon.remove(this.formView);
}
this._currentElement = null;
this.editor.editing.view.focus();
}
_getBalloonPositionData() {
const view = this.editor.editing.view;
const target = view.domConverter.viewRangeToDom(
view.document.selection.getFirstRange()
);
return { target };
}
}
