web-view-impl.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import { syncMethods, asyncMethods, properties } from '@electron/internal/common/web-view-methods';
  2. import type * as guestViewInternalModule from '@electron/internal/renderer/web-view/guest-view-internal';
  3. import type { WebViewAttribute, PartitionAttribute } from '@electron/internal/renderer/web-view/web-view-attributes';
  4. import { setupWebViewAttributes } from '@electron/internal/renderer/web-view/web-view-attributes';
  5. import { WEB_VIEW_ATTRIBUTES } from '@electron/internal/renderer/web-view/web-view-constants';
  6. // ID generator.
  7. let nextId = 0;
  8. const getNextId = function () {
  9. return ++nextId;
  10. };
  11. export interface WebViewImplHooks {
  12. readonly guestViewInternal: typeof guestViewInternalModule;
  13. readonly allowGuestViewElementDefinition: NodeJS.InternalWebFrame['allowGuestViewElementDefinition'];
  14. readonly setIsWebView: (iframe: HTMLIFrameElement) => void;
  15. }
  16. // Represents the internal state of the WebView node.
  17. export class WebViewImpl {
  18. public beforeFirstNavigation = true;
  19. public elementAttached = false;
  20. public guestInstanceId?: number;
  21. public hasFocus = false;
  22. public internalInstanceId?: number;
  23. public viewInstanceId: number;
  24. // on* Event handlers.
  25. public on: Record<string, any> = {};
  26. public internalElement: HTMLIFrameElement;
  27. public attributes: Map<string, WebViewAttribute>;
  28. constructor (public webviewNode: HTMLElement, private hooks: WebViewImplHooks) {
  29. // Create internal iframe element.
  30. this.internalElement = this.createInternalElement();
  31. const shadowRoot = this.webviewNode.attachShadow({ mode: 'open' });
  32. const style = shadowRoot.ownerDocument.createElement('style');
  33. style.textContent = ':host { display: flex; }';
  34. shadowRoot.appendChild(style);
  35. this.attributes = setupWebViewAttributes(this);
  36. this.viewInstanceId = getNextId();
  37. shadowRoot.appendChild(this.internalElement);
  38. // Provide access to contentWindow.
  39. Object.defineProperty(this.webviewNode, 'contentWindow', {
  40. get: () => {
  41. return this.internalElement.contentWindow;
  42. },
  43. enumerable: true
  44. });
  45. }
  46. createInternalElement () {
  47. const iframeElement = document.createElement('iframe');
  48. iframeElement.style.flex = '1 1 auto';
  49. iframeElement.style.width = '100%';
  50. iframeElement.style.border = '0';
  51. // used by RendererClientBase::IsWebViewFrame
  52. this.hooks.setIsWebView(iframeElement);
  53. return iframeElement;
  54. }
  55. // Resets some state upon reattaching <webview> element to the DOM.
  56. reset () {
  57. // If guestInstanceId is defined then the <webview> has navigated and has
  58. // already picked up a partition ID. Thus, we need to reset the initialization
  59. // state. However, it may be the case that beforeFirstNavigation is false BUT
  60. // guestInstanceId has yet to be initialized. This means that we have not
  61. // heard back from createGuest yet. We will not reset the flag in this case so
  62. // that we don't end up allocating a second guest.
  63. if (this.guestInstanceId) {
  64. this.guestInstanceId = undefined;
  65. }
  66. this.beforeFirstNavigation = true;
  67. (this.attributes.get(WEB_VIEW_ATTRIBUTES.PARTITION) as PartitionAttribute).validPartitionId = true;
  68. // Since attachment swaps a local frame for a remote frame, we need our
  69. // internal iframe element to be local again before we can reattach.
  70. const newFrame = this.createInternalElement();
  71. const oldFrame = this.internalElement;
  72. this.internalElement = newFrame;
  73. if (oldFrame && oldFrame.parentNode) {
  74. oldFrame.parentNode.replaceChild(newFrame, oldFrame);
  75. }
  76. }
  77. // This observer monitors mutations to attributes of the <webview> and
  78. // updates the BrowserPlugin properties accordingly. In turn, updating
  79. // a BrowserPlugin property will update the corresponding BrowserPlugin
  80. // attribute, if necessary. See BrowserPlugin::UpdateDOMAttribute for more
  81. // details.
  82. handleWebviewAttributeMutation (attributeName: string, oldValue: any, newValue: any) {
  83. if (!this.attributes.has(attributeName) || this.attributes.get(attributeName)!.ignoreMutation) {
  84. return;
  85. }
  86. // Let the changed attribute handle its own mutation
  87. this.attributes.get(attributeName)!.handleMutation(oldValue, newValue);
  88. }
  89. createGuest () {
  90. this.internalInstanceId = getNextId();
  91. this.hooks.guestViewInternal.createGuest(this.internalElement, this.internalInstanceId, this.buildParams())
  92. .then(guestInstanceId => {
  93. this.attachGuestInstance(guestInstanceId);
  94. });
  95. }
  96. dispatchEvent (eventName: string, props: Record<string, any> = {}) {
  97. const event = new Event(eventName);
  98. Object.assign(event, props);
  99. this.webviewNode.dispatchEvent(event);
  100. if (eventName === 'load-commit') {
  101. this.onLoadCommit(props);
  102. } else if (eventName === '-focus-change') {
  103. this.onFocusChange();
  104. }
  105. }
  106. // Adds an 'on<event>' property on the webview, which can be used to set/unset
  107. // an event handler.
  108. setupEventProperty (eventName: string) {
  109. const propertyName = `on${eventName.toLowerCase()}`;
  110. return Object.defineProperty(this.webviewNode, propertyName, {
  111. get: () => {
  112. return this.on[propertyName];
  113. },
  114. set: (value) => {
  115. if (this.on[propertyName]) {
  116. this.webviewNode.removeEventListener(eventName, this.on[propertyName]);
  117. }
  118. this.on[propertyName] = value;
  119. if (value) {
  120. return this.webviewNode.addEventListener(eventName, value);
  121. }
  122. },
  123. enumerable: true
  124. });
  125. }
  126. // Updates state upon loadcommit.
  127. onLoadCommit (props: Record<string, any>) {
  128. const oldValue = this.webviewNode.getAttribute(WEB_VIEW_ATTRIBUTES.SRC);
  129. const newValue = props.url;
  130. if (props.isMainFrame && (oldValue !== newValue)) {
  131. // Touching the src attribute triggers a navigation. To avoid
  132. // triggering a page reload on every guest-initiated navigation,
  133. // we do not handle this mutation.
  134. this.attributes.get(WEB_VIEW_ATTRIBUTES.SRC)!.setValueIgnoreMutation(newValue);
  135. }
  136. }
  137. // Emits focus/blur events.
  138. onFocusChange () {
  139. const hasFocus = this.webviewNode.ownerDocument.activeElement === this.webviewNode;
  140. if (hasFocus !== this.hasFocus) {
  141. this.hasFocus = hasFocus;
  142. this.dispatchEvent(hasFocus ? 'focus' : 'blur');
  143. }
  144. }
  145. onAttach (storagePartitionId: number) {
  146. return this.attributes.get(WEB_VIEW_ATTRIBUTES.PARTITION)!.setValue(storagePartitionId);
  147. }
  148. buildParams () {
  149. const params: Record<string, any> = {
  150. instanceId: this.viewInstanceId
  151. };
  152. for (const [attributeName, attribute] of this.attributes) {
  153. params[attributeName] = attribute.getValue();
  154. }
  155. return params;
  156. }
  157. attachGuestInstance (guestInstanceId: number) {
  158. if (guestInstanceId === -1) {
  159. this.dispatchEvent('destroyed');
  160. return;
  161. }
  162. if (!this.elementAttached) {
  163. // The element could be detached before we got response from browser.
  164. // Destroy the backing webContents to avoid any zombie nodes in the frame tree.
  165. this.hooks.guestViewInternal.detachGuest(guestInstanceId);
  166. return;
  167. }
  168. this.guestInstanceId = guestInstanceId;
  169. }
  170. }
  171. export const setupMethods = (WebViewElement: typeof ElectronInternal.WebViewElement, hooks: WebViewImplHooks) => {
  172. // Focusing the webview should move page focus to the underlying iframe.
  173. WebViewElement.prototype.focus = function () {
  174. this.contentWindow.focus();
  175. };
  176. // Forward proto.foo* method calls to WebViewImpl.foo*.
  177. for (const method of syncMethods) {
  178. (WebViewElement.prototype as Record<string, any>)[method] = function (this: ElectronInternal.WebViewElement, ...args: Array<any>) {
  179. return hooks.guestViewInternal.invokeSync(this.getWebContentsId(), method, args);
  180. };
  181. }
  182. for (const method of asyncMethods) {
  183. (WebViewElement.prototype as Record<string, any>)[method] = function (this: ElectronInternal.WebViewElement, ...args: Array<any>) {
  184. return hooks.guestViewInternal.invoke(this.getWebContentsId(), method, args);
  185. };
  186. }
  187. const createPropertyGetter = function (property: string) {
  188. return function (this: ElectronInternal.WebViewElement) {
  189. return hooks.guestViewInternal.propertyGet(this.getWebContentsId(), property);
  190. };
  191. };
  192. const createPropertySetter = function (property: string) {
  193. return function (this: ElectronInternal.WebViewElement, arg: any) {
  194. return hooks.guestViewInternal.propertySet(this.getWebContentsId(), property, arg);
  195. };
  196. };
  197. for (const property of properties) {
  198. Object.defineProperty(WebViewElement.prototype, property, {
  199. get: createPropertyGetter(property),
  200. set: createPropertySetter(property)
  201. });
  202. }
  203. };