Multi-Column Layout for printing (Print Only)

Convert single column layout to multi-column layout only when printing. Press Ctrl+S to open settings.

当前为 2024-11-06 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Multi-Column Layout for printing (Print Only)
  3. // @namespace http://tampermonkey.net/
  4. // @license MIT
  5. // @version 2.5
  6. // @description Convert single column layout to multi-column layout only when printing. Press Ctrl+S to open settings.
  7. // @author KQ yang
  8. // @match *://*/*
  9. // @match file:///*
  10. // @match http://127.0.0.1:*/*
  11. // @match http://localhost:*/*
  12. // @grant GM_addStyle
  13. // @run-at document-start
  14. // @noframes
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. // Add colorful title and usage instructions right after initialization
  21. console.log('%c Multi-Column Layout for Printing v2.3 ',
  22. 'background: #4A90E2; color: white; font-size: 14px; font-weight: bold; padding: 8px; border-radius: 4px; text-shadow: 1px 1px 1px rgba(0,0,0,0.2);');
  23. console.log('%c 📖 Usage: Press Ctrl+S to open settings, customize your layout, then Ctrl+P to print! ',
  24. 'background: #2ECC71; color: white; font-size: 12px; padding: 6px; border-radius: 4px;');
  25.  
  26.  
  27. // Default configuration
  28. const DEFAULT_CONFIG = {
  29. columns: 1,
  30. columnGap: '30px',
  31. fontSize: '16px',
  32. paragraphSpacing: '1em',
  33. enablePageBreak: true,
  34. lineHeight: '1.5',
  35. };
  36.  
  37. // Load config from localStorage or use defaults
  38. let CONFIG = loadConfig();
  39.  
  40. function loadConfig() {
  41. const savedConfig = localStorage.getItem('printLayoutConfig');
  42. return savedConfig ? {...DEFAULT_CONFIG, ...JSON.parse(savedConfig)} : DEFAULT_CONFIG;
  43. }
  44.  
  45. function saveConfig(config) {
  46. localStorage.setItem('printLayoutConfig', JSON.stringify(config));
  47. CONFIG = config;
  48. updateStyles();
  49. }
  50.  
  51. let configModal = null; // 将configModal提升为全局变量
  52.  
  53. // Create and inject the configuration UI
  54. function createConfigUI() {
  55.  
  56. // 如果已经存在modal,先移除
  57. if (configModal) {
  58. configModal.remove();
  59. }
  60.  
  61. configModal = document.createElement('div');
  62. configModal.id = 'print-layout-config-modal';
  63. configModal.setAttribute('style', `
  64. all: initial;
  65. position: fixed !important;
  66. top: 50% !important;
  67. left: 50% !important;
  68. transform: translate(-50%, -50%) !important;
  69. background: white !important;
  70. padding: 30px !important;
  71. border-radius: 12px !important;
  72. box-shadow: 0 8px 24px rgba(0,0,0,0.15) !important;
  73. z-index: 2147483647 !important;
  74. display: none !important;
  75. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif !important;
  76. min-width: 320px !important;
  77. max-width: 90vw !important;
  78. animation: modalFadeIn 0.3s ease-out !important;
  79. color: black !important;
  80. `);
  81.  
  82. const styleElement = document.createElement('style');
  83. styleElement.textContent = `
  84. @keyframes modalFadeIn {
  85. from {
  86. opacity: 0;
  87. transform: translate(-50%, -48%);
  88. }
  89. to {
  90. opacity: 1;
  91. transform: translate(-50%, -50%);
  92. }
  93. }
  94. .settings-row {
  95. margin-bottom: 20px;
  96. display: flex;
  97. align-items: center;
  98. justify-content: space-between;
  99. }
  100. .settings-row label {
  101. color: #333;
  102. font-size: 14px;
  103. margin-right: 15px;
  104. }
  105. .settings-row input[type="number"],
  106. .settings-row input[type="text"] {
  107. width: 100px;
  108. padding: 8px;
  109. border: 1px solid #ddd;
  110. border-radius: 6px;
  111. font-size: 14px;
  112. transition: border-color 0.2s;
  113. }
  114. .settings-row input:focus {
  115. outline: none;
  116. border-color: #4A90E2;
  117. box-shadow: 0 0 0 2px rgba(74,144,226,0.2);
  118. }
  119. .settings-row input[type="checkbox"] {
  120. width: 18px;
  121. height: 18px;
  122. cursor: pointer;
  123. }
  124. .modal-title {
  125. color: #333;
  126. margin: 0 0 25px 0;
  127. font-size: 18px;
  128. font-weight: 600;
  129. border-bottom: 2px solid #eee;
  130. padding-bottom: 15px;
  131. }
  132. .modal-footer {
  133. margin-top: 25px;
  134. padding-top: 20px;
  135. border-top: 2px solid #eee;
  136. text-align: right;
  137. }
  138. .save-button {
  139. background: #4A90E2;
  140. color: white;
  141. border: none;
  142. padding: 10px 20px;
  143. border-radius: 6px;
  144. font-size: 14px;
  145. cursor: pointer;
  146. transition: background-color 0.2s;
  147. }
  148. .save-button:hover {
  149. background: #357ABD;
  150. }
  151. .close-button {
  152. position: absolute;
  153. top: 15px;
  154. right: 15px;
  155. background: none;
  156. border: none;
  157. font-size: 20px;
  158. cursor: pointer;
  159. color: #666;
  160. padding: 5px;
  161. line-height: 1;
  162. }
  163. .close-button:hover {
  164. color: #333;
  165. }
  166. #print-layout-config-modal {
  167. visibility: visible !important;
  168. display: block !important;
  169. }
  170. `;
  171. document.head.appendChild(styleElement);
  172.  
  173. configModal.innerHTML = ''; // 清空现有内容
  174. const modalContent = document.createElement('div');
  175. modalContent.style.cssText = `
  176. all: initial;
  177. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
  178. color: black !important;
  179. `;
  180. modalContent.innerHTML = `
  181. <h3 class="modal-title">Print Layout Settings</h3>
  182. <button class="close-button" title="Close">×</button>
  183. <div class="settings-row">
  184. <label>Columns (1-4):</label>
  185. <input type="number" id="columns" min="1" max="4" value="${CONFIG.columns}">
  186. </div>
  187. <div class="settings-row">
  188. <label>Column Gap:</label>
  189. <input type="text" id="columnGap" value="${CONFIG.columnGap}">
  190. </div>
  191. <div class="settings-row">
  192. <label>Font Size:</label>
  193. <input type="text" id="fontSize" value="${CONFIG.fontSize}">
  194. </div>
  195. <div class="settings-row">
  196. <label>Paragraph Spacing:</label>
  197. <input type="text" id="paragraphSpacing" value="${CONFIG.paragraphSpacing}">
  198. </div>
  199. <div class="settings-row">
  200. <label>Line Height:</label>
  201. <input type="text" id="lineHeight" value="${CONFIG.lineHeight}">
  202. </div>
  203. <div class="settings-row">
  204. <label>Enable Page Break:</label>
  205. <input type="checkbox" id="enablePageBreak" ${CONFIG.enablePageBreak ? 'checked' : ''}>
  206. </div>
  207. <div class="modal-footer">
  208. <button class="save-button">Save Changes</button>
  209. </div>
  210. `;
  211.  
  212. configModal.appendChild(modalContent);
  213. document.documentElement.appendChild(configModal);
  214.  
  215. // Save button handler
  216. const saveButton = configModal.querySelector('.save-button');
  217. if (saveButton) {
  218. saveButton.addEventListener('click', () => {
  219. const newConfig = {
  220. columns: parseInt(configModal.querySelector('#columns').value, 10),
  221. columnGap: configModal.querySelector('#columnGap').value,
  222. fontSize: configModal.querySelector('#fontSize').value,
  223. paragraphSpacing: configModal.querySelector('#paragraphSpacing').value,
  224. lineHeight: configModal.querySelector('#lineHeight').value,
  225. enablePageBreak: configModal.querySelector('#enablePageBreak').checked
  226. };
  227. saveConfig(newConfig);
  228. configModal.style.setProperty('display', 'none', 'important');
  229. });
  230. }
  231.  
  232. // Close button handler
  233. const closeButton = configModal.querySelector('.close-button');
  234. if (closeButton) {
  235. closeButton.addEventListener('click', () => {
  236. configModal.style.setProperty('display', 'none', 'important');
  237. });
  238. }
  239.  
  240. // Click outside to close
  241. configModal.addEventListener('click', (e) => {
  242. if (e.target === configModal) {
  243. configModal.style.setProperty('display', 'none', 'important');
  244. }
  245. });
  246.  
  247. return configModal;
  248. }
  249.  
  250. function showConfigModal() {
  251. console.log('Showing config modal'); // 调试日志
  252. if (!configModal) {
  253. configModal = createConfigUI();
  254. }
  255. configModal.style.setProperty('display', 'block', 'important');
  256. configModal.style.setProperty('visibility', 'visible', 'important');
  257. }
  258.  
  259. function hideConfigModal() {
  260. if (configModal) {
  261. configModal.style.setProperty('display', 'none', 'important');
  262. }
  263. }
  264.  
  265. function toggleConfigModal() {
  266. if (!configModal || configModal.style.display === 'none') {
  267. showConfigModal();
  268. } else {
  269. hideConfigModal();
  270. }
  271. }
  272.  
  273. // Create and update styles based on current config
  274. function updateStyles() {
  275. const styleSheet = `
  276. @media print {
  277. html, body {
  278. margin: 0 !important;
  279. padding: 0 !important;
  280. min-height: 0 !important;
  281. height: auto !important;
  282. }
  283. .print-column-container {
  284. column-count: ${CONFIG.columns} !important;
  285. column-gap: ${CONFIG.columnGap} !important;
  286. column-rule: 1px solid #ddd !important;
  287. width: 100% !important;
  288. margin: 0 !important;
  289. padding: 0 !important;
  290. min-height: 0 !important;
  291. height: auto !important;
  292. overflow: visible !important;
  293. box-sizing: border-box !important;
  294. font-size: ${CONFIG.fontSize} !important;
  295. line-height: ${CONFIG.lineHeight} !important;
  296. ${CONFIG.enablePageBreak ? '' : 'page-break-inside: avoid !important;'}
  297. }
  298. .print-column-container > * {
  299. break-inside: avoid !important;
  300. margin-bottom: ${CONFIG.paragraphSpacing} !important;
  301. max-width: 100% !important;
  302. box-sizing: border-box !important;
  303. page-break-inside: avoid !important;
  304. }
  305. .print-column-container img {
  306. max-width: 100% !important;
  307. height: auto !important;
  308. page-break-inside: avoid !important;
  309. }
  310. }
  311. `;
  312.  
  313. const existingStyle = document.getElementById('print-layout-style');
  314. if (existingStyle) {
  315. existingStyle.remove();
  316. }
  317.  
  318. const style = document.createElement('style');
  319. style.id = 'print-layout-style';
  320. style.textContent = styleSheet;
  321. document.head.appendChild(style);
  322. }
  323.  
  324. // Apply columns to main content
  325. function applyPrintColumns() {
  326. const mainContent = document.querySelector('.target-content') || document.body;
  327. mainContent.classList.add('print-column-container');
  328.  
  329. const printStyle = document.createElement('style');
  330. printStyle.media = 'print';
  331. printStyle.textContent = `
  332. @page {
  333. margin: 1cm !important;
  334. padding: 0 !important;
  335. size: auto !important;
  336. }
  337. `;
  338. document.head.appendChild(printStyle);
  339. }
  340.  
  341. // Initialize modal globally
  342. configModal = createConfigUI();
  343.  
  344. // Handle Ctrl+S shortcut with improved event handling
  345. document.addEventListener('keydown', function(e) {
  346. if (e.ctrlKey && (e.key === 's' || e.key === 'S' || e.keyCode === 83)) {
  347. console.log('Ctrl+S detected'); // 调试日志
  348. e.stopPropagation();
  349. e.preventDefault();
  350. toggleConfigModal();
  351. return false;
  352. }
  353. }, true);
  354.  
  355. // Initial style application
  356. updateStyles();
  357.  
  358. // Handle DOMContentLoaded
  359. function onDOMContentLoaded() {
  360. applyPrintColumns();
  361. }
  362.  
  363. if (document.readyState === 'loading') {
  364. document.addEventListener('DOMContentLoaded', onDOMContentLoaded);
  365. } else {
  366. onDOMContentLoaded();
  367. }
  368.  
  369. // Add a global function for testing
  370. window.togglePrintLayoutConfig = toggleConfigModal;
  371.  
  372.  
  373. })();