799341a1a4
Dos problemas restantes con los enlaces de wowhead: 1) Texto blanco en vez del color de calidad. theme.css solo define q3..q6, así que un ítem de calidad 0/1/2 (p.ej. verde=q2) se quedaba sin color y salía blanco. Se completan .q0/.q1/.q2/.q7 en forum.css (colores estándar de WoW) con !important para ganar al color del enlace. Además el saneador deja los enlaces de wowhead SIN nofollow (el script de wowhead ignora los nofollow al colorear). 2) Parpadeo de «item=xxx» al recargar. Salía porque el texto del enlace era un marcador que wowhead renombraba de forma asíncrona. Ahora el botón resuelve el NOMBRE y la CALIDAD en el servidor (nuevo /api/forum/wowhead, que consulta el endpoint de tooltip de wowhead en la rama WotLK y el idioma de la web) e inserta el enlace ya con su nombre real y su clase qN: sin parpadeo y ya coloreado. Si la API falla, se cae al modo anterior (data-wh-rename-link). El editor añade los colores de calidad a su content_style para que el preview salga igual. El icono lo sigue poniendo wowhead (iconizeLinks) tanto en el editor como al postear. Verificado en producción: el endpoint devuelve nombre (es), calidad e icono; el CSS sirve q2; un post con nombre real + q2 se guarda sin nofollow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
181 lines
7.6 KiB
TypeScript
181 lines
7.6 KiB
TypeScript
'use client'
|
|
|
|
import { Editor } from '@tinymce/tinymce-react'
|
|
import { useLocale } from 'next-intl'
|
|
import { wowheadUrl, wowheadData, type WowheadType } from '@/lib/wowhead'
|
|
|
|
/**
|
|
* Editor TinyMCE del foro (community, self-hosted desde /tinymce — ver
|
|
* scripts/copy-tinymce.mjs). Mismo editor que el foro original de FusionCMS, con su
|
|
* skin oscura y su lista de plugins, adaptado a React como componente controlado:
|
|
* el HTML vive en el estado del padre (value/onChange) y se envía por fetch, en vez
|
|
* del `tinyMCE.triggerSave()` + jQuery del forum.js original.
|
|
*
|
|
* `licenseKey: 'gpl'` deja claro que es la edición community (GPL), no la de nube.
|
|
* El HTML que produce se sanea SIEMPRE en el servidor con nh3 (lib/forum-sanitize),
|
|
* nunca se confía en el cliente.
|
|
*
|
|
* Botón «Wowhead»: abre un diálogo (tipo + ID + texto) e inserta un enlace de
|
|
* wowhead con su data-wowhead en el idioma de la web, para que salga el tooltip,
|
|
* el color por calidad y el icono (lo pinta el script global tooltips.js).
|
|
*/
|
|
|
|
const WOWHEAD_TYPES: { value: WowheadType; es: string; en: string }[] = [
|
|
{ value: 'item', es: 'Ítem', en: 'Item' },
|
|
{ value: 'spell', es: 'Hechizo', en: 'Spell' },
|
|
{ value: 'quest', es: 'Misión', en: 'Quest' },
|
|
{ value: 'npc', es: 'PNJ', en: 'NPC' },
|
|
{ value: 'achievement', es: 'Logro', en: 'Achievement' },
|
|
{ value: 'object', es: 'Objeto', en: 'Object' },
|
|
]
|
|
|
|
function esc(s: string): string {
|
|
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
}
|
|
|
|
export function ForumEditor({
|
|
value,
|
|
onChange,
|
|
height = 400,
|
|
}: {
|
|
value: string
|
|
onChange: (html: string) => void
|
|
height?: number
|
|
}) {
|
|
const locale = useLocale()
|
|
const es = locale !== 'en'
|
|
const L = {
|
|
title: 'Wowhead',
|
|
tooltip: es ? 'Insertar enlace de Wowhead' : 'Insert a Wowhead link',
|
|
type: es ? 'Tipo' : 'Type',
|
|
id: 'ID',
|
|
text: es ? 'Texto (vacío = nombre automático)' : 'Text (empty = auto name)',
|
|
insert: es ? 'Insertar' : 'Insert',
|
|
cancel: es ? 'Cancelar' : 'Cancel',
|
|
}
|
|
|
|
return (
|
|
<Editor
|
|
tinymceScriptSrc="/tinymce/tinymce.min.js"
|
|
licenseKey="gpl"
|
|
value={value}
|
|
onEditorChange={(content) => onChange(content)}
|
|
init={{
|
|
height,
|
|
menubar: false,
|
|
statusbar: false,
|
|
skin: 'oxide-dark',
|
|
content_css: 'dark',
|
|
// colores de calidad de WoW dentro del editor (para el preview de wowhead)
|
|
content_style:
|
|
'a{text-decoration:none}' +
|
|
'.q0{color:#9d9d9d}.q1{color:#fff}.q2{color:#1eff00}.q3{color:#0070dd}' +
|
|
'.q4{color:#a335ee}.q5{color:#ff8000}.q6{color:#e6cc80}.q7{color:#00ccff}',
|
|
branding: false,
|
|
promotion: false,
|
|
mobile: { menubar: true },
|
|
plugins:
|
|
'preview searchreplace autolink directionality visualblocks visualchars fullscreen image link media codesample table charmap pagebreak nonbreaking anchor insertdatetime advlist lists wordcount emoticons',
|
|
toolbar:
|
|
'bold italic strikethrough forecolor backcolor emoticons | link wowhead image media | alignleft aligncenter alignright alignjustify | numlist bullist outdent indent | removeformat',
|
|
image_advtab: true,
|
|
setup: (editor) => {
|
|
// Vista previa: carga el script de tooltips de wowhead DENTRO del iframe
|
|
// del editor para que el enlace insertado se vea con icono, nombre y color
|
|
// de calidad, igual que al postear. No afecta a lo que se guarda: los
|
|
// formularios envían el HTML del estado de React (lo insertado, limpio),
|
|
// no la mutación visual que wowhead hace en el iframe.
|
|
const refreshEditorWowhead = () => {
|
|
try {
|
|
const wp = (editor.getWin() as unknown as { $WowheadPower?: { refreshLinks?: () => void } })
|
|
.$WowheadPower
|
|
wp?.refreshLinks?.()
|
|
} catch {
|
|
/* preview no crítico */
|
|
}
|
|
}
|
|
editor.on('init', () => {
|
|
try {
|
|
const doc = editor.getDoc()
|
|
const win = editor.getWin() as unknown as { whTooltips?: unknown }
|
|
win.whTooltips = { colorLinks: true, iconizeLinks: true, renameLinks: true }
|
|
if (!doc.getElementById('wh-tooltips-editor')) {
|
|
const s = doc.createElement('script')
|
|
s.id = 'wh-tooltips-editor'
|
|
s.src = 'https://wow.zamimg.com/js/tooltips.js'
|
|
doc.head.appendChild(s)
|
|
}
|
|
// procesa el contenido inicial (posts en edición) cuando el script cargue
|
|
setTimeout(refreshEditorWowhead, 400)
|
|
setTimeout(refreshEditorWowhead, 1200)
|
|
} catch {
|
|
/* preview no crítico */
|
|
}
|
|
})
|
|
editor.ui.registry.addButton('wowhead', {
|
|
text: 'Wowhead',
|
|
tooltip: L.tooltip,
|
|
onAction: () => {
|
|
editor.windowManager.open({
|
|
title: L.title,
|
|
body: {
|
|
type: 'panel',
|
|
items: [
|
|
{
|
|
type: 'listbox',
|
|
name: 'wtype',
|
|
label: L.type,
|
|
items: WOWHEAD_TYPES.map((t) => ({ value: t.value, text: es ? t.es : t.en })),
|
|
},
|
|
{ type: 'input', name: 'wid', label: L.id },
|
|
{ type: 'input', name: 'wtext', label: L.text },
|
|
],
|
|
},
|
|
initialData: { wtype: 'item', wid: '', wtext: '' },
|
|
buttons: [
|
|
{ type: 'cancel', text: L.cancel },
|
|
{ type: 'submit', text: L.insert, primary: true },
|
|
],
|
|
onSubmit: async (api) => {
|
|
const data = api.getData() as { wtype: WowheadType; wid: string; wtext: string }
|
|
const id = String(data.wid).trim().replace(/\D/g, '')
|
|
if (!id) {
|
|
api.close()
|
|
return
|
|
}
|
|
const type = (data.wtype || 'item') as WowheadType
|
|
const href = wowheadUrl(type, id, locale)
|
|
const wh = wowheadData(type, id, locale)
|
|
const custom = data.wtext.trim()
|
|
api.close()
|
|
|
|
// Resolver nombre + calidad en el servidor para insertar el enlace
|
|
// YA con su nombre real y su color (clase qN), sin el parpadeo de
|
|
// «item=xxx». Si falla, se cae al modo data-wh-rename-link.
|
|
let name = custom
|
|
let qClass = ''
|
|
try {
|
|
const res = await fetch(`/api/forum/wowhead?type=${type}&id=${id}&locale=${locale}`)
|
|
const info = (await res.json()) as { name: string | null; quality: number | null }
|
|
if (!name && info.name) name = info.name
|
|
if (typeof info.quality === 'number' && info.quality >= 0) qClass = ` q${info.quality}`
|
|
} catch {
|
|
/* wowhead no disponible: se usa el fallback */
|
|
}
|
|
const rename = name ? '' : ' data-wh-rename-link="true"'
|
|
const label = esc(name || `${type}=${id}`)
|
|
editor.insertContent(
|
|
`<a href="${href}" data-wowhead="${esc(wh)}" class="wowhead-link${qClass}"${rename} target="_blank" rel="noopener noreferrer">${label}</a> `,
|
|
)
|
|
// icono en el editor (el nombre y el color ya van puestos)
|
|
setTimeout(refreshEditorWowhead, 60)
|
|
},
|
|
})
|
|
},
|
|
})
|
|
},
|
|
}}
|
|
/>
|
|
)
|
|
}
|