미디어위키:Gadget-newtoolbar.js: 두 판 사이의 차이

편집 요약 없음
태그: 수동 되돌리기 되돌려진 기여
편집 요약 없음
태그: 수동 되돌리기
570번째 줄: 570번째 줄:
     });
     });
}
}
// ==UserScript==
// @name        Fix mw-enhanced-rc left spaces
// @match        *://*/*
// @run-at      document-end
// ==/UserScript==
(function () {
  "use strict";
  // Remove excess leading spaces from the first non-empty text node
  function fixEnhancedRcElement(el) {
    if (!el || !el.classList || !el.classList.contains("mw-enhanced-rc")) return;
    const walker = document.createTreeWalker(
      el,
      NodeFilter.SHOW_TEXT,
      {
        acceptNode(node) {
          // Skip purely whitespace text nodes
          return node.nodeValue.trim().length === 0
            ? NodeFilter.FILTER_SKIP
            : NodeFilter.FILTER_ACCEPT;
        },
      }
    );
    const firstText = walker.nextNode();
    if (!firstText) return;
    // Collapse leading whitespace to a single space
    firstText.nodeValue = firstText.nodeValue.replace(/^\s+/, " ");
  }
  // Scan a root node for any .mw-enhanced-rc
  function scanForEnhancedRc(root) {
    if (!root || root.nodeType !== 1) return;
    if (root.classList && root.classList.contains("mw-enhanced-rc")) {
      fixEnhancedRcElement(root);
    }
    const list = root.querySelectorAll(".mw-enhanced-rc");
    list.forEach(fixEnhancedRcElement);
  }
  // Initial pass (if DOM is already loaded)
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", () => scanForEnhancedRc(document));
  } else {
    scanForEnhancedRc(document);
  }
  // Watch for dynamically added nodes
  const observer = new MutationObserver((mutations) => {
    for (const m of mutations) {
      for (const node of m.addedNodes) {
        if (node.nodeType === 1) {
          scanForEnhancedRc(node);
        }
      }
    }
  });
  observer.observe(document.documentElement || document.body, {
    childList: true,
    subtree: true,
  });
})();

2025년 11월 14일 (금) 03:00 판

