layout.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. /**
  2. * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. */
  7. import { ParamDefinition as PD } from '../mol-util/param-definition';
  8. import { PluginComponent } from './component';
  9. import { PluginContext } from './context';
  10. import { PluginCommands } from './command';
  11. export type PluginLayoutControlsDisplay = 'outside' | 'portrait' | 'landscape' | 'reactive'
  12. export const PluginLayoutStateParams = {
  13. isExpanded: PD.Boolean(false),
  14. showControls: PD.Boolean(true),
  15. controlsDisplay: PD.Value<PluginLayoutControlsDisplay>('outside', { isHidden: true })
  16. }
  17. export type PluginLayoutStateProps = PD.Values<typeof PluginLayoutStateParams>
  18. interface RootState {
  19. top: string | null,
  20. bottom: string | null,
  21. left: string | null,
  22. right: string | null,
  23. width: string | null,
  24. height: string | null,
  25. maxWidth: string | null,
  26. maxHeight: string | null,
  27. margin: string | null,
  28. marginLeft: string | null,
  29. marginRight: string | null,
  30. marginTop: string | null,
  31. marginBottom: string | null,
  32. scrollTop: number,
  33. scrollLeft: number,
  34. position: string | null,
  35. overflow: string | null,
  36. viewports: HTMLElement[],
  37. zIndex: string | null
  38. }
  39. export class PluginLayout extends PluginComponent<PluginLayoutStateProps> {
  40. readonly events = {
  41. updated: this.ev()
  42. }
  43. private updateProps(state: Partial<PluginLayoutStateProps>) {
  44. const prevExpanded = !!this.state.isExpanded;
  45. this.updateState(state);
  46. if (this.root && typeof state.isExpanded === 'boolean' && state.isExpanded !== prevExpanded) this.handleExpand();
  47. this.events.updated.next();
  48. }
  49. private root: HTMLElement | undefined;
  50. private rootState: RootState | undefined = void 0;
  51. private expandedViewport: HTMLMetaElement;
  52. setProps(props: Partial<PluginLayoutStateProps>) {
  53. this.updateState(props);
  54. }
  55. setRoot(root: HTMLElement) {
  56. this.root = root;
  57. if (this.state.isExpanded) this.handleExpand();
  58. }
  59. private getScrollElement() {
  60. if ((document as any).scrollingElement) return (document as any).scrollingElement;
  61. if (document.documentElement) return document.documentElement;
  62. return document.body;
  63. }
  64. private handleExpand() {
  65. try {
  66. const body = document.getElementsByTagName('body')[0];
  67. const head = document.getElementsByTagName('head')[0];
  68. if (!body || !head || !this.root) return;
  69. if (this.state.isExpanded) {
  70. const children = head.children;
  71. const viewports: HTMLElement[] = [];
  72. let hasExp = false;
  73. for (let i = 0; i < children.length; i++) {
  74. if (children[i] === this.expandedViewport) {
  75. hasExp = true;
  76. } else if (((children[i] as any).name || '').toLowerCase() === 'viewport') {
  77. viewports.push(children[i] as any);
  78. }
  79. }
  80. for (let v of viewports) {
  81. head.removeChild(v);
  82. }
  83. if (!hasExp) head.appendChild(this.expandedViewport);
  84. const s = body.style;
  85. const doc = this.getScrollElement();
  86. const scrollLeft = doc.scrollLeft;
  87. const scrollTop = doc.scrollTop;
  88. this.rootState = {
  89. top: s.top, bottom: s.bottom, right: s.right, left: s.left, scrollTop, scrollLeft, position: s.position, overflow: s.overflow, viewports, zIndex: this.root.style.zIndex,
  90. width: s.width, height: s.height,
  91. maxWidth: s.maxWidth, maxHeight: s.maxHeight,
  92. margin: s.margin, marginLeft: s.marginLeft, marginRight: s.marginRight, marginTop: s.marginTop, marginBottom: s.marginBottom
  93. };
  94. s.overflow = 'hidden';
  95. s.position = 'fixed';
  96. s.top = '0';
  97. s.bottom = '0';
  98. s.right = '0';
  99. s.left = '0';
  100. s.width = '100%';
  101. s.height = '100%';
  102. s.maxWidth = '100%';
  103. s.maxHeight = '100%';
  104. s.margin = '0';
  105. s.marginLeft = '0';
  106. s.marginRight = '0';
  107. s.marginTop = '0';
  108. s.marginBottom = '0';
  109. // TODO: setting this breaks viewport controls for some reason. Is there a fix?
  110. // this.root.style.zIndex = '100000';
  111. } else {
  112. const children = head.children;
  113. for (let i = 0; i < children.length; i++) {
  114. if (children[i] === this.expandedViewport) {
  115. head.removeChild(this.expandedViewport);
  116. break;
  117. }
  118. }
  119. if (this.rootState) {
  120. const t = this.rootState;
  121. for (let v of t.viewports) {
  122. head.appendChild(v);
  123. }
  124. const s = body.style
  125. s.top = t.top!;
  126. s.bottom = t.bottom!;
  127. s.left = t.left!;
  128. s.right = t.right!;
  129. s.width = t.width!;
  130. s.height = t.height!;
  131. s.maxWidth = t.maxWidth!;
  132. s.maxHeight = t.maxHeight!;
  133. s.margin = t.margin!;
  134. s.marginLeft = t.marginLeft!;
  135. s.marginRight = t.marginRight!;
  136. s.marginTop = t.marginTop!;
  137. s.marginBottom = t.marginBottom!;
  138. s.position = t.position!;
  139. s.overflow = t.overflow || '';
  140. const doc = this.getScrollElement();
  141. doc.scrollTop = t.scrollTop;
  142. doc.scrollLeft = t.scrollLeft;
  143. this.rootState = void 0;
  144. this.root.style.zIndex = t.zIndex!;
  145. }
  146. }
  147. } catch (e) {
  148. const msg = 'Layout change error, you might have to reload the page.'
  149. this.context.log.error(msg);
  150. console.error(msg, e);
  151. }
  152. }
  153. constructor(private context: PluginContext) {
  154. super({ ...PD.getDefaultValues(PluginLayoutStateParams), ...(context.spec.layout && context.spec.layout.initial) });
  155. PluginCommands.Layout.Update.subscribe(context, e => this.updateProps(e.state));
  156. // TODO how best make sure it runs on node.js as well as in the browser?
  157. if (typeof document !== 'undefined') {
  158. // <meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0' />
  159. this.expandedViewport = document.createElement('meta') as any;
  160. this.expandedViewport.name = 'viewport';
  161. this.expandedViewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0';
  162. }
  163. }
  164. }