Repository: bem/bem-core
Branch: v5
Commit: 7a9e9327a56d
Files: 318
Total size: 1.2 MB
Directory structure:
gitextract_z09_3irc/
├── .github/
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .husky/
│ └── pre-commit
├── .nvmrc
├── CHANGELOG.md
├── CHANGELOG.ru.md
├── CLA.md
├── CONTRIBUTING.md
├── CONTRIBUTING.ru.md
├── LICENSE.txt
├── MIGRATION.md
├── MIGRATION.ru.md
├── PLAN.md
├── README.md
├── README.ru.md
├── SUMMARY.md
├── build/
│ ├── platforms/
│ │ ├── desktop.js
│ │ └── touch.js
│ ├── plugins/
│ │ ├── vite-plugin-bem-levels.js
│ │ └── vite-plugin-bem-levels.test.js
│ ├── vite.config.js
│ └── vite.test.config.js
├── common.blocks/
│ ├── clearfix/
│ │ ├── clearfix.css
│ │ ├── clearfix.en.md
│ │ ├── clearfix.en.title.txt
│ │ ├── clearfix.ru.md
│ │ └── clearfix.ru.title.txt
│ ├── cookie/
│ │ ├── cookie.en.md
│ │ ├── cookie.js
│ │ ├── cookie.ru.md
│ │ └── cookie.spec.js
│ ├── dom/
│ │ ├── dom.deps.js
│ │ ├── dom.en.md
│ │ ├── dom.js
│ │ ├── dom.ru.md
│ │ └── dom.spec.js
│ ├── events/
│ │ ├── __channels/
│ │ │ ├── events__channels.deps.js
│ │ │ ├── events__channels.en.md
│ │ │ ├── events__channels.ru.md
│ │ │ └── events__channels.vanilla.js
│ │ ├── __observable/
│ │ │ ├── _type/
│ │ │ │ ├── events__observable_type_bem-dom.deps.js
│ │ │ │ ├── events__observable_type_bem-dom.js
│ │ │ │ └── events__observable_type_bem-dom.spec.js
│ │ │ ├── events__observable.deps.js
│ │ │ ├── events__observable.js
│ │ │ └── events__observable.spec.js
│ │ ├── events.deps.js
│ │ ├── events.en.md
│ │ ├── events.ru.md
│ │ ├── events.spec.js
│ │ └── events.vanilla.js
│ ├── functions/
│ │ ├── __debounce/
│ │ │ ├── functions__debounce.spec.js
│ │ │ └── functions__debounce.vanilla.js
│ │ ├── __throttle/
│ │ │ ├── functions__throttle.spec.js
│ │ │ └── functions__throttle.vanilla.js
│ │ ├── functions.en.md
│ │ ├── functions.ru.md
│ │ ├── functions.spec.js
│ │ └── functions.vanilla.js
│ ├── i-bem/
│ │ ├── __collection/
│ │ │ ├── i-bem__collection.js
│ │ │ └── i-bem__collection.spec.js
│ │ ├── __internal/
│ │ │ ├── i-bem__internal.ru.title.txt
│ │ │ ├── i-bem__internal.spec.js
│ │ │ └── i-bem__internal.vanilla.js
│ │ ├── i-bem.deps.js
│ │ ├── i-bem.en.md
│ │ ├── i-bem.en.title.txt
│ │ ├── i-bem.ru.md
│ │ ├── i-bem.ru.title.txt
│ │ ├── i-bem.spec.js
│ │ └── i-bem.vanilla.js
│ ├── i-bem-dom/
│ │ ├── __collection/
│ │ │ ├── i-bem-dom__collection.deps.js
│ │ │ ├── i-bem-dom__collection.js
│ │ │ └── i-bem-dom__collection.spec.js
│ │ ├── __events/
│ │ │ ├── _type/
│ │ │ │ ├── i-bem-dom__events_type_bem.deps.js
│ │ │ │ ├── i-bem-dom__events_type_bem.js
│ │ │ │ ├── i-bem-dom__events_type_bem.spec.js
│ │ │ │ ├── i-bem-dom__events_type_dom.deps.js
│ │ │ │ ├── i-bem-dom__events_type_dom.js
│ │ │ │ └── i-bem-dom__events_type_dom.spec.js
│ │ │ ├── i-bem-dom__events.deps.js
│ │ │ └── i-bem-dom__events.js
│ │ ├── __init/
│ │ │ ├── _auto/
│ │ │ │ ├── i-bem-dom__init_auto.deps.js
│ │ │ │ └── i-bem-dom__init_auto.js
│ │ │ ├── i-bem-dom__init.deps.js
│ │ │ ├── i-bem-dom__init.js
│ │ │ └── i-bem-dom__init.spec.js
│ │ ├── i-bem-dom.deps.js
│ │ ├── i-bem-dom.en.md
│ │ ├── i-bem-dom.js
│ │ ├── i-bem-dom.ru.md
│ │ ├── i-bem-dom.spec.js
│ │ └── i-bem-dom.tests/
│ │ ├── benchmarks.bemjson.js
│ │ └── benchmarks.blocks/
│ │ ├── b1/
│ │ │ ├── b1.deps.js
│ │ │ └── b1.js
│ │ ├── b2/
│ │ │ ├── b2.deps.js
│ │ │ └── b2.js
│ │ └── page/
│ │ ├── page.deps.js
│ │ └── page.js
│ ├── i18n/
│ │ ├── i18n.deps.js
│ │ ├── i18n.en.md
│ │ ├── i18n.i18n.js
│ │ ├── i18n.ru.md
│ │ ├── i18n.test.js
│ │ ├── i18n.tests/
│ │ │ ├── blocks/
│ │ │ │ ├── logo/
│ │ │ │ │ ├── logo.bemhtml.js
│ │ │ │ │ ├── logo.bh.js
│ │ │ │ │ ├── logo.deps.js
│ │ │ │ │ ├── logo.i18n/
│ │ │ │ │ │ ├── en.js
│ │ │ │ │ │ └── ru.js
│ │ │ │ │ ├── logo.i18n.js
│ │ │ │ │ └── logo.js
│ │ │ │ └── page/
│ │ │ │ ├── __js/
│ │ │ │ │ ├── page__js.bemhtml.js
│ │ │ │ │ └── page__js.bh.js
│ │ │ │ └── page.i18n/
│ │ │ │ ├── en.js
│ │ │ │ └── ru.js
│ │ │ └── simple.bemjson.js
│ │ └── i18n.tmpl-specs/
│ │ ├── 10-simple.bemjson.js
│ │ ├── 10-simple.html
│ │ └── blocks/
│ │ └── greeting-card/
│ │ ├── greeting-card.bemhtml.js
│ │ ├── greeting-card.bh.js
│ │ └── greeting-card.deps.js
│ ├── identify/
│ │ ├── identify.en.md
│ │ ├── identify.ru.md
│ │ ├── identify.spec.js
│ │ └── identify.vanilla.js
│ ├── idle/
│ │ ├── _start/
│ │ │ └── idle_start_auto.js
│ │ ├── idle.deps.js
│ │ ├── idle.en.md
│ │ ├── idle.js
│ │ └── idle.ru.md
│ ├── inherit/
│ │ ├── inherit.en.md
│ │ ├── inherit.ru.md
│ │ ├── inherit.spec.js
│ │ └── inherit.vanilla.js
│ ├── jquery/
│ │ ├── __config/
│ │ │ ├── jquery__config.js
│ │ │ └── jquery__config.ru.md
│ │ ├── jquery.deps.js
│ │ ├── jquery.en.md
│ │ ├── jquery.js
│ │ ├── jquery.ru.md
│ │ └── jquery.ru.title.txt
│ ├── keyboard/
│ │ ├── __codes/
│ │ │ └── keyboard__codes.js
│ │ ├── keyboard.en.md
│ │ └── keyboard.ru.md
│ ├── loader/
│ │ ├── _type/
│ │ │ ├── loader_type_bundle.js
│ │ │ ├── loader_type_js.js
│ │ │ └── loader_type_js.spec.js
│ │ ├── loader.en.md
│ │ └── loader.ru.md
│ ├── next-tick/
│ │ ├── next-tick.en.md
│ │ ├── next-tick.ru.md
│ │ ├── next-tick.spec.js
│ │ └── next-tick.vanilla.js
│ ├── objects/
│ │ ├── objects.en.md
│ │ ├── objects.ru.md
│ │ ├── objects.spec.js
│ │ └── objects.vanilla.js
│ ├── page/
│ │ ├── __css/
│ │ │ ├── page__css.bemhtml.js
│ │ │ └── page__css.bh.js
│ │ ├── __js/
│ │ │ ├── page__js.bemhtml.js
│ │ │ └── page__js.bh.js
│ │ ├── page.bemhtml.js
│ │ ├── page.bh.js
│ │ ├── page.deps.js
│ │ ├── page.en.md
│ │ ├── page.en.title.txt
│ │ ├── page.examples/
│ │ │ ├── .bem/
│ │ │ │ └── level.js
│ │ │ ├── 10-simple.bemjson.js
│ │ │ ├── 10-simple.ru.title.txt
│ │ │ ├── 10-simple.ru.wiki
│ │ │ ├── 20-doctype.bemjson.js
│ │ │ └── 20-doctype.ru.title.txt
│ │ ├── page.ru.md
│ │ ├── page.ru.title.txt
│ │ └── page.tmpl-specs/
│ │ ├── 00-empty.bemjson.js
│ │ ├── 00-empty.html
│ │ ├── 10-simple.bemjson.js
│ │ ├── 10-simple.html
│ │ ├── 20-style.bemjson.js
│ │ ├── 20-style.html
│ │ ├── 25-styles.bemjson.js
│ │ ├── 25-styles.html
│ │ ├── 30-scripts.bemjson.js
│ │ ├── 30-scripts.html
│ │ ├── 40-nonce.bemjson.js
│ │ ├── 40-nonce.html
│ │ ├── 60-x-ua-compatible.bemjson.js
│ │ ├── 60-x-ua-compatible.html
│ │ ├── 70-lang.bemjson.js
│ │ └── 70-lang.html
│ ├── strings/
│ │ ├── __escape/
│ │ │ ├── strings__escape.spec.js
│ │ │ └── strings__escape.vanilla.js
│ │ ├── strings.en.md
│ │ └── strings.ru.md
│ ├── tick/
│ │ ├── _start/
│ │ │ └── tick_start_auto.vanilla.js
│ │ ├── tick.deps.js
│ │ ├── tick.en.md
│ │ ├── tick.ru.md
│ │ ├── tick.spec.js
│ │ └── tick.vanilla.js
│ ├── ua/
│ │ ├── __svg/
│ │ │ ├── ua__svg.bemhtml.js
│ │ │ ├── ua__svg.bh.js
│ │ │ ├── ua__svg.deps.js
│ │ │ ├── ua__svg.en.title.txt
│ │ │ ├── ua__svg.ru.title.txt
│ │ │ └── ua__svg.tmpl-specs/
│ │ │ ├── 00-simple.bemjson.js
│ │ │ └── 00-simple.html
│ │ ├── ua.bemhtml.js
│ │ ├── ua.bh.js
│ │ ├── ua.en.md
│ │ ├── ua.en.title.txt
│ │ ├── ua.ru.md
│ │ ├── ua.ru.title.txt
│ │ └── ua.tmpl-specs/
│ │ ├── 00-simple.bemjson.js
│ │ └── 00-simple.html
│ └── uri/
│ ├── __querystring/
│ │ ├── uri__querystring.deps.js
│ │ ├── uri__querystring.spec.js
│ │ └── uri__querystring.vanilla.js
│ ├── uri.en.md
│ ├── uri.ru.md
│ ├── uri.spec.js
│ └── uri.vanilla.js
├── common.bundles/
│ └── index/
│ ├── blocks/
│ │ └── square/
│ │ ├── _color/
│ │ │ └── square_color_green.css
│ │ ├── square.css
│ │ ├── square.deps.js
│ │ └── square.js
│ └── index.bemjson.js
├── common.docs/
│ ├── bemjson/
│ │ ├── bemjson.en.md
│ │ └── bemjson.ru.md
│ └── i-bem-js/
│ ├── i-bem-js-collections.en.md
│ ├── i-bem-js-collections.ru.md
│ ├── i-bem-js-common.en.md
│ ├── i-bem-js-common.ru.md
│ ├── i-bem-js-context.en.md
│ ├── i-bem-js-context.ru.md
│ ├── i-bem-js-decl.en.md
│ ├── i-bem-js-decl.ru.md
│ ├── i-bem-js-dom.en.md
│ ├── i-bem-js-dom.ru.md
│ ├── i-bem-js-events.en.md
│ ├── i-bem-js-events.ru.md
│ ├── i-bem-js-extras.en.md
│ ├── i-bem-js-extras.ru.md
│ ├── i-bem-js-html-binding.en.md
│ ├── i-bem-js-html-binding.ru.md
│ ├── i-bem-js-init.en.md
│ ├── i-bem-js-init.ru.md
│ ├── i-bem-js-interact.en.md
│ ├── i-bem-js-interact.ru.md
│ ├── i-bem-js-params.en.md
│ ├── i-bem-js-params.ru.md
│ ├── i-bem-js-states.en.md
│ ├── i-bem-js-states.ru.md
│ ├── i-bem-js.en.md
│ └── i-bem-js.ru.md
├── desktop.blocks/
│ ├── jquery/
│ │ ├── __config/
│ │ │ ├── jquery__config.deps.js
│ │ │ └── jquery__config.js
│ │ └── __event/
│ │ └── _type/
│ │ ├── jquery__event_type_winresize.deps.js
│ │ └── jquery__event_type_winresize.js
│ ├── page/
│ │ ├── __conditional-comment/
│ │ │ ├── page__conditional-comment.bemhtml.js
│ │ │ ├── page__conditional-comment.bh.js
│ │ │ └── page__conditional-comment.ru.md
│ │ ├── page.deps.js
│ │ ├── page.examples/
│ │ │ ├── .bem/
│ │ │ │ └── level.js
│ │ │ ├── 40-es5-shims.bemjson.js
│ │ │ └── 40-es5-shims.ru.title.txt
│ │ ├── page.ru.md
│ │ └── page.tmpl-specs/
│ │ ├── 50-conditions.bemjson.js
│ │ ├── 50-conditions.html
│ │ ├── 60-conditional-comments.bemjson.js
│ │ ├── 60-conditional-comments.html
│ │ ├── 70-custom-x-ua-compatible.bemjson.js
│ │ └── 70-custom-x-ua-compatible.html
│ └── ua/
│ ├── ua.js
│ └── ua.ru.md
├── eslint.config.js
├── jsdoc.config.json
├── package.json
├── playwright.config.js
├── test/
│ ├── browser/
│ │ ├── bemhtml-shim.js
│ │ ├── entry.js
│ │ ├── index.html
│ │ └── modules-shim.js
│ ├── browser.spec.js
│ └── dist/
│ ├── assets/
│ │ └── test.html
│ ├── build-fixtures.js
│ ├── config.js
│ └── fixtures/
│ ├── chai.js
│ ├── desktop.html
│ ├── mocha.css
│ ├── mocha.js
│ ├── sinon-chai.js
│ ├── sinon.js
│ └── touch.html
└── touch.blocks/
├── page/
│ ├── __icon/
│ │ ├── page__icon.bemhtml.js
│ │ └── page__icon.bh.js
│ ├── page.bemhtml.js
│ ├── page.bh.js
│ ├── page.deps.js
│ ├── page.ru.md
│ └── page.tmpl-specs/
│ ├── 00-empty.html
│ ├── 10-simple.html
│ ├── 20-style.html
│ ├── 25-styles.html
│ ├── 30-scripts.html
│ ├── 40-nonce.html
│ ├── 60-x-ua-compatible.html
│ ├── 70-lang.html
│ ├── 70-zoom.bemjson.js
│ └── 70-zoom.html
└── ua/
├── __dom/
│ ├── ua__dom.deps.js
│ ├── ua__dom.js
│ └── ua__dom.ru.md
├── ua.bemhtml.js
├── ua.bh.js
├── ua.deps.js
├── ua.js
└── ua.ru.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches: [master, v*]
pull_request:
branches: [master, v5]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm test
test-browser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm run test:browser
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run docs
- uses: actions/upload-artifact@v4
with:
name: jsdoc
path: docs/jsdoc/
================================================
FILE: .gitignore
================================================
.project
.settings
.bem/cache
.bem/snapshots
.enb/tmp
libs
node_modules
coverage.json
/coverage
*.bundles/*/*
!*.bundles/.bem/*
!*.bundles/*/blocks
!*.bundles/*/*.bemjson.js
/dist
/docs/jsdoc
/test-results
/*desktop.docs
/*touch.docs
/*.examples
/*.specs
/*.tmpl-specs
/*.tests
/*.sets
_book
================================================
FILE: .husky/pre-commit
================================================
npx lint-staged
================================================
FILE: .nvmrc
================================================
24
================================================
FILE: CHANGELOG.md
================================================
# Changelog
## 5.0.0
### Breaking changes
- **ESM-only**: The entire codebase has been migrated from the `ym` module system (`modules.define`/`modules.require`) to native ES modules (`import`/`export`). The `ym` runtime dependency has been removed.
- **Vite build system**: ENB and all 17+ associated packages have been replaced with [Vite](https://vite.dev/). A custom `vite-plugin-bem-levels` plugin handles BEM level scanning, `bem:*` virtual module resolution, and redefinition chain barrel generation.
- **jQuery 4.0**: The `jquery` peer dependency has been upgraded from `^3.x` to `^4.0.0`. Notable API change: `$.unique()` has been removed — use `$.uniqueSort()` instead.
- **Node.js 20+**: The minimum supported Node.js version is now 20 (was 8).
- **Native Promises**: `vow` has been removed. All code now uses native `Promise`.
- **Native test runner**: Server-side tests now use `node:test` and `node:assert` instead of `mocha`/`chai`.
- **Playwright**: Browser tests now use [Playwright](https://playwright.dev/) instead of `mocha-phantomjs` (PhantomJS is dead).
- **ESLint 10**: Linting has been migrated from `jshint`/`jscs` to ESLint 10 with flat config (`eslint.config.js`).
- **GitHub Actions CI**: Travis CI has been replaced with GitHub Actions.
- **Husky + lint-staged**: `git-hooks` has been replaced with `husky` and `lint-staged`.
### Notable changes
- All `.vanilla.js` and `.js` source files have been converted to ES modules with `import`/`export` syntax.
- Module redefinition chains (e.g., `jquery` with pointer event extensions) are now handled via auto-generated barrel files by `vite-plugin-bem-levels`.
- The `i18n` block retains its API but uses native ES modules internally.
- Browser spec tests (28 spec files, 500+ test cases) run via Vite dev server + Playwright.
- Vite produces platform-specific builds (`desktop`, `touch`) with jQuery as an external dependency.
### Removed packages
- **Build**: `enb`, `enb-bem-techs`, `enb-magic-factory`, `enb-magic-platform`, `enb-bemxjst`, `enb-bemxjst-6x`, `enb-bemxjst-7x`, `enb-bemxjst-i18n`, `enb-bh`, `enb-bh-i18n`, `enb-borschik`, `enb-css`, `enb-js`, `enb-bem-docs`, `enb-bem-examples`, `enb-bem-specs`, `enb-bem-tmpl-specs`, `enb-bem-i18n`, `borschik`
- **Linting**: `jscs`, `jscs-bem`, `jshint`, `jshint-groups`
- **Testing**: `mocha-phantomjs`, `istanbul`, `chai-as-promised`
- **Other**: `ym`, `vow`, `bower`, `git-hooks`, `gitbook-api`, `bem-naming`, `bem-walk`
### Removed config files
- `.enb/` directory (ENB build config)
- `.jshintrc`, `.jscs.json`, `.jshint-groups.js` (linting configs)
- `.bowerrc`, `bower.json` (Bower configs)
- `.travis.yml` (Travis CI config)
- `.githooks/` directory (git-hooks config)
## 4.3.1
### Bug fixes
- Reverted change that led to error when `lazyInit` in `declMod` was used ([#1594](https://github.com/bem/bem-core/pull/1594)).
## 4.3.0
### Notable changes
- `jQuery` was updated to 3.2.1 and 1.12.4 ([#1587](https://github.com/bem/bem-core/pull/1587)).
### Bug fixes
- Possibility to force `lazy` initialization from markup was fixed ([#1579](https://github.com/bem/bem-core/pull/1579)).
- Possibility to declare entities with mixins was fixed ([#1550](https://github.com/bem/bem-core/pull/1550)).
- Now `lazyInit` in `declMod` will throw an error ([#1579](https://github.com/bem/bem-core/pull/1579)).
- With bug in `isFunction` method of `functions` block, which works wrong for special functions ([#1577](https://github.com/bem/bem-core/pull/1577)).
### Other changes
- `.bemrc` config was added ([#1568](https://github.com/bem/bem-core/pull/1568)).
- `vow` was updated to 0.4.17 ([#1565](https://github.com/bem/bem-core/pull/1565)).
- `inherit` was updated to 2.2.6 ([#1519](https://github.com/bem/bem-core/pull/1519)).
- Private `_delInitedMod` method of `i-bem` block was removed ([#1523](https://github.com/bem/bem-core/pull/1523)).
- Removed code for old `bem-tools` (https://github.com/bem/bem-core/commit/e57678b2d64a3b976a53af4a7fa09bf918685821).
- npm dependencies were updated ([#1589](https://github.com/bem/bem-core/pull/1589)).
- Now tests are executed also on Node.js 8 (https://github.com/bem/bem-core/commit/dd7e5344a64ad1595eab28febb2242134fcedbb3).
- More specs for `i-bem-dom` were added ([#1517](https://github.com/bem/bem-core/pull/1517)).
- `gitbook` was added ([#1569](https://github.com/bem/bem-core/pull/1569)).
- JSDoc was fixed.
- Documentation updates.
## 4.2.1
### Bug fixes
- Fixed an issue with elems cache invalidation on DOM modifications ([#1487](https://github.com/bem/bem-core/issues/1487)).
- Fixed an issue in `i-bem-dom__events` when event's data was not passed to handler ([#1509](https://github.com/bem/bem-core/pull/1509)).
- Fixed method `isEditable` of `dom` module. Missing editable input types were added ([#1502](https://github.com/bem/bem-core/pull/1502)).
### Other changes
- Fixed syntax error in `i-bem-dom` JSDoc.
- Minor documentation updates.
- [CLA](https://github.com/bem/bem-core/blob/v4/CLA.md) introduced.
## 4.2.0
### Notable changes
- `bem-xjst 8.x` support was introduced in BEMHTML templates ([#1486](https://github.com/bem/bem-core/issues/1485)).
### Bug fixes
- `concat()` method was fixed in `i-bem-dom__collection` ([#1488](https://github.com/bem/bem-core/issues/1488)).
- An issue in `ua__dom` was fix ([#1479](https://github.com/bem/bem-core/issues/1478)).
- dist: `i-bem-dom__init_auto` was removed from `no-autoinit` bundle ([#1482](https://github.com/bem/bem-core/issues/1481)).
### Other changes
- Now `findChildBlock`, `findChildBlocks`, `findParentBlock`, `findParentBlocks`, `findMixedBlock` and `findMixedBlocks` methods throw an error if block is given as String ([#1469](https://github.com/bem/bem-core/pull/1469/)).
- `buildClassName` function was optimized ([#1404](https://github.com/bem/bem-core/pull/1404)).
- Docs: English translations were added ([#1483](https://github.com/bem/bem-core/pull/1483), [#1476](https://github.com/bem/bem-core/pull/1476), [#1475](https://github.com/bem/bem-core/pull/1475)).
- Migration: Added notes about bemTarget ([#1491](https://github.com/bem/bem-core/issues/1472)).
- Migration: added info about template options ([#1467](https://github.com/bem/bem-core/issues/1467)).
- Fixed issues in docs.
## 4.1.1
### Bug fixes
— Fixed a bug in `ua` block on `touch.blocks` level ([#1460](https://github.com/bem/bem-core/pull/1460)).
## 4.1.0
### Bug fixes
- Fixed a bug in `identify` returning different result on each call for `document` ([#1441](https://github.com/bem/bem-core/issues/1441)).
- `modules.define` recursion problem was fixed ([#1446](https://github.com/bem/bem-core/issues/1446)).
- Support for escaping in `ua` block was fixed ([#1435](https://github.com/bem/bem-core/issues/1435)).
- Workaround for `Array.prototype.push` bug in `Opera 41` was implemented.
- An issue with pointer events on iOS devices was fixed ([#1253](https://github.com/bem/bem-core/issues/1253)).
- An issue in `i-bem-dom__events` was fixed. Method `once()` was broken in some cases ([#1452](https://github.com/bem/bem-core/issues/1452)).
### Other changes
- Ability to specify `html@lang` attribute was added to `page` block ([#751](https://github.com/bem/bem-core/issues/751)).
## 4.0.0
### Breaking changes
- Changes in the `i-bem` block. See [MIGRATION.md](MIGRATION.md#changes-in-the-i-bem-block).
- Changes in the `querystring`. See [MIGRATION.md](MIGRATION.md#changes-in-the-querystring-block).
- The optional parameter `onlyGet` was removed from the `identify` module ([#1028](https://github.com/bem/bem-core/issues/1028)).
- All static methods were removed from the `events` module ([#1024](https://github.com/bem/bem-core/issues/1024)).
- The `result` field in `Event` class of `events` module was removed ([#1023](https://github.com/bem/bem-core/issues/1023)).
- The `css` element of the `page` block does not support auto insertion of conditional comments for IE ([#379](https://github.com/bem/bem-core/issues/379)).
## 3.2.0
### Bug fixes
- `modules.define` recursion problem was fixed ([#1446](https://github.com/bem/bem-core/issues/1446)).
- Support for escaping in `ua` block was fixed ([#1435](https://github.com/bem/bem-core/issues/1435)).
- Workaround for `Array.prototype.push` bug in `Opera 41` was implemented.
### Other changes
- Ability to specify `html@lang` attribute was added to `page` block ([#751](https://github.com/bem/bem-core/issues/751)).
## 3.1.0
### Bug fixes
- An issue in `getMods()` method of `i-bem` was fixed ([#1379](https://github.com/bem/bem-core/issues/1379)).
### Notable changes
- Templates: support for escaped mode was introduced ([#1406](https://github.com/bem/bem-core/issues/1406)).
### Other changes
- Minor documentation updates.
## 3.0.1
### Bug fixes
- An issue with pointer events on iOS devices was fixed ([#1253](https://github.com/bem/bem-core/issues/1253)).
## 3.0.0
### Breaking changes
- Base templates for `BEMHTML` and `BEMTREE` were removed ([#1258](https://github.com/bem/bem-core/issues/1258)). `bem-xjst` 6.3.0+ should be used instead.
- File extentions of BEMHTML templates were renamed from `*.bemhtml` to `*.bemhtml.js` ([#984](https://github.com/bem/bem-core/issues/984)). Please check that new extention is supported in you build config.
- `i-bem__i18n` element was removed ([#1304](https://github.com/bem/bem-core/issues/1304)). Please use `i18n` block for internationalization.
- `jquery__events_type_pointerclick` is not using [FastClick](https://github.com/ftlabs/fastclick) anymore ([#1088](https://github.com/bem/bem-core/issues/1088)).
### Notable changes
- `jQuery` was updated to 2.2.3 and 1.12.3 ([#1260](https://github.com/bem/bem-core/issues/1260)).
### Bug fixes
- An issue in `page` was fixed. ` ` had wrong `user-scalable` value on the touch level ([#1294](https://github.com/bem/bem-core/issues/1294)).
- An issue in `jquery__event_type_pointernative` which led to JS error in IE8 was fixed ([1317](https://github.com/bem/bem-core/issues/1317)).
### Other changes
- dist: Autoinitialisation of blocks is optional now ([#1271](https://github.com/bem/bem-core/issues/1271)).
## 2.9.0
### Notable changes
- `jQuery` was updated to 2.2.0 and 1.12.0 ([#1249](https://github.com/bem/bem-core/issues/1249)).
### Bug fixes
- Fixed bug in BEMHTML 1.x which leads to drop of `this.mods` in `reapply()` ([#97](https://github.com/bem/bem-xjst/issues/97)).
### Other changes
- `jquery__event_type_pointerpressrelease` now exposes `originalEvent` ([#1254](https://github.com/bem/bem-core/issues/1254)).
- dist: Support for `i18n` was added to dist ([#1212](https://github.com/bem/bem-core/issues/1212)).
- `page__css.bemhtml` template was updated to support new `bem-xjst` versions ([#1228](https://github.com/bem/bem-core/issues/1228)).
## 2.8.0
### Notable changes
- New [i18n](https://github.com/bem/bem-core/tree/v2/common.blocks/i18n) block was introduced, providing support for internationalization ([#1074](https://github.com/bem/bem-core/issues/1074)).
- Now jQuery is included via `https` by default ([#1202](https://github.com/bem/bem-core/issues/1202)).
- Dependency on `bemhtml-compat` was dropped ([#1186](https://github.com/bem/bem-core/issues/1186)). Users of `bem-tools` need to run `npm i bemhtml-compat --save` to install it on their projects.
### Bug fixes
- Bug with undefined handler call in `loader_type_js` was fixed ([#1159](https://github.com/bem/bem-core/pull/1159)).
### Other changes
- BH bundles in `dist` now mimic to BEMHTML ([#1210](https://github.com/bem/bem-core/issues/1210)).
- `bem create` templates for `bemhtml`, `bemtree`, `vanilla.js` and `browser.js` were improved ([#1183](https://github.com/bem/bem-core/issues/1183)).
- `vow` was updated to `0.4.10` ([#1056](https://github.com/bem/bem-core/issues/1056)).
## 2.7.0
### Notable changes
- New `detach` method was added to `i-bem__dom` ([#1102](https://github.com/bem/bem-core/issues/1102)).
- `i-bem.bemhtml` now supports nested mixes as objects ([873](https://github.com/bem/bem-core/issues/873)).
- Some minor attribute escaping optimizations were added to `i-bem.bemhtml` ([#961](https://github.com/bem/bem-core/issues/961)), ([#980](https://github.com/bem/bem-core/issues/980)) and ([#982](https://github.com/bem/bem-core/issues/982)).
- Support for [bem-xjst](https://github.com/bem/bem-xjst) 2.x was added to BEMHTML templates ([#1021](https://github.com/bem/bem-core/issues/1021)).
- `clearfix` was optimized to work properly in supported IE browsers ([#722](https://github.com/bem/bem-core/issues/722)).
- `jquery` was updated to 2.1.4 and 1.11.3 ([#999](https://github.com/bem/bem-core/issues/999)).
### Bug fixes
- An issue in `i-bem__dom` was fixed. `findElem` didn't update cache of elements that
had been found previously ([#583](https://github.com/bem/bem-core/issues/583)).
- An issue in `i-bem__dom` was fixed. `dropElemCache` worked incorrectly in some edge cases ([#1037](https://github.com/bem/bem-core/issues/1037)).
- An issue in `i-bem__dom` was fixed. `setMod` didn't add CSS classes if blocks on the same DOM node had
overlapping end parts in their names ([#1090](https://github.com/bem/bem-core/issues/1090)).
- An issue in `page` was fixed. `zoom` attribute of the block didn't work for touch levels ([#1020](https://github.com/bem/bem-core/issues/1020)).
- An issue in `keyboard__codes` was fixed. `insert` and `delete` keys had wrong key codes ([#1002](https://github.com/bem/bem-core/issues/1002)).
- An issue in `i-bem.bemhtml` was fixed. `applyNext` calls were skipped in nested templates ([b1dc50c](https://github.com/bem/bem-core/commit/b1dc50c621b5659cff33daa4dd3f210b67cf25e1)).
- An issue in `jquery__events_type_pointernative` was fixed to work properly in IE 11/Edge ([#1066](https://github.com/bem/bem-core/issues/1066)).
### Other changes
- Russian documentation for every blocks was reworked. Please visit https://ru.bem.info/libs/bem-core/ for new documentation.
- Other minor improvements of the documentation.
## 2.6.0
### Notable changes
- Since now `i-bem__dom` provides module after DOM is ready ([#859](https://github.com/bem/bem-core/issues/859)).
- Since now `setMod` and `hasMod` methods of `i-bem__dom` convert their `modVal` argument to string in case
it is not of type string or boolean ([#890](https://github.com/bem/bem-core/issues/890)).
- An ability to pass `nonce` attribute was added to `page`, to support related parts of Content Security Policy
specification ([#882](https://github.com/bem/bem-core/issues/882)).
- New `page__conditional-comment` template was added ([#551](https://github.com/bem/bem-core/issues/511)).
- `vow` was updated to 0.4.8 ([#837](https://github.com/bem/bem-core/issues/837)).
### Bug fixes
- An issue in `i-bem.bemhtml` was fixed. Block CSS class repeated in case of mix with the same
block ([#792](https://github.com/bem/bem-core/issues/792)).
- An issue in `loader_type_bundle` was fixed. Success callback might be applied after timeout
error ([67ff55f](https://github.com/bem/bem-core/commit/da5fdb9923e7e83e3ef9cd31aefc3967ff55fd3c)).
- An issue in `i-bem__dom` was fixed. `append`, `prepend` and other similar methods won't properly work with strings
in some cases ([#852](https://github.com/bem/bem-core/issues/852)).
- An issue in `jquery__event_type_winresize` was fixed. MSIE wasn't detected properly ([#862](https://github.com/bem/bem-core/issues/862)).
- An issue in `object` was fixed to proper handle `null` value as `target` argument in `extend` method ([#910](https://github.com/bem/bem-core/issues/910)).
- An issue in `page` was fixed. There was no way to disable `x-ua-compatible` meta tag from BEMJSON ([#794](https://github.com/bem/bem-core/issues/794)).
### Other changes
- Timeout in `loader_type_bundle` module was increased to 30000 ms ([4e27422](https://github.com/bem/bem-core/commit/000c6af02bfae4506fa460168de16d4e27422393)).
- Russian documentation for several blocks was fixed.
## 2.5.1
### Bug fixes
- An issue in `jquery__pointerpress` and `jquery__pointerrelease` was fixed. Events work now in
Internet Explorer 8 ([#792](https://github.com/bem/bem-core/issues/792)).
- An issue in `jquery__pointernative` was fixed. `pointerenter` and `pointerleave` events have bubbled up
to the document root, while they shouldn't ([#801](https://github.com/bem/bem-core/issues/801)).
- An issue in `loader_type_bundle` was fixed. CSS bundle has been always added to the top of the HTML `
`, so CSS rules
from the bundle might not work properly ([#808](https://github.com/bem/bem-core/issues/808)).
- Issues in BH templates for `ua` were fixed. There was no possibility to pass the content of the block from
BEMJSON ([#734](https://github.com/bem/bem-core/pull/734)).
- An issue in `page` was fixed. There was a problem with conditional comments for Internet Explorer in the BH template
of the block ([#781](https://github.com/bem/bem-core/pull/781)).
### Other changes
- `jquery` was updated to the 2.1.3 and 1.11.2 ([#778](https://github.com/bem/bem-core/pull/788)).
- Russian documentation for modules: `clearfix`, `cookie`, `identify`, `idle`, `inherit`, `keyboard`, `loader`, `next-tick`,
`string` and `tick` was added.
- Russian documentation for `i-bem.js` was updated.
- English guides to BEMHTM and BEMJSON were updated.
## 2.5.0
### Notable changes
- bem-core in now published under the [MPL 2.0](https://www.mozilla.org/MPL/2.0/) license ([#443](https://github.com/bem/bem-core/issues/443)).
- An ability to specify error handler was added to `loader_type_js` ([#672](https://github.com/bem/bem-core/issues/672)).
- `BEMContext` class was added to `oninit` export context in `i-bem.bemtree` ([#602](https://github.com/bem/bem-core/issues/602)).
- `reapply` static method was added to BEMContext class of BEMTREE ([#706](https://github.com/bem/bem-core/pull/706)).
- bh templates for block `page` were added to touch level ([#689](https://github.com/bem/bem-core/pull/689)).
- [bem-xjst](https://github.com/bem/bem-xjst) was updated to 0.9.0 ([#709](https://github.com/bem/bem-core/pull/709)).
### Bug fixes
- An issue in `i-bem__dom` was fixed. `findBlocksInside` could return blocks which weren't inited ([#699](https://github.com/bem/bem-core/issues/699)).
- An issue in `tick` was fixed. Timer was not removed by `Tick#stop()` ([#694](https://github.com/bem/bem-core/issues/694)).
- An issue in `i-bem.bemhtml` was fixed. `i-bem` CSS class was added to elements by mistake ([#633](https://github.com/bem/bem-core/issues/633)).
- `html-from-bemtree` tech was fixed to expose `vow`, `console`, `setTimeout` inside BEMTREE template context ([#438ebb8](https://github.com/bem/bem-core/commit/438ebb8f828e26977592e26511e8aad15176d7a4)).
### Other changes
- English guide to BEMJSON was added.
- Russian documentation for `querystring` module was added.
- Russian documentation for `i-bem.js` was fixed to satisfy current API.
- Documentation for BEMHML/BEMTREE for both languages was updated.
## 2.4.0
### Notable changes
- [bem-xjst](https://github.com/bem/bem-xjst) was updated to 0.8.0; [bemhtml-compat](https://github.com/bem/bemhtml-compat) was updated to 0.0.11.
### Bug fixes
- An issue in `jquery__event_type_pointerpressrelease` was fixed. `pointerpress`/`pointerrelease` events fired for any press/release
of mouse button ([#607](https://github.com/bem/bem-core/issues/607)).
- An issue in `i-bem__dom.js` was fixed. Base `live` method was not properly called in some edge cases ([#608](https://github.com/bem/bem-core/issues/608)).
### Other changes
- English documentation for JS-syntax of BEMHTML was added.
## 2.3.0
### Notable changes
- New implementation of pointer events was added. Based on pointer events polyfills from [Polymer](http://www.polymer-project.org/) ([#567](https://github.com/bem/bem-core/pull/567)).
- Ability to specify additional data for event was added to `bindTo*` methods of `i-bem__dom.js` ([#568](https://github.com/bem/bem-core/issues/568)).
### Other changes
- An issue in `i-bem.bemhtml` was fixed. There was an error when mix was used as an object (not an array) in BEMJSON and BEMHTML simultaneously ([#555](https://github.com/bem/bem-core/issues/555)).
- An issue in `page` was fixed. There was no possibility to apply standard modes to `page` in BEMHTML template and touch template was broken ([516](https://github.com/bem/bem-core/issues/516)).
## 2.2.4
### Bug fixes
- An issue in `i-bem.js` was fixed. Modifier change event has been emitted even if `beforeSetMod` handler
had prevented change ([#546](https://github.com/bem/bem-core/pull/546)).
- String decoding process of `querystring__uri` module was fixed to return original string
if decode failed ([#554](https://github.com/bem/bem-core/pull/554)).
## 2.2.3
### Bug fixes
- Destruction process of blocks was fixed to prevent unexpected block reinitialization ([#540](https://github.com/bem/bem-core/issues/540)).
- An issue in `jquery__event_type_pointer` was fixed. Native mouse events were replaced with pointer events
in unexpected cases ([#534](https://github.com/bem/bem-core/issues/534)).
- `unbindFrom*` methods of `i-bem__dom` now support multiple events to be passed in arguments ([#533](https://github.com/bem/bem-core/issues/533)).
- Lost `functions` dependency in `events` module was restored ([#532](https://github.com/bem/bem-core/issues/532)).
## 2.2.2
### Bug fixes
- An issue with block reinitialization on the DOM node, that has been processed with destructor, was fixed
in `i-bem__dom` ([#518](https://github.com/bem/bem-core/issues/518)).
- An issue in mod events subscription was fixed in `i-bem`. `false` could be used as `modVal` ([#529](https://github.com/bem/bem-core/issues/529)).
- `jquery` was updated to the latest minor releases 2.1.1 and 1.11.1 ([#515](https://github.com/bem/bem-core/issues/515)).
## 2.2.1
- An issue in `jquery__event_type_pointerpressrelease` was fixed. `pointerpress` event has been triggered twice on each mousedown
in IE10 ([#505](https://github.com/bem/bem-core/issues/505)).
## 2.2.0
### Notable changes
- New `keyboard__codes` module has been added ([#431](https://github.com/bem/bem-core/issues/431)).
- `BEMContext` class was added to oninit export context in `i-bem.bemhtml` ([#485](https://github.com/bem/bem-core/pull/485)).
- Ability to declare elements with block class has been added ([#481](https://github.com/bem/bem-core/issues/481)).
- Behaviour of `isSimple` method of `BEMContext` was fixed in `i-bem.bemhtml` ([#432](https://github.com/bem/bem-core/pull/432)).
- An issue with `liveUnbindFrom` method of `BEMDOM` was fixed in `i-bem__dom` ([#476](https://github.com/bem/bem-core/pull/476)).
- An issue with `isFocusable` method of `dom` module was fixed for cases where `domElem` is a link with `tabindex` attribute,
but without `href` ([#501](https://github.com/bem/bem-core/issues/501)).
- Short way of module declaration was fixed for `i-bem__dom_elem-instances` ([#479](https://github.com/bem/bem-core/issues/479)).
- A workaround for rendering performance of blocks initialisation in Chrome-based browsers was added
to `i-bem__dom_init_auto` ([#486](https://github.com/bem/bem-core/issues/486)).
- `vow.js` module has been moved to `vow.vanilla.js` ([#412](https://github.com/bem/bem-core/issues/412)).
### Other changes
- `vow` module has been updated to 0.4.3 ([#504](https://github.com/bem/bem-core/pull/504)).
- Russian documentation about BEMTREE technology was added ([#500](https://github.com/bem/bem-core/pull/500)).
- Russian documentation for JS-syntax of BEMHTML was updated ([#471](https://github.com/bem/bem-core/pull/471)).
- API references for JS-modules has been added as a separate branch `v2-jsdoc` ([#478](https://github.com/bem/bem-core/pull/478)).
## 2.1.0
### Notable changes
- An issue in `i-bem.js` when modifiers change event had been emitted before `onSetMod` handlers have been called was fixed ([#454](https://github.com/bem/bem-core/issues/454)).
- An issue in `i-bem.bemhtml` was fixed. Since now `this.mods` and `this.ctx.mods` use the same object ([#441](https://github.com/bem/bem-core/issues/441)).
- Error in modular declaration of element modifiers was fixed in `i-bem__dom_elem-instances` ([#447](https://github.com/bem/bem-core/issues/447)).
- [inherit](https://github.com/dfilatov/inherit) module was updated to 2.2.1 ([#466](https://github.com/bem/bem-core/issues/466)).
- An order of tags in `head` section of `page.bemhtml` was fixed ([#465](https://github.com/bem/bem-core/pull/465)).
### Other changes
- `baseMix` field description of `i-bem.js` was added to russian docs ([#461](https://github.com/bem/bem-core/pull/461)).
- CDN host was changed to `yastatic.net` ([#444](https://github.com/bem/bem-core/issues/444)).
Previous CDN host `yandex.st` is still accessible. Physically they both are the same web servers. DNS records is the only difference.
- BEMHTML template for `bem create` command was added ([#277](https://github.com/bem/bem-core/issues/277)).
- We do not support autobuilding of our tests with Node.js 0.8 in [Travis CI](http://travis-ci.com) any longer ([#455](https://github.com/bem/bem-core/issues/455)).
- Travis's build status badge [was changed to SVG version](http://blog.travis-ci.com/2014-03-20-build-status-badges-support-svg/) :)
## 2.0.0
### Breaking changes
- All deprecated methods have been removed from `i-bem.js` and `i-bem__dom.js` ([#318](https://github.com/bem/bem-core/issues/318)).
The following methods were removed:
* `destruct`, use `onSetMod js ''`;
* `extractParams`, use `elemParams`;
* `trigger`, use `emit`;
* `afterCurrentEvent`, use `next-tick` module;
* `channel`, use `events__channels` module;
* `changeThis`, use native `Function.prototype.bind`.
- `init` and `destruct` events have been removed from `i-bem.js` in favor of modifiers changes events (see "Notable changes" section below).
- `ecma` was moved to [separate repo](http://github.com/bem/es5-shims); ES5-shims should be used
for IE < 9 ([#230](https://github.com/bem/bem-core/issues/230)).
- `vow` module has been updated to 0.4.1 ([#350](https://github.com/bem/bem-core/issues/350)).
See [Vow's changelog](https://github.com/dfilatov/vow/blob/0.4.1/CHANGELOG.md) for changes.
- Support for vow@0.4 has been added to `i-bem.bemhtml` ([#385](https://github.com/bem/bem-core/issues/385)).
### Notable changes
- Support for defining BEMDOM-blocks as [ym](https://github.com/ymaps/modules) modules has been added ([#382](https://github.com/bem/bem-core/issues/382)).
- Events for modifiers changes have been added to `i-bem.js` ([#357](https://github.com/bem/bem-core/issues/357)).
- Support for passing string values has been added to `BEMDOM.init`
([#419](https://github.com/bem/bem-core/issues/419)).
and `BEMDOM.update` methods ([#420](https://github.com/bem/bem-core/issues/420)).
- DOM helpers from `i-bem__dom.js` `replace`, `append`, `prepend`, `before`, `after` now return new context and `update` returns
updated context as a jQuery object ([#410](https://github.com/bem/bem-core/issues/410)).
- New `loader_type_bundle` has been added ([#358](https://github.com/bem/bem-core/issues/358)).
- Default jQuery versions were updated to 2.1.0 and to 1.11.0, for IE < 9 ([#356](https://github.com/bem/bem-core/issues/356)).
### Other changes
- `i-bem.bemhtml` now uses strings concatination instead of pushing to buffer in it's internals ([#401](https://github.com/bem/bem-core/issues/401)).
- jQuery no longer removes itself from global scope if it exists ([#349](https://github.com/bem/bem-core/issues/349)).
- `jquery__event_type_pointerclick.js` has been moved from touch level to common ([#393](https://github.com/bem/bem-core/issues/393)).
- Modifiers `i-bem_elem-instances_yes` and `i-bem__dom_elem-instances_yes` were renamed to boolean style ([#352](https://github.com/bem/bem-core/issues/352)).
- Runtime error in `page` template in development mode has been fixed ([#417](https://github.com/bem/bem-core/issues/417)).
- Usage of `Function.prototype.bind` has been droped from `i-bem.js` internals in favor of support
for Android 2.3 ([#404](https://github.com/bem/bem-core/issues/404)).
- Some bugs in `browser-js+bemhtml` tech have been fixed ([#392](https://github.com/bem/bem-core/issues/392)).
- Up to [ym@0.0.15](https://github.com/ymaps/modules/releases) ([#414](https://github.com/bem/bem-core/issues/414)).
## 1.2.0
### Notable changes
- BEM-blocks are emit `destruct` event on destructing ([#370](https://github.com/bem/bem-core/issues/370)).
- Improvements of `pointerevents` polyfills ([#354](https://github.com/bem/bem-core/pull/354)).
### Other changes
- All JSDocs were fixed so [bem-jsd](github.com/bem/bem-jsd) could parse them ([#335](https://github.com/bem/bem-core/issues/335)).
- Russian version of BEMHTML reference was actualized to JavaScript syntax ([#355](https://github.com/bem/bem-core/pull/355)).
- Use [bower](http://bower.io) for dependency management ([#367](https://github.com/bem/bem-core/issues/367)).
## 1.1.0
### Notable changes
- `jquery__config` uses jQuery 2.x by default for modern browsers ([#319](https://github.com/bem/bem-core/issues/319)).
- Add ability to use any BEMJSON as value of attributes in BEMHTML templates ([#290](https://github.com/bem/bem-core/issues/290)).
- Fix dependencies in `i-bem__collection` ([#292](https://github.com/bem/bem-core/issues/292)).
- Remove `page` block touch styles ([#306](https://github.com/bem/bem-core/issues/306)).
- Fix `page` BEMHTML wrapping in production mode ([#309](https://github.com/bem/bem-core/issues/309)).
- Fix possible JavaScript error in script injection in IE<9 in `next-tick` ([#324](https://github.com/bem/bem-core/issues/324)).
- Fix `FastClick` initialisation in `jquery__event_type_pointerclick` of `touch.blocks` ([#332](https://github.com/bem/bem-core/issues/332)).
- Fix `node.js` tech bug on Windows systems ([#274](https://github.com/bem/bem-core/issues/274)).
- Fix `i-bem__dom_elem-instances` bug with `onElemSetMod` ([#340](https://github.com/bem/bem-core/issues/340)).
- Use bemhtml from [bem-xjst](https://github.com/bem/bem-xjst) ([#329](https://github.com/bem/bem-core/issues/329)).
### Other changes
- [ym](https://github.com/ymaps/modules) was updated to 0.0.12 ([#326](https://github.com/bem/bem-core/issues/326)).
- Do not flood `console` with messages if `i-bem__i18n` is not in debug mode ([#285](https://github.com/bem/bem-core/issues/285)).
- Fix jsdoc for `dropElemCache()` method of `i-bem__dom` module ([#296](https://github.com/bem/bem-core/issues/296)).
- Development infrastructure was updated to
[bem-pr@v0.5.x](https://github.com/narqo/bem-pr/blob/0.5.3/HISTORY.md) ([#323](https://github.com/bem/bem-core/issues/323)).
- Russian documentation for `i-bem.js` was updated.
- [List of supported browsers](https://github.com/bem/bem-core/blob/v1/README.md#supported-browsers)
was specified in project README.
## 1.0.0
### Breaking changes
- Starts using modular system [ym](https://github.com/ymaps/modules).
- Removes all deprecated methods from `i-bem` and `i-bem__dom`.
- `i-bem` now has no dependency on jQuery. `i-bem__dom` still depends on jQuery.
- BEMHTML-template can be written with [JS-syntax](https://gist.github.com/veged/6150760).
- Introduces new tech `bemtree` (based on [bem-xjst](https://github.com/bem/bem-xjst))
for describing dynamic generation of BEM-tree.
- Introduces new tech `vanilla.js` for JS-implementations that does not depend on particular JS-engine.
- Introduces new techs `browser.js` and `node.js` for JS-implementations targeted corresponding engines.
For backward compatibility we assume that `.js`-files contains `browser.js` implementation.
- Introduces polyfill (`jquery__event_type_pointer` and `jquery__event_type_pointerclick` as a jQuery-plugins)
for universalize desktop and touch pointer events.
- Introduces system for unit testing and blocks examples generation.
- Introduces "simple" modifiers (modifiers without value) support in `i-bem` and BEMHTML.
### Other changes
- Gets rid of prefixes in all block names (except `i-bem`).
- Block `i-bem__dom` becomes a module (in terms of [ym](https://github.com/ymaps/modules))
and all `BEM.DOM`-block must define additions to `i-bem__dom` ([example](https://github.com/bem/bem-core/blob/v1/common.bundles/index/blocks/b-square/b-square.js)).
- Method for blocks declaration (`.decl()`) does not accept object with `name` field as first parameter.
Required form with `block` field: `BEM.decl({ block: 'b1', modName: 'm', modVal: 'v' }, ...)`.
- Introduces `nextTick` method as replacement for `afterCurrentEvent` method
for ensure of block existence in callback invocation time.
`BEM.afterCurrentEvent` is **deprecated**.
- Introduces new `channels` module instead of `BEM.channel`. `BEM.channel` is **deprecated**.
- `changeThis` is **deprecated**. Use native `bind` instead.
- Removes `del` method from `i-bem` block.
- Removes `getWindowSize` method from `i-bem__dom` block. Use `BEMDOM.win.width()` and `BEMDOM.win.height()`.
- Introduces `jquery` module-wrapper for providing jQuery.
If jQuery already included into the page module-wrapper provides it. Otherwise it loads jQuery (version 1.10.1) on its own.
- `$.observable` becomes `events` module and not longer depends on jQuery.
- `$.inherit` becomes `inherit` module and not longer depends on jQuery.
- `$.identify` becomes `identify` module and not longer depends on jQuery.
- `$.throttle` splits into two modules: `functions__throttle` and `functions__debounce`, they both not longer depend on jQuery.
- `$.decodeURI`, `$.decodeURIComponent` moves to `querystring__uri` module and not longer depends on jQuery.
- `$.cookie` becomes `cookie` module and not longer depends on jQuery.
- Introduces `ua` module instead of `$.browser` (with same interface).
- Use `pointerclick` instead of `leftclick`. It provides by `jquery__event_type_pointerclick` polyfill.
- `i-system` block splits into two modules: `idle` and `tick`.
- Triggers for modifiers changes now splitted into two groups:
before setting new value (`beforeSetMod` and `beforeElemSetMod`)
and after the value has been set (`onSetMod` and `onElemSetMod`).
Cancellation of modifiers change is possible only from `before*`-triggers.
- Using of `{ onSetMod : { js : function() { ... } } }` is **deprecated**, use `onSetMod: { js : { inited : ... } } }`.
- `destruct` method from `i-bem` block is **deprecated**.
Use supplementary trigger for `_js` modifiers:
`onSetMod: { js : { inited : ... } } }` — `{ onSetMod : { js : { '' : ... } } }`.
- `exractParams` method from `i-bem__dom` block is **deprecated**.
Use `elemParams` method for access to elements params.
- `trigger` method from `i-bem` block is **deprecated** in flavor of `emit` method.
- `onFirst` method from `i-bem` block is **deprecated** in flavor of `once` method.
- **Deprecated** field `e.block` that provided block-target of BEM-events was removed. Use `e.target` field instead.
- Field `e.data.domElem` that provided DOM-element of block in DOM-events was removed. Use `$(e.currentTarget)` (provided by jQuery).
- Introduces parameter for `findElem` method that allows to search elements
of particular block instance (in case of nested blocks with same name).
- Introduces possibility to point particular function in `unbindFrom*` methods.
- Introduces `objects` module for work with JS-objects. It contains methods: `extend`, `isEmpty`, `each`.
- Introduces `functions` module for work with JS-functions. It contains methods: `isFunction`, `noop`.
- Introduces `dom` module for work with DOM-tree.
- Introduces `querystring` module for work with URL-based strings.
- Introduces `loader_type_js` module for JS loading.
- Introduces `vow` module for Promises/A+.
- Introduces `next-tick` module as polyfill for `nextTick`, `setImmediate`, `setTimeout(0, ...` and etc.
- Introduces `strings__escape` module for XML, HTML and attributes escaping.
- `inherit` module now supports mixins.
- Introduces `invokeAsap` parameter for `functions__throttle` module that allows to delay first invocation.
================================================
FILE: CHANGELOG.ru.md
================================================
# История изменений
## 5.0.0
### Несовместимые изменения
- **Только ESM**: Вся кодовая база мигрирована с модульной системы `ym` (`modules.define`/`modules.require`) на нативные ES-модули (`import`/`export`). Зависимость `ym` удалена.
- **Сборка через Vite**: ENB и все 17+ связанных пакетов заменены на [Vite](https://vite.dev/). Кастомный плагин `vite-plugin-bem-levels` обеспечивает сканирование BEM-уровней, разрешение виртуальных модулей `bem:*` и генерацию barrel-файлов для цепочек переопределений.
- **jQuery 4.0**: Peer-зависимость `jquery` обновлена с `^3.x` до `^4.0.0`. Основное изменение API: `$.unique()` удалён — используйте `$.uniqueSort()`.
- **Node.js 20+**: Минимальная поддерживаемая версия Node.js — 20 (было 8).
- **Нативные промисы**: `vow` удалён. Весь код использует нативный `Promise`.
- **Нативный тест-раннер**: Серверные тесты используют `node:test` и `node:assert` вместо `mocha`/`chai`.
- **Playwright**: Браузерные тесты используют [Playwright](https://playwright.dev/) вместо `mocha-phantomjs` (PhantomJS мёртв).
- **ESLint 10**: Линтинг мигрирован с `jshint`/`jscs` на ESLint 10 с flat-конфигурацией (`eslint.config.js`).
- **GitHub Actions CI**: Travis CI заменён на GitHub Actions.
- **Husky + lint-staged**: `git-hooks` заменён на `husky` и `lint-staged`.
### Крупные изменения
- Все `.vanilla.js` и `.js` файлы конвертированы в ES-модули с синтаксисом `import`/`export`.
- Цепочки переопределений модулей (напр., `jquery` с расширениями pointer-событий) обрабатываются автоматически сгенерированными barrel-файлами через `vite-plugin-bem-levels`.
- Блок `i18n` сохраняет свой API, но использует нативные ES-модули внутри.
- Браузерные спек-тесты (28 файлов, 500+ тестов) запускаются через Vite dev server + Playwright.
- Vite генерирует платформенно-специфичные сборки (`desktop`, `touch`) с jQuery как внешней зависимостью.
### Удалённые пакеты
- **Сборка**: `enb`, `enb-bem-techs`, `enb-magic-factory`, `enb-magic-platform`, `enb-bemxjst`, `enb-bemxjst-6x`, `enb-bemxjst-7x`, `enb-bemxjst-i18n`, `enb-bh`, `enb-bh-i18n`, `enb-borschik`, `enb-css`, `enb-js`, `enb-bem-docs`, `enb-bem-examples`, `enb-bem-specs`, `enb-bem-tmpl-specs`, `enb-bem-i18n`, `borschik`
- **Линтинг**: `jscs`, `jscs-bem`, `jshint`, `jshint-groups`
- **Тестирование**: `mocha-phantomjs`, `istanbul`, `chai-as-promised`
- **Прочие**: `ym`, `vow`, `bower`, `git-hooks`, `gitbook-api`, `bem-naming`, `bem-walk`
### Удалённые конфигурационные файлы
- `.enb/` (конфигурация сборки ENB)
- `.jshintrc`, `.jscs.json`, `.jshint-groups.js` (конфигурация линтинга)
- `.bowerrc`, `bower.json` (конфигурация Bower)
- `.travis.yml` (конфигурация Travis CI)
- `.githooks/` (конфигурация git-hooks)
## 4.3.1
### В релиз вошли следующие исправления ошибок
- Откатили изменение, из-за которого при попытке использования `lazyInit` при деклации модификатора выбрасывалось исключение ([#1594](https://github.com/bem/bem-core/pull/1594)).
## 4.3.0
### Крупные изменения
- `jQuery` была обновлена до 3.2.1 и 1.12.4 ([#1587](https://github.com/bem/bem-core/pull/1587)).
### В релиз вошли следующие исправления ошибок
- Исправлена возможность форсировать lazy-инициализацию из разметки ([#1579](https://github.com/bem/bem-core/pull/1579)).
- Исправлена возможность декларировать сущности с миксинами ([#1550](https://github.com/bem/bem-core/pull/1550)).
- Теперь при попытке использования `lazyInit` при деклации модификатора будет выбрасываться исключение ([#1579](https://github.com/bem/bem-core/pull/1579)).
- Исправлен баг в методе `isFunction` блока `functions`, который неправильно определял сложные функции ([#1577](https://github.com/bem/bem-core/pull/1577)).
### Также в релиз вошли следующие изменения
- Добавлен конфиг `.bemrc` ([#1568](https://github.com/bem/bem-core/pull/1568)).
- Блок `vow` обновлен до версии 0.4.17 ([#1565](https://github.com/bem/bem-core/pull/1565)).
- Блок `inherit` обновлен до версии 2.2.6 ([#1519](https://github.com/bem/bem-core/pull/1519)).
- Удален приватный метод `_delInitedMod` блока `i-bem` ([#1523](https://github.com/bem/bem-core/pull/1523)).
- Удален код, относящийся к старой версии `bem-tools` (https://github.com/bem/bem-core/commit/e57678b2d64a3b976a53af4a7fa09bf918685821).
- Обновлены npm-зависимости ([#1589](https://github.com/bem/bem-core/pull/1589)).
- Теперь тесты в CI запускаются и под Node.js 8 (https://github.com/bem/bem-core/commit/dd7e5344a64ad1595eab28febb2242134fcedbb3).
- Добавлены недостающие тесты для блока `i-bem-dom` ([#1517](https://github.com/bem/bem-core/pull/1517)).
- Добавлен gitbook ([#1569](https://github.com/bem/bem-core/pull/1569)).
- Исправления JSDoc.
- Исправления документации.
## 4.2.1
### В релиз вошли следующие исправления ошибок
- Исправлена ошибка с инвалидацией кеша элементов при изменении DOM ([#1487](https://github.com/bem/bem-core/issues/1487)).
- Исправлена ошибка в `i-bem-dom__events`, приводившая к тому, что данные события не передавались в обработчик ([#1509](https://github.com/bem/bem-core/pull/1509)).
- Исправлен метод `isEditable` модуля `dom`. Добавлены недостающие типы ([#1502](https://github.com/bem/bem-core/pull/1502)).
### Также в релиз вошли следующие изменения
- Исправлены синтаксические ошибки в JSDoc блока `i-bem-dom`.
- Незначительные исправления документации.
- Добавлено [CLA](https://github.com/bem/bem-core/blob/v4/CLA.md).
## 4.2.0
### Крупные изменения
- В BEMHTML-шаблонах реализована поддержка `bem-xjst 8.x` ([#1486](https://github.com/bem/bem-core/issues/1485)).
### В релиз вошли следующие исправления ошибок
- Исправлена работа метода `concat()` в i-bem-dom__collection ([#1488](https://github.com/bem/bem-core/issues/1488)).
- Исправлена опечатка в `ua__dom`, приводящая к runtime-ошибке при использовании touch-уровня ([#1479](https://github.com/bem/bem-core/issues/1478)).
- dist: Исправлена ошибка, при которой `i-bem-dom__init_auto` подключался в `no-autoinit`-бандл ([#1482](https://github.com/bem/bem-core/issues/1481)).
### Также в релиз вошли следующие изменения
- Теперь методы `findChildBlock`, `findChildBlocks`, `findParentBlock`, `findParentBlocks`, `findMixedBlock` и `findMixedBlocks` выбрасывают исключение, если блок передан строкой ([#1469](https://github.com/bem/bem-core/pull/1469/)).
- Оптимизирована функция построения имен классов ([#1404](https://github.com/bem/bem-core/pull/1404)).
- Добавлены переводы на английский ([#1483](https://github.com/bem/bem-core/pull/1483), [#1476](https://github.com/bem/bem-core/pull/1476), [#1475](https://github.com/bem/bem-core/pull/1475)).
- Миграционный гайд: добавлено примечание о `bemTarget` ([#1491](https://github.com/bem/bem-core/issues/1472)).
- Миграционный гайд: добавлено примечание о необходимых опциях для шаблонизаторов ([#1467](https://github.com/bem/bem-core/issues/1467)).
- Исправлены ошибки в документации.
## 4.1.1
### В релиз вошли следующие исправления ошибок
— Исправлена ошибка в блоке `ua` на уровне `touch.blocks`, которая приводила к невозможности предоставить ym-зависимости ([#1460](https://github.com/bem/bem-core/pull/1460)).
## 4.1.0
### В релиз вошли следующие исправления ошибок
- Исправлена проблема в `identify` с возвращением разного результата при вызове для `document` ([#1441](https://github.com/bem/bem-core/issues/1441)).
- Исправлена проблема с переполнением глубины стека вызовов `modules.define` ([#1446](https://github.com/bem/bem-core/issues/1446)).
- Исправлена поддержка эскейпинга в блоке `ua` ([#1435](https://github.com/bem/bem-core/issues/1435)).
- Реализован обход бага в `Array.prototype.push` в `Opera 41`.
- Исправлены pointer-события на iOS-устройствах ([#1253](https://github.com/bem/bem-core/issues/1253)).
- Исправлена ошибка в `i-bem-dom__events`, приводящая к неправильной работе метода `once()` в некоторых случаях ([#1452](https://github.com/bem/bem-core/issues/1452)).
### Также в релиз вошли следующие изменения
- Реализована возможность задавать атрибут `lang` для `` в блоке `page` ([#751](https://github.com/bem/bem-core/issues/751)).
## 4.0.0
### Изменения, ломающие обратную совместимость
- Изменения в блоке `i-bem`. Подробнее в [MIGRATION.ru.md](MIGRATION.ru.md#Изменения-в-блоке-i-bem).
- Изменения в блоке `querystring`. Подробнее в [MIGRATION.ru.md](MIGRATION.ru.md#Изменения-в-блоке-querystring).
- Из модуля `identify` удален опциональный параметр `onlyGet` ([#1028](https://github.com/bem/bem-core/issues/1028)).
- Из модуля `events` удалены все статические методы ([#1024](https://github.com/bem/bem-core/issues/1024)).
- В классе `Event` модуля `events` удалено поле `result` ([#1023](https://github.com/bem/bem-core/issues/1023)).
- Элемент `css` блока `page` больше не поддерживает автоматическое добавление условных комментариев для IE ([#379](https://github.com/bem/bem-core/issues/379)).
## 3.2.0
### В релиз вошли следующие исправления ошибок
- Исправлена проблема с переполнением глубины стека вызовов `modules.define` ([#1446](https://github.com/bem/bem-core/issues/1446)).
- Исправлена поддержка эскейпинга в блоке `ua` ([#1435](https://github.com/bem/bem-core/issues/1435)).
- Реализован обход бага в `Array.prototype.push` в `Opera 41`.
### Также в релиз вошли следующие изменения
- Реализована возможность задавать атрибут `lang` для `` в блоке `page` ([#751](https://github.com/bem/bem-core/issues/751)).
## 3.1.0
### В релиз вошли следующие исправления ошибок
- Исправлен баг в методе `getMods()` блока `i-bem` ([#1379](https://github.com/bem/bem-core/issues/1379)).
### Крупные изменения
- Шаблоны: реализована поддержка режима экранирования ([#1406](https://github.com/bem/bem-core/issues/1406)).
### Также в релиз вошли следующие изменения
- Мелкие исправления документации.
## 3.0.1
### В релиз вошли следующие исправления ошибок
- Исправлены pointer-события на iOS-устройствах ([#1253](https://github.com/bem/bem-core/issues/1253)).
## 3.0.0
### Изменения, ломающие обратную совместимость
- Удалены базовые шаблоны и документация для `BEMHTML` и `BEMTREE` ([#1258](https://github.com/bem/bem-core/issues/1258)). Следует использовать версию `bem-xjst` не ниже 6.3.0.
- Расширения файлов BEMHTML-шаблонов переименованы с `*.bemhtml` на `*.bemhtml.js` ([#984](https://github.com/bem/bem-core/issues/984)). Необходимо убедиться, что в конфиге сборки поддерживается новое расширение.
- Удален элемент `i-bem__i18n` ([#1304](https://github.com/bem/bem-core/issues/1304)). Для интернационализации следует использовать блок `i18n`.
- `jquery__events_type_pointerclick` больше не использует библиотеку [FastClick](https://github.com/ftlabs/fastclick) ([#1088](https://github.com/bem/bem-core/issues/1088)).
### Крупные изменения
- `jQuery` была обновлена до 2.2.3 и 1.12.3 ([#1260](https://github.com/bem/bem-core/issues/1260)).
### В релиз вошли следующие исправления ошибок
- В блоке `page` на уровне `blocks.touch` исправлена ошибка с невалидным значением `user-scalable=0` для ` ` ([#1294](https://github.com/bem/bem-core/issues/1294)).
- Исправлена ошибка в `jquery__event_type_pointernative`, которая приводила к возникновению JS ошибки в IE8 ([1317](https://github.com/bem/bem-core/issues/1317)).
### Также в релиз вошли следующие изменения
- dist: Автоматическая инициализация блоков теперь опциональна ([#1271](https://github.com/bem/bem-core/issues/1271)).
## 2.9.0
### Крупные изменения
- `jQuery` была обновлена до 2.2.0 и 1.12.0 ([#1249](https://github.com/bem/bem-core/issues/1249)).
### В релиз вошли следующие исправления ошибок
- Исправлена ошибка в `BEMHTML 1.x`, которая приводила к потере `this.mods` в `reapply()` ([#97](https://github.com/bem/bem-xjst/issues/97)).
### Также в релиз вошли следующие изменения
- Теперь `jquery__event_type_pointerpressrelease` предоставляет `originalEvent` ([#1254](https://github.com/bem/bem-core/issues/1254)).
- dist: Поддержка `i18n` добавлена в dist ([#1212](https://github.com/bem/bem-core/issues/1212)).
- Шаблон `page__css.bemhtml` был обновлен для поддержки новых версий `bem-xjst` ([#1228](https://github.com/bem/bem-core/issues/1228)).
## 2.8.0
### Крупные изменения
- Реализован новый блок [i18n](https://github.com/bem/bem-core/tree/v2/common.blocks/i18n), реализующий интернационализацию проектов на bem-core ([#1074](https://github.com/bem/bem-core/issues/1074)).
- Теперь `jQuery` по умолчанию подключается через `https` ([#1202](https://github.com/bem/bem-core/issues/1202)).
- Удалена зависимость от `bemhtml-compat` ([#1186](https://github.com/bem/bem-core/issues/1186)). Пользователям `bem-tools` необходимо выполнить `npm i bemhtml-compat --save` для установки пакета на уровне проекта.
### В релиз вошли следующие исправления ошибок
- Исправлена ошибка в `loader_type_js`, допускавшая вызовы неопределенного обработчика ([#1159](https://github.com/bem/bem-core/pull/1159)).
### Также в релиз вошли следующие изменения
- BH-бандлы в `dist` теперь мимикрируют под BEMHTML ([#1210](https://github.com/bem/bem-core/issues/1210)).
- Улучшены шаблоны `bem create` для `bemhtml`, `bemtree`, `vanilla.js` и `browser.js` ([#1183](https://github.com/bem/bem-core/issues/1183)).
- `vow` обновлена до `0.4.10` ([#1056](https://github.com/bem/bem-core/issues/1056)).
## 2.7.0
### Крупные изменения
- В `i-bem__dom` добавлен новый метод `detach` ([#1102](https://github.com/bem/bem-core/issues/1102)).
- В `i-bem.bemhtml` добавлена поддержка вложенных миксов ([873](https://github.com/bem/bem-core/issues/873)).
- В `i-bem.bemhtml` добавлены незначительные оптимизации, связанные с эскейпингом аттрибутов ([#961](https://github.com/bem/bem-core/issues/961)), ([#980](https://github.com/bem/bem-core/issues/980)) и ([#982](https://github.com/bem/bem-core/issues/982)).
- В BEMHTML-шаблоны добавлена поддержка [bem-xjst](https://github.com/bem/bem-xjst) 2.x ([#1021](https://github.com/bem/bem-core/issues/1021)).
- `clearfix` оптимизирован для работы в поддерживаемых библиотекой браузерах IE ([#722](https://github.com/bem/bem-core/issues/722)).
- `jquery` обновлен до версий 2.1.4 и 1.11.3 ([#999](https://github.com/bem/bem-core/issues/999)).
### В релиз вошли следующие исправления ошибок
- Исправлена ошибка в `i-bem__dom`, из-за которой метод `findElem` не обновлял кэш ранее найденных
элементов ([#583](https://github.com/bem/bem-core/issues/583)).
- Исправлена ошибка в `i-bem__dom`, приводящая к неправильной работе метода `dropElemCache` в некоторых граничных
случаях ([#1037](https://github.com/bem/bem-core/issues/1037)).
- Исправлена ошибка в `i-bem__dom`, из-за которой вызов метода `setMod` не выставлял CSS-классы блоку в случае, если
на DOM-узеле был подмешан блок с пересекающимся окончанием в имени ([#1090](https://github.com/bem/bem-core/issues/1090)).
- Исправлена ошибка в `page`, из-за которой на touch-уровнях не работало специализированное поле `zoom` ([#1020](https://github.com/bem/bem-core/issues/1020)).
- Исправлена ошибка в `keyboard__codes`. Клавиши `insert` и `delete` были описаны неправильными
кодами ([#1002](https://github.com/bem/bem-core/issues/1002)).
- Исправлена ошибка в `i-bem.bemhtml`, из-за которой неверно интерпретировались вложенные вызовы `applyNext` ([b1dc50c](https://github.com/bem/bem-core/commit/b1dc50c621b5659cff33daa4dd3f210b67cf25e1)).
- Исправлена ошибка в `jquery__events_type_pointernative`, из-за которой события работали некорректно
в браузерах IE 11 и Edge ([#1066](https://github.com/bem/bem-core/issues/1066)).
### Также в релиз вошли следующие изменения
- Обновлена русская докуметация для всех блоков библиотеки. Документация доступна по адресу https://ru.bem.info/libs/bem-core/.
- Прочие улучшения в документации к библиотеке.
## 2.6.0
### Крупные изменения
- Предоставление модуля `i-bem__dom` теперь происходит после наступления события DOM ready ([#859](https://github.com/bem/bem-core/issues/859)).
- Методы `setMod` и `hasMod` модуля `i-bem__dom` теперь явно преобразуют параметр `modVal` к строке,
если переданное значение не типа string или boolean ([#890](https://github.com/bem/bem-core/issues/890)).
- В блок `page` добавлена возможность прокидывать атрибут `nonce`, для корректной работы инлайн-скриптов, в соответствии
со спецификацией Content Security Policy ([#882](https://github.com/bem/bem-core/issues/882)).
- Добавлены шаблоны `page__conditional-comment` ([#551](https://github.com/bem/bem-core/issues/511)).
- Модуль `vow` обновлен до версии 0.4.8 ([#837](https://github.com/bem/bem-core/issues/837)).
### В релиз вошли следующие исправления ошибок
- Исправлена ошибка в `i-bem.bemhtml`, из-за которой CSS-класс блока дублировался, в случае микса с этим же блоком
([#792](https://github.com/bem/bem-core/issues/792)).
- Исправлена ошибка в `loader_type_bundle`, из-за которой функция-обработчик успешного результата могла выполняться
после наступления таймаута ([67ff55f](https://github.com/bem/bem-core/commit/da5fdb9923e7e83e3ef9cd31aefc3967ff55fd3c)).
- Исправлена ошибка в `i-bem__dom`, в некоторых случаях, приводящая к неправильной интерпретации строковых аргументов
в методах `append`, `prepend` и др. ([#852](https://github.com/bem/bem-core/issues/852)).
- Исправлена ошибка в `jquery__event_type_winresize`, из-за которой неправильно определятся браузер MSIE ([#862](https://github.com/bem/bem-core/issues/862)).
- Исправлена ошибка в `object`, из-за которой метод `extend` неправильно обрабатывал `null` в качестве значения
аргумента `target` ([#910](https://github.com/bem/bem-core/issues/910)).
- Исправлена ошибка в `page`. Из BEMJSON было невозможно отключить добавление meta-тега `x-ua-compatible` ([#794](https://github.com/bem/bem-core/issues/794)).
### Также в релиз вошли следующие изменения
- Таймаут в `loader_type_bundle` увеличен до 30000 мс ([4e27422](https://github.com/bem/bem-core/commit/000c6af02bfae4506fa460168de16d4e27422393)).
- Исправлены незначительные ошибки в русской документации блоков.
## 2.5.1
### В релиз вошли следующие исправления ошибок
- Исправлена ошибка в `jquery__pointerpress` и `jquery__pointerrelease` из-за которой события не работали в браузере
Internet Explorer 8 ([#792](https://github.com/bem/bem-core/issues/792)).
- Исправлена ошибка в `jquery__pointernative`. События `pointerenter` и `pointerleave` не должны всплывать
по DOM-дереву ([#801](https://github.com/bem/bem-core/issues/801)).
- Исправлена ошибка в `loader_type_bundle`. После загрузки, CSS-бандл добавлялся в самый верх HTML-тега ``, из-за чего
CSS-правила из содержимого бандла могли работать не корректно ([#808](https://github.com/bem/bem-core/issues/808)).
- Исправлена ошибка в BH-шаблоне `ua`. Шаблон не позволял вставить содержимое блока из входного
BEMJSON ([#734](https://github.com/bem/bem-core/pull/734)).
- Исправлена ошибка в `page`, приводящая к неработоспособности добавленных на страницу условных комментариев для браузера
Internet Explorer ([#781](https://github.com/bem/bem-core/pull/781)).
### Также в релиз вошли следующие изменения
- `jquery` обновлен до версий 2.1.3 и 1.11.2 ([#778](https://github.com/bem/bem-core/pull/788)).
- Добавлена документация на русском языке для модулей: `clearfix`, `cookie`, `identify`, `idle`, `inherit`, `keyboard`,
`loader`, `next-tick`, `string` and `tick`.
- Исправлена документация на русском языке для `i-bem.js`.
- Обновлено руководство на английском языке по технологиям BEMHTML и BEMJSON.
## 2.5.0
### Крупные изменения
- Код библиотеки переведен на использование лицензии [MPL 2.0](https://www.mozilla.org/MPL/2.0/) ([#443](https://github.com/bem/bem-core/issues/443)).
- В модуль `loader_type_js` добавлена возможность указывать функцию-обработчик ошибок ([#672](https://github.com/bem/bem-core/issues/672)).
- Класс `BEMContext` добавлен в export-параметры функции `oninit` в базовых шаблонах `i-bem.bemtree` ([#602](https://github.com/bem/bem-core/issues/602)).
- В `BEMContext` BEMTREE добавлен статический метод `reapply` по аналогии с BEMHTML ([#706](https://github.com/bem/bem-core/pull/706)).
- Добавлены bh-шаблоны блока `page` для уровней touch ([#689](https://github.com/bem/bem-core/pull/689)).
- npm-модуль [bem-xjst](https://github.com/bem/bem-xjst) обновлен до версии 0.9.0 ([#709](https://github.com/bem/bem-core/pull/709)).
### В релиз вошли следующие исправления ошибок
- Исправлена ошибка в `i-bem__dom`, из-за которой метод `findBlocksInside` мог возвращать блоки, которые еще не были
инициализированы ([#699](https://github.com/bem/bem-core/issues/699)).
- Исправлена ошибка в `tick`, позволявшая вызвать метод `stop` без освобождения внутреннего таймера ([#694](https://github.com/bem/bem-core/issues/694)).
- Исправлена ошибка в `i-bem.bemhtml`, из-за которой на элементы блока добавлялся CSS-класс `i-bem` ([#633](https://github.com/bem/bem-core/issues/633)).
- Исправлена ошибка в технологии `html-from-bemtree`, из-за которой в контексте BEMTREE-шаблонов не было глобальных объектов
`vow`, `console`, `setTimeout` ([#438ebb8](https://github.com/bem/bem-core/commit/438ebb8f828e26977592e26511e8aad15176d7a4)).
### Также в релиз вошли следующие изменения
- Добавлено английское руководство по технологии BEMJSON.
- Обновлена русская документация для i-bem.js. Теперь документация соответсвует текущему API библиотеки.
- Обновлена документация для технологий BEMHTML/BEMTREE.
## 2.4.0
### Крупные изменения
- npm-модуль [bem-xjst](https://github.com/bem/bem-xjst) обновлен до версии 0.8.0; [bemhtml-compat](https://github.com/bem/bemhtml-compat)
обновлен до 0.0.11.
### В релиз вошли следующие исправления ошибок
- Исправлена ошибка в `jquery__event_type_pointerpressrelease`, из-за которой события `pointerpress` / `pointerrelease` генерировались
на нажатие любой кнопки мыши ([#607](https://github.com/bem/bem-core/issues/607)).
- Исправлена ошибка в `i-bem__dom.js`, из-за которой в некоторых случаях не происходил вызов базового метода
`live` ([#608](https://github.com/bem/bem-core/issues/608)).
### Также в релиз вошли следующие изменения
- Добавлена английская документация на JS-синтаксис BEMHTML.
## 2.3.0
### Крупные изменения
- Добавлена новая реализация pointer-событий на основе полифилов из [Polymer](http://www.polymer-project.org/) ([#567](https://github.com/bem/bem-core/pull/567)).
- Добавлена возможность в `i-bem__dom.js` указывать дополнительные данные о событии в методах `bindTo*` ([#568](https://github.com/bem/bem-core/issues/568)).
### В релиз вошли следующие исправления ошибок
- Исправлена ошибка в `i-bem.bemhtml`, из-за которой было невозможно использовать микс в виде одного объекта (не массива) одновременно в BEMJSON и BEMHTML ([#555](https://github.com/bem/bem-core/issues/555)).
- Исправлена ошибка в BEMHTML-шаблоне блока `page`, из-за которой не выполнялись стандартные моды, и исправлена регрессия в шаблоне на touch-уровне ([516](https://github.com/bem/bem-core/issues/516)).
## 2.2.4
### В релиз вошли следующие исправления ошибок
- Исправлена ошибка в `i-bem.js`, из-за которой событие об изменении модификатора генерировалось,
даже если обработчик `beforeSetMod` предотвращал изменение ([#546](https://github.com/bem/bem-core/pull/546)).
- В случае возникновения ошибки в процессе декодирования строки, модуль `querystring__uri` теперь возвращает
оригинальную строку ([#554](https://github.com/bem/bem-core/pull/554)).
## 2.2.3
### В релиз вошли следующие исправления ошибок
- В модуле `i-bem__dom` был исправлен процесс удаления блока для предотвращения нежелательной повторной
инициализации блока ([#540](https://github.com/bem/bem-core/issues/540)).
- Исправлена ошибка в модуле `jquery__event_type_pointer`, из-за которой нативные события мыши ошибочно замещались
на pointer-события ([#534](https://github.com/bem/bem-core/issues/534)).
- `unbindFrom*`-методы в модуле `i-bem__dom` теперь поддерживают отписывание от нескольких событий
за вызов ([#533](https://github.com/bem/bem-core/issues/533)).
- Добавлена недостающая зависимость от модуля `functions` в модуле `events` ([#532](https://github.com/bem/bem-core/issues/532)).
## 2.2.2
### В релиз вошли следующие исправления ошибок
- Исправлена ошибка в модуле `i-bem__dom` приводящая к повторной инициализации блока на DOM-узле, отмеченном как
удаленный ([#518](https://github.com/bem/bem-core/issues/518)).
- Исправлена ошибка в модуле `i-bem`, из-за которой невозможно было подписаться на событие о выставлении модификатора в
значение `false` ([#529](https://github.com/bem/bem-core/issues/529)).
- Модуль `jquery` обновлен до версий 2.1.1 и 1.11.1 ([#515](https://github.com/bem/bem-core/issues/515)).
## 2.2.1
- Исправлена ошибка в модуле `jquery__event_type_pointerpressrelease`, из-за которой событие `pointerpress` генерировалось
дважды на каждое событие `mousedown` в IE10 ([#505](https://github.com/bem/bem-core/issues/505)).
## 2.2.0
### Крупные изменения
- Добавлен новый модуль `keyboard__codes` ([#431](https://github.com/bem/bem-core/issues/431)).
- Класс `BEMContext` добавлен в export-параметры функции `oninit` в базовых шаблонах `i-bem.bemhtml` ([#485](https://github.com/bem/bem-core/pull/485)).
- Добавлена возможность декларировать инстанс элемента используя класс блока ([#481](https://github.com/bem/bem-core/issues/481)).
- Исправлено поведение метода `isSimple` класса BEMContext в в базовых шаблонах `i-bem.bemhtml` ([#432](https://github.com/bem/bem-core/pull/432)).
- Исправлена ошибка в методе `liveUnbindFrom` модуля `BEMDOM` ([#476](https://github.com/bem/bem-core/pull/476)).
- Исправлена ошибка в методе `isFocusable` модуля `dom`, возникающая если переданный `domElem` является ссылкой
с атрибутом `tabindex`, но без атрибута `href` ([#501](https://github.com/bem/bem-core/issues/501)).
- Исправлена ошибка возникающая в процессе декларации БЭМ-блока как модуля, если был подключен
модуль `i-bem__dom_elem-instances` ([#479](https://github.com/bem/bem-core/issues/479)).
- В модуле `i-bem__dom_init_auto` добавлено временное решение для проблем с производительностью ренедеринга при инициализации блоков
в Chrome-браузерах ([#486](https://github.com/bem/bem-core/issues/486)).
- Модуль `vow.js` перенесен в `vow.vanilla.js` ([#412](https://github.com/bem/bem-core/issues/412)).
### Также в релиз вошли следующие изменения
- Модуль `vow` обновлен до версии 0.4.3 ([#504](https://github.com/bem/bem-core/pull/504)).
- Добавлена русская документация на технологию BEMTREE ([#500](https://github.com/bem/bem-core/pull/500)).
- Обновлена русская документация на JavaScript-синтаксис BEMHTML ([#471](https://github.com/bem/bem-core/pull/471)).
- Добавлен референс на API JavaScript-модулей. См. ветку `v2-jsdoc` ([#478](https://github.com/bem/bem-core/pull/478)).
## 2.1.0
### Крупные изменения
- Исправлена ошибка в `i-bem.js`, из-за которой событие об изменении модификатора происходило до того, как будет
вызван обработчик реакции на изменение этого модификатора в `onSetMod` ([#454](https://github.com/bem/bem-core/issues/454)).
- Свойства `this.mods` и `this.ctx.mods` базового шаблона `i-bem.bemhtml` теперь используют
один и тот же объект ([#441](https://github.com/bem/bem-core/issues/441)).
- Модуль [inherit](https://github.com/dfilatov/inherit) обновлен до версии 2.2.1 ([#466](https://github.com/bem/bem-core/issues/466)).
- Исправлен порядок тегов секции `head` в шаблоне `page.bemhtml` ([#465](https://github.com/bem/bem-core/pull/465)).
### Также в релиз вошли следующие изменения
- В русскую документацию к `i-bem.js` добавлено описание поля `baseMix` ([#461](https://github.com/bem/bem-core/pull/461)).
- CDN-хост внешних ресурсов изменен на `yastatic.net` ([#444](https://github.com/bem/bem-core/issues/444)).
Все ресурсы все так же доступны с хоста `yandex.st`. Физически `yandex.st` и `yastatic.net` находятся на
одних и тех же серверах. Различие только в DNS-записях.
- Добавлен базовый BEMHTML-шаблон для команды `bem create` технологии `bemhtml` ([#277](https://github.com/bem/bem-core/issues/277)).
- Прекращен автоматический запуск тестов под Node.js 0.8 в [Travis CI](http://travis-ci.com) ([#455](https://github.com/bem/bem-core/issues/455)).
- Иконка статуса автосборки Travis [заменена на SVG](http://blog.travis-ci.com/2014-03-20-build-status-badges-support-svg/) :)
## 2.0.0
### Изменения, ломающие обратную совместимость
- Из `i-bem.js` и `i-bem__dom.js` удалены все **deprecated** методы ([#318](https://github.com/bem/bem-core/issues/318)):
* `destruct`, используйте `onSetMod js ''`;
* `extractParams`, используйте `elemParams`;
* `trigger`, используйте `emit`;
* `afterCurrentEvent`, используйте модуль `next-tick`;
* `channel`, используйте модуль `events__channels`;
* `changeThis`, используйте нативный `Function.prototype.bind`.
- Из `i-bem.js` убраны события `init` и `destruct`. Вместо них следует использовать события об изменении модификатора
(см. «Крупные изменения»).
- Блок `ecma` перенесен [в отдельный репозиторий](http://github.com/bem/es5-shims); ES5-shims следует использовать
для IE < 9 ([#230](https://github.com/bem/bem-core/issues/230)).
- Модуль `vow` обновлен до мажорной версии 0.4.1 ([#350](https://github.com/bem/bem-core/issues/350)).
См. [изменения в Vow](https://github.com/dfilatov/vow/blob/0.4.1/CHANGELOG.md).
- В `i-bem.bemhtml` добавлена поддержка vow@0.4 ([#385](https://github.com/bem/bem-core/issues/385)).
### Крупные изменения
- Добавлена возможность декларировать BEMDOM-блоки как модули [ym](https://github.com/ymaps/modules) ([#382](https://github.com/bem/bem-core/issues/382)).
- В `i-bem.js` добавлены события об изменении модификатора ([#357](https://github.com/bem/bem-core/issues/357)).
- Добавлена поддержка использования строковых значений в качестве аргумента в методах `BEMDOM.init` ([#419](https://github.com/bem/bem-core/issues/419))
и `BEMDOM.update` ([#420](https://github.com/bem/bem-core/issues/420)).
- Методы `i-bem__dom.js` `replace`, `append`, `prepend`, `before`, `after` теперь возвращают новый контекст,
а `update` – изменённый ([#410](https://github.com/bem/bem-core/issues/410)).
- В `loader` добавлен модификатор `_type_bundle` ([#358](https://github.com/bem/bem-core/issues/358)).
- jQuery обновлен до версии 2.1.0. Для IE < 9 — до версии 1.11.0 ([#356](https://github.com/bem/bem-core/issues/356)).
### Также в релиз вошли следующие изменения
- Базовые шаблоны в `i-bem.bemhtml` используют конкатенацию строк вместо наполнения внутреннего буфера ([#401](https://github.com/bem/bem-core/issues/401)).
- jQuery больше не удаляет себя из глобальной области видимости, если присутствует на странице ([#349](https://github.com/bem/bem-core/issues/349)).
- `jquery__event_type_pointerclick.js` перемещен с уровня `touch.blocks` на уровень `common.blocks` ([#393](https://github.com/bem/bem-core/issues/393)).
- Модификаторы `i-bem_elem-instances_yes` и `i-bem__dom_elem-instances_yes` приведены к булевому стилю ([#352](https://github.com/bem/bem-core/issues/352)).
- Исправлена ошибка в шаблоне блока `page`, возникающая при использовании development-режима BEMHTML ([#417](https://github.com/bem/bem-core/issues/417)).
- Для поддержки Android 2.3 внутри `i-bem.js` отказались от использований `Function.prototype.bind` ([#404](https://github.com/bem/bem-core/issues/404)).
- Исправлены ошибки в модуле технологии `browser-js+bemhtml` ([#392](https://github.com/bem/bem-core/issues/392)).
- NPM-модуль `ym` обновлен до версии [0.0.15](https://github.com/ymaps/modules/releases) ([#414](https://github.com/bem/bem-core/issues/414)).
## 1.2.0
### Крупные изменения
- BEM-блоки инициируют событие `destruct` в процессе удаления ([#370](https://github.com/bem/bem-core/issues/370)).
- Исправлены полифилы для `pointerevents` ([#354](https://github.com/bem/bem-core/pull/354)).
### Также в релиз вошли следующие изменения
- JSDoc блоков исправлен в соответствии с поддержкой [bem-jsd](github.com/bem/bem-jsd) ([#335](https://github.com/bem/bem-core/issues/335)).
- Референс на BEMHTML обновлен для соответствия JavaScript-синтаксису шаблонизатора ([#355](https://github.com/bem/bem-core/pull/355)).
- Переход на менеджер зависимостей [bower](http://bower.io) ([#367](https://github.com/bem/bem-core/issues/367)).
## 1.1.0
### Крупные изменения
- Для современных браузеров `jquery__config` подключает jQuery 2.x ([#319](https://github.com/bem/bem-core/issues/319)).
- Добавлена возможность использовать произвольный BEMJSON в качестве значения атрибутов в BEMHTML ([#290](https://github.com/bem/bem-core/issues/290)).
- Исправлены зависимости в `i-bem__collection` ([#292](https://github.com/bem/bem-core/issues/292)).
- Удалены CSS-стили блока `page` из уровня `touch.blocks` ([#306](https://github.com/bem/bem-core/issues/306)).
- Исправлена ошибка в BEMHTML-шаблоне блока `page`, приводящая к зацикливанию шаблонизатора
в production-режиме ([#309](https://github.com/bem/bem-core/issues/309)).
- Исправлена возможная ошибка в `next-tick`, возникающая при вставке скрипта в DOM в IE<9 ([#324](https://github.com/bem/bem-core/issues/324)).
- Исправлена ошибка в инициализации плагина `FastClick` в модуле `jquery__event_type_pointerclick`
на уровне `touch.blocks` ([#332](https://github.com/bem/bem-core/issues/332)).
- Исправлена ошибка в технологии `node.js` в Windows ([#274](https://github.com/bem/bem-core/issues/274)).
- Исправлена ошибка в `onElemSetMod` в `i-bem__dom_elem-instances` ([#340](https://github.com/bem/bem-core/issues/340)).
- В технологии `bemhtml` используется [bem-xjst](https://github.com/bem/bem-xjst) ([#329](https://github.com/bem/bem-core/issues/329)).
### Также в релиз вошли следующие изменения
- Модуль [ym](https://github.com/ymaps/modules) обновлен до версии 0.0.12 ([#326](https://github.com/bem/bem-core/issues/326)).
- В ядре локализации `i-bem__i18n` отключен вывод сообщений о неизвестных ключах, если не включен
debug-режим ([#285](https://github.com/bem/bem-core/issues/285)).
- Инфраструктура сборки тестов и примеров переведена
на [bem-pr@v0.5.x](https://github.com/narqo/bem-pr/blob/0.5.3/HISTORY.md) ([#323](https://github.com/bem/bem-core/issues/323)).
- Исправлен jsdoc для метода `dropElemCache()` в `i-bem__dom` ([#296](https://github.com/bem/bem-core/issues/296)).
- Доработана документация для блока `i-bem.js` на русском языке.
- В README проекта добавлен [список поддерживаемых браузеров](https://github.com/bem/bem-core/blob/v1/README.ru.md#%D0%9F%D0%BE%D0%B4%D0%B4%D0%B5%D1%80%D0%B6%D0%B8%D0%B2%D0%B0%D0%B5%D0%BC%D1%8B%D0%B5-%D0%B1%D1%80%D0%B0%D1%83%D0%B7%D0%B5%D1%80%D1%8B).
## 1.0.0
### Крупные изменения
- Переход на модульную систему [ym](https://github.com/ymaps/modules).
- Из `i-bem`, `i-bem__dom` убраны все deprecated-методы.
- `i-bem` больше не зависит от jQuery. `i-bem__dom` продолжает зависеть от jQuery.
- BEMHTML-шаблоны можно писать с использованием [JS-синтаксиса](https://gist.github.com/veged/6150760).
- Новая технология `bemtree` (на базе [bem-xjst](https://github.com/bem/bem-xjst)) для описания процесса
динамического построения БЭМ-дерева.
- Новая технология `vanilla.js` для описания JS-реализации модулей, не зависящей от конкретного JavaScript движка.
- Новые технологии `browser.js` и `node.js` для описания JS-реализаций модулей (блоков) в соответствующих движках.
Для совместимости с существующим кодом считаем, что файлы с расширением `.js` содержат реализацию блоков
в технологии `browser.js`.
- Система модульного тестирования и примеров для блоков в библиотеке.
- Появились полифилы (`jquery__event_type_pointer` и `jquery__event_type_pointerclick` как jQuery-плагины),
позволяющие использовать универсальные события для десктопных и тач-интерфейсов.
- Плагин для jQuery, позволяющий навешивать обработчик события на нажатие левой кнопки мыши, становится модулем `jquery__pointerclick`.
- В `i-bem` и BEMHTML добавлена поддержка простых модификаторов (модификаторов без значений).
### Также в релиз вошли следующие изменения
- Все блоки-модули, кроме `i-bem`, избавились от префиксов.
- Блок `i-bem__dom` становится модулем `i-bem__dom`. Все BEM.DOM-блоки должны теперь доопределять
этот модуль ([пример](https://github.com/bem/bem-core/blob/v1/common.bundles/index/blocks/b-square/b-square.js)).
- Метод для декларации блоков (`.decl()`) больше не принимает первым параметром объект с полем `name`.
Теперь обязательная форма записи с полем `block`: `BEM.decl({ block: 'b1', modName: 'm', modVal: 'v' }, ...)`.
- Вместо метода `afterCurrentEvent` у блоков появился метод `nextTick`, который проверят существование блока в момент исполнения колбэка. `BEM.afterCurrentEvent` теперь **deprecated**.
- Вместо `BEM.channel` появился отдельный модуль `channels`. `BEM.channel` теперь **deprecated**.
- Метод `changeThis` помечен как **deprecated**. Используйте нативный `bind`.
- Метод `del` удален из блока `i-bem`.
- Метод `getWindowSize` удален из блока `i-bem__dom`. Используйте `BEMDOM.win.width()` и `BEMDOM.win.height()`.
- Добавлен модуль-обертка `jquery`, предоставляющий jQuery. Модуль либо предоставляет jQuery, уже присутствующий на странице, либо сам его загружает (версию 1.10.1).
- `$.observable` становится модулем `events` и больше не зависит от jQuery.
- `$.inherit` становится модулем `inherit` и больше не зависит от jQuery.
- `$.identify` становится модулем `identify` и больше не зависит от jQuery.
- `$.throttle` разбивается на два модуля: `functions__throttle` и `functions__debounce`, которые больше не зависят от jQuery.
- `$.decodeURI`, `$.decodeURIComponent` переезжают в модуль `querystring__uri` и больше не зависят от jQuery.
- `$.cookie` становится модулем `cookie` и больше не зависит от jQuery.
- Вместо `$.browser` появился модуль `ua` с аналогичным интерфейсом.
- Блок `i-system` разбит на 2 модуля: `idle` и `tick`.
- Вместо события `leftclick` следует использовать `pointerclick` (предоставляемый полифилом `jquery__event_type_pointerclick`).
- Триггеры на установку модификаторов теперь разделены на две группы: до установки модификатора (`beforeSetMod` и `beforeElemSetMod`) и после (`onSetMod` и `onElemSetMod`). Отмена установки модификатора теперь возможна только из триггеров первой группы.
- Использовать конструкцию `{ onSetMod : { js : function() { ... } } }` в качестве конструктора теперь **deprecated**, необходимо использовать `onSetMod: { js : { inited : ... } } }`.
- Вместо метода `destruct` в `i-bem` появился зеркальный метод
для `onSetMod: { js : { inited : ... } } }` — `{ onSetMod : { js : { '' : ... } } }`.
Метод `destruct` теперь **deprecated**.
- Метод `exractParams` в `i-bem__dom` теперь **deprecated**, для доступа к параметрам элементов нужно использовать метод `elemParams`.
- Метод `trigger` в `i-bem` теперь **deprecated**, нужно использовать `emit`.
- Метод `onFirst` в `i-bem` теперь **deprecated**, нужно использовать `once`.
- Удалено **deprecated** поле `e.block`, представляющее блок-источник события для BEM-событий. Вместо него следует использовать поле `e.target`.
- Для доступа к DOM-элементу блока в обработчике DOM-событий теперь нужно использовать поле `currentTarget`, предоставляемое jQuery. Вместо `e.data.domElem`нужно писать `$(e.currentTarget)`.
- В методе `findElem` добавлен параметр, позволяющий находить элемента блока с учетом вложенных блоков.
- Добавлена возможность указывать конкретную функцию для отписки от событий в методах `unbindFrom*`.
- Добавлен модуль `objects` для работы с JS-объектами (содержит методы `extend`, `isEmpty`, `each`).
- Добавлен модуль `functions` для работы с JS-функциями (содержит методы `isFunction`, `noop`).
- Добавлен модуль `dom` для хелперов при работе с DOM.
- Добавлен модуль `querystring` для работы с урлами.
- Добавлен модуль `loader_type_js` для загрузки JS.
- Добавлен модуль `vow` для работы с промисами.
- Добавлен модуль `next-tick` для полифила `nextTick`, `setImmediate`, `setTimeout(0, ...` и т.п..
- Добавлен модуль `strings__escape`, содержащий методы для эскейпинга XML, HTML и атрибутов.
- Модуль `inherit` теперь поддерживает миксины.
- В модуле `functions__throttle` добавлен параметр `invokeAsap`, позволяющий отложить первое исполнение.
================================================
FILE: CLA.md
================================================
# Notice to external contributors
## General info
Hello! In order for us (YANDEX LLC) to accept patches and other contributions from you, you will have to adopt our Yandex Contributor License Agreement (the “**CLA**”). The current version of the CLA you may find here:
1) https://yandex.ru/legal/cla/?lang=en (in English) and
2) https://yandex.ru/legal/cla/?lang=ru (in Russian).
By adopting the CLA, you state the following:
* You obviously wish and are willingly licensing your contributions to us for our open source projects under the terms of the CLA,
* You has read the terms and conditions of the CLA and agree with them in full,
* You are legally able to provide and license your contributions as stated,
* We may use your contributions for our open source projects and for any other our project too,
* We rely on your assurances concerning the rights of third parties in relation to your contributes.
If you agree with these principles, please read and adopt our CLA. By providing us your contributions, you hereby declare that you has already read and adopt our CLA, and we may freely merge your contributions with our corresponding open source project and use it in further in accordance with terms and conditions of the CLA.
## Provide contributions
If you have already adopted terms and conditions of the CLA, you are able to provide your contributes. When you submit your pull request, please add the following information into it:
```
I hereby agree to the terms of the CLA available at: [link].
```
Replace the bracketed text as follows:
* `[link]` is the link at the current version of the CLA (you may add here a link https://yandex.ru/legal/cla/?lang=en (in English) or a link https://yandex.ru/legal/cla/?lang=ru (in Russian).
It is enough to provide us such notification at once.
## Other questions
If you have any questions, please mail us at opensource@yandex-team.ru.
================================================
FILE: CONTRIBUTING.md
================================================
# How to contribute
1. [Create an issue](https://github.com/bem/bem-core/issues/new) with a proper description.
2. Decide which version needs your changes.
3. Create a feature-branch with an issue number and a version (`issues/@v`) based on a version branch.
For example, for an issue #42 and a version #1: `git checkout -b issues/42@v1 v1`.
If you need changes for several versions, each of them has to have a feature branch.
4. Commit changes accordingly to [CLA](CLA.md) and `push`. Rebase your branch on a corresponding version branch if it's needed.
5. Create a pull-request from your feature branch; or several pull-requests if you changed several versions.
6. Link your pull request with an issue number any way you like. A comment will work perfectly.
7. Wait for your pull request and the issue to be closed ;-)
## Contributors
The list of contributors is available at https://github.com/bem/bem-core/graphs/contributors. You may also get it with `git log --pretty=format:"%an <%ae>" | sort -u`.
================================================
FILE: CONTRIBUTING.ru.md
================================================
# Внесение изменений
1. [Создать issue](https://github.com/bem/bem-core/issues/new) с описанием сути изменений.
2. Определить в какую версию необходимо внести изменения.
3. Сделать feature-branch с указанием номера issue и версии (`issues/<номер_issue>@v<номер_версии>`) на основе ветки версии.
Например, для issue с номером 42 и версией 1: `git checkout -b issues/42@v1 v1`. Если изменения нужно внести в несколько версий,
то для каждой из версий создаётся отдельная ветка.
4. Сделать изменения, закоммитить согласно с [CLA](CLA.md) и сделать push. Если это необходимо, то нужно сделать rebase от базовой ветки версии.
5. Создать pull-request на основе созданной ветки (или несколько pull-request-ов для случая изменений в нескольких версиях).
6. Любым способом связать pull-request и issue (например, c помощью комментария).
7. Ждать закрытия pull-request и issue ;-)
## Контрибьюторы
Список контрибьютеров данного проекта доступен по ссылке https://github.com/bem/bem-core/graphs/contributors. Вы так же можете получить его с помощью команды `git log --pretty=format:"%an <%ae>" | sort -u`.
================================================
FILE: LICENSE.txt
================================================
© YANDEX LLC, 2012
The Source Code called `bem-core` available at https://github.com/bem/bem-core is subject to the terms of the Mozilla Public License, v. 2.0 (hereinafter - MPL). The text of MPL is the following:
Mozilla Public License, version 2.0
1. Definitions
1.1. "Contributor"
means each individual or legal entity that creates, contributes to the
creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used by a
Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached the
notice in Exhibit A, the Executable Form of such Source Code Form, and
Modifications of such Source Code Form, in each case including portions
thereof.
1.5. "Incompatible With Secondary Licenses"
means
a. that the initial Contributor has attached the notice described in
Exhibit B to the Covered Software; or
b. that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the terms of
a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in a
separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible, whether
at the time of the initial grant or subsequently, any and all of the
rights conveyed by this License.
1.10. "Modifications"
means any of the following:
a. any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered Software; or
b. any new file in Source Code Form that contains any Covered Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the License,
by the making, using, selling, offering for sale, having made, import,
or transfer of either its Contributions or its Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU Lesser
General Public License, Version 2.1, the GNU Affero General Public
License, Version 3.0, or any later versions of those licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that controls, is
controlled by, or is under common control with You. For purposes of this
definition, "control" means (a) the power, direct or indirect, to cause
the direction or management of such entity, whether by contract or
otherwise, or (b) ownership of more than fifty percent (50%) of the
outstanding shares or beneficial ownership of such entity.
2. License Grants and Conditions
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
a. under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
b. under Patent Claims of such Contributor to make, use, sell, offer for
sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
a. for any code that a Contributor has removed from Covered Software; or
b. for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
c. under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights to
grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
Section 2.1.
3. Responsibilities
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
a. such Covered Software must also be made available in Source Code Form,
as described in Section 3.1, and You must inform recipients of the
Executable Form how they can obtain a copy of such Source Code Form by
reasonable means in a timely manner, at a charge no more than the cost
of distribution to the recipient; and
b. You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter the
recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty, or
limitations of liability) contained within the Source Code Form of the
Covered Software, except that You may alter any license notices to the
extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
If it is impossible for You to comply with any of the terms of this License
with respect to some or all of the Covered Software due to statute,
judicial order, or regulation then You must: (a) comply with the terms of
this License to the maximum extent possible; and (b) describe the
limitations and the code they affect. Such description must be placed in a
text file included with all distributions of the Covered Software under
this License. Except to the extent prohibited by statute or regulation,
such description must be sufficiently detailed for a recipient of ordinary
skill to be able to understand it.
5. Termination
5.1. The rights granted under this License will terminate automatically if You
fail to comply with any of its terms. However, if You become compliant,
then the rights granted under this License from a particular Contributor
are reinstated (a) provisionally, unless and until such Contributor
explicitly and finally terminates Your grants, and (b) on an ongoing
basis, if such Contributor fails to notify You of the non-compliance by
some reasonable means prior to 60 days after You have come back into
compliance. Moreover, Your grants from a particular Contributor are
reinstated on an ongoing basis if such Contributor notifies You of the
non-compliance by some reasonable means, this is the first time You have
received notice of non-compliance with this License from such
Contributor, and You become compliant prior to 30 days after Your receipt
of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
license agreements (excluding distributors and resellers) which have been
validly granted by You or Your distributors under this License prior to
termination shall survive termination.
6. Disclaimer of Warranty
Covered Software is provided under this License on an "as is" basis,
without warranty of any kind, either expressed, implied, or statutory,
including, without limitation, warranties that the Covered Software is free
of defects, merchantable, fit for a particular purpose or non-infringing.
The entire risk as to the quality and performance of the Covered Software
is with You. Should any Covered Software prove defective in any respect,
You (not any Contributor) assume the cost of any necessary servicing,
repair, or correction. This disclaimer of warranty constitutes an essential
part of this License. No use of any Covered Software is authorized under
this License except under this disclaimer.
7. Limitation of Liability
Under no circumstances and under no legal theory, whether tort (including
negligence), contract, or otherwise, shall any Contributor, or anyone who
distributes Covered Software as permitted above, be liable to You for any
direct, indirect, special, incidental, or consequential damages of any
character including, without limitation, damages for lost profits, loss of
goodwill, work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses, even if such party shall have been
informed of the possibility of such damages. This limitation of liability
shall not apply to liability for death or personal injury resulting from
such party's negligence to the extent applicable law prohibits such
limitation. Some jurisdictions do not allow the exclusion or limitation of
incidental or consequential damages, so this exclusion and limitation may
not apply to You.
8. Litigation
Any litigation relating to this License may be brought only in the courts
of a jurisdiction where the defendant maintains its principal place of
business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions. Nothing
in this Section shall prevent a party's ability to bring cross-claims or
counter-claims.
9. Miscellaneous
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides that
the language of a contract shall be construed against the drafter shall not
be used to construe this License against a Contributor.
10. Versions of the License
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses If You choose to distribute Source Code Form that is
Incompatible With Secondary Licenses under the terms of this version of
the License, the notice described in Exhibit B of this License must be
attached.
Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular file,
then You may include the notice in a location (such as a LICENSE file in a
relevant directory) where a recipient would be likely to look for such a
notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
This Source Code Form is "Incompatible
With Secondary Licenses", as defined by
the Mozilla Public License, v. 2.0.
A copy of the MPL is also available at http://mozilla.org/MPL/2.0/.
================================================
FILE: MIGRATION.md
================================================
# Migration
## 5.0.0
### ym → ES modules
The `ym` module system (`modules.define`/`modules.require`) has been replaced with native ES modules.
Before:
```js
modules.define('my-block', ['i-bem-dom', 'events'], function(provide, bemDom, events) {
provide(bemDom.declBlock(this.name, { /* ... */ }));
});
```
After:
```js
import bemDom from 'bem:i-bem-dom';
import events from 'bem:events';
export default bemDom.declBlock('my-block', { /* ... */ });
```
All `bem:*` imports are resolved at build time by `vite-plugin-bem-levels` to the actual file paths, respecting platform-specific level priorities.
### Module redefinitions
Module redefinitions that previously used `modules.define` with a callback receiving the previous module value are now handled via barrel files generated by the Vite plugin.
Before (ym redefinition):
```js
modules.define('jquery', function(provide, $) {
$.event.special.pointerclick = { /* ... */ };
provide($);
});
```
After (side-effect import in barrel):
```js
// The barrel file auto-generated by vite-plugin-bem-levels:
import $ from '../../common.blocks/jquery/jquery.js';
import '../../common.blocks/jquery/__event/_type/jquery__event_type_pointerclick.js';
export default $;
```
### jQuery 3 → 4
The `jquery` peer dependency is now `^4.0.0`.
Key breaking changes in jQuery 4:
- `$.unique()` has been removed. Use `$.uniqueSort()`.
- Several deprecated methods have been removed. See [jQuery 4.0 upgrade guide](https://jquery.com/upgrade-guide/4.0/).
### vow → native Promise
The `vow` dependency has been removed. All asynchronous code now uses native `Promise`.
Before:
```js
var vow = require('vow');
var promise = vow.resolve(value);
```
After:
```js
const promise = Promise.resolve(value);
```
### Build system: ENB → Vite
The entire ENB toolchain has been replaced with Vite.
Before:
```bash
./node_modules/.bin/enb make
```
After:
```bash
npm run build # both platforms
npm run build:desktop # desktop only
npm run build:touch # touch only
```
The Vite config is in `build/vite.config.js`. The custom `vite-plugin-bem-levels` plugin in `build/plugins/` handles BEM level scanning and module resolution.
### Node.js 20+
The minimum supported Node.js version is now 20 (was 8). Update your `.nvmrc` or CI configuration accordingly.
### Linting: jshint/jscs → ESLint 10
Replace any custom `.jshintrc` or `.jscs.json` rules with ESLint flat config (`eslint.config.js`).
### Testing
- **Server-side tests**: `mocha`/`chai` have been replaced with `node:test`/`node:assert`.
- **Browser tests**: `mocha-phantomjs` has been replaced with Playwright. Run with `npm run test:browser`.
- **All tests**: `npm run test:all` runs both server-side and browser tests.
### CI/CD: Travis → GitHub Actions
Replace `.travis.yml` with `.github/workflows/ci.yml`. The new CI runs lint, test, test:browser, and build jobs.
### Git hooks: git-hooks → husky
Replace `.githooks/` with husky configuration. The `prepare` script in `package.json` sets up husky automatically on `npm install`.
## 4.0.0
### Changes in the `i-bem` block
#### Separate `i-bem-dom` block
The `dom` element of the `i-bem` block was moved to a separate `i-bem-dom` block.
Before:
```js
modules.define('my-dom-block', ['i-bem__dom'], function(provide, BEMDOM) {
/* ... */
});
```
After:
```js
modules.define('my-dom-block', ['i-bem-dom'], function(provide, bemDom) {
/* ... */
});
```
The `i-bem` and `i-bem-dom` blocks are no longer classes. They are modules with methods for declaring BEM entities, links to classes of BEM entities, and some additional helpers. These methods are no longer class methods for the corresponding blocks.
Issue: [#413](https://github.com/bem/bem-core/issues/413).
#### Declaration
#### Block declaration
Instead of the `decl()` method, use the `declBlock()` method to declare a block.
Before:
```js
modules.define('my-dom-block', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, { /* ... */ }));
});
```
After:
```js
modules.define('my-dom-block', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, { /* ... */ }));
});
```
#### Modifier declaration
Instead of the static `decl()` method, use the static `declMod()` method to declare a modifier.
Before:
```js
modules.define('my-dom-block', function(provide, MyDomBlock) {
provide(MyDomBlock.decl({ modName : 'my-mod', modVal : 'my-val' }, { /* ... */ }));
});
```
After:
```js
modules.define('my-dom-block', function(provide, MyDomBlock) {
provide(MyDomBlock.declMod({ modName : 'my-mod', modVal : 'myVal' }, { /* ... */ }));
});
```
#### Boolean modifier declaration
Before:
```js
modules.define('my-dom-block', function(provide, MyDomBlock) {
provide(MyDomBlock.decl({ modName : 'my-mod', modVal : 'true' }, { /* ... */ }));
});
```
After:
```js
modules.define('my-dom-block', function(provide, MyDomBlock) {
provide(MyDomBlock.declMod({ modName : 'my-mod' }, { /* ... */ }));
});
```
Issue: [#1374](https://github.com/bem/bem-core/issues/1374).
#### Declaration for a modifier with any value
Before:
```js
modules.define('my-dom-block', function(provide, MyDomBlock) {
provide(MyDomBlock.decl({ modName : 'my-mod' }, { /* ... */ }));
});
```
After:
```js
modules.define('my-dom-block', function(provide, MyDomBlock) {
provide(MyDomBlock.declMod({ modName : 'my-mod', modVal : '*' }, { /* ... */ }));
});
```
Issue: [#1376](https://github.com/bem/bem-core/pull/1376).
#### Block redefinition
Instead of the `decl()` method for a block class, use the `declBlock()` method for the `i-bem-dom` module.
Before:
```js
modules.define('my-dom-block', function(provide, MyDomBlock) {
provide(MyDomBlock.decl({ /* ... */ }));
});
```
After:
```js
modules.define('my-dom-block', ['i-bem-dom'], function(provide, bemDom, MyDomBlock) {
provide(bemDom.declBlock(MyDomBlock, { /* ... */ }));
});
```
#### Inherited block declaration
Before:
```js
modules.define('my-dom-block', ['i-bem__dom', 'my-base-dom-block'], function(provide, BEMDOM, MyBaseDomBlock) {
provide(BEMDOM.decl({ block : this.name, baseBlock : MyBaseDomBlock }, { /* ... */ }));
});
```
After:
```js
modules.define('my-dom-block', ['i-bem-dom', 'my-base-dom-block'], function(provide, bemDom, MyBaseDomBlock) {
provide(bemDom.declBlock(this.name, MyBaseDomBlock, { /* ... */ }));
});
```
#### Mix declaration
The `declMix` method has been renamed to `declMixin`. This clarifies the concept of [mixes of multiple BEM entities on a single DOM node](https://en.bem.info/methodology/key-concepts/#Mix) as opposed to JS-level mixins.
Before:
```js
modules.define('my-mix-block', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.declMix(this.name, { /* ... */ }));
});
```
After:
```js
modules.define('my-mixin-block', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declMixin({ /* ... */ }));
});
```
#### Mixing a mixin
Before:
```js
modules.define('my-dom-block', ['i-bem__dom', 'my-mix-1', 'my-mix-2'], function(provide, BEMDOM) {
provide(BEMDOM.decl({ block : this.name, baseMix : ['my-mix-1', 'my-mix-2']}, { /* ... */ }));
});
```
After:
```js
modules.define('my-dom-block', ['i-bem-dom', 'my-mixin-1', 'my-mixin-2'], function(provide, bemDom, MyMixin1, MyMixin2) {
provide(bemDom.declBlock(this.name, [MyMixin1, MyMixin2], { /* ... */ }));
});
```
#### Triggers for changing modifiers
When declaring a specific modifier (for example, `_my-mod_my-val`), it wasn't possible to declare the behavior for deleting this modifier. We had to make two declarations.
Before:
```js
//
modules.define('my-dom-block', function(provide, MyDomBlock) {
MyDomBlock.decl({
onSetMod : {
'my-mod' : {
'' : function() { /* ... */ } // declaration for deleting the _my-mod_my-val modifier
}
}
});
provide(MyDomBlock.decl({ modName : 'my-mod', modVal : 'my-val' }, { /* ... */ }));
});
```
After:
```js
modules.define('my-dom-block', function(provide, MyDomBlock) {
provide(MyDomBlock.declMod({ modName : 'my-mod', modVal : 'my-val' }, {
onSetMod : {
'mod1' : {
'' : function() { /* ... */ } // declaration for deleting the _my-mod_my-val modifier
}
}
}));
});
```
Issue: [#1025](https://github.com/bem/bem-core/issues/1025).
Shorthand syntax is now available for declaring behaviors for changing modifiers.
Before:
```js
onSetMod : {
'my-mod' : {
'*' : function(modName, modVal, prevModVal) {
if(prevModVal === 'my-val') {
/* ... */ // declaration for changing _my-mod_my-val to any other value
}
}
}
}
```
After:
```js
onSetMod : {
'my-mod' : {
'~my-val' : function() { /* ... */ } // declaration for changing the my-mod value from my-val to any other value
}
}
}
```
Before:
```js
onSetMod : {
'my-mod' : {
'*' : function(modName, modVal) {
if(modVal !== 'my-val') {
/* ... */ // declaration for changing my-mod to any value other than my-val
}
}
}
}
```
After:
```js
onSetMod : {
'my-mod' : {
'!my-val' : function() { /* ... */ } // declaration for changing my-mod to any value other than my-val
}
}
}
```
Issue: [#1072](https://github.com/bem/bem-core/issues/1072).
#### Lazy initialization
The functionality of the `live` field has been divided into two parts: the `lazyInit` field and the `onInit()` method.
Before:
```js
modules.define('my-dom-block', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, { /* ... */ }, {
live : true
}));
});
```
After:
```js
modules.define('my-dom-block', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, { /* ... */ }, {
lazyInit : true
}));
});
```
Before:
```js
modules.define('my-dom-block', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, { /* ... */ }, {
live : function() {
/* ... */
}
}));
});
```
After:
```js
modules.define('my-dom-block', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, { /* ... */ }, {
lazyInit : true,
onInit : function() {
/* ... */
}
}));
});
```
Before:
```js
modules.define('my-dom-block', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, { /* ... */ }, {
live : function() {
/* ... */
return false;
}
}));
});
```
After:
```js
modules.define('my-dom-block', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, { /* ... */ }, {
onInit : function() {
/* ... */
}
}));
});
```
Before:
```js
{
block : 'b1',
js : { live : false }
}
```
After:
```js
{
block : 'b1',
js : { lazyInit : false }
}
```
Issue: [#877](https://github.com/bem/bem-core/issues/877).
#### Instances for elements
Deleted the `elem-instances` element of the `i-bem` block and the `elem-instances` modifier of the `dom` element in the `i-bem` block.
Now the corresponding functionality is incorporated into `i-bem` and `i-bem-dom`.
Before:
```js
modules.define('my-dom-block__my-elem', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl({ block : 'my-dom-block', elem : 'my-elem' }, { /* ... */ }));
});
```
After:
```js
modules.define('my-dom-block__my-elem', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declElem('my-dom-block', 'my-elem', { /* ... */ }));
});
```
Now the `_elem(elemName)` method of the block instance (previously `elem(elemName)`) returns an instance of the element's class, instead of a jQuery object with all the elements named `elemName`.
To get a collection of instances of the element's class, use the `_elems()` method.
Now the caches for elements with JS implementation found with `_elem()` and `_elems()` are invalidated automatically when the DOM is modified.
Issue: [#1352](https://github.com/bem/bem-core/issues/1352).
Note: When this methods are used for elements without JS implementation you still need to use `_dropElemCache()` in cases of dynamically DOM update.
Note: don't forget to switch on support for elements instances in template engine.
For `bem-xjst` please refer to https://github.com/bem/bem-xjst/blob/master/docs/en/3-api.md#support-js-instances-for-elements-bem-core-v4 or for `BH` see https://github.com/bem/bh#jselem.
##### Ways to work with elements
Before:
```js
this.setMod(this.elem('my-elem'), 'my-mod', 'my-val');
```
After:
```js
this._elem('my-elem').setMod('my-mod', 'my-val');
```
The same is true for the methods `getMod()`, `hasMod()`, `toggleMod()`, and `delMod()`.
##### Deleted methods and fields
The following methods were deleted from the block API: `elemify()`, `elemParams()`, and the `onElemSetMod` field. The corresponding functionality is provided in instances of elements.
Also see the changes for [search methods](#Search-methods).
Issue: [#581](https://github.com/bem/bem-core/issues/581).
#### Search methods
Renamed the following methods:
- `findBlockInside()` to `findChildBlock()`
- `findBlocksInside()` to `findChildBlocks()`
- `findBlockOutside()` to `findParentBlock()`
- `findBlocksOutside()` to `findParentBlocks()`
- `findBlockOn()` to `findMixedBlock()`
- `findBlocksOn()` to `findMixedBlocks()`
The optional first parameter about the element has been removed from these methods.
Added the following methods: `findChildElem()`, `findChildElems()`, `findParentElem()`, `findParentElems()`, `findMixedElem()`, `findMixedElems()`.
Before:
```js
this.findBlockInside(this.elem('my-elem'), 'my-block-2');
```
After:
```js
this.findChildElem('my-elem').findChildBlock(MyBlock2);
```
Deleted the following methods: `findElem()`, `closestElem()`. Use the `findChildElem()` and `findParentElem()` elements, instead.
The methods `findChildBlocks()`, `findParentBlocks()`, `findMixedBlocks()`, `findChildElems()`, `findParentElems()`, and `findMixedElems()` return [collections of BEM entities](#Collections).
The `findChildElem()` and `findChildElems()` methods (unlike the previous equivalent `findElem`) don't search on their own DOM nodes of the instance.
Before:
```js
this.findElem('my-elem');
```
After:
```js
this.findChildElems('my-elem').concat(this.findMixedElems('my-elem'));
```
However, consider whether you really need both searches. In most cases, you can just use either `this.findChildElems('my-elem')` or `this.findMixedElems('my-elem')`.
##### Checking for nesting
In place of the deleted `containsDomElem()` method, use the `containsEntity()` method.
Before:
```js
this.containsDomElem(someElem);
```
After:
```js
this.containsEntity(someElem);
```
#### Collections
The functionality of the `collection` element of the `i-bem` block is no longer optional.
All methods that return an array of BEM entities now return collections.
Before:
```js
this.findBlocksInside('my-block-2')[0].setMod('my-mod', 'my-val');
```
After:
```js
this.findChildBlocks(MyBlock2).get(0).setMod('my-mod', 'my-val');
```
Before:
```js
this.findBlocksInside('my-block-2').forEach(function(myBlock2) {
return myBlock2.setMod('my-mod', 'my-val');
});
```
After:
```js
this.findChildBlocks(MyBlock2).setMod('my-mod', 'my-val');
```
Issue: [#582](https://github.com/bem/bem-core/issues/582).
#### Events
The events API has been simplified. Deleted the following block instance methods: `on()`, `un()`, `once()`, `bindTo()`, `unbindFrom()`, `bindToDoc()`, `bindToWin()`, `unbindFromDoc()`, `unbindFromWin()`, and class methods: `liveBindTo()`, `liveUnbindFrom()`, `on()`, `un()`, `once()`, `liveInitOnBlockEvent()`, `liveInitOnBlockInsideEvent()`.
They have been replaced with the new `_domEvents()` and `_events()` methods, which return an instance of the events manager class with the `on()`, `un()` and `once()` methods.
##### DOM events on instances
Before:
```js
BEMDOM.decl('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
this.bindTo('click', this._onClick);
}
}
}
});
```
After:
```js
bemDom.declBlock('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
this._domEvents().on('click', this._onClick);
}
}
}
});
```
Before:
```js
BEMDOM.decl('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
this.bindToDoc('click', this._onDocClick);
}
}
}
});
```
After:
```js
bemDom.declBlock('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
this._domEvents(bemDom.doc).on('click', this._onDocClick);
}
}
}
});
```
Before:
```js
BEMDOM.decl('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
this.bindToWin('resize', this._onWinResize);
}
}
}
});
```
After:
```js
bemDom.declBlock('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
this._domEvents(bemDom.win).on('resize', this._onWinResize);
}
}
}
});
```
##### Link to instance
If an event was fired on BEM instance the event object will contain a link to an instance:
```js
this._domEvents('my-elem').on('click', function(e) {
e.bemTarget // refers to `my-elem` instance
});
```
##### BEM events on instances
Before:
```js
BEMDOM.decl('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
this.findBlockOutside('my-block-2').on('my-event', this._onMyBlock2MyEvent, this);
},
'' : function() {
this.findBlockOutside('my-block-2').un('my-event', this._onMyBlock2MyEvent, this);
}
}
}
});
```
After:
```js
bemDom.declBlock('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
this._events(this.findParentBlock('my-block-2')).on('my-event', this._onMyBlock2MyEvent);
}
}
}
});
```
Note that unsubscribing from events is now automatic when the instance is destroyed.
##### Delegated DOM events
Before:
```js
BEMDOM.decl('my-block', { /* ... */ }, {
live : function() {
this.liveBindTo('click', this.prototype._onClick);
}
});
```
After:
```js
bemDom.declBlock('my-block', { /* ... */ }, {
onInit : function() {
this._domEvents().on('click', this.prototype._onClick);
}
});
```
Before:
```js
BEMDOM.decl('my-block', { /* ... */ }, {
live : function() {
this.liveBindTo('my-elem', 'click', this.prototype._onMyElemClick);
}
});
```
After:
```js
bemDom.declBlock('my-block', { /* ... */ }, {
onInit : function() {
this._domEvents('my-elem').on('click', this.prototype._onMyElemClick);
}
});
```
##### Delegated BEM events
Before:
```js
BEMDOM.decl('my-block', { /* ... */ }, {
live : function() {
this.liveInitOnBlockInsideEvent('my-event', 'my-block-2', this.prototype._onMyBlock2MyEvent);
}
});
```
After:
```js
bemDom.declBlock('my-block', { /* ... */ }, {
onInit : function() {
this._events(MyBlock2).on('my-event', this.prototype._onMyBlock2MyEvent);
}
});
```
Note that the parameter with the event handler function is now required.
Before:
```js
modules.define('my-block', ['i-bem__dom', 'my-block-2'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, { /* ... */ }, {
live : function() {
this.liveInitOnBlockInsideEvent('my-event', 'my-block-2');
}
}));
});
```
After:
```js
modules.define('my-block', ['i-bem-dom', 'my-block-2', 'functions'], function(provide, bemDom, MyBlock2, functions) {
provide(bemDom.declBlock(this.name, { /* ... */ }, {
onInit : function() {
this._events(MyBlock2).on('my-event', functions.noop);
}
}));
});
```
Before:
```js
BEMDOM.decl('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
MyBlock2.on(this.domElem, 'my-event', this._onMyBlock2MyEvent, this);
},
'' : function() {
MyBlock2.un(this.domElem, 'my-event', this._onMyBlock2MyEvent, this);
}
}
}
});
```
After:
```js
bemDom.declBlock('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
this._events(MyBlock2).on('my-event', this._onMyBlock2MyEvent);
}
}
}
});
```
Note that unsubscribing from events is now automatic when the instance is destroyed.
#### External code accessing BEM blocks
##### Getting an instance of a BEM block
Now the `bem()` method of a jQuery object accepts a BEM class instead of a string.
Before:
```js
modules.require(['jquery', 'i-bem__dom'], function($, BEMDOM) {
var myBlock = $('.my-block').bem('my-block');
});
```
After:
```js
modules.require(['jquery', 'my-block'], function($, MyBlock) {
var myBlock = $('.my-block').bem(MyBlock);
});
```
##### Subscribing to BEM events from external code
Before:
```js
modules.require(['jquery', 'i-bem__dom'], function($, BEMDOM) {
$('.my-block').bem('my-block').on('my-event', function() { /* ... */ });
});
```
After:
```js
modules.require(['jquery', 'my-block', 'events__observable'], function($, MyBlock, observable) {
observable($('.my-block').bem(MyBlock))
.on('my-event', function() { /* ... */ });
});
```
In addition, you must add `{ block : 'events', elem : 'observable', mods : { type : 'bem-dom' } }` to the dependency.
Issue: [#394](https://github.com/bem/bem-core/issues/394).
#### Names of protected methods begin with `_`
Renamed the protected methods:
- `emit()` to `_emit()`
- `elem()` to `_elem()`
- `dropElemCache()` to `_dropElemCache()`
- `buildClass()` to `_buildClassName()`
- `buildSelector()` to `_buildSelector()`
- `getDefaultParams()` to `_getDefaultParams()`
Issues: [#586](https://github.com/bem/bem-core/issues/586) and [#1359](https://github.com/bem/bem-core/issues/1359).
#### Deleted methods
Deleted the `getMods()` method.
### Changes in the `querystring` block
The `querystring__uri` element is now the `uri` block. The `querystring` block is now the `uri__querystring` element.
Issue: [#967](https://github.com/bem/bem-core/issues/967).
### Changes in the `page` block
The `page__css` element does not support `ie` field. Use the `page__conditional-comment` element instead.
Before:
```
{
block : 'page',
head : [
{ elem : 'css', url : 'my-css.css', ie : false },
{ elem : 'css', url : 'my-css', ie : true }
],
content : 'Page content'
}
```
After:
```
{
block : 'page',
head : [
{
elem : 'conditional-comment',
condition : '! IE',
content : { elem : 'css', url : 'my-css.css' }
},
{
elem : 'conditional-comment',
condition : '> IE 8',
content : { elem : 'css', url : 'my-css.ie.css' }
}
// and so on for needed IE versions
],
content : 'Page content'
}
```
Issue: [#379](https://github.com/bem/bem-core/issues/379).
## 3.0.0
To migrate to version 3.0.0, review the [history of changes](https://en.bem.info/libs/bem-core/v3/changelog/#300).
## 2.0.0
To migrate to version 2.0.0, review the [history of changes](https://en.bem.info/libs/bem-core/v2/changelog/#200).
## 1.0.0
For version 1.0.0, migrating requires switching from using [bem-bl](https://github.com/bem/bem-bl/) to using [bem-core](https://github.com/bem/bem-core/).
### Modules
The entire code is now written in terms of the modular system https://github.com/ymaps/modules.
All dependencies must be explicitly stated in the code. Minimize or eliminate use of global variables, if possible.
Example:
```js
modules.define(
'my-module', // module name
['module-from-library', 'my-another-module'], // module dependencies
function(provide, moduleFromLibrary, myAnotherModule) { // module declaration, called when all dependencies are resolved
// module representation
provide({
myModuleMethod : function() {}
});
});
```
TODO: Add information about the build process (usage of special technologies for JS and instructions for custom builders).
### jQuery and jQuery plugins
jQuery is represented by a `jquery` wrapper module that uses the global jQuery object if it already exists on the page, or loads it otherwise.
jQuery is now used only for operations directly related to the DOM (searching for elements, binding listeners to events, setting and getting attribute values, and so on).
All other operations have corresponding modules that provide the same functionality without depending on jQuery:
* The `objects` module for operating on objects (with the `extend`, `isEmpty`, and `each` methods).
* The `functions` module for operating on functions (with the `isFunction` and `noop` methods).
In addition, all the jQuery plugins that aren't directly related to jQuery (`$.observable`, `$.inherit`, `$.cookie`, `$.identify`, `$.throttle`) are now modules:
* The `events` module replaces `$.observable` for working with events. It provides the "classes" `EventsEmitter` and `Event`.
* The `inherit` module instead of `$.inherit` for working with "classes" and inheritance.
* The `cookie` module instead of `$.cookie`.
* The `identify` module instead of `$.identify`.
* The `functions__throttle` and `functions__debounce` modules instead of `$.throttle` and `$.debounce`.
Before:
```js
// block code
$.throttle()
// block code
```
After:
```js
module.define('my-module', ['functions__throttle'], function(provide, throttle) {
// module code
throttle()
// module code
});
```
### BEM.DOM blocks
#### Declaration
Instead of a declaration via BEM.DOM.decl, you need to extend the `i-bem__dom` module.
Before:
```js
BEM.DOM.decl('block', /* ... */);
```
After:
```js
modules.define('i-bem__dom', function(provide, BEMDOM) {
BEMDOM.decl('block', /* ... */);
provide(BEMDOM);
});
```
#### Constructor
You must use full notation for the handler for setting the `js` modifier to `inited`.
Before:
```js
onSetMod : {
js : function() {
// constructor code
}
}
```
After:
```js
onSetMod : {
'js' : {
'inited' : function() {
// constructor code
}
}
}
```
#### Desctructor
Instead of the `destruct` method, you need to use the handler for setting the `js` modifier to an empty value (remove the modifier).
You no longer need to call `__base` in order to run the base destructor defined in `i-bem__dom` on blocks.
Before:
```js
destruct : function() {
this.__base.apply(this, arguments);
// destructor code
}
```
After:
```js
onSetMod : {
js : {
'' : function() {
// destructor code
}
}
}
```
#### `changeThis` method
Instead of the `changeThis` method, use either the corresponding parameter, or the native `bind` method if there isn't a parameter.
Before:
```js
// block code
obj.on('event', this.changeThis(this._method));
// block code
```
After:
```js
obj.on('event', this._method.bind(this));
// or better
obj.on('event', this._method, this);
```
#### `afterCurrentEvent` method
Instead of the `afterCurrentEvent` method, use the `nextTick` method, which guarantees that the block still exists during the callback (if the block has already been destroyed by this time, the callback isn't executed).
Before:
```js
BEM.DOM.decl('block', {
method : function() {
this.afterCurrentEvent(function() {
/* ... */
})
}
});
```
After:
```js
modules.define('i-bem__dom', function(provide, BEMDOM) {
BEMDOM.decl('block', {
method : function() {
this.nextTick(function() {
/* ... */
});
}
});
});
```
#### `findElem` method
The context for finding an element is no longer set as a string. Instead, pass a jQuery object.
Before:
```js
var nestedElem = this.findElem('parent-elem', 'nested-elem');
```
After:
```js
var nestedElem = this.findElem(this.findElem('parent-elem'), 'nested-elem'),
oneMoreElem = this.findElem(this.elem('another-elem'), 'nested-elem');
```
#### `liveBindTo` method
The `liveBindTo` method no longer supports the `elemName` field for passing the element name. Use the `elem` field instead.
#### Access to a DOM element in an event handler
A DOM element that had an event handler bound to it is now accessed as `$(e.currentTarget)` instead of `e.data.domElem`.
Before:
```js
onClick : function(e) {
e.data.domElem.attr(/* ... */);
}
```
After:
```js
onClick : function(e) {
$(e.currentTarget).attr(/* ... */);
}
```
#### Channels
Channels are no longer an integral part of BEM. Now they are separate `events__channels` modules.
Before:
```js
BEM.DOM.decl('block', {
method : function() {
BEM.channel('channel-name').on(/* ... */);
}
});
```
After:
```js
modules.define('i-bem__dom', ['events__channels'], function(provide, channels, BEMDOM) {
BEMDOM.decl('block', {
method : function() {
channels('channel-name').on(/* ... */);
}
});
});
```
#### The `i-system` block and the `sys` channel for the `tick`, `idle`, and `wakeup` events
This block and channel no longer exist. They have been replaced with separate modules: `tick` with the "tick" event, and `idle` with the "idle" and "wakeup" events.
Before:
```js
BEM.DOM.decl('block', {
method : function() {
BEM.channel('sys').on('tick', /* ... */);
}
});
```
After:
```js
modules.define('i-bem__dom', ['tick'], function(provide, tick, BEMDOM) {
BEMDOM.decl('block', {
method : function() {
tick.on('tick', /* ... */);
}
});
});
```
Before:
```js
BEM.DOM.decl('block', {
method : function() {
BEM.channel('sys').on('wakeup', /* ... */);
}
});
```
After:
```js
modules.define('i-bem__dom', ['idle'], function(provide, idle, BEMDOM) {
BEMDOM.decl('block', {
method : function() {
idle.on('wakeup', /* ... */);
}
});
});
```
### BEM blocks
BEM blocks that were used as storage for some methods but that didn't use the BEM methodology in any way can now be written as modules.
Before:
```js
BEM.decl('i-router', {
route : function() { /* ... */ }
});
```
After:
```js
modules.define('router', function(provide) {
provide({
route : function() { /* ... */ }
});
});
```
If for some reason you need BEM blocks (not BEM.DOM blocks), you can declare them by extending the `i-bem` module.
Before:
```js
BEM.decl('my-block', { /* ... */ });
```
After:
```js
modules.define('i-bem', function(provide, BEM) {
BEM.decl('my-block', { /* ... */ });
provide(BEM);
});
```
#### Refactoring using the `b-spin` block example
Before:
```js
BEM.DOM.decl('b-spin', {
onSetMod : {
'js' : function() {
this._size = this.getMod('size') || /[\d]+/.exec(this.getMod('theme'))[0];
this._bgProp = 'background-position';
this._posPrefix = '0 -';
if (this.elem('icon').css('background-position-y')) { /* In IE, you can't get the background-position property. You can only get background-position-y, so use this workaround */
this._bgProp = 'background-position-y';
this._posPrefix = '-';
}
this._curFrame = 0;
this.hasMod('progress') && this.channel('sys').on('tick', this._onTick, this);
},
'progress' : {
'yes' : function() {
this.channel('sys').on('tick', this._onTick, this);
},
'' : function() {
this.channel('sys').un('tick', this._onTick, this);
}
}
},
_onTick: function(){
var y = ++this._curFrame * this._size;
(y >= this._size * 36) && (this._curFrame = y = 0);
this.elem('icon').css(this._bgProp, this._posPrefix + y +'px');
},
destruct : function() {
this.channel('sys').un('tick', this._onTick, this);
this.__base.apply(this, arguments);
}
});
```
After:
```js
modules.define(
'i-bem__dom',
['tick'],
function(provide, tick, BEMDOM) {
var FRAME_COUNT = 36;
BEMDOM.decl('b-spin', {
onSetMod : {
'js' : {
'inited' : function() { // constructor
var hasBackgroundPositionY = !!this.elem('icon').css('background-position-y')); /* In IE we can't get the background-position property, only background-position-y */
this._bgProp = hasBackgroundPositionY? 'background-position-y' : 'background-position';
this._posPrefix = hasBackgroundPositionY? '-' : '0 -';
this._curFrame = 0;
this._size = Number(this.getMod('size') || /[\d]+/.exec(this.getMod('theme'))[0]);
this.hasMod('progress') && this._bindToTick();
},
'' : function() { // destructor
this._unbindFromTick();
}
},
'progress' : {
'true' : function() {
this._bindToTick();
},
'' : function() {
this._unbindFromTick();
}
}
},
_bindToTick : function() {
tick.on('tick', this._onTick, this);
},
_unbindFromTick : function() {
tick.un('tick', this._onTick, this);
},
_onTick : function() {
var offset;
this._curFrame++ >= FRAME_COUNT?
offset = this._curFrame * this._size :
this._curFrame = offset = 0;
this.elem('icon').css(this._bgProp, this._posPrefix + offset + 'px');
}
});
provide(BEMDOM);
});
```
================================================
FILE: MIGRATION.ru.md
================================================
# Миграция
## 5.0.0
### ym → ES-модули
Модульная система `ym` (`modules.define`/`modules.require`) заменена на нативные ES-модули.
Было:
```js
modules.define('my-block', ['i-bem-dom', 'events'], function(provide, bemDom, events) {
provide(bemDom.declBlock(this.name, { /* ... */ }));
});
```
Стало:
```js
import bemDom from 'bem:i-bem-dom';
import events from 'bem:events';
export default bemDom.declBlock('my-block', { /* ... */ });
```
Все `bem:*` импорты разрешаются на этапе сборки плагином `vite-plugin-bem-levels` в реальные пути файлов с учётом приоритетов уровней для конкретной платформы.
### Переопределения модулей
Переопределения модулей, которые ранее использовали `modules.define` с колбэком, получающим предыдущее значение модуля, теперь обрабатываются через barrel-файлы, генерируемые Vite-плагином.
Было (переопределение ym):
```js
modules.define('jquery', function(provide, $) {
$.event.special.pointerclick = { /* ... */ };
provide($);
});
```
Стало (side-effect импорт в barrel-файле):
```js
// Barrel-файл автоматически сгенерирован vite-plugin-bem-levels:
import $ from '../../common.blocks/jquery/jquery.js';
import '../../common.blocks/jquery/__event/_type/jquery__event_type_pointerclick.js';
export default $;
```
### jQuery 3 → 4
Peer-зависимость `jquery` теперь `^4.0.0`.
Основные несовместимые изменения в jQuery 4:
- `$.unique()` удалён. Используйте `$.uniqueSort()`.
- Ряд устаревших методов удалён. См. [руководство по обновлению jQuery 4.0](https://jquery.com/upgrade-guide/4.0/).
### vow → нативный Promise
Зависимость `vow` удалена. Весь асинхронный код теперь использует нативный `Promise`.
Было:
```js
var vow = require('vow');
var promise = vow.resolve(value);
```
Стало:
```js
const promise = Promise.resolve(value);
```
### Система сборки: ENB → Vite
Весь инструментарий ENB заменён на Vite.
Было:
```bash
./node_modules/.bin/enb make
```
Стало:
```bash
npm run build # обе платформы
npm run build:desktop # только desktop
npm run build:touch # только touch
```
Конфигурация Vite находится в `build/vite.config.js`. Кастомный плагин `vite-plugin-bem-levels` в `build/plugins/` обеспечивает сканирование BEM-уровней и разрешение модулей.
### Node.js 20+
Минимальная поддерживаемая версия Node.js — 20 (было 8). Обновите `.nvmrc` или конфигурацию CI.
### Линтинг: jshint/jscs → ESLint 10
Замените кастомные `.jshintrc` или `.jscs.json` правила на flat-конфигурацию ESLint (`eslint.config.js`).
### Тестирование
- **Серверные тесты**: `mocha`/`chai` заменены на `node:test`/`node:assert`.
- **Браузерные тесты**: `mocha-phantomjs` заменён на Playwright. Запуск: `npm run test:browser`.
- **Все тесты**: `npm run test:all` запускает серверные и браузерные тесты.
### CI/CD: Travis → GitHub Actions
Замените `.travis.yml` на `.github/workflows/ci.yml`. Новый CI запускает задачи lint, test, test:browser и build.
### Git-хуки: git-hooks → husky
Замените `.githooks/` на конфигурацию husky. Скрипт `prepare` в `package.json` автоматически настраивает husky при `npm install`.
## 4.0.0
### Изменения в блоке `i-bem`
#### Отдельный блок `i-bem-dom`
Элемент `dom` блока `i-bem` был перенесён в отдельный блок `i-bem-dom`.
Было:
```js
modules.define('my-dom-block', ['i-bem__dom'], function(provide, BEMDOM) {
/* ... */
});
```
Стало:
```js
modules.define('my-dom-block', ['i-bem-dom'], function(provide, bemDom) {
/* ... */
});
```
Блоки `i-bem` и `i-bem-dom` больше не являются классами, представляя собой модули с методами для декларации
БЭМ-сущностей, ссылками на классы БЭМ-сущностей и некоторыми дополнительными хелперами. Эти методы больше не являются методами класса для соответсвующих блоков.
Задача: [#413](https://github.com/bem/bem-core/issues/413).
#### Декларация
#### Декларация блока
Для декларации блока, вместо метода `decl()`, следует использовать метод `declBlock()`.
Было:
```js
modules.define('my-dom-block', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, { /* ... */ }));
});
```
Стало:
```js
modules.define('my-dom-block', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, { /* ... */ }));
});
```
#### Декларация модификатора
Для декларации модификатора, вместо статического метода `decl()`, следует использовать статический метод `declMod()`.
Было:
```js
modules.define('my-dom-block', function(provide, MyDomBlock) {
provide(MyDomBlock.decl({ modName : 'my-mod', modVal : 'my-val' }, { /* ... */ }));
});
```
Стало:
```js
modules.define('my-dom-block', function(provide, MyDomBlock) {
provide(MyDomBlock.declMod({ modName : 'my-mod', modVal : 'myVal' }, { /* ... */ }));
});
```
#### Декларация булевого модификатора
Было:
```js
modules.define('my-dom-block', function(provide, MyDomBlock) {
provide(MyDomBlock.decl({ modName : 'my-mod', modVal : 'true' }, { /* ... */ }));
});
```
Стало:
```js
modules.define('my-dom-block', function(provide, MyDomBlock) {
provide(MyDomBlock.declMod({ modName : 'my-mod' }, { /* ... */ }));
});
```
Задача: [#1374](https://github.com/bem/bem-core/issues/1374).
#### Декларация для модификатора с любым значением
Было:
```js
modules.define('my-dom-block', function(provide, MyDomBlock) {
provide(MyDomBlock.decl({ modName : 'my-mod' }, { /* ... */ }));
});
```
Стало:
```js
modules.define('my-dom-block', function(provide, MyDomBlock) {
provide(MyDomBlock.declMod({ modName : 'my-mod', modVal : '*' }, { /* ... */ }));
});
```
Задача: [#1376](https://github.com/bem/bem-core/pull/1376).
#### Доопределение блока
Вместо метода `decl()` класса блока следует использовать метод `declBlock()` модуля `i-bem-dom`.
Было:
```js
modules.define('my-dom-block', function(provide, MyDomBlock) {
provide(MyDomBlock.decl({ /* ... */ }));
});
```
Стало:
```js
modules.define('my-dom-block', ['i-bem-dom'], function(provide, bemDom, MyDomBlock) {
provide(bemDom.declBlock(MyDomBlock, { /* ... */ }));
});
```
#### Декларация наследуемого блока
Было:
```js
modules.define('my-dom-block', ['i-bem__dom', 'my-base-dom-block'], function(provide, BEMDOM, MyBaseDomBlock) {
provide(BEMDOM.decl({ block : this.name, baseBlock : MyBaseDomBlock }, { /* ... */ }));
});
```
Стало:
```js
modules.define('my-dom-block', ['i-bem-dom', 'my-base-dom-block'], function(provide, bemDom, MyBaseDomBlock) {
provide(bemDom.declBlock(this.name, MyBaseDomBlock, { /* ... */ }));
});
```
#### Декларация миксина
Метод `declMix` переименован в `declMixin`, чтобы отделить понятие
[миксов нескольких БЭМ-сущностей на одном DOM-узле](https://ru.bem.info/methodology/key-concepts/#Микс) от миксинов на уровне JS.
Было:
```js
modules.define('my-mix-block', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.declMix(this.name, { /* ... */ }));
});
```
Стало:
```js
modules.define('my-mixin-block', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declMixin({ /* ... */ }));
});
```
#### Примешивание миксина
Было:
```js
modules.define('my-dom-block', ['i-bem__dom', 'my-mix-1', 'my-mix-2'], function(provide, BEMDOM) {
provide(BEMDOM.decl({ block : this.name, baseMix : ['my-mix-1', 'my-mix-2']}, { /* ... */ }));
});
```
Стало:
```js
modules.define('my-dom-block', ['i-bem-dom', 'my-mixin-1', 'my-mixin-2'], function(provide, bemDom, MyMixin1, MyMixin2) {
provide(bemDom.declBlock(this.name, [MyMixin1, MyMixin2], { /* ... */ }));
});
```
#### Триггеры для изменения модификаторов
При декларации определённого модификатора (например, `_my-mod_my-val`) невозможно было задекларировать поведение
на удаление этого модификатора. Приходилось делать две декларации.
Было:
```js
// my-dom-block_my-mod_my-val.js
modules.define('my-dom-block', function(provide, MyDomBlock) {
MyDomBlock.decl({
onSetMod : {
'my-mod' : {
'' : function() { /* ... */ } // декларация для удаления модификатора _my-mod_my-val
}
}
});
provide(MyDomBlock.decl({ modName : 'my-mod', modVal : 'my-val' }, { /* ... */ }));
});
```
Стало:
```js
modules.define('my-dom-block', function(provide, MyDomBlock) {
provide(MyDomBlock.declMod({ modName : 'my-mod', modVal : 'my-val' }, {
onSetMod : {
'mod1' : {
'' : function() { /* ... */ } // декларация для удаления модификатора _my-mod_my-val
}
}
}));
});
```
Задача: [#1025](https://github.com/bem/bem-core/issues/1025).
Появился сокращённый синтаксис для декларации поведения на изменение модификатора.
Было:
```js
onSetMod : {
'my-mod' : {
'*' : function(modName, modVal, prevModVal) {
if(prevModVal === 'my-val') {
/* ... */ // декларация для изменения _my-mod_my-val в любое другое значение
}
}
}
}
```
Стало:
```js
onSetMod : {
'my-mod' : {
'~my-val' : function() { /* ... */ } // декларация для изменения значения my-mod из my-val в любое другое значение
}
}
}
```
Было:
```js
onSetMod : {
'my-mod' : {
'*' : function(modName, modVal) {
if(modVal !== 'my-val') {
/* ... */ // декларация для изменения my-mod в любое значение, кроме my-val
}
}
}
}
```
Стало:
```js
onSetMod : {
'my-mod' : {
'!my-val' : function() { /* ... */ } // декларация для изменения my-mod в любое значение, кроме my-val
}
}
}
```
Задача: [#1072](https://github.com/bem/bem-core/issues/1072).
#### Ленивая инициализация
Функциональность поля `live` была разделена на две части: поле `lazyInit` и метод `onInit()`.
Было:
```js
modules.define('my-dom-block', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, { /* ... */ }, {
live : true
}));
});
```
Стало:
```js
modules.define('my-dom-block', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, { /* ... */ }, {
lazyInit : true
}));
});
```
Было:
```js
modules.define('my-dom-block', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, { /* ... */ }, {
live : function() {
/* ... */
}
}));
});
```
Стало:
```js
modules.define('my-dom-block', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, { /* ... */ }, {
lazyInit : true,
onInit : function() {
/* ... */
}
}));
});
```
Было:
```js
modules.define('my-dom-block', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, { /* ... */ }, {
live : function() {
/* ... */
return false;
}
}));
});
```
Стало:
```js
modules.define('my-dom-block', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, { /* ... */ }, {
onInit : function() {
/* ... */
}
}));
});
```
Было:
```js
{
block : 'b1',
js : { live : false }
}
```
Стало:
```js
{
block : 'b1',
js : { lazyInit : false }
}
```
Задача: [#877](https://github.com/bem/bem-core/issues/877).
#### Экземпляры для элементов
Удалены элемент `elem-instances` блока `i-bem` и модификатор `elem-instances` элемента `dom` блока `i-bem`.
Теперь соответствующая функциональность является частью `i-bem` и `i-bem-dom`.
Было:
```js
modules.define('my-dom-block__my-elem', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl({ block : 'my-dom-block', elem : 'my-elem' }, { /* ... */ }));
});
```
Стало:
```js
modules.define('my-dom-block__my-elem', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declElem('my-dom-block', 'my-elem', { /* ... */ }));
});
```
Теперь метод `_elem(elemName)` экземпляра блока (бывший `elem(elemName)`) возвращает не jQuery-объект со всеми элементами
с именем `elemName`, а экземпляр класса элемента.
Для того, чтобы получить коллекцию экземпляров класса элемента, используйте метод `_elems()`.
Теперь кэш для элементов с JS-реализацией найденных через `_elem()` и `_elems()` инвалидируется автоматически при DOM модификациях.
Задача: [#1352](https://github.com/bem/bem-core/issues/1352).
Когда эти методы используются для элементов без JS-реализации необходимо использовать `_dropElemCache()` при динамическом обновлении DOM.
Не забудьте включить поддержку экземплятор для элементов в шаблонизаторе.
Опция [elemJsInstances](https://github.com/bem/bem-xjst/blob/master/docs/ru/3-api.md#%D0%9F%D0%BE%D0%B4%D0%B4%D0%B5%D1%80%D0%B6%D0%BA%D0%B0-js-%D1%8D%D0%BA%D0%B7%D0%B5%D0%BC%D0%BF%D0%BB%D1%8F%D1%80%D0%BE%D0%B2-%D0%B4%D0%BB%D1%8F-%D1%8D%D0%BB%D0%B5%D0%BC%D0%B5%D0%BD%D1%82%D0%BE%D0%B2-bem-core-v4) для `bem-xjst` или [jsElem](https://github.com/bem/bh#jselem) для `BH`.
##### Cпособы взаимодействия с элементами
Было:
```js
this.setMod(this.elem('my-elem'), 'my-mod', 'my-val');
```
Стало:
```js
this._elem('my-elem').setMod('my-mod', 'my-val');
```
Аналогично для методов `getMod()`, `hasMod()`, `toggleMod()`, `delMod()`.
##### Удалённые методы и поля
Из API блока удалены методы: `elemify()`, `elemParams()` и поле `onElemSetMod`. Соответствующая
им функциональность выражается через экземпляры элементов .
См. также изменения про [методы поиска](#Методы-поиска).
Задача: [#581](https://github.com/bem/bem-core/issues/581).
#### Методы поиска
Переименованы следующие методы:
- `findBlockInside()` в `findChildBlock()`
- `findBlocksInside()` в `findChildBlocks()`
- `findBlockOutside()` в `findParentBlock()`
- `findBlocksOutside()` в `findParentBlocks()`
- `findBlockOn()` в `findMixedBlock()`
- `findBlocksOn()` в `findMixedBlocks()`
Из этих методов удален опциональный первый параметр про элемент.
Добавлены методы: `findChildElem()`, `findChildElems()`, `findParentElem()`, `findParentElems()`, `findMixedElem()`, `findMixedElems()`.
Было:
```js
this.findBlockInside(this.elem('my-elem'), 'my-block-2');
```
Стало:
```js
this.findChildElem('my-elem').findChildBlock(MyBlock2);
```
Удалены методы: `findElem()`, `closestElem()`, вместо них следует использовать методы `findChildElem()`
и `findParentElem()`, соответсвенно.
Методы `findChildBlocks()`, `findParentBlocks()`, `findMixedBlocks()`, `findChildElems()`, `findParentElems()`,
`findMixedElems()` возвращают [коллекции БЭМ-сущностей](#Коллекции).
Методы `findChildElem()` и `findChildElems()` (в отличие от предыдущего аналога `findElem`) не выполняют поиск на собственных DOM-узлах экземпляра.
Было:
```js
this.findElem('my-elem');
```
Стало:
```js
this.findChildElems('my-elem').concat(this.findMixedElems('my-elem'));
```
Но рекомендуется обратить внимание, действительно ли необходимы оба поиска:
в большинстве случаев достаточно использовать или `this.findChildElems('my-elem')` или `this.findMixedElems('my-elem')`.
##### Проверка вложенности
Вместо удаленного метода `containsDomElem()`, следует использовать метод `containsEntity()`.
Было:
```js
this.containsDomElem(someElem);
```
Стало:
```js
this.containsEntity(someElem);
```
#### Коллекции
Функциональность элемента `collection` блока `i-bem` перестала быть опциональной.
Все методы возвращавшие массив БЭМ-сущностей, теперь возвращают коллекции.
Было:
```js
this.findBlocksInside('my-block-2')[0].setMod('my-mod', 'my-val');
```
Стало:
```js
this.findChildBlocks(MyBlock2).get(0).setMod('my-mod', 'my-val');
```
Было:
```js
this.findBlocksInside('my-block-2').forEach(function(myBlock2) {
return myBlock2.setMod('my-mod', 'my-val');
});
```
Стало:
```js
this.findChildBlocks(MyBlock2).setMod('my-mod', 'my-val');
```
Задача: [#582](https://github.com/bem/bem-core/issues/582).
#### События
API работы с событиями значильно упрощено. Удалены методы экземпляра блока: `on()`, `un()`, `once()`, `bindTo()`,
`unbindFrom()`, `bindToDoc()`, `bindToWin()`, `unbindFromDoc()`, `unbindFromWin()` и методы класса: `liveBindTo()`,
`liveUnbindFrom()`, `on()`, `un()`, `once()`, `liveInitOnBlockEvent()`, `liveInitOnBlockInsideEvent()`.
Вместо них добавлены методы `_domEvents()` и `_events()`, возвращающие экземпляр класса менеджера событий, с методами
`on()`, `un()` и `once()`;
##### DOM-события на экземплярах
Было:
```js
BEMDOM.decl('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
this.bindTo('click', this._onClick);
}
}
}
});
```
Стало:
```js
bemDom.declBlock('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
this._domEvents().on('click', this._onClick);
}
}
}
});
```
Было:
```js
BEMDOM.decl('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
this.bindToDoc('click', this._onDocClick);
}
}
}
});
```
Стало:
```js
bemDom.declBlock('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
this._domEvents(bemDom.doc).on('click', this._onDocClick);
}
}
}
});
```
Было:
```js
BEMDOM.decl('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
this.bindToWin('resize', this._onWinResize);
}
}
}
});
```
Стало:
```js
bemDom.declBlock('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
this._domEvents(bemDom.win).on('resize', this._onWinResize);
}
}
}
});
```
##### Ссылка на экземпляр
Если событие произошло на БЭМ-экземпляре, в объект события будет добавлено поле, ссылающееся на экземпляр:
```js
this._domEvents('my-elem').on('click', function(e) {
e.bemTarget // ссылается на экземпляр `my-elem`
});
```
##### БЭМ-события на экземплярах
Было:
```js
BEMDOM.decl('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
this.findBlockOutside('my-block-2').on('my-event', this._onMyBlock2MyEvent, this);
},
'' : function() {
this.findBlockOutside('my-block-2').un('my-event', this._onMyBlock2MyEvent, this);
}
}
}
});
```
Стало:
```js
bemDom.declBlock('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
this._events(this.findParentBlock('my-block-2')).on('my-event', this._onMyBlock2MyEvent);
}
}
}
});
```
Следует обратить внимание, что теперь, отписка от событий происходит автоматически во время уничтожения экземпляра.
##### Делегированные DOM-события
Было:
```js
BEMDOM.decl('my-block', { /* ... */ }, {
live : function() {
this.liveBindTo('click', this.prototype._onClick);
}
});
```
Стало:
```js
bemDom.declBlock('my-block', { /* ... */ }, {
onInit : function() {
this._domEvents().on('click', this.prototype._onClick);
}
});
```
Было:
```js
BEMDOM.decl('my-block', { /* ... */ }, {
live : function() {
this.liveBindTo('my-elem', 'click', this.prototype._onMyElemClick);
}
});
```
Стало:
```js
bemDom.declBlock('my-block', { /* ... */ }, {
onInit : function() {
this._domEvents('my-elem').on('click', this.prototype._onMyElemClick);
}
});
```
##### Делегированные БЭМ-события
Было:
```js
BEMDOM.decl('my-block', { /* ... */ }, {
live : function() {
this.liveInitOnBlockInsideEvent('my-event', 'my-block-2', this.prototype._onMyBlock2MyEvent);
}
});
```
Стало:
```js
bemDom.declBlock('my-block', { /* ... */ }, {
onInit : function() {
this._events(MyBlock2).on('my-event', this.prototype._onMyBlock2MyEvent);
}
});
```
Следует обратить внимание, что параметр с функцией обработчиком события теперь обязательный.
Было:
```js
modules.define('my-block', ['i-bem__dom', 'my-block-2'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, { /* ... */ }, {
live : function() {
this.liveInitOnBlockInsideEvent('my-event', 'my-block-2');
}
}));
});
```
Стало:
```js
modules.define('my-block', ['i-bem-dom', 'my-block-2', 'functions'], function(provide, bemDom, MyBlock2, functions) {
provide(bemDom.declBlock(this.name, { /* ... */ }, {
onInit : function() {
this._events(MyBlock2).on('my-event', functions.noop);
}
}));
});
```
Было:
```js
BEMDOM.decl('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
MyBlock2.on(this.domElem, 'my-event', this._onMyBlock2MyEvent, this);
},
'' : function() {
MyBlock2.un(this.domElem, 'my-event', this._onMyBlock2MyEvent, this);
}
}
}
});
```
Стало:
```js
bemDom.declBlock('my-block', {
onSetMod : {
'js' : {
'inited' : function() {
this._events(MyBlock2).on('my-event', this._onMyBlock2MyEvent);
}
}
}
});
```
Следует обратить внимание, что теперь, отписка от событий происходит автоматически во время уничтожения экземпляра.
#### Взаимодействие стороннего кода с БЭМ-блоками
##### Получение экземпляра БЭМ-блока
Теперь, метод jQuery-объекта `bem()` принимает БЭМ-класс, вместо строки.
Было:
```js
modules.require(['jquery', 'i-bem__dom'], function($, BEMDOM) {
var myBlock = $('.my-block').bem('my-block');
});
```
Стало:
```js
modules.require(['jquery', 'my-block'], function($, MyBlock) {
var myBlock = $('.my-block').bem(MyBlock);
});
```
##### Подписка на БЭМ-события из стороннего кода
Было:
```js
modules.require(['jquery', 'i-bem__dom'], function($, BEMDOM) {
$('.my-block').bem('my-block').on('my-event', function() { /* ... */ });
});
```
Стало:
```js
modules.require(['jquery', 'my-block', 'events__observable'], function($, MyBlock, observable) {
observable($('.my-block').bem(MyBlock))
.on('my-event', function() { /* ... */ });
});
```
При этом в зависимости нужно добавить `{ block : 'events', elem : 'observable', mods : { type : 'bem-dom' } }`.
Задача: [#394](https://github.com/bem/bem-core/issues/394).
#### Имена protected-методов начинаются с `_`
Переименованы protected-методы:
- `emit()` в `_emit()`
- `elem()` в `_elem()`
- `dropElemCache()` в `_dropElemCache()`
- `buildClass()` в `_buildClassName()`
- `buildSelector()` в `_buildSelector()`
- `getDefaultParams()` в `_getDefaultParams()`
Задачи: [#586](https://github.com/bem/bem-core/issues/586), [#1359](https://github.com/bem/bem-core/issues/1359).
#### Удалённые методы
Удалён метод `getMods()`.
### Изменения в блоке `querystring`
Элемент `querystring__uri` стал блоком `uri`. Блок `querystring` стал элементом `uri__querystring`.
Задача: [#967](https://github.com/bem/bem-core/issues/967).
### Изменения в блоке `page`
Элемент `page__css` больше не поддреживает поле `ie`. Используйте элемент `page__conditional-comment`.
Было:
```
{
block : 'page',
head : [
{ elem : 'css', url : 'my-css.css', ie : false },
{ elem : 'css', url : 'my-css', ie : true }
],
content : 'Page content'
}
```
Стало:
```
{
block : 'page',
head : [
{
elem : 'conditional-comment',
condition : '! IE',
content : { elem : 'css', url : 'my-css.css' }
},
{
elem : 'conditional-comment',
condition : '> IE 8',
content : { elem : 'css', url : 'my-css.ie.css' }
}
// и т.д. для других нужных версий IE
],
content : 'Page content'
}
```
Задача: [#379](https://github.com/bem/bem-core/issues/379).
## 3.0.0
Для миграции на версию 3.0.0 достаточно ознакомиться с [историей изменений](https://ru.bem.info/libs/bem-core/v3/changelog/#300).
## 2.0.0
Для миграции на версию 2.0.0 достаточно ознакомиться с [историей изменений](https://ru.bem.info/libs/bem-core/v2/changelog/#200).
## 1.0.0
Для версии 1.0.0 миграция подразумевается с использования [bem-bl](https://github.com/bem/bem-bl/) на использование [bem-core](https://github.com/bem/bem-core/).
### Модули
Весь код теперь пишется в терминах модульной системы https://github.com/ymaps/modules.
Все зависимости должны явно указываться в коде, обращения к глобальным объектам необходимо минимизировать, а, по возможности, и полностью исключить.
Пример:
```js
modules.define(
'my-module', // имя модуля
['module-from-library', 'my-another-module'], // зависимости модуля
function(provide, moduleFromLibrary, myAnotherModule) { // декларация модуля, вызывается когда все зависимости "разрезолвлены"
// предоставление модуля
provide({
myModuleMethod : function() {}
});
});
```
TODO: дописать про изменение сборки (использование специальных технологий для js и как быть с кастомными сборщиками)
### jQuery и jQuery-плагины
jQuery представлен модулем-оберткой `jquery`, который использует глобальный объект jQuery,
в случае если он уже присутствует на странице, в противном случае загружая его самостоятельно.
jQuery теперь используется только для операций, связанных непосредственно с DOM
(поиск элементов, подписка на события, установка/получение атрибутов элементов, и т.д.).
Для всех остальных операций написаны соответствующие модули,
предоставляющие аналогичный функционал, но, при этом, не зависящие от jQuery:
* модуль `objects` для работы с объектами (с методами `extend`, `isEmpty`, `each`)
* модуль `functions` для работы с функциями (с методами `isFunction` и `noop`)
Также, все jQuery-плагины, не связанные непосредственно с jQuery
(`$.observable`, `$.inherit`, `$.cookie`, `$.identify`, `$.throttle`) стали модулями:
* модуль `events` вместо `$.observable` для работы с событиями, предоставляющий "классы" `EventsEmitter` и `Event`
* модуль `inherit` вместо `$.inherit` для работы с "классами" и наследованием
* модуль `cookie` вместо `$.cookie`
* модуль `identify` вместо `$.identify`
* модули `functions__throttle`, `functions__debounce` вместо `$.throttle` и `$.debounce`, соответственно
Было:
```js
// код блока
$.throttle()
// код блока
```
Стало:
```js
module.define('my-module', ['functions__throttle'], function(provide, throttle) {
// код модуля
throttle()
// код модуля
});
```
### BEM.DOM-блоки
#### Декларация
Вместо декларации через BEM.DOM.decl необходимо доопределять модуль `i-bem__dom`.
Было:
```js
BEM.DOM.decl('block', /* ... */);
```
Стало:
```js
modules.define('i-bem__dom', function(provide, BEMDOM) {
BEMDOM.decl('block', /* ... */);
provide(BEMDOM);
});
```
#### Конструктор
Необходимо использовать полную нотацию для обработчика установки модификатора `js` в значение `inited`.
Было:
```js
onSetMod : {
js : function() {
// код конструктора
}
}
```
Стало:
```js
onSetMod : {
'js' : {
'inited' : function() {
// код конструктора
}
}
}
```
#### Деструктор
Вместо метода `destruct` необходимо использовать обработчик установки модификатора `js` в пустое значение (удаление модификатора).
Вызывать `__base` для того, чтобы у блоков работал базовый деструктор, определенный в `i-bem__dom`, больше не нужно.
Было:
```js
destruct : function() {
this.__base.apply(this, arguments);
// код деструктора
}
```
Стало:
```js
onSetMod : {
js : {
'' : function() {
// код деструктора
}
}
}
```
#### Метод `changeThis`
Вместо метода `changeThis` необходимо использовать либо соответствующий параметр, либо нативный метод `bind`, если такой параметр отсутствует.
Было:
```js
// код блока
obj.on('event', this.changeThis(this._method));
// код блока
```
Стало:
```js
obj.on('event', this._method.bind(this));
// или лучше
obj.on('event', this._method, this);
```
#### Метод `afterCurrentEvent`
Вместо метода `afterCurrentEvent` необходимо использовать метод `nextTick`,
который гарантирует, что блок еще существует в момент исполнения callback'а
(если блок уже уничтожен к этому моменту, то callback не исполняется).
Было:
```js
BEM.DOM.decl('block', {
method : function() {
this.afterCurrentEvent(function() {
/* ... */
});
}
});
```
Стало:
```js
modules.define('i-bem__dom', function(provide, BEMDOM) {
BEMDOM.decl('block', {
method : function() {
this.nextTick(function() {
/* ... */
});
}
});
});
```
#### Метод `findElem`
Контекст для поиска элемента больше не задается строкой, вместо нее следует передавать jQuery-объект.
Было:
```js
var nestedElem = this.findElem('parent-elem', 'nested-elem');
```
Стало:
```js
var nestedElem = this.findElem(this.findElem('parent-elem'), 'nested-elem'),
oneMoreElem = this.findElem(this.elem('another-elem'), 'nested-elem');
```
#### Метод `liveBindTo`
Метод `liveBindTo` больше не поддерживает поле `elemName` для передачи имени элемента. Вместо него следует использовать поле `elem`.
#### Доступ до DOM-элемента в обработчике события
DOM-элемент, к которому был подвешен обработчик события теперь доступен
как `$(e.currentTarget)`вместо `e.data.domElem`.
Было:
```js
onClick : function(e) {
e.data.domElem.attr(/* ... */);
}
```
Стало:
```js
onClick : function(e) {
$(e.currentTarget).attr(/* ... */);
}
```
#### Каналы (channels)
Каналы больше не являются встроенными в BEM, теперь они являются самостоятельным модулем `events__channels`.
Было:
```js
BEM.DOM.decl('block', {
method : function() {
BEM.channel('channel-name').on(/* ... */);
}
});
```
Стало:
```js
modules.define('i-bem__dom', ['events__channels'], function(provide, channels, BEMDOM) {
BEMDOM.decl('block', {
method : function() {
channels('channel-name').on(/* ... */);
}
});
});
```
#### Блок `i-system` и канал `sys` событий `tick`, `idle`, `wakeup`
Этот блок и канал перестали существовать, вместо них появились отдельные модули: `tick` с событием tick и `idle` с событиями idle и wakeup.
Было:
```js
BEM.DOM.decl('block', {
method : function() {
BEM.channel('sys').on('tick', /* ... */);
}
});
```
Стало:
```js
modules.define('i-bem__dom', ['tick'], function(provide, tick, BEMDOM) {
BEMDOM.decl('block', {
method : function() {
tick.on('tick', /* ... */);
}
});
});
```
Было:
```js
BEM.DOM.decl('block', {
method : function() {
BEM.channel('sys').on('wakeup', /* ... */);
}
});
```
Стало:
```js
modules.define('i-bem__dom', ['idle'], function(provide, idle, BEMDOM) {
BEMDOM.decl('block', {
method : function() {
idle.on('wakeup', /* ... */);
}
});
});
```
### BEM-блоки
Те BEM-блоки, которые использовались как хранилище для каких-то методов, при этом никак не использующие BEM-методологию, теперь
могут быть написаны как модули.
Было:
```js
BEM.decl('i-router', {
route : function() { /* ... */ }
});
```
Стало:
```js
modules.define('router', function(provide) {
provide({
route : function() { /* ... */ }
});
});
```
Если же, по каким-то причинам, нужны именно BEM-блоки (не BEM.DOM-блоки), то их можно объявлять, доопределяя модуль `i-bem`.
Было:
```js
BEM.decl('my-block', { /* ... */ });
```
Стало:
```js
modules.define('i-bem', function(provide, BEM) {
BEM.decl('my-block', { /* ... */ });
provide(BEM);
});
```
#### Рефакторинг на примере блока `b-spin`
Было:
```js
BEM.DOM.decl('b-spin', {
onSetMod : {
'js' : function() {
this._size = this.getMod('size') || /[\d]+/.exec(this.getMod('theme'))[0];
this._bgProp = 'background-position';
this._posPrefix = '0 -';
if (this.elem('icon').css('background-position-y')) { /* В IE нельзя получить свойство background-position, только background-position-y, поэтому костыляем */
this._bgProp = 'background-position-y';
this._posPrefix = '-';
}
this._curFrame = 0;
this.hasMod('progress') && this.channel('sys').on('tick', this._onTick, this);
},
'progress' : {
'yes' : function() {
this.channel('sys').on('tick', this._onTick, this);
},
'' : function() {
this.channel('sys').un('tick', this._onTick, this);
}
}
},
_onTick: function(){
var y = ++this._curFrame * this._size;
(y >= this._size * 36) && (this._curFrame = y = 0);
this.elem('icon').css(this._bgProp, this._posPrefix + y +'px');
},
destruct : function() {
this.channel('sys').un('tick', this._onTick, this);
this.__base.apply(this, arguments);
}
});
```
Стало:
```js
modules.define(
'i-bem__dom',
['tick'],
function(provide, tick, BEMDOM) {
var FRAME_COUNT = 36;
BEMDOM.decl('b-spin', {
onSetMod : {
'js' : {
'inited' : function() { // конструктор
var hasBackgroundPositionY = !!this.elem('icon').css('background-position-y')); /* В IE нельзя получить свойство background-position, только background-position-y */
this._bgProp = hasBackgroundPositionY? 'background-position-y' : 'background-position';
this._posPrefix = hasBackgroundPositionY? '-' : '0 -';
this._curFrame = 0;
this._size = Number(this.getMod('size') || /[\d]+/.exec(this.getMod('theme'))[0]);
this.hasMod('progress') && this._bindToTick();
},
'' : function() { // деструктор
this._unbindFromTick();
}
},
'progress' : {
'true' : function() {
this._bindToTick();
},
'' : function() {
this._unbindFromTick();
}
}
},
_bindToTick : function() {
tick.on('tick', this._onTick, this);
},
_unbindFromTick : function() {
tick.un('tick', this._onTick, this);
},
_onTick : function() {
var offset;
this._curFrame++ >= FRAME_COUNT?
offset = this._curFrame * this._size :
this._curFrame = offset = 0;
this.elem('icon').css(this._bgProp, this._posPrefix + offset + 'px');
}
});
provide(BEMDOM);
});
```
================================================
FILE: PLAN.md
================================================
# Plan: Modernization of bem-core Dependencies
## Current State Analysis
### Runtime Environment
| Component | Current | Target | Status |
|-----------|---------|--------|--------|
| Node.js | 8 | 24 LTS (Krypton, v24.13.1) | 16 major versions behind |
| npm | 5-6 (lockfile v1) | 11+ (lockfile v3) | Needs regeneration |
### Dependencies — Current vs Latest
#### Production (`dependencies`)
| Package | Current | Latest | Last Published | Status |
|---------|---------|--------|----------------|--------|
| `ym` | ^0.1.2 | 0.1.2 | ancient | **Abandoned**. BEM module system. No updates for years. |
#### Dev Dependencies (`devDependencies`)
**Build System (ENB) — ALL ABANDONED:**
| Package | Current | Latest | Last Published | Status |
|---------|---------|--------|----------------|--------|
| `enb` | ^1.2.0 | 1.5.1 | 2017-11 | **Abandoned** |
| `enb-bem-techs` | ^2.2.2 | 2.2.2 | 2017-12 | **Abandoned** |
| `enb-magic-factory` | ^0.6.0 | 0.6.0 | 2018-02 | **Abandoned** |
| `enb-magic-platform` | 0.7.0 | 0.7.0 | 2016-04 | **Abandoned** |
| `enb-bemxjst` | ^8.10.2 | 8.10.6 | ~2018 | **Abandoned** |
| `enb-bemxjst-6x` | ^6.5.3 | — | — | **Abandoned** |
| `enb-bemxjst-7x` | ^7.3.1 | — | — | **Abandoned** |
| `enb-bemxjst-i18n` | 1.0.0-beta3 | — | — | **Abandoned** |
| `enb-bh` | ^1.2.1 | — | — | **Abandoned** |
| `enb-bh-i18n` | 1.0.0-beta2 | — | — | **Abandoned** |
| `enb-borschik` | ^2.8.0 | — | — | **Abandoned** |
| `enb-css` | ^1.2.2 | 1.2.2 | — | **Abandoned** |
| `enb-js` | ^1.1.1 | 1.1.1 | — | **Abandoned** |
| `enb-bem-docs` | 0.14.1 | 0.15.0 | 2019-02 | **Abandoned** |
| `enb-bem-examples` | ^1.0.2 | 1.0.2 | 2016-04 | **Abandoned** |
| `enb-bem-specs` | ^0.11.0 | 0.11.0 | 2016-12 | **Abandoned** |
| `enb-bem-tmpl-specs` | ^1.3.3 | 1.3.3 | 2018-03 | **Abandoned** |
| `enb-bem-i18n` | ^1.1.1 | — | — | **Abandoned** |
**Linting — ABANDONED/OUTDATED:**
| Package | Current | Latest | Last Published | Status |
|---------|---------|--------|----------------|--------|
| `jscs` | ^2.11.0 | 3.0.7 | 2016-07 | **Abandoned** (merged into ESLint) |
| `jscs-bem` | ^0.2.0 | — | — | **Abandoned** |
| `jshint` | ^2.9.1 | 2.13.6 | maintained | Functional but superseded by ESLint |
| `jshint-groups` | ^0.8.0 | — | — | **Abandoned** |
**Testing — PARTIALLY ABANDONED:**
| Package | Current | Latest | Last Published | Status |
|---------|---------|--------|----------------|--------|
| `mocha` | ^3.3.0 | 11.7.5 | active | 8 major versions behind |
| `mocha-phantomjs` | ^4.1.0 | 4.1.0 | 2016-06 | **Abandoned** (PhantomJS is dead) |
| `chai` | ^3.2.0 | 6.2.2 | active | 3 major versions behind; v5+ is ESM-only |
| `chai-as-promised` | ^5.1.0 | — | — | Outdated |
| `istanbul` | ^0.4.3 | 0.4.5 | 2016-08 | **Abandoned** (replaced by nyc → c8) |
**BEM Tools:**
| Package | Current | Latest | Last Published | Status |
|---------|---------|--------|----------------|--------|
| `bem-naming` | ^1.0.1 | 1.0.1 | — | **Abandoned** |
| `bem-walk` | 1.0.0-alpha1 | 1.0.0-1 | — | **Never left alpha** |
**Other:**
| Package | Current | Latest | Last Published | Status |
|---------|---------|--------|----------------|--------|
| `borschik` | ^1.5.3 | 3.0.0 | 2021-02 | Unmaintained |
| `bower` | ^1.7.9 | 1.8.14 | 2022-03 | **Deprecated** since 2017 |
| `git-hooks` | ^1.0.2 | 1.1.10 | — | Superseded by husky |
| `gitbook-api` | ^3.0.2 | — | — | **Abandoned** |
| `jsdoc` | ^3.5.5 | 4.0.5 | active | 1 major version behind |
| `vow` | ^0.4.17 | 0.4.20 | 2019-07 | **Abandoned** (native Promises exist) |
### Configuration Files to Replace
| File | Purpose | Modern Replacement |
|------|---------|-------------------|
| `.jshintrc` | JSHint config | `eslint.config.js` (ESLint flat config) |
| `.jscs.json` | JSCS style config | `eslint.config.js` (ESLint flat config) |
| `.jshint-groups.js` | JSHint groups config | `eslint.config.js` (ESLint flat config) |
| `.bowerrc` | Bower directory config | Remove (drop Bower) |
| `bower.json` | Bower package manifest | Remove (drop Bower) |
| `.travis.yml` | Travis CI config | `.github/workflows/ci.yml` (GitHub Actions) |
| `.enb/` (entire dir) | ENB build config | New build system config |
| `.githooks/pre-commit/lint` | Pre-commit hook | `.husky/pre-commit` |
### CI/CD
| Component | Current | Target |
|-----------|---------|--------|
| CI system | Travis CI | GitHub Actions |
| Coverage | Istanbul + Coveralls | c8 + Coveralls (or Codecov) |
| Node.js in CI | 8 | 24 |
---
## Implementation Plan
### Phase 0: Preparation
1. Create feature branch `claude/update-dependencies-fWO1e`
2. Verify project builds and tests in current state (baseline)
### Phase 1: Node.js & npm Modernization
1. Add `.nvmrc` with `24`
2. Add `engines` field to `package.json`: `"node": ">=24"`, `"npm": ">=11"`
3. Delete `package-lock.json` (will regenerate with lockfile v3)
### Phase 2: Remove Abandoned/Deprecated Tools
1. **Remove Bower**: delete `bower.json`, `.bowerrc`, remove `bower i` from scripts
2. **Remove JSCS**: delete `.jscs.json`, uninstall `jscs`, `jscs-bem`
3. **Remove JSHint**: delete `.jshintrc`, `.jshint-groups.js`, uninstall `jshint`, `jshint-groups`
4. **Remove Istanbul**: uninstall `istanbul`
5. **Remove mocha-phantomjs**: uninstall `mocha-phantomjs`
6. **Remove gitbook-api**: uninstall `gitbook-api`
7. **Remove git-hooks**: delete `.githooks/` directory, uninstall `git-hooks`
### Phase 3: Linting — Migrate to ESLint 10
1. Install `eslint@^10.0.1`
2. Create `eslint.config.js` (flat config, required for ESLint 10) migrating rules from:
- `.jshintrc` rules → ESLint equivalents
- `.jscs.json` BEM preset rules → ESLint equivalents
- `.jshint-groups.js` file-group-specific overrides → ESLint flat config overrides
3. Support file extensions: `.js`, `.bemtree`, `.bemhtml`
4. Update `package.json` `lint` script: `"lint": "eslint ."`
5. Delete old config files: `.jshintrc`, `.jscs.json`, `.jshint-groups.js`
### Phase 4: Testing Modernization
1. **Upgrade Mocha**: `mocha@^11.7.5`
2. **Upgrade Chai**: `chai@^6.2.2` (ESM-only — requires `"type": "module"` or `.mjs` for test files using it)
- Alternative: stay on `chai@^4.x` (last CJS version) if ESM migration is too invasive
3. **Replace Istanbul with c8**: install `c8@^10.1.3`
4. **Replace mocha-phantomjs with Playwright**: install `playwright@^1.58.2` and `@playwright/test`
- Browser spec tests (`.spec.js` files using `modules.define`) need adaptation for Playwright
5. Update test scripts in `package.json`
### Phase 5: Build System — ENB → Vite 6 + BEM Levels Plugin
> **This is the highest-risk, highest-effort phase.** The entire ENB ecosystem (17+ packages) is abandoned.
> **Chosen approach: Vite 6** with custom `vite-plugin-bem-levels` for BEM level resolution + barrel file generation, plus dedicated plugins for BEMHTML/BH template compilation and i18n.
#### What ENB Currently Does (to be replaced)
| ENB Function | Packages | Vite Replacement |
|---|---|---|
| BEM level scanning & file resolution | `enb-bem-techs` | `vite-plugin-bem-levels` (custom) |
| JS bundling with `ym` module system | `enb-js` + `ym` | Vite native ES modules + barrel files |
| Module redefinition (`modules.define` chains) | `ym` runtime | Platform barrel files (auto-generated) |
| CSS concatenation | `enb-css` | Vite native CSS handling |
| BEMHTML template compilation | `enb-bemxjst`, `enb-bemxjst-6x`, `enb-bemxjst-7x` | `vite-plugin-bemhtml` (custom, wraps `bem-xjst`) |
| BH template compilation | `enb-bh` | `vite-plugin-bh` (custom, wraps `bh`) |
| i18n keysets processing | `enb-bem-i18n`, `enb-bemxjst-i18n`, `enb-bh-i18n` | `vite-plugin-bem-i18n` (custom) |
| Minification (borschik) | `enb-borschik`, `borschik` | Vite built-in (esbuild/terser for JS, lightningcss for CSS) |
| HTML from BEMJSON | `enb-bemxjst`, `enb-bh` | Build script using compiled templates |
| Examples/tests/specs building | `enb-bem-examples`, `enb-bem-specs`, `enb-bem-docs`, `enb-magic-*` | Vite dev server + Playwright |
#### Phase 5.0: Preparation & Coexistence
> ENB and Vite will coexist during migration. ENB stays functional until Vite fully replaces it.
1. Install Vite 6 and core dependencies:
```
npm i -D vite@^6 @anthropic-ai/vite-plugin-bem-levels
```
(Initially `vite-plugin-bem-levels` will live in `build/plugins/` as a local module)
2. Create build directory structure:
```
build/
├── plugins/
│ ├── vite-plugin-bem-levels.js — BEM level resolution + barrel generation
│ ├── vite-plugin-bemhtml.js — BEMHTML compilation
│ ├── vite-plugin-bh.js — BH compilation
│ └── vite-plugin-bem-i18n.js — i18n keysets
├── platforms/
│ ├── desktop.js — entry point for desktop platform
│ └── touch.js — entry point for touch platform
└── vite.config.js — main Vite config
```
3. Keep `.enb/` directory intact for fallback
#### Phase 5.1: `vite-plugin-bem-levels` — Core BEM Resolution Plugin
This is the central piece. The plugin:
**5.1.1. Level scanning** — scans BEM levels like ENB does:
```js
// Configuration mirrors .enb/config/levels.js
const LEVELS = {
common: ['common.blocks'],
desktop: ['common.blocks', 'desktop.blocks'],
touch: ['common.blocks', 'touch.blocks']
};
```
**5.1.2. Virtual module resolution** — resolves `bem:*` imports:
```js
// In source code:
import $ from 'bem:jquery';
import bemDom from 'bem:i-bem-dom';
// Plugin resolves 'bem:jquery' → generated barrel file
```
**5.1.3. Barrel file generation for module redefinition chains**
For each module with redefinitions, generates a platform-specific barrel file.
Based on analysis, there are exactly **5 modules with redefinitions** (8 total redefinition instances):
| Module | Files in chain | Generated barrel |
|---|---|---|
| `jquery` | base + 4 redefinitions (3 common + 1 desktop) | `@bem/desktop/jquery.js`, `@bem/touch/jquery.js` |
| `jquery__config` | base (common) + 1 redefinition (desktop) | `@bem/desktop/jquery__config.js` |
| `ua` | alternative defs (desktop vs touch) + 1 redefinition (touch) | `@bem/desktop/ua.js`, `@bem/touch/ua.js` |
| `events__observable` | base + 1 redefinition | `@bem/common/events__observable.js` |
| `i-bem-dom__init` | base + dynamic redefinition | Special handling (see below) |
Example generated barrel for `jquery` on desktop platform:
```js
// @generated by vite-plugin-bem-levels for platform: desktop
import { $ } from '../../common.blocks/jquery/jquery.js';
// Redefinition: pointer events polyfill (mutates $.event.special)
import '../../common.blocks/jquery/__event/_type/jquery__event_type_pointernative.js';
// Redefinition: pointerclick event
import '../../common.blocks/jquery/__event/_type/jquery__event_type_pointerclick.js';
// Redefinition: pointerpress/pointerrelease events
import '../../common.blocks/jquery/__event/_type/jquery__event_type_pointerpressrelease.js';
// Redefinition: IE8 window resize fix (desktop only)
import '../../desktop.blocks/jquery/__event/_type/jquery__event_type_winresize.js';
export { $ };
```
**5.1.4. Automatic redefinition detection**
The plugin scans all `.js` files in BEM levels and detects `modules.define('name', ...)` calls:
- If a module name appears in multiple files → it's a redefinition chain
- Files are ordered by level priority (common < desktop/touch)
- Within a level, element/modifier files redefine block files
**5.1.5. deps.js → import graph**
Parse existing `.deps.js` files and generate import statements:
```js
// From: { shouldDeps: [{ block: 'events' }] }
// To: import 'bem:events';
```
This runs as a build-time code generation step, not at runtime.
#### Phase 5.2: ym → ES Modules Migration
**5.2.1. Source file transformation** — Each `modules.define` file gets an ES module equivalent:
Before (ym):
```js
modules.define('jquery', ['loader_type_js', 'jquery__config'],
function(provide, loader, cfg) {
// ...
provide(jQuery);
});
```
After (ES module):
```js
import loader from 'bem:loader_type_js';
import cfg from 'bem:jquery__config';
let jQuery;
// ... loading logic ...
export default jQuery;
```
**5.2.2. Redefinition files** — become side-effect imports or wrapper modules:
Before (ym redefinition):
```js
modules.define('jquery', function(provide, $) {
$.event.special.pointerclick = { /* ... */ };
provide($);
});
```
After (ES module side-effect):
```js
import $ from 'bem:jquery'; // gets the base jquery
$.event.special.pointerclick = { /* ... */ };
// No export needed — this is a side-effect module imported by the barrel
```
**5.2.3. Scope of ym migration**
| Category | Count | Migration complexity |
|---|---|---|
| `.vanilla.js` files (never redefined) | ~13 | Low — straightforward `export default` |
| `.js` files (base definitions, no redefinition) | ~30 | Low — `import` deps + `export default` |
| `.js` files (redefinition participants) | 14 | Medium — need barrel coordination |
| `i-bem-dom__init` dynamic redefinition | 1 | High — needs architectural redesign |
**5.2.4. `i-bem-dom__init` special case**
The dynamic `modules.define` monkey-patching in `i-bem-dom.js` (lines 1141-1158) cannot be directly expressed in ES modules. Solution:
- The Vite plugin generates the `i-bem-dom__init` barrel by scanning which blocks depend on `i-bem-dom`
- This replaces the runtime monkey-patching with build-time dependency collection
- The barrel imports all BEM DOM blocks, then calls `bemDom.init()`
#### Phase 5.3: Template Engine Plugins
**5.3.1. `vite-plugin-bemhtml`**
- Handles `.bemhtml` and `.bemhtml.js` files
- Wraps `bem-xjst` compiler (keep as dependency)
- Produces compiled JS that can be imported as ES module
- Supports HMR in dev mode
**5.3.2. `vite-plugin-bh`**
- Handles `.bh.js` files
- Wraps `bh` runtime
- Produces CommonJS-compatible bundle (BH uses `module.exports`)
#### Phase 5.4: i18n Plugin
**`vite-plugin-bem-i18n`**
- Scans `*.i18n/` directories for keysets
- Generates per-language JS modules
- Supports `{lang}` placeholder pattern from current ENB config
- Integrates with BEMHTML i18n via `bem-xjst`
#### Phase 5.5: Vite Configuration
```js
// build/vite.config.js
import { defineConfig } from 'vite';
import bemLevels from './plugins/vite-plugin-bem-levels.js';
import bemhtml from './plugins/vite-plugin-bemhtml.js';
import bh from './plugins/vite-plugin-bh.js';
import bemI18n from './plugins/vite-plugin-bem-i18n.js';
export default defineConfig(({ mode }) => {
const platform = process.env.BEM_PLATFORM || 'desktop';
return {
plugins: [
bemLevels({
platform,
levels: {
common: ['common.blocks'],
desktop: ['common.blocks', 'desktop.blocks'],
touch: ['common.blocks', 'touch.blocks']
}
}),
bemhtml(),
bh({ jsAttrName: 'data-bem', jsAttrScheme: 'json' }),
bemI18n({ langs: ['ru', 'en'] })
],
build: {
lib: {
entry: `./build/platforms/${platform}.js`,
name: 'bemCore',
formats: ['es', 'umd']
},
outDir: `dist/${platform}`,
rollupOptions: {
output: {
// Reproduce ENB dist structure:
// bem-core.js, bem-core.css, bem-core.bemhtml.js, etc.
}
}
}
};
});
```
#### Phase 5.6: dist Task Replacement
ENB `dist` task currently produces these artifacts per platform:
| Artifact | dev | min | Vite equivalent |
|---|---|---|---|
| `bem-core.css` | `.dev.css` | `.css` | Vite CSS output (dev: unminified, build: minified) |
| `bem-core.js` | `.dev.js` | `.js` | Vite JS bundle (with autoinit) |
| `bem-core.no-autoinit.js` | `.dev.no-autoinit.js` | `.no-autoinit.js` | Separate entry point without `i-bem-dom__init_auto` |
| `bem-core.bemhtml.js` | `.dev.bemhtml.js` | `.bemhtml.js` | BEMHTML-only bundle via separate entry |
| `bem-core.bh.js` | `.dev.bh.js` | `.bh.js` | BH-only bundle via separate entry |
| `bem-core.js+bemhtml.js` | `.dev.js+bemhtml.js` | `.js+bemhtml.js` | Combined bundle (JS + BEMHTML) |
| `bem-core.js+bh.js` | `.dev.js+bh.js` | `.js+bh.js` | Combined bundle (JS + BH) |
Vite handles dev/production modes natively (no borschik needed).
npm scripts:
```json
{
"build": "npm run build:desktop && npm run build:touch",
"build:desktop": "BEM_PLATFORM=desktop vite build -c build/vite.config.js",
"build:touch": "BEM_PLATFORM=touch vite build -c build/vite.config.js",
"dev": "BEM_PLATFORM=desktop vite -c build/vite.config.js"
}
```
#### Phase 5.7: specs/tests/examples Migration
1. **Browser specs** (`*.spec.js`) — Currently use `enb-bem-specs` + mocha-phantomjs:
- Migrate to Playwright (already planned in Phase 4)
- Vite dev server serves spec pages instead of ENB magic nodes
- `vite-plugin-bem-levels` resolves spec level: `libs/bem-pr/spec.blocks`
2. **Examples** (`*.examples/`) — Currently use `enb-bem-examples`:
- Vite dev server with HTML plugin serves example pages
- BEMJSON → HTML conversion done via imported compiled templates
3. **Template specs** (`tmpl-specs`) — Currently use `enb-bem-tmpl-specs`:
- Run as Node.js tests with Mocha (import compiled templates, compare output)
#### Phase 5.8: Cleanup
1. Delete `.enb/` directory entirely (17 files)
2. Uninstall all ENB packages (17 packages):
```
npm rm enb enb-bem-techs enb-magic-factory enb-magic-platform \
enb-bemxjst enb-bemxjst-6x enb-bemxjst-7x enb-bemxjst-i18n \
enb-bh enb-bh-i18n enb-borschik enb-css enb-js \
enb-bem-docs enb-bem-examples enb-bem-specs enb-bem-tmpl-specs \
enb-bem-i18n
```
3. Uninstall `borschik` (replaced by Vite built-in minification)
4. Remove `ym` from production dependencies (replaced by ES modules)
5. Remove `bem-walk` and `bem-naming` if no longer used outside ENB
6. Update `package.json` scripts to use Vite commands
#### Phase 5 Sub-Phase Execution Order
```
5.0 Preparation & coexistence setup
│
├── 5.1 vite-plugin-bem-levels (CRITICAL PATH — everything depends on this)
│ ├── 5.1.1 Level scanning
│ ├── 5.1.2 Virtual module resolution (bem:* imports)
│ ├── 5.1.3 Barrel file generation
│ ├── 5.1.4 Redefinition detection
│ └── 5.1.5 deps.js parsing
│
├── 5.2 ym → ES modules migration (can start after 5.1 is functional)
│ ├── 5.2.1 .vanilla.js files (easiest, start here)
│ ├── 5.2.2 .js base definitions
│ ├── 5.2.3 .js redefinition files
│ └── 5.2.4 i-bem-dom__init special case
│
├── 5.3 Template plugins (parallel with 5.2)
│ ├── 5.3.1 vite-plugin-bemhtml
│ └── 5.3.2 vite-plugin-bh
│
├── 5.4 i18n plugin (parallel with 5.2, 5.3)
│
├── 5.5 Vite config (after 5.1-5.4 plugins exist)
│
├── 5.6 dist replacement (after 5.5)
│ └── Verify output matches ENB dist artifacts
│
├── 5.7 specs/tests/examples (after 5.6)
│
└── 5.8 Cleanup (LAST — only after full verification)
```
#### Phase 5 Risk Mitigation
| Risk | Impact | Mitigation |
|---|---|---|
| Barrel file import order matters for side-effects | High | Plugin sorts by level priority; test thoroughly |
| `i-bem-dom__init` dynamic dep collection hard to replicate | High | Build-time scanning replaces runtime monkey-patching |
| `bem-xjst` may not work as Vite plugin | Medium | Keep as Node.js pre-compilation step if needed |
| Circular dependencies between BEM blocks | Medium | Vite handles circular ESM; add cycle detection to plugin |
| Output bundle size differs from ENB | Low | Compare sizes; adjust Rollup chunking |
| Dev server HMR with BEM redefinitions | Low | Regenerate barrels on file change; full reload as fallback |
### Phase 6: Git Hooks Modernization
1. Install `husky@^9.1.7` + `lint-staged@^16.2.7`
2. Configure `.husky/pre-commit` to run `lint-staged`
3. Configure `lint-staged` in `package.json` to run ESLint on staged files
4. Delete `.githooks/` directory
### Phase 7: CI/CD — Travis CI → GitHub Actions
1. Create `.github/workflows/ci.yml`:
- Matrix: Node.js 24
- Steps: install, lint, test
- Coverage: c8 + upload to Coveralls
2. Delete `.travis.yml`
### Phase 8: Update Remaining Packages
1. `mocha` → ^11.7.5
2. `jsdoc` → ^4.0.5
3. `vow` → replace with native `Promise` where possible; remove if fully replaced
4. `borschik` → remove (replaced by Vite built-in minification in Phase 5.8)
5. `bem-naming` → remove if only used by ENB plugins; keep if used by `vite-plugin-bem-levels`
6. `bem-walk` → remove (replaced by custom level scanning in `vite-plugin-bem-levels`)
7. `ym` → remove from production deps (replaced by ES modules in Phase 5.2)
### Phase 9: Regenerate Lock File & Validate
1. Run `npm install` to generate new `package-lock.json` (lockfile v3)
2. Run `npm run lint` — fix any ESLint issues
3. Run `npm run build` — verify Vite builds succeed for both platforms
4. Run `npm run test` — verify all tests pass
5. Compare Vite dist output with archived ENB dist output (size, functionality)
6. Run examples in Vite dev server, verify they work
---
## Risk Assessment
| Risk | Impact | Mitigation |
|------|--------|------------|
| `vite-plugin-bem-levels` barrel generation order incorrect | **CRITICAL** | Comprehensive tests comparing ENB and Vite output |
| `i-bem-dom__init` dynamic monkey-patching hard to replicate statically | **HIGH** | Build-time BEM block scanning replaces runtime logic |
| `bem-xjst` (BEMHTML compiler) integration with Vite plugin | High | Keep as pre-compilation step if direct plugin fails |
| Chai 6.x ESM-only breaks test imports | Medium | Use Chai 4.x (last CJS) or add ESM wrapper |
| mocha-phantomjs removal breaks browser tests | High | Playwright migration for spec tests |
| Circular dependencies between BEM blocks in ES modules | Medium | Vite handles circular ESM natively; add cycle detection |
| BEM-specific spec files (`modules.define`) won't work without `ym` | High | Migrate spec files to ES imports as part of Phase 5.2 |
| Output bundle size/behavior differs from ENB | Medium | Comparison testing: run both builds, diff output |
| ym → ES modules migration introduces regressions | High | Phased migration with coexistence; ENB stays as reference |
## Suggested Execution Order (by priority/safety)
1. **Phase 0** — Baseline
2. **Phase 1** — Node.js 24 (needed for Vite 6 and modern tooling)
3. **Phase 2** — Remove dead packages (safe, reduces surface)
4. **Phase 3** — ESLint migration (independent, high value)
5. **Phase 6** — Git hooks (small, independent)
6. **Phase 7** — GitHub Actions (independent)
7. **Phase 8** — Update remaining packages
8. **Phase 4** — Testing modernization (Playwright needed for Phase 5.7)
9. **Phase 5** — Build system migration to Vite (largest effort):
- 5.0 → 5.1 → 5.2 + 5.3 + 5.4 (parallel) → 5.5 → 5.6 → 5.7 → 5.8
10. **Phase 9** — Final validation (verify all Vite builds match ENB output)
================================================
FILE: README.md
================================================
# bem-core library [](https://github.com/bem/bem-core/actions/workflows/ci.yml) [](https://github.com/bem/bem-core/releases)
Documentation on `bem-core` is also available at [bem.info](https://en.bem.info/libs/bem-core/). It is also available [in Russian](https://ru.bem.info/libs/bem-core/).
## What is this?
`bem-core` is a base library for web interface development.
It provides the minimal stack for coding client-side JavaScript and templating.
## Use
Install as an npm dependency:
```shell
npm install bem-core@5
```
jQuery 4 is a peer dependency — install it alongside:
```shell
npm install jquery@^4.0.0
```
## Inside
### Levels
- `common.blocks` — suited for any devices and browsers
- `desktop.blocks` — should be used for desktop browsers
- `touch.blocks` — implement some touch-platforms specifics
### Blocks
- `i-bem` — base block with helpers for JS and HTML
- `strings` — helpers for JS-strings
- `objects` — helpers for JS-objects
- `functions` — helpers for JS-functions
- `events` — JS-events
- `uri` — helpers for work with URIs and querystrings
- `tick` — global timer
- `idle` — IDLE event
- `next-tick` — polyfill for `nextTick`/`setTimeout(0, ...)`
- `inherit` — OOP helpers
- `jquery` — jQuery
- `clearfix` — CSS clearfix trick
- `identify` — identify JS-objects
- `cookie` — helpers for work with browser cookies
- `dom` — helpers for work with DOM
- `loader` — loader for JS files
- `ua` — browser features detection
- `keyboard` — keyboard helpers
- `page` — html/head/body scaffold
### Technologies
- vanilla.js + browser.js
- bemhtml
- bemtree
## API
The autogenerated JSDoc API can be found on bem.info. E.g. JSDoc for `i-bem` is here https://en.bem.info/platform/libs/bem-core/current/desktop/i-bem/#jsdoc
## Changelog
See [CHANGELOG.md](CHANGELOG.md).
## Migration
If you are upgrading from v4, see [MIGRATION.md](MIGRATION.md).
## Development
### Working copy
1. Get the source code:
```shell
git clone -b v5 git://github.com/bem/bem-core.git
cd bem-core
```
2. Install the dependencies (requires Node.js 20+):
```shell
npm install
```
3. Run linting:
```shell
npm run lint
```
4. Run tests:
```shell
npm test # server-side tests (node:test)
npm run test:browser # browser tests (Playwright)
npm run test:all # both
```
5. Build:
```shell
npm run build # desktop + touch platforms
```
### How to contribute
Please refer to [How to contribute](/CONTRIBUTING.md) guide.
## Supported browsers
- Google Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
## License
Code and documentation copyright 2012 YANDEX LLC. Code released under the [Mozilla Public License 2.0](LICENSE.txt).
================================================
FILE: README.ru.md
================================================
# Библиотека BEM Core
`bem-core` — это библиотека с открытым кодом, которая предоставляет набор блоков для разработки веб-интерфейсов. Содержит необходимый минимум для разработки клиентского JS и HTML-шаблонов.
[](https://github.com/bem/bem-core/actions/workflows/ci.yml) [](https://github.com/bem/bem-core/releases)
> **Примечание.** Информация о библиотеке в более информативном виде доступна на [bem.info](https://ru.bem.info/libs/bem-core/). This README is also available [in English](https://en.bem.info/libs/bem-core/).
## Содержание
* [Уровни](#Уровни-переопределения)
* [Блоки](#Блоки)
* [Использование](#Использование)
* [Поддерживаемые браузеры](#Поддерживаемые-браузеры)
* [Технологии](#Технологии)
* [API](#api)
* [Разработка](#Разработка)
**Дополнительная информация**
* [История изменений](CHANGELOG.ru.md)
* [Миграция на последующие версии](MIGRATION.ru.md)
## Уровни переопределения
* `common.blocks` — поддержка всех устройств и браузеров;
* `desktop.blocks` — поддержка всех десктопных браузеров;
* `touch.blocks` — реализация специфических особенностей для touch-платформ.
## Блоки
* [i-bem](common.blocks/i-bem/i-bem.ru.md) — базовый блок с хелперами для JS и HTML;
* [i-bem-dom](common.blocks/i-bem-dom/i-bem-dom.ru.md) — базовый блок с хелперами для HTML;
* [strings](common.blocks/strings/strings.ru.md) — хелперы для JS-строк;
* [objects](common.blocks/objects/objects.ru.md) — хелперы для JS-объектов;
* [functions](common.blocks/functions/functions.ru.md) — хелперы для JS-функций;
* [events](common.blocks/events/events.ru.md) — JS-события;
* [uri](common.blocks/uri/uri.ru.md) — работа с URI и строкой запроса;
* [tick](common.blocks/tick/tick.ru.md) — глобальный таймер;
* [idle](common.blocks/idle/idle.ru.md) — IDLE-событие;
* [next-tick](common.blocks/next-tick/next-tick.ru.md) — полифил для `nextTick`/`setTimeout(0, ...)`;
* [inherit](common.blocks/inherit/inherit.ru.md) — ООП-хелперы;
* [jquery](common.blocks/jquery/jquery.ru.md) — jQuery;
* [clearfix](common.blocks/clearfix/clearfix.ru.md) — CSS-трюк clearfix;
* [identify](common.blocks/identify/identify.ru.md) — идентификация JS-объектов;
* [cookie](common.blocks/cookie/cookie.ru.md) — хелперы для работы с браузерными куками;
* [dom](common.blocks/dom/dom.ru.md) — хелперы для работы с DOM;
* [loader](common.blocks/loader/loader.ru.md) — загрузчик для JS-файлов;
* [ua](common.blocks/ua/ua.ru.md) — определение возможностей браузера;
* [uri](common.blocks/uri/uri.ru.md) — декодирование строки из формата URI;
* [keyboard](common.blocks/keyboard/keyboard.ru.md) — хелперы для работы с клавиатурой;
* [page](common.blocks/page/page.ru.md) — скелет для html/head/body.
## Использование
Установите как npm-зависимость:
```shell
npm install bem-core@5
```
jQuery 4 — peer-зависимость, установите рядом:
```shell
npm install jquery@^4.0.0
```
## Поддерживаемые браузеры
* Google Chrome (последняя версия)
* Firefox (последняя версия)
* Safari (последняя версия)
* Edge (последняя версия)
## Технологии
* vanilla.js + browser.js;
* DEPS;
* bemhtml;
* bemtree.
## API
Автосгенерированную документацию на JavaScript API блоков (JSDoc) можно посмотреть на bem.info. Например, для блока `i-bem` она доступна по ссылке https://ru.bem.info/platform/libs/bem-core/current/desktop/i-bem/#jsdoc
## Разработка
### Рабочая копия
1. Получаем исходники:
```bash
git clone -b v5 git://github.com/bem/bem-core.git
cd bem-core
```
2. Устанавливаем зависимости (требуется Node.js 20+):
```bash
npm install
```
3. Запускаем линтер:
```bash
npm run lint
```
4. Запускаем тесты:
```bash
npm test # серверные тесты (node:test)
npm run test:browser # браузерные тесты (Playwright)
npm run test:all # все тесты
```
5. Собираем:
```bash
npm run build # desktop + touch платформы
```
## Команда основной разработки
* [veged](https://github.com/veged)
* [dfilatov](https://github.com/dfilatov)
* [tadatuta](https://github.com/tadatuta)
## Лицензия
© 2012 YANDEX LLC. Код лицензирован [Mozilla Public License 2.0](LICENSE.txt).
================================================
FILE: SUMMARY.md
================================================
# Summary
* [Введение](README.ru.md)
* [Общие сведения](common.docs/i-bem-js/i-bem-js-common.ru.md)
* [Привязка JS-блоков к HTML](common.docs/i-bem-js/i-bem-js-html-binding.ru.md)
* [Декларация блока](common.docs/i-bem-js/i-bem-js-decl.ru.md)
* [Передача параметров](common.docs/i-bem-js/i-bem-js-params.ru.md)
* [Работа с DOM-деревом](common.docs/i-bem-js/i-bem-js-dom.ru.md)
* [Состояния блока](common.docs/i-bem-js/i-bem-js-states.ru.md)
* [Коллекции](common.docs/i-bem-js/i-bem-js-collections.ru.md)
* [События](common.docs/i-bem-js/i-bem-js-events.ru.md)
* [Инициализация](common.docs/i-bem-js/i-bem-js-init.ru.md)
* [Взаимодействие блоков](common.docs/i-bem-js/i-bem-js-interact.ru.md)
* [Контекст](common.docs/i-bem-js/i-bem-js-context.ru.md)
* [Что дальше?](common.docs/i-bem-js/i-bem-js-extras.ru.md)
================================================
FILE: build/platforms/desktop.js
================================================
/**
* Entry point for the desktop platform build.
*
* This file imports all BEM blocks in the correct order for the desktop platform.
* The vite-plugin-bem-levels plugin resolves `bem:*` imports to actual files
* from the level chain: common.blocks → desktop.blocks
*/
// Core utilities (no dependencies)
import 'bem:identify';
import 'bem:inherit';
import 'bem:objects';
import 'bem:functions';
import 'bem:functions__throttle';
import 'bem:functions__debounce';
import 'bem:next-tick';
import 'bem:strings__escape';
// Events system
import 'bem:events';
import 'bem:events__channels';
// i-bem core
import 'bem:i-bem__internal';
import 'bem:i-bem';
import 'bem:i-bem__collection';
// DOM utilities
import 'bem:jquery__config';
import 'bem:jquery';
import 'bem:jquery__event_type_winresize';
import 'bem:dom';
// i-bem-dom and its subsystems
import 'bem:i-bem-dom__events';
import 'bem:i-bem-dom__events_type_dom';
import 'bem:i-bem-dom__events_type_bem';
import 'bem:i-bem-dom__collection';
import 'bem:i-bem-dom';
import 'bem:i-bem-dom__init';
import 'bem:i-bem-dom__init_auto';
// Observable events (with BEM DOM support)
import 'bem:events__observable';
import 'bem:events__observable_type_bem-dom';
// Browser APIs
import 'bem:cookie';
import 'bem:idle';
import 'bem:idle_start_auto';
import 'bem:tick';
import 'bem:tick_start_auto';
import 'bem:keyboard__codes';
import 'bem:ua';
// URI utilities
import 'bem:uri';
import 'bem:uri__querystring';
// Loaders
import 'bem:loader_type_js';
import 'bem:loader_type_bundle';
================================================
FILE: build/platforms/touch.js
================================================
/**
* Entry point for the touch platform build.
*
* This file imports all BEM blocks in the correct order for the touch platform.
* The vite-plugin-bem-levels plugin resolves `bem:*` imports to actual files
* from the level chain: common.blocks → touch.blocks
*/
// Core utilities (no dependencies)
import 'bem:identify';
import 'bem:inherit';
import 'bem:objects';
import 'bem:functions';
import 'bem:functions__throttle';
import 'bem:functions__debounce';
import 'bem:next-tick';
import 'bem:strings__escape';
// Events system
import 'bem:events';
import 'bem:events__channels';
// i-bem core
import 'bem:i-bem__internal';
import 'bem:i-bem';
import 'bem:i-bem__collection';
// DOM utilities
import 'bem:jquery__config';
import 'bem:jquery';
import 'bem:dom';
// i-bem-dom and its subsystems
import 'bem:i-bem-dom__events';
import 'bem:i-bem-dom__events_type_dom';
import 'bem:i-bem-dom__events_type_bem';
import 'bem:i-bem-dom__collection';
import 'bem:i-bem-dom';
import 'bem:i-bem-dom__init';
import 'bem:i-bem-dom__init_auto';
// Observable events (with BEM DOM support)
import 'bem:events__observable';
import 'bem:events__observable_type_bem-dom';
// Browser APIs
import 'bem:cookie';
import 'bem:idle';
import 'bem:idle_start_auto';
import 'bem:tick';
import 'bem:tick_start_auto';
import 'bem:keyboard__codes';
import 'bem:ua';
import 'bem:ua__dom';
// URI utilities
import 'bem:uri';
import 'bem:uri__querystring';
// Loaders
import 'bem:loader_type_js';
import 'bem:loader_type_bundle';
================================================
FILE: build/plugins/vite-plugin-bem-levels.js
================================================
import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'
import { join, resolve, relative, dirname, basename } from 'node:path'
const BEM_PREFIX = 'bem:'
const VIRTUAL_PREFIX = '\0bem:'
/**
* Scans a BEM level directory and returns all BEM entities found.
*
* BEM nested file structure:
* block/
* block.js
* block.deps.js
* __elem/
* block__elem.js
* _mod/
* block_mod.js
* block_mod_val.js
*
* @param {string} levelDir - absolute path to a BEM level directory
* @returns {Map} moduleName → array of file entries
*/
function scanLevel(levelDir) {
const modules = new Map()
if (!existsSync(levelDir)) return modules
const blocks = readdirSync(levelDir).filter(name => {
const fullPath = join(levelDir, name)
return statSync(fullPath).isDirectory() && !name.startsWith('.')
})
for (const block of blocks) {
const blockDir = join(levelDir, block)
scanDirectory(blockDir, modules, levelDir)
}
return modules
}
/**
* Recursively scans a BEM entity directory for JS source files.
* Module names are derived from file names per BEM naming convention —
* file contents are never read during scanning.
*
* Only recurses into BEM-named subdirectories:
* __elemName (element) or _modName (modifier).
* This automatically excludes .tests/, .examples/, .tmpl-specs/, etc.
*/
function scanDirectory(dir, modules, levelDir) {
let entries
try {
entries = readdirSync(dir)
} catch {
return
}
for (const entry of entries) {
const fullPath = join(dir, entry)
let stat
try {
stat = statSync(fullPath)
} catch {
continue
}
if (stat.isDirectory()) {
// Only recurse into BEM-named subdirectories: __elem or _mod
if (entry.startsWith('__') || entry.startsWith('_')) {
scanDirectory(fullPath, modules, levelDir)
}
continue
}
if (!stat.isFile()) continue
// Consider JS source files and .post.css files
const isPostCss = entry.endsWith('.post.css')
const isVanillaJs = entry.endsWith('.vanilla.js')
const isPlainJs = !isVanillaJs && entry.endsWith('.js')
&& !entry.endsWith('.deps.js')
&& !entry.endsWith('.spec.js')
&& !entry.endsWith('.bemhtml.js')
&& !entry.endsWith('.bh.js')
&& !entry.endsWith('.bemjson.js')
&& !entry.endsWith('.test.js')
&& !entry.endsWith('.i18n.js')
if (!isVanillaJs && !isPlainJs && !isPostCss) continue
// Module name is derived from the filename per BEM naming convention
const name = filePathToModuleName(fullPath, levelDir)
if (!name) continue
const suffix = isPostCss ? '.post.css' : (isVanillaJs ? '.vanilla.js' : '.js')
const existing = modules.get(name) || []
existing.push({
name,
filePath: fullPath,
suffix,
levelDir,
})
modules.set(name, existing)
}
}
/**
* Parses a modules.define() call from source code.
* Returns { name, deps, callbackParamCount, isRedefinition } or null.
*
* In ym, a redefinition is detected by the callback having one extra parameter
* beyond provide + deps. That extra parameter receives the previous module value.
*
* Base: modules.define('name', ['dep1'], function(provide, dep1) { ... })
* → callbackParams = 2, isRedefinition = false
*
* Redefinition: modules.define('name', ['dep1'], function(provide, dep1, prev) { ... })
* → callbackParams = 3, isRedefinition = true
*/
function parseModulesDefine(source) {
// Match modules.define('name' or modules.define("name"
const defineMatch = source.match(
/modules\.define\s*\(\s*(['"])([^'"]+)\1/
)
if (!defineMatch) return null
const name = defineMatch[2]
// Try to extract dependency array
// Look for the pattern after the name: , ['dep1', 'dep2']
const afterName = source.slice(defineMatch.index + defineMatch[0].length)
const depsMatch = afterName.match(
/^\s*,\s*\[([^\]]*)\]/
)
const deps = []
if (depsMatch) {
const depsStr = depsMatch[1]
const depPattern = /['"]([^'"]+)['"]/g
let m
while ((m = depPattern.exec(depsStr)) !== null) {
deps.push(m[1])
}
}
// Extract callback parameter count to detect redefinitions.
// Look for `function(` after the deps array (or after the name if no deps).
const afterDeps = depsMatch
? afterName.slice(depsMatch[0].length)
: afterName
const callbackMatch = afterDeps.match(
/,\s*function\s*\(([^)]*)\)/
)
let callbackParamCount = 0
if (callbackMatch) {
const params = callbackMatch[1].trim()
callbackParamCount = params ? params.split(/\s*,\s*/).length : 0
}
// A redefinition has more callback params than provide (1) + deps count.
// The extra parameter receives the previous module value.
const isRedefinition = callbackParamCount > 1 + deps.length
return { name, deps, callbackParamCount, isRedefinition }
}
/**
* Parses a migrated ES module file.
* Detects `export default` and derives the module name from the file path.
*
* For redefinitions (transformer functions), detects:
* export default function(prev) { ... }
*
* Extracts `import ... from 'bem:...'` as dependencies.
*
* @param {string} source - file content
* @param {string} filePath - absolute path to the file
* @param {string} levelDir - absolute path to the level directory
* @returns {{ name: string, deps: string[], isRedefinition: boolean } | null}
*/
function parseEsModule(source, filePath, levelDir) {
// Must have export default
if (!/export\s+default\b/.test(source)) return null
// Derive module name from file path using BEM naming
const name = filePathToModuleName(filePath, levelDir)
if (!name) return null
// Extract bem: imports as dependencies
const deps = []
const importPattern = /import\s+\w+\s+from\s+['"]bem:([^'"]+)['"]/g
let m
while ((m = importPattern.exec(source)) !== null) {
deps.push(m[1])
}
// Detect if this is a transformer (redefinition):
// export default function(prev) { ... }
// The pattern is: export default function with exactly one parameter
const isRedefinition = /export\s+default\s+function\s*\([^)]+\)\s*\{/.test(source)
return { name, deps, isRedefinition }
}
/**
* Derives a BEM module name from a file path.
*
* common.blocks/objects/objects.vanilla.js → 'objects'
* common.blocks/i-bem/__internal/i-bem__internal.vanilla.js → 'i-bem__internal'
* common.blocks/functions/__debounce/functions__debounce.vanilla.js → 'functions__debounce'
* common.blocks/loader/_type/loader_type_js.js → 'loader_type_js'
*/
function filePathToModuleName(filePath, levelDir) {
const rel = relative(levelDir, filePath)
// Get the filename without extensions
const fileName = basename(rel)
// Strip .vanilla.js, .js, or .post.css
const name = fileName.replace(/\.(vanilla\.js|js|post\.css)$/, '')
return name || null
}
/**
* Parses a .deps.js file and returns the dependency declarations.
*
* deps.js format:
* ({ shouldDeps: [...], mustDeps: [...], noDeps: [...] })
* or
* ([{ shouldDeps: ... }, { ... }])
*/
function parseDepsFile(filePath) {
if (!existsSync(filePath)) return null
const content = readFileSync(filePath, 'utf8')
try {
// deps.js files are wrapped in parentheses: ({ ... }) or ([...])
// Use Function constructor to evaluate (safer than eval, no access to scope)
const fn = new Function('return ' + content)
const result = fn()
return normalizeDeps(result)
} catch {
return null
}
}
/**
* Normalizes deps.js result into { mustDeps: [], shouldDeps: [], noDeps: [] }
*/
function normalizeDeps(raw) {
if (Array.isArray(raw)) {
// Array of dep declarations — merge them
const merged = { mustDeps: [], shouldDeps: [], noDeps: [] }
for (const item of raw) {
const norm = normalizeDeps(item)
if (norm) {
merged.mustDeps.push(...norm.mustDeps)
merged.shouldDeps.push(...norm.shouldDeps)
merged.noDeps.push(...norm.noDeps)
}
}
return merged
}
if (raw && typeof raw === 'object') {
return {
mustDeps: normalizeDep(raw.mustDeps || []),
shouldDeps: normalizeDep(raw.shouldDeps || []),
noDeps: normalizeDep(raw.noDeps || []),
}
}
return null
}
/**
* Normalizes a single deps declaration value to an array of BEM entity references.
* Input can be: string | object | array
*/
function normalizeDep(dep) {
if (!dep) return []
if (typeof dep === 'string') return [{ block: dep }]
if (Array.isArray(dep)) return dep.flatMap(d => normalizeDep(d))
if (typeof dep === 'object') {
// Could be { block: 'name' } or { elem: 'name' } or { mods: {...} } etc.
return [dep]
}
return []
}
/**
* Converts a BEM entity reference to a module name.
* { block: 'jquery', elem: 'event', mods: { type: 'pointer' } }
* → 'jquery__event_type_pointer'
*
* This is a simplified conversion — real BEM naming is more complex,
* but for the modules actually defined in bem-core, this covers all cases.
*/
function bemEntityToModuleName(entity, contextBlock) {
if (typeof entity === 'string') return entity
const block = entity.block || contextBlock
if (!block) return null
let name = block
// Handle elem / elems
if (entity.elem) {
name += '__' + entity.elem
}
// Handle mod / mods
if (entity.mod) {
name += '_' + entity.mod
if (entity.val && entity.val !== true) {
name += '_' + entity.val
}
}
if (entity.mods) {
for (const [mod, vals] of Object.entries(entity.mods)) {
if (Array.isArray(vals)) {
// Multiple values → multiple modules (e.g., type: ['dom', 'bem'])
// Return only the first for now; caller should handle arrays
name += '_' + mod
} else if (vals === true) {
name += '_' + mod
} else {
name += '_' + mod + '_' + vals
}
}
}
return name
}
/**
* Expands a BEM dependency entity into one or more module names.
* Handles elems (array) and mods (array values).
*/
function expandBemEntity(entity, contextBlock) {
if (typeof entity === 'string') return [entity]
const block = entity.block || contextBlock
if (!block) return []
const results = []
// If entity has elems (array of elements), expand each
if (entity.elems) {
const elems = Array.isArray(entity.elems) ? entity.elems : [entity.elems]
for (const elem of elems) {
if (typeof elem === 'string') {
results.push(block + '__' + elem)
} else if (elem && typeof elem === 'object') {
// { elem: 'init', mods: { auto: true } }
const elemName = elem.elem
results.push(block + '__' + elemName)
if (elem.mods) {
for (const [mod, vals] of Object.entries(elem.mods)) {
const modVals = Array.isArray(vals) ? vals : [vals]
for (const val of modVals) {
if (val === true) {
results.push(block + '__' + elemName + '_' + mod)
} else {
results.push(block + '__' + elemName + '_' + mod + '_' + val)
}
}
}
}
}
}
return results
}
// If entity has elem (single)
if (entity.elem) {
const base = block + '__' + entity.elem
if (entity.mods) {
for (const [mod, vals] of Object.entries(entity.mods)) {
const modVals = Array.isArray(vals) ? vals : [vals]
for (const val of modVals) {
if (val === true) {
results.push(base + '_' + mod)
} else {
results.push(base + '_' + mod + '_' + val)
}
}
}
return results
}
return [base]
}
// Block with mods
if (entity.mods) {
for (const [mod, vals] of Object.entries(entity.mods)) {
const modVals = Array.isArray(vals) ? vals : [vals]
for (const val of modVals) {
if (val === true) {
results.push(block + '_' + mod)
} else {
results.push(block + '_' + mod + '_' + val)
}
}
}
return results
}
// Simple block reference
return [block]
}
/**
* Build a complete module registry from BEM levels.
*
* @param {string[]} levels - ordered list of level directories (e.g., ['common.blocks', 'desktop.blocks'])
* @param {string} rootDir - project root directory
* @returns {{ modules: Map, deps: Map, redefinitions: Map }}
*/
function buildRegistry(levels, rootDir) {
// moduleName → [{ name, deps, filePath, suffix, levelDir, levelIndex }]
const allModules = new Map()
for (let i = 0; i < levels.length; i++) {
const levelDir = resolve(rootDir, levels[i])
const levelModules = scanLevel(levelDir)
for (const [name, entries] of levelModules) {
const existing = allModules.get(name) || []
for (const entry of entries) {
entry.levelIndex = i
}
existing.push(...entries)
allModules.set(name, existing)
}
}
// Detect cross-level redefinitions: same module name from different levels.
// First entry (lowest level index) is the base, subsequent entries are redefinitions.
const redefinitions = new Map()
for (const [name, entries] of allModules) {
entries.sort((a, b) => a.levelIndex - b.levelIndex)
if (entries.length > 1) {
redefinitions.set(name, entries)
}
}
// Collect deps.js files
const depsMap = new Map()
for (const [name, entries] of allModules) {
for (const entry of entries) {
// Find corresponding .deps.js file
// e.g., common.blocks/jquery/jquery.js → common.blocks/jquery/jquery.deps.js
const dir = dirname(entry.filePath)
const possibleDepsFiles = [
// Same directory, same base name
entry.filePath.replace(/\.(vanilla\.)?js$/, '.deps.js'),
]
// Also look for block-level deps.js
const blockDir = dirname(dir) === entry.levelDir ? dir : dirname(dir)
const blockName = basename(blockDir)
const blockDeps = join(blockDir, blockName + '.deps.js')
if (!possibleDepsFiles.includes(blockDeps)) {
possibleDepsFiles.push(blockDeps)
}
for (const depsFile of possibleDepsFiles) {
if (existsSync(depsFile) && !depsMap.has(depsFile)) {
const parsed = parseDepsFile(depsFile)
if (parsed) {
depsMap.set(depsFile, { ...parsed, forModule: name })
}
}
}
}
}
// Separate CSS entries from JS entries.
// CSS files are side-effect imports, not module redefinitions.
const cssModules = new Map()
for (const [name, entries] of allModules) {
const cssEntries = entries.filter(e => e.suffix === '.post.css')
const jsEntries = entries.filter(e => e.suffix !== '.post.css')
if (cssEntries.length > 0) {
cssModules.set(name, cssEntries)
}
if (jsEntries.length > 0) {
allModules.set(name, jsEntries)
} else if (cssEntries.length > 0) {
// CSS-only module — keep in allModules so it can be resolved
allModules.set(name, [])
}
}
// Recompute redefinitions after removing CSS entries
redefinitions.clear()
for (const [name, entries] of allModules) {
if (entries.length > 1) {
redefinitions.set(name, entries)
}
}
return { modules: allModules, deps: depsMap, redefinitions, cssModules }
}
/**
* Generate a barrel (re-export) module for a module with redefinitions.
*
* In ym, each redefinition receives the previous module value as its last
* callback parameter and calls provide() with a new/modified value.
*
* In ES modules, we model this as:
* - Base file: `export default value;`
* - Redefinition file: `export default function(prev) { return newValue; }`
*
* The barrel chains them:
* import _base from './base.js'
* import _redef0 from './redef0.js'
* import _redef1 from './redef1.js'
* let _module = _base
* _module = _redef0(_module)
* _module = _redef1(_module)
* export default _module
*
* @param {string} name - module name
* @param {object[]} entries - sorted array of file entries (base + redefinitions)
* @param {string} rootDir - project root
* @returns {string} generated ES module source code
*/
function generateBarrel(name, entries, rootDir) {
const lines = [`// @generated by vite-plugin-bem-levels`]
const base = entries[0]
const basePath = './' + relative(rootDir, base.filePath).replace(/\\/g, '/')
const baseId = safeIdentifier(name) + '_base'
lines.push(`import ${baseId} from '${basePath}';`)
// Import each redefinition as a named transformer
const redefIds = []
for (let i = 1; i < entries.length; i++) {
const redef = entries[i]
const redefPath = './' + relative(rootDir, redef.filePath).replace(/\\/g, '/')
const redefId = safeIdentifier(name) + '_redef' + (i - 1)
lines.push(`import ${redefId} from '${redefPath}';`)
redefIds.push(redefId)
}
lines.push('')
lines.push(`let _module = ${baseId};`)
for (const redefId of redefIds) {
lines.push(`_module = ${redefId}(_module);`)
}
lines.push(`export default _module;`)
return lines.join('\n')
}
/**
* Generate a safe JavaScript identifier from a BEM module name.
* 'jquery__config' → '_jquery__config'
* 'i-bem-dom' → '_iBemDom'
*/
function safeIdentifier(name) {
// Replace hyphens with camelCase, prefix with underscore
let id = name
.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
.replace(/-/g, '_')
// Ensure starts with valid identifier char
if (/^[0-9]/.test(id)) id = '_' + id
return '_' + id
}
/**
* Vite plugin for BEM level resolution and barrel file generation.
*
* @param {object} options
* @param {string} options.platform - 'desktop' or 'touch'
* @param {object} options.levels - platform → level directories mapping
* @param {string} [options.rootDir] - project root (default: process.cwd())
*/
export default function bemLevels(options = {}) {
const {
platform = 'desktop',
levels = {
common: ['common.blocks'],
desktop: ['common.blocks', 'desktop.blocks'],
touch: ['common.blocks', 'touch.blocks'],
},
rootDir = process.cwd(),
} = options
const platformLevels = levels[platform]
if (!platformLevels) {
throw new Error(`Unknown platform: ${platform}. Available: ${Object.keys(levels).join(', ')}`)
}
let registry = null
function getRegistry() {
if (!registry) {
registry = buildRegistry(platformLevels, rootDir)
}
return registry
}
return {
name: 'vite-plugin-bem-levels',
resolveId(id) {
if (id.startsWith(BEM_PREFIX)) {
return VIRTUAL_PREFIX + id.slice(BEM_PREFIX.length)
}
return null
},
load(id) {
if (!id.startsWith(VIRTUAL_PREFIX)) return null
const moduleName = id.slice(VIRTUAL_PREFIX.length)
const reg = getRegistry()
const entries = reg.modules.get(moduleName)
const cssEntries = reg.cssModules ? reg.cssModules.get(moduleName) : null
if ((!entries || entries.length === 0) && !cssEntries) {
this.error(`BEM module not found: ${moduleName}`)
return null
}
// Generate CSS side-effect imports
const cssImports = []
if (cssEntries) {
for (const cssEntry of cssEntries) {
const cssPath = './' + relative(rootDir, cssEntry.filePath).replace(/\\/g, '/')
cssImports.push(`import '${cssPath}';`)
}
}
// CSS-only module (no JS)
if (!entries || entries.length === 0) {
return cssImports.join('\n') + '\n'
}
// If the module has redefinitions, generate a barrel
if (entries.length > 1) {
const barrel = generateBarrel(moduleName, entries, rootDir)
if (cssImports.length > 0) {
return cssImports.join('\n') + '\n' + barrel
}
return barrel
}
// Single definition — re-export or side-effect import
const entry = entries[0]
const entryPath = './' + relative(rootDir, entry.filePath).replace(/\\/g, '/')
const source = readFileSync(entry.filePath, 'utf8')
const hasDefaultExport = /export\s+default\b/.test(source)
const jsCode = hasDefaultExport
? `export { default } from '${entryPath}';\n`
: `import '${entryPath}';\n`
if (cssImports.length > 0) {
return cssImports.join('\n') + '\n' + jsCode
}
return jsCode
},
// Invalidate registry on file changes in BEM levels
configureServer(server) {
const levelDirs = platformLevels.map(l => resolve(rootDir, l))
server.watcher.on('all', (event, filePath) => {
for (const levelDir of levelDirs) {
if (filePath.startsWith(levelDir)) {
registry = null; // invalidate cache
break
}
}
})
},
// Expose registry for testing and introspection
api: {
getRegistry() { return getRegistry(); },
scanLevel,
parseModulesDefine,
parseDepsFile,
normalizeDeps,
expandBemEntity,
generateBarrel,
buildRegistry,
},
}
}
// Named exports for testing
export {
scanLevel,
parseModulesDefine,
parseEsModule,
filePathToModuleName,
parseDepsFile,
normalizeDeps,
normalizeDep,
bemEntityToModuleName,
expandBemEntity,
buildRegistry,
generateBarrel,
safeIdentifier,
}
================================================
FILE: build/plugins/vite-plugin-bem-levels.test.js
================================================
import { describe, it } from 'node:test';
import { strict as assert } from 'node:assert';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import {
scanLevel,
parseModulesDefine,
parseDepsFile,
expandBemEntity,
buildRegistry,
generateBarrel,
safeIdentifier,
} from './vite-plugin-bem-levels.js';
const ROOT = resolve(import.meta.dirname, '../..');
// --- parseModulesDefine ---
describe('parseModulesDefine', function() {
it('parses simple define without deps', function() {
const result = parseModulesDefine(
"modules.define('cookie', function(provide) { provide({}); });"
);
assert.strictEqual(result.name, 'cookie');
assert.deepStrictEqual(result.deps, []);
assert.strictEqual(result.callbackParamCount, 1);
assert.strictEqual(result.isRedefinition, false);
});
it('parses define with dependency array', function() {
const result = parseModulesDefine(
"modules.define('i-bem', ['i-bem__internal', 'inherit'], function(provide, internal, inherit) {});"
);
assert.strictEqual(result.name, 'i-bem');
assert.deepStrictEqual(result.deps, ['i-bem__internal', 'inherit']);
assert.strictEqual(result.callbackParamCount, 3);
assert.strictEqual(result.isRedefinition, false);
});
it('parses define with double quotes', function() {
const result = parseModulesDefine(
'modules.define("jquery", ["loader_type_js"], function(provide, loader) {});'
);
assert.strictEqual(result.name, 'jquery');
assert.deepStrictEqual(result.deps, ['loader_type_js']);
assert.strictEqual(result.isRedefinition, false);
});
it('returns null for non-module files', function() {
const result = parseModulesDefine('var x = 1;');
assert.strictEqual(result, null);
});
it('handles multiline define', function() {
const result = parseModulesDefine(`
modules.define(
'i-bem-dom',
[
'i-bem', 'i-bem__internal', 'inherit',
'identify', 'objects', 'functions',
'jquery', 'dom'
],
function(provide, BEM, BEMINTERNAL, inherit, identify, objects, functions, $, dom) {
});
`);
assert.strictEqual(result.name, 'i-bem-dom');
assert.deepStrictEqual(result.deps, [
'i-bem', 'i-bem__internal', 'inherit',
'identify', 'objects', 'functions',
'jquery', 'dom',
]);
assert.strictEqual(result.callbackParamCount, 9);
assert.strictEqual(result.isRedefinition, false);
});
});
// --- parseModulesDefine: redefinition detection ---
describe('parseModulesDefine (redefinition detection)', function() {
it('detects redefinition: jquery__config on desktop (2 deps + prev)', function() {
const result = parseModulesDefine(
"modules.define('jquery__config', ['ua', 'objects'], function(provide, ua, objects, base) {});"
);
assert.strictEqual(result.name, 'jquery__config');
assert.deepStrictEqual(result.deps, ['ua', 'objects']);
assert.strictEqual(result.callbackParamCount, 4);
assert.strictEqual(result.isRedefinition, true);
});
it('detects redefinition: jquery extension (1 dep + prev)', function() {
const result = parseModulesDefine(
"modules.define('jquery', ['next-tick'], function(provide, nextTick, $) {});"
);
assert.strictEqual(result.isRedefinition, true);
assert.strictEqual(result.callbackParamCount, 3);
});
it('detects redefinition: jquery extension (0 deps + prev)', function() {
const result = parseModulesDefine(
"modules.define('jquery', function(provide, $) {});"
);
assert.strictEqual(result.isRedefinition, true);
assert.strictEqual(result.callbackParamCount, 2);
});
it('detects redefinition: events__observable type_bem-dom (1 dep + prev)', function() {
const result = parseModulesDefine(
"modules.define('events__observable', ['i-bem-dom'], function(provide, bemDom, observable) {});"
);
assert.strictEqual(result.isRedefinition, true);
assert.strictEqual(result.callbackParamCount, 3);
});
it('detects redefinition: ua__dom on touch (1 dep + prev)', function() {
const result = parseModulesDefine(
"modules.define('ua', ['i-bem-dom'], function(provide, bemDom, ua) {});"
);
assert.strictEqual(result.isRedefinition, true);
});
it('does NOT detect base as redefinition: cookie (0 deps, 1 param)', function() {
const result = parseModulesDefine(
"modules.define('cookie', function(provide) { provide({}); });"
);
assert.strictEqual(result.isRedefinition, false);
});
it('does NOT detect base as redefinition: events (3 deps, 4 params)', function() {
const result = parseModulesDefine(
"modules.define('events', ['identify', 'inherit', 'functions'], function(provide, identify, inherit, functions) {});"
);
assert.strictEqual(result.isRedefinition, false);
assert.strictEqual(result.callbackParamCount, 4);
});
});
// --- parseModulesDefine: real file detection ---
describe('parseModulesDefine (real file cross-check)', function() {
it('real file: jquery base is now ESM (migrated)', function() {
const source = readFileSync(resolve(ROOT, 'common.blocks/jquery/jquery.js'), 'utf8');
const result = parseModulesDefine(source);
assert.strictEqual(result, null, 'migrated ESM file has no modules.define');
assert.ok(source.includes('export default'), 'should have export default');
});
it('real file: jquery__config base is now ESM (migrated)', function() {
const source = readFileSync(
resolve(ROOT, 'common.blocks/jquery/__config/jquery__config.js'), 'utf8'
);
const result = parseModulesDefine(source);
assert.strictEqual(result, null, 'migrated ESM file has no modules.define');
assert.ok(source.includes('export default'), 'should have export default');
});
it('real file: jquery__config desktop is now ESM (migrated)', function() {
const source = readFileSync(
resolve(ROOT, 'desktop.blocks/jquery/__config/jquery__config.js'), 'utf8'
);
const result = parseModulesDefine(source);
assert.strictEqual(result, null, 'migrated ESM file has no modules.define');
assert.ok(source.includes('export default'), 'should have export default');
});
it('real file: events__observable base is now ESM (migrated)', function() {
const source = readFileSync(
resolve(ROOT, 'common.blocks/events/__observable/events__observable.js'), 'utf8'
);
const result = parseModulesDefine(source);
assert.strictEqual(result, null, 'migrated ESM file has no modules.define');
assert.ok(source.includes('export default'), 'should have export default');
});
it('real file: events__observable type_bem-dom is now ESM (migrated)', function() {
const source = readFileSync(
resolve(ROOT, 'common.blocks/events/__observable/_type/events__observable_type_bem-dom.js'), 'utf8'
);
const result = parseModulesDefine(source);
assert.strictEqual(result, null, 'migrated ESM file has no modules.define');
assert.ok(source.includes('export default'), 'should have export default');
});
it('real file: ua touch base is now ESM (migrated)', function() {
const source = readFileSync(
resolve(ROOT, 'touch.blocks/ua/ua.js'), 'utf8'
);
const result = parseModulesDefine(source);
assert.strictEqual(result, null, 'migrated ESM file has no modules.define');
assert.ok(source.includes('export default'), 'should have export default');
});
it('real file: ua__dom touch is now ESM (migrated)', function() {
const source = readFileSync(
resolve(ROOT, 'touch.blocks/ua/__dom/ua__dom.js'), 'utf8'
);
const result = parseModulesDefine(source);
assert.strictEqual(result, null, 'migrated ESM file has no modules.define');
assert.ok(source.includes('export default'), 'should have export default');
});
it('real file: desktop winresize is now ESM (migrated)', function() {
const source = readFileSync(
resolve(ROOT, 'desktop.blocks/jquery/__event/_type/jquery__event_type_winresize.js'), 'utf8'
);
const result = parseModulesDefine(source);
assert.strictEqual(result, null, 'migrated ESM file has no modules.define');
assert.ok(source.includes('export default'), 'should have export default');
});
});
// --- scanLevel ---
describe('scanLevel', function() {
it('scans common.blocks and finds modules', function() {
const modules = scanLevel(resolve(ROOT, 'common.blocks'));
assert.ok(modules.size > 0, 'Should find modules');
assert.ok(modules.has('jquery'), 'Should find jquery');
assert.ok(modules.has('i-bem'), 'Should find i-bem');
assert.ok(modules.has('i-bem-dom'), 'Should find i-bem-dom');
assert.ok(modules.has('events'), 'Should find events');
assert.ok(modules.has('cookie'), 'Should find cookie');
assert.ok(modules.has('objects'), 'Should find objects');
assert.ok(modules.has('inherit'), 'Should find inherit');
});
it('finds correct file suffixes', function() {
const modules = scanLevel(resolve(ROOT, 'common.blocks'));
const objects = modules.get('objects');
assert.ok(objects, 'objects should exist');
assert.strictEqual(objects[0].suffix, '.vanilla.js');
const cookie = modules.get('cookie');
assert.ok(cookie, 'cookie should exist');
assert.strictEqual(cookie[0].suffix, '.js');
});
it('scans desktop.blocks', function() {
const modules = scanLevel(resolve(ROOT, 'desktop.blocks'));
assert.ok(modules.has('ua'), 'Should find ua on desktop');
assert.ok(modules.has('jquery__config'), 'Should find jquery__config on desktop');
});
it('scans touch.blocks', function() {
const modules = scanLevel(resolve(ROOT, 'touch.blocks'));
assert.ok(modules.has('ua'), 'Should find ua on touch');
});
it('returns empty map for non-existent directory', function() {
const modules = scanLevel(resolve(ROOT, 'nonexistent.blocks'));
assert.strictEqual(modules.size, 0);
});
it('derives module name from filename, not file content', function() {
const modules = scanLevel(resolve(ROOT, 'common.blocks'));
assert.ok(modules.has('events__observable_type_bem-dom'),
'Should derive module name from filename');
const jqEntries = modules.get('jquery');
assert.ok(jqEntries, 'jquery base should still exist');
assert.strictEqual(jqEntries.length, 1,
'jquery should have exactly 1 entry (base only, no within-level redefinitions)');
});
it('finds new modules from BEM naming', function() {
const modules = scanLevel(resolve(ROOT, 'common.blocks'));
assert.ok(modules.has('events__observable_type_bem-dom'));
assert.ok(modules.has('tick_start_auto'));
assert.ok(modules.has('idle_start_auto'));
assert.ok(modules.has('i-bem-dom__init_auto'));
});
});
// --- buildRegistry ---
describe('buildRegistry', function() {
it('builds registry for desktop platform', function() {
const reg = buildRegistry(['common.blocks', 'desktop.blocks'], ROOT);
assert.ok(reg.modules.size > 0);
assert.ok(reg.modules.has('jquery'));
assert.ok(reg.modules.has('ua'));
assert.ok(reg.modules.has('i-bem-dom'));
});
it('builds registry for touch platform', function() {
const reg = buildRegistry(['common.blocks', 'touch.blocks'], ROOT);
assert.ok(reg.modules.has('ua'));
const uaEntries = reg.modules.get('ua');
assert.strictEqual(uaEntries.length, 1, 'ua has single touch definition');
assert.ok(reg.modules.has('ua__dom'), 'ua__dom is a separate module');
});
it('jquery has single entry per level (BEM naming)', function() {
const reg = buildRegistry(['common.blocks', 'desktop.blocks'], ROOT);
const jquery = reg.modules.get('jquery');
assert.ok(jquery, 'jquery should exist');
assert.strictEqual(jquery.length, 1,
'jquery should have exactly 1 entry (only common.blocks/jquery/jquery.js)');
});
it('detects jquery__config cross-level redefinition on desktop', function() {
const reg = buildRegistry(['common.blocks', 'desktop.blocks'], ROOT);
assert.ok(reg.redefinitions.has('jquery__config'), 'jquery__config should have redefinitions');
const entries = reg.redefinitions.get('jquery__config');
assert.strictEqual(entries.length, 2);
assert.ok(entries[0].filePath.includes('common.blocks'));
assert.ok(entries[1].filePath.includes('desktop.blocks'));
});
it('events__observable has no cross-level redefinition (type_bem-dom is separate module)', function() {
const reg = buildRegistry(['common.blocks', 'desktop.blocks'], ROOT);
assert.ok(!reg.redefinitions.has('events__observable'),
'events__observable should NOT have redefinitions — type_bem-dom is a separate module');
assert.ok(reg.modules.has('events__observable_type_bem-dom'),
'events__observable_type_bem-dom should be its own module');
});
it('ua has single entry per platform (no common.blocks/ua/ua.js)', function() {
const regDesktop = buildRegistry(['common.blocks', 'desktop.blocks'], ROOT);
const uaDesktop = regDesktop.modules.get('ua');
assert.ok(uaDesktop, 'ua should exist on desktop');
assert.strictEqual(uaDesktop.length, 1, 'desktop ua has 1 entry');
const regTouch = buildRegistry(['common.blocks', 'touch.blocks'], ROOT);
const uaTouch = regTouch.modules.get('ua');
assert.ok(uaTouch, 'ua should exist on touch');
assert.strictEqual(uaTouch.length, 1, 'touch ua has 1 entry');
assert.ok(regTouch.modules.has('ua__dom'),
'ua__dom should be its own module on touch');
});
it('detects i-bem-dom__init definition', function() {
const reg = buildRegistry(['common.blocks', 'desktop.blocks'], ROOT);
const init = reg.modules.get('i-bem-dom__init');
assert.ok(init, 'i-bem-dom__init should exist');
assert.ok(init.length >= 1, 'i-bem-dom__init should have at least base definition');
});
it('finds all expected modules (complete inventory, BEM naming)', function() {
const reg = buildRegistry(['common.blocks', 'desktop.blocks'], ROOT);
const expectedModules = [
// common.blocks .vanilla.js
'objects', 'identify', 'functions', 'inherit', 'i-bem',
'tick', 'tick_start_auto', 'uri', 'next-tick',
'events', 'functions__throttle', 'functions__debounce',
'i-bem__internal', 'uri__querystring', 'strings__escape',
'events__channels',
// common.blocks .js
'cookie', 'dom', 'jquery', 'idle', 'idle_start_auto',
'keyboard__codes', 'loader_type_js', 'loader_type_bundle',
'events__observable', 'events__observable_type_bem-dom',
'i-bem__collection', 'i-bem-dom', 'i-bem-dom__collection',
'i-bem-dom__init', 'i-bem-dom__init_auto',
'i-bem-dom__events', 'i-bem-dom__events_type_bem',
'i-bem-dom__events_type_dom', 'jquery__config',
// desktop.blocks .js
'ua', 'jquery__event_type_winresize',
];
const missing = expectedModules.filter(m => !reg.modules.has(m));
assert.deepStrictEqual(missing, [],
`Missing modules: ${missing.join(', ')}`);
});
});
// --- parseDepsFile ---
describe('parseDepsFile', function() {
it('parses simple shouldDeps', function() {
const result = parseDepsFile(resolve(ROOT, 'common.blocks/dom/dom.deps.js'));
assert.ok(result);
assert.ok(result.shouldDeps.length > 0);
});
it('parses mustDeps', function() {
const result = parseDepsFile(
resolve(ROOT, 'touch.blocks/ua/__dom/ua__dom.deps.js')
);
assert.ok(result);
assert.ok(result.mustDeps.length > 0);
});
it('parses complex deps with elem and mods', function() {
const result = parseDepsFile(resolve(ROOT, 'common.blocks/i-bem-dom/i-bem-dom.deps.js'));
assert.ok(result);
assert.ok(result.shouldDeps.length > 0);
});
it('returns null for non-existent file', function() {
const result = parseDepsFile(resolve(ROOT, 'nonexistent.deps.js'));
assert.strictEqual(result, null);
});
});
// --- expandBemEntity ---
describe('expandBemEntity', function() {
it('expands string to itself', function() {
assert.deepStrictEqual(expandBemEntity('jquery'), ['jquery']);
});
it('expands block with elem', function() {
assert.deepStrictEqual(
expandBemEntity({ block: 'i-bem', elem: 'internal' }),
['i-bem__internal']
);
});
it('expands block with elems array', function() {
const result = expandBemEntity({ block: 'i-bem', elems: ['internal', 'collection'] });
assert.deepStrictEqual(result, ['i-bem__internal', 'i-bem__collection']);
});
it('expands elem with mods', function() {
const result = expandBemEntity({
block: 'i-bem-dom',
elem: 'events',
mods: { type: ['dom', 'bem'] },
});
assert.deepStrictEqual(result, [
'i-bem-dom__events_type_dom',
'i-bem-dom__events_type_bem',
]);
});
it('expands block with bool mod', function() {
const result = expandBemEntity({ block: 'i-bem-dom', elem: 'init', mods: { auto: true } });
assert.deepStrictEqual(result, ['i-bem-dom__init_auto']);
});
it('expands block-level mods', function() {
const result = expandBemEntity({ block: 'loader', mods: { type: 'js' } });
assert.deepStrictEqual(result, ['loader_type_js']);
});
it('expands elems with nested mods', function() {
const result = expandBemEntity({
block: 'i-bem-dom',
elems: { elem: 'init', mods: { auto: true } },
});
assert.deepStrictEqual(result, ['i-bem-dom__init', 'i-bem-dom__init_auto']);
});
});
// --- generateBarrel ---
describe('generateBarrel', function() {
it('generates chained barrel for module with redefinitions', function() {
const entries = [
{ filePath: resolve(ROOT, 'common.blocks/jquery/__config/jquery__config.js'), isRedefinition: false },
{ filePath: resolve(ROOT, 'desktop.blocks/jquery/__config/jquery__config.js'), isRedefinition: true },
];
const barrel = generateBarrel('jquery__config', entries, ROOT);
assert.ok(barrel.includes('@generated'));
assert.ok(barrel.includes("import _jquery__config_base from './common.blocks/jquery/__config/jquery__config.js'"));
assert.ok(barrel.includes("import _jquery__config_redef0 from './desktop.blocks/jquery/__config/jquery__config.js'"));
assert.ok(barrel.includes('let _module = _jquery__config_base;'));
assert.ok(barrel.includes('_module = _jquery__config_redef0(_module);'));
assert.ok(barrel.includes('export default _module;'));
});
it('generates barrel for jquery__config with desktop redefinition', function() {
const entries = [
{ filePath: resolve(ROOT, 'common.blocks/jquery/__config/jquery__config.js'), isRedefinition: false },
{ filePath: resolve(ROOT, 'desktop.blocks/jquery/__config/jquery__config.js'), isRedefinition: true },
];
const barrel = generateBarrel('jquery__config', entries, ROOT);
assert.ok(barrel.includes("import _jquery__config_base from './common.blocks/jquery/__config/jquery__config.js'"));
assert.ok(barrel.includes("import _jquery__config_redef0 from './desktop.blocks/jquery/__config/jquery__config.js'"));
assert.ok(barrel.includes('let _module = _jquery__config_base;'));
assert.ok(barrel.includes('_module = _jquery__config_redef0(_module);'));
assert.ok(barrel.includes('export default _module;'));
});
it('generates barrel for events__observable with type redefinition', function() {
const entries = [
{ filePath: resolve(ROOT, 'common.blocks/events/__observable/events__observable.js'), isRedefinition: false },
{ filePath: resolve(ROOT, 'common.blocks/events/__observable/_type/events__observable_type_bem-dom.js'), isRedefinition: true },
];
const barrel = generateBarrel('events__observable', entries, ROOT);
assert.ok(barrel.includes('_events__observable_base'));
assert.ok(barrel.includes('_events__observable_redef0'));
assert.ok(barrel.includes('_module = _events__observable_redef0(_module);'));
});
it('barrel does NOT use side-effect imports', function() {
const entries = [
{ filePath: resolve(ROOT, 'common.blocks/jquery/__config/jquery__config.js'), isRedefinition: false },
{ filePath: resolve(ROOT, 'desktop.blocks/jquery/__config/jquery__config.js'), isRedefinition: true },
];
const barrel = generateBarrel('jquery__config', entries, ROOT);
const sideEffectImport = /^import\s+'/m;
assert.ok(!sideEffectImport.test(barrel),
'barrel should not contain side-effect imports');
});
it('barrel with single redefinition produces correct chain', function() {
const entries = [
{ filePath: '/root/common.blocks/foo/foo.js', isRedefinition: false },
{ filePath: '/root/desktop.blocks/foo/foo.js', isRedefinition: true },
];
const barrel = generateBarrel('foo', entries, '/root');
const expectedLines = [
"// @generated by vite-plugin-bem-levels",
"import _foo_base from './common.blocks/foo/foo.js';",
"import _foo_redef0 from './desktop.blocks/foo/foo.js';",
"",
"let _module = _foo_base;",
"_module = _foo_redef0(_module);",
"export default _module;",
];
assert.strictEqual(barrel, expectedLines.join('\n'));
});
it('barrel with 4 redefinitions produces full chain', function() {
const entries = [
{ filePath: '/root/a/m.js' },
{ filePath: '/root/b/m.js' },
{ filePath: '/root/c/m.js' },
{ filePath: '/root/d/m.js' },
{ filePath: '/root/e/m.js' },
];
const barrel = generateBarrel('m', entries, '/root');
assert.ok(barrel.includes('_m_redef0'));
assert.ok(barrel.includes('_m_redef1'));
assert.ok(barrel.includes('_m_redef2'));
assert.ok(barrel.includes('_m_redef3'));
assert.strictEqual((barrel.match(/_module = _m_redef/g) || []).length, 4);
});
});
// --- safeIdentifier ---
describe('safeIdentifier', function() {
it('converts simple name', function() {
assert.strictEqual(safeIdentifier('jquery'), '_jquery');
});
it('converts hyphenated name', function() {
assert.strictEqual(safeIdentifier('i-bem-dom'), '_iBemDom');
});
it('converts name with double underscore', function() {
assert.strictEqual(safeIdentifier('jquery__config'), '_jquery__config');
});
});
================================================
FILE: build/vite.config.js
================================================
import { defineConfig } from 'vite';
import { resolve } from 'node:path';
import bemLevels from './plugins/vite-plugin-bem-levels.js';
const rootDir = resolve(import.meta.dirname, '..');
export default defineConfig(({ mode }) => {
const platform = process.env.BEM_PLATFORM || 'desktop';
return {
root: rootDir,
plugins: [
bemLevels({
platform,
levels: {
common: ['common.blocks'],
desktop: ['common.blocks', 'desktop.blocks'],
touch: ['common.blocks', 'touch.blocks'],
},
rootDir,
}),
],
build: {
lib: {
entry: resolve(import.meta.dirname, 'platforms', `${platform}.js`),
name: 'bemCore',
formats: ['es', 'umd'],
fileName: (format) => `bem-core.${format === 'es' ? 'mjs' : 'js'}`,
},
outDir: resolve(rootDir, 'dist', platform),
emptyOutDir: true,
sourcemap: true,
minify: mode === 'production',
rolldownOptions: {
external: ['jquery'],
output: {
globals: {
jquery: 'jQuery',
},
},
},
},
server: {
open: false,
},
};
});
================================================
FILE: build/vite.test.config.js
================================================
import { defineConfig } from 'vite';
import { resolve } from 'node:path';
import bemLevels from './plugins/vite-plugin-bem-levels.js';
const rootDir = resolve(import.meta.dirname, '..');
export default defineConfig({
root: rootDir,
plugins: [
bemLevels({
// Use desktop platform for browser tests
platform: 'desktop',
levels: {
common: ['common.blocks'],
desktop: ['common.blocks', 'desktop.blocks'],
touch: ['common.blocks', 'touch.blocks'],
},
rootDir,
}),
],
// NOTE: jQuery is NOT external here — it must be bundled into the test page.
// (In the production build it's a peerDependency / external.)
resolve: {
alias: {},
},
server: {
port: 5174,
open: false,
},
// Polyfill Node.js globals used by mocha's browser-entry.js
define: {
'process.env.NODE_ENV': JSON.stringify('test'),
'process.env': JSON.stringify({ NODE_ENV: 'test' }),
'process.stdout': 'null',
'process.version': JSON.stringify('v22.0.0'),
global: 'globalThis',
},
optimizeDeps: {
include: ['chai', 'sinon', 'sinon-chai', 'jquery'],
},
});
================================================
FILE: common.blocks/clearfix/clearfix.css
================================================
.clearfix:after
{
display: table;
clear: both;
content: '';
}
================================================
FILE: common.blocks/clearfix/clearfix.en.md
================================================
# clearfix
This block provides a CSS class that implements the **clearfix** layout hack, also known as the [Easy Clearing Method](http://www.456bereastreet.com/archive/200603/new_clearing_method_needed_for_ie7/). The hack allows you to clear wrapping for elements with the CSS `float` property, without making any changes to the document's original HTML structure.
This block can be used as a container for elements with the `float` property, or mixed with such a container.
Example when used as a container:
```bemjson
[{
block : 'header',
attrs : { style : 'border : 2px solid blue;' },
content : 'Top element'
},
{
block : 'clearfix',
attrs : { style : 'border : 2px dotted yellow;' },
content : [
{
block : 'float',
attrs : { style : 'float : left; border : 1px solid green;' },
content : 'Floating item 1'
},
{
block : 'float',
attrs : { style : 'float : left; border : 1px solid green;' },
content : 'Floating item 2'
}]
},
{
block : 'footer',
attrs : { style : 'border : 2px solid red' },
content : 'Footer'
}]
```
Mixed with a container block:
```bemjson
[{
block : 'header',
attrs : { style : 'border : 2px solid blue;' },
content : 'Top element'
},
{
block : 'some-container',
mix : [{ block : 'clearfix' }],
attrs : { style : 'border : 2px dotted yellow;' },
content : [
{
block : 'float',
attrs : { style : 'float : left; border : 1px solid green;' },
content : 'Floating item 1'
},
{
block : 'float',
attrs : { style : 'float : left; border : 1px solid green;' },
content : 'Floating item 2'
}]
},
{
block : 'footer',
attrs : { style : 'border : 2px solid red' },
content : 'Footer'
}]
```
## Public block technologies
The block is implemented in:
* `css`.
================================================
FILE: common.blocks/clearfix/clearfix.en.title.txt
================================================
Block with floats inside
================================================
FILE: common.blocks/clearfix/clearfix.ru.md
================================================
# clearfix
Блок предоставляет СSS-класс, реализующий прием верстки **clearfix**, также известный как [Easy Clearing Hack](http://www.456bereastreet.com/archive/200603/new_clearing_method_needed_for_ie7/). Прием позволяет отменить обтекание для элементов с CSS-свойством `float`, без внесения изменений в исходную HTML-структуру документа.
Блок можно использовать в качестве контейнера для элементов со свойством `float`, или примешивая его к такому контейнеру.
Пример использования в качестве контейнера:
```bemjson
[{
block : 'header',
attrs : { style : 'border : 2px solid blue;' },
content : 'Top element'
},
{
block : 'clearfix',
attrs : { style : 'border : 2px dotted yellow;' },
content : [
{
block : 'float',
attrs : { style : 'float : left; border : 1px solid green;' },
content : 'Floating item 1'
},
{
block : 'float',
attrs : { style : 'float : left; border : 1px solid green;' },
content : 'Floating item 2'
}]
},
{
block : 'footer',
attrs : { style : 'border : 2px solid red' },
content : 'Footer'
}]
```
Примешивание к блоку-контейнеру:
```bemjson
[{
block : 'header',
attrs : { style : 'border : 2px solid blue;' },
content : 'Top element'
},
{
block : 'some-container',
mix : [{ block : 'clearfix' }],
attrs : { style : 'border : 2px dotted yellow;' },
content : [
{
block : 'float',
attrs : { style : 'float : left; border : 1px solid green;' },
content : 'Floating item 1'
},
{
block : 'float',
attrs : { style : 'float : left; border : 1px solid green;' },
content : 'Floating item 2'
}]
},
{
block : 'footer',
attrs : { style : 'border : 2px solid red' },
content : 'Footer'
}]
```
## Публичные технологии блока
Блок реализован в технологиях:
* `css`.
================================================
FILE: common.blocks/clearfix/clearfix.ru.title.txt
================================================
Блок с float элементами внутри
================================================
FILE: common.blocks/cookie/cookie.en.md
================================================
# cookie
This block provides an object with a set of methods for working with browser cookies (the JS `document.cookie` property).
## Overview
### Object properties and methods
| Name | Return type | Description |
| -------- | --- | -------- |
| get (`name`) | `String` | `null` | Gets the value stored in a browser cookie. |
| set (`name`, `val`, `[options]`) | `String` | Sets the cookie with the specified name.|
### Public block technologies
The block is implemented in:
* `js`
## Description
### Object properties and methods
#### `get` method
Use this method to get the value stored in a cookie for the name passed in the argument.
**Accepted arguments:**
| Argument | Type | Description |
| ------- | --- | -------- |
| `name`* | `String` | The name of the cookie. |
* Required argument.
**Returns:**
* `String` — If a cookie with the specified name was set. The value is automatically decoded using [decodeURIComponent](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent).
* `null` — If a cookie with the specified name doesn't exist.
Example:
```js
modules.require('cookie', function(cookie) {
cookie.set('mycookie', 'foobar');
console.log(cookie.get('mycookie')); // 'foobar'
console.log(cookie.get('foo')); // null
});
```
#### `set` method
Use this method to set the cookie with the specified name. In addition to the name and value, you can pass the method a hash with additional cookie parameters.
**Accepted arguments:**
| Argument | Type | Description |
| ------- | --- | -------- |
| `name`* | `String` | The name of the cookie. |
| `val`* | `String` | `null` | The value of the cookie. If the value is set to `null`, the cookie is deleted.|
| [`options`] | `Object` | Options. Object properties • `expires` (`Number`) – The cookie's time to live, in days. If the value is negative, the cookie is deleted. Alternatively, you can pass a generated date object (`new Date()`) for the value. • `path` (`String`) – The path from the domain root where the cookie will be available. • `domain` (`String`) – The domain. By default, this is the current domain. • `secure` (`Boolean`) – Flag indicating that an encrypted SSL connection must be used with the cookie. By default, it is `false`. |
* Required argument.
**Returns:** the `this` object.
Example:
```js
modules.require('cookie', function(cookie) {
cookie.set('mycookie', 'foobar', {
expires : 1, // lifetime is one day
path : '/', // available for all pages secure
secure : true // only send the cookie over SSL
});
console.log(cookie.get('mycookie')); // 'foobar'
cookie.set('mycookie', null); // deleting the cookie
console.log(cookie.get('mycookie')); // null
});
```
================================================
FILE: common.blocks/cookie/cookie.js
================================================
/**
* @module cookie
* @description Inspired from $.cookie plugin by Klaus Hartl (stilbuero.de)
*/
export default {
/**
* Returns cookie by given name
* @param {String} name
* @returns {String|null}
*/
get(name) {
let res = null
if(document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';')
for(let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim()
// Does this cookie string begin with the name we want?
if(cookie.substring(0, name.length + 1) === (name + '=')) {
res = decodeURIComponent(cookie.substring(name.length + 1))
break
}
}
}
return res
},
/**
* Sets cookie by given name
* @param {String} name
* @param {String} val
* @param {Object} options
* @returns {cookie} this
*/
set(name, val, options) {
options = options || {}
if(val === null) {
val = ''
options.expires = -1
}
let expires = ''
if(options.expires && (typeof options.expires === 'number' || options.expires.toUTCString)) {
let date
if(typeof options.expires === 'number') {
date = new Date()
date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000))
} else {
date = options.expires
}
expires = '; expires=' + date.toUTCString() // use expires attribute, max-age is not supported by IE
}
// CAUTION: Needed to parenthesize options.path and options.domain
// in the following expressions, otherwise they evaluate to undefined
// in the packed version for some reason...
const path = options.path? '; path=' + (options.path) : '',
domain = options.domain? '; domain=' + (options.domain) : '',
secure = options.secure? '; secure' : ''
document.cookie = [name, '=', encodeURIComponent(val), expires, path, domain, secure].join('')
return this
}
}
================================================
FILE: common.blocks/cookie/cookie.ru.md
================================================
# cookie
Блок предоставляет объект, содержащий набор методов для работы с cookie браузера (JS-свойство `document.cookie`).
## Обзор
### Свойства и методы объекта
| Имя | Тип возвращаемого значения | Описание |
| -------- | --- | -------- |
| get (`name`) | `String` | `null` | Служит для получения значения, хранящегося в cookie браузера. |
| set (`name`, `val`, `[options]`) | `String` | Cлужит для записи cookie с заданным именем.|
### Публичные технологии блока
Блок реализован в технологиях:
* `js`
## Описание
### Свойства и методы объекта
#### Метод `get`
Метод служит для получения значения, хранящегося в cookie, для имени переданного аргументом.
**Принимаемые аргументы:**
| Аргумент | Тип | Описание |
| ------- | --- | -------- |
| `name`* | `String` | Имя cookie. |
* Обязательный аргумент.
**Возвращает:**
* `String` — если cookie с заданным именем было установлено. Значение автоматически декодируется с помощью [decodeURIComponent](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent).
* `null` — если cookie с заданным именем отсутствует.
Пример:
```js
modules.require('cookie', function(cookie) {
cookie.set('mycookie', 'foobar');
console.log(cookie.get('mycookie')); // 'foobar'
console.log(cookie.get('foo')); // null
});
```
#### Метод `set`
Метод служит для записи cookie с заданным именем. Помимо имени и значения, методу можно передать хеш с дополнительными параметрами cookie.
**Принимаемые аргументы:**
| Аргумент | Тип | Описание |
| ------- | --- | -------- |
| `name`* | `String` | Имя cookie. |
| `val`* | `String` | `null` | Значение cookie. При установке в качестве значения `null` cookie удаляется.|
| [`options`] | `Object` | Опции. Свойства объекта: • `expires` (`Number`) – срок жизни cookie в сутках. При отрицательном значении cookie будет удалено. Альтернативно, можно передать в качестве значения сформированный объект даты (`new Date()`). • `path` (`String`) – путь от корня домена внутри которого будет доступно cookie. • `domain` (`String`) – домен. По умолчанию текущий домен. • `secure` (`Boolean`) – флаг, указывающий на необходимость использования с cookie шифрованного соединения SSL. По умолчанию `false`. |
* Обязательный аргумент.
**Возвращает:** объект `this`.
Пример:
```js
modules.require('cookie', function(cookie) {
cookie.set('mycookie', 'foobar', {
expires : 1, // срок жизни одни сутки
path : '/', // доступно для всех страниц
secure : true // передавать cookie только по SSL
});
console.log(cookie.get('mycookie')); // 'foobar'
cookie.set('mycookie', null); // удаляем cookie
console.log(cookie.get('mycookie')); // null
});
```
================================================
FILE: common.blocks/cookie/cookie.spec.js
================================================
modules.define('spec', [
'cookie',
'chai'
], function(provide,
cookie,
chai
) {
var expect = chai.expect;
describe('cookie', function() {
describe('get', function() {
it('should return value of defined cookie', function() {
document.cookie = 'name1=val1';
document.cookie = 'name2=val2';
cookie.get('name1').should.be.equal('val1');
cookie.get('name2').should.be.equal('val2');
});
it('should return null if cookie is undefined', function() {
expect(cookie.get('name3')).to.be.null;
});
});
describe('set', function() {
it('should properly set cookie', function() {
cookie.set('name', 'val');
cookie.get('name').should.be.equal('val');
});
it('should properly remove cookie', function() {
cookie.set('name', 'val');
cookie.set('name', null);
expect(cookie.get('name')).to.be.null;
});
});
});
provide();
});
================================================
FILE: common.blocks/dom/dom.deps.js
================================================
({
shouldDeps : 'jquery'
})
================================================
FILE: common.blocks/dom/dom.en.md
================================================
# dom
This block provides an object with a set of methods for working with the DOM tree.
## Overview
### Object properties and methods
| Name | Return type | Description |
| -------- | --- | -------- |
| contains ( `ctx {jQuery}`, `domElem {jQuery}`) | `Boolean` | Checks whether a DOM element contains another DOM element. |
| getFocused ( `domElem {jQuery} `) | `jQuery` | Gets a reference to the DOM element that is in focus. |
| containsFocus ( `domElem {jQuery} `) | `Boolean` | Checks whether a DOM element or its descendants contains the focus. |
| isFocusable ( `domElem {jQuery} `) | `Boolean` | Checks whether the DOM element is in focus. |
| isEditable ( `domElem {jQuery}`) | `Boolean` | Checks whether text can be entered in the DOM element. |
### Public block technologies
The block is implemented in:
* `js`
## Description
### Object properties and methods
#### `contains` method
Use this method to check whether a `ctx` DOM element contains `domElem`.
**Accepted arguments:**
* `ctx {jQuery}` – The DOM element to search inside. Required argument.
* `domElem {jQuery}` – The DOM element to search for. Required argument.
**Return value:** `Boolean`. If found, then `true`.
Example:
```js
modules.require(['dom', 'jquery'], function(dom, $) {
/*
*/
dom.contains($('.block1'), $('.block2')); // true
});
```
#### `getFocused` method
Gets a reference to the DOM element that is in focus.
Doesn't accept arguments.
**Return value:** `jQuery` – The object in focus.
Example:
```js
modules.require(['dom'], function(dom) {
dom.getFocused(); // a reference to the element in focus
});
```
#### `containsFocus` method
This method checks whether the focus is on the DOM element passed in the argument or one of its descendants.
**Accepted arguments:**
* `domElem {jQuery}` – The DOM element to check. Required argument.
**Return value:** `Boolean`. If this element is in focus, then `true`.
Example:
```js
modules.require(['dom', 'jquery'], function(dom, $) {
/*
*/
$('.block1__control').focus();
dom.containsFocus($('.block1')); // true
});
```
#### `isFocusable` method
This method checks whether the user's browser can set the focus on the DOM element passed in the argument.
**Accepted arguments:**
* `domElem {jQuery}` – The DOM element to check. Required argument. If there are mutiple DOM elements in the jQuery chain, the first one is checked.
**Return value:** `Boolean`. If the focus can be set on this element, then `true`.
Example:
```js
modules.require(['dom', 'jquery'], function(dom, $) {
/*
*/
dom.isFocusable($('.menu__item')); // true
/*
*/
dom.isFocusable($('.menu__item')); // false
});
```
#### `isEditable` method
This method checks whether text can be entered in the DOM element passed in the argument. In other words, you can use this method to check whether the element is an input field, text field, and so on.
**Accepted arguments:**
* `domElem {jQuery}` – The DOM element to check. Required argument. If there are mutiple DOM elements in the jQuery chain, the first one is checked.
**Return value:** `Boolean`. If text can be entered in the DOM element, then `true`.
Example:
```js
modules.require(['dom', 'jquery'], function(dom, $) {
dom.isEditable($('input, textarea')); // true
});
```
================================================
FILE: common.blocks/dom/dom.js
================================================
/**
* @module dom
* @description some DOM utils
*/
import $ from 'bem:jquery'
const EDITABLE_INPUT_TYPES = new Set([
'datetime-local',
'date',
'month',
'number',
'password',
'search',
'tel',
'text',
'time',
'url',
'week'
])
export default {
/**
* Checks whether a DOM elem is in a context
* @param {jQuery} ctx DOM elem where check is being performed
* @param {jQuery} domElem DOM elem to check
* @returns {Boolean}
*/
contains(ctx, domElem) {
let res = false
domElem.each(function() {
let domNode = this
do {
if(~ctx.index(domNode)) return !(res = true)
} while(domNode = domNode.parentNode)
return res
})
return res
},
/**
* Returns current focused DOM elem in document
* @returns {jQuery}
*/
getFocused() {
// "Error: Unspecified error." in iframe in IE9
try { return $(document.activeElement) } catch(e) {}
},
/**
* Checks whether a DOM element contains focus
* @param {jQuery} domElem
* @returns {Boolean}
*/
containsFocus(domElem) {
return this.contains(domElem, this.getFocused())
},
/**
* Checks whether a browser currently can set focus on DOM elem
* @param {jQuery} domElem
* @returns {Boolean}
*/
isFocusable(domElem) {
const domNode = domElem[0]
if(!domNode) return false
if(domNode.hasAttribute('tabindex')) return true
switch(domNode.tagName.toLowerCase()) {
case 'iframe':
return true
case 'input':
case 'button':
case 'textarea':
case 'select':
return !domNode.disabled
case 'a':
return !!domNode.href
}
return false
},
/**
* Checks whether a domElem is intended to edit text
* @param {jQuery} domElem
* @returns {Boolean}
*/
isEditable(domElem) {
const domNode = domElem[0]
if(!domNode) return false
switch(domNode.tagName.toLowerCase()) {
case 'input':
return EDITABLE_INPUT_TYPES.has(domNode.type) && !domNode.disabled && !domNode.readOnly
case 'textarea':
return !domNode.disabled && !domNode.readOnly
default:
return domNode.contentEditable === 'true'
}
}
}
================================================
FILE: common.blocks/dom/dom.ru.md
================================================
# dom
Блок предоставляет объект, содержащий набор методов для работы с DOM-деревом.
## Обзор
### Свойства и методы объекта
| Имя | Тип возвращаемого значения | Описание |
| -------- | --- | -------- |
| contains ( `ctx {jQuery}`, `domElem {jQuery}`) | `Boolean` | Проверяет, содержит ли один DOM-элемент другой. |
| getFocused ( `domElem {jQuery} `) | `jQuery` | Служит для получения ссылки на DOM-элемент в фокусе. |
| containsFocus ( `domElem {jQuery} `) | `Boolean` | Проверят, содержит ли DOM-элемент или его потомки фокус. |
| isFocusable ( `domElem {jQuery} `) | `Boolean` | Проверят, может ли DOM-элемент находиться в фокусе. |
| isEditable ( `domElem {jQuery}`) | `Boolean` | Проверят, возможен ли в DOM-элементе ввод текста. |
### Публичные технологии блока
Блок реализован в технологиях:
* `js`
## Описание
### Свойства и методы объекта
#### Метод `contains`
Метод позволяет проверить содержит ли некоторый DOM-элемент `ctx` элемент `domElem`.
**Принимаемые аргументы:**
* `ctx {jQuery}` – DOM-элемент внутри которого производится поиск. Обязательный аргумент.
* `domElem {jQuery}` – искомый DOM-элемент. Обязательный аргумент.
**Возвращаемое значение:** `Boolean`. Если искомый элемент найден – `true`.
Пример:
```js
modules.require(['dom', 'jquery'], function(dom, $) {
/*
*/
dom.contains($('.block1'), $('.block2')); // true
});
```
#### Метод `getFocused`
Метод служит для получения ссылки на DOM-элемент, находящийся в фокусе.
Не принимает аргументов.
**Возвращаемое значение:** `jQuery` – объект в фокусе.
Пример:
```js
modules.require(['dom'], function(dom) {
dom.getFocused(); // ссылка на элемент в фокусе
});
```
#### Метод `containsFocus`
Метод проверяет находится ли в фокусе переданный аргументом DOM-элемент или один из его потомков.
**Принимаемые аргументы:**
* `domElem {jQuery}` – проверяемый DOM-элемент. Обязательный аргумент.
**Возвращаемое значение:** `Boolean`. Если искомый элемент в фокусе – `true`.
Пример:
```js
modules.require(['dom', 'jquery'], function(dom, $) {
/*
*/
$('.block1__control').focus();
dom.containsFocus($('.block1')); // true
});
```
#### Метод `isFocusable`
Метод проверят может ли браузер пользователя установить фокус на переданный аргументом DOM-элемент.
**Принимаемые аргументы:**
* `domElem {jQuery}` – проверяемый DOM-элемент. Обязательный аргумент. Если в jQuery-цепочке несколько DOM-элементов, то проверяется первый из них.
**Возвращаемое значение:** `Boolean`. Если фокус может быть установлен – `true`.
Пример:
```js
modules.require(['dom', 'jquery'], function(dom, $) {
/*
*/
dom.isFocusable($('.menu__item')); // true
/*
*/
dom.isFocusable($('.menu__item')); // false
});
```
#### Метод `isEditable`
Метод проверят возможен ли в переданном аргументом DOM-элементе ввод текста. Другими словами, с помощью метода можно проверить является ли элемент полем ввода, текстовой областью и т.п.
**Принимаемые аргументы:**
* `domElem {jQuery}` – проверяемый DOM-элемент. Обязательный аргумент. Если в jQuery-цепочке несколько DOM-элементов, то проверяется первый из них.
**Возвращаемое значение:** `Boolean`. Если ввод текста в элементе возможен – `true`.
Пример:
```js
modules.require(['dom', 'jquery'], function(dom, $) {
dom.isEditable($('input, textarea')); // true
});
```
================================================
FILE: common.blocks/dom/dom.spec.js
================================================
modules.define('spec', [
'dom',
'jquery'
], function(provide,
dom,
$
) {
describe('dom', function() {
describe('contains', function() {
var domElem;
beforeEach(function() {
domElem = $(
'')
.appendTo('body');
});
afterEach(function() {
domElem.remove();
});
it('should properly checks for nested dom elem', function() {
dom.contains(domElem.find('.a'), domElem.find('.x')).should.be.true;
dom.contains(domElem.find('.a'), domElem.find('.y')).should.be.true;
dom.contains(domElem.find('.c'), domElem.find('.x')).should.be.false;
});
it('should returns true for itself', function() {
dom.contains(domElem.find('.x'), domElem.find('.x')).should.be.true;
});
it('should returns false for empty DOM elem', function() {
dom.contains(domElem.find('.a'), domElem.find('.no-exist')).should.be.false;
});
});
describe('getFocused', function() {
it('should returns focused DOM elem', function() {
var elem = $(' ')
.appendTo('body')
.focus();
dom.getFocused()[0].should.be.eql(elem[0]);
elem.blur();
dom.getFocused()[0].should.not.be.eql(elem[0]);
elem.remove();
});
});
describe('isFocusable', function() {
it('should returns true if given DOM elem is iframe, input, button, textarea or select', function() {
dom.isFocusable($('')).should.be.true;
dom.isFocusable($(' ')).should.be.true;
dom.isFocusable($(' ')).should.be.true;
dom.isFocusable($('')).should.be.true;
dom.isFocusable($(' ')).should.be.true;
});
it('should returns false if given DOM elem is disabled', function() {
dom.isFocusable($(' ')).should.be.false;
dom.isFocusable($(' ')).should.be.false;
dom.isFocusable($('')).should.be.false;
dom.isFocusable($(' ')).should.be.false;
});
it('should returns true if given DOM elem is link with href', function() {
dom.isFocusable($(' ')).should.be.true;
dom.isFocusable($(' ')).should.be.false;
});
it('should returns true if given DOM elem has tabindex', function() {
dom.isFocusable($(' ')).should.be.true;
dom.isFocusable($(' ')).should.be.true;
dom.isFocusable($(' ')).should.be.false;
});
it('should returns false if given DOM elem is empty', function() {
dom.isFocusable($('.__no-exist')).should.be.false;
});
});
describe('containsFocus', function() {
var domElem;
beforeEach(function() {
domElem = $(
'' +
'
' +
' ' +
'
' +
'
' +
'
')
.appendTo('body');
domElem.find('.x').focus();
});
afterEach(function() {
domElem.remove();
});
it('should returns true if context contains focused DOM elem', function() {
dom.containsFocus(domElem.find('.a')).should.be.true;
});
it('should returns true if context self-focused', function() {
dom.containsFocus(domElem.find('.x')).should.be.true;
});
it('should returns false if context not contains focused DOM elem', function() {
dom.containsFocus(domElem.find('.b')).should.be.false;
});
it('should returns false if context is empty', function() {
dom.containsFocus(domElem.find('.__no-exist')).should.be.false;
});
});
describe('isEditable', function() {
it('should returns true if given DOM elem is text or password input', function() {
dom.isEditable($(' ')).should.be.true;
dom.isEditable($(' ')).should.be.true;
dom.isEditable($('')).should.be.true;
dom.isEditable($(' ')).should.be.false;
dom.isEditable($(' ')).should.be.false;
dom.isEditable($('
')).should.be.false;
});
it('should returns false if given input is readonly', function() {
dom.isEditable($(' ')).should.be.false;
dom.isEditable($(' ')).should.be.false;
});
it('should returns false if given input is disabled', function() {
dom.isEditable($(' ')).should.be.false;
dom.isEditable($(' ')).should.be.false;
});
it('should returns true for contenteditable DOM elems', function() {
dom.isEditable($('
')).should.be.true;
dom.isEditable($('
')).should.be.false;
dom.isEditable($('
')).should.be.false;
});
it('should returns false if given DOM elem is empty', function() {
dom.isEditable($('.__no-exist')).should.be.false;
});
});
});
provide();
});
================================================
FILE: common.blocks/events/__channels/events__channels.deps.js
================================================
({
shouldDeps : 'events'
})
================================================
FILE: common.blocks/events/__channels/events__channels.en.md
================================================
# `channels` element in the `events` block
Use the `channels` element in the `events` block for working with named event channels. Named channels allow you to work with events using the observer pattern (also known as the publish-subscribe pattern).
This element implements a function to:
* Get a reference to a named channel by its `id`.
* Get a reference to a standard channel.
* Remove a standard channel or a named channel with an `id`.
**Accepted arguments:**
* [`id {String}`] – Channel ID. If omitted, the default channel is used (`'default'`).
* [`drop {Boolean}`] – A boolean flag to remove the channel (when `true`). By default, `false`.
**Returned value:**
* `Object`. Object of the `Emitter` "class" – a named channel.
* `undefined`. If the function was called with the `drop` parameter set to `true`.
Example:
```js
modules.require(['events__channels'], function(channels) {
var myChannel = channels('my-channel');
myChannel.on('test', function(e, data) { console.log(data.foo) });
myChannel.emit('test', { foo : 'bar' }); // 'bar'
});
```
================================================
FILE: common.blocks/events/__channels/events__channels.ru.md
================================================
# Элемент `channels` блока `events`
Элемент `channels` блока `events` предназначен для работы с именованными каналами событий. Именные каналы позволяют организовать работу с событиями, используя шаблон проектирования «наблюдатель» (также известный как Publisher-subscriber).
Элемент реализует функцию, позволяющую:
* получить ссылку на именной канал по `id`;
* получить ссылку на стандартный канал;
* удалить канал – стандартный или по `id`.
**Принимаемые аргументы:**
* [`id {String}`] – Идентификатор канала. Если не задан будет использоваться канал по умолчанию (`'default'`).
* [`drop {Boolean}`] – Логический флаг, указывающий (в значении `true`) на необходимость удалить канал. По умолчанию `false`.
**Возвращаемое значение:**
* `Object`. Объект «класса» `Emitter` – именной канал.
* `undefined`. В случае если функция была вызвана с параметром `drop` в значении `true`.
Пример:
```js
modules.require(['events__channels'], function(channels) {
var myChannel = channels('my-channel');
myChannel.on('test', function(e, data) { console.log(data.foo) });
myChannel.emit('test', { foo : 'bar' }); // 'bar'
});
```
================================================
FILE: common.blocks/events/__channels/events__channels.vanilla.js
================================================
/**
* @module events__channels
*/
import events from 'bem:events'
const channels = new Map()
export default
/**
* Returns/destroys a named communication channel
* @param {String} [id='default'] Channel ID
* @param {Boolean} [drop=false] Destroy the channel
* @returns {events:Emitter|undefined} Communication channel
*/
function(id, drop) {
if(typeof id === 'boolean') {
drop = id
id = undefined
}
id || (id = 'default')
if(drop) {
if(channels.has(id)) {
channels.get(id).un()
channels.delete(id)
}
return
}
let channel = channels.get(id)
if(!channel) {
channel = new events.Emitter()
channels.set(id, channel)
}
return channel
}
================================================
FILE: common.blocks/events/__observable/_type/events__observable_type_bem-dom.deps.js
================================================
[{
mustDeps : { elem : 'observable' },
shouldDeps : 'i-bem-dom'
},
{
tech : 'spec.js',
mustDeps : [
{
block : 'i-bem-dom',
tech : 'js'
}
]
}]
================================================
FILE: common.blocks/events/__observable/_type/events__observable_type_bem-dom.js
================================================
/**
* @module events__observable_type_bem-dom
*/
import bemDom from 'bem:i-bem-dom';
import observable from 'bem:events__observable';
export default
/**
* Creates new observable with BEM DOM entity support
* @param {i-bem-dom:Block|i-bem-dom:Elem|events:Emitter} bemEntity
* @returns {Observable}
*/
function(bemEntity) {
return observable(bemDom.isEntity(bemEntity)?
bemEntity._events() :
bemEntity);
};
================================================
FILE: common.blocks/events/__observable/_type/events__observable_type_bem-dom.spec.js
================================================
modules.define('spec', [
'events__observable',
'sinon',
'i-bem-dom',
'BEMHTML'
], function(provide,
observable,
sinon,
bemDom,
BEMHTML
) {
describe('events__observable_type_bem-dom', function() {
var spy1, spy2, spy3, block;
beforeEach(function() {
spy1 = sinon.spy();
spy2 = sinon.spy();
spy3 = sinon.spy();
block = bemDom.init(BEMHTML.apply({
block : 'obs-dom-block'
})).appendTo('body').bem(bemDom.declBlock('obs-dom-block'));
});
afterEach(function() {
bemDom.destruct(block.domElem);
});
describe('on', function() {
it('should properly bind handlers', function() {
var data = {},
ctx = {};
observable(block)
.on('myevent', spy1)
.on('myevent', spy2, ctx)
.on('myevent', data, spy3, ctx);
block._emit('myevent');
spy1.should.have.been.calledOn(block);
spy2.should.have.been.calledOn(ctx);
spy3.args[0][0].data.should.be.equal(data);
});
});
describe('once', function() {
it('should properly bind handlers', function() {
var data = {},
ctx = {};
observable(block)
.once('myevent', spy1)
.once('myevent', spy2, ctx)
.once('myevent', data, spy3, ctx);
block._emit('myevent');
block._emit('myevent');
spy1.should.have.been.calledOnce;
spy1.should.have.been.calledOn(block);
spy2.should.have.been.calledOnce;
spy2.should.have.been.calledOn(ctx);
spy3.should.have.been.calledOnce;
spy3.args[0][0].data.should.be.equal(data);
});
});
describe('un', function() {
it('should properly unbind handlers', function() {
var ctx = {},
blockObserver = observable(block)
.on('myevent', spy1)
.on('myevent', spy2, ctx)
.un('myevent', spy1)
.un('myevent', spy2);
block._emit('myevent');
spy1.should.not.have.been.called;
spy2.should.have.been.called;
blockObserver.un('myevent', spy2, ctx);
block._emit('myevent');
spy2.should.have.been.calledOnce;
});
});
});
provide();
});
================================================
FILE: common.blocks/events/__observable/events__observable.deps.js
================================================
[
{ shouldDeps : 'inherit' },
{
tech : 'spec.js',
shouldDeps : { block : 'events', tech : 'js' }
}
]
================================================
FILE: common.blocks/events/__observable/events__observable.js
================================================
/**
* @module events__observable
*/
import inherit from 'bem:inherit';
/**
* @class Observable
*/
const Observable = inherit(/** @lends Observable.prototype */{
/**
* @constructor
* @param {Object} emitter
*/
__constructor : function(emitter) {
this._emitter = emitter;
},
/**
* Adds an event handler
* @param {String} e Event type
* @param {Object} [data] Additional data that the handler gets as e.data
* @param {Function} fn Handler
* @param {Object} [fnCtx] Context
* @returns {Observable} this
*/
on : function(e, data, fn, fnCtx) {
this._emitter.on.apply(this._emitter, arguments);
return this;
},
/**
* Adds an event handler
* @param {String} e Event type
* @param {Object} [data] Additional data that the handler gets as e.data
* @param {Function} fn Handler
* @param {Object} [fnCtx] Context
* @returns {Observable} this
*/
once : function(e, data, fn, fnCtx) {
this._emitter.once.apply(this._emitter, arguments);
return this;
},
/**
* Removes event handler
* @param {String} [e] Event type
* @param {Function} [fn] Handler
* @param {Object} [fnCtx] Context
* @returns {Observable} this
*/
un : function(e, fn, fnCtx) {
this._emitter.un.apply(this._emitter, arguments);
return this;
}
});
export default
/**
* Creates new observable
* @param {events:Emitter} emitter
* @returns {Observable}
*/
function(emitter) {
return new Observable(emitter);
};
================================================
FILE: common.blocks/events/__observable/events__observable.spec.js
================================================
modules.define('spec', [
'events__observable',
'sinon',
'events'
], function(provide,
observable,
sinon,
events
) {
describe('events__observable', function() {
var spy1, spy2, spy3, emitter;
beforeEach(function() {
spy1 = sinon.spy();
spy2 = sinon.spy();
spy3 = sinon.spy();
emitter = new events.Emitter();
});
describe('on', function() {
it('should properly bind handlers', function() {
var data = {},
ctx = {};
observable(emitter)
.on('myevent', spy1)
.on('myevent', spy2, ctx)
.on('myevent', data, spy3, ctx);
emitter.emit('myevent');
spy1.should.have.been.calledOn(emitter);
spy2.should.have.been.calledOn(ctx);
spy3.args[0][0].data.should.be.equal(data);
});
});
describe('once', function() {
it('should properly bind handlers', function() {
var data = {},
ctx = {};
observable(emitter)
.once('myevent', spy1)
.once('myevent', spy2, ctx)
.once('myevent', data, spy3, ctx);
emitter.emit('myevent');
emitter.emit('myevent');
spy1.should.have.been.calledOnce;
spy1.should.have.been.calledOn(emitter);
spy2.should.have.been.calledOnce;
spy2.should.have.been.calledOn(ctx);
spy3.should.have.been.calledOnce;
spy3.args[0][0].data.should.be.equal(data);
});
});
describe('un', function() {
it('should properly unbind handlers', function() {
var ctx = {},
blockObserver = observable(emitter)
.on('myevent', spy1)
.on('myevent', spy2, ctx)
.un('myevent', spy1)
.un('myevent', spy2);
emitter.emit('myevent');
spy1.should.not.have.been.called;
spy2.should.have.been.called;
blockObserver.un('myevent', spy2, ctx);
emitter.emit('myevent');
spy2.should.have.been.calledOnce;
});
});
});
provide();
});
================================================
FILE: common.blocks/events/events.deps.js
================================================
({
shouldDeps : ['inherit', 'identify', 'functions']
})
================================================
FILE: common.blocks/events/events.en.md
================================================
# events
This block provides a set of JS classes for working with events.
## Overview
### Classes provided by the block
| Class | Constructor | Description |
| ----- | ----------- | -------- |
| Event | Event( `type {String}`, `target {Object}`) | Creates the event object and changes and checks its states. |
| Emitter | - | Generates events and subscriptions to them. |
### Properties and methods of the class object
| Class | Name | Type or return value | Description |
| ----- | --- | ----------------------------- | -------- |
| Event | type | `String` | Type of event. |
| | result | `*` | The result returned by the event's last handler. |
| | target | `Object` | The object where the event occurred. |
| | data | `*` | Data to pass to the handler as an argument. |
| | preventDefault ()| - | Allows you to prevent execution of the default action for the event. |
| | isDefaultPrevented ()| `Boolean` | Checks whether the default action for the event was prevented from being executed. |
| | stopPropagation ()| - | Allows you to stop event propagation. |
| | isPropagationStopped ()| `Boolean` | Checks whether event propagation was stopped. |
| Emitter | on ( `type {String}`, `[data {Object}]`, `fn {Function}`, `[ {Object} ctx]`) | - | Subscribes to a specific type of event. |
| | once ( `type {String}`, `[data {Object}]`, `fn {Function}`, `[ctx {Object}]`) | - | Subscribes to a specific type of event. The handler executes only once. |
| | un ( `type {String}`, `fn {Function}`, `[ctx {Object}]`) | - | Unsubscribes to a specific type of event. |
| | emit ( `type {String`|`events:Event}`, `[data {Object}]`) | - | Generates an event. |
### Elements of the block
| Element | Usage | Description |
| ------- | --------------------- | -------- |
| channels | `JS` | Used for working with named event channels. |
### Functions provided by block elements
| Element | Function | Return type | Description |
| ------- | ------- | ----------------------------- | -------- |
| channels | channels( `[id {String}]`, `[drop {Boolean}]`) | `Object`|`undefined` | Creates or deletes a named event channel. |
### Public block technologies
The block is implemented in:
* `vanilla.js`
## Description
### `Event` class
You can use this class to instantiate an event object by indicating its type and source. To do this, use the `Event` constructor function.
**Accepted arguments:**
* `type {String}` – Type of event. Required argument.
* `target {Object}` – Object (source) where the event occurred. Required argument.
**Return value:** `Event`. The event object.
#### Properties and methods of the class object
##### `type` property
Type: `String`.
Type of event.
```js
modules.require(['events'], function(events) {
var myevent = new events.Event('myevent', this);
console.log(myevent.type); // 'myevent'
});
```
##### `target` property
Type: `Object`.
The object where the event occurred.
##### `result` property
Type: `*`.
Contains the data returned by the event's last handler function.
```js
modules.require(['events'], function(events) {
var myEmitter = new events.Emitter();
myEmitter.on('myevent', function() { return 'hi-hi-hi'; });
var myEvent = new events.Event('myevent');
myEmitter.emit(myEvent)
console.log(myEvent.result); // 'hi-hi-hi'
});
```
##### `data` property
Type: `*`.
Contains the data passed to the event's handler function as an argument.
```js
modules.require(['events'], function(events) {
var myEmitter = new events.Emitter();
myEmitter.on('myevent', 'my-data', function(e) { console.log(e.data); });
myEmitter.emit('myevent'); // my-data
});
```
##### `preventDefault` method
Allows you to prevent execution of the default action for the event.
Doesn't accept arguments.
No return value.
##### `isDefaultPrevented` method
Allows you to check whether the default action for the event was prevented from being executed.
Doesn't accept arguments.
**Return value:** `Boolean`. If the default action for the event was prevented from being executed, it is `true`.
##### `stopPropagation` method
Allows you to stop event propagation.
Doesn't accept arguments.
No return value.
##### `isPropagationStopped` method
Allows you to check whether event propagation was stopped.
Doesn't accept arguments.
**Return value:** `Boolean`. If event propagation was stopped, it is `true`.
### `Emitter` class
This class instantiates objects that you can use for generating events and subscribing to them.
```js
modules.require(['events'], function(events) {
var myEmitter = new events.Emitter();
});
```
#### Properties and methods of the class object
##### `on` method
Subscribes to a specific type of event.
**Accepted arguments:**
* `type {String}` – The type of event being subscribed to. Required argument.
* [`data {Object}`] – Additional data available to the handler as the value of the `e.data` field in the event object.
* `fn {Function}` – The handler function to call for the event. Required argument.
* [`ctx {Object}`] – Context for the handler function.
Returns the `this` object.
```js
modules.require(['events'], function(events) {
var myEmitter = new events.Emitter();
myEmitter.on('myevent', function() { console.log('foo'); });
myEmitter.emit('myevent'); // 'foo'
});
```
In addition, the value of the `type` argument may be:
* Multiple event types separated by spaces, in order to set a single handler function for all of them.
```js
modules.require(['events'], function(events) {
var myEmitter = new events.Emitter();
myEmitter.on('myevent1 myevent2', function(e) { console.log(e.type) });
myEmitter.emit('myevent1'); // 'myevent1'
myEmitter.emit('myevent2'); // 'myevent2'
});
```
* A hash of `{ 'event-1' : handler-1, ... , 'event-n' : handler-n }`, in order to set multiple handlers for different event types.
```js
modules.require(['events'], function(events) {
var myEmitter = new events.Emitter();
myEmitter.on({
myevent1 : function(e) { console.log(e.type) },
myevent2 : function(e) { console.log(e.type) }
});
myEmitter.emit('myevent1'); // 'myevent1'
myEmitter.emit('myevent2'); // 'myevent2'
});
```
The same is true for the `once` and `un` methods.
##### `once` method
Identical to the `on` method, but it only executes once. After the first event, the subscription is removed.
**Accepted arguments:**
* `type {String}` – The type of event being subscribed to. Required argument.
* [`data {Object}`] – Additional data available as the value of the `e.data` field in the event object.
* `fn {Function}` – The handler function to call for the event. Required argument.
* [`ctx {Object}`] – Context for the handler function.
Returns the `this` object.
```js
modules.require(['events'], function(events) {
var myEmitter = new events.Emitter();
myEmitter.on('myevent', function() { console.log('foo') });
myEmitter.emit('myevent'); // 'foo'
myEmitter.emit('myevent'); //handler isn't called
});
```
##### `un` method
Removes a previously set subscription to a specific type of event.
**Accepted arguments:**
* `type {String}` – The type of event being unsubscribed from. Required argument.
* [`fn {Function}`] – The handler to delete.
* [`ctx {Object}`] – The handler context.
The method returns a reference to the `this` object.
```js
modules.require(['events'], function(events) {
var myEmitter = new events.Emitter(),
shout = function() { console.log('foo') };
myEmitter.on('myevent', shout);
myEmitter.emit('myevent'); // 'foo'
myEmitter.un('myevent', shout);
myEmitter.emit('myevent'); //handler isn't called
});
```
##### `emit` method
Generates an event.
This method calls all the handler functions set for the event.
**Accepted arguments:**
* `type {String|events:Event}` – The event to generate, in the form of a string or a prepared event object. Required argument.
* [`data {Object}`] – Additional data available as the second argument of the handler function.
Returns the `this` object.
```js
modules.require(['events'], function(events) {
var myEmitter = new events.Emitter();
myEmitter.on('myevent', function(e, data) { console.log(data) });
myEmitter.emit('myevent', 'ololo'); // 'ololo'
});
```
#### Static methods of the class
The set of static methods and their signatures is exactly the same as for the methods of the object being instantiated by the class.
================================================
FILE: common.blocks/events/events.ru.md
================================================
# events
Блок предоставляет набор JS-классов, реализующий механизмы работы с событиями.
## Обзор
### Классы, предоставляемые блоком
| Класс | Конструктор | Описание |
| ----- | ----------- | -------- |
| Event | Event( `type {String}`, `target {Object}`) | Служит для создания объекта события, изменения и проверки его состояний. |
| Emitter | - | Служит для генерации событий и подписки на них. |
### Свойства и методы объекта класса
| Класс | Имя | Тип или возвращаемое значение | Описание |
| ----- | --- | ----------------------------- | -------- |
| Event | type | `String` | Тип события. |
| | result | `*` | Результат, возвращенный последним обработчиком события. |
| | target | `Object` | Объект на котором возникло событие. |
| | data | `*` | Данные, передаваемые как аргумент обработчику. |
| | preventDefault ()| - | Позволяет предотвратить выполнение стандартного действия предусмотренного для события. |
| | isDefaultPrevented ()| `Boolean` | Проверяет, было ли предотвращено выполнение стандартного действия, предусмотренного для события. |
| | stopPropagation ()| - | Позволяет остановить всплывание события. |
| | isPropagationStopped ()| `Boolean` | Проверяет, было ли остановлено всплывание события. |
| Emitter | on ( `type {String}`, `[data {Object}]`, `fn {Function}`, `[ {Object} ctx]`) | - | Служит для подписки на событие определенного типа. |
| | once ( `type {String}`, `[data {Object}]`, `fn {Function}`, `[ctx {Object}]`) | - | Служит для подписки на событие определенного типа. Обработчик выполняется единожды. |
| | un ( `type {String}`, `fn {Function}`, `[ctx {Object}]`) | - | Служит для удаления подписки на событие определенного типа. |
| | emit ( `type {String`|`events:Event}`, `[data {Object}]`) | - | Служит для генерации события. |
### Элементы блока
| Элемент | Способы использования | Описание |
| ------- | --------------------- | -------- |
| channels | `JS` | Предназначен для работы с именованными каналами событий. |
### Функции предоставляемые элементами блока
| Элемент | Функция | Тип возвращаемого значения | Описание |
| ------- | ------- | ----------------------------- | -------- |
| channels | channels( `[id {String}]`, `[drop {Boolean}]`) | `Object`|`undefined` | Создает или удаляет именованный канал событий. |
### Публичные технологии блока
Блок реализован в технологиях:
* `vanilla.js`
## Описание
### Класс `Event`
С помощью класса можно инстанцировать объект события, указав его тип и источник. Для этого нужно воспользоваться функцией-конструктором `Event`.
**Принимаемые аргументы:**
* `type {String}` – тип события. Обязательный аргумент.
* `target {Object}` – объект (источник) на котором событие возникло. Обязательный аргумент.
**Возвращаемое значение:** `Event`. Объект события.
#### Свойства и методы объекта класса
##### Свойство `type`
Тип: `String`.
Тип события.
```js
modules.require(['events'], function(events) {
var myevent = new events.Event('myevent', this);
console.log(myevent.type); // 'myevent'
});
```
##### Свойство `target`
Тип: `Object`.
Объект, на котором возникло событие.
##### Свойство `result`
Тип: `*`.
Содержит данные, возвращаемые последней функцией-обработчиком события.
```js
modules.require(['events'], function(events) {
var myEmitter = new events.Emitter();
myEmitter.on('myevent', function() { return 'hi-hi-hi'; });
var myEvent = new events.Event('myevent');
myEmitter.emit(myEvent)
console.log(myEvent.result); // 'hi-hi-hi'
});
```
##### Свойство `data`
Тип: `*`.
Содержит данные, передаваемые функции-обработчику события в качестве аргумента.
```js
modules.require(['events'], function(events) {
var myEmitter = new events.Emitter();
myEmitter.on('myevent', 'my-data', function(e) { console.log(e.data); });
myEmitter.emit('myevent'); // my-data
});
```
##### Метод `preventDefault`
Позволяет предотвратить выполнение стандартного действия предусмотренного для события.
Не принимает аргументов.
Не имеет возвращаемого значения.
##### Метод `isDefaultPrevented`
Позволяет проверить было ли предотвращено выполнение стандартного действия для события.
Не принимает аргументов.
**Возвращаемое значение:** `Boolean`. В случае, если выполнение стандартного действия было предотвращено – `true`.
##### Метод `stopPropagation`
Позволяет остановить всплывание события.
Не принимает аргументов.
Не имеет возвращаемого значения.
##### Метод `isPropagationStopped`
Позволяет проверить, было ли остановлено всплывание события.
Не принимает аргументов.
**Возвращаемое значение:** `Boolean`. В случае, если всплывание события было остановлено – `true`.
### Класс `Emitter`
Класс позволяет инстанцировать объекты, с помощью которых можно генерировать события и осуществлять подписку на них.
```js
modules.require(['events'], function(events) {
var myEmitter = new events.Emitter();
});
```
#### Свойства и методы объекта класса
##### Метод `on`
Служит для подписки на событие определенного типа.
**Принимаемые аргументы:**
* `type {String}` – тип события, на которое производится подписка. Обязательный аргумент.
* [`data {Object}`] – дополнительные данные, доступные обработчику как значение поля `e.data` объекта события.
* `fn {Function}` – функция-обработчик, вызываемая для события. Обязательный аргумент.
* [`ctx {Object}`] – контекст функции-обработчика.
Возвращает объект `this`.
```js
modules.require(['events'], function(events) {
var myEmitter = new events.Emitter();
myEmitter.on('myevent', function() { console.log('foo'); });
myEmitter.emit('myevent'); // 'foo'
});
```
Кроме того, значением аргумента `type` могут быть:
* несколько типов событий, перечисленных через пробел – чтобы установить для них общую функцию-обработчик;
```js
modules.require(['events'], function(events) {
var myEmitter = new events.Emitter();
myEmitter.on('myevent1 myevent2', function(e) { console.log(e.type) });
myEmitter.emit('myevent1'); // 'myevent1'
myEmitter.emit('myevent2'); // 'myevent2'
});
```
* хеш вида `{ 'событие-1' : обработчик-1, ... , 'событие-n' : обработчик-n }` – чтобы установить сразу несколько обработчиков для разных типов событий;
```js
modules.require(['events'], function(events) {
var myEmitter = new events.Emitter();
myEmitter.on({
myevent1 : function(e) { console.log(e.type) },
myevent2 : function(e) { console.log(e.type) }
});
myEmitter.emit('myevent1'); // 'myevent1'
myEmitter.emit('myevent2'); // 'myevent2'
});
```
Сказанное выше верно и для методов `once` и `un`.
##### Метод `once`
Идентичен методу `on`, но выполняется единожды – после первого события подписка удаляется.
**Принимаемые аргументы:**
* `type {String}` – тип события, на которое производится подписка. Обязательный аргумент.
* [`data {Object}`] – дополнительные данные, доступные как значение поля `e.data` объекта события.
* `fn {Function}` – функция-обработчик, вызываемая для события. Обязательный аргумент.
* [`ctx {Object}`] – контекст функции-обработчика.
Возвращает объект `this`.
```js
modules.require(['events'], function(events) {
var myEmitter = new events.Emitter();
myEmitter.on('myevent', function() { console.log('foo') });
myEmitter.emit('myevent'); // 'foo'
myEmitter.emit('myevent'); // обработчик не вызывается
});
```
##### Метод `un`
Служит для удаления установленной ранее подписки на событие определенного типа.
**Принимаемые аргументы:**
* `type {String}` – тип события, подписка на которое удаляется. Обязательный аргумент.
* [`fn {Function}`] – удаляемый обработчик.
* [`ctx {Object}`] – контекст обработчика.
Метод возвращает ссылку на объект `this`.
```js
modules.require(['events'], function(events) {
var myEmitter = new events.Emitter(),
shout = function() { console.log('foo') };
myEmitter.on('myevent', shout);
myEmitter.emit('myevent'); // 'foo'
myEmitter.un('myevent', shout);
myEmitter.emit('myevent'); // обработчик не вызывается
});
```
##### Метод `emit`
Служит для генерации события.
Метод вызывает все функции-обработчики, заданные для события.
**Принимаемые аргументы:**
* `type {String|events:Event}` – генерируемое событие в виде строки или готового объекта события. Обязательный аргумент.
* [`data {Object}`] – дополнительные данные, доступные как второй аргумент функции-обработчика.
Возвращает объект `this`.
```js
modules.require(['events'], function(events) {
var myEmitter = new events.Emitter();
myEmitter.on('myevent', function(e, data) { console.log(data) });
myEmitter.emit('myevent', 'ololo'); // 'ololo'
});
```
#### Статические методы класса
Набор и сигнатуры статических методов идентичны набору и сигнатурам методов объекта, инстанцируемого классом.
================================================
FILE: common.blocks/events/events.spec.js
================================================
modules.define('spec', [
'events',
'sinon'
], function(provide,
events,
sinon
) {
describe('events', function() {
describe('Emitter', function() {
var emitter;
beforeEach(function() {
emitter = new events.Emitter();
});
describe('on/emit', function() {
it('should call callbacks according to the type of event', function() {
var spy1 = sinon.spy(),
spy1_1 = sinon.spy(),
spy2 = sinon.spy();
emitter
.on('event1', spy1)
.on('event1', spy1_1)
.on('event2', spy2)
.emit('event1');
spy1.should.have.been.calledOnce;
spy1_1.should.have.been.calledOnce;
spy2.should.not.have.been.called;
emitter.emit('event2');
spy2.should.have.been.calledOnce;
emitter.emit('event1');
spy1.should.have.been.calledTwice;
spy1_1.should.have.been.calledTwice;
});
it('should call callbacks according to all types of event', function() {
var spy = sinon.spy();
emitter
.on('event1 event2', spy)
.emit('event1');
spy.should.have.been.calledOnce;
emitter.emit('event2');
spy.should.have.been.calledTwice;
});
it('should call callbacks for all types of event', function() {
var spy = sinon.spy();
emitter
.on('*', spy)
.emit('event1');
spy.should.have.been.calledOnce;
emitter.emit('event2');
spy.should.have.been.calledTwice;
emitter.emit('event3');
spy.should.have.been.calledThrice;
});
it('should call callback with given context', function() {
var spy = sinon.spy(),
ctx = {};
emitter
.on('event', spy, ctx)
.emit('event');
spy.should.have.been.calledOn(ctx);
});
it('should pass event to callback', function() {
var spy = sinon.spy(),
data = { data : 'ok' };
emitter
.on('event', spy)
.emit('event', data);
var event = spy.args[0][0];
event.should.be.instanceOf(events.Event);
event.type.should.be.equal('event');
});
it('should pass additional data to callback', function() {
var spy = sinon.spy(),
data = { data : 'ok' };
emitter
.on('event', spy)
.emit('event', data);
spy.args[0][1].should.be.equal(data);
});
it('should allow to Event instance to be passed', function() {
var spy = sinon.spy(),
e = new events.Event('event');
emitter
.on('event', spy)
.emit(e);
spy.args[0][0].should.be.equal(e);
});
it('should set default target', function() {
var spy = sinon.spy();
emitter
.on('event', spy)
.emit('event');
spy.args[0][0].target.should.be.equal(emitter);
});
it('should pass custom target', function() {
var spy = sinon.spy(),
target = {},
e = new events.Event('event', target);
emitter
.on('event', spy)
.emit(e);
spy.args[0][0].target.should.be.equal(target);
});
it('should call stopPropagation and preventDefault if callback returns false', function() {
var e = new events.Event('event');
emitter
.on('event', function() {
return false;
})
.emit(e);
e.isPropagationStopped().should.be.true;
e.isDefaultPrevented().should.be.true;
});
it('should not immediately call callback that was binded in callback', function() {
var spy = sinon.spy();
emitter
.on('event', function() {
emitter.on('event', spy);
})
.emit('event');
spy.should.not.have.been.called;
emitter.emit('event');
spy.should.have.been.called;
});
});
describe('once/emit', function() {
it('should call callback once', function() {
var spy = sinon.spy();
emitter
.once('event', spy)
.emit('event')
.emit('event')
.emit('event');
spy.should.have.been.calledOnce;
});
});
describe('un/emit', function() {
it('should unbind given callback according to the type of event', function() {
var spy1 = sinon.spy(),
spy2 = sinon.spy();
emitter
.on('event', spy1)
.on('event2', spy1)
.on('event', spy2)
.un('event', spy1)
.emit('event');
spy1.should.not.have.been.called;
spy2.should.have.been.called;
emitter.emit('event2');
spy1.should.have.been.called;
});
it('should unbind given callback according to the type of all given events', function() {
var spy = sinon.spy();
emitter
.on('event', spy)
.on('event2', spy)
.un('event event2', spy)
.emit('event')
.emit('event2');
spy.should.not.have.been.called;
});
it('should unbind given callback according to the type of event and context', function() {
var spy = sinon.spy(),
ctx1 = {},
ctx2 = {};
emitter
.on('event', spy, ctx1)
.on('event', spy, ctx2)
.on('event', spy)
.un('event', spy, ctx1)
.emit('event');
spy.should.have.been.calledTwice;
});
it('should unbind all callbacks according to the type of event', function() {
var spy1 = sinon.spy(),
spy2 = sinon.spy();
emitter
.on('event', spy1)
.on('event2', spy1)
.on('event', spy2)
.un('event')
.emit('event');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
emitter.emit('event2');
spy1.should.have.been.called;
});
it('should unbind all callbacks', function() {
var spy1 = sinon.spy(),
spy2 = sinon.spy();
emitter
.on('event', spy1)
.on('event2', spy1)
.on('event', spy2)
.un()
.emit('event');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
emitter.emit('event2');
spy1.should.not.have.been.called;
});
});
});
});
provide();
});
================================================
FILE: common.blocks/events/events.vanilla.js
================================================
/**
* @module events
*/
import identify from 'bem:identify'
import inherit from 'bem:inherit'
import functions from 'bem:functions'
const storageExpando = '__' + (+new Date) + 'storage'
/**
* @class Event
* @exports events:Event
*/
const Event = inherit(/** @lends Event.prototype */{
/**
* @constructor
* @param {String} type
* @param {Object} target
*/
__constructor : function(type, target) {
/**
* Type
* @member {String}
*/
this.type = type
/**
* Target
* @member {Object}
*/
this.target = target
/**
* Data
* @member {*}
*/
this.data = undefined
this._isDefaultPrevented = false
this._isPropagationStopped = false
},
/**
* Prevents default action
*/
preventDefault : function() {
this._isDefaultPrevented = true
},
/**
* Returns whether is default action prevented
* @returns {Boolean}
*/
isDefaultPrevented : function() {
return this._isDefaultPrevented
},
/**
* Stops propagation
*/
stopPropagation : function() {
this._isPropagationStopped = true
},
/**
* Returns whether is propagation stopped
* @returns {Boolean}
*/
isPropagationStopped : function() {
return this._isPropagationStopped
}
})
/**
* @class Emitter
* @exports events:Emitter
*/
const Emitter = inherit(/** @lends Emitter.prototype */{
/**
* Adds an event handler
* @param {String} e Event type
* @param {Object} [data] Additional data that the handler gets as e.data
* @param {Function} fn Handler
* @param {Object} [ctx] Handler context
* @returns {Emitter} this
*/
on : function(e, data, fn, ctx, _special) {
if(typeof e === 'string') {
if(functions.isFunction(data)) {
ctx = fn
fn = data
data = undefined
}
const id = identify(fn, ctx)
const storage = this[storageExpando] || (this[storageExpando] = {})
const eventTypes = e.split(' ')
for(const eventType of eventTypes) {
const eventStorage = storage[eventType] || (storage[eventType] = { ids : new Map(), list : {} })
if(!eventStorage.ids.has(id)) {
const list = eventStorage.list
const item = { fn, data, ctx, special : _special }
if(list.last) {
list.last.next = item
item.prev = list.last
} else {
list.first = item
}
eventStorage.ids.set(id, item)
list.last = item
}
}
} else {
for(const [key, val] of Object.entries(e)) {
this.on(key, val, data, _special)
}
}
return this
},
/**
* Adds a one time handler for the event.
* Handler is executed only the next time the event is fired, after which it is removed.
* @param {String} e Event type
* @param {Object} [data] Additional data that the handler gets as e.data
* @param {Function} fn Handler
* @param {Object} [ctx] Handler context
* @returns {Emitter} this
*/
once : function(e, data, fn, ctx) {
return this.on(e, data, fn, ctx, { once : true })
},
/**
* Removes event handler or handlers
* @param {String} [e] Event type
* @param {Function} [fn] Handler
* @param {Object} [ctx] Handler context
* @returns {Emitter} this
*/
un : function(e, fn, ctx) {
if(typeof e === 'string' || typeof e === 'undefined') {
const storage = this[storageExpando]
if(storage) {
if(e) { // if event type was passed
const eventTypes = e.split(' ')
for(const eventType of eventTypes) {
const eventStorage = storage[eventType]
if(eventStorage) {
if(fn) { // if specific handler was passed
const id = identify(fn, ctx)
const ids = eventStorage.ids
if(ids.has(id)) {
const list = eventStorage.list
const item = ids.get(id)
const prev = item.prev
const next = item.next
if(prev) {
prev.next = next
} else if(item === list.first) {
list.first = next
}
if(next) {
next.prev = prev
} else if(item === list.last) {
list.last = prev
}
ids.delete(id)
}
} else {
delete this[storageExpando][eventType]
}
}
}
} else {
delete this[storageExpando]
}
}
} else {
for(const [key, val] of Object.entries(e)) {
this.un(key, val, fn)
}
}
return this
},
/**
* Fires event handlers
* @param {String|events:Event} e Event
* @param {Object} [data] Additional data
* @returns {Emitter} this
*/
emit : function(e, data) {
const storage = this[storageExpando]
let eventInstantiated = false
if(storage) {
const eventTypes = [typeof e === 'string'? e : e.type, '*']
for(const eventType of eventTypes) {
const eventStorage = storage[eventType]
if(eventStorage) {
let item = eventStorage.list.first
const lastItem = eventStorage.list.last
while(item) {
if(!eventInstantiated) { // instantiate Event only on demand
eventInstantiated = true
typeof e === 'string' && (e = new Event(e))
e.target || (e.target = this)
}
e.data = item.data
const res = item.fn.call(item.ctx || this, e, data)
if(res === false) {
e.preventDefault()
e.stopPropagation()
}
item.special && item.special.once &&
this.un(e.type, item.fn, item.ctx)
if(item === lastItem) {
break
}
item = item.next
}
}
}
}
return this
}
})
export default { Emitter, Event }
================================================
FILE: common.blocks/functions/__debounce/functions__debounce.spec.js
================================================
modules.define('spec', ['functions__debounce'], function(provide, debounce) {
describe('functions__debounce', function() {
it('should properly debounce given function', function(done) {
var res = [],
debouncedFn = debounce(
function(arg) {
res.push(arg);
},
50);
debouncedFn(1);
debouncedFn(2);
debouncedFn(3);
setTimeout(function() {
debouncedFn(4);
debouncedFn(5);
}, 25);
setTimeout(function() {
debouncedFn(6);
debouncedFn(7);
}, 220);
setTimeout(function() {
res.should.be.eql([5, 7]);
done();
}, 400);
});
it('should properly debounce given function according "invokeAsap" param', function(done) {
var res = [],
debouncedFn = debounce(
function(arg) {
res.push(arg);
},
50,
true);
debouncedFn(1);
debouncedFn(2);
debouncedFn(3);
setTimeout(function() {
debouncedFn(4);
debouncedFn(5);
}, 25);
setTimeout(function() {
debouncedFn(6);
debouncedFn(7);
}, 230);
setTimeout(function() {
res.should.be.eql([1, 6]);
done();
}, 400);
});
it('should call debounced function with given "ctx" param', function(done) {
var ctx = {},
debouncedFn = debounce(
function() {
this.should.be.eql(ctx);
done();
},
20,
ctx);
debouncedFn();
});
});
provide();
});
================================================
FILE: common.blocks/functions/__debounce/functions__debounce.vanilla.js
================================================
/**
* @module functions__debounce
*/
export default
/**
* Debounces given function
* @param {Function} fn function to debounce
* @param {Number} timeout debounce interval
* @param {Boolean} [invokeAsap=false] invoke before first interval
* @param {Object} [ctx] context of function invocation
* @returns {Function} debounced function
*/
function(fn, timeout, invokeAsap, ctx) {
if(arguments.length === 3 && typeof invokeAsap !== 'boolean') {
ctx = invokeAsap
invokeAsap = false
}
let timer
return function() {
const args = arguments
ctx || (ctx = this)
invokeAsap && !timer && fn.apply(ctx, args)
globalThis.clearTimeout(timer)
timer = globalThis.setTimeout(() => {
invokeAsap || fn.apply(ctx, args)
timer = null
}, timeout)
}
}
================================================
FILE: common.blocks/functions/__throttle/functions__throttle.spec.js
================================================
modules.define('spec', ['functions__throttle'], function(provide, throttle) {
describe('functions__throttle', function() {
it('should properly throttle given function', function(done) {
var res = [],
throttledFn = throttle(
function(arg) {
res.push(arg);
},
20);
throttledFn(1);
throttledFn(2);
throttledFn(3);
setTimeout(function() {
throttledFn(4);
}, 10);
setTimeout(function() {
throttledFn(5);
res.should.be.eql([1, 4]);
done();
}, 30);
});
it('should properly throttle given function according "invokeAsap" param', function(done) {
var res = [],
throttledFn = throttle(
function(arg) {
res.push(arg);
},
20,
false);
throttledFn(1);
throttledFn(2);
throttledFn(3);
setTimeout(function() {
throttledFn(4);
}, 10);
setTimeout(function() {
throttledFn(5);
setTimeout(function() {
res.should.be.eql([4, 5]);
done();
}, 30);
}, 30);
});
it('should call throttled function with given "ctx" param', function(done) {
var ctx = {},
throttledFn = throttle(
function() {
this.should.be.eql(ctx);
done();
},
20,
ctx);
throttledFn();
});
});
provide();
});
================================================
FILE: common.blocks/functions/__throttle/functions__throttle.vanilla.js
================================================
/**
* @module functions__throttle
*/
export default
/**
* Throttle given function
* @param {Function} fn function to throttle
* @param {Number} timeout throttle interval
* @param {Boolean} [invokeAsap=true] invoke before first interval
* @param {Object} [ctx] context of function invocation
* @returns {Function} throttled function
*/
function(fn, timeout, invokeAsap, ctx) {
const typeofInvokeAsap = typeof invokeAsap
if(typeofInvokeAsap === 'undefined') {
invokeAsap = true
} else if(arguments.length === 3 && typeofInvokeAsap !== 'boolean') {
ctx = invokeAsap
invokeAsap = true
}
let timer, args, needInvoke
const wrapper = function() {
if(needInvoke) {
fn.apply(ctx, args)
needInvoke = false
timer = globalThis.setTimeout(wrapper, timeout)
} else {
timer = null
}
}
return function() {
args = arguments
ctx || (ctx = this)
needInvoke = true
if(!timer) {
invokeAsap?
wrapper() :
timer = globalThis.setTimeout(wrapper, timeout)
}
}
}
================================================
FILE: common.blocks/functions/functions.en.md
================================================
# functions
This block provides an object with a set of methods for working with JavaScript functions.
## Overview
### Properties and methods of the object
| Name | Type or return value | Description |
| -------- | --- | -------- |
| isFunction (`obj {*}`) | `Boolean` | Checks whether a passed argument is a function. |
| noop | `Function` | Empty function. |
### Elements of the block
| Element | Usage | Description |
| --------| ---- | -------- |
| debounce | `JS` | Function decorator that combines multiple function calls within a specified time period into one call. |
| throttle | `JS` | Function decorator that limits the frequency of function execution to once per specified period. |
### Public block technologies
The block is implemented in:
* `vanilla.js`
## Description
### Properties and methods of the object
#### `isFunction` method
Checks whether a passed argument is a function.
**Accepted arguments:**
* `obj {*}` – The object being checked. Required argument.
**Return value:** `Boolean`. If the argument is a function, then `true`.
```js
modules.require('functions', function(func) {
var a = function(){},
b = {};
console.log(func.isFunction(a)); //true
console.log(func.isFunction(b)); //false
});
```
#### `noop` property
Empty function (`function() {}`).
No arguments or return value.
You can use `noop` when you need a function but there isn't a reason to add the logic. For example, you can use it as a placeholder for base classes at the design stage when using OOP.
Example:
```js
modules.define('base-class', ['inherit', 'functions'], function(provide, inherit, functions) {
provide(inherit({
getData : function() {
this._sendRequest();
},
_sendRequest : functions.noop
}));
});
```
### Elements of the block
The block elements implement a set of function decorators.
The decorators add logic to the function without changing its original signature.
#### `debounce` element
A decorator that postpones function calls for the specified delay time. After each attempt to make a call, the delay starts over again.
**Accepted arguments:**
* `fn {Function}` — Original function. Required argument.
* `timeout {Number}` — Time of delay, in milliseconds. Required argument.
* [`invokeAsap {Boolean}`] — The `debounce` mode. By default, the first mode is used (corresponding to the `false` value).
* [`context {Object}`] — The context for executing the original function.
There are two `debounce` modes, depending on the value of `invokeAsap`:
1. The original function is called when the delay expires after the last call attempt.
2. The original function is first called as soon as the decorated function is called. After this, the behavior is the same as in the first mode.
**Return value:** `Function`. The decorated function.
Example:
```js
modules.require('functions__debounce', function(provide, debounce) {
function log() {
console.log('hello!');
}
var debouncedLog = debounce(log, 300);
setInterval(debouncedLog, 50);
});
```
#### `throttle` element
This decorator allows you to "slow down" the function. It won't be executed more than once during the specified period, no matter how many times it is called during this time. All calls in the meantime are ignored.
**Accepted arguments:**
* `fn {Function}` — Original function. Required argument.
* `period {Number}` — The interval between calls, in milliseconds. Required argument.
* [`context {Object}`] — The context for executing the original function.
**Return value:** `Function`. The decorated function.
This method is convenient for setting resource-intensive handlers for frequently generated events, such as `resize`, `pointermove`, and so on.
Example:
```js
modules.require('functions__throttle', function(provide, throttle) {
function log() {
console.log('hello!');
}
var throttledLog = throttle(log, 300);
setInterval(throttledLog, 50);
});
```
As a result, the function is executed no more than once every 300 milliseconds.
================================================
FILE: common.blocks/functions/functions.ru.md
================================================
# functions
Блок предоставляет объект, содержащий набор методов для работы с функциями JavaScript.
## Обзор
### Свойства и методы объекта
| Имя | Тип или возвращаемое значение | Описание |
| -------- | --- | -------- |
| isFunction (`obj {*}`) | `Boolean` | Проверяет, является ли переданный аргумент функцией. |
| noop | `Function` | Пустая функция. |
### Элементы блока
| Элемент | Способы использования | Описание |
| --------| ---- | -------- |
| debounce | `JS` | Декоратор функции. Объединяет несколько вызовов функции, производимых в заданном временном интервале, в один. |
| throttle | `JS` | Декоратор функции. Ограничивает частоту выполнения функции до одного раза в указанный период. |
### Публичные технологии блока
Блок реализован в технологиях:
* `vanilla.js`
## Описание
### Свойства и методы объекта
#### Метод `isFunction`
Метод проверяет, является ли переданный аргумент функцией.
**Принимаемые аргументы:**
* `obj {*}` – проверяемый объект. Обязательный аргумент.
**Возвращаемое значение:** `Boolean`. В случае, если аргумент является функцией – `true`.
```js
modules.require('functions', function(func) {
var a = function(){},
b = {};
console.log(func.isFunction(a)); // true
console.log(func.isFunction(b)); // false
});
```
#### Свойство `noop`
Пустая функция (`function() {}`).
Не имеет аргументов и возвращаемого значения.
`noop` можно использовать в ситуациях, когда для работы требуется функция, но нет смысла добавлять логику. Например в качестве «заглушки» для базовых классов при проектировании в парадигме ООП.
Пример:
```js
modules.define('base-class', ['inherit', 'functions'], function(provide, inherit, functions) {
provide(inherit({
getData : function() {
this._sendRequest();
},
_sendRequest : functions.noop
}));
});
```
### Элементы блока
Элементы блока реализуют набор декораторов функций.
Декораторы добавляют функции логику, не меняя ее оригинальной сигнатуры.
#### Элемент `debounce`
Декоратор, откладывающий вызовов функции до истечения задержки. После каждой попытки вызова задержка начинает отсчитываться заново.
**Принимаемые аргументы:**
* `fn {Function}` — оригинальная функция. Обязательный аргумент.
* `timeout {Number}` — время задержки в миллисекундах. Обязательный аргумент.
* [`invokeAsap {Boolean}`] — режим работы `debounce`. По умолчанию используется первый режим (соответствует значению `false`).
* [`context {Object}`] — контекст для выполнения оригинальной функции.
В зависимости от значения аргумента `invokeAsap` `debounce` может работать в двух режимах:
1. Вызов оригинальной функции производится по истечению задержки после последней попытки вызова.
2. Первый вызов оригинальной функции производится сразу же при вызове декорированной функции. Дальнейшее поведение аналогично режиму 1.
**Возвращаемое значение:** `Function`. Декорированная функция.
Пример:
```js
modules.require('functions__debounce', function(provide, debounce) {
function log() {
console.log('hello!');
}
var debouncedLog = debounce(log, 300);
setInterval(debouncedLog, 50);
});
```
#### Элемент `throttle`
Декоратор позволяет «затормозить» функцию. Она будет выполняться не чаще одного раза в указанный период, сколько бы раз в течение этого периода ни была вызвана. Все промежуточные вызовы игнорируются.
**Принимаемые аргументы:**
* `fn {Function}` — оригинальная функция. Обязательный аргумент.
* `period {Number}` — интервал между вызовами в миллисекундах. Обязательный аргумент.
* [`context {Object}`] — контекст для выполнения оригинальной функции.
**Возвращаемое значение:** `Function`. Декорированная функция.
Метод удобно использовать, например, для установки ресурсоемких обработчиков для часто генерируемых событий – `resize`, `pointermove` и т.п.
Пример:
```js
modules.require('functions__throttle', function(provide, throttle) {
function log() {
console.log('hello!');
}
var throttledLog = throttle(log, 300);
setInterval(throttledLog, 50);
});
```
В результате, функция будет выполняться не чаще чем раз в 300 миллисекунд.
================================================
FILE: common.blocks/functions/functions.spec.js
================================================
modules.define('spec', ['functions'], function(provide, functions) {
describe('functions', function() {
describe('isFunction', function() {
it('should returns true only for function', function() {
functions.isFunction({}).should.be.false;
functions.isFunction(null).should.be.false;
functions.isFunction(5).should.be.false;
functions.isFunction().should.be.false;
functions.isFunction('').should.be.false;
functions.isFunction([]).should.be.false;
functions.isFunction(new function() {}).should.be.false;
functions.isFunction(function() {}).should.be.true;
});
});
describe('noop', function() {
it('should be a function', function() {
functions.isFunction(functions.noop).should.be.true;
});
});
});
provide();
});
================================================
FILE: common.blocks/functions/functions.vanilla.js
================================================
/**
* @module functions
* @description A set of helpers to work with JavaScript functions
*/
export default {
/**
* Checks whether a given object is function
* @param {*} obj
* @returns {Boolean}
*/
isFunction(obj) {
// In some browsers, typeof returns "function" for HTML elements
// (i.e., `typeof document.createElement( "object" ) === "function"`).
// We don't want to classify *any* DOM node as a function.
return typeof obj === 'function' && typeof obj.nodeType !== 'number'
},
/**
* Empty function
*/
noop() {}
}
================================================
FILE: common.blocks/i-bem/__collection/i-bem__collection.js
================================================
/**
* @module i-bem__collection
*/
import inherit from 'bem:inherit'
/**
* @class BemCollection
*/
const BemCollection = inherit(/** @lends BemCollection.prototype */{
/**
* @constructor
* @param {Array} entities BEM entities
*/
__constructor(entities) {
const _entities = this._entities = []
const uniq = {}
;(Array.isArray(entities) ? entities : [...arguments]).forEach((entity) => {
if(!uniq[entity._uniqId]) {
uniq[entity._uniqId] = true
_entities.push(entity)
}
})
},
/**
* Sets the modifier for entities in Collection.
* @param {String} modName Modifier name
* @param {String|Boolean} [modVal=true] Modifier value. If not of type String or Boolean, it is casted to String
* @returns {Collection} this
*/
setMod : buildForEachEntityMethodProxyFn('setMod'),
/**
* Sets multiple modifiers at once for entities in Collection.
* @param {Object} mods Hash of modifiers (modName: modVal)
* @returns {Collection} this
*/
setMods : buildForEachEntityMethodProxyFn('setMods'),
/**
* Removes the modifier from entities in Collection.
* @param {String} modName Modifier name
* @returns {Collection} this
*/
delMod : buildForEachEntityMethodProxyFn('delMod'),
/**
* Sets a modifier for entities in Collection, depending on conditions.
* If the condition parameter is passed: when true, modVal1 is set; when false, modVal2 is set.
* If the condition parameter is not passed: modVal1 is set if modVal2 was set, or vice versa.
* @param {String} modName Modifier name
* @param {String} modVal1 First modifier value
* @param {String} [modVal2] Second modifier value
* @param {Boolean} [condition] Condition
* @returns {Collection} this
*/
toggleMod : buildForEachEntityMethodProxyFn('toggleMod'),
/**
* Checks whether every entity in Collection has a modifier.
* @param {String} modName Modifier name
* @param {String|Boolean} [modVal] Modifier value. If not of type String or Boolean, it is casted to String
* @returns {Boolean}
*/
everyHasMod : buildComplexProxyFn('every', 'hasMod'),
/**
* Checks whether some entities in Collection has a modifier.
* @param {String} modName Modifier name
* @param {String|Boolean} [modVal] Modifier value. If not of type String or Boolean, it is casted to String
* @returns {Boolean}
*/
someHasMod : buildComplexProxyFn('some', 'hasMod'),
/**
* Returns entity by index.
* @param {Number} i Index
* @returns {BemEntity}
*/
get(i) {
return this._entities[i]
},
/**
* Calls callback once for each entity in collection.
* @param {Function} fn Callback
* @param {Object} ctx Callback context
*/
forEach : buildEntitiesMethodProxyFn('forEach'),
/**
* Creates an array with the results of calling callback on every entity in collection.
* @param {Function} fn Callback
* @param {Object} ctx Callback context
* @returns {Array}
*/
map : buildEntitiesMethodProxyFn('map'),
/**
* Applies callback against an accumulator and each entity in collection (from left-to-right)
* to reduce it to a single value.
* @param {Function} fn Callback
* @param {Object} [initial] Initial value
* @returns {Array}
*/
reduce : buildEntitiesMethodProxyFn('reduce'),
/**
* Applies callback against an accumulator and each entity in collection (from right-to-left)
* to reduce it to a single value.
* @param {Function} fn Callback
* @param {Object} [initial] Initial value
* @returns {Array}
*/
reduceRight : buildEntitiesMethodProxyFn('reduceRight'),
/**
* Creates a new collection with all entities that pass the test implemented by the provided callback.
* @param {Function} fn Callback
* @param {Object} ctx Callback context
* @returns {Collection}
*/
filter(...args) {
return new this.__self(buildEntitiesMethodProxyFn('filter').apply(this, args))
},
/**
* Tests whether some entities in the collection passes the test implemented by the provided callback.
* @param {Function} fn Callback
* @param {Object} ctx Callback context
* @returns {Boolean}
*/
some : buildEntitiesMethodProxyFn('some'),
/**
* Tests whether every entities in the collection passes the test implemented by the provided callback.
* @param {Function} fn Callback
* @param {Object} ctx Callback context
* @returns {Boolean}
*/
every : buildEntitiesMethodProxyFn('every'),
/**
* Returns a boolean asserting whether an entity is present in the collection.
* @param {BemEntity} entity BEM entity
* @returns {Boolean}
*/
has(entity) {
return this._entities.includes(entity)
},
/**
* Returns an entity, if it satisfies the provided testing callback.
* @param {Function} fn Callback
* @param {Object} ctx Callback context
* @returns {BemEntity}
*/
find(fn, ctx) {
return this._entities.find((entity, i) => fn.call(ctx || this, entity, i, this)) || null
},
/**
* Returns a new collection comprised of collection on which it is called joined with
* the collection(s) and/or array(s) and/or entity(es) provided as arguments.
* @param {...(Collection|Array|BemEntity)} args
* @returns {Collection}
*/
concat(...args) {
const argsForConcat = args.map((arg) =>
arg instanceof BemCollection ? arg._entities : arg)
return new this.__self(this._entities.concat(...argsForConcat))
},
/**
* Returns size of the collection.
* @returns {Number}
*/
size() {
return this._entities.length
},
/**
* Converts the collection into array.
* @returns {Array}
*/
toArray() {
return this._entities.slice()
}
})
function buildForEachEntityMethodProxyFn(methodName) {
return function(...args) {
this._entities.forEach((entity) => {
entity[methodName].apply(entity, args)
})
return this
}
}
function buildEntitiesMethodProxyFn(methodName) {
return function(...args) {
const entities = this._entities
return entities[methodName].apply(entities, args)
}
}
function buildComplexProxyFn(arrayMethodName, entityMethodName) {
return function(...args) {
return this._entities[arrayMethodName]((entity) =>
entity[entityMethodName].apply(entity, args))
}
}
export default BemCollection
================================================
FILE: common.blocks/i-bem/__collection/i-bem__collection.spec.js
================================================
modules.define('spec', [
'i-bem',
'i-bem__collection',
'sinon',
'chai'
], function(provide,
bem,
BemCollection,
sinon,
chai
) {
var expect = chai.expect;
describe('BEM collections', function() {
var Block = bem.declBlock('collection-block');
describe('constructor', function() {
it('should create collection of unique entities', function() {
var b1 = Block.create(),
collection = new BemCollection([b1, b1]);
collection.size().should.be.equal(1);
collection.get(0).should.be.equal(b1);
});
it('should create collection via arguments', function() {
var b1 = Block.create(),
b2 = Block.create(),
collection = new BemCollection(b1, b2);
collection.size().should.be.equal(2);
collection.get(0).should.be.equal(b1);
collection.get(1).should.be.equal(b2);
});
});
describe('common methods', function() {
it('get', function() {
var b1 = Block.create(),
b2 = Block.create(),
collection = new BemCollection([b1, b2]);
collection.get(1).should.be.equal(b2);
});
it('has', function() {
var b1 = Block.create(),
b2 = Block.create(),
collection = new BemCollection([b1]);
collection.has(b1).should.be.true;
collection.has(b2).should.be.false;
});
it('size', function() {
new BemCollection([]).size().should.be.equal(0);
var b1 = Block.create(),
b2 = Block.create();
new BemCollection([b1, b2]).size().should.be.equal(2);
});
it('toArray', function() {
var b1 = Block.create(),
b2 = Block.create(),
collection = new BemCollection([b1, b2]);
collection.toArray().should.be.eql([b1, b2]);
});
describe('forEach', function() {
it('should call callback for every entity', function() {
var collection = new BemCollection([Block.create(), Block.create()]),
spy = sinon.spy();
collection.forEach(spy);
spy.should.be.calledTwice;
});
it('should call callback with proper arguments', function() {
var b1 = Block.create(),
collection = new BemCollection([b1]);
collection.forEach(function(entity, i) {
entity.should.be.equal(b1);
i.should.be.equal(0);
});
});
it('should call callback with proper context', function() {
var collection = new BemCollection([Block.create()]),
spy = sinon.spy(),
ctx = {};
collection.forEach(spy, ctx);
spy.should.be.calledOn(ctx);
});
});
describe('map', function() {
it('should call callback for every entity', function() {
var collection = new BemCollection([Block.create(), Block.create()]),
spy = sinon.spy();
collection.map(spy);
spy.should.be.calledTwice;
});
it('should call callback with proper arguments', function() {
var b1 = Block.create(),
collection = new BemCollection([b1]);
collection.map(function(entity, i) {
entity.should.be.equal(b1);
i.should.be.equal(0);
});
});
it('should call callback with proper context', function() {
var collection = new BemCollection([Block.create()]),
spy = sinon.spy(),
ctx = {};
collection.map(spy, ctx);
spy.should.be.calledOn(ctx);
});
it('should return proper result', function() {
var b1 = Block.create({ m : 'v1' }),
b2 = Block.create({ m : 'v2' }),
collection = new BemCollection([b1, b2]);
collection.map(function(entity) {
return entity.getMod('m');
}).should.be.eql(['v1', 'v2']);
});
});
describe('reduce', function() {
it('should call callback for every entity if no initial passed', function() {
var collection = new BemCollection([Block.create(), Block.create()]),
spy = sinon.spy();
collection.reduce(spy);
spy.should.be.calledOnce;
});
it('should call callback for every entity and initial value if it is passed', function() {
var collection = new BemCollection([Block.create(), Block.create()]),
spy = sinon.spy();
collection.reduce(spy, Block.create());
spy.should.be.calledTwice;
});
it('should call callback with proper arguments', function() {
var b1 = Block.create({ m : 'v1' }),
collection = new BemCollection([b1]);
collection.reduce(function(res, entity) {
res.should.be.equal('');
entity.should.be.equal(b1);
}, '');
});
it('should return proper result', function() {
var b1 = Block.create({ m : 'v1' }),
b2 = Block.create({ m : 'v2' }),
collection = new BemCollection([b1, b2]);
collection.reduce(function(res, entity) {
return res + entity.getMod('m');
}, '').should.be.equal('v1v2');
});
});
describe('reduceRight', function() {
it('should call callback for every entity if no initial passed', function() {
var collection = new BemCollection([Block.create(), Block.create()]),
spy = sinon.spy();
collection.reduceRight(spy);
spy.should.be.calledOnce;
});
it('should call callback for every entity and initial value if it is passed', function() {
var collection = new BemCollection([Block.create(), Block.create()]),
spy = sinon.spy();
collection.reduceRight(spy, Block.create());
spy.should.be.calledTwice;
});
it('should call callback with proper arguments', function() {
var b1 = Block.create({ m : 'v1' }),
collection = new BemCollection([b1]);
collection.reduceRight(function(res, entity) {
res.should.be.equal('');
entity.should.be.equal(b1);
}, '');
});
it('should return proper result', function() {
var b1 = Block.create({ m : 'v1' }),
b2 = Block.create({ m : 'v2' }),
collection = new BemCollection([b1, b2]);
collection.reduceRight(function(res, entity) {
return res + entity.getMod('m');
}, '').should.be.equal('v2v1');
});
});
describe('concat', function() {
it('should return proper value', function() {
var b1 = Block.create(),
b2 = Block.create(),
b3 = Block.create(),
b4 = Block.create(),
collection = new BemCollection([b1]).concat(b2, new BemCollection([b3]), [b4]);
collection.get(0).should.be.equal(b1);
collection.get(1).should.be.equal(b2);
collection.get(2).should.be.equal(b3);
collection.get(3).should.be.equal(b4);
});
});
describe('filter', function() {
it('should call callback for every entity', function() {
var collection = new BemCollection([Block.create(), Block.create()]),
spy = sinon.spy();
collection.filter(spy);
spy.should.be.calledTwice;
});
it('should call callback with proper arguments', function() {
var b1 = Block.create(),
collection = new BemCollection([b1]);
collection.filter(function(entity, i) {
entity.should.be.equal(b1);
i.should.be.equal(0);
});
});
it('should call callback with proper context', function() {
var collection = new BemCollection([Block.create()]),
spy = sinon.spy(),
ctx = {};
collection.filter(spy, ctx);
spy.should.be.calledOn(ctx);
});
it('should return proper result', function() {
var b1 = Block.create(),
b2 = Block.create({ m : 'v1' }),
b3 = Block.create(),
collection = new BemCollection([b1, b2, b3]),
res;
res = collection.filter(function(entity) {
return entity.hasMod('m');
});
res.get(0).should.be.equal(b2);
});
});
describe('some', function() {
it('should return proper result', function() {
var collection = new BemCollection([Block.create()]);
collection.some(function() { return true; }).should.be.ok;
collection.some(function() { return false; }).should.not.be.ok;
new BemCollection([]).some(function() { return true; }).should.not.be.ok;
});
it('should not call callback for every item if valid item present', function() {
var collection = new BemCollection([
Block.create(),
Block.create()
]),
stub = sinon.stub().returns(true);
collection.some(stub);
stub.should.be.calledOnce;
});
it('should call callback for every item if no valid item present', function() {
var collection = new BemCollection([
Block.create(),
Block.create()
]),
stub = sinon.stub().returns(false);
collection.some(stub);
stub.should.be.calledTwice;
});
it('should call callback with proper arguments', function() {
var b1 = Block.create(),
collection = new BemCollection([b1]);
collection.some(function(entity, i) {
entity.should.be.equal(b1);
i.should.be.equal(0);
});
});
});
describe('every', function() {
it('should return proper result', function() {
var collection1 = new BemCollection([Block.create()]);
collection1.every(function() { return true; }).should.be.ok;
collection1.every(function() { return false; }).should.not.be.ok;
var collection2 = new BemCollection([]);
collection2.every(function() { return true; }).should.be.ok;
collection2.every(function() { return false; }).should.be.ok;
});
it('should call callback for every item if all items valid', function() {
var collection = new BemCollection([
Block.create(),
Block.create()
]),
stub = sinon.stub().returns(true);
collection.every(stub);
stub.should.be.calledTwice;
});
it('should not call callback for every item if invalid item present', function() {
var collection = new BemCollection([
Block.create(),
Block.create()
]),
stub = sinon.stub().returns(true);
stub.onFirstCall().returns(false);
collection.every(stub);
stub.should.be.calledOnce;
});
it('should call callback with proper arguments', function() {
var b1 = Block.create(),
collection = new BemCollection([b1]);
collection.every(function(entity, i) {
entity.should.be.equal(b1);
i.should.be.equal(0);
});
});
});
describe('find', function() {
it('should return proper result', function() {
var b1 = Block.create(),
collection = new BemCollection([b1]);
collection.find(function() { return true; }).should.be.equal(b1);
expect(collection.find(function() { return false; })).to.be.null;
});
it('should not call callback for every item if valid item present', function() {
var collection = new BemCollection([
Block.create(),
Block.create()
]),
stub = sinon.stub().returns(true);
collection.find(stub);
stub.should.be.calledOnce;
});
it('should call callback for every item if no valid item present', function() {
var collection = new BemCollection([
Block.create(),
Block.create()
]),
stub = sinon.stub().returns(false);
collection.find(stub);
stub.should.be.calledTwice;
});
it('should call callback with proper arguments', function() {
var b1 = Block.create(),
collection = new BemCollection([b1]);
collection.find(function(entity, i, thisCollection) {
entity.should.be.equal(b1);
i.should.be.equal(0);
thisCollection.should.be.equal(collection);
});
});
});
});
describe('for each entity', function() {
var entities, collection;
beforeEach(function() {
entities = [
Block.create(),
Block.create({ m1 : 'v1' }),
Block.create({ m1 : 'v2' })
];
collection = new BemCollection(entities);
});
it('setMod', function() {
collection.setMod('m1', 'v3');
entities.every(function(entity) {
return entity.hasMod('m1', 'v3');
}).should.be.true;
});
it('delMod', function() {
collection.delMod('m1');
entities.every(function(entity) {
return !entity.hasMod('m1');
}).should.be.true;
});
it('toggleMod', function() {
collection.toggleMod('m1', 'v1', 'v2');
entities[1].hasMod('m1', 'v2').should.be.true;
entities[2].hasMod('m1', 'v1').should.be.true;
});
});
describe('*HasMod', function() {
var collection;
beforeEach(function() {
collection = new BemCollection([
Block.create({ m1 : 'v1' }),
Block.create({ m1 : 'v1', m2 : 'v2' })
]);
});
it('everyHasMod', function() {
collection.everyHasMod('m1', 'v1').should.be.true;
collection.everyHasMod('m2').should.be.false;
});
it('someHasMod', function() {
collection.someHasMod('m2', 'v2').should.be.true;
collection.someHasMod('m3').should.be.false;
});
});
});
provide();
});
================================================
FILE: common.blocks/i-bem/__internal/i-bem__internal.ru.title.txt
================================================
Модуль для внутренних хелперов
================================================
FILE: common.blocks/i-bem/__internal/i-bem__internal.spec.js
================================================
modules.define('spec', ['i-bem__internal'], function(provide, bemInternal) {
describe('i-bem__internal', function() {
describe('buildClassName', function() {
[
{
title : 'block class name should be valid',
input : ['b-foo'],
output : 'b-foo'
},
{
title : 'elem class name should be valid',
input : ['b-foo', 'elem'],
output : 'b-foo__elem'
},
{
title : 'block with mod class name should be valid',
input : ['b-foo', 'mod1', 'val1'],
output : 'b-foo_mod1_val1'
},
{
title : 'block with number mod class name should be valid',
input : ['b-foo', 'mod1', 5],
output : 'b-foo_mod1_5'
},
{
title : 'block with zero number mod class name should be valid',
input : ['b-foo', 'mod1', 0],
output : 'b-foo_mod1_0'
},
{
title : 'block with undefined elem mod class name should be valid',
input : ['b-foo', undefined, 'mod1', 'val1'],
output : 'b-foo_mod1_val1'
},
{
title : 'block with truly boolean mod class name should be valid',
input : ['b-foo', 'mod1', true],
output : 'b-foo_mod1'
},
{
title : 'block with falsy boolean mod class name should be valid',
input : ['b-foo', 'mod1', false],
output : 'b-foo'
},
{
title : 'elem with mod class name should be valid',
input : ['b-foo', 'elem', 'mod1', 'val1'],
output : 'b-foo__elem_mod1_val1'
},
{
title : 'elem with number mod class name should be valid',
input : ['b-foo', 'elem', 'mod1', 3],
output : 'b-foo__elem_mod1_3'
},
{
title : 'elem with zero number mod class name should be valid',
input : ['b-foo', 'elem', 'mod1', 0],
output : 'b-foo__elem_mod1_0'
},
{
title : 'elem with truly boolean mod class name should be valid',
input : ['b-foo', 'elem', 'mod1', true],
output : 'b-foo__elem_mod1'
},
{
title : 'elem with falsy boolean mod class name should be valid',
input : ['b-foo', 'elem', 'mod1', false],
output : 'b-foo__elem'
}
].forEach(function(spec) {
it(spec.title, function() {
bemInternal.buildClassName.apply(bemInternal, spec.input).should.to.equal(spec.output);
});
});
});
describe('buildClassNames', function() {
[
{
title : 'block class names should be valid',
input : ['b-foo'],
output : 'b-foo'
},
{
title : 'elem class names should be valid',
input : ['b-foo', 'elem'],
output : 'b-foo__elem'
},
{
title : 'block with mods class name should be valid',
input : ['b-foo', { mod1 : 'val1', mod2 : 'val2', mod3 : true, mod4 : false }],
output : 'b-foo b-foo_mod1_val1 b-foo_mod2_val2 b-foo_mod3'
},
{
title : 'block with undefined elem and mods class name should be valid',
input : ['b-foo', undefined, { mod1 : 'val1', mod2 : 'val2', mod3 : true, mod4 : false }],
output : 'b-foo b-foo_mod1_val1 b-foo_mod2_val2 b-foo_mod3'
},
{
title : 'elem with mods class name should be valid',
input : ['b-foo', 'elem', { mod1 : 'val1', mod2 : 'val2', mod3 : true, mod4 : false }],
output : 'b-foo__elem b-foo__elem_mod1_val1 b-foo__elem_mod2_val2 b-foo__elem_mod3'
}
].forEach(function(spec) {
it(spec.title, function() {
bemInternal.buildClassNames.apply(bemInternal, spec.input).should.to.equal(spec.output);
});
});
});
});
provide();
});
================================================
FILE: common.blocks/i-bem/__internal/i-bem__internal.vanilla.js
================================================
/**
* @module i-bem__internal
*/
/**
* Separator for modifiers and their values
* @const
* @type String
*/
const MOD_DELIM = '_'
/**
* Separator between names of a block and a nested element
* @const
* @type String
*/
const ELEM_DELIM = '__'
/**
* Pattern for acceptable element and modifier names
* @const
* @type String
*/
const NAME_PATTERN = '[a-zA-Z0-9-]+'
function isSimple(obj) {
const typeOf = typeof obj
return typeOf === 'string' || typeOf === 'number' || typeOf === 'boolean'
}
function buildModPostfix(modName, modVal) {
let res = ''
if(modVal != null && modVal !== false) {
res += MOD_DELIM + modName
modVal !== true && (res += MOD_DELIM + modVal)
}
return res
}
function buildBlockClassName(name, modName, modVal) {
return name + buildModPostfix(modName, modVal)
}
function buildElemClassName(block, name, modName, modVal) {
return buildBlockClassName(block, undefined, undefined) +
ELEM_DELIM + name +
buildModPostfix(modName, modVal)
}
export default {
NAME_PATTERN,
MOD_DELIM,
ELEM_DELIM,
buildModPostfix,
/**
* Builds the class name of a block or element with a modifier
* @param {String} block Block name
* @param {String} [elem] Element name
* @param {String} [modName] Modifier name
* @param {String|Number} [modVal] Modifier value
* @returns {String} Class name
*/
buildClassName(block, elem, modName, modVal) {
if(isSimple(modName)) {
if(!isSimple(modVal)) {
modVal = modName
modName = elem
elem = undefined
}
} else if(typeof modName !== 'undefined') {
modName = undefined
} else if(elem && typeof elem !== 'string') {
elem = undefined
}
if(!(elem || modName)) { // optimization for simple case
return block
}
return elem?
buildElemClassName(block, elem, modName, modVal) :
buildBlockClassName(block, modName, modVal)
},
/**
* Builds full class names for a buffer or element with modifiers
* @param {String} block Block name
* @param {String} [elem] Element name
* @param {Object} [mods] Modifiers
* @returns {String} Class
*/
buildClassNames(block, elem, mods) {
if(elem && typeof elem !== 'string') {
mods = elem
elem = undefined
}
let res = elem?
buildElemClassName(block, elem, undefined, undefined) :
buildBlockClassName(block, undefined, undefined)
if(mods) {
for(const [modName, modVal] of Object.entries(mods)) {
if(modVal) {
res += ' ' + (elem?
buildElemClassName(block, elem, modName, modVal) :
buildBlockClassName(block, modName, modVal))
}
}
}
return res
}
}
================================================
FILE: common.blocks/i-bem/i-bem.deps.js
================================================
({
shouldDeps : [
{ elem : 'internal' },
'inherit',
'identify',
'next-tick',
'objects',
'functions'
]
})
================================================
FILE: common.blocks/i-bem/i-bem.en.md
================================================
# i-bem
A helper block for creating other blocks.
The block is implemented as a specialized JavaScript framework for web development using the BEM methodology.
There is a separate document with a detailed [user's guide](https://en.bem.info/technology/i-bem/v4/i-bem-js/).
================================================
FILE: common.blocks/i-bem/i-bem.en.title.txt
================================================
Helper to create other blocks
================================================
FILE: common.blocks/i-bem/i-bem.ru.md
================================================
# i-bem
Блок-хелпер, позволяющий создавать другие блоки.
Реализация блока представляет собой специализированный JavaScript-фреймворк для веб-разработки в рамках методологии БЭМ.
В виде отдельного документа доступно [подробное руководство пользователя](https://ru.bem.info/technology/i-bem/v4/i-bem-js/).
================================================
FILE: common.blocks/i-bem/i-bem.ru.title.txt
================================================
Помощник для создания других блоков
================================================
FILE: common.blocks/i-bem/i-bem.spec.js
================================================
modules.define('spec', [
'i-bem',
'sinon',
'objects'
], function(provide,
bem,
sinon,
objects
) {
describe('i-bem', function() {
afterEach(function() {
objects.each(bem.entities, function(_, entityName) {
delete bem.entities[entityName];
});
});
describe('decl', function() {
it('should enable to declare block', function() {
var Block = bem.declBlock('block', {});
Block.should.be.equal(bem.entities['block']);
Block.getEntityName().should.be.equal('block');
(new Block()).should.be.instanceOf(bem.Block);
});
it('should enable to declare element', function() {
var Elem = bem.declElem('block', 'elem', {});
Elem.should.be.equal(bem.entities['block__elem']);
Elem.getEntityName().should.be.equal('block__elem');
(new Elem()).should.be.instanceOf(bem.Elem);
});
it('should enable to inherit block', function() {
var Block = bem.declBlock('block', {}),
Block2 = bem.declBlock('block2', Block, {});
(new Block2()).should.be.instanceOf(Block);
(new Block2()).should.be.instanceOf(Block2);
});
it('should enable to inherit block to itself', function() {
var spy1 = sinon.spy(),
spy2 = sinon.spy(),
Block = bem.declBlock('block', {
onSetMod : {
js : {
inited : spy1
}
}
}),
Block2 = bem.declBlock('block', {
onSetMod : {
js : {
inited : spy2
}
}
});
Block.create();
Block2.should.be.equal(Block);
spy1.should.not.have.been.called;
spy2.should.have.been.called;
});
it('should enable to inherit block to itself using entity class', function() {
var spy1 = sinon.spy(),
spy2 = sinon.spy(),
Block = bem.declBlock('block', {
onSetMod : {
js : {
inited : spy1
}
}
}),
Block2 = bem.declBlock(Block, {
onSetMod : {
js : {
inited : spy2
}
}
});
Block.create();
Block2.should.be.equal(Block);
spy1.should.not.have.been.called;
spy2.should.have.been.called;
});
it('should enable to inherit elem to itself', function() {
var spy1 = sinon.spy(),
spy2 = sinon.spy(),
Elem = bem.declElem('block', 'elem', {
onSetMod : {
js : {
inited : spy1
}
}
}),
Elem2 = bem.declElem('block', 'elem', {
onSetMod : {
js : {
inited : spy2
}
}
});
Elem.create();
Elem2.should.be.equal(Elem);
spy1.should.not.have.been.called;
spy2.should.have.been.called;
});
it('should enable to inherit elem to itself using entity', function() {
var spy1 = sinon.spy(),
spy2 = sinon.spy(),
Elem = bem.declElem('block', 'elem', {
onSetMod : {
js : {
inited : spy1
}
}
}),
Elem2 = bem.declElem(Elem, {
onSetMod : {
js : {
inited : spy2
}
}
});
Elem.create();
Elem2.should.be.equal(Elem);
spy1.should.not.have.been.called;
spy2.should.have.been.called;
});
it('should enable to mix block', function() {
var MixBlock = bem.declMixin({}),
Block = bem.declBlock('block', MixBlock, {}),
block = Block.create();
(new Block()).should.be.instanceOf(bem.Block);
});
it('should enable to mix blocks', function() {
var MixBlock1 = bem.declMixin({}),
MixBlock2 = bem.declMixin({}),
Block = bem.declBlock('block', [MixBlock1, MixBlock2], {});
(new Block()).should.be.instanceOf(bem.Block);
});
it('should enable to inherit and mix blocks', function() {
var MixBlock = bem.declMixin({}),
BaseBlock = bem.declBlock('base-block', {}),
Block = bem.declBlock('block', [BaseBlock, MixBlock], {});
(new Block()).should.be.instanceOf(bem.Block);
});
it('should enable to declare modifier', function() {
var Block = bem.declBlock('block', {}),
Block2 = Block.declMod({ modName : 'm1', modVal : 'v1' }, {});
Block2.should.be.equal(Block);
});
it('should apply method only if block has mod', function() {
var baseMethodSpy = sinon.spy(),
modsMethodSpy = sinon.spy(),
Block = bem
.declBlock('block', { method : baseMethodSpy })
.declMod({ modName : 'mod1', modVal : 'val1' }, { method : modsMethodSpy }),
instance = new Block({ mod1 : 'val1' });
instance.method();
baseMethodSpy.should.not.have.been.called;
modsMethodSpy.should.have.been.calledOnce;
instance.setMod('mod1', 'val2');
instance.method();
baseMethodSpy.should.have.been.calledOnce;
modsMethodSpy.should.have.been.calledOnce;
});
it('should apply method only if block has boolean mod', function() {
var baseMethodSpy = sinon.spy(),
modsMethodSpy = sinon.spy(),
Block = bem
.declBlock('block', { method : baseMethodSpy })
.declMod({ modName : 'mod1', modVal : true }, { method : modsMethodSpy }),
instance = new Block({ mod1 : true });
instance.method();
baseMethodSpy.should.not.have.been.called;
modsMethodSpy.should.have.been.calledOnce;
instance.delMod('mod1');
instance.method();
baseMethodSpy.should.have.been.calledOnce;
modsMethodSpy.should.have.been.calledOnce;
});
it('should apply method if block has any mod', function() {
var baseMethodSpy = sinon.spy(),
modsMethodSpy = sinon.spy(),
Block = bem
.declBlock('block', { method : baseMethodSpy })
.declMod({ modName : 'mod1', modVal : '*' }, { method : modsMethodSpy }),
instance = new Block({ mod1 : 'val1' });
instance.method();
baseMethodSpy.should.not.have.been.called;
modsMethodSpy.should.have.been.calledOnce;
instance.setMod('mod1', 'val2');
instance.method();
baseMethodSpy.should.not.have.been.called;
modsMethodSpy.should.have.been.calledTwice;
instance.delMod('mod1');
instance.method();
baseMethodSpy.should.have.been.calledOnce;
modsMethodSpy.should.have.been.calledTwice;
});
});
describe('create', function() {
it('should return instance of block', function() {
var Block = bem.declBlock('block', {}),
instance = Block.create();
instance.should.be.instanceOf(Block);
});
it('should return instance of element with proper block', function() {
var Block = bem.declBlock('block', {}),
block = Block.create(),
Elem = bem.declElem('block', 'elem', {}),
elem = Elem.create(block);
elem.should.be.instanceOf(Elem);
elem._block().should.be.instanceOf(Block);
});
});
describe('mods', function() {
var block;
beforeEach(function() {
block = bem
.declBlock('block', {})
.create({ mod1 : 'val1', mod2 : true, mod3 : false });
});
describe('getMod', function() {
it('should return current mod\'s value', function() {
block.getMod('mod1').should.be.equal('val1');
});
it('should return current boolean mod\'s value', function() {
block.getMod('mod2').should.be.true;
block.getMod('mod3').should.be.equal('');
});
it('should return \'\' for undefined mod', function() {
block.getMod('mod4').should.be.equal('');
});
});
describe('setMod', function() {
it('should update mod value', function() {
block
.setMod('mod1', 'val2')
.getMod('mod1')
.should.be.equal('val2');
});
it('should update boolean mod value', function() {
block
.setMod('mod1', true)
.getMod('mod1')
.should.be.true;
block
.setMod('mod1', false)
.getMod('mod1')
.should.be.equal('');
block
.setMod('mod1')
.getMod('mod1')
.should.be.true;
});
it('should cast non-boolean mod value to string', function() {
block
.setMod('mod1', 0)
.getMod('mod1').should.be.equal('0');
});
});
describe('delMod', function() {
it('should set mod\'s value to \'\'', function() {
block
.delMod('mod1')
.getMod('mod1')
.should.be.equal('');
});
});
describe('hasMod', function() {
it('should return true for matching mod\'s value', function() {
block.hasMod('mod1', 'val1').should.be.true;
});
it('should return false for non-matching mod\'s value', function() {
block.hasMod('mod1', 'val2').should.be.false;
});
it('should return false for undefined mod\'s value', function() {
block.hasMod('mod2', 'val2').should.be.false;
});
it('in short form should return true for non-empty mod\'s value', function() {
block.hasMod('mod1').should.be.true;
});
it('in short form should return false for empty mod\'s value', function() {
block
.setMod('mod1', '')
.hasMod('mod1')
.should.be.false;
});
it('in short form should return false for undefined mod', function() {
block.hasMod('mod4').should.be.false;
});
it('should return true for matching boolean mod\'s value', function() {
block
.setMod('mod1', true)
.hasMod('mod1').should.be.true;
block.hasMod('mod1', true).should.be.true;
});
it('should not treat passed but undefined mod value as a short form', function() {
var modVal;
block.hasMod('mod1', modVal).should.be.false;
});
it('should treat defined non-boolean mod value as a string', function() {
block
.setMod('mod1', 0)
.hasMod('mod1', 0)
.should.be.true;
block.hasMod('mod1', '0')
.should.be.true;
block
.setMod('mod1', '1')
.hasMod('mod1', 1)
.should.be.true;
});
});
describe('toggleMod', function() {
it('should switch mod\'s values', function() {
block
.toggleMod('mod1', 'val1', 'val2')
.hasMod('mod1', 'val2')
.should.be.true;
block
.toggleMod('mod1', 'val1', 'val2')
.hasMod('mod1', 'val1')
.should.be.true;
});
it('should switch mod\'s value if "modVal2" param omited', function() {
block
.toggleMod('mod1', 'val1')
.hasMod('mod1')
.should.be.false;
block
.toggleMod('mod1', 'val1')
.hasMod('mod1', 'val1')
.should.be.true;
});
it('should switch boolean mod\'s value', function() {
block
.toggleMod('mod2')
.hasMod('mod2')
.should.be.false;
block
.toggleMod('mod2')
.hasMod('mod2')
.should.be.true;
});
it('should switch mod\'s values according to "condition" param', function() {
block
.toggleMod('mod1', 'val1', 'val2', true)
.hasMod('mod1', 'val1')
.should.be.true;
block
.toggleMod('mod1', 'val1', 'val2', false)
.hasMod('mod1', 'val2')
.should.be.true;
});
it('should switch mod\'s value according to "condition" param if "modVal2" param omited', function() {
block
.toggleMod('mod1', 'val1', true)
.hasMod('mod1', 'val1')
.should.be.true;
block
.toggleMod('mod1', 'val1', false)
.hasMod('mod1')
.should.be.false;
});
it('should work with numeric modVal', function() {
block.delMod('mod1');
block
.toggleMod('mod1', 1)
.hasMod('mod1', '1')
.should.be.true;
block
.toggleMod('mod1', 1)
.hasMod('mod1')
.should.be.false;
block
.toggleMod('mod1', 1)
.hasMod('mod1', '1')
.should.be.true;
});
it('should switch numeric modVal values', function() {
block.setMod('mod1', 1);
block
.toggleMod('mod1', 1, 2)
.hasMod('mod1', '2')
.should.be.true;
block
.toggleMod('mod1', 1, 2)
.hasMod('mod1', '1')
.should.be.true;
});
});
});
describe('beforeSetMod', function() {
it('should call properly matched callbacks by order', function() {
var order = [],
spyMod1Val2 = sinon.spy(),
spyMod2Val1 = sinon.spy(),
spyMod2Val2 = sinon.spy();
bem.declBlock('block', {
beforeSetMod : {
'mod1' : {
'val1' : function() {
order.push(5);
}
}
}
});
bem.declBlock('block', {
beforeSetMod : {
'mod1' : function() {
order.push(3);
},
'*' : function(modName) {
modName === 'mod1' && order.push(1);
}
}
});
bem.declBlock('block', {
beforeSetMod : function(modName) {
this.__base.apply(this, arguments);
modName === 'mod1' && order.push(2);
}
});
bem.declBlock('block', {
beforeSetMod : {
'mod1' : {
'*' : function() {
this.__base.apply(this, arguments);
order.push(4);
},
'val1' : function() {
this.__base.apply(this, arguments);
order.push(6);
},
'val2' : spyMod1Val2
},
'mod2' : {
'val1' : spyMod2Val1,
'val2' : spyMod2Val2
}
}
});
var block = bem.entities['block'].create({ mod1 : 'val0', mod2 : 'val0' });
block.setMod('mod1', 'val1');
order.should.be.eql([1, 2, 3, 4, 5, 6]);
spyMod1Val2.should.not.have.been.called;
spyMod2Val1.should.not.have.been.called;
spyMod2Val2.should.not.have.been.called;
});
it('should properly call callbacks for special modifier value `!`-syntax', function() {
var spyMod1ValStar = sinon.spy(),
spyMod1NotVal1 = sinon.spy(),
spyMod1NotVal2 = sinon.spy(),
Block = bem.declBlock('block', {
beforeSetMod : {
'mod1' : {
'*' : spyMod1ValStar,
'!val1' : spyMod1NotVal1,
'!val2' : spyMod1NotVal2
}
}
}),
block = Block.create();
block.setMod('mod1', 'val1');
spyMod1ValStar.should.have.been.called;
spyMod1NotVal1.should.not.have.been.called;
spyMod1NotVal2.should.have.been.called;
});
it('should properly call callbacks for special modifier value `~`-syntax', function() {
var spyMod1ValStar = sinon.spy(),
spyMod1DelVal1 = sinon.spy(),
spyMod1DelVal2 = sinon.spy(),
Block = bem.declBlock('block', {
beforeSetMod : {
'mod1' : {
'*' : spyMod1ValStar,
'~val1' : spyMod1DelVal1,
'~val2' : spyMod1DelVal2
}
}
}),
block = Block.create();
block.setMod('mod1', 'val1');
spyMod1ValStar.should.have.been.called;
spyMod1DelVal1.should.not.have.been.called;
spyMod1DelVal2.should.not.have.been.called;
block.setMod('mod1', 'val2');
spyMod1ValStar.should.have.been.calledTwice;
spyMod1DelVal1.should.have.been.called;
spyMod1DelVal2.should.not.have.been.called;
});
it('should call callbacks before set mod', function(done) {
bem
.declBlock('block', {
beforeSetMod : {
'mod1' : {
'val1' : function() {
this.hasMod('mod1', 'val1').should.be.false;
done();
}
}
}
})
.create({ mod1 : 'val0' })
.setMod('mod1', 'val1');
});
it('should set mod after callbacks', function() {
bem
.declBlock('block', {
beforeSetMod : {
'mod1' : {
'val1' : function() {}
}
}
})
.create({ mod1 : 'val0' })
.setMod('mod1', 'val1')
.hasMod('mod1', 'val1')
.should.be.true;
});
it('shouldn\'t set mod when callback returns false', function() {
bem
.declBlock('block', {
beforeSetMod : {
'mod1' : {
'val1' : function() {
return false;
}
}
}
})
.create({ mod1 : 'val0' })
.setMod('mod1', 'val1')
.hasMod('mod1', 'val1')
.should.be.false;
});
it('shouldn\'t set mod when callback for special `!`-syntax value returns false', function() {
var block = bem.declBlock('block', {
beforeSetMod : {
'mod1' : {
'*' : function() {
return false;
},
'!val1' : function() {
return false;
}
},
'mod2' : {
'*' : function() {
return false;
},
'!val1' : function() {}
},
'mod3' : {
'*' : function() {},
'!val1' : function() {
return false;
}
}
}
}).create();
block.setMod('mod1', 'val2').hasMod('mod1', 'val2')
.should.be.false;
block.setMod('mod2', 'val2').hasMod('mod2', 'val2')
.should.be.false;
block.setMod('mod3', 'val2').hasMod('mod3', 'val2')
.should.be.false;
block.setMod('mod3', 'val1').hasMod('mod3', 'val1')
.should.be.true;
});
it('shouldn\'t set mod when callback for special `~`-syntax value returns false', function() {
var block = bem.declBlock('block', {
beforeSetMod : {
'mod1' : {
'*' : function() {
return false;
},
'~val1' : function() {
return false;
}
},
'mod2' : {
'*' : function() {
return false;
},
'~val1' : function() {}
},
'mod3' : {
'*' : function() {},
'~val1' : function() {
return false;
}
}
}
}).create({ mod1 : 'val1', mod2 : 'val1', mod3 : 'val1' });
block.setMod('mod1', 'val2').hasMod('mod1', 'val2')
.should.be.false;
block.setMod('mod2', 'val2').hasMod('mod2', 'val2')
.should.be.false;
block.setMod('mod3', 'val2').hasMod('mod3', 'val2')
.should.be.false;
});
});
describe('onSetMod', function() {
it('should call properly matched callbacks by order', function() {
var order = [],
spyMod1Val2 = sinon.spy(),
spyMod2Val1 = sinon.spy(),
spyMod2Val2 = sinon.spy();
bem.declBlock('block', {
onSetMod : {
'mod1' : {
'val1' : function() {
order.push(5);
}
}
}
});
bem.declBlock('block', {
onSetMod : {
'mod1' : function() {
order.push(3);
},
'*' : function(modName) {
modName === 'mod1' && order.push(1);
}
}
});
bem.declBlock('block', {
onSetMod : function(modName) {
this.__base.apply(this, arguments);
modName === 'mod1' && order.push(2);
}
});
bem.declBlock('block', {
onSetMod : {
'mod1' : {
'*' : function() {
this.__base.apply(this, arguments);
order.push(4);
},
'val1' : function() {
this.__base.apply(this, arguments);
order.push(6);
},
'val2' : spyMod1Val2
},
'mod2' : {
'val1' : spyMod2Val1,
'val2' : spyMod2Val2
}
}
});
bem.entities['block']
.create({ mod1 : 'val0', mod2 : 'val0' })
.setMod('mod1', 'val1');
order.should.be.eql([1, 2, 3, 4, 5, 6]);
spyMod1Val2.should.not.have.been.called;
spyMod2Val1.should.not.have.been.called;
spyMod2Val2.should.not.have.been.called;
});
it('should properly call callbacks for special modifier value `!`-syntax', function() {
var spyMod1ValStar = sinon.spy(),
spyMod1NotVal1 = sinon.spy(),
spyMod1NotVal2 = sinon.spy(),
Block = bem.declBlock('block', {
onSetMod : {
'mod1' : {
'*' : spyMod1ValStar,
'!val1' : spyMod1NotVal1,
'!val2' : spyMod1NotVal2
}
}
}),
block = Block.create();
block.setMod('mod1', 'val1');
spyMod1ValStar.should.have.been.called;
spyMod1NotVal1.should.not.have.been.called;
spyMod1NotVal2.should.have.been.called;
});
it('should properly call callbacks for special modifier value `~`-syntax', function() {
var spyMod1ValStar = sinon.spy(),
spyMod1DelVal1 = sinon.spy(),
spyMod1DelVal2 = sinon.spy(),
Block = bem.declBlock('block', {
onSetMod : {
'mod1' : {
'*' : spyMod1ValStar,
'~val1' : spyMod1DelVal1,
'~val2' : spyMod1DelVal2
}
}
}),
block = Block.create();
block.setMod('mod1', 'val1');
spyMod1ValStar.should.have.been.called;
spyMod1DelVal1.should.not.have.been.called;
spyMod1DelVal2.should.not.have.been.called;
block.setMod('mod1', 'val2');
spyMod1ValStar.should.have.been.calledTwice;
spyMod1DelVal1.should.have.been.called;
spyMod1DelVal2.should.not.have.been.called;
});
it('should call callbacks after set mod', function(done) {
bem
.declBlock('block', {
onSetMod : {
'mod1' : {
'val1' : function() {
this.hasMod('mod1', 'val1').should.be.true;
done();
}
}
}
})
.create({ mod1 : 'val0' })
.setMod('mod1', 'val1');
});
it('shouldn\'t call callbacks if beforeSetMod cancel set mod', function() {
var spy = sinon.spy();
bem
.declBlock('block', {
beforeSetMod : {
'mod1' : {
'val1' : function() {
return false;
}
}
},
onSetMod : {
'mod1' : {
'val1' : spy
}
}
})
.create({ mod1 : 'val0' })
.setMod('mod1', 'val1');
spy.should.not.have.been.called;
});
it('should properly call callbacks for declaration with mod', function() {
var spy1 = sinon.spy(),
spy2 = sinon.spy(),
spy3 = sinon.spy(),
spy4 = sinon.spy(),
Block = bem.declBlock('block');
Block.declMod({ modName : 'm1', modVal : 'v1' }, {
onSetMod : {
'm1' : {
'v1' : spy1,
'v2' : spy2
}
}
});
Block.declMod({ modName : 'm1', modVal : 'v2' }, {
onSetMod : {
'm1' : {
'v1' : function() {
this.__base.apply(this, arguments);
spy3.apply(this, arguments);
},
'v2' : function() {
this.__base.apply(this, arguments);
spy4.apply(this, arguments);
}
}
}
});
var block = Block.create();
block.setMod('m1', 'v1');
spy1.should.have.been.called;
spy2.should.not.have.been.called;
spy3.should.not.have.been.called;
spy4.should.not.have.been.called;
block.setMod('m1', 'v2');
spy1.should.have.been.calledOnce;
spy2.should.have.been.called;
spy3.should.not.have.been.called;
spy4.should.have.been.called;
block
.setMod('m1', 'v3')
.setMod('m1', 'v2');
spy1.should.have.been.calledOnce;
spy2.should.have.been.calledOnce;
spy3.should.not.have.been.called;
spy4.should.have.been.calledTwice;
});
});
describe('beforeSetMod/onSetMod for boolean mods', function() {
it('should call properly matched callbacks for boolean mods by order', function() {
var order = [],
spyMod1Val2 = sinon.spy(),
spyMod2ValFalse = sinon.spy(),
spyMod2Val2 = sinon.spy();
bem.declBlock('block', {
beforeSetMod : {
'mod1' : {
'true' : function(modName, modVal, oldModVal) {
modVal.should.be.true;
oldModVal.should.be.equal('');
order.push(5);
}
}
},
onSetMod : {
'mod1' : {
'true' : function() {
order.push(11);
}
}
}
});
bem.declBlock('block', {
beforeSetMod : {
'mod1' : function() {
order.push(3);
},
'*' : function(modName) {
modName === 'mod1' && order.push(1);
}
},
onSetMod : {
'mod1' : function() {
order.push(9);
},
'*' : function(modName) {
modName === 'mod1' && order.push(7);
}
}
});
bem.declBlock('block', {
beforeSetMod : function(modName) {
this.__base.apply(this, arguments);
modName === 'mod1' && order.push(2);
},
onSetMod : function(modName) {
this.__base.apply(this, arguments);
modName === 'mod1' && order.push(8);
}
});
bem.declBlock('block', {
beforeSetMod : {
'mod1' : {
'*' : function(modName, modVal, oldModVal) {
this.__base.apply(this, arguments);
order.push(4);
},
'true' : function() {
this.__base.apply(this, arguments);
order.push(6);
},
'val2' : function() {
spyMod1Val2();
}
},
'mod2' : {
'' : function(modName, modVal, oldModVal) {
modVal.should.be.equal('');
oldModVal.should.be.true;
spyMod2ValFalse();
},
'val2' : function() {
spyMod2Val2();
}
}
},
onSetMod : {
'mod1' : {
'*' : function() {
this.__base.apply(this, arguments);
order.push(10);
},
'true' : function() {
this.__base.apply(this, arguments);
order.push(12);
},
'val2' : spyMod1Val2
},
'mod2' : {
'' : spyMod2ValFalse,
'val2' : spyMod2Val2
}
}
});
var block = bem.entities['block'].create({ mod1 : false, mod2 : true });
block.setMod('mod1', true);
spyMod1Val2.should.not.have.been.called;
spyMod2ValFalse.should.not.have.been.called;
spyMod2Val2.should.not.have.been.called;
order.should.be.eql([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
block.setMod('mod2', false);
spyMod2ValFalse.should.have.been.calledTwice;
});
});
describe('_nextTick', function() {
var block;
beforeEach(function() {
block = bem
.declBlock('block', {})
.create({ mod1 : 'val1' });
});
it('should call callback asynchronously', function(done) {
var isAsync = false;
block._nextTick(function() {
isAsync.should.be.true;
done();
});
isAsync = true;
});
it('should call callback with block\'s context', function(done) {
block._nextTick(function() {
this.should.be.equal(block);
done();
});
});
it('should not call callback if block destructed', function(done) {
var spy = sinon.spy();
block._nextTick(spy);
block.delMod('js');
setTimeout(function() {
spy.called.should.be.false;
done();
}, 0);
});
});
});
provide();
});
================================================
FILE: common.blocks/i-bem/i-bem.vanilla.js
================================================
/**
* @module i-bem
*/
import bemInternal from 'bem:i-bem__internal'
import inherit from 'bem:inherit'
import identify from 'bem:identify'
import nextTick from 'bem:next-tick'
import objects from 'bem:objects'
import functions from 'bem:functions'
const ELEM_DELIM = bemInternal.ELEM_DELIM
/**
* Storage for block init functions
* @private
* @type Array
*/
let initFns = []
/**
* Storage for block declarations (hash by block name)
* @private
* @type Object
*/
const entities = {}
/**
* Builds the name of the handler method for setting a modifier
* @param {String} prefix
* @param {String} modName Modifier name
* @param {String} modVal Modifier value
* @returns {String}
*/
function buildModFnName(prefix, modName, modVal) {
return '__' + prefix +
'__mod' +
(modName? '_' + modName : '') +
(modVal? '_' + modVal : '')
}
/**
* Builds the function for the handler method for setting a modifier
* for special syntax
* @param {String} modVal Declared modifier value
* @param {Function} curModFn Declared modifier handler
* @param {Function} [prevModFn] Previous handler
* @param {Function} [condition] Condition function
* (called with declared, set and previous modifier values)
* @returns {Function}
*/
function buildSpecialModFn(modVal, curModFn, prevModFn, condition) {
return prevModFn || condition?
function(_modName, _modVal, _prevModVal) {
let res1, res2
prevModFn &&
(res1 = prevModFn.apply(this, arguments) === false)
;(condition? condition(modVal, _modVal, _prevModVal) : true) &&
(res2 = curModFn.apply(this, arguments) === false)
if(res1 || res2) return false
} :
curModFn
}
const specialModConditions = {
'!' : function(modVal, _modVal, _prevModVal) {
return _modVal !== modVal
},
'~' : function(modVal, _modVal, _prevModVal) {
return _prevModVal === modVal
}
}
/**
* Transforms a hash of modifier handlers to methods
* @param {String} prefix
* @param {Object} modFns
* @param {Object} props
*/
function modFnsToProps(prefix, modFns, props) {
if(functions.isFunction(modFns)) {
props[buildModFnName(prefix, '*', '*')] = modFns
} else {
for(const [modName, modFn] of Object.entries(modFns)) {
if(functions.isFunction(modFn)) {
props[buildModFnName(prefix, modName, '*')] = modFn
} else {
const starModFnName = buildModFnName(prefix, modName, '*')
for(const [modValKey, curModFn] of Object.entries(modFn)) {
let modVal = modValKey
const modValPrefix = modVal[0]
if(modValPrefix === '!' || modValPrefix === '~' || modVal === '*') {
modVal === '*' || (modVal = modVal.substr(1))
props[starModFnName] = buildSpecialModFn(
modVal,
curModFn,
props[starModFnName],
specialModConditions[modValPrefix])
} else {
props[buildModFnName(prefix, modName, modVal)] = curModFn
}
}
}
}
}
}
function buildCheckMod(modName, modVal) {
/* treat modVal: false as modVal: '' (modifier removal) — #1457 */
if(modVal === false) modVal = ''
return modVal != null?
Array.isArray(modVal)?
function(block) {
for(const val of modVal)
if(checkMod(block, modName, val))
return true
return false
} :
function(block) {
return checkMod(block, modName, modVal)
} :
function(block) {
return checkMod(block, modName, true)
}
}
function checkMod(block, modName, modVal) {
const prevModVal = block._processingMods[modName]
// check if a block has either current or previous modifier value equal to passed modVal
return modVal === '*'?
block.hasMod(modName) || prevModVal != null :
block.hasMod(modName, modVal) || prevModVal === modVal
}
function convertModHandlersToMethods(props) {
for(const [key, prefix] of [['beforeSetMod', 'before'], ['onSetMod', 'after']]) {
if(props[key]) {
modFnsToProps(prefix, props[key], props)
delete props[key]
}
}
}
function declEntity(baseCls, entityName, base, props, staticProps) {
base || (base = entities[entityName] || baseCls)
Array.isArray(base) || (base = [base])
if(!base[0].__bemEntity) {
base = base.slice()
base.unshift(entities[entityName] || baseCls)
}
props && convertModHandlersToMethods(props)
let entityCls
entityName === base[0].getEntityName()?
// makes a new "init" if the old one was already executed
(entityCls = inherit.self(base, props, staticProps))._processInit(true) :
(entityCls = entities[entityName] = inherit(base, props, staticProps))
return entityCls
}
/**
* @class BemEntity
* @description Base block for creating BEM blocks
*/
const BemEntity = inherit(/** @lends BemEntity.prototype */ {
/**
* @constructor
* @private
* @param {Object} mods BemEntity modifiers
* @param {Object} params BemEntity parameters
* @param {Boolean} [initImmediately=true]
*/
__constructor : function(mods, params, initImmediately) {
/**
* Cache of modifiers
* @member {Object}
* @private
*/
this._modCache = mods || {}
/**
* Current modifiers in the stack
* @member {Object}
* @private
*/
this._processingMods = {}
/**
* BemEntity parameters, taking into account the defaults
* @member {Object}
* @readonly
*/
this.params = objects.extend(this._getDefaultParams(), params)
/**
* @member {String} Unique entity ID
* @private
*/
this._uniqId = this.params.uniqId || identify(this)
initImmediately !== false?
this._setInitedMod() :
initFns.push(this._setInitedMod, this)
},
/**
* Initializes a BEM entity
* @private
*/
_setInitedMod : function() {
return this.setMod('js', 'inited')
},
/**
* Checks whether a BEM entity has a modifier
* @param {String} modName Modifier name
* @param {String|Boolean} [modVal] Modifier value. If not of type String or Boolean, it is casted to String
* @returns {Boolean}
*/
hasMod : function(modName, modVal) {
const typeModVal = typeof modVal
typeModVal === 'undefined' || typeModVal === 'boolean' || (modVal = modVal.toString())
const res = this.getMod(modName) === (modVal || '')
return arguments.length === 1? !res : res
},
/**
* Returns the value of the modifier of the BEM entity
* @param {String} modName Modifier name
* @returns {String} Modifier value
*/
getMod : function(modName) {
const modCache = this._modCache
return modName in modCache?
modCache[modName] || '' :
modCache[modName] = this._extractModVal(modName)
},
/**
* Sets the modifier for a BEM entity
* @param {String} modName Modifier name
* @param {String|Boolean} [modVal=true] Modifier value. If not of type String or Boolean, it is casted to String
* @returns {BemEntity} this
*/
setMod : function(modName, modVal) {
const typeModVal = typeof modVal
if(typeModVal === 'undefined') {
modVal = true
} else if(typeModVal === 'boolean') {
modVal === false && (modVal = '')
} else {
modVal = modVal.toString()
}
if(this._processingMods[modName] != null) return this
const curModVal = this.getMod(modName)
if(curModVal === modVal) return this
this._processingMods[modName] = curModVal
let needSetMod = true
const modFnParams = [modName, modVal, curModVal]
const modVars = [['*', '*'], [modName, '*'], [modName, modVal]]
const prefixes = ['before', 'after']
for(const prefix of prefixes) {
for(const modVar of modVars) {
if(this._callModFn(prefix, modVar[0], modVar[1], modFnParams) === false) {
needSetMod = false
break
}
}
if(!needSetMod) break
if(prefix === 'before') {
this._modCache[modName] = modVal
this._onSetMod(modName, modVal, curModVal)
}
}
this._processingMods[modName] = null
needSetMod && this._afterSetMod(modName, modVal, curModVal)
return this
},
/**
* Sets multiple modifiers at once
* @param {Object} mods Hash of modifiers (modName: modVal)
* @returns {BemEntity} this
*/
setMods : function(mods) {
for(const modName of Object.keys(mods))
this.setMod(modName, mods[modName])
return this
},
/**
* @protected
* @param {String} modName Modifier name
* @param {String} modVal Modifier value
* @param {String} oldModVal Old modifier value
*/
_onSetMod : function(modName, modVal, oldModVal) {},
/**
* @protected
* @param {String} modName Modifier name
* @param {String} modVal Modifier value
* @param {String} oldModVal Old modifier value
*/
_afterSetMod : function(modName, modVal, oldModVal) {},
/**
* Sets a modifier for a BEM entity, depending on conditions.
* If the condition parameter is passed: when true, modVal1 is set; when false, modVal2 is set.
* If the condition parameter is not passed: modVal1 is set if modVal2 was set, or vice versa.
* @param {String} modName Modifier name
* @param {String} [modVal1=true] First modifier value, optional for boolean modifiers
* @param {String} [modVal2] Second modifier value
* @param {Boolean} [condition] Condition
* @returns {BemEntity} this
*/
toggleMod : function(modName, modVal1, modVal2, condition) {
if(typeof modVal1 === 'undefined') {
modVal1 = true // boolean mod
} else if(typeof modVal1 !== 'boolean') {
modVal1 = modVal1.toString()
}
if(typeof modVal2 === 'undefined') {
modVal2 = ''
} else if(typeof modVal2 === 'boolean') {
condition = modVal2
modVal2 = ''
} else {
modVal2 = modVal2.toString()
}
const modVal = this.getMod(modName)
;(modVal === modVal1 || modVal === modVal2) &&
this.setMod(
modName,
typeof condition === 'boolean'?
(condition? modVal1 : modVal2) :
this.hasMod(modName, modVal1)? modVal2 : modVal1)
return this
},
/**
* Removes a modifier from a BEM entity
* @param {String} modName Modifier name
* @returns {BemEntity} this
*/
delMod : function(modName) {
return this.setMod(modName, '')
},
/**
* Executes handlers for setting modifiers
* @private
* @param {String} prefix
* @param {String} modName Modifier name
* @param {String} modVal Modifier value
* @param {Array} modFnParams Handler parameters
*/
_callModFn : function(prefix, modName, modVal, modFnParams) {
const modFnName = buildModFnName(prefix, modName, modVal)
return this[modFnName]?
this[modFnName].apply(this, modFnParams) :
undefined
},
_extractModVal : function(modName) {
return ''
},
/**
* Returns a BEM entity's default parameters
* @protected
* @returns {Object}
*/
_getDefaultParams : function() {
return {}
},
/**
* Executes given callback on next turn eventloop in BEM entity's context
* @protected
* @param {Function} fn callback
* @returns {BemEntity} this
*/
_nextTick : function(fn) {
nextTick(() => {
this.hasMod('js', 'inited') && fn.call(this)
})
return this
}
}, /** @lends BemEntity */{
/**
* Factory method for creating an instance
* @param {Object} mods modifiers
* @param {Object} params params
* @returns {BemEntity}
*/
create : function(mods, params) {
return new this(mods, params)
},
/**
* Declares modifier
* @param {Object} mod
* @param {String} mod.modName
* @param {String|Boolean|Array} [mod.modVal]
* @param {Object} props
* @param {Object} [staticProps]
* @returns {Function}
*/
declMod : function(mod, props, staticProps) {
props && convertModHandlersToMethods(props)
const checkMod = buildCheckMod(mod.modName, mod.modVal)
const basePtp = this.prototype
objects.each(props, function(prop, name) {
functions.isFunction(prop) &&
(props[name] = function() {
let method
if(checkMod(this)) {
method = prop
} else {
const baseMethod = basePtp[name]
baseMethod && baseMethod !== prop &&
(method = this.__base)
}
return method?
method.apply(this, arguments) :
undefined
})
})
return inherit.self(this, props, staticProps)
},
__bemEntity : true,
_name : null,
/**
* Processes a BEM entity's init
* @private
* @param {Boolean} [heedInit=false] Whether to take into account that the BEM entity already processed its init property
*/
_processInit : function(heedInit) {
this._inited = true
},
/**
* Returns the name of the current BEM entity
* @returns {String}
*/
getName : function() {
return this._name
},
/**
* Returns the name of the current BEM entity
* @returns {String}
*/
getEntityName : function() {
return this._name
}
})
/**
* @class Block
* @description Class for creating BEM blocks
* @augments BemEntity
*/
const Block = BemEntity
/**
* @class Elem
* @description Class for creating BEM elems
* @augments BemEntity
*/
const Elem = inherit(BemEntity, /** @lends Elem.prototype */ {
/**
* Returns the own block of current element
* @protected
* @returns {Block}
*/
_block : function() {
return this._blockInstance
}
}, /** @lends Elem */{
/**
* Factory method for creating an instance
* @param {Object} block block instance
* @param {Object} mods modifiers
* @param {Object} params params
* @returns {BemEntity}
*/
create : function(block, mods, params) {
const res = new this(mods, params)
res._blockInstance = block
return res
},
/**
* Returns the name of the current BEM entity
* @returns {String}
*/
getEntityName : function() {
return this._blockName + ELEM_DELIM + this._name
}
})
export default {
/**
* Block class
* @type Function
*/
Block,
/**
* Elem class
* @type Function
*/
Elem,
/**
* Storage for block declarations (hash by block name)
* @type Object
*/
entities,
/**
* Declares block and creates a block class
* @param {String|Function} blockName Block name or block class
* @param {Function|Array.} [base] base block + mixes
* @param {Object} [props] Methods
* @param {Object} [staticProps] Static methods
* @returns {Function} Block class
*/
declBlock(blockName, base, props, staticProps) {
if(typeof base === 'object' && !Array.isArray(base)) {
staticProps = props
props = base
base = undefined
}
let baseCls = Block
if(typeof blockName !== 'string') {
baseCls = blockName
blockName = blockName.getEntityName()
}
const res = declEntity(baseCls, blockName, base, props, staticProps)
res._name = res._blockName = blockName
return res
},
/**
* Declares elem and creates an elem class
* @param {String} [blockName] Block name
* @param {String|Function} elemName Elem name or elem class
* @param {Function|Function[]} [base] base elem + mixes
* @param {Object} [props] Methods
* @param {Object} [staticProps] Static methods
* @returns {Function} Elem class
*/
declElem(blockName, elemName, base, props, staticProps) {
let baseCls = Elem
let entityName
if(typeof blockName !== 'string') {
staticProps = props
props = base
base = elemName
elemName = blockName._name
baseCls = blockName
blockName = baseCls._blockName
entityName = baseCls.getEntityName()
} else {
entityName = blockName + ELEM_DELIM + elemName
}
if(typeof base === 'object' && !Array.isArray(base)) {
staticProps = props
props = base
base = undefined
}
const res = declEntity(baseCls, entityName, base, props, staticProps)
res._blockName = blockName
res._name = elemName
return res
},
/**
* Declares mixin
* @param {Object} [props] Methods
* @param {Object} [staticProps] Static methods
* @returns {Function} mix
*/
declMixin(props, staticProps) {
convertModHandlersToMethods(props || (props = {}))
return inherit(props, staticProps)
},
/**
* Executes the block init functions
* @private
*/
_runInitFns() {
if(initFns.length) {
const fns = initFns
initFns = []
for(let i = 0; i < fns.length; i += 2) {
fns[i].call(fns[i + 1])
}
}
}
}
================================================
FILE: common.blocks/i-bem-dom/__collection/i-bem-dom__collection.deps.js
================================================
[{
shouldDeps : [
'inherit',
{ block : 'i-bem', elem : 'collection' }
]
},
{
tech : 'spec.js',
shouldDeps : [
{ block : 'i-bem', tech : 'js' },
{ block : 'objects', tech : 'js' },
{ block : 'i-bem-dom', tech : 'js' }
]
}]
================================================
FILE: common.blocks/i-bem-dom/__collection/i-bem-dom__collection.js
================================================
/**
* @module i-bem-dom__collection
*/
import inherit from 'bem:inherit'
import BemCollection from 'bem:i-bem__collection'
/**
* @class BemDomCollection
*/
const BemDomCollection = inherit(BemCollection, /** @lends BemDomCollection.prototype */{
/**
* Finds the first child block for every entities in collection
* @param {Function|Object} Block Block class or description (block, modName, modVal) of the block to find
* @returns {BemDomCollection}
*/
findChildBlock : buildProxyMethodForOne('findChildBlock'),
/**
* Finds child block for every entities in collections
* @param {Function|Object} Block Block class or description (block, modName, modVal) of the block to find
* @returns {BemDomCollection}
*/
findChildBlocks : buildProxyMethodForMany('findChildBlocks'),
/**
* Finds the first parent block for every entities in collection
* @param {Function|Object} Block Block class or description (block, modName, modVal) of the block to find
* @returns {BemDomCollection}
*/
findParentBlock : buildProxyMethodForOne('findParentBlock'),
/**
* Finds parent block for every entities in collections
* @param {Function|Object} Block Block class or description (block, modName, modVal) of the block to find
* @returns {BemDomCollection}
*/
findParentBlocks : buildProxyMethodForMany('findParentBlocks'),
/**
* Finds first mixed bloc for every entities in collectionk
* @param {Function|Object} Block Block class or description (block, modName, modVal) of the block to find
* @returns {BemDomCollection}
*/
findMixedBlock : buildProxyMethodForOne('findMixedBlock'),
/**
* Finds mixed block for every entities in collections
* @param {Function|Object} Block Block class or description (block, modName, modVal) of the block to find
* @returns {BemDomCollection}
*/
findMixedBlocks : buildProxyMethodForMany('findMixedBlocks'),
/**
* Finds the first child elemen for every entities in collectiont
* @param {Function|String|Object} Elem Element class or name or description elem, modName, modVal
* @param {Boolean} [strictMode=false]
* @returns {BemDomCollection}
*/
findChildElem : buildProxyMethodForOne('findChildElem'),
/**
* Finds child element for every entities in collections
* @param {Function|String|Object} Elem Element class or name or description elem, modName, modVal
* @param {Boolean} [strictMode=false]
* @returns {BemDomCollection}
*/
findChildElems : buildProxyMethodForMany('findChildElems'),
/**
* Finds the first parent elemen for every entities in collectiont
* @param {Function|String|Object} Elem Element class or name or description elem, modName, modVal
* @param {Boolean} [strictMode=false]
* @returns {BemDomCollection}
*/
findParentElem : buildProxyMethodForOne('findParentElem'),
/**
* Finds parent element for every entities in collections
* @param {Function|String|Object} Elem Element class or name or description elem, modName, modVal
* @param {Boolean} [strictMode=false]
* @returns {BemDomCollection}
*/
findParentElems : buildProxyMethodForMany('findParentElems'),
/**
* Finds the first mixed elemen for every entities in collectiont
* @param {Function|String|Object} Elem Element class or name or description elem, modName, modVal
* @returns {BemDomCollection}
*/
findMixedElem : buildProxyMethodForOne('findMixedElem'),
/**
* Finds mixed element for every entities in collections
* @param {Function|String|Object} Elem Element class or name or description elem, modName, modVal
* @returns {BemDomCollection}
*/
findMixedElems : buildProxyMethodForMany('findMixedElems'),
/**
* Checks whether any entity in collection has a modifier.
* @param {String} modName Modifier name
* @param {String|Boolean} [modVal] Modifier value
* @returns {Boolean}
*/
hasMod : buildAggregateProxyFn('some', 'hasMod'),
/**
* Returns an array of modifier values from all entities in collection.
* @param {String} modName Modifier name
* @returns {Array}
*/
getMod : buildMapProxyFn('getMod'),
/**
* Checks whether any entity in collection contains the specified entity.
* @param {BemDomEntity} entity entity to check
* @returns {Boolean}
*/
containsEntity : buildAggregateProxyFn('some', 'containsEntity')
})
function collectionMapMethod(collection, methodName, args) {
return collection.map((entity) => entity[methodName].apply(entity, args))
}
function buildProxyMethodForOne(methodName) {
return function() {
return new BemDomCollection(collectionMapMethod(this, methodName, arguments))
}
}
function buildAggregateProxyFn(arrayMethodName, entityMethodName) {
return function(...args) {
return this._entities[arrayMethodName]((entity) =>
entity[entityMethodName].apply(entity, args))
}
}
function buildMapProxyFn(entityMethodName) {
return function(...args) {
return this._entities.map((entity) =>
entity[entityMethodName].apply(entity, args))
}
}
function buildProxyMethodForMany(methodName) {
return function() {
const res = []
collectionMapMethod(this, methodName, arguments).forEach((collection) => {
collection.forEach((entity) => {
res.push(entity)
})
})
return new BemDomCollection(res)
}
}
export default BemDomCollection
================================================
FILE: common.blocks/i-bem-dom/__collection/i-bem-dom__collection.spec.js
================================================
modules.define('spec', [
'i-bem',
'i-bem-dom',
'i-bem-dom__collection',
'objects',
'BEMHTML'
], function(provide,
bem,
bemDom,
BemDomCollection,
objects,
BEMHTML
) {
describe('BEM collections', function() {
var rootNode;
afterEach(function() {
if(rootNode) {
bemDom.destruct(rootNode);
rootNode = null;
}
objects.each(bem.entities, function(_, entityName) {
delete bem.entities[entityName];
});
});
describe('find', function() {
var rootBlock, B1Block, B2Block, B3Block;
beforeEach(function() {
var RootBlock = bemDom.declBlock('root');
B1Block = bemDom.declBlock('b1');
B2Block = bemDom.declBlock('b2');
B3Block = bemDom.declBlock('b3');
rootNode = bemDom.init(BEMHTML.apply({
block : 'root',
content : [
{
block : 'b1',
js : { id : 'b1_1' },
mix : { block : 'b3', js : { id : 'b3_1' } },
content : [
{
block : 'b2',
js : { id : 'b2_1' }
},
{
block : 'b2',
js : { id : 'b2_2' }
},
]
},
{
block : 'b1',
js : { id : 'b1_2' },
mix : { block : 'b3', js : { id : 'b3_2' } },
content : [
{
block : 'b2',
js : { id : 'b2_3' }
},
{
block : 'b2',
js : { id : 'b2_4' }
},
]
}
]
}));
rootBlock = rootNode.bem(RootBlock);
});
describe('findChildBlock', function() {
it('should find the first child block for every entities in collection', function() {
var res = rootBlock.findChildBlocks(B1Block).findChildBlock(B2Block);
res.should.be.instanceOf(BemDomCollection);
getEntityIds(res).should.be.eql(['b2_1', 'b2_3']);
});
});
describe('findChildBlocks', function() {
it('should find child block for every entities in collection', function() {
var res = rootBlock.findChildBlocks(B1Block).findChildBlocks(B2Block);
res.should.be.instanceOf(BemDomCollection);
getEntityIds(res).should.be.eql(['b2_1', 'b2_2', 'b2_3', 'b2_4']);
});
});
describe('findParentBlock', function() {
it('should find the first parent block for every entities in collection', function() {
var res = rootBlock.findChildBlocks(B2Block).findParentBlock(B1Block);
res.should.be.instanceOf(BemDomCollection);
getEntityIds(res).should.be.eql(['b1_1', 'b1_2']);
});
});
describe('findParentBlocks', function() {
it('should find parent block for every entities in collection', function() {
var res = rootBlock.findChildBlocks(B2Block).findParentBlocks(B1Block);
res.should.be.instanceOf(BemDomCollection);
getEntityIds(res).should.be.eql(['b1_1', 'b1_2']);
});
});
describe('findMixedBlock', function() {
it('should find the first parent block for every entities in collection', function() {
var res = rootBlock.findChildBlocks(B1Block).findMixedBlock(B3Block);
res.should.be.instanceOf(BemDomCollection);
getEntityIds(res).should.be.eql(['b3_1', 'b3_2']);
});
});
describe('findMixedBlocks', function() {
it('should find parent block for every entities in collection', function() {
var res = rootBlock.findChildBlocks(B1Block).findMixedBlocks(B3Block);
res.should.be.instanceOf(BemDomCollection);
getEntityIds(res).should.be.eql(['b3_1', 'b3_2']);
});
});
});
});
function getEntityIds(entities) {
return entities.map(function(entity) {
return entity.params && entity.params.id;
});
}
provide();
});
================================================
FILE: common.blocks/i-bem-dom/__events/_type/i-bem-dom__events_type_bem.deps.js
================================================
[{
shouldDeps : [
'inherit',
'jquery',
'events',
{ block : 'i-bem', elem : 'internal' }
]
},
{
tech : 'spec.js',
shouldDeps : [
{
block : 'i-bem',
tech : 'js'
},
{
block : 'i-bem-dom',
tech : 'js'
},
{
block : 'events',
tech : 'js'
}
]
}]
================================================
FILE: common.blocks/i-bem-dom/__events/_type/i-bem-dom__events_type_bem.js
================================================
/**
* @module i-bem-dom__events_type_bem
*/
import bemDomEvents from 'bem:i-bem-dom__events';
import bemInternal from 'bem:i-bem__internal';
import inherit from 'bem:inherit';
import functions from 'bem:functions';
import $ from 'bem:jquery';
import events from 'bem:events';
const EVENT_PREFIX = '__bem__',
MOD_CHANGE_EVENT = 'modchange',
specialEvents = $.event.special,
specialEventsStorage = {},
createSpecialEvent = function(event) {
return {
setup : function() {
specialEventsStorage[event] || (specialEventsStorage[event] = true);
},
teardown : functions.noop
};
},
eventBuilder = function(e, params) {
const event = EVENT_PREFIX + params.bindEntityCls.getEntityName() +
(typeof e === 'object'?
e instanceof events.Event?
e.type :
/* treat modVal: false as modVal: '' (modifier removal) — #1457 */
bemInternal.buildModPostfix(e.modName, e.modVal === false? '' : e.modVal) :
e);
specialEvents[event] ||
(specialEvents[event] = createSpecialEvent(event));
return event;
},
/**
* @class EventManagerFactory
* @augments i-bem-dom__events:EventManagerFactory
* @exports i-bem-dom__events_type_bem:EventManagerFactory
*/
EventManagerFactory = inherit(bemDomEvents.EventManagerFactory,/** @lends EventManagerFactory.prototype */{
/** @override */
_createEventManager : function(ctx, params, isInstance) {
function wrapperFn(fn, fnCtx, fnId) {
return function(e, data, flags, originalEvent) {
if(flags.fns[fnId]) return;
let instance,
instanceDomElem;
if(isInstance) {
instance = ctx;
instanceDomElem = instance.domElem;
} else {
// TODO: we could optimize all these "closest" to a single traversing
instanceDomElem = $(e.target).closest(params.ctxSelector);
instanceDomElem.length && (instance = instanceDomElem.bem(ctx));
}
// Skip events that bubbled up from nested blocks of the same type (#1525)
if(!params.bindSelector &&
params.bindDomElem.index(e.target) < 0) return
if(instance &&
(!flags.propagationStoppedDomNode ||
!$.contains(instanceDomElem[0], flags.propagationStoppedDomNode))) {
originalEvent.data = e.data;
// TODO: do we really need both target and bemTarget?
originalEvent.bemTarget = originalEvent.target;
flags.fns[fnId] = true;
fn.call(fnCtx || instance, originalEvent, data);
if(originalEvent.isPropagationStopped()) {
e.stopPropagation();
flags.propagationStoppedDomNode = instanceDomElem[0];
}
}
};
}
return new this._eventManagerCls(params, wrapperFn, eventBuilder);
}
});
export default {
/**
* Emits BEM event
* @augments i-bem-dom__events_type_bem
* @param {BemDomEntity} ctx
* @param {String|Object|events:Event} e Event name
* @param {Object} [data]
*/
emit : function(ctx, e, data) {
let originalEvent;
if(typeof e === 'string') {
originalEvent = new events.Event(e, ctx);
} else if(e.modName) {
originalEvent = new events.Event(MOD_CHANGE_EVENT, ctx);
} else if(!e.target) {
e.target = ctx;
originalEvent = e;
}
const event = eventBuilder(e, { bindEntityCls : ctx.__self });
specialEventsStorage[event] &&
ctx.domElem.trigger(event, [data, { fns : {}, propagationStoppedDomNode : null }, originalEvent]);
},
EventManagerFactory : EventManagerFactory
};
================================================
FILE: common.blocks/i-bem-dom/__events/_type/i-bem-dom__events_type_bem.spec.js
================================================
modules.define('spec', [
'i-bem',
'i-bem-dom',
'objects',
'events',
'jquery',
'chai',
'sinon',
'BEMHTML'
], function(provide,
bem,
bemDom,
objects,
events,
$,
chai,
sinon,
BEMHTML
) {
describe('BEM events', function() {
var Block1, Block2, Block3, block1, spy1, spy2, spy3, spy4, spy5, spy6, spy7, spy8, spy9,
wrapSpy = function(spy) {
return function(e) {
// NOTE: we need to pass bemTarget and data explicitly, as `e` is being
// changed while event is propagating
spy.call(this, e, e.bemTarget, e.data);
};
},
data = { data : 'data' };
beforeEach(function() {
spy1 = sinon.spy();
spy2 = sinon.spy();
spy3 = sinon.spy();
spy4 = sinon.spy();
spy5 = sinon.spy();
spy6 = sinon.spy();
spy7 = sinon.spy();
spy8 = sinon.spy();
spy9 = sinon.spy();
});
afterEach(function() {
bemDom.destruct(bemDom.scope, true);
objects.each(bem.entities, function(_, entityName) {
delete bem.entities[entityName];
});
});
function initDom(bemjson) {
return createDomNode(bemjson).appendTo(bemDom.scope);
}
describe('on instance events', function() {
describe('block BEM events', function() {
beforeEach(function() {
Block1 = bemDom.declBlock('block1', {
onSetMod : {
'js' : {
'inited' : function() {
this._events()
.on('click', spy1)
.on('click', spy2)
.on('click', data, wrapSpy(spy3))
.on({ modName : 'm1', modVal : 'v1' }, spy4)
.on({ modName : 'm1', modVal : '*' }, spy5)
.on({ modName : 'm2', modVal : true }, spy6)
.once('click', spy7);
}
}
}
});
Block2 = bemDom.declBlock('block2', {
onSetMod : {
'js' : {
'inited' : function() {
this._events()
.on('click', spy5);
}
}
}
});
block1 = initDom({
block : 'block1',
mix : { block : 'block2', js : true }
}).bem(Block1);
});
it('should properly bind handlers', function() {
block1._emit('click');
spy1.should.have.been.called;
spy2.should.have.been.called;
spy3.should.have.been.calledOn(block1);
var spy3args = spy3.args[0];
spy3args[0].should.be.instanceOf(events.Event);
spy3args[1].should.be.instanceOf(Block1);
spy3args[1].should.be.equal(block1);
spy3args[2].should.have.been.equal(data);
});
it('should properly bind once handler', function() {
block1._emit('click');
spy7.should.have.been.called;
block1._emit('click');
spy7.should.have.been.calledOnce;
block1._events().once('click', spy7);
block1._emit('click');
spy7.should.have.been.calledTwice;
});
it('should properly bind the same handler', function() {
block1._events()
.on('click', spy8)
.on('click', spy8);
block1._emit('click');
spy8.should.have.been.calledOnce;
block1._events().un('click', spy8);
block1._emit('click');
spy8.should.have.been.calledOnce;
});
it('should not handle homonymous dom event', function() {
block1.domElem.trigger('click');
spy1.should.not.have.been.called;
});
it('should not handle homonymous bem event', function() {
block1._domEvents().on('click', spy8);
block1._emit('click');
spy8.should.not.have.been.called;
});
it('should properly unbind all handlers', function() {
block1._events().un('click');
block1._emit('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
});
it('should properly unbind specified handler', function() {
block1._events().un('click', spy1);
block1._emit('click');
spy1.should.not.have.been.called;
spy3.should.have.been.called;
});
it('should unbind only own handlers', function() {
block1._events().un('click');
block1._emit('click');
block1.findMixedBlock(Block2)._emit('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
spy5.should.have.been.called;
});
it('should properly unbind once handler', function() {
block1._events().un('click', spy7);
block1._emit('click');
spy7.should.not.have.been.called;
});
it('should properly emit event as instance (not string)', function() {
var e = new events.Event('click');
block1._emit(e);
spy1.args[0][0].should.be.equal(e);
});
it('should handle modifier change events', function() {
block1.setMod('m1', 'v1');
spy4.should.have.been.called;
spy4.args[0][0].should.be.instanceOf(events.Event);
var eventData = spy4.args[0][1];
eventData.modName.should.be.equal('m1');
eventData.modVal.should.be.equal('v1');
eventData.oldModVal.should.be.equal('');
spy5.should.have.been.called;
block1.delMod('m1');
spy4.should.have.been.calledOnce;
spy5.should.have.been.calledTwice;
spy6.should.not.have.been.called;
block1.setMod('m2');
spy6.should.have.been.called;
});
it('should emit only destructing event after destruction', function() {
block1._events().on({ modName : 'js', modVal : '' }, spy8);
bemDom.destruct(block1.domElem);
block1.setMod('m1', 'v1');
spy8.should.have.been.called;
spy4.should.not.been.called;
spy5.should.not.been.called;
});
});
describe('block instance events', function() {
var block2_1, block2_2;
beforeEach(function() {
Block1 = bemDom.declBlock('block1', {
onSetMod : {
'js' : {
'inited' : function() {
this._events(block2_1 = this.findChildBlocks(Block2).get(0))
.on('click', spy1)
.on('click', spy2)
.on('click', data, wrapSpy(spy3));
this._events(block2_2 = this.findChildBlocks(Block2).get(1))
.on('click', spy5);
}
}
}
});
Block2 = bemDom.declBlock('block2');
block1 = initDom({
block : 'block1',
content : [
{ block : 'block2' },
{ block : 'block2' }
]
}).bem(Block1);
});
it('should properly bind handlers', function() {
block2_1._emit('click');
spy1.should.have.been.called;
spy2.should.have.been.called;
spy3.should.have.been.calledOn(block1);
spy3.args[0][0].should.be.instanceOf(events.Event);
spy3.args[0][1].should.be.equal(block2_1);
spy3.args[0][2].should.be.equal(data);
spy5.should.not.have.been.called;
});
it('should properly unbind all handlers', function() {
block1._events(block2_1).un('click');
block2_1._emit('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
spy5.should.not.have.been.called;
block2_2._emit('click');
spy5.should.have.been.called;
});
it('should properly unbind specified handler', function() {
block1._events(block2_1).un('click', spy1);
block2_1._emit('click');
spy1.should.not.have.been.called;
spy3.should.have.been.called;
});
});
describe('nested blocks events', function() {
beforeEach(function() {
Block1 = bemDom.declBlock('block', {
onSetMod : {
'js' : {
'inited' : function() {
this._events(Block2).on('click', wrapSpy(spy1));
}
}
}
});
Block2 = bemDom.declBlock('block2');
block1 = initDom({
block : 'block',
content : [
{
block : 'block2',
content : { block : 'block2' }
}
]
}).bem(Block1);
});
it('should properly handle events (bound in class context) from nested block', function() {
var block2 = block1.findChildBlocks(Block2).get(1);
block2._emit('click');
spy1.should.have.been.calledOnce;
spy1.args[0][1].should.be.equal(block2);
});
});
describe('block elems events', function() {
['string', 'Class'].forEach(function(elemType) {
var elem1, elem2;
describe('elem as ' + elemType, function() {
var Elem1, Elem2;
beforeEach(function() {
elem1 = elemType === 'string'?
'e1' :
bemDom.declElem('block', 'e1');
Block1 = bemDom.declBlock('block', {
onSetMod : {
'js' : {
'inited' : function() {
this._events(elem1)
.on('click', spy1)
.on('click', spy2)
.on('click', data, spy3);
this._events('e2').on('click', spy5);
this._events('e6').on('click', spy9);
}
}
}
});
Block2 = bemDom.declBlock('block2');
Elem1 = elemType === 'string'?
bemDom.declElem('block', 'e1') :
elem1;
Elem2 = bemDom.declElem('block', 'e2', {
onSetMod : {
'js' : {
'inited' : function() {
this._events(elem1)
.on('click', wrapSpy(spy6))
.on('click', spy7);
}
}
}
});
block1 = initDom({
block : 'block',
mix : { block : 'block', elem : 'e6' },
content : [
{ elem : 'e1', content : { elem : 'e3' } },
{ elem : 'e2', content : { elem : 'e1' } },
{ elem : 'e4', js : { id : 'ie4' } },
{ elem : 'e4', js : { id : 'ie4' } },
{ elem : 'e5', content : { elem : 'e5' } }
]
}).bem(Block1);
elem2 = block1._elem('e2');
});
describe('block', function() {
it('should properly handle events on elems with multiple DOM nodes', function() {
block1._events('e4').on('click', spy8);
block1._elem('e4')._emit('click');
spy8.should.have.been.calledOnce;
});
it('should properly handle events (bound in class context) from nested elems', function() {
block1._events('e5').on('click', wrapSpy(spy8));
var nestedE5 = block1.findChildElems('e5').get(1);
nestedE5._emit('click');
spy8.should.have.been.calledOnce;
spy8.args[0][0].bemTarget.domElem[0]
.should.be.equal(nestedE5.domElem[0]);
});
it('should properly bind handlers', function() {
block1._elem('e3')._emit('click');
spy1.should.not.have.been.called;
});
it('should properly bind handler to mixed elem', function() {
block1._elem('e6')._emit('click');
spy9.should.have.been.called;
});
it('should properly unbind all handlers', function() {
block1._events(elem1).un('click');
block1._elem(elem1)._emit('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
spy3.should.not.have.been.called;
elem2._emit('click');
spy5.should.have.been.called;
});
it('should properly unbind specified handler', function() {
block1._events(elem1).un('click', spy2);
block1._elem(elem1)._emit('click');
spy1.should.have.been.called;
spy2.should.not.have.been.called;
spy3.should.have.been.called;
});
it('should properly unbind handler from mixed elem', function() {
block1._events('e6').un('click', spy9);
block1._elem('e6')._emit('click');
spy9.should.not.have.been.called;
});
});
describe('elem instance', function() {
it('should properly bind handlers', function() {
var e2elem1 = elem2.findChildElem('e1');
e2elem1._emit('click');
spy6.should.have.been.called;
spy6.should.have.been.calledOn(elem2);
spy6.args[0][1].should.be.instanceOf(Elem1);
spy6.args[0][1].domElem[0]
.should.be.equal(e2elem1.domElem[0]);
spy7.should.have.been.called;
});
it('should properly unbind all handlers', function() {
elem2._events(elem1).un('click');
var e2elem1 = elem2.findChildElem('e1');
e2elem1._emit('click');
spy6.should.not.have.been.called;
spy7.should.not.have.been.called;
});
it('should properly unbind specified handler', function() {
elem2._events(elem1).un('click', spy7);
var e2elem1 = elem2.findChildElem('e1');
e2elem1._emit('click');
spy6.should.have.been.called;
spy7.should.not.have.been.called;
});
});
});
describe('elem as ' + elemType + ', modName, modVal', function() {
beforeEach(function() {
Block1 = bemDom.declBlock('block', {
onSetMod : {
'js' : {
'inited' : function() {
this._events({ elem : elem1 })
.on('click', spy1);
this._events({ elem : elem1, modName : 'm1', modVal : 'v1' })
.on('click', spy2)
.on('click', spy3);
}
}
}
});
block1 = createDomNode({
block : 'block',
content : [
{ elem : 'e1' },
{ elem : 'e1', elemMods : { m1 : 'v1' } }
]
}).bem(Block1);
});
it('should properly bind handlers', function() {
block1._elem({ elem : 'e1', modName : 'm1', modVal : 'v1' })._emit('click');
spy1.should.have.been.called;
spy2.should.have.been.called;
block1._elem('e1')._emit('click');
spy1.should.have.been.calledTwice;
spy2.should.have.been.calledOnce;
});
it('should properly unbind all handlers', function() {
block1._events({ elem : elem1, modName : 'm1', modVal : 'v1' }).un('click');
block1._elem({ elem : 'e1', modName : 'm1', modVal : 'v1' })._emit('click');
spy1.should.have.been.called;
spy2.should.not.have.been.called;
});
it('should properly unbind specified handler', function() {
block1._events({ elem : elem1, modName : 'm1', modVal : 'v1' }).un('click', spy2);
block1._elem({ elem : 'e1', modName : 'm1', modVal : 'v1' })._emit('click');
spy1.should.have.been.called;
spy2.should.not.have.been.called;
spy3.should.have.been.called;
});
});
});
});
describe('collection events', function() {
var collection, block2;
beforeEach(function() {
Block1 = bemDom.declBlock('block1', {
onSetMod : {
'js' : {
'inited' : function() {
collection = this.findChildBlocks(Block2);
block2 = collection.get(0);
this._events(collection)
.on('click', spy1)
.on('click', spy2)
.on('click', data, wrapSpy(spy3));
}
}
}
});
Block2 = bemDom.declBlock('block2');
block1 = initDom({
block : 'block1',
content : { block : 'block2' }
}).bem(Block1);
});
it('should properly bind handlers', function() {
block2._emit('click');
spy1.should.have.been.called;
spy2.should.have.been.called;
spy3.should.have.been.calledOn(block1);
spy3.args[0][0].should.be.instanceOf(events.Event);
spy3.args[0][1].should.be.equal(block2);
spy3.args[0][2].should.be.equal(data);
});
it('should properly unbind all handlers', function() {
block1._events(collection).un('click');
block2._emit('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
});
it('should properly unbind specified handler', function() {
block1._events(collection).un('click', spy1);
block2._emit('click');
spy1.should.not.have.been.called;
spy3.should.have.been.called;
});
});
describe('stop propagation', function() {
beforeEach(function() {
Block1 = bemDom.declBlock('block', {
onSetMod : {
'js' : {
'inited' : function() {
this._events(Block3).on('click', spy1);
}
}
}
});
Block2 = bemDom.declBlock('block2', {
onSetMod : {
'js' : {
'inited' : function() {
this._events(Block3).on('click', function(e) {
e.stopPropagation();
spy2();
});
}
}
}
});
Block3 = bemDom.declBlock('block3');
block1 = initDom({
block : 'block',
content : [
{
block : 'block2',
js : true,
content : { block : 'block3' }
}
]
}).bem(Block1);
});
it('should properly stop propagation', function() {
var block3 = block1.findChildBlock(Block3);
block3._emit('click');
spy2.should.have.been.called;
spy1.should.not.have.been.called;
});
});
});
describe('delegated events', function() {
function initDom(bemjson) {
return createDomNode(bemjson).appendTo(bemDom.scope);
}
describe('block events', function() {
beforeEach(function() {
Block1 = bemDom.declBlock('block1', {}, {
onInit : function() {
this._events()
.on('click', spy1)
.on('click', spy2)
.on('click', data, wrapSpy(spy3))
.once('click', spy4)
.on({ modName : 'm1', modVal : 'v1' }, spy6)
.on({ modName : 'm1', modVal : '*' }, spy7)
.on({ modName : 'm2', modVal : true }, spy8);
}
});
Block2 = bemDom.declBlock('block2', {}, {
onInit : function() {
this._events().on('click', spy5);
}
});
block1 = initDom({
block : 'block1',
mix : { block : 'block2', js : true }
}).bem(Block1);
});
it('should properly bind handlers', function() {
block1._emit('click');
spy1.should.have.been.called;
spy2.should.have.been.called;
spy3.should.have.been.calledOn(block1);
spy3.args[0][1].should.be.instanceOf(Block1);
spy3.args[0][2].should.have.been.equal(data);
spy5.should.not.have.been.called;
});
it('should properly bind once handler', function() {
block1._emit('click');
spy4.should.have.been.called;
block1._emit('click');
spy4.should.have.been.calledOnce;
block1._events().once('click', spy4);
block1._emit('click');
spy4.should.have.been.calledTwice;
});
it('should properly unbind all handlers', function() {
Block1._events().un('click');
block1._emit('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
});
it('should properly unbind specified handler', function() {
Block1._events().un('click', spy1);
block1._emit('click');
spy1.should.not.have.been.called;
spy3.should.have.been.called;
});
it('should unbind only own handlers', function() {
Block1._events().un('click');
block1._emit('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
block1
.findMixedBlock(Block2)
._emit('click');
spy5.should.have.been.called;
});
it('should properly unbind once handler', function() {
Block1._events().un('click', spy4);
block1._emit('click');
spy4.should.not.have.been.called;
});
it('should handle modifier change events', function() {
block1.setMod('m1', 'v1');
spy6.should.have.been.called;
var eventData = spy6.args[0][1];
eventData.modName.should.be.equal('m1');
eventData.modVal.should.be.equal('v1');
eventData.oldModVal.should.be.equal('');
spy7.should.have.been.called;
block1.delMod('m1');
spy6.should.have.been.calledOnce;
spy7.should.have.been.calledTwice;
spy8.should.not.have.been.called;
block1.setMod('m2');
spy8.should.have.been.called;
});
});
describe('block elems events', function() {
['string', 'Class'].forEach(function(elemType) {
var elem1, elem2;
describe('elem as ' + elemType, function() {
var Elem1, Elem2;
beforeEach(function() {
elem1 = elemType === 'string'?
'e1' :
bemDom.declElem('block', 'e1');
Block1 = bemDom.declBlock('block', {}, {
onInit : function() {
this._events(elem1)
.on('click', spy1)
.on('click', spy2)
.on('click', data, wrapSpy(spy3));
this._events('e2').on('click', spy5);
}
});
Elem1 = elemType === 'string'?
bemDom.declElem('block', 'e1') :
elem1;
Elem2 = bemDom.declElem('block', 'e2', {}, {
onInit : function() {
this._events(elem1)
.on('click', wrapSpy(spy6))
.on('click', spy7);
}
});
block1 = initDom({
block : 'block',
content : [
{ elem : 'e1', content : { elem : 'e3' } },
{ elem : 'e2', content : { elem : 'e1' } }
]
}).bem(Block1);
elem2 = block1._elem('e2');
});
describe('block', function() {
it('should properly bind handlers', function() {
block1._elem('e1')._emit('click');
spy1.should.have.been.called;
spy1.should.have.been.calledOn(block1);
spy2.should.have.been.called;
spy3.should.have.been.called;
spy3.args[0][0].data.should.have.been.equal(data);
spy3.args[0][1].should.be.instanceOf(Elem1);
spy3.args[0][1].domElem[0]
.should.be.equal(block1._elem(elem1).domElem[0]);
spy5.should.not.have.been.called;
});
it('should properly unbind all handlers', function() {
Block1._events(elem1).un('click');
block1._elem(elem1)._emit('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
spy3.should.not.have.been.called;
});
it('should properly unbind specified handler', function() {
Block1._events(elem1).un('click', spy2);
block1._elem(elem1)._emit('click');
spy1.should.have.been.called;
spy2.should.not.have.been.called;
spy3.should.have.been.called;
});
});
describe('elem instance', function() {
it('should properly bind handlers', function() {
var e2elem1 = elem2.findChildElem('e1');
e2elem1._emit('click');
spy6.should.have.been.called;
spy6.should.have.been.calledOn(elem2);
spy6.args[0][1].should.be.instanceOf(Elem1);
spy6.args[0][1].domElem[0]
.should.be.equal(e2elem1.domElem[0]);
spy7.should.have.been.called;
});
it('should properly unbind all handlers', function() {
Elem2._events(elem1).un('click');
var e2elem1 = elem2.findChildElem('e1');
e2elem1._emit('click');
spy6.should.not.have.been.called;
spy7.should.not.have.been.called;
});
it('should properly unbind specified handler', function() {
Elem2._events(elem1).un('click', spy7);
var e2elem1 = elem2.findChildElem('e1');
e2elem1._emit('click');
spy6.should.have.been.called;
spy7.should.not.have.been.called;
});
});
});
describe('elem as ' + elemType + ', modName, modVal', function() {
beforeEach(function() {
Block1 = bemDom.declBlock('block', {}, {
onInit : function() {
this._events({ elem : elem1 })
.on('click', spy1);
this._events({ elem : elem1, modName : 'm1', modVal : 'v1' })
.on('click', spy2)
.on('click', spy3);
}
});
block1 = initDom({
block : 'block',
content : [
{ elem : 'e1' },
{ elem : 'e1', elemMods : { m1 : 'v1' } }
]
}).bem(Block1);
});
it('should properly bind handlers', function() {
block1._elem({ elem : 'e1', modName : 'm1', modVal : 'v1' })._emit('click');
spy1.should.have.been.called;
spy2.should.have.been.called;
block1._elem('e1')._emit('click');
spy1.should.have.been.calledTwice;
spy2.should.have.been.calledOnce;
});
it('should properly unbind all handlers', function() {
Block1._events({ elem : elem1, modName : 'm1', modVal : 'v1' }).un('click');
block1._elem({ elem : 'e1', modName : 'm1', modVal : 'v1' })._emit('click');
spy1.should.have.been.called;
spy2.should.not.have.been.called;
});
it('should properly unbind specified handler', function() {
Block1._events({ elem : elem1, modName : 'm1', modVal : 'v1' }).un('click', spy2);
block1._elem({ elem : 'e1', modName : 'm1', modVal : 'v1' })._emit('click');
spy1.should.have.been.called;
spy2.should.not.have.been.called;
spy3.should.have.been.called;
});
});
});
});
describe('stop propagation', function() {
beforeEach(function() {
Block1 = bemDom.declBlock('block', {}, {
onInit : function() {
this._events(Block3).on('click', spy1);
}
});
Block2 = bemDom.declBlock('block2', {}, {
onInit : function() {
this._events(Block3).on('click', function(e) {
e.stopPropagation();
});
}
});
Block3 = bemDom.declBlock('block3');
block1 = initDom({
block : 'block',
content : [
{
block : 'block2',
js : true,
content : { block : 'block3' }
}
]
}).bem(Block1);
});
it('should properly stop propagation', function() {
var block3 = block1.findChildBlock(Block3);
block3._emit('click');
spy1.should.not.have.been.called;
});
});
});
});
describe('same-type nested blocks destruct (#1525)', function() {
var ParentBlock, rootNode;
beforeEach(function() {
ParentBlock = bemDom.declBlock('nested-same', {
onInit : function() {
this._events().on({ modName : 'js', modVal : '' }, this._onDestruct);
},
_onDestruct : function() {}
});
rootNode = createDomNode({
block : 'nested-same',
js : true,
content : {
block : 'nested-same',
js : true
}
});
});
afterEach(function() {
bemDom.destruct(rootNode);
});
it('should not fire parent destruct handler when child of same type is destructed', function() {
var parent = rootNode.bem(ParentBlock),
child = parent.findChildBlock(ParentBlock),
spy = sinon.spy(parent, '_onDestruct');
bemDom.destruct(child.domElem);
spy.should.not.have.been.called;
});
});
provide();
function createDomNode(bemjson) {
return bemDom.init(BEMHTML.apply(bemjson));
}
});
================================================
FILE: common.blocks/i-bem-dom/__events/_type/i-bem-dom__events_type_dom.deps.js
================================================
[{
shouldDeps : [
'identify',
'inherit',
'jquery'
]
},
{
tech : 'spec.js',
shouldDeps : { block : 'i-bem-dom', tech : 'js' }
}]
================================================
FILE: common.blocks/i-bem-dom/__events/_type/i-bem-dom__events_type_dom.js
================================================
/**
* @module i-bem-dom__events_type_dom
*/
import bemDomEvents from 'bem:i-bem-dom__events';
import inherit from 'bem:inherit';
import $ from 'bem:jquery';
const eventBuilder = function(e) {
return e;
},
/**
* @class EventManagerFactory
* @augments i-bem-dom__events:EventManagerFactory
* @exports i-bem-dom__events_type_dom:EventManagerFactory
*/
EventManagerFactory = inherit(bemDomEvents.EventManagerFactory,/** @lends EventManagerFactory.prototype */{
/** @override */
_createEventManager : function(ctx, params, isInstance) {
function wrapperFn(fn) {
return function(e) {
let instance;
if(isInstance) {
instance = ctx;
} else {
// TODO: we could optimize all these "closest" to a single traversing
const entityDomNode = $(e.target).closest(params.ctxSelector);
entityDomNode.length && (instance = entityDomNode.bem(ctx));
}
if(instance) {
params.bindEntityCls && (e.bemTarget = $(this).bem(params.bindEntityCls));
fn.apply(instance, arguments);
}
};
}
return new this._eventManagerCls(params, wrapperFn, eventBuilder);
}
});
export default { EventManagerFactory : EventManagerFactory };
================================================
FILE: common.blocks/i-bem-dom/__events/_type/i-bem-dom__events_type_dom.spec.js
================================================
modules.define('spec', [
'i-bem',
'i-bem-dom',
'objects',
'jquery',
'chai',
'sinon',
'BEMHTML'
], function(provide,
bem,
bemDom,
objects,
$,
chai,
sinon,
BEMHTML
) {
var undef,
expect = chai.expect;
describe('DOM events', function() {
var Block1, Block2, Block3, block1, spy1, spy2, spy3, spy4, spy5, spy6, spy7, spy8,
wrapSpy = function(spy) {
return function(e) {
// NOTE: we need to pass bemTarget and data explicitly, as `e` is being
// changed while event is propagating
spy.call(this, e, e.bemTarget, e.data);
};
},
data = { data : 'data' };
beforeEach(function() {
spy1 = sinon.spy();
spy2 = sinon.spy();
spy3 = sinon.spy();
spy4 = sinon.spy();
spy5 = sinon.spy();
spy6 = sinon.spy();
spy7 = sinon.spy();
spy8 = sinon.spy();
});
afterEach(function() {
bemDom.destruct(bemDom.scope, true);
objects.each(bem.entities, function(_, entityName) {
delete bem.entities[entityName];
});
});
describe('on instance events', function() {
describe('block domElem events', function() {
beforeEach(function() {
Block1 = bemDom.declBlock('block1', {
onSetMod : {
'js' : {
'inited' : function() {
this._domEvents()
.on('click', spy1)
.on('click', spy2)
.on('click', data, wrapSpy(spy3))
.once('click', spy4);
}
}
}
});
Block2 = bemDom.declBlock('block2', {
onSetMod : {
'js' : {
'inited' : function() {
this._domEvents()
.on('click', spy5);
}
}
}
});
block1 = createDomNode({
block : 'block1',
mix : { block : 'block2', js : true }
}).bem(Block1);
});
it('should properly bind handlers', function() {
block1.domElem.trigger('click');
spy1.should.have.been.called;
spy2.should.have.been.called;
spy3.should.have.been.calledOn(block1);
spy3.args[0][1].should.be.instanceOf(Block1);
spy3.args[0][2].should.have.been.equal(data);
});
it('should pass data to handler', function() {
var data = { test : 'data' };
block1.domElem.trigger('click', data);
spy1.args[0][1].should.have.been.equal(data);
});
it('should properly bind once handler', function() {
block1.domElem.trigger('click');
spy4.should.have.been.called;
block1.domElem.trigger('click');
spy4.should.have.been.calledOnce;
block1._domEvents().once('click', spy4);
block1.domElem.trigger('click');
spy4.should.have.been.calledTwice;
});
it('should properly bind the same handler', function() {
block1._domEvents()
.on('click', spy6)
.on('click', spy6);
block1.domElem.trigger('click');
spy6.should.have.been.calledOnce;
block1._domEvents().un('click', spy6);
block1.domElem.trigger('click');
spy6.should.have.been.calledOnce;
});
it('should properly unbind all handlers', function() {
block1._domEvents().un('click');
block1.domElem.trigger('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
});
it('should properly unbind specified handler', function() {
block1._domEvents().un('click', spy1);
block1.domElem.trigger('click');
spy1.should.not.have.been.called;
spy3.should.have.been.called;
});
it('should unbind only own handlers', function() {
block1._domEvents().un('click');
block1.domElem.trigger('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
spy5.should.have.been.called;
});
it('should properly unbind once handler', function() {
block1._domEvents().un('click', spy4);
block1.domElem.trigger('click');
spy4.should.not.have.been.called;
});
it('should unbind single event from multi-event subscription', function() {
var multiSpy = sinon.spy();
block1._domEvents().on('click keypress', multiSpy);
block1._domEvents().un('click', multiSpy);
block1.domElem.trigger('click');
multiSpy.should.not.have.been.called;
block1.domElem.trigger('keypress');
multiSpy.should.have.been.calledOnce;
});
});
describe('block elems events', function() {
['string', 'Class'].forEach(function(elemType) {
var elem1, elem2;
describe('elem as ' + elemType, function() {
var Elem1, Elem2;
beforeEach(function() {
elem1 = elemType === 'string'?
'e1' :
bemDom.declElem('block', 'e1');
Block1 = bemDom.declBlock('block', {
onSetMod : {
'js' : {
'inited' : function() {
this._domEvents(elem1)
.on('click', spy1)
.on('click', spy2)
.on('click', data, wrapSpy(spy3));
this._domEvents('e2').on('click', spy5);
this._domEvents('e4').on('click', spy8);
}
}
}
});
Elem1 = elemType === 'string'?
bemDom.declElem('block', 'e1') :
elem1;
Elem2 = bemDom.declElem('block', 'e2', {
onSetMod : {
'js' : {
'inited' : function() {
this._domEvents(elem1)
.on('click', wrapSpy(spy6))
.on('click', spy7);
}
}
}
});
block1 = createDomNode({
block : 'block',
mix : { block : 'block', elem : 'e4' },
content : [
{ elem : 'e1', content : { elem : 'e3' } },
{ elem : 'e2', content : { elem : 'e1' } }
]
}).bem(Block1);
elem2 = block1._elem('e2');
});
describe('block', function() {
it('should properly bind handlers', function() {
block1._elem('e3').domElem.trigger('click');
spy1.should.have.been.called;
spy1.should.have.been.calledOn(block1);
spy2.should.have.been.called;
spy3.should.have.been.called;
spy3.args[0][2].should.have.been.equal(data);
spy3.args[0][1].should.be.instanceOf(Elem1);
spy3.args[0][1].domElem[0]
.should.be.equal(block1._elem(elem1).domElem[0]);
spy5.should.not.have.been.called;
});
it('should properly bind handlers to mixed elem', function() {
block1._elem('e4').domElem.trigger('click');
spy8.should.have.been.called;
});
it('should properly unbind all handlers', function() {
block1._domEvents(elem1).un('click');
block1._elem(elem1).domElem.trigger('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
spy3.should.not.have.been.called;
elem2.domElem.trigger('click');
spy5.should.have.been.called;
});
it('should properly unbind specified handler', function() {
block1._domEvents(elem1).un('click', spy2);
block1._elem(elem1).domElem.trigger('click');
spy1.should.have.been.called;
spy2.should.not.have.been.called;
spy3.should.have.been.called;
});
it('should properly unbind handlers from mixed elem', function() {
block1._domEvents('e4').un('click', spy8);
block1._elem('e4').domElem.trigger('click');
spy8.should.not.have.been.called;
});
});
describe('elem instance', function() {
it('should properly bind handlers', function() {
var e2elem1 = elem2.findChildElem('e1');
e2elem1.domElem.trigger('click');
spy6.should.have.been.called;
spy6.should.have.been.calledOn(elem2);
spy6.args[0][1].should.be.instanceOf(Elem1);
spy6.args[0][1].domElem[0]
.should.be.equal(e2elem1.domElem[0]);
spy7.should.have.been.called;
});
it('should properly unbind all handlers', function() {
elem2._domEvents(elem1).un('click');
var e2elem1 = elem2.findChildElem('e1');
e2elem1.domElem.trigger('click');
spy6.should.not.have.been.called;
spy7.should.not.have.been.called;
});
it('should properly unbind specified handler', function() {
elem2._domEvents(elem1).un('click', spy7);
var e2elem1 = elem2.findChildElem('e1');
e2elem1.domElem.trigger('click');
spy6.should.have.been.called;
spy7.should.not.have.been.called;
});
});
});
describe('elem as ' + elemType + ', modName, modVal', function() {
beforeEach(function() {
Block1 = bemDom.declBlock('block', {
onSetMod : {
'js' : {
'inited' : function() {
this._domEvents({ elem : elem1 })
.on('click', spy1);
this._domEvents({ elem : elem1, modName : 'm1', modVal : 'v1' })
.on('click', spy2)
.on('click', spy3);
}
}
}
});
block1 = createDomNode({
block : 'block',
content : [
{ elem : 'e1' },
{ elem : 'e1', elemMods : { m1 : 'v1' } }
]
}).bem(Block1);
});
it('should properly bind handlers', function() {
block1._elem({ elem : 'e1', modName : 'm1', modVal : 'v1' }).domElem.trigger('click');
spy1.should.have.been.called;
spy2.should.have.been.called;
block1._elem('e1').domElem.trigger('click');
spy1.should.have.been.calledTwice;
spy2.should.have.been.calledOnce;
});
it('should properly unbind all handlers', function() {
block1._domEvents({ elem : elem1, modName : 'm1', modVal : 'v1' }).un('click');
block1._elem({ elem : 'e1', modName : 'm1', modVal : 'v1' }).domElem.trigger('click');
spy1.should.have.been.called;
spy2.should.not.have.been.called;
});
it('should properly unbind specified handler', function() {
block1._domEvents({ elem : elem1, modName : 'm1', modVal : 'v1' }).un('click', spy2);
block1._elem({ elem : 'e1', modName : 'm1', modVal : 'v1' }).domElem.trigger('click');
spy1.should.have.been.called;
spy2.should.not.have.been.called;
spy3.should.have.been.called;
});
});
});
describe('elem as instance', function() {
var elem1, elem2;
beforeEach(function() {
Block1 = bemDom.declBlock('block', {
onSetMod : {
'js' : {
'inited' : function() {
this._domEvents(this._elem('e1'))
.on('click', spy1)
.on('click', spy2)
.on('click', data, wrapSpy(spy3));
this._domEvents(this._elem('e2')).on('click', spy5);
this._domEvents(this._elems('e1').get(1)).on('click', spy6);
}
}
}
});
block1 = createDomNode({
block : 'block',
content : [
{ elem : 'e1', content : { elem : 'e3' } },
{ elem : 'e2', content : { elem : 'e1' } }
]
}).bem(Block1);
elem1 = block1._elem('e1');
elem2 = block1._elem('e2');
});
describe('block', function() {
it('should properly bind handlers', function() {
block1._elem('e3').domElem.trigger('click');
spy1.should.have.been.called;
spy1.should.have.been.calledOn(block1);
spy2.should.have.been.called;
spy3.should.have.been.called;
spy3.args[0][1].should.be.equal(elem1);
spy3.args[0][2].should.be.equal(data);
spy5.should.not.have.been.called;
});
it('should properly bind handlers on elem with the same name', function() {
block1._elem('e1').domElem.trigger('click');
spy1.should.have.been.called;
spy6.should.not.have.been.called;
});
it('should properly unbind all handlers', function() {
block1._domEvents(elem1).un('click');
elem1.domElem.trigger('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
spy3.should.not.have.been.called;
elem2.domElem.trigger('click');
spy5.should.have.been.called;
});
it('should properly unbind specified handler', function() {
block1._domEvents(elem1).un('click', spy2);
elem1.domElem.trigger('click');
spy1.should.have.been.called;
spy2.should.not.have.been.called;
spy3.should.have.been.called;
});
});
});
});
describe('collection events', function() {
var Elem1, collection;
beforeEach(function() {
Block1 = bemDom.declBlock('block1', {
onSetMod : {
'js' : {
'inited' : function() {
this._domEvents(collection = this.findChildElems(Elem1))
.on('click', spy1)
.on('click', spy2)
.on('click', data, wrapSpy(spy3))
.once('click', spy4);
}
}
}
});
Elem1 = bemDom.declElem('block1', 'elem1');
block1 = createDomNode({
block : 'block1',
content : { elem : 'elem1' }
}).bem(Block1);
});
it('should properly bind handlers', function() {
block1._elem('elem1').domElem.trigger('click');
spy1.should.have.been.called;
spy2.should.have.been.called;
spy3.should.have.been.calledOn(block1);
spy3.args[0][1].should.be.instanceOf(Elem1);
spy3.args[0][2].should.have.been.equal(data);
});
it('should properly bind once handler', function() {
block1._elem('elem1').domElem.trigger('click');
spy4.should.have.been.called;
block1._elem('elem1').domElem.trigger('click');
spy4.should.have.been.calledOnce;
block1._domEvents(collection).once('click', spy4);
block1._elem('elem1').domElem.trigger('click');
spy4.should.have.been.calledTwice;
});
it('should properly bind the same handler', function() {
block1._domEvents(collection)
.on('click', spy6)
.on('click', spy6);
block1._elem('elem1').domElem.trigger('click');
spy6.should.have.been.calledOnce;
block1._domEvents(collection).un('click', spy6);
block1._elem('elem1').domElem.trigger('click');
spy6.should.have.been.calledOnce;
});
it('should properly unbind all handlers', function() {
block1._domEvents(collection).un('click');
block1._elem('elem1').domElem.trigger('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
});
it('should properly unbind specified handler', function() {
block1._domEvents(collection).un('click', spy1);
block1._elem('elem1').domElem.trigger('click');
spy1.should.not.have.been.called;
spy3.should.have.been.called;
});
it('should properly unbind once handler', function() {
block1._domEvents(collection).un('click', spy4);
block1._elem('elem1').domElem.trigger('click');
spy4.should.not.have.been.called;
});
});
describe('document events', function() {
beforeEach(function() {
Block1 = bemDom.declBlock('block', {
onSetMod : {
'js' : {
'inited' : function() {
this._domEvents(document)
.on('click', spy1)
.on('click', spy2);
this._domEvents(bemDom.doc)
.on('click', data, wrapSpy(spy3))
.once('click', spy4);
}
}
}
});
block1 = bemDom.init(BEMHTML.apply({ block : 'block' })).bem(Block1);
});
it('should properly bind handlers', function() {
bemDom.doc.trigger('click');
spy1.should.have.been.called;
spy2.should.have.been.called;
spy3.should.have.been.calledOn(block1);
spy3.args[0][2].should.have.been.equal(data);
});
it('should properly bind once handler', function() {
bemDom.doc.trigger('click');
spy4.should.have.been.called;
bemDom.doc.trigger('click');
spy4.should.have.been.calledOnce;
block1._domEvents(bemDom.doc).once('click', spy4);
bemDom.doc.trigger('click');
spy4.should.have.been.calledTwice;
});
it('should properly unbind all handlers', function() {
block1._domEvents(document).un('click');
bemDom.doc.trigger('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
});
it('should properly unbind specified handler', function() {
block1._domEvents($(document)).un('click', spy1);
bemDom.doc.trigger('click');
spy1.should.not.have.been.called;
spy2.should.have.been.called;
});
it('should properly unbind once handler', function() {
block1._domEvents($(document)).un('click', spy4);
bemDom.doc.trigger('click');
spy4.should.not.have.been.called;
});
it('should properly unbind all handlers on block destruct', function() {
bemDom.destruct(block1.domElem);
bemDom.doc.trigger('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
spy4.should.not.have.been.called;
});
});
describe('window events', function() {
beforeEach(function() {
Block1 = bemDom.declBlock('block', {
onSetMod : {
'js' : {
'inited' : function() {
this._domEvents(window)
.on('resize', spy1)
.on('resize', spy2);
this._domEvents($(window))
.on('resize', data, wrapSpy(spy3))
.once('resize', spy4);
}
}
}
});
block1 = createDomNode({ block : 'block' }).bem(Block1);
});
it('should properly bind handlers', function() {
bemDom.win.trigger('resize');
spy1.should.have.been.called;
spy2.should.have.been.called;
spy3.should.have.been.calledOn(block1);
spy3.args[0][2].should.have.been.equal(data);
});
it('should properly bind once handler', function() {
bemDom.win.trigger('resize');
spy4.should.have.been.called;
bemDom.win.trigger('resize');
spy4.should.have.been.calledOnce;
block1._domEvents(bemDom.win).once('click', spy4);
bemDom.win.trigger('click');
spy4.should.have.been.calledTwice;
});
it('should properly unbind all handlers', function() {
block1._domEvents(window).un('resize');
bemDom.win.trigger('resize');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
});
it('should properly unbind specified handler', function() {
block1._domEvents($(window)).un('resize', spy1);
bemDom.win.trigger('resize');
spy1.should.not.have.been.called;
spy2.should.have.been.called;
});
it('should properly unbind once handler', function() {
block1._domEvents($(window)).un('resize', spy4);
bemDom.win.trigger('resize');
spy4.should.not.have.been.called;
});
it('should properly unbind all handlers on block destruct', function() {
bemDom.destruct(block1.domElem);
bemDom.win.trigger('resize');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
spy4.should.not.have.been.called;
});
});
describe('arbitrary jQuery-chain or DOM-node events', function() {
var rootNode;
beforeEach(function() {
Block1 = bemDom.declBlock('block', {
onSetMod : {
'js' : {
'inited' : function() {
this._domEvents(rootNode[0])
.on('click', spy1)
.on('click', spy2);
this._domEvents(rootNode.find('div').addBack())
.on('dblclick', data, wrapSpy(spy3))
.once('dblclick', spy4);
}
}
}
});
rootNode = createDomNode({
content : {
content : { block : 'block', tag : 'p' }
}
});
block1 = rootNode.find(Block1._buildSelector()).bem(Block1);
});
it('should properly bind handlers', function() {
rootNode.trigger('click');
spy1.should.have.been.calledOnce;
spy2.should.have.been.calledOnce;
rootNode.find('div').trigger('dblclick');
spy3.should.have.been.calledTwice;
spy3.should.have.been.calledOn(block1);
spy3.args[0][2].should.have.been.equal(data);
});
it('should properly bind once handler', function() {
rootNode.trigger('dblclick');
spy4.should.have.been.called;
rootNode.trigger('dblclick');
spy4.should.have.been.calledOnce;
block1._domEvents(rootNode.find('div').addBack()).once('dblclick', spy4);
rootNode.trigger('dblclick');
spy4.should.have.been.calledTwice;
});
it('should properly unbind all handlers', function() {
block1._domEvents(rootNode[0]).un('click');
rootNode.trigger('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
block1._domEvents(rootNode.find('div').addBack()).un('dblclick');
rootNode.find('div').trigger('dblclick');
spy3.should.not.have.been.called;
spy4.should.not.have.been.called;
});
it('should properly unbind specified handler', function() {
block1._domEvents(rootNode[0]).un('click', spy1);
rootNode.trigger('click');
spy1.should.not.have.been.called;
spy2.should.have.been.called;
});
it('should properly unbind once handler', function() {
block1._domEvents(rootNode.find('div').addBack()).un('dblclick', spy4);
rootNode.find('div').trigger('dblclick');
spy4.should.not.have.been.called;
});
it('should properly unbind all handlers on block destruct', function() {
bemDom.destruct(block1.domElem);
rootNode.trigger('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
});
});
});
describe('delegated events', function() {
function initDom(bemjson) {
return createDomNode(bemjson).appendTo(bemDom.scope);
}
describe('block domElem events', function() {
beforeEach(function() {
Block1 = bemDom.declBlock('block1', {}, {
onInit : function() {
this._domEvents()
.on('click', spy1)
.on('click', spy2)
.on('click', data, wrapSpy(spy3))
.once('click', spy4);
}
});
Block2 = bemDom.declBlock('block2', {}, {
onInit : function() {
this._domEvents()
.on('click', spy5);
}
});
Block3 = bemDom
.declBlock('block3')
.declMod({ modName : 'm1', modVal : 'v1' }, {}, {
onInit : function() {
this._domEvents({ modName : 'm1', modVal : 'v1' }).on('click', spy6);
}
});
block1 = initDom({
block : 'block1',
mix : { block : 'block2', js : true },
content : [
{ block : 'block3', js : true },
{ block : 'block3', mods : { m1 : 'v1' }, js : true }
]
}).bem(Block1);
});
it('should properly bind handlers', function() {
block1.domElem.trigger('click');
spy1.should.have.been.called;
spy2.should.have.been.called;
spy3.should.have.been.calledOn(block1);
spy3.args[0][1].should.be.instanceOf(Block1);
spy3.args[0][0].data.should.have.been.equal(data);
});
it('should properly bind once handler', function() {
block1.domElem.trigger('click');
spy4.should.have.been.called;
block1.domElem.trigger('click');
spy4.should.have.been.calledOnce;
block1._domEvents().once('click', spy4);
block1.domElem.trigger('click');
spy4.should.have.been.calledTwice;
});
it('should properly unbind all handlers', function() {
Block1._domEvents().un('click');
block1.domElem.trigger('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
});
it('should properly unbind specified handler', function() {
Block1._domEvents().un('click', spy1);
block1.domElem.trigger('click');
spy1.should.not.have.been.called;
spy3.should.have.been.called;
});
it('should unbind only own handlers', function() {
Block1._domEvents().un('click');
block1.domElem.trigger('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
spy5.should.have.been.called;
});
it('should properly unbind once handler', function() {
Block1._domEvents().un('click', spy4);
block1.domElem.trigger('click');
spy4.should.not.have.been.called;
});
it('should properly bind to self with modifier', function() {
var blocks = block1.findChildBlocks(Block3);
blocks.get(0).domElem.trigger('click');
spy6.should.not.have.been.called;
blocks.get(1).domElem.trigger('click');
spy6.should.have.been.called;
});
});
describe('block elems events', function() {
['string', 'Class'].forEach(function(elemType) {
var elem1, elem2;
describe('elem as ' + elemType, function() {
var Elem1, Elem2;
beforeEach(function() {
elem1 = elemType === 'string'?
'e1' :
bemDom.declElem('block', 'e1');
Block1 = bemDom.declBlock('block', {}, {
onInit : function() {
this._domEvents(elem1)
.on('click', spy1)
.on('click', spy2)
.on('click', data, wrapSpy(spy3));
this._domEvents('e2').on('click', spy5);
}
});
Elem1 = elemType === 'string'?
bemDom.declElem('block', 'e1') :
elem1;
Elem2 = bemDom.declElem('block', 'e2', {}, {
onInit : function() {
this._domEvents(elem1)
.on('click', wrapSpy(spy6))
.on('click', spy7);
}
});
block1 = initDom({
block : 'block',
content : [
{ elem : 'e1', content : { elem : 'e3' } },
{ elem : 'e2', content : { elem : 'e1' } }
]
}).bem(Block1);
elem2 = block1._elem('e2');
});
describe('block', function() {
it('should properly bind handlers', function() {
block1._elem('e3').domElem.trigger('click');
spy1.should.have.been.called;
spy1.should.have.been.calledOn(block1);
spy2.should.have.been.called;
spy3.should.have.been.called;
spy3.args[0][0].data.should.have.been.equal(data);
spy3.args[0][1].should.be.instanceOf(Elem1);
spy3.args[0][1].domElem[0]
.should.be.equal(block1._elem(elem1).domElem[0]);
spy5.should.not.have.been.called;
});
it('should properly unbind all handlers', function() {
Block1._domEvents(elem1).un('click');
block1._elem(elem1).domElem.trigger('click');
spy1.should.not.have.been.called;
spy2.should.not.have.been.called;
spy3.should.not.have.been.called;
});
it('should properly unbind specified handler', function() {
Block1._domEvents(elem1).un('click', spy2);
block1._elem(elem1).domElem.trigger('click');
spy1.should.have.been.called;
spy2.should.not.have.been.called;
spy3.should.have.been.called;
});
});
describe('elem instance', function() {
it('should properly bind handlers', function() {
var e2elem1 = elem2.findChildElem('e1');
e2elem1.domElem.trigger('click');
spy6.should.have.been.called;
spy6.should.have.been.calledOn(elem2);
spy6.args[0][1].should.be.instanceOf(Elem1);
spy6.args[0][1].domElem[0]
.should.be.equal(e2elem1.domElem[0]);
spy7.should.have.been.called;
});
it('should properly unbind all handlers', function() {
Elem2._domEvents(elem1).un('click');
var e2elem1 = elem2.findChildElem('e1');
e2elem1.domElem.trigger('click');
spy6.should.not.have.been.called;
spy7.should.not.have.been.called;
});
it('should properly unbind specified handler', function() {
Elem2._domEvents(elem1).un('click', spy7);
var e2elem1 = elem2.findChildElem('e1');
e2elem1.domElem.trigger('click');
spy6.should.have.been.called;
spy7.should.not.have.been.called;
});
});
});
describe('elem as ' + elemType + ', modName, modVal', function() {
beforeEach(function() {
Block1 = bemDom.declBlock('block', {}, {
onInit : function() {
this._domEvents({ elem : elem1 })
.on('click', spy1);
this._domEvents({ elem : elem1, modName : 'm1', modVal : 'v1' })
.on('click', spy2)
.on('click', spy3);
}
});
block1 = initDom({
block : 'block',
content : [
{ elem : 'e1' },
{ elem : 'e1', elemMods : { m1 : 'v1' } }
]
}).bem(Block1);
});
it('should properly bind handlers', function() {
block1._elem({ elem : 'e1', modName : 'm1', modVal : 'v1' }).domElem.trigger('click');
spy1.should.have.been.called;
spy2.should.have.been.called;
block1._elem('e1').domElem.trigger('click');
spy1.should.have.been.calledTwice;
spy2.should.have.been.calledOnce;
});
it('should properly unbind all handlers', function() {
Block1._domEvents({ elem : elem1, modName : 'm1', modVal : 'v1' }).un('click');
block1._elem({ elem : 'e1', modName : 'm1', modVal : 'v1' }).domElem.trigger('click');
spy1.should.have.been.called;
spy2.should.not.have.been.called;
});
it('should properly unbind specified handler', function() {
Block1._domEvents({ elem : elem1, modName : 'm1', modVal : 'v1' }).un('click', spy2);
block1._elem({ elem : 'e1', modName : 'm1', modVal : 'v1' }).domElem.trigger('click');
spy1.should.have.been.called;
spy2.should.not.have.been.called;
spy3.should.have.been.called;
});
});
});
});
});
});
provide();
function createDomNode(bemjson) {
return bemDom.init(BEMHTML.apply(bemjson));
}
});
================================================
FILE: common.blocks/i-bem-dom/__events/i-bem-dom__events.deps.js
================================================
({
shouldDeps : [
'inherit',
'identify',
'objects',
'jquery',
'functions',
{ block : 'i-bem', elem : 'internal' },
{ elem : 'collection' }
]
})
================================================
FILE: common.blocks/i-bem-dom/__events/i-bem-dom__events.js
================================================
/**
* @module i-bem-dom__events
*/
import bemInternal from 'bem:i-bem__internal'
import BemDomCollection from 'bem:i-bem-dom__collection'
import inherit from 'bem:inherit'
import identify from 'bem:identify'
import $ from 'bem:jquery'
import functions from 'bem:functions'
const winNode = window
const docNode = document
const eventStorage = new Map()
/**
* @class EventManager
*/
const EventManager = inherit(/** @lends EventManager.prototype */{
/**
* @constructor
* @param {Object} params EventManager parameters
* @param {Function} fnWrapper Wrapper function to build event handler
* @param {Function} eventBuilder Function to build event
*/
__constructor(params, fnWrapper, eventBuilder) {
this._params = params
this._fnWrapper = fnWrapper
this._eventBuilder = eventBuilder
this._storage = new Map()
},
/**
* Adds an event handler
* @param {String|Object|events:Event} e Event type
* @param {*} [data] Additional data that the handler gets as e.data
* @param {Function} fn Handler
* @returns {EventManager} this
*/
on(e, data, fn, _fnCtx, _isOnce) {
const params = this._params
const event = this._eventBuilder(e, params)
if(functions.isFunction(data)) {
_isOnce = _fnCtx
_fnCtx = fn
fn = data
data = undefined
}
const events = typeof event === 'string' ? event.split(/\s+/) : [event]
events.forEach(singleEvent => {
this._bindSingle(singleEvent, e, data, fn, _fnCtx, _isOnce, params)
})
return this
},
_bindSingle(event, origEvent, data, fn, _fnCtx, _isOnce, params) {
let fnStorage = this._storage.get(event)
if(!fnStorage) {
fnStorage = new Map()
this._storage.set(event, fnStorage)
}
const fnId = identify(fn, _fnCtx)
if(!fnStorage.get(fnId)) {
const bindDomElem = params.bindDomElem
const bindSelector = params.bindSelector
const _this = this
const handler = this._fnWrapper(
_isOnce?
function() {
_this.un(origEvent, fn, _fnCtx)
fn.apply(this, arguments)
} :
fn,
_fnCtx,
fnId)
fnStorage.set(fnId, handler)
bindDomElem.on(event, bindSelector, data, handler)
bindSelector && bindDomElem.is(bindSelector) && bindDomElem.on(event, data, handler)
// FIXME: "once" won't properly work in case of nested and mixed elem with the same name
}
},
/**
* Adds an event handler
* @param {String} e Event type
* @param {*} [data] Additional data that the handler gets as e.data
* @param {Function} fn Handler
* @returns {EventManager} this
*/
once(e, data, fn, _fnCtx) {
if(functions.isFunction(data)) {
_fnCtx = fn
fn = data
data = undefined
}
return this.on(e, data, fn, _fnCtx, true)
},
/**
* Removes event handler or handlers
* @param {String|Object|events:Event} [e] Event type
* @param {Function} [fn] Handler
* @returns {EventManager} this
*/
un(e, fn, _fnCtx) {
const argsLen = arguments.length
if(argsLen) {
const params = this._params
const event = this._eventBuilder(e, params)
const events = typeof event === 'string' ? event.split(/\s+/) : [event]
events.forEach(singleEvent => {
if(argsLen === 1) {
this._unbindByEvent(this._storage.get(singleEvent), singleEvent)
} else {
const fnId = identify(fn, _fnCtx)
const fnStorage = this._storage.get(singleEvent)
const bindDomElem = params.bindDomElem
const bindSelector = params.bindSelector
let wrappedFn
if(wrappedFn = fnStorage && fnStorage.get(fnId))
fnStorage.delete(fnId)
const handler = wrappedFn || fn
bindDomElem.off(singleEvent, params.bindSelector, handler)
bindSelector && bindDomElem.is(bindSelector) && bindDomElem.off(singleEvent, handler)
}
})
} else {
this._storage.forEach((fnStorage, e) => this._unbindByEvent(fnStorage, e))
}
return this
},
_unbindByEvent(fnStorage, e) {
const params = this._params
const bindDomElem = params.bindDomElem
const bindSelector = params.bindSelector
const unbindWithoutSelector = bindSelector && bindDomElem.is(bindSelector)
fnStorage && fnStorage.forEach((fn) => {
bindDomElem.off(e, bindSelector, fn)
unbindWithoutSelector && bindDomElem.off(e, fn)
})
this._storage.set(e, null)
}
})
const buildForEachEventManagerProxyFn = (methodName) => {
return function() {
const args = arguments
this._eventManagers.forEach((eventManager) => {
eventManager[methodName].apply(eventManager, args)
})
return this
}
}
/**
* @class CollectionEventManager
*/
const CollectionEventManager = inherit(/** @lends CollectionEventManager.prototype */{
/**
* @constructor
* @param {Array} eventManagers Array of event managers
*/
__constructor(eventManagers) {
this._eventManagers = eventManagers
},
/**
* Adds an event handler
* @param {String|Object|events:Event} e Event type
* @param {Object} [data] Additional data that the handler gets as e.data
* @param {Function} fn Handler
* @returns {CollectionEventManager} this
*/
on : buildForEachEventManagerProxyFn('on'),
/**
* Adds an event handler
* @param {String} e Event type
* @param {Object} [data] Additional data that the handler gets as e.data
* @param {Function} fn Handler
* @returns {CollectionEventManager} this
*/
once : buildForEachEventManagerProxyFn('once'),
/**
* Removes event handler or handlers
* @param {String|Object|events:Event} [e] Event type
* @param {Function} [fn] Handler
* @returns {CollectionEventManager} this
*/
un : buildForEachEventManagerProxyFn('un')
})
/**
* @class EventManagerFactory
* @exports i-bem-dom__events:EventManagerFactory
*/
const EventManagerFactory = inherit(/** @lends EventManagerFactory.prototype */{
__constructor(getEntityCls) {
this._storageSuffix = identify()
this._getEntityCls = getEntityCls
this._eventManagerCls = EventManager
},
/**
* Instantiates event manager
* @param {Function|i-bem-dom:BemDomEntity} ctx BemDomEntity class or instance
* @param {*} bindCtx context to bind
* @param {jQuery} bindScope bind scope
* @returns {EventManager}
*/
getEventManager(ctx, bindCtx, bindScope) {
if(bindCtx instanceof BemDomCollection) {
return new CollectionEventManager(bindCtx.map((entity) => {
return this.getEventManager(ctx, entity, bindScope)
}, this))
}
const ctxId = identify(ctx)
let ctxStorage = eventStorage.get(ctxId)
const storageSuffix = this._storageSuffix
const isBindToInstance = typeof ctx !== 'function'
let ctxCls
let selector = ''
if(isBindToInstance) {
ctxCls = ctx.__self
} else {
ctxCls = ctx
selector = ctx._buildSelector()
}
const params = this._buildEventManagerParams(bindCtx, bindScope, selector, ctxCls)
const storageKey = params.key + storageSuffix
if(!ctxStorage) {
ctxStorage = {}
eventStorage.set(ctxId, ctxStorage)
if(isBindToInstance) {
ctx._events().on({ modName : 'js', modVal : '' }, () => {
Object.keys(ctxStorage).forEach(key => {
ctxStorage[key] && ctxStorage[key].un()
})
eventStorage.delete(ctxId)
})
}
}
return ctxStorage[storageKey] ||
(ctxStorage[storageKey] = this._createEventManager(ctx, params, isBindToInstance))
},
_buildEventManagerParams(bindCtx, bindScope, ctxSelector, ctxCls) {
const res = {
bindEntityCls : null,
bindDomElem : bindScope,
bindToArbitraryDomElem : false,
bindSelector : ctxSelector,
ctxSelector : ctxSelector,
key : ''
}
if(bindCtx) {
const typeOfCtx = typeof bindCtx
if(bindCtx.jquery) {
res.bindDomElem = bindCtx
res.key = identify.apply(null, bindCtx.get())
res.bindToArbitraryDomElem = true
} else if(bindCtx === winNode || bindCtx === docNode || (typeOfCtx === 'object' && bindCtx.nodeType === 1)) { // NOTE: duck-typing check for "is-DOM-element"
res.bindDomElem = $(bindCtx)
res.key = identify(bindCtx)
res.bindToArbitraryDomElem = true
} else if(typeOfCtx === 'object' && bindCtx.__self) { // bem entity instance
res.bindDomElem = bindCtx.domElem
res.key = bindCtx._uniqId
res.bindEntityCls = bindCtx.__self
} else if(typeOfCtx === 'string' || typeOfCtx === 'object' || typeOfCtx === 'function') {
let blockName, elemName, modName, modVal
if(typeOfCtx === 'string') { // elem name
blockName = ctxCls._blockName
elemName = bindCtx
} else if(typeOfCtx === 'object') { // bem entity with optional mod val
blockName = bindCtx.block?
bindCtx.block.getName() :
ctxCls._blockName
elemName = typeof bindCtx.elem === 'function'?
bindCtx.elem.getName() :
bindCtx.elem
modName = bindCtx.modName
/* treat modVal: false as modVal: '' (modifier removal) — #1457 */
modVal = bindCtx.modVal === false? '' : bindCtx.modVal
} else if(bindCtx.getName() === bindCtx.getEntityName()) { // block class
blockName = bindCtx.getName()
} else { // elem class
blockName = ctxCls._blockName
elemName = bindCtx.getName()
}
const entityName = bemInternal.buildClassName(blockName, elemName)
res.bindEntityCls = this._getEntityCls(entityName)
res.bindSelector = '.' + (res.key = entityName + bemInternal.buildModPostfix(modName, modVal))
}
} else {
res.bindEntityCls = ctxCls
}
return res
},
_createEventManager(ctx, params, isInstance) {
throw new Error('not implemented')
}
})
export default { EventManagerFactory }
================================================
FILE: common.blocks/i-bem-dom/__init/_auto/i-bem-dom__init_auto.deps.js
================================================
({
shouldDeps : ['jquery', 'next-tick']
})
================================================
FILE: common.blocks/i-bem-dom/__init/_auto/i-bem-dom__init_auto.js
================================================
/**
* Auto initialization on DOM ready
*/
import init from 'bem:i-bem-dom__init'
import $ from 'bem:jquery'
import nextTick from 'bem:next-tick'
$(function() {
nextTick(init)
})
================================================
FILE: common.blocks/i-bem-dom/__init/i-bem-dom__init.deps.js
================================================
({
shouldDeps : ['i-bem-dom']
})
================================================
FILE: common.blocks/i-bem-dom/__init/i-bem-dom__init.js
================================================
/**
* @module i-bem-dom__init
*/
import bemDom from 'bem:i-bem-dom'
export default
/**
* Initializes blocks on a fragment of the DOM tree
* @param {jQuery} [ctx=scope] Root DOM node
* @returns {jQuery} ctx Initialization context
*/
function(ctx) {
return bemDom.init(ctx)
}
================================================
FILE: common.blocks/i-bem-dom/__init/i-bem-dom__init.spec.js
================================================
modules.define('spec', ['i-bem'], function(provide, bem) {
describe('i-bem-dom__init', function() {
it('block should exist on init', function(done) {
var name = 'b' + Math.random();
modules.define(name, ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, {}));
});
modules.require(['i-bem-dom__init'], function() {
bem.entities.should.have.property(name);
done();
});
});
});
provide();
});
================================================
FILE: common.blocks/i-bem-dom/i-bem-dom.deps.js
================================================
({
shouldDeps : [
'inherit',
'jquery',
'objects',
'functions',
'dom',
{ elem : 'init' },
{ block : 'i-bem', elems : ['internal'] },
{ elem : 'events', mods : { type : ['dom', 'bem'] } },
{ elem : 'collection' }
]
})
================================================
FILE: common.blocks/i-bem-dom/i-bem-dom.en.md
================================================
# i-bem-dom
A helper block for creating other blocks that have a DOM representation.
The block is implemented as a specialized JavaScript framework for web development using the BEM methodology.
There is a separate document with a detailed [user's guide](https://en.bem.info/technology/i-bem/v4/i-bem-js/).
================================================
FILE: common.blocks/i-bem-dom/i-bem-dom.js
================================================
/**
* @module i-bem-dom
*/
import bem from 'bem:i-bem'
import bemInternal from 'bem:i-bem__internal'
import BemDomCollection from 'bem:i-bem-dom__collection'
import domEvents from 'bem:i-bem-dom__events_type_dom'
import bemEvents from 'bem:i-bem-dom__events_type_bem'
import inherit from 'bem:inherit'
import identify from 'bem:identify'
import objects from 'bem:objects'
import functions from 'bem:functions'
import $ from 'bem:jquery'
import dom from 'bem:dom'
/**
* Storage for DOM elements by unique key
* @type Map
*/
const uniqIdToDomElems = new Map()
/**
* Storage for blocks by unique key
* @type Map
*/
const uniqIdToEntity = new Map()
/**
* Storage for DOM element's parent nodes
* @type Map
*/
const domNodesToParents = new Map()
/**
* Storage for block parameters
* @type Map
*/
const domElemToParams = new Map()
/**
* Storage for DOM nodes that are being destructed
* @type Map
*/
const destructingDomNodes = new Map()
const entities = bem.entities
const BEM_CLASS_NAME = 'i-bem'
const BEM_SELECTOR = '.' + BEM_CLASS_NAME
const BEM_PARAMS_ATTR = 'data-bem'
const NAME_PATTERN = bemInternal.NAME_PATTERN
const MOD_DELIM = bemInternal.MOD_DELIM
const ELEM_DELIM = bemInternal.ELEM_DELIM
const buildModPostfix = bemInternal.buildModPostfix
const buildClassName = bemInternal.buildClassName
const reverse = Array.prototype.reverse
const domEventManagerFactory = new domEvents.EventManagerFactory(getEntityCls)
const bemEventManagerFactory = new bemEvents.EventManagerFactory(getEntityCls)
// eslint-disable-next-line prefer-const -- assigned later as object literal
let bemDom
/**
* Initializes entities on a DOM element
* @param {jQuery} domElem DOM element
* @param {String} uniqInitId ID of the "initialization wave"
* @param {Object} [dropElemCacheQueue] queue of elems to be droped from cache
*/
function initEntities(domElem, uniqInitId, dropElemCacheQueue) {
const domNode = domElem[0]
const params = getParams(domNode)
for(const entityName of Object.keys(params)) {
const splitted = entityName.split(ELEM_DELIM)
const blockName = splitted[0]
const elemName = splitted[1]
elemName &&
((dropElemCacheQueue[blockName] ||
(dropElemCacheQueue[blockName] = {}))[elemName] = true)
initEntity(
entityName,
domElem,
processParams(params[entityName], entityName, uniqInitId))
}
}
/**
* Initializes a specific entity on a DOM element, or returns the existing entity if it was already created
* @param {String} entityName Entity name
* @param {jQuery} domElem DOM element
* @param {Object} [params] Initialization parameters
* @param {Boolean} [ignoreLazyInit=false] Ignore lazy initialization
* @param {Function} [callback] Handler to call after complete initialization
*/
function initEntity(entityName, domElem, params, ignoreLazyInit, callback) {
const domNode = domElem[0]
if(destructingDomNodes.has(identify(domNode))) return
params || (params = processParams(getEntityParams(domNode, entityName), entityName))
const uniqId = params.uniqId
let entity = uniqIdToEntity.get(uniqId)
if(entity) {
if(entity.domElem.index(domNode) < 0) {
entity.domElem = entity.domElem.add(domElem)
objects.extend(entity.params, params)
}
return entity
}
uniqIdToDomElems.set(uniqId, uniqIdToDomElems.has(uniqId)?
uniqIdToDomElems.get(uniqId).add(domElem) :
domElem)
const parentDomNode = domNode.parentNode
if(!parentDomNode || parentDomNode.nodeType === 11) { // jquery doesn't unique disconnected node
$.uniqueSort(uniqIdToDomElems.get(uniqId))
}
const entityCls = getEntityCls(entityName)
entityCls._processInit()
if(ignoreLazyInit || params.lazyInit === false || !entityCls.lazyInit && !params.lazyInit) {
ignoreLazyInit && domElem.addClass(BEM_CLASS_NAME) // add css class for preventing memory leaks in further destructing
entity = new entityCls(uniqIdToDomElems.get(uniqId), params, !!ignoreLazyInit)
uniqIdToDomElems.delete(uniqId)
callback && callback.apply(entity, [...arguments].slice(4))
return entity
}
}
function getEntityCls(entityName) {
if(entities[entityName]) return entities[entityName]
const splitted = entityName.split(ELEM_DELIM)
return splitted[1]?
bemDom.declElem(splitted[0], splitted[1], {}, { lazyInit : true }) :
bemDom.declBlock(entityName, {}, { lazyInit : true })
}
/**
* Processes and adds necessary entity parameters
* @param {Object} params Initialization parameters
* @param {String} entityName Entity name
* @param {String} [uniqInitId] ID of the "initialization wave"
*/
function processParams(params, entityName, uniqInitId) {
params.uniqId ||
(params.uniqId = (params.id?
entityName + '-id-' + params.id :
identify()) + (uniqInitId || identify()))
return params
}
/**
* Helper for searching for a DOM element using a selector inside the context, including the context itself
* @param {jQuery} ctx Context
* @param {String} selector CSS selector
* @param {Boolean} [excludeSelf=false] Exclude context from search
* @returns {jQuery}
*/
function findDomElem(ctx, selector, excludeSelf) {
const res = ctx.find(selector)
return excludeSelf?
res :
res.add(ctx.filter(selector))
}
/**
* Returns parameters of an entity's DOM element
* @param {HTMLElement} domNode DOM node
* @returns {Object}
*/
function getParams(domNode) {
const uniqId = identify(domNode)
if(domElemToParams.has(uniqId)) return domElemToParams.get(uniqId)
const params = extractParams(domNode)
domElemToParams.set(uniqId, params)
return params
}
/**
* Returns parameters of an entity extracted from DOM node
* @param {HTMLElement} domNode DOM node
* @param {String} entityName
* @returns {Object}
*/
function getEntityParams(domNode, entityName) {
const params = getParams(domNode)
return params[entityName] || (params[entityName] = {})
}
/**
* Retrieves entity parameters from a DOM element
* @param {HTMLElement} domNode DOM node
* @returns {Object}
*/
function extractParams(domNode) {
const attrVal = domNode.getAttribute(BEM_PARAMS_ATTR)
return attrVal? JSON.parse(attrVal) : {}
}
/**
* Uncouple DOM node from the entity. If this is the last node, then destroys the entity.
* @param {BemDomEntity} entity entity
* @param {HTMLElement} domNode DOM node
*/
function removeDomNodeFromEntity(entity, domNode) {
if(entity.domElem.length === 1) {
entity.delMod('js')
uniqIdToEntity.delete(entity._uniqId)
} else {
entity.domElem = entity.domElem.not(domNode)
}
}
/**
* Stores DOM node's parent nodes to the storage
* @param {jQuery} domElem
*/
function storeDomNodeParents(domElem) {
domElem.each(function() {
domNodesToParents.set(identify(this), this.parentNode)
})
}
/**
* Clears the cache for elements in context
* @param {jQuery} ctx
*/
function dropElemCacheForCtx(ctx, dropElemCacheQueue) {
ctx.add(ctx.parents()).each((_, domNode) => {
const params = domElemToParams.get(identify(domNode))
params && objects.each(params, (entityParams) => {
const entity = uniqIdToEntity.get(entityParams.uniqId)
if(entity) {
const elemNames = dropElemCacheQueue[entity.__self._blockName]
elemNames && entity._dropElemCache(Object.keys(elemNames))
}
})
})
}
/**
* Build key for elem
* @param {Function|String|Object} elem Element class or name or description elem, modName, modVal
* @returns {Object}
*/
function buildElemKey(elem) {
if(typeof elem === 'string') {
elem = { elem : elem }
} else if(functions.isFunction(elem)) {
elem = { elem : elem.getName() }
} else if(functions.isFunction(elem.elem)) {
elem.elem = elem.elem.getName()
}
return {
elem : elem.elem,
mod : buildModPostfix(elem.modName, typeof elem.modVal === 'undefined' ? true : elem.modVal)
}
}
/**
* Returns jQuery collection for provided HTML or BEM entity
* @param {jQuery|String|BemDomEntity} html
* @returns {jQuery}
*/
function getJqueryCollection(html) {
if(typeof html === 'string') return $($.parseHTML(html, null, true))
if(html && html.domElem) return html.domElem
return $(html)
}
/**
* Validates block to be class or specified description
* @param {*} Block Any argument passed to find*Block as Block
* @throws {Error} Will throw an error if the Block argument isn't correct
*/
function validateBlockParam(Block) {
if(
typeof Block === 'string' ||
typeof Block === 'object' && typeof Block.block === 'string'
) {
throw new Error('Block must be a class or description (block, modName, modVal) of the block to find')
}
}
/**
* Returns base entities for declaration
* @param {Function} baseCls block|elem class
* @param {String} entityName entityName
* @param {Function|Array.} [base] base block|elem + mixes
* @returns {Array}
*/
function getEntityBase(baseCls, entityName, base) {
base || (base = entities[entityName] || baseCls)
Array.isArray(base) || (base = [base])
if(!base[0].__bemEntity) {
base = base.slice()
base.unshift(entities[entityName] || baseCls)
}
return base
}
/**
* @class BemDomEntity
* @description Base mix for BEM entities that have DOM representation
*/
const BemDomEntity = inherit(/** @lends BemDomEntity.prototype */{
/**
* @constructor
* @private
* @param {jQuery} domElem DOM element that the entity is created on
* @param {Object} params parameters
* @param {Boolean} [initImmediately=true]
*/
__constructor : function(domElem, params, initImmediately) {
/**
* DOM elements of entity
* @member {jQuery}
* @readonly
*/
this.domElem = domElem
/**
* Cache for elements collections
* @member {Object}
* @private
*/
this._elemsCache = {}
/**
* Cache for elements
* @member {Object}
* @private
*/
this._elemCache = {}
/**
* References to parent entities which found current entity ever
* @type {Array}
* @private
*/
this._findBackRefs = []
uniqIdToEntity.set(params.uniqId || identify(this), this)
this.__base(null, params, initImmediately)
},
/**
* @abstract
* @protected
* @returns {Block}
*/
_block : function() {},
/**
* Lazy search for elements nested in a block (caches results)
* @protected
* @param {Function|String|Object} Elem Element class or name or description elem, modName, modVal
* @returns {BemDomCollection}
*/
_elems : function(Elem) {
const key = buildElemKey(Elem)
const elemsCache = this._elemsCache[key.elem]
if(elemsCache && key.mod in elemsCache)
return elemsCache[key.mod]
const res = (elemsCache || (this._elemsCache[key.elem] = {}))[key.mod] =
this.findMixedElems(Elem).concat(this.findChildElems(Elem))
res.forEach(function(entity) {
entity._findBackRefs.push(this)
}, this)
return res
},
/**
* Lazy search for the first element nested in a block (caches results)
* @protected
* @param {Function|String|Object} Elem Element class or name or description elem, modName, modVal
* @returns {Elem}
*/
_elem : function(Elem) {
const key = buildElemKey(Elem)
const elemCache = this._elemCache[key.elem]
// NOTE: can use this._elemsCache but it's too rare case
if(elemCache && key.mod in elemCache)
return elemCache[key.mod]
const res = (elemCache || (this._elemCache[key.elem] = {}))[key.mod] =
this.findMixedElem(Elem) || this.findChildElem(Elem)
res && res._findBackRefs.push(this)
return res
},
/**
* Clears the cache for elements
* @private
* @param {...(Function|String|Object)} elems Nested elements names or description elem, modName, modVal
* @returns {BemDomEntity} this
*/
_dropElemCache : function(elems) {
if(!arguments.length) {
this._elemsCache = {}
this._elemCache = {}
return this
}
(Array.isArray(elems)? elems : [...arguments]).forEach(function(elem) {
const key = buildElemKey(elem)
if(key.mod) {
this._elemsCache[key.elem] && delete this._elemsCache[key.elem][key.mod]
this._elemCache[key.elem] && delete this._elemCache[key.elem][key.mod]
} else {
delete this._elemsCache[key.elem]
delete this._elemCache[key.elem]
}
}, this)
return this
},
/**
* Finds the first child block
* @param {Function|Object} Block Block class or description (block, modName, modVal) of the block to find
* @returns {Block}
*/
findChildBlock : function(Block) {
validateBlockParam(Block)
return this._findEntities('find', Block, true)
},
/**
* Finds child blocks
* @param {Function|Object} Block Block class or description (block, modName, modVal) of the block to find
* @returns {BemDomCollection}
*/
findChildBlocks : function(Block) {
validateBlockParam(Block)
return this._findEntities('find', Block)
},
/**
* Finds the first parent block
* @param {Function|Object} Block Block class or description (block, modName, modVal) of the block to find
* @returns {Block}
*/
findParentBlock : function(Block) {
validateBlockParam(Block)
return this._findEntities('parents', Block, true)
},
/**
* Finds parent blocks
* @param {Function|Object} Block Block class or description (block, modName, modVal) of the block to find
* @returns {BemDomCollection}
*/
findParentBlocks : function(Block) {
validateBlockParam(Block)
return this._findEntities('parents', Block)
},
/**
* Finds first mixed block
* @param {Function|Object} Block Block class or description (block, modName, modVal) of the block to find
* @returns {Block}
*/
findMixedBlock : function(Block) {
validateBlockParam(Block)
return this._findEntities('filter', Block, true)
},
/**
* Finds mixed blocks
* @param {Function|Object} Block Block class or description (block, modName, modVal) of the block to find
* @returns {BemDomCollection}
*/
findMixedBlocks : function(Block) {
validateBlockParam(Block)
return this._findEntities('filter', Block)
},
/**
* Finds the first child element
* @param {Function|String|Object} Elem Element class or name or description elem, modName, modVal
* @param {Boolean} [strictMode=false]
* @returns {Elem}
*/
findChildElem : function(Elem, strictMode) {
return strictMode?
this._filterFindElemResults(this._findEntities('find', Elem)).get(0) :
this._findEntities('find', Elem, true)
},
/**
* Finds child elements
* @param {Function|String|Object} Elem Element class or name or description elem, modName, modVal
* @param {Boolean} [strictMode=false]
* @returns {BemDomCollection}
*/
findChildElems : function(Elem, strictMode) {
const res = this._findEntities('find', Elem)
return strictMode?
this._filterFindElemResults(res) :
res
},
/**
* Finds the first parent element
* @param {Function|String|Object} Elem Element class or name or description elem, modName, modVal
* @param {Boolean} [strictMode=false]
* @returns {Elem}
*/
findParentElem : function(Elem, strictMode) {
return strictMode?
this._filterFindElemResults(this._findEntities('parents', Elem))[0] :
this._findEntities('parents', Elem, true)
},
/**
* Finds parent elements
* @param {Function|String|Object} Elem Element class or name or description elem, modName, modVal
* @param {Boolean} [strictMode=false]
* @returns {BemDomCollection}
*/
findParentElems : function(Elem, strictMode) {
const res = this._findEntities('parents', Elem)
return strictMode? this._filterFindElemResults(res) : res
},
/**
* Finds the first mixed element
* @param {Function|String|Object} Elem Element class or name or description elem, modName, modVal
* @returns {Elem}
*/
findMixedElem : function(Elem) {
return this._findEntities('filter', Elem, true)
},
/**
* Finds mixed elements.
* @param {Function|String|Object} Elem Element class or name or description elem, modName, modVal
* @returns {BemDomCollection}
*/
findMixedElems : function(Elem) {
return this._findEntities('filter', Elem)
},
/**
* Filters results of findElem helper execution in strict mode
* @private
* @param {BemDomCollection} res Elements
* @returns {BemDomCollection}
*/
_filterFindElemResults : function(res) {
const block = this._block()
return res.filter((elem) => elem._block() === block)
},
/**
* Finds entities
* @private
* @param {String} select
* @param {Function|String|Object} entity
* @param {Boolean} [onlyFirst=false]
* @returns {*}
*/
_findEntities : function(select, entity, onlyFirst) {
const entityName = functions.isFunction(entity)?
entity.getEntityName() :
typeof entity === 'object'?
entity.block?
entity.block.getEntityName() :
typeof entity.elem === 'string'?
this.__self._blockName + ELEM_DELIM + entity.elem :
entity.elem.getEntityName() :
this.__self._blockName + ELEM_DELIM + entity
const selector = '.' +
(typeof entity === 'object'?
buildClassName(
entityName,
entity.modName,
typeof entity.modVal === 'undefined'?
true :
entity.modVal) :
entityName) +
(onlyFirst? ':first' : '')
const domElems = this.domElem[select](selector)
if(onlyFirst) return domElems[0]?
initEntity(entityName, domElems.eq(0), undefined, true)._setInitedMod() :
null
const res = []
const uniqIds = {}
domElems.each((i, domElem) => {
const block = initEntity(entityName, $(domElem), undefined, true)._setInitedMod()
if(!uniqIds[block._uniqId]) {
uniqIds[block._uniqId] = true
res.push(block)
}
})
return new BemDomCollection(res)
},
/**
* Returns an manager to bind and unbind DOM events for particular context
* @protected
* @param {Function|String|Object|Elem|BemDomCollection|document|window} [ctx=this.domElem] context to bind,
* can be BEM-entity class, instance, collection of BEM-entities,
* element name or description (elem, modName, modVal), document or window
* @returns {EventManager}
*/
_domEvents : function(ctx) {
const bindScope = ctx === document || ctx === window ? $(ctx) : this.domElem
return domEventManagerFactory.getEventManager(this, ctx, bindScope)
},
/**
* Returns an manager to bind and unbind BEM events for particular context
* @protected
* @param {Function|String|BemDomEntity|BemDomCollection|Object} [ctx=this.domElem] context to bind,
* can be BEM-entity class, instance, collection of BEM-entities,
* element name or description (elem, modName, modVal)
* @returns {EventManager}
*/
_events : function(ctx) {
return bemEventManagerFactory.getEventManager(this, ctx, this.domElem)
},
/**
* Executes the BEM entity's event handlers and delegated handlers
* @protected
* @param {String|Object|events:Event} e Event name
* @param {Object} [data] Additional information
* @returns {BemEntity} this
*/
_emit : function(e, data) {
if((typeof e === 'object' && e.modName === 'js') || this.hasMod('js', 'inited')) {
bemEvents.emit(this, e, data)
}
return this
},
/** @override */
_extractModVal : function(modName) {
const domNode = this.domElem[0]
let matches
domNode &&
(matches = String(domNode.className)
.match(this.__self._buildModValRE(modName)))
return matches? matches[2] || true : ''
},
/** @override */
_onSetMod : function(modName, modVal, oldModVal) {
const _self = this.__self
const name = _self.getName()
this._findBackRefs.forEach(function(ref) {
oldModVal === '' || ref._dropElemCache({ elem : name, modName : modName, modVal : oldModVal })
ref._dropElemCache(modVal === ''? name : { elem : name, modName : modName, modVal : modVal })
})
this.__base.apply(this, arguments)
if(modName !== 'js' || modVal !== '') {
const classNamePrefix = _self._buildModClassNamePrefix(modName)
const classNameRE = _self._buildModValRE(modName)
const needDel = modVal === ''
this.domElem.each(function() {
const className = String(this.className)
let modClassName = classNamePrefix
modVal !== true && (modClassName += MOD_DELIM + modVal)
;(oldModVal === true?
classNameRE.test(className) :
(' ' + className).indexOf(' ' + classNamePrefix + MOD_DELIM) > -1)?
this.className = className.replace(
classNameRE,
(needDel? '' : '$1' + modClassName)) :
needDel || $(this).addClass(modClassName)
})
}
},
/** @override */
_afterSetMod : function(modName, modVal, oldModVal) {
const eventData = { modName, modVal, oldModVal }
this
._emit({ modName, modVal : '*' }, eventData)
._emit({ modName, modVal }, eventData)
},
/**
* Checks whether an entity is in the entity
* @param {BemDomEntity} entity entity
* @returns {Boolean}
*/
containsEntity : function(entity) {
return dom.contains(this.domElem, entity.domElem)
},
/**
* Replaces the content of the entity's DOM element
* @param {jQuery|String|BemDomEntity} content New content
* @returns {jQuery}
*/
update : function(content) {
return bemDom.update(this.domElem, content)
},
/**
* Appends content to the entity's DOM element
* @param {jQuery|String|BemDomEntity} content Content to be added
* @returns {jQuery}
*/
append : function(content) {
return bemDom.append(this.domElem, content)
},
/**
* Prepends content to the entity's DOM element
* @param {jQuery|String|BemDomEntity} content Content to be added
* @returns {jQuery}
*/
prepend : function(content) {
return bemDom.prepend(this.domElem, content)
},
/**
* Adds content before the entity's DOM element
* @param {jQuery|String|BemDomEntity} content Content to be added
* @returns {jQuery}
*/
before : function(content) {
return bemDom.before(this.domElem, content)
},
/**
* Adds content after the entity's DOM element
* @param {jQuery|String|BemDomEntity} content Content to be added
* @returns {jQuery}
*/
after : function(content) {
return bemDom.after(this.domElem, content)
}
}, /** @lends BemDomEntity */{
/** @override */
create : function() {
throw Error('bemDom entities can not be created otherwise than from DOM')
},
/** @override */
_processInit : function(heedInit) {
if(this.onInit && this._inited == heedInit) {
this.__base(heedInit)
this.onInit()
const name = this.getName()
const origOnInit = this.onInit
// allow future calls of init only in case of inheritance in other block
this.init = function() {
this.getName() === name && origOnInit.apply(this, arguments)
}
}
},
/**
* Returns an manager to bind and unbind events for particular context
* @protected
* @param {Function|String|Object} [ctx] context to bind,
* can be BEM-entity class, instance, element name or description (elem, modName, modVal)
* @returns {EventManager}
*/
_domEvents : function(ctx) {
return domEventManagerFactory.getEventManager(this, ctx, bemDom.scope)
},
/**
* Returns an manager to bind and unbind BEM events for particular context
* @protected
* @param {Function|String|Object} [ctx] context to bind,
* can be BEM-entity class, instance, element name or description (block or elem, modName, modVal)
* @returns {EventManager}
*/
_events : function(ctx) {
return bemEventManagerFactory.getEventManager(this, ctx, bemDom.scope)
},
/**
* Builds a prefix for the CSS class of a DOM element of the entity, based on modifier name
* @private
* @param {String} modName Modifier name
* @returns {String}
*/
_buildModClassNamePrefix : function(modName) {
return this.getEntityName() + MOD_DELIM + modName
},
/**
* Builds a regular expression for extracting modifier values from a DOM element of an entity
* @private
* @param {String} modName Modifier name
* @returns {RegExp}
*/
_buildModValRE : function(modName) {
return new RegExp(
'(\\s|^)' +
this._buildModClassNamePrefix(modName) +
'(?:' + MOD_DELIM + '(' + NAME_PATTERN + '))?(?=\\s|$)')
},
/**
* Builds a CSS class name corresponding to the entity and modifier
* @protected
* @param {String} [modName] Modifier name
* @param {String} [modVal] Modifier value
* @returns {String}
*/
_buildClassName : function(modName, modVal) {
return buildClassName(this.getEntityName(), modName, modVal)
},
/**
* Builds a CSS selector corresponding to an entity and modifier
* @protected
* @param {String} [modName] Modifier name
* @param {String} [modVal] Modifier value
* @returns {String}
*/
_buildSelector : function(modName, modVal) {
return '.' + this._buildClassName(modName, modVal)
}
})
/**
* @class Block
* @description Base class for creating BEM blocks that have DOM representation
* @augments i-bem:Block
* @exports i-bem-dom:Block
*/
const Block = inherit([bem.Block, BemDomEntity], /** @lends Block.prototype */{
/** @override */
_block : function() {
return this
}
})
/**
* @class Elem
* @description Base class for creating BEM elements that have DOM representation
* @augments i-bem:Elem
* @exports i-bem-dom:Elem
*/
const Elem = inherit([bem.Elem, BemDomEntity], /** @lends Elem.prototype */{
/** @override */
_block : function() {
return this._blockInstance || (this._blockInstance = this.findParentBlock(getEntityCls(this.__self._blockName)))
}
})
/**
* Returns a block on a DOM element and initializes it if necessary
* @param {Function} BemDomEntity entity
* @param {Object} [params] entity parameters
* @returns {BemDomEntity|null}
*/
$.fn.bem = function(BemDomEntity, params) {
const entity = initEntity(BemDomEntity.getEntityName(), this, params, true)
return entity? entity._setInitedMod() : null
}
/**
* Returns an existing BEM entity instance from a DOM node without initialization
* @param {HTMLElement} domNode DOM node
* @param {Function} BemDomEntity entity class
* @returns {BemDomEntity|null}
*/
function getEntityFromDom(domNode, BemDomEntity) {
const entityName = BemDomEntity.getEntityName()
const params = getParams(domNode)
const entityParams = params[entityName]
return entityParams && entityParams.uniqId?
uniqIdToEntity.get(entityParams.uniqId) || null :
null
}
bemDom = {
/**
* Scope (set on DOM ready)
* @type jQuery
*/
scope : null,
/**
* Document shortcut
* @type jQuery
*/
doc : null,
/**
* Window shortcut
* @type jQuery
*/
win : null,
/**
* Base bemDom block
* @type Function
*/
Block,
/**
* Base bemDom element
* @type Function
*/
Elem,
/**
* @param {*} entity
* @returns {Boolean}
*/
isEntity : function(entity) {
return entity instanceof Block || entity instanceof Elem
},
/**
* Declares DOM-based block and creates block class
* @param {String|Function} blockName Block name or block class
* @param {Function|Array.} [base] base block + mixes
* @param {Object} [props] Methods
* @param {Object} [staticProps] Static methods
* @returns {Function} Block class
*/
declBlock : function(blockName, base, props, staticProps) {
if(!base || (typeof base === 'object' && !Array.isArray(base))) {
staticProps = props
props = base
base = typeof blockName === 'string'?
entities[blockName] || Block :
blockName
}
base = getEntityBase(Block, blockName, base)
return bem.declBlock(blockName, base, props, staticProps)
},
/**
* Declares elem and creates elem class
* @param {String} blockName Block name
* @param {String} elemName Elem name
* @param {Function|Array.} [base] base elem + mixes
* @param {Object} [props] Methods
* @param {Object} [staticProps] Static methods
* @returns {Function} Elem class
*/
declElem : function(blockName, elemName, base, props, staticProps) {
if(typeof blockName === 'function') {
if(typeof elemName !== 'string') {
// declElem(ElemClass, base?, props?, staticProps?) — redeclaration of elem class
staticProps = props
props = base
base = elemName
elemName = blockName._name
blockName = blockName._blockName
} else {
// declElem(BlockClass, 'elemName', ...) — block class as first arg
blockName = blockName.getName()
}
}
const entityName = blockName + ELEM_DELIM + elemName
if(!base || (typeof base === 'object' && !Array.isArray(base))) {
staticProps = props
props = base
base = entities[entityName] || Elem
}
base = getEntityBase(Elem, entityName, base)
return bem.declElem(blockName, elemName, base, props, staticProps)
},
declMixin : bem.declMixin,
/**
* Initializes blocks on a fragment of the DOM tree
* @param {jQuery|String} [ctx=scope] Root DOM node
* @returns {jQuery} ctx Initialization context
*/
init : function(ctx) {
ctx = typeof ctx === 'string'?
$(ctx) :
ctx || bemDom.scope
const dropElemCacheQueue = {}
const uniqInitId = identify()
// NOTE: we find only js-entities, so cahced elems without js can't be dropped from cache
findDomElem(ctx, BEM_SELECTOR).each(function() {
initEntities($(this), uniqInitId, dropElemCacheQueue)
})
bem._runInitFns()
dropElemCacheForCtx(ctx, dropElemCacheQueue)
return ctx
},
/**
* @param {jQuery} ctx Root DOM node
* @param {Boolean} [excludeSelf=false] Exclude the main domElem
* @param {Boolean} [destructDom=false] Remove DOM node during destruction
* @private
*/
_destruct : function(ctx, excludeSelf, destructDom) {
let _ctx
const currentDestructingDomNodes = []
storeDomNodeParents(_ctx = excludeSelf? ctx.children() : ctx)
reverse.call(findDomElem(_ctx, BEM_SELECTOR)).each((_, domNode) => {
const params = getParams(domNode)
const domNodeId = identify(domNode)
destructingDomNodes.set(domNodeId, true)
currentDestructingDomNodes.push(domNodeId)
objects.each(params, (entityParams) => {
if(entityParams.uniqId) {
const entity = uniqIdToEntity.get(entityParams.uniqId)
entity?
removeDomNodeFromEntity(entity, domNode) :
uniqIdToDomElems.delete(entityParams.uniqId)
}
})
domElemToParams.delete(identify(domNode))
})
// NOTE: it was moved here as jquery events aren't triggered on detached DOM elements
destructDom &&
(excludeSelf? ctx.empty() : ctx.remove())
// flush parent nodes storage that has been filled above
domNodesToParents.clear()
currentDestructingDomNodes.forEach((domNodeId) => {
destructingDomNodes.delete(domNodeId)
})
},
/**
* Destroys blocks on a fragment of the DOM tree
* @param {jQuery} ctx Root DOM node
* @param {Boolean} [excludeSelf=false] Exclude the main domElem
*/
destruct : function(ctx, excludeSelf) {
this._destruct(ctx, excludeSelf, true)
},
/**
* Detaches blocks on a fragment of the DOM tree without DOM tree destruction
* @param {jQuery} ctx Root DOM node
* @param {Boolean} [excludeSelf=false] Exclude the main domElem
*/
detach : function(ctx, excludeSelf) {
this._destruct(ctx, excludeSelf)
},
/**
* Replaces a fragment of the DOM tree inside the context, destroying old blocks and intializing new ones
* @param {jQuery} ctx Root DOM node
* @param {jQuery|String} content New content
* @returns {jQuery} Updated root DOM node
*/
update : function(ctx, content) {
this.destruct(ctx, true)
return this.init(ctx.html(content))
},
/**
* Changes a fragment of the DOM tree including the context and initializes blocks.
* @param {jQuery} ctx Root DOM node
* @param {jQuery|String} content Content to be added
* @returns {jQuery} New content
*/
replace : function(ctx, content) {
const prev = ctx.prev()
const parent = ctx.parent()
content = getJqueryCollection(content)
this.destruct(ctx)
return this.init(prev.length?
content.insertAfter(prev) :
content.prependTo(parent))
},
/**
* Adds a fragment of the DOM tree at the end of the context and initializes blocks
* @param {jQuery} ctx Root DOM node
* @param {jQuery|String} content Content to be added
* @returns {jQuery} New content
*/
append : function(ctx, content) {
return this.init(getJqueryCollection(content).appendTo(ctx))
},
/**
* Adds a fragment of the DOM tree at the beginning of the context and initializes blocks
* @param {jQuery} ctx Root DOM node
* @param {jQuery|String} content Content to be added
* @returns {jQuery} New content
*/
prepend : function(ctx, content) {
return this.init(getJqueryCollection(content).prependTo(ctx))
},
/**
* Adds a fragment of the DOM tree before the context and initializes blocks
* @param {jQuery} ctx Contextual DOM node
* @param {jQuery|String} content Content to be added
* @returns {jQuery} New content
*/
before : function(ctx, content) {
return this.init(getJqueryCollection(content).insertBefore(ctx))
},
/**
* Adds a fragment of the DOM tree after the context and initializes blocks
* @param {jQuery} ctx Contextual DOM node
* @param {jQuery|String} content Content to be added
* @returns {jQuery} New content
*/
after : function(ctx, content) {
return this.init(getJqueryCollection(content).insertAfter(ctx))
},
/**
* Returns an existing BEM entity instance from a DOM node without initialization
* @param {HTMLElement} domNode DOM node
* @param {Function} BemDomEntity entity class
* @returns {BemDomEntity|null}
*/
getFromDom : function(domNode, BemDomEntity) {
return getEntityFromDom(domNode, BemDomEntity)
},
/**
* Initializes a BEM entity on a DOM node and returns the instance
* @param {HTMLElement|jQuery} domNode DOM node or jQuery element
* @param {Function|String} Entity Entity class or entity name
* @param {Object} [params] Initialization parameters
* @returns {BemDomEntity|null}
*/
initOnDom : function(domNode, Entity, params) {
const domElem = domNode.jquery ? domNode : $(domNode)
const entityName = typeof Entity === 'string' ? Entity : Entity.getEntityName()
const entity = initEntity(entityName, domElem, params, true)
return entity ? entity._setInitedMod() : null
}
}
// Initialize DOM-dependent properties on DOM ready
$(function() {
bemDom.scope = $('body')
bemDom.doc = $(document)
bemDom.win = $(window)
})
export default bemDom
================================================
FILE: common.blocks/i-bem-dom/i-bem-dom.ru.md
================================================
# i-bem-dom
Блок-хелпер, позволяющий создавать другие блоки, имеющие DOM-представление.
Реализация блока представляет собой специализированный JavaScript-фреймворк для веб-разработки в рамках методологии БЭМ.
В виде отдельного документа доступно [подробное руководство пользователя](https://ru.bem.info/technology/i-bem/v4/i-bem-js/).
================================================
FILE: common.blocks/i-bem-dom/i-bem-dom.spec.js
================================================
modules.define('spec', [
'i-bem',
'i-bem-dom',
'i-bem-dom__collection',
'objects',
'functions',
'jquery',
'chai',
'sinon',
'BEMHTML'
], function(provide,
bem,
bemDom,
BemDomCollection,
objects,
functions,
$,
chai,
sinon,
BEMHTML
) {
var undef,
expect = chai.expect;
describe('i-bem-dom', function() {
var rootNode;
afterEach(function() {
if(rootNode) {
bemDom.destruct(rootNode);
rootNode = null;
}
objects.each(bem.entities, function(_, entityName) {
delete bem.entities[entityName];
});
});
describe('decl', function() {
it('should enable to inherit block to itself', function() {
var spy1 = sinon.spy(),
spy2 = sinon.spy(),
Block = bemDom.declBlock('block', {
onSetMod : {
js : {
inited : spy1
}
}
}),
Block2 = bemDom.declBlock('block', {
onSetMod : {
js : {
inited : spy2
}
}
}),
block = (rootNode = createDomNode({
block : 'block'
})).bem(Block);
Block2.should.be.equal(Block);
spy1.should.not.have.been.called;
spy2.should.have.been.called;
});
it('should enable to inherit block to itself using entity class', function() {
var spy1 = sinon.spy(),
spy2 = sinon.spy(),
Block = bemDom.declBlock('block', {
onSetMod : {
js : {
inited : spy1
}
}
}),
Block2 = bemDom.declBlock(Block, {
onSetMod : {
js : {
inited : spy2
}
}
}),
block = (rootNode = createDomNode({
block : 'block'
})).bem(Block);
Block2.should.be.equal(Block);
spy1.should.not.have.been.called;
spy2.should.have.been.called;
});
it('should enable to inherit elem to itself', function() {
var spy1 = sinon.spy(),
spy2 = sinon.spy(),
Elem = bemDom.declElem('block', 'elem', {
onSetMod : {
js : {
inited : spy1
}
}
}),
Elem2 = bemDom.declElem('block', 'elem', {
onSetMod : {
js : {
inited : spy2
}
}
}),
elem = (rootNode = createDomNode({
block : 'block',
elem : 'elem'
})).bem(Elem);
Elem2.should.be.equal(Elem);
spy1.should.not.have.been.called;
spy2.should.have.been.called;
});
it('should enable to inherit elem to itself using entity class', function() {
var spy1 = sinon.spy(),
spy2 = sinon.spy(),
Elem = bemDom.declElem('block', 'elem', {
onSetMod : {
js : {
inited : spy1
}
}
}),
Elem2 = bemDom.declElem(Elem, {
onSetMod : {
js : {
inited : spy2
}
}
}),
elem = (rootNode = createDomNode({
block : 'block',
elem : 'elem'
})).bem(Elem);
Elem2.should.be.equal(Elem);
spy1.should.not.have.been.called;
spy2.should.have.been.called;
});
it('should enable to mix block', function() {
var MixBlock = bemDom.declMixin({}),
Block = bemDom.declBlock('block', MixBlock),
block = (rootNode = createDomNode({
block : 'block'
})).bem(Block),
Elem = bemDom.declElem('block', 'elem', MixBlock),
elem = (rootNode = createDomNode({
block : 'block',
elem : 'elem'
})).bem(Elem);
block.should.be.instanceOf(bemDom.Block);
elem.should.be.instanceOf(bemDom.Elem);
});
it('should enable to inherit and mix blocks', function() {
var MixBlock = bemDom.declMixin({}),
Block1 = bemDom.declBlock('block1'),
Block2 = bemDom.declBlock('block2', [Block1, MixBlock]),
block2 = (rootNode = createDomNode({
block : 'block2'
})).bem(Block2),
Elem1 = bemDom.declElem('block', 'elem1'),
Elem2 = bemDom.declElem('block', 'elem2', [Elem1, MixBlock]),
elem2 = (rootNode = createDomNode({
block : 'block',
elem : 'elem2'
})).bem(Elem2);
block2.should.be.instanceOf(Block1);
elem2.should.be.instanceOf(Elem1);
});
});
describe('getMod', function() {
it('should return properly extracted mod from html', function() {
var Block = bemDom.declBlock('block');
[
{
mods : undef,
val : ''
},
{
mods : { m1 : 'v1' },
val : 'v1'
},
{
mods : { m1 : 'v1' },
mix : { block : 'block2', mods : { 'm1' : 'v2' } },
val : 'v1'
},
{
mods : { m1 : true },
val : true
}
].forEach(function(data) {
(rootNode = createDomNode({
block : 'block',
mods : data.mods,
mix : data.mix
})).bem(Block).getMod('m1')
.should.be.eql(data.val);
bemDom.destruct(rootNode);
});
});
it('should return properly extracted elem mod from html', function() {
var Block = bemDom.declBlock('block');
[
{
elemMods : undef,
val : ''
},
{
elemMods : { m1 : 'v1' },
val : 'v1'
},
{
elemMods : { m1 : 'v1', m2 : 'v2' },
val : 'v1'
},
{
elemMods : { m1 : 'v1', m2 : 'v11' },
mix : { elem : 'elem1', elemMods : { m1 : 'v2' } },
val : 'v1'
},
{
elemMods : { m1 : true },
val : true
}
].forEach(function(data) {
(rootNode = createDomNode({
block : 'block',
content : {
elem : 'elem',
elemMods : data.elemMods,
mix : data.mix
}
})).bem(Block)._elem('elem').getMod('m1')
.should.be.equal(data.val);
bemDom.destruct(rootNode);
});
});
});
describe('setMod', function() {
it('should properly set CSS class names', function() {
var Block = bemDom.declBlock('block');
[
{
beforeMods : undef,
afterCls : 'block i-bem block_js_inited block_m1_v1',
mods : { m1 : 'v1' }
},
{
beforeMods : { m6 : true, m7 : 'v7' },
afterCls : 'block i-bem block_js_inited block_m1_v1 block_m2_v2 block_m3 block_m4_v4 block_m5',
mods : { m1 : 'v1', m2 : 'v2', m3 : true, m4 : 'v4', m5 : true, m6 : false, m7 : '' }
},
{
beforeMods : { m6 : true, m7 : 'v7' },
afterCls : 'block bla-block bla-block_m3 bla-block_m1_v1 i-bem block_js_inited block_m1_v1 block_m2_v2 block_m3 block_m4_v4 block_m5',
mods : { m1 : 'v1', m2 : 'v2', m3 : true, m4 : 'v4', m5 : true, m6 : false, m7 : '' },
mix : { block : 'bla-block', mods : { m3 : true, m1 : 'v1' } }
}
].forEach(function(data) {
var block = (rootNode = createDomNode({
block : 'block',
mods : data.beforeMods,
mix : data.mix
})).bem(Block);
objects.each(data.mods, function(modVal, modName) {
modName === 'm3'?
block.setMod(modName) :
block.setMod(modName, modVal);
});
block.domElem[0].className.should.be.equal(data.afterCls);
bemDom.destruct(rootNode);
});
});
it('should properly set elem CSS class names', function() {
var Block = bemDom.declBlock('block'),
rootNode;
[
{
elemMods : undef,
afterCls : 'block__elem i-bem block__elem_js_inited block__elem_m1_v1',
mods : { m1 : 'v1' }
},
{
elemMods : { m6 : true, m7 : 'v7' },
afterCls : 'block__elem i-bem block__elem_js_inited block__elem_m1_v1 block__elem_m2_v2 block__elem_m3 block__elem_m4_v4 block__elem_m5',
mods : { m1 : 'v1', m2 : 'v2', m3 : true, m4 : 'v4', m5 : true, m6 : false, m7 : '' }
}
].forEach(function(data) {
var elem = (rootNode = createDomNode({
block : 'block',
content : {
elem : 'elem',
mods : data.elemMods
}
})).bem(Block)._elem('elem');
objects.each(data.mods, function(modVal, modName) {
modName === 'm3'?
elem.setMod(modName) :
elem.setMod(modName, modVal);
});
elem.domElem[0].className.should.be.equal(data.afterCls);
bemDom.destruct(rootNode);
});
});
});
describe('find*Block(s)', function() {
var rootBlock,
B1Block, B3Block, B4Block, B5Block;
beforeEach(function() {
var RootBlock = bemDom.declBlock('root');
B1Block = bemDom.declBlock('b1');
B3Block = bemDom.declBlock('b3');
B4Block = bemDom.declBlock('b4');
B5Block = bemDom.declBlock('b5');
rootNode = createDomNode({
block : 'root',
content : {
block : 'b1',
js : { id : '1' },
mods : { m2 : 'v1', m3 : true },
content : [
{ block : 'b2' },
{
block : 'b1',
mods : { m1 : 'v1' },
js : { id : '2' }
},
{
block : 'b3',
mods : { m1 : true },
js : { id : '5' },
mix : [
{ block : 'b4', js : { id : '6' } },
{
block : 'b5',
mods : { m1 : 'v1' },
js : { id : '7' }
}
],
content : {
block : 'b1',
mods : { m1 : 'v2' },
js : { id : '3' },
content : {
block : 'b1',
mods : { m1 : true },
js : { id : '4' }
}
}
},
{
block : 'b3', js : { id : '5' },
mix : { block : 'b4', js : { id : '8' } }
}
]
}
});
rootBlock = rootNode.bem(RootBlock);
});
describe('findChildBlocks', function() {
it('should throw error if Block given as string', function() {
function find() {
rootBlock.findChildBlocks('string');
}
find.should.throw(Error, 'Block must be a class or description (block, modName, modVal) of the block to find');
});
it('should throw error if Block given as description object with block as string', function() {
function find() {
rootBlock.findChildBlocks({ block : 'string' });
}
find.should.throw(Error, 'Block must be a class or description (block, modName, modVal) of the block to find');
});
it('should return BEM-collection', function() {
rootBlock.findChildBlocks(B1Block).should.be.instanceOf(BemDomCollection);
});
it('should return instances of Block founded by class', function() {
rootBlock.findChildBlocks(B1Block).forEach(function(block) {
block.should.be.instanceOf(B1Block);
});
});
it('should find all blocks by block class', function() {
getEntityIds(rootBlock.findChildBlocks(B1Block)).should.be.eql(['1', '2', '3', '4']);
});
it('should find all blocks by block class, modName and modVal', function() {
getEntityIds(rootBlock.findChildBlocks({ block : B1Block, modName : 'm1', modVal : 'v1' }))
.should.be.eql(['2']);
});
it('should find all blocks by block class and boolean mod', function() {
getEntityIds(rootBlock.findChildBlocks({ block : B1Block, modName : 'm1', modVal : true }))
.should.be.eql(['4']);
});
it('should find all blocks by block class and boolean mod without modVal', function() {
getEntityIds(rootBlock.findChildBlocks({ block : B1Block, modName : 'm1' }))
.should.be.eql(['4']);
});
});
describe('findChildBlock', function() {
it('should throw error if Block given as string', function() {
function find() {
rootBlock.findChildBlock('string');
}
find.should.throw(Error, 'Block must be a class or description (block, modName, modVal) of the block to find');
});
it('should throw error if Block given as description object with block as string', function() {
function find() {
rootBlock.findChildBlock({ block : 'string' });
}
find.should.throw(Error, 'Block must be a class or description (block, modName, modVal) of the block to find');
});
it('should return instance of Block found by class', function() {
rootBlock.findChildBlock(B1Block).should.be.instanceOf(B1Block);
});
it('should return null if nothing found', function() {
var B99Block = bemDom.declBlock('b99');
expect(rootBlock.findChildBlock(B99Block)).to.be.null;
});
it('should find first block by block class', function() {
rootBlock.findChildBlock(B1Block).params.id
.should.be.equal('1');
});
it('should find first block by block class, modName and modVal', function() {
rootBlock.findChildBlock({ block : B1Block, modName : 'm1', modVal : 'v1' })
.params.id
.should.be.equal('2');
});
it('should find first block by block class and boolean mod', function() {
rootBlock.findChildBlock({ block : B1Block, modName : 'm1', modVal : true })
.params.id
.should.be.equal('4');
});
it('should find first block by block class and boolean mod without modVal', function() {
rootBlock.findChildBlock({ block : B1Block, modName : 'm1' })
.params.id
.should.be.equal('4');
});
});
describe('findParentBlocks', function() {
var leafBlock;
beforeEach(function() {
leafBlock = rootBlock.findChildBlock({ block : B1Block, modName : 'm1', modVal : true });
});
it('should throw error if Block given as string', function() {
function find() {
rootBlock.findParentBlocks('string');
}
find.should.throw(Error, 'Block must be a class or description (block, modName, modVal) of the block to find');
});
it('should throw error if Block given as description object with block as string', function() {
function find() {
rootBlock.findParentBlocks({ block : 'string' });
}
find.should.throw(Error, 'Block must be a class or description (block, modName, modVal) of the block to find');
});
it('should return BEM-collection', function() {
leafBlock.findParentBlocks(B1Block).should.be.instanceOf(BemDomCollection);
});
it('should find all ancestor blocks by block class', function() {
getEntityIds(leafBlock.findParentBlocks(B1Block)).should.be.eql(['3', '1']);
});
it('should find all ancestor blocks by block class, modName and modVal', function() {
getEntityIds(leafBlock.findParentBlocks({ block : B1Block, modName : 'm1', modVal : 'v2' }))
.should.be.eql(['3']);
});
it('should find all ancestor blocks by block class and boolean mod', function() {
getEntityIds(leafBlock.findParentBlocks({ block : B3Block, modName : 'm1', modVal : true }))
.should.be.eql(['5']);
});
it('should find all ancestor blocks by block class and boolean mod without modVal', function() {
getEntityIds(leafBlock.findParentBlocks({ block : B3Block, modName : 'm1' }))
.should.be.eql(['5']);
});
});
describe('findParentBlock', function() {
var leafBlock;
beforeEach(function() {
leafBlock = rootBlock.findChildBlock({ block : B1Block, modName : 'm1', modVal : true });
});
it('should throw error if Block given as string', function() {
function find() {
rootBlock.findParentBlock('string');
}
find.should.throw(Error, 'Block must be a class or description (block, modName, modVal) of the block to find');
});
it('should throw error if Block given as description object with block as string', function() {
function find() {
rootBlock.findParentBlock({ block : 'string' });
}
find.should.throw(Error, 'Block must be a class or description (block, modName, modVal) of the block to find');
});
it('should find first ancestor block by block class', function() {
leafBlock.findParentBlock(B1Block).params.id.should.be.equal('3');
});
it('should find first ancestor block by block class, modName and modVal', function() {
leafBlock.findParentBlock({ block : B1Block, modName : 'm2', modVal : 'v1' })
.params.id
.should.be.equal('1');
});
it('should find first ancestor block by block class and boolean mod', function() {
leafBlock.findParentBlock({ block : B1Block, modName : 'm3', modVal : true })
.params.id
.should.be.equal('1');
});
it('should find first ancestor block by block class and boolean mod without modVal', function() {
leafBlock.findParentBlock({ block : B1Block, modName : 'm3' })
.params.id
.should.be.equal('1');
});
});
describe('findMixedBlocks', function() {
it('should throw error if Block given as string', function() {
function find() {
rootBlock.findMixedBlocks('string');
}
find.should.throw(Error, 'Block must be a class or description (block, modName, modVal) of the block to find');
});
it('should throw error if Block given as description object with block as string', function() {
function find() {
rootBlock.findMixedBlocks({ block : 'string' });
}
find.should.throw(Error, 'Block must be a class or description (block, modName, modVal) of the block to find');
});
it('should return BEM-collection', function() {
rootBlock.findChildBlock({ block : B3Block }).findMixedBlocks(B4Block)
.should.be.instanceOf(BemDomCollection);
});
it('should find all mixed blocks by block class', function() {
getEntityIds(
rootBlock.findChildBlock({ block : B3Block }).findMixedBlocks(B4Block))
.should.be.eql(['6', '8']);
});
});
describe('findMixedBlock', function() {
it('should throw error if Block given as string', function() {
function find() {
rootBlock.findMixedBlock('string');
}
find.should.throw(Error, 'Block must be a class or description (block, modName, modVal) of the block to find');
});
it('should throw error if Block given as description object with block as string', function() {
function find() {
rootBlock.findMixedBlock({ block : 'string' });
}
find.should.throw(Error, 'Block must be a class or description (block, modName, modVal) of the block to find');
});
it('should find first mixed block by block class', function() {
rootBlock.findChildBlock({ block : B3Block })
.findMixedBlock(B4Block)
.params.id
.should.be.equal('6');
});
it('should find mixed block by block class, modName and modVal', function() {
rootBlock.findChildBlock({ block : B4Block })
.findMixedBlock({ block : B5Block, modName : 'm1', modVal : 'v1' })
.params.id
.should.be.equal('7');
});
it('should find mixed block by block class and boolean mod', function() {
rootBlock.findChildBlock({ block : B4Block })
.findMixedBlock({ block : B3Block, modName : 'm1', modVal : true })
.params.id
.should.be.equal('5');
});
it('should find mixed block by block class and boolean mod without modVal', function() {
rootBlock.findChildBlock({ block : B4Block })
.findMixedBlock({ block : B3Block, modName : 'm1' })
.params.id
.should.be.equal('5');
});
});
});
describe('find*Elem(s)', function() {
var b1Block,
B1E1Elem, B1E2Elem, B1E3Elem, B1E4Elem, B1E5Elem, B1E6Elem;
beforeEach(function() {
var B1Block = bemDom.declBlock('b1');
B1E1Elem = bemDom.declElem('b1', 'e1');
B1E2Elem = bemDom.declElem('b1', 'e2');
B1E3Elem = bemDom.declElem('b1', 'e3');
B1E4Elem = bemDom.declElem('b1', 'e4');
B1E5Elem = bemDom.declElem('b1', 'e5');
B1E6Elem = bemDom.declElem('b1', 'e6');
rootNode = createDomNode(
{
block : 'b1',
content : [
{
block : 'b2',
content : { elem : 'e1' }
},
{
elem : 'e2',
content : { elem : 'e5', js : { id : '9' } }
},
{
elem : 'e1',
elemMods : { m2 : 'v1' },
js : { id : '1' },
content : [
{
elem : 'e1',
elemMods : { m1 : 'v1' },
js : { id : '2' }
},
{
block : 'b3',
content : {
block : 'b1',
elem : 'e1',
elemMods : { m1 : 'v2', m2 : true },
mix : [
{ elem : 'e3', js : { id : '5' } },
{ elem : 'e4', js : { id : '6' } }
],
js : { id : '3' },
content : [
{
elem : 'e1',
elemMods : { m1 : true },
js : { id : '4' }
},
{
block : 'b1',
content : { elem : 'e6', js : { id : '10' } }
}
]
}
},
{ elem : 'e6', js : { id : '11' } }
]
},
{
block : 'b1', elem : 'e3', js : { id : '5' },
mix : {
elem : 'e4',
elemMods : { m2 : 'v1' },
js : { id : '8' }
}
}
]
});
b1Block = rootNode.bem(B1Block);
});
describe('findChildElems', function() {
it('should return BEM-collection', function() {
b1Block.findChildElems(B1E1Elem).should.be.instanceOf(BemDomCollection);
});
it('should return instances of Elem founded by class', function() {
b1Block.findChildElems(B1E1Elem).forEach(function(elem) {
elem.should.be.instanceOf(B1E1Elem);
});
});
it('should find all elems by elem class', function() {
getEntityIds(b1Block.findChildElems(B1E1Elem)).should.be.eql(['1', '2', '3', '4']);
});
it('should find all elems by elem name', function() {
getEntityIds(b1Block.findChildElems('e1')).should.be.eql(['1', '2', '3', '4']);
});
it('should find all elems by elem class, modName and modVal', function() {
getEntityIds(
b1Block.findChildElems({ elem : B1E1Elem, modName : 'm1', modVal : 'v1' }))
.should.be.eql(['2']);
});
it('should find all elems by elem class and boolean mod', function() {
getEntityIds(
b1Block.findChildElems({ elem : B1E1Elem, modName : 'm1', modVal : true }))
.should.be.eql(['4']);
});
it('should find all elems by elem class and boolean mod without modVal', function() {
getEntityIds(
b1Block.findChildElems({ elem : B1E1Elem, modName : 'm1' }))
.should.be.eql(['4']);
});
it('should find elems in strict mode', function() {
getEntityIds(b1Block.findChildElems(B1E6Elem, true)).should.be.eql(['11']);
});
});
describe('findChildElem', function() {
it('should return instance of Elem founded by class', function() {
b1Block.findChildElem(B1E1Elem).should.be.instanceOf(B1E1Elem);
});
it('should return null if nothing found', function() {
var B99Elem = bemDom.declElem('b1', 'e99');
expect(b1Block.findChildElem(B99Elem)).to.be.null;
});
it('should find first elem by elem class', function() {
b1Block.findChildElem(B1E1Elem).params.id.should.be.equal('1');
});
it('should find first elem by elem name', function() {
b1Block.findChildElem('e1').params.id.should.be.equal('1');
});
it('should find first elem by elem class, modName and modVal', function() {
b1Block.findChildElem({ elem : B1E1Elem, modName : 'm1', modVal : 'v1' })
.params.id
.should.be.equal('2');
});
it('should find first elem by elem name, modName and modVal', function() {
b1Block.findChildElem({ elem : 'e1', modName : 'm1', modVal : 'v1' })
.params.id
.should.be.equal('2');
});
it('should find first elem by elem class and boolean mod', function() {
b1Block.findChildElem({ elem : B1E1Elem, modName : 'm1', modVal : true })
.params.id
.should.be.equal('4');
});
it('should find first elem by elem class and boolean mod without modVal', function() {
b1Block.findChildElem({ elem : B1E1Elem, modName : 'm1' })
.params.id
.should.be.equal('4');
});
it('should find first elem inside elem', function() {
b1Block
.findChildElem({ elem : B1E2Elem })
.findChildElem({ elem : B1E5Elem })
.params.id
.should.be.equal('9');
});
it('should find elem in strict mode', function() {
b1Block.findChildElem(B1E6Elem, true)
.params.id
.should.be.equal('11');
});
});
describe('findParentElems', function() {
var leafEntity;
beforeEach(function() {
leafEntity = b1Block.findChildElem({ elem : B1E1Elem, modName : 'm1', modVal : true });
});
it('should return BEM-collection', function() {
leafEntity.findParentElems(B1E1Elem).should.be.instanceOf(BemDomCollection);
});
it('should find all ancestor elems by elem class', function() {
getEntityIds(leafEntity.findParentElems(B1E1Elem)).should.be.eql(['3', '1']);
});
it('should find all ancestor elems by elem class, modName and modVal', function() {
getEntityIds(leafEntity.findParentElems({ elem : B1E1Elem, modName : 'm2', modVal : 'v1' }))
.should.be.eql(['1']);
});
it('should find all ancestor elems by elem name, modName and modVal', function() {
getEntityIds(leafEntity.findParentElems({ elem : 'e1', modName : 'm2', modVal : 'v1' }))
.should.be.eql(['1']);
});
it('should find all ancestor elems by elem class and boolean mod', function() {
getEntityIds(leafEntity.findParentElems({ elem : B1E1Elem, modName : 'm2', modVal : true }))
.should.be.eql(['3']);
});
it('should find all ancestor elems by elem class and boolean mod without modVal', function() {
getEntityIds(leafEntity.findParentElems({ elem : B1E1Elem, modName : 'm2' }))
.should.be.eql(['3']);
});
});
describe('findParentElem', function() {
var leafEntity;
beforeEach(function() {
leafEntity = b1Block.findChildElem({ elem : B1E1Elem, modName : 'm1', modVal : true });
});
it('should find first ancestor elem by elem class', function() {
leafEntity.findParentElem(B1E1Elem)
.params.id
.should.be.equal('3');
});
it('should find first ancestor elem by elem class, modName and modVal', function() {
leafEntity.findParentElem({ elem : B1E1Elem, modName : 'm2', modVal : 'v1' })
.params.id
.should.be.equal('1');
});
it('should find first ancestor elem by elem name, modName and modVal', function() {
leafEntity.findParentElem({ elem : 'e1', modName : 'm2', modVal : 'v1' })
.params.id
.should.be.equal('1');
});
it('should find first ancestor elem by elem class and boolean mod', function() {
leafEntity.findParentElem({ elem : B1E1Elem, modName : 'm2', modVal : true })
.params.id
.should.be.equal('3');
});
it('should find first ancestor elem by elem class and boolean mod without modVal', function() {
leafEntity.findParentElem({ elem : B1E1Elem, modName : 'm2' })
.params.id
.should.be.equal('3');
});
});
describe('findMixedElems', function() {
it('should return BEM-collection', function() {
b1Block.findChildElem(B1E3Elem).findMixedElems(B1E4Elem)
.should.be.instanceOf(BemDomCollection);
});
it('should find all mixed elems by elem class', function() {
getEntityIds(
b1Block.findChildElem(B1E3Elem).findMixedElems(B1E4Elem))
.should.be.eql(['6', '8']);
});
});
describe('findMixedElem', function() {
it('should find mixed elem by elem class', function() {
b1Block.findChildElem(B1E3Elem)
.findMixedElem(B1E4Elem)
.params.id
.should.be.equal('6');
});
it('should find first mixed elem by elem name', function() {
b1Block.findChildElem(B1E3Elem)
.findMixedElem('e4')
.params.id
.should.be.equal('6');
});
it('should find first mixed elem by elem class, modName and modVal', function() {
b1Block.findChildElem(B1E3Elem)
.findMixedElem({ elem : B1E4Elem, modName : 'm2', modVal : 'v1' })
.params.id
.should.be.equal('8');
});
it('should find first mixed elem by elem class and boolean mod', function() {
b1Block.findChildElem(B1E3Elem)
.findMixedElem({ elem : B1E1Elem, modName : 'm2', modVal : true })
.params.id
.should.be.equal('3');
});
it('should find first mixed elem by elem class and boolean mod without modVal', function() {
b1Block.findChildElem(B1E3Elem)
.findMixedElem({ elem : B1E1Elem, modName : 'm2' })
.params.id
.should.be.equal('3');
});
});
});
describe('elem(s)', function() {
var b1Block,
B1E1Elem,
B1E2Elem,
B1E3Elem,
B1E4Elem,
spy;
beforeEach(function() {
var B1Block = bemDom.declBlock('b1');
B1E1Elem = bemDom.declElem('b1', 'e1');
B1E2Elem = bemDom.declElem('b1', 'e2');
B1E3Elem = bemDom.declElem('b1', 'e3');
B1E4Elem = bemDom.declElem('b1', 'e4');
rootNode = createDomNode({
block : 'b1',
mix : { elem : 'e1', js : { id : 1 } },
content : [
{ elem : 'e1', elemMods : { m1 : 'v1' }, js : { id : 2 } },
{
elem : 'e2', js : { id : 3 },
content : [
{
elem : 'e1', js : { id : 4 },
elemMods : { inner : 'no' }
},
{
elem : 'e3', js : { id : 5 },
elemMods : { inner : 'no', bool : true }
}
]
},
{
elem : 'e3', js : { id : 6 },
content : {
elem : 'e2', js : { id : 7 },
elemMods : { inner : 'yes', bool : true },
content : {
elem : 'e1', js : { id : 8 },
elemMods : { inner : 'yes', bool : true }
}
}
},
{ elem : 'e2', js : { id : 9 }, elemMods : { bool : true } },
{ elem : 'e4' }
]
});
b1Block = rootNode.bem(B1Block);
});
afterEach(function() {
spy.restore();
});
describe('elems', function() {
beforeEach(function() {
spy = sinon.spy(b1Block, 'findChildElems');
});
it('should find all elems by elem class', function() {
getEntityIds(b1Block._elems(B1E1Elem))
.should.be.eql([1, 2, 4, 8]);
});
it('should find all elems by elem class modName and modVal', function() {
getEntityIds(b1Block._elems({ elem : B1E1Elem, modName : 'm1', modVal : 'v1' }))
.should.be.eql([2]);
});
it('should cache found elems', function() {
b1Block._elems(B1E1Elem).should.be.equal(b1Block._elems(B1E1Elem));
spy.should.be.calledOnce;
});
it('should cache found elems with respect to mods', function() {
b1Block._elems({ elem : B1E1Elem, modName : 'm1', modVal : 'v1' });
spy.should.be.calledOnce;
b1Block._elems(B1E1Elem);
spy.should.be.calledTwice;
});
it('should find elems by boolean mod without modVal', function() {
getEntityIds(b1Block._elems({ elem : B1E1Elem, modName : 'bool' }))
.should.be.eql([8]);
});
it('should cache found elems with respect to boolean mod without modVal', function() {
b1Block._elems({ elem : B1E1Elem, modName : 'bool' });
spy.should.be.calledOnce;
b1Block._elems({ elem : B1E1Elem, modName : 'bool' });
spy.should.be.calledOnce;
b1Block._elems(B1E1Elem);
spy.should.be.calledTwice;
});
it('should not drop elems cache in case elem mods change', function() {
var elem = b1Block._elems(B1E1Elem).get(0);
spy.should.be.calledOnce;
elem.setMod('m2', 'v1');
b1Block._elems(B1E1Elem);
spy.should.be.calledOnce;
});
it('should drop elems cache in case mods change', function() {
var elem = b1Block._elem(B1E1Elem);
b1Block._elems({ elem : B1E1Elem, modName : 'm2', modVal : 'v1' });
spy.should.be.calledOnce;
elem.setMod('m2', 'v1');
b1Block._elems({ elem : B1E1Elem, modName : 'm2', modVal : 'v1' });
spy.should.be.calledTwice;
elem.delMod('m2');
b1Block._elems({ elem : B1E1Elem, modName : 'm2', modVal : 'v1' });
spy.should.be.calledThrice;
});
});
describe('elem', function() {
beforeEach(function() {
spy = sinon.spy(b1Block, 'findMixedElem');
});
it('should find first elem by elem class', function() {
b1Block._elem(B1E1Elem)
.params.id
.should.be.equal(1);
});
it('should find first elem by elem class modName and modVal', function() {
b1Block._elem({ elem : B1E1Elem, modName : 'm1', modVal : 'v1' })
.params.id
.should.be.equal(2);
});
it('should cache found elem', function() {
b1Block._elem(B1E1Elem);
b1Block._elem(B1E1Elem);
spy.should.be.calledOnce;
});
it('should cache found elem with respect to mods', function() {
b1Block._elem({ elem : B1E1Elem, modName : 'm1', modVal : 'v1' });
spy.should.be.calledOnce;
b1Block._elem(B1E1Elem);
spy.should.be.calledTwice;
});
it('should not drop elem cache in case elem mods change', function() {
var elem = b1Block._elem(B1E1Elem);
spy.should.be.calledOnce;
elem.setMod('m2', 'v1');
b1Block._elem(B1E1Elem);
spy.should.be.calledOnce;
});
it('should drop elem cache in case mods change', function() {
var elem = b1Block._elems(B1E1Elem).get(0);
b1Block._elem({ elem : B1E1Elem, modName : 'm2', modVal : 'v1' });
spy.should.be.calledOnce;
elem.setMod('m2', 'v1');
b1Block._elem({ elem : B1E1Elem, modName : 'm2', modVal : 'v1' });
spy.should.be.calledTwice;
elem.delMod('m2');
b1Block._elem({ elem : B1E1Elem, modName : 'm2', modVal : 'v1' });
spy.should.be.calledThrice;
});
});
describe('drop cache', function() {
var b1e2DomElem;
beforeEach(function() {
b1e2DomElem = b1Block.findChildElem(B1E2Elem).domElem;
spy = sinon.spy(b1Block, 'findChildElems');
});
describe('for affected elems', function() {
it('should drop elems cache on DOM destruct', function() {
b1Block._elems(B1E1Elem);
bemDom.destruct(b1e2DomElem);
b1Block._elems(B1E1Elem);
spy.should.be.calledTwice;
});
it('should drop elems cache on DOM update', function() {
b1Block._elems(B1E1Elem);
bemDom.update(b1e2DomElem, BEMHTML.apply({
block : 'b1',
elem : 'e1'
}));
b1Block._elems(B1E1Elem);
spy.should.be.calledTwice;
});
it('should drop elems cache on DOM replace', function() {
b1Block._elems(B1E1Elem);
bemDom.replace(b1e2DomElem, BEMHTML.apply({
block : 'b1',
elem : 'e1'
}));
b1Block._elems(B1E1Elem);
spy.should.be.calledTwice;
});
it('should drop elems cache on DOM append', function() {
b1Block._elems(B1E1Elem);
bemDom.append(b1Block.domElem, BEMHTML.apply({
block : 'b1',
elem : 'e1',
js : true
}));
b1Block._elems(B1E1Elem);
spy.should.be.calledTwice;
});
// NOTE: does't work because of too complex elems cache maintaince in case of elems without js
it.skip('should drop elems cache on DOM append of elems without js', function() {
b1Block._elems(B1E1Elem);
bemDom.append(b1Block.domElem, BEMHTML.apply({
block : 'b1',
elem : 'e1'
}));
b1Block._elems(B1E1Elem);
spy.should.be.calledTwice;
});
it('should drop elems cache on DOM update in case of elem without data-bem', function() {
b1Block._elems(B1E1Elem);
bemDom.append(b1Block.domElem, BEMHTML.apply({
block : 'b1',
elem : 'e2',
js : true,
mix : { block : 'b1', elem : 'e1', js : true }
}));
b1Block._elems(B1E1Elem);
spy.should.be.calledTwice;
});
});
describe('for not affected elems', function() {
it('should not drop elems cache on DOM destruct', function() {
b1Block._elems(B1E4Elem);
bemDom.destruct(b1e2DomElem);
b1Block._elems(B1E4Elem);
spy.should.have.been.calledOnce;
});
it('should not drop elems cache on DOM update', function() {
b1Block._elems(B1E4Elem);
bemDom.update(b1e2DomElem, BEMHTML.apply({
block : 'b1',
elem : 'e1'
}));
b1Block._elems(B1E4Elem);
spy.should.have.been.calledOnce;
});
it('should not drop elems cache on DOM replace', function() {
b1Block._elems(B1E4Elem);
bemDom.replace(b1e2DomElem, BEMHTML.apply({
block : 'b1',
elem : 'e1'
}));
b1Block._elems(B1E4Elem);
spy.should.have.been.calledOnce;
});
});
});
});
describe('bemDom.init', function() {
var spy;
beforeEach(function() {
spy = sinon.spy();
});
it('should init block', function() {
bemDom.declBlock('block', {
onSetMod : {
js : {
inited : spy
}
}
});
rootNode = createDomNode({
tag : 'div',
content : { block : 'block', js : true }
});
spy.should.have.been.called;
});
it('should properly init block with multiple DOM nodes', function(done) {
bemDom.declBlock('block', {
onSetMod : {
js : {
inited : function() {
this.domElem.length.should.be.equal(2);
done();
}
}
}
});
rootNode = createDomNode({
tag : 'div',
content : [
{ block : 'block', js : { id : 'id' } },
{ block : 'block', js : { id : 'id' } }
]
});
});
it('should properly init elem with multiple DOM nodes', function(done) {
bemDom.declBlock('block');
bemDom.declElem('block', 'e1', {
onSetMod : {
js : {
inited : function() {
this.domElem.length.should.be.equal(2);
done();
}
}
}
});
rootNode = createDomNode({
block : 'block',
content : [
{ elem : 'e1', js : { id : 'id' } },
{ elem : 'e1', js : { id : 'id' } }
]
});
});
it('shouldn\'t init lazy block', function() {
bemDom.declBlock('block', {
onSetMod : {
js : {
inited : spy
}
}
}, {
lazyInit : true
});
rootNode = initDom({
tag : 'div',
content : { block : 'block', js : true }
});
spy.should.not.have.been.called;
});
it('should allow to pass string', function() {
bemDom.declBlock('block', {
onSetMod : {
js : {
inited : spy
}
}
});
rootNode = initDom({
tag : 'div',
content : { block : 'block', js : true }
});
spy.should.have.been.called;
});
});
describe('bemDom.detach', function() {
it('should detach block but leave DOM node', function() {
var spy = sinon.spy();
bemDom.declBlock('block1', {
onSetMod : {
js : {
'' : spy
}
}
});
rootNode = createDomNode({
tag : 'div',
content : { block : 'block1', js : true }
});
bemDom.detach(rootNode.find('.block1'));
spy.should.have.been.calledOnce;
rootNode.find('.block1').length.should.be.equal(1);
});
});
describe('bemDom.destruct', function() {
var spy;
beforeEach(function() {
spy = sinon.spy();
});
it('should destruct block only if it has no dom nodes', function() {
bemDom.declBlock('block', {
onSetMod : {
js : {
'' : spy
}
}
});
rootNode = createDomNode({
tag : 'div',
content : [
{ block : 'block', js : { id : 'block' } },
{ block : 'block', js : { id : 'block' } }
]
});
bemDom.destruct(rootNode.find('.block :eq(0)'));
spy.should.not.have.been.called;
bemDom.destruct(rootNode.find('.block'));
spy.should.have.been.called;
});
it('should destruct implicitly inited block', function() {
var Block = bemDom.declBlock('block', {
onSetMod : {
js : {
'' : spy
}
}
});
rootNode = createDomNode({
tag : 'div',
content : { block : 'block' }
});
var blockNode = rootNode.find('.block');
blockNode.bem(Block);
bemDom.destruct(blockNode);
spy.should.have.been.called;
});
// see https://github.com/bem/bem-core/issues/1383
it('should not re-initialize destructing entities during destruct', function() {
var Block = bemDom.declBlock('block', {
__constructor : function() {
this.__base.apply(this, arguments);
spy();
}
}, {
onInit : function() {
this._domEvents().on('blur', functions.noop);
}
});
rootNode = initDom({
block : 'block',
js : true,
tag : 'input'
});
rootNode.focus();
bemDom.destruct(rootNode);
spy.should.have.been.calledOnce;
});
});
describe('bemDom.update', function() {
it('should properly update tree', function() {
var spyBlock1Destructed = sinon.spy(),
spyBlock2Inited = sinon.spy();
bemDom.declBlock('block1', {
onSetMod : {
js : {
'' : spyBlock1Destructed
}
}
});
bemDom.declBlock('block2', {
onSetMod : {
js : {
inited : spyBlock2Inited
}
}
});
rootNode = createDomNode({
tag : 'div',
content : { block : 'block1', js : true }
});
bemDom.update(rootNode, BEMHTML.apply({ block : 'block2', js : true }))
.should.be.equal(rootNode);
spyBlock1Destructed.called.should.be.true;
spyBlock2Inited.called.should.be.true;
});
it('should allow to pass simple string', function() {
var domElem = $('
');
bemDom.update(domElem, 'simple string');
domElem.html().should.be.equal('simple string');
});
});
describe('bemDom.before', function() {
it('should properly update tree', function() {
var spyBlock2Inited = sinon.spy(),
block2DomElem;
bemDom.declBlock('block2', {
onSetMod : {
js : {
inited : function() {
spyBlock2Inited();
block2DomElem = this.domElem;
}
}
}
});
rootNode = createDomNode({
tag : 'div',
content : { block : 'block1', js : true }
});
var newCtx = bemDom.before(rootNode.find('.block1'), BEMHTML.apply({ block : 'block2', js : true }));
newCtx.is(block2DomElem).should.be.true;
rootNode.children().eq(0).is(block2DomElem).should.be.true;
spyBlock2Inited.called.should.be.true;
});
});
describe('bemDom.replace', function() {
it('should properly replace tree', function() {
var spyBlock1Destructed = sinon.spy(),
spyBlock2Inited = sinon.spy();
bemDom.declBlock('block1', {
onSetMod : {
js : {
'' : spyBlock1Destructed
}
}
});
bemDom.declBlock('block2', {
onSetMod : {
js : {
inited : spyBlock2Inited
}
}
});
rootNode = initDom({
tag : 'div',
content : { block : 'block1', js : true }
});
bemDom.replace(rootNode.find('.block1'), BEMHTML.apply({ block : 'block2', js : true }));
spyBlock1Destructed.should.have.been.calledOnce;
spyBlock2Inited.should.have.been.calledOnce;
rootNode.html().should.be.equal('
');
bemDom.destruct(rootNode);
rootNode = createDomNode({
tag : 'div',
content : [{ tag : 'p' }, { block : 'block1', js : true }, { tag : 'p' }]
});
bemDom.replace(rootNode.find('.block1'), BEMHTML.apply({ block : 'block2', js : true }));
spyBlock1Destructed.should.have.been.calledTwice;
spyBlock2Inited.should.have.been.calledTwice;
rootNode.html().should.be.equal('
');
});
});
// don't add specs for other DOM changing methods as they are implemented the same way
describe('params', function() {
it('should properly join params', function(done) {
var Block = bemDom.declBlock('block', {
_getDefaultParams : function() {
return { p1 : 1 };
}
});
bemDom.declBlock('block2', {
onSetMod : {
'js' : {
'inited' : function() {
var params = this.findMixedBlock(Block).params;
params.p1.should.be.equal(1);
params.p2.should.be.equal(2);
params.p3.should.be.equal(3);
done();
}
}
}
});
rootNode = createDomNode({
tag : 'div',
content : [
{ block : 'block', js : { id : 'bla', p2 : 2 }, mix : { block : 'block2', js : true } },
{ block : 'block', js : { id : 'bla', p3 : 3 } }
]
});
});
});
describe('containsEntity', function() {
var domElem, block, block2;
beforeEach(function() {
var Block = bemDom.declBlock('block'),
Block2 = bemDom.declBlock('block2');
domElem = initDom([
{
block : 'block',
js : { id : '1' },
content : [
{ elem : 'e1' },
{ elem : 'e2' }
]
},
{
block : 'block',
js : { id : '1' },
content : [
{ elem : 'e1' },
{ elem : 'e2', content : { elem : 'e2-1' } }
]
},
{
block : 'block2'
}
]);
block = domElem.filter('.block').bem(Block);
block2 = domElem.filter('.block2').bem(Block2);
});
it('should properly checks for nested entities', function() {
block.containsEntity(block._elem('e2-1')).should.be.true;
block.containsEntity(block2).should.be.false;
});
});
describe('onInit', function() {
var spy, Block;
beforeEach(function() {
spy = sinon.spy();
Block = bemDom.declBlock('block', {}, {
onInit : spy
});
});
it('should have been called once', function() {
rootNode = initDom([{
block : 'block',
js : true
}, {
block : 'block',
js : true
}]);
spy.should.have.been.calledOnce;
});
it('should have been properly called in case of additional declaration after first initialization', function() {
rootNode = initDom({
block : 'block',
js : true
});
var spy2 = sinon.spy();
bemDom.declBlock('block', {}, {
onInit : spy2
});
spy.should.have.been.calledOnce;
spy2.should.have.been.calledOnce;
});
});
describe('lazy init', function() {
var spy;
it('should be possible to force initialization', function() {
spy = sinon.spy();
bemDom.declBlock('block', {
onSetMod : {
'js' : {
'inited' : spy
}
}
}, {
lazyInit : true
});
rootNode = initDom({
block : 'block',
js : { lazyInit : false }
});
spy.should.have.been.called;
});
it('should be possible to force lazy initialization', function() {
spy = sinon.spy();
bemDom.declBlock('block', {
onSetMod : {
'js' : {
'inited' : spy
}
}
}, {
lazyInit : false
});
rootNode = initDom({
block : 'block',
js : { lazyInit : true }
});
spy.should.have.not.been.called;
});
describe('on DOM events', function() {
beforeEach(function() {
spy = sinon.spy();
bemDom.declBlock('block', {
onSetMod : {
'js' : {
'inited' : spy
}
}
}, {
lazyInit : true,
onInit : function() {
this._domEvents().on('click', functions.noop);
}
});
rootNode = initDom({
block : 'block',
js : true
});
});
it('should init block on DOM event', function() {
spy.should.not.have.been.called;
rootNode.trigger('click');
spy.should.have.been.called;
});
});
describe('on BEM events', function() {
var block2;
beforeEach(function() {
spy = sinon.spy();
bemDom.declBlock('block', {
onSetMod : {
'js' : {
'inited' : spy
}
}
}, {
lazyInit : true,
onInit : function() {
this._events(Block2).on('click', functions.noop);
}
});
var Block2 = bemDom.declBlock('block2');
block2 = initDom({
block : 'block',
js : true,
content : {
block : 'block2',
js : true
}
})
.find(Block2._buildSelector())
.bem(Block2);
});
it('should init block on BEM event', function() {
spy.should.not.have.been.called;
block2._emit('click');
spy.should.have.been.called;
});
});
});
describe('modules.define patching', function() {
it('should provide bemDom block', function(done) {
var name = 'b' + Math.random(),
spy = sinon.spy();
modules.define(name, ['i-bem-dom'], function(provide, bemDom) {
spy();
provide(bemDom.declBlock(this.name, {}));
});
modules.define(name, function(provide, Prev) {
spy();
Prev.should.be.eql(bem.entities[this.name]);
provide(bemDom.declBlock(this.name, {}));
});
modules.require([name], function(Block) {
spy.should.have.been.calledTwice;
Block.should.be.eql(bem.entities[name]);
done();
});
});
});
});
provide();
function createDomNode(bemjson) {
return bemDom.init(BEMHTML.apply(bemjson));
}
function initDom(bemjson) {
return createDomNode(bemjson).appendTo(bemDom.scope);
}
function getEntityIds(entities) {
return entities.map(function(entity) {
return entity.params.id;
});
}
});
================================================
FILE: common.blocks/i-bem-dom/i-bem-dom.tests/benchmarks.bemjson.js
================================================
({
block : 'page',
title : 'v3 benchmarks',
head : {
elem : 'js', url : 'https://yastatic.net/jquery/2.1.3/jquery.js'
},
styles : { elem : 'css', url : '_benchmarks.css' },
scripts : { elem : 'js', url : '_benchmarks.js' },
content : Array.apply(null, { length : 1000 }).map(function() {
return {
block : 'b1',
js : true,
content : {
block : 'b2',
js : true
}
};
})
});
================================================
FILE: common.blocks/i-bem-dom/i-bem-dom.tests/benchmarks.blocks/b1/b1.deps.js
================================================
({
mustDeps : {
block : 'i-bem-dom'
},
shouldDeps : 'b2'
})
================================================
FILE: common.blocks/i-bem-dom/i-bem-dom.tests/benchmarks.blocks/b1/b1.js
================================================
modules.define('b1', ['i-bem-dom', 'b2'], function(provide, bemDom, B2) {
provide(bemDom.declBlock(this.name, {
onSetMod : {
'js' : {
'inited' : function() {
this._events(B2).on('click', this._onEvent);
}
}
},
_onEvent : function() { }
}));
});
================================================
FILE: common.blocks/i-bem-dom/i-bem-dom.tests/benchmarks.blocks/b2/b2.deps.js
================================================
({
mustDeps : {
block : 'i-bem-dom'
}
})
================================================
FILE: common.blocks/i-bem-dom/i-bem-dom.tests/benchmarks.blocks/b2/b2.js
================================================
modules.define('b2', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name));
});
================================================
FILE: common.blocks/i-bem-dom/i-bem-dom.tests/benchmarks.blocks/page/page.deps.js
================================================
({
noDeps : {
block : 'i-bem-dom',
elem : 'init',
mods : { auto : true }
},
shouldDeps : [
{
block : 'i-bem-dom',
elem : 'init'
},
'jquery'
]
})
================================================
FILE: common.blocks/i-bem-dom/i-bem-dom.tests/benchmarks.blocks/page/page.js
================================================
modules.require(['i-bem-dom__init', 'jquery'], function(init, $) {
$(function() {
var timeStart = Date.now();
init();
var time = Date.now() - timeStart;
$('body').append($('' + time + '
'));
});
});
================================================
FILE: common.blocks/i18n/i18n.deps.js
================================================
({
tech : 'tmpl-spec.js',
shouldDeps : 'greeting-card'
})
================================================
FILE: common.blocks/i18n/i18n.en.md
================================================
# i18n
This block provides a function for project internationalization.
It can be used in a browser and in a node.js environment.
## Signature
```js
/**
* @exports
* @param {String} keyset
* @param {String} key
* @param {Object} [params]
* @returns {String}
*/
i18n(keyset, key, params);
```
For example:
```js
i18n('keyset1', 'key2', { a : '1' });
```
Use the `decl` method to add translations:
```js
i18n.decl({
keyset1 : {
key1 : 'keyset1 key1 string',
key2 : function(params) {
return 'keyset1 key2 function ' + JSON.stringify(params);
},
key3 : function(params) {
return 'keyset1 key3 ' + this('keyset1', 'key2', params);
}
}
});
```
For information about building an internationalized project, see [enb-bem-i18n](https://ru.bem.info/tools/bem/enb-bem-i18n/readme/).
================================================
FILE: common.blocks/i18n/i18n.i18n.js
================================================
export default {
i18n : {
i18n : function() {
let data;
/**
* @param {String} keyset
* @param {String} key
* @param {Object} [params]
* @returns {String}
*/
function i18n(keyset, key, params) {
if(!data) throw Error('i18n need to be filled with data');
const val = data[keyset] && data[keyset][key];
return typeof val === 'undefined'?
keyset + ':' + key :
typeof val === 'string'?
val :
val.call(i18n, params, i18n);
}
i18n.decl = function(i18nData) {
if(!data) {
data = i18nData;
return this;
}
for(const ks in i18nData) {
const dataKs = data[ks] || (data[ks] = {}),
i18nDataKs = i18nData[ks];
for(const k in i18nDataKs)
dataKs[k] = i18nDataKs[k];
}
return this;
};
return i18n;
}
}
};
================================================
FILE: common.blocks/i18n/i18n.ru.md
================================================
# i18n
Блок предоставляет функцию для интернационализации проекта.
Может быть использован в браузере и в node.js-окружении.
## Сигнатура
```js
/**
* @exports
* @param {String} keyset
* @param {String} key
* @param {Object} [params]
* @returns {String}
*/
i18n(keyset, key, params);
```
Например:
```js
i18n('keyset1', 'key2', { a : '1' });
```
Для добавления переводов используется метод `decl`:
```js
i18n.decl({
keyset1 : {
key1 : 'keyset1 key1 string',
key2 : function(params) {
return 'keyset1 key2 function ' + JSON.stringify(params);
},
key3 : function(params) {
return 'keyset1 key3 ' + this('keyset1', 'key2', params);
}
}
});
```
Документацию на сборку проекта с интернационализацией см. в пакете для сборки [enb-bem-i18n](https://ru.bem.info/tools/bem/enb-bem-i18n/readme/).
================================================
FILE: common.blocks/i18n/i18n.test.js
================================================
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import i18nModule from './i18n.i18n.js';
const init = i18nModule.i18n.i18n;
describe('i18n', function() {
let i18n;
beforeEach(function () {
i18n = init();
i18n.decl({
'keyset1' : {
'key1' : 'keyset1 key1 string',
'key2' : function(params) {
return 'keyset1 key2 function ' + JSON.stringify(params);
},
'key3' : function(params) {
return 'keyset1 key3 ' + this('keyset1', 'key2', params);
}
}
});
});
it('should throw exception without data', function() {
const empty = init();
assert.throws(function() { empty('keyset1', 'key1'); }, Error);
});
it('should return "keyset:key" if they do not exist in data', function() {
assert.equal(i18n('undefkeyset', 'undefkey'), 'undefkeyset:undefkey');
assert.equal(i18n('keyset1', 'undefkey'), 'keyset1:undefkey');
});
it('should return string value', function() {
assert.equal(i18n('keyset1', 'key1'), 'keyset1 key1 string');
});
it('should return value as function result', function() {
assert.equal(i18n('keyset1', 'key2', { a : '1' }), 'keyset1 key2 function {"a":"1"}');
});
it('should properly call another i18n items', function() {
assert.equal(i18n('keyset1', 'key3', { b : '2' }), 'keyset1 key3 keyset1 key2 function {"b":"2"}');
});
it('should properly extend existed data', function() {
i18n.decl({
'keyset1' : {
'key0' : 'keyset1 key0 string',
'key1' : 'keyset1 key1 new string'
},
'keyset2' : {
'key1' : 'keyset2 key1 string'
}
});
assert.equal(i18n('keyset1', 'key0'), 'keyset1 key0 string');
assert.equal(i18n('keyset1', 'key1'), 'keyset1 key1 new string');
assert.equal(i18n('keyset1', 'key2', { a : '1' }), 'keyset1 key2 function {"a":"1"}');
assert.equal(i18n('keyset2', 'key1'), 'keyset2 key1 string');
});
});
================================================
FILE: common.blocks/i18n/i18n.tests/blocks/logo/logo.bemhtml.js
================================================
block('logo').content()(function() {
return this.i18n('logo', 'yandex');
});
================================================
FILE: common.blocks/i18n/i18n.tests/blocks/logo/logo.bh.js
================================================
module.exports = function (bh) {
bh.match('logo', function (ctx) {
var i18n = bh.lib.i18n;
ctx.content(i18n('logo', 'yandex'));
});
};
================================================
FILE: common.blocks/i18n/i18n.tests/blocks/logo/logo.deps.js
================================================
({
shouldDeps : [
{ block : 'i-bem', elems : 'dom' },
{ block : 'i18n' }
]
})
================================================
FILE: common.blocks/i18n/i18n.tests/blocks/logo/logo.i18n/en.js
================================================
module.exports = {
'yandex-service' : {
'Lego' : 'Lego'
},
'logo' : {
'yandex' : 'Yandex'
}
};
================================================
FILE: common.blocks/i18n/i18n.tests/blocks/logo/logo.i18n/ru.js
================================================
module.exports = {
'yandex-service' : {
'Lego' : 'Лего'
},
'logo' : {
'yandex' : 'Яндекс'
}
};
================================================
FILE: common.blocks/i18n/i18n.tests/blocks/logo/logo.i18n.js
================================================
module.exports = {
'logo' : {
'yandex-service' : function(serviceName, i18n) {
return i18n('logo', 'yandex') + '.' + i18n('yandex-service', serviceName);
}
}
};
================================================
FILE: common.blocks/i18n/i18n.tests/blocks/logo/logo.js
================================================
modules.define('logo', ['i-bem__dom', 'i18n'], function(provide, BEMDOM, i18n) {
provide(BEMDOM.decl(this.name, {
onSetMod : {
'js' : {
'inited' : function() {
var domElem = this.domElem,
params = this.params;
this.bindTo('click', function () {
domElem.text(i18n('logo', 'yandex-service', params.service));
});
}
}
}
}));
});
================================================
FILE: common.blocks/i18n/i18n.tests/blocks/page/__js/page__js.bemhtml.js
================================================
block('page').elem('js').def()(function() {
this.ctx.url = this.i18n('page', 'js');
return applyNext();
});
================================================
FILE: common.blocks/i18n/i18n.tests/blocks/page/__js/page__js.bh.js
================================================
module.exports = function (bh) {
bh.match('page__js', function (ctx, json) {
var i18n = bh.lib.i18n;
json.url = i18n('page', 'js');
});
};
================================================
FILE: common.blocks/i18n/i18n.tests/blocks/page/page.i18n/en.js
================================================
module.exports = {
'page' : {
'js' : 'simple.en.js'
}
};
================================================
FILE: common.blocks/i18n/i18n.tests/blocks/page/page.i18n/ru.js
================================================
module.exports = {
'page' : {
'js' : 'simple.ru.js'
}
};
================================================
FILE: common.blocks/i18n/i18n.tests/simple.bemjson.js
================================================
({
block : 'page',
title : 'i18n: test',
scripts : { elem : 'js' },
content : {
block : 'logo',
js : { service : 'Lego' }
}
});
================================================
FILE: common.blocks/i18n/i18n.tmpl-specs/10-simple.bemjson.js
================================================
({
block : 'greeting-card'
})
================================================
FILE: common.blocks/i18n/i18n.tmpl-specs/10-simple.html
================================================
greeting-card:message
================================================
FILE: common.blocks/i18n/i18n.tmpl-specs/blocks/greeting-card/greeting-card.bemhtml.js
================================================
block('greeting-card').content()(function () {
var i18n = this.require('i18n');
return i18n('greeting-card', 'message');
});
================================================
FILE: common.blocks/i18n/i18n.tmpl-specs/blocks/greeting-card/greeting-card.bh.js
================================================
module.exports = function (bh) {
bh.match('greeting-card', function (ctx) {
var i18n = bh.lib.i18n,
message = i18n('greeting-card', 'message');
ctx.content(message);
});
};
================================================
FILE: common.blocks/i18n/i18n.tmpl-specs/blocks/greeting-card/greeting-card.deps.js
================================================
({
mustDeps : ['i-bem', 'i18n']
})
================================================
FILE: common.blocks/identify/identify.en.md
================================================
# identify
This block provides a function for working with unique identifiers. It allows you to:
* Create object identifiers.
* Check whether objects have an identifier.
* Create a unique identifier string.
**Accepted arguments:**
* [`obj {Object}`] – The object to identify.
* [`onlyGet {Boolean}`] – Flag for checking whether the object has an identifier. If `true`, the function returns a string with the identifier if the object was previously assigned an identifier. By default, `false`.
**Return value:** `String`. A string with the identifier assigned to the object. Subsequent calls will always return the same identifier.
Example:
```js
modules.require(['identify'], function(identify) {
var a = {},
b = {},
identA = identify(a);
console.log(identA === identify(a)); //true
console.log(identA === identify(b)); //false
});
```
When called without arguments, the function returns a string with a unique identifier every time.
Example:
```js
modules.require(['identify'], function(identify) {
var a = identify(),
b = identify();
console.log(a === b); //false
});
```
## Public block technologies
The block is implemented in:
* `vanilla.js`
================================================
FILE: common.blocks/identify/identify.ru.md
================================================
# identify
Блок предоставляет функцию для работы с уникальными идентификаторами, которая позволяет:
* создавать идентификаторы объектов;
* проверять у объектов наличие идентификатора;
* создавать уникальную строку-идентификатор.
**Принимаемые аргументы:**
* [`obj {Object}`] – идентифицируемый объект.
* [`onlyGet {Boolean}`] – флаг для проверки наличия у объекта идентификатора. Если `true`, функция будет возвращать строку с идентификатором только если объект был заранее идентифицирован. По умолчанию `false`.
**Возвращаемое значение:** `String`. Строка с идентификатором, присвоенным объекту. При последующих вызовах всегда будет возвращаться один и тот же идентификатор.
Пример:
```js
modules.require(['identify'], function(identify) {
var a = {},
b = {},
identA = identify(a);
console.log(identA === identify(a)); // true
console.log(identA === identify(b)); // false
});
```
При вызове без аргументов, функция будет каждый раз возвращать строку с уникальным идентификатором.
Пример:
```js
modules.require(['identify'], function(identify) {
var a = identify(),
b = identify();
console.log(a === b); // false
});
```
## Публичные технологии блока
Блок реализован в технологиях:
* `vanilla.js`
================================================
FILE: common.blocks/identify/identify.spec.js
================================================
modules.define('spec', [
'identify',
'chai'
], function(provide,
identify,
chai
) {
var should = chai.should();
describe('identify', function() {
it('should return different values for different objects', function() {
var obj1 = {},
obj2 = {};
identify(obj1).should.not.be.equal(identify(obj2));
});
it('should return same values for same objects', function() {
var obj1 = {},
obj2 = {};
identify(obj1).should.be.equal(identify(obj1));
identify(obj2).should.be.equal(identify(obj2));
});
it('should use "uniqueID" property if exists', function() {
var obj = { uniqueID : 'id007' };
identify(obj).should.be.equal('id007');
});
it('should generate unique values for each calls if no params passed', function() {
var id1 = identify(),
id2 = identify(),
id3 = identify();
should.exist(id1);
should.exist(id2);
should.exist(id3);
id1.should.not.be.equal(id2);
id1.should.not.be.equal(id3);
id2.should.not.be.equal(id3);
});
it('should accept several arguments', function() {
var obj1 = {},
obj2 = {};
identify(obj1, obj2).should.be.equal(identify(obj1) + identify(obj2));
});
it('should not depend on order of several arguments', function() {
var obj1 = {},
obj2 = {};
identify(obj1, obj2).should.be.equal(identify(obj2, obj1));
});
it('should properly process arguments', function() {
var obj1 = {},
obj2,
obj3 = '123',
obj4 = null,
obj5 = function() {};
[obj2, obj3, obj4].forEach(function(obj) {
identify(obj).should.be.equal('');
});
identify(obj1, obj2, obj3, obj4, obj5).should.be.equal(identify(obj1, obj5));
});
});
provide();
});
================================================
FILE: common.blocks/identify/identify.vanilla.js
================================================
/**
* @module identify
*/
let counter = 0
const expando = '__' + (+new Date),
get = () => 'uniq' + (++counter),
identify = obj => {
if((typeof obj === 'object' && obj !== null) || typeof obj === 'function') {
let key
if('uniqueID' in obj) {
obj === globalThis.document && (obj = obj.documentElement)
key = 'uniqueID'
} else {
key = expando
}
return key in obj
? obj[key]
: obj[key] = get()
}
return ''
}
export default
/**
* Makes unique ID
* @param {...Object} obj Object that needs to be identified
* @returns {String} ID
*/
function(obj) {
if(arguments.length) {
if(arguments.length === 1) {
return identify(obj)
}
const res = []
for(const arg of arguments)
res.push(identify(arg))
return res.sort().join('')
}
return get()
}
================================================
FILE: common.blocks/idle/_start/idle_start_auto.js
================================================
/**
* Automatically starts idle module
*/
import idle from 'bem:idle';
idle.start();
================================================
FILE: common.blocks/idle/idle.deps.js
================================================
({
shouldDeps : ['events', 'inherit', 'jquery']
})
================================================
FILE: common.blocks/idle/idle.en.md
================================================
# idle
This block provides an object with a set of methods for generating an event when user activity ends (i.e. the user switches to another window or doesn't finish actions).
## Overview
### Object events
The following events are available:
| Name | Description |
| -------- | -------- |
| idle | The browser is idle. |
| wakeup | The user has resumed activity. |
### Properties and methods of the object
| Name | Returned value | Description |
| -------- | --- | -------- |
| start () | - | Starts tracking user activity. |
| stop () | - | Stops tracking user activity. |
| isIdle () | `Boolean` | Checks the current state. |
### Block modifiers
| Modifier | Acceptable values | Usage | Description |
| ----------- | ------------------- | --------------------- | -------- |
| start | `auto` | `JS` | Automatically starts tracking user activity. |
### Public block technologies
The block is implemented in:
* `js`
## Description
Subscribing to the block's event allows you to suspend operations, such as displaying animation, when there isn't any user activity.
The block is a descendant of the `Emitter` class in the `events` block, which allows it to call these methods.
```js
modules.require(['idle'], function(idle) {
idle
.on({
idle : function() {
// idle event handler
},
wakeup : function() {
// wakeup event handler
}
})
.start(); // start event generation
});
```
### Object events
#### `idle` event
Generated when user activity ends.
#### `wakeup` event
Generated when user activity resumes.
### Properties and methods of the object
#### `start` method
Starts tracking user activity.
Doesn't accept arguments.
No return value.
```js
modules.require(['idle'], function(idle) {
idle.start()
});
```
#### `stop` method
Stops user activity tracking.
Doesn't accept arguments.
No return value.
```js
modules.require(['idle'], function(idle) {
idle.start() // start tracking activity
idle.stop() // stop tracking activity
});
```
#### `isIdle` method
Checks whether there is any user activity.
Doesn't accept arguments.
**Return value:** `Boolean`. If there isn't any activity, `true`.
```js
modules.require(['idle'], function(idle) {
idle.isIdle() // true or false, depending on the current state
});
```
### Block modifiers
#### `start` modifier
Acceptable values: `'auto'`.
Usage: enabled in the `deps.js` dependencies file.
Automatically starts tracking user activity.
================================================
FILE: common.blocks/idle/idle.js
================================================
/**
* @module idle
*/
import inherit from 'bem:inherit'
import events from 'bem:events'
import $ from 'bem:jquery'
const IDLE_TIMEOUT = 3000,
USER_EVENTS = 'mousemove keydown click',
/**
* @class Idle
* @augments events:Emitter
*/
Idle = inherit(events.Emitter, /** @lends Idle.prototype */{
/**
* @constructor
*/
__constructor : function() {
this._timer = null
this._isStarted = false
this._isIdle = false
},
/**
* Starts monitoring of idle state
*/
start : function() {
if(!this._isStarted) {
this._isStarted = true
this._startTimer()
this._onUserActionBound = this._onUserAction.bind(this)
$(document).on(USER_EVENTS, this._onUserActionBound)
}
},
/**
* Stops monitoring of idle state
*/
stop : function() {
if(this._isStarted) {
this._isStarted = false
this._stopTimer()
$(document).off(USER_EVENTS, this._onUserActionBound)
}
},
/**
* Returns whether state is idle
* @returns {Boolean}
*/
isIdle : function() {
return this._isIdle
},
_onUserAction : function() {
if(this._isIdle) {
this._isIdle = false
this.emit('wakeup')
}
this._stopTimer()
this._startTimer()
},
_startTimer : function() {
this._timer = setTimeout(
() => this._onTimeout(),
IDLE_TIMEOUT)
},
_stopTimer : function() {
this._timer && clearTimeout(this._timer)
},
_onTimeout : function() {
this._isIdle = true
this.emit('idle')
}
})
/**
* @type Idle
*/
export default new Idle()
================================================
FILE: common.blocks/idle/idle.ru.md
================================================
# idle
Блок предоставляет объект, содержащий набор методов для генерации события в момент прекращения пользовательской активности (т.е. пользователь работает с другим окном или не совершает действий).
## Обзор
### События объекта
Доступен следующий набор событий:
| Имя | Описание |
| -------- | -------- |
| idle | Браузер простаивает. |
| wakeup | Пользователь возобновил активность. |
### Свойства и методы объекта
| Имя | Возвращаемое значение | Описание |
| -------- | --- | -------- |
| start () | - | Запуск отслеживания пользовательской активности. |
| stop () | - | Остановка отслеживания пользовательской активности. |
| isIdle () | `Boolean` | Проверка текущего состояния. |
### Модификаторы блока
| Модификатор | Допустимые значения | Способы использования | Описание |
| ----------- | ------------------- | --------------------- | -------- |
| start | `auto` | `JS` | Автоматический запуск отслеживания пользовательской активности. |
### Публичные технологии блока
Блок реализован в технологиях:
* `js`
## Описание
Подписавшись на события блока можно приостанавливать выполнение операций, например, отображение анимации, при отсутствии пользовательской активности.
Блок наследуется от класса `Emitter` блока `events`, что позволяет вызывать его методы.
```js
modules.require(['idle'], function(idle) {
idle
.on({
idle : function() {
// обработчик события idle
},
wakeup : function() {
// обработчик события wakeup
}
})
.start(); // запуск генерации событий
});
```
### События объекта
#### Событие `idle`
Генерируется при прекращении пользовательской активности.
#### Событие `wakeup`
Генерируется в момент возобновления пользовательской активности.
### Свойства и методы объекта
#### Метод `start`
Запуск отслеживания пользовательской активности.
Не принимает аргументов.
Не имеет возвращаемого значения.
```js
modules.require(['idle'], function(idle) {
idle.start()
});
```
#### Метод `stop`
Служит для прекращения отслеживания пользовательской активности.
Не принимает аргументов.
Не имеет возвращаемого значения.
```js
modules.require(['idle'], function(idle) {
idle.start() // начинаем отслеживать активность
idle.stop() // прекращаем отслеживать активность
});
```
#### Метод `isIdle`
Служит для проверки наличия пользовательской активности.
Не принимает аргументов.
**Возвращаемое значение:** `Boolean`. В случае если активность отсутствует – `true`.
```js
modules.require(['idle'], function(idle) {
idle.isIdle() // true или false, в зависимости от текущего состояния
});
```
### Модификаторы блока
#### Модификатор `start`
Допустимые значения: `'auto'`.
Способ использования: подключается в файле зависимостей `deps.js`.
Автоматический запуск отслеживания пользовательской активности.
================================================
FILE: common.blocks/inherit/inherit.en.md
================================================
# inherit
This block provides a function for declaring and inheriting classes.
## Overview
### Usage
| Use | Signature | Return type | Description |
| ----- | --------- | --------------------- | -------- |
| Declaring a base class | inherit( `props {Object}`, `[staticProps {Object}]`) | `Function` | Use for creating (declaring) a base class from the object properties. |
| Creating a derived class | inherit( `BaseClass {Function} `|` {Array}`, `props {Object}`, `[staticProps {Object}]`) | `Function` | Use for inheriting and redefining the properties and methods of a base class. |
### Special fields of the declared class
| Name | Data type | Description |
| --- | ---------- | -------- |
| __constructor | `Function` | The function that will be called when creating a class instance. |
### Special fields of the declared class instance
| Field | Data type | Description |
| ---- | ---------- | -------- |
| __self | `*` | Allows you to access the class and its instance. |
| __base | `Function` | Allows you to use the methods of the base class inside the derived class (super call). |
### Public block technologies
The block is implemented in:
* `vanilla.js`
## Description
Use the `inherit` function to:
* Create a class using a declaration.
* Set a constructor method.
* Use mix-ins.
* Call the methods of the base implementation (super call).
* Get access to static properties of a class from its instance.
This is the main block inheritance mechanism in `bem-core`.
The function is polymorphic and, depending on the first argument type, it can be used for:
* `Object` type – declaring the base class.
* `Function` type – deriving a class from the base class.
The signature of the function's other arguments depends on how it is run.
### Usage
#### Declaring a base class
This approach allows you to define the base class by passing the function an object with the class properties.
**Accepted arguments:**
* `props {Object}` – An object with its own properties for the base class. Required argument.
* [`staticProps {Object}`] – An object with static properties of the base class.
**Return value:** `Function`. The fully-formed class.
```js
modules.require(['inherit'], function(inherit) {
var props = {}, // object for the base class properties
baseClass = inherit(props); // base class
});
```
##### Base class with static properties
Properties of the `staticProps` object are added as static properties for the class being created.
Example:
```js
modules.require(['inherit'], function(inherit) {
var A = inherit(props, {
callMe : function() {
console.log('mr.Static');
}
});
A.callMe(); // mr.Static
});
```
##### Special fields of the declared class
###### `__constructor` field
Type: `Function`.
The object with the base class properties can contain the reserved `__constructor` property, a function that is called automatically when a class instance is created.
Example:
```js
modules.require(['inherit'], function(inherit) {
var A = inherit({
__constructor : function(property) { // constructor
this.property = property;
},
getProperty : function() {
return this.property + ' of instanceA';
}
}),
aInst = new A('Property');
aInst.getProperty(); // Property of instanceA
});
```
#### Creating a derived class
This approach allows you to create a derived class from the base class and the objects with the static properties and the custom properties.
**Accepted arguments:**
* `BaseClass {Function} | {Array}` – The base class. Can be an array of mix-in functions. Required argument.
* `props {Object}` – Custom properties (added to the prototype). Required argument.
* [`staticProps {Object}`] – Static properties.
If one of the objects contains properties that already exist in the base class, the base class properties are redefined.
**Return value:** `Function`. Derived class.
Example:
```js
modules.require(['inherit'], function(inherit) {
var A = inherit({
getType : function() {
return 'A';
}
});
// class derived from A
var B = inherit(A, {
getType : function() { // redefinition + super call
return this.__base() + 'B';
}
});
var instanceOfB = new B();
instanceOfB.getType(); // returns 'AB'
});
```
##### Creating a derived class with mix-ins
When declaring a derived class, you can specify an additional set of functions. Their properties will be mixed in to the created class. To do this, the first argument for `inherit` should specify an array that has the base class as its first element, followed by the functions to mix in.
Example:
```js
modules.require(['inherit'], function(inherit) {
var A = inherit({
getA : function() {
return 'A';
}
});
var B = inherit({
getB : function() {
return 'B';
}
});
// class derived from A and B
var C = inherit([A, B], {
getAll : function() {
return this.getA() + this.getB();
}
});
var instanceOfC = new C();
instanceOfC.getAll(); // returns 'AB'
});
```
##### Special fields of the declared class instance
###### `__self` field
Type: `*`.
Allows you to access the class and its instance.
Example:
```js
modules.require(['inherit'], function(inherit) {
var A = inherit({
getStaticProperty : function() {
return this.__self.staticMethod; // access to static methods
}
}, {
staticProperty : 'staticA',
staticMethod : function() {
return this.staticProperty;
}
}),
aInst = new A();
aInst.getStaticProperty(); //staticA
});
```
###### `__base`
Type: `Function`.
Allows you to call base class methods inside the derived class (super call). When used in a static method, it will call the static method of the same name in the base class.
Example:
```js
modules.require(['inherit'], function(inherit) {
var A = inherit({
getType : function() {
return 'A';
}
}, {
staticProperty : 'staticA',
staticMethod : function() {
return this.staticProperty;
}
});
// class derived from A
var B = inherit(A, {
getType : function() { // redefinition + super call
return this.__base() + 'B';
}
}, {
staticMethod : function() { // static redefinition + super call
return this.__base() + ' of staticB';
}
});
var instanceOfB = new B();
instanceOfB.getType(); // returns 'AB'
B.staticMethod(); // returns 'staticA of staticB'
});
```
### More examples
For more examples, see the repository of the [inherit](https://github.com/dfilatov/inherit) library.
================================================
FILE: common.blocks/inherit/inherit.ru.md
================================================
# inherit
Блок предоставляет функцию, реализующую механизмы для объявления и наследования классов.
## Обзор
### Способы использования функции
| Способ | Сигнатура | Тип возвращаемого значения | Описание |
| ----- | --------- | --------------------- | -------- |
| Объявление базового класса | inherit( `props {Object}`, `[staticProps {Object}]`) | `Function` | Служит для создания (декларации), базового класса на основе свойств объекта. |
| Создание производного класса | inherit( `BaseClass {Function} `|` {Array}`, `props {Object}`, `[staticProps {Object}]`) | `Function` | Позволяет наследовать и доопределять свойства и методы базового класса. |
### Специальные поля объявляемого класса
| Имя | Тип данных | Описание |
| --- | ---------- | -------- |
| __constructor | `Function` | Функция, которая будет вызвана в ходе создании экземпляра класса. |
### Специальные поля экземпляра объявляемого класса
| Поле | Тип данных | Описание |
| ---- | ---------- | -------- |
| __self | `*` | Позволяет получить доступ к классу из его экземпляра. |
| __base | `Function` | Позволяет внутри производного класса использовать методы базового (supercall). |
### Публичные технологии блока
Блок реализован в технологиях:
* `vanilla.js`
## Описание
Функция `inherit` позволяет:
* создавать класс по декларации;
* задавать метод-конструктор;
* использовать миксины;
* вызывать методы базовой реализации (super call);
* получать доступ к статическим свойствам класса из его экземпляра.
Блок является основой механизма наследования блоков в `bem-core`.
Функция полиморфна и, в зависимости от типа первого аргумента, может быть использована для:
* тип `Object` – объявления базового класса.
* тип `Function` – создания производного класса на основе базового.
Сигнатуры других аргументов функции зависят от способа выполнения.
### Способы использования функции
#### Объявление базового класса
Способ позволяет объявить базовый класс, передав функции объект со свойствами класса.
**Принимаемые аргументы:**
* `props {Object}` – объект с собственными свойствами базового класса. Обязательный аргумент.
* [`staticProps {Object}`] – объект со статическими свойствами базового класса.
**Возвращаемое значение:** `Function`. Полностью сформированный класс.
```js
modules.require(['inherit'], function(inherit) {
var props = {}, // объект свойств базового класса
baseClass = inherit(props); // базовый класс
});
```
##### Базовый класс со статическими свойствами
Свойства объекта `staticProps` добавляются как статические к создаваемому классу.
Пример:
```js
modules.require(['inherit'], function(inherit) {
var A = inherit(props, {
callMe : function() {
console.log('mr.Static');
}
});
A.callMe(); // mr.Static
});
```
##### Специальные поля объявляемого класса
###### Поле `__constructor`
Тип: `Function`.
Объект собственных свойств базового класса может содержать зарезервированное свойство `__constructor` – функцию, которая будет автоматически вызвана при создании экземпляра класса.
Пример:
```js
modules.require(['inherit'], function(inherit) {
var A = inherit({
__constructor : function(property) { // конструктор
this.property = property;
},
getProperty : function() {
return this.property + ' of instanceA';
}
}),
aInst = new A('Property');
aInst.getProperty(); // Property of instanceA
});
```
#### Создание производного класса
Способ позволяет создать производный класс на основе базового класса и объектов статических и собственных свойств.
**Принимаемые аргументы:**
* `BaseClass {Function} | {Array}` – базовый класс. Может быть массивом функций-миксинов. Обязательный аргумент.
* `props {Object}` – собственные свойства (добавляются к прототипу). Обязательный аргумент.
* [`staticProps {Object}`] – статические свойства.
Если один из объектов содержит свойства, которые уже есть в базовом классе – свойства базового класса будут переопределены.
**Возвращаемое значение:** `Function`. Производный класс.
Пример:
```js
modules.require(['inherit'], function(inherit) {
var A = inherit({
getType : function() {
return 'A';
}
});
// класс, производный от A
var B = inherit(A, {
getType : function() { // переопределение + 'super' call
return this.__base() + 'B';
}
});
var instanceOfB = new B();
instanceOfB.getType(); // возвращает 'AB'
});
```
##### Создание производного класса с миксинами
При объявлении производного класса можно указать дополнительный набор функций. Их свойства будут примешаны к создаваемому классу. Для этого первым аргументом `inherit` нужно указать массив, первым элементом которого должен быть базовый класс, а последующими – примешиваемые функции.
Пример:
```js
modules.require(['inherit'], function(inherit) {
var A = inherit({
getA : function() {
return 'A';
}
});
var B = inherit({
getB : function() {
return 'B';
}
});
// класс, производный от A и B
var C = inherit([A, B], {
getAll : function() {
return this.getA() + this.getB();
}
});
var instanceOfC = new C();
instanceOfC.getAll(); // возвращает 'AB'
});
```
##### Специальные поля экземпляра объявляемого класса
###### Поле `__self`
Тип: `*`.
Позволяет получить доступ к классу из его экземпляра.
Пример:
```js
modules.require(['inherit'], function(inherit) {
var A = inherit({
getStaticProperty : function() {
return this.__self.staticMethod; // доступ к статическим методам
}
}, {
staticProperty : 'staticA',
staticMethod : function() {
return this.staticProperty;
}
}),
aInst = new A();
aInst.getStaticProperty(); //staticA
});
```
###### `__base`
Тип: `Function`.
Позволяет внутри производного класса вызывать одноименные методы базового (supercall). При использовании в статическом методе, будет вызван одноименный статический метод базового класса.
Пример:
```js
modules.require(['inherit'], function(inherit) {
var A = inherit({
getType : function() {
return 'A';
}
}, {
staticProperty : 'staticA',
staticMethod : function() {
return this.staticProperty;
}
});
// класс, производный от A
var B = inherit(A, {
getType : function() { // переопределение + 'super' call
return this.__base() + 'B';
}
}, {
staticMethod : function() { // статическое переопределение + 'super' call
return this.__base() + ' of staticB';
}
});
var instanceOfB = new B();
instanceOfB.getType(); // возвращает 'AB'
B.staticMethod(); // возвращает 'staticA of staticB'
});
```
### Дополнительные примеры
Дополнительные примеры смотрите в репозитории библиотеки [inherit](https://github.com/dfilatov/inherit).
================================================
FILE: common.blocks/inherit/inherit.spec.js
================================================
modules.define('spec', ['inherit'], function(provide, inherit) {
describe('inherit', function() {
describe('instance', function() {
it('should be instance of class', function() {
var Cls = inherit({}),
instance = new Cls();
instance.should.be.instanceOf(Cls);
});
it('should be instance of all classes in hierarchy', function() {
var ClsA = inherit({}),
ClsB = inherit(ClsA, {}),
ClsC = inherit(ClsB, {}),
instance = new ClsC();
instance.should.be.instanceOf(ClsA);
instance.should.be.instanceOf(ClsB);
instance.should.be.instanceOf(ClsC);
});
it('should be instance of constructor return value', function() {
var ClsA = inherit({}),
ClsB = inherit({
__constructor : function() {
return new ClsA();
}
}),
instance = new ClsB();
instance.should.be.instanceOf(ClsA);
instance.should.not.be.instanceOf(ClsB);
});
it('instance should have properties from constructor', function() {
var Cls = inherit({
__constructor : function() {
this._p1 = 'v1';
this._p2 = 'v2';
}
}),
instance = new Cls();
instance._p1.should.be.equal('v1');
instance._p2.should.be.equal('v2');
});
it('"__self" property should be pointed to class', function() {
var Cls = inherit({}),
instance = new Cls();
instance.__self.should.be.equal(Cls);
});
it('should override methods of base class', function() {
var ClsA = inherit({
method1 : function() {
return 'A1';
},
method2 : function() {
return 'A2';
}
}),
ClsB = inherit(ClsA, {
method1 : function() {
return 'B1';
}
}),
ClsC = inherit(ClsB, {
method2 : function() {
return 'C2';
}
}),
instance = new ClsC();
instance.method1().should.be.equal('B1');
instance.method2().should.be.equal('C2');
});
it('__base should call methods of base class', function() {
var ClsA = inherit({
method1 : function() {
return 'A1';
},
method2 : function() {
return 'A2';
}
}),
ClsB = inherit(ClsA, {
method1 : function() {
return this.__base() + 'B1';
}
}),
ClsC = inherit(ClsB, {
method1 : function() {
return this.__base() + 'C1';
},
method2 : function() {
return this.__base() + 'C2';
}
}),
instance = new ClsC();
instance.method1().should.be.equal('A1B1C1');
instance.method2().should.be.equal('A2C2');
});
});
describe('static', function() {
it('properties should be assigned', function() {
var Cls = inherit({}, {
method : function() {
return 'method';
},
prop : 'val'
});
Cls.method().should.be.equal('method');
Cls.prop.should.be.equal('val');
});
it('properties should override properties of base class', function() {
var ClsA = inherit({}, {
method1 : function() {
return 'A1';
},
method2 : function() {
return 'A2';
}
}),
ClsB = inherit(ClsA, {}, {
method1 : function() {
return 'B1';
}
}),
ClsC = inherit(ClsB, {}, {
method2 : function() {
return 'C2';
}
});
ClsC.method1().should.be.equal('B1');
ClsC.method2().should.be.equal('C2');
});
it('__base should call methods of base class', function() {
var ClsA = inherit({}, {
method1 : function() {
return 'A1';
},
method2 : function() {
return 'A2';
}
}),
ClsB = inherit(ClsA, {}, {
method1 : function() {
return this.__base() + 'B1';
}
}),
ClsC = inherit(ClsB, {}, {
method1 : function() {
return this.__base() + 'C1';
},
method2 : function() {
return this.__base() + 'C2';
}
});
ClsC.method1().should.be.equal('A1B1C1');
ClsC.method2().should.be.equal('A2C2');
});
});
describe('mixin', function() {
it('properties should be assigned', function() {
var ClsA = inherit({
method : function() {
return 'method';
}
}),
Mix1 = inherit({
method1 : function() {
return 'mix1method';
}
}),
Mix2 = inherit({
method2 : function() {
return 'mix2method';
}
}),
ClsB = inherit([ClsA, Mix1, Mix2]),
instance = new ClsB();
instance.method().should.be.equal('method');
instance.method1().should.be.equal('mix1method');
instance.method2().should.be.equal('mix2method');
});
it('static properties should be assigned', function() {
var ClsA = inherit({}, {
method : function() {
return 'method';
}
}),
Mix1 = inherit({}, {
method1 : function() {
return 'mix1method';
}
}),
Mix2 = inherit({}, {
method2 : function() {
return 'mix2method';
}
}),
ClsB = inherit([ClsA, Mix1, Mix2]);
ClsB.method().should.be.equal('method');
ClsB.method1().should.be.equal('mix1method');
ClsB.method2().should.be.equal('mix2method');
});
it('__base should call methods of previous object', function() {
var ClsA = inherit({
method : function() {
return 'methodA';
}
}),
Mix1 = inherit({
method : function() {
return this.__base() + '_mix1method';
}
}),
Mix2 = inherit({
method : function() {
return this.__base() + '_mix2method';
}
}),
ClsB = inherit([ClsA, Mix1, Mix2], {
method : function() {
return this.__base() + '_methodB';
}
}),
instance = new ClsB();
instance.method().should.be.equal('methodA_mix1method_mix2method_methodB');
});
it('__base in static methods should call methods of previous object', function() {
var ClsA = inherit(null, {
method : function() {
return 'methodA';
}
}),
Mix1 = inherit(null, {
method : function() {
return this.__base() + '_mix1method';
}
}),
Mix2 = inherit(null, {
method : function() {
return this.__base() + '_mix2method';
}
}),
ClsB = inherit([ClsA, Mix1, Mix2], null, {
method : function() {
return this.__base() + '_methodB';
}
});
ClsB.method().should.be.equal('methodA_mix1method_mix2method_methodB');
});
});
});
provide();
});
================================================
FILE: common.blocks/inherit/inherit.vanilla.js
================================================
/**
* @module inherit
* @version 2.2.6
* @author Filatov Dmitry
*/
const noop = () => {},
extend = (o1, o2) => {
if(o2) {
for(const [key, val] of Object.entries(o2))
o1[key] = val
}
return o1
},
toStr = Object.prototype.toString,
isFunction = obj => toStr.call(obj) === '[object Function]'
function override(base, res, add) {
for(const name of Object.keys(add)) {
if(name === '__self') continue
const prop = add[name]
if(isFunction(prop) &&
(!prop.prototype || !prop.prototype.__self) &&
(prop.toString().indexOf('.__base') > -1)) {
res[name] = (name => {
const baseMethod = base[name]
? base[name]
: name === '__constructor'
? res.__self.__parent
: noop,
result = function() {
const baseSaved = this.__base
this.__base = result.__base
const res = prop.apply(this, arguments)
this.__base = baseSaved
return res
}
result.__base = baseMethod
return result
})(name)
} else {
res[name] = prop
}
}
}
function applyMixins(mixins, res) {
for(let i = 1; i < mixins.length; i++) {
const mixin = mixins[i]
res
? isFunction(mixin)
? inherit.self(res, mixin.prototype, mixin)
: inherit.self(res, mixin)
: res = isFunction(mixin)
? inherit(mixins[0], mixin.prototype, mixin)
: inherit(mixins[0], mixin)
}
return res || mixins[0]
}
/**
* Creates class
* @param {Function|Array} [baseClass|baseClassAndMixins] class (or class and mixins) to inherit from
* @param {Object} prototypeFields
* @param {Object} [staticFields]
* @returns {Function} class
*/
function inherit() {
const args = arguments,
withMixins = Array.isArray(args[0]),
hasBase = withMixins || isFunction(args[0]),
base = hasBase ? withMixins ? applyMixins(args[0]) : args[0] : noop,
props = args[hasBase ? 1 : 0] || {},
staticProps = args[hasBase ? 2 : 1],
res = props.__constructor || (hasBase && base.prototype && base.prototype.__constructor)
? function() {
return this.__constructor.apply(this, arguments)
}
: hasBase
? function() {
return base.apply(this, arguments)
}
: function() {}
if(!hasBase) {
res.prototype = props
res.prototype.__self = res.prototype.constructor = res
return extend(res, staticProps)
}
extend(res, base)
res.__parent = base
const basePtp = base.prototype,
resPtp = res.prototype = Object.create(basePtp)
resPtp.__self = resPtp.constructor = res
props && override(basePtp, resPtp, props)
staticProps && override(base, res, staticProps)
return res
}
inherit.self = function() {
const args = arguments,
withMixins = Array.isArray(args[0]),
base = withMixins ? applyMixins(args[0], args[0][0]) : args[0],
props = args[1],
staticProps = args[2],
basePtp = base.prototype
props && override(basePtp, basePtp, props)
staticProps && override(base, base, staticProps)
return base
}
export default inherit
================================================
FILE: common.blocks/jquery/__config/jquery__config.js
================================================
/**
* @module jquery__config
* @description Configuration for jQuery.
* jQuery is now provided via npm package (peer dependency).
*/
export default {
/**
* Required jQuery version range
* @type {String}
*/
version : '>=4.0.0'
};
================================================
FILE: common.blocks/jquery/__config/jquery__config.ru.md
================================================
# Элемент `config` блока `jquery`
Элемент предоставляет объект с настройками подключаемой библиотеки jQuery. Настройки хранятся как свойства объекта.
## Свойства и методы объекта
### Свойство `url`
Тип: `String`.
Содержит строку с URL для загрузки jQuery.
В проекте значение свойства может быть переопределено. Тогда при подключении блока будет использовано новое значение, если библиотека jQuery не была подключена предварительно.
```js
modules.define('jquery__config', function(provide) {
provide({ url: '//foo.bar/my-custom-jquery.js' });
});
```
================================================
FILE: common.blocks/jquery/jquery.deps.js
================================================
({
shouldDeps : [
{ elem : 'config' }
]
})
================================================
FILE: common.blocks/jquery/jquery.en.md
================================================
# jquery
This block is for downloading the [jQuery](https://jquery.com) library and its extensions and enabling them on a page.
Extensions are enabled via dependencies on the block elements.
## Usage
```js
modules.require(['jquery'], function($) {
console.log($);
});
```
## Overview
### Elements of the block
| Element | Usage | Description |
| --------| --------------------- | -------- |
| config | `JS` | jQuery configuration. |
### Properties and methods of the block elements
| Element| Name | Return type | Description |
| -------| --- | ----------------------------- | -------- |
| config | url | `String` | String with the URL for connecting the jQuery library. |
### Public block technologies
The block is implemented in:
* `js`
================================================
FILE: common.blocks/jquery/jquery.js
================================================
/**
* @module jquery
* @description Provide jQuery from npm package.
*/
import jQuery from 'jquery'
/**
* @type Function
*/
export default jQuery
================================================
FILE: common.blocks/jquery/jquery.ru.md
================================================
# jquery
Блок служит для загрузки и подключения на страницу библиотеки [jQuery](https://jquery.com) и ее расширений.
Расширения подключаются через зависимости от элементов блока.
## Способы использования
```js
modules.require(['jquery'], function($) {
console.log($);
});
```
## Обзор
### Элементы блока
| Элемент | Способы использования | Описание |
| --------| --------------------- | -------- |
| config | `JS` | Настройки jQuery. |
### Свойства и методы элементов блока
| Элемент| Имя | Тип возвращаемого значения | Описание |
| -------| --- | ----------------------------- | -------- |
| config | url | `String` | Строка с URL, подключаемой библиотеки jQuery. |
### Публичные технологии блока
Блок реализован в технологиях:
* `js`
================================================
FILE: common.blocks/jquery/jquery.ru.title.txt
================================================
Блок с плагинами для jQuery
================================================
FILE: common.blocks/keyboard/__codes/keyboard__codes.js
================================================
/**
* @module keyboard__codes
*/
export default {
/** @type {Number} */
BACKSPACE : 8,
/** @type {Number} */
TAB : 9,
/** @type {Number} */
ENTER : 13,
/** @type {Number} */
CAPS_LOCK : 20,
/** @type {Number} */
ESC : 27,
/** @type {Number} */
SPACE : 32,
/** @type {Number} */
PAGE_UP : 33,
/** @type {Number} */
PAGE_DOWN : 34,
/** @type {Number} */
END : 35,
/** @type {Number} */
HOME : 36,
/** @type {Number} */
LEFT : 37,
/** @type {Number} */
UP : 38,
/** @type {Number} */
RIGHT : 39,
/** @type {Number} */
DOWN : 40,
/** @type {Number} */
INSERT : 45,
/** @type {Number} */
DELETE : 46
}
================================================
FILE: common.blocks/keyboard/keyboard.en.md
================================================
# keyboard
This block is used for working with keyboard input.
## Overview
### Elements of the block
| Element | Usage | Description |
| --------| --------------------- | -------- |
| codes | `JS` | Provides an object with a set of constant names for frequently used keyboard codes. |
### Properties and methods of the block elements
| Element | Name | Type |
| ------- | --- | --- |
| codes | BACKSPACE | `String` |
| | TAB | `String` |
| | ENTER | `String` |
| | CAPS_LOCK | `String` |
| | ESC | `String` |
| | SPACE | `String` |
| | PAGE_UP | `String` |
| | PAGE_DOWN | `String` |
| | END | `String` |
| | HOME | `String` |
| | LEFT | `String` |
| | UP | `String` |
| | RIGHT | `String` |
| | DOWN | `String` |
| | INSERT | `String` |
| | DELETE | `String` |
### Public block technologies
The block is implemented in:
* `js`
## Description
### Elements of the block
#### `codes` element
Provides an object with a set of constant names for frequently used keyboard codes.
##### Properties and methods of the object
Type: `String`.
The name values (object properties) are the key codes. Using meaningful names instead of the key codes makes the code easier to understand.
For example, the `_onKeyDown` method uses the names of the `UP` and `DOWN` keys when checking the `keyCode` field for an event object:
```js
modules.define('input', ['i-bem-dom', 'keyboard__codes'], function(provide, bemDom, keyCodes) {
provide(bemDom.declBlock(this.name, /** @lends input.prototype */{
onSetMod : {
js : {
inited : function() {
this._domEvents().on('keydown', this._onKeyDown);
}
}
},
_onKeyDown : function(e) {
if((e.keyCode === keyCodes.UP || e.keyCode === keyCodes.DOWN) && !e.shiftKey) {
// ...
}
}
}));
});
```
The following properties are available:
* `BACKSPACE`
* `TAB`
* `ENTER`
* `CAPS_LOCK`
* `ESC`
* `SPACE`
* `PAGE_UP`
* `PAGE_DOWN`
* `END`
* `HOME`
* `LEFT`
* `UP`
* `RIGHT`
* `DOWN`
* `INSERT`
* `DELETE`
================================================
FILE: common.blocks/keyboard/keyboard.ru.md
================================================
# keyboard
Блок предназначен для работы с клавиатурным вводом.
## Обзор
### Элементы блока
| Элемент | Способы использования | Описание |
| --------| --------------------- | -------- |
| codes | `JS` | Предоставляет объект, содержащий набор констант – имен часто используемых клавиатурных кодов. |
### Свойства и методы элементов блока
| Элемент | Имя | Тип |
| ------- | --- | --- |
| codes | BACKSPACE | `String` |
| | TAB | `String` |
| | ENTER | `String` |
| | CAPS_LOCK | `String` |
| | ESC | `String` |
| | SPACE | `String` |
| | PAGE_UP | `String` |
| | PAGE_DOWN | `String` |
| | END | `String` |
| | HOME | `String` |
| | LEFT | `String` |
| | UP | `String` |
| | RIGHT | `String` |
| | DOWN | `String` |
| | INSERT | `String` |
| | DELETE | `String` |
### Публичные технологии блока
Блок реализован в технологиях:
* `js`
## Описание
### Элементы блока
#### Элемент `codes`
Предоставляет объект, содержащий набор констант – имен часто используемых клавиатурных кодов.
##### Свойства и методы объекта
Тип: `String`.
Значениями имен (свойств объекта) являются коды клавиш. Использование осмысленных имен вместо кодов клавиш делает код понятнее.
Например, метод `_onKeyDown` использует имена клавиш `UP` и `DOWN` при проверке поля `keyCode` объекта события:
```js
modules.define('input', ['i-bem-dom', 'keyboard__codes'], function(provide, bemDom, keyCodes) {
provide(bemDom.declBlock(this.name, /** @lends input.prototype */{
onSetMod : {
js : {
inited : function() {
this._domEvents().on('keydown', this._onKeyDown);
}
}
},
_onKeyDown : function(e) {
if((e.keyCode === keyCodes.UP || e.keyCode === keyCodes.DOWN) && !e.shiftKey) {
// ...
}
}
}));
});
```
Доступен следующий набор свойств:
* `BACKSPACE`
* `TAB`
* `ENTER`
* `CAPS_LOCK`
* `ESC`
* `SPACE`
* `PAGE_UP`
* `PAGE_DOWN`
* `END`
* `HOME`
* `LEFT`
* `UP`
* `RIGHT`
* `DOWN`
* `INSERT`
* `DELETE`
================================================
FILE: common.blocks/loader/_type/loader_type_bundle.js
================================================
/**
* @module loader_type_bundle
* @description Load BEM bundle (JS+CSS) from external URL.
*/
const LOADING_TIMEOUT = 30000
const doc = document
let head
const bundles = new Map()
const handleError = (bundleId) => {
const bundleDesc = bundles.get(bundleId)
if(!bundleDesc) return
const fns = bundleDesc.errorFns
clearTimeout(bundleDesc.timer)
for(const fn of fns) fn()
bundles.delete(bundleId)
}
const appendCss = (css) => {
const style = doc.createElement('style')
style.type = 'text/css'
head.appendChild(style)
style.appendChild(doc.createTextNode(css))
}
/**
* Loads bundle
* @param {String} id
* @param {String} url
* @param {Function} onSuccess
* @param {Function} [onError]
*/
const load = (id, url, onSuccess, onError) => {
const bundle = bundles.get(id)
if(bundle) {
if(bundle.successFns) { // bundle is being loaded
bundle.successFns.push(onSuccess)
onError && bundle.errorFns.push(onError)
} else { // bundle was loaded before
setTimeout(onSuccess, 0)
}
return
}
const script = doc.createElement('script')
const errorFn = () => {
handleError(id)
}
script.type = 'text/javascript'
script.charset = 'utf-8'
script.src = url
script.onerror = errorFn // for browsers that support
setTimeout(() => {
(head || (head = doc.getElementsByTagName('head')[0])).insertBefore(script, head.firstChild)
}, 0)
bundles.set(id, {
successFns : [onSuccess],
errorFns : onError? [onError] : [],
timer : setTimeout(errorFn, LOADING_TIMEOUT)
})
}
load._loaded = (bundle) => {
const bundleDesc = bundles.get(bundle.id)
if(!bundleDesc) return
clearTimeout(bundleDesc.timer)
bundle.js && bundle.js.call(globalThis)
bundle.css && appendCss(bundle.css)
if(bundle.hcss) {
const styles = []
const _ycssjs = window._ycssjs
bundle.hcss.forEach((hsh) => {
if(_ycssjs) {
if(hsh[0] in _ycssjs) return
_ycssjs(hsh[0])
}
styles.push(hsh[1])
})
styles.length && appendCss(styles.join(''))
}
const onSuccess = () => {
const fns = bundleDesc.successFns
for(const fn of fns) fn()
delete bundleDesc.successFns
}
onSuccess()
}
export default load
================================================
FILE: common.blocks/loader/_type/loader_type_js.js
================================================
/**
* @module loader_type_js
* @description Load JS from external URL.
*/
const loading = new Map()
const loaded = new Map()
const head = document.getElementsByTagName('head')[0]
const runCallbacks = (path, type) => {
const cbs = loading.get(path)
loading.delete(path)
for(const cb of cbs) {
cb[type] && cb[type]()
}
}
const onSuccess = (path) => {
loaded.set(path, true)
runCallbacks(path, 'success')
}
const onError = (path) => {
runCallbacks(path, 'error')
}
export default
/**
* @param {String} path resource link
* @param {Function} [success] to be called if the script succeeds
* @param {Function} [error] to be called if the script fails
*/
(path, success, error) => {
if(loaded.has(path)) {
success && success()
return
}
if(loading.get(path)) {
loading.get(path).push({ success, error })
return
}
loading.set(path, [{ success, error }])
const script = document.createElement('script')
script.type = 'text/javascript'
script.charset = 'utf-8'
script.src = (location.protocol === 'file:' && !path.indexOf('//')? 'http:' : '') + path
script.onload = () => {
script.onload = script.onerror = null
onSuccess(path)
}
script.onerror = () => {
script.onload = script.onerror = null
onError(path)
}
head.insertBefore(script, head.lastChild)
}
================================================
FILE: common.blocks/loader/_type/loader_type_js.spec.js
================================================
modules.define('spec', [
'loader_type_js',
'sinon'
], function(provide,
loader,
sinon
) {
describe('loader_type_js', function() {
it('should call success callback', function(done) {
var spyError = sinon.spy();
loader('data:text/javascript;charset=utf-8,;', function() {
spyError.should.not.have.been.called;
done();
}, spyError);
});
it('should call error callback', function(done) {
var spySuccess = sinon.spy();
loader('about:error', spySuccess, function() {
spySuccess.should.not.have.been.called;
done();
});
});
});
provide();
});
================================================
FILE: common.blocks/loader/loader.en.md
================================================
# loader
Use the `loader` block for downloading and connecting scripts by URLs.
## Overview
### Block modifiers
| Modifier | Acceptable values | Usage | Description |
| ----------- | ------------------- | --------------------- | -------- |
| type | `'js'`, `'bundle'` | `JS` | Uses a URL to get and connect JS code or a bundle. |
### Functions enabled by block elements
| Modifier | Function | Returned value | Description |
| ----------- | --- | ----------------------------- | -------- |
| js | loader(`id {String}`, `url {String}`, `[success {Function}]`, `[error {Function}]`) | - | Downloads and connects a fragment of JavaScript code. |
| bundle | loader(`url {String}`, `success {Function}`, `[error {Function}]`) | - | Downloads and connects a bundle of CSS and JS files. |
### Public block technologies
The block is implemented in:
* `js`
## Description
### Block modifiers
#### `type` modifier
Provides a set of functions to download and connect different data types.
Acceptable values: `'js'`, `'bundle'`.
Usage: `JS`.
Depending on the value of the `type` modifier, the `loader` block lets you download from a URL and connect:
* `js` – A JavaScript fragment.
* `bundle` – A bundle of CSS and JS files.
##### `type` modifier with the `js` value
Provides a function to download and connect a JavaScript fragment.
**Accepted arguments:**
* `url {String}` – URL of the JavaScript fragment to download. Required argument.
* [`success {Function}`] – The callback function to run when the code is loaded successfully.
* [`error {Function}`] – The callback function to run when the code couldn't load because of an error.
No return value.
For example, `loader_type_js` can be used for downloading and enabling jQuery:
```js
modules.define('jquery', ['loader_type_js'], function(provide, loader) {
loader(
'https://yastatic.net/jquery/2.2.0/jquery.min.js',
function() { provide(jQuery) });
});
```
For a more advanced example, see the [common.blocks/jquery](https://github.com/bem/bem-core/blob/v2/common.blocks/jquery/jquery.js) block in the `bem-core` library.
##### `type` modifier with the `bundle` value
Provides a function to download and connect a bundle of CSS and JS files.
**Accepted arguments:**
* `id {String}` – Bundle ID. Required argument.
* `url {String}` – The path to the bundle file in URL format. Required argument.
* `onSuccess {Function}` – The callback to run when the bundle is loaded successfully. Required argument.
* [`onError {Function}`] – The callback to run when the bundle didn't load.
No return value.
The specification for the `bundle` technology is currently under development. For more details, write your questions in the [forum](https://ru.bem.info/forum/).
###### `_loaded` static method
The function connected with the `type_bundle` modifier has the `_loaded` static method. It is used as a helper method after successfully loading the bundle.
**Accepted arguments:**
* `id {String}` – Bundle ID. Required argument.
No return value.
================================================
FILE: common.blocks/loader/loader.ru.md
================================================
# loader
Блок `loader` служит для загрузки и подключения скриптов по URL.
## Обзор
### Модификаторы блока
| Модификатор | Допустимые значения | Способы использования | Описание |
| ----------- | ------------------- | --------------------- | -------- |
| type | `'js'`, `'bundle'` | `JS` | Позволяет по URL получить и подключить JS-код или бандл. |
### Функции, подключаемые модификаторами блока
| Модификатор | Функция | Возвращаемое значение | Описание |
| ----------- | --- | ----------------------------- | -------- |
| js | loader(`id {String}`, `url {String}`, `[success {Function}]`, `[error {Function}]`) | - | Загружает и подключает фрагмент JavaScript-кода. |
| bundle | loader(`url {String}`, `success {Function}`, `[error {Function}]`) | - | Загружает и подключает пакет, собранный из CSS и JS-файлов – «бандл». |
### Публичные технологии блока
Блок реализован в технологиях:
* `js`
## Описание
### Модификаторы блока
#### Модификатор `type`
Предоставляет набор функций для загрузки и подключение различных типов данных.
Допустимые значения: `'js'`, `'bundle'`.
Способ использования: `JS`.
В зависимости от значения модификатора `type` блок `loader` позволяет получить по URL и подключить:
* `js` – фрагмент JS-кода.
* `bundle` – пакет, собранный из CSS и JS-файлов – «бандл».
##### Модификатор `type` в значении `js`
Предоставляет функцию, позволяющую загрузить и подключить фрагмент JS-кода.
**Принимаемые аргументы:**
* `url {String}` – URL загружаемого фрагмента JS-кода. Обязательный аргумент.
* [`success {Function}`] – callback-функция, выполняемая по завершению загрузки кода.
* [`error {Function}`] – callback-функция, выполняемая при ошибке в ходе загрузки кода.
Не имеет возвращаемого значения.
Например, `loader_type_js` может использоваться для загрузки и подключения jQuery:
```js
modules.define('jquery', ['loader_type_js'], function(provide, loader) {
loader(
'https://yastatic.net/jquery/2.2.0/jquery.min.js',
function() { provide(jQuery) });
});
```
Расширенный пример смотрите в блоке [common.blocks/jquery](https://github.com/bem/bem-core/blob/v2/common.blocks/jquery/jquery.js) библиотеки `bem-core`.
##### Модификатор `type` в значении `bundle`
Предоставляет функцию, позволяющую загрузить и подключить пакет, собранный из CSS и JS-файлов – «бандл».
**Принимаемые аргументы:**
* `id {String}` – идентификатор бандла. Обязательный аргумент.
* `url {String}` – путь до файла бандла в формате URL. Обязательный аргумент.
* `onSuccess {Function}` – callback, вызываемая по завершению загрузки бандла. Обязательный аргумент.
* [`onError {Function}`] – callback, вызываемая при неудачной загрузке бандла.
Не имеет возвращаемого значения.
Спецификации технологии `bundle` находятся в процессе разработки. Для получения детальной информации пишите на [форум](https://ru.bem.info/forum/).
###### Статический метод `_loaded`
Функция, подключаемая с модификатором `type_bundle`, обладает статическим методом – `_loaded`. Он используется как вспомогательный после успешной загрузки бандла.
**Принимаемые аргументы:**
* `id {String}` – идентификатор бандла. Обязательный аргумент.
Не имеет возвращаемого значения.
================================================
FILE: common.blocks/next-tick/next-tick.en.md
================================================
# next-tick
This block provides a function that performs an asynchronous call of the callback function passed as an argument in the next tick of the event loop.
`next-tick` – A polyfill that implements:
* A simulated event loop for outdated browser versions.
* A unified interface for working with various browsers and NodeJS.
This function works in cases when you need the callback to be invoked after the other functions in the event loop have finished. For example, you need to be sure that data will be available that is dynamically calculated in the current loop.
**Accepted arguments:**
* `fn {Function}` – The function to invoke in the next event loop. Required argument.
No return value.
Example:
```js
modules.require(['next-tick', 'events'], function(nextTick, events) {
var event = new events.Event();
nextTick(function() { event.emit('click') });
// ···
event.on('click', function(e) { console.log(e.type) })
});
```
## Order of callbacks
The block forms a queue within the event cycle, adding each subsequent callback function to the end of the queue. The callbacks are invoked in order.
Example:
```js
modules.require(['next-tick'], function(nextTick) {
var order = [];
nextTick(function() { order.push(1); });
nextTick(function() { order.push(2); });
nextTick(function() { order.push(3); });
nextTick(function() { console.log(order); }); // should be [1, 2, 3]
});
```
## Public block technologies
The block is implemented in:
* `vanilla.js`
================================================
FILE: common.blocks/next-tick/next-tick.ru.md
================================================
# next-tick
Блок предоставляет функцию, производящую асинхронный вызов callback-функции, переданной аргументом, в следующем витке событийного цикла.
`next-tick` – полифил, реализующий:
* симуляцию событийного цикла для старых версий браузеров;
* унифицированный интерфейс для работы с различными браузерами и NodeJS.
Функция подходит для случаев, когда нужно, чтобы callback был выполнен после того, как другие функции в рамках событийного цикла отработали. Например, чтобы убедиться что доступны данные, динамически вычислявшиеся в текущем цикле.
**Принимаемые аргументы:**
* `fn {Function}` – функция, которую нужно вызвать в следующем событийном цикле. Обязательный аргумент.
Не имеет возвращаемого значения.
Пример:
```js
modules.require(['next-tick', 'events'], function(nextTick, events) {
var event = new events.Event();
nextTick(function() { event.emit('click') });
// ···
event.on('click', function(e) { console.log(e.type) })
});
```
## Порядок вызова callback
В рамках событийного цикла работы блок формирует очередь, добавляя каждую следующую callback функцию в ее конец. Порядок вызова callback сохраняется.
Пример:
```js
modules.require(['next-tick'], function(nextTick) {
var order = [];
nextTick(function() { order.push(1); });
nextTick(function() { order.push(2); });
nextTick(function() { order.push(3); });
nextTick(function() { console.log(order); }); // should be [1, 2, 3]
});
```
## Публичные технологии блока
Блок реализован в технологиях:
* `vanilla.js`
================================================
FILE: common.blocks/next-tick/next-tick.spec.js
================================================
modules.define('spec', ['next-tick'], function(provide, nextTick) {
describe('next-tick', function() {
it('should call callback asynchronously', function(done) {
var isSync = true;
nextTick(function() {
isSync.should.be.false;
done();
});
isSync = false;
});
it('should call callbacks in the order of their originating calls', function(done) {
var order = [];
nextTick(function() { order.push(1); });
nextTick(function() { order.push(2); });
nextTick(function() { order.push(3); });
nextTick(function() {
order.should.be.eql([1, 2, 3]);
done();
});
});
});
provide();
});
================================================
FILE: common.blocks/next-tick/next-tick.vanilla.js
================================================
/**
* @module next-tick
*/
/**
* Executes given function on next tick.
* @type Function
* @param {Function} fn
*/
let fns = []
const enqueueFn = fn => {
fns.push(fn)
return fns.length === 1
},
callFns = () => {
const fnsToCall = fns
fns = []
for(const fn of fnsToCall)
fn()
}
/* global process */
const nextTick = typeof queueMicrotask === 'function'
? fn => { enqueueFn(fn) && queueMicrotask(callFns) }
: typeof process === 'object' && process.nextTick
? fn => { enqueueFn(fn) && process.nextTick(callFns) }
: fn => { enqueueFn(fn) && globalThis.setTimeout(callFns, 0) }
export default nextTick
================================================
FILE: common.blocks/objects/objects.en.md
================================================
# objects
This block provides an object with a set of methods for working with JavaScript objects.
## Overview
### Properties and methods of the object
| Name | Return type | Description |
| -------- | --- | -------- |
| extend ( `target {Object}`, `source {Object}`) | `Object` | Extends an object with the properties of another object. |
| isEmpty (`obj {Object}`) | `Boolean` | Determines whether the passed object is empty. |
| each ( `obj {Object}`, `fn {Function}`, `[ctx {Object}]`) | - | Iteratively traverses its own object properties. |
### Public block technologies
The block is implemented in:
* `vanilla.js`
## Description
### Properties and methods of the object
#### `extend` method
Extends an object with the properties of another object. It only copies its own properties that weren't taken from the prototype chain.
**Accepted arguments:**
* `target {Object}` – Target object. Required argument.
* `source {Object}` – The object whose properties are added to the target object. Multiple objects can be passed. The properties of each of them will be added to the target object. Required argument.
**Return value:** `Event`. The target object with the added properties.
Example:
```js
modules.require(['objects'], function(objects) {
var obj1 = { a : 1, b : 2 },
obj2 = { b : 3, c : 4 };
console.log(objects.extend(obj1, obj2)); // { a : 1, b : 3, c : 4 }
});
```
#### `isEmpty` method
Determines whether the passed object is empty. In other words, whether the object has its own properties.
**Accepted arguments:**
* `obj {Object}` – The object to check. Required argument.
**Return value:** `Boolean`. If the object doesn't have its own properties, `true`.
Example:
```js
modules.require(['objects'], function(objects) {
var obj1 = {},
obj2 = { foo : 'bar' };
console.log(objects.isEmpty(obj1)); // true
console.log(objects.isEmpty(obj2)); // false
});
```
#### `each` method
Used for iterating through an object's properties. The handler function is invoked for each of the object's own properties.
**Accepted arguments:**
* `obj {Object}` – The object whose properties are being traversed. Required argument.
* `fn {Function}` – The handler function to call for each property. Required argument.
* [`ctx {Object}`] – The handler context.
No return value.
The handler function receives arguments with the value and key of the object property that it was invoked for.
Example:
```js
modules.require(['objects'], function(objects) {
objects.each(
{ a : 1, b : 2 },
function(val, key) {
console.log(key, val);
});
// a 1
// b 2
});
```
================================================
FILE: common.blocks/objects/objects.ru.md
================================================
# objects
Блок предоставляет объект, содержащий набор методов для работы с объектами JavaScript.
## Обзор
### Свойства и методы объекта
| Имя | Тип возвращаемого значения | Описание |
| -------- | --- | -------- |
| extend ( `target {Object}`, `source {Object}`) | `Object` | Расширяет объект свойствами другого объекта. |
| isEmpty (`obj {Object}`) | `Boolean` | Позволяет выяснить пуст ли переданный объект. |
| each ( `obj {Object}`, `fn {Function}`, `[ctx {Object}]`) | - | Итеративно обходит собственные свойства объекта. |
### Публичные технологии блока
Блок реализован в технологиях:
* `vanilla.js`
## Описание
### Свойства и методы объекта
#### Метод `extend`
Расширяет объект свойствами другого объекта. Копируются только собственные свойства, не полученные по цепочке прототипов.
**Принимаемые аргументы:**
* `target {Object}` – целевой объект. Обязательный аргумент.
* `source {Object}` – объект, свойства которого добавляются к целевому. Может быть передано несколько объектов. Свойства каждого из них будут добавлены к целевому. Обязательный аргумент.
**Возвращаемое значение:** `Object`. Целевой объект с добавленными свойствами.
Пример:
```js
modules.require(['objects'], function(objects) {
var obj1 = { a : 1, b : 2 },
obj2 = { b : 3, c : 4 };
console.log(objects.extend(obj1, obj2)); // { a : 1, b : 3, c : 4 }
});
```
#### Метод `isEmpty`
Позволяет выяснить пуст ли переданный объект. Другими словами, имеет ли объект собственные свойства.
**Принимаемые аргументы:**
* `obj {Object}` – объект для проверки. Обязательный аргумент.
**Возвращаемое значение:** `Boolean`. В случае, если объект не имеет собственных свойств – `true`.
Пример:
```js
modules.require(['objects'], function(objects) {
var obj1 = {},
obj2 = { foo : 'bar' };
console.log(objects.isEmpty(obj1)); // true
console.log(objects.isEmpty(obj2)); // false
});
```
#### Метод `each`
Служит для итерации по собственным свойствам объекта. Для каждого собственного свойства вызывается функция-обработчик.
**Принимаемые аргументы:**
* `obj {Object}` – объект, обход свойств которого производится. Обязательный аргумент.
* `fn {Function}` – функция-обработчик, вызываемая для каждого свойства. Обязательный аргумент.
* [`ctx {Object}`] – контекст обработчика.
Не имеет возвращаемого значения.
Функция-обработчик получает в качестве аргументов значение и ключ свойства объекта, для которого была вызвана.
Пример:
```js
modules.require(['objects'], function(objects) {
objects.each(
{ a : 1, b : 2 },
function(val, key) {
console.log(key, val);
});
// a 1
// b 2
});
```
================================================
FILE: common.blocks/objects/objects.spec.js
================================================
modules.define('spec', ['objects'], function(provide, objects) {
describe('objects', function() {
var undef;
/* jshint -W001 */
describe('extend', function() {
it('should returns target object', function() {
var target = {};
objects.extend(target).should.be.equal(target);
});
it('should copy properties to target object', function() {
objects.extend(
{ p1 : 'v1', p2 : 'v2' },
{ p2 : 'v2_2', p3 : false },
{ p4 : null },
{ p5 : 0 })
.should.be.eql({
p1 : 'v1',
p2 : 'v2_2',
p3 : false,
p4 : null,
p5 : 0
});
});
it('should return new object if target is not a object', function() {
objects.extend(true, { p1 : 'v1' })
.should.be.eql({ p1 : 'v1' });
});
it('should return new object if target is null', function() {
objects.extend(null, { p1 : 'v1' })
.should.be.eql({ p1 : 'v1' });
});
it('should properly extend object with "hasOwnProperty" property', function() {
objects.extend(
{ hasOwnProperty : '' },
{ hasOwnProperty : 'has' })
.should.be.eql({ hasOwnProperty : 'has' });
});
});
describe('isEmpty', function() {
it('should returns true for object with no properties', function() {
objects.isEmpty({}).should.be.true;
});
it('should returns false for object with properties', function() {
objects.isEmpty({ prop : '' }).should.be.false;
});
it('should properly checks object with "hasOwnProperty" property', function() {
objects.isEmpty({ hasOwnProperty : true }).should.be.false;
});
});
describe('each', function() {
it('should iterates over all properties', function() {
var res = [],
undef;
objects.each(
{ a : 'str', b : false, c : null, d : undef },
function(val, key) {
res.push({ val : val, key : key });
});
res.should.be.eql([
{ val : 'str', key : 'a' },
{ val : false, key : 'b' },
{ val : null, key : 'c' },
{ val : undef, key : 'd' }
]);
});
it('should properly iterates over object with "hasOwnProperty" property', function() {
var res = [];
objects.each(
{ hasOwnProperty : false },
function(val, key) {
res.push({ val : val, key : key });
});
res.should.be.eql([{ val : false, key : 'hasOwnProperty' }]);
});
it('should call callback with given context', function() {
var ctx = {};
objects.each(
{ key : 'val' },
function() {
this.should.be.equal(ctx);
},
ctx);
});
});
});
provide();
});
================================================
FILE: common.blocks/objects/objects.vanilla.js
================================================
/**
* @module objects
* @description A set of helpers to work with JavaScript objects
*/
export default {
/**
* Extends a given target by
* @param {Object} target object to extend
* @param {Object} source
* @returns {Object}
*/
extend(target, source) {
(typeof target !== 'object' || target === null) && (target = {})
for(let i = 1, len = arguments.length; i < len; i++) {
const obj = arguments[i]
if(obj) {
for(const [key, val] of Object.entries(obj))
target[key] = val
}
}
return target
},
/**
* Check whether a given object is empty (contains no enumerable properties)
* @param {Object} obj
* @returns {Boolean}
*/
isEmpty(obj) {
return Object.keys(obj).length === 0
},
/**
* Generic iterator function over object
* @param {Object} obj object to iterate
* @param {Function} fn callback
* @param {Object} [ctx] callbacks's context
*/
each(obj, fn, ctx) {
for(const [key, val] of Object.entries(obj))
ctx ? fn.call(ctx, val, key) : fn(val, key)
}
}
================================================
FILE: common.blocks/page/__css/page__css.bemhtml.js
================================================
block('page').elem('css')(
bem()(false),
tag()('style'),
match(function() { return this.ctx.url; })(
tag()('link'),
attrs()(function() {
return this.extend(applyNext() || {}, { rel : 'stylesheet', href : this.ctx.url });
})
)
);
================================================
FILE: common.blocks/page/__css/page__css.bh.js
================================================
module.exports = function(bh) {
bh.match('page__css', function(ctx, json) {
ctx.bem(false);
if(json.url) {
ctx
.tag('link')
.attr('rel', 'stylesheet')
.attr('href', json.url);
} else {
ctx.tag('style');
}
});
};
================================================
FILE: common.blocks/page/__js/page__js.bemhtml.js
================================================
block('page').elem('js')(
bem()(false),
tag()('script'),
attrs()(function() {
var attrs = {};
if(this.ctx.url) {
attrs.src = this.ctx.url;
} else if(this._nonceCsp) {
attrs.nonce = this._nonceCsp;
}
return this.extend(applyNext() || {}, attrs);
})
);
================================================
FILE: common.blocks/page/__js/page__js.bh.js
================================================
module.exports = function(bh) {
bh.match('page__js', function(ctx, json) {
var nonce = ctx.tParam('nonceCsp');
ctx
.bem(false)
.tag('script');
if(json.url) {
ctx.attr('src', json.url);
} else if(nonce) {
ctx.attr('nonce', nonce);
}
});
};
================================================
FILE: common.blocks/page/page.bemhtml.js
================================================
block('page')(
mode('doctype')(function() {
return { html : this.ctx.doctype || '' };
}),
wrap()(function() {
var ctx = this.ctx;
this._nonceCsp = ctx.nonce;
return [
apply('doctype'),
{
tag : 'html',
attrs : { lang : ctx.lang },
cls : 'ua_js_no',
content : [
{
elem : 'head',
content : [
{ tag : 'meta', attrs : { charset : 'utf-8' } },
ctx.uaCompatible === false? '' : {
tag : 'meta',
attrs : {
'http-equiv' : 'X-UA-Compatible',
content : ctx.uaCompatible || 'IE=edge'
}
},
{ tag : 'title', content : ctx.title },
{ block : 'ua', attrs : { nonce : ctx.nonce } },
ctx.head,
ctx.styles,
ctx.favicon? { elem : 'favicon', url : ctx.favicon } : ''
]
},
ctx
]
}
];
}),
tag()('body'),
content()(function() {
return [
applyNext(),
this.ctx.scripts
];
}),
elem('head')(
bem()(false),
tag()('head')
),
elem('meta')(
bem()(false),
tag()('meta')
),
elem('link')(
bem()(false),
tag()('link')
),
elem('favicon')(
bem()(false),
tag()('link'),
attrs()(function() {
return this.extend(applyNext() || {}, { rel : 'shortcut icon', href : this.ctx.url });
})
)
);
================================================
FILE: common.blocks/page/page.bh.js
================================================
module.exports = function(bh) {
bh.match('page', function(ctx, json) {
ctx
.tag('body')
.tParam('nonceCsp', json.nonce)
.content([
ctx.content(),
json.scripts
], true);
return [
{ html : json.doctype || '', tag : false },
{
tag : 'html',
attrs : { lang : json.lang },
cls : 'ua_js_no',
content : [
{
elem : 'head',
content : [
{ tag : 'meta', attrs : { charset : 'utf-8' } },
json.uaCompatible === false? '' : {
tag : 'meta',
attrs : {
'http-equiv' : 'X-UA-Compatible',
content : json.uaCompatible || 'IE=edge'
}
},
{ tag : 'title', content : json.title },
{ block : 'ua', attrs : { nonce : json.nonce } },
json.head,
json.styles,
json.favicon? { elem : 'favicon', url : json.favicon } : '',
]
},
json
]
}
];
});
bh.match('page__head', function(ctx) {
ctx.bem(false).tag('head');
});
bh.match('page__meta', function(ctx) {
ctx.bem(false).tag('meta');
});
bh.match('page__link', function(ctx) {
ctx.bem(false).tag('link');
});
bh.match('page__favicon', function(ctx, json) {
ctx
.bem(false)
.tag('link')
.attr('rel', 'shortcut icon')
.attr('href', json.url);
});
};
================================================
FILE: common.blocks/page/page.deps.js
================================================
({
shouldDeps : [
{ block : 'i-bem-dom', elems : { elem : 'init', mods : { auto : true } } },
{ elems : ['css', 'js'] }
]
})
================================================
FILE: common.blocks/page/page.en.md
================================================
# page
This block provides templates that create a set of top-level HTML elements for a page: ``, ``, and ``.
## Overview
### Special fields of the block
| Field | Type | Description |
| ---- | --- | -------- |
| doctype | `String` | Use this field to redefine the DTD string for the current document. |
| title | `String` | Use this field to specify the content of ``. |
| favicon | `String` | Use this field to specify the URL of the favicon for the page. |
| head | `BEMJSON` | Use this field to add content to ``. |
| styles | `BEMJSON` | Use this field to connect CSS style sheets to the document. |
| scripts | `BEMJSON` | Use this field to embed scripts in the body of the document. |
| content | `BEMJSON` | Use this field to set the page content. |
### Elements of the block
| Element | Usage | Description |
| ------- | --------------------- | -------- |
| css | `BEMJSON` | Connects CSS using a URL or a string. |
| js | `BEMJSON` | Connects JS using a URL or a string. |
| meta | `BEMJSON` | Creates ` ` HTML elements. |
### Special fields of block elements
| Element | Field | Type | Description |
| ------- | ---- | --- | -------- |
| css | url | `String` | Sets the URL for downloading styles. |
| | content | `String` | Sets styles in string format. |
| js | url | `String` | Sets the URL for downloading a script. |
| | content | `String` | Sets scripts in string format |
### Public block technologies
The block is implemented in:
* `bh.js`
* `bemhtml`
## Description
This block is responsible for creating top-level HTML elements, connecting CSS, JS, and ` ` elements to a page, and defining the title. The BEMJSON declaration for the block and its elements have special fields reserved for this purpose.
### Special fields of the block
#### `doctype` field
Type: `String`.
Use this field to explicitly set the DTD (Document Type Definition) for the current document. If omitted, `` is used by default.
#### `title` field
Type: `String`.
Title of the page. It becomes the `` HTML element.
```js
{
block : 'page',
title : 'title',
content : 'Block page'
}
```
#### `favicon` field
Type: `String`.
Use this field to specify the URL of the favicon for the page:
```js
{
block : 'page',
title : 'title',
favicon : 'favicon.ico',
content : 'Page with users favicon.ico'
}
```
#### `head` field
Type: `BEMJSON`.
Use this field to add content to the `` `HTML` element that is defined in the block template:
```js
{
block : 'page',
title : 'title',
head : [
{ elem : 'js', url : 'jquery-min.js' },
{ elem : 'meta', attrs : { name : 'description', content : 'Yet another webdev blog' } }
],
content : 'Page with JS and meta-data'
}
```
#### `styles` field
Type: `BEMJSON`.
Use this field to connect `CSS`:
```js
{
block : 'page',
title : 'title',
styles : { elem : 'css', url : '_index.css' },
content : 'Page with CSS'
}
```
#### `scripts` field
Type: `BEMJSON`.
Embeds JS in the body of the page, at the end of the ` ` HTML element:
```js
{
block : 'page',
title : 'title',
scripts : { elem : 'js', url : '_index.js' },
content : 'Page with JS in body'
}
```
#### `content` field
Type: `BEMJSON`.
Use this field to set the page content.
```js
{
block : 'page',
title : 'title',
content : {
block : 'link',
mods : { pseudo : 'yes', togcolor : 'yes', color : 'green' },
url : '#',
target : '_blank',
title : 'Click me',
content : 'Pseudo link'
}
}
```
### Elements of the block
#### `css` element
Connects CSS using a URL or a string. Depending on whether the `url` field is specified in the element declaration, an HTML element is created with the tag:
* ` ` and the `stylesheet` property, if `url` is specified.
* `
================================================
FILE: common.blocks/page/page.tmpl-specs/25-styles.bemjson.js
================================================
({
block : 'page',
styles : [
{ elem : 'css', content : '.b-blah { color: red }' },
{ elem : 'css', content : '.b-blah2 { color: green }' }
]
})
================================================
FILE: common.blocks/page/page.tmpl-specs/25-styles.html
================================================
================================================
FILE: common.blocks/page/page.tmpl-specs/30-scripts.bemjson.js
================================================
({
block : 'page',
scripts : [
{ elem : 'js', url : 'https://yastatic.net/jquery/2.1.1/jquery.min.js' },
{ elem : 'js', url : 'https://yastatic.net/jquery/easing/1.3/jquery.easing.min.js' }
],
content : {
block : 'bla',
content : 'bla-bla'
}
})
================================================
FILE: common.blocks/page/page.tmpl-specs/30-scripts.html
================================================
bla-bla
================================================
FILE: common.blocks/page/page.tmpl-specs/40-nonce.bemjson.js
================================================
({
block : 'page',
scripts : [
{ elem : 'js', url : 'https://yastatic.net/jquery/2.1.1/jquery.min.js' },
{ elem : 'js', url : 'https://yastatic.net/jquery/easing/1.3/jquery.easing.min.js' },
{ elem : 'js', content : 'var a = true;' }
],
nonce : '123',
content : {
block : 'bla',
content : 'bla-bla'
}
})
================================================
FILE: common.blocks/page/page.tmpl-specs/40-nonce.html
================================================
bla-bla
================================================
FILE: common.blocks/page/page.tmpl-specs/60-x-ua-compatible.bemjson.js
================================================
({
block : 'page',
title : 'Remove x-ua-compatible',
uaCompatible : false
})
================================================
FILE: common.blocks/page/page.tmpl-specs/60-x-ua-compatible.html
================================================
Remove x-ua-compatible
================================================
FILE: common.blocks/page/page.tmpl-specs/70-lang.bemjson.js
================================================
({
block : 'page',
lang : 'en-us'
})
================================================
FILE: common.blocks/page/page.tmpl-specs/70-lang.html
================================================
================================================
FILE: common.blocks/strings/__escape/strings__escape.spec.js
================================================
modules.define('spec', ['strings__escape'], function(provide, escape) {
describe('strings__escape', function() {
it('should properly escape XML', function() {
escape.xml('a & b ')
.should.be.equal('<x y="z">a & b</x>');
});
it('should properly escape HTML', function() {
escape.html('Bold & bold ')
.should.be.equal('<b class="bold">Bold & bold</b>');
});
it('should properly escape attributes', function() {
escape.attr('some <> attr with different "quo" & \'tes\' inside')
.should.be.equal('some <> attr with different "quo" & 'tes' inside');
});
});
provide();
});
================================================
FILE: common.blocks/strings/__escape/strings__escape.vanilla.js
================================================
/**
* @module strings__escape
* @description A set of string escaping functions
*/
const symbols = {
'"' : '"',
'\'' : ''',
'&' : '&',
'<' : '<',
'>' : '>'
},
mapSymbol = s => symbols[s] || s,
buildEscape = regexp => {
regexp = new RegExp(regexp, 'g')
return str => ('' + str).replace(regexp, mapSymbol)
}
export default {
/**
* Escape string to use in XML
* @type Function
* @param {String} str
* @returns {String}
*/
xml : buildEscape('[&<>]'),
/**
* Escape string to use in HTML
* @type Function
* @param {String} str
* @returns {String}
*/
html : buildEscape('[&<>]'),
/**
* Escape string to use in attributes
* @type Function
* @param {String} str
* @returns {String}
*/
attr : buildEscape('["\'&<>]')
}
================================================
FILE: common.blocks/strings/strings.en.md
================================================
# strings
This block provides helpers for manipulating string data.
## Overview
### Elements of the block
| Element | Usage | Description |
| --------| --------------------- | -------- |
| escape | `JS` | A set of methods for escaping XML and HTML control characters. |
### Properties and methods of the block elements
| Element| Name | Type or return value | Description |
| -------| --- | ----------------------------- | -------- |
| escape | xml (`str {String}`) | `String` | Use for escaping XML control characters. |
| | html (`str {String}`) | `String` | Use for escaping HTML control characters. |
| | attr (`str {String}`) | `String` | Use for escaping control characters in HTML and XML attributes. |
### Public block technologies
The block is implemented in:
* `vanilla.js`
## Description
### Elements of the block
#### `escape` element
This element provides an object with a set of methods for escaping XML and HTML control characters.
### Properties and methods of the object
#### `xml` method
Use for escaping XML control characters. Processes the symbols `&`, `<`, `>`.
**Accepted arguments:**
* `str {String}` – String to process. Required argument.
**Return value:** `String`. The string with escaped control characters.
#### `html` method
Use for escaping HTML control characters. It is a synonym of the `xml` method.
#### `attr` method
Use for escaping control characters in HTML and XML attributes. Processes the control characters `"`, `\`, `'`, `&`, `<`, `>`.
**Accepted arguments:**
* `str {String}` – String to process. Required argument.
**Return value:** `String`. The string with escaped control characters.
For example, in the [`common.blocks/select`](https://github.com/bem/bem-components/blob/v2/common.blocks/select/select.js#L237) block in the `bem-components` library, `strings__escape` is used for escaping control characters in the `value` property of an HTML element:
```js
_createControlHTML : function(name, val) {
// Using string concatenation to not depend on template engines
return ' ';
}
```
================================================
FILE: common.blocks/strings/strings.ru.md
================================================
# strings
Блок предоставляет хелперы для манипуляций с данными строчного типа.
## Обзор
### Элементы блока
| Элемент | Способы использования | Описание |
| --------| --------------------- | -------- |
| escape | `JS` | Набор методов для экранирования (эскейпинга) управляющих символов XML и HTML. |
### Свойства и методы элементов блока
| Элемент| Имя | Тип или возвращаемое значение | Описание |
| -------| --- | ----------------------------- | -------- |
| escape | xml (`str {String}`) | `String` | Служит для экранирования управляющих символов XML. |
| | html (`str {String}`) | `String` | Служит для экранирования управляющих символов HTML. |
| | attr (`str {String}`) | `String` | Служит для экранирования управляющих символов в HTML и XML атрибутах. |
### Публичные технологии блока
Блок реализован в технологиях:
* `vanilla.js`
## Описание
### Элементы блока
#### Элемент `escape`
Элемент предоставляет объект, содержащий набор методов для экранирования (эскейпинга) управляющих символов XML и HTML.
### Свойства и методы объекта
#### Метод `xml`
Служит для экранирования управляющих символов XML. Обрабатываются символы `&`, `<`, `>`.
**Принимаемые аргументы:**
* `str {String}` – строка для обработки. Обязательный аргумент.
**Возвращаемое значение:** `String`. Строка с экранированными управляющими символами.
#### Метод `html`
Служит для экранирования управляющих символов HTML. Является синонимом метода `xml`.
#### Метод `attr`
Служит для экранирования управляющих символов в HTML и XML атрибутах. Обрабатываются управляющие символы `"`, `\`, `'`, `&`, `<`, `>`.
**Принимаемые аргументы:**
* `str {String}` – строка для обработки. Обязательный аргумент.
**Возвращаемое значение:** `String`. Строка с экранированными управляющими символами.
Например, в блоке [`common.blocks/select`](https://github.com/bem/bem-components/blob/v2/common.blocks/select/select.js#L237) библиотеки `bem-components`, `strings__escape` используется для экранирования управляющих символов в свойстве `value` HTML-элемента:
```js
_createControlHTML : function(name, val) {
// Using string concatenation to not depend on template engines
return ' ';
}
```
================================================
FILE: common.blocks/tick/_start/tick_start_auto.vanilla.js
================================================
/**
* Automatically starts tick module
*/
import tick from 'bem:tick';
tick.start();
================================================
FILE: common.blocks/tick/tick.deps.js
================================================
({
shouldDeps : ['events', 'inherit']
})
================================================
FILE: common.blocks/tick/tick.en.md
================================================
# tick
This block provides an object for working with a regularly generated `tick` event (to implement the polling pattern).
## Overview
### Object events
| Name | Description |
| -------- | -------- |
| tick | A regularly generated event. |
### Properties and methods of the object
| Name | Return type | Description |
| -------- | --- | -------- |
| start () | - | Starts generating `tick` events if the process hasn't started yet. |
| stop () | - | Stops generating `tick` events if the process hasn't stopped yet. |
### Block modifiers
| Modifier | Acceptable values | Usage | Description |
| ----------- | ------------------- | --------------------- | -------- |
| start | `'auto'` | `JS` | Automatically starts generating events |
### Public block technologies
The block is implemented in:
* `vanilla.js`
## Description
### Object events
#### `tick` event
Subscribe to the event to use it to implement the polling pattern.
An event is generated every 50 milliseconds.
### Properties and methods of the object
The block is a descendant of the `Emitter` class in the `events` block, which allows it to call these classes.
```js
modules.require('tick', function(tick) {
var update = function() { /* ... */ };
tick
.on('tick', update) // subscribing to the tick event
.start(); // starting generation of tick events
});
```
#### `start` method
Starts generating [tick](#fields-tick) events if the process hasn't started yet. A `tick` is generated with an interval of 50 milliseconds after invoking the method.
Doesn't accept arguments.
No return value.
#### `stop` method
Stops generating [tick](#fields-tick) events.
Doesn't accept arguments.
No return value.
### Block modifiers
#### `start` modifier
Acceptable values: `'auto'`.
Usage: `JS`.
Use the block with the `start` modifier set to `auto` in order to automatically start generating [tick](#fields-tick) events. The event starts being generated at the time of block initialization.
================================================
FILE: common.blocks/tick/tick.ru.md
================================================
# tick
Блок предоставляет объект для работы с регулярно генерируемым событием `tick` (для реализации паттерна polling).
## Обзор
### События объекта
| Имя | Описание |
| -------- | -------- |
| tick | Регулярно генерируемое событие. |
### Свойства и методы объекта
| Имя | Тп возвращаемого значения | Описание |
| -------- | --- | -------- |
| start () | - | Запускает генерацию события `tick`, если она еще не запущена. |
| stop () | - | Останавливает генерацию события `tick`, если она еще не остановлена. |
### Модификаторы блока
| Модификатор | Допустимые значения | Способы использования | Описание |
| ----------- | ------------------- | --------------------- | -------- |
| start | `'auto'` | `JS` | Автоматический запуск генерации события |
### Публичные технологии блока
Блок реализован в технологиях:
* `vanilla.js`
## Описание
### События объекта
#### Событие `tick`
Подписавшись на событие, можно использовать его для реализации паттерна polling.
Событие генерируется с интервалом в 50 миллисекунд.
### Свойства и методы объекта
Блок наследуется от клсаса `Emitter` блока `events`, что позволяет вызывать методы класса.
```js
modules.require('tick', function(tick) {
var update = function() { /* ... */ };
tick
.on('tick', update) // подписываемся на событие tick
.start(); // запускаем генерацию события tick
});
```
#### Метод `start`
Запускает генерацию события [tick](#fields-tick), если они еще не запущены. Событие `tick` генерируется через интервал в 50 миллисекунд после вызова метода.
Не принимает аргументов.
Не имеет возвращаемого значения.
#### Метод `stop`
Останавливает генерацию события [tick](#fields-tick).
Не принимает аргументов.
Не имеет возвращаемого значения.
### Модификаторы блока
#### Модификатор `start`
Допустимые значения: `'auto'`.
Способ использования: `JS`.
Блок с модификатором `start` в значении `auto` используется для автоматического запуска генерации события [tick](#fields-tick). Событие начинает генерироваться в момент инициализации блока.
================================================
FILE: common.blocks/tick/tick.spec.js
================================================
modules.define('spec', [
'tick',
'sinon'
], function(provide,
tick,
sinon
) {
describe('tick', function() {
describe('start/stop', function() {
var TICK_INTERVAL = 50,
clock;
beforeEach(function() {
clock = sinon.useFakeTimers();
});
afterEach(function() {
clock.restore();
});
it('should emit tick event only if started', function(done) {
var spy = sinon.spy();
tick
.on('tick', spy)
.start();
setTimeout(function() {
tick.stop();
setTimeout(function() {
spy.should.have.been.calledOnce;
done();
}, TICK_INTERVAL);
clock.tick(TICK_INTERVAL);
}, TICK_INTERVAL);
clock.tick(TICK_INTERVAL);
});
it('should continue ticking after exception in callback', function(done) {
var spy = sinon.spy();
tick
.on('tick', function() {
if(!spy.called) {
spy();
throw new Error('test error');
}
spy();
tick.stop();
done();
})
.start();
setTimeout(function() {
clock.tick(TICK_INTERVAL);
}, TICK_INTERVAL);
clock.tick(TICK_INTERVAL);
});
it('should not emit tick event after .stop() in callback', function(done) {
var spy = sinon.spy();
tick
.on('tick', function() {
spy();
tick.stop();
})
.start();
setTimeout(function() {
setTimeout(function() {
spy.should.have.been.calledOnce;
tick.stop();
done();
}, TICK_INTERVAL);
clock.tick(TICK_INTERVAL);
}, TICK_INTERVAL);
clock.tick(TICK_INTERVAL);
});
});
});
provide();
});
================================================
FILE: common.blocks/tick/tick.vanilla.js
================================================
/**
* @module tick
* @description Helpers for polling anything
*/
import inherit from 'bem:inherit'
import events from 'bem:events'
const TICK_INTERVAL = 50,
/**
* @class Tick
* @augments events:Emitter
*/
Tick = inherit(events.Emitter, /** @lends Tick.prototype */{
/**
* @constructor
*/
__constructor : function() {
this._timer = null
this._isStarted = false
},
/**
* Starts polling
*/
start : function() {
if(!this._isStarted) {
this._isStarted = true
this._scheduleTick()
}
},
/**
* Stops polling
*/
stop : function() {
if(this._isStarted) {
this._isStarted = false
globalThis.clearTimeout(this._timer)
}
},
_scheduleTick : function() {
this._timer = globalThis.setTimeout(
() => this._onTick(),
TICK_INTERVAL)
},
_onTick : function() {
try {
this.emit('tick')
} finally {
this._isStarted && this._scheduleTick()
}
}
})
/**
* @type Tick
*/
export default new Tick()
================================================
FILE: common.blocks/ua/__svg/ua__svg.bemhtml.js
================================================
block('ua').content()(function() {
return [
applyNext(),
{
html : [
'(function(d,n){',
'd.documentElement.className+=',
'" ua_svg_"+(d[n]&&d[n]("http://www.w3.org/2000/svg","svg").createSVGRect?"yes":"no");',
'})(document,"createElementNS");'
].join('')
}
];
});
================================================
FILE: common.blocks/ua/__svg/ua__svg.bh.js
================================================
module.exports = function(bh) {
bh.match('ua', function(ctx, json) {
ctx.applyBase();
ctx.content([
json.content,
{
tag : false,
html : [
'(function(d,n){',
'd.documentElement.className+=',
'" ua_svg_"+(d[n]&&d[n]("http://www.w3.org/2000/svg","svg").createSVGRect?"yes":"no");',
'})(document,"createElementNS");'
].join('')
}
], true);
});
};
================================================
FILE: common.blocks/ua/__svg/ua__svg.deps.js
================================================
({
mustDeps : 'ua'
})
================================================
FILE: common.blocks/ua/__svg/ua__svg.en.title.txt
================================================
SVG support detection
================================================
FILE: common.blocks/ua/__svg/ua__svg.ru.title.txt
================================================
Определение поддержки SVG
================================================
FILE: common.blocks/ua/__svg/ua__svg.tmpl-specs/00-simple.bemjson.js
================================================
({
block : 'ua'
})
================================================
FILE: common.blocks/ua/__svg/ua__svg.tmpl-specs/00-simple.html
================================================
================================================
FILE: common.blocks/ua/ua.bemhtml.js
================================================
block('ua')(
tag()('script'),
bem()(false),
content()([
'(function(e,c){',
'e[c]=e[c].replace(/(ua_js_)no/g,"$1yes");',
'})(document.documentElement,"className");'
])
);
================================================
FILE: common.blocks/ua/ua.bh.js
================================================
module.exports = function(bh) {
bh.match('ua', function(ctx) {
ctx
.bem(false)
.tag('script')
.content([
'(function(e,c){',
'e[c]=e[c].replace(/(ua_js_)no/g,"$1yes");',
'})(document.documentElement,"className");'
], true);
});
};
================================================
FILE: common.blocks/ua/ua.en.md
================================================
# ua
Use this block to collect data about the user's browser.
## Overview
### Elements of the block
| Element | Usage | Description |
| ------- | --------------------- | -------- |
| svg | `deps` | Checks whether the browser supports SVG format. |
### Public block technologies
The block is implemented in:
* `bh.js`
* `bemhtml`
## Description
The block enables an inline script that adds `CSS` classes to the `` tag to specify whether JavaScript is enabled – `ua_js_no` or `ua_js_yes`.
It doesn't have a visual representation on the page.
Used inside the [page](https://github.com/bem/bem-core/blob/v2/common.blocks/page/page.en.md) block. You normally don't need to connect it to the page yourself.
### Elements of the block
#### `svg` element
This element enables an inline script that adds `CSS` classes to the `` tag to specify whether SVG is supported – `ua_svg_no` or `ua_svg_yes`.
It doesn't have a visual representation on the page.
To use it, add the element to the `deps.js` dependencies file for the block that needs information about SVG support:
```js
({ shouldDeps : { block : 'ua', elem : 'svg' } })
```
================================================
FILE: common.blocks/ua/ua.en.title.txt
================================================
Block for gathering and providing UserAgent information
================================================
FILE: common.blocks/ua/ua.ru.md
================================================
# ua
Блок служит для сбора данных о браузере пользователя.
## Обзор
### Элементы блока
| Элемент | Способы использования | Описание |
| ------- | --------------------- | -------- |
| svg | `deps` | Проверяет, поддерживает ли браузер формат SVG. |
### Публичные технологии блока
Блок реализован в технологиях:
* `bh.js`
* `bemhtml`
## Описание
Блок подключает инлайновый скрипт, добавляющий тегу `` `CSS`-классы, указывающие, включен ли JavaScript – `ua_js_no`/`ua_js_yes`.
Не имеет визуального представления на странице.
Используется внутри блока [page](https://github.com/bem/bem-core/blob/v2/common.blocks/page/page.ru.md) и самостоятельно подключать его к странице обычно не требуется.
### Элементы блока
#### Элемент `svg`
Элемент подключает инлайновый скрипт, добавляющий тегу `` `CSS`-классы, указывающие, поддерживается ли SVG – `ua_svg_no`/`ua_svg_yes`.
Не имеет визуального представления на странице.
Для использования включите элемент в файл зависимостей `deps.js` блока, которому требуются данные о поддержке SVG:
```js
({ shouldDeps : { block : 'ua', elem : 'svg' } })
```
================================================
FILE: common.blocks/ua/ua.ru.title.txt
================================================
Сбор и провайдинг информации о UserAgent
================================================
FILE: common.blocks/ua/ua.tmpl-specs/00-simple.bemjson.js
================================================
({
block : 'ua'
})
================================================
FILE: common.blocks/ua/ua.tmpl-specs/00-simple.html
================================================
================================================
FILE: common.blocks/uri/__querystring/uri__querystring.deps.js
================================================
({
shouldDeps : 'uri'
})
================================================
FILE: common.blocks/uri/__querystring/uri__querystring.spec.js
================================================
modules.define('spec', ['uri__querystring'], function(provide, qs) {
describe('querystring', function() {
describe('parse()', function() {
it('should support the basics', function() {
qs.parse('0=foo').should.eql({ '0' : 'foo' });
qs.parse('foo').should.eql({ foo : '' });
qs.parse('foo=bar').should.eql({ foo : 'bar' });
qs.parse(' foo = bar = baz ').should.eql({ ' foo ' : ' bar = baz ' });
qs.parse('foo=bar=baz').should.eql({ foo : 'bar=baz' });
qs.parse('foo=bar&bar=baz').should.eql({ foo : 'bar', bar : 'baz' });
qs.parse('foo=bar&baz').should.eql({ foo : 'bar', baz : '' });
qs.parse('cht=p3&chd=t :60,40&chs=250x100&chl=Hello|World').should.eql({
cht : 'p3',
chd : 't :60,40',
chs : '250x100',
chl : 'Hello|World'
});
qs.parse('=').should.eql({ '' : '' });
qs.parse('==').should.eql({ '' : '=' });
qs.parse('_r=1&').should.eql({ _r : '1' });
});
it('should support encoded = signs', function() {
qs.parse('he%3Dllo=th%3Dere').should.eql({ 'he=llo' : 'th=ere' });
});
it('should expand to an array when dupliate keys are present', function() {
qs.parse('items=bar&items=baz&items=raz').should.eql({ items : ['bar', 'baz', 'raz'] });
qs.parse('=&=').should.eql({ '' : ['', ''] });
});
it('should support empty values', function(){
qs.parse('').should.eql({});
qs.parse(undefined).should.eql({});
qs.parse(null).should.eql({});
});
it('should support names of built-in Object properties', function() {
/* jshint -W001 */
qs.parse('hasOwnProperty=x&toString=foo&valueOf=bar&__defineGetter__=baz&constructor=1')
.should.eql({
hasOwnProperty : 'x',
toString : 'foo',
valueOf : 'bar',
__defineGetter__ : 'baz',
constructor : '1'
});
});
});
describe('stringify()', function() {
function test(cases) {
cases.forEach(function(testCase) {
qs.stringify(testCase.obj).should.eq(testCase.str);
});
}
it('should support the basics', function() {
/* jshint quotmark: false */
test([
{ str : 'foo=bar', obj : { 'foo' : 'bar' } },
{ str : 'foo=%22bar%22', obj : { 'foo' : '\"bar\"' } },
{ str : 'foo=', obj : { 'foo' : '' } },
{ str : 'foo=1&bar=2', obj : { 'foo' : '1', 'bar' : '2' } },
{
str : 'my%20weird%20field=q1!2%22\'w%245%267%2Fz8)%3F',
obj : { 'my weird field' : 'q1!2"\'w$5&7/z8)?' }
},
{ str : 'foo%3Dbaz=bar', obj : { 'foo=baz' : 'bar' } },
{ str : 'foo=bar&bar=baz', obj : { foo : 'bar', bar : 'baz' } },
{ str : 'foo=bar&baz=&raz=', obj : { foo : 'bar', baz : null, raz : undefined } },
{ str : 'foo=bar&=', obj : { foo : 'bar', '' : '' } }
]);
});
it('should support escapes', function() {
test([
{ str : 'foo=foo%20bar', obj : { foo : 'foo bar' } },
{
str : 'cht=p3&chd=t%3A60%2C40&chs=250x100&chl=Hello%7CWorld',
obj : {
cht : 'p3',
chd : 't:60,40',
chs : '250x100',
chl : 'Hello|World'
}
}
]);
});
it('should support arrays', function() {
test([
{ str : 'limit=1&limit=2&limit=a', obj : { limit : [1, 2, 'a'] } }
]);
});
it('should support others types', function() {
var date = new Date(0);
test([
{ str : 'at=' + encodeURIComponent(date), obj : { at : date } }
]);
});
it('should support names of built-in Object properties', function() {
/* jshint -W001 */
test([
{
str : 'hasOwnProperty=x&toString=foo&valueOf=bar&__defineGetter__=baz&constructor=1',
obj : {
hasOwnProperty : 'x',
toString : 'foo',
valueOf : 'bar',
__defineGetter__ : 'baz',
constructor : '1'
}
}
]);
});
});
});
provide();
});
================================================
FILE: common.blocks/uri/__querystring/uri__querystring.vanilla.js
================================================
/**
* @module uri__querystring
* @description A set of helpers to work with query strings
*/
import uri from 'bem:uri'
function addParam(res, name, val) {
res.push(encodeURIComponent(name) + '=' + (val == null? '' : encodeURIComponent(val)))
}
export default {
/**
* Parse a query string to an object
* @param {String} str
* @returns {Object}
*/
parse(str) {
if(!str) {
return {}
}
return str.split('&').reduce(
(res, pair) => {
if(!pair) {
return res
}
const eq = pair.indexOf('=')
let name, val
if(eq >= 0) {
name = pair.substr(0, eq)
val = pair.substr(eq + 1)
} else {
name = pair
val = ''
}
name = uri.decodeURIComponent(name)
val = uri.decodeURIComponent(val)
Object.hasOwn(res, name)?
Array.isArray(res[name])?
res[name].push(val) :
res[name] = [res[name], val] :
res[name] = val
return res
},
{})
},
/**
* Serialize an object to a query string
* @param {Object} obj
* @returns {String}
*/
stringify(obj) {
return Object.keys(obj)
.reduce(
(res, name) => {
const val = obj[name]
Array.isArray(val)?
val.forEach(function(val) {
addParam(res, name, val)
}) :
addParam(res, name, val)
return res
},
[])
.join('&')
}
}
================================================
FILE: common.blocks/uri/uri.en.md
================================================
# uri
This block provides an object with a set of methods for decoding a URI-encoded string.
## Overview
### Properties and methods of the object
| Name | Return type | Description |
| --- | -------------------------- | -------- |
| decodeURI (`str {String}`) | `String` | Decodes a URI. |
| decodeURIComponent (`str {String}`) | `String` | Decodes a URI component. |
### Elements of the block
| Element | Usage | Description |
| --------| --------------------- | -------- |
| querystring | `JS` | This element provides an object with a set of methods for working with a URI query string. It decodes the string from URI format. |
#### Properties and methods of the object
| Element | Name | Returned value | Description |
| ------- | --- | --------------------- | -------- |
| querystring | parse (`str {String}`) | `Object` | Creates an object using the query parameters from the address bar. |
| | stringify (`obj {Object}`) | `String` | Creates a query string based on the object properties. |
### Public block technologies
The block is implemented in:
* `vanilla.js`
### Properties and methods of the object
Both of these methods function as wrappers for the standard JavaScript methods `decodeURI` and `decodeURIComponent`.
As they execute, the methods check whether the passed string is in UTF-8 format. If not, they generate an error.
#### `decodeURI` method
Decodes a URI. This method is identical to the standard JavaScript method [decodeURI](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI), but it supports Cyrillic encoding `CP-1251`.
**Accepted arguments:**
* `str {String}` – A string with escape sequences. Required argument.
**Return value:** `String`. If escape sequences are not found in the string, the method returns the string without any changes.
Example:
```js
modules.require('uri', function(uri){
uri.decodeURI("https://developer.mozilla.org/ru/docs/JavaScript_%D1%88%D0%B5%D0%BB%D0%BB%D1%8B");
// "https://developer.mozilla.org/ru/docs/JavaScript_Shells"
})
```
#### `decodeURIComponent` method
Decodes a URI component. This method is identical to the standard JavaScript method [decodeURIComponent](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent), but it supports Cyrillic encoding `CP-1251`.
**Accepted arguments:**
* `str {String}` – A string with escape sequences. Required argument.
**Return value:** `String`. If escape sequences are not found in the string, the method returns the string without any changes.
Example:
```js
modules.require('uri', function(uri){
uri.decodeURIComponent("JavaScript_%D1%88%D0%B5%D0%BB%D0%BB%D1%8B");
// "JavaScript_Shells"
})
```
### Elements of the block
#### `querystring` element
This element provides an object with a set of methods for working with a URI query string.
### Properties and methods of the object
#### `parse` method
Creates an object using the parameters from a URI query string.
**Accepted arguments:**
* `str {String}` – A string with parameters as key-value pairs. The `=` symbol separates a key from its value. Pairs are separated by the `&` symbol. During parsing, keys and values are decoded from URI format. Required argument.
**Return value:** `Object`. The object created from the parameters in the address bar.
#### `stringify` method
Creates a URI query string from an object.
**Accepted arguments:**
* `obj {Object}` – The object to create the string from. Required argument.
**Return value:** `String`. Property names are separated from values by the `=` symbol, and the `&` symbol separates pairs in the string.
================================================
FILE: common.blocks/uri/uri.ru.md
================================================
# uri
Блок предоставляет объект, содержащий набор методов для декодирования строки из формата URI.
## Обзор
### Свойства и методы объекта
| Имя | Тип возвращаемого значения | Описание |
| --- | -------------------------- | -------- |
| decodeURI (`str {String}`) | `String` | Служит для декодирования URI. |
| decodeURIComponent (`str {String}`) | `String` | Служит для декодирования URI компонента. |
### Элементы блока
| Элемент | Способы использования | Описание |
| --------| --------------------- | -------- |
| querystring | `JS` | Элемент предоставляет объект, содержащий набор методов для работы со строкой запроса формата URI. предназначен для декодирования строки из формата URI. |
#### Свойства и методы объекта
| Элемент | Имя | Возвращаемое значение | Описание |
| ------- | --- | --------------------- | -------- |
| querystring | parse (`str {String}`) | `Object` | Формирует объект на основании параметров запроса адресной строки. |
| | stringify (`obj {Object}`) | `String` | Формирует строку запроса на основании свойств объекта. |
### Публичные технологии блока
Блок реализован в технологиях:
* `vanilla.js`
### Свойства и методы объекта
Оба метода являются оберткой над соответствующими стандартными методами JavaScript `decodeURI` и `decodeURIComponent`.
В ходе работы методы проверяют соответствие формата переданной строки UTF-8. При несоответствии генерируется ошибка.
#### Метод `decodeURI`
Служит для декодирования URI. Метод идентичен стандартному методу JavaScript [decodeURI](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI), но поддерживает кириллическую кодировку `CP-1251`.
**Принимаемые аргументы:**
* `str {String}` – строка с последовательностями экранирования. Обязательный аргумент.
**Возвращаемое значение:** `String`. В случае если последовательности экранирования в строке не найдены метод возвращают ее без изменений.
Пример:
```js
modules.require('uri', function(uri){
uri.decodeURI("https://developer.mozilla.org/ru/docs/JavaScript_%D1%88%D0%B5%D0%BB%D0%BB%D1%8B");
// "https://developer.mozilla.org/ru/docs/JavaScript_шеллы"
})
```
#### Метод `decodeURIComponent`
Служит для декодирование компонента URI. Метод идентичен стандартному методу JavaScript [decodeURIComponent](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent), но поддерживает кириллическую кодировку `CP-1251`.
**Принимаемые аргументы:**
* `str {String}` – строка с последовательностями экранирования. Обязательный аргумент.
**Возвращаемое значение:** `String`. В случае если последовательности экранирования в строке не найдены метод возвращают ее без изменений.
Пример:
```js
modules.require('uri', function(uri){
uri.decodeURIComponent("JavaScript_%D1%88%D0%B5%D0%BB%D0%BB%D1%8B");
// "JavaScript_шеллы"
})
```
### Элементы блока
#### Элемент `querystring`
Элемент предоставляет объект, содержащий набор методов для работы со строкой запроса формата URI.
### Свойства и методы объекта
#### Метод `parse`
Формирует объект на основании строки параметров в формате URI.
**Принимаемые аргументы:**
* `str {String}` – строка с параметрами в виде пар ключ-значение. Ключ отделяется от значения символом `=`. Пары разделяются символом `&`. В процессе обработки, ключи и значения декодируются из формата URI. Обязательный аргумент.
**Возвращаемое значение:** `Object`. Объект, сформированный на основании параметров адресной строки.
#### Метод `stringify`
Формирует строку запроса в формате URI на основании объекта.
**Принимаемые аргументы:**
* `obj {Object}` – объект, на основании которого формируется строка. Обязательный аргумент.
**Возвращаемое значение:** `String`. Имена свойств в строке отделяются от значений символом `=`, пары разделяются символом `&`.
================================================
FILE: common.blocks/uri/uri.spec.js
================================================
modules.define('spec', ['uri'], function(provide, uri) {
describe('uri', function() {
describe('decodeURIComponent()', function() {
it('should be able to decode cp1251 encoded params', function() {
uri.decodeURIComponent('%F2%E0%E1%EB%EE').should.be.eql('табло');
});
it('should not fall on params encoded with unknown encoding', function() {
uri.decodeURIComponent('%COCO%C0C0').should.be.eql('%COCO%C0C0');
});
});
describe('decodeURI()', function() {
it('should be able to decode url with cp1251 encoded params', function() {
uri
.decodeURI('http://test.com/ololo/trololo.html?text=%F2%E0%E1%EB%EE')
.should.be.eql('http://test.com/ololo/trololo.html?text=табло');
});
it('should not fall on url with params encoded with unknown encoding', function() {
uri
.decodeURI('http://test.com/ololo/trololo.html?text=%COCO%C0C0')
.should.be.eql('http://test.com/ololo/trololo.html?text=%COCO%C0C0');
});
});
});
provide();
});
================================================
FILE: common.blocks/uri/uri.vanilla.js
================================================
/**
* @module uri
* @description A set of helpers to work with URI
*/
// Equivalency table for cp1251 and utf8.
const map = { '%D0' : '%D0%A0', '%C0' : '%D0%90', '%C1' : '%D0%91', '%C2' : '%D0%92', '%C3' : '%D0%93', '%C4' : '%D0%94', '%C5' : '%D0%95', '%A8' : '%D0%81', '%C6' : '%D0%96', '%C7' : '%D0%97', '%C8' : '%D0%98', '%C9' : '%D0%99', '%CA' : '%D0%9A', '%CB' : '%D0%9B', '%CC' : '%D0%9C', '%CD' : '%D0%9D', '%CE' : '%D0%9E', '%CF' : '%D0%9F', '%D1' : '%D0%A1', '%D2' : '%D0%A2', '%D3' : '%D0%A3', '%D4' : '%D0%A4', '%D5' : '%D0%A5', '%D6' : '%D0%A6', '%D7' : '%D0%A7', '%D8' : '%D0%A8', '%D9' : '%D0%A9', '%DA' : '%D0%AA', '%DB' : '%D0%AB', '%DC' : '%D0%AC', '%DD' : '%D0%AD', '%DE' : '%D0%AE', '%DF' : '%D0%AF', '%E0' : '%D0%B0', '%E1' : '%D0%B1', '%E2' : '%D0%B2', '%E3' : '%D0%B3', '%E4' : '%D0%B4', '%E5' : '%D0%B5', '%B8' : '%D1%91', '%E6' : '%D0%B6', '%E7' : '%D0%B7', '%E8' : '%D0%B8', '%E9' : '%D0%B9', '%EA' : '%D0%BA', '%EB' : '%D0%BB', '%EC' : '%D0%BC', '%ED' : '%D0%BD', '%EE' : '%D0%BE', '%EF' : '%D0%BF', '%F0' : '%D1%80', '%F1' : '%D1%81', '%F2' : '%D1%82', '%F3' : '%D1%83', '%F4' : '%D1%84', '%F5' : '%D1%85', '%F6' : '%D1%86', '%F7' : '%D1%87', '%F8' : '%D1%88', '%F9' : '%D1%89', '%FA' : '%D1%8A', '%FB' : '%D1%8B', '%FC' : '%D1%8C', '%FD' : '%D1%8D', '%FE' : '%D1%8E', '%FF' : '%D1%8F' }
function convert(str) {
// Symbol code in cp1251 (hex) : symbol code in utf8)
return str.replace(
/%.{2}/g,
function($0) {
return map[$0] || $0
})
}
function decode(fn, str) {
// Try/catch block for getting the encoding of the source string.
// Error is thrown if a non-UTF8 string is input.
// If the string was not decoded, it is returned without changes.
try {
return fn(str)
} catch (e1) {
try {
return fn(convert(str))
} catch (e2) {
return str
}
}
}
export default {
/**
* Decodes URI string
* @param {String} str
* @returns {String}
*/
decodeURI(str) {
return decode(decodeURI, str)
},
/**
* Decodes URI component string
* @param {String} str
* @returns {String}
*/
decodeURIComponent(str) {
return decode(decodeURIComponent, str)
}
}
================================================
FILE: common.bundles/index/blocks/square/_color/square_color_green.css
================================================
.square_color_green
{
background-color: green;
}
================================================
FILE: common.bundles/index/blocks/square/square.css
================================================
.square
{
background-color: red;
cursor: hand;
width: 100px;
height: 100px;
}
================================================
FILE: common.bundles/index/blocks/square/square.deps.js
================================================
[{
mustDeps : ['i-bem', 'i-bem-dom'],
shouldDeps : { mods : { color : 'green' } }
},
{
tech : 'js',
mustDeps : { tech : 'bemhtml', block : 'i-bem' }
}]
================================================
FILE: common.bundles/index/blocks/square/square.js
================================================
/**
* The block's BEM declaration can state which block (a block with a modifier or a block
* with a specific modifier value)
* a given JavaScript component refers to.
*
* You can find various declarations on the i-bem block's wiki page, blocks/i-bem/i-bem.wiki
*/
modules.define('square', ['i-bem-dom', 'BEMHTML'], function(provide, bemDom, BEMHTML) {
provide(bemDom.declBlock(this.name, {
_onSquareClick : function() {
this.toggleMod('color', '', 'green');
bemDom.update(this.domElem, BEMHTML.apply({
block : 'test',
content : 'client BEMHTML test'
}));
}
}, {
live : function() {
this._domEvents().on('click', this.prototype._onSquareClick);
}
}));
});
================================================
FILE: common.bundles/index/index.bemjson.js
================================================
({
block : 'page',
title : 'Обработчик события click',
styles : { elem : 'css', url : '_index.css' },
scripts : { elem : 'js', url : '_index.js' },
content : [{
block : 'square',
js : { id : 1 }
},{
block : 'square',
js : { id : 1 }
}]
});
================================================
FILE: common.docs/bemjson/bemjson.en.md
================================================
# BEMJSON reference
## Introduction
**This document** is a guide to the format for describing input data called BEMJSON.
The guide describes:
* BEMJSON's main features distinguishing it from other formats;
* BEMJSON syntax for data description.
**The target audience for this guide** are web developers and HTML coders who use the [BEM methodology](https://en.bem.info/method/).
The reader is assumed to be familiar with:
* HTML
* JavaScript
* CSS
* [BEM](https://en.bem.info/method/)
The description of tools for generating a BEM tree in BEMJSON format is **beyond the scope of this document**.
## Key concepts
To describe web page markup in BEM terms, BEM projects introduce the concept of a **BEM tree**, named by analogy to the DOM tree data structure.
A BEM tree is a data structure that describes:
* web page structure – the order and nesting of the blocks;
* names of BEM entities – the names of the blocks, elements, and their modifiers;
* states of BEM entities – the occurrence of logical modifiers and their values;
* arbitrary fields – custom data (hash keys, public API addresses, etc.)
The standard BEM tree format in the bem-core library (and many other BEM projects) is **BEMJSON**.
BEMJSON is a JavaScript data structure (object) with a set of extra conventions on the representation of BEM entities.
## BEMJSON and data templating in bem-core
A BEMJSON-formatted BEM tree is an integral part of the data templating mechanisms implemented in `bem-core`. BEMJSON is used as an input data format for these template engines:
* [BEMTREE](https://en.bem.info/technology/bemtree/current/bemtree/)
* [BEMHTML](https://en.bem.info/technology/bemhtml/current/intro/)
From a BEMTREE and BEMHTML templates perspective, a portion of input data corresponding to the current BEM tree element (node) and its child elements is contained in the context field `this.ctx`.
**NB** The BEMTREE template engine is used for generating BEMJSON from arbitrary data (the data normally comes in the form of a web page skeleton in BEMJSON format, which gets filled with content element by element as it is processed by the template engine).
## BEMJSON and the build process
Certain build systems, such as [bem-tools](https://en.bem.info/tools/bem/bem-tools/), use files that contain the literal record BEMJSON as a build **declaration**. In `bem-tools`, `bemjson.js`-suffixed files serve this purpose. Based on a BEM tree defined in such files, the build system determines a set of BEM entities whose implementations are to be built from block folders.
In practice, it works like this: first, based on the `bemjson.js` declaration and the build settings, the build tool creates a basic declaration file in `bemdecl.js` format. The latter is then used to build a file in `deps.js` format that describes build dependencies. The dependencies file is a flat list of BEM entities involved in the build, which looks like this:
```js
exports.deps = [
{
"block": "page",
"elem": "css"
},
{
"block": "page",
"elem": "js"
},
{
"block": "page",
"elem": "meta"
},
{
"block": "header"
},
{
"block": "content"
},
{
"block": "footer"
}
];
```
The dependencies file serves as the basis for the subsequent building of tech files from the folders of blocks, elements and modifiers targeted by the declaration. The files are grouped into technology bundles according to their **suffixes**.
The part of a filename that follows the first occurrence of the period is considered a suffix. For example, in the filename `index.bemjson.js`, the suffix is `bemjson.js`.
**See also**:
* [Dependencies in bem-tools](https://en.bem.info/technology/deps/)
* [Building and connecting BEMTREE and BEMHTML technology bundles](https://ru.bem.info/technology/bemhtml/current/templating/#polymorph) (Russian version only)
## BEMJSON syntax
### Data types
Data types in BEMJSON correspond to data types in JavaScript.
* Strings and numbers:
* **String** `` 'a' `` `"a"`;
* **Number** `1` `0.1`;
A data structure consisting of a single string or number is valid BEMJSON.
* **Boolean**. Values: `true`, `false`.
* **Object** (associative array) '{key: value}' and other types except array.
* **Array** – a list; can include elements of different types (strings, numbers, objects, arrays)
`[ "a", 1, {key: value}, [ "b", 2, ... ] ]`.
### BEMJSON special fields
For the BEM domain data and HTML data representation, BEMJSON uses objects with special reserved field names.
#### Representation of BEM entities
BEM entities are represented in BEMJSON as objects that can contain the following fields:
Field
Value
Value type
Example
block
Block name
String
{ block: 'menu' }
elem
Element name
String
{ elem: 'item' }
mods
Block modifiers
Object containing the names and values of block modifiers as key-value pairs:
{modifier_name: 'modifier_value'}
{
block: 'link',
mods: { pseudo: true, color: 'green' }
}
elemMods
Element modifiers
Object containing the names and values of element modifiers as key-value pairs:
{modifier_name: 'modifier_value'}
{
elem: 'item',
elemMods: { selected: 'yes' }
}
mix
Mixed blocks/elements
Array of objects that describe mixed blocks and elements or Object interpreted as an array consisting of a single element.
{
block: 'link',
mix: [ { block: 'serp-item', elem: 'link' } ]
}
**See also**:
* [Context-aided completion of BEM entities](https://ru.bem.info/technology/bemhtml/current/templating/#extensionbem) (Russian version only)
#### HTML representation
BEMJSON supports the ability to specify certain aspects of the resulting HTML directly in the input data. Admittedly, that is not recommended as common practice, considering that BEMJSON essentially describes data, while actual HTML layout is built at the BEMHMTL template engine level. Still there may be situations that warrant the use of HTML representation at BEMJSON level.
The following fields in BEMJSON are used to control HMTL rendering:
Field
Value
Value type
Example
tag
HTML tag for the current entity
String
{
block: 'my-block',
tag: 'img'
}
attrs
HTML attributes for the current entity
Object
{
block: 'my-block',
tag: 'img',
attrs: { src: '//yandex.ru/favicon.ico', alt: '' }
}
cls
Line added to the HTML attribute class (besides automatically generated classes)
String
{
block: 'my-block',
cls: 'some-blah-class'
}
bem
Flag to cancel the generation of BEM classes in the HTML attribute class for the current entity
Boolean
{
block: 'page',
tag: 'html',
bem: false
}
js
Either flag to indicate the presence of client JavaScript in the entity or JavaScript parameters
Boolean|Object
{
block: 'form-input',
mods: { autocomplete: 'yes' },
js: {
dataprovider: { url: 'http://suggest.yandex.ru/...' }
}
}
Note that the names and meanings of these HTML-specific BEMJSON fields are equivalent to those of the corresponding BEMHTML [standard modes](https://en.bem.info/technology/bemhtml/current/reference/#standardmoda) (tags, attributes, classes, etc.) If the same HTML aspects are specified **in both the input data and BEMHTML templates**, the values specified in the BEMHTML templates take priority.
During the HTML generation process, the BEMHTML template engine will perform one of two actions:
* **Merge** the values of the HTML parameters set in the BEMJSON with those specified in the BEMHTML template. Such merging is done only for those parameters where it makes obvious sense: `attrs`, `js`, `mix`.
* **Override** the values of the HTML parameters set in the BEMJSON with those specified in the **BEMHTML template**. This is done for all other values: `tag`, `cls`, `bem`, `content`.
#### Nesting: content
The field `content` is reserved in BEMJSON for the representation of nested BEM entities (BEM tree). The field can take arbitrary BEMJSON as its value:
* A primitive data type (string, number) - the value is used as the content (text) of the HTML element that corresponds to the context entity.
* An object describing a BEM tree - the value is used for generating HTML elements nested inside the HTML element that corresponds to the context entity.
There is no fixed limit on nesting depth for a tree of BEM entities that can be built from the `content` field.
#### Custom fields
In addition to special fields that describe the BEM entity and its HTML representation, an object can contain any fields with custom data. The data will be available for use in BEMHTML and BEMTREE templates.
An example of a custom field is the field `url` in a link block:
```js
{
block: 'link',
url: '//yandex.ru'
}
```
To see how data from a custom field is used, refer to the section [Condition-based template selection](https://en.bem.info/technology/bemhtml/current/reference/#select_template) of the BEMHTML document.
### Arbitrary JavaScript in BEMJSON
As a format, BEMJSON has fewer restrictions than JSON. Arbitrary JavaScript expressions are all valid BEMJSON.
BEMJSON differs from other data formats in its adherence to the above listed naming conventions for fields in objects (in what concerns the representation of BEM entities and HTML) as well as the object nesting rules.
================================================
FILE: common.docs/bemjson/bemjson.ru.md
================================================
# Справочное руководство по BEMJSON
## Введение
**Данный документ** представляет собой справочное руководство по формату описания входных данных BEMJSON.
В документе описаны:
* основные особенности BEMJSON, отличающие его от других форматов;
* синтаксис описания данных BEMJSON;
**Целевая аудитория документа** — веб-разработчики и HTML-верстальщики, использующие
[БЭМ-методологию](https://ru.bem.info/method/).
Предполагается, что читатель знаком с:
* HTML;
* JavaScript;
* CSS;
* БЭМ.
**В документе не описаны** средства гененерации БЭМ-дерева в формате BEMJSON.
## Общие понятия
В БЭМ-проектах для описания разметки веб-страницы в БЭМ-терминах вводится специальное понятие – **БЭМ-дерево**. Название выбрано по аналогии с DOM-деревом.
БЭМ-дерево – структура данных, которая описывает:
* структуру страницы – порядок и вложенность блоков;
* названия БЭМ-сущностей – имена блоков, элементов, модификаторов блока или элемента;
* состояния БЭМ-сущностей – наличие логических модификаторов, значения модификаторов;
* произвольные поля – вспомогательные данные (хеш-ключи, адреса публичных API и т.п.).
В библиотеке `bem-core` (и многих других БЭМ-проектах) стандартным форматом представления БЭМ-дерева является **BEMJSON** .
BEMJSON – структура данных (объект) JavaScript, с набором дополнительных соглашений о представлении БЭМ-сущностей.
## BEMJSON и шаблонизация данных в bem-core
БЭМ-дерево в формате BEMJSON является неотъемлемой частью механизмов [шаблонизации данных](https://ru.bem.info/technology/bemhtml/current/templating/), реализованных в `bem-core`. BEMJSON используется в качестве входных данных для шаблонизаторов:
* [BEMTREE](https://ru.bem.info/technology/bemtree/);
* [BEMHTML](http://ru.bem.info/technology/bemhtml/current/intro/).
В рамках BEMTREE и BEMHTML шаблонов фрагмент входных данных, относящийся к текущему элементу BEMJSON-дерева и его потомкам, содержится в поле контекста `this.ctx`.
**NB** Шаблонизатор BEMTREE предназначен для генерации BEMJSON из произвольных данных.
## BEMJSON и сборка
Некоторые системы сборки, например, [bem-tools](https://ru.bem.info/tools/bem/bem-tools/), использует файлы, содержащие литеральную запись BEMJSON, в качестве **декларации** сборки. В `bem-tools` для этих целей служат файлы с суффиксом `bemjson.js`. На основе БЭМ-дерева, описанного в этих файлах, система сборки определяет набор БЭМ-сущностей, реализации которых должны быть собраны из папок блоков.
На практике это означает, что на основании декларации`bemjson.js` и настроек сборки строится файл базовой декларации в формате `bemdecl.js`. Затем из него - файл в формате `deps.js`, описывающий зависимости сборки. Файл зависимостей представляет собой плоский список БЭМ-сущностей, участвующих в сборке, вида:
```js
exports.deps = [
{
"block": "page",
"elem": "css"
},
{
"block": "page",
"elem": "js"
},
{
"block": "page",
"elem": "meta"
},
{
"block": "header"
},
{
"block": "content"
},
{
"block": "footer"
}
];
```
На основании файла зависимостей производится дальнейшая сборка файлов технологий из папок блоков, элементов и модификаторов, попадающих под декларацию. Файлы собираются в бандлы технологий на основании **суффиксов**.
Суффиксом считается часть имени файла следующая за первой точкой. Например, в имени файла `index.bemjson.js` суффиксом является `bemjson.js`.
**См. также**:
* [Зависимости в bem-tools](https://ru.bem.info/tools/bem/bem-tools/depsjs/);
* [Сборка и подключение бандла технологий BEMTREE и BEMHTML](https://ru.bem.info/technology/bemhtml/current/templating/#polymorph)
## Синтаксис BEMJSON
### Типы данных
Типы данных в BEMJSON соответствуют типам данных в JavaScript.
* Строки и числа:
* **Строка** `` 'a' `` `"a"`;
* **Число** `1` `0.1`;
Структура данных, состоящая из строки или числа, является валидным BEMJSON.
* **Boolean**. Значения: `true`, `false`.
* **Объект** (ассоциативный массив) '{ключ: значение}' и остальные типы, кроме массива.
* **Массив** — список, может содержать элементы различных типов (строки, числа, объекты, массивы)
`[ "a", 1, {ключ: значение}, [ "b", 2, ... ] ]`.
### Специальные поля BEMJSON
Для представления данных предметной области БЭМ и HTML в BEMJSON используются объекты, в которых зарезервированы
специальные имена полей.
#### Представление БЭМ-сущностей
БЭМ-сущности представляются в BEMJSON в виде объектов, в которых могут присутствовать следующие поля:
Поле
Значение
Тип значения
Пример
block
Имя блока
Строка
{ block: 'menu' }
elem
Имя элемента
Строка
{ elem: 'item' }
mods
Модификаторы блока
Объект, содержащий имена и значения модификаторов в качестве пар ключ-значение:
{имя_модификатора: 'значение_модификатора'}
{
block: 'link',
mods: { pseudo: true, color: 'green' }
}
elemMods
Модификаторы элемента
Объект, содержащий имена и значения модификаторов элемента в качестве пар ключ-значение:
{имя_модификатора: 'значение_модификатора'}
{
elem: 'item',
elemMods: { selected: 'yes' }
}
mix
Подмешанные блоки/элементы
Массив, содержащий объекты, описывающие подмешанные блоки и элементы. В качестве значения может выступать
объект, который трактуется как массив, состоящий из одного элемента.
{
block: 'link',
mix: [ { block: 'serp-item', elem: 'link' } ]
}
**См. также**:
* [Достраивание БЭМ-сущностей по контексту](https://ru.bem.info/technology/bemhtml/current/templating/#extensionbem)
#### Представление HTML
BEMJSON предоставляет возможность задавать некоторые аспекты выходного HTML непосредственно во входных данных.
Этой возможностью не следует злоупотреблять, так как BEMJSON представляет собой уровень данных, а непосредственное
оформление HTML должно выполняться на уровне шаблонизатора BEMHTML. Однако возможны ситуации, когда оправданно
описание HTML-представления на уровне BEMJSON.
В BEMJSON предусмотрены следующие поля для непосредственного управления HTML-представлением:
Поле
Значение
Тип значения
Пример
tag
HTML-тег для данной сущности
String
{
block: 'my-block',
tag: 'img'
}
attrs
HTML-атрибуты для данной сущности
Object
{
block: 'my-block',
tag: 'img',
attrs: { src: '//yandex.ru/favicon.ico', alt: '' }
}
cls
Строка, добавляемая к HTML-атрибуту class (помимо автоматически генерируемых классов)
String
{
block: 'my-block',
cls: 'some-blah-class'
}
bem
Флаг — отменить генерацию БЭМ-классов в HTML-атрибуте class для данной сущности
Boolean
{
block: 'page',
tag: 'html',
bem: false
}
js
Либо флаг о наличии клиентского JavaScript у данной сущности, либо параметры JavaScript
Boolean|Object
{
block: 'form-input',
mods: { autocomplete: 'yes' },
js: {
dataprovider: { url: 'http://suggest.yandex.ru/...' }
}
}
Обратите внимание, что имена и смысл полей BEMJSON, управляющих HTML-представлением, совпадают с именами и смыслом соответствующих [стандартных мод](https://ru.bem.info/technology/bemhtml/current/reference/#standardmoda) BEMHTML (тег, атрибуты, класс и т.п.). В случае, если какие-то из аспектов выходного HTML заданы **и во входных данных, и в BEMHTML-шаблонах**, более высокий приоритет имеют значения, заданные в BEMHTML-шаблонах.
При генерации HTML будет выполнено одно из двух действий:
* **Объединение** значений HTML-параметров, заданных в BEMJSON, cо значениями параметров, заданных в BEMHTML-шаблоне. Объединение значений производится только для тех параметров, для которых оно имеет очевидный смысл: `attrs`, `js`, `mix`.
* **Замещение** значений HTML-параметров, заданных в BEMJSON, значениями, заданными в **BEMHTML-шаблоне**. Выполняется для всех прочих значений: `tag`, `cls`, `bem`, `content`.
**NB:** Приоритет BEMHTML-шаблонов позволяет **автору шаблонов** принимать решение, какие HTML-параметры будут приоритетнее в каждом конкретном случае: заданные в BEMHTML или в BEMJSON. Значения HTML-параметров, заданных в BEMJSON, доступны в шаблонах при обращении к фрагменту входного BEMJSON-дерева в контексте (поле `this.ctx`).
#### Вложенность: content
Для представления вложенных БЭМ-сущностей (БЭМ-дерева) в BEMJSON зарезервировано поле `content`. В качестве значения
данного поля может выступать произвольный BEMJSON:
* Примитивный тип (строка, число). Значение используется в качестве содержимого (текста) HTML-элемента, соответствующего
контекстной сущности.
* Объект, описывающий БЭМ-дерево. Значение используется для генерации HTML-элементов, вложенных в HTML-элемент,
соответствующий контекстной сущности.
Уровень вложенности дерева БЭМ-сущностей, построенного с помощью поля `content`, не ограничен.
#### Произвольные поля
Помимо специальных полей, описывающих БЭМ-сущность и ее HTML-представление, в том же объекте могут присутствовать
любые поля с произвольными данными, которые будут доступны для использования в шаблонах BEMHTML или BEMTREE.
Примером произвольного поля может служить поле `url` в блоке ссылки:
```js
{
block: 'link',
url: '//yandex.ru'
}
```
Пример использования данных из произвольного поля см. в разделе [Выбор шаблона по условию](https://ru.bem.info/technology/bemhtml/current/reference/#select_template) из документации по BEMHTML.
### Произвольный JavaScript в BEMJSON
BEMJSON является менее ограниченным форматом, чем JSON. Произвольные JavaScript-выражения будут валидным BEMJSON.
Специфика BEMJSON как формата данных заключается в соблюдении описанных в предшествующих разделах соглашений
по именованию полей в объектах (для представления БЭМ-сущностей и HTML-представления) и правил вложения объектов.
================================================
FILE: common.docs/i-bem-js/i-bem-js-collections.en.md
================================================
# Collections of blocks and elements
================================================
FILE: common.docs/i-bem-js/i-bem-js-collections.ru.md
================================================
## Коллекции блоков и элементов
Общее описание
Способы получения
API
Для удобства работы одновременно с несколькими экземплярами блоков или элементов существует специальный класс **коллекции**, реализованный в элементах `collection` блоков `i-bem` и `i-bem-dom`.
### Способы получения коллекции
#### Создание экземпляра класса коллекции
Экземпляр класса коллекции создается базовыми средствами JavaScript,
с помощью класса `BemCollection` модуля `i-bem__collection` или `BemDomCollection` модуля `i-bem-dom__collection`.
Конструктор обоих классов принимает один аргумент в виде массива, либо несколько аргументов:
* `entities` `{Array|...i-bem:Entity|...i-bem-dom:Entity}` — массив или несколько экземпляров БЭМ-сущностей.
**Пример**
```js
modules.define(
'my-form',
['i-bem-dom', 'i-bem-dom__collection', 'button', 'input'],
function(provide, bemDom, BemDomCollection, Button, Input) {
provide(bemDom.declBlock(this.name, {
onSetMod : {
'js' : {
'inited' : function() {
var button = this.findChildBlock(Button),
input = this.findChildBlock(Input);
this._controls = new BemDomCollection(button, input);
}
}
}
}));
});
```
#### Методы поиска
[Методы поиска](./i-bem-js-dom.ru.md#Поиск-экземпляров-блоков-и-элементов-в-dom-дереве), способные найти несколько экземпляров блоков или элементов, возвращают коллекцию, состоящую из найденных экземпляров.
**Пример**
```js
modules.define('my-form', ['i-bem-dom', 'input'], function(provide, bemDom, Input) {
provide(bemDom.declBlock(this.name, {
onSetMod : {
'js' : {
'inited' : function() {
this._inputs = this.findChildBlocks(Input);
}
}
}
}));
});
```
### Методы коллекции
* `setMod(modName, [modVal=true])`, `delMod(modName)`, `toggleMod(modName, modVal1, [modVal2], [condition])` — соответсвуют одноименным методам [управления модификаторами](./i-bem-js-states.ru.md#Управление-модификаторами) экземпляра блока и элемента.
* `everyHasMod(modName, [modVal])`, `someHasMod(modName, [modVal])` — применяют метод `hasMod(modName, modVal)` для каждой сущности коллекции. Возвращает `true`, если все вызовы вернули `true` и если хотя бы один вызов вернул `true`, соответственно.
* `get(i)` — возвращает элемент коллекции по индексу i.
* `size()` — возвращает размер коллекции.
* `forEach(fn, ctx)`, `map(fn, ctx)`, `reduce(fn, ctx)`, `reduceRight(fn, ctx)`, `filter(fn, ctx)`, `some(fn, ctx)`, `every(fn, ctx)`, `has(entity)`, `find(fn, ctx)`, `concat(...args)` — соответствует одноименным методам [объекта Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array).
* `toArray()` — преобразовывает коллекцию в массив экземпляров блоков и элементов.
**Пример**
```js
modules.define('my-form', ['i-bem-dom', 'input'], function(provide, bemDom, Input) {
provide(bemDom.declBlock(this.name, {
onSetMod : {
'js' : {
'inited' : function() {
this._inputs = this.findChildBlocks(Input);
}
},
'disabled' : function(modName, modVal) {
this._inputs.setMod(modName, modVal);
}
},
getValue : function() {
return this._inputs
.filter(function(input) {
return !input.hasMod('disabled');
})
.map(function(input) {
return input.getValue();
});
}
}));
});
```
================================================
FILE: common.docs/i-bem-js/i-bem-js-common.en.md
================================================
## Overview
### The BEM methodology and JavaScript
In the BEM methodology, a web interface is built from independent
**blocks**, which may have **elements**. Both blocks
and elements can have states or characteristics described by **modifiers**.
A web interface uses various **technologies**
(HTML, CSS, JS, and others). Its implementation is divided into components by block. A block contains a set of **technology files** that represent aspects of its implementation:
* `my-block.css` — The block appearance.
* `my-block.bemhtml` — Templates for generating HTML representations of the block.
* `my-block.js` — The block **dynamic behavior** in the browser.
The `i-bem.js` framework allows us to break down the client JavaScript into components in BEM terms:
* **Block** — The JS component that describes the logic of same-type interface elements. For example, all buttons can be implemented as a `button` block. In this case, `button.css` defines how all buttons look, and `button.js` defines how they work.
Each page can have more than one **block instance** (such as buttons). Each block instance corresponds to a JS object in the browser memory that stores its state. The JS object contains a reference to the DOM node that this block instance is bound to.
* **Elements** — DOM nodes nested in the block DOM node, with the `class` attribute pointing to their role in the BEM subject domain (the name of the block and element). Block elements are accessible via the block instance [JS-API] [dom].
* **Modifiers** — Provide information about the state of a block and its elements. The state of modifiers is written in the `class` attribute on the DOM nodes of a block and elements. Modifiers are controlled using a block instance [JS-API](i-bem-js-states.en.md#js-api).
### Assembly
In the BEM methodology, development is modular — each block
is programmed separately. The final source code of web pages is generated
from the code of individual blocks using **assembly** procedures.
In the file system, it is convenient to represent a block as a directory, and the block implementation in each of the technologies as a separate file:
```html
desktop.blocks/
my-block/
my-block.css
my-block.js
my-block.bemhtml
...
desktop.blocks/
other-block/
other-block.css
other-block.js
other-block.bemhtml
...
```
For each web page, the code of the blocks used on it can be put in the same types of files:
```html
desktop.bundles/
index/
index.html
index.css
index.js
...
```
There are two tools that support the BEM subject domain for assembling separate block descriptions into the code of resulting web pages:
* [bem-tools](https://en.bem.info/tools/bem/)
* [ENB](https://en.bem.info/tools/bem/enb-bem/)
Both tools automate the creation of HTML markup for [binding JS blocks](./i-bem-js-html-binding.en.md) and [passing parameters to a block instance](./i-bem-js-params.en.md).
### Why i-bem.js is named this way
According to the BEM methodology, the base JS library of the BEM platform was originally developed
as a special service block. This approach allows us to work with base libraries the same way as with
regular blocks. In particular, it allows us to structure code in terms of elements and modifiers and flexibly
configure the library behavior on various redefinition levels.
Service blocks in BEM were conventionally given names with the `i-` prefix. Thus, the name `i-bem.js`
is read as *an implementation of the `i-bem` block in the `JS` technology*.
### How to use i-bem.js
The `i-bem.js` framework is a part of the [bem-core](https://en.bem.info/libs/bem-core/) library.
The implementation of `i-bem.js` consists of two modules:
* **The [i-bem](https://en.bem.info/libs/bem-core/current/desktop/i-bem/jsdoc/) module**. Base implementation of the `i-bem` JS block, which all the blocks in `i-bem.js` inherit from. The `i-bem` block is written for use in any of the JS environments: both on the client and on the server (for example, in Node.js).
* **The [i-bem__dom](https://en.bem.info/libs/bem-core/current/desktop/i-bem/jsdoc/) module**. The base implementation of a block bound to a DOM node. Intended for use on the client, and relies on browsers working with DOM. Depends on `jQuery`.
Dependencies:
* jQuery (only for the `i-bem__dom` module). When using `bem-core`, separate installation of jQuery is not necessary.
* The [ym/modules](https://github.com/ymaps/modules) module system. When using [bem-tools](https://en.bem.info/tools/bem/) with `.browser.js` technology (and derivatives of it), this dependency is satisfied automatically.
You can use `i-bem.js` as a part of the full stack
of BEM tools. In this case, it is convenient to base your project on the
[project-stub](https://en.bem.info/tutorials/project-stub/) template repository, where automatic installation of dependent libraries and assembly is set up.
If you aren't planning to use other technologies of the BEM platform, you can just put the `bem-core` library code in an existing project.
================================================
FILE: common.docs/i-bem-js/i-bem-js-common.ru.md
================================================
## Общие сведения
### БЭМ-методология и JavaScript
В БЭМ-методологии веб-интерфейс строится из независимых **блоков** у которых могут быть **элементы**. И блоки, и элементы могут иметь состояния или особенности, описываемые **модификаторами**.
Работа веб-интерфейса обеспечивается несколькими **технологиями** (HTML, CSS, JS и т.д.). Его реализация разбита на компоненты по блокам. Блок содержит набор **файлов технологий**, составляющих аспекты его реализации:
* `my-block.css` — внешний вид блока;
* `my-block.bemhtml` — шаблоны для генерации HTML-представления блока;
* `my-block.js` — **динамическое поведение** блока в браузере.
Фреймворк `i-bem.js` позволяет разложить клиентский JavaScript на компоненты в терминах БЭМ:
* **Блок** — JS-компонент, описывающий логику работы однотипных элементов интерфейса. Например, все кнопки могут быть реализованы в виде блока `button`. В этом случае, `button.css` определяет внешний вид всех кнопок, а `button.js` — логику их работы. На каждой странице может размещаться более одного **экземпляра блока** (например, кнопки). Каждому экземпляру блока соответствует JS-объект, в памяти браузера, хранящий его состояние. JS-объект содержит ссылку на DOM-узел, к которому привязан данный экземпляр блока.
* **Элементы** — DOM-узлы, вложенные в DOM-узел блока, с атрибутом `class`, указывающим на их роль в БЭМ-предметной области (имя блока и элемента). Элементы блока доступны через [JS-API](./i-bem-js-states.ru.md#Управление-модификаторами) экземпляра блока.
* **Модификаторы** — предоставляют информацию о состоянии блока и его элементов. Состояние модификаторов записывается в атрибуте `class` на DOM-узлах блока и элементов. Управление модификаторами производится через [JS-API](./i-bem-js-states.ru.md#Управление-модификаторами) экземпляра блока.
### Сборка
Разработка в рамках БЭМ-методологии ведется модульно — каждый блок программируется отдельно. Финальный исходный код веб-страниц формируется из кода отдельных блоков с помощью процедур **сборки**.
В файловой системе блок удобно представлять в виде каталога, а реализацию блока в каждой из технологий — в виде отдельного файла:
```html
desktop.blocks/
my-block/
my-block.css
my-block.js
my-block.bemhtml
...
desktop.blocks/
other-block/
other-block.css
other-block.js
other-block.bemhtml
...
```
Для каждой веб-страницы код использованных на ней блоков может быть собран в единые файлы:
```html
desktop.bundles/
index/
index.html
index.css
index.js
...
```
Существует два инструмента, поддерживающих БЭМ-предметную область, для сборки кода результирующих веб-страниц из отдельных описаний блоков:
* [bem-tools](https://ru.bem.info/toolbox/bem-tools/)
* [ENB](https://ru.bem.info/toolbox/enb/)
Оба инструмента позволяют автоматизировать создание HTML-разметки для [привязки JS-блоков](./i-bem-js-html-binding.ru.md#Привязка-js-блоков-к-html) и [передачи параметров экземпляру блока](./i-bem-js-params.ru.md#Передача-параметров-экземпляру-блока-и-элемента).
### Почему i-bem.js так называется
В соответствии с БЭМ-методологией, базовая JS-библиотека БЭМ-платформы изначально разрабатывалась как особый служебный блок. Такой подход позволяет работать с базовыми библиотеками так же, как и с обычными блоками. В частности, структурировать код в терминах элементов и модификаторов и гибко настраивать поведение библиотеки на разных уровнях переопределения.
Служебным блокам в БЭМ было принято давать имена с префиксом `i-`. Таким образом, имя `i-bem.js` читается как *реализация блока `i-bem` в технологии `JS`*.
### Как использовать i-bem.js
Фреймворк `i-bem.js` входит в состав библиотеки [bem-core](https://ru.bem.info/platform/libs/bem-core/).
Реализация `i-bem.js` состоит из двух модулей:
* **Модуль [i-bem](https://ru.bem.info/platform/libs/bem-core/)**.
Предоставляет базовую реализацию JS-блока `i-bem`, от которой наследуются все блоки и элементы в `i-bem.js`. Блок `i-bem` написан с расчетом на использование в любом JS-окружении: как на клиенте, так и на сервере (например, в Node.js).
* **Модуль [i-bem-dom](https://ru.bem.info/platform/libs/bem-core/)**.
Предоставляет базовую реализацию блока и элемента, привязанных к DOM-узлу. Рассчитан на использование на клиенте, опирается на работу браузеров с DOM. Зависит от `jQuery`.
Зависимости:
* jQuery (только для модуля `i-bem-dom`). При использовании `bem-core` отдельная установка jQuery не требуется.
* Модульная система [ym/modules](https://github.com/ymaps/modules). При использовании [ENB](https://ru.bem.info/toolbox/enb/) с технологией `.browser.js` (и производных от нее) эта зависимость удовлетворяется автоматически.
Можно использовать `i-bem.js` как часть полного стека БЭМ-инструментов. В этом случае свой проект удобно создавать на основе шаблонного репозитория [project-stub](https://ru.bem.info/platform/project-stub/), в котором настроена автоматическая установка зависимых библиотек и сборка.
Если не планируется использование других технологий БЭМ-платформы, достаточно поместить код библиотеки `bem-core` в существующий проект.
================================================
FILE: common.docs/i-bem-js/i-bem-js-context.en.md
================================================
Context
-------
**A block instance methods** are executed in the context of the block instance JS object. The keyword `this` in the block instance methods references the JS object of the **block instance**.
**Static methods** are executed in the context of the JS object that corresponds to the block class. The keyword `this` in a block static methods references the **block class**.
> **Note** When developing blocks using `i-bem.js` in internal block methods that are not intended for use outside the block, it is customary to assign names that start with an underscore. For example, `_onClick`.
### Properties of a block instance
#### With DOM representation
* `params` is a hash of parameters passed to the block instance during initialization.
* `domElem` is a jQuery object containing references to DOM elements that the block is [bound](./i-bem-js-html-binding.en.md) to.
#### Without DOM representation
* `params` is a hash of parameters passed to the block instance during initialization.
#### Helper properties
A block instance provides a set of helper properties:
* `__self` — For access to static properties and methods of the block and its instance.
**Example**
Calling `staticMethod` in the `onEvent` method of the `my-block` block instance.
```js
BEMDOM.decl('my-block', {
onEvent : function() {
this.__self.staticMethod(); // calling a static method
this.doMore();
}
}, {
staticMethod : function() { /* ... */ }; // defining a static method
});
```
* `__base` – For calling the implementation of the method with the same name from the base class that this one inherits from (`super call`).
**Example**
Calling the base implementation of the `_onClick` method of the `button` base class.
```js
BEMDOM.decl({ block : 'my-button', baseBlock : 'button' }, {
_onClick : function() {
this.__base(); // calling the base _onClick
this.doMore();
}
});
```
Helper properties are provided by the [inherit](../../common.blocks/inherit) module, which implements the inheritance mechanism in `bem-core`.
### Static block properties
#### Helper properties
Helper properties are available in the declaration of a block static methods:
* `__base` – For calling the implementation of the method with the same name from the base class that this one inherits from (`super call`).
```js
BEMDOM.decl({ block : 'extra', baseBlock : 'my-block' },
{ /* ... */ },
{
staticMethod: function() {
this.__base();
this.doMore();
}
}
);
```
### Static properties of the BEMDOM module
* `scope` — The root element of the DOM tree being processed. Allows executing several different versios of `i-bem.js` in the same runtime. By default, contains a reference to the `body` jQuery object.
* `doc` — A reference to the `document` jQuery object.
* `win` — A reference to the `window` jQuery object.
================================================
FILE: common.docs/i-bem-js/i-bem-js-context.ru.md
================================================
## Контекст
**Методы экземпляра** исполняются в контексте JS-объекта экземпляра блока и элемента. Ключевое слово `this` в методах экземпляра блока ссылается на JS-объект **экземпляра**.
**Статические методы** исполняются в контексте JS-объекта, соответствующего классу блока или элемента. Ключевое слово `this` в статических методах блока ссылается на **класс**.
> **Примечание** При разработке с использованием `i-bem.js` внутренним методам блока и элемента, не предназначенным для использования извне, принято давать имена, начинающиеся с символа подчеркивания. Например, `_onClick`.
### Свойства и методы экземпляра блока и элемента
#### С DOM-представлением
* `params` – [хеш параметров](./i-bem-js-params.ru.md), соответсвующий параметрам заданных в HTML-экземпляра.
* `domElem` – объект jQuery, содержащий ссылки на DOM-элементы, к которым [привязан](./i-bem-js-html-binding.ru.md#Привязка-js-блоков-к-html) экземпляр.
#### Без DOM-представления
* `params` – хеш параметров, переданных экземпляру при инициализации.
#### Вспомогательные свойства
Экземпляр предоставляет набор вспомогательных свойств:
* `__self` – для доступа к статическим свойствам и методам из экземпляра.
**Пример**
Вызов статического метода `staticMethod()` в методе `_onEvent()` экземпляра блока `my-block`.
```js
bemDom.declBlock('my-block', {
_onEvent : function() {
this.__self.staticMethod(); // вызов статического метода
this.doMore();
}
}, {
staticMethod : function() { /* ... */ } // определение статического метода
});
```
* `__base` – для вызова реализации одноименного метода из базового класса, от которого [наследуется](./i-bem-js-decl.ru.md#Наследование) данный («super call»).
**Пример**
Вызов базовой реализации метода `_onClick` базового класса `button`.
```js
modules.define('my-button', ['i-bem-dom', 'button'], function(provide, bemDom, Button) {
provide(bemDom.declBlock(this.name, Button, {
_onClick : function() {
this.__base(); // вызываем базовый _onClick
this.doMore();
}
}));
});
```
Вспомогательные свойства предоставляются модулем [inherit](../../common.blocks/inherit/inherit.ru.md), реализующим механизм наследования в `bem-core`.
#### Вспомогательные методы
* `_nextTick(fn)` — производит асинхронный вызов функции `fn` `{Function}`, в следующем витке событийного цикла. Функция `fn` вызывается в контексте текущего экземпляра, при условии, что он ещё существует.
**Пример**
Подписываемся на событие `pointerclick` на документе в следующем витке событийного цикла, чтобы попап не закрывался (по клику во вне) до того, как обработается клик на нём.
```js
modules.define('popup', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.decl(this.name, {
onSetMod : {
'visible' : {
'true' : function() {
this._nextTick(function() {
this._domEvents(bemDom.doc).on('pointerclick', this._onDocPointerClick);
});
}
}
},
_onDocPointerClick : function() {
// ...
this.delMod('visible');
}
}));
});
```
### Статические свойства блока и элемента
#### Вспомогательные свойства
В декларации статических методов доступны вспомогательные свойства:
* `__base` – для вызова реализации одноименного метода из базового класса, от которого наследуется данный («super call»).
```js
modules.define('my-button', ['i-bem-dom', 'button'], function(provide, bemDom, Button) {
provide(bemDom.declBlock(this.name, Button,
{ /* ... */ },
{
staticMethod: function() {
this.__base();
this.doMore();
}
}
));
});
```
### Статические свойства модуля `i-bem-dom`
* `scope` – корневой элемент обрабатываемого DOM-дерева. Позволяет выполнять несколько разных версий `i-bem.js` в одном рантайме. По умолчанию содержит ссылку на jQuery объект `body`;
* `doc` – ссылка на jQuery объект `document`;
* `win` – ссылка на jQuery объект `window`.
================================================
FILE: common.docs/i-bem-js/i-bem-js-decl.en.md
================================================
Block declaration
-----------------
A block JS implementation defines the behavior of a specific class of web interface elements. In the actual interfaces, each block can be represented by multiple instances.
A block instance implements the functionality of its class and has its own independent state.
In **object-oriented programming** terms:
* A block is a class
* And a block instance is a class instance
In accordance with OOP, all the functionality of a block is implemented modularly in the methods of the class *(=block)*.
The block methods are divided into:
* Block instance methods
* Static methods
The code of a block in `i-bem.js` is called a **declaration** to emphasize the declarative programming style
adopted in BEM.
A block behavior is programmed in declarative style as statements: `set of conditions` — `block reaction`.
### Declaration syntax
#### Blocks with DOM representation
##### Declaring a new block without a parent
To declare a new JS block **with a DOM representation** (bound to an HTML element), use the `decl` method of the [ym](https://github.com/ymaps/modules) module in `i-bem__dom`.
The `decl` method accepts the following arguments:
1. A block description as `{String}` or `{Object}`.
2. Methods of the block instance — `{Object}`.
3. Static methods — `{Object}`.
The declared methods will be applied to all instances of the block, regardless of their states (modifiers).
**Example**
Declaration of methods for the `button` block.
```js
modules.define('button', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name,
{
/* instance's methods */
},
{
/* static methods */
})
);
});
```
The `this.name` field of the `ym` context is passed to the `BEMDOM.decl` method as the first argument. It contains a reference to the name of the block specified as the first argument of `modules.define`.
#### Blocks without DOM representation
For declaring blocks without DOM representation, use the `decl` method of the [ym](https://github.com/ymaps/modules) module in `i-bem`.
The method accepts the same parameters as the `decl` method of the `i-bem__dom` module:
```js
modules.define('my-block', ['i-bem'], function(provide, BEM) {
provide(BEM.decl(this.name,
{
/* instance's methods */
},
{
/* static methods */
})
);
});
```
> **Note** It is convenient to format infrastructure code as a block without DOM representation if you are planning to use BEM block APIs in it (states expressed as modifiers, BEM events, and so on). If you are not planning to use the BEM subject domain, you can format infrastructure code as a [ym](https://github.com/ymaps/modules) module.
>
> **Example**
>
> ```js
> modules.define('router', function(provide) {
>
> provide({
> route : function() { /* ... */ }
> });
>
> });
> ```
### Block inheritance
Various blocks in a project often use identical functionality.
For example, several blocks might use AJAX to request data from the backend,
perform the same operations with the DOM tree, and so on. To avoid unnecessary repetitions in the code, the shared functionality can be encapsulated as modules, then added to blocks.
Inheritance allows reusing block functionality by extending it with new logic.
Several inheritance mechanisms are available in `i-bem.js`. The choice of a particular mechanism depends on the needs of the block being created.
#### Simple inheritance
With simple inheritance, the block being created is declared as a descendant of an existing one. To do this:
1. Specify the base block in the module system dependencies.
2. Pass a reference to the base block in the special `baseBlock` field in the declaration.
For example, the `bblock` block inherits from the `ablock` block:
```js
modules.define('ablock', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {}));
});
modules.define('bblock', ['i-bem__dom', 'ablock'], function(provide, BEMDOM, ABlock) {
provide(BEMDOM.decl({ block : this.name, baseBlock : ABlock }));
});
```
This mechanism allows using the methods of the base block inside a derived block.
To call base block methods of the same name, use the [helper property](i-bem-js-context.en.md#helper-property) `this.__base`.
> **Note** You can create inheritance chains in `i-bem`, meaning that a block inherits from another one that, in turn, inherits from a third block, and so on.
#### Redefining a block
To create a variation of an existing block that alters or supplements its functionality, you can **redefine** a base block on the project *redefinition level*.
In the project, create a declaration of a new block with the same name as the base block. As a result, the block will have access to all the base block functionality. However, the implementation of methods and modifiers with the same name will be taken from the new declaration.
```js
modules.define('ablock', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {})); // Declaring the base block
});
modules.define('ablock', function(provide, ABlock) {
provide(ABlock.decl({})); // Redefining the base block
});
```
This type of inheritance is often used when working with library blocks.
##### Adding a modifier to a block
According to the BEM methodology, a block states must be defined by [modifers](i-bem-js-states.en.md#modifers).
So in order to extend a block functionality, you often need to implement support for new modifiers.
To add a modifier, pass the redefined block `decl` method:
* A hash with the `modName` and `modVal` keys. The `modName` value is a string with the modifier name. The `modVal` value is a string with the modifier value.
* A hash of methods that will be available for the block with the corresponding modifier. If there are methods and modifiers of the same name, their implementation from the hash is used.
```js
modules.define('ablock', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {})); // Declaring the base block
});
modules.define('ablock', function(provide, ABlock) {
provide(ABlock.decl({ modName : 'm1', modVal : 'v1' }, {})); // Redefining the base block with the modifier _m1_v1
});
```
> **Note** The block [static methods](./i-bem-js-context.en.md) will be available to all its instances, *regardless of modifier values*. Modifiers are properties of the block instance, but static methods belong to the block class and do not take the status of modifiers into account.
#### Mixed blocks
In `i-bem.js`, a special type of block is used for adding needed
functionality to blocks — **mixed blocks**. The main feature of mixed blocks is that they do not participate in the inheritance chain. This means their functionality can be combined with other blocks, without risk of breaking their [relationships with parent blocks](i-bem-js-context.en.md#relationships-with-parent-blocks) (`this.__base`).
##### Adding mixed blocks
To add one or more mixed blocks to a block, assign a value to the optional `baseMix` field in the block declaration. The value is an array of strings — the names of mixed blocks to add in:
```js
modules.define('my-block', ['i-bem__dom', 'foo', 'bar'], function(provide, BEMDOM) {
provide(BEMDOM.decl({ block : this.name, baseMix : ['foo', 'bar']},
{ /* instance's methods */ },
{ /* static methods */ }
}));
});
```
##### Mixed block declaration
Only blocks created using `declMix` can be used as mixed blocks.
The method accepts the block declaration in the same format as for the `decl` method.
```js
modules.define('mymix', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.declMix('mymix', //only a string with the name
{ /* instance's methods */ },
{ /* static methods */ }
}));
});
```
> **Note** You can't instantiate a mixed block and use it as an independent block.
#### Trigger declaration
[Triggers](./i-bem-js-states.en.md) that are executed when setting modifiers are described in the block declaration. The following properties are reserved for this purpose in the hash of the block instance methods:
* `beforeSetMod` — Triggers called before setting **block modifiers**.
* `beforeElemSetMod` — Triggers called before setting **element modifiers**.
* `onSetMod` — Triggers called after setting **block modifiers**.
* `onElemSetMod` — Triggers called after setting block **element modifiers**.
```js
modules.define('block-name', function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name,
{
/* instance's methods */
beforeSetMod: { /* triggers before setting block modifiers*/}
beforeElemSetMod: { /* triggers before setting element modifiers*/}
onSetMod: { /* triggers after setting block modifiers */ }
onElemSetMod: { /* triggers after setting element modifiers */ }
},
{
/* static methods */
}
));
});
```
The value of the `beforeSetMod` and `onSetMod` properties is a hash associating changes to modifiers with triggers. A trigger receives the following arguments:
* `modName` – The modifier name.
* `modVal` – The value of the modifier being set.
* `prevModVal` – The previous modifier value. For `beforeSetMod`, this is the current value of the modifier, which will be changed to `modVal` if the trigger doesn't return `false`.
```js
{
'mod1': function(modName, modVal, prevModVal) { /* ... */ }, // setting mod1 to any value
'mod2': {
'val1': function(modName, modVal, prevModVal) { /* ... */ }, // trigger to set mod2 to the value val1
'val2': function(modName, modVal, prevModVal) { /* ... */ }, // trigger to set mod2 to the value val2
'': function(modName, modVal, prevModVal) { /* ... */ } // trigger to delete the mod2 modifier
}
'mod3': {
'true': function(modName, modVal, prevModVal) { /* ... */ }, // trigger to set the simple modifier mod3
'': function(modName, modVal, prevModVal) { /* ... */ }, // trigger to delete the simple modifier mod3
},
'*': function(modName, modVal, prevModVal) { /* ... */ } // trigger to set any modifier to any value
}
```
The shorthand for a trigger to set any block modifier to any value is:
```js
beforeSetMod: function(modName, modVal, prevModVal) { /* ... */ }
onSetMod: function(modName, modVal, prevModVal) { /* ... */ }
```
Triggers to set **element modifiers** are described in the `beforeElemSetMod` and `onElemSetMod` properties. The hash in the property values has an extra nesting level — the **element name**.
The trigger is passed the following as arguments:
* `elem` — The element name.
* `modName` – The modifier name.
* `modVal` – The value of the modifier being set.
* `prevModVal` – The previous modifier value. For `beforeSetMod`, this is the current value of the modifier, which will be changed to `modVal` if the trigger doesn't return `false`.
```js
{
'elem1': {
'mod1': function(elem, modName, modVal, prevModVal) { /* ... */ }, // trigger to set mod1 of elem 1 to any value
'mod2': {
'val1': function(elem, modName, modVal, prevModVal) { /* ... */ }, // trigger to set mod2 of elem1 to val1
'val2': function(elem, modName, modVal, prevModVal) { /* ... */ } // trigger to set mod2 of elem1 to val2
}
},
'elem2': function(elem, modName, modVal, prevModVal) { /* ... */ } // trigger to set any modifier of elem2 to any value
}
```
Shorthand for a trigger to set any modifier of the `elem` element to any value:
```js
beforeElemSetMod: { 'elem1': function(elem, modName, modVal, prevModVal) { /* ... */ } }
onElemSetMod: { 'elem1': function(elem, modName, modVal, prevModVal) { /* ... */ } }
```
================================================
FILE: common.docs/i-bem-js/i-bem-js-decl.ru.md
================================================
## Декларация блоков и элементов
JS-реализация блока или элемента описывает поведение определенных частей веб-интерфейса. В конкретных интерфейсах каждый блок или элемент может быть представлен несколькими экземплярами. Экземпляр реализует функциональность своего класса и имеет собственное, независимое состояние.
В терминах парадигмы **объектно-ориентированного программирования**:
* блок и элемент — классы;
* экземпляры блока и элемента — экземпляры классов.
В соответствии с ООП, вся функциональность блока и элемента реализуется модульно в методах класса.
Методы блока и элемента подразделяются на:
* методы экземпляра;
* статические методы (методы класса).
Код блока и элемента в `i-bem.js` принято называть **декларацией**, чтобы подчеркнуть принятый в БЭМ декларативный стиль программирования.
Поведение блока программируется в декларативном стиле в виде утверждений: `набор условий` — `реакция блока`.
### Синтаксис декларации
#### Блоки и элементы с DOM-представлением
##### Объявление нового блока без родителя
Чтобы задекларировать новый JS-блок **с DOM-представлением** (привязанный к HTML-элементу), нужно воспользоваться методом `declBlock` [ym-модуля](https://github.com/ymaps/modules) `i-bem-dom`.
Метод `declBlock` принимает аргументы:
1. Имя `{String}` или класс `{Function}` блока.
2. Методы экземпляра — `{Object}`.
3. Статические методы (методы класса) — `{Object}`.
Объявленные методы будут применяться во всех экземплярах блока независимо от их состояний (модификаторов).
**Пример**
Декларация методов для блока `button`.
```js
modules.define('button', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name,
{
/* методы экземпляра */
},
{
/* статические методы */
})
);
});
```
Поле контекста `ym` `this.name`, передаваемое первым аргументом методу `bemDom.declBlock`, содержит ссылку на имя блока, указанное первым аргументом `modules.define`.
##### Объявление нового элемента без родителя
Чтобы задекларировать новый JS-элемент **с DOM-представлением** (привязанный к HTML-элементу), нужно воспользоваться методом `declElem` [ym-модуля](https://github.com/ymaps/modules) `i-bem-dom`.
Метод `declElem` принимает аргументы:
1. Имя `{String}` или класс `{Function}` блока.
2. Имя `{String}` или класс `{Function}` элемента.
3. Методы экземпляра — `{Object}`.
4. Статические методы (методы класса) — `{Object}`.
Объявленные методы будут применяться во всех экземплярах элемента независимо от их состояний (модификаторов).
**Пример**
Декларация методов для элемента `menu__item`.
```js
modules.define('menu__item', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declElem('menu', 'item',
{
/* методы экземпляра */
},
{
/* статические методы */
})
);
});
```
#### Блоки и элементы без DOM-представления
Для декларации блоков и элементов без DOM-представления служат аналогичные методы `declBlock` и `declElem` [ym-модуля](https://github.com/ymaps/modules) `i-bem`.
Методы принимают те же параметры, что и метод `declBlock` и `declElem` модуля `i-bem-dom`:
```js
modules.define('my-block', ['i-bem'], function(provide, bem) {
provide(bem.declBlock(this.name,
{
/* методы экземпляра */
},
{
/* статические методы */
})
);
});
```
> **Примечание** Оформлять инфраструктурный код в виде блока без DOM-представления удобно, если в нем планируется использовать API БЭМ-блоков (состояния, выражаемые модификаторами, БЭМ-события и т. п.). Если использовать БЭМ-предметную область не планируется, инфраструктурный код можно оформлять в виде [ym-модуля](https://github.com/ymaps/modules).
>
> **Пример**
>
> ```js
> modules.define('router', function(provide) {
>
> provide({
> route : function() { /* ... */ }
> });
>
> });
> ```
### Наследование
Одна и та же функциональность может быть востребована в нескольких блоках или элементах проекта.
Например, разные блоки могут обращаться за данными к бэкенду, используя AJAX, или совершать однотипные операции с DOM-деревом и т.д.
Чтобы избежать ненужных повторов в коде, общую функциональность можно инкапсулировать в виде модулей, а затем добавлять к блокам.
Наследование позволяет повторно использовать функциональность блока, расширяя её новой логикой.
В `i-bem.js` доступно несколько механизмов наследования. Выбор конкретного механизма зависит от специфики создаваемого блока.
#### Простое наследование
В случае простого наследования создаваемый блок объявляется как наследник существующего. Для этого нужно:
1. Указать базовый блок в зависимостях модульной системы.
2. Передать ссылку на базовый блок во втором параметре декларации.
Например, блок `b-block` наследуется от блока `a-block`:
```js
modules.define('a-block', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, {}));
});
modules.define('b-block', ['i-bem-dom', 'a-block'], function(provide, bemDom, ABlock) {
provide(bemDom.declBlock(this.name, ABlock, {}));
});
```
Аналогично для элемента:
```js
modules.define('block__a-elem', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declElem('block', 'a-elem', {}));
});
modules.define('block__b-elem', ['i-bem-dom', 'block__a-elem'], function(provide, bemDom, BlockAElem) {
provide(bemDom.declElem('block', 'b-elem', BlockAElem, {}));
});
```
Такой механизм позволяет использовать методы базового блока или элемента внутри производного. Для вызова одноименных методов базового блока служит [вспомогательный метод](./i-bem-js-context.ru.md#Вспомогательные-свойства) `this.__base`.
> **Примечание** В `i-bem` можно создавать цепочки наследования – блок наследуется от другого, который, в свою очередь, наследуется от третьего и т.д.
#### Доопределение блока или элемента
Чтобы добавить и/или изменить поведение уже существующего блока или элемента, можно **доопределить** базовый класс на *уровне переопределения* проекта.
Для этого в проекте создается декларация блока или элемента с тем же именем, что и у базового. В результате блоку будет доступна вся функциональностью базового. Реализация одноименных методов и модификаторов, при этом, будет взята из новой декларации.
```js
modules.define('a-block', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, {})); // Объявляем базовый блок
});
modules.define('a-block', ['i-bem-dom'], function(provide, bemDom, ABlock) {
provide(bemDom.declBlock(ABlock, {})); // Доопределяем базовый блок
});
```
Аналогично для элементов с использованием метода `declElem`.
Такая схема наследования часто используется при работе с библиотечными блоками.
##### Добавление модификатора
В соответствии с БЭМ-методологией состояния блока или элемента должны описываться [модификаторами](./i-bem-js-states.ru.md#Модификаторы). Поэтому чтобы расширить функциональность блока или элемента часто нужно реализовать поддержку новых модификаторов.
Для декларации модификатора используется метод `declMod` у класса блока или элемента. В обоих случаях метод принимает аргументы:
1. Хеш с ключами `modName` и `modVal`. Значением для `modName` – имя модификатора, `{String}`. Значением `modVal`, `{String|Boolean|Array}` — значение модификатора.
2. Хеш методов, которые будут доступны для блока с соответствующим модификатором. При наличии одноименных методов и модификаторов, будет использована их реализация из хеша.
```js
modules.define('a-block', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.decl(this.name, {})); // Объявляем базовый блок
});
modules.define('a-block', function(provide, ABlock) {
provide(ABlock.declMod({ modName : 'm1', modVal : 'v1' }, {})); // Доопределяем базовый блок с модификтором _m1_v1
});
```
Аналогично для элементов.
> **Примечание** [Cтатические методы](./i-bem-js-context.ru.md#Контекст) блока и элемента будут доступны всем их экземплярам *вне зависимости от значений модификаторов*. Модификаторы — это свойства экземпляра, а статические методы принадлежат классу и не учитывают состояния модификаторов.
#### Миксины
В `i-bem.js` для добавления востребованной функциональности к блокам или элементам могут использоваться специальные сущности – **миксины**. Главная особенность миксинов состоит в том, что они не участвуют в цепочке наследования. Это позволяет примешивать реализованную в них функциональность к другим блокам или элементам без риска нарушить их [связи с родительскими классами](./i-bem-js-context.ru.md#Вспомогательные-свойства) `this.__base`.
##### Декларация блока-микса
Для декларации миксина используется метод `declMixin` модуля `i-bem-dom` (или `i-bem`).
Метод принимает параметры:
1. Методы экземпляра — `{Object}`.
2. Статические методы (методы класса) — `{Object}`.
```js
modules.define('my-mixin', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declMixin(
{ /* методы экземпляра */ },
{ /* статические методы */ }
));
});
```
> **Примечание** Миксины нельзя инстанцировать и использовать как самостоятельный блок.
##### Примешивание миксина
Чтобы примешать к блоку или элементу один или несколько миксинов, необходимо:
1. Указать необходимые миксины в зависимостях модульной системы.
2. Передать ссылку миксин (или миксины) во втором параметре декларации блока или элемента.
```js
modules.define('my-block', ['i-bem-dom', 'my-mixin'], function(provide, bemDom, myMixin) {
provide(bemDom.declBlock(this.name, myMixin,
{ /* методы экземпляра */ },
{ /* статические методы */ }
));
});
```
В случае нескольких миксинов:
```js
modules.define('my-block', ['i-bem-dom', 'a-mixin', 'b-mixin'], function(provide, bemDom, aMixin, bMixin) {
provide(bemDom.declBlock(this.name, [aMixin, bMixin], {}));
});
```
Аналогично для элемента с использованием метода `declElem`.
> **Примечание** В случае если нужно задекларировать наследование и миксины одновременно, во второй параметр следует передавать массив.Родительский класс обязательно должен быть первым элементом этого массива.
>
> ```js
> modules.define('b-block', ['i-bem-dom', 'a-block', 'a-mixin'], function(provide, bemDom, ABlock, aMixin) {
>
> provide(bemDom.declBlock(this.name, [ABlock, aMixin], {}));
>
> });
> ```
#### Декларация триггеров
[Триггеры](./i-bem-js-states.ru.md#Триггеры-на-установку-модификаторов), выполняемые при установке модификаторов, описываются в декларации блока или элемента. Для этого в хеше методов экземпляра зарезервированы свойства:
* `beforeSetMod` — триггеры, вызываемые до установки **модификаторов**;
* `onSetMod` — триггеры, вызываемые после установки **модификаторов**;
```js
modules.define('my-block', function(provide, bemDom) {
provide(bemDom.declBlock(this.name,
{
/* методы экземпляра */
beforeSetMod: { /* триггеры до установки модификаторов */},
onSetMod: { /* триггеры после установки модификаторов */ },
},
{
/* статические методы */
}
));
});
```
Значение свойств `beforeSetMod` и `onSetMod` — хеш, связывающий изменения модификаторов с триггерами. Триггер получает аргументами:
* `modName` – имя модификатора;
* `modVal` – выставляемое значение модификатора;
* `prevModVal` – предыдущее значение модификатора. Для `beforeSetMod` это текущее значение модификатора, которое будет заменено на `modVal`, если триггер не вернет `false`.
```js
bemDom.declBlock(this.name, {
onSetMod: {
'mod1': function(modName, modVal, prevModVal) { /* ... */ }, // установка mod1 в любое значение
'mod2': {
'val1': function(modName, modVal, prevModVal) { /* ... */ }, // триггер на установку mod2 в значение val1
'val2': function(modName, modVal, prevModVal) { /* ... */ }, // триггер на установку mod2 в значение val2
'': function(modName, modVal, prevModVal) { /* ... */ } // триггер на удаление модификатора mod2
},
'mod3': {
'true': function(modName, modVal, prevModVal) { /* ... */ }, // триггер на установку простого модификатора mod3
'': function(modName, modVal, prevModVal) { /* ... */ } // триггер на удаление простого модификатора mod3
},
'mod4': {
'!val1' : function() { /* ... */ } // декларация для изменения mod4 в любое значение, кроме val1
'~val2' : function() { /* ... */ } // декларация для изменения значения mod4 из val2 в любое другое значение
},
'*': function(modName, modVal, prevModVal) { /* ... */ } // триггер на установку любого модификатора в любое значение
}
})
```
Для триггера на установку любого модификатора блока в любое значение существует сокращенная форма записи:
```js
beforeSetMod: function(modName, modVal, prevModVal) { /* ... */ }
onSetMod: function(modName, modVal, prevModVal) { /* ... */ }
```
Аналогично для элементов с использованием метода `declElem`.
> **Примечание** Если модификаторы были заданы в HTML-элементе блока или элемента до момента его инициализации, триггеры на установку данных модификаторов **не выполняются**. Экземпляр в этом случае получает начальное состояние, а не меняет его.
================================================
FILE: common.docs/i-bem-js/i-bem-js-dom.en.md
================================================
## Working with the DOM tree
### DOM node of a block instance
The `this.domElem` field is reserved in the context of a block instance with DOM representation. This field contains a jQuery object with references to all the DOM nodes that this instance is connected to.
### Elements
BEM elements of blocks are represented in `i-bem.js` as DOM nodes nested in the DOM node of a block instance.
To access elements DOM nodes and work with their modifiers, use the block instance API:
* Cached access: `elem(elems, [modName], [modVal])`. An element
obtained this way does not need to be stored in a variable.
```js
BEMDOM.decl('link', {
setInnerText: function() {
this.elem('inner').text('Link text');
/* ... */
this.elem('inner').text('Another text');
}
});
```
* Uncached access: `findElem(elems, [modName], [modVal])`.
```js
BEMDOM.decl('link', {
setInnerText: function() {
var inner = this.findElem('inner');
inner.text('Link text');
/* ... */
inner.text('Another text');
}
});
```
When [block elements are added and removed dynamically](#block-elements-are-added-and-removed-dynamically), the cache of elements
may need to be cleared. Use the `dropElemCache('elements')` method for this purpose. It accepts a string with a space-separated list of names of elements to drop the cache for.
```js
BEMDOM.decl('attach', {
clear: function() {
BEMDOM.destruct(this.elem('control'));
BEMDOM.destruct(this.elem('file'));
return this.dropElemCache('control file');
}
});
```
### Searching for block instances in the DOM tree
Accessing a different block in `i-bem.js` is performed from the current block
located on a particular node of the DOM tree. The search for other blocks in
the DOM tree can be made in three directions (axes) relative to
the current block DOM node:
* **Inside the block** — On DOM nodes nested in the DOM node of the current block. Helper methods: `findBlocksInside([elem], block)` and `findBlockInside([elem], block)`.
* **Outside the block** — On DOM nodes that the current block DOM node
is a descendent of. Helper methods: `findBlocksOutside([elem], block)` and `findBlockOutside([elem], block)`.
* **On itself** — On the same DOM node where the current block is located. This is relevant when [multiple JS blocks are located on a single DOM node](i-bem-js-html-binding.en.md#multiple-js-blocks-are-located-on-a-single-dom-node) (a mix). Helper methods: `findBlocksOn([elem], block)` and `findBlockOn([elem], block)`.
The signature of the helper methods is identical:
* `[elem]` `{String|jQuery}` — The name or DOM node of the block element.
* `block` `{String|Object}` – Name or description of the block being searched for. A description is a hash in the format `{ block : 'name', modName : 'foo', modVal : 'bar' }`.
The helper methods for searching are paired. They differ in the values they return:
* `findBlocks` – Returns an array of found blocks.
* `findBlock` – Returns the first block found.
**Example**
When the `disabled` modifier is toggled, the instance of the `attach` block finds the `button` block nested inside it and toggles its `disabled` modifer to the same value that it received itself:
```js
modules.define('attach', ['i-bem__dom', 'button'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
onSetMod: {
'disabled': function(modName, modVal) {
this.findBlockInside('button').setMod(modName, modVal);
}
}
}));
});
```
> **Note** Don't use jQuery selectors to search for blocks and elements. `i-bem.js` provides a high-level API for accessing DOM nodes of blocks and elements. Accessing the DOM tree directly makes the code less robust to changes in the BEM libraries, and may cause errors that are difficult to detect.
### Dynamic changes to blocks and elements in the DOM tree
In modern interfaces, it is often necessary to create new
fragments of the DOM tree and replace old ones as part of the workflow (using AJAX). The following functions
are provided in `i-bem.js` for adding and replacing
fragments of the DOM tree.
* Add a DOM fragment:
* `append` — to the end of the specified context.
* `prepend` — to the beginning of the specified context.
* `before` — before the specified context.
* `after` — after the specified context.
* Replace a DOM fragment:
* `update` — inside the specified context.
* `replace` — replace the specified context with a new DOM fragment.
All the functions automatically [initialize blocks on the updated fragment of the DOM tree](i-bem-js-init.en.md#initialize-blocks-on-the-updated-fragment-of-the-dom-tree).
To simplify the creation of BEM entities on updated fragments
of the DOM tree, you can use the
[BEMHTML](https://en.bem.info/technology/bemhtml/current/intro/)template engine by enabling
it as a [ym](https://github.com/ymaps/modules) module. BEM entities are described in
[BEMJSON](https://en.bem.info/technology/bemjson/current/bemjson/)
format directly in the block code. The `BEMHTML.apply` function generates
HTML elements for the BEMJSON declarations according to
BEM naming conventions.
**Example**
The `attach` block `_updateFileElem` method deletes the `file` element if it exists, and creates a new element using the `BEMHTML.apply` function:
```js
modules.define(
'attach',
['BEMHTML', 'strings__escape', 'i-bem__dom'],
function(provide, BEMHTML, escape, BEMDOM) {
provide(BEMDOM.decl(this.name, {
_updateFileElem : function() {
var fileName = extractFileNameFromPath(this.getVal());
this.elem('file').length && BEMDOM.destruct(this.elem('file'));
BEMDOM.append(
this.domElem,
BEMHTML.apply({
block : 'attach',
elem : 'file',
content : [
{
elem : 'icon',
mods : { file : extractExtensionFromFileName(fileName) }
},
{ elem : 'text', content : escape.html(fileName) },
{ elem : 'clear' }
]
}));
return this.dropElemCache('file');
}
}));
});
```
================================================
FILE: common.docs/i-bem-js/i-bem-js-dom.ru.md
================================================
## Работа с DOM-деревом
### DOM-узел экземпляра блока и элемента
В контексте экземпляра блока и элемента с DOM-представлением зарезервировано поле `this.domElem`, содержащее jQuery-объект со ссылками на все DOM-узлы, с которыми связан данный экземпляр.
### Поиск экземпляров блоков и элементов в DOM-дереве
Обращение к другому блоку в `i-bem.js` выполняется из текущего блока, размещенного на определенном узле DOM-дерева. Поиск других блоков в DOM-дереве может вестись по трём направлениям (осям) относительно DOM-узла текущего блока:
* **Внутри блока** — на DOM-узлах, вложенных в DOM-узел текущего блока.
Вспомогательные методы:
* `findChildBlock(block)`;
* `findChildBlocks(block)`;
* `findChildElem(elem)`;
* `findChildElems(elem)`.
* **Снаружи блока** — на DOM-узлах, потомком которых является DOM-узел текущего блока.
Вспомогательные методы:
* `findParentBlock(block)`;
* `findParentBlocks(block)`;
* `findParentElem(elem)`;
* `findParentElems(elem)`.
* **На себе** — на том же DOM-узле, на котором размещен текущий блок. Это актуально в случае [размещения нескольких JS-блоков на одном DOM-узле](./i-bem-js-html-binding.ru.md#Один-html-элемент--несколько-js-блоков) (микс).
Вспомогательные методы:
* `findMixedBlock(block)`;
* `findMixedBlocks(block)`;
* `findMixedElem(elem)`;
* `findMixedElems(elem)`.
Методы `findMixedBlocks(block)` и `findMixedElems(elem)` могут возвращать больше одного экземпляра в случае, когда к [блоку или элементу с несколькими DOM-узлами](./i-bem-js-html-binding.ru.md#Один-js-блок-на-нескольких-html-элементах) примешаны несколько разных экземпляров одного и того же блока (`block`) или (`elem`).
Сигнатура вспомогательных методов поиска блоков идентична:
* `block` `{Function|Object}` – класс или описание искомого блока. Описанием служит хеш вида `{ block : MyBlock, modName : 'my-mod', modVal : 'my-val' }`.
Для методов поиска элементов:
* `elem` `{String|Function|Object}` – имя, класс или описание искомого элемента. Описанием служит хеш вида `{ elem : MyElem, modName : 'my-mod', modVal : 'my-val' }` или `{ elem : 'my-elem', modName : 'my-mod', modVal : 'my-val' }`;
* `[strictMode=false]` `{Boolean}` – нужно ли учитывать вложенность одноимённых блоков.
Вспомогательные методы для поиска парные. Различаются возвращаемым значением:
* `findBlock` и `findElem` – возвращает первый найденный экземпляр
* `findBlocks` `findElems` – возвращает [коллекцию](./i-bem-js-collections.ru.md) найденных экземпляров
**Пример**
```js
modules.define('attach', ['i-bem-dom', 'button'], function(provide, bemDom, Button) {
provide(bemDom.declBlock(this.name, {
onSetMod: {
'js': {
'inited' : function(modName, modVal) {
this._button = this.findChildBlock(Button);
}
}
}
}));
});
```
> **Примечание** Не используйте jQuery-селекторы для поиска блоков и элементов. `i-bem.js` предоставляет высокоуровневое API для доступа к DOM-узлам блоков и элементов. Прямое обращение к DOM-дереву делает код менее устойчивым к изменениям БЭМ-библиотек и может привести к возникновению сложно обнаруживаемых ошибок.
#### Кэширующие методы поиска экземпляров элементов
Для оптимизации производительности для распространённых случаев поиска элементов одновременно по двум осям (**внутри** и **на себе**), служат кэширующие методы `_elem(elem)` и `_elems(elem)`. Оба метода принимают один параметр:
* `elem` `{String|Function|Object}` – имя, класс или описание искомого элемента. Описанием служит хеш вида `{ elem : MyElem, modName : 'my-mod', modVal : 'my-val' }` или `{ elem : 'my-elem', modName : 'my-mod', modVal : 'my-val' }`.
Аналогично с некэширующими методами поиска кеширующие методы различаются возвращаемым значением:
* `_elem()` – возвращает первый найденный экземпляр элемента
* `_elems()` – возвращает [коллекцию](./i-bem-js-collections.ru.md) найденных экземпляров элементов
**Пример**
```js
modules.define('button', ['i-bem-dom', 'button__control'], function(provide, bemDom, ButtonControl) {
provide(bemDom.declBlock(this.name, {
setName : function(name) {
this._elem(ButtonControl).setName(name);
},
setValue : function(value) {
this._elem(ButtonControl).setValue(value);
}
}));
});
```
> **Примечание** Результат кеширующих методов нет необходимости сохранять в переменную (см. предыдущий пример). В то время как для некеширующих методов хорошей практикой является единоразовый поиск всего, что нужно, с сохранением в переменную или внутреннее поле.
> **Примечание** В случае использования элементов без JS реализации и [динамического обновления DOM-дерева](#Динамическое-обновление-блоков-и-элементов-в-dom-дереве) может понадобиться метод инвалидации кеша элементов `_dropElemCache('elements')`.
Он принимает опциональный параметр с одним или несколькими именами элементов через пробел.
#### Кеширующий метод поиска экземпляра блока элемента
* `_block()` — возвращает экземпляр блока для элемента.
**Пример**
```js
modules.define('my-form__submit-control', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declElem('my-form', 'submit-control', {
_onClick : function() {
this._block().submit();
}
}));
});
```
#### Проверка вложенности
* `containsEntity(entity)` — проверяет вложен ли переданный экземпляр `entity` `{i-bem-dom:Entity}` в текущий экземпляр.
### Динамическое обновление блоков и элементов в DOM-дереве
В модуле `i-bem-dom` предусмотрены следующие функции для добавления и замены фрагментов DOM-дерева.
* Удалить DOM-фрагмент:
* `destruct(ctx, [excludeSelf])`
Сигнатура функции:
* `ctx` `{jQuery}` – корневой DOM-элемент. Удаляется со всем вложенными DOM-узлами.
* `[excludeSelf]` `{Boolean}` – не удалять корневой DOM-элемент, если значение `true`. По умолчанию `false`.
* Добавить DOM-фрагмент:
* `append(ctx, content)` — в конец указанного контекста;
* `prepend(ctx, content)` — в начало указанного контекста;
* `before(ctx, content)` — перед указанным контекстом;
* `after(ctx, content)` — после указанного контекста;
* Заместить DOM-фрагмент:
* `update(ctx, content)` — внутри указанного контекста;
* `replace(ctx, content)` — заменить указанный контекст новым DOM-фрагментом.
Сигнатура функций добавления и замены идентична:
* `ctx` `{jQuery}` – DOM-элемент
* `content` `{jQuery|String}` – содержимое
Все функции возвращают DOM-элемент с содержимым для которого была выполнена [инициализация для новых блоков и элементов](./i-bem-js-init.ru.md#Инициализация-блоков-и-элементов-на-фрагменте-dom-дерева).
Чтобы упростить создание БЭМ-сущностей на обновляемых фрагментах DOM-дерева, можно использовать шаблонизатор [BEMHTML](https://ru.bem.info/platform/bem-xjst/), подключив его в качестве [ym-модуля](https://github.com/ymaps/modules). БЭМ-сущности описываются в формате [BEMJSON](https://ru.bem.info/platform/bemjson/) непосредственно в коде блока. Функция `BEMHTML.apply` генерирует HTML-элементы по BEMJSON-декларации в соответствии с правилами именования БЭМ.
**Пример**
Метод `_updateFileElem` блока `attach` удаляет элемент `file`, если он существовал, и создает новый элемент с помощью функции `BEMHTML.apply`:
```js
modules.define( 'attach', ['BEMHTML', 'i-bem-dom'], function(provide, BEMHTML, bemDom) {
provide(bemDom.declBlock(this.name, {
_updateFileElem : function() {
bemDom.replace(
this._elem('file').domElem,
BEMHTML.apply({
block : 'attach',
elem : 'file',
content : this.getValue()
}));
return this;
}
}));
});
```
================================================
FILE: common.docs/i-bem-js/i-bem-js-events.en.md
================================================
Events
------
In `i-bem.js`, two types of events are supported:
* **DOM events** — jQuery events on the DOM node connected with the block. They reflect the user's interaction with the interface (clicks, moving the mouse, entering text, and so on). DOM events are usually handled by the block instance of the DOM nodes where they occur.
* **BEM events** — Private events generated by the block. They make it possible to form an API for [interaction with the block](./i-bem-js-interact.en.md). BEM events are usually handled by an instance of the block that monitors the state of other blocks where events are generated.
DOM events should be used only in *internal* block procedures. Use BEM events for
a block interaction with the *external* environment (other blocks).
### Delegating events
Handling BEM events and DOM events can be **delegated** to a container
(the entire document, or a specific DOM node). In this case, the container
serves as a handling point for events that occur on any of its
child nodes, even if some of the child nodes didn't exist yet
at the time of subscribing to events.
For example, a menu block can contain nested blocks — the menu items. Handling
clicks on the menu items should logically be delegated to the menu
block. First, this saves resources on
subscribing to events (less resources are consumed by subscribing to a container single event
than by subscribing to many events on elements). Second, this makes it possible to add and remove menu items without subscribing to the events of added items or unsubscribing from the events of removed items.
Both BEM events and DOM events can be delegated.
### DOM events
Interaction with DOM events in `i-bem.js` is fully implemented using the jQuery framework.
#### Subscribing to DOM events
A set of methods for subscribing to DOM events is reserved on the block instance object:
* `bindTo([elem], event, handler)` — To events of the block main DOM node and its elements DOM nodes.
* `bindToDoc(event, [data], handler)` – To events of the `document` DOM node.
* `bindToWin(event, [data], handler)` – To events of the `window` DOM node.
**Example**
At [initialization of the block instance](./i-bem-js-init.en.md) `my-block`, the `click` event is subscribed to. When this event occurs, the block sets its `size` [modifier](./i-bem-js-states.en.md) to `big`.
```js
BEMDOM.decl('my-block', {
onSetMod : {
'js' : {
'inited': function() {
this.bindTo('click', function(e) {
this.setMod('size', 'big');
});
}
}
}
});
```
**Example**
At [initialization of the block instance](./i-bem-js-init.en.md) `my-form`, it subscribes to the `click` event on the `submit` element. When the event occurs, the `_onSubmit` handler function will be invoked.
```js
BEMDOM.decl('my-block', {
onSetMod : {
'js' : {
'inited': function() {
this.bindTo('submit', 'click', this._onSubmit);
}
}
},
_onSubmit : function() { /* ... */ }
});
```
> **Note** The handler function is executed in the context of the block instance where the event occurred.
#### Removing subscriptions to DOM events
Subscriptions to DOM events are removed automatically when a block instance is destroyed. However, the block instance object has a set of methods reserved for removing subscriptions manually while the block is working:
* `unbindFrom([elem], event, [handler])` — Unsubscribing from events of the block main DOM node and its elements DOM nodes.
* `unbindFromDoc(event, [handler])` – Unsubscribing from events of the `document` DOM node.
* `unbindFromWin(event, [handler])` – Unsubscribing from events of the `window` DOM node.
If the handler function isn't specified when calling one of these methods, all the handlers are removed that were set by the block on the DOM node for this event.
```js
_stopKeysListening : function() {
this.unbindFromDoc('keydown'); // removing all the handlers of the 'keydown' event
// set by the block for the 'document' DOM node
}
```
#### DOM event object
The first argument the handler function gets is a jQuery object for the DOM event — [`{jQuery.Event}`](https://api.jquery.com/category/events/event-object/).
This allows using the `stopPropagation` and `preventDefault` object methods for managing event propagation and the browser reaction to an event.
```js
BEMDOM.decl('my-block', {
onSetMod : {
'js' : {
'inited': function() {
this.bindTo('click', function(e) {
e.stopPropаgation(); // prevents the event from bubbling up
this._onSubmit();
});
}
}
},
_onSubmit : function() {
/* ... */
}
});
```
A DOM event can be generated manually, such as using the jQuery `trigger` function. After the event object, the handler function of the DOM event gets arguments with the parameters that were used to call `trigger` when the event was created.
> **Note** Parameters for the environment and behavior of an event handler function are identical to the jQuery [handler function](http://api.jquery.com/on/#event-handler).
#### Delegating DOM events
We recommend using the `liveBindTo([elem], event, handler)` method to delegate handling DOM events. In the block [static declaration methods](./i-bem-js-decl.en.md), the `live` property is reserved for subscribing to delegated DOM events.
**Example**
All instances of the `menu` block subscribe to the delegated `click` DOM event for their `item` elements. The `_onItemClick` method of the `menu` block instance will be invoked when any `item` in the menu is clicked. It doesn't matter whether this item existed when the instance was initialized.
```js
BEMDOM.decl('menu', {
_onItemClick : function(e) { /* ... */ }
}, {
live : function() {
this.liveBindTo('item', 'click', function(e) {
this._onItemClick(e);
});
}
});
```
If the `live` property is set in the block declaration, the initialization of block instances will be *deferred* until the moment when the block instance is needed ([lazy initialization](./i-bem-js-init.en.md#lazy-initialization)). This moment could be a DOM event on the block instance that was delegated the subscription, or a request sent to the block instance [from another block](./i-bem-js-interact.en.md).
> **Note** A handler function is executed in the context of the nearest block of this type in the direction of the DOM event bubbling (from bottom to top through the DOM tree).
To use delegated events in a block without deferring initialization, the function set in the `live` property should return `false`:
```js
modules.define('my-block', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name,
{
_onClick: function() { /* ... */ } // will be run each time
// the 'click' event occurs
},
{
live: function() {
this.liveBindTo('click', function() { this._onClick() });
return false; // block instances will be initialized automatically
}
}
));
});
```
### BEM events
In contrast to DOM events, BEM events are not generated on DOM elements, but on **block instances**. Block elements can't generate BEM events.
#### Generating BEM events
The `emit(event, [data])` method of a block instance is used for generating BEM events.
DOM events occur when a user interacts with a block controls. BEM events can be created as part of their processing by the block. This allows a level of abstraction over DOM events. BEM events are generated as a reaction to DOM events, but subject to certain conditions, such as whether a modifier is present or has a specific value.
For example, a click on the `submit` button (the `click` DOM event) will generate the `click` **BEM event** only if the block doesn't have the `disabled` modifier set:
```js
BEMDOM.decl('submit', {
onSetMod: {
'js': {
'inited': function() {
this.bindTo('click', this._onClick); // subscribing to the "click" DOM event
}
}
},
_onClick: function() {
if(!this.hasMod('disabled')) {
this.emit('click'); // creating the "click" BEM event
}
}
});
```
You can pass any data as the second `emit` argument, which will be accessible as the second argument of the handler function.
#### Subscribing to BEM events
The `on(event, [data], handler, [handlerCtx])` method of a block instance is used for subscribing to BEM events on block instances.
**Example**
At initialization of an HTML form (an instance of the `my-form` block), a search is performed for the `submit` button embedded in the form, and its `click` BEM event is subscribed to. As a result, when the button (an instance of the `submit` block) is clicked, the `_onSubmit` method is executed for the form (the instance of the `my-form` block).
```js
BEMDOM.decl('my-form', {
onSetMod: {
'js': {
'inited': function() {
this.findBlockInside('submit').on(
'click', // name of the BEM event
this._onSubmit, // method of the 'my-form' block instance
this); // context for executing _onSubmit — the my-form block
}
}
},
_onSubmit: function() { /* ... */ }
});
```
> **Note** If you don't pass the `[handlerCtx]` argument, the context for the handler function will be the block where the BEM event occurred (in the example above, this is the `submit` block).
#### Removing subscriptions to BEM events
Subscriptions to BEM events are removed automatically when the block instance is destroyed. To remove a subscription manually, use the
`un(event, [handler], [handlerCtx])` method of the block instance.
#### Events when modifiers are changed
Use the `on(event, [data], handler, [handlerCtx])` block instance method for subscribing to BEM events for changes to a modifier of a block or element. The method accepts the arguments:
* The properties object of the modifier that is being subscribed to.
* The handler function that is executed when setting the corresponding modifier.
The object describing the modifier can contain the following reserved properties:
* `modName` `{String}` – Modifier name. Required property.
* `modVal` `{String}` – Modifier value. Required property. With the value `*`, the subscription is for setting the modifier to **any** value. With the value `''`, the subscription is for **deleting** the modifier. For more information, see the section [Triggers for setting modifiers](i-bem-js-states.en.md#triggers-for-setting-modifiers).
* `elem` `{String}` – Element name (for element modifiers).
**Example**
At initialization, the `form` block subscribes to the event of changing a modifier on the nested `submit` block. For example, subscriptions can be for:
* Setting the `disabled` modifier to any value.
```js
BEMDOM.decl('form', {
onSetMod: {
'js': {
'inited': function() {
var submit = findBlockInside('submit');
submit.on({ modName : 'disabled', modVal : '*' }, function() {});
}
}
},
});
```
* Setting the `'disabled'` modifier to `'true'`.
```js
submit.on({ modName : 'disabled', modVal : 'true' }, function() {});
```
* Removing the `'disabled'` modifier.
```js
submit.on({ modName : 'disabled', modVal : '' }, function() {});
```
* Removing the `m1` modifier from the `'control'` element.
```js
submit.on({ elem : 'control', modName : 'm1', modVal : '' }, function() {});
```
#### Delegating BEM events
Delegating BEM events means that the block subscribes to a particular BEM event on **all instances** of the block with the specified name **in the scope of the specified context**.
Subscribing to delegated BEM events is performed using the `MyBlock.on([ctx], event, [data], handler, [handlerCtx])` static method of the block class.
* `{jQuery} [ctx]` — The DOM node where BEM events are monitored (the container). If omitted, the entire document is used as the container.
* `{String} event` — Name of the BEM event.
* `{Object} [data]` — Any data passed to the handler function.
* `{Function} handler` — Event handler function.
* `{Object} [handlerCtx]` — Context of the event handler function. If omitted, the handler function executes in the context of the block instance where the event occurred.
**Example**
During initialization of the `menu` block instance, it subscribes to the `click` BEM event on all links (instances of the `link` block) in the scope of the block DOM node (`this.domElem`). The current block instance is passed as the handler function context.
```js
modules.define('menu', ['i-bem__dom', 'link'], function(provide, BEMDOM, Link) {
provide(BEMDOM.decl(this.name,
onSetMod : {
'js' : {
'inited' : function() {
Link.on( // subscribing to BEM event
this.domElem, // container — the DOM node of the 'menu' block instance
'click', // BEM event
this._onLinkClick, // handler
this); // handler context — an instance of the 'menu' block
},
'' : function() {
Link.un( // unsubscribing from the BEM event
this.domElem,
'click',
this._onLinkClick,
this);
}
}
},
_onLinkClick : function(e) {
var clickedLink = e.target; // instance of the 'link' block
// where the 'click' BEM event occurred
}
));
});
```
Any BEM events can be delegated, including events for changes to modifiers.
> **Note** **Unsubscribing** from delegated BEM events never happens automatically. Subscriptions should always be removed explicitly using the block static method `un([ctx], event, [handler], [handlerCtx])`.
### BEM event object
When invoked, a handler function gets an argument with an object describing the BEM event. The BEM event object class `events.Event` is defined in the [ym](https://github.com/ymaps/modules) module of [`events`](../../common.blocks/events/events.vanilla.js) in the bem-core library. This object contains the fields:
* `target` — Instance of the block where the BEM event occurred.
* `data` — Any additional data passed as the `data` argument when subscribing to a BEM event.
* `result` — The last value returned by this event handler. The same as [jQuery.Event.result](https://api.jquery.com/event.result/).
* `type` — The type of event. The same as [jQuery.Event.type](https://api.jquery.com/event.type/).
For more information about properties and methods of a BEM event object, see the [documentation for the 'events' block](../../common.blocks/events).
================================================
FILE: common.docs/i-bem-js/i-bem-js-events.ru.md
================================================
## События
В `i-bem.js` поддерживается два вида событий:
* **DOM-события** — события на DOM-узле, связанном с блоком или элементом. Отражают взаимодействие пользователя с интерфейсом (клик, наведение мыши, ввод текста и т.п.). DOM-события обычно обрабатывает тот экземпляр блока или элемента, на DOM-узлах которого они возникают.
* **БЭМ-события** — собственные события, генерируемые блоком или элементом. Позволяют организовать API для [взаимодействия с блоком](./i-bem-js-interact.ru.md#Взаимодействие-блоков-и-элементов). БЭМ-события обычно обрабатывает экземпляр, отслеживающий состояние других блоков или элементов, на которых генерируются события.
DOM-события следует использовать только во взаимодействиях экземпляра со своим DOM-узлом или блока со своими элементами. Для взаимодействия с другими блоками или элементами предназначены БЭМ-события.
### DOM-события
Работа с DOM-событиями в `i-bem-dom` полностью реализована средствами фреймворка jQuery.
#### Подписка на DOM-события
##### Из экземпляра
Для подписки на DOM-события из экземпляра служит метод `_domEvents()`, создающий специальный объект [менеджера событий](#Объект-менеджера-событий).
Метод принимает один опциональный параметр, задающий контекст, который может быть разных типов:
* `elemInstance` `{Elem|BemDomCollection}` – экземпляр или коллекция элементов.
* `elemClass` `{String|Function|Object}` — класс, имя или описание элемента. Описанием служит хеш вида `{ elem : MyElem, modName : 'my-mod', modVal : 'my-val' }` или `{ elem : 'my-elem', modName : 'my-mod', modVal : 'my-val' }`.
* `document` `{Document|jQuery}` — документ
* `window` `{Window|jQuery}` — окно
[Менеджер событий](#Объект-менеджера-событий) обладает необходимым интерфейсом для подписки на события и отписки от них.
**Пример**
В момент [инициализации экземпляра блока](./i-bem-js-init.ru.md#Инициализация) `my-block` выполняется подписка на событие `click`, при наступлении которого блок выставляет себе [модификатор](./i-bem-js-states.ru.md#Модификаторы) `size` в значение `big`.
```js
bemDom.declBlock('my-block', {
onSetMod : {
'js' : {
'inited': function() {
this._domEvents().on('click', function() {
this.setMod('size', 'big');
});
}
}
}
});
```
**Пример**
При [инициализации экземпляра блока](./i-bem-js-init.ru.md#Инициализация) `my-form` выполняется подписка на событие `click` элемента `button`, при наступлении которого будет вызвана функция-обработчик `_onSubmit`.
```js
bemDom.declBlock('my-form', {
onSetMod : {
'js' : {
'inited': function() {
this._domEvents('button').on('click', this._onSubmit);
}
}
},
_onSubmit : function() { /* ... */ }
});
```
> **Примечание** Функция-обработчик выполняется в контексте того экземпляра, от которого создавался [менеджер событий](#Объект-менеджера-событий).
##### Из класса
Для подписки на DOM-события из класса служит статический метод `_domEvents()`, создающий специальный объект [менеджера событий](#Объект-менеджера-событий). Метод принимает один опциональный параметр, задающий контекст, который может быть разных типов:
* `elemClass` `{String|Function|Object}` — класс, имя или описание элемента. Описанием служит хеш вида `{ elem : MyElem, modName : 'my-mod', modVal : 'my-val' }` или `{ elem : 'my-elem', modName : 'my-mod', modVal : 'my-val' }`.
* `document` `{Document|jQuery}` — документ
* `window` `{Window|jQuery}` — окно
[Менеджер событий](#Объект-менеджера-событий) обладает необходимым интерфейсом для подписки на события и отписки от них.
**Пример**
При [инициализации класса блока](./i-bem-js-init.ru.md#Инициализация-класса) `my-form` выполняется подписка на событие `click` всех элементов `button` внутри любого блока `my-form`, при наступлении которого выполнится инициализация блока `my-form` (если он уже не проинициализирован) и у полученного экземпляра будет вызвана функция-обработчик `_onSubmit`.
```js
bemDom.declBlock('my-form', {
_onSubmit : function() { /* ... */ }
}, {
lazyInit : true,
onInit : function() {
this._domEvents('button').on('click', this.prototype._onSubmit);
}
});
```
> **Примечание** Функция-обработчик выполняется в контексте того экземпляра, внутри которого случилось событие.
#### Удаление подписки на DOM-событие
Удаление подписки на DOM-события выполняется автоматически при уничтожении экземпляра. Тем не менее, с помощью [менеджера событий](#Объект-менеджера-событий) можно удалить подписки вручную во время работы экземпляра.
```js
_stopKeysListening : function() {
this._domEvents().un('keydown', this._onKeydown); // удаляем обработчик события 'keydown'
}
```
#### Объект DOM-события
Первым аргументом функция-обработчик получает jQuery-объект DOM-события — [{jQuery.Event}](https://api.jquery.com/category/events/event-object/).
Это позволяет использовать методы объекта `stopPropagation` и `preventDefault` для управления всплытием события и реакцией на него браузера.
```js
bemDom.declBlock('my-form', {
onSetMod : {
'js' : {
'inited': function() {
this._domEvents('button').on('click', function(e) {
e.stopPropagation(); // останавливаем всплытие события
this._onSubmit();
});
}
}
},
_onSubmit : function() {
/* ... */
}
});
```
### БЭМ-события
В отличие от DOM-событий, БЭМ-события генерируются не на DOM-элементах, а на **экземплярах** блоков и элементов.
#### Генерация БЭМ-события
Для генерации БЭМ-события используется метод экземпляра `_emit(event, [data])`.
* `event` `{String|events:Event}` — имя или объект события.
* `[data]` `{*}` — дополнительные данные для события, которые будут доступны во втором аргументе обработчика.
При взаимодействии пользователя с элементом управления блока возникают DOM-события. В ходе их обработки блоком можно создавать БЭМ-события. Это позволяет реализовать уровень абстракции над DOM-событиями.
Например, при клике по кнопке `button` (DOM-событие `click`) **БЭМ-событие** `click` генерируется только в том случае, если у блока в этот момент не установлен модификатор `disabled`:
```js
bemDom.declBlock('button', {
onSetMod: {
'js': {
'inited': function() {
this._domEvents().on('click', this._onClick); // подписка на DOM-событие "click"
}
}
},
_onClick: function() {
if(!this.hasMod('disabled')) {
this._emit('click'); // создание БЭМ-события "click"
}
}
});
```
#### Подписка на БЭМ-события
##### Из экземпляра
Для подписки на БЭМ-события из экземпляра служит метод `_events()`, создающий специальный объект [менеджера событий](#Объект-менеджера-событий). Метод принимает один опциональный параметр, задающий контекст, который может быть разных типов:
* `entityInstance` `{Elem|BemDomCollection}` – экземпляр или коллекция БЭМ-сущностей.
* `entityClass` `{String|Function|Object}` — класс, имя или описание БЭМ-сущности. Описанием служит хеш вида `{ block : MyBlock, modName : 'my-mod', modVal : 'my-val' }`, `{ elem : MyElem, modName : 'my-mod', modVal : 'my-val' }` или `{ elem : 'my-elem', modName : 'my-mod', modVal : 'my-val' }`.
[Менеджер событий](#Объект-менеджера-событий) обладает необходимым интерфейсом для подписки на события и отписки от них, в том числе для работы с [событиями на изменения модификатора](#События-при-изменении-модификаторов).
**Пример**
В момент инициализации HTML-формы (экземпляра блока `my-form`) выполняется поиск вложенной в форму кнопки `button` и подписка на ее БЭМ-событие `click`. В результате при нажатии на кнопку (экземпляр блока `button`) будет выполнен метод `_onSubmit` формы (экземпляр блока `my-form`).
```js
modules.define('my-form', ['i-bem-dom', 'button'], function(provide, bemDom, Button) {
provide(bemDom.declBlock(this.name, {
onSetMod: {
'js': {
'inited': function() {
this._events(this.findChildBlock(Button))
.on('click', this._onSubmit);
}
}
},
_onSubmit: function() { /* ... */ }
}));
});
```
##### Из класса
Для подписки на БЭМ-события из класса служит статический метод `_events()`, создающий специальный объект [менеджера событий](#Объект-менеджера-событий).
Метод принимает один опциональный параметр, задающий контекст, который может быть разных типов:
* `entityClass` `{String|Function|Object}` — класс, имя или описание БЭМ-сущности. Описанием служит хеш вида `{ block : MyBlock, modName : 'my-mod', modVal : 'my-val' }`, `{ elem : MyElem, modName : 'my-mod', modVal : 'my-val' }` или `{ elem : 'my-elem', modName : 'my-mod', modVal : 'my-val' }`.
[Менеджер событий](#Объект-менеджера-событий) обладает необходимым интерфейсом для подписки на события и отписки от них, в том числе для работы с [событиями на изменения модификатора](#События-при-изменении-модификаторов).
**Пример**
При [инициализации класса блока](./i-bem-js-init.ru.md#Инициализация-класса) `my-form` выполняется подписка на событие `click` любого блока `button` внутри любого `my-form`, при наступлении которого выполнится инициализация блока `my-form` (если он уже не проинициализирован) и у полученного экземпляра будет вызвана функция-обработчик `_onSubmit`.
```js
modules.define('my-form', ['i-bem-dom', 'button'], function(provide, bemDom, Button) {
provide(bemDom.declBlock(this.name, {
_onSubmit: function() { /* ... */ }
}, {
lazyInit : true,
onInit : function() {
this._events(Button).on('click', this.prototype._onSubmit);
}
}));
});
```
> **Примечание** Функция-обработчик выполняется в контексте того экземпляра класса, производящего подписку, внутри которого случилось событие.
### Объект менеджера событий
Менеджер событий служит для унификации работы со всеми видами событий.
Обладает API:
Метод `on(event, [data], fn)` служит для подписки на событие `event`, обработчика `fn`, с возможностью передачи опциональных данных `data`. Принимает аргументы:
* `event` `String|Object` — имя события, хеш для [события при изменении модификатора](#События-при-изменении-модификаторов) или объект события (`jQuery.Event` для DOM-событий или `events:Event` для БЭМ-событий).
* `[data]` `*` — дополнительные данные для события, которые будут доступны в поле `data` объекта события.
* `fn` `Function` — функция-обработчик события.
Метод `once(event, [data], fn)` служит для единоразовой подписки на событие `event`, обработчика `fn`, с возможностью передачи опциональных данных `data`. Аргументы аналогичны методу `on()`.
Метод `un([event], [fn])` служит для удаления подписки на события.
Принимает аргументы:
* `[event]` `String|Object` — опциональное имя события, хеш для [события при изменении модификатора](#События-при-изменении-модификаторов), или объект события (`jQuery.Event` для DOM-событий или `events:Event` для БЭМ-событий). В случае, если аргумент не указан, происходит удаление подписок на все события.
* `[fn]` `Function` — функция-обработчик события. В случае, если аргумент не указан, происходит удаление всех обработчиков события `event`.
#### События при изменении модификаторов
В случае с БЭМ-событиями, существуют специальные события на изменение модификаторов, которые генерируются автоматически.
Для работы с такими событиями используется специальный хеш с полями:
* `modName` `{String}` – имя модификатора.
* `modVal` `{String}` – значение модификатора. Со значением `*` производится подписка на установку модификатора в **любое** значение. Со значением `''` – на **удаление** модификатора.
**Пример**
В момент инициализации блок `my-form` подписывается на событие изменения модификатора у вложенного блока `button`.
К примеру, можно подписаться на:
* установку модификатора `disabled` в любое значение;
```js
modules.define('my-form', ['i-bem-dom', 'button'], function(provide, bemDom, Button) {
bemDom.declBlock('form', {
onSetMod: {
'js': {
'inited': function() {
this._events(this.findChildBlock(Button))
.on({ modName : 'disabled', modVal : '*' }, this._onButtonDisabledChange);
}
}
},
_onButtonDisabledChange() {}
});
});
```
* установку модификатора `'disabled'` в значение `'true'`;
```js
this._events(this.findChildBlock(Button)).on({ modName : 'disabled', modVal : 'true' }, this._onButtonDisable);
```
* удаление модификатора `'disabled'`;
```js
this._events(this.findChildBlock(Button)).on({ modName : 'disabled', modVal : '' }, this._onButtonEnable);
```
### Объект БЭМ-события
При вызове функция-обработчик получает аргументом объект, описывающий БЭМ-событие. Класс объекта БЭМ-события `events.Event` определен в [ym](https://github.com/ymaps/modules)-модуле [`events`](../../common.blocks/events/events.ru.md) библиотеки bem-core.
Объект содержит поля:
* `type` `{String}` — тип события. Аналогично [jQuery.Event.type](https://api.jquery.com/event.type/).
* `target` `{i-bem-dom:Entity}` — экземпляр блока или элемента, в котором произошло БЭМ-событие.
* `data` `{*}` — произвольные дополнительные данные, переданные как аргумент `data` при подписке на БЭМ-событие.
* `result` `{*}` — последнее значение, возвращенное обработчиком данного события. Аналогично [jQuery.Event.result](https://api.jquery.com/event.result/).
================================================
FILE: common.docs/i-bem-js/i-bem-js-extras.en.md
================================================
## What next?
For information about the BEM methodology, tools, and news in the BEM world, visit the website [bem.info](https://en.bem.info/).
For complete information about all the `i-bem.js` API methods, see the section [JSDoc](https://en.bem.info/libs/bem-core/current/desktop/i-bem/jsdoc/) for the `i-bem` block.
To share your experience with BEM or submit questions for experienced users and `i-bem.js` developers, visit the [forum](https://en.bem.info/forum/).
For `i-bem.js` usage examples and additional information, see these articles:
* [JavaScript for BEM: main terms](https://en.bem.info/articles/bem-js-main-terms/)
* [i-bem.js tutorial](https://en.bem.info/tutorials/bem-js-tutorial/)
* [Starting your own BEM project](https://en.bem.info/tutorials/start-with-project-stub/)
* [Creating BEM application on Leaflet and 2GIS API](https://en.bem.info/articles/firm-card-story/)
================================================
FILE: common.docs/i-bem-js/i-bem-js-extras.ru.md
================================================
## Что дальше?
Информацию о БЭМ-методологии, инструментарии и новостях в мире БЭМ смотрите на сайте [bem.info](https://ru.bem.info).
Полную информацию обо всех методах API `i-bem.js` можно найти в разделе JSDoc [блока i-bem](https://ru.bem.info/platform/i-bem/) и [i-bem-dom](https://ru.bem.info/platform/i-bem/).
Обменяться опытом и задать вопрос опытным пользователям и разработчикам `i-bem.js` можно на [форуме](https://ru.bem.info/forum/).
Примеры использования `i-bem.js` и дополнительную информацию смотрите в статьях:
* [JavaScript по БЭМ: основные понятия](https://ru.bem.info/articles/bem-js-main-terms/)
* [Справочное руководство по i-bem.js](https://ru.bem.info/platform/tutorials/i-bem/)
* [Создаем свой проект на БЭМ](https://ru.bem.info/platform/tutorials/start-with-project-stub/)
* [БЭМ-приложение на Leaflet и API 2GIS](https://ru.bem.info/articles/firm-card-story/)
================================================
FILE: common.docs/i-bem-js/i-bem-js-html-binding.en.md
================================================
## Binding JS blocks to HTML
JavaScript components in `i-bem.js` are used for bringing a page HTML elements
to life. The typical task of a JS block is to set reactions to events inside an HTML fragment.
In `i-bem.js`, the primary ”framework“ is the document HTML tree. Points are marked in it where interactive interface elements, the JS blocks, are connected.
The binding point for a JS block is an HTML element (DOM node) whose `class` attribute
specifies the name of the block, and the `data-bem` attribute specifies the [block parameters](./i-bem-js-params.en.md).
When loading a page in the browser, [blocks are initialized](./i-bem-js-init.en.md). This creates instances of blocks — JS objects for all the blocks mentioned in classes of the page HTML elements. A JS object bound to an HTML element
handles the [DOM events](i-bem-js-events.en.md#dom-events) that occur on it and stores the states of this block instance.
This method of binding JavaScript components to HTML has the following advantages:
* Natural degradation of the interface on clients with JavaScript disabled.
* Progressive rendering — the ability to begin rendering interface elements before all the page data has finished loading (for example, images).
### Mechanism for binding blocks
To bind a block to an HTML element (for example, `...
`), it is necessary to:
* **Declare the block in `i-bem`**.
Create the [ym](https://github.com/ymaps/modules) module containing the JS implementation of the block ([the declaration](./i-bem-js-decl.en.md)). To do this, pass a string with the block name as the first argument to the `modules.define` and `BEMDOM.decl` methods.
```js
modules.define('my-block', ['i-bem__dom'], function(provide, BEMDOM){
provide(BEMDOM.decl(this.name,
{
/* instance methods */
},
{
/* static methods */
}
));
});
```
On the project level, each `ym` module is usually stored as a separate `js` file. For example, the `my-block` declaration might be stored in the project as `my-block/my-block.js` – the file `my-block.js`, inside the folder `my-block`.
* **Mark the block in the HTML tree**.
Add the `class` attribute with the block name to the HTML element.
```html
...
```
* **Allow initialization of a block instance**.
Include the `i-bem` class in the list of classes for an HTML element. The presence of this class will show the framework that the HTML element is connected to the JS block.
```html
...
```
* **Pass parameters to a block instance**.
Put block parameters in the `data-bem` attribute. Block parameters are written in JSON format as a hash of the format: `block name : hash of parameters`. The parameters will be passed to the block instance [at the time of initialization](./i-bem-js-init.en.md).
```html
...
```
### The relation of blocks to HTML elements
A single HTML element doesn't have to correspond to a single block instance. The following relationships between blocks and HTML elements are possible:
* [One HTML element to one JS block](#one-html-element-to-one-js-block)
* [One HTML element to multiple JS blocks](#one-html-element-to-multiple-js-blocks)
* [One JS block to multiple HTML elements](#one-js-block-to-multiple-html-elements)
#### One HTML element to one JS block
The simplest and most common way of binding blocks to HTML.
**Example**
The `div` HTML element with `my-block` placed on it. Block parameters: an empty list `{}`.
```html
...
```
#### One HTML element to multiple JS blocks
The technique of placing multiple blocks on a single HTML element is called a [mix](i-bem-js-decl.en.md#mix) in BEM methodology.
**Example**
The `div` HTML element, with the following blocks on it:
* `user` with the parameter `name`: `pushkin`
* `avatar` with the parameter `img`: `http:// ...`
```html
...
```
#### One JS block to multiple HTML elements
This design is convenient if you need to coordinate the states of multiple components of a block.
To bind a block instance to multiple HTML elements, you must set the same value for the `id` parameter in the `data-bem` attribute. The value of `id` can be any string.
**Example**
An instance of the `notebook` block bound to the `div` and `span` HTML elements. The parameters specify the shared `id` — `maintab`.
```html
...
```
As a result, when the blocks are initialized, a single JS object is created, with a [`domElem`](./i-bem-js-dom.en.md) field that contains references to the jQuery objects of both DOM nodes.
For example, the ”tab“ widget, where a click on the tab title (the first HTML element) changes its content (the second HTML element).
Another example is a placemark that marks a point on a map (the first element), and the related description of the point in the list next to it (the second element).
The `id` is used *only at the time of initializing* the block instance. The `id` value must be unique for instances of the same block in the context of a single [wave of initialization](i-bem-js-init.en.md#wave-of-initialization).
### Blocks without DOM representation
Infrastructure code that performs general interface tasks (access to the backend, or helper methods) can be formatted as a block. This allows expressing block states using [modifiers](./i-bem-js-states.en.md), so that [other blocks can subscribe](i-bem-js-states.en.md#other-blocks-can-subscribe) to their changes.
To avoid binding these blocks to the HTML tree artificially, these blocks can be created in `i-bem.js` without DOM representation.
Blocks without DOM representation:
* Do not require binding to a page's HTML code.
* Must be explicitly [initialized](i-bem-js-init.en.md#initialized) and destroyed.
#### Access to block instances without DOM representation
When creating a block instance without DOM representation, you must see to it that references to this instance are stored for blocks that need to interact with it.
See also:
* [Initializing and deleting blocks without DOM representation](i-bem-js-init.en.md#initializing-and-deleting-blocks-without-dom-representation)
================================================
FILE: common.docs/i-bem-js/i-bem-js-html-binding.ru.md
================================================
## Привязка JS-блоков к HTML
JavaScript-компоненты в `i-bem.js` служат для «оживления» HTML-элементов страницы. Типовая задача JS-блока — установка реакции на события внутри HTML-фрагмента.
В `i-bem.js` первичным «каркасом» является HTML-дерево документа. В нем размечаются точки, к которым привязаны интерактивные элементы интерфейса — JS-блоки. Точка привязки JS-блока — HTML-элемент (DOM-узел), в атрибуте `class` которого указано имя блока, а в атрибуте `data-bem` — [параметры блока](./i-bem-js-params.ru.md#Передача-параметров-экземпляру-блока-и-элемента).
При загрузке страницы в браузере выполняется [инициализация блоков](./i-bem-js-init.ru.md#Инициализация). В ходе нее создаются экземпляры блоков — JS-объекты всех блоков, упомянутых в классах HTML-элементов страницы. JS-объект, привязанный к HTML-элементу, обрабатывает происходящие на нем [DOM-события](./i-bem-js-events.ru.md#dom-события) и хранит состояния данного экземпляра блока.
Такой способ привязки JavaScript-компонентов к HTML имеет следующие преимущества:
* естественная деградация интерфейса на клиентах с отключенным JavaScript;
* прогрессивный рендеринг — возможность начинать отрисовку элементов интерфейса до окончания загрузки всех данных страницы (например, изображений).
> **Примечание** Начиная с версии `bem-core@v4.0.0` всё описанное ниже для блоков так же справедливо для элементов.
### Механизм привязки блоков
Чтобы привязать блок к HTML-элементу (например, `...
`), необходимо:
* **Декларировать блок в `i-bem`**.
Cоздать модуль [ym](https://github.com/ymaps/modules), содержащий JS-реализацию блока ([декларацию](./i-bem-js-decl.ru.md#Декларация-блоков-и-элементов)). Для этого строка с именем блока передается первым аргументом методам `modules.define` и `bemDom.declBlock`.
```js
modules.define('my-block', ['i-bem-dom'], function(provide, bemDom){
provide(bemDom.declBlock(this.name,
{
/* методы экземпляра */
},
{
/* статические методы */
}
));
});
```
На уровне проекта каждый модуль `ym` обычно хранится как отдельный файл технологии `js`. Например, декларация `my-block` в проекте может храниться как `my-block/my-block.js` – файл `my-block.js`, вложенный в папку `my-block`.
* **Отметить блок в HTML-дереве**.
Добавить HTML-элементу атрибут `class` с именем блока.
```html
...
```
* **Разрешить инициализацию экземпляра блока**.
Включить класс `i-bem` в список классов HTML-элемента. Наличие этого класса укажет фреймворку, что HTML-элемент связан с JS-блоком.
```html
...
```
* **Передать параметры экземпляру блока**.
Поместить параметры блока в атрибут `data-bem`. Параметры блока записываются в формате JSON и представляют собой хеш вида: `имя блока : хэш параметров`. Параметры будут переданы экземпляру блока [в момент инициализации](./i-bem-js-init.ru.md#Инициализация).
```html
...
```
### Связь блоков с HTML-элементами
Одному HTML-элементу не обязательно должен соответствовать один экземпляр блока. Возможны следующие типы связи между блоками и HTML-элементами:
* [Один HTML-элемент — один JS-блок](#Один-html-элемент--один-js-блок)
* [Один HTML-элемент — несколько JS-блоков](#Один-html-элемент--несколько-js-блоков)
* [Один JS-блок на нескольких HTML-элементах](#Один-js-блок-на-нескольких-html-элементах)
#### Один HTML-элемент — один JS-блок
Самый простой и распространенный способ привязки блоков к HTML.
**Пример**
HTML-элемент `div`, на котором размещен блок `my-block`. Параметры блока: пустой список `{}`.
```html
...
```
#### Один HTML-элемент — несколько JS-блоков
Техника размещения нескольких блоков на одном HTML-элементе в БЭМ-методологии называется [микс](./i-bem-js-decl.ru.md#Миксины).
**Пример**
HTML-элемент `div`, на котором размещены:
* блок `user` с параметром `name`: `pushkin`;
* блок `avatar` с параметром `img`: `http://...`.
```html
...
```
#### Один JS-блок на нескольких HTML-элементах
Такой дизайн удобен, если нужно согласовать состояния нескольких компонентов блока.
Чтобы привязать экземпляр блока к нескольким HTML-элементам, нужно указать им в атрибуте `data-bem` одинаковое значение параметра `id`. Значением `id` может быть произвольная строка.
**Пример**
Экземпляр блока `notebook` привязан к HTML-элементам `div` и `span`. В параметрах блока указан общий `id` — `maintab`.
```html
...
```
В результате при инициализации блоков создается один JS-объект, поле [domElem](./i-bem-js-dom.ru.md#Работа-с-dom-деревом) которого содержит ссылки на jQuery-объекты обоих DOM-узлов.
Например, виджет «вкладка», где клик по заголовку вкладки (первый HTML-элемент), меняет ее содержимое (второй HTML-элемент).
Другой пример: маркер, обозначающий точку на карте (первый элемент), и связанное с ним описание точки в списке рядом (второй элемент).
Идентификатор `id` используется *только в момент инициализации* экземпляра блока. Значение `id` должно быть уникальным для экземпляров одного блока в рамках одной [волны инициализации](./i-bem-js-init.ru.md#Волны-инициализации).
### Блоки без DOM-представления
Инфраструктурный код, решающий общие задачи интерфейса (связь с бэкэндом, вспомогательные методы), можно оформить в виде блока. Это позволит выражать состояния блока с помощью [модификаторов](./i-bem-js-states.ru.md#Модификаторы), на изменение которых смогут [подписаться другие блоки](./i-bem-js-states.ru.md#Триггеры-на-установку-модификаторов).
Чтобы не привязывать такие блоки к HTML-дереву искусственно в `i-bem.js` можно создавать блоки без DOM-представления.
Блоки без DOM-представления:
* не требуют привязки к HTML-коду страницы;
* должны быть явно [инициализированы](./i-bem-js-init.ru.md#Инициализация-и-удаление-блоков-без-dom-представления) и уничтожены.
#### Доступ к экземплярам блоков без DOM-представления
При создании экземпляра блока без DOM-представления необходимо позаботиться о сохранении ссылки на этот экземпляр для блоков, которым нужно с ним взаимодействовать.
================================================
FILE: common.docs/i-bem-js/i-bem-js-init.en.md
================================================
Initialization
--------------
Block initialization creates a JS object corresponding to the block instance
in the browser memory. Initialization of block instances is performed by the
`init()` method of the `i-bem__dom` module on the specified fragment of the DOM tree.
Each instance of a block can be assigned three states:
* The block instance is not initialized (the JS object has not been created).
* The block instance is initialized (the JS object has been created in the browser memory).
* The block instance was destroyed (all references to the block instance
were deleted, and it may be removed by the garbage collector).
In `i-bem.js`, these states of the block instance are described using the
auxiliary `js` modifier.
* Before initialization, the block instance does not have a `js` modifier.
```html
...
```
* At the time of the block instance initialization, the `js` modifier is set to `inited`.
```html
...
```
* If a fragment of the DOM tree is deleted during workflow (using the `destruct` method of the `i-bem__dom` module), block instances are also deleted with it if their HTML elements are all located in this fragment. Before deleting a block instance, the `js` modifier is deleted so that the block [instance destructors](#instance-destructors) are executed.
> **Note** If a block instance was [bound to multiple HTML elements](i-bem-js-html-binding.en.md#bound-to-multiple-html-elements), the block will exist as long as at least one element it is connected to remains in the HTML tree.
If multiple instances of other blocks are located on an HTML element, the
initialization of one of them (the appearance of the `js_inited` modifier)
doesn't affect the initialization of the rest of them.
**Example**
Only the `my-block` instance is initialized on the HTML element.
The `lazy-block` instance is not initialized:
```html
...
```
> **Note** The presence of the `js` modifier makes it possible to write various CSS styles for a block that depend on whether it is initialized or not.
### Block instance constructor
[Triggers](i-bem-js-states.en.md#triggers) can be assigned for changing the values of the `js` modifier, the same way as for other block modifiers.
The trigger to set the `js` modifier to the `inited` value is executed
during block creation. This trigger can be considered a **block instance constructor**:
```js
onSetMod: {
'js': {
'inited': function() { /* ... */ } // block instance constructor
}
}
```
### Block instance destructor
The moment of block deletion is the moment when all references to
the block JS object are destroyed. After this, the garbage collector can delete it from
browser memory.
The trigger to delete the `js` modifier (set it to an empty value
`''`) is executed before deleting the block. This trigger can be considered a
**block instance destructor**.
```js
onSetMod: {
'js': {
'': function() { /* ... */ } // block instance destructor
}
}
```
### Waves of initialization
The instances of blocks that are present on a page do not have
to be initialized simultaneously. The blocks can be added dynamically
and initialized on request or on an event.
Initialization of a consecutive group of blocks is called a **wave of initialization**.
A new wave of initialization is created in the following cases:
* [Automatic initialization of blocks when the `domReady` event occurs](#automatic-initialization-of-blocks-when-the-)
* [Initialization of a block when an event occurs](#initialization-of-a-block-when-an-event-occurs) (lazy initialization)
* [Directly calling block initialization on a specified fragment of the DOM tree](#directly-calling-block-initialization-on-a-specified-fragment-of-the-dom-tree)
### Automatic initialization
The *i-bem.js* framework allows automatically initializing blocks with DOM representation when the `domReady` event occurs.
For automatic initialization, JS objects will be created in browser memory for all the DOM nodes containing `i-bem` in the `class` attribute. Initialization is performed by the `init` function of the [i-bem__dom](https://en.bem.info/libs/bem-core/current/desktop/i-bem/jsdoc/) module.
To enable automatic initialization, specify the `i-bem` block with the `init` modifier set to the `auto` value in the `.deps.js` dependencies file.
**Example of** `.deps.js`:
```js
({
shouldDeps: [
{
block: 'i-bem',
elem: 'dom',
mods: { 'init': 'auto' }
}
]
})
```
The [page](../../common.blocks/page/) block already contains `i-bem__dom_init_auto` in dependencies, so if it is used in the project, nothing else needs to be enabled.
> **Note** Blocks that have lazy initialization set will not be initialized automatically.
### Initialization on event (lazy initialization)
If a page has many instances of blocks, automatic initialization of
all the blocks at the time of loading is undesirable, since this increases the loading time
and the amount of memory consumed by the browser.
It is more convenient to initialize JS objects only when their functionality is needed by the user,
such as when the block is clicked. This is called **lazy** or **live** initialization.
The static property `live` is reserved in the declaration for describing conditions for lazy initialization. The `live` property can have the following types of values:
`Boolean`
* `true` — Instances of blocks in this class will be initialized only when attempting to get the corresponding instance (see the section [Interaction of blocks](./i-bem-js-interact.en.md)).
```js
modules.define('my-block', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name,
{
onSetMod: {
'js': {
'inited': function() { /* ... */ } // this code will be executed
// the first time the block instance is accessed
}
}
},
{ live: true } // static methods and properties
));
});
```
* `false` — Allows cancelling lazy initialization of blocks that is set on another redefinition level.
`Function` – a function that is executed before initializing the **first instance** of a block of the specified class. If the function returns `false`, instances of the block will be initialized [automatically](#automatically).
```js
modules.define('my-block', ['i-bem__dom', 'ua'], function(provide, BEMDOM, ua) {
provide(BEMDOM.decl(this.name, {
onSetMod : {
'js' : {
'inited' : function() {
// executed when the block instance is first accessed
}
}
}
}, {
live : function() { // executed before initialization of the first instance of the block
if(ua.msie && ua.version < 9) {
// disables lazy initialization of the block
return false; // for old versions of Internet Explorer
}
}
}));
});
```
> **Note** Lazy initialization can be canceled for a specific instance of a block. To do this, specify `data-bem='{"live": false}'` in the [parameters](./i-bem-js-params.en.md) of the HTML element that the block instance is bound to.
To initialize block instances as DOM events or BEM events occur, subscribe to [delegated events](i-bem-js-events.en.md#delegated-events) in the function body or use a [helper](#helper).
**Example**
Instances of `my-block` will be initialized on the `click` DOM event on the block DOM node. For each `click` DOM event, the `_onClick` method of the block instance is called:
```js
modules.define('my-block', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
onSetMod: {
'js': {
'inited': function() { /* ... */ } // executed on the first "click" DOM event
}
},
_onClick: function(e) { /* ... */ } // executed on every ”click“ DOM event
}, {
live: function() {
this.liveBindTo('click', function(e) {
this._onClick(e); // block instance will be created when a click occurs
// and its _onClick method will be called
});
}
}));
});
```
> **Note** The `live` property applies to static methods of a block class. So even if it is set in the block declaration with a particular modifier, `live` will be applied to all the blocks in this class, regardless of the modifiers.
### Helpers for initialization on an event
To simplify initialization on events in the context of a block instance, a set of helper methods is reserved for subscribing to the following types of events:
* DOM events:
* `liveBindTo([elemName], event, [callback])` — Subscribes to an event on the block DOM node or its elements, with deferred initialization. The block will be initialized on the first `event`. The `callback` handler function will be called on `event` and after block initialization.
* `liveUnbindFrom([elemName], event, [callback])` — Deletes the subscription with deferred initialization on an event on the block DOM node or its elements.
* `liveInitOnEvent([elemName], event, callback)` — Initialization on an event on the block DOM node or its elements.
* BEM events:
* `liveInitOnBlockEvent(event, blockName, callback)` — Initialization on a BEM event of an instance of a different block placed on the DOM node of the current block instance.
* `liveInitOnBlockInsideEvent(event, blockName, [callback])` — Initialization on a BEM event of an instance of a different block nested in the DOM node of the current block instance.
For example, the `menu` block is initialized on the `click` **BEM event** of the nested `menu-item` block.
```js
modules.define('menu', ['i-bem__dom', 'menu-item'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
_onItemClick : function(e, data) {
// handler function for the ”click“ BEM event on nested ”menu-item“ instances
}
}, {
live : function() {
this.liveInitOnBlockInsideEvent('click', 'menu-item', function(e, data) {
this._onItemClick(e, data);
});
}
}));
});
```
### Initialization of blocks on a fragment of the DOM tree
The initialization of JS objects can be called
directly for a specified fragment of the DOM tree. This is often necessary when developing AJAX interfaces,
when new instances of blocks have to be [dynamically added](i-bem-js-dom.en.md#dynamically-added) to a page or existing ones have to be updated.
In `i-bem.js`, the following functions perform dynamic initialization of blocks:
* `init`, `destruct` – Initialization/destruction of blocks on a specified fragment of the DOM tree.
* `update`, `replace`, `append`, `prepend`, `before`, `after` – Adding/replacing a fragment of the DOM tree with simultaneous initialization of blocks on the updated fragment.
For an example of using functions that perform dynamic initialization, see [Dynamically updating blocks and elements in the DOM tree](i-bem-js-dom.en.md#dynamically-updating-blocks-and-elements-in-the-dom-tree).
### Deleting blocks on a fragment of the DOM tree
Like the block initialization process, the deletion process can be called directly for a specified fragment of the DOM tree. For example, you may use this for dynamically deleting instances of blocks from a page when developing AJAX interfaces.
Explicitly invoking this procedure guarantees correct deletion of:
* Nested DOM nodes.
* Blocks mixed into other blocks.
Use the `BEMDOM.destruct` static method to explicitly invoke deletion.
The method accepts:
* `ctx` `{jQuery}` – The root DOM element. Deleted together with all the nested DOM nodes.
* `excludeSelf` `{Boolean}` – Doesn't delete the root DOM element if set to `true`. By default, `false`.
### Initializing and deleting blocks without DOM representation
Use the `BEM.create` method for creating JS objects of a block without DOM representation (that aren't bound to an HTML element).
The method accepts:
* `name` `{String|Object}` – The name of the block.
Returns an instance of a block of the specified class.
**Deletion** of instances of blocks without DOM representation can't be
performed automatically. Blocks without DOM representation are normal JS objects and are deleted when
all the references to the block object are deleted.
================================================
FILE: common.docs/i-bem-js/i-bem-js-init.ru.md
================================================
## Инициализация
Инициализация — это создание в памяти браузера JS-объекта, соответствующего экземпляру блока или элемента. Инициализация экземпляров блоков или элементов выполняется функцией `init([ctx])` из модуля `i-bem-dom` на заданном фрагменте DOM-дерева `ctx`.
Каждому экземпляру можно приписать три состояния:
* не инициализирован — JS-объект не создан
* инициализирован — JS-объект создан в памяти браузера
* уничтожен — удалены все ссылки на JS-объект экземпляра, и он может быть удален сборщиком мусора.
В `i-bem.js` эти состояния описываются с помощью служебного модификатора `js`.
* До инициализации экземпляр не имеет модификатора `js`.
```html
...
```
* В момент инициализации экземпляру устанавливается модификатор `js` в значении `inited`.
```html
...
```
* Если в процессе работы удаляется фрагмент DOM-дерева (при помощи метода `destruct()` модуля `i-bem-dom`), то вместе с ним удаляются экземпляры, все HTML-элементы которых находятся в этом фрагменте. Перед удалением экземпляра модификатор `js` удаляется, чтобы выполнились [деструкторы экземпляра](#Деструктор-экземпляра-блока-и-элемента).
> **Примечание** Если экземпляр блока или элемента был [привязан к нескольким HTML-элементам](./i-bem-js-html-binding.ru.md#Один-js-блок-на-нескольких-html-элементах), экземпляр будет существовать, пока в HTML-дереве сохраняется хотя бы один HTML-элемент, с которым он связан.
Если на HTML-элементе размещено несколько экземпляров других блоков или элементов, то инициализация одного из них (появление модификатора `js` со значением `inited`) не влияет на инициализацию остальных.
**Пример**
На HTML-элементе инициализирован только экземпляр блока `my-block`. Экземпляр блока `lazy-block` не инициализирован:
```html
...
```
> **Примечание** Наличие модификатора `js` позволяет писать разные CSS-стили для блока или элемента в зависимости от того, инициализирован он или нет.
### Конструктор экземпляра блока и элемента
На изменение значений модификатора `js` можно назначать [триггеры](./i-bem-js-states.ru.md#Триггеры-на-установку-модификаторов) так же, как и для любых других модификаторов.
Триггер на установку модификатора `js` в значение `inited` выполняется при инициализации экземпляра.
Этот триггер можно считать **конструктором**:
```js
bemDom.declBlock('my-block', {
onSetMod : {
'js' : {
'inited' : function() { /* ... */ } // конструктор экземпляра
}
}
});
```
### Деструктор экземпляра блока и элемента
Моментом удаления экземпляра является момент уничтожения всех ссылок на его JS-объект, после чего он может быть удален из памяти браузера сборщиком мусора.
Триггер на удаление модификатора `js` (установку в пустое значение `''`) выполняется перед удалением блока. Такой триггер можно считать **деструктором**.
```js
bemDom.declBlock('my-block', {
onSetMod : {
'js' : {
'' : function() { /* ... */ } // деструктор экземпляра
}
}
});
```
### Волны инициализации
Инициализация экземпляров блоков и элементов, присутствующих на странице, не обязательно происходит одновременно. Они могут динамически добавляться в ходе работы, инициализироваться по запросу или событию. Инициализация очередной группы блоков или элементов называется **волной инициализации**.
Новая волна инициализации создается в следующих случаях:
* [Автоматическая инициализация блоков и элементов по событию `domReady`](#Автоматическая-инициализация).
* [Ленивая инициализация](#Ленивая-инициализация).
* [Явный вызов инициализации на указанном фрагменте DOM-дерева](#Инициализация-блоков-и-элементов-на-фрагменте-dom-дерева).
### Автоматическая инициализация
`i-bem.js` позволяет автоматически инициализировать блоки и элементы с DOM-представлением в момент наступления DOM-события `domReady`.
Включить автоматическую инициализацию можно, указав блок `i-bem` с модификатором `init` в значении `auto` в файле зависимостей `.deps.js`.
**Пример файла** `.deps.js`:
```js
({
shouldDeps : [
{
block : 'i-bem',
elem : 'dom',
mods : { init : 'auto' }
}
]
})
```
Блок [page](../../common.blocks/page/page.ru.md) уже содержит в зависимостях `i-bem-dom_init_auto`, поэтому если он используется в проекте, не требуется ничего дополнительно подключать.
> **Примечание** Блоки и элементы, для которых задекларирована [ленивая инициализация](#Ленивая-инициализация), не будут инициализированы автоматически.
### Ленивая инициализация
Если на странице размещено много экземпляров блоков и элементов, их автоматическая инициализация в момент загрузки страницы нежелательна, так как она увеличивает время загрузки и объем памяти, затрачиваемой браузером.
Рекомендуется инициализировать блоки и элементы только в тот момент, когда их функциональность потребуется пользователю, например, по клику на блок. Такая инициализация называется **ленивой**.
Для декларации ленивой инициализации, в декларации зарезервировано статическое свойство `lazyInit` типа `Boolean`.
При `lazyInit : true`, блоки или элемнеты данного класса будут инициализированы только при попытке получить соответствующий экземпляр (см. раздел «[Взаимодействие блоков](./i-bem-js-interact.ru.md#Взаимодействие-блоков-и-элементов)»).
```js
modules.define('my-block', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, {
onSetMod : {
'js' : {
'inited' : function() {
// этот код будет выполняться при первом обращении к экземпляру блока
}
}
}
}, {
lazyInit : true
}));
});
```
Декларация с `lazyInit : false` позволяет отменить ленивую инициализацию, заданную на другом уровне переопределения.
> **Примечание** Ленивая инициализация может быть отменена для конкретного экземпляра. Для этого нужно указать в [параметрах](./i-bem-js-params.ru.md#Передача-параметров-экземпляру-блока-и-элемента) HTML-элемента, к которому привязан экземпляр `data-bem='{ "my-block" : { "lazyInit" : false } }'`.
#### Инициализация класса
В терминах `i-bem-dom` существует понятие **инициализация класса**. Она происходит в момент прохождения [волны инициализации](#Волны-инициализации) на HTML-фрагменте, когда в нем впервые за время жизни приложения встречается блок или элемент данного класса.
Инициализация класса необходима для реализации ленивой инициализации по DOM- или БЭМ-событию. Для этого в декларации зарезервирован статический метод `onInit`, внутри которого можно [подписаться на нужные события](./i-bem-js-events.ru.md#События).
**Пример**
Блок `button` будет инициализирован по DOM-событию `click` на DOM-узле блока.
```js
modules.define('button', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, {
onSetMod : {
'js' : {
'inited' : function() {
// выполняется при первом DOM-событии "click"
}
}
},
_onClick: function(e) {
// выполняется при каждом DOM-событии "click"
}
}, {
lazyInit : true,
onInit : function() {
this._domEvents().on(
'click',
this.prototype._onClick); // в момент клика будет создан экземпляр блока и вызван его метод _onClick
}
}));
});
```
Блок `my-form` инициализируется по БЭМ-событию `click` вложенного в него блока `button`.
```js
modules.define('my-form', ['i-bem-dom', 'button'], function(provide, bemDom, Button) {
provide(bemDom.declBlock(this.name, {
_onButtonClick : function(e, data) {
// функция-обработчик БЭМ-события click на вложенных блоках button
}
}, {
lazyInit : true,
onInit : function() {
this._events(Button).on('click', this.prototype._onButtonClick);
}
}));
});
```
> **Примечание** Свойства `lazyInit` и `onInit` относятся к статическим свойствам класса. Поэтому даже если оно задано в декларации блока или элемента с определенным модификатором, они будут применены ко всем экземплярам данного класса, вне зависимости от модификаторов.
### Инициализация блоков и элементов на фрагменте DOM-дерева
Процедура инициализации JS-объектов может быть вызвана явно для указанного фрагмента DOM-дерева. Такая необходимость возникает при [динамическом обновлении](./i-bem-js-dom.ru.md#Динамическое-обновление-блоков-и-элементов-в-dom-дереве) блоков или элементов.
Следующие функции выполняют динамическую инициализацию блоков и элементов:
* `init()`, `destruct()` – инициализация и уничтожение экземпляров на указанном фрагменте DOM-дерева.
* `update()`, `replace()`, `append()`, `prepend()`, `before()`, `after()` – обновление фрагмента DOM-дерева
с одновременной инициализацией на обновленном фрагменте.
Пример использования функций, выполняющих динамическую инициализацию см. в разделе «[Динамическое обновление блоков и элементов в DOM-дереве](./i-bem-js-dom.ru.md#Динамическое-обновление-блоков-и-элементов-в-dom-дереве)».
### Инициализация и удаление блоков без DOM-представления
Для создания JS-объектов блока или элемента без DOM-представления (не привязанного к HTML-элементу) служит статический метод `create()` классов `Block` или `Elem` из модуля `i-bem`.
Метод принимает аргументы:
* `mods` `{Object}` – модификаторы создаваемого блока или элемента.
* `params` `{Object}` – параметры блока или элемента.
Возвращает экземпляр указанного класса.
**Удаление** экземпляров блоков и элементов без DOM-представления не может быть выполнено автоматически. Блоки и элементы без DOM-представления представляют собой обычные JS-объекты и удаляются в момент удаления всех ссылок на объект.
================================================
FILE: common.docs/i-bem-js/i-bem-js-interact.en.md
================================================
Interaction of blocks
---------------------
In the scope of the BEM methodology, blocks should be developed in a way that minimizes their dependency on each others' states. However, the ideal of fully independent blocks is not achievable in practice.
Block interaction can be implemented in the following ways:
* By subscribing to [BEM events](i-bem-js-events.en.md#bem-events) on other block instances
or subscribing to [delegated BEM events](i-bem-js-events.en.md#delegated-bem-events).
* By directly calling methods of other block instances
or static methods of another block class.
* By checking the states of one of the blocks.
* Through *event channels* (for example, using the [channels](../../common.blocks/events/__channels) element in the `events` block).
> **Note** Don't use [DOM events](i-bem-js-events.en.md#dom-events) for arranging interaction between blocks. DOM events are intended only for implementing internal procedures of a block.
The following `i-bem.js` APIs are provided for implementing interaction between blocks:
* [Searching for block instances in the DOM tree](i-bem-js-dom.en.md#searching-for-block-instances-in-the-dom-tree)
* [Access to block instances without DOM representation](i-bem-js-html-binding.en.md#access-to-block-instances-without-dom-representation)
* [Access to block classes](#access-to-block-classes)
### Access to block classes
You can get JS components corresponding to block classes via the [module system](https://github.com/ymaps/modules). This is also true for blocks [without DOM representation](i-bem-js-html-binding.en.md#without-dom-representation).
Access to block classes is needed for:
* [Delegating BEM events](i-bem-js-events.en.md#delegating-bem-events).
* [Redefining](i-bem-js-decl.en.md#redefining) a block declaration.
* Calling static methods of a class.
**Example**
Calling the `close` static method for the `popup` block will close all popups on the page.
```js
modules.define('switcher', ['i-bem__dom', 'popup'], function(provide, BEMDOM, Popup) {
provide(BEMDOM.decl(this.name,
{
onSetMod : {
'popup' : {
'disabled' : function() {
Popup.close();
}
}
}
}
));
});
```
================================================
FILE: common.docs/i-bem-js/i-bem-js-interact.ru.md
================================================
## Взаимодействие блоков и элементов
В рамках БЭМ-методологии блоки и элементы следует разрабатывать так, чтобы свести к минимуму зависимость состояний одних сущностей от других. Однако на практике идеал полной независимости блоков недостижим.
Взаимодействие блоков и элементов может быть реализовано:
* с помощью подписки на [БЭМ-события](./i-bem-js-events.ru.md#БЭМ-события);
* с помощью непосредственного вызова методов других экземпляров или статических методов класса;
* через проверку [состояний](./i-bem-js-states.ru.md) одного экземпляра из другого.
> **Примечание** Не используйте [DOM-события](./i-bem-js-events.ru.md#dom-события) для организации взаимодействия между экземплярами. DOM-события следует использовать только во взаимодействиях экземпляра со своим DOM-узлом.
Для реализации взаимодействия блоков или элементов `i-bem.js` предоставляет API:
* [Поиск экземпляров в DOM-дереве](./i-bem-js-dom.ru.md#Поиск-экземпляров-блоков-и-элементов-в-dom-дереве).
* [Доступ к экземплярам без DOM-представления](./i-bem-js-html-binding.ru.md#Блоки-без-dom-представления).
* [Доступ к классам блоков и элементов](#Доступ-к-классам-блоков-и-элементов).
### Доступ к классам блоков и элементов
Классы блоков и элементов, можно получить через [модульную систему ym](https://github.com/ymaps/modules). Это же верно и для блоков и элементов [без DOM-представления](./i-bem-js-html-binding.ru.md#Блоки-без-dom-представления).
Доступ к классам блоков и элементов необходим для:
* [Доопределения](./i-bem-js-decl.ru.md#Доопределение-блока-или-элемента) декларации блока и [наследования](./i-bem-js-decl.ru.md#Наследование).
* [Поиска](./i-bem-js-dom.ru.md#Поиск-экземпляров-блоков-и-элементов-в-dom-дереве) их экземпляров в DOM-дереве.
* [Для работы с событиями в контексте класса](./i-bem-js-events.ru.md).
* Вызова статических методов класса.
**Пример**
Блок `button` наследуется от базового блока `control`:
```js
modules.define('button', ['i-bem-dom', 'control'], function(provide, bemDom, Control) {
provide(bemDom.declBlock(this.name, Control, {}));
});
```
================================================
FILE: common.docs/i-bem-js/i-bem-js-params.en.md
================================================
Passing parameters to a block instance
--------------------------------------
### Syntax for passing parameters
Block parameters are stored in the `data-bem` attribute of an HTML element, and are passed to the block at the time of initialization. Use parameters to control the behavior of a specific block instance that is bound to a given HTML element.
The value of the `data-bem` attribute must contain valid JSON describing a hash in the format:
* key — `{String}`, name of the block.
* value — `{Object}`, parameters of the block. If this instance of the block does not need
parameters, specify an empty hash `{}`.
```html
```
If an HTML element has [multiple JS blocks bound to it](./i-bem-js-html-binding.en.md#multiple-js-blocks-bound-to-it), the value of the `data-bem` attribute must contain the parameters for each of them:
```html
```
**Element parameters** are passed via the `data-bem` attribute of the element DOM node. For example, you can pass parameters to the `my-elem` element in the `my-block` block like this:
```html
```
Specifying the block name in the parameters provides the following advantages:
* Blocks are initialized faster, since the value of the `class` attribute doesn't have to be parsed.
* Multiple blocks can be put on the same HTML element without having to multiply its attributes.
### Accessing parameters from a block instance
You can access parameters from a block instance via the `this.params` field. Its value is a hash of parameters from the `data-bem` attribute of the block DOM element (`this.domElem`).
For example, you can access parameters of the `my-block` block like this:
```html
```
```js
modules.define('my-block', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
onSetMod : {
'js' : {
'inited': function() {
console.log(this.params); // { foo : 'bar' }
}
}
}
}));
});
```
To get element parameters, use the `elemParams` method of the block instance. It accepts a string argument with the element name or its jQuery object. It returns a hash of element parameters.
```html
```
```js
modules.define('my-block', ['i-bem__dom'], function(provide, BEMDOM) {
provide(BEMDOM.decl(this.name, {
onSetMod : {
'js' : {
'inited': function() {
console.log(this.elemParams('my-elem')); // { foo : 'bar' }
}
}
}
}));
});
```
================================================
FILE: common.docs/i-bem-js/i-bem-js-params.ru.md
================================================
## Передача параметров экземпляру блока и элемента
### Синтаксис передачи параметров
Параметры блока и элемента хранятся в атрибуте `data-bem` HTML-элемента и передаются экземпляру в момент инициализации. Параметры позволяют влиять на поведение конкретного экземпляра, привязанного к данному HTML-элементу.
Значение атрибута `data-bem` должно содержать валидный JSON описывающий хеш вида:
* ключ — `{String}`, имя блока;
* значение — `{Object}`, параметры данного блока. Если данному экземпляру не требуются параметры, указывается пустой хеш `{}`.
```html
```
Если к HTML-элементу [привязано несколько блоков или элементов в технологии JS](./i-bem-js-html-binding.ru.md#Один-html-элемент--несколько-js-блоков),
то в значении атрибута `data-bem` должны содержаться параметры для каждого из них:
```html
```
Указание имени блока в параметрах позволяет:
* размещать несколько блоков на одном HTML-элементе без необходимости множить его атрибуты
* ускорить инициализацию блоков – не нужно парсить значение атрибута `class`
### Задание параметров по умолчанию
Для задания параметров по умолчанию в декларации блока или элемента необходимо переопределить метод `_getDefaultParams()`. Его результат будет объединён со значениями параметров из атрибута `data-bem` DOM-элемента, при этом параметры из атрибута будут иметь приоритет.
**Пример**
```js
modules.define('my-block', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, {
_getDefaultParams : function() {
return {
param1 : 'val1'
param2 : 'val2'
}
}
}));
});
```
```html
```
Итоговые параметры:
```js
{
param1 : 'val2',
param2 : 'val2',
param3 : 'val3'
}
```
### Доступ к параметрам из экземпляра
Доступ к параметрам из экземпляра блока и элемента можно получить через поле `this.params`.
**Пример**
```html
```
```js
modules.define('my-block', ['i-bem-dom'], function(provide, bemDom) {
provide(bemDom.declBlock(this.name, {
onSetMod : {
'js' : {
'inited': function() {
console.log(this.params); // { param1 : 'val1' }
}
}
}
}));
});
```
================================================
FILE: common.docs/i-bem-js/i-bem-js-states.en.md
================================================
States of a block
-----------------
When designing a dynamic block in BEM style, you need to provide the complete logic of
changes that occur in it as a set of **states** for the block. Then the block behavior is determined by
**triggers** — callback functions that are performed when the block switches
from one state to another.
This allows you to write the block code declaratively as a set of statements in the format: `state description` — `action performed when switching to this state`.
### Modifiers
According to the BEM methodology,
**modifiers** describe the state of a block and its elements.
A modifier indicates which of the possible states the block is in. A modifier is a **name** — **value** pair. The list of acceptable modifier values describes the set of block states. For example,
to describe a block size, you can use the `size` modifier with the possible values `s`, `m` and `l`.
A **simple modifier** is a special case when only the presence or absence
of the modifier on the block is important, and its value is insignificant. An example is the modifier describing the ”disabled“ state: `disabled`. A modifier with an unspecified `i-bem.js` value is interpreted as boolean and automatically assigned the value `true`.
Each block can have one or more modifiers set. A block isn't required to have
any modifiers. The block developer defines the list of acceptable modifiers and their
values.
Modifiers are set during [initialization of a block instance](./i-bem-js-init.en.md) (if modifiers and their values are specified in the `class` attribute of the corresponding HTML element).
Modifiers can change as part of the block functioning (for example, as a reaction to a [DOM event](i-bem-js-events.en.md#dom-event) of the block), or at the request of other blocks (see [Interaction of blocks](./i-bem-js-interact.en.md)).
When setting, deleting, and changing modifier values, [triggers](#triggers) are executed.
> **Note** If modifiers were set in a block HTML element before its initialization, the triggers to set these modifiers **are not executed**. In this case, the block instance gets its original state, and doesn't change it.
#### Managing modifiers
Methods of a block instance for working with modifiers:
* `hasMod([elem], modName, [modVal])` – Checks for the presence of a modifier. Returns `true` if the `modName` modifier is set.
* `getMod([elem], modName)` – Returns the value of `modName`.
* `getMods([elem], [...modNames])` – Returns a hash with the values of all modifiers. You can get the values of multiple modifiers by passing their names in separate arguments (`[...modNames]`). To get the modifiers of an element, you can specify the `[elem]` argument.
* `setMod([elem], modName, [modVal=true])` – Sets the `modName` modifier. If the value of `modVal` isn't specified, a *simple modifier* will be set.
* `toggleMod([elem], modName, modVal1, [modVal2], [condition])` – Toggles a modifier's value. If the `[modVal2]` argument is passed, it switches between `modVal1` and `modVal2`. If not, `modVal1` will be set and removed in turn. The `condition` argument with the `true` value allows inverting the order for toggling modifier values.
* `delMod([elem], modName)` – Deletes `modName`.
**Example**
The `changeColor` method of the `square` block toggles the `color` modifier between the values `green` and `red`, if the block has the `has-color` modifier set:
```js
BEMDOM.decl('square', {
changeColor : function(e) {
if(this.hasMod('has-color')) {
this.toggleMod('color', 'green', 'red');
}
}
});
```
The same methods allow managing modifiers of the block elements. To do this, a reference to the **element DOM node** (not the element name) is passed as the first argument.
**Example**
On a click, the `searchbox` block can assign its `input` element the simple modifier `clean` (the assumed value is `true`):
```js
BEMDOM.decl('searchbox', {
_onClick: function() {
this.setMod(this.elem('input'), 'clean');
}
});
```
> **Note** Use the API for changing the values of modifiers. Don't set modifiers by altering the CSS classes of the corresponding DOM node yourself.
For complete documentation of the API for managing modifiers, see the [JSDoc](https://en.bem.info/libs/bem-core/current/desktop/i-bem/jsdoc/) section for the `i-bem` block.
### Triggers to set modifiers
Triggers to set modifiers are executed in two phases:
1. **Before setting the modifier**. This phase is reserved for the ability to
**cancel** setting modifiers. If at least one of the triggers executed in this phase returns
`false`, modifiers are not set.
2. **After setting the modifier**. Triggers executed in this phase
can't cancel setting modifiers.
Triggers can be bound to the following types of changes to modifier values:
1. Setting *any* modifier to *any* value.
2. Setting a *specific* `modName` modifier to *any* value (including
setting a simple modifier to `true`).
3. Setting a *specific* `modName` modifier to a *specific* `modVal` value.
4. Setting a modifier to the value `''` (empty string), which is
equivalent to deleting the modifier or setting a simple modifier
to `false`.
When setting the `modName` modifier to the `modVal` value, triggers in
each phase (if they are defined) are fired in the same order as they are
listed in the list of events above (from general to specific).
Thus, when defining a trigger, the user specifies:
* The execution phase (before or after setting a modifier).
* The event type (the modifier name and value to set).
#### Execution phases
An additional phase prior to setting a modifier allows performing
certain checks without risk of affecting the logic for setting the modifier. For example, if there are mutually exclusive modifiers, it makes sense before setting one of them to check whether the other is already set.
**Example**
The `focused` modifier won't be set on the `searchbox` block if it has the `disabled` modifier.
```js
BEMDOM.decl('searchbox', {
beforeSetMod : {
'focused' : {
'true' : function() {
return !this.hasMod('disabled');
}
}
},
onSetMod : {
'focused' : {
'true' : function() { /* ... */ }
}
}
});
```
If the trigger for the phase prior to setting (`beforeSetMod`) returns `false`, the modifier is not set.
For more information about using triggers, see [Declaring triggers](i-bem-js-decl.en.md#declaring-triggers).
> **Note** The trigger to set the `js` modifier to `inited` is a constructor of a block instance, but with the value `''` it is a destructor of a block instance. For more information, see [Initialization](./i-bem-js-init.en.md).
================================================
FILE: common.docs/i-bem-js/i-bem-js-states.ru.md
================================================
## Состояния блока и элемента
Проектируя динамический блок или элемент в стиле БЭМ, нужно представить всю логику изменений, происходящих в нем, как набор **состояний**. Тогда поведение блока и элемента определяется **триггерами** — callback-функциями, которые выполняются при переходе из одного состояния в другое.
Это позволяет писать код блока в декларативном стиле как набор утверждений вида:
* «описание состояния» — «действия, выполняемые при переходе в данное состояние».
### Модификаторы
Согласно БЭМ-методологии, состояние блока и его элементов описывается **модификаторами**.
Модификатор указывает, в каком из возможных состояний находится блок или элемент. Модификатор представляет собой пару: **ключ-значение**. Список допустимых значений модификатора описывает набор состояний блока и элемента. Например, для описания размеров блока можно использовать модификатор `size` с допустимыми значениями `s`, `m` и `l`.
**Простой модификатор** — частный случай, когда важно только наличие или отсутствие модификатора у блока или элемента, а его значение несущественно. Например, модификатор, описывающий состояние «отключен»: `disabled`. Модификатор с неуказанным значением `i-bem.js` интерпретирует как булев и автоматически присваивает ему значение `true`.
Каждому блоку и элементу можно установить один или несколько модификаторов. Блок и элемент могут не иметь модификаторов. Список допустимых модификаторов и их значений определяет разработчик.
Модификаторы устанавливаются при [инициализации экземпляра](./i-bem-js-init.ru.md#Инициализация) (если модификаторы и их значения указаны в атрибуте `class` соответствующего HTML-элемента).
Модификаторы могут изменяться как в процессе работы блока и элемента (например, как реакция на [DOM-события](./i-bem-js-events.ru.md#dom-события) блока), так и по запросу из других блоков и элементов (см. раздел [Взаимодействие блоков](./i-bem-js-interact.ru.md#Взаимодействие-блоков-и-элементов)).
При установке, удалении и изменении значений модификаторов, выполняются [триггеры](#Триггеры-на-установку-модификаторов).
> **Примечание** Если модификаторы были заданы в HTML-элементе блока или элемента до момента его инициализации, триггеры на установку данных модификаторов **не выполняются**. Экземпляр в этом случае получает начальное состояние, а не меняет его.
#### Управление модификаторами
Методы экземпляра для работы с модификаторами:
* `hasMod(modName, [modVal])` – проверяет наличие модификатора. Возвращает `true`, если модификатор `modName` установлен.
* `getMod(modName)` – возвращает значение модификатора `modName`.
* `setMod(modName, [modVal=true])` – устанавливает модификатор `modName`. Если значение `modVal` не задано, будет установлен *простой модификатор*.
* `toggleMod(modName, modVal1, [modVal2], [condition])` – переключает значения модификатора. Если передан аргумент `[modVal2]`, переключение происходит между `modVal1` и `modVal2`, если нет, `modVal1` будет поочередно устанавливаться и удаляться. Аргумент `condition` в значении `true` позволяет инвертировать порядок переключения значений модификатора.
* `delMod(modName)` – удаляет модификатор `modName`.
**Пример**
```js
bemDom.declBlock('link', {
// ...
_onClick : function() {
if(!this.hasMod('disabled')) {
this._emit('click');
}
},
_onFocus : function() {
this.setMod('focused');
},
_onBlur : function() {
this.delMod('focused');
}
// ...
});
```
> **Примечание** Для изменения значений модификаторов используйте API. Не следует устанавливать модификаторы, самостоятельно изменяя CSS-классы соответствующего DOM-узла.
Полное описание API для управления модификаторами приведено в разделе [JSDoc](https://ru.bem.info/platform/i-bem/) блока `i-bem`.
### Триггеры на установку модификаторов
Выполнение триггеров на установку модификаторов разбито на две фазы:
1. **До установки модификатора**. Эта фаза зарезервирована для возможности **отменить** установку модификатора. Если хотя бы один из триггеров, выполняемых в этой фазе, вернет `false`, установки модификатора не произойдет.
2. **После установки модификатора**. Триггеры, выполняемые в этой фазе, уже не могут отменить установку модификаторов.
Триггеры могут быть привязаны к следующим типам изменений значений модификаторов:
1. Установка *любого* модификатора в *любое* значение.
2. Установка *конкретного* модификатора `modName` в *любое* значение (в том числе установка простого модификатора в значение `true` и удаление модификатора).
3. Установка *конкретного* модификатора `modName` в *конкретное* значение `modVal`.
4. Установка модификатора в значение `''` (пустая строка), что эквивалентно удалению модификатора или установке простого модификатора в значение `false`.
5. Установка *конкретного* модификатора `modName` в *любое, отличное от* конкретного значения `modVal`.
6. Установка *конкретного* модификатора `modName` из *конкретного* значения `modVal` в любое другое.
При установке модификатора `modName` в значение `modVal` триггеры каждой фазы (если они определены) вызываются в том порядке, в котором они перечислены в приведенном выше списке событий (от общего к частному).
Таким образом, при определении триггера пользователь указывает:
* фазу выполнения (до или после установки модификатора);
* тип действия (имя и устанавливаемое значение модификатора).
#### Фазы выполнения
Дополнительная фаза, предшествующая установке модификатора, позволяет
произвести некоторые проверки без риска повлиять на логику, связанную с установкой модификатора.
Например, если существуют взаимоисключающие модификаторы, перед установкой одного из них логично проверить, не установлен ли другой.
**Пример**
Модификатор `focused` не будет установлен блоку `searchbox`, если у него есть модификатор `disabled`.
```js
bemDom.declBlock('searchbox', {
beforeSetMod : {
'focused' : {
'true' : function() {
return !this.hasMod('disabled');
}
}
},
onSetMod : {
'focused' : {
'true' : function() { /* ... */ }
}
}
});
```
Если триггер для фазы, предшествующей установке (`beforeSetMod`), возвращает `false`, установка модификатора не производится.
Подробнее об использовании триггеров читайте в разделе [Декларация триггеров](./i-bem-js-decl.ru.md#Декларация-триггеров).
> **Примечание** Триггер на установку модификатора `js` в значение `inited` является конструктором экземпляра блока, а в значение `''` – деструктором экземпляра блока. Подробности смотрите в разделе [Инициализация](./i-bem-js-init.ru.md#Инициализация).
[ym]: https://github.com/ymaps/modules
[bem-tools]: https://ru.bem.info/tools/bem/
[i-bem]: https://ru.bem.info/libs/bem-core/current/desktop/i-bem/jsdoc/
[i-bem__dom]: https://ru.bem.info/libs/bem-core/current/desktop/i-bem/jsdoc/
[html]: ./i-bem-js-html-binding.ru.md
[decl]: ./i-bem-js-decl.ru.md
[dom]: ./i-bem-js-dom.ru.md
[states]: ./i-bem-js-states.ru.md
[events]: ./i-bem-js-events.ru.md
[init]: ./i-bem-js-init.ru.md
[interact]: ./i-bem-js-interact.ru.md
================================================
FILE: common.docs/i-bem-js/i-bem-js.en.md
================================================
# i-bem.js: User's guide
## i-bem.js: JavaScript framework for BEM
`i-bem.js` is a specialized JavaScript framework for web development using the [BEM methodology](https://en.bem.info/method/).
`i-bem.js` makes it possible to:
* Develop a web interface in terms of blocks, elements, and modifiers.
* Describe a block logic in declarative style, as a set of states.
* Easily integrate JavaScript code with BEMHTML or BH templates and CSS in BEM style.
* Flexibly redefine the behavior of library blocks.
`i-bem.js` is not meant to replace the general-purpose framework, like jQuery.
**What this document covers**:
* [Overview](./i-bem-js-common.en.md) of the framework: its relationship to the BEM subject domain, and a summary of the framework modular structure, a template project, and assembly tools written using `i-bem.js`.
* [Binding JS blocks to HTML](./i-bem-js-html-binding.en.md) — Markup for JS blocks in a page HTML code and the possible relationships of HTML elements to JS blocks.
* [Block declaration](./i-bem-js-decl.en.md) — Syntax for describing JS blocks.
* [Passing parameters](./i-bem-js-params.en.md) — Passing parameters to a block instance and accessing block parameters from an instance.
* [Working with the DOM tree](./i-bem-js-dom.en.md) — The API for working with DOM nodes of blocks: elements, dynamic changes to the DOM tree (using AJAX), and searching DOM nodes.
* [Block states](./i-bem-js-states.en.md) — Modifiers and triggers for state changes (setting modifiers).
* [Events](./i-bem-js-events.en.md) — The `i-bem.js` event model: DOM and BEM events and event delegation.
* [Initialization](./i-bem-js-init.en.md) — Initializing and deleting block instances; deferred and automatic initialization.
* [Interaction of blocks](./i-bem-js-interact.en.md) — Calls from a block to other blocks and classes of blocks.
* [Context](./i-bem-js-context.en.md) — Private and static properties of a block. BEMDOM static properties.
* [What next?](./i-bem-js-extras.en.md) — Links to documentation and supplemental materials.
================================================
FILE: common.docs/i-bem-js/i-bem-js.ru.md
================================================
# i-bem.js: руководство пользователя
## i-bem.js: JavaScript-фреймворк для БЭМ
`i-bem.js` — специализированный JavaScript-фреймворк для веб-разработки в рамках [БЭМ-методологии](https://ru.bem.info/methodology/).
`i-bem.js` позволяет:
* разрабатывать веб-интерфейс в терминах блоков, элементов, модификаторов;
* описывать логику работы блока в декларативном стиле — как набор состояний;
* легко интегрировать JavaScript-код с BEMHTML- или BH-шаблонами и CSS в стиле БЭМ;
* гибко переопределять поведение библиотечных блоков.
`i-bem.js` не предназначен для замены фреймворка общего назначения, подобного jQuery.
**Краткий обзор содержания документа**:
* [Общие сведения](./i-bem-js-common.ru.md) о фреймворке: связь с предметной областью БЭМ, краткое описание модульной структуры фреймворка, шаблонного проекта и инструментов для сборки кода, написанного с использованием `i-bem.js`.
* [Привязка JS-экземпляров к HTML](./i-bem-js-html-binding.ru.md) — разметка JS-блоков в HTML-коде страницы, варианты соотношения HTML-элементов и JS-экземпляров.
* [Декларация](./i-bem-js-decl.ru.md) — синтаксис описания класса блока и элемента.
* [Передача параметров](./i-bem-js-params.ru.md) — передача параметров экземпляру блока и элемента, получение доступа к параметрам из экземпляра.
* [Работа с DOM-деревом](./i-bem-js-dom.ru.md) — API для работы с DOM-узлами блоков и элементов.
* [Состояния](./i-bem-js-states.ru.md) — модификаторы, триггеры на изменение модификаторов.
* [Коллекции](./i-bem-js-collections.ru.md) — работа с несколькими экземплярами блоков или элементов.
* [События](./i-bem-js-events.ru.md) — событийная модель `i-bem.js`: DOM- и БЭМ-события.
* [Инициализация](./i-bem-js-init.ru.md) — инициализация экземпляров блоков и элементов.
* [Взаимодействие блоков](./i-bem-js-interact.ru.md) — обращение из экземпляра блока или элемента к другим экземплярам и классам.
* [Контекст](./i-bem-js-context.ru.md) — собственные и статические свойства блока и элемента. Статические свойства `i-bem-dom`.
* [Что дальше?](./i-bem-js-extras.ru.md) — ссылки на документацию и дополнительные материалы.
================================================
FILE: desktop.blocks/jquery/__config/jquery__config.deps.js
================================================
({
shouldDeps : ['ua', 'objects']
})
================================================
FILE: desktop.blocks/jquery/__config/jquery__config.js
================================================
/**
* @module jquery__config
* @description Configuration for jQuery (desktop override).
* Previously downgraded jQuery for IE < 9, no longer needed.
*/
export default function(base) {
return base;
};
================================================
FILE: desktop.blocks/jquery/__event/_type/jquery__event_type_winresize.deps.js
================================================
({
shouldDeps : ['jquery', 'ua']
})
================================================
FILE: desktop.blocks/jquery/__event/_type/jquery__event_type_winresize.js
================================================
/**
* @module jquery
*/
import ua from 'bem:ua';
import $ from 'bem:jquery';
// IE8 and below, https://msdn.microsoft.com/en-us/library/ie/ms536959%28v=vs.85%29.aspx
if(ua.msie && document.documentMode < 9) {
const win = window,
$win = $(window);
let winWidth = $win.width(),
winHeight = $win.height();
($.event.special.resize || ($.event.special.resize = {})).preDispatch = function(e) {
if(e.target === win) {
const curWinWidth = $win.width(),
curWinHeight = $win.height();
if(curWinWidth === winWidth && curWinHeight === winHeight) {
return false;
} else {
winWidth = curWinWidth;
winHeight = curWinHeight;
}
}
};
}
export default $;
================================================
FILE: desktop.blocks/page/__conditional-comment/page__conditional-comment.bemhtml.js
================================================
block('page').elem('conditional-comment')(
tag()(false),
content()(function() {
var ctx = this.ctx,
cond = ctx.condition
.replace('<', 'lt')
.replace('>', 'gt')
.replace('=', 'e'),
hasNegation = cond.indexOf('!') > -1,
includeOthers = ctx.msieOnly === false,
hasNegationOrIncludeOthers = hasNegation || includeOthers;
return [
{ html : '' } : '',
applyNext(),
hasNegationOrIncludeOthers? { html : '' }
];
})
);
================================================
FILE: desktop.blocks/page/__conditional-comment/page__conditional-comment.bh.js
================================================
module.exports = function(bh) {
bh.match('page__conditional-comment', function(ctx, json) {
ctx.tag(false);
var cond = json.condition
.replace('<', 'lt')
.replace('>', 'gt')
.replace('=', 'e'),
hasNegation = cond.indexOf('!') > -1,
includeOthers = json.msieOnly === false,
hasNegationOrIncludeOthers = hasNegation || includeOthers;
return [
{ html : '', tag : false } : '',
json,
hasNegationOrIncludeOthers? { html : '', tag : false }
];
});
};
================================================
FILE: desktop.blocks/page/__conditional-comment/page__conditional-comment.ru.md
================================================
# Элемент `conditional-comments` блока `page`
```javascript
({
block : 'page',
title : 'page__conditional-comments',
head : [
{
elem : 'conditional-comment',
condition : '<= IE 8',
content : { elem : 'css', url : '_page.ie.css' }
},
{
elem : 'conditional-comment',
condition : '! IE',
content : 'Not for IE'
},
{
elem : 'conditional-comment',
condition : '> IE 8',
msieOnly : false,
content : 'For IE9+ and all other browsers'
}
],
scripts : [
{
elem : 'conditional-comment',
condition : 'lte IE 8',
content : { elem : 'js', url : 'https://yastatic.net/es5-shims/0.0.1/es5-shims.min.js' }
}
]
})
```
================================================
FILE: desktop.blocks/page/page.deps.js
================================================
({
tech : 'tmpl-spec.js',
shouldDeps : {
elems : ['conditional-comment']
}
})
================================================
FILE: desktop.blocks/page/page.examples/.bem/level.js
================================================
exports.baseLevelPath = require.resolve('../../../../.bem/levels/examples.js');
================================================
FILE: desktop.blocks/page/page.examples/40-es5-shims.bemjson.js
================================================
({
block : 'page',
title : 'Пример подключения es5-shims для IE',
head : [
''
],
content : 'Подключение es5-shims для IE'
})
================================================
FILE: desktop.blocks/page/page.examples/40-es5-shims.ru.title.txt
================================================
Пример подключения es5-shims для IE
================================================
FILE: desktop.blocks/page/page.ru.md
================================================
# page
На уровне переопределения `desktop.blocks` блок предоставляет шаблон, создающий дополнительный HTML-элемент ` `.
## Обзор
### Специализированные поля блока
| Поле | Тип | Описание |
| ---- | --- | -------- |
| x-ua-compatible | `{String}`|`{Boolean}` | Управляет поведением создаваемого блоком HTML-элемента ` ` с атрибутом `http-equiv` `X-UA-Compatible`. |
### Элементы блока
| Элемент | Способы использования | Описание |
| ------- | --------------------- | -------- |
| css | `BEMJSON` | Элемент служит для подключения CSS. |
| conditional-comment | `BEMJSON` | Помогает использовать условные комментарии. |
### Специализированные поля элементов блока
| Элемент | Поле | Тип | Описание |
| ------- | ---- | --- | -------- |
| css | `ie` | `{String}`|`{Boolean}` | Используется для указания применимости стилей к Internet Explorer версий 6-9 и подключения специальных стилей для Internet Explorer. |
| conditional-comment | `condition` | `{String}` | Позволяет указать условие, при выполнении которого содержимое поля `content` декларации элемента, будет доступно. |
| | `msieOnly` | `{Boolean}` | Указывает, предназначен ли данный условный комментарий для использования исключительно в Internet Explorer. |
### Публичные технологии блока
Блок реализован в технологиях:
* `bh.js`
* `bemhtml`
## Подробности
Создает HTML-элемент ` ` с атрибутом `http-equiv` `X-UA-Compatible`, определяющий совместимость с юзер-агентами. По умолчанию, значением атрибута `content` элемента является `IE=edge` (совместим с последними версиями Internet Explorer).
### Специализированные поля блока
#### Поле `x-ua-compatible`
Тип: `{String}`|`{Boolean}`.
Управляет поведением создаваемого блоком HTML-элемента ` ` с атрибутом `http-equiv` `X-UA-Compatible`:
* со значением `false` HTML-элемент ` ` не будет создаваться.
*
```js
{
block : 'page',
title : 'Hello, World!',
'x-ua-compatible' : false,
content : 'Отмена создания HTML-элемента '
}
```
* строчное значение будет присвоено свойству `content` HTML-элемента ` `.
```js
{
block : 'page',
title : 'Hello, World!',
'x-ua-compatible' : 'IE=6',
content : 'Совместим с Internet Explorer 6'
}
```
### Элементы блока
#### Элемент `css`
##### Специализированное поле `ie`
Тип: `{String}`|`{Boolean}`.
Используется для указания применимости стилей к Internet Explorer версий 6-9 и подключения специальных стилей Internet Explorer.
Допустимы следующие значения:
* строка вида `'lt IE 8'` – элемент ` ` будет обернут в условные комментарии, для использования в соответствующих версиях Internet Explorer (для текущего примера `lt IE 8` – ниже восьмой версии).
* `false` – будут использоваться условные комментарии, предотвращающие использование стилей в IE 9 и ниже.
* `true` – используется в случае, если в проекте есть отдельный CSS для каждой версии Internet Explorer. Значением свойства `url`, при этом, должна быть строка с путем и именем файла без суффикса. Во время подключения создаются элементы ` ` с отдельным суффиксом для каждой версии. Другими словами, при значении `url` равном `foo.com/index` будут подключены стили `foo.com/index.ie6.css`, `foo.com/index.ie7.css` и т.д. до `...ie9.css`. При этом каждый HTML-элемент будет обернут в условный комментарий, обеспечивающий его подключение только в соответствующей версии Internet Explorer.
```js
{
block : 'page',
title : 'Page title',
head : [
{ elem : 'css', url : 'example.css', ie : false },
{ elem : 'css', url : 'example.ie.css', ie : 'lt IE 8' }
],
content : 'Страница с отдельными CSS правилами для IE'
}
```
#### Элемент `conditional-comment`
Позволяет обернуть содержимое поля `content`, определенное в BEMJSON-декларации элемента, в условные комментарии. Условие, при котором содержимое поля будет доступно, определяется специализированным полем `condition`.
```js
({
block : 'page',
title : 'page__conditional-comments',
styles :
{
elem : 'conditional-comment',
condition : '<= IE 8',
content : { elem : 'css', url : '_page.ie.css' }
},
scripts :
{
elem : 'conditional-comment',
condition : 'lte IE 8',
content : { elem : 'js', url : 'https://yastatic.net/es5-shims/0.0.1/es5-shims.min.js' }
}
})
```
##### Специализированное поле `condition`
Тип: `{String}`.
Условие, при выполнении которого содержимое поля `content` декларации элемента, будет доступно. Например, определенная версия Internet Explorer.
Значение поля составляется из:
* квантора – `>`, `<`, `=`, `<=`, `>=`, `lt`, `gt`, `e` или `!` (логическое «не»);
* слова `IE` отделенного с обеих сторон пробелами;
* номера версии (6, 7, 8, 9). Может отсутствовать, если указан квантор `!`. Тогда, значение поля `content` будет доступно для всех браузеров, кроме Internet Explorer.
```js
({
block : 'page',
head :
{
elem : 'conditional-comment',
condition : '! IE',
content : 'Not for IE'
}
})
```
##### Специализированное поле `msieOnly`
Тип: `{Boolean}`.
Указывает на то, предназначен ли данный условный комментарий для использования исключительно в Internet Explorer. Со значением `true` поле можно не указывать.
```js
({
block : 'page',
head :
{
elem : 'conditional-comment',
condition : '> IE 8',
msieOnly : false,
content : 'For IE9+ and all other browsers'
}
})
```
================================================
FILE: desktop.blocks/page/page.tmpl-specs/50-conditions.bemjson.js
================================================
({
block : 'page',
title : 'Пример подключения es5-shims для IE',
head : [
{ html : '', tag : false }
]
})
================================================
FILE: desktop.blocks/page/page.tmpl-specs/50-conditions.html
================================================
Пример подключения es5-shims для IE
================================================
FILE: desktop.blocks/page/page.tmpl-specs/60-conditional-comments.bemjson.js
================================================
({
block : 'page',
title : 'page__conditional-comments',
head : [
{
elem : 'conditional-comment',
condition : '<= IE 8',
content : { elem : 'css', url : '60-conditional-comment.ie.css' }
},
{
elem : 'conditional-comment',
condition : '! IE',
content : 'Not for IE'
},
{
elem : 'conditional-comment',
condition : '> IE 8',
msieOnly : false,
content : 'For IE9+ and all other browsers'
}
],
scripts : [
{
elem : 'conditional-comment',
condition : 'lte IE 8',
content : { elem : 'js', url : 'https://yastatic.net/es5-shims/0.0.1/es5-shims.min.js' }
}
]
})
================================================
FILE: desktop.blocks/page/page.tmpl-specs/60-conditional-comments.html
================================================
page__conditional-comments
Not for IE
For IE9+ and all other browsers
================================================
FILE: desktop.blocks/page/page.tmpl-specs/70-custom-x-ua-compatible.bemjson.js
================================================
({
block : 'page',
title : 'X-UA-Compatible',
uaCompatible : 'IE=EmulateIE8'
})
================================================
FILE: desktop.blocks/page/page.tmpl-specs/70-custom-x-ua-compatible.html
================================================
X-UA-Compatible
================================================
FILE: desktop.blocks/ua/ua.js
================================================
/**
* @module ua
* @description Detect some user agent features (works like jQuery.browser in jQuery 1.8)
* @see http://code.jquery.com/jquery-migrate-1.1.1.js
*/
const ua = navigator.userAgent.toLowerCase(),
match = /(chrome)[ /]([\w.]+)/.exec(ua) ||
/(webkit)[ /]([\w.]+)/.exec(ua) ||
/(opera)(?:.*version|)[ /]([\w.]+)/.exec(ua) ||
/(msie) ([\w.]+)/.exec(ua) ||
ua.indexOf('compatible') < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
[],
matched = {
browser : match[1] || '',
version : match[2] || '0'
},
browser = {};
if(matched.browser) {
browser[matched.browser] = true;
browser.version = matched.version;
}
if(browser.chrome) {
browser.webkit = true;
} else if(browser.webkit) {
browser.safari = true;
}
/**
* @type Object
*/
export default browser;
================================================
FILE: desktop.blocks/ua/ua.ru.md
================================================
# ua
На уровне `desktop`, блок предоставляет объект, содержащий набор свойств, указывающих особенности браузера.
## Обзор
### Свойства и методы объекта
| Имя | Тип | Описание |
| --- | -------------- | -------- |
| chrome | `{Boolean}` | Тип браузера: Google Chrome. |
| opera | `{Boolean}` | Тип браузера: Opera. |
| msie | `{Boolean}` | Тип браузера: Microsoft Internet Explorer. |
| mozilla | `{Boolean}` | Тип браузера: Mozilla Firefox. |
| safari | `{Boolean}` | Тип браузера: Safari. |
| webkit | `{Boolean}` | Браузер построен на движке WebKit. |
| version | `{String}` | Версия браузера. |
### Публичные технологии блока
Блок реализован в технологиях:
* `js`
## Подробности
Блок позволяет определить:
* Тип браузера.
* Совместимость с WebKit.
* Версию браузера.
```js
modules.require('ua', function(ua) {
console.dir(ua);
});
```
### Свойства и методы объекта
#### Свойство `chrome`
Тип: `{Boolean}`.
Тип браузера. `true`, если Google Chrome.
#### Свойство `opera`
Тип: `{Boolean}`.
Тип браузера. `true`, если Opera.
#### Свойство `msie`
Тип: `{Boolean}`.
Тип браузера. `true`, если Microsoft Internet Explorer.
#### Свойство `mozilla`
Тип: `{Boolean}`.
Тип браузера. `true`, если Mozilla Firefox.
#### Свойство `safari`
Тип: `{Boolean}`.
Тип браузера. `true`, если Safari.
#### Свойство `webkit`
Тип: `{Boolean}`.
`true`, если браузер построен на движке WebKit.
#### Свойство `version`
Тип: `{String}`.
Значение – строка с версией браузера вида `'600.2.5'` (для Safari). Если определить версию браузера не удается, в качестве значения устанавливается `'0'`.
================================================
FILE: eslint.config.js
================================================
import js from '@eslint/js';
import globals from 'globals';
export default [
{
ignores: [
'dist/',
'docs/',
'node_modules/',
'libs/',
'test/',
'common.blocks/inherit/',
'**/*.spec.js',
'**/*.tests/**',
'**/*.tmpl-specs/**',
'**/*.examples/**',
'**/*.bemhtml.js',
'**/*.bh.js',
'common.bundles/',
'docs/',
],
},
js.configs.recommended,
{
languageOptions: {
ecmaVersion: 2024,
sourceType: 'module',
globals: {
...globals.browser,
},
},
rules: {
'no-var': 'error',
'prefer-const': 'error',
'no-unused-vars': ['error', { args: 'none', caughtErrors: 'none' }],
'no-prototype-builtins': 'error',
'no-empty': ['error', { allowEmptyCatch: true }],
'no-cond-assign': 'off',
},
},
{
files: ['build/**/*.js', '**/*.test.js'],
languageOptions: {
globals: {
...globals.node,
},
},
},
];
================================================
FILE: jsdoc.config.json
================================================
{
"source": {
"include": [
"common.blocks",
"desktop.blocks",
"touch.blocks"
],
"includePattern": ".+\\.(vanilla\\.js|js)$",
"excludePattern": "(spec|test|__tests__)\\.js$"
},
"opts": {
"destination": "docs/jsdoc",
"recurse": true,
"readme": "README.md"
},
"plugins": ["plugins/markdown"],
"templates": {
"cleverLinks": true,
"monospaceLinks": false
}
}
================================================
FILE: package.json
================================================
{
"name": "bem-core",
"version": "5.0.0",
"type": "module",
"description": "bem-core Library",
"repository": {
"type": "git",
"url": "git://github.com/bem/bem-core.git"
},
"keywords": [
"bem",
"core"
],
"author": "",
"license": "MPL-2.0",
"engines": {
"node": ">=20"
},
"exports": {
".": "./dist/desktop/bem-core.mjs",
"./build/plugins/*": "./build/plugins/*",
"./*": "./*"
},
"peerDependencies": {
"jquery": "^4.0.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.58.2",
"chai": "^6.2.2",
"eslint": "^10.0.1",
"globals": "^16.5.0",
"husky": "^9.1.7",
"jquery": "^4.0.0",
"jsdoc": "^4.0.0",
"lint-staged": "^16.2.7",
"mocha": "^11.7.5",
"sinon": "^21.0.1",
"sinon-chai": "^4.0.1",
"vite": "^8.0.1"
},
"scripts": {
"lint": "eslint .",
"test": "node --test build/plugins/vite-plugin-bem-levels.test.js common.blocks/i18n/i18n.test.js",
"test:browser": "playwright test",
"test:browser:ui": "playwright test --ui",
"test:all": "npm test && npm run test:browser",
"build": "BEM_PLATFORM=desktop vite build --config build/vite.config.js && BEM_PLATFORM=touch vite build --config build/vite.config.js",
"build:desktop": "BEM_PLATFORM=desktop vite build --config build/vite.config.js",
"build:touch": "BEM_PLATFORM=touch vite build --config build/vite.config.js",
"docs": "jsdoc -c jsdoc.config.json",
"prepare": "husky"
},
"lint-staged": {
"*.js": "eslint --fix"
}
}
================================================
FILE: playwright.config.js
================================================
/* global process */
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './test',
testMatch: ['browser.spec.js'],
timeout: 60_000,
retries: 1,
use: {
browserName: 'chromium',
headless: true,
baseURL: 'http://localhost:5174',
},
webServer: {
command: 'node node_modules/.bin/vite --config build/vite.test.config.js --port 5174',
url: 'http://localhost:5174/test/browser/index.html',
reuseExistingServer: !process.env.CI,
timeout: 30_000,
},
});
================================================
FILE: test/browser/bemhtml-shim.js
================================================
/**
* Minimal BEMJSON-to-HTML converter for browser tests.
*
* Handles the BEMJSON patterns used in bem-core spec files.
* Produces HTML compatible with bemDom.init() expectations:
* – correct BEM CSS class names
* – 'i-bem' CSS class for JS-enabled blocks (so bemDom.init finds them)
* – data-bem attribute with JSON params
* – block context propagation to child elements
*/
const BEM_JS_CLASS = 'i-bem';
const BEMHTML = {
apply(bemjson, ctx) {
if (bemjson === null || bemjson === undefined) return '';
if (typeof bemjson === 'string' || typeof bemjson === 'number') {
return String(bemjson);
}
if (Array.isArray(bemjson)) {
return bemjson.map(item => this.apply(item, ctx)).join('');
}
return this._render(bemjson, ctx);
},
_render(node, ctx) {
if (!node || typeof node !== 'object') return String(node ?? '');
let {
block,
elem,
mods,
elemMods,
mix,
content,
js,
tag = 'div',
attrs = {},
cls,
} = node;
// Inherit block context from parent when only elem is specified
if (!block && elem && ctx) {
block = ctx;
}
// Determine block context for children
const childCtx = block || ctx;
const classes = [];
let dataBem = null;
let needBemClass = false;
if (block && !elem) {
// Block
classes.push(block);
if (mods) {
for (const [m, v] of Object.entries(mods)) {
if (v === true) classes.push(`${block}_${m}`);
else if (v) classes.push(`${block}_${m}_${v}`);
}
}
if (js) {
needBemClass = true;
dataBem = dataBem || {};
dataBem[block] = typeof js === 'object' ? js : {};
}
} else if (block && elem) {
// Element
const elemName = `${block}__${elem}`;
classes.push(elemName);
if (elemMods) {
for (const [m, v] of Object.entries(elemMods)) {
if (v === true) classes.push(`${elemName}_${m}`);
else if (v) classes.push(`${elemName}_${m}_${v}`);
}
}
// Elements get data-bem and i-bem if js is explicitly provided
if (js) {
needBemClass = true;
dataBem = dataBem || {};
dataBem[elemName] = typeof js === 'object' ? js : {};
}
}
// Mix
if (mix) {
const mixes = Array.isArray(mix) ? mix : [mix];
for (const m of mixes) {
if (!m) continue;
const mixBlock = m.block || block || ctx;
if (!mixBlock) continue;
const mixClass = m.elem
? `${mixBlock}__${m.elem}`
: mixBlock;
classes.push(mixClass);
// Add modifier classes for mix
if (m.mods) {
for (const [mm, mv] of Object.entries(m.mods)) {
if (mv === true) classes.push(`${mixClass}_${mm}`);
else if (mv) classes.push(`${mixClass}_${mm}_${mv}`);
}
}
if (m.elemMods) {
for (const [mm, mv] of Object.entries(m.elemMods)) {
if (mv === true) classes.push(`${mixClass}_${mm}`);
else if (mv) classes.push(`${mixClass}_${mm}_${mv}`);
}
}
if (!m.elem && m.js !== false) {
needBemClass = true;
dataBem = dataBem || {};
dataBem[mixBlock] =
m.js && typeof m.js === 'object' ? m.js : {};
} else if (m.elem && m.js) {
needBemClass = true;
dataBem = dataBem || {};
dataBem[mixClass] = typeof m.js === 'object' ? m.js : {};
}
}
}
// Add i-bem class AFTER all block/mix classes (matching real BEMHTML order)
if (needBemClass) {
classes.push(BEM_JS_CLASS);
}
// Extra CSS class
if (cls) {
classes.push(cls);
}
// Build attributes string
let attrsStr = '';
if (classes.length) {
attrsStr += ` class="${[...new Set(classes)].join(' ')}"`;
}
if (dataBem) {
attrsStr += ` data-bem='${JSON.stringify(dataBem)}'`;
}
if (attrs) {
for (const [k, v] of Object.entries(attrs)) {
attrsStr += ` ${k}="${String(v).replace(/"/g, '"')}"`;
}
}
const inner =
content !== undefined ? this.apply(content, childCtx) : '';
return `<${tag}${attrsStr}>${inner}${tag}>`;
},
};
export default BEMHTML;
================================================
FILE: test/browser/entry.js
================================================
/**
* Browser test entry point.
*
* 1. Sets up mocha (BDD globals: describe, it, before, after, beforeEach, afterEach)
* 2. Imports all bem-core blocks and registers them via the modules shim
* 3. Imports all *.spec.js files (they call modules.define('spec', …))
* 4. Resolves the 'spec' module (runs all spec factories, registering mocha tests)
* 5. Runs mocha and writes window.__testResults
*/
// ── 1. Mocha browser setup ────────────────────────────────────────────────────
// mocha.js is loaded as a classic
================================================
FILE: test/browser/modules-shim.js
================================================
/**
* Minimal synchronous ym-compatible modules shim for browser tests.
*
* Supports:
* modules.define(name, deps, factory) — normal definition
* modules.define(name, factory) — redefinition (factory receives prev value as 2nd arg)
* modules.require(deps, factory) — resolve deps synchronously, call factory
*
* Eagerly resolves first definitions when all deps are available (ym behavior).
* Supports incremental resolution: previously resolved entries are skipped on re-resolve.
*/
export function createModulesShim(preRegistered) {
// name → [{deps, factory, isRedef, resolved}]
const registry = new Map();
// name → resolved value (cache)
const resolved = new Map();
for (const [name, value] of Object.entries(preRegistered)) {
resolved.set(name, value);
}
function resolve(name) {
const entries = registry.get(name);
const hasUnresolved = entries && entries.some(e => !e.resolved);
if (resolved.has(name) && !hasUnresolved) return resolved.get(name);
if (!entries || !entries.length) {
if (resolved.has(name)) return resolved.get(name);
throw new Error(
`Module "${name}" is not defined.\nKnown: ${[
...resolved.keys(),
...registry.keys(),
].join(', ')}`
);
}
let value = resolved.get(name);
let runCount = 0;
for (const entry of entries) {
if (entry.resolved) continue;
try {
const depVals = entry.deps.map(d => resolve(d));
const thisCtx = { name };
if (entry.isRedef) {
entry.factory.call(thisCtx, v => { value = v; }, value, ...depVals);
} else {
entry.factory.call(thisCtx, v => { value = v; }, ...depVals);
}
entry.resolved = true;
runCount++;
} catch (err) {
// Log but continue — one failing spec factory must not block others
console.error(`[modules] factory #${runCount} for "${name}" threw:`, err);
}
}
resolved.set(name, value);
return value;
}
const shim = {
define(name, depsOrFactory, factoryArg) {
const isRedef = typeof depsOrFactory === 'function';
const deps = isRedef ? [] : depsOrFactory;
const factory = isRedef ? depsOrFactory : factoryArg;
const isFirst = !registry.has(name);
if (isFirst) registry.set(name, []);
registry.get(name).push({ deps, factory, isRedef, resolved: false });
// Eagerly resolve first non-redef definitions when all deps are available
if (isFirst && !isRedef && deps.every(d => resolved.has(d))) {
try { resolve(name); } catch (e) { /* ignore — will be resolved on require */ }
}
},
require(deps, factory) {
const depVals = deps.map(d => resolve(d));
factory(...depVals);
},
};
return shim;
}
================================================
FILE: test/browser.spec.js
================================================
import { test, expect } from '@playwright/test';
const MAX_ALLOWED_FAILURES = 0;
test('bem-core browser spec tests', async ({ page }) => {
const pageErrors = [];
page.on('pageerror', err => {
pageErrors.push(`[pageerror] ${err.message}`);
});
page.on('console', msg => {
if (msg.type() === 'warn' || msg.type() === 'error') {
console.log(`[browser:${msg.type()}] ${msg.text()}`);
}
});
await page.goto('/test/browser/index.html');
// Wait for mocha to finish (window.__testResults is set in the run callback)
await page.waitForFunction(
() => window.__testResults !== undefined,
{ timeout: 50_000 }
).catch(async () => {
const errors = await page.evaluate(() => window.__testFailures ?? []);
const info = [
...pageErrors,
...errors.map(f => `FAIL: ${f.title}\n ${f.err}`),
].join('\n');
throw new Error(`Mocha did not finish within timeout.\n${info}`);
});
const { failures, total, passed, pending } =
await page.evaluate(() => window.__testResults);
// Ensure tests actually ran
expect(total, 'mocha should have run tests').toBeGreaterThan(400);
if (failures > MAX_ALLOWED_FAILURES) {
const failDetails = await page.evaluate(() => window.__testFailures ?? []);
const details = failDetails
.map(f => ` ✗ ${f.title}\n ${f.err}`)
.join('\n');
expect(
failures,
`Too many failures: ${failures}/${total} (max allowed: ${MAX_ALLOWED_FAILURES}).\n` +
`Passed: ${passed}, pending: ${pending}\n${details}`
).toBeLessThanOrEqual(MAX_ALLOWED_FAILURES);
}
console.log(`Browser tests: ${passed} passed, ${failures} failed, ${total} total`);
expect(failures).toBeLessThanOrEqual(MAX_ALLOWED_FAILURES);
});
================================================
FILE: test/dist/assets/test.html
================================================
${ scripts }
================================================
FILE: test/dist/build-fixtures.js
================================================
var EOL = require('os').EOL,
fs = require('fs'),
path = require('path'),
walk = require('bem-walk'),
borschik = require('borschik').api,
sets = require('./config').sets,
platforms = Object.keys(sets),
fixturesDir = path.join(__dirname, 'fixtures'),
blocksDir = path.resolve('libs/bem-pr/spec.blocks'),
asset = fs.readFileSync(path.join(__dirname, 'assets', 'test.html'), 'utf-8');
if(!fs.existsSync(fixturesDir)) {
fs.mkdirSync(fixturesDir);
}
[
'mocha.css',
'mocha.js',
'chai.js',
'sinon.js',
'sinon-chai.js'
].map(function (basename) {
var name = basename.split('.')[0];
borschik({
input : path.join(blocksDir, name, basename),
output : path.join(fixturesDir, basename)
});
});
platforms.forEach(function (platform) {
var levels = sets[platform],
config = {
levels : levels.map(function (levelname) {
return { path : levelname };
})
},
walker = walk(levels, config),
specs = [];
walker.on('data', function (data) {
if(data.tech === 'spec.js') {
specs.push(data.path);
}
});
walker.on('end', function () {
html = buildHTML(platform, specs);
fs.writeFileSync(path.join(fixturesDir, platform + '.html'), html, 'utf-8');
});
});
function buildHTML(platform, specs) {
return asset
.replace(/\${ platform }/g, platform)
.replace(/\${ scripts }/g, specs.map(function (url) {
return ' ';
}).join(EOL));
}
================================================
FILE: test/dist/config.js
================================================
module.exports = {
sets : {
desktop : [
'common.blocks',
'desktop.blocks'
],
'touch' : [
'common.blocks',
'touch.blocks'
]
}
};
================================================
FILE: test/dist/fixtures/chai.js
================================================
!function(e,t){var i=t.call(e);"object"==typeof modules?modules.define("chai",function(e){e(i)}):e.chai=i}(this,function(){return function(){function require(e){var t=require.modules[e];if(!t)throw new Error('failed to require "'+e+'"');return"exports"in t||"function"!=typeof t.definition||(t.client=t.component=!0,t.definition.call(this,t.exports={},t),delete t.definition),t.exports}require.loader="component",require.helper={},require.helper.semVerSort=function(e,t){for(var i=e.version.split("."),r=t.version.split("."),n=0;ns?1:-1;var a=i[n].substr((""+o).length),c=r[n].substr((""+s).length);if(""===a&&""!==c)return 1;if(""!==a&&""===c)return-1;if(""!==a&&""!==c)return a>c?1:-1}return 0},require.latest=function(e,t){function i(e){throw new Error('failed to find latest module of "'+e+'"')}var r=/(.*)~(.*)@v?(\d+\.\d+\.\d+[^\/]*)$/,n=/(.*)~(.*)/;n.test(e)||i(e);for(var o=Object.keys(require.modules),s=[],a=[],c=0;c0){var f=s.sort(require.helper.semVerSort).pop().name;return t===!0?f:require(f)}var f=a.pop().name;return t===!0?f:require(f)},require.modules={},require.register=function(e,t){require.modules[e]={definition:t}},require.define=function(e,t){require.modules[e]={exports:t}},require.register("chaijs~assertion-error@1.0.0",function(e,t){function i(){function e(e,i){Object.keys(i).forEach(function(r){~t.indexOf(r)||(e[r]=i[r])})}var t=[].slice.call(arguments);return function(){for(var t=[].slice.call(arguments),i=0,r={};i=0;n--)if(h=o[n],!i(e[h],t[h],r))return!1;return!0}var p,d=require("chaijs~type-detect@0.1.1");try{p=require("buffer").Buffer}catch(b){p={},p.isBuffer=function(){return!1}}t.exports=i}),require.register("chai",function(e,t){t.exports=require("chai/lib/chai.js")}),require.register("chai/lib/chai.js",function(e,t){var i=[],e=t.exports={};e.version="1.10.0",e.AssertionError=require("chaijs~assertion-error@1.0.0");var r=require("chai/lib/chai/utils/index.js");e.use=function(e){return~i.indexOf(e)||(e(this,r),i.push(e)),this};var n=require("chai/lib/chai/config.js");e.config=n;var o=require("chai/lib/chai/assertion.js");e.use(o);var s=require("chai/lib/chai/core/assertions.js");e.use(s);var a=require("chai/lib/chai/interface/expect.js");e.use(a);var c=require("chai/lib/chai/interface/should.js");e.use(c);var u=require("chai/lib/chai/interface/assert.js");e.use(u)}),require.register("chai/lib/chai/assertion.js",function(e,t){var i=require("chai/lib/chai/config.js"),r=function(){};t.exports=function(e,t){function n(e,t,i){s(this,"ssfi",i||arguments.callee),s(this,"object",e),s(this,"message",t)}var o=e.AssertionError,s=t.flag;e.Assertion=n,Object.defineProperty(n,"includeStack",{get:function(){return console.warn("Assertion.includeStack is deprecated, use chai.config.includeStack instead."),i.includeStack},set:function(e){console.warn("Assertion.includeStack is deprecated, use chai.config.includeStack instead."),i.includeStack=e}}),Object.defineProperty(n,"showDiff",{get:function(){return console.warn("Assertion.showDiff is deprecated, use chai.config.showDiff instead."),i.showDiff},set:function(e){console.warn("Assertion.showDiff is deprecated, use chai.config.showDiff instead."),i.showDiff=e}}),n.addProperty=function(e,i){t.addProperty(this.prototype,e,i)},n.addMethod=function(e,i){t.addMethod(this.prototype,e,i)},n.addChainableMethod=function(e,i,r){t.addChainableMethod(this.prototype,e,i,r)},n.addChainableNoop=function(e,i){t.addChainableMethod(this.prototype,e,r,i)},n.overwriteProperty=function(e,i){t.overwriteProperty(this.prototype,e,i)},n.overwriteMethod=function(e,i){t.overwriteMethod(this.prototype,e,i)},n.overwriteChainableMethod=function(e,i,r){t.overwriteChainableMethod(this.prototype,e,i,r)},n.prototype.assert=function(e,r,n,a,c,u){var h=t.test(this,arguments);if(!0!==u&&(u=!1),!0!==i.showDiff&&(u=!1),!h){var r=t.getMessage(this,arguments),l=t.getActual(this,arguments);throw new o(r,{actual:l,expected:a,showDiff:u},i.includeStack?this.assert:s(this,"ssfi"))}},Object.defineProperty(n.prototype,"_obj",{get:function(){return s(this,"object")},set:function(e){s(this,"object",e)}})}}),require.register("chai/lib/chai/config.js",function(e,t){t.exports={includeStack:!1,showDiff:!0,truncateThreshold:40}}),require.register("chai/lib/chai/core/assertions.js",function(e,t){t.exports=function(e,t){function i(e,i){i&&x(this,"message",i),e=e.toLowerCase();var r=x(this,"object"),n=~["a","e","i","o","u"].indexOf(e.charAt(0))?"an ":"a ";this.assert(e===t.type(r),"expected #{this} to be "+n+e,"expected #{this} not to be "+n+e)}function r(){x(this,"contains",!0)}function n(e,i){i&&x(this,"message",i);var r=x(this,"object"),n=!1;if("array"===t.type(r)&&"object"===t.type(e)){for(var o in r)if(t.eql(r[o],e)){n=!0;break}}else if("object"===t.type(e)){if(!x(this,"negate")){for(var s in e)new j(r).property(s,e[s]);return}var a={};for(var s in e)a[s]=r[s];n=t.eql(a,e)}else n=r&&~r.indexOf(e);this.assert(n,"expected #{this} to include "+t.inspect(e),"expected #{this} to not include "+t.inspect(e))}function o(){var e=x(this,"object"),t=Object.prototype.toString.call(e);this.assert("[object Arguments]"===t,"expected #{this} to be arguments but got "+t,"expected #{this} to not be arguments")}function s(e,t){t&&x(this,"message",t);var i=x(this,"object");return x(this,"deep")?this.eql(e):void this.assert(e===i,"expected #{this} to equal #{exp}","expected #{this} to not equal #{exp}",e,this._obj,!0)}function a(e,i){i&&x(this,"message",i),this.assert(t.eql(e,x(this,"object")),"expected #{this} to deeply equal #{exp}","expected #{this} to not deeply equal #{exp}",e,this._obj,!0)}function c(e,t){t&&x(this,"message",t);var i=x(this,"object");if(x(this,"doLength")){new j(i,t).to.have.property("length");var r=i.length;this.assert(r>e,"expected #{this} to have a length above #{exp} but got #{act}","expected #{this} to not have a length above #{exp}",e,r)}else this.assert(i>e,"expected #{this} to be above "+e,"expected #{this} to be at most "+e)}function u(e,t){t&&x(this,"message",t);var i=x(this,"object");if(x(this,"doLength")){new j(i,t).to.have.property("length");var r=i.length;this.assert(r>=e,"expected #{this} to have a length at least #{exp} but got #{act}","expected #{this} to have a length below #{exp}",e,r)}else this.assert(i>=e,"expected #{this} to be at least "+e,"expected #{this} to be below "+e)}function h(e,t){t&&x(this,"message",t);var i=x(this,"object");if(x(this,"doLength")){new j(i,t).to.have.property("length");var r=i.length;this.assert(e>r,"expected #{this} to have a length below #{exp} but got #{act}","expected #{this} to not have a length below #{exp}",e,r)}else this.assert(e>i,"expected #{this} to be below "+e,"expected #{this} to be at least "+e)}function l(e,t){t&&x(this,"message",t);var i=x(this,"object");if(x(this,"doLength")){new j(i,t).to.have.property("length");var r=i.length;this.assert(e>=r,"expected #{this} to have a length at most #{exp} but got #{act}","expected #{this} to have a length above #{exp}",e,r)}else this.assert(e>=i,"expected #{this} to be at most "+e,"expected #{this} to be above "+e)}function f(e,i){i&&x(this,"message",i);var r=t.getName(e);this.assert(x(this,"object")instanceof e,"expected #{this} to be an instance of "+r,"expected #{this} to not be an instance of "+r)}function p(e,i){i&&x(this,"message",i);var r=x(this,"object");this.assert(r.hasOwnProperty(e),"expected #{this} to have own property "+t.inspect(e),"expected #{this} to not have own property "+t.inspect(e))}function d(){x(this,"doLength",!0)}function b(e,t){t&&x(this,"message",t);var i=x(this,"object");new j(i,t).to.have.property("length");var r=i.length;this.assert(r==e,"expected #{this} to have a length of #{exp} but got #{act}","expected #{this} to not have a length of #{act}",e,r)}function g(e){var i,r=x(this,"object"),n=!0;if(e=e instanceof Array?e:Array.prototype.slice.call(arguments),!e.length)throw new Error("keys required");var o=Object.keys(r),s=e,a=e.length;if(n=e.every(function(e){return~o.indexOf(e)}),x(this,"negate")||x(this,"contains")||(n=n&&e.length==o.length),a>1){e=e.map(function(e){return t.inspect(e)});var c=e.pop();i=e.join(", ")+", and "+c}else i=t.inspect(e[0]);i=(a>1?"keys ":"key ")+i,i=(x(this,"contains")?"contain ":"have ")+i,this.assert(n,"expected #{this} to "+i,"expected #{this} to not "+i,s.sort(),o.sort(),!0)}function v(e,i,r){r&&x(this,"message",r);var n=x(this,"object");new j(n,r).is.a("function");var o=!1,s=null,a=null,c=null;0===arguments.length?(i=null,e=null):e&&(e instanceof RegExp||"string"==typeof e)?(i=e,e=null):e&&e instanceof Error?(s=e,e=null,i=null):"function"==typeof e?(a=e.prototype.name||e.name,"Error"===a&&e!==Error&&(a=(new e).name)):e=null;try{n()}catch(u){if(s)return this.assert(u===s,"expected #{this} to throw #{exp} but #{act} was thrown","expected #{this} to not throw #{exp}",s instanceof Error?s.toString():s,u instanceof Error?u.toString():u),x(this,"object",u),this;if(e&&(this.assert(u instanceof e,"expected #{this} to throw #{exp} but #{act} was thrown","expected #{this} to not throw #{exp} but #{act} was thrown",a,u instanceof Error?u.toString():u),!i))return x(this,"object",u),this;var h="object"===t.type(u)&&"message"in u?u.message:""+u;if(null!=h&&i&&i instanceof RegExp)return this.assert(i.exec(h),"expected #{this} to throw error matching #{exp} but got #{act}","expected #{this} to throw error not matching #{exp}",i,h),x(this,"object",u),this;if(null!=h&&i&&"string"==typeof i)return this.assert(~h.indexOf(i),"expected #{this} to throw error including #{exp} but got #{act}","expected #{this} to throw error not including #{act}",i,h),x(this,"object",u),this;o=!0,c=u}var l="",f=null!==a?a:s?"#{exp}":"an error";o&&(l=" but #{act} was thrown"),this.assert(o===!0,"expected #{this} to throw "+f+l,"expected #{this} to not throw "+f+l,s instanceof Error?s.toString():s,c instanceof Error?c.toString():c),x(this,"object",c)}function y(e,t,i){return e.every(function(e){return i?t.some(function(t){return i(e,t)}):-1!==t.indexOf(e)})}var j=e.Assertion,x=(Object.prototype.toString,t.flag);["to","be","been","is","and","has","have","with","that","at","of","same"].forEach(function(e){j.addProperty(e,function(){return this})}),j.addProperty("not",function(){x(this,"negate",!0)}),j.addProperty("deep",function(){x(this,"deep",!0)}),j.addChainableMethod("an",i),j.addChainableMethod("a",i),j.addChainableMethod("include",n,r),j.addChainableMethod("contain",n,r),j.addChainableNoop("ok",function(){this.assert(x(this,"object"),"expected #{this} to be truthy","expected #{this} to be falsy")}),j.addChainableNoop("true",function(){this.assert(!0===x(this,"object"),"expected #{this} to be true","expected #{this} to be false",!this.negate)}),j.addChainableNoop("false",function(){this.assert(!1===x(this,"object"),"expected #{this} to be false","expected #{this} to be true",!!this.negate)}),j.addChainableNoop("null",function(){this.assert(null===x(this,"object"),"expected #{this} to be null","expected #{this} not to be null")}),j.addChainableNoop("undefined",function(){this.assert(void 0===x(this,"object"),"expected #{this} to be undefined","expected #{this} not to be undefined")}),j.addChainableNoop("exist",function(){this.assert(null!=x(this,"object"),"expected #{this} to exist","expected #{this} to not exist")}),j.addChainableNoop("empty",function(){var e=x(this,"object"),t=e;Array.isArray(e)||"string"==typeof object?t=e.length:"object"==typeof e&&(t=Object.keys(e).length),this.assert(!t,"expected #{this} to be empty","expected #{this} not to be empty")}),j.addChainableNoop("arguments",o),j.addChainableNoop("Arguments",o),j.addMethod("equal",s),j.addMethod("equals",s),j.addMethod("eq",s),j.addMethod("eql",a),j.addMethod("eqls",a),j.addMethod("above",c),j.addMethod("gt",c),j.addMethod("greaterThan",c),j.addMethod("least",u),j.addMethod("gte",u),j.addMethod("below",h),j.addMethod("lt",h),j.addMethod("lessThan",h),j.addMethod("most",l),j.addMethod("lte",l),j.addMethod("within",function(e,t,i){i&&x(this,"message",i);var r=x(this,"object"),n=e+".."+t;if(x(this,"doLength")){new j(r,i).to.have.property("length");var o=r.length;this.assert(o>=e&&t>=o,"expected #{this} to have a length within "+n,"expected #{this} to not have a length within "+n)}else this.assert(r>=e&&t>=r,"expected #{this} to be within "+n,"expected #{this} to not be within "+n)}),j.addMethod("instanceof",f),j.addMethod("instanceOf",f),j.addMethod("property",function(e,i,r){r&&x(this,"message",r);var n=x(this,"deep")?"deep property ":"property ",o=x(this,"negate"),s=x(this,"object"),a=x(this,"deep")?t.getPathValue(e,s):s[e];if(o&&void 0!==i){if(void 0===a)throw r=null!=r?r+": ":"",new Error(r+t.inspect(s)+" has no "+n+t.inspect(e))}else this.assert(void 0!==a,"expected #{this} to have a "+n+t.inspect(e),"expected #{this} to not have "+n+t.inspect(e));void 0!==i&&this.assert(i===a,"expected #{this} to have a "+n+t.inspect(e)+" of #{exp}, but got #{act}","expected #{this} to not have a "+n+t.inspect(e)+" of #{act}",i,a),x(this,"object",a)}),j.addMethod("ownProperty",p),j.addMethod("haveOwnProperty",p),j.addChainableMethod("length",b,d),j.addMethod("lengthOf",b),j.addMethod("match",function(e,t){t&&x(this,"message",t);var i=x(this,"object");this.assert(e.exec(i),"expected #{this} to match "+e,"expected #{this} not to match "+e)}),j.addMethod("string",function(e,i){i&&x(this,"message",i);var r=x(this,"object");new j(r,i).is.a("string"),this.assert(~r.indexOf(e),"expected #{this} to contain "+t.inspect(e),"expected #{this} to not contain "+t.inspect(e))}),j.addMethod("keys",g),j.addMethod("key",g),j.addMethod("throw",v),j.addMethod("throws",v),j.addMethod("Throw",v),j.addMethod("respondTo",function(e,i){i&&x(this,"message",i);var r=x(this,"object"),n=x(this,"itself"),o="function"!==t.type(r)||n?r[e]:r.prototype[e];this.assert("function"==typeof o,"expected #{this} to respond to "+t.inspect(e),"expected #{this} to not respond to "+t.inspect(e))}),j.addProperty("itself",function(){x(this,"itself",!0)}),j.addMethod("satisfy",function(e,i){i&&x(this,"message",i);var r=x(this,"object"),n=e(r);this.assert(n,"expected #{this} to satisfy "+t.objDisplay(e),"expected #{this} to not satisfy"+t.objDisplay(e),!this.negate,n)}),j.addMethod("closeTo",function(e,i,r){r&&x(this,"message",r);var n=x(this,"object");if(new j(n,r).is.a("number"),"number"!==t.type(e)||"number"!==t.type(i))throw new Error("the arguments to closeTo must be numbers");this.assert(Math.abs(n-e)<=i,"expected #{this} to be close to "+e+" +/- "+i,"expected #{this} not to be close to "+e+" +/- "+i)}),j.addMethod("members",function(e,i){i&&x(this,"message",i);var r=x(this,"object");new j(r).to.be.an("array"),new j(e).to.be.an("array");var n=x(this,"deep")?t.eql:void 0;return x(this,"contains")?this.assert(y(e,r,n),"expected #{this} to be a superset of #{act}","expected #{this} to not be a superset of #{act}",r,e):void this.assert(y(r,e,n)&&y(e,r,n),"expected #{this} to have the same members as #{act}","expected #{this} to not have the same members as #{act}",r,e)})}}),require.register("chai/lib/chai/interface/assert.js",function(exports,module){module.exports=function(chai,util){var Assertion=chai.Assertion,flag=util.flag,assert=chai.assert=function(e,t){var i=new Assertion(null,null,chai.assert);i.assert(e,t,"[ negation message unavailable ]")};assert.fail=function(e,t,i,r){throw i=i||"assert.fail()",new chai.AssertionError(i,{actual:e,expected:t,operator:r},assert.fail)},assert.ok=function(e,t){new Assertion(e,t).is.ok},assert.notOk=function(e,t){new Assertion(e,t).is.not.ok},assert.equal=function(e,t,i){var r=new Assertion(e,i,assert.equal);r.assert(t==flag(r,"object"),"expected #{this} to equal #{exp}","expected #{this} to not equal #{act}",t,e)},assert.notEqual=function(e,t,i){var r=new Assertion(e,i,assert.notEqual);r.assert(t!=flag(r,"object"),"expected #{this} to not equal #{exp}","expected #{this} to equal #{act}",t,e)},assert.strictEqual=function(e,t,i){new Assertion(e,i).to.equal(t)},assert.notStrictEqual=function(e,t,i){new Assertion(e,i).to.not.equal(t)},assert.deepEqual=function(e,t,i){new Assertion(e,i).to.eql(t)},assert.notDeepEqual=function(e,t,i){new Assertion(e,i).to.not.eql(t)},assert.isTrue=function(e,t){new Assertion(e,t).is["true"]},assert.isFalse=function(e,t){new Assertion(e,t).is["false"]},assert.isNull=function(e,t){new Assertion(e,t).to.equal(null)},assert.isNotNull=function(e,t){new Assertion(e,t).to.not.equal(null)},assert.isUndefined=function(e,t){new Assertion(e,t).to.equal(void 0)},assert.isDefined=function(e,t){new Assertion(e,t).to.not.equal(void 0)},assert.isFunction=function(e,t){new Assertion(e,t).to.be.a("function")},assert.isNotFunction=function(e,t){new Assertion(e,t).to.not.be.a("function")},assert.isObject=function(e,t){new Assertion(e,t).to.be.a("object")},assert.isNotObject=function(e,t){new Assertion(e,t).to.not.be.a("object")},assert.isArray=function(e,t){new Assertion(e,t).to.be.an("array")},assert.isNotArray=function(e,t){new Assertion(e,t).to.not.be.an("array")},assert.isString=function(e,t){new Assertion(e,t).to.be.a("string")},assert.isNotString=function(e,t){new Assertion(e,t).to.not.be.a("string")},assert.isNumber=function(e,t){new Assertion(e,t).to.be.a("number")},assert.isNotNumber=function(e,t){new Assertion(e,t).to.not.be.a("number")},assert.isBoolean=function(e,t){new Assertion(e,t).to.be.a("boolean")},assert.isNotBoolean=function(e,t){new Assertion(e,t).to.not.be.a("boolean")},assert.typeOf=function(e,t,i){new Assertion(e,i).to.be.a(t)},assert.notTypeOf=function(e,t,i){new Assertion(e,i).to.not.be.a(t)},assert.instanceOf=function(e,t,i){new Assertion(e,i).to.be.instanceOf(t)},assert.notInstanceOf=function(e,t,i){new Assertion(e,i).to.not.be.instanceOf(t)},assert.include=function(e,t,i){new Assertion(e,i,assert.include).include(t)},assert.notInclude=function(e,t,i){new Assertion(e,i,assert.notInclude).not.include(t)},assert.match=function(e,t,i){new Assertion(e,i).to.match(t)},assert.notMatch=function(e,t,i){new Assertion(e,i).to.not.match(t)},assert.property=function(e,t,i){new Assertion(e,i).to.have.property(t)},assert.notProperty=function(e,t,i){new Assertion(e,i).to.not.have.property(t)},assert.deepProperty=function(e,t,i){new Assertion(e,i).to.have.deep.property(t)},assert.notDeepProperty=function(e,t,i){new Assertion(e,i).to.not.have.deep.property(t)},assert.propertyVal=function(e,t,i,r){new Assertion(e,r).to.have.property(t,i)},assert.propertyNotVal=function(e,t,i,r){new Assertion(e,r).to.not.have.property(t,i)},assert.deepPropertyVal=function(e,t,i,r){new Assertion(e,r).to.have.deep.property(t,i)},assert.deepPropertyNotVal=function(e,t,i,r){new Assertion(e,r).to.not.have.deep.property(t,i)},assert.lengthOf=function(e,t,i){new Assertion(e,i).to.have.length(t)},assert.Throw=function(e,t,i,r){("string"==typeof t||t instanceof RegExp)&&(i=t,t=null);var n=new Assertion(e,r).to.Throw(t,i);return flag(n,"object")},assert.doesNotThrow=function(e,t,i){"string"==typeof t&&(i=t,t=null),new Assertion(e,i).to.not.Throw(t)},assert.operator=function(val,operator,val2,msg){if(!~["==","===",">",">=","<","<=","!=","!=="].indexOf(operator))throw new Error('Invalid operator "'+operator+'"');var test=new Assertion(eval(val+operator+val2),msg);test.assert(!0===flag(test,"object"),"expected "+util.inspect(val)+" to be "+operator+" "+util.inspect(val2),"expected "+util.inspect(val)+" to not be "+operator+" "+util.inspect(val2))},assert.closeTo=function(e,t,i,r){new Assertion(e,r).to.be.closeTo(t,i)},assert.sameMembers=function(e,t,i){new Assertion(e,i).to.have.same.members(t)},assert.includeMembers=function(e,t,i){new Assertion(e,i).to.include.members(t)},assert.ifError=function(e,t){new Assertion(e,t).to.not.be.ok},function e(t,i){return assert[i]=assert[t],e}("Throw","throw")("Throw","throws")}}),require.register("chai/lib/chai/interface/expect.js",function(e,t){t.exports=function(e,t){e.expect=function(t,i){return new e.Assertion(t,i)}}}),require.register("chai/lib/chai/interface/should.js",function(e,t){t.exports=function(e,t){function i(){function e(){return this instanceof String||this instanceof Number?new r(this.constructor(this),null,e):this instanceof Boolean?new r(1==this,null,e):new r(this,null,e)}function t(e){Object.defineProperty(this,"should",{value:e,enumerable:!0,configurable:!0,writable:!0})}Object.defineProperty(Object.prototype,"should",{set:t,get:e,configurable:!0});var i={};return i.equal=function(e,t,i){new r(e,i).to.equal(t)},i.Throw=function(e,t,i,n){new r(e,n).to.Throw(t,i)},i.exist=function(e,t){new r(e,t).to.exist},i.not={},i.not.equal=function(e,t,i){new r(e,i).to.not.equal(t)},i.not.Throw=function(e,t,i,n){new r(e,n).to.not.Throw(t,i)},i.not.exist=function(e,t){new r(e,t).to.not.exist},i["throw"]=i.Throw,i.not["throw"]=i.not.Throw,i}var r=e.Assertion;e.should=i,e.Should=i}}),require.register("chai/lib/chai/utils/addChainableMethod.js",function(e,t){var i=require("chai/lib/chai/utils/transferFlags.js"),r=require("chai/lib/chai/utils/flag.js"),n=require("chai/lib/chai/config.js"),o="__proto__"in Object,s=/^(?:length|name|arguments|caller)$/,a=Function.prototype.call,c=Function.prototype.apply;t.exports=function(e,t,u,h){"function"!=typeof h&&(h=function(){});var l={method:u,chainingBehavior:h};e.__methods||(e.__methods={}),e.__methods[t]=l,Object.defineProperty(e,t,{get:function(){l.chainingBehavior.call(this);var t=function f(){var e=r(this,"ssfi");e&&n.includeStack===!1&&r(this,"ssfi",f);var t=l.method.apply(this,arguments);return void 0===t?this:t};if(o){var u=t.__proto__=Object.create(this);u.call=a,u.apply=c}else{var h=Object.getOwnPropertyNames(e);h.forEach(function(i){if(!s.test(i)){var r=Object.getOwnPropertyDescriptor(e,i);Object.defineProperty(t,i,r)}})}return i(this,t),t},configurable:!0})}}),require.register("chai/lib/chai/utils/addMethod.js",function(e,t){var i=require("chai/lib/chai/config.js"),r=require("chai/lib/chai/utils/flag.js");t.exports=function(e,t,n){e[t]=function(){var o=r(this,"ssfi");o&&i.includeStack===!1&&r(this,"ssfi",e[t]);var s=n.apply(this,arguments);return void 0===s?this:s}}}),require.register("chai/lib/chai/utils/addProperty.js",function(e,t){t.exports=function(e,t,i){Object.defineProperty(e,t,{get:function(){var e=i.call(this);return void 0===e?this:e},configurable:!0})}}),require.register("chai/lib/chai/utils/flag.js",function(e,t){t.exports=function(e,t,i){var r=e.__flags||(e.__flags=Object.create(null));return 3!==arguments.length?r[t]:void(r[t]=i)}}),require.register("chai/lib/chai/utils/getActual.js",function(e,t){t.exports=function(e,t){return t.length>4?t[4]:e._obj}}),require.register("chai/lib/chai/utils/getEnumerableProperties.js",function(e,t){t.exports=function(e){var t=[];for(var i in e)t.push(i);return t}}),require.register("chai/lib/chai/utils/getMessage.js",function(e,t){var i=require("chai/lib/chai/utils/flag.js"),r=require("chai/lib/chai/utils/getActual.js"),n=(require("chai/lib/chai/utils/inspect.js"),require("chai/lib/chai/utils/objDisplay.js"));t.exports=function(e,t){var o=i(e,"negate"),s=i(e,"object"),a=t[3],c=r(e,t),u=o?t[2]:t[1],h=i(e,"message");return"function"==typeof u&&(u=u()),u=u||"",u=u.replace(/#{this}/g,n(s)).replace(/#{act}/g,n(c)).replace(/#{exp}/g,n(a)),h?h+": "+u:u}}),require.register("chai/lib/chai/utils/getName.js",function(e,t){t.exports=function(e){if(e.name)return e.name;var t=/^\s?function ([^(]*)\(/.exec(e);return t&&t[1]?t[1]:""}}),require.register("chai/lib/chai/utils/getPathValue.js",function(e,t){function i(e){var t=e.replace(/\[/g,".["),i=t.match(/(\\\.|[^.]+?)+/g);return i.map(function(e){var t=/\[(\d+)\]$/,i=t.exec(e);return i?{i:parseFloat(i[1])}:{p:e}})}function r(e,t){for(var i,r=t,n=0,o=e.length;o>n;n++){var s=e[n];r?("undefined"!=typeof s.p?r=r[s.p]:"undefined"!=typeof s.i&&(r=r[s.i]),n==o-1&&(i=r)):i=void 0}return i}t.exports=function(e,t){var n=i(e);return r(n,t)}}),require.register("chai/lib/chai/utils/getProperties.js",function(e,t){t.exports=function(e){function t(e){-1===i.indexOf(e)&&i.push(e)}for(var i=Object.getOwnPropertyNames(subject),r=Object.getPrototypeOf(subject);null!==r;)Object.getOwnPropertyNames(r).forEach(t),r=Object.getPrototypeOf(r);return i}}),require.register("chai/lib/chai/utils/index.js",function(e,t){var e=t.exports={};e.test=require("chai/lib/chai/utils/test.js"),e.type=require("chai/lib/chai/utils/type.js"),e.getMessage=require("chai/lib/chai/utils/getMessage.js"),e.getActual=require("chai/lib/chai/utils/getActual.js"),e.inspect=require("chai/lib/chai/utils/inspect.js"),e.objDisplay=require("chai/lib/chai/utils/objDisplay.js"),e.flag=require("chai/lib/chai/utils/flag.js"),e.transferFlags=require("chai/lib/chai/utils/transferFlags.js"),e.eql=require("chaijs~deep-eql@0.1.3"),e.getPathValue=require("chai/lib/chai/utils/getPathValue.js"),e.getName=require("chai/lib/chai/utils/getName.js"),e.addProperty=require("chai/lib/chai/utils/addProperty.js"),e.addMethod=require("chai/lib/chai/utils/addMethod.js"),e.overwriteProperty=require("chai/lib/chai/utils/overwriteProperty.js"),e.overwriteMethod=require("chai/lib/chai/utils/overwriteMethod.js"),e.addChainableMethod=require("chai/lib/chai/utils/addChainableMethod.js"),e.overwriteChainableMethod=require("chai/lib/chai/utils/overwriteChainableMethod.js")}),require.register("chai/lib/chai/utils/inspect.js",function(e,t){function i(e,t,i,n){var o={showHidden:t,seen:[],stylize:function(e){return e}};return r(o,e,"undefined"==typeof i?2:i)}function r(t,i,p){if(i&&"function"==typeof i.inspect&&i.inspect!==e.inspect&&(!i.constructor||i.constructor.prototype!==i)){var y=i.inspect(p);return"string"!=typeof y&&(y=r(t,y,p)),y}var j=n(t,i);if(j)return j;if(v(i)){if("outerHTML"in i)return i.outerHTML;try{if(document.xmlVersion){var x=new XMLSerializer;return x.serializeToString(i)}var w="http://www.w3.org/1999/xhtml",m=document.createElementNS(w,"_");return m.appendChild(i.cloneNode(!1)),html=m.innerHTML.replace("><",">"+i.innerHTML+"<"),m.innerHTML="",html}catch(q){}}var A=g(i),O=t.showHidden?b(i):A;if(0===O.length||f(i)&&(1===O.length&&"stack"===O[0]||2===O.length&&"description"===O[0]&&"stack"===O[1])){if("function"==typeof i){var M=d(i),S=M?": "+M:"";return t.stylize("[Function"+S+"]","special")}if(h(i))return t.stylize(RegExp.prototype.toString.call(i),"regexp");if(l(i))return t.stylize(Date.prototype.toUTCString.call(i),"date");if(f(i))return o(i)}var _="",E=!1,P=["{","}"];if(u(i)&&(E=!0,P=["[","]"]),"function"==typeof i){var M=d(i),S=M?": "+M:"";_=" [Function"+S+"]"}if(h(i)&&(_=" "+RegExp.prototype.toString.call(i)),l(i)&&(_=" "+Date.prototype.toUTCString.call(i)),f(i))return o(i);if(0===O.length&&(!E||0==i.length))return P[0]+_+P[1];if(0>p)return h(i)?t.stylize(RegExp.prototype.toString.call(i),"regexp"):t.stylize("[Object]","special");t.seen.push(i);var k;return k=E?s(t,i,p,A,O):O.map(function(e){return a(t,i,p,A,e,E)}),t.seen.pop(),c(k,_,P)}function n(e,t){switch(typeof t){case"undefined":return e.stylize("undefined","undefined");case"string":var i="'"+JSON.stringify(t).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return e.stylize(i,"string");case"number":return 0===t&&1/t===-(1/0)?e.stylize("-0","number"):e.stylize(""+t,"number");case"boolean":return e.stylize(""+t,"boolean")}return null===t?e.stylize("null","null"):void 0}function o(e){return"["+Error.prototype.toString.call(e)+"]"}function s(e,t,i,r,n){for(var o=[],s=0,c=t.length;c>s;++s)Object.prototype.hasOwnProperty.call(t,String(s))?o.push(a(e,t,i,r,String(s),!0)):o.push("");return n.forEach(function(n){n.match(/^\d+$/)||o.push(a(e,t,i,r,n,!0))}),o}function a(e,t,i,n,o,s){var a,c;if(t.__lookupGetter__&&(t.__lookupGetter__(o)?c=t.__lookupSetter__(o)?e.stylize("[Getter/Setter]","special"):e.stylize("[Getter]","special"):t.__lookupSetter__(o)&&(c=e.stylize("[Setter]","special"))),n.indexOf(o)<0&&(a="["+o+"]"),c||(e.seen.indexOf(t[o])<0?(c=null===i?r(e,t[o],null):r(e,t[o],i-1),c.indexOf("\n")>-1&&(c=s?c.split("\n").map(function(e){return" "+e}).join("\n").substr(2):"\n"+c.split("\n").map(function(e){return" "+e}).join("\n"))):c=e.stylize("[Circular]","special")),"undefined"==typeof a){if(s&&o.match(/^\d+$/))return c;a=JSON.stringify(""+o),a.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(a=a.substr(1,a.length-2),a=e.stylize(a,"name")):(a=a.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),a=e.stylize(a,"string"))}return a+": "+c}function c(e,t,i){var r=0,n=e.reduce(function(e,t){return r++,t.indexOf("\n")>=0&&r++,e+t.length+1},0);return n>60?i[0]+(""===t?"":t+"\n ")+" "+e.join(",\n ")+" "+i[1]:i[0]+t+" "+e.join(", ")+" "+i[1]}function u(e){return Array.isArray(e)||"object"==typeof e&&"[object Array]"===p(e)}function h(e){return"object"==typeof e&&"[object RegExp]"===p(e)}function l(e){return"object"==typeof e&&"[object Date]"===p(e)}function f(e){return"object"==typeof e&&"[object Error]"===p(e)}function p(e){return Object.prototype.toString.call(e)}var d=require("chai/lib/chai/utils/getName.js"),b=require("chai/lib/chai/utils/getProperties.js"),g=require("chai/lib/chai/utils/getEnumerableProperties.js");t.exports=i;var v=function(e){return"object"==typeof HTMLElement?e instanceof HTMLElement:e&&"object"==typeof e&&1===e.nodeType&&"string"==typeof e.nodeName}}),require.register("chai/lib/chai/utils/objDisplay.js",function(e,t){var i=require("chai/lib/chai/utils/inspect.js"),r=require("chai/lib/chai/config.js");
t.exports=function(e){var t=i(e),n=Object.prototype.toString.call(e);if(r.truncateThreshold&&t.length>=r.truncateThreshold){if("[object Function]"===n)return e.name&&""!==e.name?"[Function: "+e.name+"]":"[Function]";if("[object Array]"===n)return"[ Array("+e.length+") ]";if("[object Object]"===n){var o=Object.keys(e),s=o.length>2?o.splice(0,2).join(", ")+", ...":o.join(", ");return"{ Object ("+s+") }"}return t}return t}}),require.register("chai/lib/chai/utils/overwriteMethod.js",function(e,t){t.exports=function(e,t,i){var r=e[t],n=function(){return this};r&&"function"==typeof r&&(n=r),e[t]=function(){var e=i(n).apply(this,arguments);return void 0===e?this:e}}}),require.register("chai/lib/chai/utils/overwriteProperty.js",function(e,t){t.exports=function(e,t,i){var r=Object.getOwnPropertyDescriptor(e,t),n=function(){};r&&"function"==typeof r.get&&(n=r.get),Object.defineProperty(e,t,{get:function(){var e=i(n).call(this);return void 0===e?this:e},configurable:!0})}}),require.register("chai/lib/chai/utils/overwriteChainableMethod.js",function(e,t){t.exports=function(e,t,i,r){var n=e.__methods[t],o=n.chainingBehavior;n.chainingBehavior=function(){var e=r(o).call(this);return void 0===e?this:e};var s=n.method;n.method=function(){var e=i(s).apply(this,arguments);return void 0===e?this:e}}}),require.register("chai/lib/chai/utils/test.js",function(e,t){var i=require("chai/lib/chai/utils/flag.js");t.exports=function(e,t){var r=i(e,"negate"),n=t[0];return r?!n:n}}),require.register("chai/lib/chai/utils/transferFlags.js",function(e,t){t.exports=function(e,t,i){var r=e.__flags||(e.__flags=Object.create(null));t.__flags||(t.__flags=Object.create(null)),i=3===arguments.length?i:!0;for(var n in r)(i||"object"!==n&&"ssfi"!==n&&"message"!=n)&&(t.__flags[n]=r[n])}}),require.register("chai/lib/chai/utils/type.js",function(e,t){var i={"[object Arguments]":"arguments","[object Array]":"array","[object Date]":"date","[object Function]":"function","[object Number]":"number","[object RegExp]":"regexp","[object String]":"string"};t.exports=function(e){var t=Object.prototype.toString.call(e);return i[t]?i[t]:null===e?"null":void 0===e?"undefined":e===Object(e)?"object":typeof e}}),"object"==typeof exports?module.exports=require("chai"):"function"==typeof define&&define.amd?define("chai",[],function(){return require("chai")}):(this||window).chai=require("chai")}(),this.chai});
================================================
FILE: test/dist/fixtures/desktop.html
================================================
================================================
FILE: test/dist/fixtures/mocha.css
================================================
@charset "utf-8";#mocha h1,#mocha h2,body{margin:0}#mocha{font:20px/1.5 "Helvetica Neue",Helvetica,Arial,sans-serif;margin:60px 50px}#mocha li,#mocha ul{margin:0;padding:0}#mocha ul{list-style:none}#mocha h1{margin-top:15px;font-size:1em;font-weight:200}#mocha h1 a,#mocha-stats a{text-decoration:none;color:inherit}#mocha h1 a:hover{text-decoration:underline}#mocha .suite .suite h1{margin-top:0;font-size:.8em}#mocha .hidden{display:none}#mocha h2{font-size:12px;font-weight:400;cursor:pointer}#mocha .suite,#mocha .test{margin-left:15px}#mocha .test{overflow:hidden}#mocha .test.pending:hover h2::after{content:'(pending)';font-family:arial,sans-serif}#mocha .test.pass.medium .duration{background:#c09853}#mocha .test.pass.slow .duration{background:#b94a48}#mocha .test.pass::before{content:'✓';font-size:12px;display:block;float:left;margin-right:5px;color:#00d6b2}#mocha .test.pass .duration{font-size:9px;margin-left:5px;padding:2px 5px;color:#fff;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.2);-moz-box-shadow:inset 0 1px 1px rgba(0,0,0,.2);box-shadow:inset 0 1px 1px rgba(0,0,0,.2);-webkit-border-radius:5px;-moz-border-radius:5px;-ms-border-radius:5px;-o-border-radius:5px;border-radius:5px}#mocha .test.pass.fast .duration,#mocha-report.fail .test.pass,#mocha-report.pass .test.fail,#mocha-report.pending .test.fail,#mocha-report.pending .test.pass{display:none}#mocha .test.pending{color:#0b97c4}#mocha .test.pending::before{content:'◦';color:#0b97c4}#mocha .test.fail{color:#c00}#mocha .test.fail pre,#mocha-stats em{color:#000}#mocha .test.fail::before{content:'✖';font-size:12px;display:block;float:left;margin-right:5px;color:#c00}#mocha .test pre.error{color:#c00;max-height:300px;overflow:auto}#mocha .test pre{display:block;float:left;clear:left;font:12px/1.5 monaco,monospace;margin:5px;padding:15px;border:1px solid #eee;max-width:85%;max-width:calc(100% - 42px);word-wrap:break-word;border-bottom-color:#ddd;-webkit-border-radius:3px;-webkit-box-shadow:0 1px 3px #eee;-moz-border-radius:3px;-moz-box-shadow:0 1px 3px #eee;border-radius:3px}#mocha .test h2{position:relative}#mocha .test a.replay{position:absolute;top:3px;right:0;text-decoration:none;vertical-align:middle;display:block;width:15px;height:15px;line-height:15px;text-align:center;background:#eee;font-size:15px;-moz-border-radius:15px;border-radius:15px;-webkit-transition:opacity 200ms;-moz-transition:opacity 200ms;transition:opacity 200ms;opacity:.3;color:#888}#mocha .test:hover a.replay{opacity:1}#mocha-report.pending .test.pass.pending{display:block}#mocha-error{color:#c00;font-size:1.5em;font-weight:100;letter-spacing:1px}#mocha-stats{position:fixed;top:15px;right:10px;font-size:12px;margin:0;color:#888;z-index:1}#mocha-stats .progress{float:right;padding-top:0}#mocha-stats a:hover{border-bottom:1px solid #eee}#mocha-stats li{display:inline-block;margin:0 5px;list-style:none;padding-top:11px}#mocha-stats canvas{width:40px;height:40px}#mocha code .comment{color:#ddd}#mocha code .init{color:#2f6fad}#mocha code .string{color:#5890ad}#mocha code .keyword{color:#8a6343}#mocha code .number{color:#2f6fad}@media screen and (max-device-width:480px){#mocha{margin:60px 0}#mocha #stats{position:absolute}}#mocha{display:none}
================================================
FILE: test/dist/fixtures/mocha.js
================================================
!function(t,e,n){var r=n.call(e,e);"object"==typeof modules?modules.define(t,function(t){t(r)}):e[t]=r}("mocha",this,function(t){return function(){function t(e){var n=t.resolve(e),r=t.modules[n];if(!r)throw new Error('failed to require "'+e+'"');return r.exports||(r.exports={},r.call(r.exports,r,r.exports,t.relative(n))),r.exports}function e(){for(var t=(new r).getTime();f.length&&(new r).getTime()-t<100;)f.shift()();c=f.length?o(e,0):null}t.modules={},t.resolve=function(e){var n=e,r=e+".js",o=e+"/index.js";return t.modules[r]&&r||t.modules[o]&&o||n},t.register=function(e,n){t.modules[e]=n},t.relative=function(e){return function(n){if("."!=n.charAt(0))return t(n);var r=e.split("/"),o=n.split("/");r.pop();for(var i=0;i/g,">"),e=e.replace(/"/g,""")}var r=function(t){this.ignoreWhitespace=t};r.prototype={diff:function(e,n){if(n===e)return[{value:n}];if(!n)return[{value:e,removed:!0}];if(!e)return[{value:n,added:!0}];n=this.tokenize(n),e=this.tokenize(e);var r=n.length,o=e.length,i=r+o,s=[{newPos:-1,components:[]}],a=this.extractCommon(s[0],n,e,0);if(s[0].newPos+1>=r&&a+1>=o)return s[0].components;for(var u=1;i>=u;u++)for(var l=-1*u;u>=l;l+=2){var c,f=s[l-1],p=s[l+1];a=(p?p.newPos:0)-l,f&&(s[l-1]=void 0);var h=f&&f.newPos+1=0&&o>a;if(h||d){!h||d&&f.newPos=r&&a+1>=o)return c.components;s[l]=c}else s[l]=void 0}},pushComponent:function(t,e,n,r){var o=t[t.length-1];o&&o.added===n&&o.removed===r?t[t.length-1]={value:this.join(o.value,e),added:n,removed:r}:t.push({value:e,added:n,removed:r})},extractCommon:function(t,e,n,r){for(var o=e.length,i=n.length,s=t.newPos,a=s-r;o>s+1&&i>a+1&&this.equals(e[s+1],n[a+1]);)s++,a++,this.pushComponent(t.components,e[s],void 0,void 0);return t.newPos=s,a},equals:function(t,e){var n=/\S/;return!this.ignoreWhitespace||n.test(t)||n.test(e)?t===e:!0},join:function(t,e){return t+e},tokenize:function(t){return t}};var o=new r,i=new r(!0),s=new r;i.tokenize=s.tokenize=function(t){return e(t.split(/(\s+|\b)/))};var a=new r(!0);a.tokenize=function(t){return e(t.split(/([{}:;,]|\s+)/))};var u=new r;return u.tokenize=function(t){return t.split(/^/m)},{Diff:r,diffChars:function(t,e){return o.diff(t,e)},diffWords:function(t,e){return i.diff(t,e)},diffWordsWithSpace:function(t,e){return s.diff(t,e)},diffLines:function(t,e){return u.diff(t,e)},diffCss:function(t,e){return a.diff(t,e)},createPatch:function(t,e,n,r,o){function i(t){return t.map(function(t){return" "+t})}function s(t,e,n){var r=l[l.length-2],o=e===l.length-2,i=e===l.length-3&&(n.added!==r.added||n.removed!==r.removed);/\n$/.test(n.value)||!o&&!i||t.push("\\ No newline at end of file")}var a=[];a.push("Index: "+t),a.push("==================================================================="),a.push("--- "+t+("undefined"==typeof r?"":" "+r)),a.push("+++ "+t+("undefined"==typeof o?"":" "+o));var l=u.diff(e,n);l[l.length-1].value||l.pop(),l.push({value:"",lines:[]});for(var c=0,f=0,p=[],h=1,d=1,g=0;g=0;s--){for(var l=r[s],c=0;c"):o.removed&&e.push(""),e.push(n(o.value)),o.added?e.push(""):o.removed&&e.push("")}return e.join("")},convertChangesToDMP:function(t){for(var e,n=[],r=0;ri;i++)if(n[i]===e||n[i].listener&&n[i].listener===e){o=i;break}if(0>o)return this;n.splice(o,1),n.length||delete this.$events[t]}else(n===e||n.listener&&n.listener===e)&&delete this.$events[t]}return this},o.prototype.removeAllListeners=function(t){return void 0===t?(this.$events={},this):(this.$events&&this.$events[t]&&(this.$events[t]=null),this)},o.prototype.listeners=function(t){return this.$events||(this.$events={}),this.$events[t]||(this.$events[t]=[]),r(this.$events[t])||(this.$events[t]=[this.$events[t]]),this.$events[t]},o.prototype.emit=function(t){if(!this.$events)return!1;var e=this.$events[t];if(!e)return!1;var n=[].slice.call(arguments,1);if("function"==typeof e)e.apply(this,n);else{if(!r(e))return!1;for(var o=e.slice(),i=0,s=o.length;s>i;i++)o[i].apply(this,n)}return!0}}),t.register("browser/fs.js",function(t,e,n){}),t.register("browser/glob.js",function(t,e,n){}),t.register("browser/path.js",function(t,e,n){}),t.register("browser/progress.js",function(t,e,n){function r(){this.percent=0,this.size(0),this.fontSize(11),this.font("helvetica, arial, sans-serif")}t.exports=r,r.prototype.size=function(t){return this._size=t,this},r.prototype.text=function(t){return this._text=t,this},r.prototype.fontSize=function(t){return this._fontSize=t,this},r.prototype.font=function(t){return this._font=t,this},r.prototype.update=function(t){return this.percent=t,this},r.prototype.draw=function(t){try{var e=Math.min(this.percent,100),n=this._size,r=n/2,o=r,i=r,s=r-1,a=this._fontSize;t.font=a+"px "+this._font;var u=2*Math.PI*(e/100);t.clearRect(0,0,n,n),t.strokeStyle="#9f9f9f",t.beginPath(),t.arc(o,i,s,0,u,!1),t.stroke(),t.strokeStyle="#eee",t.beginPath(),t.arc(o,i,s-1,0,u,!0),t.stroke();var l=this._text||(0|e)+"%",c=t.measureText(l).width;t.fillText(l,o-c/2+1,i+a/2-1)}catch(f){}return this}}),t.register("browser/tty.js",function(t,e,r){e.isatty=function(){return!0},e.getWindowSize=function(){return"innerHeight"in n?[n.innerHeight,n.innerWidth]:[640,480]}}),t.register("context.js",function(t,e,n){function r(){}t.exports=r,r.prototype.runnable=function(t){return 0==arguments.length?this._runnable:(this.test=this._runnable=t,this)},r.prototype.timeout=function(t){return 0===arguments.length?this.runnable().timeout():(this.runnable().timeout(t),this)},r.prototype.enableTimeouts=function(t){return this.runnable().enableTimeouts(t),this},r.prototype.slow=function(t){return this.runnable().slow(t),this},r.prototype.inspect=function(){return JSON.stringify(this,function(t,e){return"_runnable"!=t&&"test"!=t?e:void 0},2)}}),t.register("hook.js",function(t,e,n){function r(t,e){i.call(this,t,e),this.type="hook"}function o(){}var i=n("./runnable");t.exports=r,o.prototype=i.prototype,r.prototype=new o,r.prototype.constructor=r,r.prototype.error=function(t){if(0==arguments.length){var t=this._error;return this._error=null,t}this._error=t}}),t.register("interfaces/bdd.js",function(t,e,n){var r=n("../suite"),o=n("../test"),i=(n("../utils"),n("browser/escape-string-regexp"));t.exports=function(t){var e=[t];t.on("pre-require",function(t,n,s){t.before=function(t,n){e[0].beforeAll(t,n)},t.after=function(t,n){e[0].afterAll(t,n)},t.beforeEach=function(t,n){e[0].beforeEach(t,n)},t.afterEach=function(t,n){e[0].afterEach(t,n)},t.describe=t.context=function(t,o){var i=r.create(e[0],t);return i.file=n,e.unshift(i),o.call(i),e.shift(),i},t.xdescribe=t.xcontext=t.describe.skip=function(t,n){var o=r.create(e[0],t);o.pending=!0,e.unshift(o),n.call(o),e.shift()},t.describe.only=function(e,n){var r=t.describe(e,n);return s.grep(r.fullTitle()),r},t.it=t.specify=function(t,r){var i=e[0];i.pending&&(r=null);var s=new o(t,r);return s.file=n,i.addTest(s),s},t.it.only=function(e,n){var r=t.it(e,n),o="^"+i(r.fullTitle())+"$";return s.grep(new RegExp(o)),r},t.xit=t.xspecify=t.it.skip=function(e){t.it(e)}})}}),t.register("interfaces/exports.js",function(t,e,n){var r=n("../suite"),o=n("../test");t.exports=function(t){function e(t,i){var s;for(var a in t)if("function"==typeof t[a]){var u=t[a];switch(a){case"before":n[0].beforeAll(u);break;case"after":n[0].afterAll(u);break;case"beforeEach":n[0].beforeEach(u);break;case"afterEach":n[0].afterEach(u);break;default:var l=new o(a,u);l.file=i,n[0].addTest(l)}}else s=r.create(n[0],a),n.unshift(s),e(t[a]),n.shift()}var n=[t];t.on("require",e)}}),t.register("interfaces/index.js",function(t,e,n){e.bdd=n("./bdd"),e.tdd=n("./tdd"),e.qunit=n("./qunit"),e.exports=n("./exports")}),t.register("interfaces/qunit.js",function(t,e,n){var r=n("../suite"),o=n("../test"),i=n("browser/escape-string-regexp");n("../utils");t.exports=function(t){var e=[t];t.on("pre-require",function(t,n,s){t.before=function(t,n){e[0].beforeAll(t,n)},t.after=function(t,n){e[0].afterAll(t,n)},t.beforeEach=function(t,n){e[0].beforeEach(t,n)},t.afterEach=function(t,n){e[0].afterEach(t,n)},t.suite=function(t){e.length>1&&e.shift();var o=r.create(e[0],t);return o.file=n,e.unshift(o),o},t.suite.only=function(e,n){var r=t.suite(e,n);s.grep(r.fullTitle())},t.test=function(t,r){var i=new o(t,r);return i.file=n,e[0].addTest(i),i},t.test.only=function(e,n){var r=t.test(e,n),o="^"+i(r.fullTitle())+"$";s.grep(new RegExp(o))},t.test.skip=function(e){t.test(e)}})}}),t.register("interfaces/tdd.js",function(t,e,n){var r=n("../suite"),o=n("../test"),i=n("browser/escape-string-regexp");n("../utils");t.exports=function(t){var e=[t];t.on("pre-require",function(t,n,s){t.setup=function(t,n){e[0].beforeEach(t,n)},t.teardown=function(t,n){e[0].afterEach(t,n)},t.suiteSetup=function(t,n){e[0].beforeAll(t,n)},t.suiteTeardown=function(t,n){e[0].afterAll(t,n)},t.suite=function(t,o){var i=r.create(e[0],t);return i.file=n,e.unshift(i),o.call(i),e.shift(),i},t.suite.skip=function(t,n){var o=r.create(e[0],t);o.pending=!0,e.unshift(o),n.call(o),e.shift()},t.suite.only=function(e,n){var r=t.suite(e,n);s.grep(r.fullTitle())},t.test=function(t,r){var i=e[0];i.pending&&(r=null);var s=new o(t,r);return s.file=n,i.addTest(s),s},t.test.only=function(e,n){var r=t.test(e,n),o="^"+i(r.fullTitle())+"$";s.grep(new RegExp(o))},t.test.skip=function(e){t.test(e)}})}}),t.register("mocha.js",function(t,e,r){function o(t){return __dirname+"/../images/"+t+".png"}function s(t){t=t||{},this.files=[],this.options=t,this.grep(t.grep),this.suite=new e.Suite("",new e.Context),this.ui(t.ui),this.bail(t.bail),this.reporter(t.reporter),null!=t.timeout&&this.timeout(t.timeout),this.useColors(t.useColors),null!==t.enableTimeouts&&this.enableTimeouts(t.enableTimeouts),t.slow&&this.slow(t.slow),this.suite.on("pre-require",function(t){e.afterEach=t.afterEach||t.teardown,e.after=t.after||t.suiteTeardown,e.beforeEach=t.beforeEach||t.setup,e.before=t.before||t.suiteSetup,e.describe=t.describe||t.suite,e.it=t.it||t.test,e.setup=t.setup||t.beforeEach,e.suiteSetup=t.suiteSetup||t.before,e.suiteTeardown=t.suiteTeardown||t.after,e.suite=t.suite||t.describe,e.teardown=t.teardown||t.afterEach,e.test=t.test||t.it})}var a=r("browser/path"),u=r("browser/escape-string-regexp"),l=r("./utils");if(e=t.exports=s,"undefined"!=typeof i&&"function"==typeof i.cwd){var c=a.join,f=i.cwd();t.paths.push(f,c(f,"node_modules"))}e.utils=l,e.interfaces=r("./interfaces"),e.reporters=r("./reporters"),e.Runnable=r("./runnable"),e.Context=r("./context"),e.Runner=r("./runner"),e.Suite=r("./suite"),e.Hook=r("./hook"),e.Test=r("./test"),s.prototype.bail=function(t){return 0==arguments.length&&(t=!0),this.suite.bail(t),this},s.prototype.addFile=function(t){return this.files.push(t),this},s.prototype.reporter=function(t){if("function"==typeof t)this._reporter=t;else{t=t||"spec";var e;try{e=r("./reporters/"+t)}catch(n){}if(!e)try{e=r(t)}catch(n){}if(e||"teamcity"!==t||console.warn("The Teamcity reporter was moved to a package named mocha-teamcity-reporter (https://npmjs.org/package/mocha-teamcity-reporter)."),!e)throw new Error('invalid reporter "'+t+'"');this._reporter=e}return this},s.prototype.ui=function(t){if(t=t||"bdd",this._ui=e.interfaces[t],!this._ui)try{this._ui=r(t)}catch(n){}if(!this._ui)throw new Error('invalid interface "'+t+'"');return this._ui=this._ui(this.suite),this},s.prototype.loadFiles=function(t){var e=this,o=this.suite,i=this.files.length;this.files.forEach(function(s){s=a.resolve(s),o.emit("pre-require",n,s,e),o.emit("require",r(s),s,e),o.emit("post-require",n,s,e),--i||t&&t()})},s.prototype._growl=function(t,e){var n=r("growl");t.on("end",function(){var r=e.stats;if(r.failures){var i=r.failures+" of "+t.total+" tests failed";n(i,{name:"mocha",title:"Failed",image:o("error")})}else n(r.passes+" tests passed in "+r.duration+"ms",{name:"mocha",title:"Passed",image:o("ok")})})},s.prototype.grep=function(t){return this.options.grep="string"==typeof t?new RegExp(u(t)):t,this},s.prototype.invert=function(){return this.options.invert=!0,this},s.prototype.ignoreLeaks=function(t){return this.options.ignoreLeaks=!!t,this},s.prototype.checkLeaks=function(){return this.options.ignoreLeaks=!1,this},s.prototype.growl=function(){return this.options.growl=!0,this},s.prototype.globals=function(t){return this.options.globals=(this.options.globals||[]).concat(t),this},s.prototype.useColors=function(t){return this.options.useColors=arguments.length&&void 0!=t?t:!0,this},s.prototype.useInlineDiffs=function(t){return this.options.useInlineDiffs=arguments.length&&void 0!=t?t:!1,this},s.prototype.timeout=function(t){return this.suite.timeout(t),this},s.prototype.slow=function(t){return this.suite.slow(t),this},s.prototype.enableTimeouts=function(t){return this.suite.enableTimeouts(arguments.length&&void 0!==t?t:!0),this},s.prototype.asyncOnly=function(){return this.options.asyncOnly=!0,this},s.prototype.noHighlighting=function(){return this.options.noHighlighting=!0,this},s.prototype.run=function(t){this.files.length&&this.loadFiles();var n=this.suite,r=this.options;r.files=this.files;var o=new e.Runner(n),i=new this._reporter(o,r);return o.ignoreLeaks=!1!==r.ignoreLeaks,o.asyncOnly=r.asyncOnly,r.grep&&o.grep(r.grep,r.invert),r.globals&&o.globals(r.globals),r.growl&&this._growl(o,i),e.reporters.Base.useColors=r.useColors,e.reporters.Base.inlineDiffs=r.useInlineDiffs,o.run(t)}}),t.register("ms.js",function(t,e,n){function r(t){var e=/^((?:\d+)?\.?\d+) *(ms|seconds?|s|minutes?|m|hours?|h|days?|d|years?|y)?$/i.exec(t);if(e){var n=parseFloat(e[1]),r=(e[2]||"ms").toLowerCase();switch(r){case"years":case"year":case"y":return n*f;case"days":case"day":case"d":return n*c;case"hours":case"hour":case"h":return n*l;case"minutes":case"minute":case"m":return n*u;case"seconds":case"second":case"s":return n*a;case"ms":return n}}}function o(t){return t>=c?Math.round(t/c)+"d":t>=l?Math.round(t/l)+"h":t>=u?Math.round(t/u)+"m":t>=a?Math.round(t/a)+"s":t+"ms"}function i(t){return s(t,c,"day")||s(t,l,"hour")||s(t,u,"minute")||s(t,a,"second")||t+" ms"}function s(t,e,n){return e>t?void 0:1.5*e>t?Math.floor(t/e)+" "+n:Math.ceil(t/e)+" "+n+"s"}var a=1e3,u=60*a,l=60*u,c=24*l,f=365.25*c;t.exports=function(t,e){return e=e||{},"string"==typeof t?r(t):e["long"]?i(t):o(t)}}),t.register("reporters/base.js",function(t,e,r){function o(t){var e=this.stats={suites:0,tests:0,passes:0,pending:0,failures:0},n=this.failures=[];t&&(this.runner=t,t.stats=e,t.on("start",function(){e.start=new v}),t.on("suite",function(t){e.suites=e.suites||0,t.root||e.suites++}),t.on("test end",function(t){e.tests=e.tests||0,e.tests++}),t.on("pass",function(t){e.passes=e.passes||0;var n=t.slow()/2;t.speed=t.duration>t.slow()?"slow":t.duration>n?"medium":"fast",e.passes++}),t.on("fail",function(t,r){e.failures=e.failures||0,e.failures++,t.err=r,n.push(t)}),t.on("end",function(){e.end=new v,e.duration=new v-e.start}),t.on("pending",function(){e.pending++}))}function s(t,e){return t=String(t),Array(e-t.length+1).join(" ")+t}function a(t,e){var n=l(t,"WordsWithSpace",e),r=n.split("\n");if(r.length>4){var o=String(r.length).length;n=r.map(function(t,e){return s(++e,o)+" | "+t}).join("\n")}return n="\n"+w("diff removed","actual")+" "+w("diff added","expected")+"\n\n"+n+"\n",n=n.replace(/^/gm," ")}function u(t,e){function n(t){return e&&(t=c(t)),"+"===t[0]?o+f("diff added",t):"-"===t[0]?o+f("diff removed",t):t.match(/\@\@/)?null:t.match(/\\ No newline/)?null:o+t}function r(t){return null!=t}var o=" ";msg=d.createPatch("string",t.actual,t.expected);var i=msg.split("\n").splice(4);return"\n "+f("diff added","+ expected")+" "+f("diff removed","- actual")+"\n\n"+i.map(n).filter(r).join("\n")}function l(t,e,n){var r=n?c(t.actual):t.actual,o=n?c(t.expected):t.expected;return d["diff"+e](r,o).map(function(t){return t.added?f("diff added",t.value):t.removed?f("diff removed",t.value):t.value}).join("")}function c(t){return t.replace(/\t/g,"").replace(/\r/g,"").replace(/\n/g,"\n")}function f(t,e){return e.split("\n").map(function(e){return w(t,e)}).join("\n")}function p(t,e){return t=Object.prototype.toString.call(t),e=Object.prototype.toString.call(e),t==e}var h=r("browser/tty"),d=r("browser/diff"),g=r("../ms"),m=r("../utils"),v=n.Date,y=(n.setTimeout,n.setInterval,n.clearTimeout,n.clearInterval,h.isatty(1)&&h.isatty(2));e=t.exports=o,e.useColors=y||void 0!==i.env.MOCHA_COLORS,e.inlineDiffs=!1,e.colors={pass:90,fail:31,"bright pass":92,"bright fail":91,"bright yellow":93,pending:36,suite:0,"error title":0,"error message":31,"error stack":90,checkmark:32,fast:90,medium:33,slow:31,green:32,light:90,"diff gutter":90,"diff added":42,"diff removed":41},e.symbols={ok:"✓",err:"✖",dot:"․"},"win32"==i.platform&&(e.symbols.ok="√",e.symbols.err="×",e.symbols.dot=".");var w=e.color=function(t,n){return e.useColors?"["+e.colors[t]+"m"+n+"[0m":n};e.window={width:y?i.stdout.getWindowSize?i.stdout.getWindowSize(1)[0]:h.getWindowSize()[1]:75},e.cursor={hide:function(){y&&i.stdout.write("[?25l")},show:function(){y&&i.stdout.write("[?25h")},deleteLine:function(){y&&i.stdout.write("[2K")},beginningOfLine:function(){y&&i.stdout.write("[0G")},CR:function(){y?(e.cursor.deleteLine(),e.cursor.beginningOfLine()):i.stdout.write("\r")}},e.list=function(t){console.error(),t.forEach(function(t,n){var r=w("error title"," %s) %s:\n")+w("error message"," %s")+w("error stack","\n%s\n"),o=t.err,i=o.message||"",s=o.stack||i,l=s.indexOf(i)+i.length,c=s.slice(0,l),f=o.actual,h=o.expected,d=!0;if(o.uncaught&&(c="Uncaught "+c),o.showDiff&&p(f,h)&&(d=!1,o.actual=f=m.stringify(f),o.expected=h=m.stringify(h)),o.showDiff&&"string"==typeof f&&"string"==typeof h){r=w("error title"," %s) %s:\n%s")+w("error stack","\n%s\n");var g=i.match(/^([^:]+): expected/);c="\n "+w("error message",g?g[1]:c),c+=e.inlineDiffs?a(o,d):u(o,d)}s=s.slice(l?l+1:l).replace(/^/gm," "),console.error(r,n+1,t.fullTitle(),c,s)})},o.prototype.epilogue=function(){var t,e=this.stats;console.log(),t=w("bright pass"," ")+w("green"," %d passing")+w("light"," (%s)"),console.log(t,e.passes||0,g(e.duration)),e.pending&&(t=w("pending"," ")+w("pending"," %d pending"),console.log(t,e.pending)),e.failures&&(t=w("fail"," %d failing"),console.error(t,e.failures),o.list(this.failures),console.error()),console.log()}}),t.register("reporters/doc.js",function(t,e,n){function r(t){function e(){return Array(n).join(" ")}o.call(this,t);var n=(this.stats,t.total,2);t.on("suite",function(t){t.root||(++n,console.log('%s',e()),++n,console.log("%s%s ",e(),i.escape(t.title)),console.log("%s",e()))}),t.on("suite end",function(t){t.root||(console.log("%s ",e()),--n,console.log("%s ",e()),--n)}),t.on("pass",function(t){console.log("%s %s ",e(),i.escape(t.title));var n=i.escape(i.clean(t.fn.toString()));console.log("%s %s ",e(),n)}),t.on("fail",function(t,n){console.log('%s %s ',e(),i.escape(t.title));var r=i.escape(i.clean(t.fn.toString()));console.log('%s %s ',e(),r),console.log('%s %s ',e(),i.escape(n))})}var o=n("./base"),i=n("../utils");e=t.exports=r}),t.register("reporters/dot.js",function(t,e,n){function r(t){s.call(this,t);var e=this,n=(this.stats,.75*s.window.width|0),r=-1;t.on("start",function(){i.stdout.write("\n ")}),t.on("pending",function(t){++r%n==0&&i.stdout.write("\n "),i.stdout.write(a("pending",s.symbols.dot))}),t.on("pass",function(t){++r%n==0&&i.stdout.write("\n "),"slow"==t.speed?i.stdout.write(a("bright yellow",s.symbols.dot)):i.stdout.write(a(t.speed,s.symbols.dot))}),t.on("fail",function(t,e){++r%n==0&&i.stdout.write("\n "),i.stdout.write(a("fail",s.symbols.dot))}),t.on("end",function(){console.log(),e.epilogue()})}function o(){}var s=n("./base"),a=s.color;e=t.exports=r,o.prototype=s.prototype,r.prototype=new o,r.prototype.constructor=r}),t.register("reporters/html-cov.js",function(t,e,n){function r(t){var e=n("jade"),r=__dirname+"/templates/coverage.jade",u=a.readFileSync(r,"utf8"),l=e.compile(u,{filename:r}),c=this;s.call(this,t,!1),t.on("end",function(){i.stdout.write(l({cov:c.cov,coverageClass:o}))})}function o(t){return t>=75?"high":t>=50?"medium":t>=25?"low":"terrible"}var s=n("./json-cov"),a=n("browser/fs");e=t.exports=r}),t.register("reporters/html.js",function(t,e,r){function o(t){f.call(this,t);var e,n,r=this,o=this.stats,v=(t.total,s(m)),y=v.getElementsByTagName("li"),w=y[1].getElementsByTagName("em")[0],b=y[1].getElementsByTagName("a")[0],x=y[2].getElementsByTagName("em")[0],T=y[2].getElementsByTagName("a")[0],k=y[3].getElementsByTagName("em")[0],E=v.getElementsByTagName("canvas")[0],j=s(''),_=[j],C=document.getElementById("mocha");if(E.getContext){var S=window.devicePixelRatio||1;E.style.width=E.width,E.style.height=E.height,E.width*=S,E.height*=S,n=E.getContext("2d"),n.scale(S,S),e=new h}return C?(c(b,"click",function(){u();var t=/pass/.test(j.className)?"":" pass";j.className=j.className.replace(/fail|pass/g,"")+t,j.className.trim()&&a("test pass")}),c(T,"click",function(){u();var t=/fail/.test(j.className)?"":" fail";j.className=j.className.replace(/fail|pass/g,"")+t,j.className.trim()&&a("test fail")}),C.appendChild(v),C.appendChild(j),e&&e.size(40),t.on("suite",function(t){if(!t.root){var e=r.suiteURL(t),n=s(' ',e,d(t.title));_[0].appendChild(n),_.unshift(document.createElement("ul")),n.appendChild(_[0])}}),t.on("suite end",function(t){t.root||_.shift()}),t.on("fail",function(e,n){"hook"==e.type&&t.emit("test end",e)}),void t.on("test end",function(t){var i=o.tests/this.total*100|0;e&&e.update(i).draw(n);var a=new g-o.start;if(l(w,o.passes),l(x,o.failures),l(k,(a/1e3).toFixed(2)),"passed"==t.state)var u=r.testURL(t),f=s('%e%ems ‣ ',t.speed,t.title,t.duration,u);else if(t.pending)var f=s('%e ',t.title);else{var f=s('%e ‣ ',t.title,encodeURIComponent(t.fullTitle())),h=t.err.stack||t.err.toString();~h.indexOf(t.err.message)||(h=t.err.message+"\n"+h),"[object Error]"==h&&(h=t.err.message),!t.err.stack&&t.err.sourceURL&&void 0!==t.err.line&&(h+="\n("+t.err.sourceURL+":"+t.err.line+")"),f.appendChild(s('%e ',h))}if(!t.pending){var d=f.getElementsByTagName("h2")[0];c(d,"click",function(){m.style.display="none"==m.style.display?"block":"none"});var m=s("%e ",p.clean(t.fn.toString()));f.appendChild(m),m.style.display="none"}_[0]&&_[0].appendChild(f)})):i("#mocha div missing, add it to your document")}function i(t){document.body.appendChild(s('%s
',t))}function s(t){var e=arguments,n=document.createElement("div"),r=1;return n.innerHTML=t.replace(/%([se])/g,function(t,n){switch(n){case"s":return String(e[r++]);case"e":return d(e[r++])}}),n.firstChild}function a(t){for(var e=document.getElementsByClassName("suite"),n=0;n0&&(e.coverage=e.hits/e.sloc*100),e}function a(t,e){var n={filename:t,coverage:0,hits:0,misses:0,sloc:0,source:{}};return e.source.forEach(function(t,r){r++,0===e[r]?(n.misses++,n.sloc++):void 0!==e[r]&&(n.hits++,n.sloc++),n.source[r]={source:t,coverage:void 0===e[r]?"":e[r]}}),n.coverage=n.hits/n.sloc*100,n}function u(t){return{title:t.title,fullTitle:t.fullTitle(),duration:t.duration}}var l=r("./base");e=t.exports=o}),t.register("reporters/json-stream.js",function(t,e,n){function r(t){s.call(this,t);var e=this,n=(this.stats,t.total);t.on("start",function(){console.log(JSON.stringify(["start",{total:n}]))}),t.on("pass",function(t){console.log(JSON.stringify(["pass",o(t)]))}),t.on("fail",function(t,e){t=o(t),t.err=e.message,console.log(JSON.stringify(["fail",t]))}),t.on("end",function(){i.stdout.write(JSON.stringify(["end",e.stats]))})}function o(t){return{title:t.title,fullTitle:t.fullTitle(),duration:t.duration}}var s=n("./base");s.color;e=t.exports=r}),t.register("reporters/json.js",function(t,e,n){function r(t){var e=this;a.call(this,t);var n=[],r=[],s=[],u=[];t.on("test end",function(t){n.push(t)}),t.on("pass",function(t){u.push(t)}),t.on("fail",function(t){s.push(t)}),t.on("pending",function(t){r.push(t)}),t.on("end",function(){var a={stats:e.stats,tests:n.map(o),pending:r.map(o),failures:s.map(o),passes:u.map(o)};t.testResults=a,i.stdout.write(JSON.stringify(a,null,2))})}function o(t){return{title:t.title,fullTitle:t.fullTitle(),duration:t.duration,err:s(t.err||{})}}function s(t){var e={};return Object.getOwnPropertyNames(t).forEach(function(n){e[n]=t[n]},t),e}var a=n("./base");a.cursor,a.color;e=t.exports=r}),t.register("reporters/landing.js",function(t,e,n){function r(t){function e(){var t=Array(r).join("-");return" "+u("runway",t)}s.call(this,t);var n=this,r=(this.stats,.75*s.window.width|0),o=t.total,l=i.stdout,c=u("plane","✈"),f=-1,p=0;t.on("start",function(){l.write("\n\n\n "),a.hide()}),t.on("test end",function(t){var n=-1==f?r*++p/o|0:f;"failed"==t.state&&(c=u("plane crash","✈"),f=n),l.write("["+(r+1)+"D[2A"),l.write(e()),l.write("\n "),l.write(u("runway",Array(n).join("⋅"))),l.write(c),l.write(u("runway",Array(r-n).join("⋅")+"\n")),l.write(e()),l.write("[0m")}),t.on("end",function(){a.show(),console.log(),n.epilogue()})}function o(){}var s=n("./base"),a=s.cursor,u=s.color;e=t.exports=r,s.colors.plane=0,s.colors["plane crash"]=31,s.colors.runway=90,o.prototype=s.prototype,r.prototype=new o,r.prototype.constructor=r}),t.register("reporters/list.js",function(t,e,n){function r(t){s.call(this,t);var e=this,n=(this.stats,0);t.on("start",function(){console.log()}),t.on("test",function(t){i.stdout.write(u("pass"," "+t.fullTitle()+": "))}),t.on("pending",function(t){var e=u("checkmark"," -")+u("pending"," %s");console.log(e,t.fullTitle())}),t.on("pass",function(t){var e=u("checkmark"," "+s.symbols.dot)+u("pass"," %s: ")+u(t.speed,"%dms");a.CR(),console.log(e,t.fullTitle(),t.duration)}),t.on("fail",function(t,e){a.CR(),console.log(u("fail"," %d) %s"),++n,t.fullTitle())}),t.on("end",e.epilogue.bind(e))}function o(){}var s=n("./base"),a=s.cursor,u=s.color;e=t.exports=r,o.prototype=s.prototype,r.prototype=new o,r.prototype.constructor=r}),t.register("reporters/markdown.js",function(t,e,n){function r(t){function e(t){return Array(u).join("#")+" "+t}function n(t,e){var r=e;return e=e[t.title]=e[t.title]||{suite:t},t.suites.forEach(function(t){n(t,e)}),r}function r(t,e){++e;var n,o="";for(var i in t)"suite"!=i&&(i&&(n=" - ["+i+"](#"+s.slug(t[i].suite.fullTitle())+")\n"),i&&(o+=Array(e).join(" ")+n),o+=r(t[i],e));return--e,o}function a(t){var e=n(t,{});return r(e,0)}o.call(this,t);var u=(this.stats,0),l="";a(t.suite),t.on("suite",function(t){++u;var n=s.slug(t.fullTitle());
l+=' \n',l+=e(t.title)+"\n"}),t.on("suite end",function(t){--u}),t.on("pass",function(t){var e=s.clean(t.fn.toString());l+=t.title+".\n",l+="\n```js\n",l+=e+"\n",l+="```\n\n"}),t.on("end",function(){i.stdout.write("# TOC\n"),i.stdout.write(a(t.suite)),i.stdout.write(l)})}var o=n("./base"),s=n("../utils");e=t.exports=r}),t.register("reporters/min.js",function(t,e,n){function r(t){s.call(this,t),t.on("start",function(){i.stdout.write("[2J"),i.stdout.write("[1;3H")}),t.on("end",this.epilogue.bind(this))}function o(){}var s=n("./base");e=t.exports=r,o.prototype=s.prototype,r.prototype=new o,r.prototype.constructor=r}),t.register("reporters/nyan.js",function(t,e,n){function r(t){a.call(this,t);var e=this,n=(this.stats,.75*a.window.width|0),r=(this.rainbowColors=e.generateColors(),this.colorIndex=0,this.numberOfLines=4,this.trajectories=[[],[],[],[]],this.nyanCatWidth=11);this.trajectoryWidthMax=n-r,this.scoreboardWidth=5,this.tick=0;t.on("start",function(){a.cursor.hide(),e.draw()}),t.on("pending",function(t){e.draw()}),t.on("pass",function(t){e.draw()}),t.on("fail",function(t,n){e.draw()}),t.on("end",function(){a.cursor.show();for(var t=0;t=this.trajectoryWidthMax&&r.shift(),r.push(e)}},r.prototype.drawRainbow=function(){var t=this;this.trajectories.forEach(function(e,n){o("["+t.scoreboardWidth+"C"),o(e.join("")),o("\n")}),this.cursorUp(this.numberOfLines)},r.prototype.drawNyanCat=function(){var t=this,e=this.scoreboardWidth+this.trajectories[0].length,n="["+e+"C",r="";o(n),o("_,------,"),o("\n"),o(n),r=t.tick?" ":" ",o("_|"+r+"/\\_/\\ "),o("\n"),o(n),r=t.tick?"_":"__";var i=t.tick?"~":"^";o(i+"|"+r+this.face()+" "),o("\n"),o(n),r=t.tick?" ":" ",o(r+'"" "" '),o("\n"),this.cursorUp(this.numberOfLines)},r.prototype.face=function(){var t=this.stats;return t.failures?"( x .x)":t.pending?"( o .o)":t.passes?"( ^ .^)":"( - .-)"},r.prototype.cursorUp=function(t){o("["+t+"A")},r.prototype.cursorDown=function(t){o("["+t+"B")},r.prototype.generateColors=function(){for(var t=[],e=0;42>e;e++){var n=Math.floor(Math.PI/3),r=e*(1/6),o=Math.floor(3*Math.sin(r)+3),i=Math.floor(3*Math.sin(r+2*n)+3),s=Math.floor(3*Math.sin(r+4*n)+3);t.push(36*o+6*i+s+16)}return t},r.prototype.rainbowify=function(t){var e=this.rainbowColors[this.colorIndex%this.rainbowColors.length];return this.colorIndex+=1,"[38;5;"+e+"m"+t+"[0m"},s.prototype=a.prototype,r.prototype=new s,r.prototype.constructor=r}),t.register("reporters/progress.js",function(t,e,n){function r(t,e){s.call(this,t);var n=this,e=e||{},r=(this.stats,.5*s.window.width|0),o=t.total,l=0,c=(Math.max,-1);e.open=e.open||"[",e.complete=e.complete||"▬",e.incomplete=e.incomplete||s.symbols.dot,e.close=e.close||"]",e.verbose=!1,t.on("start",function(){console.log(),a.hide()}),t.on("test end",function(){l++;var t=l/o,n=r*t|0,s=r-n;(c!==n||e.verbose)&&(c=n,a.CR(),i.stdout.write("[J"),i.stdout.write(u("progress"," "+e.open)),i.stdout.write(Array(n).join(e.complete)),i.stdout.write(Array(s).join(e.incomplete)),i.stdout.write(u("progress",e.close)),e.verbose&&i.stdout.write(u("progress"," "+l+" of "+o)))}),t.on("end",function(){a.show(),console.log(),n.epilogue()})}function o(){}var s=n("./base"),a=s.cursor,u=s.color;e=t.exports=r,s.colors.progress=90,o.prototype=s.prototype,r.prototype=new o,r.prototype.constructor=r}),t.register("reporters/spec.js",function(t,e,n){function r(t){function e(){return Array(r).join(" ")}i.call(this,t);var n=this,r=(this.stats,0),o=0;t.on("start",function(){console.log()}),t.on("suite",function(t){++r,console.log(a("suite","%s%s"),e(),t.title)}),t.on("suite end",function(t){--r,1==r&&console.log()}),t.on("pending",function(t){var n=e()+a("pending"," - %s");console.log(n,t.title)}),t.on("pass",function(t){if("fast"==t.speed){var n=e()+a("checkmark"," "+i.symbols.ok)+a("pass"," %s ");s.CR(),console.log(n,t.title)}else{var n=e()+a("checkmark"," "+i.symbols.ok)+a("pass"," %s ")+a(t.speed,"(%dms)");s.CR(),console.log(n,t.title,t.duration)}}),t.on("fail",function(t,n){s.CR(),console.log(e()+a("fail"," %d) %s"),++o,t.title)}),t.on("end",n.epilogue.bind(n))}function o(){}var i=n("./base"),s=i.cursor,a=i.color;e=t.exports=r,o.prototype=i.prototype,r.prototype=new o,r.prototype.constructor=r}),t.register("reporters/tap.js",function(t,e,n){function r(t){i.call(this,t);var e=(this.stats,1),n=0,r=0;t.on("start",function(){var e=t.grepTotal(t.suite);console.log("%d..%d",1,e)}),t.on("test end",function(){++e}),t.on("pending",function(t){console.log("ok %d %s # SKIP -",e,o(t))}),t.on("pass",function(t){n++,console.log("ok %d %s",e,o(t))}),t.on("fail",function(t,n){r++,console.log("not ok %d %s",e,o(t)),n.stack&&console.log(n.stack.replace(/^/gm," "))}),t.on("end",function(){console.log("# tests "+(n+r)),console.log("# pass "+n),console.log("# fail "+r)})}function o(t){return t.fullTitle().replace(/#/g,"")}var i=n("./base");i.cursor,i.color;e=t.exports=r}),t.register("reporters/xunit.js",function(t,e,r){function o(t){l.call(this,t);var e=this.stats,n=[];t.on("pending",function(t){n.push(t)}),t.on("pass",function(t){n.push(t)}),t.on("fail",function(t){n.push(t)}),t.on("end",function(){console.log(a("testsuite",{name:"Mocha Tests",tests:e.tests,failures:e.failures,errors:e.failures,skipped:e.tests-e.failures-e.passes,timestamp:(new p).toUTCString(),time:e.duration/1e3||0},!1)),n.forEach(s),console.log("")})}function i(){}function s(t){var e={classname:t.parent.fullTitle(),name:t.title,time:t.duration/1e3||0};if("failed"==t.state){var n=t.err;console.log(a("testcase",e,!1,a("failure",{},!1,u(f(n.message)+"\n"+n.stack))))}else t.pending?console.log(a("testcase",e,!1,a("skipped",{},!0))):console.log(a("testcase",e,!0))}function a(t,e,n,r){var o,i=n?"/>":">",s=[];for(var a in e)s.push(a+'="'+f(e[a])+'"');return o="<"+t+(s.length?" "+s.join(" "):"")+i,r&&(o+=r+""+t+i),o}function u(t){return""}var l=r("./base"),c=r("../utils"),f=c.escape,p=n.Date;n.setTimeout,n.setInterval,n.clearTimeout,n.clearInterval;e=t.exports=o,i.prototype=l.prototype,o.prototype=new i,o.prototype.constructor=o}),t.register("runnable.js",function(t,e,r){function o(t,e){this.title=t,this.fn=e,this.async=e&&e.length,this.sync=!this.async,this._timeout=2e3,this._slow=75,this._enableTimeouts=!0,this.timedOut=!1,this._trace=new Error("done() called multiple times")}function i(){}var s=r("browser/events").EventEmitter,a=r("browser/debug")("mocha:runnable"),u=r("./ms"),l=n.Date,c=n.setTimeout,f=(n.setInterval,n.clearTimeout),p=(n.clearInterval,Object.prototype.toString);t.exports=o,i.prototype=s.prototype,o.prototype=new i,o.prototype.constructor=o,o.prototype.timeout=function(t){return 0==arguments.length?this._timeout:(0===t&&(this._enableTimeouts=!1),"string"==typeof t&&(t=u(t)),a("timeout %d",t),this._timeout=t,this.timer&&this.resetTimeout(),this)},o.prototype.slow=function(t){return 0===arguments.length?this._slow:("string"==typeof t&&(t=u(t)),a("timeout %d",t),this._slow=t,this)},o.prototype.enableTimeouts=function(t){return 0===arguments.length?this._enableTimeouts:(a("enableTimeouts %s",t),this._enableTimeouts=t,this)},o.prototype.fullTitle=function(){return this.parent.fullTitle()+" "+this.title},o.prototype.clearTimeout=function(){f(this.timer)},o.prototype.inspect=function(){return JSON.stringify(this,function(t,e){return"_"!=t[0]?"parent"==t?"#":"ctx"==t?"#":e:void 0},2)},o.prototype.resetTimeout=function(){var t=this,e=this.timeout()||1e9;this._enableTimeouts&&(this.clearTimeout(),this.timer=c(function(){t._enableTimeouts&&(t.callback(new Error("timeout of "+e+"ms exceeded")),t.timedOut=!0)},e))},o.prototype.globals=function(t){this._allowedGlobals=t},o.prototype.run=function(t){function e(t){i||(i=!0,s.emit("error",t||new Error("done() called multiple times; stacktrace may be inaccurate")))}function n(n){var r=s.timeout();if(!s.timedOut){if(o)return e(n||s._trace);s.clearTimeout(),s.duration=new l-a,o=!0,!n&&s.duration>r&&s._enableTimeouts&&(n=new Error("timeout of "+r+"ms exceeded")),t(n)}}function r(t){var e=t.call(u);e&&"function"==typeof e.then?(s.resetTimeout(),e.then(function(){n()},function(t){n(t||new Error("Promise rejected with no or falsy reason"))})):n()}var o,i,s=this,a=new l,u=this.ctx;if(u&&u.runnable&&u.runnable(this),this.callback=n,this.async){this.resetTimeout();try{this.fn.call(u,function(t){return t instanceof Error||"[object Error]"===p.call(t)?n(t):null!=t?n("[object Object]"===Object.prototype.toString.call(t)?new Error("done() invoked with non-Error: "+JSON.stringify(t)):new Error("done() invoked with non-Error: "+t)):void n()})}catch(c){n(c)}}else{if(this.asyncOnly)return n(new Error("--async-only option in use without declaring `done()`"));try{this.pending?n():r(this.fn)}catch(c){n(c)}}}}),t.register("runner.js",function(t,e,r){function o(t){var e=this;this._globals=[],this._abort=!1,this.suite=t,this.total=t.total(),this.failures=0,this.on("test end",function(t){e.checkGlobals(t)}),this.on("hook end",function(t){e.checkGlobals(t)}),this.grep(/.*/),this.globals(this.globalProps().concat(u()))}function s(){}function a(t,e){return p(e,function(e){if(/^d+/.test(e))return!1;if(n.navigator&&/^getInterface/.test(e))return!1;if(n.navigator&&/^\d+/.test(e))return!1;if(/^mocha-/.test(e))return!1;var r=p(t,function(t){return~t.indexOf("*")?0==e.indexOf(t.split("*")[0]):e==t});return 0==r.length&&(!n.navigator||"onerror"!==e)})}function u(){if("object"==typeof i&&"string"==typeof i.version){var t=i.version.split(".").reduce(function(t,e){return t<<8|e});if(2315>t)return["errno"]}return[]}var l=r("browser/events").EventEmitter,c=r("browser/debug")("mocha:runner"),f=(r("./test"),r("./utils")),p=f.filter,h=(f.keys,["setTimeout","clearTimeout","setInterval","clearInterval","XMLHttpRequest","Date"]);t.exports=o,o.immediately=n.setImmediate||i.nextTick,s.prototype=l.prototype,o.prototype=new s,o.prototype.constructor=o,o.prototype.grep=function(t,e){return c("grep %s",t),this._grep=t,this._invert=e,this.total=this.grepTotal(this.suite),this},o.prototype.grepTotal=function(t){var e=this,n=0;return t.eachTest(function(t){var r=e._grep.test(t.fullTitle());e._invert&&(r=!r),r&&n++}),n},o.prototype.globalProps=function(){for(var t=f.keys(n),e=0;e1?this.fail(t,new Error("global leaks detected: "+e.join(", "))):e.length&&this.fail(t,new Error("global leak detected: "+e[0])))}},o.prototype.fail=function(t,e){++this.failures,t.state="failed","string"==typeof e&&(e=new Error('the string "'+e+'" was thrown, throw an Error :)')),this.emit("fail",t,e)},o.prototype.failHook=function(t,e){this.fail(t,e),this.suite.bail()&&this.emit("end")},o.prototype.hook=function(t,e){function n(t){var o=i[t];return o?s.failures&&r.bail()?e():(s.currentRunnable=o,o.ctx.currentTest=s.test,s.emit("hook",o),o.on("error",function(t){s.failHook(o,t)}),void o.run(function(r){o.removeAllListeners("error");var i=o.error();return i&&s.fail(s.test,i),r?(s.failHook(o,r),e(r)):(s.emit("hook end",o),delete o.ctx.currentTest,void n(++t))})):e()}var r=this.suite,i=r["_"+t],s=this;o.immediately(function(){n(0)})},o.prototype.hooks=function(t,e,n){function r(s){return o.suite=s,s?void o.hook(t,function(t){if(t){var s=o.suite;return o.suite=i,n(t,s)}r(e.pop())}):(o.suite=i,n())}var o=this,i=this.suite;r(e.pop())},o.prototype.hookUp=function(t,e){var n=[this.suite].concat(this.parents()).reverse();this.hooks(t,n,e)},o.prototype.hookDown=function(t,e){var n=[this.suite].concat(this.parents());this.hooks(t,n,e)},o.prototype.parents=function(){for(var t=this.suite,e=[];t=t.parent;)e.push(t);return e},o.prototype.runTest=function(t){var e=this.test,n=this;this.asyncOnly&&(e.asyncOnly=!0);try{e.on("error",function(t){n.fail(e,t)}),e.run(t)}catch(r){t(r)}},o.prototype.runTests=function(t,e){function n(t,r,o){var s=i.suite;i.suite=o?r.parent:r,i.suite?i.hookUp("afterEach",function(t,o){return i.suite=s,t?n(t,o,!0):void e(r)}):(i.suite=s,e(r))}function r(a,u){if(i.failures&&t._bail)return e();if(i._abort)return e();if(a)return n(a,u,!0);if(o=s.shift(),!o)return e();var l=i._grep.test(o.fullTitle());return i._invert&&(l=!l),l?o.pending?(i.emit("pending",o),i.emit("test end",o),r()):(i.emit("test",i.test=o),void i.hookDown("beforeEach",function(t,e){return t?n(t,e,!1):(i.currentRunnable=i.test,void i.runTest(function(t){return o=i.test,t?(i.fail(o,t),i.emit("test end",o),i.hookUp("afterEach",r)):(o.state="passed",i.emit("pass",o),i.emit("test end",o),void i.hookUp("afterEach",r))}))})):r()}var o,i=this,s=t.tests.slice();this.next=r,r()},o.prototype.runSuite=function(t,e){function n(e){if(e)return e==t?r():r(e);if(i._abort)return r();var o=t.suites[s++];return o?void i.runSuite(o,n):r()}function r(n){i.suite=t,i.hook("afterAll",function(){i.emit("suite end",t),e(n)})}var o=this.grepTotal(t),i=this,s=0;return c("run suite %s",t.fullTitle()),o?(this.emit("suite",this.suite=t),void this.hook("beforeAll",function(e){return e?r():void i.runTests(t,n)})):e()},o.prototype.uncaught=function(t){t?c("uncaught exception %s",t!==function(){return this}.call(t)?t:t.message||t):(c("uncaught undefined exception"),t=new Error("Caught undefined error, did you throw without specifying what?")),t.uncaught=!0;var e=this.currentRunnable;if(e){var n=e.state;if(this.fail(e,t),e.clearTimeout(),!n)return"test"==e.type?(this.emit("test end",e),void this.hookUp("afterEach",this.next)):void this.emit("end")}},o.prototype.run=function(t){function e(t){n.uncaught(t)}var n=this,t=t||function(){};return c("start"),this.on("end",function(){c("end"),i.removeListener("uncaughtException",e),t(n.failures)}),this.emit("start"),this.runSuite(this.suite,function(){c("finished running"),n.emit("end")}),i.on("uncaughtException",e),this},o.prototype.abort=function(){c("aborting"),this._abort=!0}}),t.register("suite.js",function(t,e,n){function r(t,e){this.title=t;var n=function(){};n.prototype=e,this.ctx=new n,this.suites=[],this.tests=[],this.pending=!1,this._beforeEach=[],this._beforeAll=[],this._afterEach=[],this._afterAll=[],this.root=!t,this._timeout=2e3,this._enableTimeouts=!0,this._slow=75,this._bail=!1}function o(){}var i=n("browser/events").EventEmitter,s=n("browser/debug")("mocha:suite"),a=n("./ms"),u=n("./utils"),l=n("./hook");e=t.exports=r,e.create=function(t,e){var n=new r(e,t.ctx);return n.parent=t,t.pending&&(n.pending=!0),e=n.fullTitle(),t.addSuite(n),n},o.prototype=i.prototype,r.prototype=new o,r.prototype.constructor=r,r.prototype.clone=function(){var t=new r(this.title);return s("clone"),t.ctx=this.ctx,t.timeout(this.timeout()),t.enableTimeouts(this.enableTimeouts()),t.slow(this.slow()),t.bail(this.bail()),t},r.prototype.timeout=function(t){return 0==arguments.length?this._timeout:(0===t&&(this._enableTimeouts=!1),"string"==typeof t&&(t=a(t)),s("timeout %d",t),this._timeout=parseInt(t,10),this)},r.prototype.enableTimeouts=function(t){return 0===arguments.length?this._enableTimeouts:(s("enableTimeouts %s",t),this._enableTimeouts=t,this)},r.prototype.slow=function(t){return 0===arguments.length?this._slow:("string"==typeof t&&(t=a(t)),s("slow %d",t),this._slow=t,this)},r.prototype.bail=function(t){return 0==arguments.length?this._bail:(s("bail %s",t),this._bail=t,this)},r.prototype.beforeAll=function(t,e){if(this.pending)return this;"function"==typeof t&&(e=t,t=e.name),t='"before all" hook'+(t?": "+t:"");var n=new l(t,e);return n.parent=this,n.timeout(this.timeout()),n.enableTimeouts(this.enableTimeouts()),n.slow(this.slow()),n.ctx=this.ctx,this._beforeAll.push(n),this.emit("beforeAll",n),this},r.prototype.afterAll=function(t,e){if(this.pending)return this;"function"==typeof t&&(e=t,t=e.name),t='"after all" hook'+(t?": "+t:"");var n=new l(t,e);return n.parent=this,n.timeout(this.timeout()),n.enableTimeouts(this.enableTimeouts()),n.slow(this.slow()),n.ctx=this.ctx,this._afterAll.push(n),this.emit("afterAll",n),this},r.prototype.beforeEach=function(t,e){if(this.pending)return this;"function"==typeof t&&(e=t,t=e.name),t='"before each" hook'+(t?": "+t:"");var n=new l(t,e);return n.parent=this,n.timeout(this.timeout()),n.enableTimeouts(this.enableTimeouts()),n.slow(this.slow()),n.ctx=this.ctx,this._beforeEach.push(n),this.emit("beforeEach",n),this},r.prototype.afterEach=function(t,e){if(this.pending)return this;"function"==typeof t&&(e=t,t=e.name),t='"after each" hook'+(t?": "+t:"");var n=new l(t,e);return n.parent=this,n.timeout(this.timeout()),n.enableTimeouts(this.enableTimeouts()),n.slow(this.slow()),n.ctx=this.ctx,this._afterEach.push(n),this.emit("afterEach",n),this},r.prototype.addSuite=function(t){return t.parent=this,t.timeout(this.timeout()),t.enableTimeouts(this.enableTimeouts()),t.slow(this.slow()),t.bail(this.bail()),this.suites.push(t),this.emit("suite",t),this},r.prototype.addTest=function(t){return t.parent=this,t.timeout(this.timeout()),t.enableTimeouts(this.enableTimeouts()),t.slow(this.slow()),t.ctx=this.ctx,this.tests.push(t),this.emit("test",t),this},r.prototype.fullTitle=function(){if(this.parent){var t=this.parent.fullTitle();if(t)return t+" "+this.title}return this.title},r.prototype.total=function(){return u.reduce(this.suites,function(t,e){return t+e.total()},0)+this.tests.length},r.prototype.eachTest=function(t){return u.forEach(this.tests,t),u.forEach(this.suites,function(e){e.eachTest(t)}),this}}),t.register("test.js",function(t,e,n){function r(t,e){i.call(this,t,e),this.pending=!e,this.type="test"}function o(){}var i=n("./runnable");t.exports=r,o.prototype=i.prototype,r.prototype=new o,r.prototype.constructor=r}),t.register("utils.js",function(t,e,n){function r(t){return!~p.indexOf(t)}function o(t){return t.replace(//g,">").replace(/\/\/(.*)/gm,'').replace(/('.*?')/gm,'$1 ').replace(/(\d+\.\d+)/gm,'$1 ').replace(/(\d+)/gm,'$1 ').replace(/\bnew[ \t]+(\w+)/gm,'new $1 ').replace(/\b(function|new|throw|return|var|if|else)\b/gm,'$1 ')}var i=n("browser/fs"),s=n("browser/path"),a=s.basename,u=i.existsSync||s.existsSync,l=n("browser/glob"),c=s.join,f=n("browser/debug")("mocha:watch"),p=["node_modules",".git"];e.escape=function(t){return String(t).replace(/&/g,"&").replace(/"/g,""").replace(//g,">")},e.forEach=function(t,e,n){for(var r=0,o=t.length;o>r;r++)e.call(n,t[r],r)},e.map=function(t,e,n){for(var r=[],o=0,i=t.length;i>o;o++)r.push(e.call(n,t[o],o));return r},e.indexOf=function(t,e,n){for(var r=n||0,o=t.length;o>r;r++)if(t[r]===e)return r;return-1},e.reduce=function(t,e,n){for(var r=n,o=0,i=t.length;i>o;o++)r=e(r,t[o],o,t);return r},e.filter=function(t,e){for(var n=[],r=0,o=t.length;o>r;r++){var i=t[r];e(i,r,t)&&n.push(i)}return n},e.keys=Object.keys||function(t){var e=[],n=Object.prototype.hasOwnProperty;for(var r in t)n.call(t,r)&&e.push(r);return e},e.watch=function(t,e){var n={interval:100};t.forEach(function(t){f("file %s",t),i.watchFile(t,n,function(n,r){r.mtime *{?/,"").replace(/\s+\}$/,"");var n=t.match(/^\n?( *)/)[1].length,r=t.match(/^\n?(\t*)/)[1].length,o=new RegExp("^\n?"+(r?" ":" ")+"{"+(r?r:n)+"}","gm");return t=t.replace(o,""),e.trim(t)},e.trim=function(t){return t.replace(/^\s+|\s+$/g,"")},e.parseQuery=function(t){return e.reduce(t.replace("?","").split("&"),function(t,e){var n=e.indexOf("="),r=e.slice(0,n),o=e.slice(++n);return t[r]=decodeURIComponent(o),t},{})},e.highlightTags=function(t){for(var e=document.getElementById("mocha").getElementsByTagName(t),n=0,r=e.length;r>n;++n)e[n].innerHTML=o(e[n].innerHTML)},e.stringify=function(t){return t instanceof RegExp?t.toString():JSON.stringify(e.canonicalize(t),null,2).replace(/,(\n|$)/g,"$1")},e.canonicalize=function(t,n){if(n=n||[],-1!==e.indexOf(n,t))return"[Circular]";var r;return"[object Array]"==={}.toString.call(t)?(n.push(t),r=e.map(t,function(t){return e.canonicalize(t,n)}),n.pop()):"object"==typeof t&&null!==t?(n.push(t),r={},e.forEach(e.keys(t).sort(),function(o){r[o]=e.canonicalize(t[o],n)}),n.pop()):r=t,r},e.lookupFiles=function h(t,e,n){var r=[],o=new RegExp("\\.("+e.join("|")+")$");if(!u(t)){if(!u(t+".js")){if(r=l.sync(t),!r.length)throw new Error("cannot resolve path (or pattern) '"+t+"'");return r}t+=".js"}try{var s=i.statSync(t);if(s.isFile())return t}catch(f){return}return i.readdirSync(t).forEach(function(s){s=c(t,s);try{var u=i.statSync(s);if(u.isDirectory())return void(n&&(r=r.concat(h(s,e,n))))}catch(l){return}u.isFile()&&o.test(s)&&"."!==a(s)[0]&&r.push(s)}),r}});var n=function(){return this}(),r=n.Date,o=n.setTimeout,i=(n.setInterval,n.clearTimeout,n.clearInterval,{});i.exit=function(t){},i.stdout={};var s=[],a=n.onerror;i.removeListener=function(t,e){if("uncaughtException"==t){a?n.onerror=a:n.onerror=function(){};var r=u.utils.indexOf(s,e);-1!=r&&s.splice(r,1)}},i.on=function(t,e){"uncaughtException"==t&&(n.onerror=function(t,n,r){return e(new Error(t+" ("+n+":"+r+")")),!0},s.push(e))};var u=n.Mocha=t("mocha"),l=n.mocha=new u({reporter:"html"});l.suite.removeAllListeners("pre-require");var c,f=[];u.Runner.immediately=function(t){f.push(t),c||(c=o(e,0))},l.throwError=function(t){throw u.utils.forEach(s,function(e){e(t)}),t},l.ui=function(t){return u.prototype.ui.call(this,t),this.suite.emit("pre-require",n,null,this),this},l.setup=function(t){"string"==typeof t&&(t={ui:t});for(var e in t)this[e](t[e]);return this},l.run=function(t){var e=l.options;l.globals("location");var r=u.utils.parseQuery(n.location.search||"");return r.grep&&l.grep(r.grep),r.invert&&l.invert(),u.prototype.run.call(l,function(r){var o=n.document;o&&o.getElementById("mocha")&&e.noHighlighting!==!0&&u.utils.highlightTags("code"),t&&t(r)})},u.process=i}(),t.mocha});
================================================
FILE: test/dist/fixtures/sinon-chai.js
================================================
!function(t,e,n){var a=n.call(t);"object"==typeof modules?modules.define("sinon-chai",e,function(t){t(a)}):t.sinonChai=a}(this,["chai"],function(){var t;return function(t){!function(e){"use strict";"function"==typeof require&&"object"==typeof exports&&"object"==typeof module?module.exports=e:"function"==typeof define&&define.amd?define(function(){return e}):t.use(e)}(function(t,e){"use strict";function n(t){return"function"==typeof t&&"function"==typeof t.getCall&&"function"==typeof t.calledWithExactly}function a(t){return 1===t?"once":2===t?"twice":3===t?"thrice":(t||0)+" times"}function i(t){return t&&n(t.proxy)}function o(t){if(!n(t._obj)&&!i(t._obj))throw new TypeError(e.inspect(t._obj)+" is not a spy or a call to a spy!")}function c(t,e,a,i,o){function c(e){return t.printf.apply(t,e)}var l=i?"always have ":"have ";return a=a||"",n(t.proxy)&&(t=t.proxy),{affirmative:function(){return c(["expected %n to "+l+e+a].concat(o))},negative:function(){return c(["expected %n to not "+l+e].concat(o))}}}function l(n,a,i){e.addProperty(t.Assertion.prototype,n,function(){o(this);var t=c(this._obj,a,i,!1);this.assert(this._obj[n],t.affirmative,t.negative)})}function r(n,i,l){e.addMethod(t.Assertion.prototype,n,function(t){o(this);var e=c(this._obj,i,l,!1,[a(t)]);this.assert(this._obj[n]===t,e.affirmative,e.negative)})}function s(t,n,a){return function(){o(this);var i="always"+t[0].toUpperCase()+t.substring(1),l=e.flag(this,"always")&&"function"==typeof this._obj[i],r=l?i:t,s=c(this._obj,n,a,l,h.call(arguments));this.assert(this._obj[r].apply(this._obj,arguments),s.affirmative,s.negative)}}function u(n,a,i){var o=s(n,a,i);e.addProperty(t.Assertion.prototype,n,o)}function f(n,a,i,o){var c=s(a,i,o);e.addMethod(t.Assertion.prototype,n,c)}function d(t,e,n){f(t,t,e,n)}var h=Array.prototype.slice;e.addProperty(t.Assertion.prototype,"always",function(){e.flag(this,"always",!0)}),l("called","been called"," at least once, but it was never called"),r("callCount","been called exactly %1",", but it was called %c%C"),l("calledOnce","been called exactly once",", but it was called %c%C"),l("calledTwice","been called exactly twice",", but it was called %c%C"),l("calledThrice","been called exactly thrice",", but it was called %c%C"),u("calledWithNew","been called with new"),d("calledBefore","been called before %1"),d("calledAfter","been called after %1"),d("calledOn","been called with %1 as this",", but it was called with %t instead"),d("calledWith","been called with arguments %*","%C"),d("calledWithExactly","been called with exact arguments %*","%C"),d("calledWithMatch","been called with arguments matching %*","%C"),d("returned","returned %1"),f("thrown","threw","thrown %1")})}.call(this,{use:function(e){t=e}}),t});
================================================
FILE: test/dist/fixtures/sinon.js
================================================
!function(e,t){var n=t.call(e);"object"==typeof modules?modules.define("sinon",function(e){e(n)}):e.sinon=n}(this,function(){return function(e,t){"use strict";"function"==typeof define&&define.amd?define("sinon",[],function(){return e.sinon=t()}):"object"==typeof exports?module.exports=t():e.sinon=t()}(this,function(){"use strict";var samsam,formatio,lolex;!function(){function define(e,t,n){"samsam"==e?samsam=t():"function"==typeof t&&0===e.length?lolex=t():"function"==typeof n&&(formatio=n(samsam))}define.amd={},("function"==typeof define&&define.amd&&function(e){define("samsam",e)}||"object"==typeof module&&function(e){module.exports=e()}||function(e){this.samsam=e()})(function(){function e(e){var t=e;return"number"==typeof e&&e!==t}function t(e){return f.toString.call(e).split(/[ \]]/)[1]}function n(e){if("Arguments"===t(e))return!0;if("object"!=typeof e||"number"!=typeof e.length||"Array"===t(e))return!1;if("function"==typeof e.callee)return!0;try{e[e.length]=6,delete e[e.length]}catch(n){return!0}return!1}function r(e){if(!e||1!==e.nodeType||!d)return!1;try{e.appendChild(d),e.removeChild(d)}catch(t){return!1}return!0}function o(e){var t,n=[];for(t in e)f.hasOwnProperty.call(e,t)&&n.push(t);return n}function i(e){return"function"==typeof e.getTime&&e.getTime()==e.valueOf()}function s(e){return 0===e&&1/e===-(1/0)}function a(t,n){return t===n||e(t)&&e(n)?0!==t||s(t)===s(n):void 0}function u(s,u){function l(e){return!("object"!=typeof e||null===e||e instanceof Boolean||e instanceof Date||e instanceof Number||e instanceof RegExp||e instanceof String)}function c(e,t){var n;for(n=0;nO;O++){if(S=A[O],!f.hasOwnProperty.call(u,S))return!1;if(R=s[S],D=u[S],I=l(R),N=l(D),P=I?c(d,R):-1,M=N?c(h,D):-1,L=-1!==P?p[P]:g+"["+JSON.stringify(S)+"]",F=-1!==M?y[M]:b+"["+JSON.stringify(S)+"]",m[L+F])return!0;if(-1===P&&I&&(d.push(R),p.push(L)),-1===M&&N&&(h.push(D),y.push(F)),I&&N&&(m[L+F]=!0),!v(R,D,L,F))return!1}return!0}(s,u,"$1","$2")}function l(e,t){if(0===t.length)return!0;var n,r,o,i;for(n=0,r=e.length;r>n;++n)if(c(e[n],t[0])){for(o=0,i=t.length;i>o;++o)if(!c(e[n+o],t[o]))return!1;return!0}return!1}var c,f=Object.prototype,d="undefined"!=typeof document&&document.createElement("div");return c=function h(e,n){if(n&&"function"==typeof n.test)return n.test(e);if("function"==typeof n)return n(e)===!0;if("string"==typeof n){n=n.toLowerCase();var r="string"==typeof e||!!e;return r&&String(e).toLowerCase().indexOf(n)>=0}if("number"==typeof n)return n===e;if("boolean"==typeof n)return n===e;if("undefined"==typeof n)return"undefined"==typeof e;if(null===n)return null===e;if("Array"===t(e)&&"Array"===t(n))return l(e,n);if(n&&"object"==typeof n){if(n===e)return!0;var o;for(o in n){var i=e[o];if("undefined"==typeof i&&"function"==typeof e.getAttribute&&(i=e.getAttribute(o)),null===n[o]||"undefined"==typeof n[o]){if(i!==n[o])return!1}else if("undefined"==typeof i||!h(i,n[o]))return!1}return!0}throw new Error("Matcher was not a string, a number, a function, a boolean or an object")},{isArguments:n,isElement:r,isDate:i,isNegZero:s,identical:a,deepEqual:u,match:c,keys:o}}),("function"==typeof define&&define.amd&&function(e){define("formatio",["samsam"],e)}||"object"==typeof module&&function(e){module.exports=e(require("samsam"))}||function(e){this.formatio=e(this.samsam)})(function(e){function t(e){if(!e)return"";if(e.displayName)return e.displayName;if(e.name)return e.name;var t=e.toString().match(/function\s+([^\(]+)/m);return t&&t[1]||""}function n(e,n){var r,o,i=t(n&&n.constructor),a=e.excludeConstructors||s.excludeConstructors||[];for(r=0,o=a.length;o>r;++r){if("string"==typeof a[r]&&a[r]===i)return"";if(a[r].test&&a[r].test(i))return""}return i}function r(e,t){if("object"!=typeof e)return!1;var n,r;for(n=0,r=t.length;r>n;++n)if(t[n]===e)return!0;return!1}function o(t,n,i,s){if("string"==typeof n){var u=t.quoteStrings,l="boolean"!=typeof u||u;return i||l?'"'+n+'"':n}if("function"==typeof n&&!(n instanceof RegExp))return o.func(n);if(i=i||[],r(n,i))return"[Circular]";if("[object Array]"===Object.prototype.toString.call(n))return o.array.call(t,n,i);if(!n)return String(1/n===-(1/0)?"-0":n);if(e.isElement(n))return o.element(n);if("function"==typeof n.toString&&n.toString!==Object.prototype.toString)return n.toString();var c,f;for(c=0,f=a.length;f>c;c++)if(n===a[c].object)return a[c].value;return o.object.call(t,n,i,s)}function i(e){for(var t in e)this[t]=e[t]}var s={excludeConstructors:["Object",/^.$/],quoteStrings:!0,limitChildrenCount:0},a=(Object.prototype.hasOwnProperty,[]);return"undefined"!=typeof global&&a.push({object:global,value:"[object global]"}),"undefined"!=typeof document&&a.push({object:document,value:"[object HTMLDocument]"}),"undefined"!=typeof window&&a.push({object:window,value:"[object Window]"}),o.func=function(e){return"function "+t(e)+"() {}"},o.array=function(e,t){t=t||[],t.push(e);var n,r,i=[];for(r=this.limitChildrenCount>0?Math.min(this.limitChildrenCount,e.length):e.length,n=0;r>n;++n)i.push(o(this,e[n],t));return r0?Math.min(this.limitChildrenCount,p.length):p.length,c=0;d>c;++c)a=p[c],l=t[a],u=r(l,i)?"[Circular]":o(this,l,i,s+2),u=(/\s/.test(a)?'"'+a+'"':a)+": "+u,y+=u.length,h.push(u);var m=n(this,t),v=m?"["+m+"] ":"",g="";for(c=0,f=s;f>c;++c)g+=" ";return d80?v+"{\n "+g+h.join(",\n "+g)+"\n"+g+"}":v+"{ "+h.join(", ")+" }"},o.element=function(e){var t,n,r,o,i,s=e.tagName.toLowerCase(),a=e.attributes,u=[];for(r=0,o=a.length;o>r;++r)t=a.item(r),n=t.nodeName.toLowerCase().replace("html:",""),i=t.nodeValue,"contenteditable"===n&&"inherit"===i||i&&u.push(n+'="'+i+'"');var l="<"+s+(u.length>0?" ":""),c=e.innerHTML;c.length>20&&(c=c.substr(0,20)+"[...]");var f=l+u.join(" ")+">"+c+""+s+">";return f.replace(/ contentEditable="inherit"/,"")},i.prototype={functionName:t,configure:function(e){return new i(e)},constructorName:function(e){return n(this,e)},ascii:function(e,t,n){return o(this,e,t,n)}},i.prototype}),!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;"undefined"!=typeof window?t=window:"undefined"!=typeof global?t=global:"undefined"!=typeof self&&(t=self),t.lolex=e()}}(function(){var define,module,exports;return function e(t,n,r){function o(s,a){if(!n[s]){if(!t[s]){var u="function"==typeof require&&require;if(!a&&u)return u(s,!0);if(i)return i(s,!0);var l=new Error("Cannot find module '"+s+"'");throw l.code="MODULE_NOT_FOUND",l}var c=n[s]={exports:{}};t[s][0].call(c.exports,function(e){var n=t[s][1][e];return o(n?n:e)},c,c.exports,e,t,n,r)}return n[s].exports}for(var i="function"==typeof require&&require,s=0;s3||!/^(\d\d:){0,2}\d\d?$/.test(e))throw new Error("tick only understands numbers and 'h:m:s'");for(;o--;){if(t=parseInt(n[o],10),t>=60)throw new Error("Invalid time "+e);i+=t*Math.pow(60,r-o-1)}return 1e3*i}function getEpoch(e){if(!e)return 0;if("function"==typeof e.getTime)return e.getTime();if("number"==typeof e)return e;throw new TypeError("now should be milliseconds since UNIX epoch")}function inRange(e,t,n){return n&&n.callAt>=e&&n.callAt<=t}function mirrorDateProperties(e,t){var n;for(n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return t.now?e.now=function(){return e.clock.now}:delete e.now,t.toSource?e.toSource=function(){return t.toSource()}:delete e.toSource,e.toString=function(){return t.toString()},e.prototype=t.prototype,e.parse=t.parse,e.UTC=t.UTC,e.prototype.toUTCString=t.prototype.toUTCString,e}function createDate(){function e(t,n,r,o,i,s,a){switch(arguments.length){case 0:return new NativeDate(e.clock.now);case 1:return new NativeDate(t);case 2:return new NativeDate(t,n);case 3:return new NativeDate(t,n,r);case 4:return new NativeDate(t,n,r,o);case 5:return new NativeDate(t,n,r,o,i);case 6:return new NativeDate(t,n,r,o,i,s);default:return new NativeDate(t,n,r,o,i,s,a)}}return mirrorDateProperties(e,NativeDate)}function addTimer(e,t){if(void 0===t.func)throw new Error("Callback must be provided to timer calls");return e.timers||(e.timers={}),t.id=uniqueTimerId++,t.createdAt=e.now,t.callAt=e.now+(t.delay||(e.duringTick?1:0)),e.timers[t.id]=t,addTimerReturnsObject?{id:t.id,ref:NOOP,unref:NOOP}:t.id}function compareTimers(e,t){return e.callAtt.callAt?1:e.immediate&&!t.immediate?-1:!e.immediate&&t.immediate?1:e.createdAtt.createdAt?1:e.idt.id?1:void 0}function firstTimerInRange(e,t,n){var r,o,i=e.timers,s=null;for(r in i)i.hasOwnProperty(r)&&(o=inRange(t,n,i[r]),!o||s&&1!==compareTimers(s,i[r])||(s=i[r]));return s}function callTimer(clock,timer){var exception;"number"==typeof timer.interval?clock.timers[timer.id].callAt+=timer.interval:delete clock.timers[timer.id];try{"function"==typeof timer.func?timer.func.apply(null,timer.args):eval(timer.func)}catch(e){exception=e}if(clock.timers[timer.id]){if(exception)throw exception}else if(exception)throw exception}function uninstall(e,t){var n,r,o;for(r=0,o=e.methods.length;o>r;r++)if(n=e.methods[r],t[n].hadOwnProperty)t[n]=e["_"+n];else try{delete t[n]}catch(i){}e.methods=[]}function hijackMethod(e,t,n){var r;if(n[t].hadOwnProperty=Object.prototype.hasOwnProperty.call(e,t),n["_"+t]=e[t],"Date"===t){var o=mirrorDateProperties(n[t],e[t]);e[t]=o}else{e[t]=function(){return n[t].apply(n,arguments)};for(r in n[t])n[t].hasOwnProperty(r)&&(e[t][r]=n[t][r])}e[t].clock=n}function createClock(e){var t={now:getEpoch(e),timeouts:{},Date:createDate()};return t.Date.clock=t,t.setTimeout=function(e,n){return addTimer(t,{func:e,args:Array.prototype.slice.call(arguments,2),delay:n})},t.clearTimeout=function(e){e&&(t.timers||(t.timers=[]),"object"==typeof e&&(e=e.id),t.timers.hasOwnProperty(e)&&delete t.timers[e])},t.setInterval=function(e,n){return addTimer(t,{func:e,args:Array.prototype.slice.call(arguments,2),delay:n,interval:n})},t.clearInterval=function(e){t.clearTimeout(e)},t.setImmediate=function(e){return addTimer(t,{func:e,args:Array.prototype.slice.call(arguments,1),immediate:!0})},t.clearImmediate=function(e){t.clearTimeout(e)},t.tick=function(e){e="number"==typeof e?e:parseTime(e);var n=t.now,r=t.now+e,o=t.now,i=firstTimerInRange(t,n,r);t.duringTick=!0;for(var s;i&&r>=n;){if(t.timers[i.id]){n=t.now=i.callAt;try{callTimer(t,i)}catch(a){s=s||a}}i=firstTimerInRange(t,o,r),o=n}if(t.duringTick=!1,t.now=r,s)throw s;return t.now},t.reset=function(){t.timers={}},t}function detectKnownFailSituation(e){if(!(e.indexOf("Date")<0)){if(e.indexOf("setTimeout")<0)throw new Error("Native setTimeout will not work when Date is faked");if(e.indexOf("setImmediate")<0)throw new Error("Native setImmediate will not work when Date is faked")}}var glbl=global;global.setTimeout=glbl.setTimeout,global.clearTimeout=glbl.clearTimeout,global.setImmediate=glbl.setImmediate,global.clearImmediate=glbl.clearImmediate,global.setInterval=glbl.setInterval,global.clearInterval=glbl.clearInterval,global.Date=glbl.Date;var NOOP=function(){},timeoutResult=setTimeout(NOOP,0),addTimerReturnsObject="object"==typeof timeoutResult;clearTimeout(timeoutResult);var NativeDate=Date,uniqueTimerId=1,timers={setTimeout:setTimeout,clearTimeout:clearTimeout,setImmediate:global.setImmediate,clearImmediate:global.clearImmediate,setInterval:setInterval,clearInterval:clearInterval,Date:Date},keys=Object.keys||function(e){var t,n=[];for(t in e)e.hasOwnProperty(t)&&n.push(t);return n};exports.timers=timers,exports.createClock=createClock,exports.install=function(e,t,n){var r,o;"number"==typeof e&&(n=t,t=e,e=null),e||(e=global);var i=createClock(t);for(i.uninstall=function(){uninstall(i,e)},i.methods=n||[],0===i.methods.length&&(i.methods=keys(timers)),detectKnownFailSituation(i.methods),r=0,o=i.methods.length;o>r;r++)hijackMethod(e,i.methods[r],i);return i}}(global||this)}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}]},{},[1])(1)})}();var define,sinon=function(){function e(e,n,r){t=r.exports=e("./sinon/util/core"),e("./sinon/extend"),e("./sinon/typeOf"),e("./sinon/times_in_words"),e("./sinon/spy"),e("./sinon/call"),e("./sinon/behavior"),e("./sinon/stub"),e("./sinon/mock"),e("./sinon/collection"),e("./sinon/assert"),e("./sinon/sandbox"),e("./sinon/test"),e("./sinon/test_case"),e("./sinon/match"),e("./sinon/format"),e("./sinon/log_error")}var t,n="undefined"!=typeof module&&module.exports&&"function"==typeof require,r="function"==typeof define&&"object"==typeof define.amd&&define.amd;return r?define(e):n?(e(require,module.exports,module),t=module.exports):t={},t}();return function(e){function t(e){var t=!1;try{e.appendChild(l),t=l.parentNode===e}catch(n){return!1}finally{try{e.removeChild(l)}catch(n){}}return t}function n(e){return l&&e&&1===e.nodeType&&t(e)}function r(e){return"function"==typeof e||!!(e&&e.constructor&&e.call&&e.apply)}function o(e){return"number"==typeof e&&isNaN(e)}function i(e,t){for(var n in t)c.call(e,n)||(e[n]=t[n])}function s(e){return"function"==typeof e&&"function"==typeof e.restore&&e.restore.sinon}function a(e){return e.wrapMethod=function(t,n,o){function s(e){var t;if(r(e)){if(e.restore&&e.restore.sinon)t=new TypeError("Attempted to wrap "+n+" which is already wrapped");else if(e.calledBefore){var o=e.returns?"stubbed":"spied on";t=new TypeError("Attempted to wrap "+n+" which is already "+o)}}else t=new TypeError("Attempted to wrap "+typeof e+" property "+n+" as function");if(t)throw e&&e.stackTrace&&(t.stack+="\n--------------\n"+e.stackTrace),t}if(!t)throw new TypeError("Should wrap property of object");if("function"!=typeof o&&"object"!=typeof o)throw new TypeError("Method wrapper should be a function or a property descriptor");var a,u,l,d=t.hasOwnProperty?t.hasOwnProperty(n):c.call(t,n);if(f){var h="function"==typeof o?{value:o}:o,p=e.getPropertyDescriptor(t,n);if(p?p.restore&&p.restore.sinon&&(a=new TypeError("Attempted to wrap "+n+" which is already wrapped")):a=new TypeError("Attempted to wrap "+typeof u+" property "+n+" as function"),a)throw p&&p.stackTrace&&(a.stack+="\n--------------\n"+p.stackTrace),a;var y=e.objectKeys(h);for(l=0;lt;t++)if(!e[t-1].calledBefore(e[t])||!e[t].called)return!1;return!0},e.orderByFirstCall=function(e){return e.sort(function(e,t){var n=e.getCall(0),r=t.getCall(0),o=n&&n.callId||-1,i=r&&r.callId||-1;return i>o?-1:1})},e.createStubInstance=function(t){if("function"!=typeof t)throw new TypeError("The constructor should be a function.");return e.stub(e.create(t.prototype))},e.restore=function(e){if(null!==e&&"object"==typeof e)for(var t in e)s(e[t])&&e[t].restore();else s(e)&&e.restore()},e}function u(e,t){a(t)}var l="undefined"!=typeof document&&document.createElement("div"),c=Object.prototype.hasOwnProperty,f="keys"in Object,d="undefined"!=typeof module&&module.exports&&"function"==typeof require,h="function"==typeof define&&"object"==typeof define.amd&&define.amd;return h?void define(u):d?void u(require,module.exports,module):void(e&&a(e))}("object"==typeof sinon&&sinon),function(e){function t(e){function t(e){var t,r,o,i=Array.prototype.slice.call(arguments,1);for(r=0;rthis.args.length)return!1;for(var n=0;t>n;n+=1)if(!e.deepEqual(arguments[n],this.args[n]))return!1;return!0},calledWithMatch:function(){var t=arguments.length;if(t>this.args.length)return!1;for(var n=0;t>n;n+=1){var r=this.args[n],o=arguments[n];if(!e.match||!e.match(o).test(r))return!1}return!0},calledWithExactly:function(){return arguments.length===this.args.length&&this.calledWith.apply(this,arguments)},notCalledWith:function(){return!this.calledWith.apply(this,arguments)},notCalledWithMatch:function(){return!this.calledWithMatch.apply(this,arguments)},returned:function(t){return e.deepEqual(t,this.returnValue)},threw:function(e){return"undefined"!=typeof e&&this.exception?this.exception===e||this.exception.name===e:!!this.exception},calledWithNew:function(){return this.proxy.prototype&&this.thisValue instanceof this.proxy},calledBefore:function(e){return this.callIde.callId},callArg:function(e){this.args[e]()},callArgOn:function(e,t){this.args[e].apply(t)},callArgWith:function(e){this.callArgOnWith.apply(this,[e,null].concat(r.call(arguments,1)))},callArgOnWith:function(e,t){var n=r.call(arguments,2);this.args[e].apply(t,n)},"yield":function(){this.yieldOn.apply(this,[null].concat(r.call(arguments,0)))},yieldOn:function(e){for(var n=this.args,o=0,i=n.length;i>o;++o)if("function"==typeof n[o])return void n[o].apply(e,r.call(arguments,1));t(this.proxy," cannot yield since no callback was passed.",n)},yieldTo:function(e){this.yieldToOn.apply(this,[e,null].concat(r.call(arguments,1)))},yieldToOn:function(e,n){for(var o=this.args,i=0,s=o.length;s>i;++i)if(o[i]&&"function"==typeof o[i][e])return void o[i][e].apply(n,r.call(arguments,2));t(this.proxy," cannot yield to '"+e+"' since no callback was passed.",o)},getStackFrames:function(){return this.stack&&this.stack.split("\n").slice(3)},toString:function(){for(var t=this.proxy.toString()+"(",n=[],r=0,o=this.args.length;o>r;++r)n.push(e.format(this.args[r]));return t=t+n.join(", ")+")","undefined"!=typeof this.returnValue&&(t+=" => "+e.format(this.returnValue)),this.exception&&(t+=" !"+this.exception.name,this.exception.message&&(t+="("+this.exception.message+")")),this.stack&&(t+=this.getStackFrames()[0].replace(/^\s*(?:at\s+|@)?/," at ")),t}};return o.invokeCallback=o["yield"],n.toString=o.toString,e.spyCall=n,n}function n(e,n,r){var o=e("./util/core");e("./match"),e("./format"),r.exports=t(o)}var r=Array.prototype.slice,o="undefined"!=typeof module&&module.exports&&"function"==typeof require,i="function"==typeof define&&"object"==typeof define.amd&&define.amd;return i?void define(n):o?void n(require,module.exports,module):void(e&&t(e))}("object"==typeof sinon&&sinon),function(sinonGlobal){function makeApi(sinon){function spy(e,t,n){if(!t&&"function"==typeof e)return spy.create(e);if(!e&&!t)return spy.create(function(){});if(n){for(var r=sinon.getPropertyDescriptor(e,t),o=0;or;r++)if(e[r].matches(t,n))return e[r]}function incrementCallCount(){this.called=!0,this.callCount+=1,this.notCalled=!1,this.calledOnce=1===this.callCount,this.calledTwice=2===this.callCount,this.calledThrice=3===this.callCount}function createCallProperties(){this.firstCall=this.getCall(0),this.secondCall=this.getCall(1),this.thirdCall=this.getCall(2),this.lastCall=this.getCall(this.callCount-1)}function createProxy(func,proxyLength){var p;return proxyLength?eval("p = (function proxy("+vars.substring(0,2*proxyLength-1)+") { return p.invoke(func, this, slice.call(arguments)); });"):p=function(){return p.invoke(func,this,slice.call(arguments))},p.isSinonProxy=!0,p}function delegateToCalls(e,t,n,r){spyApi[e]=function(){if(!this.called)return r?r.apply(this,arguments):!1;for(var o,i=0,s=0,a=this.callCount;a>s;s+=1)if(o=this.getCall(s),o[n||e].apply(o,arguments)&&(i+=1,t))return!0;return i===this.callCount}}var push=Array.prototype.push,slice=Array.prototype.slice,callId=0,vars="a,b,c,d,e,f,g,h,i,j,k,l",uuid=0,spyApi={reset:function(){if(this.invoking){var e=new Error("Cannot reset Sinon function while invoking it. Move the call to .reset outside of the callback.");throw e.name="InvalidResetException",e}if(this.called=!1,this.notCalled=!0,this.calledOnce=!1,this.calledTwice=!1,this.calledThrice=!1,this.callCount=0,this.firstCall=null,this.secondCall=null,this.thirdCall=null,this.lastCall=null,this.args=[],this.returnValues=[],this.thisValues=[],this.exceptions=[],this.callIds=[],this.stacks=[],this.fakes)for(var t=0;te||e>=this.callCount?null:sinon.spyCall(this,this.thisValues[e],this.args[e],this.returnValues[e],this.exceptions[e],this.callIds[e],this.stacks[e])},getCalls:function(){var e,t=[];for(e=0;ee.callIds[e.callCount-1]:!1},withArgs:function(){var e=slice.call(arguments);if(this.fakes){var t=matchingFake(this.fakes,e,!0);if(t)return t}else this.fakes=[];var n=this,r=this.instantiateFake();r.matchingAguments=e,r.parent=this,push.call(this.fakes,r),r.withArgs=function(){return n.withArgs.apply(n,arguments)};for(var o=0;on;++n){var o=" "+e.getCall(n).toString();/\n/.test(t[n-1])&&(o="\n"+o),push.call(t,o)}return t.length>0?"\n"+t.join("\n"):""},t:function(e){for(var t=[],n=0,r=e.callCount;r>n;++n)push.call(t,sinon.format(e.thisValues[n]));return t.join(", ")},"*":function(e,t){for(var n=[],r=0,o=t.length;o>r;++r)push.call(n,sinon.format(t[r]));return n.join(", ")}},sinon.extend(spy,spyApi),spy.spyCall=sinon.spyCall,sinon.spy=spy,spy}function loadDependencies(e,t,n){var r=e("./util/core");e("./call"),e("./extend"),e("./times_in_words"),e("./format"),n.exports=makeApi(r)}var isNode="undefined"!=typeof module&&module.exports&&"function"==typeof require,isAMD="function"==typeof define&&"object"==typeof define.amd&&define.amd;return isAMD?void define(loadDependencies):isNode?void loadDependencies(require,module.exports,module):void(sinonGlobal&&makeApi(sinonGlobal))}("object"==typeof sinon&&sinon),function(e){function t(e,t){return"string"==typeof e?(this.exception=new Error(t||""),this.exception.name=e):e?this.exception=e:this.exception=new Error("Error"),this}function n(e,t){var n=e.callArgAt;if(n>=0)return t[n];var r;n===a&&(r=t),n===u&&(r=i.call(t).reverse());for(var o=e.callArgProp,s=0,l=r.length;l>s;++s){if(!o&&"function"==typeof r[s])return r[s];if(o&&r[s]&&"function"==typeof r[s][o])return r[s][o]}return null}function r(e){function r(t,n,r){if(t.callArgAt<0){var o;return o=t.callArgProp?e.functionName(t.stub)+" expected to yield to '"+t.callArgProp+"', but no object with such a property was passed.":e.functionName(t.stub)+" expected to yield, but no callback was passed.",r.length>0&&(o+=" Received ["+s.call(r,", ")+"]"),o}return"argument at index "+t.callArgAt+" is not a function: "+n}function o(e,t){if("number"==typeof e.callArgAt){var o=n(e,t);if("function"!=typeof o)throw new TypeError(r(e,o,t));e.callbackAsync?l(function(){o.apply(e.callbackContext,e.callbackArguments)}):o.apply(e.callbackContext,e.callbackArguments)}}function c(e){return function(){var t=this[e].apply(this,arguments);return this.callbackAsync=!0,t}}var f={create:function(t){var n=e.extend({},e.behavior);return delete n.create,n.stub=t,n},isPresent:function(){return"number"==typeof this.callArgAt||this.exception||"number"==typeof this.returnArgAt||this.returnThis||this.returnValueDefined},invoke:function(e,t){if(o(this,t),this.exception)throw this.exception;return"number"==typeof this.returnArgAt?t[this.returnArgAt]:this.returnThis?e:this.returnValue},onCall:function(e){return this.stub.onCall(e)},onFirstCall:function(){return this.stub.onFirstCall()},onSecondCall:function(){return this.stub.onSecondCall()},onThirdCall:function(){return this.stub.onThirdCall()},withArgs:function(){throw new Error('Defining a stub by invoking "stub.onCall(...).withArgs(...)" is not supported. Use "stub.withArgs(...).onCall(...)" to define sequential behavior for calls with certain arguments.')},callsArg:function(e){if("number"!=typeof e)throw new TypeError("argument index is not number");return this.callArgAt=e,this.callbackArguments=[],this.callbackContext=void 0,this.callArgProp=void 0,this.callbackAsync=!1,this},callsArgOn:function(e,t){if("number"!=typeof e)throw new TypeError("argument index is not number");if("object"!=typeof t)throw new TypeError("argument context is not an object");return this.callArgAt=e,this.callbackArguments=[],this.callbackContext=t,this.callArgProp=void 0,this.callbackAsync=!1,this},callsArgWith:function(e){if("number"!=typeof e)throw new TypeError("argument index is not number");return this.callArgAt=e,this.callbackArguments=i.call(arguments,1),this.callbackContext=void 0,this.callArgProp=void 0,this.callbackAsync=!1,this},callsArgOnWith:function(e,t){if("number"!=typeof e)throw new TypeError("argument index is not number");if("object"!=typeof t)throw new TypeError("argument context is not an object");return this.callArgAt=e,this.callbackArguments=i.call(arguments,2),this.callbackContext=t,this.callArgProp=void 0,this.callbackAsync=!1,this},yields:function(){return this.callArgAt=a,this.callbackArguments=i.call(arguments,0),this.callbackContext=void 0,this.callArgProp=void 0,this.callbackAsync=!1,this},yieldsRight:function(){return this.callArgAt=u,this.callbackArguments=i.call(arguments,0),this.callbackContext=void 0,this.callArgProp=void 0,this.callbackAsync=!1,this},yieldsOn:function(e){if("object"!=typeof e)throw new TypeError("argument context is not an object");return this.callArgAt=a,this.callbackArguments=i.call(arguments,1),this.callbackContext=e,this.callArgProp=void 0,this.callbackAsync=!1,this},yieldsTo:function(e){return this.callArgAt=a,this.callbackArguments=i.call(arguments,1),this.callbackContext=void 0,this.callArgProp=e,this.callbackAsync=!1,this},yieldsToOn:function(e,t){if("object"!=typeof t)throw new TypeError("argument context is not an object");return this.callArgAt=a,this.callbackArguments=i.call(arguments,2),this.callbackContext=t,this.callArgProp=e,this.callbackAsync=!1,this},"throws":t,throwsException:t,returns:function(e){return this.returnValue=e,this.returnValueDefined=!0,this.exception=void 0,this},returnsArg:function(e){if("number"!=typeof e)throw new TypeError("argument index is not number");return this.returnArgAt=e,this},returnsThis:function(){return this.returnThis=!0,this}};for(var d in f)f.hasOwnProperty(d)&&d.match(/^(callsArg|yields)/)&&!d.match(/Async/)&&(f[d+"Async"]=c(d));return e.behavior=f,f}function o(e,t,n){var o=e("./util/core");e("./extend"),n.exports=r(o)}var i=Array.prototype.slice,s=Array.prototype.join,a=-1,u=-2,l=function(){return"object"==typeof process&&"function"==typeof process.nextTick?process.nextTick:"function"==typeof setImmediate?setImmediate:function(e){setTimeout(e,0)}}(),c="undefined"!=typeof module&&module.exports&&"function"==typeof require,f="function"==typeof define&&"object"==typeof define.amd&&define.amd;return f?void define(o):c?void o(require,module.exports,module):void(e&&r(e))}("object"==typeof sinon&&sinon),function(e){function t(e){function t(n,r,o){if(o&&"function"!=typeof o&&"object"!=typeof o)throw new TypeError("Custom stub should be a function or a property descriptor");var i,s;if(o){if("function"==typeof o)i=e.spy&&e.spy.create?e.spy.create(o):o;else if(i=o,e.spy&&e.spy.create)for(var a=e.objectKeys(i),u=0;un;n+=1)t(e[n])}function r(t,n,r){if(r&&t.length!==n.length)return!1;for(var o=0,i=t.length;i>o;o++)if(!e.deepEqual(t[o],n[o]))return!1;return!0}function o(e){return 0===e?"never called":"called "+f(e)}function i(e){var t=e.minCalls,n=e.maxCalls;if("number"==typeof t&&"number"==typeof n){var r=f(t);return t!==n&&(r="at least "+r+" and at most "+f(n)),r}return"number"==typeof t?"at least "+f(t):"at most "+f(n)}function s(e){var t="number"==typeof e.minCalls;return!t||e.callCount>=e.minCalls}function a(e){return"number"!=typeof e.maxCalls?!1:e.callCount===e.maxCalls}function u(e,t){var n=c&&c.isMatcher(e);return n&&e.test(t)||!0}var l=[].push,c=e.match;e.extend(t,{create:function(n){if(!n)throw new TypeError("object is null");var r=e.extend({},t);return r.object=n,delete r.create,r},expects:function(t){if(!t)throw new TypeError("method is falsy");if(this.expectations||(this.expectations={},this.proxies=[]),!this.expectations[t]){this.expectations[t]=[];var n=this;e.wrapMethod(this.object,t,function(){return n.invokeMethod(t,this,arguments)}),l.call(this.proxies,t)}var r=e.expectation.create(t);return l.call(this.expectations[t],r),r},restore:function(){var e=this.object;n(this.proxies,function(t){"function"==typeof e[t].restore&&e[t].restore()})},verify:function(){var t=this.expectations||{},r=[],o=[];return n(this.proxies,function(e){n(t[e],function(e){e.met()?l.call(o,e.toString()):l.call(r,e.toString())})}),this.restore(),r.length>0?e.expectation.fail(r.concat(o).join("\n")):o.length>0&&e.expectation.pass(r.concat(o).join("\n")),!0},invokeMethod:function(t,n,o){var i,s,a=this.expectations&&this.expectations[t]?this.expectations[t]:[],u=[],c=o||[];for(i=0;ir;r+=1)u(this.expectedArguments[r],n[r])||e.expectation.fail(this.method+" received wrong arguments "+e.format(n)+", didn't match "+this.expectedArguments.toString()),e.deepEqual(this.expectedArguments[r],n[r])||e.expectation.fail(this.method+" received wrong arguments "+e.format(n)+", expected "+e.format(this.expectedArguments))}},allowsCall:function(t,n){if(this.met()&&a(this))return!1;if("expectedThis"in this&&this.expectedThis!==t)return!1;if(!("expectedArguments"in this))return!0;if(n=n||[],n.lengthr;r+=1){if(!u(this.expectedArguments[r],n[r]))return!1;if(!e.deepEqual(this.expectedArguments[r],n[r]))return!1}return!0},withArgs:function(){return this.expectedArguments=d.call(arguments),this},withExactArgs:function(){return this.withArgs.apply(this,arguments),this.expectsExactArgCount=!0,this},on:function(e){return this.expectedThis=e,this},toString:function(){var t=(this.expectedArguments||[]).slice();this.expectsExactArgCount||l.call(t,"[...]");var n=e.spyCall.toString.call({proxy:this.method||"anonymous mock expectation",args:t}),r=n.replace(", [...","[, ...")+" "+i(this);return this.met()?"Expectation met: "+r:"Expected "+r+" ("+o(this.callCount)+")"},verify:function(){return this.met()?e.expectation.pass(this.toString()):e.expectation.fail(this.toString()),!0},pass:function(t){e.assert.pass(t)},fail:function(e){var t=new Error(e);throw t.name="ExpectationError",t}},e.mock=t,t}function n(e,n,r){var o=e("./util/core");e("./times_in_words"),e("./call"),e("./extend"),e("./match"),e("./spy"),e("./stub"),e("./format"),r.exports=t(o)}var r="undefined"!=typeof module&&module.exports&&"function"==typeof require,o="function"==typeof define&&"object"==typeof define.amd&&define.amd;return o?void define(n):r?void n(require,module.exports,module):void(e&&t(e))}("object"==typeof sinon&&sinon),function(e){function t(e){return e.fakes||(e.fakes=[]),e.fakes}function n(e,n){for(var r=t(e),o=0,i=r.length;i>o;o+=1)"function"==typeof r[o][n]&&r[o][n]()}function r(e){for(var n=t(e),r=0;rr;++r)if(n[r]===t)return n.splice(r,1)},dispatchEvent:function(e){for(var t=e.type,n=this.eventListeners&&this.eventListeners[t]||[],r=0;r299?"onerror":"onload"}if(r&&"function"==typeof this[r])try{this[r]()}catch(o){t.logError("Fake XHR "+r+" handler",o)}},send:function(e){o(this),/^(get|head)$/i.test(this.method)||(this.requestBody=e),this.requestHeaders["Content-Type"]="text/plain;charset=utf-8",this.errorFlag=!1,this.sendFlag=!0,this.readyStateChange(n.OPENED),"function"==typeof this.onSend&&this.onSend(this)},abort:function(){this.aborted=!0,this.responseText=null,this.errorFlag=!0,this.readyState>t.FakeXDomainRequest.UNSENT&&this.sendFlag&&(this.readyStateChange(t.FakeXDomainRequest.DONE),this.sendFlag=!1)},setResponseBody:function(e){i(this),s(e);var t=this.chunkSize||10,r=0;this.responseText="";do this.readyStateChange(n.LOADING),this.responseText+=e.substring(r,r+t),r+=t;while(r=0;i--)e(n[i]);"function"==typeof o.onCreate&&o.onCreate(this)}function i(e){if(e.readyState!==o.OPENED)throw new Error("INVALID_STATE_ERR");if(e.sendFlag)throw new Error("INVALID_STATE_ERR")}function s(e,t){t=t.toLowerCase();for(var n in e)if(n.toLowerCase()===t)return n;return null}function a(e,t){if(e)for(var n=0,r=e.length;r>n;n+=1)t(e[n])}function u(e,t){for(var n=0;no.UNSENT&&this.sendFlag&&(this.readyStateChange(o.DONE),this.sendFlag=!1),this.readyState=o.UNSENT,this.dispatchEvent(new e.Event("abort",!1,!1,this)),this.upload.dispatchEvent(new e.Event("abort",!1,!1,this)),"function"==typeof this.onerror&&this.onerror()},getResponseHeader:function(e){return this.readyStater;++r)if(n[r]===t)return n.splice(r,1)},r.prototype.dispatchEvent=function(e){for(var t,n=this.eventListeners[e.type]||[],r=0;null!=(t=n[r]);r++)t(e)};var w=function(e,t,n){switch(n.length){case 0:return e[t]();case 1:return e[t](n[0]);case 2:return e[t](n[0],n[1]);case 3:return e[t](n[0],n[1],n[2]);case 4:return e[t](n[0],n[1],n[2],n[3]);case 5:return e[t](n[0],n[1],n[2],n[3],n[4])}};o.filters=[],o.addFilter=function(e){this.filters.push(e)};var x=/MSIE 6/;o.defake=function(e,t){var n=new g.workingXHR;a(["open","setRequestHeader","send","abort","getResponseHeader","getAllResponseHeaders","addEventListener","overrideMimeType","removeEventListener"],function(t){e[t]=function(){return w(n,t,arguments)}});var r=function(t){a(t,function(t){try{e[t]=n[t]}catch(r){if(!x.test(navigator.userAgent))throw r}})},i=function(){e.readyState=n.readyState,n.readyState>=o.HEADERS_RECEIVED&&r(["status","statusText"]),n.readyState>=o.LOADING&&r(["responseText","response"]),n.readyState===o.DONE&&r(["responseXML"]),e.onreadystatechange&&e.onreadystatechange.call(e,{target:e})};if(n.addEventListener){for(var s in e.eventListeners)e.eventListeners.hasOwnProperty(s)&&a(e.eventListeners[s],function(e){n.addEventListener(s,e)});n.addEventListener("readystatechange",i)}else n.onreadystatechange=i;w(n,"open",t)},o.useFilters=!1,o.parseXML=function(e){var t;if("undefined"!=typeof DOMParser){var n=new DOMParser;t=n.parseFromString(e,"text/xml")}else t=new window.ActiveXObject("Microsoft.XMLDOM"),t.async="false",t.loadXML(e);return t},o.statusCodes={100:"Continue",101:"Switching Protocols",200:"OK",201:"Created",
202:"Accepted",203:"Non-Authoritative Information",204:"No Content",205:"Reset Content",206:"Partial Content",207:"Multi-Status",300:"Multiple Choice",301:"Moved Permanently",302:"Found",303:"See Other",304:"Not Modified",305:"Use Proxy",307:"Temporary Redirect",400:"Bad Request",401:"Unauthorized",402:"Payment Required",403:"Forbidden",404:"Not Found",405:"Method Not Allowed",406:"Not Acceptable",407:"Proxy Authentication Required",408:"Request Timeout",409:"Conflict",410:"Gone",411:"Length Required",412:"Precondition Failed",413:"Request Entity Too Large",414:"Request-URI Too Long",415:"Unsupported Media Type",416:"Requested Range Not Satisfiable",417:"Expectation Failed",422:"Unprocessable Entity",500:"Internal Server Error",501:"Not Implemented",502:"Bad Gateway",503:"Service Unavailable",504:"Gateway Timeout",505:"HTTP Version Not Supported"};var C="undefined"!=typeof module&&module.exports&&"function"==typeof require,E="function"==typeof define&&"object"==typeof define.amd&&define.amd;return E?void define(p):C?void p(require,module.exports,module):void(e&&h(e))}("object"==typeof sinon&&sinon,"undefined"!=typeof global?global:self),function(){function e(e){var t=e;if("[object Array]"!==Object.prototype.toString.call(e)&&(t=[200,{},e]),"string"!=typeof t[2])throw new TypeError("Fake server response body should be string, but was "+typeof t[2]);return t}function t(e,t,n){var r=e.method,o=!r||r.toLowerCase()===t.toLowerCase(),i=e.url,s=!i||i===n||"function"==typeof i.test&&i.test(n);return o&&s}function n(e,n){var r=n.url;if(/^https?:\/\//.test(r)&&!a.test(r)||(r=r.replace(a,"")),t(e,this.getHTTPMethod(n),r)){if("function"==typeof e.response){var o=e.url,i=[n].concat(o&&"function"==typeof o.exec?o.exec(r).slice(1):[]);return e.response.apply(e,i)}return!0}return!1}function r(t){t.fakeServer={create:function(e){var n=t.create(this);return n.configure(e),t.xhr.supportsCORS?this.xhr=t.useFakeXMLHttpRequest():this.xhr=t.useFakeXDomainRequest(),n.requests=[],this.xhr.onCreate=function(e){n.addRequest(e)},n},configure:function(e){var t,n={autoRespond:!0,autoRespondAfter:!0,respondImmediately:!0,fakeHTTPMethods:!0};e=e||{};for(t in e)n.hasOwnProperty(t)&&e.hasOwnProperty(t)&&(this[t]=e[t])},addRequest:function(e){var t=this;i.call(this.requests,e),e.onSend=function(){t.handleRequest(this),t.respondImmediately?t.respond():t.autoRespond&&!t.responding&&(setTimeout(function(){t.responding=!1,t.respond()},t.autoRespondAfter||10),t.responding=!0)}},getHTTPMethod:function(e){if(this.fakeHTTPMethods&&/post/i.test(e.method)){var t=(e.requestBody||"").match(/_method=([^\b;]+)/);return t?t[1]:e.method}return e.method},handleRequest:function(e){e.async?(this.queue||(this.queue=[]),i.call(this.queue,e)):this.processRequest(e)},log:function(e,n){var r;r="Request:\n"+t.format(n)+"\n\n",r+="Response:\n"+t.format(e)+"\n\n",t.log(r)},respondWith:function(t,n,r){return 1===arguments.length&&"function"!=typeof t?void(this.response=e(t)):(this.responses||(this.responses=[]),1===arguments.length&&(r=t,n=t=null),2===arguments.length&&(r=n,n=t,t=null),void i.call(this.responses,{method:t,url:n,response:"function"==typeof r?r:e(r)}))},respond:function(){arguments.length>0&&this.respondWith.apply(this,arguments);for(var e=this.queue||[],t=e.splice(0,e.length),n=0;n=0;i--)if(n.call(this,this.responses[i],e)){r=this.responses[i].response;break}4!==e.readyState&&(this.log(r,e),e.respond(r[0],r[1],r[2]))}catch(s){t.logError("Fake server request processing",s)}},restore:function(){return this.xhr.restore&&this.xhr.restore.apply(this.xhr,arguments)}}}function o(e,t,n){var o=e("./core");e("./fake_xdomain_request"),e("./fake_xml_http_request"),e("../format"),r(o),n.exports=o}var i=[].push,s="undefined"!=typeof window?window.location:{},a=new RegExp("^"+s.protocol+"//"+s.host),u="undefined"!=typeof module&&module.exports&&"function"==typeof require,l="function"==typeof define&&"object"==typeof define.amd&&define.amd;l?define(o):u?o(require,module.exports,module):r(sinon)}(),function(){function e(e){function t(){}t.prototype=e.fakeServer,e.fakeServerWithClock=new t,e.fakeServerWithClock.addRequest=function(t){if(t.async&&("object"==typeof setTimeout.clock?this.clock=setTimeout.clock:(this.clock=e.useFakeTimers(),this.resetClock=!0),!this.longestTimeout)){var n=this.clock.setTimeout,r=this.clock.setInterval,o=this;this.clock.setTimeout=function(e,t){return o.longestTimeout=Math.max(t,o.longestTimeout||0),n.apply(this,arguments)},this.clock.setInterval=function(e,t){return o.longestTimeout=Math.max(t,o.longestTimeout||0),r.apply(this,arguments)}}return e.fakeServer.addRequest.call(this,t)},e.fakeServerWithClock.respond=function(){var t=e.fakeServer.respond.apply(this,arguments);return this.clock&&(this.clock.tick(this.longestTimeout||0),this.longestTimeout=0,this.resetClock&&(this.clock.restore(),this.resetClock=!1)),t},e.fakeServerWithClock.restore=function(){return this.clock&&this.clock.restore(),e.fakeServer.restore.apply(this,arguments)}}function t(t){var n=t("./core");t("./fake_server"),t("./fake_timers"),e(n)}var n="undefined"!=typeof module&&module.exports&&"function"==typeof require,r="function"==typeof define&&"object"==typeof define.amd&&define.amd;r?define(t):n?t(require):e(sinon)}(),function(e){function t(e){function t(e,t,n,o){o&&(!t.injectInto||n in t.injectInto?r.call(e.args,o):(t.injectInto[n]=o,e.injectedKeys.push(n)))}function n(t){var n=e.create(e.sandbox);return t.useFakeServer&&("object"==typeof t.useFakeServer&&(n.serverPrototype=t.useFakeServer),n.useFakeServer()),t.useFakeTimers&&("object"==typeof t.useFakeTimers?n.useFakeTimers.apply(n,t.useFakeTimers):n.useFakeTimers()),n}var r=[].push;return e.sandbox=e.extend(e.create(e.collection),{useFakeTimers:function(){return this.clock=e.useFakeTimers.apply(e,arguments),this.add(this.clock)},serverPrototype:e.fakeServer,useFakeServer:function(){var t=this.serverPrototype||e.fakeServer;return t&&t.create?(this.server=t.create(),this.add(this.server)):null},inject:function(t){return e.collection.inject.call(this,t),this.clock&&(t.clock=this.clock),this.server&&(t.server=this.server,t.requests=this.server.requests),t.match=e.match,t},restore:function(){e.collection.restore.apply(this,arguments),this.restoreContext()},restoreContext:function(){if(this.injectedKeys){for(var e=0,t=this.injectedKeys.length;t>e;e++)delete this.injectInto[this.injectedKeys[e]];this.injectedKeys=[]}},create:function(r){if(!r)return e.create(e.sandbox);var o=n(r);o.args=o.args||[],o.injectedKeys=[],o.injectInto=r.injectInto;var i,s,a=o.inject({});if(r.properties)for(var u=0,l=r.properties.length;l>u;u++)i=r.properties[u],s=a[i]||"sandbox"===i&&o,t(o,r,i,s);else t(o,r,"sandbox",s);return o},match:e.match}),e.sandbox.useFakeXMLHttpRequest=e.sandbox.useFakeServer,e.sandbox}function n(e,n,r){var o=e("./util/core");e("./extend"),e("./util/fake_server_with_clock"),e("./util/fake_timers"),e("./collection"),r.exports=t(o)}var r="undefined"!=typeof module&&module.exports&&"function"==typeof require,o="function"==typeof define&&"object"==typeof define.amd&&define.amd;return o?void define(n):r?void n(require,module.exports,module):void(e&&t(e))}("object"==typeof sinon&&sinon),function(e){function t(e){function t(t){function r(){var r=e.getConfig(e.config);r.injectInto=r.injectIntoThis&&this||r.injectInto;var o,i,s=e.sandbox.create(r),a=n.call(arguments),u=a.length&&a[a.length-1];"function"==typeof u&&(a[a.length-1]=function(e){if(e)throw s.restore(),o;s.verifyAndRestore(),u(e)});try{i=t.apply(this,a.concat(s.args))}catch(l){o=l}if("function"!=typeof u){if("undefined"!=typeof o)throw s.restore(),o;s.verifyAndRestore()}return i}var o=typeof t;if("function"!==o)throw new TypeError("sinon.test needs to wrap a test function, got "+o);return t.length?function(e){return r.apply(this,arguments)}:r}var n=Array.prototype.slice;return t.config={injectIntoThis:!0,injectInto:null,properties:["spy","stub","mock","clock","server","requests"],useFakeTimers:!0,useFakeServer:!0},e.test=t,t}function n(e,n,r){var o=e("./util/core");e("./sandbox"),r.exports=t(o)}var r="undefined"!=typeof module&&module.exports&&"function"==typeof require,o="function"==typeof define&&"object"==typeof define.amd&&define.amd;o?define(n):r?n(require,module.exports,module):e&&t(e)}("object"==typeof sinon&&sinon||null),function(e){function t(e,t,n){return function(){t&&t.apply(this,arguments);var r,o;try{o=e.apply(this,arguments)}catch(i){r=i}if(n&&n.apply(this,arguments),r)throw r;return o}}function n(e){function n(n,r){if(!n||"object"!=typeof n)throw new TypeError("sinon.testCase needs an object with test functions");r=r||"test";var o,i,s,a=new RegExp("^"+r),u={},l=n.setUp,c=n.tearDown;for(o in n)n.hasOwnProperty(o)&&!/^(setUp|tearDown)$/.test(o)&&(i=n[o],"function"==typeof i&&a.test(o)?(s=i,(l||c)&&(s=t(i,l,c)),u[o]=e.test(s)):u[o]=n[o]);return u}return e.testCase=n,n}function r(e,t,r){var o=e("./util/core");e("./test"),r.exports=n(o)}var o="undefined"!=typeof module&&module.exports&&"function"==typeof require,i="function"==typeof define&&"object"==typeof define.amd&&define.amd;return i?void define(r):o?void r(require,module.exports,module):void(e&&n(e))}("object"==typeof sinon&&sinon),function(e,t){function n(e){function n(){for(var e,t=0,r=arguments.length;r>t;++t)e=arguments[t],e||a.fail("fake is not a spy"),e.proxy&&e.proxy.isSinonProxy?n(e.proxy):("function"!=typeof e&&a.fail(e+" is not a function"),"function"!=typeof e.getCall&&a.fail(e+" is not stubbed"))}function r(e,n){e=e||t;var r=e.fail||a.fail;r.call(e,n)}function i(e,t,i){2===arguments.length&&(i=t,t=e),a[e]=function(s){n(s);var u=o.call(arguments,1),l=!1;l="function"==typeof t?!t(s):"function"==typeof s[t]?!s[t].apply(s,u):!s[t],l?r(this,(s.printf||s.proxy.printf).apply(s,[i].concat(u))):a.pass(e)}}function s(e,t){return!e||/^fail/.test(t)?t:e+t.slice(0,1).toUpperCase()+t.slice(1)}var a;return a={failException:"AssertError",fail:function(e){var t=new Error(e);throw t.name=this.failException||a.failException,t},pass:function(){},callOrder:function(){n.apply(null,arguments);var t="",i="";if(e.calledInOrder(arguments))a.pass("callOrder");else{try{t=[].join.call(arguments,", ");for(var s=o.call(arguments),u=s.length;u;)s[--u].called||s.splice(u,1);i=e.orderByFirstCall(s).join(", ")}catch(l){}r(this,"expected "+t+" to be called in order but were called as "+i)}},callCount:function(t,o){if(n(t),t.callCount!==o){var i="expected %n to be called "+e.timesInWords(o)+" but was called %c%C";r(this,t.printf(i))}else a.pass("callCount")},expose:function(e,t){if(!e)throw new TypeError("target is null or undefined");var n=t||{},r="undefined"==typeof n.prefix&&"assert"||n.prefix,o="undefined"==typeof n.includeFail||!!n.includeFail;for(var i in this)"expose"===i||!o&&/^(fail)/.test(i)||(e[s(r,i)]=this[i]);return e},match:function(t,n){var o=e.match(n);if(o.test(t))a.pass("match");else{var i=["expected value to match"," expected = "+e.format(n)," actual = "+e.format(t)];r(this,i.join("\n"))}}},i("called","expected %n to have been called at least once but was never called"),i("notCalled",function(e){return!e.called},"expected %n to not have been called but was called %c%C"),i("calledOnce","expected %n to be called once but was called %c%C"),i("calledTwice","expected %n to be called twice but was called %c%C"),i("calledThrice","expected %n to be called thrice but was called %c%C"),i("calledOn","expected %n to be called with %1 as this but was called with %t"),i("alwaysCalledOn","expected %n to always be called with %1 as this but was called with %t"),i("calledWithNew","expected %n to be called with new"),i("alwaysCalledWithNew","expected %n to always be called with new"),i("calledWith","expected %n to be called with arguments %*%C"),i("calledWithMatch","expected %n to be called with match %*%C"),i("alwaysCalledWith","expected %n to always be called with arguments %*%C"),i("alwaysCalledWithMatch","expected %n to always be called with match %*%C"),i("calledWithExactly","expected %n to be called with exact arguments %*%C"),i("alwaysCalledWithExactly","expected %n to always be called with exact arguments %*%C"),i("neverCalledWith","expected %n to never be called with arguments %*%C"),i("neverCalledWithMatch","expected %n to never be called with match %*%C"),i("threw","%n did not throw exception%C"),i("alwaysThrew","%n did not always throw exception%C"),e.assert=a,a}function r(e,t,r){var o=e("./util/core");e("./match"),e("./format"),r.exports=n(o)}var o=Array.prototype.slice,i="undefined"!=typeof module&&module.exports&&"function"==typeof require,s="function"==typeof define&&"object"==typeof define.amd&&define.amd;return s?void define(r):i?void r(require,module.exports,module):void(e&&n(e))}("object"==typeof sinon&&sinon,"undefined"!=typeof global?global:self),sinon}),this.sinon});
================================================
FILE: test/dist/fixtures/touch.html
================================================
================================================
FILE: touch.blocks/page/__icon/page__icon.bemhtml.js
================================================
block('page').elem('icon').def()(function() {
var ctx = this.ctx;
return applyCtx([
ctx.src16 && {
elem : 'link',
attrs : { rel : 'shortcut icon', href : ctx.src16 }
},
ctx.src114 && {
elem : 'link',
attrs : {
rel : 'apple-touch-icon-precomposed',
sizes : '114x114',
href : ctx.src114
}
},
ctx.src72 && {
elem : 'link',
attrs : {
rel : 'apple-touch-icon-precomposed',
sizes : '72x72',
href : ctx.src72
}
},
ctx.src57 && {
elem : 'link',
attrs : { rel : 'apple-touch-icon-precomposed', href : ctx.src57 }
}
]);
});
================================================
FILE: touch.blocks/page/__icon/page__icon.bh.js
================================================
module.exports = function(bh) {
bh.match('page__icon', function(ctx, json) {
ctx.content([
json.src16 && {
elem : 'link',
attrs : { rel : 'shortcut icon', href : json.src16 }
},
json.src114 && {
elem : 'link',
attrs : {
rel : 'apple-touch-icon-precomposed',
sizes : '114x114',
href : json.src114
}
},
json.src72 && {
elem : 'link',
attrs : {
rel : 'apple-touch-icon-precomposed',
sizes : '72x72',
href : json.src72
}
},
json.src57 && {
elem : 'link',
attrs : { rel : 'apple-touch-icon-precomposed', href : json.src57 }
}
], true);
});
};
================================================
FILE: touch.blocks/page/page.bemhtml.js
================================================
block('page')(
def()(function() {
return applyNext({ _zoom : this.ctx.zoom });
}),
elem('head').content()(function() {
return [
applyNext(),
{
elem : 'meta',
attrs : {
name : 'viewport',
content : 'width=device-width,' +
(this._zoom?
'initial-scale=1' :
'maximum-scale=1,initial-scale=1,user-scalable=no')
}
},
{ elem : 'meta', attrs : { name : 'format-detection', content : 'telephone=no' } },
{ elem : 'link', attrs : { name : 'apple-mobile-web-app-capable', content : 'yes' } }
];
}),
mix()(function() {
var mix = applyNext(),
uaMix = [{ block : 'ua', attrs : { nonce : this._nonceCsp }, js : true }];
return mix? uaMix.concat(mix) : uaMix;
})
);
================================================
FILE: touch.blocks/page/page.bh.js
================================================
module.exports = function(bh) {
bh.match('page', function(ctx, json) {
ctx
.mix({ block : 'ua', js : true })
.tParam('zoom', json.zoom);
});
bh.match('page__head', function(ctx, json) {
ctx
.applyBase()
.content([
json.content,
{
elem : 'meta',
attrs : {
name : 'viewport',
content : 'width=device-width,' +
(ctx.tParam('zoom')?
'initial-scale=1' :
'maximum-scale=1,initial-scale=1,user-scalable=no')
}
},
{ elem : 'meta', attrs : { name : 'format-detection', content : 'telephone=no' } },
{ elem : 'link', attrs : { name : 'apple-mobile-web-app-capable', content : 'yes' } }
], true);
});
};
================================================
FILE: touch.blocks/page/page.deps.js
================================================
({})
================================================
FILE: touch.blocks/page/page.ru.md
================================================
# page
На уровне переопределения `touch.blocks` блок предоставляет шаблоны, создающие дополнительный набор HTML-элементов внутри `head`.
## Обзор
### Специализированные поля блока
| Поле | Тип | Описание |
| ---- | --- | -------- |
| zoom | `{Boolean}` | Наличие масштабирования. |
### Элементы блока
| Элемент | Способы использования | Описание |
| ------- | --------------------- | -------- |
| icon | `BEMJSON` | Позволяет задать ссылку на значки Web Clips, для отображения на рабочем столе iOS при добавлении ссылки на сайт. |
### Специализированные поля элементов блока
| Элемент | Поле | Тип | Описание |
| ------- | ---- | --- | -------- |
| icon | src{X} | `{String}` | Используются для указания пути к файлу значка. |
### Публичные технологии блока
Блок реализован в технологиях:
* `bh.js`
* `bemhtml`
## Подробности
Блок создает HTML-элементы:
* ` ` с атрибутом `name` в значении `'format-detection'`. Значением `content` служит `'telephone=no'`. Элемент отключает автоматическое распознавание телефонных номеров в html-коде и их набор по нажатию.
* ` ` с атрибутом `name` в значении `'apple-mobile-web-app-capable'`. Значением `content` служит `'yes'`. Элемент задает для страницы полноэкранный режим отображения на устройствах с iOS.
* ` ` с атрибутом `name` в значении `'viewport'`. Элемент позволяет управлять масштабированием страницы. По умолчанию, масштабирование отключено. Для включения используйте специализированное поле `zoom` со значением `true`.
Кроме того, к элементу с классом `page` подмешивается блок [ua](https://github.com/bem/bem-core/blob/v2/desktop.blocks/ua/ua.ru.md).
### Специализированные поля блока
##### Специализированное поле `zoom`
Тип: `{Boolean}`.
Управляет масштабированием страницы. Определяет значение атрибута `content` HTML-элемент ` ` с атрибутом `name` в значении `'viewport'`:
* со значением `true` – `'initial-scale=1'`. Масштабирование включено. Масштаб по умолчанию устанавливается равным 100%.
* без значения или `false` – `'maximum-scale=1,initial-scale=1,user-scalable=no'`. Масштаб по умолчанию устанавливается равным 100%. Масштабирование отключено.
```js
{
block : 'page',
title : 'Hello, World!',
zoom : true,
content : 'Включение масштабирования страницы'
}
```
### Элементы блока
#### Элемент `icon`
Позволяет задать ссылку на значки Web Clips, для отображения на рабочем столе iOS при добавлении ссылки на сайт. Ссылка задается через специализированное поле `src{X}`.
##### Специализированное поле `src{X}`
Тип: `String`.
Поле вида `src{X}` используются для указания пути к файлу значка. В зависимости от значения `{X}` элемент `icon` преобразуется в HTML-элемент:
* `src16` – ` ` c атрибутом `rel` со значением `'shortcut icon'`.
* `src57` – ` ` c атрибутами:
* `sizes` со значением `'57x57'`;
* `rel` со значением `'apple-touch-icon-precomposed'`.
* `src72` – ` ` c атрибутами:
* `sizes` со значением `'72x72'`;
* `rel` со значением `'apple-touch-icon-precomposed'`.
* `src114` – ` ` c атрибутом `rel` со значением `'apple-touch-icon-precomposed'`.
```js
{
block : 'page',
title : 'Page title',
head : { elem : 'icon', src72 : 'example.png' },
content : 'Страница с подключенным значком'
}
```
================================================
FILE: touch.blocks/page/page.tmpl-specs/00-empty.html
================================================
================================================
FILE: touch.blocks/page/page.tmpl-specs/10-simple.html
================================================
Простой пример html/head/body-обвязки страницы
bla
================================================
FILE: touch.blocks/page/page.tmpl-specs/20-style.html
================================================
================================================
FILE: touch.blocks/page/page.tmpl-specs/25-styles.html
================================================
================================================
FILE: touch.blocks/page/page.tmpl-specs/30-scripts.html
================================================
bla-bla
================================================
FILE: touch.blocks/page/page.tmpl-specs/40-nonce.html
================================================
bla-bla
================================================
FILE: touch.blocks/page/page.tmpl-specs/60-x-ua-compatible.html
================================================
Remove x-ua-compatible
================================================
FILE: touch.blocks/page/page.tmpl-specs/70-lang.html
================================================
================================================
FILE: touch.blocks/page/page.tmpl-specs/70-zoom.bemjson.js
================================================
({
block : 'page',
zoom : true
})
================================================
FILE: touch.blocks/page/page.tmpl-specs/70-zoom.html
================================================
================================================
FILE: touch.blocks/ua/__dom/ua__dom.deps.js
================================================
({
mustDeps : ['ua', 'i-bem-dom']
})
================================================
FILE: touch.blocks/ua/__dom/ua__dom.js
================================================
/**
* @module ua__dom
* @description Use ua module to provide user agent features by modifiers and update some on orient change
*/
import bemDom from 'bem:i-bem-dom';
import ua from 'bem:ua';
export default bemDom.declBlock('ua',
{
onSetMod : {
'js' : {
'inited' : function() {
this
.setMod('platform',
ua.ios? 'ios' :
ua.android? 'android' :
ua.bada? 'bada' :
ua.wp? 'wp' :
ua.opera? 'opera' :
'other')
.setMod('browser',
ua.opera? 'opera' :
ua.chrome? 'chrome' :
'')
.setMod('ios', ua.ios? ua.ios.charAt(0) : '')
.setMod('android', ua.android? ua.android.charAt(0) : '')
.setMod('ios-subversion', ua.ios? ua.ios.match(/(\d\.\d)/)[1].replace('.', '') : '')
.setMod('screen-size', ua.screenSize)
.setMod('svg', ua.svg? 'yes' : 'no')
.setMod('orient', ua.landscape? 'landscape' : 'portrait')
._domEvents(bemDom.win).on(
'orientchange',
function(e, data) {
ua.width = data.width;
ua.height = data.height;
ua.landscape = data.landscape;
this.setMod('orient', data.landscape? 'landscape' : 'portrait');
});
}
}
}
},
ua);
================================================
FILE: touch.blocks/ua/__dom/ua__dom.ru.md
================================================
# Элемент `dom` блока `ua`
Элемент служит для дополнения базовой БЭМ-сущности блока `ua` набором модификаторов на основе данных, собранных блоком `ua` на touch-уровне.
Это позволяет учитывать особенности мобильного устройства, проверяя наличие и значение модификаторов.
```js
modules.define('ios-test', ['i-bem-dom', 'ua'], function(provide, bemDom, Ua) {
provide(bemDom.declBlock(this.name, {
onSetMod: {
js: {
inited: function() {
this.findParentBlock(Ua).hasMod('platform', 'ios') &&
this.setMod('ios');
}
},
'ios': function() {
console.log('You are iOS user');
}
}
}));
});
```
Элемент автоматически подключается с блоком `page`. Не требуется подключать его вручную, если в проекте используется `page`.
## Модификаторы элемента
Значения всех модификаторов элемента, кроме `orient`, устанавливаются в момент инициализации блока и остаются неизменными.
### Модификатор `platform`
Допустимые значения: `'ios'`, `'android'`, `'bada'`, `'wp'`, `'other'`.
Способ использования: `JS`.
Модификатор указывает мобильную платформу пользовательского устройства.
* `ios` – iOS.
* `android` – Android.
* `bada` – Bada OS.
* `wp` – Windows Phone.
* `other` – все остальные мобильные платформы.
### Модификатор `browser`
Допустимые значения: `'opera'`, `'chrome'`.
Способ использования: `JS`.
Модификатор указывает тип мобильного браузера.
* `opera` – Opera.
* `chrome` – Chrome.
### Модификатор `ios`
Допустимые значения: `'8'`, `'7'` ...
Способ использования: `JS`.
Модификатор указывает версию операционной системы для устройств iOS.
### Модификатор `ios-subversion`
Допустимые значения: `'81'`, `'80'` ...
Способ использования: `JS`.
Модификатор указывает подверсию операционной системы для устройств iOS. Номер подверсии состоит из номера версии и первого символа после разделителя. Номер указывается без символов-разделителей `'.'`. Например, для iOS версии 8.1.3 значением модификатора будет `'81'`.
### Модификатор `android`
Допустимые значения: `'4'`, `'3'` ...
Способ использования: `JS`.
Модификатор указывает версию операционной системы для устройств Android.
### Модификатор `screen-size`
Допустимые значения: `'large'`, `'normal'`, `'small'`.
Способ использования: `JS`.
Модификатор указывает размер экрана пользовательского устройства.
* `large` – размер экрана больше 320 px.
* `normal` – размер экрана равен 320 px.
* `small` – размер экрана меньше 320 px.
### Модификатор `svg`
Допустимые значения: `'yes'`, `'no'`.
Способ использования: `JS`.
Модификатор указывает на наличие у пользовательского устройства поддержки формата SVG.
* `yes` – поддержка SVG присутствует.
* `no` – поддержка SVG отсутствует.
### Модификатор `orient`
Допустимые значения: `'landscape'`, `'portrait'`.
Способ использования: `JS`.
Модификатор указывает текущую ориентацию устройства.
* `landscape` – горизонтальная ориентация.
* `portrait` – вертикальная ориентация.
Значение модификатора изменяется динамически при смене ориентации устройства. Поэтому можно подписываться на изменение значения модификатора:
```js
modules.define('inner', ['i-bem-dom', 'ua'], function(provide, bemDom, Ua) {
provide(bemDom.declBlock(this.name, {
onSetMod: {
js: {
inited: function() {
this._ua = this.findParentBlock(Ua);
this
._events(this.ua)
.on({ modName : 'orient', modVal : '*' }, this._onOrientChange, this);
this.setMod('orient', this._ua.getMod('orient'));
}
},
'orient': {
'portrait': function() {
this._reDraw('portrait');
},
'landscape': function() {
this._reDraw('landscape');
}
}
},
_onOrientChange: function(e, data) {
// переключаемся между значениям собственного модификатора `orient`
this.setMod(data.modName, data.modVal);
},
_reDraw: function(orient) {
// обновляем содержимое контейнера `inner` при смене ориентации устройства
console.log(orient);
bemDom.update(this.domElem, orient);
}
}));
});
```
В примере блок-контейнер `inner`, вложенный в `page`, подменяет свое содержимое при смене ориентации устройства.
================================================
FILE: touch.blocks/ua/ua.bemhtml.js
================================================
block('ua').js()(function() {
var ctxJS = applyNext();
if(ctxJS === false) return false;
return ctxJS || true;
});
================================================
FILE: touch.blocks/ua/ua.bh.js
================================================
module.exports = function(bh) {
bh.match('ua', function(ctx) {
ctx.js(true);
});
};
================================================
FILE: touch.blocks/ua/ua.deps.js
================================================
({
shouldDeps : 'jquery'
})
================================================
FILE: touch.blocks/ua/ua.js
================================================
/**
* @module ua
* @description Detect some user agent features
*/
import $ from 'bem:jquery';
const win = window,
doc = document,
ua = navigator.userAgent,
platform = {},
device = {};
let match;
if(match = ua.match(/Android\s+([\d.]+)/)) {
platform.android = match[1];
} else if(ua.match(/\sHTC[\s_].*AppleWebKit/)) {
// фэйковый десктопный UA по умолчанию у некоторых HTC (например, HTC Sensation)
platform.android = '2.3';
} else if(match = ua.match(/iPhone\sOS\s([\d_]+)/)) {
platform.ios = match[1].replace(/_/g, '.');
device.iphone = true;
} else if(match = ua.match(/iPad.*OS\s([\d_]+)/)) {
platform.ios = match[1].replace(/_/g, '.');
device.ipad = true;
} else if(match = ua.match(/Bada\/([\d.]+)/)) {
platform.bada = match[1];
} else if(match = ua.match(/Windows\sPhone.*\s([\d.]+)/)) {
platform.wp = match[1];
} else {
platform.other = true;
}
const browser = {};
if(win.opera) {
browser.opera = win.opera.version();
} else if(match = ua.match(/\sCrMo\/([\d.]+)/)) {
browser.chrome = match[1];
}
const support = {},
connection = navigator.connection;
if(connection) {
const connections = {};
connections[connection.ETHERNET] = connections[connection.WIFI] = 'wifi';
connections[connection.CELL_3G] = '3g';
connections[connection.CELL_2G] = '2g';
support.connection = connections[connection.type];
}
const videoElem = doc.createElement('video');
support.video = !!(videoElem.canPlayType && videoElem.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, ''));
support.svg = !!(doc.createElementNS && doc.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect);
const plugins = navigator.plugins;
let i = plugins.length;
if(plugins && i) {
let plugin;
while(plugin = plugins[--i])
if(plugin.name === 'Shockwave Flash' && (match = plugin.description.match(/Flash ([\d.]+)/))) {
support.flash = match[1];
break;
}
}
// http://stackoverflow.com/a/6603537
let lastOrient = win.innerWidth > win.innerHeight,
lastWidth = win.innerWidth;
const $win = $(win).bind('resize', function() {
const width = win.innerWidth,
height = win.innerHeight,
landscape = width > height;
// http://alxgbsn.co.uk/2012/08/27/trouble-with-web-browser-orientation/
// check previous device width to disallow Android shrink page and change orientation on opening software keyboard
if(landscape !== lastOrient && width !== lastWidth) {
$win.trigger('orientchange', {
landscape : landscape,
width : width,
height : height
});
lastOrient = landscape;
lastWidth = width;
}
});
export default {
/**
* User agent
* @type String
*/
ua : ua,
/**
* iOS version
* @type String|undefined
*/
ios : platform.ios,
/**
* Is iPhone
* @type Boolean|undefined
*/
iphone : device.iphone,
/**
* Is iPad
* @type Boolean|undefined
*/
ipad : device.ipad,
/**
* Android version
* @type String|undefined
*/
android : platform.android,
/**
* Bada version
* @type String|undefined
*/
bada : platform.bada,
/**
* Windows Phone version
* @type String|undefined
*/
wp : platform.wp,
/**
* Undetected platform
* @type Boolean|undefined
*/
other : platform.other,
/**
* Opera version
* @type String|undefined
*/
opera : browser.opera,
/**
* Chrome version
* @type String|undefined
*/
chrome : browser.chrome,
/**
* Screen size, one of: large, normal, small
* @type String
*/
screenSize : screen.width > 320? 'large' : screen.width < 320? 'small' : 'normal',
/**
* Device pixel ratio
* @type Number
*/
dpr : win.devicePixelRatio || 1,
/**
* Connection type, one of: wifi, 3g, 2g
* @type String
*/
connection : support.connection,
/**
* Flash version
* @type String|undefined
*/
flash : support.flash,
/**
* Is video supported?
* @type Boolean
*/
video : support.video,
/**
* Is SVG supported?
* @type Boolean
*/
svg : support.svg,
/**
* Viewport width
* @type Number
*/
width : win.innerWidth,
/**
* Viewport height
* @type Number
*/
height : win.innerHeight,
/**
* Is landscape oriented?
* @type Boolean
*/
landscape : lastOrient
};
================================================
FILE: touch.blocks/ua/ua.ru.md
================================================
# ua
На уровне `touch`, блок предоставляет объект, содержащий набор свойств, указывающих особенности мобильного устройства.
## Обзор
### Свойства и методы объекта
| Имя | Тип | Описание |
| --- | --- | -------- |
| ua | {String} | Значение HTTP-заголовка юзер-агента. |
| ios | {String}|{undefined} | Версия мобильной платформы iOS. |
| android | {String}|{undefined} | Версия мобильной платформы Android. |
| bada | {String}|{undefined} | Версия мобильной платформы Bada OS. |
| wp | {String}|{undefined} | Версия мобильной платформы Windows Phone. |
| other | {Boolean} | Мобильная платформа неопределена. |
| opera | {String} | Версия браузера Opera. |
| chrome | {String} | Версия браузера Chrome. |
| iphone | {Boolean} | Устройство – iPhone. |
| ipad | {Boolean} | Устройство – iPad. |
| screenSize | {String} | Размер экрана устройства. |
| connection | {String} | Тип активного соединения. |
| dpr | {Number} | Относительная плотность пикселей. |
| flash | {String}|{undefined} | Версия Adobe Flash. |
| video | {Boolean} | Поддержка видео. |
| width | {Number} | Ширина рабочей области экрана в px. |
| height | {Number} | Высота рабочей области экрана в px. |
| landscape | {Boolean} | Ориентация устройства. |
### Элементы блока
| Элемент | Способы использования | Описание |
| ------- | --------------------- | -------- |
| dom | `JS` | Предоставляет набор модификаторов на основании свойств блока `ua` на тач-уровне. |
### Модификаторы элемента блока
| Элемент | Модификатор | Допустимые значения | Способы использования | Описание |
| ------- | ----------- | ------------------- | --------------------- | -------- |
| dom | platform | `'ios'`, `'android'`, `'bada'`, `'wp'`, `'other'` | `JS` | Мобильная платформа пользовательского устройства. |
| | browser | `'opera'`, `'chrome'` | `JS` | Тип браузера. |
| | ios | `'8'`, `'7'` ... | `JS` | Версия операционной системы для устройств iOS. |
| | ios-subversion | `'81'`, `'80'` ... | `JS` | Подверсия операционной системы для устройств iOS. |
| | android | `'4'`, `'3'` ... | `JS` | Версия операционной системы для устройств Android. |
| | screen-size | `'large'`, `'normal'`, `'small'` | `JS` | Размер экрана устройства. |
| | svg | `'yes'`, `'no'` | `JS` | Поддержка формата SVG. |
| | orient | `'landscape'`, `'portrait'` | `JS` | Ориентация устройства. |
### Публичные технологии блока
Блок реализован в технологиях:
* `js`
* `bh.js`
* `bemhtml`
## Подробности
Блок позволяет определить:
* Версию мобильной платформы.
* Типа браузера.
* Версию браузера.
* Тип соединения.
* Наличие поддержки видео и SVG.
* Поддержку технологии Adobe Flash.
* Ориентацию и размер экрана.
* Соотношение сторон экрана устройства.
```js
modules.require('ua', function(ua) {
console.dir(ua);
});
```
### Свойства и методы объекта
#### Свойство `ua`
Тип: `{String}`.
Тип мобильного браузера.
#### Свойство `ios`
Тип: `{String|undefined}`.
Версия мобильной платформы. Строка с номером версии, если платформа распознана как iOS.
#### Свойство `android`
Тип: `{String|undefined}`.
Версия мобильной платформы. Строка с номером версии, если платформа распознана как Android.
#### Свойство `bada`
Тип: `{String|undefined}`.
Версия мобильной платформы. Строка с номером версии, если платформа распознана как Bada OS.
#### Свойство `wp`
Тип: `{String|undefined}`.
Версия мобильной платформы. Строка с номером версии, если платформа распознана как Windows Phone.
#### Свойство `other`
Тип: `{Boolean}`.
Мобильная платформа неопределена. Устанавливается в значение `true` для всех мобильных платформ, кроме вышеперечисленных.
#### Свойство `opera`
Тип: `{String}`.
Версия браузера Opera.
#### Свойство `chrome`
Тип: `{String}`.
Версия браузера Chrome.
#### Свойство `iphone`
Тип: `{Boolean}`.
Значение `true` характеризует устройство как iPhone.
#### Свойство `ipad`
Тип: `{Boolean}`.
Значение `true` характеризует устройство как iPad.
#### Свойство `screenSize`
Тип: `{String}`.
Размер экрана устройства.
Доступны следующие значения:
* `large` – размер экрана больше 320 px.
* `normal` – размер экрана равен 320 px.
* `small` – размер экрана меньше 320 px.
#### Свойство `connection`
Тип: `{String}`.
Тип активного сетевого соединения.
Доступны следующие значения:
* `wifi` – соединение по Wi-Fi.
* `3g` – соединение по 3G.
* `2g` – соединение по EDGE и GSM.
#### Свойство `dpr`
Тип: `{Number}`.
Коэффициент относительной плотности пикселей. Характеризует отношение физических пикселей устройства к аппаратно независимым (dppx). Позволяет определить использует ли устройство дисплей с повышенной плотностью пикселей (например, Retina). По умолчанию `1`.
Например, можно проверить, что устройство использует Retina и отдавать браузеру изображения с высоким разрешением:
```js
modules.require('ua', function(ua) {
var imgFile = ua.dpr === 1 ? 'image.png' : 'image@2x.png';
// ···
});
```
#### Свойство `flash`
Тип: `{String|undefined}`.
Версия Adobe Flash. `undefined`, если Flash недоступен.
#### Свойство `video`
Тип: `{Boolean}`.
Значение `true`, если видео поддерживается.
#### Свойство `svg`
Тип: `{Boolean}`.
Значение `true`, если SVG поддерживается.
#### Свойство `width`
Тип: `{Number}`.
Ширина рабочей области экрана в пикселях.
#### Свойство `height`
Тип: `{Number}`.
Высота рабочей области экрана в пикселях.
#### Свойство `landscape`
Тип: `{Boolean}`.
Значение `true` при горизонтальной ориентации.