/* MediaWiki:Gadget-NewToolbar.js */
( function ( mw, $ ) {
	'use strict';

	/**
	 * 선택 영역을 앞뒤 텍스트로 감싸기 (간단 버전)
	 */
	function surroundSelection( textarea, before, after ) {
		var $ta = $( textarea );
		var start = textarea.selectionStart;
		var end = textarea.selectionEnd;

		if ( start == null || end == null ) {
			// 구형 브라우저 fallback (거의 안 씀)
			$ta.text( $ta.text() + before + after );
			return;
		}

		var text = $ta.val();
		var selected = text.slice( start, end );
		var newText = text.slice( 0, start ) + before + selected + after + text.slice( end );

		$ta.val( newText );

		// 커서/선택 다시 잡기
		var newStart = start + before.length;
		var newEnd = newStart + selected.length;
		textarea.selectionStart = newStart;
		textarea.selectionEnd = newEnd;
		$ta.trigger( 'change' );
	}

	/**
	 * 현재 커서 위치에 텍스트 삽입
	 */
	function insertAtCursor( textarea, snippet ) {
		var $ta = $( textarea );
		var start = textarea.selectionStart;
		var end = textarea.selectionEnd;
		var text = $ta.val();

		if ( start == null || end == null ) {
			$ta.val( text + snippet );
			return;
		}

		var newText = text.slice( 0, start ) + snippet + text.slice( end );
		$ta.val( newText );
		var pos = start + snippet.length;
		textarea.selectionStart = textarea.selectionEnd = pos;
		$ta.trigger( 'change' );
	}
	
		/**
	 * 간단 알림 (mw.notify 있으면 그걸 쓰고, 없으면 alert)
	 */
	function notify( msg ) {
		if ( mw && mw.notify ) {
			mw.notify( msg );
		} else {
			alert( msg );
		}
	}

	/**
	 * 찾기/바꾸기 상태
	 */
	var findState = {
		open: false
	};

	/**
	 * 찾기/바꾸기 패널 만들기 (없으면 생성, 있으면 그대로 반환)
	 */
	function createFindPanel( textarea ) {
		var $panel = $( '#ntb-find-panel' );
		if ( $panel.length ) {
			return $panel;
		}

		$panel = $( '<div>' )
			.attr( 'id', 'ntb-find-panel' )
			.addClass( 'ntb-find-panel' );

		var $rowFind = $( '<div>' ).addClass( 'ntb-find-row' );
		$rowFind.append(
			$( '<label>' ).text( '찾기' ),
			$( '<input>' ).attr( { type: 'text', id: 'ntb-find-text' } )
		);

		var $rowReplace = $( '<div>' ).addClass( 'ntb-find-row' );
		$rowReplace.append(
			$( '<label>' ).text( '바꾸기' ),
			$( '<input>' ).attr( { type: 'text', id: 'ntb-replace-text' } )
		);

		var $btns = $( '<div>' ).addClass( 'ntb-find-buttons' );
		var $btnNext = $( '<button>' ).attr( 'type', 'button' ).text( '다음 찾기' );
		var $btnReplace = $( '<button>' ).attr( 'type', 'button' ).text( '바꾸기' );
		var $btnReplaceAll = $( '<button>' ).attr( 'type', 'button' ).text( '모두 바꾸기' );
		var $btnClose = $( '<button>' ).attr( 'type', 'button' ).text( '닫기' );

		$btns.append( $btnNext, $btnReplace, $btnReplaceAll, $btnClose );
		$panel.append( $rowFind, $rowReplace, $btns );

		// 버튼 동작 정의
		$btnNext.on( 'click', function () {
			findNext( textarea );
		} );

		$btnReplace.on( 'click', function () {
			replaceOne( textarea );
		} );

		$btnReplaceAll.on( 'click', function () {
			replaceAll( textarea );
		} );

		$btnClose.on( 'click', function () {
			closeFindPanel();
		} );

		return $panel;
	}

	/**
	 * 찾기/바꾸기 패널 열기/닫기 토글
	 */
	function toggleFindPanel( textarea ) {
		var $panel = createFindPanel( textarea );
		if ( findState.open ) {
			closeFindPanel();
		} else {
			openFindPanel( $panel, textarea );
		}
	}

	function openFindPanel( $panel, textarea ) {
		// 툴바 바로 아래에 붙이기
		var $toolbar = $( '#ntb-toolbar' );
		if ( !$panel.parent().length ) {
			$toolbar.after( $panel );
		}
		$panel.show();
		findState.open = true;
		findState.term = '';
		findState.lastIndex = 0;

		// 현재 선택 텍스트가 있으면 "찾기" 칸에 채우기
		var sel = textarea.value.slice( textarea.selectionStart, textarea.selectionEnd );
		if ( sel ) {
			$( '#ntb-find-text' ).val( sel );
		}
		$( '#ntb-find-text' ).focus().select();
	}

	function closeFindPanel() {
		var $panel = $( '#ntb-find-panel' );
		$panel.hide();
		findState.open = false;
	}

	/**
	 * 다음 찾기
	 */
	function findNext( textarea ) {
		var $findInput = $( '#ntb-find-text' );
		if ( !$findInput.length ) {
			return;
		}
		var term = $findInput.val();
		if ( !term ) {
			notify( '찾을 문자열을 입력하세요.' );
			return;
		}

		var text = textarea.value;
		var startFrom;

		// 검색어가 바뀌면 처음부터
		if ( findState.term !== term ) {
			findState.term = term;
			findState.lastIndex = textarea.selectionEnd || 0;
		}

		startFrom = textarea.selectionEnd || 0;

		var idx = text.indexOf( term, startFrom );
		if ( idx === -1 && startFrom !== 0 ) {
			// 못 찾으면 처음부터 다시
			idx = text.indexOf( term, 0 );
		}

		if ( idx === -1 ) {
			notify( '더 이상 찾을 문자열이 없습니다.' );
			return;
		}

		textarea.selectionStart = idx;
		textarea.selectionEnd = idx + term.length;
		textarea.focus();
		findState.lastIndex = idx + term.length;
	}

	/**
	 * 현재 선택된 항목 한 번만 바꾸기
	 */
	function replaceOne( textarea ) {
		var $findInput = $( '#ntb-find-text' );
		var $replaceInput = $( '#ntb-replace-text' );
		if ( !$findInput.length || !$replaceInput.length ) {
			return;
		}

		var term = $findInput.val();
		var replacement = $replaceInput.val();
		if ( !term ) {
			notify( '찾을 문자열을 입력하세요.' );
			return;
		}

		var start = textarea.selectionStart;
		var end = textarea.selectionEnd;
		var text = textarea.value;
		var selected = text.slice( start, end );

		if ( selected && selected === term ) {
			// 현재 선택 영역이 검색어와 일치하면 교체
			var newText = text.slice( 0, start ) + replacement + text.slice( end );
			textarea.value = newText;
			var newEnd = start + replacement.length;
			textarea.selectionStart = start;
			textarea.selectionEnd = newEnd;
			textarea.focus();
			findState.lastIndex = newEnd;
		} else {
			// 선택이 없거나 일치하지 않으면 "다음 찾기" 먼저
			findNext( textarea );
		}
	}

	/**
	 * 전체 텍스트 모두 바꾸기
	 */
	function replaceAll( textarea ) {
		var $findInput = $( '#ntb-find-text' );
		var $replaceInput = $( '#ntb-replace-text' );
		if ( !$findInput.length || !$replaceInput.length ) {
			return;
		}

		var term = $findInput.val();
		var replacement = $replaceInput.val();
		if ( !term ) {
			notify( '찾을 문자열을 입력하세요.' );
			return;
		}

		if ( term === replacement ) {
			notify( '같은 문자열로 바꾸려고 합니다.' );
			return;
		}

		var text = textarea.value;
		if ( text.indexOf( term ) === -1 ) {
			notify( '문서 안에 해당 문자열이 없습니다.' );
			return;
		}

		// 간단하게 split/join 사용
		var count = ( text.match( new RegExp( term.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' ), 'g' ) ) || [] ).length;
		textarea.value = text.split( term ).join( replacement );
		textarea.selectionStart = textarea.selectionEnd = 0;
		textarea.focus();
		notify( count + '개를 모두 바꿨습니다.' );
	}

	/**
	 * 버튼 생성 헬퍼
	 */
	function createButton( opts ) {
		var $btn = $( '<button>' )
			.attr( 'type', 'button' )
			.addClass( 'ntb-button' )
			.append(
				$( '<span>' )
					.addClass( 'ntb-icon ' + ( opts.icon || '' ) )
			)
			.append(
				$( '<span>' )
					.addClass( 'ntb-label' )
					.text( opts.label )
			)
			.on( 'click', function () {
				var textarea = document.getElementById( 'wpTextbox1' );
				if ( textarea && typeof opts.onClick === 'function' ) {
					opts.onClick( textarea );
					textarea.focus();
				}
			} );

		if ( opts.title ) {
			$btn.attr( 'title', opts.title );
		}

		return $btn;
	}

	/**
	 * 새 툴바 초기화
	 */
	function initNewToolbar( $textarea ) {
		var $oldToolbar = $( '#wikiEditor-ui-toolbar' );

		// wikiEditor 없는 페이지 or 이미 생성된 경우
		if ( !$oldToolbar.length || $( '#ntb-toolbar' ).length ) {
			return;
		}

		var $toolbar = $( '<div>' )
			.attr( 'id', 'ntb-toolbar' )
			.addClass( 'ntb-toolbar' );

		// --- 그룹 1: 텍스트 스타일 ---
		var $groupText = $( '<div>' )
			.addClass( 'ntb-group ntb-group-text' )
			.append(
				$( '<span>' ).addClass( 'ntb-group-label' ).text( '텍스트' )
			);

		$groupText.append(
			createButton( {
				icon: 'ntb-icon-bold',
				label: '굵게',
				title: "'''굵게'''",
				onClick: function ( ta ) {
					surroundSelection( ta, "'''", "'''" );
				}
			} ),
			createButton( {
				icon: 'ntb-icon-italic',
				label: '기울임',
				title: "''기울임''",
				onClick: function ( ta ) {
					surroundSelection( ta, "''", "''" );
				}
			} )
		);

		// --- 그룹 2: 문단/목록 ---
		var $groupBlock = $( '<div>' )
			.addClass( 'ntb-group ntb-group-block' )
			.append(
				$( '<span>' ).addClass( 'ntb-group-label' ).text( '문단' )
			);

		$groupBlock.append(
			createButton( {
				icon: 'ntb-icon-heading',
				label: '제목',
				title: '== 문단 제목 ==',
				onClick: function ( ta ) {
					insertAtCursor( ta, '\n== 새 문단 제목 ==\n' );
				}
			} ),
			createButton( {
				icon: 'ntb-icon-ul',
				label: '글머리 목록',
				title: '* 글머리',
				onClick: function ( ta ) {
					insertAtCursor( ta, '\n* 항목\n' );
				}
			} ),
			createButton( {
				icon: 'ntb-icon-ol',
				label: '번호 목록',
				title: '# 번호 항목',
				onClick: function ( ta ) {
					insertAtCursor( ta, '\n# 항목\n' );
				}
			} )
		);

		// --- 그룹 3: 삽입 ---
		var $groupInsert = $( '<div>' )
			.addClass( 'ntb-group ntb-group-insert' )
			.append(
				$( '<span>' ).addClass( 'ntb-group-label' ).text( '삽입' )
			);

		$groupInsert.append(
			createButton( {
				icon: 'ntb-icon-link',
				label: '링크',
				title: '[[문서 링크]] 또는 [https://example.com 외부 링크]',
				onClick: function ( ta ) {
					insertAtCursor( ta, '[[링크 대상|표시 텍스트]]' );
				}
			} ),
			createButton( {
				icon: 'ntb-icon-file',
				label: '파일',
				title: '[[파일:Example.png|thumb|캡션]]',
				onClick: function ( ta ) {
					insertAtCursor( ta, '[[파일:Example.png|thumb|캡션]]' );
				}
			} ),
			createButton( {
				icon: 'ntb-icon-ref',
				label: '각주',
				title: '<ref>내용</ref>',
				onClick: function ( ta ) {
					surroundSelection( ta, '<ref>', '</ref>' );
				}
			} )
		);

		// --- 그룹 4: 기타 도구 ---
		var $groupTools = $( '<div>' )
			.addClass( 'ntb-group ntb-group-tools' )
			.append(
				$( '<span>' ).addClass( 'ntb-group-label' ).text( '도구' )
			);

		$groupTools.append(
			createButton( {
				icon: 'ntb-icon-search',
				label: '찾기/바꾸기',
				title: '문서 안에서 텍스트 찾기/바꾸기',
				onClick: function ( ta ) {
					toggleFindPanel( ta );
				}
			} )
		 );
		 $groupTools.append(
    createButton({
        icon: 'ntb-icon-preview',
        label: '미리보기',
        title: '미리 보기 패널 열기/닫기',
        onClick: function (ta) {
            togglePreviewPanel(ta);
        }
    })
);


		$toolbar
			.append( $groupText )
			.append( $groupBlock )
			.append( $groupInsert )
			.append( $groupTools );

		// 기존 툴바 숨기고, 새 툴바 삽입
		$oldToolbar.hide().before( $toolbar );
	}

	// wikiEditor 준비되면 호출
	mw.hook( 'wikiEditor.toolbarReady' ).add( initNewToolbar );

}( mediaWiki, jQuery) );
var previewState = {
	open: false,
};

/**
 * 에디터 & 미리보기 레이아웃 준비
 * - #wpTextbox1를 .ntb-edit-area로 감싸고
 * - 가운데에 splitter, 오른쪽에 preview panel을 넣는다.
 */
function ensureEditLayout(textarea) {
	var $ta = $(textarea);
	var $ed = $ta.closest('.wikiEditor-ui-text');
    var $wrapper = $ta.parent();

	// preview panel 생성
	var $panel = createPreviewPanel();

	// wrapper 안에 순서대로: textarea | splitter | preview
	if ($panel.parent()[0] !== $wrapper[0]) {
		$wrapper.append($panel);
	}

	// 초기 flex 비율 설정 (에디터 50%, 미리보기 50%)
	$ta.hide();
	$panel.css('width', '100%');
    $panel.css('height', '100%');
}


/**
 * 미리보기 패널 상태
 */
var previewState = {
    open: false
};
/**
 * 미리보기 패널 상태
 */
var previewState = {
    open: false
};

/**
 * 미리보기 패널 생성
 */
function createPreviewPanel() {
    var $panel = $('#ntb-preview-panel');
    if ($panel.length) return $panel;

    $panel = $('<div>')
        .attr('id', 'ntb-preview-panel')
        .addClass('ntb-preview-panel')
        .html(`
            <div class="ntb-preview-title">미리 보기</div>
            <div id="ntb-preview-content">내용 없음</div>
        `);

    return $panel;
}

/**
 * 미리보기 켜기/끄기
 */
function togglePreviewPanel(textarea) {
	var $ta = $(textarea);
	ensureEditLayout(textarea);

	var $panel = $('#ntb-preview-panel');

	if (previewState.open) {
		$('body').removeClass('ntb-preview-open');
		$panel.hide();
		// 에디터를 다시 100%로 돌리고 싶다면:
        $ta.show();
		$ta.css('width', '100%');
		previewState.open = false;
		// 입력 이벤트 unbind
		$ta.off('input.preview');
		return;
	}

	$('body').addClass('ntb-preview-open');
	$panel.show();
	previewState.open = true;

	updatePreview(textarea);

	$ta.off('input.preview').on('input.preview', function () {
		updatePreview(textarea);
	});
}

/**
 * API를 이용해 wikitext → HTML 렌더링
 */
function updatePreview(textarea) {
    var text = textarea.value;

    $('#ntb-preview-content').text('렌더링 중...');

    new mw.Api().post({
        action: 'parse',
        format: 'json',
        contentmodel: 'wikitext',
        text: text
    }).done(function (data) {
        $('#ntb-preview-content').html(data.parse.text['*']);
    }).fail(function () {
        $('#ntb-preview-content').text('렌더링 실패');
    });
}