nodebb-plugin-timestamp 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,5 +11,5 @@
11
11
  [[timestamp:dialog.description]]
12
12
  </div>
13
13
  </div>
14
- `,buttons:{cancel:{label:`[[timestamp:dialog.cancel]]`,className:`btn-secondary`},confirm:{label:`[[timestamp:dialog.confirm]]`,className:`btn-primary`,callback(){let input=document.getElementById(`plugin-ts-picker`);if(!input?.value)return;let ts=new Date(input.value).getTime();isNaN(ts)||insertAtCursor(textarea,selectionStart,selectionEnd,`[timestamp]${ts}[/timestamp]`)}}}})})}function localizeTimestamps(root=document){root.querySelectorAll(`time.plugin-timestamp`).forEach(el=>{let iso=el.getAttribute(`datetime`);if(!iso)return;let date=new Date(iso);isNaN(date.getTime())||(el.title=date.toLocaleString(void 0,{year:`numeric`,month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`,second:`2-digit`}))})}$(window).on(`action:app.load`,()=>{require([`composer/formatting`],formatting=>{formatting.addButtonDispatch(`timestamp`,function(textarea,selectionStart,selectionEnd){openTimePicker(textarea,selectionStart,selectionEnd)})})}),$(window).on(`action:app.load action:ajaxify.end`,()=>{localizeTimestamps()}),$(window).on(`action:posts.loaded`,()=>{localizeTimestamps()})});
14
+ `,buttons:{cancel:{label:`[[timestamp:dialog.cancel]]`,className:`btn-secondary`},confirm:{label:`[[timestamp:dialog.confirm]]`,className:`btn-primary`,callback(){let input=document.getElementById(`plugin-ts-picker`);if(!input?.value)return;let ts=new Date(input.value).getTime();isNaN(ts)||insertAtCursor(textarea,selectionStart,selectionEnd,`[timestamp]${ts}[/timestamp]`)}}}})})}function localizeTimestamps(root=document){root.querySelectorAll(`time.plugin-timestamp`).forEach(el=>{let iso=el.getAttribute(`datetime`);if(!iso)return;let date=new Date(iso);if(isNaN(date.getTime()))return;let localized=date.toLocaleString(void 0,{year:`numeric`,month:`2-digit`,day:`2-digit`,hour:`2-digit`,minute:`2-digit`,second:`2-digit`});el.title=localized,el.textContent?.trim()===iso&&(el.textContent=localized)})}$(window).on(`action:app.load`,()=>{require([`composer/formatting`],formatting=>{formatting.addButtonDispatch(`timestamp`,function(textarea,selectionStart,selectionEnd){openTimePicker(textarea,selectionStart,selectionEnd)})})}),$(window).on(`action:app.load action:ajaxify.end`,()=>{localizeTimestamps()}),$(window).on(`action:posts.loaded`,()=>{localizeTimestamps()})});
15
15
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":[],"sources":["../../public/client.ts"],"sourcesContent":["/// <reference types=\"jquery\"/>\n\n// bootbox 模块类型(通过 require([\"bootbox\"], ...) 按需加载)\ninterface Bootbox {\n dialog: (options: {\n title: string;\n message: string;\n buttons: Record<\n string,\n {\n label: string;\n className: string;\n callback?: () => void | boolean;\n }\n >;\n }) => void;\n}\n\n// NodeBB 的 AMD require(RequireJS 风格)\ndeclare function require(deps: string[], callback: (...modules: unknown[]) => void): void;\n\n// composer/formatting 模块只用到 addButtonDispatch\ninterface FormattingModule {\n // handler 由 dispatch table 调用,this 指向 postContainer\n addButtonDispatch: (\n name: string,\n onClick: (\n this: JQuery,\n textarea: HTMLTextAreaElement,\n selectionStart: number,\n selectionEnd: number,\n event: JQuery.Event\n ) => void\n ) => void;\n}\n\n/**\n * 获取 datetime-local input 所需的格式 \"YYYY-MM-DDTHH:MM\"\n */\nfunction toDatetimeLocalValue(date: Date): string {\n const pad = (n: number): string => String(n).padStart(2, \"0\");\n return (\n `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +\n `T${pad(date.getHours())}:${pad(date.getMinutes())}`\n );\n}\n\n/**\n * 在 textarea 光标位置插入文本\n */\nfunction insertAtCursor(\n textarea: HTMLTextAreaElement,\n start: number,\n end: number,\n text: string\n): void {\n const before = textarea.value.substring(0, start);\n const after = textarea.value.substring(end);\n textarea.value = before + text + after;\n textarea.selectionStart = textarea.selectionEnd = start + text.length;\n textarea.focus();\n // 触发 NodeBB 编辑器的响应式更新与预览刷新\n textarea.dispatchEvent(new Event(\"input\", { bubbles: true }));\n}\n\n/**\n * 打开时间选择弹窗,确认后把标记插入到 textarea 光标处\n */\nfunction openTimePicker(\n textarea: HTMLTextAreaElement,\n selectionStart: number,\n selectionEnd: number\n): void {\n const defaultValue = toDatetimeLocalValue(new Date());\n\n require([\"bootbox\"], (bootbox: unknown) => {\n (bootbox as Bootbox).dialog({\n title: \"[[timestamp:dialog.title]]\",\n message: `\n <div class=\"form-group\">\n <label for=\"plugin-ts-picker\" class=\"form-label\">[[timestamp:dialog.label]]</label>\n <input\n type=\"datetime-local\"\n id=\"plugin-ts-picker\"\n class=\"form-control\"\n value=\"${defaultValue}\"\n />\n <div class=\"form-text text-muted\" style=\"margin-top:6px\">\n [[timestamp:dialog.description]]\n </div>\n </div>\n `,\n buttons: {\n cancel: {\n label: \"[[timestamp:dialog.cancel]]\",\n className: \"btn-secondary\"\n },\n confirm: {\n label: \"[[timestamp:dialog.confirm]]\",\n className: \"btn-primary\",\n callback(): void {\n const input = document.getElementById(\"plugin-ts-picker\") as HTMLInputElement | null;\n if (!input?.value) return;\n\n const ts = new Date(input.value).getTime();\n if (isNaN(ts)) return;\n\n insertAtCursor(textarea, selectionStart, selectionEnd, `[timestamp]${ts}[/timestamp]`);\n }\n }\n }\n });\n });\n}\n\n/**\n * 将页面中所有 .plugin-timestamp 元素的 title(hover tooltip)\n * 替换为用户浏览器本地时区的格式化时间。\n * 不修改 textContent —— 让 NodeBB 内置的 timeago 继续渲染相对时间。\n */\nfunction localizeTimestamps(root: ParentNode = document): void {\n const elements = root.querySelectorAll<HTMLTimeElement>(\"time.plugin-timestamp\");\n elements.forEach((el) => {\n const iso = el.getAttribute(\"datetime\");\n if (!iso) return;\n\n const date = new Date(iso);\n if (isNaN(date.getTime())) return;\n\n el.title = date.toLocaleString(undefined, {\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\"\n });\n });\n}\n\n// NodeBB 页面加载完成后,向 composer 的 dispatch table 注册按钮点击处理\n$(window).on(\"action:app.load\", () => {\n require([\"composer/formatting\"], (formatting: unknown) => {\n (formatting as FormattingModule).addButtonDispatch(\n \"timestamp\",\n function (textarea, selectionStart, selectionEnd): void {\n openTimePicker(textarea, selectionStart, selectionEnd);\n }\n );\n });\n});\n\n// 初次加载 + ajaxify 切换页面时,本地化所有时间戳\n$(window).on(\"action:app.load action:ajaxify.end\", () => {\n localizeTimestamps();\n});\n\n// 帖子动态追加时(无限滚动等),也需要处理新出现的时间戳\n$(window).on(\"action:posts.loaded\", () => {\n localizeTimestamps();\n});\n"],"mappings":"gHAuCA,SAAS,qBAAqB,KAAoB,CAChD,IAAM,IAAO,GAAsB,OAAO,CAAC,CAAC,CAAC,SAAS,EAAG,GAAG,EAC5D,MACE,GAAG,KAAK,YAAY,EAAE,GAAG,IAAI,KAAK,SAAS,EAAI,CAAC,EAAE,GAAG,IAAI,KAAK,QAAQ,CAAC,EAAA,GACnE,IAAI,KAAK,SAAS,CAAC,EAAE,GAAG,IAAI,KAAK,WAAW,CAAC,GAErD,CAKA,SAAS,eACP,SACA,MACA,IACA,KACM,CACN,IAAM,OAAS,SAAS,MAAM,UAAU,EAAG,KAAK,EAC1C,MAAQ,SAAS,MAAM,UAAU,GAAG,EAC1C,SAAS,MAAQ,OAAS,KAAO,MACjC,SAAS,eAAiB,SAAS,aAAe,MAAQ,KAAK,OAC/D,SAAS,MAAM,EAEf,SAAS,cAAc,IAAI,MAAM,QAAS,CAAE,QAAS,EAAK,CAAC,CAAC,CAC9D,CAKA,SAAS,eACP,SACA,eACA,aACM,CACN,IAAM,aAAe,qBAAqB,IAAI,IAAM,EAEpD,QAAQ,CAAC,SAAS,EAAI,SAAqB,CACzC,QAAqB,OAAO,CAC1B,MAAO,6BACP,QAAS;;;;;;;qBAOM,aAAa;;;;;;QAO5B,QAAS,CACP,OAAQ,CACN,MAAO,8BACP,UAAW,eACb,EACA,QAAS,CACP,MAAO,+BACP,UAAW,cACX,UAAiB,CACf,IAAM,MAAQ,SAAS,eAAe,kBAAkB,EACxD,GAAI,CAAC,OAAO,MAAO,OAEnB,IAAM,GAAK,IAAI,KAAK,MAAM,KAAK,CAAC,CAAC,QAAQ,EACrC,MAAM,EAAE,GAEZ,eAAe,SAAU,eAAgB,aAAc,cAAc,GAAG,aAAa,CACvF,CACF,CACF,CACF,CAAC,CACH,CAAC,CACH,CAOA,SAAS,mBAAmB,KAAmB,SAAgB,CAE7D,KADsB,iBAAkC,uBACjD,CAAC,CAAC,QAAS,IAAO,CACvB,IAAM,IAAM,GAAG,aAAa,UAAU,EACtC,GAAI,CAAC,IAAK,OAEV,IAAM,KAAO,IAAI,KAAK,GAAG,EACrB,MAAM,KAAK,QAAQ,CAAC,IAExB,GAAG,MAAQ,KAAK,eAAe,IAAA,GAAW,CACxC,KAAM,UACN,MAAO,UACP,IAAK,UACL,KAAM,UACN,OAAQ,UACR,OAAQ,SACV,CAAC,EACH,CAAC,CACH,CAGA,EAAE,MAAM,CAAC,CAAC,GAAG,sBAAyB,CACpC,QAAQ,CAAC,qBAAqB,EAAI,YAAwB,CACxD,WAAiC,kBAC/B,YACA,SAAU,SAAU,eAAgB,aAAoB,CACtD,eAAe,SAAU,eAAgB,YAAY,CACvD,CACF,CACF,CAAC,CACH,CAAC,EAGD,EAAE,MAAM,CAAC,CAAC,GAAG,yCAA4C,CACvD,mBAAmB,CACrB,CAAC,EAGD,EAAE,MAAM,CAAC,CAAC,GAAG,0BAA6B,CACxC,mBAAmB,CACrB,CAAC"}
1
+ {"version":3,"file":"client.js","names":[],"sources":["../../public/client.ts"],"sourcesContent":["/// <reference types=\"jquery\"/>\n\n// bootbox 模块类型(通过 require([\"bootbox\"], ...) 按需加载)\ninterface Bootbox {\n dialog: (options: {\n title: string;\n message: string;\n buttons: Record<\n string,\n {\n label: string;\n className: string;\n callback?: () => void | boolean;\n }\n >;\n }) => void;\n}\n\n// NodeBB 的 AMD require(RequireJS 风格)\ndeclare function require(deps: string[], callback: (...modules: unknown[]) => void): void;\n\n// composer/formatting 模块只用到 addButtonDispatch\ninterface FormattingModule {\n // handler 由 dispatch table 调用,this 指向 postContainer\n addButtonDispatch: (\n name: string,\n onClick: (\n this: JQuery,\n textarea: HTMLTextAreaElement,\n selectionStart: number,\n selectionEnd: number,\n event: JQuery.Event\n ) => void\n ) => void;\n}\n\n/**\n * 获取 datetime-local input 所需的格式 \"YYYY-MM-DDTHH:MM\"\n */\nfunction toDatetimeLocalValue(date: Date): string {\n const pad = (n: number): string => String(n).padStart(2, \"0\");\n return (\n `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +\n `T${pad(date.getHours())}:${pad(date.getMinutes())}`\n );\n}\n\n/**\n * 在 textarea 光标位置插入文本\n */\nfunction insertAtCursor(\n textarea: HTMLTextAreaElement,\n start: number,\n end: number,\n text: string\n): void {\n const before = textarea.value.substring(0, start);\n const after = textarea.value.substring(end);\n textarea.value = before + text + after;\n textarea.selectionStart = textarea.selectionEnd = start + text.length;\n textarea.focus();\n // 触发 NodeBB 编辑器的响应式更新与预览刷新\n textarea.dispatchEvent(new Event(\"input\", { bubbles: true }));\n}\n\n/**\n * 打开时间选择弹窗,确认后把标记插入到 textarea 光标处\n */\nfunction openTimePicker(\n textarea: HTMLTextAreaElement,\n selectionStart: number,\n selectionEnd: number\n): void {\n const defaultValue = toDatetimeLocalValue(new Date());\n\n require([\"bootbox\"], (bootbox: unknown) => {\n (bootbox as Bootbox).dialog({\n title: \"[[timestamp:dialog.title]]\",\n message: `\n <div class=\"form-group\">\n <label for=\"plugin-ts-picker\" class=\"form-label\">[[timestamp:dialog.label]]</label>\n <input\n type=\"datetime-local\"\n id=\"plugin-ts-picker\"\n class=\"form-control\"\n value=\"${defaultValue}\"\n />\n <div class=\"form-text text-muted\" style=\"margin-top:6px\">\n [[timestamp:dialog.description]]\n </div>\n </div>\n `,\n buttons: {\n cancel: {\n label: \"[[timestamp:dialog.cancel]]\",\n className: \"btn-secondary\"\n },\n confirm: {\n label: \"[[timestamp:dialog.confirm]]\",\n className: \"btn-primary\",\n callback(): void {\n const input = document.getElementById(\"plugin-ts-picker\") as HTMLInputElement | null;\n if (!input?.value) return;\n\n const ts = new Date(input.value).getTime();\n if (isNaN(ts)) return;\n\n insertAtCursor(textarea, selectionStart, selectionEnd, `[timestamp]${ts}[/timestamp]`);\n }\n }\n }\n });\n });\n}\n\n/**\n * 将页面中所有 .plugin-timestamp 元素本地化为用户浏览器所在时区的时间。\n * - title(hover tooltip)始终设为本地时间。\n * - 文本内容在 timeago 接管时由 NodeBB 渲染相对时间;一旦超过 timeago 的\n * cutoff(约一个月),文本会停留在服务端的 ISO fallback,此时改为本地时间。\n */\nfunction localizeTimestamps(root: ParentNode = document): void {\n const elements = root.querySelectorAll<HTMLTimeElement>(\"time.plugin-timestamp\");\n elements.forEach((el) => {\n const iso = el.getAttribute(\"datetime\");\n if (!iso) return;\n\n const date = new Date(iso);\n if (isNaN(date.getTime())) return;\n\n const localized = date.toLocaleString(undefined, {\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\"\n });\n\n // hover tooltip 始终显示用户本地时间\n el.title = localized;\n\n // NodeBB 内置 timeago 只在 cutoff(默认约一个月)内把文本替换为相对时间\n // (如 \"in 3 days\");超过 cutoff 后 timeago 不再处理,元素文本会停留在\n // 服务端输出的 ISO fallback 上。这里在文本仍为 ISO(即尚未被 timeago 接管)\n // 时,把它替换为用户本地时间,使得超过一个月后展示的是本地时间而非 ISO。\n if (el.textContent?.trim() === iso) {\n el.textContent = localized;\n }\n });\n}\n\n// NodeBB 页面加载完成后,向 composer 的 dispatch table 注册按钮点击处理\n$(window).on(\"action:app.load\", () => {\n require([\"composer/formatting\"], (formatting: unknown) => {\n (formatting as FormattingModule).addButtonDispatch(\n \"timestamp\",\n function (textarea, selectionStart, selectionEnd): void {\n openTimePicker(textarea, selectionStart, selectionEnd);\n }\n );\n });\n});\n\n// 初次加载 + ajaxify 切换页面时,本地化所有时间戳\n$(window).on(\"action:app.load action:ajaxify.end\", () => {\n localizeTimestamps();\n});\n\n// 帖子动态追加时(无限滚动等),也需要处理新出现的时间戳\n$(window).on(\"action:posts.loaded\", () => {\n localizeTimestamps();\n});\n"],"mappings":"gHAuCA,SAAS,qBAAqB,KAAoB,CAChD,IAAM,IAAO,GAAsB,OAAO,CAAC,CAAC,CAAC,SAAS,EAAG,GAAG,EAC5D,MACE,GAAG,KAAK,YAAY,EAAE,GAAG,IAAI,KAAK,SAAS,EAAI,CAAC,EAAE,GAAG,IAAI,KAAK,QAAQ,CAAC,EAAA,GACnE,IAAI,KAAK,SAAS,CAAC,EAAE,GAAG,IAAI,KAAK,WAAW,CAAC,GAErD,CAKA,SAAS,eACP,SACA,MACA,IACA,KACM,CACN,IAAM,OAAS,SAAS,MAAM,UAAU,EAAG,KAAK,EAC1C,MAAQ,SAAS,MAAM,UAAU,GAAG,EAC1C,SAAS,MAAQ,OAAS,KAAO,MACjC,SAAS,eAAiB,SAAS,aAAe,MAAQ,KAAK,OAC/D,SAAS,MAAM,EAEf,SAAS,cAAc,IAAI,MAAM,QAAS,CAAE,QAAS,EAAK,CAAC,CAAC,CAC9D,CAKA,SAAS,eACP,SACA,eACA,aACM,CACN,IAAM,aAAe,qBAAqB,IAAI,IAAM,EAEpD,QAAQ,CAAC,SAAS,EAAI,SAAqB,CACzC,QAAqB,OAAO,CAC1B,MAAO,6BACP,QAAS;;;;;;;qBAOM,aAAa;;;;;;QAO5B,QAAS,CACP,OAAQ,CACN,MAAO,8BACP,UAAW,eACb,EACA,QAAS,CACP,MAAO,+BACP,UAAW,cACX,UAAiB,CACf,IAAM,MAAQ,SAAS,eAAe,kBAAkB,EACxD,GAAI,CAAC,OAAO,MAAO,OAEnB,IAAM,GAAK,IAAI,KAAK,MAAM,KAAK,CAAC,CAAC,QAAQ,EACrC,MAAM,EAAE,GAEZ,eAAe,SAAU,eAAgB,aAAc,cAAc,GAAG,aAAa,CACvF,CACF,CACF,CACF,CAAC,CACH,CAAC,CACH,CAQA,SAAS,mBAAmB,KAAmB,SAAgB,CAE7D,KADsB,iBAAkC,uBACjD,CAAC,CAAC,QAAS,IAAO,CACvB,IAAM,IAAM,GAAG,aAAa,UAAU,EACtC,GAAI,CAAC,IAAK,OAEV,IAAM,KAAO,IAAI,KAAK,GAAG,EACzB,GAAI,MAAM,KAAK,QAAQ,CAAC,EAAG,OAE3B,IAAM,UAAY,KAAK,eAAe,IAAA,GAAW,CAC/C,KAAM,UACN,MAAO,UACP,IAAK,UACL,KAAM,UACN,OAAQ,UACR,OAAQ,SACV,CAAC,EAGD,GAAG,MAAQ,UAMP,GAAG,aAAa,KAAK,IAAM,MAC7B,GAAG,YAAc,UAErB,CAAC,CACH,CAGA,EAAE,MAAM,CAAC,CAAC,GAAG,sBAAyB,CACpC,QAAQ,CAAC,qBAAqB,EAAI,YAAwB,CACxD,WAAiC,kBAC/B,YACA,SAAU,SAAU,eAAgB,aAAoB,CACtD,eAAe,SAAU,eAAgB,YAAY,CACvD,CACF,CACF,CAAC,CACH,CAAC,EAGD,EAAE,MAAM,CAAC,CAAC,GAAG,yCAA4C,CACvD,mBAAmB,CACrB,CAAC,EAGD,EAAE,MAAM,CAAC,CAAC,GAAG,0BAA6B,CACxC,mBAAmB,CACrB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-timestamp",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Insert a relative time component into the post | 在帖子中插入相对时间组件",
5
5
  "homepage": "https://github.com/TASA-Ed/nodebb-plugin-timestamp",
6
6
  "bugs": {
package/public/client.ts CHANGED
@@ -114,9 +114,10 @@ function openTimePicker(
114
114
  }
115
115
 
116
116
  /**
117
- * 将页面中所有 .plugin-timestamp 元素的 title(hover tooltip)
118
- * 替换为用户浏览器本地时区的格式化时间。
119
- * 不修改 textContent —— NodeBB 内置的 timeago 继续渲染相对时间。
117
+ * 将页面中所有 .plugin-timestamp 元素本地化为用户浏览器所在时区的时间。
118
+ * - title(hover tooltip)始终设为本地时间。
119
+ * - 文本内容在 timeago 接管时由 NodeBB 渲染相对时间;一旦超过 timeago
120
+ * cutoff(约一个月),文本会停留在服务端的 ISO fallback,此时改为本地时间。
120
121
  */
121
122
  function localizeTimestamps(root: ParentNode = document): void {
122
123
  const elements = root.querySelectorAll<HTMLTimeElement>("time.plugin-timestamp");
@@ -127,7 +128,7 @@ function localizeTimestamps(root: ParentNode = document): void {
127
128
  const date = new Date(iso);
128
129
  if (isNaN(date.getTime())) return;
129
130
 
130
- el.title = date.toLocaleString(undefined, {
131
+ const localized = date.toLocaleString(undefined, {
131
132
  year: "numeric",
132
133
  month: "2-digit",
133
134
  day: "2-digit",
@@ -135,6 +136,17 @@ function localizeTimestamps(root: ParentNode = document): void {
135
136
  minute: "2-digit",
136
137
  second: "2-digit"
137
138
  });
139
+
140
+ // hover tooltip 始终显示用户本地时间
141
+ el.title = localized;
142
+
143
+ // NodeBB 内置 timeago 只在 cutoff(默认约一个月)内把文本替换为相对时间
144
+ // (如 "in 3 days");超过 cutoff 后 timeago 不再处理,元素文本会停留在
145
+ // 服务端输出的 ISO fallback 上。这里在文本仍为 ISO(即尚未被 timeago 接管)
146
+ // 时,把它替换为用户本地时间,使得超过一个月后展示的是本地时间而非 ISO。
147
+ if (el.textContent?.trim() === iso) {
148
+ el.textContent = localized;
149
+ }
138
150
  });
139
151
  }
140
152
 
@@ -48,7 +48,7 @@ export default defineConfig([
48
48
  },
49
49
  {
50
50
  input: ["public/client.ts"],
51
- platform: "node",
51
+ platform: "browser",
52
52
  output: {
53
53
  dir: "build/public",
54
54
  format: "umd",