설명 없음

index.ts 8.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. import { SyncEvent } from './../../event/Event';
  2. import * as Siema from 'siema';
  3. import { WhiteboardMode } from '../types';
  4. import { WhitePage } from '../WhitePage/index';
  5. import { EventHub } from '../../event/EventHub';
  6. import { uuid } from '../../utils/uuid';
  7. import { addClassName, createDivWithClassName } from '../../utils/dom';
  8. import './index.less';
  9. const LeftArrowIcon = require('../../assets/bx-left-arrow.svg');
  10. const RightArrowIcon = require('../../assets/bx-right-arrow.svg');
  11. const prefix = 'fcw-board';
  12. export class SerializableWhiteboard {
  13. id: string;
  14. sources: string[];
  15. pageIds: string[];
  16. visiblePageIndex: number;
  17. }
  18. export class Whiteboard {
  19. id: string = uuid();
  20. sources: string[] = [];
  21. /** 元素 */
  22. // 如果传入的是图片地址,则需要挂载到该 Target 元素下
  23. target: HTMLDivElement;
  24. imgsContainer: HTMLDivElement;
  25. pagesContainer: HTMLDivElement;
  26. /** UI Options */
  27. // 事件中心
  28. eventHub?: EventHub;
  29. // 编辑模式
  30. mode: WhiteboardMode = 'master';
  31. // 是否为全屏模式
  32. isFullscreen: boolean = false;
  33. /** 句柄 */
  34. pages: WhitePage[] = [];
  35. get activePage() {
  36. return this.pages[this.visiblePageIndex];
  37. }
  38. siema: any;
  39. /** State | 内部状态 */
  40. // 是否被初始化过,如果尚未被初始化,则等待来自于 Master 的同步消息
  41. isInitialized: boolean = false;
  42. isSyncing: boolean = false;
  43. visiblePageIndex: number = 0;
  44. emitInterval: any;
  45. constructor(
  46. target: HTMLDivElement,
  47. {
  48. sources,
  49. eventHub,
  50. mode,
  51. visiblePageIndex
  52. }: {
  53. sources?: string[];
  54. eventHub?: EventHub;
  55. mode?: WhiteboardMode;
  56. visiblePageIndex?: number;
  57. } = {}
  58. ) {
  59. if (target) {
  60. this.target = target;
  61. } else {
  62. this.target = document.createElement('div');
  63. document.body.appendChild(this.target);
  64. }
  65. if (!this.target.id) {
  66. this.target.id = this.id;
  67. }
  68. addClassName(this.target, prefix);
  69. if (sources) {
  70. this.sources = sources;
  71. }
  72. this.eventHub = eventHub;
  73. if (mode) {
  74. this.mode = mode;
  75. }
  76. // set inner state
  77. if (typeof visiblePageIndex !== 'undefined') {
  78. this.visiblePageIndex = visiblePageIndex;
  79. }
  80. this.init();
  81. }
  82. /** LifeCycle */
  83. public open() {
  84. // 依次渲染所有的页,隐藏非当前页之外的其他页
  85. this.pages.forEach((page, i) => {
  86. page.open();
  87. if (i !== this.visiblePageIndex) {
  88. page.hide();
  89. }
  90. });
  91. }
  92. /** 关闭当前的 Whiteboard */
  93. public close() {
  94. if (this.emitInterval) {
  95. clearInterval(this.emitInterval);
  96. }
  97. }
  98. /** 展示当前的 WhitePage */
  99. public show() {
  100. if (this.activePage) {
  101. this.activePage.show();
  102. }
  103. }
  104. public hide() {
  105. if (this.activePage) {
  106. this.activePage.hide();
  107. }
  108. }
  109. /** 获取当前快照 */
  110. public snap(): SerializableWhiteboard {
  111. return {
  112. id: this.id,
  113. sources: this.sources,
  114. pageIds: this.pages.map(page => page.id),
  115. visiblePageIndex: this.visiblePageIndex
  116. };
  117. }
  118. /** 初始化操作 */
  119. private init() {
  120. // 为 target 添加子 imgs 容器
  121. this.imgsContainer = createDivWithClassName(`${prefix}-imgs`, this.target);
  122. // 为 target 添加子 pages 容器
  123. this.pagesContainer = createDivWithClassName(`${prefix}-pages`, this.target);
  124. if (this.mode === 'master') {
  125. this.initMaster();
  126. this.emitSnapshot();
  127. }
  128. if (this.mode === 'mirror') {
  129. this.initMirror();
  130. }
  131. }
  132. /** 以主模式启动 */
  133. private initMaster() {
  134. // 初始化所有的 WhitePages
  135. this.sources.forEach(source => {
  136. const page = new WhitePage(
  137. { imgSrc: source },
  138. {
  139. mode: this.mode,
  140. eventHub: this.eventHub,
  141. parentContainer: this.pagesContainer
  142. }
  143. );
  144. // 这里隐藏 Dashboard 的图片源,Siema 切换的是占位图片
  145. page.container.style.visibility = 'hidden';
  146. this.pages.push(page);
  147. });
  148. this.initSiema();
  149. // 初始化控制节点
  150. const controller = createDivWithClassName(`${prefix}-controller`, this.target);
  151. const prevEle = createDivWithClassName(`${prefix}-flip-arrow`, controller);
  152. prevEle.innerHTML = LeftArrowIcon;
  153. const nextEle = createDivWithClassName(`${prefix}-flip-arrow`, controller);
  154. nextEle.innerHTML = RightArrowIcon;
  155. nextEle!.addEventListener('click', () => {
  156. const nextPageIndex =
  157. this.visiblePageIndex + 1 > this.pages.length - 1 ? 0 : this.visiblePageIndex + 1;
  158. this.onPageChange(nextPageIndex);
  159. });
  160. prevEle!.addEventListener('click', () => {
  161. const nextPageIndex =
  162. this.visiblePageIndex - 1 < 0 ? this.pages.length - 1 : this.visiblePageIndex - 1;
  163. this.onPageChange(nextPageIndex);
  164. });
  165. }
  166. /** 以镜像模式启动 */
  167. private initMirror() {
  168. if (!this.eventHub) {
  169. throw new Error('Invalid eventHub');
  170. }
  171. this.eventHub.on('sync', (ev: SyncEvent) => {
  172. if (ev.target !== 'whiteboard') {
  173. return;
  174. }
  175. if (ev.event === 'snap') {
  176. // 如果已经初始化完毕,则直接跳过
  177. if (this.isInitialized) {
  178. return;
  179. }
  180. this.onSnapshot(ev.data as SerializableWhiteboard);
  181. }
  182. if (ev.event === 'changeIndex' && ev.id === this.id) {
  183. if (this.isInitialized) {
  184. this.onPageChange(ev.data as number);
  185. }
  186. }
  187. });
  188. }
  189. /** 初始化 Siema */
  190. private initSiema() {
  191. // 初始化所有的占位图片,用于给 Siema 播放使用
  192. this.sources.forEach(source => {
  193. const imgEle = document.createElement('img');
  194. addClassName(imgEle, `${prefix}-img`);
  195. imgEle.src = source;
  196. imgEle.alt = 'Siema image';
  197. this.imgsContainer.appendChild(imgEle);
  198. });
  199. // 初始化 Siema,并且添加控制节点
  200. this.siema = new Siema({
  201. selector: this.imgsContainer,
  202. duration: 200,
  203. easing: 'ease-out',
  204. perPage: 1,
  205. startIndex: 0,
  206. draggable: false,
  207. multipleDrag: true,
  208. threshold: 20,
  209. loop: false,
  210. rtl: false
  211. });
  212. }
  213. /** 响应页面切换的事件 */
  214. private onPageChange(nextPageIndex: number) {
  215. this.siema.goTo(nextPageIndex);
  216. this.visiblePageIndex = nextPageIndex;
  217. // 将所有的 Page 隐藏
  218. this.pages.forEach((page, i) => {
  219. if (nextPageIndex === i) {
  220. page.show();
  221. } else {
  222. page.hide();
  223. }
  224. });
  225. if (this.mode === 'master' && this.eventHub) {
  226. this.eventHub.emit('sync', {
  227. event: 'changeIndex',
  228. id: this.id,
  229. target: 'whiteboard',
  230. data: nextPageIndex
  231. });
  232. }
  233. }
  234. private emitSnapshot() {
  235. const innerFunc = () => {
  236. if (this.eventHub) {
  237. this.eventHub.emit('sync', {
  238. event: 'snap',
  239. id: this.id,
  240. target: 'whiteboard',
  241. data: this.snap()
  242. });
  243. }
  244. };
  245. // 定期触发事件
  246. this.emitInterval = setInterval(() => {
  247. innerFunc();
  248. }, 5 * 1000);
  249. // 首次事件,延时 500ms 发出
  250. setTimeout(innerFunc, 500);
  251. }
  252. /** 响应获取到的快照事件 */
  253. private onSnapshot(snap: SerializableWhiteboard) {
  254. const { id, sources, pageIds, visiblePageIndex } = snap;
  255. if (!this.isInitialized && !this.isSyncing) {
  256. this.id = id;
  257. this.sources = sources;
  258. this.isSyncing = true;
  259. // 初始化所有的 WhitePages
  260. this.sources.forEach((source, i) => {
  261. const page = new WhitePage(
  262. { imgSrc: source },
  263. {
  264. mode: this.mode,
  265. eventHub: this.eventHub,
  266. parentContainer: this.pagesContainer
  267. }
  268. );
  269. page.id = pageIds[i];
  270. // 这里隐藏 Dashboard 的图片源,Siema 切换的是占位图片
  271. page.container.style.visibility = 'hidden';
  272. this.pages.push(page);
  273. page.open();
  274. });
  275. this.initSiema();
  276. }
  277. this.isInitialized = true;
  278. this.isSyncing = false;
  279. this.onPageChange(visiblePageIndex);
  280. }
  281. }