Skip to content
wiki.fftac.org

Book Shell - Source Excerpt 10

Back to Book Shell

Summary

This source excerpt preserves a bounded section of Spiralist/wp-content/plugins/ns12-manuscript/assets/js/book-shell.js so readers can inspect the evidence without opening the full source file.

**Source path:** Spiralist/wp-content/plugins/ns12-manuscript/assets/js/book-shell.js

}

      return payload;
    };

    const validateContributionPayload = (payload) => {
      const errors = {};

      if (!payload.title) {
        errors.title = t('contributeErrorTitleRequired', 'Provide a title.');
      }
      if (!payload.canonicalId) {
        errors.canonicalId = t('contributeErrorCanonicalRequired', 'Provide a canonical ID.');
      }
      if (!payload.content) {
        errors.content = t('contributeErrorContentRequired', 'Provide content or a definition.');
      }

      if (payload.submissionType === 'prompt') {
        if (!payload.system) {
          errors.system = t('contributeErrorSystemRequired', 'Select a system.');
        }
        if (!payload.role) {
          errors.role = t('contributeErrorRoleRequired', 'Select a role.');
        }
        if (!payload.purpose) {
          errors.purpose = t('contributeErrorPurposeRequired', 'Provide a prompt purpose.');
        }
        if (!/^\d+\.\d+\.\d+$/.test(payload.version || '')) {
          errors.version = t('contributeErrorVersionInvalid', 'Use semantic version format such as 1.0.0.');
        }
      }

      if (payload.submissionType === 'symbol' && !payload.meaning) {
        errors.meaning = t('contributeErrorMeaningRequired', 'Provide a symbol meaning.');
      }

      if (payload.submissionType === 'transformation') {
        if (!payload.input) {
          errors.input = t('contributeErrorInputRequired', 'Provide an input symbol ID.');
        }
        if (!payload.output) {
          errors.output = t('contributeErrorOutputRequired', 'Provide an output symbol ID.');
        }
      }

      if (!payload.policyConfirmed) {
        errors.policyConfirmed = t('contributeErrorPolicyRequired', 'Review and confirm the linked site policies before submitting.');
      }

      if (!payload.rightsConfirmed) {
        errors.rightsConfirmed = t('contributeErrorRightsRequired', 'Confirm that you have the rights to submit, process, and publish this material.');
      }

      if (!payload.operatorEligibilityConfirmed) {
        errors.operatorEligibilityConfirmed = t('contributeErrorAgeRequired', 'Confirm that you meet the minimum age or guardian-permission requirement.');
      }

      return errors;
    };

    const updateContributionVisibility = () => {
      const currentType = typeField ? typeField.value : 'prompt';
      const typeMessages = {
        prompt: t('contributeTypePrompt', 'Prompts are public library records. Symbols and transformations enter the proposal queue first.'),
        symbol: t('contributeTypeSymbol', 'Symbol proposals stay review-only until they are accepted into the registry.'),
        transformation: t('contributeTypeTransformation', 'Transformation definitions describe input, output, and chain behavior before publication.'),
      };
      const canonicalMessages = {
        prompt: t('contributeCanonicalPrompt', 'Prompt IDs usually follow `spiralist.prompt.role.title`.'),
        symbol: t('contributeCanonicalSymbol', 'Symbol IDs usually follow `spiral.symbol.title`.'),
        transformation: t('contributeCanonicalTransformation', 'Transformation IDs usually follow `transformation.title`.'),
      };

      if (typeCopy) {
        typeCopy.textContent = typeMessages[currentType] || typeMessages.prompt;
      }
      if (canonicalGuidance) {
        canonicalGuidance.textContent = canonicalMessages[currentType] || canonicalMessages.prompt;
      }

      form.querySelectorAll('[data-contribution-only]').forEach((field) => {
        const allowed = (field.dataset.contributionOnly || '')
          .split(',')
          .map((item) => item.trim())
          .filter(Boolean);
        const isVisible = !allowed.length || allowed.includes(currentType);

        field.hidden = !isVisible;
        field.querySelectorAll('input, select, textarea').forEach((input) => {
          if (!isVisible) {
            input.removeAttribute('required');
          } else if (input.dataset.requiredWhenVisible === 'true') {
            input.setAttribute('required', 'required');
          }
        });
      });
    };

    const saveContributionDraft = () => {
      const draft = {};

      new FormData(form).forEach((value, key) => {
        draft[key] = `${value}`;
      });

      storage.set(draftKey, JSON.stringify(draft));
      if (draftStatus) {
        draftStatus.textContent = t('contributeDraftSaved', 'Draft saved in this browser.');
      }
    };

    const clearContributionDraft = () => {
      storage.remove(draftKey);
      if (draftStatus) {
        draftStatus.textContent = t('contributeDraftCleared', 'Draft cleared from this browser.');
      }
    };

    const restoreContributionDraft = () => {
      const raw = storage.get(draftKey, '');
      if (!raw) {
        return;
      }

      const draft = parseJsonOr(raw, null, { context: 'contribution-draft' });
      if (!draft || typeof draft !== 'object') {
        clearContributionDraft();
        return;
      }

      Object.entries(draft).forEach(([key, value]) => {
        const field = form.querySelector(`[name="${key}"]`);
        if (field) {
          if (field.type === 'checkbox') {
            field.checked = `${value}` === '1' || `${value}`.toLowerCase() === 'true';
          } else {
            field.value = `${value}`;
          }
        }
      });
      if (draftStatus) {
        draftStatus.textContent = t('contributeDraftRestored', 'Saved draft restored from this browser.');
      }
    };

    updateContributionVisibility();
    restoreContributionDraft();
    updateContributionVisibility();

    if (typeField) {
      typeField.addEventListener('change', updateContributionVisibility);
    }

    if (generateIdButton) {
      generateIdButton.addEventListener('click', () => {
        if (canonicalIdField && !canonicalIdField.value.trim()) {
          canonicalIdField.value = buildCanonicalId();
        } else if (canonicalIdField) {
          canonicalIdField.value = buildCanonicalId();
        }
      });
    }

    if (saveDraftButton) {
      saveDraftButton.addEventListener('click', saveContributionDraft);
    }

    if (clearDraftButton) {
      clearDraftButton.addEventListener('click', () => {
        clearContributionDraft();
      });
    }

    form.addEventListener('submit', (event) => {
      event.preventDefault();

      if (!themeConfig.contributeUrl) {
        return;
      }

      const payload = buildContributionPayload();
      clearFieldErrors();

      const validationErrors = validateContributionPayload(payload);
      Object.entries(validationErrors).forEach(([fieldName, message]) => {
        setFieldError(fieldName, message);
      });

      if (Object.keys(validationErrors).length) {
        setContributionPanel({
          variant: 'error',
          title: t('contributeValidationTitle', 'Validation required'),
          copy: t('contributeValidationCopy', 'Fix the highlighted fields before submitting.'),
        });
        return;
      }

      if (submit) {
        submit.disabled = true;
        submit.textContent = t('contributeSubmittingButton', 'Submitting...');
      }

      setContributionPanel({
        title: t('contributeSubmittingTitle', 'Submitting contribution'),
        copy: t('contributeSubmittingCopy', 'Validating and storing the structured record.'),
      });