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 };
  }
}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc