├── .eslintrc.json ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .npmrc ├── LICENSE.md ├── README.md ├── dist └── iframily.min.js ├── examples ├── .eslintrc.json ├── house │ ├── images │ │ ├── baby_button.png │ │ ├── baby_room.png │ │ ├── background.png │ │ ├── brother_button.png │ │ ├── brother_room.png │ │ ├── ekoengineering_logo.svg │ │ ├── father_button.png │ │ ├── father_room.png │ │ ├── iframily_logo.png │ │ ├── sister_button.png │ │ └── sister_room.png │ ├── index.html │ ├── index.js │ ├── rooms │ │ ├── baby.html │ │ ├── brother.html │ │ ├── father.html │ │ ├── frame.js │ │ └── sister.html │ └── style │ │ ├── index.css │ │ ├── index.scss │ │ ├── room.css │ │ └── room.scss ├── itetrisly │ ├── game │ │ ├── index.html │ │ └── texture.jpg │ ├── images │ │ ├── arrows.png │ │ ├── ekoengineering_logo.svg │ │ ├── game_splash.png │ │ └── iframily_logo.png │ ├── index.html │ ├── index.js │ └── style │ │ ├── index.css │ │ └── index.scss └── treehouse │ ├── child.html │ ├── dialogue.js │ ├── images │ ├── background.jpg │ ├── bg.gif │ ├── ekoengineering_logo.svg │ ├── iframe_background.jpg │ └── iframily_logo.png │ ├── index.html │ ├── index.js │ └── style │ ├── child.css │ ├── child.scss │ ├── index.css │ └── index.scss ├── jest-puppeteer.config.js ├── jest.config.js ├── package.json ├── playground ├── child.html └── parent.html ├── src ├── base.js ├── child.js ├── constants.js ├── iframily.js └── parent.js ├── test ├── .eslintrc.js ├── advanced.test.js ├── basic.test.js ├── constants.js ├── helpers.js └── public │ ├── basic │ ├── child.html │ └── parent.html │ ├── crossdomain │ ├── child.html │ └── parent.html │ └── multiple_frames │ ├── child1.html │ ├── child2.html │ ├── child2innerchild.html │ └── parent.html └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | /* 2 | * Eko's ESLint JSON Config file (eslint allows JavaScript-style comments in JSON config files). 3 | */ 4 | 5 | { 6 | // Enable the ESLint recommended rules as a starting point. 7 | // These are rules that report common problems, see https://eslint.org/docs/rules/ 8 | "extends": "eslint:recommended", 9 | 10 | // Specify the envs (an environment defines global variables that are predefined). 11 | // See https://eslint.org/docs/user-guide/configuring#specifying-environments 12 | "env": { 13 | // Browser global variables. 14 | "browser": true, 15 | 16 | // Node.js global variables and Node.js scoping. 17 | "node": true, 18 | 19 | // CommonJS global variables and CommonJS scoping (use this for browser-only code that uses Browserify/WebPack). 20 | "commonjs": true, 21 | 22 | // Globals common to both Node.js and Browser. 23 | "shared-node-browser": true, 24 | 25 | // enable all ECMAScript 6 features except for modules (this automatically sets the ecmaVersion parser option to 6). 26 | "es6": true, 27 | 28 | // web workers global variables. 29 | "worker": true 30 | }, 31 | 32 | // https://eslint.org/docs/rules 33 | "rules": { 34 | 35 | /////////////////////////////////////////////////////////////////////////////////// 36 | // Possible Errors https://eslint.org/docs/rules/#possible-errors 37 | /////////////////////////////////////////////////////////////////////////////////// 38 | 39 | // https://eslint.org/docs/rules/no-console 40 | "no-console": ["error", { "allow": ["error"] }], 41 | 42 | // https://eslint.org/docs/rules/no-empty 43 | "no-empty": ["error", { "allowEmptyCatch": true }], 44 | 45 | // https://eslint.org/docs/rules/valid-jsdoc 46 | // TODO - valid-jsdoc 47 | 48 | /////////////////////////////////////////////////////////////////////////////////// 49 | // Best Practices https://eslint.org/docs/rules/#best-practices 50 | /////////////////////////////////////////////////////////////////////////////////// 51 | 52 | // https://eslint.org/docs/rules/accessor-pairs 53 | "accessor-pairs": "error", 54 | 55 | // https://eslint.org/docs/rules/array-callback-return 56 | "array-callback-return": ["error", { "allowImplicit": true }], 57 | 58 | // https://eslint.org/docs/rules/block-scoped-var 59 | "block-scoped-var": "error", 60 | 61 | // https://eslint.org/docs/rules/curly 62 | "curly": ["error", "all"], 63 | 64 | // https://eslint.org/docs/rules/default-case 65 | "default-case": "error", 66 | 67 | // https://eslint.org/docs/rules/dot-location 68 | "dot-location": ["error", "property"], 69 | 70 | // https://eslint.org/docs/rules/dot-notation 71 | "dot-notation": "error", 72 | 73 | // https://eslint.org/docs/rules/eqeqeq 74 | "eqeqeq": ["error", "always"], 75 | 76 | // https://eslint.org/docs/rules/no-alert 77 | "no-alert": "error", 78 | 79 | // https://eslint.org/docs/rules/no-caller 80 | "no-caller": "error", 81 | 82 | // https://eslint.org/docs/rules/no-else-return 83 | "no-else-return": ["error", { "allowElseIf": false }], 84 | 85 | // https://eslint.org/docs/rules/no-eq-null 86 | "no-eq-null": "error", 87 | 88 | // https://eslint.org/docs/rules/no-eval 89 | "no-eval": "error", 90 | 91 | // https://eslint.org/docs/rules/no-extra-bind 92 | "no-extra-bind": "error", 93 | 94 | // https://eslint.org/docs/rules/no-fallthrough 95 | "no-fallthrough": ["error", { "commentPattern": "fall-?thr(ough|u)" }], 96 | 97 | // https://eslint.org/docs/rules/no-floating-decimal 98 | "no-floating-decimal": "error", 99 | 100 | // https://eslint.org/docs/rules/no-implicit-coercion 101 | "no-implicit-coercion": ["error", { "allow": ["!!"] }], 102 | 103 | // https://eslint.org/docs/rules/no-implicit-globals 104 | "no-implicit-globals": "error", 105 | 106 | // https://eslint.org/docs/rules/no-implied-eval 107 | "no-implied-eval": "error", 108 | 109 | // https://eslint.org/docs/rules/no-invalid-this 110 | "no-invalid-this": "error", 111 | 112 | // https://eslint.org/docs/rules/no-iterator 113 | "no-iterator": "error", 114 | 115 | // https://eslint.org/docs/rules/no-labels 116 | "no-labels": "error", 117 | 118 | // https://eslint.org/docs/rules/no-lone-blocks 119 | "no-lone-blocks": "error", 120 | 121 | // https://eslint.org/docs/rules/no-loop-func 122 | "no-loop-func": "error", 123 | 124 | // https://eslint.org/docs/rules/no-magic-numbers 125 | "no-magic-numbers": ["error", { 126 | "ignore": [ 127 | 0, 1, -1, 128 | 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 129 | 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 130 | 24, 60, 131 | 0.5, 0.25, 1.5 132 | ] 133 | }], 134 | 135 | // https://eslint.org/docs/rules/no-multi-spaces 136 | "no-multi-spaces": ["error", { "ignoreEOLComments": true, "exceptions": { 137 | "Property": true, 138 | "VariableDeclarator": true, 139 | "ImportDeclaration": true 140 | }}], 141 | 142 | // https://eslint.org/docs/rules/no-multi-str 143 | "no-multi-str": "error", 144 | 145 | // https://eslint.org/docs/rules/no-new 146 | "no-new": "error", 147 | 148 | // https://eslint.org/docs/rules/no-new-wrappers 149 | "no-new-wrappers": "error", 150 | 151 | // https://eslint.org/docs/rules/no-octal-escape 152 | "no-octal-escape": "error", 153 | 154 | // https://eslint.org/docs/rules/no-proto 155 | "no-proto": "error", 156 | 157 | // https://eslint.org/docs/rules/no-return-assign 158 | "no-return-assign": "error", 159 | 160 | // https://eslint.org/docs/rules/no-return-await 161 | "no-return-await": "error", 162 | 163 | // https://eslint.org/docs/rules/no-script-url 164 | "no-script-url": "error", 165 | 166 | // https://eslint.org/docs/rules/no-self-compare 167 | "no-self-compare": "error", 168 | 169 | // https://eslint.org/docs/rules/no-sequences 170 | "no-sequences": "error", 171 | 172 | // https://eslint.org/docs/rules/no-throw-literal 173 | "no-throw-literal": "error", 174 | 175 | // https://eslint.org/docs/rules/no-unmodified-loop-condition 176 | "no-unmodified-loop-condition": "error", 177 | 178 | // https://eslint.org/docs/rules/no-unused-expressions 179 | "no-unused-expressions": "error", 180 | 181 | // https://eslint.org/docs/rules/no-useless-call 182 | "no-useless-call": "error", 183 | 184 | // https://eslint.org/docs/rules/no-useless-concat 185 | "no-useless-concat": "error", 186 | 187 | // https://eslint.org/docs/rules/no-useless-return 188 | "no-useless-return": "error", 189 | 190 | // https://eslint.org/docs/rules/no-void 191 | "no-void": "error", 192 | 193 | // https://eslint.org/docs/rules/no-with 194 | "no-with": "error", 195 | 196 | // https://eslint.org/docs/rules/prefer-promise-reject-errors 197 | "prefer-promise-reject-errors": ["error", { "allowEmptyReject": true }], 198 | 199 | // https://eslint.org/docs/rules/radix 200 | "radix": ["error", "always"], 201 | 202 | // https://eslint.org/docs/rules/require-await 203 | "require-await": "error", 204 | 205 | // https://eslint.org/docs/rules/wrap-iife 206 | "wrap-iife": ["error", "any"], 207 | 208 | /////////////////////////////////////////////////////////////////////////////////// 209 | // Strict Mode https://eslint.org/docs/rules/#strict-mode 210 | /////////////////////////////////////////////////////////////////////////////////// 211 | 212 | // https://eslint.org/docs/rules/strict 213 | "strict": ["error", "safe"], 214 | 215 | /////////////////////////////////////////////////////////////////////////////////// 216 | // Variables https://eslint.org/docs/rules/#variables 217 | /////////////////////////////////////////////////////////////////////////////////// 218 | 219 | // https://eslint.org/docs/rules/no-shadow 220 | "no-shadow": ["error", { 221 | "builtinGlobals": true, 222 | "hoist": "functions", 223 | "allow": [ 224 | "name", 225 | "parent", 226 | "event", 227 | "status" 228 | ] 229 | }], 230 | 231 | // https://eslint.org/docs/rules/no-shadow-restricted-names 232 | "no-shadow-restricted-names": "error", 233 | 234 | // https://eslint.org/docs/rules/no-undef-init 235 | "no-undef-init": "error", 236 | 237 | // https://eslint.org/docs/rules/no-use-before-define 238 | "no-use-before-define": ["error", { "functions": true, "classes": true, "variables": true }], 239 | 240 | /////////////////////////////////////////////////////////////////////////////////// 241 | // Node.js and CommonJS https://eslint.org/docs/rules/#nodejs-and-commonjs 242 | /////////////////////////////////////////////////////////////////////////////////// 243 | 244 | // https://eslint.org/docs/rules/global-require 245 | "global-require": "error", 246 | 247 | // https://eslint.org/docs/rules/no-buffer-constructor 248 | "no-buffer-constructor": "error", 249 | 250 | // https://eslint.org/docs/rules/no-new-require 251 | "no-new-require": "error", 252 | 253 | // https://eslint.org/docs/rules/no-path-concat 254 | "no-path-concat": "error", 255 | 256 | /////////////////////////////////////////////////////////////////////////////////// 257 | // Stylistic Issues https://eslint.org/docs/rules/#stylistic-issues 258 | /////////////////////////////////////////////////////////////////////////////////// 259 | 260 | // https://eslint.org/docs/rules/array-bracket-newline 261 | "array-bracket-newline": ["warn", { "multiline": true }], 262 | 263 | // https://eslint.org/docs/rules/array-bracket-spacing 264 | "array-bracket-spacing": ["warn", "never"], 265 | 266 | // https://eslint.org/docs/rules/block-spacing 267 | "block-spacing": ["warn", "always"], 268 | 269 | // https://eslint.org/docs/rules/brace-style 270 | "brace-style": ["warn", "1tbs", { "allowSingleLine": true }], 271 | 272 | // https://eslint.org/docs/rules/camelcase 273 | "camelcase": ["warn", { "properties": "always" }], 274 | 275 | // https://eslint.org/docs/rules/capitalized-comments 276 | "capitalized-comments": ["warn", "always", { "ignoreInlineComments": true, "ignoreConsecutiveComments": true }], 277 | 278 | // https://eslint.org/docs/rules/comma-dangle 279 | "comma-dangle": ["warn", "only-multiline"], 280 | 281 | // https://eslint.org/docs/rules/comma-spacing 282 | "comma-spacing": ["warn", { "before": false, "after": true }], 283 | 284 | // https://eslint.org/docs/rules/comma-style 285 | "comma-style": ["warn", "last"], 286 | 287 | // https://eslint.org/docs/rules/computed-property-spacing 288 | "computed-property-spacing": ["warn", "never"], 289 | 290 | // TODO - discuss with TEAM! 291 | // https://eslint.org/docs/rules/consistent-this 292 | "consistent-this": ["warn", "that", "_this", "self"], 293 | 294 | // https://eslint.org/docs/rules/eol-last 295 | "eol-last": ["warn", "always"], 296 | 297 | // https://eslint.org/docs/rules/func-call-spacing 298 | "func-call-spacing": ["warn", "never"], 299 | 300 | // https://eslint.org/docs/rules/func-name-matching 301 | "func-name-matching": ["warn", "always"], 302 | 303 | // https://eslint.org/docs/rules/function-paren-newline 304 | "function-paren-newline": ["warn", "consistent"], 305 | 306 | // https://eslint.org/docs/rules/implicit-arrow-linebreak 307 | "implicit-arrow-linebreak": ["warn", "beside"], 308 | 309 | // https://eslint.org/docs/rules/indent 310 | "indent": ["warn", 4, { 311 | "SwitchCase": 1, 312 | "FunctionDeclaration": { 313 | "parameters": 1, 314 | "body": 1 315 | }, 316 | "FunctionExpression": { 317 | "parameters": 1, 318 | "body": 1 319 | }, 320 | "CallExpression": { 321 | "arguments": 1 322 | }, 323 | "ArrayExpression": 1, 324 | "ObjectExpression": 1, 325 | "ImportDeclaration": 1, 326 | "ignoredNodes": [] 327 | }], 328 | 329 | // https://eslint.org/docs/rules/jsx-quotes 330 | "jsx-quotes": ["warn", "prefer-double"], 331 | 332 | // https://eslint.org/docs/rules/key-spacing 333 | "key-spacing": [ 334 | "warn", 335 | { 336 | "singleLine": { 337 | "beforeColon": false, 338 | "afterColon": true, 339 | "mode": "strict" 340 | }, 341 | "multiLine": { 342 | "beforeColon": false, 343 | "afterColon": true, 344 | "mode": "minimum" 345 | } 346 | } 347 | ], 348 | 349 | // https://eslint.org/docs/rules/keyword-spacing 350 | "keyword-spacing": ["warn", { "before": true, "after": true }], 351 | 352 | // https://eslint.org/docs/rules/linebreak-style 353 | "linebreak-style": ["warn", "unix"], 354 | 355 | // https://eslint.org/docs/rules/lines-around-comment 356 | "lines-around-comment": ["warn", { 357 | "beforeBlockComment": true, 358 | "afterBlockComment": false, 359 | "beforeLineComment": true, 360 | "afterLineComment": false, 361 | "allowBlockStart": true, 362 | "allowBlockEnd": false, 363 | "allowClassStart": true, 364 | "allowClassEnd": false, 365 | "allowObjectStart": true, 366 | "allowObjectEnd": false, 367 | "allowArrayStart": true, 368 | "allowArrayEnd": false 369 | }], 370 | 371 | // https://eslint.org/docs/rules/max-len 372 | "max-len": ["warn", { 373 | "code": 120, 374 | "tabWidth": 4, 375 | "ignoreUrls": true, 376 | "ignoreComments": true 377 | }], 378 | 379 | // https://eslint.org/docs/rules/max-lines 380 | "max-lines": ["warn", { 381 | "max": 700, 382 | "skipBlankLines": true, 383 | "skipComments": true 384 | }], 385 | 386 | // https://eslint.org/docs/rules/max-statements 387 | "max-statements": ["warn", 30], 388 | 389 | // https://eslint.org/docs/rules/max-statements-per-line 390 | "max-statements-per-line": ["warn", { 391 | "max": 1 392 | }], 393 | 394 | // https://eslint.org/docs/rules/multiline-ternary 395 | "multiline-ternary": ["warn", "always-multiline"], 396 | 397 | // https://eslint.org/docs/rules/new-cap 398 | "new-cap": ["warn", { "newIsCap": true, "capIsNew": true, "properties": true }], 399 | 400 | // https://eslint.org/docs/rules/new-parens 401 | "new-parens": "warn", 402 | 403 | // https://eslint.org/docs/rules/newline-per-chained-call 404 | "newline-per-chained-call": ["warn", { "ignoreChainWithDepth": 2 }], 405 | 406 | // https://eslint.org/docs/rules/no-array-constructor 407 | "no-array-constructor": "warn", 408 | 409 | // https://eslint.org/docs/rules/no-bitwise 410 | "no-bitwise": "warn", 411 | 412 | // https://eslint.org/docs/rules/no-lonely-if 413 | "no-lonely-if": "warn", 414 | 415 | // https://eslint.org/docs/rules/no-mixed-operators 416 | "no-mixed-operators": "warn", 417 | 418 | // https://eslint.org/docs/rules/no-multi-assign 419 | "no-multi-assign": "warn", 420 | 421 | // https://eslint.org/docs/rules/no-multiple-empty-lines 422 | "no-multiple-empty-lines": ["warn", { "max": 2, "maxEOF": 1, "maxBOF": 0 }], 423 | 424 | // https://eslint.org/docs/rules/no-negated-condition 425 | "no-negated-condition": "warn", 426 | 427 | // https://eslint.org/docs/rules/no-new-object 428 | "no-new-object": "warn", 429 | 430 | // https://eslint.org/docs/rules/no-tabs 431 | "no-tabs": "warn", 432 | 433 | // https://eslint.org/docs/rules/no-trailing-spaces 434 | "no-trailing-spaces": ["warn", { "skipBlankLines": false, "ignoreComments": false }], 435 | 436 | // https://eslint.org/docs/rules/no-unneeded-ternary 437 | "no-unneeded-ternary": ["warn", { "defaultAssignment": false }], 438 | 439 | // https://eslint.org/docs/rules/no-whitespace-before-property 440 | "no-whitespace-before-property": "warn", 441 | 442 | // https://eslint.org/docs/rules/object-curly-newline 443 | "object-curly-newline": ["warn", { "consistent": true }], 444 | 445 | // https://eslint.org/docs/rules/object-curly-spacing 446 | "object-curly-spacing": ["warn", "always"], 447 | 448 | // https://eslint.org/docs/rules/one-var 449 | "one-var": ["error", "never"], 450 | 451 | // https://eslint.org/docs/rules/operator-linebreak 452 | "operator-linebreak": ["warn", "after"], 453 | 454 | // https://eslint.org/docs/rules/padded-blocks 455 | "padded-blocks": ["warn", "never"], 456 | 457 | // https://eslint.org/docs/rules/quote-props 458 | "quote-props": ["warn", "as-needed"], 459 | 460 | // https://eslint.org/docs/rules/quotes 461 | "quotes": ["warn", "single", { "avoidEscape": true, "allowTemplateLiterals": true }], 462 | 463 | // https://eslint.org/docs/rules/semi 464 | "semi": ["warn", "always"], 465 | 466 | // https://eslint.org/docs/rules/semi-spacing 467 | "semi-spacing": ["warn", { "before": false, "after": true }], 468 | 469 | // https://eslint.org/docs/rules/semi-style 470 | "semi-style": ["warn", "last"], 471 | 472 | // https://eslint.org/docs/rules/space-before-blocks 473 | "space-before-blocks": ["warn", "always"], 474 | 475 | // https://eslint.org/docs/rules/space-before-function-paren 476 | "space-before-function-paren": ["warn", { 477 | "anonymous": "never", 478 | "named": "never", 479 | "asyncArrow": "always" 480 | }], 481 | 482 | // https://eslint.org/docs/rules/space-in-parens 483 | "space-in-parens": ["warn", "never"], 484 | 485 | // https://eslint.org/docs/rules/space-infix-ops 486 | "space-infix-ops": "warn", 487 | 488 | // https://eslint.org/docs/rules/space-unary-ops 489 | "space-unary-ops": ["warn", { "words": true, "nonwords": false }], 490 | 491 | // https://eslint.org/docs/rules/spaced-comment 492 | "spaced-comment": ["warn", "always", { "exceptions": ["-", "/", "=", "*"] }], 493 | 494 | // https://eslint.org/docs/rules/switch-colon-spacing 495 | "switch-colon-spacing": ["warn", { "after": true, "before": false }], 496 | 497 | // https://eslint.org/docs/rules/unicode-bom 498 | "unicode-bom": ["error", "never"], 499 | 500 | /////////////////////////////////////////////////////////////////////////////////// 501 | // ECMAScript 6 https://eslint.org/docs/rules/#ecmascript-6 502 | /////////////////////////////////////////////////////////////////////////////////// 503 | 504 | // https://eslint.org/docs/rules/arrow-spacing 505 | "arrow-spacing": ["warn", { "before": true, "after": true }], 506 | 507 | // https://eslint.org/docs/rules/no-duplicate-imports 508 | "no-duplicate-imports": ["error", { "includeExports": true }], 509 | 510 | // https://eslint.org/docs/rules/no-useless-computed-key 511 | "no-useless-computed-key": "error", 512 | 513 | // https://eslint.org/docs/rules/no-useless-constructor 514 | "no-useless-constructor": "error", 515 | 516 | // https://eslint.org/docs/rules/no-useless-rename 517 | "no-useless-rename": ["error", { 518 | "ignoreImport": false, 519 | "ignoreExport": false, 520 | "ignoreDestructuring": false 521 | }], 522 | 523 | // https://eslint.org/docs/rules/rest-spread-spacing 524 | "rest-spread-spacing": ["warn", "never"], 525 | 526 | // https://eslint.org/docs/rules/template-curly-spacing 527 | "template-curly-spacing": ["warn", "never"] 528 | 529 | // TODO - more ES6 rules 530 | } 531 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **iframily version** 27 | x.x.x 28 | 29 | **Desktop (please complete the following information):** 30 | - OS: [e.g. iOS] 31 | - Browser [e.g. chrome, safari] 32 | - Version [e.g. 22] 33 | 34 | **Smartphone (please complete the following information):** 35 | - Device: [e.g. iPhone6] 36 | - OS: [e.g. iOS8.1] 37 | - Browser [e.g. stock browser, safari] 38 | - Version [e.g. 22] 39 | 40 | **Code example** 41 | Please provide a link to a CodeSandbox / GitHub repo or a minimal code example that reproduces the problem 42 | 43 | **Additional context** 44 | Add any other context about the problem here. 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .nvmrc 4 | .vscode 5 | .idea 6 | # Local Netlify folder 7 | .netlify -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Copyright 2020 Eko 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 6 | use this file except in compliance with the License. You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | License for the specific language governing permissions and limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Iframily 2 | 3 | # Iframily - postMessage made simple & safe 4 | 5 | Iframily simplifies working and establishing communication between frames. 6 | 7 | It provides a simpler API than [postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage), which includes Promise-based responses, message queuing, and holding on to messages until both frames are ready to talk. 8 | 9 | This is how it works: 10 | 11 | 1. Create iframilies in the parent and child frame. 12 | 2. If a parent id and child id match, they will pair. 13 | 3. Send messages between paired iframilies. 14 | 15 | **With vanilla postMessage:** 16 | 17 | ![postMessage](https://s3.amazonaws.com/eko.com/s/iframily/postmessage.png) 18 | 19 | **With Iframily:** 20 | 21 | ![Iframily](https://s3.amazonaws.com/eko.com/s/iframily/iframily.png) 22 | 23 | By the team from [eko Engineering](https://eko.engineering) 24 | 25 | ## Table of Contents 26 | 27 | * [Features](#features) 28 | * [Examples](#examples) 29 | * [Installation](#installation) 30 | * [API](#api) 31 | * [Iframily singleton](#----iframily-singleton----) 32 | * [iframily instance](#----iframily-instance----) 33 | * [Notes](#notes) 34 | * [Contributing](#contributing) 35 | 36 | ## Features 37 | 38 | ### What makes it special 39 | 40 | Iframily pushes you to be more responsible with your frame security by keeping it front and center. You'll need to explicitly specify the allowed origin for communication and the sender's origin for verification, both on the parent and child frames. See more on [initParent() / initChild() API](#----iframily-singleton----). 41 | 42 | ### What makes it awesome 43 | 44 | * Enforces security best practices: Iframily prevents you from accidentally setting too permissive configuration, making you less vulnerable to exploits. 45 | * Long-lived two-way communication between frames. While other libraries treat messages separately, Iframily enables you to easily respond to incoming messages and maintain the communication chain, keeping track of messages and their responses to allow for more complex communications. 46 | * Each frame connection is named and identified, allowing for continuous communication even when a frame redirects or reloads. 47 | * Fast and intuitive promise-based API (supports async responses). 48 | * Discrete handlers can be defined for specific frames or modules in frames, allowing true separation of concerns and code between the different frames and their uses. 49 | * Unlike other libraries, you don’t need to change how you initialize your iframes. Iframily works out of the box with any iframe. Just give it a frame identifier and let it do its thing. 50 | * Message queue until paring completed so messages don’t get lost, even if a frame page hasn’t completed loading yet. 51 | * Zero dependencies. 52 | * Unit + battle tested 53 | 54 | ## Examples 55 | 56 | [Invasion of the Frame Snatchers](https://codepen.io/asimen1/pen/ZEBzaLd) 57 | 58 | Demonstrates a simple usage of Iframily to communicate between a parent and child frame through an interactive, sweet father-son dialogue about world domination. 59 | 60 | [](https://codepen.io/asimen1/pen/ZEBzaLd) 61 | 62 | [Text Chat Madness](https://codepen.io/asimen1/pen/KKNdgXb) 63 | 64 | Demonstrates usage of Iframily to communicate between a parent frame and multiple child frames through interactive conversations in a family of five. How will mom handle all these questions? You decide! 65 | 66 | [](https://codepen.io/asimen1/pen/KKNdgXb) 67 | 68 | [Crazy Tetris](https://codepen.io/asimen1/pen/qBqOZMY) 69 | 70 | A novel (and somewhat strange) reimplementation of the classic Tetris using Iframily! Each frame runs its own self-contained Tetris instance. The twist? Instead of moving the pieces, the player moves the frames themselves to align the pieces and allow them to flow freely between frames. 71 | 72 | [](https://codepen.io/asimen1/pen/qBqOZMY) 73 | 74 | See the repo's `examples/` directory for the source code. 75 | 76 | ## Installation 77 | 78 | ```shell 79 | npm i @ekolabs/iframily --save 80 | ``` 81 | 82 | **ES6** 83 | ```javascript 84 | import Iframily from '@ekolabs/iframily'; 85 | ``` 86 | **CommonJS** 87 | ```javascript 88 | const Iframily = require('@ekolabs/iframily'); 89 | ``` 90 | 91 | You can also add the library via script tag and use `window.Iframily`, like so: 92 | 93 | ```html 94 | 95 | 96 | 97 | 98 | 99 | ``` 100 | 101 | ## API 102 | 103 | ### --- Iframily singleton --- 104 | 105 | Iframily is a singleton and will allow you to initialize parent/child [iframily instances](#----iframily-instance----). 106 | 107 | #### 🔵 `Iframily.initParent(id, targetOrigin, msgHandler, options) -> iframily instance` 108 | 109 | #### 🔵 `Iframily.initChild(id, targetOrigin, msgHandler, options) -> iframily instance` 110 | 111 | Creates a parent/child [iframily instance](#----iframily-instance----) respectively (to be used in the parent/child frame) and returns it, if successful. 112 | 113 | | Param | Type | Description | 114 | | :-------------: |:--------------:| :------------| 115 | | id | `string` | A unique id to identify this [iframily instance](#----iframily-instance----), this is used in order to match parent and child iframilies. Will abort and log an error if the id already exists in the current frame. | 116 | | targetOrigin | `string` | The URI to use as the target origin for the `postMessage` call and for validating the sender origin on incoming messages, for example: `https://subdomain.domain.com`. For security concerns this argument is mandatory, see [link](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) for more info. If both frames our on the same origin, you can use `window.location.origin` as the value. Passing `"*"` (wildcard) is not supported, if you are 100% certain that you want to allow messages to be received and handled by everyone, you can pass `"DANGEROUSLY_SET_WILDCARD"` instead. | 117 | | msgHandler | `function` | Optional - A handler for incoming messages from the paired iframily in the parent/child frame. The `msgHandler` can return back a response value or a promise that will be resolved/rejected with a response value. | 118 | | options | `object` | Optional - Additional options, see possible options below: | 119 | | options.onPairedHandler | `function` | Optional - A handler that will be invoked upon pairing. | 120 | | options.onDisposedHandler | `function` | Optional - A handler that will be invoked when the instance is disposed. | 121 | 122 | #### 🔵 `Iframily.isIframilyMessage(event) -> boolean` 123 | 124 | If you manually listen to messages using the window ["message" event](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#The_dispatched_event) you will also receive internal iframily messages. This method will return `true` for this events so you can easily identify them. 125 | 126 | ```javascript 127 | window.addEventListener('message', receiveMessage); 128 | 129 | function receiveMessage(event) { 130 | // Ignore Iframily messages. 131 | if (Iframily.isIframilyMessage(event)) { 132 | return; 133 | } 134 | 135 | // Handle other messages below: 136 | // ... 137 | } 138 | ``` 139 | 140 | ### --- iframily instance --- 141 | 142 | The iframily instance is the object returned when initializing a new iframily using the `initParent()` or `initChild()` methods. 143 | 144 | #### 🟢 `f.sendMessage(msg) -> Promise` 145 | 146 | | Param | Type | Description | 147 | | :-------------: |:--------------:| :------------| 148 | | msg | `serializable` | The message to be sent, can be any serializable data (see [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)). 149 | 150 | Returns a promise that will be resolved with the response value from the receiving iframily instance message handler. The promise will be rejected if the promised returned by the receiving iframily instance rejects. 151 | 152 | > If the receiving iframily instance did not return an explicit response value in its message handler, the promise will be resolved with `undefined`. 153 | 154 | #### 🟢 `f.dispose()` 155 | 156 | Dispose of the iframily instance, making it obsolete. 157 | 158 | > * You cannot reuse a disposed instance, you can however create a new instance with the same id. 159 | > * Any iframilies paired with this instance will still be able to keep sending messages to the disposed instance but the messages will be ignored by the disposed target. 160 | 161 | This method is useful when you have a parent frame which recreates the same child frame and you want to use the same id for the iframilies. 162 | 163 | #### 🟢 `f.isDisposed -> boolean (read only)` 164 | 165 | Returns `true` if this iframily instance has been disposed. 166 | 167 | #### 🟢 `f.id -> string (read only)` 168 | 169 | Returns the id that this iframily was initialized with. 170 | 171 | ## Usage example 172 | 173 | ```javascript 174 | // ------------------------- 175 | // In the parent frame. 176 | // ------------------------- 177 | const Iframily = require('@ekolabs/iframily'); 178 | 179 | let msgHandler = function(msg) { 180 | console.log('parent got message:', msg); 181 | }; 182 | 183 | let iframilyParent = Iframily.initParent('myUniqueId', window.location.origin, msgHandler); 184 | 185 | iframilyParent.sendMessage('do something').then((response) => { 186 | console.log('parent got response:', response); 187 | }); 188 | 189 | iframilyParent.sendMessage('do something async').then((response) => { 190 | console.log('parent got async response:', response); 191 | }); 192 | ``` 193 | 194 | ```javascript 195 | // ------------------------- 196 | // In the child frame. 197 | // ------------------------- 198 | const Iframily = require('@ekolabs/iframily'); 199 | 200 | let msgHandler = function(msg) { 201 | console.log('child got message:', msg); 202 | 203 | if (msg === 'do something') { 204 | return 'OK! done!'; 205 | } else if (msg === 'do something async') { 206 | return new Promise((resolve, reject) => { 207 | setTimeout(() => { 208 | resolve('OK! done async!'); 209 | }, 1000); 210 | }); 211 | } 212 | }; 213 | 214 | let iframilyChild = Iframily.initChild('myUniqueId', window.location.origin, msgHandler); 215 | 216 | iframilyChild.sendMessage({ text: 'fancy...' }); 217 | ``` 218 | 219 | * Your unique id (`myUniqueId` in the example) should match between parent and child. 220 | 221 | ## Notes 222 | 223 | * You can create multiple iframilies in each frame but the unique ids cannot be used more than once per frame (unless the previous one has been disposed). 224 | * Parent can pair with one child only and vice versa (due to previous note). 225 | * Specifying "targetOrigin" as regex is not supported mostly to avoid "dot" unescaping security vulnerability. 226 | * Designed for modern browsers (IE not supported). 227 | 228 | ## Contributing 229 | 230 | * `npm run dev` - builds unuglified bundle and watches for changes. 231 | * `npm run playground` - launch a simple page with a parent and child frame iframilies to manually test out stuff. 232 | * Please make all pull requests against the `develop` branch. 233 | * Please update/add tests coverage. 234 | * Pay attention to linting (they will fail the production bundle build). 235 | * `npm run test` - run tests. 236 | * `npm run test-debug-mac` or `npm run test-debug-win` for debugging the tests. 237 | * Tests run against the built bundle, so make sure your changes are built before running. 238 | * Tests require a modification in your hosts file (see note below). 239 | 240 | Running tests require `node >=10` and modifying your hosts file like so: 241 | 242 | ```text 243 | # iframily 244 | 127.0.0.1 sub1.domain1iframily.com 245 | 127.0.0.1 sub2.domain2iframily.com 246 | ``` 247 | 248 | When your done developing, make sure to run the production build via `npm run build` so the correct bundle will be committed to the `dist` folder. 249 | -------------------------------------------------------------------------------- /dist/iframily.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Iframily=t():e.Iframily=t()}(this,(function(){return function(e){var t={};function n(i){if(t[i])return t[i].exports;var r=t[i]={i:i,l:!1,exports:{}};return e[i].call(r.exports,r,r.exports,n),r.l=!0,r.exports}return n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)n.d(i,r,function(t){return e[t]}.bind(null,r));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=2)}([function(e,t,n){"use strict";e.exports={PARENT:"parent",CHILD:"child",FRAMILY_ID_PREFIX:"__iframily__",FRAMILY_INIT:"iframily_init",FRAMILY_INIT_SUCCESSFUL:"iframily_init_successful",DANGEROUSLY_SET_WILDCARD:"DANGEROUSLY_SET_WILDCARD",ATTEMPT_TO_CONNECT_INTERVAL:200,README_SINGLETON_URL:"https://github.com/EkoLabs/iframily#----iframily-singleton----"}},function(e,t,n){"use strict";function i(e,t){for(var n=0;n2&&void 0!==arguments[2]?arguments[2]:{},i={_iframilyUid:this._iframilyUid,_fromType:this._iframilyType,msg:e};t&&(this._cbUid++,this._pendingCb[this._cbUid]=t,i._cbUid=this._cbUid),this._hasConnected||n.forceSend?this._postMessage(i):(console.warn("[Iframily] - No one paired yet, queuing message sent by ".concat(this._iframilyType," id: ").concat(this._id)),this._msgQueue.push(this._postMessage.bind(this,i)))}},{key:"_sendResponse",value:function(e,t){var n={_iframilyUid:this._iframilyUid,_fromType:this._iframilyType,_isResponse:!0,_cbUid:e,_isResolved:t.isResolved,_isRejected:t.isRejected,_cbResolveValue:t.value,_cbRejectError:t.err};this._postMessage(n)}},{key:"_handleMessage",value:function(e){var t=this;e._isResponse?this._handleResponse(e):Promise.resolve().then((function(){return t._msgHandler(e.msg)})).then((function(n){t._sendResponse(e._cbUid,{isResolved:!0,value:n})})).catch((function(n){t._sendResponse(e._cbUid,{isRejected:!0,err:n})}))}},{key:"_handleResponse",value:function(e){if(e._cbUid){var t=this._pendingCb[e._cbUid];if(e._isResolved)t.resolve(e._cbResolveValue);else{if(!e._isRejected)throw new Error("[Iframily] - Missing resolve/reject information on response sent by ".concat(e._fromType," id: ").concat(e._iframilyUid));t.reject(e._cbRejectError)}delete this._pendingCb[e._cbUid]}}},{key:"_displayDisposedError",value:function(){console.error("[Iframily] - Attempting to use a disposed instance")}},{key:"sendMessage",value:function(e){var t=this;return this._isDisposed?this._displayDisposedError():new Promise((function(n,i){t._sendMessage(e,{resolve:n,reject:i})}))}},{key:"dispose",value:function(){if(this._isDisposed)return this._displayDisposedError();this._hasConnected=!1,this._msgQueue=[],this._isDisposed=!0,this._onDisposedHandler()}},{key:"isDisposed",get:function(){return this._isDisposed}},{key:"id",get:function(){return this._id}}])&&i(t.prototype,n),s&&i(t,s),e}()},function(e,t,n){"use strict";function i(e,t){for(var n=0;n3&&void 0!==arguments[3]?arguments[3]:{};if(f(e,c,"parent")&&l(t,e))return c[e]=new s(e,t,n,i),c[e]}},{key:"initChild",value:function(e,t,n){var i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{};if(f(e,u,"child")&&l(t,e)&&d(e))return u[e]=new o(e,t,n,i),u[e]}},{key:"isIframilyMessage",value:function(e){var t=e&&e.data;return t&&t._iframilyUid&&t._iframilyUid.includes(a.FRAMILY_ID_PREFIX)}}],(n=null)&&i(t.prototype,n),r&&i(t,r),e}()},function(e,t,n){"use strict";function i(e){return(i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){for(var n=0;n 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 36 | 40 | 42 | 44 | 47 | 50 | 52 | 54 | 56 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /examples/house/images/father_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EkoLabs/iframily/01c190894766764ae24d085c4309b286f8d11fbb/examples/house/images/father_button.png -------------------------------------------------------------------------------- /examples/house/images/father_room.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EkoLabs/iframily/01c190894766764ae24d085c4309b286f8d11fbb/examples/house/images/father_room.png -------------------------------------------------------------------------------- /examples/house/images/iframily_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EkoLabs/iframily/01c190894766764ae24d085c4309b286f8d11fbb/examples/house/images/iframily_logo.png -------------------------------------------------------------------------------- /examples/house/images/sister_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EkoLabs/iframily/01c190894766764ae24d085c4309b286f8d11fbb/examples/house/images/sister_button.png -------------------------------------------------------------------------------- /examples/house/images/sister_room.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EkoLabs/iframily/01c190894766764ae24d085c4309b286f8d11fbb/examples/house/images/sister_room.png -------------------------------------------------------------------------------- /examples/house/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | iframily house 6 | 7 | 8 | 9 | 10 |
mother.html
11 |
12 | 13 |
postMessage made simple & safe
14 |
15 | Being a mom isn't easy. 16 | Lots to do and no time to type! Reply to a family member's incoming message by clicking on their face, and choosing the appropriate emoji response. 17 |
18 |
19 |
20 |
21 |
by the team
22 |
23 |
24 | 25 | 26 | 27 | 28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 |
57 | 58 | 59 | 60 | 61 |
62 |
63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /examples/house/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const SHAKE_MESSAGE_TIME = 15000; 4 | 5 | let currCharacterSelected; 6 | let responsePromisesMap = {}; 7 | 8 | ['brother_button', 'baby_button', 'sister_button', 'father_button'].forEach((characterBtnId) => { 9 | document.getElementById(characterBtnId).onclick = (e) => { 10 | let characterName = characterBtnId.split('_')[0]; 11 | 12 | // Deselect all first. 13 | let characters = document.querySelectorAll('.character'); 14 | characters.forEach(c => c.classList.remove('selected')); 15 | 16 | e.target.classList.add('selected'); 17 | currCharacterSelected = characterName; 18 | }; 19 | }); 20 | 21 | document.querySelectorAll('.emoji').forEach((emoji) => { 22 | emoji.addEventListener('click', () => { 23 | if (currCharacterSelected === 'baby') { 24 | responsePromisesMap.baby.resolve(emoji.innerText); 25 | } else if (currCharacterSelected === 'brother') { 26 | responsePromisesMap.brother.resolve(emoji.innerText); 27 | } else if (currCharacterSelected === 'father') { 28 | responsePromisesMap.father.resolve(emoji.innerText); 29 | } else if (currCharacterSelected === 'sister') { 30 | responsePromisesMap.sister.resolve(emoji.innerText); 31 | } 32 | }); 33 | }); 34 | 35 | function initIframily(id) { 36 | responsePromisesMap[id] = { 37 | resolve: () => {}, 38 | reject: () => {} 39 | }; 40 | 41 | window.Iframily.initParent(id, window.location.origin, (msg) => { 42 | if (msg === 'timeout') { 43 | responsePromisesMap[id].reject(); 44 | setIsMessageWaiting(id, false); 45 | } else { 46 | setIsMessageWaiting(id, true); 47 | 48 | return new Promise((resolve, reject) => { 49 | responsePromisesMap[id].resolve = resolve; 50 | responsePromisesMap[id].reject = reject; 51 | }).finally(() => { 52 | setIsMessageWaiting(id, false); 53 | }); 54 | } 55 | }); 56 | } 57 | 58 | function setIsMessageWaiting(character, status) { 59 | let buttonEl = document.getElementById(`${character}_button`); 60 | 61 | if (status) { 62 | buttonEl.classList.add('message_waiting'); 63 | setTimeout(() => buttonEl.classList.add('shake'), SHAKE_MESSAGE_TIME); 64 | } else { 65 | buttonEl.classList.remove('message_waiting'); 66 | buttonEl.classList.remove('shake'); 67 | } 68 | } 69 | 70 | initIframily('baby'); 71 | initIframily('brother'); 72 | initIframily('father'); 73 | initIframily('sister'); 74 | -------------------------------------------------------------------------------- /examples/house/rooms/baby.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
baby.html
11 |
    12 |
  • 13 |
    14 | 15 | 16 | 17 |
    18 |
    19 |
  • 20 |
21 | 22 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /examples/house/rooms/brother.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
brother.html
11 |
    12 |
  • 13 |
    14 | 15 | 16 | 17 |
    18 |
    19 |
  • 20 |
21 | 22 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /examples/house/rooms/father.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
father.html
11 |
    12 |
  • 13 |
    14 | 15 | 16 | 17 |
    18 |
    19 |
  • 20 |
21 | 22 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /examples/house/rooms/frame.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const START_DELAY = (Math.random() * (12000 - 3000)) + 3000; 4 | const QUESTION_RANDOM_TIME_MIN = 3000; 5 | const QUESTION_RANDOM_TIME_MAX = 30000; 6 | const QUESTION_TIMEOUT = 20000; 7 | const RESPONSE_OF_THE_RESPONSE_DELAY = 1000; 8 | 9 | // Keep original questions and a clone to mutate on. 10 | let origQuestions = Object.keys(window.dialogue); 11 | let questionsPool = Array.from(origQuestions); 12 | 13 | let prevQuestion; 14 | 15 | function getRandomQuestion() { 16 | let randQuestionIndex = Math.floor(Math.random() * questionsPool.length); 17 | if (prevQuestion && questionsPool[randQuestionIndex] === prevQuestion) { 18 | return getRandomQuestion(); 19 | } 20 | 21 | return questionsPool.splice(randQuestionIndex, 1)[0]; 22 | } 23 | 24 | function autoScroll() { 25 | let dialogEl = document.querySelector('#dialog'); 26 | let latestNewMessage = [...dialogEl.querySelectorAll('.new')].pop(); 27 | 28 | dialogEl.scrollTo({ 29 | top: latestNewMessage.offsetTop + 4, 30 | behavior: 'smooth', 31 | }); 32 | } 33 | 34 | function addMessage(text, showDots, type) { 35 | return new Promise((resolve) => { 36 | let newMessageEl = document.querySelector('.message.template').cloneNode(true); 37 | newMessageEl.classList.remove('template'); 38 | let contentEl = newMessageEl.querySelector('.content'); 39 | contentEl.innerText = text; 40 | if (type) { 41 | newMessageEl.classList.add(type); 42 | } 43 | 44 | document.querySelector('#dialog').appendChild(newMessageEl); 45 | 46 | if (showDots) { 47 | newMessageEl.classList.add('loading'); 48 | setTimeout(() => { 49 | newMessageEl.classList.remove('loading'); 50 | autoScroll(); 51 | resolve(); 52 | }, Math.max(1000, text.length * 100)); 53 | } else { 54 | resolve(); 55 | } 56 | }); 57 | } 58 | 59 | function askQuestionInRandomTime(randTime) { 60 | // Generate random time if not given. 61 | if (typeof randTime !== 'number') { 62 | randTime = (Math.random() * (QUESTION_RANDOM_TIME_MAX - QUESTION_RANDOM_TIME_MIN)) + QUESTION_RANDOM_TIME_MIN; 63 | } 64 | 65 | setTimeout(() => { 66 | let currQuestion = getRandomQuestion(); 67 | prevQuestion = currQuestion; 68 | 69 | addMessage(currQuestion, true, 'new') 70 | .then(() => { 71 | let timeoutID; 72 | window.frameIframily.sendMessage(currQuestion) 73 | .then((resp) => { 74 | clearTimeout(timeoutID); 75 | 76 | addMessage(resp, false, 'sent'); 77 | 78 | let responses = window.dialogue[currQuestion]; 79 | let responseOfTheResponse = responses[resp] || responses.default; 80 | 81 | return new Promise((resolve) => { 82 | setTimeout(() => { 83 | addMessage(responseOfTheResponse, true); 84 | resolve(); 85 | }, RESPONSE_OF_THE_RESPONSE_DELAY); 86 | }); 87 | }) 88 | .catch(() => { 89 | let responses = window.dialogue[currQuestion]; 90 | let responseOfTheResponse = responses.timeout; 91 | return addMessage(responseOfTheResponse, true); 92 | }) 93 | .finally(() => { 94 | if (questionsPool.length === 0) { 95 | questionsPool = Array.from(origQuestions); 96 | } 97 | 98 | askQuestionInRandomTime(); 99 | }); 100 | 101 | timeoutID = setTimeout(() => { 102 | window.frameIframily.sendMessage('timeout'); 103 | }, QUESTION_TIMEOUT); 104 | }); 105 | }, randTime); 106 | } 107 | 108 | 109 | // Wait a bit before starting so it's not overwhelming. 110 | setTimeout(()=> { 111 | askQuestionInRandomTime(0); 112 | }, START_DELAY); 113 | 114 | 115 | // Disable scrolling 116 | document.querySelector("#dialog").addEventListener("wheel", e=>{ 117 | e.preventDefault(); 118 | return false; 119 | }); 120 | 121 | document.body.ontouchend = (e) => { 122 | e.preventDefault(); 123 | }; 124 | -------------------------------------------------------------------------------- /examples/house/rooms/sister.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
sister.html
> 11 |
    12 |
  • 13 |
    14 | 15 | 16 | 17 |
    18 |
    19 |
  • 20 |
21 | 22 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /examples/house/style/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Raleway&display=swap"); 2 | html { 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; } 6 | 7 | body { 8 | width: 100%; 9 | height: 100%; 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | background: linear-gradient(to bottom, #a2b2c3 0%, #a2b2c3 50%, #a6b8a0 50%, #a6b8a0 100%); 14 | margin: 0; 15 | padding: 0; 16 | font-family: 'Open Sans', sans-serif; } 17 | 18 | .instructionsContainer { 19 | position: absolute; 20 | top: 180px; 21 | left: 0; 22 | width: 23%; 23 | display: flex; 24 | flex-direction: column; 25 | align-items: center; } 26 | 27 | @media only screen and (max-height: 1000px) { 28 | .instructionsContainer { 29 | top: 0; } } 30 | 31 | .label { 32 | position: absolute; 33 | top: 0px; 34 | left: 0; 35 | background: #e6e6e6; 36 | font-size: 14px; 37 | padding: 4px 6px; 38 | font-weight: bold; 39 | font-family: arial; 40 | z-index: 100; } 41 | 42 | .logo { 43 | display: block; 44 | width: 133px; 45 | height: 133px; 46 | z-index: 10; 47 | margin-top: 30px; } 48 | 49 | .logo img { 50 | width: 100%; 51 | height: 100%; 52 | object-fit: contain; } 53 | 54 | .description { 55 | z-index: 10; 56 | color: black; 57 | font-size: 14px; 58 | text-align: center; 59 | width: 100%; 60 | font-family: 'Raleway', sans-serif; } 61 | 62 | .instructions { 63 | margin-top: 20px; 64 | padding: 15px; 65 | z-index: 10; 66 | color: black; 67 | font-size: 16px; } 68 | 69 | @media only screen and (max-height: 800px) { 70 | .instructions { 71 | width: 190px; 72 | font-size: 14px; 73 | align-self: flex-start; } } 74 | 75 | .instructions em { 76 | display: block; 77 | font-size: 24px; 78 | margin-bottom: 10px; } 79 | 80 | .shoutout { 81 | position: absolute; 82 | z-index: 10; 83 | left: 25px; 84 | bottom: 0px; 85 | width: 500px; 86 | color: white; 87 | font-size: 14px; } 88 | 89 | .shoutout a.ekologo { 90 | position: relative; 91 | top: 4px; 92 | color: rgba(255, 255, 255, 0); 93 | font-size: 30px; 94 | background-size: 100%; 95 | background-repeat: no-repeat; 96 | background-image: url(../images/ekoengineering_logo.svg); 97 | margin: 0 6px; } 98 | 99 | .shoutout a.ekologo span { 100 | padding-left: 5px; 101 | font-size: 16px; } 102 | 103 | #aspect_ratio_box { 104 | height: 0; 105 | overflow: hidden; 106 | padding-bottom: 58.59408%; 107 | width: 100%; 108 | position: relative; } 109 | 110 | @media only screen and (max-height: 1000px) { 111 | #aspect_ratio_box { 112 | top: -114px; } } 113 | 114 | #container { 115 | position: absolute; 116 | top: 0; 117 | left: 0; 118 | width: 100%; 119 | height: 100%; 120 | background: url(../images/background.png); 121 | background-size: 100% 100%; 122 | background-repeat: no-repeat; } 123 | 124 | #phone { 125 | position: relative; 126 | width: 16.2%; 127 | height: 43.4%; 128 | left: 25.6%; 129 | top: 44.9%; 130 | display: flex; 131 | flex-direction: column; } 132 | 133 | #character_select { 134 | display: grid; 135 | grid-template: repeat(2, 1fr)/repeat(2, 1fr); 136 | height: 60%; 137 | background: #22b573; } 138 | 139 | #character_select button { 140 | position: relative; 141 | background-size: cover; 142 | background-position: center; 143 | background-repeat: no-repeat; 144 | font-size: 0; 145 | outline: none; 146 | transition: all 0.06s ease-in; } 147 | 148 | #character_select button:hover { 149 | filter: brightness(1.2); } 150 | 151 | #character_select button:after { 152 | content: "!"; 153 | position: absolute; 154 | top: 5px; 155 | right: 10px; 156 | background: #ff0000; 157 | box-shadow: rgba(0, 0, 0, 0.4) 5px 5px 5px; 158 | border-radius: 5px; 159 | border-top-left-radius: 0; 160 | width: 20px; 161 | height: 14px; 162 | transition: all 0.3s ease-in; 163 | opacity: 0; 164 | pointer-events: none; 165 | font-size: 12px; 166 | color: white; } 167 | 168 | #character_select button.shake:after { 169 | animation-name: shake-little; 170 | animation-duration: 100ms; 171 | animation-timing-function: ease-in-out; 172 | animation-iteration-count: infinite; } 173 | 174 | #character_select button.selected { 175 | filter: brightness(1.5); } 176 | 177 | #character_select button.message_waiting:after { 178 | opacity: 1; } 179 | 180 | #brother_button { 181 | background-image: url("../images/brother_button.png"); } 182 | 183 | #sister_button { 184 | background-image: url("../images/sister_button.png"); } 185 | 186 | #father_button { 187 | background-image: url("../images/father_button.png"); } 188 | 189 | #baby_button { 190 | background-image: url("../images/baby_button.png"); } 191 | 192 | #emoji_keyboard { 193 | display: grid; 194 | flex: 1; 195 | grid-template: repeat(5, 1fr)/repeat(5, 1fr); } 196 | 197 | #emoji_keyboard button { 198 | font-size: 14px; } 199 | 200 | @media only screen and (min-width: 1200px) { 201 | #emoji_keyboard button { 202 | font-size: 18px; } } 203 | 204 | button { 205 | border: 0; 206 | background: transparent; 207 | cursor: pointer; } 208 | 209 | iframe { 210 | position: absolute; 211 | width: 24.4%; 212 | height: 31.9%; } 213 | 214 | #brother_frame { 215 | left: 45.8%; 216 | top: 29.5%; } 217 | 218 | #baby_frame { 219 | left: 71.4%; 220 | top: 29.5%; } 221 | 222 | #sister_frame { 223 | left: 45.8%; 224 | top: 63.9%; } 225 | 226 | #father_frame { 227 | left: 71.4%; 228 | top: 63.9%; } 229 | 230 | @keyframes shake-little { 231 | 2% { 232 | transform: translate(0px, 0px) rotate(0.5deg); } 233 | 4% { 234 | transform: translate(0px, 1px) rotate(0.5deg); } 235 | 6% { 236 | transform: translate(1px, 0px) rotate(0.5deg); } 237 | 8% { 238 | transform: translate(1px, 1px) rotate(0.5deg); } 239 | 10% { 240 | transform: translate(0px, 1px) rotate(0.5deg); } 241 | 12% { 242 | transform: translate(1px, 0px) rotate(0.5deg); } 243 | 14% { 244 | transform: translate(1px, 1px) rotate(0.5deg); } 245 | 16% { 246 | transform: translate(0px, 1px) rotate(0.5deg); } 247 | 18% { 248 | transform: translate(1px, 0px) rotate(0.5deg); } 249 | 20% { 250 | transform: translate(1px, 0px) rotate(0.5deg); } 251 | 22% { 252 | transform: translate(0px, 1px) rotate(0.5deg); } 253 | 24% { 254 | transform: translate(0px, 0px) rotate(0.5deg); } 255 | 26% { 256 | transform: translate(0px, 1px) rotate(0.5deg); } 257 | 28% { 258 | transform: translate(0px, 1px) rotate(0.5deg); } 259 | 30% { 260 | transform: translate(1px, 1px) rotate(0.5deg); } 261 | 32% { 262 | transform: translate(0px, 1px) rotate(0.5deg); } 263 | 34% { 264 | transform: translate(1px, 0px) rotate(0.5deg); } 265 | 36% { 266 | transform: translate(1px, 0px) rotate(0.5deg); } 267 | 38% { 268 | transform: translate(0px, 1px) rotate(0.5deg); } 269 | 40% { 270 | transform: translate(0px, 1px) rotate(0.5deg); } 271 | 42% { 272 | transform: translate(0px, 1px) rotate(0.5deg); } 273 | 44% { 274 | transform: translate(0px, 1px) rotate(0.5deg); } 275 | 46% { 276 | transform: translate(0px, 0px) rotate(0.5deg); } 277 | 48% { 278 | transform: translate(1px, 0px) rotate(0.5deg); } 279 | 50% { 280 | transform: translate(1px, 1px) rotate(0.5deg); } 281 | 52% { 282 | transform: translate(1px, 0px) rotate(0.5deg); } 283 | 54% { 284 | transform: translate(1px, 1px) rotate(0.5deg); } 285 | 56% { 286 | transform: translate(0px, 0px) rotate(0.5deg); } 287 | 58% { 288 | transform: translate(1px, 0px) rotate(0.5deg); } 289 | 60% { 290 | transform: translate(1px, 1px) rotate(0.5deg); } 291 | 62% { 292 | transform: translate(0px, 0px) rotate(0.5deg); } 293 | 64% { 294 | transform: translate(1px, 1px) rotate(0.5deg); } 295 | 66% { 296 | transform: translate(0px, 1px) rotate(0.5deg); } 297 | 68% { 298 | transform: translate(1px, 1px) rotate(0.5deg); } 299 | 70% { 300 | transform: translate(0px, 1px) rotate(0.5deg); } 301 | 72% { 302 | transform: translate(1px, 0px) rotate(0.5deg); } 303 | 74% { 304 | transform: translate(0px, 0px) rotate(0.5deg); } 305 | 76% { 306 | transform: translate(1px, 0px) rotate(0.5deg); } 307 | 78% { 308 | transform: translate(0px, 0px) rotate(0.5deg); } 309 | 80% { 310 | transform: translate(1px, 1px) rotate(0.5deg); } 311 | 82% { 312 | transform: translate(0px, 0px) rotate(0.5deg); } 313 | 84% { 314 | transform: translate(0px, 1px) rotate(0.5deg); } 315 | 86% { 316 | transform: translate(1px, 0px) rotate(0.5deg); } 317 | 88% { 318 | transform: translate(0px, 0px) rotate(0.5deg); } 319 | 90% { 320 | transform: translate(1px, 1px) rotate(0.5deg); } 321 | 92% { 322 | transform: translate(1px, 0px) rotate(0.5deg); } 323 | 94% { 324 | transform: translate(1px, 1px) rotate(0.5deg); } 325 | 96% { 326 | transform: translate(0px, 1px) rotate(0.5deg); } 327 | 98% { 328 | transform: translate(1px, 0px) rotate(0.5deg); } 329 | 0%, 100% { 330 | transform: translate(0, 0) rotate(0); } } 331 | -------------------------------------------------------------------------------- /examples/house/style/index.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Raleway&display=swap'); 2 | 3 | @mixin bp3{ 4 | @media only screen and (min-width: 1200px){ 5 | @content; 6 | } 7 | } 8 | @mixin shortAndWide{ 9 | @media only screen and (max-height: 1000px){ 10 | @content; 11 | } 12 | } 13 | 14 | html{ 15 | height: 100%; 16 | margin: 0; 17 | padding: 0; 18 | } 19 | 20 | body{ 21 | width: 100%; 22 | height: 100%; 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | background: linear-gradient(to bottom, #a2b2c3 0%, #a2b2c3 50%, #a6b8a0 50%, #a6b8a0 100%); 27 | margin: 0; 28 | padding:0; 29 | font-family: 'Open Sans', sans-serif; 30 | } 31 | 32 | .instructionsContainer{ 33 | position: absolute; 34 | top: 180px; 35 | left: 0; 36 | width: 23%; 37 | display: flex; 38 | flex-direction: column; 39 | align-items: center; 40 | 41 | @include shortAndWide{ 42 | top: 0; 43 | } 44 | } 45 | 46 | .label{ 47 | position: absolute; 48 | top: 0px; 49 | left:0; 50 | background: #e6e6e6; 51 | font-size: 14px; 52 | padding: 4px 6px; 53 | font-weight: bold; 54 | font-family: arial; 55 | z-index: 100; 56 | } 57 | 58 | .logo { 59 | display: block; 60 | width: 133px; 61 | height: 133px; 62 | z-index: 10; 63 | margin-top: 30px; 64 | 65 | img { 66 | width: 100%; 67 | height: 100%; 68 | object-fit: contain; 69 | } 70 | } 71 | 72 | 73 | 74 | .description{ 75 | z-index: 10; 76 | color: black; 77 | font-size: 14px; 78 | text-align: center; 79 | width: 100%; 80 | font-family: 'Raleway', sans-serif; 81 | } 82 | 83 | .instructions{ 84 | margin-top: 20px; 85 | padding: 15px; 86 | z-index: 10; 87 | color: black; 88 | font-size: 16px; 89 | 90 | 91 | @media only screen and (max-height: 800px){ 92 | width: 190px; 93 | font-size: 14px; 94 | align-self: flex-start; 95 | } 96 | 97 | em{ 98 | display: block; 99 | font-size: 24px; 100 | margin-bottom: 10px; 101 | } 102 | 103 | } 104 | 105 | .shoutout{ 106 | position: absolute; 107 | z-index: 10; 108 | left: 25px; 109 | bottom: 0px; 110 | width: 500px; 111 | color: white; 112 | font-size: 14px; 113 | } 114 | 115 | .shoutout a.ekologo { 116 | position: relative; 117 | top: 4px; 118 | color: hsla(0,0%,100%,0); 119 | font-size: 30px; 120 | background-size: 100%; 121 | background-repeat: no-repeat; 122 | background-image: url(../images/ekoengineering_logo.svg); 123 | margin: 0 6px; 124 | } 125 | 126 | .shoutout a.ekologo span { 127 | padding-left: 5px; 128 | font-size: 16px; 129 | } 130 | 131 | #aspect_ratio_box{ 132 | height: 0; 133 | overflow: hidden; 134 | padding-bottom: 1367px / 2333px * 100%; 135 | width: 100%; 136 | position: relative; 137 | 138 | @include shortAndWide{ 139 | top: -114px; 140 | } 141 | } 142 | 143 | #container{ 144 | position: absolute; 145 | top: 0; 146 | left: 0; 147 | width: 100%; 148 | height: 100%; 149 | background: url(../images/background.png); 150 | background-size: 100% 100%; 151 | background-repeat: no-repeat; 152 | } 153 | 154 | #phone{ 155 | position: relative; 156 | width: 16.2%; 157 | height: 43.4%; 158 | left: 25.6%; 159 | top: 44.9%; 160 | display:flex; 161 | flex-direction: column; 162 | } 163 | 164 | #character_select{ 165 | display: grid; 166 | grid-template: repeat(2, 1fr) / repeat(2, 1fr); 167 | height: 60%; 168 | background: #22b573; 169 | } 170 | 171 | #character_select button{ 172 | position: relative; 173 | background-size: cover; 174 | background-position: center; 175 | background-repeat: no-repeat; 176 | font-size: 0; 177 | 178 | outline: none; 179 | transition: all 0.06s ease-in; 180 | 181 | &:hover{ 182 | filter: brightness(1.2); 183 | } 184 | &:after{ 185 | content: "!"; 186 | position: absolute; 187 | top: 5px; 188 | right: 10px; 189 | background: #ff0000; 190 | box-shadow: rgba(0,0,0,0.4) 5px 5px 5px; 191 | border-radius: 5px; 192 | border-top-left-radius: 0; 193 | width: 20px; 194 | height: 14px; 195 | transition: all 0.3s ease-in; 196 | opacity: 0; 197 | pointer-events: none; 198 | font-size: 12px; 199 | color: white; 200 | } 201 | 202 | &.shake:after{ 203 | animation-name: shake-little; 204 | animation-duration: 100ms; 205 | animation-timing-function: ease-in-out; 206 | animation-iteration-count: infinite; 207 | } 208 | 209 | 210 | &.selected{ 211 | filter: brightness(1.5); 212 | } 213 | 214 | &.message_waiting:after{ 215 | opacity: 1; 216 | } 217 | } 218 | 219 | #brother_button{ 220 | background-image: url('../images/brother_button.png'); 221 | } 222 | 223 | #sister_button{ 224 | background-image: url('../images/sister_button.png'); 225 | } 226 | 227 | #father_button{ 228 | background-image: url('../images/father_button.png'); 229 | } 230 | 231 | #baby_button{ 232 | background-image: url('../images/baby_button.png'); 233 | } 234 | 235 | #emoji_keyboard{ 236 | display: grid; 237 | flex: 1; 238 | grid-template: repeat(5, 1fr) / repeat(5, 1fr); 239 | button { 240 | font-size: 14px; 241 | @include bp3{ 242 | font-size: 18px; 243 | } 244 | } 245 | } 246 | 247 | button{ 248 | border: 0; 249 | background: transparent; 250 | cursor: pointer; 251 | } 252 | 253 | iframe{ 254 | position: absolute; 255 | width: 24.4%; 256 | height: 31.9%; 257 | } 258 | 259 | #brother_frame { 260 | left: 45.8%; 261 | top: 29.5%; 262 | } 263 | 264 | #baby_frame { 265 | left: 71.4%; 266 | top: 29.5%; 267 | } 268 | 269 | #sister_frame { 270 | left: 45.8%; 271 | top: 63.9%; 272 | } 273 | 274 | #father_frame { 275 | left: 71.4%; 276 | top: 63.9%; 277 | } 278 | 279 | @keyframes shake-little { 280 | 2% { 281 | transform: translate(0px, 0px) rotate(0.5deg); } 282 | 4% { 283 | transform: translate(0px, 1px) rotate(0.5deg); } 284 | 6% { 285 | transform: translate(1px, 0px) rotate(0.5deg); } 286 | 8% { 287 | transform: translate(1px, 1px) rotate(0.5deg); } 288 | 10% { 289 | transform: translate(0px, 1px) rotate(0.5deg); } 290 | 12% { 291 | transform: translate(1px, 0px) rotate(0.5deg); } 292 | 14% { 293 | transform: translate(1px, 1px) rotate(0.5deg); } 294 | 16% { 295 | transform: translate(0px, 1px) rotate(0.5deg); } 296 | 18% { 297 | transform: translate(1px, 0px) rotate(0.5deg); } 298 | 20% { 299 | transform: translate(1px, 0px) rotate(0.5deg); } 300 | 22% { 301 | transform: translate(0px, 1px) rotate(0.5deg); } 302 | 24% { 303 | transform: translate(0px, 0px) rotate(0.5deg); } 304 | 26% { 305 | transform: translate(0px, 1px) rotate(0.5deg); } 306 | 28% { 307 | transform: translate(0px, 1px) rotate(0.5deg); } 308 | 30% { 309 | transform: translate(1px, 1px) rotate(0.5deg); } 310 | 32% { 311 | transform: translate(0px, 1px) rotate(0.5deg); } 312 | 34% { 313 | transform: translate(1px, 0px) rotate(0.5deg); } 314 | 36% { 315 | transform: translate(1px, 0px) rotate(0.5deg); } 316 | 38% { 317 | transform: translate(0px, 1px) rotate(0.5deg); } 318 | 40% { 319 | transform: translate(0px, 1px) rotate(0.5deg); } 320 | 42% { 321 | transform: translate(0px, 1px) rotate(0.5deg); } 322 | 44% { 323 | transform: translate(0px, 1px) rotate(0.5deg); } 324 | 46% { 325 | transform: translate(0px, 0px) rotate(0.5deg); } 326 | 48% { 327 | transform: translate(1px, 0px) rotate(0.5deg); } 328 | 50% { 329 | transform: translate(1px, 1px) rotate(0.5deg); } 330 | 52% { 331 | transform: translate(1px, 0px) rotate(0.5deg); } 332 | 54% { 333 | transform: translate(1px, 1px) rotate(0.5deg); } 334 | 56% { 335 | transform: translate(0px, 0px) rotate(0.5deg); } 336 | 58% { 337 | transform: translate(1px, 0px) rotate(0.5deg); } 338 | 60% { 339 | transform: translate(1px, 1px) rotate(0.5deg); } 340 | 62% { 341 | transform: translate(0px, 0px) rotate(0.5deg); } 342 | 64% { 343 | transform: translate(1px, 1px) rotate(0.5deg); } 344 | 66% { 345 | transform: translate(0px, 1px) rotate(0.5deg); } 346 | 68% { 347 | transform: translate(1px, 1px) rotate(0.5deg); } 348 | 70% { 349 | transform: translate(0px, 1px) rotate(0.5deg); } 350 | 72% { 351 | transform: translate(1px, 0px) rotate(0.5deg); } 352 | 74% { 353 | transform: translate(0px, 0px) rotate(0.5deg); } 354 | 76% { 355 | transform: translate(1px, 0px) rotate(0.5deg); } 356 | 78% { 357 | transform: translate(0px, 0px) rotate(0.5deg); } 358 | 80% { 359 | transform: translate(1px, 1px) rotate(0.5deg); } 360 | 82% { 361 | transform: translate(0px, 0px) rotate(0.5deg); } 362 | 84% { 363 | transform: translate(0px, 1px) rotate(0.5deg); } 364 | 86% { 365 | transform: translate(1px, 0px) rotate(0.5deg); } 366 | 88% { 367 | transform: translate(0px, 0px) rotate(0.5deg); } 368 | 90% { 369 | transform: translate(1px, 1px) rotate(0.5deg); } 370 | 92% { 371 | transform: translate(1px, 0px) rotate(0.5deg); } 372 | 94% { 373 | transform: translate(1px, 1px) rotate(0.5deg); } 374 | 96% { 375 | transform: translate(0px, 1px) rotate(0.5deg); } 376 | 98% { 377 | transform: translate(1px, 0px) rotate(0.5deg); } 378 | 0%, 100% { 379 | transform: translate(0, 0) rotate(0); } } 380 | -------------------------------------------------------------------------------- /examples/house/style/room.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Open+Sans&display=swap"); 2 | html { 3 | height: 100%; 4 | padding: 0; 5 | margin: 0; } 6 | 7 | body { 8 | width: 100%; 9 | height: 100%; 10 | padding: 0; 11 | margin: 0; 12 | background-size: cover; } 13 | 14 | body.brother { 15 | background-image: url(../images/brother_room.png); 16 | --highlight-color: #cd79d1; } 17 | 18 | body.baby { 19 | background-image: url(../images/baby_room.png); 20 | --highlight-color: #52bf5a; } 21 | 22 | body.sister { 23 | background-image: url(../images/sister_room.png); 24 | --highlight-color: #fca147; } 25 | 26 | body.father { 27 | background-image: url(../images/father_room.png); 28 | --highlight-color: #72d3ca; } 29 | 30 | .label { 31 | position: absolute; 32 | top: 0; 33 | left: 0; 34 | background: #e6e6e6; 35 | font-size: 14px; 36 | padding: 4px 6px; 37 | font-weight: bold; 38 | font-family: arial; } 39 | 40 | ul { 41 | position: absolute; 42 | height: 100%; 43 | width: 50%; 44 | top: 0; 45 | right: 0; 46 | background: rgba(255, 255, 255, 0.6); 47 | list-style-type: none; 48 | margin: 0; 49 | padding: 0; 50 | display: flex; 51 | flex-direction: column; 52 | font-size: 15px; 53 | overflow-y: scroll; 54 | touch-action: none; 55 | scroll-behavior: smooth; 56 | scrollbar-width: none; 57 | /* Firefox */ 58 | -ms-overflow-style: none; 59 | /* IE 10+ */ } 60 | 61 | ul::-webkit-scrollbar { 62 | width: 0px; 63 | background: transparent; 64 | /* Chrome/Safari/Webkit */ } 65 | 66 | li { 67 | box-sizing: border-box; 68 | margin: 10px; 69 | box-shadow: black 5px 5px 5px; 70 | border-radius: 5px; 71 | border-top-left-radius: 0; 72 | padding: 6px; 73 | background: var(--highlight-color); 74 | max-width: 70%; 75 | font-family: 'Open Sans', sans-serif; 76 | scroll-margin: 10px; } 77 | 78 | li.sent { 79 | background: white; 80 | border-radius: 5px; 81 | border-top-right-radius: 0; 82 | align-self: flex-end; } 83 | 84 | li:last-child { 85 | margin-bottom: 1000px; } 86 | 87 | .message { 88 | animation: showBubble 0.3s ease-out; } 89 | 90 | .message.template { 91 | display: none; } 92 | 93 | .message .dots { 94 | display: none; } 95 | 96 | .message.loading .content { 97 | display: none; } 98 | 99 | .message.loading .dots { 100 | display: flex; } 101 | 102 | .dots { 103 | align-items: center; 104 | display: flex; 105 | height: 17px; } 106 | 107 | .dots span { 108 | background-color: white; 109 | animation: mercuryTypingAnimation 1.5s infinite ease-in-out; 110 | border-radius: 2px; 111 | display: inline-block; 112 | height: 4px; 113 | margin-right: 2px; 114 | width: 4px; } 115 | 116 | .dots span:nth-child(1) { 117 | nimation-delay: 200ms; } 118 | 119 | .dots span:nth-child(2) { 120 | animation-delay: 300ms; } 121 | 122 | .dots span:nth-child(3) { 123 | animation-delay: 400ms; } 124 | 125 | @keyframes showBubble { 126 | 0% { 127 | transform: translateY(30px); 128 | opacity: 0; } 129 | 100% { 130 | transform: translateY(0px); 131 | opacity: 1; } } 132 | 133 | @keyframes mercuryTypingAnimation { 134 | 0% { 135 | transform: translateY(0px); } 136 | 28% { 137 | transform: translateY(-5px); } 138 | 44% { 139 | transform: translateY(0px); } } 140 | -------------------------------------------------------------------------------- /examples/house/style/room.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Open+Sans&display=swap'); 2 | 3 | html{ 4 | height: 100%; 5 | padding: 0; 6 | margin: 0; 7 | } 8 | 9 | body{ 10 | width: 100%; 11 | height: 100%; 12 | padding: 0; 13 | margin: 0; 14 | background-size: cover; 15 | } 16 | 17 | body.brother{ 18 | background-image: url(../images/brother_room.png); 19 | --highlight-color: #cd79d1; 20 | } 21 | 22 | body.baby{ 23 | background-image: url(../images/baby_room.png); 24 | --highlight-color: #52bf5a; 25 | } 26 | 27 | body.sister{ 28 | background-image: url(../images/sister_room.png); 29 | --highlight-color: #fca147; 30 | } 31 | 32 | body.father{ 33 | background-image: url(../images/father_room.png); 34 | --highlight-color: #72d3ca; 35 | } 36 | 37 | .label{ 38 | position: absolute; 39 | top: 0; 40 | left:0; 41 | background: #e6e6e6; 42 | font-size: 14px; 43 | padding: 4px 6px; 44 | font-weight: bold; 45 | font-family: arial; 46 | } 47 | 48 | ul{ 49 | position: absolute; 50 | height: 100%; 51 | width: 50%; 52 | top: 0; 53 | right: 0; 54 | background: rgba(255,255,255,0.6); 55 | list-style-type: none; 56 | margin:0 ; 57 | padding:0 ; 58 | display: flex; 59 | flex-direction: column; 60 | font-size: 15px; 61 | overflow-y: scroll; 62 | touch-action: none; 63 | scroll-behavior: smooth; 64 | 65 | scrollbar-width: none; /* Firefox */ 66 | -ms-overflow-style: none; /* IE 10+ */ 67 | &::-webkit-scrollbar { 68 | width: 0px; 69 | background: transparent; /* Chrome/Safari/Webkit */ 70 | } 71 | 72 | } 73 | 74 | li{ 75 | box-sizing: border-box; 76 | margin: 10px; 77 | box-shadow: black 5px 5px 5px; 78 | border-radius: 5px; 79 | border-top-left-radius: 0; 80 | padding: 6px; 81 | background: var(--highlight-color); 82 | max-width: 70%; 83 | font-family: 'Open Sans', sans-serif; 84 | scroll-margin: 10px; 85 | 86 | &.sent { 87 | background: white; 88 | border-radius: 5px; 89 | border-top-right-radius: 0; 90 | align-self: flex-end; 91 | } 92 | 93 | &:last-child{ 94 | margin-bottom: 1000px; 95 | } 96 | } 97 | 98 | 99 | 100 | .message{ 101 | animation: showBubble 0.3s ease-out; 102 | } 103 | 104 | .message.template{ 105 | display: none; 106 | } 107 | 108 | .message .dots{ 109 | display: none; 110 | } 111 | 112 | .message.loading { 113 | .content{ 114 | display: none; 115 | } 116 | 117 | .dots{ 118 | display: flex; 119 | } 120 | } 121 | 122 | .dots { 123 | align-items: center; 124 | display: flex; 125 | height: 17px; 126 | 127 | span { 128 | background-color: white; 129 | animation: mercuryTypingAnimation 1.5s infinite ease-in-out; 130 | border-radius: 2px; 131 | display: inline-block; 132 | height: 4px; 133 | margin-right: 2px; 134 | width: 4px; 135 | 136 | &:nth-child(1){ 137 | nimation-delay:200ms; 138 | } 139 | &:nth-child(2){ 140 | animation-delay:300ms; 141 | } 142 | &:nth-child(3){ 143 | animation-delay:400ms; 144 | } 145 | } 146 | } 147 | 148 | 149 | 150 | @keyframes showBubble{ 151 | 0%{ 152 | transform: translateY(30px); 153 | opacity: 0; 154 | } 155 | 100%{ 156 | transform: translateY(0px); 157 | opacity: 1; 158 | } 159 | 160 | } 161 | 162 | 163 | 164 | @keyframes mercuryTypingAnimation{ 165 | 0%{ transform:translateY(0px) } 166 | 28%{ transform:translateY(-5px) } 167 | 44%{ transform:translateY(0px) } 168 | } 169 | -------------------------------------------------------------------------------- /examples/itetrisly/game/texture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EkoLabs/iframily/01c190894766764ae24d085c4309b286f8d11fbb/examples/itetrisly/game/texture.jpg -------------------------------------------------------------------------------- /examples/itetrisly/images/arrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EkoLabs/iframily/01c190894766764ae24d085c4309b286f8d11fbb/examples/itetrisly/images/arrows.png -------------------------------------------------------------------------------- /examples/itetrisly/images/ekoengineering_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 36 | 40 | 42 | 44 | 47 | 50 | 52 | 54 | 56 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /examples/itetrisly/images/game_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EkoLabs/iframily/01c190894766764ae24d085c4309b286f8d11fbb/examples/itetrisly/images/game_splash.png -------------------------------------------------------------------------------- /examples/itetrisly/images/iframily_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EkoLabs/iframily/01c190894766764ae24d085c4309b286f8d11fbb/examples/itetrisly/images/iframily_logo.png -------------------------------------------------------------------------------- /examples/itetrisly/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | iframily itetrisly 6 | 7 | 8 | 9 | 10 |
11 | 12 |
postMessage made simple & safe
13 |
hit space to start
14 |
15 | 16 | 17 |
18 | 19 | 20 | 21 |
22 |
23 | 33 |
34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/itetrisly/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // ------------------------------------------------ 4 | // CONSTANTS 5 | // ------------------------------------------------ 6 | 7 | const BOARD_X_SIZE = 10; 8 | const PIECE_GRID_SIZE = 4; 9 | const NUMBER_OF_BOTTOM_BOARDS = 3; 10 | 11 | // ------------------------------------------------ 12 | // VARIABLES 13 | // ------------------------------------------------ 14 | 15 | let parentTopIframily; 16 | let parentBottomIframiliesArr = []; 17 | let currPosX = 0; 18 | let translateValueX = 0; 19 | let doneMessagesCounter = 0; 20 | let bottomContainerEl = document.querySelector('#bottomContainer'); 21 | let gameMessageEl = document.querySelector('#gameMessage'); 22 | let gamePendingStart = true; 23 | let shouldReloadOnStart = false; 24 | 25 | // ------------------------------------------------ 26 | // METHODS 27 | // ------------------------------------------------ 28 | 29 | function initFramilies() { 30 | // Dispose previous iframilies (important when restarting the game). 31 | if (parentTopIframily) { 32 | parentTopIframily.dispose(); 33 | } 34 | 35 | parentBottomIframiliesArr.forEach((parentBottomIframily) => { 36 | parentBottomIframily.dispose(); 37 | }); 38 | 39 | // Iframily connection between this frame and the top tetris. 40 | parentTopIframily = window.Iframily.initParent('top', window.location.origin, (msg) => { 41 | if (msg.action === 'drop') { 42 | drop(msg); 43 | } 44 | }); 45 | 46 | // Iframily connection between this frame and the bottom tetrises. 47 | function initParentBottomIframily(id) { 48 | return window.Iframily.initParent(id, window.location.origin, (msg) => { 49 | if (msg.action === 'lose') { 50 | gameMessageEl.innerHTML = 'game overhit space to restart'; 51 | document.body.classList.add("lost"); 52 | document.body.classList.remove("started"); 53 | 54 | gamePendingStart = true; 55 | shouldReloadOnStart = true; 56 | } else if (msg.action === 'done') { 57 | // Only when all expected 'done' messages received, relay the message 58 | // to the top tetris so can start with a new piece. 59 | doneMessagesCounter--; 60 | if (doneMessagesCounter === 0) { 61 | parentTopIframily.sendMessage(msg); 62 | } 63 | } else if (msg.action === 'addScore') { 64 | parentTopIframily.sendMessage(msg); 65 | } 66 | }); 67 | } 68 | 69 | parentBottomIframiliesArr = [ 70 | initParentBottomIframily('bottom-1'), 71 | initParentBottomIframily('bottom-2'), 72 | initParentBottomIframily('bottom-3') 73 | ]; 74 | } 75 | 76 | function move(dir) { 77 | let moveVals = { left: -1, right: 1 }; 78 | let moveVal = moveVals[dir]; 79 | 80 | // Apply new position only if don't reach limit (set as the size of two boards). 81 | let newPosX = currPosX + moveVal; 82 | let twoBoardsSize = BOARD_X_SIZE * 2; 83 | if (newPosX >= -twoBoardsSize && newPosX <= twoBoardsSize) { 84 | currPosX = currPosX + moveVal; 85 | translateValueX = currPosX * (BOARD_X_SIZE * 2); 86 | bottomContainerEl.style.transform = `translate(${translateValueX}px, 0px)`; 87 | } 88 | } 89 | 90 | function drop(msg) { 91 | let current = msg.current; 92 | let origBlocks = current.type.blocks; 93 | let currBlock = origBlocks[current.dir]; 94 | let currAbsX = current.x + BOARD_X_SIZE; 95 | 96 | // eslint-disable-next-line no-unused-vars 97 | let { blocksAbsData, minAbsX, maxAbsX } = getBlocksAbsoluteData(current); 98 | 99 | // Should always start from top y in the bottom frames. 100 | current.y = 0; 101 | 102 | for (let index = 0; index < NUMBER_OF_BOTTOM_BOARDS; index++) { 103 | let boardAbsStartX = currPosX + (index * BOARD_X_SIZE); 104 | let boardAbsEndX = boardAbsStartX + BOARD_X_SIZE; 105 | 106 | if (minAbsX >= boardAbsStartX && maxAbsX < boardAbsEndX) { // Fits entirely in one bottom tetris. 107 | current.x = (current.x + BOARD_X_SIZE) - boardAbsStartX; 108 | 109 | parentBottomIframiliesArr[index].sendMessage(msg); 110 | doneMessagesCounter++; 111 | } else if (boardAbsEndX > minAbsX && boardAbsEndX <= maxAbsX) { // Fits but gets cut off at the right side. 112 | let splitPosition = boardAbsEndX - currAbsX; 113 | 114 | current.x = BOARD_X_SIZE - PIECE_GRID_SIZE; 115 | current.type.blocks[current.dir] = current.type.blocksSplit[currBlock][`splitIn${splitPosition}`].left; 116 | 117 | parentBottomIframiliesArr[index].sendMessage(msg); 118 | doneMessagesCounter++; 119 | } else if (boardAbsStartX >= minAbsX && boardAbsStartX <= maxAbsX) { // Fits but gets cut off at the left side. 120 | let splitPosition = boardAbsStartX - currAbsX; 121 | 122 | current.x = 0; 123 | current.type.blocks[current.dir] = current.type.blocksSplit[currBlock][`splitIn${splitPosition}`].right; 124 | 125 | parentBottomIframiliesArr[index].sendMessage(msg); 126 | doneMessagesCounter++; 127 | } 128 | } 129 | 130 | // Didn't drop to any bottom frame (because nothing was below). 131 | if (doneMessagesCounter === 0) { 132 | parentTopIframily.sendMessage({ action: 'done' }); 133 | } 134 | } 135 | 136 | function getBlocksAbsoluteData(current) { 137 | let blocksAbsPos = []; 138 | let minAbsX = Number.MAX_SAFE_INTEGER; 139 | let maxAbsX = Number.MIN_SAFE_INTEGER; 140 | 141 | eachblock(current.type, current.x, current.y, current.dir, (x, y) => { 142 | let xAbsPos = x + BOARD_X_SIZE; 143 | blocksAbsPos.push({ x: xAbsPos, y: y }); 144 | minAbsX = Math.min(minAbsX, xAbsPos); 145 | maxAbsX = Math.max(maxAbsX, xAbsPos); 146 | }); 147 | 148 | return { 149 | blocksAbsPos, 150 | minAbsX, 151 | maxAbsX 152 | }; 153 | } 154 | 155 | function eachblock(type, x, y, dir, fn) { 156 | // eslint-disable-next-line one-var 157 | let bit, row = 0, col = 0, blocks = type.blocks[dir]; 158 | // eslint-disable-next-line no-magic-numbers, no-bitwise 159 | for (bit = 0x8000; bit > 0; bit = bit >> 1) { 160 | // eslint-disable-next-line no-bitwise 161 | if (blocks & bit) { 162 | fn(x + col, y + row); 163 | } 164 | if (++col === 4) { 165 | col = 0; 166 | ++row; 167 | } 168 | } 169 | } 170 | 171 | // ------------------------------------------------ 172 | // MAIN LOGIC 173 | // ------------------------------------------------ 174 | 175 | initFramilies(); 176 | document.addEventListener('keydown', keydown, false); 177 | 178 | function keydown(e) { 179 | /* eslint-disable no-magic-numbers */ 180 | switch (e.keyCode) { 181 | case 32: // Space 182 | if (gamePendingStart) { 183 | if (shouldReloadOnStart) { 184 | parentTopIframily.sendMessage({ action: 'reload' }); 185 | parentBottomIframiliesArr.forEach((parentBottomIframily) => { 186 | parentBottomIframily.sendMessage({ action: 'reload' }); 187 | }); 188 | 189 | doneMessagesCounter = 0; 190 | initFramilies(); 191 | } 192 | 193 | parentTopIframily.sendMessage({ action: 'start' }); 194 | document.body.classList.remove("ready"); 195 | document.body.classList.remove("lost"); 196 | document.body.classList.add("started"); 197 | gamePendingStart = false; 198 | } 199 | 200 | e.preventDefault(); 201 | break; 202 | 203 | case 39: // Right 204 | move('right'); 205 | 206 | e.preventDefault(); 207 | break; 208 | 209 | case 37: // Left 210 | move('left'); 211 | 212 | e.preventDefault(); 213 | break; 214 | 215 | case 38: // Up 216 | parentTopIframily.sendMessage({ action: 'up' }); 217 | 218 | e.preventDefault(); 219 | break; 220 | 221 | case 40: // Down 222 | parentTopIframily.sendMessage({ action: 'down' }); 223 | parentBottomIframiliesArr.forEach((parentBottomIframily) => { 224 | parentBottomIframily.sendMessage({ action: 'down' }); 225 | }); 226 | 227 | e.preventDefault(); 228 | break; 229 | 230 | default: 231 | break; 232 | } 233 | /* eslint-enable no-magic-numbers */ 234 | } 235 | -------------------------------------------------------------------------------- /examples/itetrisly/style/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Raleway&display=swap"); 2 | html { 3 | width: 100%; 4 | height: 100%; 5 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } 6 | 7 | .label { 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | background: #e6e6e6; 12 | font-size: 12px; 13 | padding: 4px 6px; 14 | font-weight: bold; 15 | font-family: arial; 16 | color: black; } 17 | 18 | body.parent { 19 | height: 100%; 20 | background: black; 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | color: white; } 25 | 26 | body.parent.lost #gameMessage { 27 | color: red; } 28 | 29 | body.parent.started #gameMessage { 30 | display: none; } 31 | 32 | body.parent iframe { 33 | height: 200px; 34 | width: 200px; 35 | border: none; 36 | pointer-events: none; 37 | /* no need for interaction */ } 38 | 39 | body.parent #grid { 40 | display: grid; 41 | grid-template-columns: repeat(3, 200px); 42 | position: relative; 43 | /* center */ 44 | margin: auto; } 45 | 46 | body.parent #grid:before { 47 | content: ""; 48 | background-image: url(../images/game_splash.png); 49 | background-size: cover; 50 | position: absolute; 51 | width: 100%; 52 | height: 100%; } 53 | 54 | body.parent.started #grid:before, body.parent.lost #grid:before { 55 | display: none; } 56 | 57 | body.parent #topIframe { 58 | height: 312px; 59 | grid-row: 1; 60 | grid-column: 2; } 61 | 62 | body.parent #bottomContainer { 63 | width: 600px; 64 | grid-row: 2; 65 | grid-column-start: 1; 66 | grid-column-end: 3; 67 | gap: 0; 68 | display: flex; } 69 | 70 | .container { 71 | position: relative; 72 | min-width: 70vw; 73 | border: 1px solid #22b573; 74 | border-radius: 10px; 75 | display: flex; 76 | flex-direction: column; 77 | padding: 20px; } 78 | 79 | @media only screen and (max-height: 600px) { 80 | .container { 81 | transform: scale(0.75); } } 82 | 83 | .container .logo { 84 | position: absolute; 85 | display: block; 86 | width: 133px; 87 | height: 133px; 88 | left: 20px; 89 | top: 20px; 90 | z-index: 10; } 91 | 92 | .container .logo img { 93 | width: 100%; 94 | height: 100%; 95 | object-fit: contain; } 96 | 97 | .container #gameMessage { 98 | font-family: ChessType; 99 | position: absolute; 100 | top: 50%; 101 | left: 50%; 102 | transform: translateX(-50%) translateY(-50%); 103 | background: black; 104 | color: white; 105 | padding: 30px; 106 | border: 1px solid white; 107 | font-size: 40px; 108 | white-space: nowrap; 109 | text-align: center; 110 | z-index: 100; } 111 | 112 | .container #gameMessage small { 113 | color: white; 114 | display: block; 115 | font-size: 20px; } 116 | 117 | .container .description { 118 | position: absolute; 119 | display: block; 120 | width: 165px; 121 | left: 20px; 122 | top: 160px; 123 | z-index: 10; 124 | color: white; 125 | font-family: 'Raleway', sans-serif; } 126 | 127 | .container .footer { 128 | height: 100px; 129 | padding-top: 10px; 130 | border-top: 4px solid white; 131 | display: flex; 132 | align-items: flex-end; } 133 | 134 | .container .footer .controls { 135 | font-family: ChessType; } 136 | 137 | .container .footer .credits { 138 | margin-left: auto; } 139 | 140 | .container .footer .credits small { 141 | display: block; 142 | font-size: 12px; 143 | margin-top: 10px; } 144 | 145 | .container .footer .credits small a { 146 | color: white; } 147 | 148 | .container .footer .credits .ekologo { 149 | position: relative; 150 | top: 4px; 151 | color: rgba(255, 255, 255, 0); 152 | font-size: 30px; 153 | background-image: url(../images/ekoengineering_logo.svg); 154 | background-size: 100%; 155 | background-repeat: no-repeat; 156 | margin: 0 5px; } 157 | 158 | .container .footer .credits .ekologo span { 159 | padding-left: 5px; 160 | font-size: 16px; } 161 | 162 | .container .footer .arrows { 163 | position: relative; 164 | vertical-align: bottom; 165 | margin-left: 9px; } 166 | 167 | body.child { 168 | color: white; 169 | overflow-y: hidden; } 170 | 171 | body.child #tetris #menu { 172 | display: none; 173 | margin-top: 16px; 174 | margin-bottom: 15px; } 175 | 176 | body.child #tetris #start { 177 | display: none; } 178 | 179 | body.child #tetris #score { 180 | border: 2px solid white; 181 | font-family: ChessType; 182 | font-size: 24px; 183 | padding: 10px; 184 | height: 26px; 185 | margin-top: 20px; } 186 | 187 | body.child #tetris #upcoming { 188 | margin: 0; 189 | background-color: black; 190 | border: 2px solid white; 191 | width: 78px; 192 | height: 78px; 193 | margin-left: auto; } 194 | 195 | body.child #tetris .canvasContainer { 196 | position: relative; 197 | display: inline-block; 198 | vertical-align: top; 199 | width: 100%; 200 | height: 100%; } 201 | 202 | body.child #tetris .canvasContainer canvas { 203 | position: absolute; 204 | top: 0; 205 | left: 0; 206 | width: 100%; 207 | height: 100%; } 208 | 209 | body.child #tetris .canvasContainer:before { 210 | content: ""; 211 | position: absolute; 212 | width: 100%; 213 | height: 100%; 214 | background-size: 20px 20px, 20px 20px; 215 | background-image: linear-gradient(to right, white 0px, transparent 1px), linear-gradient(to bottom, white 0px, transparent 1px); 216 | background-position: 0px 0px; } 217 | 218 | body.child #tetris .canvasContainer:after { 219 | content: ""; 220 | position: absolute; 221 | width: 100%; 222 | height: 100%; 223 | background-size: 100% 100%; 224 | background-image: linear-gradient(to top, white 1px, transparent 1px), linear-gradient(to bottom, white 1px, transparent 1px), linear-gradient(to right, white 1px, transparent 1px), linear-gradient(to left, white 1px, transparent 1px); 225 | background-position: 0px 0px; } 226 | 227 | @font-face { 228 | font-family: 'ChessType'; 229 | font-style: normal; 230 | font-weight: normal; 231 | src: local("ChessType"), url("https://video.eko.com/s/sonorous/demos/track_mixer/ChessType.woff") format("woff"); } 232 | -------------------------------------------------------------------------------- /examples/itetrisly/style/index.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Raleway&display=swap'); 2 | 3 | @mixin shortAndWide { 4 | @media only screen and (max-height: 600px) { 5 | @content; 6 | } 7 | } 8 | 9 | html { 10 | width: 100%; 11 | height: 100%; 12 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 13 | } 14 | 15 | .label{ 16 | position: absolute; 17 | top: 0; 18 | left:0; 19 | background: #e6e6e6; 20 | font-size: 12px; 21 | padding: 4px 6px; 22 | font-weight: bold; 23 | font-family: arial; 24 | color: black; 25 | } 26 | 27 | body.parent{ 28 | height: 100%; 29 | background: black; 30 | display:flex; 31 | justify-content:center; 32 | align-items:center; 33 | color: white; 34 | 35 | &.lost #gameMessage{ 36 | color: red; 37 | } 38 | 39 | &.started #gameMessage{ 40 | display: none; 41 | } 42 | 43 | 44 | iframe { 45 | height: 200px; 46 | width: 200px; 47 | border: none; 48 | pointer-events: none; /* no need for interaction */ 49 | } 50 | 51 | #grid { 52 | display: grid; 53 | grid-template-columns: repeat(3, 200px); 54 | position: relative; 55 | /* center */ 56 | margin: auto; 57 | 58 | &:before { 59 | content: ""; 60 | background-image: url(../images/game_splash.png); 61 | background-size: cover; 62 | position: absolute; 63 | width: 100%; 64 | height: 100%; 65 | } 66 | } 67 | 68 | &.started #grid, &.lost #grid{ 69 | &:before{ 70 | display: none; 71 | } 72 | } 73 | 74 | #topIframe { 75 | height: 312px; 76 | grid-row: 1; 77 | grid-column: 2; 78 | } 79 | 80 | #bottomContainer { 81 | width: 600px; 82 | grid-row: 2; 83 | grid-column-start: 1; 84 | grid-column-end: 3; 85 | gap: 0; 86 | display: flex; 87 | } 88 | 89 | } 90 | 91 | .container{ 92 | position: relative; 93 | min-width: 70vw; 94 | border: 1px solid #22b573; 95 | border-radius: 10px; 96 | display: flex; 97 | flex-direction: column; 98 | padding: 20px; 99 | 100 | @include shortAndWide{ 101 | transform: scale(0.75); 102 | } 103 | 104 | .logo { 105 | position: absolute; 106 | display: block; 107 | width: 133px; 108 | height: 133px; 109 | left: 20px; 110 | top: 20px; 111 | z-index: 10; 112 | 113 | img { 114 | width: 100%; 115 | height: 100%; 116 | object-fit: contain; 117 | } 118 | } 119 | 120 | #gameMessage{ 121 | font-family: ChessType; 122 | position: absolute; 123 | top: 50%; 124 | left: 50%; 125 | transform: translateX(-50%) translateY(-50%); 126 | background: black; 127 | color: white; 128 | padding: 30px; 129 | border: 1px solid white; 130 | font-size: 40px; 131 | white-space: nowrap; 132 | text-align: center; 133 | z-index: 100; 134 | small{ 135 | color: white; 136 | display: block; 137 | font-size: 20px; 138 | } 139 | } 140 | 141 | 142 | .description { 143 | position: absolute; 144 | display: block; 145 | width: 165px; 146 | left: 20px; 147 | top: 160px; 148 | z-index: 10; 149 | color: white; 150 | font-family: 'Raleway', sans-serif; 151 | } 152 | 153 | 154 | 155 | .footer{ 156 | height: 100px; 157 | padding-top: 10px; 158 | border-top: 4px solid white; 159 | display: flex; 160 | align-items: flex-end; 161 | 162 | .controls { 163 | font-family: ChessType; 164 | } 165 | 166 | .credits{ 167 | margin-left: auto; 168 | 169 | small{ 170 | display: block; 171 | font-size: 12px; 172 | a {color: white} 173 | margin-top: 10px; 174 | } 175 | 176 | .ekologo{ 177 | position: relative; 178 | top: 4px; 179 | color: hsla(0,0%,100%,0); 180 | font-size: 30px; 181 | background-image: url(../images/ekoengineering_logo.svg); 182 | background-size: 100%; 183 | background-repeat: no-repeat; 184 | margin: 0 5px; 185 | 186 | span { 187 | padding-left: 5px; 188 | font-size: 16px; 189 | } 190 | 191 | } 192 | } 193 | 194 | .arrows{ 195 | position: relative; 196 | vertical-align: bottom; 197 | margin-left: 9px; 198 | } 199 | 200 | 201 | } 202 | } 203 | 204 | 205 | body.child{ 206 | color: white; 207 | overflow-y: hidden; 208 | 209 | #tetris{ 210 | 211 | #menu{ 212 | display: none; 213 | margin-top: 16px; 214 | margin-bottom: 15px; 215 | } 216 | 217 | #start{ 218 | display: none; 219 | } 220 | 221 | #score { 222 | border: 2px solid white; 223 | font-family: ChessType; 224 | font-size: 24px; 225 | padding: 10px; 226 | height: 26px; 227 | margin-top: 20px; 228 | } 229 | 230 | #upcoming { 231 | margin: 0; 232 | background-color: black; 233 | border: 2px solid white; 234 | width: 78px; 235 | height: 78px; 236 | margin-left: auto; 237 | } 238 | 239 | 240 | .canvasContainer{ 241 | position: relative; 242 | display: inline-block; 243 | vertical-align: top; 244 | width: 100%; 245 | height: 100%; 246 | 247 | canvas{ 248 | position: absolute; 249 | top: 0; 250 | left: 0; 251 | width: 100%; 252 | height: 100%; 253 | } 254 | 255 | &:before{ 256 | content: ""; 257 | position: absolute; 258 | width: 100%; 259 | height: 100%; 260 | background-size: 20px 20px, 261 | 20px 20px; 262 | background-image: 263 | linear-gradient(to right, rgba(255,255,255, 1) 0px, transparent 1px), 264 | linear-gradient(to bottom, rgba(255,255,255, 1) 0px, transparent 1px); 265 | background-position: 0px 0px; 266 | } 267 | 268 | &:after{ 269 | content: ""; 270 | position: absolute; 271 | width: 100%; 272 | height: 100%; 273 | background-size: 100% 100%; 274 | 275 | background-image: 276 | linear-gradient(to top, white 1px, transparent 1px), 277 | linear-gradient(to bottom, white 1px, transparent 1px), 278 | linear-gradient(to right, white 1px, transparent 1px), 279 | linear-gradient(to left, white 1px, transparent 1px); 280 | background-position: 0px 0px; 281 | } 282 | } 283 | } 284 | 285 | } 286 | 287 | @font-face { 288 | font-family: 'ChessType'; 289 | font-style: normal; 290 | font-weight: normal; 291 | src: local('ChessType'), url('https://video.eko.com/s/sonorous/demos/track_mixer/ChessType.woff') format('woff'); 292 | } -------------------------------------------------------------------------------- /examples/treehouse/child.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 41 | 42 | 43 | 44 |
45 |
child.html
46 | 47 | 48 | -------------------------------------------------------------------------------- /examples/treehouse/dialogue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let dialogue = { 4 | 'root': [ 5 | { 6 | 'text': 'Everything is going according to plan.', 7 | 'replyId': '0_1. Dad: Everything is going according to plan.' 8 | } 9 | ], 10 | '0_1. Dad: Everything is going according to plan.': [ 11 | { 12 | 'text': 'According to plan, what?', 13 | 'replyId': '1_Kid: According to plan, what?' 14 | } 15 | ], 16 | '1_Kid: According to plan, what?': [ 17 | { 18 | 'text': 'According to plan, SIR!', 19 | 'replyId': '2_Dad: According to plan, sir!' 20 | } 21 | ], 22 | '2_Dad: According to plan, sir!': [ 23 | { 24 | 'text': 'Excellent news!
Prepare for the next step.', 25 | 'replyId': '3_Kid: Excellent news! Prepare for the next step.' 26 | } 27 | ], 28 | '3_Kid: Excellent news! Prepare for the next step.': [ 29 | { 30 | 'text': 'Yes, sir. Shall I start with the mother?', 31 | 'replyId': '4_Dad: Yes, sir. Shall I start with the mother?' 32 | }, 33 | { 34 | 'text': 'Everything is already set up, sir.', 35 | 'replyId': '11_Dad: Everything is already set up, sir.' 36 | }, 37 | { 38 | 'text': 'Are you sure we are ready?', 39 | 'replyId': '18_Dad: Are you sure we are ready?' 40 | } 41 | ], 42 | '4_Dad: Yes, sir. Shall I start with the mother?': [ 43 | { 44 | 'text': 'Yes. She will be a great asset to us.', 45 | 'replyId': '5_Kid: Yes. She will be a great asset to us.' 46 | }, 47 | { 48 | 'text': 'No. The daughter is smarter. Start with her. ', 49 | 'replyId': '6_Kid: No. The daughter is smarter. Start with her. ' 50 | }, 51 | { 52 | 'text': 'Start with whomever you choose. Just be done by morning.', 53 | 'replyId': '7_Kid: Start with whomever you choose. Just be done by morning.' 54 | } 55 | ], 56 | '7_Kid: Start with whomever you choose. Just be done by morning.': [ 57 | { 58 | 'text': 'Yes, sir. Soon all of humanity will be turned, and our lost home planet of Zorg will live again on earth!', 59 | 'replyId': '8_Dad: Yes, sir. Soon all of humanity will be turned, and our lost home planet of Zorg will live again on earth!' 60 | } 61 | ], 62 | '8_Dad: Yes, sir. Soon all of humanity will be turned, and our lost home planet of Zorg will live again on earth!': [ 63 | { 64 | 'text': 'You have served me well, my loyal first officer, and shall be greatly rewarded.', 65 | 'replyId': '9_Kid: You have served me well, my loyal first officer, and shall be greatly rewarded.' 66 | } 67 | ], 68 | '9_Kid: You have served me well, my loyal first officer, and shall be greatly rewarded.': [ 69 | { 70 | 'text': 'This is the last time I let you watch Invasion of the Body Snatchers, sir!', 71 | 'replyId': '21_Kid: Muhahahaha' 72 | } 73 | ], 74 | '11_Dad: Everything is already set up, sir.': [ 75 | { 76 | 'text': 'Good. The daughter goes first, then the mother.', 77 | 'replyId': '12_Kid: Good. The daughter goes first, then the mother.' 78 | }, 79 | { 80 | 'text': 'Are you sure everything is in place?', 81 | 'replyId': '13_Kid: Are you sure everything is in place?' 82 | }, 83 | { 84 | 'text': 'Wonderful. We have no time to waste.', 85 | 'replyId': '14_Kid: Wonderful. We have no time to waste.' 86 | } 87 | ], 88 | '14_Kid: Wonderful. We have no time to waste.': [ 89 | { 90 | 'text': 'Yes, sir. Soon all of humanity will be turned, and our lost home planet of Zorg will live again on earth!', 91 | 'replyId': '15_Dad: Yes, sir. Soon all of humanity will be turned, and our lost home planet of Zorg will live again on earth!' 92 | } 93 | ], 94 | '15_Dad: Yes, sir. Soon all of humanity will be turned, and our lost home planet of Zorg will live again on earth!': [ 95 | { 96 | 'text': 'You have served me well, my loyal first officer, and shall be greatly rewarded.', 97 | 'replyId': '16_Kid: You have served me well, my loyal first officer, and shall be greatly rewarded.' 98 | } 99 | ], 100 | '16_Kid: You have served me well, my loyal first officer, and shall be greatly rewarded.': [ 101 | { 102 | 'text': 'I require nothing more than the honor of fulfilling our mission.', 103 | 'replyId': '21_Kid: Muhahahaha' 104 | } 105 | ], 106 | '18_Dad: Are you sure we are ready?': [ 107 | { 108 | 'text': 'Yes. Soon all of humanity will be turned, and our lost home planet of Zorg will live again on earth!', 109 | 'replyId': '19_Kid: Yes. Soon all of humanity will be turned, and our lost home planet of Zorg will live again on earth!' 110 | } 111 | ], 112 | '19_Kid: Yes. Soon all of humanity will be turned, and our lost home planet of Zorg will live again on earth!': [ 113 | { 114 | 'text': 'You are most wise and powerful, sir. The people of Zorg are forever in your debt.', 115 | 'replyId': '21_Kid: Muhahahaha' 116 | } 117 | ], 118 | '21_Kid: Muhahahaha': [ 119 | { 120 | 'text': ' Muhahahaha', 121 | 'replyId': '' 122 | } 123 | ] 124 | }; 125 | 126 | window.getDialogueOptions = function(id, onChoose) { 127 | let optionButtons = []; 128 | if (dialogue[id]) { 129 | optionButtons = dialogue[id].map(dialogueOption => { 130 | let optionButton = document.createElement('button'); 131 | optionButton.setAttribute('data-replyId', dialogueOption.replyId); 132 | optionButton.innerHTML = `${dialogueOption.text}`; 133 | optionButton.addEventListener('click', () => onChoose(dialogueOption.replyId)); 134 | 135 | return optionButton; 136 | }); 137 | } 138 | 139 | return optionButtons; 140 | }; 141 | -------------------------------------------------------------------------------- /examples/treehouse/images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EkoLabs/iframily/01c190894766764ae24d085c4309b286f8d11fbb/examples/treehouse/images/background.jpg -------------------------------------------------------------------------------- /examples/treehouse/images/bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EkoLabs/iframily/01c190894766764ae24d085c4309b286f8d11fbb/examples/treehouse/images/bg.gif -------------------------------------------------------------------------------- /examples/treehouse/images/ekoengineering_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 36 | 40 | 42 | 44 | 47 | 50 | 52 | 54 | 56 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /examples/treehouse/images/iframe_background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EkoLabs/iframily/01c190894766764ae24d085c4309b286f8d11fbb/examples/treehouse/images/iframe_background.jpg -------------------------------------------------------------------------------- /examples/treehouse/images/iframily_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EkoLabs/iframily/01c190894766764ae24d085c4309b286f8d11fbb/examples/treehouse/images/iframily_logo.png -------------------------------------------------------------------------------- /examples/treehouse/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | iframily treehouse 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
postMessage made simple & safe
15 |
16 |
17 |
18 | 19 |
20 |
21 |
22 |
23 |
by theteam
24 |
25 | 26 |
27 |
father.html
28 | 29 | -------------------------------------------------------------------------------- /examples/treehouse/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let parentIframily = window.Iframily.initParent('treehouse', window.location.origin); 4 | 5 | document.addEventListener('DOMContentLoaded', () => { 6 | setupDialogue('root'); 7 | 8 | document.querySelector('.end button').addEventListener('click', () => { 9 | parentIframily.sendMessage('reload'); 10 | document.body.classList.remove('ended'); 11 | setupDialogue('root'); 12 | 13 | parentIframily.dispose(); 14 | parentIframily = window.Iframily.initParent('treehouse', window.location.origin); 15 | }); 16 | }); 17 | 18 | function setupDialogue(dialogueId) { 19 | // This is the initial dialogue which starts it all 20 | let dialogOptions = window.getDialogueOptions(dialogueId, parentReplyId => { 21 | setSelectedMessage(parentReplyId); 22 | parentIframily.sendMessage(parentReplyId).then(childReplyId => setupDialogue(childReplyId)); 23 | }); 24 | 25 | updateDialogueOptions(dialogOptions); 26 | } 27 | 28 | function updateDialogueOptions(optionElements) { 29 | let dialogueOptionsContainerEl = document.querySelector('.dialogueOptionsContainer'); 30 | dialogueOptionsContainerEl.classList.remove('selected0', 'selected1', 'selected2'); 31 | dialogueOptionsContainerEl.innerHTML = ''; 32 | 33 | if (optionElements.length > 0) { 34 | optionElements.forEach(optionElement => dialogueOptionsContainerEl.appendChild(optionElement)); 35 | } else { 36 | document.body.classList.add('ended'); 37 | } 38 | } 39 | 40 | function setSelectedMessage(replyId) { 41 | let dialogueOptionsContainerEl = document.querySelector('.dialogueOptionsContainer'); 42 | let optionEl = dialogueOptionsContainerEl.querySelector(`button[data-replyId='${replyId}']`); 43 | const optionIndex = [...dialogueOptionsContainerEl.children].indexOf(optionEl); 44 | dialogueOptionsContainerEl.classList.add(`selected${optionIndex}`); 45 | } 46 | -------------------------------------------------------------------------------- /examples/treehouse/style/child.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Bangers&display=swap"); 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | background: url(../images/iframe_background.jpg); 6 | background-size: cover; 7 | overflow: hidden; } 8 | 9 | *:focus:not(.focus-visible) { 10 | outline: none; } 11 | 12 | .dialogueOptionsContainer { 13 | position: absolute; 14 | left: 5%; 15 | top: 9%; } 16 | 17 | .dialogueOptionsContainer button { 18 | min-height: 50px; 19 | border: none; 20 | text-align: left; 21 | background: transparent; 22 | cursor: pointer; 23 | transition: all 0.15s ease-in; 24 | position: absolute; 25 | top: 0; 26 | left: 0; } 27 | 28 | .dialogueOptionsContainer button:hover { 29 | filter: brightness(0.9); } 30 | 31 | .dialogueOptionsContainer button span { 32 | display: block; 33 | position: relative; 34 | top: 0; 35 | left: 0; 36 | font-family: 'Bangers', cursive; 37 | font-size: 24px; 38 | color: #4d4d4d; 39 | z-index: 5; 40 | padding: 10px 20px; 41 | min-width: 230px; 42 | max-width: 350px; } 43 | 44 | .dialogueOptionsContainer button span:before, .dialogueOptionsContainer button span:after { 45 | bottom: -41px; 46 | right: 14%; 47 | border: solid transparent; 48 | content: ""; 49 | height: 0; 50 | width: 0; 51 | position: absolute; 52 | pointer-events: none; 53 | border-top-color: #e6e6e6; 54 | border-width: 25px; 55 | margin-left: -25px; 56 | transform: scaleX(0.3) scaleY(0.7) skewX(24deg); 57 | z-index: 3; 58 | opacity: 0; 59 | transition: 0.15s ease-in all; } 60 | 61 | .dialogueOptionsContainer button span:after { 62 | bottom: -46px; 63 | border-top-color: #4d4d4d; 64 | z-index: 2; } 65 | 66 | .dialogueOptionsContainer button:before, .dialogueOptionsContainer button:after { 67 | content: ""; 68 | position: absolute; 69 | top: 0; 70 | left: 0; 71 | width: 103%; 72 | height: 100%; 73 | border-top-left-radius: 30px; 74 | border-top-right-radius: 5px; 75 | border-bottom-right-radius: 30px; 76 | border-bottom-left-radius: 5px; } 77 | 78 | .dialogueOptionsContainer button:before { 79 | background: #e6e6e6; 80 | z-index: 2; } 81 | 82 | .dialogueOptionsContainer button:after { 83 | top: 4px; 84 | background-color: #4d4d4d; 85 | z-index: 1; } 86 | 87 | .dialogueOptionsContainer button:nth-child(1) { 88 | transform: translateY(0px); } 89 | 90 | .dialogueOptionsContainer button:nth-child(2) { 91 | transform: translateY(93px); } 92 | 93 | .dialogueOptionsContainer button:nth-child(3) { 94 | transform: translateY(185px); } 95 | 96 | .dialogueOptionsContainer:not([class*="selected"]):not(:empty):before { 97 | content: "Choose reply:"; 98 | position: absolute; 99 | top: -20px; 100 | left: 0px; 101 | font-size: 14px; 102 | color: white; 103 | width: 321px; 104 | font-weight: bold; 105 | text-align: center; } 106 | 107 | .dialogueOptionsContainer.selected0 button, .dialogueOptionsContainer.selected1 button, .dialogueOptionsContainer.selected2 button { 108 | opacity: 0; 109 | pointer-events: none; 110 | filter: brightness(60%); } 111 | 112 | .dialogueOptionsContainer.selected0 button:nth-child(1) { 113 | transform: translateY(58px) translateX(50px); 114 | opacity: 1; } 115 | 116 | .dialogueOptionsContainer.selected0 button:nth-child(1) span:before, .dialogueOptionsContainer.selected0 button:nth-child(1) span:after { 117 | opacity: 1; } 118 | 119 | .dialogueOptionsContainer.selected1 button:nth-child(2) { 120 | transform: translateY(58px) translateX(50px); 121 | opacity: 1; 122 | pointer-events: none; } 123 | 124 | .dialogueOptionsContainer.selected1 button:nth-child(2) span:before, .dialogueOptionsContainer.selected1 button:nth-child(2) span:after { 125 | opacity: 1; } 126 | 127 | .dialogueOptionsContainer.selected2 button:nth-child(3) { 128 | transform: translateY(58px) translateX(50px); 129 | opacity: 1; } 130 | 131 | .dialogueOptionsContainer.selected2 button:nth-child(3) span:before, .dialogueOptionsContainer.selected2 button:nth-child(3) span:after { 132 | opacity: 1; } 133 | 134 | .label { 135 | position: absolute; 136 | top: 0px; 137 | left: 0; 138 | background: #e6e6e6; 139 | font-size: 14px; 140 | padding: 4px 6px; 141 | font-weight: bold; 142 | font-family: arial; 143 | z-index: 1; 144 | opacity: 0.6; } 145 | -------------------------------------------------------------------------------- /examples/treehouse/style/child.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Bangers&display=swap'); 2 | 3 | body{ 4 | margin: 0; 5 | padding: 0; 6 | background: url(../images/iframe_background.jpg); 7 | background-size: cover; 8 | overflow: hidden; 9 | } 10 | 11 | *:focus:not(.focus-visible) { 12 | outline: none; 13 | } 14 | 15 | .dialogueOptionsContainer{ 16 | position: absolute; 17 | left: 5%; 18 | top: 9%; 19 | } 20 | 21 | 22 | .dialogueOptionsContainer button{ 23 | min-height: 50px; 24 | border: none; 25 | text-align: left; 26 | background: transparent; 27 | cursor: pointer; 28 | transition: all 0.15s ease-in; 29 | position: absolute; 30 | top: 0; 31 | left: 0; 32 | 33 | &:hover{ 34 | filter: brightness(0.9); 35 | } 36 | 37 | span{ 38 | display: block; 39 | position: relative; 40 | top: 0; 41 | left: 0; 42 | font-family: 'Bangers', cursive; 43 | font-size: 24px; 44 | color: #4d4d4d; 45 | z-index: 5; 46 | padding: 10px 20px; 47 | min-width: 230px; 48 | max-width: 350px; 49 | 50 | &:before, &:after{ 51 | bottom: -41px; 52 | right: 14%; 53 | border: solid transparent; 54 | content: ""; 55 | height: 0; 56 | width: 0; 57 | position: absolute; 58 | pointer-events: none; 59 | border-top-color: #e6e6e6; 60 | border-width: 25px; 61 | margin-left: -25px; 62 | transform: scaleX(0.3) scaleY(0.7) skewX(24deg); 63 | z-index: 3; 64 | opacity: 0; 65 | transition: 0.15s ease-in all; 66 | } 67 | 68 | &:after { 69 | bottom: -46px; 70 | border-top-color: #4d4d4d; 71 | z-index: 2; 72 | } 73 | 74 | } 75 | 76 | 77 | &:before, &:after{ 78 | content: ""; 79 | position: absolute; 80 | top: 0; 81 | left: 0; 82 | width: 103%; 83 | height: 100%; 84 | border-top-left-radius: 30px; 85 | border-top-right-radius: 5px; 86 | border-bottom-right-radius: 30px; 87 | border-bottom-left-radius: 5px; 88 | } 89 | 90 | &:before{ 91 | background: #e6e6e6; 92 | z-index: 2; 93 | } 94 | 95 | &:after{ 96 | top: 4px; 97 | background-color: #4d4d4d; 98 | z-index: 1; 99 | } 100 | 101 | &:nth-child(1){ 102 | transform: translateY(0px); 103 | } 104 | 105 | &:nth-child(2){ 106 | transform: translateY(93px); 107 | } 108 | 109 | &:nth-child(3){ 110 | transform: translateY(185px); 111 | } 112 | } 113 | 114 | .dialogueOptionsContainer:not([class*="selected"]):not(:empty):before, { 115 | content: "Choose reply:"; 116 | position: absolute; 117 | top: -20px; 118 | left: 0px; 119 | font-size: 14px; 120 | color: white; 121 | width: 321px; 122 | font-weight: bold; 123 | text-align: center; 124 | } 125 | 126 | .dialogueOptionsContainer.selected0, .dialogueOptionsContainer.selected1, .dialogueOptionsContainer.selected2{ 127 | button { 128 | opacity: 0; 129 | pointer-events: none; 130 | filter: brightness(60%); 131 | } 132 | } 133 | 134 | .dialogueOptionsContainer.selected0{ 135 | button:nth-child(1){ 136 | transform: translateY(58px) translateX(50px); 137 | opacity: 1; 138 | 139 | span:before, span:after{ 140 | opacity: 1; 141 | } 142 | } 143 | } 144 | 145 | .dialogueOptionsContainer.selected1{ 146 | button:nth-child(2){ 147 | transform: translateY(58px) translateX(50px); 148 | opacity: 1; 149 | pointer-events: none; 150 | 151 | span:before, span:after{ 152 | opacity: 1; 153 | } 154 | } 155 | } 156 | 157 | .dialogueOptionsContainer.selected2{ 158 | button:nth-child(3){ 159 | transform: translateY(58px) translateX(50px); 160 | opacity: 1; 161 | 162 | span:before, span:after{ 163 | opacity: 1; 164 | } 165 | } 166 | } 167 | 168 | .label{ 169 | position: absolute; 170 | top: 0px; 171 | left:0; 172 | background: #e6e6e6; 173 | font-size: 14px; 174 | padding: 4px 6px; 175 | font-weight: bold; 176 | font-family: arial; 177 | z-index: 1; 178 | opacity: 0.6; 179 | } -------------------------------------------------------------------------------- /examples/treehouse/style/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Neucha&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=Raleway&display=swap"); 3 | html { 4 | height: 100%; 5 | margin: 0; 6 | padding: 0; } 7 | 8 | body { 9 | width: 100%; 10 | height: 100%; 11 | display: flex; 12 | align-items: center; 13 | justify-content: center; 14 | background: linear-gradient(to bottom, #7bcdcb 0%, #7bcdcb 50%, #b3c636 50%, #b3c636 100%); 15 | margin: 0; 16 | padding: 0; 17 | font-family: 'Open Sans', sans-serif; } 18 | 19 | @media only screen and (max-height: 700px) { 20 | body { 21 | overflow: hidden; } } 22 | 23 | body .end { 24 | opacity: 0; 25 | pointer-events: none; } 26 | 27 | body.ended .end { 28 | opacity: 1; 29 | pointer-events: all; 30 | position: absolute; 31 | top: 0; 32 | left: 0; 33 | width: 100%; 34 | height: 100%; 35 | transition: opacity 1s ease-out; 36 | transition-delay: 3.5s; 37 | background: linear-gradient(90deg, rgba(0, 0, 0, 0.5) 2px, transparent 1%), url(../images/bg.gif); 38 | background-size: 3px 2px, cover; 39 | background-position: center; 40 | display: flex; 41 | align-items: center; 42 | justify-content: center; } 43 | 44 | body.ended .end button { 45 | position: relative; } 46 | 47 | body.ended .end button span { 48 | min-width: auto; 49 | font-family: sans-serif, "arial"; } 50 | 51 | *:focus:not(.focus-visible) { 52 | outline: none; } 53 | 54 | #fit_container { 55 | position: absolute; 56 | width: 100%; 57 | left: 50%; 58 | transform: translateX(-50%); 59 | min-width: 1200px; } 60 | 61 | #aspect_ratio_box { 62 | height: 0; 63 | overflow: hidden; 64 | padding-bottom: 54.16313%; 65 | width: 100%; 66 | position: relative; 67 | margin-top: 10vw; } 68 | 69 | @media only screen and (max-height: 700px) { 70 | #aspect_ratio_box { 71 | position: absolute; 72 | top: 0; 73 | margin-top: -61vh; } } 74 | 75 | @media only screen and (max-height: 700px) and (max-width: 1300px) { 76 | #aspect_ratio_box { 77 | position: absolute; 78 | top: 0; 79 | margin-top: -60vmin; } } 80 | 81 | #container { 82 | position: absolute; 83 | top: 0; 84 | left: 0; 85 | width: 100%; 86 | height: 100%; 87 | background: url(../images/background.jpg); 88 | background-size: 100% 100%; 89 | background-repeat: no-repeat; } 90 | 91 | .logo { 92 | position: absolute; 93 | display: block; 94 | width: 133px; 95 | height: 133px; 96 | left: 84px; 97 | top: 65px; 98 | z-index: 10; } 99 | 100 | .logo img { 101 | width: 100%; 102 | height: 100%; 103 | object-fit: contain; } 104 | 105 | .shoutout { 106 | position: absolute; 107 | z-index: 10; 108 | right: 25px; 109 | bottom: 15px; 110 | width: 500px; 111 | color: white; 112 | font-size: 14px; } 113 | 114 | @media only screen and (max-height: 700px) { 115 | .shoutout { 116 | bottom: 10px; 117 | left: 40px; } } 118 | 119 | .shoutout a.ekologo { 120 | position: relative; 121 | top: 4px; 122 | color: rgba(255, 255, 255, 0); 123 | font-size: 30px; 124 | background-size: 100%; 125 | background-repeat: no-repeat; 126 | background-image: url(../images/ekoengineering_logo.svg); 127 | margin: 0 6px; } 128 | 129 | .shoutout a.ekologo span { 130 | padding-left: 5px; 131 | font-size: 16px; } 132 | 133 | iframe { 134 | position: absolute; 135 | top: 11.6%; 136 | left: 33%; 137 | width: 27.3%; 138 | height: 45.3%; } 139 | 140 | .description { 141 | position: absolute; 142 | display: block; 143 | width: 284px; 144 | left: 27px; 145 | top: 219px; 146 | z-index: 10; 147 | font-size: 18px; 148 | color: black; 149 | font-family: 'Raleway', sans-serif; } 150 | 151 | .dialogueOptionsContainer { 152 | position: absolute; 153 | left: 65%; 154 | top: 15%; } 155 | 156 | .dialogueOptionsContainer:not([class*="selected"]):not(:empty):before { 157 | content: "Choose reply:"; 158 | position: absolute; 159 | top: -20px; 160 | left: 0px; 161 | font-size: 14px; 162 | color: #037777; 163 | width: 350px; 164 | font-weight: bold; 165 | text-align: center; } 166 | 167 | button { 168 | min-height: 50px; 169 | border: none; 170 | text-align: left; 171 | background: transparent; 172 | cursor: pointer; 173 | transition: all 0.15s ease-in; 174 | margin-bottom: 25px; 175 | position: absolute; 176 | top: 0; 177 | left: 0; } 178 | 179 | button:hover { 180 | filter: brightness(1.1); } 181 | 182 | button span { 183 | display: block; 184 | position: relative; 185 | top: 0; 186 | left: 0; 187 | width: 103%; 188 | height: 100%; 189 | font-family: 'Neucha', cursive; 190 | font-size: 24px; 191 | color: white; 192 | z-index: 5; 193 | padding: 10px 12px; 194 | min-width: 320px; } 195 | 196 | button span:before, button span:after { 197 | bottom: -41px; 198 | left: 14%; 199 | border: solid transparent; 200 | content: ""; 201 | height: 0; 202 | width: 0; 203 | position: absolute; 204 | pointer-events: none; 205 | border-top-color: #52b3b5; 206 | border-width: 25px; 207 | margin-left: -25px; 208 | transform: scaleX(0.3) scaleY(0.7) skewX(-24deg); 209 | z-index: 3; 210 | opacity: 0; 211 | transition: 0.15s ease-in all; } 212 | 213 | button span:after { 214 | bottom: -46px; 215 | border-top-color: #037777; 216 | z-index: 2; } 217 | 218 | button:before, button:after { 219 | content: ""; 220 | position: absolute; 221 | top: 0; 222 | left: 0; 223 | width: 100%; 224 | height: 100%; 225 | border-top-left-radius: 30px; 226 | border-top-right-radius: 5px; 227 | border-bottom-right-radius: 30px; 228 | border-bottom-left-radius: 5px; } 229 | 230 | button:before { 231 | background: #52b3b5; 232 | z-index: 2; } 233 | 234 | button:after { 235 | top: 4px; 236 | background-color: #037777; 237 | z-index: 1; } 238 | 239 | button:nth-child(1) { 240 | transform: translateY(0px); } 241 | 242 | button:nth-child(2) { 243 | transform: translateY(85px); } 244 | 245 | button:nth-child(3) { 246 | transform: translateY(170px); } 247 | 248 | .dialogueOptionsContainer.selected0 button, .dialogueOptionsContainer.selected1 button, .dialogueOptionsContainer.selected2 button { 249 | opacity: 0; 250 | pointer-events: none; 251 | filter: brightness(60%); } 252 | 253 | .dialogueOptionsContainer.selected0 button:nth-child(1) { 254 | transform: translateY(170px); 255 | opacity: 1; } 256 | 257 | .dialogueOptionsContainer.selected0 button:nth-child(1) span:before, .dialogueOptionsContainer.selected0 button:nth-child(1) span:after { 258 | opacity: 1; } 259 | 260 | .dialogueOptionsContainer.selected1 button:nth-child(2) { 261 | transform: translateY(170px); 262 | opacity: 1; 263 | pointer-events: none; } 264 | 265 | .dialogueOptionsContainer.selected1 button:nth-child(2) span:before, .dialogueOptionsContainer.selected1 button:nth-child(2) span:after { 266 | opacity: 1; } 267 | 268 | .dialogueOptionsContainer.selected2 button:nth-child(3) { 269 | transform: translateY(170px); 270 | opacity: 1; } 271 | 272 | .dialogueOptionsContainer.selected2 button:nth-child(3) span:before, .dialogueOptionsContainer.selected2 button:nth-child(3) span:after { 273 | opacity: 1; } 274 | 275 | .label { 276 | position: absolute; 277 | top: 0px; 278 | left: 0; 279 | background: #e6e6e6; 280 | font-size: 14px; 281 | padding: 4px 6px; 282 | font-weight: bold; 283 | font-family: arial; 284 | z-index: 1; 285 | opacity: 0.6; } 286 | -------------------------------------------------------------------------------- /examples/treehouse/style/index.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Neucha&display=swap'); 2 | @import url('https://fonts.googleapis.com/css2?family=Raleway&display=swap'); 3 | 4 | @mixin shortAndWide { 5 | @media only screen and (max-height: 700px) { 6 | @content; 7 | } 8 | } 9 | 10 | @mixin shortAndHalfWide { 11 | @media only screen and (max-height: 700px) and (max-width: 1300px){ 12 | @content; 13 | } 14 | } 15 | 16 | html{ 17 | height: 100%; 18 | margin: 0; 19 | padding: 0; 20 | } 21 | 22 | body{ 23 | width: 100%; 24 | height: 100%; 25 | display: flex; 26 | align-items: center; 27 | justify-content: center; 28 | background: linear-gradient(to bottom, #7bcdcb 0%, #7bcdcb 50%, #b3c636 50%, #b3c636 100%); 29 | margin: 0; 30 | padding:0; 31 | font-family: 'Open Sans', sans-serif; 32 | 33 | @include shortAndWide{ 34 | overflow: hidden; 35 | } 36 | } 37 | 38 | body .end{ 39 | opacity: 0; 40 | pointer-events: none; 41 | } 42 | 43 | body.ended .end{ 44 | opacity: 1; 45 | pointer-events: all; 46 | position: absolute; 47 | top: 0; 48 | left: 0; 49 | width: 100%; 50 | height: 100%; 51 | transition: opacity 1s ease-out; 52 | transition-delay: 3.5s; 53 | background: linear-gradient(90deg, rgba(0,0,0,0.5) 2px, transparent 1%), 54 | url(../images/bg.gif); 55 | background-size: 3px 2px, cover; 56 | background-position: center; 57 | 58 | display: flex; 59 | align-items: center; 60 | justify-content: center; 61 | 62 | button{ 63 | position: relative; 64 | } 65 | 66 | button span{ 67 | min-width: auto; 68 | font-family: sans-serif, "arial"; 69 | } 70 | } 71 | 72 | 73 | *:focus:not(.focus-visible) { 74 | outline: none; 75 | } 76 | 77 | #fit_container{ 78 | position: absolute; 79 | width: 100%; 80 | left: 50%; 81 | transform: translateX(-50%); 82 | min-width: 1200px; 83 | } 84 | 85 | #aspect_ratio_box{ 86 | height: 0; 87 | overflow: hidden; 88 | padding-bottom: 1275px / 2354px * 100%; 89 | width: 100%; 90 | position: relative; 91 | margin-top: 10vw; 92 | 93 | @include shortAndWide{ 94 | position: absolute; 95 | top: 0; 96 | margin-top: -61vh; 97 | } 98 | 99 | @include shortAndHalfWide{ 100 | position: absolute; 101 | top: 0; 102 | margin-top: -60vmin; 103 | } 104 | } 105 | 106 | #container{ 107 | position: absolute; 108 | top: 0; 109 | left: 0; 110 | width: 100%; 111 | height: 100%; 112 | background: url(../images/background.jpg); 113 | background-size: 100% 100%; 114 | background-repeat: no-repeat; 115 | 116 | } 117 | 118 | .logo { 119 | position: absolute; 120 | display: block; 121 | width: 133px; 122 | height: 133px; 123 | left: 84px; 124 | top: 65px; 125 | z-index: 10; 126 | 127 | img { 128 | width: 100%; 129 | height: 100%; 130 | object-fit: contain; 131 | } 132 | } 133 | 134 | .shoutout{ 135 | position: absolute; 136 | z-index: 10; 137 | right: 25px; 138 | bottom: 15px; 139 | width: 500px; 140 | color: white; 141 | font-size: 14px; 142 | 143 | @include shortAndWide{ 144 | bottom: 10px; 145 | left: 40px; 146 | } 147 | } 148 | 149 | .shoutout a.ekologo { 150 | position: relative; 151 | top: 4px; 152 | color: hsla(0,0%,100%,0); 153 | font-size: 30px; 154 | background-size: 100%; 155 | background-repeat: no-repeat; 156 | background-image: url(../images/ekoengineering_logo.svg); 157 | margin: 0 6px; 158 | } 159 | 160 | .shoutout a.ekologo span { 161 | padding-left: 5px; 162 | font-size: 16px; 163 | } 164 | 165 | iframe{ 166 | position: absolute; 167 | top: 11.6%; 168 | left: 33%; 169 | width: 27.3%; 170 | height: 45.3%; 171 | } 172 | 173 | .description { 174 | position: absolute; 175 | display: block; 176 | width: 284px; 177 | left: 27px; 178 | top: 219px; 179 | z-index: 10; 180 | font-size: 18px; 181 | //text-shadow: 2px 2px 27px black; 182 | color: black; 183 | font-family: 'Raleway', sans-serif; 184 | } 185 | 186 | 187 | .dialogueOptionsContainer{ 188 | position: absolute; 189 | left: 65%; 190 | top: 15%; 191 | 192 | &:not([class*="selected"]):not(:empty):before, { 193 | content: "Choose reply:"; 194 | position: absolute; 195 | top: -20px; 196 | left: 0px; 197 | font-size: 14px; 198 | color: #037777; 199 | width: 350px; 200 | font-weight: bold; 201 | text-align: center; 202 | } 203 | } 204 | 205 | button{ 206 | min-height: 50px; 207 | border: none; 208 | text-align: left; 209 | background: transparent; 210 | cursor: pointer; 211 | transition: all 0.15s ease-in; 212 | margin-bottom: 25px; 213 | position: absolute; 214 | top: 0; 215 | left: 0; 216 | 217 | &:hover{ 218 | filter: brightness(1.1); 219 | } 220 | 221 | span{ 222 | display: block; 223 | position: relative; 224 | top: 0; 225 | left: 0; 226 | width: 103%; 227 | height: 100%; 228 | font-family: 'Neucha', cursive; 229 | font-size: 24px; 230 | color: white; 231 | z-index: 5; 232 | padding: 10px 12px; 233 | min-width: 320px; 234 | 235 | &:before, &:after{ 236 | bottom: -41px; 237 | left: 14%; 238 | border: solid transparent; 239 | content: ""; 240 | height: 0; 241 | width: 0; 242 | position: absolute; 243 | pointer-events: none; 244 | border-top-color: #52b3b5; 245 | border-width: 25px; 246 | margin-left: -25px; 247 | transform: scaleX(0.3) scaleY(0.7) skewX(-24deg); 248 | z-index: 3; 249 | opacity: 0; 250 | transition: 0.15s ease-in all; 251 | } 252 | 253 | &:after { 254 | bottom: -46px; 255 | border-top-color: #037777; 256 | z-index: 2; 257 | } 258 | 259 | } 260 | 261 | 262 | &:before, &:after{ 263 | content: ""; 264 | position: absolute; 265 | top: 0; 266 | left: 0; 267 | width: 100%; 268 | height: 100%; 269 | border-top-left-radius: 30px; 270 | border-top-right-radius: 5px; 271 | border-bottom-right-radius: 30px; 272 | border-bottom-left-radius: 5px; 273 | } 274 | 275 | &:before{ 276 | background: #52b3b5; 277 | z-index: 2; 278 | } 279 | 280 | &:after{ 281 | top: 4px; 282 | background-color: #037777; 283 | z-index: 1; 284 | } 285 | 286 | &:nth-child(1){ 287 | transform: translateY(0px); 288 | } 289 | 290 | &:nth-child(2){ 291 | transform: translateY(85px); 292 | } 293 | 294 | &:nth-child(3){ 295 | transform: translateY(170px); 296 | } 297 | } 298 | 299 | .dialogueOptionsContainer.selected0, .dialogueOptionsContainer.selected1, .dialogueOptionsContainer.selected2{ 300 | button{ 301 | opacity: 0; 302 | pointer-events: none; 303 | filter: brightness(60%); 304 | } 305 | } 306 | 307 | .dialogueOptionsContainer.selected0{ 308 | button:nth-child(1){ 309 | transform: translateY(170px); 310 | opacity: 1; 311 | 312 | span:before, span:after{ 313 | opacity: 1; 314 | } 315 | } 316 | } 317 | 318 | .dialogueOptionsContainer.selected1{ 319 | button:nth-child(2){ 320 | transform: translateY(170px); 321 | opacity: 1; 322 | pointer-events: none; 323 | 324 | span:before, span:after{ 325 | opacity: 1; 326 | } 327 | } 328 | } 329 | 330 | .dialogueOptionsContainer.selected2{ 331 | button:nth-child(3){ 332 | transform: translateY(170px); 333 | opacity: 1; 334 | 335 | span:before, span:after{ 336 | opacity: 1; 337 | } 338 | } 339 | } 340 | 341 | .label{ 342 | position: absolute; 343 | top: 0px; 344 | left:0; 345 | background: #e6e6e6; 346 | font-size: 14px; 347 | padding: 4px 6px; 348 | font-weight: bold; 349 | font-family: arial; 350 | z-index: 1; 351 | opacity: 0.6; 352 | } -------------------------------------------------------------------------------- /jest-puppeteer.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | launch: { 5 | // For debugging: 6 | // headless: false, 7 | // devtools: true 8 | }, 9 | 10 | server: { 11 | command: 'http-server ./ -c-1 -p 9135', 12 | port: 9135, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // For a detailed explanation regarding each configuration property, visit: 4 | // https://jestjs.io/docs/en/configuration.html 5 | 6 | module.exports = { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // Respect "browser" field in package.json when resolving modules 14 | // browser: false, 15 | 16 | // The directory where Jest should store its cached dependency information 17 | // cacheDirectory: "/private/var/folders/mh/_qbxmmhs71sgg5t4fzz0whjc0000gn/T/jest_dx", 18 | 19 | // Automatically clear mock calls and instances between every test 20 | // clearMocks: true, 21 | 22 | // Indicates whether the coverage information should be collected while executing the test 23 | // collectCoverage: false, 24 | 25 | // An array of glob patterns indicating a set of files for which coverage information should be collected 26 | // collectCoverageFrom: undefined, 27 | 28 | // The directory where Jest should output its coverage files 29 | // coverageDirectory: undefined, 30 | 31 | // An array of regexp pattern strings used to skip coverage collection 32 | // coveragePathIgnorePatterns: [ 33 | // "/node_modules/" 34 | // ], 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "json", 77 | // "jsx", 78 | // "ts", 79 | // "tsx", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | // moduleNameMapper: {}, 85 | 86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 87 | // modulePathIgnorePatterns: [], 88 | 89 | // Activates notifications for test results 90 | // notify: false, 91 | 92 | // An enum that specifies notification mode. Requires { notify: true } 93 | // notifyMode: "failure-change", 94 | 95 | // A preset that is used as a base for Jest's configuration 96 | preset: 'jest-puppeteer', 97 | 98 | // Run tests from one or more projects 99 | // projects: undefined, 100 | 101 | // Use this configuration option to add custom reporters to Jest 102 | // reporters: undefined, 103 | 104 | // Automatically reset mock state between every test 105 | // resetMocks: false, 106 | 107 | // Reset the module registry before running each individual test 108 | // resetModules: false, 109 | 110 | // A path to a custom resolver 111 | // resolver: undefined, 112 | 113 | // Automatically restore mock state between every test 114 | // restoreMocks: false, 115 | 116 | // The root directory that Jest should scan for tests and modules within 117 | // rootDir: undefined, 118 | 119 | // A list of paths to directories that Jest should use to search for files in 120 | // roots: [ 121 | // "" 122 | // ], 123 | 124 | // Allows you to use a custom runner instead of Jest's default test runner 125 | // runner: "jest-runner", 126 | 127 | // The paths to modules that run some code to configure or set up the testing environment before each test 128 | // setupFiles: [], 129 | 130 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 131 | // setupFilesAfterEnv: [], 132 | 133 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 134 | // snapshotSerializers: [], 135 | 136 | // The test environment that will be used for testing 137 | // testEnvironment: "node", 138 | 139 | // Options that will be passed to the testEnvironment 140 | // testEnvironmentOptions: {}, 141 | 142 | // Adds a location field to test results 143 | // testLocationInResults: false, 144 | 145 | // The glob patterns Jest uses to detect test files 146 | // testMatch: [ 147 | // "**/__tests__/**/*.[jt]s?(x)", 148 | // "**/?(*.)+(spec|test).[tj]s?(x)" 149 | // ], 150 | 151 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 152 | // testPathIgnorePatterns: [ 153 | // "/node_modules/" 154 | // ], 155 | 156 | // The regexp pattern or array of patterns that Jest uses to detect test files 157 | // testRegex: [], 158 | 159 | // This option allows the use of a custom results processor 160 | // testResultsProcessor: undefined, 161 | 162 | // This option allows use of a custom test runner 163 | // testRunner: "jasmine2", 164 | 165 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 166 | // testURL: "http://localhost", 167 | 168 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 169 | // timers: "real", 170 | 171 | // A map from regular expressions to paths to transformers 172 | // transform: undefined, 173 | 174 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 175 | // transformIgnorePatterns: [ 176 | // "/node_modules/" 177 | // ], 178 | 179 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 180 | // unmockedModulePathPatterns: undefined, 181 | 182 | // Indicates whether each individual test should be reported during the run 183 | // verbose: undefined, 184 | 185 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 186 | // watchPathIgnorePatterns: [], 187 | 188 | // Whether to use watchman for file crawling 189 | // watchman: true, 190 | }; 191 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ekolabs/iframily", 3 | "version": "1.0.3", 4 | "description": "postMessage made simple & safe", 5 | "main": "dist/iframily.min.js", 6 | "license": "Apache-2.0", 7 | "engines": { 8 | "node": ">=10.0.0" 9 | }, 10 | "scripts": { 11 | "build": "webpack --mode=production", 12 | "dev": "webpack --mode=development --watch", 13 | "playground": "http-server ./ -c-1 -p 8135 -o /playground/parent.html", 14 | "examples": "http-server ./ -c-1 -p 8136 -o /examples", 15 | "test": "jest", 16 | "test-debug-mac": "node --inspect-brk node_modules/.bin/jest --runInBand --testTimeout=9999999 --watchAll", 17 | "test-debug-win": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --testTimeout=9999999 --watchAll" 18 | }, 19 | "dependencies": {}, 20 | "devDependencies": { 21 | "@babel/core": "^7.9.6", 22 | "@babel/preset-env": "^7.9.6", 23 | "babel-loader": "^8.1.0", 24 | "clean-webpack-plugin": "^3.0.0", 25 | "eslint": "^7.0.0", 26 | "eslint-loader": "^4.0.2", 27 | "http-server": "^0.12.3", 28 | "jest": "^26.0.1", 29 | "jest-puppeteer": "^4.4.0", 30 | "lodash": "^4.17.15", 31 | "minimist": "^1.2.0", 32 | "puppeteer": "^3.0.4", 33 | "webpack": "^4.43.0", 34 | "webpack-cli": "^3.3.11" 35 | }, 36 | "files": [ 37 | "dist/", 38 | "package.json", 39 | "README.md", 40 | "LICENSE.md" 41 | ], 42 | "repository": { 43 | "type": "git", 44 | "url": "https://github.com/EkoLabs/iframily.git" 45 | }, 46 | "keywords": [ 47 | "iframe", 48 | "iframes", 49 | "communication", 50 | "messaging", 51 | "postMessage" 52 | ], 53 | "author": { 54 | "name": "Eko labs", 55 | "url": "https://developer.eko.com", 56 | "email": "dev@eko.com" 57 | }, 58 | "contributors": [ 59 | { 60 | "name": "Asaf Menahem" 61 | } 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /playground/child.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 |
child.html
13 |
-> iframily initialized in window.childIframily
14 | 15 | 23 | 24 | -------------------------------------------------------------------------------- /playground/parent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | PLAYGROUND 4 | 5 | 6 | 11 | 12 | 13 |
parent.html
14 |
-> iframily initialized in window.parentIframily
15 | 16 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = require('./constants'); 4 | 5 | const PUBLIC_METHODS = ['sendMessage', 'dispose']; 6 | 7 | module.exports = class Base { 8 | constructor(id, targetOrigin, msgHandler, options) { 9 | this._id = id; 10 | this._targetOrigin = targetOrigin === constants.DANGEROUSLY_SET_WILDCARD ? '*' : targetOrigin; 11 | this._msgHandler = msgHandler || function() {}; 12 | this._onPairedHandler = options.onPairedHandler || function() {}; 13 | this._onDisposedHandler = options.onDisposedHandler || function() {}; 14 | 15 | // Identifier for all messages. 16 | // This will allow us to identify iframily messages and to match parent and child. 17 | this._iframilyUid = `${constants.FRAMILY_ID_PREFIX}${this._id}`; 18 | 19 | this._hasConnected = false; 20 | this._isDisposed = false; 21 | 22 | // Queue for messages requested to be sent before connection was made. 23 | this._msgQueue = []; 24 | 25 | this._pendingCb = {}; 26 | this._cbUid = 0; 27 | 28 | // Fix (public) methods binding to 'this'. 29 | this._bindPublicMethods(); 30 | 31 | this._init(); 32 | } 33 | 34 | // #region PRIVATE METHODS 35 | 36 | _bindPublicMethods() { 37 | PUBLIC_METHODS.forEach(function(method) { 38 | this[method] = this[method].bind(this); 39 | }.bind(this)); 40 | } 41 | 42 | _init() { throw new Error('To be overridden in extending classes.'); } 43 | 44 | // Sends all the queued messages. 45 | _sendQueuedMessages() { 46 | this._msgQueue.forEach((msgQueueFunc) => { 47 | msgQueueFunc(); 48 | }); 49 | 50 | this._msgQueue = []; 51 | } 52 | 53 | // The acutal 'postMessage' action. 54 | _postMessage(wrappedMsg) { 55 | try { 56 | this._targetWindow.postMessage(wrappedMsg, this._targetOrigin); 57 | } catch (err) { 58 | console.error('[Iframily] - Error when posting message:', err); 59 | } 60 | } 61 | 62 | // Sends a message. 63 | _sendMessage(msg, cbDefer, options = {}) { 64 | let wrappedMsg = { 65 | _iframilyUid: this._iframilyUid, 66 | _fromType: this._iframilyType, 67 | msg: msg 68 | }; 69 | 70 | // Callbacks logic. 71 | if (cbDefer) { 72 | this._cbUid++; 73 | this._pendingCb[this._cbUid] = cbDefer; 74 | wrappedMsg._cbUid = this._cbUid; 75 | } 76 | 77 | // If connected (or force send requested), send the message. 78 | // Otherwise, add it to the message queue. 79 | if (this._hasConnected || options.forceSend) { 80 | this._postMessage(wrappedMsg); 81 | } else { 82 | // eslint-disable-next-line no-console, max-len 83 | console.warn(`[Iframily] - No one paired yet, queuing message sent by ${this._iframilyType} id: ${this._id}`); 84 | this._msgQueue.push(this._postMessage.bind(this, wrappedMsg)); 85 | } 86 | } 87 | 88 | // Posts a response. 89 | _sendResponse(cbUid, options) { 90 | let wrappedMsg = { 91 | _iframilyUid: this._iframilyUid, 92 | _fromType: this._iframilyType, 93 | _isResponse: true, 94 | 95 | _cbUid: cbUid, 96 | _isResolved: options.isResolved, 97 | _isRejected: options.isRejected, 98 | _cbResolveValue: options.value, 99 | _cbRejectError: options.err, 100 | }; 101 | 102 | this._postMessage(wrappedMsg); 103 | } 104 | 105 | /* NOTE: overridden in extending class */ 106 | _handleMessage(wrappedMsg) { 107 | if (wrappedMsg._isResponse) { 108 | this._handleResponse(wrappedMsg); 109 | } else { 110 | Promise.resolve() 111 | .then(() => { 112 | return this._msgHandler(wrappedMsg.msg); 113 | }) 114 | .then((value) => { this._sendResponse(wrappedMsg._cbUid, { isResolved: true, value: value }); }) 115 | .catch((err) => { this._sendResponse(wrappedMsg._cbUid, { isRejected: true, err: err }); }); 116 | } 117 | } 118 | 119 | // Resolve the original message promise with the response value. 120 | _handleResponse(wrappedMsg) { 121 | if (wrappedMsg._cbUid) { 122 | let cbDefer = this._pendingCb[wrappedMsg._cbUid]; 123 | if (wrappedMsg._isResolved) { 124 | cbDefer.resolve(wrappedMsg._cbResolveValue); 125 | } else if (wrappedMsg._isRejected) { 126 | cbDefer.reject(wrappedMsg._cbRejectError); 127 | } else { 128 | // eslint-disable-next-line max-len 129 | throw new Error(`[Iframily] - Missing resolve/reject information on response sent by ${wrappedMsg._fromType} id: ${wrappedMsg._iframilyUid}`); 130 | } 131 | 132 | // Cleanup. 133 | delete this._pendingCb[wrappedMsg._cbUid]; 134 | } 135 | } 136 | 137 | _displayDisposedError() { 138 | console.error('[Iframily] - Attempting to use a disposed instance'); 139 | } 140 | 141 | // #endregion 142 | 143 | // #region MAIN API 144 | 145 | sendMessage(msg) { 146 | if (this._isDisposed) { 147 | return this._displayDisposedError(); 148 | } 149 | 150 | return new Promise((resolve, reject) => { 151 | this._sendMessage(msg, { resolve, reject }); 152 | }); 153 | } 154 | 155 | /* NOTE: overridden in extending class */ 156 | dispose() { 157 | if (this._isDisposed) { 158 | return this._displayDisposedError(); 159 | } 160 | 161 | this._hasConnected = false; 162 | this._msgQueue = []; 163 | 164 | this._isDisposed = true; 165 | this._onDisposedHandler(); 166 | } 167 | 168 | get isDisposed() { 169 | return this._isDisposed; 170 | } 171 | 172 | get id() { 173 | return this._id; 174 | } 175 | 176 | // #endregion 177 | }; 178 | -------------------------------------------------------------------------------- /src/child.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('./base'); 4 | const constants = require('./constants'); 5 | 6 | module.exports = class Child extends Base { 7 | _init() { 8 | this._iframilyType = constants.CHILD; 9 | this._targetWindow = window.parent; 10 | 11 | this._handleMessage = this._handleMessage.bind(this); 12 | window.addEventListener('message', this._handleMessage); 13 | 14 | // Attempt to connect to parent in interval (do first attempt immediately). 15 | let attemptToConnectFunc = () => { 16 | // 'forceSend' set true since we want to send this message even though we are not connected yet. 17 | this._sendMessage(constants.FRAMILY_INIT, null, { forceSend: true }); 18 | 19 | return attemptToConnectFunc; 20 | }; 21 | this._attemptToConnectInterval = setInterval(attemptToConnectFunc(), constants.ATTEMPT_TO_CONNECT_INTERVAL); 22 | } 23 | 24 | _handleMessage(event) { 25 | // Validate that the sender origin matches the target origin specified. 26 | if (this._targetOrigin !== '*' && event.origin !== this._targetOrigin) { 27 | return; 28 | } 29 | 30 | let eventData = event && event.data; 31 | 32 | // Only handle this iframily message (according id). 33 | if (eventData && eventData._iframilyUid === this._iframilyUid && eventData._fromType === constants.PARENT) { 34 | // If not connected and got init successful message => init connection. 35 | // If connected => handle it. 36 | if (!this._hasConnected && eventData.msg === constants.FRAMILY_INIT_SUCCESSFUL) { 37 | this._hasConnected = true; 38 | clearInterval(this._attemptToConnectInterval); 39 | 40 | this._onPairedHandler(); 41 | 42 | this._sendQueuedMessages(); 43 | } else if (this._hasConnected) { 44 | super._handleMessage(eventData); 45 | } 46 | } 47 | } 48 | 49 | dispose() { 50 | super.dispose(); 51 | 52 | window.removeEventListener('message', this._handleMessage); 53 | 54 | // Don't attempt to connect anymore. 55 | clearInterval(this._attemptToConnectInterval); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | PARENT: 'parent', 5 | CHILD: 'child', 6 | FRAMILY_ID_PREFIX: '__iframily__', 7 | FRAMILY_INIT: 'iframily_init', 8 | FRAMILY_INIT_SUCCESSFUL: 'iframily_init_successful', 9 | DANGEROUSLY_SET_WILDCARD: 'DANGEROUSLY_SET_WILDCARD', 10 | ATTEMPT_TO_CONNECT_INTERVAL: 200, 11 | README_SINGLETON_URL: 'https://github.com/EkoLabs/iframily#----iframily-singleton----' 12 | }; 13 | -------------------------------------------------------------------------------- /src/iframily.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Child = require('./child'); 4 | const Parent = require('./parent'); 5 | const constants = require('./constants'); 6 | 7 | let parentIframilies = {}; 8 | let childIframilies = {}; 9 | 10 | function validateIdAvailable(id, iframilies, type) { 11 | if (iframilies[id] && !iframilies[id].isDisposed) { 12 | // eslint-disable-next-line max-len 13 | console.error(`[Iframily] - A ${type} iframily with id "${id}" was already initialized, please use another id or dispose the existing one first.`); 14 | return false; 15 | } 16 | 17 | return true; 18 | } 19 | 20 | function validateTargetOrigin(targetOrigin, id) { 21 | if (!targetOrigin) { 22 | console.error(`[Iframily] - Missing "targetOrigin" argument, not initializing "${id}" iframily id.`); 23 | return false; 24 | } 25 | 26 | if (typeof targetOrigin !== 'string') { 27 | // eslint-disable-next-line max-len 28 | console.error(`[Iframily] - "targetOrigin" is of type ${typeof targetOrigin} but must be of type string, not initializing "${id}" iframily id.`); 29 | return false; 30 | } 31 | 32 | if (targetOrigin === '*') { 33 | // eslint-disable-next-line max-len 34 | console.error(`[Iframily] - "*" (wildcard) is not allowed for "targetOrigin" argument. If you are sure about what you are doing, use "${constants.DANGEROUSLY_SET_WILDCARD}". See more info here: ${constants.README_SINGLETON_URL}`); 35 | return false; 36 | } 37 | 38 | return true; 39 | } 40 | 41 | function validateInFrame(id) { 42 | if (window.parent === window) { 43 | // eslint-disable-next-line max-len 44 | console.error(`[Iframily] - Attempted to initialize a child iframily in a non embedded window, not initializing "${id}" iframily id.`); 45 | return false; 46 | } 47 | 48 | return true; 49 | } 50 | 51 | module.exports = class Iframily { 52 | static initParent(id, targetOrigin, msgHandler, options = {}) { 53 | /* eslint-disable max-statements-per-line */ 54 | if (!validateIdAvailable(id, parentIframilies, 'parent')) { return; } 55 | if (!validateTargetOrigin(targetOrigin, id)) { return; } 56 | /* eslint-enable max-statements-per-line */ 57 | 58 | parentIframilies[id] = new Parent(id, targetOrigin, msgHandler, options); 59 | return parentIframilies[id]; 60 | } 61 | 62 | static initChild(id, targetOrigin, msgHandler, options = {}) { 63 | /* eslint-disable max-statements-per-line */ 64 | if (!validateIdAvailable(id, childIframilies, 'child')) { return; } 65 | if (!validateTargetOrigin(targetOrigin, id)) { return; } 66 | if (!validateInFrame(id)) { return; } 67 | /* eslint-enable max-statements-per-line */ 68 | 69 | childIframilies[id] = new Child(id, targetOrigin, msgHandler, options); 70 | return childIframilies[id]; 71 | } 72 | 73 | static isIframilyMessage(event) { 74 | let eventData = event && event.data; 75 | return eventData && eventData._iframilyUid && eventData._iframilyUid.includes(constants.FRAMILY_ID_PREFIX); 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /src/parent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Base = require('./base'); 4 | const constants = require('./constants'); 5 | 6 | module.exports = class Parent extends Base { 7 | _init() { 8 | this._iframilyType = constants.PARENT; 9 | 10 | this._handleMessage = this._handleMessage.bind(this); 11 | window.addEventListener('message', this._handleMessage); 12 | } 13 | 14 | _handleMessage(event) { 15 | // Validate that the sender origin matches the target origin specified. 16 | if (this._targetOrigin !== '*' && event.origin !== this._targetOrigin) { 17 | return; 18 | } 19 | 20 | // Ignore messages sent from the current window to avoid child connections that are in the same frame. 21 | if (event.source === window) { 22 | return; 23 | } 24 | 25 | let eventData = event && event.data; 26 | 27 | // Only handle this iframily message (according id) and originating from child. 28 | if (eventData && eventData._iframilyUid === this._iframilyUid && eventData._fromType === constants.CHILD) { 29 | // If got init request (and not connected already) => init connection. 30 | // If connected => handle it. 31 | if (eventData.msg === constants.FRAMILY_INIT) { 32 | // Child might still be sending connection init messages even though 33 | // we already started the connection process, ignore them. 34 | if (!this._hasConnected) { 35 | this._targetWindow = event.source; 36 | 37 | // Set that we are connected and send init successful message. 38 | // NOTE: Setting connected first is important in order for the message to be sent. 39 | this._hasConnected = true; 40 | 41 | this._onPairedHandler(); 42 | 43 | // NOTE: Sending the 'init successful' event before sending queued messages is also important 44 | // NOTE: in order for child to be connected before receiving messages. 45 | this._sendMessage(constants.FRAMILY_INIT_SUCCESSFUL); 46 | 47 | this._sendQueuedMessages(); 48 | } 49 | } else if (this._hasConnected) { 50 | super._handleMessage(eventData); 51 | } 52 | } 53 | } 54 | 55 | dispose() { 56 | super.dispose(); 57 | 58 | window.removeEventListener('message', this._handleMessage); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | env: { 5 | node: true, 6 | jest: true 7 | }, 8 | parserOptions: { 9 | ecmaVersion: 2018 10 | }, 11 | globals: { 12 | page: 'readonly' 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /test/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const iframilyConstants = require('../src/constants'); 4 | 5 | const FRAME_TYPE_PARENT = 'parent'; 6 | const FRAME_TYPE_CHILD = 'child'; 7 | const FRAME_TYPE_CHILD1 = 'child1'; 8 | const FRAME_TYPE_CHILD2 = 'child2'; 9 | const FRAME_TYPE_CHILD2_INNER_CHILD = 'child2_innerchild'; 10 | 11 | const DEFAULT_FRAMILY_VAR_NAME = 'iframily'; 12 | const DEFAULT_FRAMILY_ID = 'testId-default'; 13 | 14 | const INIT_METHOD_NAMES = { 15 | [FRAME_TYPE_PARENT]: 'initParent', 16 | [FRAME_TYPE_CHILD]: 'initChild', 17 | [FRAME_TYPE_CHILD1]: 'initChild', 18 | [FRAME_TYPE_CHILD2]: 'initChild', 19 | [FRAME_TYPE_CHILD2_INNER_CHILD]: 'initChild', 20 | }; 21 | 22 | module.exports = { 23 | DANGEROUSLY_SET_WILDCARD: iframilyConstants.DANGEROUSLY_SET_WILDCARD, 24 | README_SINGLETON_URL: iframilyConstants.README_SINGLETON_URL, 25 | FRAME_TYPE_PARENT, 26 | FRAME_TYPE_CHILD, 27 | FRAME_TYPE_CHILD1, 28 | FRAME_TYPE_CHILD2, 29 | FRAME_TYPE_CHILD2_INNER_CHILD, 30 | DEFAULT_FRAMILY_VAR_NAME, 31 | DEFAULT_FRAMILY_ID, 32 | INIT_METHOD_NAMES 33 | }; 34 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = require('./constants'); 4 | 5 | let framesMap = {}; 6 | function resetFrames() { 7 | framesMap = {}; 8 | } 9 | function getFrame(type) { 10 | return framesMap[type]; 11 | } 12 | function setFrame(frame, type) { 13 | framesMap[type] = frame; 14 | } 15 | 16 | async function initIframily(type, options = {}) { 17 | let iframilyVarName = options.iframilyVarName || constants.DEFAULT_FRAMILY_VAR_NAME; 18 | let id = options.id || constants.DEFAULT_FRAMILY_ID; 19 | let targetOrigin = options.targetOrigin || constants.DANGEROUSLY_SET_WILDCARD; 20 | 21 | let frame = framesMap[type]; 22 | let initMethodName = constants.INIT_METHOD_NAMES[type]; 23 | 24 | await frame.evaluate((_iframilyVarName, _id, _targetOrigin, _initMethodName) => { 25 | // This object might have already been initialized by other iframilies init. 26 | window.messagesReceived = window.messagesReceived || {}; 27 | 28 | // Messages received specific to this iframily id. 29 | window.messagesReceived[_id] = []; 30 | 31 | window.pairedCount = window.pairedCount || {}; 32 | window.disposedCount = window.disposedCount || {}; 33 | window.pairedCount[_id] = window.pairedCount[_id] || 0; 34 | window.disposedCount[_id] = window.disposedCount[_id] || 0; 35 | let onPairedHandler = function() { 36 | window.pairedCount[_id]++; 37 | }; 38 | let onDisposedHandler = function() { 39 | window.disposedCount[_id]++; 40 | }; 41 | 42 | window[_iframilyVarName] = window.Iframily[_initMethodName](_id, _targetOrigin, (msg) => { 43 | window.messagesReceived[_id].push(msg); 44 | }, { 45 | onPairedHandler: onPairedHandler, 46 | onDisposedHandler: onDisposedHandler 47 | }); 48 | }, iframilyVarName, id, targetOrigin, initMethodName); 49 | } 50 | 51 | async function sendMessage(type, msg, options = {}) { 52 | let iframilyVarName = options.iframilyVarName || constants.DEFAULT_FRAMILY_VAR_NAME; 53 | 54 | let frame = framesMap[type]; 55 | 56 | await frame.evaluate((_iframilyVarName, _msg) => { 57 | window[_iframilyVarName].sendMessage(_msg); 58 | }, iframilyVarName, msg); 59 | } 60 | 61 | // NOTE: undefined will convert to null (since in array)... https://github.com/puppeteer/puppeteer/issues/1510 62 | function getMessages(type, options = {}) { 63 | let id = options.id || constants.DEFAULT_FRAMILY_ID; 64 | 65 | let frame = framesMap[type]; 66 | return frame.evaluate(_id => window.messagesReceived[_id], id); 67 | } 68 | 69 | async function waitForMessages(type, expectedTotalMessagesCount, options = {}) { 70 | let id = options.id || constants.DEFAULT_FRAMILY_ID; 71 | 72 | let frame = framesMap[type]; 73 | 74 | await frame.waitForFunction((_id, _expectedTotalMessagesCount) => { 75 | return window.messagesReceived[_id].length === _expectedTotalMessagesCount; 76 | }, {}, id, expectedTotalMessagesCount); 77 | 78 | return getMessages(type, options); 79 | } 80 | 81 | async function dispose(type, options = {}) { 82 | let iframilyVarName = options.iframilyVarName || constants.DEFAULT_FRAMILY_VAR_NAME; 83 | 84 | let frame = framesMap[type]; 85 | 86 | await frame.evaluate((_iframilyVarName) => { 87 | window[_iframilyVarName].dispose(); 88 | }, iframilyVarName); 89 | } 90 | 91 | function isDisposed(type, options = {}) { 92 | let iframilyVarName = options.iframilyVarName || constants.DEFAULT_FRAMILY_VAR_NAME; 93 | 94 | let frame = framesMap[type]; 95 | 96 | return frame.evaluate((_iframilyVarName) => { 97 | return window[_iframilyVarName].isDisposed; 98 | }, iframilyVarName); 99 | } 100 | 101 | function getPairedCount(type, options = {}) { 102 | let id = options.id || constants.DEFAULT_FRAMILY_ID; 103 | 104 | let frame = framesMap[type]; 105 | 106 | return frame.evaluate((_id) => { 107 | return window.pairedCount[_id]; 108 | }, id); 109 | } 110 | 111 | function getDisposedCount(type, options = {}) { 112 | let id = options.id || constants.DEFAULT_FRAMILY_ID; 113 | 114 | let frame = framesMap[type]; 115 | 116 | return frame.evaluate((_id) => { 117 | return window.disposedCount[_id]; 118 | }, id); 119 | } 120 | 121 | function wrapConsoleError(type) { 122 | let frame = framesMap[type]; 123 | 124 | return frame.evaluate(() => { 125 | let origConsoleError = console.error; 126 | 127 | window.errorMessages = []; 128 | let errorWrapper = function() { 129 | let argsArr = Array.from(arguments); 130 | window.errorMessages.push(argsArr); 131 | origConsoleError.apply(null, arguments); 132 | }; 133 | 134 | console.error = errorWrapper; 135 | }); 136 | } 137 | 138 | function getConsoleErrors(type) { 139 | let frame = framesMap[type]; 140 | return frame.evaluate(() => window.errorMessages); 141 | } 142 | 143 | module.exports = { 144 | resetFrames, 145 | getFrame, 146 | setFrame, 147 | 148 | initIframily, 149 | sendMessage, 150 | getMessages, 151 | waitForMessages, 152 | dispose, 153 | isDisposed, 154 | getPairedCount, 155 | getDisposedCount, 156 | 157 | wrapConsoleError, 158 | getConsoleErrors 159 | }; 160 | -------------------------------------------------------------------------------- /test/public/basic/child.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
child
7 | 8 | -------------------------------------------------------------------------------- /test/public/basic/parent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
parent
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/public/crossdomain/child.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
child
7 | 8 | -------------------------------------------------------------------------------- /test/public/crossdomain/parent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
parent
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/public/multiple_frames/child1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
child1
7 | 8 | -------------------------------------------------------------------------------- /test/public/multiple_frames/child2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
child2
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/public/multiple_frames/child2innerchild.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
child2innerchild
7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/public/multiple_frames/parent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
parent
7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, node */ 2 | 'use strict'; 3 | 4 | const webpack = require('webpack'); 5 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 6 | const path = require('path'); 7 | 8 | module.exports = (env, argv) => { 9 | const isDevelopmentMode = argv.mode === 'development'; 10 | 11 | const config = { 12 | entry: path.join(__dirname, 'src', 'iframily.js'), 13 | 14 | output: { 15 | path: path.join(__dirname, 'dist'), 16 | filename: 'iframily.min.js', 17 | library: 'Iframily', 18 | libraryTarget: 'umd', 19 | 20 | // NOTE: Important in order for the library to support handling "require()" in node env. 21 | // NOTE: see https://github.com/webpack/webpack/issues/6677 22 | // NOTE: see https://medium.com/@JakeXiao/window-is-undefined-in-umd-library-output-for-webpack4-858af1b881df 23 | // NOTE: see https://webpack.js.org/configuration/output/#outputglobalobject 24 | globalObject: 'this' 25 | }, 26 | 27 | devtool: isDevelopmentMode ? 'eval' : '', 28 | 29 | module: { 30 | rules: [ 31 | { 32 | // NOTE: This lints only files bundled by webpack (and not the "tests" folder for example...) 33 | enforce: 'pre', 34 | test: /\.js$/, 35 | exclude: /node_modules/, 36 | loader: 'eslint-loader', 37 | options: { 38 | // Show only errors while developing. 39 | quiet: isDevelopmentMode, 40 | 41 | // Fail on warning/errors when not developing. 42 | failOnWarning: !isDevelopmentMode, 43 | failOnError: !isDevelopmentMode 44 | }, 45 | }, 46 | { 47 | test: /\.js$/, 48 | exclude: path.join(__dirname, 'node_modules'), 49 | use: { 50 | loader: 'babel-loader', 51 | options: { 52 | presets: ['@babel/preset-env'] 53 | } 54 | } 55 | } 56 | ] 57 | }, 58 | 59 | plugins: [new CleanWebpackPlugin()] 60 | }; 61 | 62 | if (isDevelopmentMode) { 63 | config.plugins = config.plugins || []; 64 | 65 | // Used for tests to identify development bundle. 66 | config.plugins.push(new webpack.BannerPlugin({ 67 | banner: 'DEVELOPMENT BUNDLE' 68 | })); 69 | } 70 | 71 | return config; 72 | }; 73 | --------------------------------------------------------------------------------