From 78e7c15ef0a0b3b5f495c03d71c7728187227f12 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 28 Jun 2014 23:11:34 +0400 Subject: [PATCH 001/130] export tutorial for tutorial/02-ui/03-event-details --- tutorial/01-js/01-getting-started/01-intro.md | 3 + .../01-getting-started/02-alternatives.md | 3 + .../01-js/01-getting-started/03-pre-coding.md | 3 + .../01-js/01-getting-started/04-editor.md | 3 + .../01-js/01-getting-started/05-devtools.md | 3 + tutorial/01-js/01-getting-started/index.md | 3 + .../01-js/02-test-test-test/01-test-html.md | 3 + .../01-js/02-test-test-test/02-test-key.md | 3 + .../01-js/02-test-test-test/03-hello-test.md | 3 + tutorial/01-js/02-test-test-test/index.md | 3 + .../01-js/03-first-steps/01-hello-world.md | 3 + tutorial/01-js/03-first-steps/02-structure.md | 3 + tutorial/01-js/03-first-steps/03-variables.md | 3 + .../01-js/03-first-steps/04-variable-names.md | 3 + .../01-js/03-first-steps/05-strict-mode.md | 3 + .../01-js/03-first-steps/06-types-intro.md | 3 + .../07-properties-and-methods.md | 3 + tutorial/01-js/03-first-steps/08-operators.md | 3 + .../01-js/03-first-steps/09-comparison.md | 3 + .../03-first-steps/10-bitwise-operators.md | 3 + tutorial/01-js/03-first-steps/11-uibasic.md | 3 + tutorial/01-js/03-first-steps/12-ifelse.md | 3 + .../01-js/03-first-steps/13-logical-ops.md | 3 + .../03-first-steps/14-types-conversion.md | 3 + tutorial/01-js/03-first-steps/15-while-for.md | 3 + .../01-js/03-first-steps/16-break-continue.md | 3 + tutorial/01-js/03-first-steps/17-switch.md | 3 + .../03-first-steps/18-function-basics.md | 3 + tutorial/01-js/03-first-steps/19-recursion.md | 3 + .../20-function-declaration-expression.md | 3 + .../21-named-function-expression.md | 3 + .../03-first-steps/22-javascript-specials.md | 3 + tutorial/01-js/03-first-steps/index.md | 3 + .../04-writing-js/01-debugging-chrome.md | 3 + .../01-js/04-writing-js/02-coding-style.md | 3 + .../04-writing-js/03-write-unmain-code.md | 3 + tutorial/01-js/04-writing-js/04-testing.md | 3 + tutorial/01-js/04-writing-js/index.md | 3 + .../01-js/05-data-structures/01-string.md | 3 + .../01-js/05-data-structures/02-number.md | 3 + .../01-js/05-data-structures/03-object.md | 3 + .../05-data-structures/04-object-for-in.md | 3 + .../05-data-structures/05-object-reference.md | 3 + tutorial/01-js/05-data-structures/06-array.md | 3 + .../05-data-structures/07-array-methods.md | 3 + .../05-data-structures/08-array-iteration.md | 3 + .../09-arguments-pseudoarray.md | 3 + .../01-js/05-data-structures/10-datetime.md | 3 + .../11-typeof-duck-typing.md | 3 + tutorial/01-js/05-data-structures/index.md | 3 + .../06-functions-closures/01-global-object.md | 3 + .../06-functions-closures/02-closures.md | 3 + .../03-scope-new-function.md | 3 + .../04-closures-module.md | 3 + .../05-closures-usage.md | 3 + .../06-memory-management.md | 3 + .../01-js/06-functions-closures/07-with.md | 3 + tutorial/01-js/06-functions-closures/index.md | 3 + .../07-objects-more/01-object-methods.md | 3 + .../07-objects-more/02-constructor-new.md | 3 + .../03-static-properties-and-methods.md | 3 + .../01-js/07-objects-more/04-call-apply.md | 3 + tutorial/01-js/07-objects-more/05-bind.md | 3 + .../01-js/07-objects-more/06-decorators.md | 3 + tutorial/01-js/07-objects-more/index.md | 3 + .../01-js/08-js-misc/01-object-conversion.md | 3 + .../01-js/08-js-misc/02-class-property.md | 3 + tutorial/01-js/08-js-misc/03-json.md | 3 + .../08-js-misc/04-setTimeout-setInterval.md | 3 + tutorial/01-js/08-js-misc/05-eval.md | 3 + tutorial/01-js/08-js-misc/06-exception.md | 3 + tutorial/01-js/08-js-misc/index.md | 3 + tutorial/01-js/09-oop/01-about-oop.md | 3 + .../09-oop/02-internal-external-interface.md | 3 + tutorial/01-js/09-oop/03-getters-setters.md | 3 + .../09-oop/04-descriptors-getters-setters.md | 3 + .../01-js/09-oop/05-functional-inheritance.md | 3 + tutorial/01-js/09-oop/index.md | 3 + tutorial/01-js/10-prototypes/01-prototype.md | 3 + .../01-js/10-prototypes/02-new-prototype.md | 3 + .../10-prototypes/03-native-prototypes.md | 3 + tutorial/01-js/10-prototypes/04-classes.md | 3 + .../10-prototypes/05-class-inheritance.md | 3 + .../01-js/10-prototypes/06-constructor.md | 3 + tutorial/01-js/10-prototypes/07-instanceof.md | 3 + .../01-js/10-prototypes/08-class-extend.md | 3 + .../10-prototypes/09-why-prototypes-better.md | 3 + tutorial/01-js/10-prototypes/index.md | 3 + tutorial/01-js/index.md | 3 + .../01-document/01-browser-environment.md | 3 + tutorial/02-ui/01-document/02-dom-nodes.md | 3 + tutorial/02-ui/01-document/03-dom-console.md | 3 + .../02-ui/01-document/04-traversing-dom.md | 3 + .../02-ui/01-document/05-traversing-tables.md | 3 + .../06-basic-dom-node-properties.md | 3 + .../07-attributes-and-custom-properties.md | 3 + .../01-document/08-searching-elements-dom.md | 3 + .../09-searching-elements-internals.md | 3 + .../10-compare-document-position.md | 3 + .../01-document/11-modifying-document.md | 3 + tutorial/02-ui/01-document/12-multi-insert.md | 3 + .../02-ui/01-document/13-document-write.md | 3 + .../01-document/14-styles-and-classes.md | 3 + tutorial/02-ui/01-document/15-metrics.md | 3 + .../02-ui/01-document/16-metrics-window.md | 3 + tutorial/02-ui/01-document/17-coordinates.md | 3 + .../01-document/18-coordinates-document.md | 3 + .../02-ui/01-document/19-support-polyfill.md | 3 + .../02-ui/01-document/20-dom-cheatsheet.md | 3 + tutorial/02-ui/01-document/index.md | 3 + .../01-introduction-browser-events.md | 3 + .../02-events-and-timing-depth.md | 3 + .../03-obtaining-event-object.md | 3 + .../04-event-bubbling.md | 3 + .../05-event-delegation.md | 3 + .../02-events-and-interfaces/06-behavior.md | 3 + .../07-default-browser-action.md | 3 + .../08-dispatch-events.md | 3 + .../02-ui/02-events-and-interfaces/index.md | 3 + .../02-ui/03-event-details/01-mouse-clicks.md | 286 ++++++++ .../question.code/index.html | 47 ++ .../01-selectable-list.task/question.md | 20 + .../solution.code/index.html | 108 +++ .../selectable-list-1/index.html | 61 ++ .../selectable-list-2/index.html | 67 ++ .../selectable-list-3/index.html | 108 +++ .../01-selectable-list.task/solution.md | 5 + .../question.code/index.html | 68 ++ .../question.code/tree-coords-src/index.html | 68 ++ .../02-tree-coords.task/question.md | 37 + .../solution.code/index.html | 92 +++ .../02-tree-coords.task/solution.md | 30 + .../02-ui/03-event-details/02-unselectable.md | 184 +++++ ...ouseover-mouseout-mouseenter-mouseleave.md | 192 +++++ .../question.md | 9 + .../solution.md | 7 + .../02-rollover.task/question.code/index.html | 10 + .../02-rollover.task/question.md | 19 + .../02-rollover.task/solution.code/index.html | 50 ++ .../solution.code/rollover-css/index.html | 32 + .../solution.code/rollover/index.html | 50 ++ .../02-rollover.task/solution.md | 22 + .../question.code/index.html | 72 ++ .../03-behavior-tooltip.task/question.md | 35 + .../solution.code/behavior-tooltip/index.html | 124 ++++ .../solution.code/index.html | 124 ++++ .../03-behavior-tooltip.task/solution.md | 1 + .../question.code/index.html | 95 +++ .../question.md | 25 + .../behavior-tooltip-nested/index.html | 170 +++++ .../solution.code/index.html | 170 +++++ .../solution.md | 1 + .../02-ui/03-event-details/04-mousewheel.md | 96 +++ .../question.md | 14 + .../solution.code/index.html | 50 ++ .../solution.code/scale-wheel/index.html | 50 ++ .../solution.md | 3 + .../question.code/index.html | 37 + .../no-doc-scroll-src/index.html | 37 + .../02-no-doc-scroll.task/question.md | 28 + .../solution.code/fix-textarea-scroll.js | 14 + .../solution.code/index.html | 38 + .../no-doc-scroll/fix-textarea-scroll.js | 14 + .../solution.code/no-doc-scroll/index.html | 38 + .../02-no-doc-scroll.task/solution.md | 1 + .../04-mousewheel/wheel.code/index.html | 59 ++ .../02-ui/03-event-details/05-fixevent.md | 83 +++ .../03-event-details/06-drag-and-drop.md | 255 +++++++ .../01-slider.task/question.code/index.html | 17 + .../01-slider.task/question.code/lib.js | 44 ++ .../01-slider.task/question.md | 19 + .../01-slider.task/solution.code/index.html | 79 ++ .../01-slider.task/solution.code/lib.js | 44 ++ .../solution.code/slider-simple/index.html | 79 ++ .../solution.code/slider-simple/lib.js | 44 ++ .../01-slider.task/solution.md | 45 ++ .../question.code/index.html | 38 + .../question.code/soccer.css | 58 ++ .../question.code/soccer.js | 2 + .../02-drag-heroes.task/question.md | 13 + .../solution.code/drag-heroes/index.html | 37 + .../solution.code/drag-heroes/soccer.css | 58 ++ .../solution.code/drag-heroes/soccer.js | 80 +++ .../solution.code/index.html | 37 + .../solution.code/soccer.css | 58 ++ .../solution.code/soccer.js | 80 +++ .../02-drag-heroes.task/solution.md | 1 + .../06-drag-and-drop/ball_shift.png | Bin 0 -> 17913 bytes .../07-drag-and-drop-objects.md | 672 ++++++++++++++++++ .../07-drag-and-drop-objects/between.png | Bin 0 -> 7518 bytes .../dragDemo.code/DragManager.js | 151 ++++ .../dragDemo.code/dragDemo.css | 11 + .../dragDemo.code/index.html | 35 + .../dragDemo.code/lib.js | 54 ++ .../07-drag-and-drop-objects/shiftx.png | Bin 0 -> 4847 bytes .../03-event-details/08-keyboard-events.md | 340 +++++++++ .../question.code/index.html | 33 + .../01-numeric-input.task/question.md | 13 + .../solution.code/index.html | 46 ++ .../solution.code/numeric-input/index.html | 46 ++ .../01-numeric-input.task/solution.md | 35 + .../02-check-sync-keydown.task/question.md | 16 + .../solution.code/index.html | 52 ++ .../solution.code/multikeys/index.html | 52 ++ .../02-check-sync-keydown.task/solution.md | 11 + .../03-event-details/09-event-onscroll.md | 34 + .../question.code/index.html | 91 +++ .../01-avatar-above-scroll.task/question.md | 12 + .../solution.code/index.html | 109 +++ .../solution.code/scroll-position/index.html | 109 +++ .../01-avatar-above-scroll.task/solution.md | 1 + .../question.code/index.html | 35 + .../02-updown-button.task/question.md | 21 + .../solution.code/index.html | 97 +++ .../solution.code/updown/index.html | 97 +++ .../02-updown-button.task/solution.md | 92 +++ .../question.code/index.html | 101 +++ .../03-load-visible-img.task/question.md | 45 ++ .../solution.code/index.html | 159 +++++ .../03-load-visible-img.task/solution.md | 36 + .../10-onload-ondomcontentloaded.md | 208 ++++++ .../window-onbeforeunload.code/index.html | 19 + .../03-event-details/11-onload-onerror.md | 237 ++++++ .../01-nice-alt.task/question.code/index.html | 32 + .../01-nice-alt.task/question.md | 18 + .../solution.code/img-onload/index.html | 42 ++ .../01-nice-alt.task/solution.code/index.html | 42 ++ .../01-nice-alt.task/solution.md | 9 + .../question.code/index.html | 49 ++ .../02-load-img-callback.task/question.md | 23 + .../solution.code/index.html | 56 ++ .../02-load-img-callback.task/solution.md | 9 + .../question.code/go.js | 3 + .../question.code/index.html | 21 + .../03-script-callback.task/question.md | 19 + .../solution.code/go.js | 3 + .../solution.code/index.html | 41 ++ .../03-script-callback.task/solution.md | 18 + .../question.code/a.js | 3 + .../question.code/b.js | 3 + .../question.code/c.js | 3 + .../question.code/index.html | 18 + .../04-scripts-callback.task/question.md | 22 + .../solution.code/a.js | 3 + .../solution.code/b.js | 3 + .../solution.code/c.js | 3 + .../solution.code/index.html | 51 ++ .../04-scripts-callback.task/solution.md | 9 + tutorial/02-ui/03-event-details/index.md | 5 + .../04-forms-controls/01-form-elements.md | 3 + .../02-ui/04-forms-controls/02-focus-blur.md | 3 + .../04-forms-controls/03-events-change.md | 3 + .../04-forms-controls/04-forms-submit.md | 3 + tutorial/02-ui/04-forms-controls/index.md | 3 + .../02-ui/05-widgets/01-architect-intro.md | 3 + .../02-ui/05-widgets/02-widgets-structure.md | 3 + .../02-ui/05-widgets/03-widgets-markup.md | 3 + .../02-ui/05-widgets/04-template-lodash.md | 3 + tutorial/02-ui/05-widgets/05-custom-events.md | 3 + tutorial/02-ui/05-widgets/06-widget-tasks.md | 3 + tutorial/02-ui/05-widgets/07-what-next.md | 3 + .../02-ui/05-widgets/08-widget-tasks-2.md | 3 + tutorial/02-ui/05-widgets/index.md | 3 + tutorial/02-ui/index.md | 3 + .../01-webcomponents-intro.md | 3 + .../03-webcomponents/02-webcomponent-core.md | 3 + tutorial/03-webcomponents/03-shadow-dom.md | 3 + tutorial/03-webcomponents/04-link-import.md | 3 + .../03-webcomponents/05-webcomponent-build.md | 3 + tutorial/03-webcomponents/index.md | 3 + tutorial/04-animation/01-js-animation.md | 3 + tutorial/04-animation/02-bezier.md | 3 + tutorial/04-animation/03-css-animation.md | 3 + tutorial/04-animation/index.md | 3 + tutorial/05-jquery-stub/01-jquery-intro.md | 3 + tutorial/05-jquery-stub/02-jquery-search.md | 3 + .../05-jquery-stub/03-jquery-traversal.md | 3 + tutorial/05-jquery-stub/04-jquery-dom.md | 3 + .../05-jquery-stub/05-jquery-stub-article.md | 3 + tutorial/05-jquery-stub/index.md | 3 + tutorial/06-optimize/01-memory-leaks.md | 3 + .../06-optimize/02-script-place-optimize.md | 3 + tutorial/06-optimize/03-reflow.md | 3 + tutorial/06-optimize/index.md | 3 + tutorial/07-compress/01-minification.md | 3 + .../07-compress/02-better-minification.md | 3 + .../03-gcc-advanced-optimization.md | 3 + tutorial/07-compress/04-gcc-check-types.md | 3 + .../07-compress/05-gcc-closure-library.md | 3 + tutorial/07-compress/index.md | 3 + .../01-memory-removechild-innerhtml.md | 3 + tutorial/08-extra/02-javascript-quiz.md | 3 + tutorial/08-extra/03-templates.md | 3 + tutorial/08-extra/04-books.md | 3 + tutorial/08-extra/05-setImmediate.md | 3 + tutorial/08-extra/06-bind-late.md | 3 + tutorial/08-extra/07-sublime.md | 3 + .../08-extra/08-range-textrange-selection.md | 3 + tutorial/08-extra/09-drag-and-drop-plus.md | 3 + tutorial/08-extra/10-cookie.md | 3 + tutorial/08-extra/11-intl.md | 3 + tutorial/08-extra/12-regexp-specials.md | 3 + tutorial/08-extra/index.md | 3 + .../09-frames-and-windows/01-window-open.md | 3 + .../02-window-properties-and-methods.md | 3 + .../09-frames-and-windows/03-window-focus.md | 3 + tutorial/09-frames-and-windows/04-iframes.md | 3 + .../05-same-origin-policy.md | 3 + ...cross-window-messaging-with-postmessage.md | 3 + .../09-frames-and-windows/07-clickjacking.md | 3 + tutorial/09-frames-and-windows/index.md | 3 + .../01-regexp-introduction.md | 3 + .../02-regexp-methods.md | 3 + .../03-regexp-character-classes.md | 3 + .../04-regexp-special-characters.md | 3 + .../05-regexp-character-sets-and-ranges.md | 3 + .../06-regexp-numeric-quantifiers.md | 3 + .../07-regexp-quantifiers.md | 3 + .../08-regexp-greedy-and-lazy.md | 3 + .../09-regexp-groups.md | 3 + .../10-regexp-backreferences.md | 3 + ...11-regexp-infinite-backtracking-problem.md | 3 + .../12-regexp-alternation.md | 3 + .../13-regexp-ahchors-and-multiline-mode.md | 3 + .../14-regexp-multiline-mode.md | 3 + .../15-regexp-word-boundary.md | 3 + .../16-regexp-practice.md | 3 + .../17-regexp-orphans.md | 3 + .../index.md | 3 + .../11-tools/01-tools-browser-extensions.md | 3 + tutorial/11-tools/02-fiddler.md | 3 + tutorial/11-tools/03-ie-http-analyzer.md | 3 + tutorial/11-tools/index.md | 3 + tutorial/12-ajax/01-ajax-intro.md | 3 + tutorial/12-ajax/02-ajax-nodejs.md | 3 + tutorial/12-ajax/03-ajax-xmlhttprequest.md | 3 + tutorial/12-ajax/04-xhr-forms.md | 3 + tutorial/12-ajax/05-xhr-longpoll.md | 3 + tutorial/12-ajax/06-xhr-crossdomain.md | 3 + tutorial/12-ajax/07-xhr-onprogress.md | 3 + tutorial/12-ajax/08-xhr-resume.md | 3 + tutorial/12-ajax/09-websockets.md | 3 + tutorial/12-ajax/10-ajax-jsonp.md | 3 + tutorial/12-ajax/11-server-sent-events.md | 3 + tutorial/12-ajax/12-ajax-iframe.md | 3 + tutorial/12-ajax/13-ajax-iframe-htmlfile.md | 3 + tutorial/12-ajax/14-ajax-iframe-xdomain.md | 3 + tutorial/12-ajax/15-comet-iframe.md | 3 + tutorial/12-ajax/16-ajax-summary.md | 3 + tutorial/12-ajax/index.md | 3 + tutorial/13-css-for-js/01-css-why.md | 3 + tutorial/13-css-for-js/02-css-units.md | 3 + tutorial/13-css-for-js/03-display.md | 3 + tutorial/13-css-for-js/04-float.md | 3 + tutorial/13-css-for-js/05-position.md | 3 + tutorial/13-css-for-js/06-css-center.md | 3 + .../13-css-for-js/07-font-size-line-height.md | 3 + tutorial/13-css-for-js/08-white-space.md | 3 + tutorial/13-css-for-js/09-outline.md | 3 + tutorial/13-css-for-js/10-box-sizing.md | 3 + tutorial/13-css-for-js/11-margin.md | 3 + tutorial/13-css-for-js/12-space-under-img.md | 3 + tutorial/13-css-for-js/13-overflow.md | 3 + tutorial/13-css-for-js/14-height-percent.md | 3 + tutorial/13-css-for-js/15-css-selectors.md | 3 + tutorial/13-css-for-js/16-css-no-ie6.md | 3 + tutorial/13-css-for-js/17-css-sprite.md | 3 + tutorial/13-css-for-js/18-css-format.md | 3 + tutorial/13-css-for-js/index.md | 3 + tutorial/14-archive/01-ie-visual-studio.md | 3 + .../14-archive/02-install-old-browsers.md | 3 + tutorial/14-archive/index.md | 3 + tutorial/index.md | 3 + 373 files changed, 8346 insertions(+) create mode 100644 tutorial/01-js/01-getting-started/01-intro.md create mode 100644 tutorial/01-js/01-getting-started/02-alternatives.md create mode 100644 tutorial/01-js/01-getting-started/03-pre-coding.md create mode 100644 tutorial/01-js/01-getting-started/04-editor.md create mode 100644 tutorial/01-js/01-getting-started/05-devtools.md create mode 100644 tutorial/01-js/01-getting-started/index.md create mode 100644 tutorial/01-js/02-test-test-test/01-test-html.md create mode 100644 tutorial/01-js/02-test-test-test/02-test-key.md create mode 100644 tutorial/01-js/02-test-test-test/03-hello-test.md create mode 100644 tutorial/01-js/02-test-test-test/index.md create mode 100644 tutorial/01-js/03-first-steps/01-hello-world.md create mode 100644 tutorial/01-js/03-first-steps/02-structure.md create mode 100644 tutorial/01-js/03-first-steps/03-variables.md create mode 100644 tutorial/01-js/03-first-steps/04-variable-names.md create mode 100644 tutorial/01-js/03-first-steps/05-strict-mode.md create mode 100644 tutorial/01-js/03-first-steps/06-types-intro.md create mode 100644 tutorial/01-js/03-first-steps/07-properties-and-methods.md create mode 100644 tutorial/01-js/03-first-steps/08-operators.md create mode 100644 tutorial/01-js/03-first-steps/09-comparison.md create mode 100644 tutorial/01-js/03-first-steps/10-bitwise-operators.md create mode 100644 tutorial/01-js/03-first-steps/11-uibasic.md create mode 100644 tutorial/01-js/03-first-steps/12-ifelse.md create mode 100644 tutorial/01-js/03-first-steps/13-logical-ops.md create mode 100644 tutorial/01-js/03-first-steps/14-types-conversion.md create mode 100644 tutorial/01-js/03-first-steps/15-while-for.md create mode 100644 tutorial/01-js/03-first-steps/16-break-continue.md create mode 100644 tutorial/01-js/03-first-steps/17-switch.md create mode 100644 tutorial/01-js/03-first-steps/18-function-basics.md create mode 100644 tutorial/01-js/03-first-steps/19-recursion.md create mode 100644 tutorial/01-js/03-first-steps/20-function-declaration-expression.md create mode 100644 tutorial/01-js/03-first-steps/21-named-function-expression.md create mode 100644 tutorial/01-js/03-first-steps/22-javascript-specials.md create mode 100644 tutorial/01-js/03-first-steps/index.md create mode 100644 tutorial/01-js/04-writing-js/01-debugging-chrome.md create mode 100644 tutorial/01-js/04-writing-js/02-coding-style.md create mode 100644 tutorial/01-js/04-writing-js/03-write-unmain-code.md create mode 100644 tutorial/01-js/04-writing-js/04-testing.md create mode 100644 tutorial/01-js/04-writing-js/index.md create mode 100644 tutorial/01-js/05-data-structures/01-string.md create mode 100644 tutorial/01-js/05-data-structures/02-number.md create mode 100644 tutorial/01-js/05-data-structures/03-object.md create mode 100644 tutorial/01-js/05-data-structures/04-object-for-in.md create mode 100644 tutorial/01-js/05-data-structures/05-object-reference.md create mode 100644 tutorial/01-js/05-data-structures/06-array.md create mode 100644 tutorial/01-js/05-data-structures/07-array-methods.md create mode 100644 tutorial/01-js/05-data-structures/08-array-iteration.md create mode 100644 tutorial/01-js/05-data-structures/09-arguments-pseudoarray.md create mode 100644 tutorial/01-js/05-data-structures/10-datetime.md create mode 100644 tutorial/01-js/05-data-structures/11-typeof-duck-typing.md create mode 100644 tutorial/01-js/05-data-structures/index.md create mode 100644 tutorial/01-js/06-functions-closures/01-global-object.md create mode 100644 tutorial/01-js/06-functions-closures/02-closures.md create mode 100644 tutorial/01-js/06-functions-closures/03-scope-new-function.md create mode 100644 tutorial/01-js/06-functions-closures/04-closures-module.md create mode 100644 tutorial/01-js/06-functions-closures/05-closures-usage.md create mode 100644 tutorial/01-js/06-functions-closures/06-memory-management.md create mode 100644 tutorial/01-js/06-functions-closures/07-with.md create mode 100644 tutorial/01-js/06-functions-closures/index.md create mode 100644 tutorial/01-js/07-objects-more/01-object-methods.md create mode 100644 tutorial/01-js/07-objects-more/02-constructor-new.md create mode 100644 tutorial/01-js/07-objects-more/03-static-properties-and-methods.md create mode 100644 tutorial/01-js/07-objects-more/04-call-apply.md create mode 100644 tutorial/01-js/07-objects-more/05-bind.md create mode 100644 tutorial/01-js/07-objects-more/06-decorators.md create mode 100644 tutorial/01-js/07-objects-more/index.md create mode 100644 tutorial/01-js/08-js-misc/01-object-conversion.md create mode 100644 tutorial/01-js/08-js-misc/02-class-property.md create mode 100644 tutorial/01-js/08-js-misc/03-json.md create mode 100644 tutorial/01-js/08-js-misc/04-setTimeout-setInterval.md create mode 100644 tutorial/01-js/08-js-misc/05-eval.md create mode 100644 tutorial/01-js/08-js-misc/06-exception.md create mode 100644 tutorial/01-js/08-js-misc/index.md create mode 100644 tutorial/01-js/09-oop/01-about-oop.md create mode 100644 tutorial/01-js/09-oop/02-internal-external-interface.md create mode 100644 tutorial/01-js/09-oop/03-getters-setters.md create mode 100644 tutorial/01-js/09-oop/04-descriptors-getters-setters.md create mode 100644 tutorial/01-js/09-oop/05-functional-inheritance.md create mode 100644 tutorial/01-js/09-oop/index.md create mode 100644 tutorial/01-js/10-prototypes/01-prototype.md create mode 100644 tutorial/01-js/10-prototypes/02-new-prototype.md create mode 100644 tutorial/01-js/10-prototypes/03-native-prototypes.md create mode 100644 tutorial/01-js/10-prototypes/04-classes.md create mode 100644 tutorial/01-js/10-prototypes/05-class-inheritance.md create mode 100644 tutorial/01-js/10-prototypes/06-constructor.md create mode 100644 tutorial/01-js/10-prototypes/07-instanceof.md create mode 100644 tutorial/01-js/10-prototypes/08-class-extend.md create mode 100644 tutorial/01-js/10-prototypes/09-why-prototypes-better.md create mode 100644 tutorial/01-js/10-prototypes/index.md create mode 100644 tutorial/01-js/index.md create mode 100644 tutorial/02-ui/01-document/01-browser-environment.md create mode 100644 tutorial/02-ui/01-document/02-dom-nodes.md create mode 100644 tutorial/02-ui/01-document/03-dom-console.md create mode 100644 tutorial/02-ui/01-document/04-traversing-dom.md create mode 100644 tutorial/02-ui/01-document/05-traversing-tables.md create mode 100644 tutorial/02-ui/01-document/06-basic-dom-node-properties.md create mode 100644 tutorial/02-ui/01-document/07-attributes-and-custom-properties.md create mode 100644 tutorial/02-ui/01-document/08-searching-elements-dom.md create mode 100644 tutorial/02-ui/01-document/09-searching-elements-internals.md create mode 100644 tutorial/02-ui/01-document/10-compare-document-position.md create mode 100644 tutorial/02-ui/01-document/11-modifying-document.md create mode 100644 tutorial/02-ui/01-document/12-multi-insert.md create mode 100644 tutorial/02-ui/01-document/13-document-write.md create mode 100644 tutorial/02-ui/01-document/14-styles-and-classes.md create mode 100644 tutorial/02-ui/01-document/15-metrics.md create mode 100644 tutorial/02-ui/01-document/16-metrics-window.md create mode 100644 tutorial/02-ui/01-document/17-coordinates.md create mode 100644 tutorial/02-ui/01-document/18-coordinates-document.md create mode 100644 tutorial/02-ui/01-document/19-support-polyfill.md create mode 100644 tutorial/02-ui/01-document/20-dom-cheatsheet.md create mode 100644 tutorial/02-ui/01-document/index.md create mode 100644 tutorial/02-ui/02-events-and-interfaces/01-introduction-browser-events.md create mode 100644 tutorial/02-ui/02-events-and-interfaces/02-events-and-timing-depth.md create mode 100644 tutorial/02-ui/02-events-and-interfaces/03-obtaining-event-object.md create mode 100644 tutorial/02-ui/02-events-and-interfaces/04-event-bubbling.md create mode 100644 tutorial/02-ui/02-events-and-interfaces/05-event-delegation.md create mode 100644 tutorial/02-ui/02-events-and-interfaces/06-behavior.md create mode 100644 tutorial/02-ui/02-events-and-interfaces/07-default-browser-action.md create mode 100644 tutorial/02-ui/02-events-and-interfaces/08-dispatch-events.md create mode 100644 tutorial/02-ui/02-events-and-interfaces/index.md create mode 100644 tutorial/02-ui/03-event-details/01-mouse-clicks.md create mode 100755 tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/question.code/index.html create mode 100644 tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/question.md create mode 100755 tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/solution.code/index.html create mode 100755 tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/solution.code/selectable-list-1/index.html create mode 100755 tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/solution.code/selectable-list-2/index.html create mode 100755 tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/solution.code/selectable-list-3/index.html create mode 100644 tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/solution.md create mode 100755 tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/question.code/index.html create mode 100755 tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/question.code/tree-coords-src/index.html create mode 100644 tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/question.md create mode 100755 tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/solution.code/index.html create mode 100644 tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/solution.md create mode 100644 tutorial/02-ui/03-event-details/02-unselectable.md create mode 100644 tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave.md create mode 100644 tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/01-track-cursor-movements.task/question.md create mode 100644 tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/01-track-cursor-movements.task/solution.md create mode 100755 tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/question.code/index.html create mode 100644 tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/question.md create mode 100755 tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/solution.code/index.html create mode 100755 tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/solution.code/rollover-css/index.html create mode 100755 tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/solution.code/rollover/index.html create mode 100644 tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/solution.md create mode 100755 tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/question.code/index.html create mode 100644 tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/question.md create mode 100755 tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/solution.code/behavior-tooltip/index.html create mode 100755 tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/solution.code/index.html create mode 100644 tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/solution.md create mode 100755 tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/question.code/index.html create mode 100644 tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/question.md create mode 100755 tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/solution.code/behavior-tooltip-nested/index.html create mode 100755 tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/solution.code/index.html create mode 100644 tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/solution.md create mode 100644 tutorial/02-ui/03-event-details/04-mousewheel.md create mode 100644 tutorial/02-ui/03-event-details/04-mousewheel/01-scale-with-mouse-wheel.task/question.md create mode 100755 tutorial/02-ui/03-event-details/04-mousewheel/01-scale-with-mouse-wheel.task/solution.code/index.html create mode 100755 tutorial/02-ui/03-event-details/04-mousewheel/01-scale-with-mouse-wheel.task/solution.code/scale-wheel/index.html create mode 100644 tutorial/02-ui/03-event-details/04-mousewheel/01-scale-with-mouse-wheel.task/solution.md create mode 100755 tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/question.code/index.html create mode 100755 tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/question.code/no-doc-scroll-src/index.html create mode 100644 tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/question.md create mode 100755 tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.code/fix-textarea-scroll.js create mode 100755 tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.code/index.html create mode 100755 tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.code/no-doc-scroll/fix-textarea-scroll.js create mode 100755 tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.code/no-doc-scroll/index.html create mode 100644 tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.md create mode 100755 tutorial/02-ui/03-event-details/04-mousewheel/wheel.code/index.html create mode 100644 tutorial/02-ui/03-event-details/05-fixevent.md create mode 100644 tutorial/02-ui/03-event-details/06-drag-and-drop.md create mode 100755 tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/question.code/index.html create mode 100755 tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/question.code/lib.js create mode 100644 tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/question.md create mode 100755 tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.code/index.html create mode 100755 tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.code/lib.js create mode 100755 tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.code/slider-simple/index.html create mode 100755 tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.code/slider-simple/lib.js create mode 100644 tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.md create mode 100755 tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/question.code/index.html create mode 100755 tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/question.code/soccer.css create mode 100755 tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/question.code/soccer.js create mode 100644 tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/question.md create mode 100755 tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/drag-heroes/index.html create mode 100755 tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/drag-heroes/soccer.css create mode 100755 tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/drag-heroes/soccer.js create mode 100755 tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/index.html create mode 100755 tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/soccer.css create mode 100755 tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/soccer.js create mode 100644 tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.md create mode 100755 tutorial/02-ui/03-event-details/06-drag-and-drop/ball_shift.png create mode 100644 tutorial/02-ui/03-event-details/07-drag-and-drop-objects.md create mode 100755 tutorial/02-ui/03-event-details/07-drag-and-drop-objects/between.png create mode 100755 tutorial/02-ui/03-event-details/07-drag-and-drop-objects/dragDemo.code/DragManager.js create mode 100755 tutorial/02-ui/03-event-details/07-drag-and-drop-objects/dragDemo.code/dragDemo.css create mode 100755 tutorial/02-ui/03-event-details/07-drag-and-drop-objects/dragDemo.code/index.html create mode 100755 tutorial/02-ui/03-event-details/07-drag-and-drop-objects/dragDemo.code/lib.js create mode 100755 tutorial/02-ui/03-event-details/07-drag-and-drop-objects/shiftx.png create mode 100644 tutorial/02-ui/03-event-details/08-keyboard-events.md create mode 100755 tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/question.code/index.html create mode 100644 tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/question.md create mode 100755 tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/solution.code/index.html create mode 100755 tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/solution.code/numeric-input/index.html create mode 100644 tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/solution.md create mode 100644 tutorial/02-ui/03-event-details/08-keyboard-events/02-check-sync-keydown.task/question.md create mode 100755 tutorial/02-ui/03-event-details/08-keyboard-events/02-check-sync-keydown.task/solution.code/index.html create mode 100755 tutorial/02-ui/03-event-details/08-keyboard-events/02-check-sync-keydown.task/solution.code/multikeys/index.html create mode 100644 tutorial/02-ui/03-event-details/08-keyboard-events/02-check-sync-keydown.task/solution.md create mode 100644 tutorial/02-ui/03-event-details/09-event-onscroll.md create mode 100755 tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/question.code/index.html create mode 100644 tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/question.md create mode 100755 tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/solution.code/index.html create mode 100755 tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/solution.code/scroll-position/index.html create mode 100644 tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/solution.md create mode 100755 tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/question.code/index.html create mode 100644 tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/question.md create mode 100755 tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/solution.code/index.html create mode 100755 tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/solution.code/updown/index.html create mode 100644 tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/solution.md create mode 100755 tutorial/02-ui/03-event-details/09-event-onscroll/03-load-visible-img.task/question.code/index.html create mode 100644 tutorial/02-ui/03-event-details/09-event-onscroll/03-load-visible-img.task/question.md create mode 100755 tutorial/02-ui/03-event-details/09-event-onscroll/03-load-visible-img.task/solution.code/index.html create mode 100644 tutorial/02-ui/03-event-details/09-event-onscroll/03-load-visible-img.task/solution.md create mode 100644 tutorial/02-ui/03-event-details/10-onload-ondomcontentloaded.md create mode 100755 tutorial/02-ui/03-event-details/10-onload-ondomcontentloaded/window-onbeforeunload.code/index.html create mode 100644 tutorial/02-ui/03-event-details/11-onload-onerror.md create mode 100755 tutorial/02-ui/03-event-details/11-onload-onerror/01-nice-alt.task/question.code/index.html create mode 100644 tutorial/02-ui/03-event-details/11-onload-onerror/01-nice-alt.task/question.md create mode 100755 tutorial/02-ui/03-event-details/11-onload-onerror/01-nice-alt.task/solution.code/img-onload/index.html create mode 100755 tutorial/02-ui/03-event-details/11-onload-onerror/01-nice-alt.task/solution.code/index.html create mode 100644 tutorial/02-ui/03-event-details/11-onload-onerror/01-nice-alt.task/solution.md create mode 100755 tutorial/02-ui/03-event-details/11-onload-onerror/02-load-img-callback.task/question.code/index.html create mode 100644 tutorial/02-ui/03-event-details/11-onload-onerror/02-load-img-callback.task/question.md create mode 100755 tutorial/02-ui/03-event-details/11-onload-onerror/02-load-img-callback.task/solution.code/index.html create mode 100644 tutorial/02-ui/03-event-details/11-onload-onerror/02-load-img-callback.task/solution.md create mode 100755 tutorial/02-ui/03-event-details/11-onload-onerror/03-script-callback.task/question.code/go.js create mode 100755 tutorial/02-ui/03-event-details/11-onload-onerror/03-script-callback.task/question.code/index.html create mode 100644 tutorial/02-ui/03-event-details/11-onload-onerror/03-script-callback.task/question.md create mode 100755 tutorial/02-ui/03-event-details/11-onload-onerror/03-script-callback.task/solution.code/go.js create mode 100755 tutorial/02-ui/03-event-details/11-onload-onerror/03-script-callback.task/solution.code/index.html create mode 100644 tutorial/02-ui/03-event-details/11-onload-onerror/03-script-callback.task/solution.md create mode 100755 tutorial/02-ui/03-event-details/11-onload-onerror/04-scripts-callback.task/question.code/a.js create mode 100755 tutorial/02-ui/03-event-details/11-onload-onerror/04-scripts-callback.task/question.code/b.js create mode 100755 tutorial/02-ui/03-event-details/11-onload-onerror/04-scripts-callback.task/question.code/c.js create mode 100755 tutorial/02-ui/03-event-details/11-onload-onerror/04-scripts-callback.task/question.code/index.html create mode 100644 tutorial/02-ui/03-event-details/11-onload-onerror/04-scripts-callback.task/question.md create mode 100755 tutorial/02-ui/03-event-details/11-onload-onerror/04-scripts-callback.task/solution.code/a.js create mode 100755 tutorial/02-ui/03-event-details/11-onload-onerror/04-scripts-callback.task/solution.code/b.js create mode 100755 tutorial/02-ui/03-event-details/11-onload-onerror/04-scripts-callback.task/solution.code/c.js create mode 100755 tutorial/02-ui/03-event-details/11-onload-onerror/04-scripts-callback.task/solution.code/index.html create mode 100644 tutorial/02-ui/03-event-details/11-onload-onerror/04-scripts-callback.task/solution.md create mode 100644 tutorial/02-ui/03-event-details/index.md create mode 100644 tutorial/02-ui/04-forms-controls/01-form-elements.md create mode 100644 tutorial/02-ui/04-forms-controls/02-focus-blur.md create mode 100644 tutorial/02-ui/04-forms-controls/03-events-change.md create mode 100644 tutorial/02-ui/04-forms-controls/04-forms-submit.md create mode 100644 tutorial/02-ui/04-forms-controls/index.md create mode 100644 tutorial/02-ui/05-widgets/01-architect-intro.md create mode 100644 tutorial/02-ui/05-widgets/02-widgets-structure.md create mode 100644 tutorial/02-ui/05-widgets/03-widgets-markup.md create mode 100644 tutorial/02-ui/05-widgets/04-template-lodash.md create mode 100644 tutorial/02-ui/05-widgets/05-custom-events.md create mode 100644 tutorial/02-ui/05-widgets/06-widget-tasks.md create mode 100644 tutorial/02-ui/05-widgets/07-what-next.md create mode 100644 tutorial/02-ui/05-widgets/08-widget-tasks-2.md create mode 100644 tutorial/02-ui/05-widgets/index.md create mode 100644 tutorial/02-ui/index.md create mode 100644 tutorial/03-webcomponents/01-webcomponents-intro.md create mode 100644 tutorial/03-webcomponents/02-webcomponent-core.md create mode 100644 tutorial/03-webcomponents/03-shadow-dom.md create mode 100644 tutorial/03-webcomponents/04-link-import.md create mode 100644 tutorial/03-webcomponents/05-webcomponent-build.md create mode 100644 tutorial/03-webcomponents/index.md create mode 100644 tutorial/04-animation/01-js-animation.md create mode 100644 tutorial/04-animation/02-bezier.md create mode 100644 tutorial/04-animation/03-css-animation.md create mode 100644 tutorial/04-animation/index.md create mode 100644 tutorial/05-jquery-stub/01-jquery-intro.md create mode 100644 tutorial/05-jquery-stub/02-jquery-search.md create mode 100644 tutorial/05-jquery-stub/03-jquery-traversal.md create mode 100644 tutorial/05-jquery-stub/04-jquery-dom.md create mode 100644 tutorial/05-jquery-stub/05-jquery-stub-article.md create mode 100644 tutorial/05-jquery-stub/index.md create mode 100644 tutorial/06-optimize/01-memory-leaks.md create mode 100644 tutorial/06-optimize/02-script-place-optimize.md create mode 100644 tutorial/06-optimize/03-reflow.md create mode 100644 tutorial/06-optimize/index.md create mode 100644 tutorial/07-compress/01-minification.md create mode 100644 tutorial/07-compress/02-better-minification.md create mode 100644 tutorial/07-compress/03-gcc-advanced-optimization.md create mode 100644 tutorial/07-compress/04-gcc-check-types.md create mode 100644 tutorial/07-compress/05-gcc-closure-library.md create mode 100644 tutorial/07-compress/index.md create mode 100644 tutorial/08-extra/01-memory-removechild-innerhtml.md create mode 100644 tutorial/08-extra/02-javascript-quiz.md create mode 100644 tutorial/08-extra/03-templates.md create mode 100644 tutorial/08-extra/04-books.md create mode 100644 tutorial/08-extra/05-setImmediate.md create mode 100644 tutorial/08-extra/06-bind-late.md create mode 100644 tutorial/08-extra/07-sublime.md create mode 100644 tutorial/08-extra/08-range-textrange-selection.md create mode 100644 tutorial/08-extra/09-drag-and-drop-plus.md create mode 100644 tutorial/08-extra/10-cookie.md create mode 100644 tutorial/08-extra/11-intl.md create mode 100644 tutorial/08-extra/12-regexp-specials.md create mode 100644 tutorial/08-extra/index.md create mode 100644 tutorial/09-frames-and-windows/01-window-open.md create mode 100644 tutorial/09-frames-and-windows/02-window-properties-and-methods.md create mode 100644 tutorial/09-frames-and-windows/03-window-focus.md create mode 100644 tutorial/09-frames-and-windows/04-iframes.md create mode 100644 tutorial/09-frames-and-windows/05-same-origin-policy.md create mode 100644 tutorial/09-frames-and-windows/06-cross-window-messaging-with-postmessage.md create mode 100644 tutorial/09-frames-and-windows/07-clickjacking.md create mode 100644 tutorial/09-frames-and-windows/index.md create mode 100644 tutorial/10-regular-expressions-javascript/01-regexp-introduction.md create mode 100644 tutorial/10-regular-expressions-javascript/02-regexp-methods.md create mode 100644 tutorial/10-regular-expressions-javascript/03-regexp-character-classes.md create mode 100644 tutorial/10-regular-expressions-javascript/04-regexp-special-characters.md create mode 100644 tutorial/10-regular-expressions-javascript/05-regexp-character-sets-and-ranges.md create mode 100644 tutorial/10-regular-expressions-javascript/06-regexp-numeric-quantifiers.md create mode 100644 tutorial/10-regular-expressions-javascript/07-regexp-quantifiers.md create mode 100644 tutorial/10-regular-expressions-javascript/08-regexp-greedy-and-lazy.md create mode 100644 tutorial/10-regular-expressions-javascript/09-regexp-groups.md create mode 100644 tutorial/10-regular-expressions-javascript/10-regexp-backreferences.md create mode 100644 tutorial/10-regular-expressions-javascript/11-regexp-infinite-backtracking-problem.md create mode 100644 tutorial/10-regular-expressions-javascript/12-regexp-alternation.md create mode 100644 tutorial/10-regular-expressions-javascript/13-regexp-ahchors-and-multiline-mode.md create mode 100644 tutorial/10-regular-expressions-javascript/14-regexp-multiline-mode.md create mode 100644 tutorial/10-regular-expressions-javascript/15-regexp-word-boundary.md create mode 100644 tutorial/10-regular-expressions-javascript/16-regexp-practice.md create mode 100644 tutorial/10-regular-expressions-javascript/17-regexp-orphans.md create mode 100644 tutorial/10-regular-expressions-javascript/index.md create mode 100644 tutorial/11-tools/01-tools-browser-extensions.md create mode 100644 tutorial/11-tools/02-fiddler.md create mode 100644 tutorial/11-tools/03-ie-http-analyzer.md create mode 100644 tutorial/11-tools/index.md create mode 100644 tutorial/12-ajax/01-ajax-intro.md create mode 100644 tutorial/12-ajax/02-ajax-nodejs.md create mode 100644 tutorial/12-ajax/03-ajax-xmlhttprequest.md create mode 100644 tutorial/12-ajax/04-xhr-forms.md create mode 100644 tutorial/12-ajax/05-xhr-longpoll.md create mode 100644 tutorial/12-ajax/06-xhr-crossdomain.md create mode 100644 tutorial/12-ajax/07-xhr-onprogress.md create mode 100644 tutorial/12-ajax/08-xhr-resume.md create mode 100644 tutorial/12-ajax/09-websockets.md create mode 100644 tutorial/12-ajax/10-ajax-jsonp.md create mode 100644 tutorial/12-ajax/11-server-sent-events.md create mode 100644 tutorial/12-ajax/12-ajax-iframe.md create mode 100644 tutorial/12-ajax/13-ajax-iframe-htmlfile.md create mode 100644 tutorial/12-ajax/14-ajax-iframe-xdomain.md create mode 100644 tutorial/12-ajax/15-comet-iframe.md create mode 100644 tutorial/12-ajax/16-ajax-summary.md create mode 100644 tutorial/12-ajax/index.md create mode 100644 tutorial/13-css-for-js/01-css-why.md create mode 100644 tutorial/13-css-for-js/02-css-units.md create mode 100644 tutorial/13-css-for-js/03-display.md create mode 100644 tutorial/13-css-for-js/04-float.md create mode 100644 tutorial/13-css-for-js/05-position.md create mode 100644 tutorial/13-css-for-js/06-css-center.md create mode 100644 tutorial/13-css-for-js/07-font-size-line-height.md create mode 100644 tutorial/13-css-for-js/08-white-space.md create mode 100644 tutorial/13-css-for-js/09-outline.md create mode 100644 tutorial/13-css-for-js/10-box-sizing.md create mode 100644 tutorial/13-css-for-js/11-margin.md create mode 100644 tutorial/13-css-for-js/12-space-under-img.md create mode 100644 tutorial/13-css-for-js/13-overflow.md create mode 100644 tutorial/13-css-for-js/14-height-percent.md create mode 100644 tutorial/13-css-for-js/15-css-selectors.md create mode 100644 tutorial/13-css-for-js/16-css-no-ie6.md create mode 100644 tutorial/13-css-for-js/17-css-sprite.md create mode 100644 tutorial/13-css-for-js/18-css-format.md create mode 100644 tutorial/13-css-for-js/index.md create mode 100644 tutorial/14-archive/01-ie-visual-studio.md create mode 100644 tutorial/14-archive/02-install-old-browsers.md create mode 100644 tutorial/14-archive/index.md create mode 100644 tutorial/index.md diff --git a/tutorial/01-js/01-getting-started/01-intro.md b/tutorial/01-js/01-getting-started/01-intro.md new file mode 100644 index 000000000..799b3a3e8 --- /dev/null +++ b/tutorial/01-js/01-getting-started/01-intro.md @@ -0,0 +1,3 @@ +# Введение в JavaScript + +Content tutorial/01-js/01-getting-started/01-intro \ No newline at end of file diff --git a/tutorial/01-js/01-getting-started/02-alternatives.md b/tutorial/01-js/01-getting-started/02-alternatives.md new file mode 100644 index 000000000..72178b220 --- /dev/null +++ b/tutorial/01-js/01-getting-started/02-alternatives.md @@ -0,0 +1,3 @@ +# Альтернативные браузерные технологии + +Content tutorial/01-js/01-getting-started/02-alternatives \ No newline at end of file diff --git a/tutorial/01-js/01-getting-started/03-pre-coding.md b/tutorial/01-js/01-getting-started/03-pre-coding.md new file mode 100644 index 000000000..cfc9d63de --- /dev/null +++ b/tutorial/01-js/01-getting-started/03-pre-coding.md @@ -0,0 +1,3 @@ +# Справочники и спецификации + +Content tutorial/01-js/01-getting-started/03-pre-coding \ No newline at end of file diff --git a/tutorial/01-js/01-getting-started/04-editor.md b/tutorial/01-js/01-getting-started/04-editor.md new file mode 100644 index 000000000..5b9d1d940 --- /dev/null +++ b/tutorial/01-js/01-getting-started/04-editor.md @@ -0,0 +1,3 @@ +# Редакторы для кода + +Content tutorial/01-js/01-getting-started/04-editor \ No newline at end of file diff --git a/tutorial/01-js/01-getting-started/05-devtools.md b/tutorial/01-js/01-getting-started/05-devtools.md new file mode 100644 index 000000000..c88525962 --- /dev/null +++ b/tutorial/01-js/01-getting-started/05-devtools.md @@ -0,0 +1,3 @@ +# Консоль разработчика + +Content tutorial/01-js/01-getting-started/05-devtools \ No newline at end of file diff --git a/tutorial/01-js/01-getting-started/index.md b/tutorial/01-js/01-getting-started/index.md new file mode 100644 index 000000000..d8d488d6e --- /dev/null +++ b/tutorial/01-js/01-getting-started/index.md @@ -0,0 +1,3 @@ +# Введение + +Content tutorial/01-js/01-getting-started \ No newline at end of file diff --git a/tutorial/01-js/02-test-test-test/01-test-html.md b/tutorial/01-js/02-test-test-test/01-test-html.md new file mode 100644 index 000000000..d84951b97 --- /dev/null +++ b/tutorial/01-js/02-test-test-test/01-test-html.md @@ -0,0 +1,3 @@ +# Тест запуска + +Content tutorial/01-js/02-test-test-test/01-test-html \ No newline at end of file diff --git a/tutorial/01-js/02-test-test-test/02-test-key.md b/tutorial/01-js/02-test-test-test/02-test-key.md new file mode 100644 index 000000000..ca52c20bb --- /dev/null +++ b/tutorial/01-js/02-test-test-test/02-test-key.md @@ -0,0 +1,3 @@ +# Тест клавиш + +Content tutorial/01-js/02-test-test-test/02-test-key \ No newline at end of file diff --git a/tutorial/01-js/02-test-test-test/03-hello-test.md b/tutorial/01-js/02-test-test-test/03-hello-test.md new file mode 100644 index 000000000..b6b1bd684 --- /dev/null +++ b/tutorial/01-js/02-test-test-test/03-hello-test.md @@ -0,0 +1,3 @@ +# Тест подсветки + +Content tutorial/01-js/02-test-test-test/03-hello-test \ No newline at end of file diff --git a/tutorial/01-js/02-test-test-test/index.md b/tutorial/01-js/02-test-test-test/index.md new file mode 100644 index 000000000..c37b6253c --- /dev/null +++ b/tutorial/01-js/02-test-test-test/index.md @@ -0,0 +1,3 @@ +# Тест [не заходить, идёт стройка] + +Content tutorial/01-js/02-test-test-test \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/01-hello-world.md b/tutorial/01-js/03-first-steps/01-hello-world.md new file mode 100644 index 000000000..50f9ec5f0 --- /dev/null +++ b/tutorial/01-js/03-first-steps/01-hello-world.md @@ -0,0 +1,3 @@ +# Привет, мир! + +Content tutorial/01-js/03-first-steps/01-hello-world \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/02-structure.md b/tutorial/01-js/03-first-steps/02-structure.md new file mode 100644 index 000000000..93e45e100 --- /dev/null +++ b/tutorial/01-js/03-first-steps/02-structure.md @@ -0,0 +1,3 @@ +# Структура кода + +Content tutorial/01-js/03-first-steps/02-structure \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/03-variables.md b/tutorial/01-js/03-first-steps/03-variables.md new file mode 100644 index 000000000..50aa88e12 --- /dev/null +++ b/tutorial/01-js/03-first-steps/03-variables.md @@ -0,0 +1,3 @@ +# Переменные + +Content tutorial/01-js/03-first-steps/03-variables \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/04-variable-names.md b/tutorial/01-js/03-first-steps/04-variable-names.md new file mode 100644 index 000000000..894842ed5 --- /dev/null +++ b/tutorial/01-js/03-first-steps/04-variable-names.md @@ -0,0 +1,3 @@ +# Правильный выбор имени переменной + +Content tutorial/01-js/03-first-steps/04-variable-names \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/05-strict-mode.md b/tutorial/01-js/03-first-steps/05-strict-mode.md new file mode 100644 index 000000000..b7914d691 --- /dev/null +++ b/tutorial/01-js/03-first-steps/05-strict-mode.md @@ -0,0 +1,3 @@ +# Современный стандарт, "use strict" + +Content tutorial/01-js/03-first-steps/05-strict-mode \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/06-types-intro.md b/tutorial/01-js/03-first-steps/06-types-intro.md new file mode 100644 index 000000000..3461bea10 --- /dev/null +++ b/tutorial/01-js/03-first-steps/06-types-intro.md @@ -0,0 +1,3 @@ +# Шесть типов данных + +Content tutorial/01-js/03-first-steps/06-types-intro \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/07-properties-and-methods.md b/tutorial/01-js/03-first-steps/07-properties-and-methods.md new file mode 100644 index 000000000..b3ddc65fb --- /dev/null +++ b/tutorial/01-js/03-first-steps/07-properties-and-methods.md @@ -0,0 +1,3 @@ +# Методы и свойства + +Content tutorial/01-js/03-first-steps/07-properties-and-methods \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/08-operators.md b/tutorial/01-js/03-first-steps/08-operators.md new file mode 100644 index 000000000..cfad9092d --- /dev/null +++ b/tutorial/01-js/03-first-steps/08-operators.md @@ -0,0 +1,3 @@ +# Основные операторы + +Content tutorial/01-js/03-first-steps/08-operators \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/09-comparison.md b/tutorial/01-js/03-first-steps/09-comparison.md new file mode 100644 index 000000000..8b39b87af --- /dev/null +++ b/tutorial/01-js/03-first-steps/09-comparison.md @@ -0,0 +1,3 @@ +# Операторы сравнения и логические значения + +Content tutorial/01-js/03-first-steps/09-comparison \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/10-bitwise-operators.md b/tutorial/01-js/03-first-steps/10-bitwise-operators.md new file mode 100644 index 000000000..082dca0ce --- /dev/null +++ b/tutorial/01-js/03-first-steps/10-bitwise-operators.md @@ -0,0 +1,3 @@ +# Побитовые операторы + +Content tutorial/01-js/03-first-steps/10-bitwise-operators \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/11-uibasic.md b/tutorial/01-js/03-first-steps/11-uibasic.md new file mode 100644 index 000000000..3e50640b5 --- /dev/null +++ b/tutorial/01-js/03-first-steps/11-uibasic.md @@ -0,0 +1,3 @@ +# Взаимодействие с пользователем: alert, prompt, confirm + +Content tutorial/01-js/03-first-steps/11-uibasic \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/12-ifelse.md b/tutorial/01-js/03-first-steps/12-ifelse.md new file mode 100644 index 000000000..24f6784f2 --- /dev/null +++ b/tutorial/01-js/03-first-steps/12-ifelse.md @@ -0,0 +1,3 @@ +# Условные операторы: if, '?' + +Content tutorial/01-js/03-first-steps/12-ifelse \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/13-logical-ops.md b/tutorial/01-js/03-first-steps/13-logical-ops.md new file mode 100644 index 000000000..b371c501c --- /dev/null +++ b/tutorial/01-js/03-first-steps/13-logical-ops.md @@ -0,0 +1,3 @@ +# Логические операторы + +Content tutorial/01-js/03-first-steps/13-logical-ops \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/14-types-conversion.md b/tutorial/01-js/03-first-steps/14-types-conversion.md new file mode 100644 index 000000000..e1a916d01 --- /dev/null +++ b/tutorial/01-js/03-first-steps/14-types-conversion.md @@ -0,0 +1,3 @@ +# Преобразование типов для примитивов + +Content tutorial/01-js/03-first-steps/14-types-conversion \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/15-while-for.md b/tutorial/01-js/03-first-steps/15-while-for.md new file mode 100644 index 000000000..87dab8ee7 --- /dev/null +++ b/tutorial/01-js/03-first-steps/15-while-for.md @@ -0,0 +1,3 @@ +# Циклы while, for + +Content tutorial/01-js/03-first-steps/15-while-for \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/16-break-continue.md b/tutorial/01-js/03-first-steps/16-break-continue.md new file mode 100644 index 000000000..80186ac13 --- /dev/null +++ b/tutorial/01-js/03-first-steps/16-break-continue.md @@ -0,0 +1,3 @@ +# Директивы break и continue + +Content tutorial/01-js/03-first-steps/16-break-continue \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/17-switch.md b/tutorial/01-js/03-first-steps/17-switch.md new file mode 100644 index 000000000..a4617e814 --- /dev/null +++ b/tutorial/01-js/03-first-steps/17-switch.md @@ -0,0 +1,3 @@ +# Конструкция switch + +Content tutorial/01-js/03-first-steps/17-switch \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/18-function-basics.md b/tutorial/01-js/03-first-steps/18-function-basics.md new file mode 100644 index 000000000..c184e2be9 --- /dev/null +++ b/tutorial/01-js/03-first-steps/18-function-basics.md @@ -0,0 +1,3 @@ +# Функции + +Content tutorial/01-js/03-first-steps/18-function-basics \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/19-recursion.md b/tutorial/01-js/03-first-steps/19-recursion.md new file mode 100644 index 000000000..045c9ebc3 --- /dev/null +++ b/tutorial/01-js/03-first-steps/19-recursion.md @@ -0,0 +1,3 @@ +# Рекурсия, стек + +Content tutorial/01-js/03-first-steps/19-recursion \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/20-function-declaration-expression.md b/tutorial/01-js/03-first-steps/20-function-declaration-expression.md new file mode 100644 index 000000000..a6075382b --- /dev/null +++ b/tutorial/01-js/03-first-steps/20-function-declaration-expression.md @@ -0,0 +1,3 @@ +# Функциональные выражения + +Content tutorial/01-js/03-first-steps/20-function-declaration-expression \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/21-named-function-expression.md b/tutorial/01-js/03-first-steps/21-named-function-expression.md new file mode 100644 index 000000000..b7943c1f0 --- /dev/null +++ b/tutorial/01-js/03-first-steps/21-named-function-expression.md @@ -0,0 +1,3 @@ +# Именованные функциональные выражения + +Content tutorial/01-js/03-first-steps/21-named-function-expression \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/22-javascript-specials.md b/tutorial/01-js/03-first-steps/22-javascript-specials.md new file mode 100644 index 000000000..846c1ef74 --- /dev/null +++ b/tutorial/01-js/03-first-steps/22-javascript-specials.md @@ -0,0 +1,3 @@ +# Всё вместе: особенности JavaScript + +Content tutorial/01-js/03-first-steps/22-javascript-specials \ No newline at end of file diff --git a/tutorial/01-js/03-first-steps/index.md b/tutorial/01-js/03-first-steps/index.md new file mode 100644 index 000000000..affdb19f4 --- /dev/null +++ b/tutorial/01-js/03-first-steps/index.md @@ -0,0 +1,3 @@ +# Основы JavaScript + +Content tutorial/01-js/03-first-steps \ No newline at end of file diff --git a/tutorial/01-js/04-writing-js/01-debugging-chrome.md b/tutorial/01-js/04-writing-js/01-debugging-chrome.md new file mode 100644 index 000000000..285f43d39 --- /dev/null +++ b/tutorial/01-js/04-writing-js/01-debugging-chrome.md @@ -0,0 +1,3 @@ +# Отладка в браузере Chrome + +Content tutorial/01-js/04-writing-js/01-debugging-chrome \ No newline at end of file diff --git a/tutorial/01-js/04-writing-js/02-coding-style.md b/tutorial/01-js/04-writing-js/02-coding-style.md new file mode 100644 index 000000000..86d7c98d9 --- /dev/null +++ b/tutorial/01-js/04-writing-js/02-coding-style.md @@ -0,0 +1,3 @@ +# Советы по стилю кода + +Content tutorial/01-js/04-writing-js/02-coding-style \ No newline at end of file diff --git a/tutorial/01-js/04-writing-js/03-write-unmain-code.md b/tutorial/01-js/04-writing-js/03-write-unmain-code.md new file mode 100644 index 000000000..009f20886 --- /dev/null +++ b/tutorial/01-js/04-writing-js/03-write-unmain-code.md @@ -0,0 +1,3 @@ +# Как писать неподдерживаемый код? + +Content tutorial/01-js/04-writing-js/03-write-unmain-code \ No newline at end of file diff --git a/tutorial/01-js/04-writing-js/04-testing.md b/tutorial/01-js/04-writing-js/04-testing.md new file mode 100644 index 000000000..693c94d09 --- /dev/null +++ b/tutorial/01-js/04-writing-js/04-testing.md @@ -0,0 +1,3 @@ +# Автоматические тесты при помощи chai и mocha + +Content tutorial/01-js/04-writing-js/04-testing \ No newline at end of file diff --git a/tutorial/01-js/04-writing-js/index.md b/tutorial/01-js/04-writing-js/index.md new file mode 100644 index 000000000..d0a8e4430 --- /dev/null +++ b/tutorial/01-js/04-writing-js/index.md @@ -0,0 +1,3 @@ +# Качество кода + +Content tutorial/01-js/04-writing-js \ No newline at end of file diff --git a/tutorial/01-js/05-data-structures/01-string.md b/tutorial/01-js/05-data-structures/01-string.md new file mode 100644 index 000000000..6d88011bc --- /dev/null +++ b/tutorial/01-js/05-data-structures/01-string.md @@ -0,0 +1,3 @@ +# Строки + +Content tutorial/01-js/05-data-structures/01-string \ No newline at end of file diff --git a/tutorial/01-js/05-data-structures/02-number.md b/tutorial/01-js/05-data-structures/02-number.md new file mode 100644 index 000000000..45dcbe579 --- /dev/null +++ b/tutorial/01-js/05-data-structures/02-number.md @@ -0,0 +1,3 @@ +# Числа + +Content tutorial/01-js/05-data-structures/02-number \ No newline at end of file diff --git a/tutorial/01-js/05-data-structures/03-object.md b/tutorial/01-js/05-data-structures/03-object.md new file mode 100644 index 000000000..e8db34da2 --- /dev/null +++ b/tutorial/01-js/05-data-structures/03-object.md @@ -0,0 +1,3 @@ +# Объекты как ассоциативные массивы + +Content tutorial/01-js/05-data-structures/03-object \ No newline at end of file diff --git a/tutorial/01-js/05-data-structures/04-object-for-in.md b/tutorial/01-js/05-data-structures/04-object-for-in.md new file mode 100644 index 000000000..631495180 --- /dev/null +++ b/tutorial/01-js/05-data-structures/04-object-for-in.md @@ -0,0 +1,3 @@ +# Объекты: перебор свойств + +Content tutorial/01-js/05-data-structures/04-object-for-in \ No newline at end of file diff --git a/tutorial/01-js/05-data-structures/05-object-reference.md b/tutorial/01-js/05-data-structures/05-object-reference.md new file mode 100644 index 000000000..74e9c53fd --- /dev/null +++ b/tutorial/01-js/05-data-structures/05-object-reference.md @@ -0,0 +1,3 @@ +# Объекты: передача по ссылке + +Content tutorial/01-js/05-data-structures/05-object-reference \ No newline at end of file diff --git a/tutorial/01-js/05-data-structures/06-array.md b/tutorial/01-js/05-data-structures/06-array.md new file mode 100644 index 000000000..293b07c04 --- /dev/null +++ b/tutorial/01-js/05-data-structures/06-array.md @@ -0,0 +1,3 @@ +# Массивы c числовыми индексами + +Content tutorial/01-js/05-data-structures/06-array \ No newline at end of file diff --git a/tutorial/01-js/05-data-structures/07-array-methods.md b/tutorial/01-js/05-data-structures/07-array-methods.md new file mode 100644 index 000000000..d17f14788 --- /dev/null +++ b/tutorial/01-js/05-data-structures/07-array-methods.md @@ -0,0 +1,3 @@ +# Массивы: методы + +Content tutorial/01-js/05-data-structures/07-array-methods \ No newline at end of file diff --git a/tutorial/01-js/05-data-structures/08-array-iteration.md b/tutorial/01-js/05-data-structures/08-array-iteration.md new file mode 100644 index 000000000..965ad7c00 --- /dev/null +++ b/tutorial/01-js/05-data-structures/08-array-iteration.md @@ -0,0 +1,3 @@ +# Массив: перебирающие методы + +Content tutorial/01-js/05-data-structures/08-array-iteration \ No newline at end of file diff --git a/tutorial/01-js/05-data-structures/09-arguments-pseudoarray.md b/tutorial/01-js/05-data-structures/09-arguments-pseudoarray.md new file mode 100644 index 000000000..b7e736fc0 --- /dev/null +++ b/tutorial/01-js/05-data-structures/09-arguments-pseudoarray.md @@ -0,0 +1,3 @@ +# Псевдомассив аргументов "arguments" + +Content tutorial/01-js/05-data-structures/09-arguments-pseudoarray \ No newline at end of file diff --git a/tutorial/01-js/05-data-structures/10-datetime.md b/tutorial/01-js/05-data-structures/10-datetime.md new file mode 100644 index 000000000..3fd26c141 --- /dev/null +++ b/tutorial/01-js/05-data-structures/10-datetime.md @@ -0,0 +1,3 @@ +# Дата и Время + +Content tutorial/01-js/05-data-structures/10-datetime \ No newline at end of file diff --git a/tutorial/01-js/05-data-structures/11-typeof-duck-typing.md b/tutorial/01-js/05-data-structures/11-typeof-duck-typing.md new file mode 100644 index 000000000..859f73e77 --- /dev/null +++ b/tutorial/01-js/05-data-structures/11-typeof-duck-typing.md @@ -0,0 +1,3 @@ +# Оператор typeof и утиная типизация + +Content tutorial/01-js/05-data-structures/11-typeof-duck-typing \ No newline at end of file diff --git a/tutorial/01-js/05-data-structures/index.md b/tutorial/01-js/05-data-structures/index.md new file mode 100644 index 000000000..b813724be --- /dev/null +++ b/tutorial/01-js/05-data-structures/index.md @@ -0,0 +1,3 @@ +# Структуры данных + +Content tutorial/01-js/05-data-structures \ No newline at end of file diff --git a/tutorial/01-js/06-functions-closures/01-global-object.md b/tutorial/01-js/06-functions-closures/01-global-object.md new file mode 100644 index 000000000..465e67386 --- /dev/null +++ b/tutorial/01-js/06-functions-closures/01-global-object.md @@ -0,0 +1,3 @@ +# Глобальный объект + +Content tutorial/01-js/06-functions-closures/01-global-object \ No newline at end of file diff --git a/tutorial/01-js/06-functions-closures/02-closures.md b/tutorial/01-js/06-functions-closures/02-closures.md new file mode 100644 index 000000000..0ed6e93d3 --- /dev/null +++ b/tutorial/01-js/06-functions-closures/02-closures.md @@ -0,0 +1,3 @@ +# Замыкания, функции изнутри + +Content tutorial/01-js/06-functions-closures/02-closures \ No newline at end of file diff --git a/tutorial/01-js/06-functions-closures/03-scope-new-function.md b/tutorial/01-js/06-functions-closures/03-scope-new-function.md new file mode 100644 index 000000000..b1fb68c8e --- /dev/null +++ b/tutorial/01-js/06-functions-closures/03-scope-new-function.md @@ -0,0 +1,3 @@ +# [[Scope]] для new Function + +Content tutorial/01-js/06-functions-closures/03-scope-new-function \ No newline at end of file diff --git a/tutorial/01-js/06-functions-closures/04-closures-module.md b/tutorial/01-js/06-functions-closures/04-closures-module.md new file mode 100644 index 000000000..67fbfc759 --- /dev/null +++ b/tutorial/01-js/06-functions-closures/04-closures-module.md @@ -0,0 +1,3 @@ +# Модули через замыкания + +Content tutorial/01-js/06-functions-closures/04-closures-module \ No newline at end of file diff --git a/tutorial/01-js/06-functions-closures/05-closures-usage.md b/tutorial/01-js/06-functions-closures/05-closures-usage.md new file mode 100644 index 000000000..04f93a75d --- /dev/null +++ b/tutorial/01-js/06-functions-closures/05-closures-usage.md @@ -0,0 +1,3 @@ +# Использование замыканий + +Content tutorial/01-js/06-functions-closures/05-closures-usage \ No newline at end of file diff --git a/tutorial/01-js/06-functions-closures/06-memory-management.md b/tutorial/01-js/06-functions-closures/06-memory-management.md new file mode 100644 index 000000000..4dd3314d3 --- /dev/null +++ b/tutorial/01-js/06-functions-closures/06-memory-management.md @@ -0,0 +1,3 @@ +# Управление памятью в JavaScript + +Content tutorial/01-js/06-functions-closures/06-memory-management \ No newline at end of file diff --git a/tutorial/01-js/06-functions-closures/07-with.md b/tutorial/01-js/06-functions-closures/07-with.md new file mode 100644 index 000000000..c00da5de7 --- /dev/null +++ b/tutorial/01-js/06-functions-closures/07-with.md @@ -0,0 +1,3 @@ +# Устаревшая конструкция "with" + +Content tutorial/01-js/06-functions-closures/07-with \ No newline at end of file diff --git a/tutorial/01-js/06-functions-closures/index.md b/tutorial/01-js/06-functions-closures/index.md new file mode 100644 index 000000000..446c0b956 --- /dev/null +++ b/tutorial/01-js/06-functions-closures/index.md @@ -0,0 +1,3 @@ +# Замыкания, область видимости + +Content tutorial/01-js/06-functions-closures \ No newline at end of file diff --git a/tutorial/01-js/07-objects-more/01-object-methods.md b/tutorial/01-js/07-objects-more/01-object-methods.md new file mode 100644 index 000000000..4e9f856ed --- /dev/null +++ b/tutorial/01-js/07-objects-more/01-object-methods.md @@ -0,0 +1,3 @@ +# Методы объектов, this + +Content tutorial/01-js/07-objects-more/01-object-methods \ No newline at end of file diff --git a/tutorial/01-js/07-objects-more/02-constructor-new.md b/tutorial/01-js/07-objects-more/02-constructor-new.md new file mode 100644 index 000000000..d06d17a9f --- /dev/null +++ b/tutorial/01-js/07-objects-more/02-constructor-new.md @@ -0,0 +1,3 @@ +# Создание объектов через "new" + +Content tutorial/01-js/07-objects-more/02-constructor-new \ No newline at end of file diff --git a/tutorial/01-js/07-objects-more/03-static-properties-and-methods.md b/tutorial/01-js/07-objects-more/03-static-properties-and-methods.md new file mode 100644 index 000000000..096726e98 --- /dev/null +++ b/tutorial/01-js/07-objects-more/03-static-properties-and-methods.md @@ -0,0 +1,3 @@ +# Статические и фабричные методы + +Content tutorial/01-js/07-objects-more/03-static-properties-and-methods \ No newline at end of file diff --git a/tutorial/01-js/07-objects-more/04-call-apply.md b/tutorial/01-js/07-objects-more/04-call-apply.md new file mode 100644 index 000000000..b6308ddab --- /dev/null +++ b/tutorial/01-js/07-objects-more/04-call-apply.md @@ -0,0 +1,3 @@ +# Явное указание this: "call", "apply" + +Content tutorial/01-js/07-objects-more/04-call-apply \ No newline at end of file diff --git a/tutorial/01-js/07-objects-more/05-bind.md b/tutorial/01-js/07-objects-more/05-bind.md new file mode 100644 index 000000000..0c4c5a5cf --- /dev/null +++ b/tutorial/01-js/07-objects-more/05-bind.md @@ -0,0 +1,3 @@ +# Привязка контекста и карринг: "bind" + +Content tutorial/01-js/07-objects-more/05-bind \ No newline at end of file diff --git a/tutorial/01-js/07-objects-more/06-decorators.md b/tutorial/01-js/07-objects-more/06-decorators.md new file mode 100644 index 000000000..dfaf67a95 --- /dev/null +++ b/tutorial/01-js/07-objects-more/06-decorators.md @@ -0,0 +1,3 @@ +# Функции-обёртки, декораторы + +Content tutorial/01-js/07-objects-more/06-decorators \ No newline at end of file diff --git a/tutorial/01-js/07-objects-more/index.md b/tutorial/01-js/07-objects-more/index.md new file mode 100644 index 000000000..371d50fdd --- /dev/null +++ b/tutorial/01-js/07-objects-more/index.md @@ -0,0 +1,3 @@ +# Методы объектов и контекст вызова + +Content tutorial/01-js/07-objects-more \ No newline at end of file diff --git a/tutorial/01-js/08-js-misc/01-object-conversion.md b/tutorial/01-js/08-js-misc/01-object-conversion.md new file mode 100644 index 000000000..1914b3b67 --- /dev/null +++ b/tutorial/01-js/08-js-misc/01-object-conversion.md @@ -0,0 +1,3 @@ +# Преобразование объектов: toString и valueOf + +Content tutorial/01-js/08-js-misc/01-object-conversion \ No newline at end of file diff --git a/tutorial/01-js/08-js-misc/02-class-property.md b/tutorial/01-js/08-js-misc/02-class-property.md new file mode 100644 index 000000000..103bc1c97 --- /dev/null +++ b/tutorial/01-js/08-js-misc/02-class-property.md @@ -0,0 +1,3 @@ +# Секретное свойство [[Class]] + +Content tutorial/01-js/08-js-misc/02-class-property \ No newline at end of file diff --git a/tutorial/01-js/08-js-misc/03-json.md b/tutorial/01-js/08-js-misc/03-json.md new file mode 100644 index 000000000..9e06dc1bd --- /dev/null +++ b/tutorial/01-js/08-js-misc/03-json.md @@ -0,0 +1,3 @@ +# Формат JSON, метод toJSON + +Content tutorial/01-js/08-js-misc/03-json \ No newline at end of file diff --git a/tutorial/01-js/08-js-misc/04-setTimeout-setInterval.md b/tutorial/01-js/08-js-misc/04-setTimeout-setInterval.md new file mode 100644 index 000000000..eef945ef4 --- /dev/null +++ b/tutorial/01-js/08-js-misc/04-setTimeout-setInterval.md @@ -0,0 +1,3 @@ +# setTimeout и setInterval + +Content tutorial/01-js/08-js-misc/04-setTimeout-setInterval \ No newline at end of file diff --git a/tutorial/01-js/08-js-misc/05-eval.md b/tutorial/01-js/08-js-misc/05-eval.md new file mode 100644 index 000000000..e621abb46 --- /dev/null +++ b/tutorial/01-js/08-js-misc/05-eval.md @@ -0,0 +1,3 @@ +# Запуск кода из строки: eval + +Content tutorial/01-js/08-js-misc/05-eval \ No newline at end of file diff --git a/tutorial/01-js/08-js-misc/06-exception.md b/tutorial/01-js/08-js-misc/06-exception.md new file mode 100644 index 000000000..cc392abbe --- /dev/null +++ b/tutorial/01-js/08-js-misc/06-exception.md @@ -0,0 +1,3 @@ +# Перехват ошибок, "try..catch" + +Content tutorial/01-js/08-js-misc/06-exception \ No newline at end of file diff --git a/tutorial/01-js/08-js-misc/index.md b/tutorial/01-js/08-js-misc/index.md new file mode 100644 index 000000000..4de4e9ec8 --- /dev/null +++ b/tutorial/01-js/08-js-misc/index.md @@ -0,0 +1,3 @@ +# Некоторые другие возможности + +Content tutorial/01-js/08-js-misc \ No newline at end of file diff --git a/tutorial/01-js/09-oop/01-about-oop.md b/tutorial/01-js/09-oop/01-about-oop.md new file mode 100644 index 000000000..edd000482 --- /dev/null +++ b/tutorial/01-js/09-oop/01-about-oop.md @@ -0,0 +1,3 @@ +# Введение + +Content tutorial/01-js/09-oop/01-about-oop \ No newline at end of file diff --git a/tutorial/01-js/09-oop/02-internal-external-interface.md b/tutorial/01-js/09-oop/02-internal-external-interface.md new file mode 100644 index 000000000..ed58adf0b --- /dev/null +++ b/tutorial/01-js/09-oop/02-internal-external-interface.md @@ -0,0 +1,3 @@ +# Внутренний и внешний интерфейс + +Content tutorial/01-js/09-oop/02-internal-external-interface \ No newline at end of file diff --git a/tutorial/01-js/09-oop/03-getters-setters.md b/tutorial/01-js/09-oop/03-getters-setters.md new file mode 100644 index 000000000..0343015fa --- /dev/null +++ b/tutorial/01-js/09-oop/03-getters-setters.md @@ -0,0 +1,3 @@ +# Геттеры и сеттеры + +Content tutorial/01-js/09-oop/03-getters-setters \ No newline at end of file diff --git a/tutorial/01-js/09-oop/04-descriptors-getters-setters.md b/tutorial/01-js/09-oop/04-descriptors-getters-setters.md new file mode 100644 index 000000000..0d3b71437 --- /dev/null +++ b/tutorial/01-js/09-oop/04-descriptors-getters-setters.md @@ -0,0 +1,3 @@ +# Дескрипторы, геттеры и сеттеры свойств + +Content tutorial/01-js/09-oop/04-descriptors-getters-setters \ No newline at end of file diff --git a/tutorial/01-js/09-oop/05-functional-inheritance.md b/tutorial/01-js/09-oop/05-functional-inheritance.md new file mode 100644 index 000000000..37a0a5fbe --- /dev/null +++ b/tutorial/01-js/09-oop/05-functional-inheritance.md @@ -0,0 +1,3 @@ +# Функциональное наследование + +Content tutorial/01-js/09-oop/05-functional-inheritance \ No newline at end of file diff --git a/tutorial/01-js/09-oop/index.md b/tutorial/01-js/09-oop/index.md new file mode 100644 index 000000000..227ca6ce5 --- /dev/null +++ b/tutorial/01-js/09-oop/index.md @@ -0,0 +1,3 @@ +# ООП в функциональном стиле + +Content tutorial/01-js/09-oop \ No newline at end of file diff --git a/tutorial/01-js/10-prototypes/01-prototype.md b/tutorial/01-js/10-prototypes/01-prototype.md new file mode 100644 index 000000000..afdb5ebf5 --- /dev/null +++ b/tutorial/01-js/10-prototypes/01-prototype.md @@ -0,0 +1,3 @@ +# Прототип объекта + +Content tutorial/01-js/10-prototypes/01-prototype \ No newline at end of file diff --git a/tutorial/01-js/10-prototypes/02-new-prototype.md b/tutorial/01-js/10-prototypes/02-new-prototype.md new file mode 100644 index 000000000..9560f1674 --- /dev/null +++ b/tutorial/01-js/10-prototypes/02-new-prototype.md @@ -0,0 +1,3 @@ +# Свойство F.prototype и создание объектов через new + +Content tutorial/01-js/10-prototypes/02-new-prototype \ No newline at end of file diff --git a/tutorial/01-js/10-prototypes/03-native-prototypes.md b/tutorial/01-js/10-prototypes/03-native-prototypes.md new file mode 100644 index 000000000..e65230b26 --- /dev/null +++ b/tutorial/01-js/10-prototypes/03-native-prototypes.md @@ -0,0 +1,3 @@ +# Встроенные "классы" в JavaScript + +Content tutorial/01-js/10-prototypes/03-native-prototypes \ No newline at end of file diff --git a/tutorial/01-js/10-prototypes/04-classes.md b/tutorial/01-js/10-prototypes/04-classes.md new file mode 100644 index 000000000..159be8793 --- /dev/null +++ b/tutorial/01-js/10-prototypes/04-classes.md @@ -0,0 +1,3 @@ +# Свои классы на прототипах + +Content tutorial/01-js/10-prototypes/04-classes \ No newline at end of file diff --git a/tutorial/01-js/10-prototypes/05-class-inheritance.md b/tutorial/01-js/10-prototypes/05-class-inheritance.md new file mode 100644 index 000000000..86dd5f8ce --- /dev/null +++ b/tutorial/01-js/10-prototypes/05-class-inheritance.md @@ -0,0 +1,3 @@ +# Наследование классов в JavaScript + +Content tutorial/01-js/10-prototypes/05-class-inheritance \ No newline at end of file diff --git a/tutorial/01-js/10-prototypes/06-constructor.md b/tutorial/01-js/10-prototypes/06-constructor.md new file mode 100644 index 000000000..2cf32e453 --- /dev/null +++ b/tutorial/01-js/10-prototypes/06-constructor.md @@ -0,0 +1,3 @@ +# F.prototype по умолчанию, "constructor" + +Content tutorial/01-js/10-prototypes/06-constructor \ No newline at end of file diff --git a/tutorial/01-js/10-prototypes/07-instanceof.md b/tutorial/01-js/10-prototypes/07-instanceof.md new file mode 100644 index 000000000..e749e47e8 --- /dev/null +++ b/tutorial/01-js/10-prototypes/07-instanceof.md @@ -0,0 +1,3 @@ +# Проверка класса: "instanceof" + +Content tutorial/01-js/10-prototypes/07-instanceof \ No newline at end of file diff --git a/tutorial/01-js/10-prototypes/08-class-extend.md b/tutorial/01-js/10-prototypes/08-class-extend.md new file mode 100644 index 000000000..4a6dd2f1f --- /dev/null +++ b/tutorial/01-js/10-prototypes/08-class-extend.md @@ -0,0 +1,3 @@ +# Фреймворк Class.extend + +Content tutorial/01-js/10-prototypes/08-class-extend \ No newline at end of file diff --git a/tutorial/01-js/10-prototypes/09-why-prototypes-better.md b/tutorial/01-js/10-prototypes/09-why-prototypes-better.md new file mode 100644 index 000000000..306c752ee --- /dev/null +++ b/tutorial/01-js/10-prototypes/09-why-prototypes-better.md @@ -0,0 +1,3 @@ +# Сравнение с функциональным наследованием + +Content tutorial/01-js/10-prototypes/09-why-prototypes-better \ No newline at end of file diff --git a/tutorial/01-js/10-prototypes/index.md b/tutorial/01-js/10-prototypes/index.md new file mode 100644 index 000000000..830ce22a4 --- /dev/null +++ b/tutorial/01-js/10-prototypes/index.md @@ -0,0 +1,3 @@ +# ООП в прототипном стиле + +Content tutorial/01-js/10-prototypes \ No newline at end of file diff --git a/tutorial/01-js/index.md b/tutorial/01-js/index.md new file mode 100644 index 000000000..f92369eb1 --- /dev/null +++ b/tutorial/01-js/index.md @@ -0,0 +1,3 @@ +# Язык JavaScript + +Content tutorial/01-js \ No newline at end of file diff --git a/tutorial/02-ui/01-document/01-browser-environment.md b/tutorial/02-ui/01-document/01-browser-environment.md new file mode 100644 index 000000000..d151f16e3 --- /dev/null +++ b/tutorial/02-ui/01-document/01-browser-environment.md @@ -0,0 +1,3 @@ +# Окружение: DOM, BOM и JS + +Content tutorial/02-ui/01-document/01-browser-environment \ No newline at end of file diff --git a/tutorial/02-ui/01-document/02-dom-nodes.md b/tutorial/02-ui/01-document/02-dom-nodes.md new file mode 100644 index 000000000..4acdad98c --- /dev/null +++ b/tutorial/02-ui/01-document/02-dom-nodes.md @@ -0,0 +1,3 @@ +# Дерево DOM + +Content tutorial/02-ui/01-document/02-dom-nodes \ No newline at end of file diff --git a/tutorial/02-ui/01-document/03-dom-console.md b/tutorial/02-ui/01-document/03-dom-console.md new file mode 100644 index 000000000..f4bd606da --- /dev/null +++ b/tutorial/02-ui/01-document/03-dom-console.md @@ -0,0 +1,3 @@ +# Работа с DOM из консоли + +Content tutorial/02-ui/01-document/03-dom-console \ No newline at end of file diff --git a/tutorial/02-ui/01-document/04-traversing-dom.md b/tutorial/02-ui/01-document/04-traversing-dom.md new file mode 100644 index 000000000..044ab502e --- /dev/null +++ b/tutorial/02-ui/01-document/04-traversing-dom.md @@ -0,0 +1,3 @@ +# Ссылки между DOM-элементами + +Content tutorial/02-ui/01-document/04-traversing-dom \ No newline at end of file diff --git a/tutorial/02-ui/01-document/05-traversing-tables.md b/tutorial/02-ui/01-document/05-traversing-tables.md new file mode 100644 index 000000000..a5b295666 --- /dev/null +++ b/tutorial/02-ui/01-document/05-traversing-tables.md @@ -0,0 +1,3 @@ +# Особые ссылки для таблиц + +Content tutorial/02-ui/01-document/05-traversing-tables \ No newline at end of file diff --git a/tutorial/02-ui/01-document/06-basic-dom-node-properties.md b/tutorial/02-ui/01-document/06-basic-dom-node-properties.md new file mode 100644 index 000000000..bab388424 --- /dev/null +++ b/tutorial/02-ui/01-document/06-basic-dom-node-properties.md @@ -0,0 +1,3 @@ +# Свойства узлов: тип, тег и содержимое + +Content tutorial/02-ui/01-document/06-basic-dom-node-properties \ No newline at end of file diff --git a/tutorial/02-ui/01-document/07-attributes-and-custom-properties.md b/tutorial/02-ui/01-document/07-attributes-and-custom-properties.md new file mode 100644 index 000000000..48d0e7ec6 --- /dev/null +++ b/tutorial/02-ui/01-document/07-attributes-and-custom-properties.md @@ -0,0 +1,3 @@ +# Атрибуты и "свои" свойства + +Content tutorial/02-ui/01-document/07-attributes-and-custom-properties \ No newline at end of file diff --git a/tutorial/02-ui/01-document/08-searching-elements-dom.md b/tutorial/02-ui/01-document/08-searching-elements-dom.md new file mode 100644 index 000000000..910bb4fcd --- /dev/null +++ b/tutorial/02-ui/01-document/08-searching-elements-dom.md @@ -0,0 +1,3 @@ +# Поиск: getElement* и querySelector* + +Content tutorial/02-ui/01-document/08-searching-elements-dom \ No newline at end of file diff --git a/tutorial/02-ui/01-document/09-searching-elements-internals.md b/tutorial/02-ui/01-document/09-searching-elements-internals.md new file mode 100644 index 000000000..80acc5722 --- /dev/null +++ b/tutorial/02-ui/01-document/09-searching-elements-internals.md @@ -0,0 +1,3 @@ +# Внутреннее устройство поисковых методов + +Content tutorial/02-ui/01-document/09-searching-elements-internals \ No newline at end of file diff --git a/tutorial/02-ui/01-document/10-compare-document-position.md b/tutorial/02-ui/01-document/10-compare-document-position.md new file mode 100644 index 000000000..f9f2fa132 --- /dev/null +++ b/tutorial/02-ui/01-document/10-compare-document-position.md @@ -0,0 +1,3 @@ +# Методы contains и compareDocumentPosition + +Content tutorial/02-ui/01-document/10-compare-document-position \ No newline at end of file diff --git a/tutorial/02-ui/01-document/11-modifying-document.md b/tutorial/02-ui/01-document/11-modifying-document.md new file mode 100644 index 000000000..6827385d7 --- /dev/null +++ b/tutorial/02-ui/01-document/11-modifying-document.md @@ -0,0 +1,3 @@ +# Добавление и удаление узлов + +Content tutorial/02-ui/01-document/11-modifying-document \ No newline at end of file diff --git a/tutorial/02-ui/01-document/12-multi-insert.md b/tutorial/02-ui/01-document/12-multi-insert.md new file mode 100644 index 000000000..fd8e6fd08 --- /dev/null +++ b/tutorial/02-ui/01-document/12-multi-insert.md @@ -0,0 +1,3 @@ +# Мультивставка: insertAdjacentHTML и DocumentFragment + +Content tutorial/02-ui/01-document/12-multi-insert \ No newline at end of file diff --git a/tutorial/02-ui/01-document/13-document-write.md b/tutorial/02-ui/01-document/13-document-write.md new file mode 100644 index 000000000..fb7a89dd7 --- /dev/null +++ b/tutorial/02-ui/01-document/13-document-write.md @@ -0,0 +1,3 @@ +# Метод document.write + +Content tutorial/02-ui/01-document/13-document-write \ No newline at end of file diff --git a/tutorial/02-ui/01-document/14-styles-and-classes.md b/tutorial/02-ui/01-document/14-styles-and-classes.md new file mode 100644 index 000000000..cf55ab4be --- /dev/null +++ b/tutorial/02-ui/01-document/14-styles-and-classes.md @@ -0,0 +1,3 @@ +# Стили, getComputedStyle + +Content tutorial/02-ui/01-document/14-styles-and-classes \ No newline at end of file diff --git a/tutorial/02-ui/01-document/15-metrics.md b/tutorial/02-ui/01-document/15-metrics.md new file mode 100644 index 000000000..103570584 --- /dev/null +++ b/tutorial/02-ui/01-document/15-metrics.md @@ -0,0 +1,3 @@ +# Размеры и прокрутка элементов + +Content tutorial/02-ui/01-document/15-metrics \ No newline at end of file diff --git a/tutorial/02-ui/01-document/16-metrics-window.md b/tutorial/02-ui/01-document/16-metrics-window.md new file mode 100644 index 000000000..475b363a0 --- /dev/null +++ b/tutorial/02-ui/01-document/16-metrics-window.md @@ -0,0 +1,3 @@ +# Размеры и прокрутка страницы + +Content tutorial/02-ui/01-document/16-metrics-window \ No newline at end of file diff --git a/tutorial/02-ui/01-document/17-coordinates.md b/tutorial/02-ui/01-document/17-coordinates.md new file mode 100644 index 000000000..42fe5e523 --- /dev/null +++ b/tutorial/02-ui/01-document/17-coordinates.md @@ -0,0 +1,3 @@ +# Координаты в окне + +Content tutorial/02-ui/01-document/17-coordinates \ No newline at end of file diff --git a/tutorial/02-ui/01-document/18-coordinates-document.md b/tutorial/02-ui/01-document/18-coordinates-document.md new file mode 100644 index 000000000..adc71150b --- /dev/null +++ b/tutorial/02-ui/01-document/18-coordinates-document.md @@ -0,0 +1,3 @@ +# Координаты в документе + +Content tutorial/02-ui/01-document/18-coordinates-document \ No newline at end of file diff --git a/tutorial/02-ui/01-document/19-support-polyfill.md b/tutorial/02-ui/01-document/19-support-polyfill.md new file mode 100644 index 000000000..3b97f5cf5 --- /dev/null +++ b/tutorial/02-ui/01-document/19-support-polyfill.md @@ -0,0 +1,3 @@ +# Современный DOM: полифиллы + +Content tutorial/02-ui/01-document/19-support-polyfill \ No newline at end of file diff --git a/tutorial/02-ui/01-document/20-dom-cheatsheet.md b/tutorial/02-ui/01-document/20-dom-cheatsheet.md new file mode 100644 index 000000000..ee3f80900 --- /dev/null +++ b/tutorial/02-ui/01-document/20-dom-cheatsheet.md @@ -0,0 +1,3 @@ +# Итого: DOM-шпаргалка + +Content tutorial/02-ui/01-document/20-dom-cheatsheet \ No newline at end of file diff --git a/tutorial/02-ui/01-document/index.md b/tutorial/02-ui/01-document/index.md new file mode 100644 index 000000000..6b067b77d --- /dev/null +++ b/tutorial/02-ui/01-document/index.md @@ -0,0 +1,3 @@ +# Документ и объекты страницы + +Content tutorial/02-ui/01-document \ No newline at end of file diff --git a/tutorial/02-ui/02-events-and-interfaces/01-introduction-browser-events.md b/tutorial/02-ui/02-events-and-interfaces/01-introduction-browser-events.md new file mode 100644 index 000000000..25798463a --- /dev/null +++ b/tutorial/02-ui/02-events-and-interfaces/01-introduction-browser-events.md @@ -0,0 +1,3 @@ +# Введение в браузерные события + +Content tutorial/02-ui/02-events-and-interfaces/01-introduction-browser-events \ No newline at end of file diff --git a/tutorial/02-ui/02-events-and-interfaces/02-events-and-timing-depth.md b/tutorial/02-ui/02-events-and-interfaces/02-events-and-timing-depth.md new file mode 100644 index 000000000..cc68e5c06 --- /dev/null +++ b/tutorial/02-ui/02-events-and-interfaces/02-events-and-timing-depth.md @@ -0,0 +1,3 @@ +# Порядок обработки событий + +Content tutorial/02-ui/02-events-and-interfaces/02-events-and-timing-depth \ No newline at end of file diff --git a/tutorial/02-ui/02-events-and-interfaces/03-obtaining-event-object.md b/tutorial/02-ui/02-events-and-interfaces/03-obtaining-event-object.md new file mode 100644 index 000000000..c5841b6d0 --- /dev/null +++ b/tutorial/02-ui/02-events-and-interfaces/03-obtaining-event-object.md @@ -0,0 +1,3 @@ +# Объект события + +Content tutorial/02-ui/02-events-and-interfaces/03-obtaining-event-object \ No newline at end of file diff --git a/tutorial/02-ui/02-events-and-interfaces/04-event-bubbling.md b/tutorial/02-ui/02-events-and-interfaces/04-event-bubbling.md new file mode 100644 index 000000000..a96fa52a1 --- /dev/null +++ b/tutorial/02-ui/02-events-and-interfaces/04-event-bubbling.md @@ -0,0 +1,3 @@ +# Всплытие и перехват + +Content tutorial/02-ui/02-events-and-interfaces/04-event-bubbling \ No newline at end of file diff --git a/tutorial/02-ui/02-events-and-interfaces/05-event-delegation.md b/tutorial/02-ui/02-events-and-interfaces/05-event-delegation.md new file mode 100644 index 000000000..a0a47f4ce --- /dev/null +++ b/tutorial/02-ui/02-events-and-interfaces/05-event-delegation.md @@ -0,0 +1,3 @@ +# Делегирование событий + +Content tutorial/02-ui/02-events-and-interfaces/05-event-delegation \ No newline at end of file diff --git a/tutorial/02-ui/02-events-and-interfaces/06-behavior.md b/tutorial/02-ui/02-events-and-interfaces/06-behavior.md new file mode 100644 index 000000000..50837f771 --- /dev/null +++ b/tutorial/02-ui/02-events-and-interfaces/06-behavior.md @@ -0,0 +1,3 @@ +# Приём проектирования "поведение" + +Content tutorial/02-ui/02-events-and-interfaces/06-behavior \ No newline at end of file diff --git a/tutorial/02-ui/02-events-and-interfaces/07-default-browser-action.md b/tutorial/02-ui/02-events-and-interfaces/07-default-browser-action.md new file mode 100644 index 000000000..58414945f --- /dev/null +++ b/tutorial/02-ui/02-events-and-interfaces/07-default-browser-action.md @@ -0,0 +1,3 @@ +# Действия браузера по умолчанию + +Content tutorial/02-ui/02-events-and-interfaces/07-default-browser-action \ No newline at end of file diff --git a/tutorial/02-ui/02-events-and-interfaces/08-dispatch-events.md b/tutorial/02-ui/02-events-and-interfaces/08-dispatch-events.md new file mode 100644 index 000000000..ac8f69443 --- /dev/null +++ b/tutorial/02-ui/02-events-and-interfaces/08-dispatch-events.md @@ -0,0 +1,3 @@ +# Генерация событий на элементах + +Content tutorial/02-ui/02-events-and-interfaces/08-dispatch-events \ No newline at end of file diff --git a/tutorial/02-ui/02-events-and-interfaces/index.md b/tutorial/02-ui/02-events-and-interfaces/index.md new file mode 100644 index 000000000..f4d2861a1 --- /dev/null +++ b/tutorial/02-ui/02-events-and-interfaces/index.md @@ -0,0 +1,3 @@ +# Основы работы с событиями + +Content tutorial/02-ui/02-events-and-interfaces \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/01-mouse-clicks.md b/tutorial/02-ui/03-event-details/01-mouse-clicks.md new file mode 100644 index 000000000..1b0ad8efd --- /dev/null +++ b/tutorial/02-ui/03-event-details/01-mouse-clicks.md @@ -0,0 +1,286 @@ +# Мышь: клики, кнопка, координаты + +В этой главе мы глубже разберёмся со списком событий мыши, рассмотрим их общие свойства, а также те события, которые связаны с кликом. +[cut] +## Типы событий мыши + +Условно можно разделить события на два типа: "простые" и "комплексные". + +### Простые события + +
+
`mousedown`
+
Кнопка мыши нажата над элементом.
+
`mouseup`
+
Кнопка мыши отпущена над элементом.
+
`mouseover`
+
Мышь появилась над элементом.
+
`mouseout`
+
Мышь ушла с элемента.
+
`mousemove`
+
Каждое движение мыши над элементом генерирует это событие.
+
+ +### Комплексные события + +
+
`click`
+
Вызывается при клике мышью, то есть при `mousedown`, а затем `mouseup` на одном элементе
+
`contextmenu`
+
Вызывается при клике правой кнопкой мыши на элементе.
+
`dblclick`
+
Вызывается при двойном клике по элементу.
+
+ +Комплексные можно составить из простых, поэтому в теории можно было бы обойтись вообще без них. Но они есть, и это хорошо, потому что с ними удобнее. + +### Порядок срабатывания событий + +**Одно действие может вызывать несколько событий.** + +Например, клик вызывает сначала `mousedown` при нажатии, а затем `mouseup` и `click` при отпускании кнопки. + +В тех случаях, когда одно действие генерирует несколько событий, их порядок фиксирован. То есть, обработчики вызовутся в порядке `mousedown -> mouseup -> click`. + +Кликните по кнопке ниже и вы увидите, какие при этом происходят события. Попробуйте также двойной клик. + +На тест-стенде ниже все мышиные события записываются, и если между событиями проходит больше 1 секунды, то они для удобства чтения отделяются линией. Также присутствуют свойства `which/button`, по которым можно определить кнопку мыши. Мы их рассмотрим далее. + +
+ +**Каждое событие обрабатывается независимо.** + +Например, при клике события `mouseup + click` возникают одновременно, но обрабатываются последовательно. Сначала полностью завершается обработка `mouseup`, затем запускается `click`. + + +## Получение информации о кнопке: `which` + +При обработке событий, связанных с кликами мыши, бывает важно знать, какая кнопка нажата. + +**Для получения кнопки мыши в объекте `event` есть свойство `which`.** + +На практике оно используется редко, т.к. обычно обработчик вешается либо `onclick` -- только на левую кнопку мыши, либо `oncontextmenu` -- только на правую. + +Возможны следующие значения: + + +Это свойство не поддерживается IE8-, но его можно получить способом, описанным в конце главы. + +## Правый клик: `oncontextmenu` + +При клике правой кнопкой мыши браузер показывает свое контекстное меню. Это является его действием по умолчанию: + +[html autorun height=auto] + +[/html] + +...Но если мы не хотим, чтобы показывалось встроенное меню, например потому что показываем своё, то можно отменить действие по умолчанию. + +В примере ниже встроенное меню показано не будет: +[html autorun height=auto] + +[/html] + + + + +## Модификаторы `shift`, `alt`, `ctrl` и `meta` + +Во всех событиях мыши присутствует информация о нажатых клавишах-модификаторах. + +Соответствующие свойства: + + +Например, кнопка ниже сработает только на Alt+Shift+Клик: + +[html autorun] + + + +[/html] + +[warn header="Внимание: на Mac вместо `Ctrl` используется `Cmd`"] +На компьютерах Mac кроме клавиш [key Alt], [key Shift] и [key Ctrl], есть ещё одна специальная клавиша: [key Cmd], которой соответствует свойство `metaKey`. + +В большинстве случаев на Mac вместо [key Ctrl] используется [key Cmd]. Там, где пользователь Windows нажимает [key Ctrl+Enter] или [key Ctrl+A], пользователь Mac нажмёт [key Cmd+Enter] или [key Cmd+A], и так далее, почти всегда [key Cmd] вместо [key Ctrl]. + +Поэтому, если мы хотим поддерживать [key Ctrl]+click, то под Mac имеет смысл обрабатывать [key Cmd]+click. + +Даже если бы мы хотели бы заставить пользователей Mac использовать именно [key Ctrl]+click -- это было бы затруднительно. Дело в том, что обычный клик с зажатым [key Ctrl] под Mac работает как *правый клик* и генерирует другое событие: `oncontextmenu`, так что сгенерировать именно [key Ctrl]+click под Mac достаточно сложно. + +Вывод -- чтобы пользователи обоих операционных систем работали с комфортом, в паре с `ctrlKey` нужно обязательно использовать `metaKey`. + +В JS-коде это означает, что для удобства пользователей Mac нужно проверять `if (event.ctrlKey || event.metaKey)`. +[/warn] + +## Координаты мыши + +Все мышиные события предоставляют текущие координаты курсора в двух видах: относительно окна и относительно документа. + +### Относительно окна: `clientX/Y` + +**Есть отличное кросс-браузерное свойство `clientX`(`clientY`), которое содержит координаты курсора относительно `window`.** + +При этом, например, если ваше окно размером 500x500, а мышь находится в центре, тогда и `clientX` и `clientY` будут равны 250. + +Можно как угодно прокручивать страницу, но если не двигать при этом мышь, то координаты курсора `clientX/clientY` не изменятся, потому что они считаются относительно окна, а не документа. + +Проведите мышью над полем ввода, чтобы увидеть `clientX/clientY`: +[html] + +[/html] + + + +### Относительно документа: `pageX/Y` + +**Координаты курсора относительно документа находятся в свойствах `pageX/pageY`.** + +Так как эти координаты -- относительно левого-верхнего узла документа, а не окна, то они учитывают прокрутку. Если прокрутить страницу, а мышь не трогать, то координаты курсора `pageX/pageY` изменятся на величину прокрутки, они привязаны к конкретной точке в документе. + +В IE8- этих свойств нет, но можно получить их способом, описанным в конце главы. + +Проведите мышью над полем ввода, чтобы увидеть `pageX/pageY` (кроме IE8-): +[html] + +[/html] + + +[warn header="Устарели: `x, y, layerX, layerY`"] +Некоторые браузеры поддерживают свойства `event.x/y`, `event.layerX/layerY`. + +Эти свойства устарели, они нестандартные и не добавляют ничего к описанным ваше. Использовать их не стоит. +[/warn] + + +## Особенности IE8- + +### Двойной клик + +Все браузеры, кроме IE8-, генерируют `dblclick` *в дополнение* к другим событиям. + +То есть, обычно: + + +**IE8- на втором клике не генерирует `mousedown` и `click`.** + +Получается: + + +**Поэтому отловить двойной клик в IE8-, отслеживая только `click`, нельзя, ведь при втором нажатии его нет. Нужно именно событие `dblclick`.** + +### Свойство `which/button` + + +В старых IE8- не поддерживалось свойство `which`, а вместо него использовалось свойство `button`, которое является 3-х битным числом, в котором каждому биту соответствует кнопка мыши. Бит установлен в 1, только если соответствующая кнопка нажата. + +Чтобы его расшифровать -- нужна [побитовая операция](article:520) `&` ("битовое И"): + + + +Что интересно, при этом мы можем узнать, были ли две кнопки нажаты одновременно, в то время как стандартный `which` такой возможности не даёт. Так что, в некотором смысле, свойство `button` -- более мощное. + +Можно легко сделать функцию, которая будет ставить свойство `which` из `button`, если его нет: +[js] +function fixWhich(e) { + if (!e.which && e.button) { // если which нет, но есть button... (IE8-) + if (e.button & 1) e.which = 1; // левая кнопка + else if (e.button & 4) e.which = 2; // средняя кнопка + else if (e.button & 2) e.which = 3; // правая кнопка + } +} +[/js] + + +[ref id="fixPageXY"] + +### Свойства `pageX/pageY` + +В IE до версии 9 не поддерживаются свойства `pageX/pageY`, но их можно получить, прибавив к `clientX/clientY` величину прокрутки страницы. + +Более подробно о её вычислении вы можете прочитать в разделе [прокрутка страницы](#page-scroll). + +Мы же здесь приведем готовый вариант, который позволяет нам получить `pageX/pageY` для старых IE: + +[js] +function fixPageXY(e) { + if (e.pageX == null && e.clientX != null ) { // если нет pageX.. + var html = document.documentElement; + var body = document.body; + + e.pageX = e.clientX + (html.scrollLeft || body && body.scrollLeft || 0); + e.pageX -= html.clientLeft || 0; + + e.pageY = e.clientY + (html.scrollTop || body && body.scrollTop || 0); + e.pageY -= html.clientTop || 0; + } +} +[/js] + + + +## Итого + +**События мыши имеют следующие свойства:** + + + + +[task id=560] + + +[task id=561] +[head] + + + + +[/head] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/question.code/index.html b/tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/question.code/index.html new file mode 100755 index 000000000..b9607c6a1 --- /dev/null +++ b/tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/question.code/index.html @@ -0,0 +1,47 @@ + + + + + + + + +Клик на элементе выделяет только его.
+Ctrl(Cmd)+Клик добавляет/убирает элемент из выделенных.
+Shift+Клик добавляет промежуток от последнего кликнутого к выделению.
+ + + + + + diff --git a/tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/solution.code/selectable-list-1/index.html b/tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/solution.code/selectable-list-1/index.html new file mode 100755 index 000000000..19496d9c4 --- /dev/null +++ b/tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/solution.code/selectable-list-1/index.html @@ -0,0 +1,61 @@ + + + + + + + + +Клик на элементе выделяет только его.
+ + + + + + diff --git a/tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/solution.code/selectable-list-2/index.html b/tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/solution.code/selectable-list-2/index.html new file mode 100755 index 000000000..ca49cb2ad --- /dev/null +++ b/tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/solution.code/selectable-list-2/index.html @@ -0,0 +1,67 @@ + + + + + + + + +Клик на элементе выделяет только его.
+Ctrl(Cmd)+Клик добавляет/убирает элемент из выделенных.
+ + + + + + diff --git a/tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/solution.code/selectable-list-3/index.html b/tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/solution.code/selectable-list-3/index.html new file mode 100755 index 000000000..530a64aa8 --- /dev/null +++ b/tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/solution.code/selectable-list-3/index.html @@ -0,0 +1,108 @@ + + + + + + + + +Клик на элементе выделяет только его.
+Ctrl(Cmd)+Клик добавляет/убирает элемент из выделенных.
+Shift+Клик добавляет промежуток от последнего кликнутого к выделению.
+ + + + + + diff --git a/tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/solution.md b/tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/solution.md new file mode 100644 index 000000000..2fc6d4ad5 --- /dev/null +++ b/tutorial/02-ui/03-event-details/01-mouse-clicks/01-selectable-list.task/solution.md @@ -0,0 +1,5 @@ +Выделение одного элемента: [edit src="solution"]открыть[/edit]. + +[edit src="solution"]Выделение с Ctrl[/edit] + +[edit src="solution"]Выделение с Shift[/edit] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/question.code/index.html b/tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/question.code/index.html new file mode 100755 index 000000000..6ee56ce0a --- /dev/null +++ b/tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/question.code/index.html @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + diff --git a/tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/question.code/tree-coords-src/index.html b/tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/question.code/tree-coords-src/index.html new file mode 100755 index 000000000..6ee56ce0a --- /dev/null +++ b/tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/question.code/tree-coords-src/index.html @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + diff --git a/tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/question.md b/tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/question.md new file mode 100644 index 000000000..57b19b4f8 --- /dev/null +++ b/tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/question.md @@ -0,0 +1,37 @@ +# Дерево: проверка клика на заголовке + +[importance 5] + +Есть кликабельное JavaScript-дерево UL/LI (см. задачу [](task:122)). + +[html] + +[/html] + +При клике на заголовке его список его детей скрывается-раскрывается. +Выглядит это так: (кликайте на заголовки) + +[iframe play link border="1" src="question"] + +Однако, проблема в том, что скрытие-раскрытие происходит даже при клике *вне заголовка*, на пустом пространстве справа от него. + +**Как скрывать/раскрывать детей только при клике на заголовок?** + +В задаче [](task:122) это решено так: заголовки завёрнуты в элементы `SPAN` и проверяются клики только на них. Представим на минуту, что мы не хотим оборачивать текст в `SPAN`, а хотим оставить как есть. Например, по соображениям производительности, если дерево и так очень большое, ведь оборачивание всех заголовков в `SPAN` увеличит количество DOM-узлов в 2 раза. + +**Решите задачу без обёртывания заголовков в `SPAN`, используя работу с координатами.** + +Исходный документ содержит кликабельное дерево. + +[edit src="question" task/] + +P.S. Задача -- скорее на сообразительность, однако подход может быть полезен в реальной жизни. diff --git a/tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/solution.code/index.html b/tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/solution.code/index.html new file mode 100755 index 000000000..f96de868d --- /dev/null +++ b/tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/solution.code/index.html @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + diff --git a/tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/solution.md b/tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/solution.md new file mode 100644 index 000000000..4d29d0076 --- /dev/null +++ b/tutorial/02-ui/03-event-details/01-mouse-clicks/02-tree-coords.task/solution.md @@ -0,0 +1,30 @@ +# Подсказка + +У события клика есть координаты. Проверьте по ним, попал ли клик на заголовок. + +Самый глубокий узел на координатах можно получить вызовом [document.elementFromPoint(clientX, clientY)](https://developer.mozilla.org/en/DOM/document.elementFromPoint). + +...Но заголовок является текстовым узлом, поэтому эта функция для него работать не будет. Однако это, всё же, можно обойти. Как? + +# Подсказка 2 + +Можно при клике на `LI` сделать временный `SPAN` и переместить в него текстовый узел-заголовок. + +После этого проверить, попал ли клик в него и вернуть всё как было. + + +[js] +// 1) заворачиваем текстовый узел в SPAN + +// 2) проверяем +var elem = document.elementFromPoint(e.clientX, e.clientY); +var isClickOnTitle = (elem == span); + +// 3) возвращаем текстовый узел обратно из SPAN +[/js] + +На шаге 3 текстовый узел вынимается обратно из `SPAN`, всё возвращается в исходное состояние. + +# Решение + +[edit src="solution"]Открыть в песочнице[/edit] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/02-unselectable.md b/tutorial/02-ui/03-event-details/02-unselectable.md new file mode 100644 index 000000000..d2a0044e2 --- /dev/null +++ b/tutorial/02-ui/03-event-details/02-unselectable.md @@ -0,0 +1,184 @@ +# Мышь: отмена выделения, невыделяемые элементы + +В ряде случаев, например, когда мы хотим обработать клик или двойной клик, браузер также добавляет "от себя" выделение текста, на котором кликнули. + +Это некрасиво и неудобно. В этой главе мы рассмотрим основные способы, как делать элемент невыделяемым. В том числе и такие, которые применимы не только к событиям мыши. + +[cut] +## Способ 1: отмена `mousedown/selectstart` + +Проблема: браузер выделяет текст при движении мышью с зажатой левой кнопкой , а также при двойном клике на элемент. Даже там, где это не нужно. + +Для примера -- попробуйте сделать двойной клик на элементе ниже. + +[html autorun height=auto] +Текст +[/html] + +Обработчик сработает. Но побочным эффектом является *выделение текста браузером*. + +**Чтобы избежать выделения, мы должны предотвратить действие браузера по умолчанию для события [selectstart](http://msdn.microsoft.com/en-us/library/ms536969%28VS.85%29.aspx) в IE и `mousedown` в других браузерах.** + +Полный код элемента, который обрабатывает двойной клик без выделения: + +[html autorun height=auto] +
+ Двойной клик сюда выведет "Тест", без выделения +
+[/html] + +При установке на родителя -- все его потомки станут невыделяемыми: + +[html autorun] +Элементы списка не выделяются при клике: + +[/html] + +[smart header="Выделение, всё же, возможно"] +Отмена действия браузера при `mousedown/selectstart` отменяет выделение при клике, но не запрещает его полностью. + +Если пользователь всё же хочет выделить текстовое содержимое элемента, то он может сделать это. + +Достаточно начать выделение (зажать кнопку мыши) не на самом элементе, а рядом с ним. Ведь там отмены не произойдёт, выделение начнётся, и дальше можно передвинуть мышь уже на элемент. +[/smart] + +## Способ 2: снятие выделения пост-фактум + +Вместо *предотвращения* выделения, можно его снять в обработчике события, *после* того, как оно уже произошло. + +Для этого мы используем методы работы с выделением, которые описаны в отдельной главе [](article:563). Здесь нам понадобится всего лишь одна функция `clearSelection`, которая будет снимать выделение. + +Например, попробуйте двойной клик на этот элемент списка: + +[html autorun height=auto] + + + +[/html] + +У этого подхода есть две особенности: + + + + + +## Способ 3: свойство `user-select` + +**Существует нестандартное CSS-свойство `user-select`, которые делает элемент невыделяемым.** + +Оно когда-то планировалось в стандарте CSS3, потом от него отказались, но поддержка в браузерах уже была сделана и потому осталась. + +Это свойство работает (с префиксом) везде, кроме IE9-: + +[html autorun height=auto] + + +Строка до.. +
+ Этот текст нельзя выделить (кроме IE9-) +
+.. Строка после +[/html] + +Читайте на эту тему также [Controlling Selection with CSS user-select](http://blogs.msdn.com/b/ie/archive/2012/01/11/controlling-selection-with-css-user-select.aspx). + + +### IE9-: атрибут `unselectable="on"` + +В IE9- нет `user-select`, но есть атрибут [unselectable](http://msdn.microsoft.com/en-us/library/ms534706%28v=vs.85%29.aspx). + +Он отменяет выделение, но у него есть особенности: +
    +
  1. Во-первых, невыделяемость не наследуется. То есть, невыделяемость родителя не делает невыделяемыми детей.
  2. +
  3. Во-вторых, текст, в отличие от `user-select`, всё равно можно выделить, если начать выделение не на самом элементе, а рядом с ним.
  4. +
+ + +[html] +
+ Этот текст невыделяем в IE, кроме дочерних элементов +
+[/html] +В действии: +
+ Этот текст невыделяем в IE, кроме дочерних элементов +
+ +Левая часть текста в IE не выделяется при двойном клике. Правую часть (`em`) можно выделить, т.к. на ней нет атрибута `unselectable`. + + +## Итого + +Для отмены выделения есть несколько способов: + +
    +
  1. CSS-свойство `user-select` -- везде кроме IE9- (нужен префикс, нестандарт).
  2. +
  3. Атрибут `unselectable="on"` -- работает для любых IE (должен быть у всех потомков)
  4. +
  5. Отмена действий на `mousedown` и `selectstart`: +[js] +elem.onmousedown = elem.onselectstart = function() { + return false; +}; +[/js] +
  6. +
  7. Отмена выделения пост-фактум через функцию `clearSelection()`, описанную выше.
  8. +
+ +Какой же способ выбирать? + +Это зависит от задач и вашего удобства, а также конкретного случая. Все описанные способы работают. Обычно через JavaScript -- наиболее просто и кросс-браузерно. + +**В любом случае эти способы не предназначены для защиты от выделения-и-копирования.** + +Если уж хочется запретить копирование -- можно использовать событие `oncopy`: + +[html autorun] +
+ Уважаемый копирователь, + почему-то автор хочет заставить вас покопаться в исходном коде этой страницы. + Если вы знаете JS или HTML, то скопировать текст не составит для вас проблемы, + ну а если нет, то увы... +
+[/html] + + +[head] + + + + +[/head] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave.md b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave.md new file mode 100644 index 000000000..3517ebb56 --- /dev/null +++ b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave.md @@ -0,0 +1,192 @@ +# Мышь: движение mouseover/out, mouseenter/leave + +В этой главе мы рассмотрим события, возникающие при движении мыши над элементами. +[cut] +## События `mouseover/mouseout`, свойство `relatedTarget` + +**Событие `mouseover` происходит, когда мышь появляется над элементом, а `mouseout` -- когда уходит из него.** + +При этом мы можем узнать, с какого элемента пришла (или на какой ушла) мышь, используя дополнительное свойство `relatedTarget`. + +В случае `mouseover` оно содержит элемент, *с которого* пришла мышь, а для `mouseout` -- *на который* ушла. + +В примере ниже вы можете наглядно посмотреть события `mouseover/out`, возникающие на всех элементах. + +[example src="tutorial/browser/events/mouseoverout" height=220] + +Можно заметить, что в некоторых случаях значение `relatedTarget` может быть `null`. Это вполне нормально и означает, что мышь пришла из-за пределов окна (или ушла за окно). + +## Частота событий `mousemove` и `mouseover/out` + +**Событие `mousemove` срабатывает при передвижении мыши. Но это не значит, что каждый пиксель экрана порождает отдельное событие!** + +События `mousemove` и `mouseover/mouseout` срабатывают так часто, насколько это позволяет внутренняя система взаимодействия с мышью браузера. + +Это означает, что если вы двигаете мышью очень быстро, то DOM-элементы, через которые мышь проходит на большой скорости, могут быть пропущены. + +К примеру: + + +Попробуйте увидеть это "вживую" на тестовом стенде ниже. + +Его HTML представляет собой два вложенных `div'а`. + +Молниеносно проведите мышью над вложенными элементами. При этом может не быть ни одного события или их получит только красный `div`, а может быть только зеленый. + +А еще попробуйте зайти курсором мыши на красный `div` и потом быстро вывести мышь из него куда-нибудь сквозь зеленый. Если движение мыши достаточно быстрое, то родительский элемент будет проигнорирован. + +[example src="tutorial/browser/events/mouseoverout-fast"] + + +Важно иметь в виду эту особенность событий, чтобы не написать код, который рассчитан на последовательный проход над элементами. +## "Лишний" `mouseout` при уходе на потомка + +Представьте ситуацию -- курсор зашёл на элемент. Сработал `mouseover` на нём. Потом курсор идёт на дочерний... И, оказывается, на элементе-родителе при этом происходит `mouseout`! Как будто курсор с него ушёл, хотя он всего лишь перешёл на потомка. + +**При переходе на потомка срабатывает `mouseout` на родителе.** + +Это кажется странным, но легко объяснимо. + +**Согласно браузерной логике, курсор мыши может быть только над *одним* элементом -- самым глубоким в DOM (и верхним по z-index).** + +Так что если он перешел куда-нибудь, то автоматически ушёл с предыдущего элемента. Всё просто. + +К чему это приводит на практике, можно увидеть в примере ниже. В нём красный `div` вложен в синий. На синем стоит обработчик, который записывает его `mouseover/mouseout`. + +Зайдите на синий элемент, а потом переведите мышь на красный -- и наблюдайте за событиями: + +[example src="tutorial/browser/events/mouseoverout-child"] + +
    +
  1. При заходе на синий -- на нём сработает `mouseover [target: blue]`.
  2. +
  3. При переходе с синего на красный -- будет `mouseout [target: blue]` -- уход с родителя.
  4. +
  5. ...И тут же `mouseover [target: red]` -- как ни странно, "обратный переход" на родителя.
  6. +
+ +На самом деле, обратного перехода нет. Событие `mouseover` сработало на потомке (видно по `target: red`), а затем всплыло. + +**То есть, у кода создаётся впечатление, что курсор ушёл `mouseout` с родителя, а затем тут же перешёл `mouseover` на него (за счёт всплытия `mouseover` с потомка).** + +Как это влияет на его поведение? + +Если действия при наведении и уходе курсора с родителя простые, например скрытие/показ подсказки, то можно вообще ничего не заметить. Ведь события происходят одновременно, подсказка будет скрыта по `mouseout` и тут же показана по `mouseover`. + +Если же происходит что-то более сложное, то бывает важно отследить момент "настоящего" ухода, то есть понять, когда элемент зашёл на родителя, а когда ушёл -- без учёта переходов по дочерним элементам. + +Для этого можно использовать события `mouseenter/mouseleave`, которые мы рассмотрим далее. + +## События `mouseenter` и `mouseleave` + +События `mouseenter/mouseleave` похожи на `mouseover/mouseout`. Они тоже срабатывают, когда курсор заходит на элемент и уходит с него, но с двумя отличиями. + +
    +
  1. При переходе на потомка курсор не уходит с родителя.
  2. +
  3. События `mouseenter/mouseleave` не всплывают.
  4. +
+ +Эти события более интуитивно понятны. Курсор заходит на элемент -- срабатывает `mouseenter`, а затем -- неважно, куда он внутри него переходит, `mouseleave` будет, когда курсор окажется за пределами элемента. + +Вы можете увидеть, как они работают проведя курсором над голубым `DIV'ом` ниже. Обработчик стоит только на внешнем, синем элементе. Обратите внимание -- лишних событий при переходе на красного потомка нет! + +[example src="tutorial/browser/events/mouseleave"] + +## Делегирование -- проблема `mouseenter/leave` + +События `mouseenter/leave` более наглядны и понятны, но они не всплывают, а значит с ними нельзя использовать делегирование. + +Представьте себе, что нам нужно обработать вход/выход мыши для ячеек таблицы. А в таблице таких ячеек тысяча. + +Естественное решение -- поставить обработчик на верхний элемент `` и ловить все события в нём. Но события `mouseenter/leave` не всплывают, а срабатывают именно на том элементе, на котором стоит обработчик и только на нём. + +Это легко видеть в примере ниже: обработчики `mouseenter/leave` стоят на `
` и сработают при входе-выходе из таблицы, получить из них какую-то информацию о переходах по её ячейкам не представляется возможным: + +[example src="tutorial/browser/events/mouseleave-table"] + +Не беда -- воспользуемся `mouseover/mouseout`. + +Но мы хотели бы, чтобы наши действия выполнялись только при входе-выходе в ячейку, без учета переходов внутри самих ячеек. + +Если нужно делегирование -- нужно использовать `mouseover/out`. + +Получится так: + +[example src="tutorial/browser/events/mouseenter-mouseleave-delegation"] + +В этом примере код обработчиков выглядит так: +[js] +table.onmouseover = function(event) { + var target = event.target; + target.style.background = 'pink'; +}; + +table.onmouseout = function(event) { + var target = event.target; + target.style.background = ''; +}; +[/js] + +Пока что они срабатывают на всём подряд. Их нужно фильтровать: +'); } } - + pagesList.find('.pager__pages').width(pagesWidth); pager.empty(); pager.append(pagesList); } - + function initScroll() { if (pagesWidth > pagerWidth) { pager.append($('
')); @@ -934,7 +969,7 @@ function getRandomIdentifier(prefix) { }); } } - + function positionCurrent() { currentPageElem = pager.find('.pager__page-link_current'); scrollbar = pager.find('.pager__scroll'); @@ -946,7 +981,7 @@ function getRandomIdentifier(prefix) { scrollHandle.css('left', -1 * currentElemOffset * pagerWidth / pagesWidth); } } - + function addPageByPage() { var pageByPage = $('
'+ '
Ctrl +
' + @@ -954,9 +989,9 @@ function getRandomIdentifier(prefix) { '
Ctrl +
' + '
'); var next = null, prev = null; - + pageByPage.find('.pager__numberofpages').text(pagesNumber + ' ' + getNumEnding(pagesNumber, ['страница', 'страницы', 'страниц'])); - + if (currentPage == 1) { $('Предыдущая страница') .insertBefore(pageByPage.find('.pager__shortcut_prev kbd')); @@ -965,7 +1000,7 @@ function getRandomIdentifier(prefix) { $('Предыдущая страница') .insertBefore(pageByPage.find('.pager__shortcut_prev kbd')); } - + if (currentPage == pagesNumber) { $('Следующая страница') .insertBefore(pageByPage.find('.pager__shortcut_next kbd')); @@ -974,7 +1009,7 @@ function getRandomIdentifier(prefix) { $('Следующая страница') .insertBefore(pageByPage.find('.pager__shortcut_next kbd')); } - + $(document).keydown(function (e) { if (e.ctrlKey || e.metaKey) { switch (e.keyCode) { @@ -987,16 +1022,19 @@ function getRandomIdentifier(prefix) { } } }); - + pager.append(pageByPage); } - + initPages(); initScroll(); positionCurrent(); addPageByPage(); }); - + + + // страница результатов поиска по сайту + // sticky результат поиска сверху $('.search-query').each(function() { var queryForm = $(this); var mainContainer = $('.main'); // used for size and position calculations @@ -1004,7 +1042,7 @@ function getRandomIdentifier(prefix) { var fixedForm = queryForm.find('.search-query__wrap_fixed'); var fixedFormTopPadding = parseInt(fixedForm.find('.search-query__input-wrap').css('paddingTop')); var jqWindow = $(window); - + function updateFixedForm() { fixedForm.css({ 'left': mainContainer.offset().left - jqWindow.scrollLeft(), @@ -1013,15 +1051,15 @@ function getRandomIdentifier(prefix) { 'padding-right': mainContainer.css('paddingRight') }) } - + function syncRegularToFixed() { fixedForm.find('.search-query__input').val(regularForm.find('.search-query__input').val()); } - + function syncFixedToRegular() { regularForm.find('.search-query__input').val(fixedForm.find('.search-query__input').val()); } - + if (fixedForm.length > 0) { $('.main').append(fixedForm); jqWindow.scroll(function() { @@ -1046,7 +1084,10 @@ function getRandomIdentifier(prefix) { }); } }); - + + // блок кода с табами на разные файлы + // недоделан + /////////////////////////////////////////////////////////////// $('.complex-code.tabs_inited').each(function() { var root = $(this); var link = root.data('link'); @@ -1054,7 +1095,7 @@ function getRandomIdentifier(prefix) { for ( ; $('.' + uniqueClass).length > 0 ; ) { uniqueClass = getRandomIdentifier('complex-code_'); } - + root.addClass(uniqueClass); root.find('.tabs__switches') .wrap('
'); @@ -1074,14 +1115,14 @@ function getRandomIdentifier(prefix) { } initDropdowns(); }); - + $('.complex-code.tabs_inited').first().each(function() { $(document).on('click.complex-code', '.complex-code__dropdown .tabs__switch-control', function() { var jqComplexCodeDropdownItem = $(this); var jqComplexCodeDropdown = jqComplexCodeDropdownItem.parents('.complex-code__dropdown'); var jqComplexCode = $('.' + jqComplexCodeDropdown.data('parent')); var index = jqComplexCodeDropdown.find('.tabs__switch-control').index(jqComplexCodeDropdownItem); - + jqComplexCode.find('.tabs__tab').removeClass('tabs__tab_current') .eq(index) .addClass('tabs__tab_current'); @@ -1091,14 +1132,15 @@ function getRandomIdentifier(prefix) { closeAllDropdowns(); }); }); - + + // подсветка текущего раздела в сайдбаре, при скролле // run initialization once if there is at least one block, // all blocks are inited at once $('.page-contents').first().each(function() { var fadeTimeout; var headers = $('.main > h2 > a[href^=\'#\']'); var currentHeader, jqCurrentHeader, i; - + $(window).off('.pageContents'); // turbolinks $(window).on('scroll.pageContents', function() { $('.page-contents.fixed:not(.invisible):not(.page-contents_fading):not(.page-contents_faded)').each(function() { @@ -1109,7 +1151,7 @@ function getRandomIdentifier(prefix) { }) }, 10 * 1000); }); - + $('.page-contents.fixed.invisible.page-contents_fading, .page-contents.fixed.invisible.page-contents_faded').each(function() { clearTimeout(fadeTimeout); $(this).removeClass('page-contents_fading page-contents_faded').css({ 'opacity': 1 }); @@ -1133,7 +1175,7 @@ function getRandomIdentifier(prefix) { } } }); - + $('.sidebar').off('.pageContents'); // turbolinks $('.sidebar').on('mouseenter.pageContents mouseleave.pageContents', '.page-contents.fixed.page-contents_faded', function(e) { var fixedContents = $(this); diff --git a/app/js/head.js b/app/js/head.js index efdc3a8d4..aa99e9e2f 100644 --- a/app/js/head.js +++ b/app/js/head.js @@ -1,6 +1,6 @@ +require('./polyfill'); -/* File to be loaded at the top of the page */ -/* No jQuery here */ - -require('./hi'); - +document.on('click', 'a', function(e) { + alert('ok'); + e.preventDefault(); +}); diff --git a/app/js/main.js b/app/js/main.js index 6461764ac..373d16cc1 100644 --- a/app/js/main.js +++ b/app/js/main.js @@ -1,5 +1,6 @@ var hi = require('./hi'); +require('jquery'); hi(); //window.$ = require('jquery'); diff --git a/app/js/polyfill/index.js b/app/js/polyfill/index.js new file mode 100644 index 000000000..18a239889 --- /dev/null +++ b/app/js/polyfill/index.js @@ -0,0 +1,2 @@ +require('./matches'); +require('./on'); diff --git a/app/js/polyfill/matches.js b/app/js/polyfill/matches.js new file mode 100644 index 000000000..7d2a3bcaf --- /dev/null +++ b/app/js/polyfill/matches.js @@ -0,0 +1,3 @@ +if (!Element.prototype.matches) { + Element.prototype.matches = Element.prototype.matchesSelector || Element.prototype.msMatchesSelector; +} diff --git a/app/js/polyfill/on.js b/app/js/polyfill/on.js new file mode 100644 index 000000000..a569accc1 --- /dev/null +++ b/app/js/polyfill/on.js @@ -0,0 +1,40 @@ +require('./matches'); + +function findDelegateTarget(event, selector) { + var currentNode = event.target; + + while (currentNode) { + if (currentNode.matches(selector)) { + return currentNode; + } + + if (currentNode != event.currentTarget) { + currentNode = currentNode.parentElement; + } + } + return null; +} + +// IE doesn't have EventTarget, corresponding methods are in Node +var prototype = (window.EventTarget || Node).prototype; + + +prototype.on = function(eventName, selector, handler) { + this.addEventListener(eventName, function(event) { + var found = findDelegateTarget(event, selector); + + // currentTarget is read only, I can not fix it + // Object.create wrapper would break event.preventDefault() + // so, keep in mind: + // --> event.currentTarget is top-level element! + + event.delegateTarget = event.currentTarget; // for copat. with jQuery + if (found) { + handler.call(found, event); + } + }); +}; + +prototype.off = function() { + throw new Error("Not implemented (you need it? write an issue)"); +}; diff --git a/app/js/vendor.js b/app/js/vendor.js deleted file mode 100644 index ab48c02b2..000000000 --- a/app/js/vendor.js +++ /dev/null @@ -1 +0,0 @@ -/* @see task for the list of vendor js */ diff --git a/gulp b/gulp index a448229ad..cae819387 100644 --- a/gulp +++ b/gulp @@ -1,2 +1,2 @@ #!/bin/bash -NODE_ENV=development NODE_PATH=. node --harmony `which gulp` $* +NODE_ENV=development node --harmony `which gulp` $* diff --git a/gulp-debug b/gulp-debug new file mode 100644 index 000000000..51118e6c0 --- /dev/null +++ b/gulp-debug @@ -0,0 +1,2 @@ +#!/bin/bash +NODE_ENV=development node --debug-brk --harmony `which gulp` $* diff --git a/gulpfile.js b/gulpfile.js index abe4bfb90..6e9457bc9 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -9,110 +9,80 @@ const path = require('path'); const fs = require('fs'); const assert = require('assert'); +const development = (process.env.NODE_ENV == 'development'); const serverSources = [ 'hmvc/**/*.js', 'modules/**/*.js', 'tasks/**/*.js', '*.js' ]; -gulp.task('lint', function() { - return gp.jshintCache({ src: serverSources }).apply(this, arguments); -}); - -gulp.task('lint-or-die', function() { - return gp.jshintCache({ src: serverSources, dieOnError: true }).apply(this, arguments); -}); - -gulp.task('lint-watch', ['lint'], function(neverCalled) { - gulp.watch(serverSources, ['lint']); -}); - -// usage: gulp loaddb --db fixture/db -gulp.task('loaddb', function(callback) { - var task = require('tasks/loadDb'); - - var args = require('yargs') - .usage("Path to DB is required.") - .demand(['db']) - .argv; +function lazyRequireTask(name) { + var args = [].slice.call(arguments, 1); - var dbPath = path.join(__dirname, args.db); - assert(fs.existsSync(dbPath)); + return function(callback) { + var task = require('./tasks/' + name).apply(this, args); - task(dbPath)(callback); -}); - -gulp.task("watch:server", function() { - - gp.supervisor("bin/www", { - args: [], - watch: ['hmvc', 'modules'], - pollInterval: 100, - extensions: [ "js" ], - debug: true, - harmony: true - }); -}); + return task(callback); + }; +} +gulp.task('lint-once', lazyRequireTask('lintOnce', { src: serverSources })); -gulp.task("watch:app:resources", ['stylus'], function() { +gulp.task('lint-or-die', lazyRequireTask('lintOnce', { src: serverSources, dieOnError: true })); +gulp.task('lint', ['lint-once'], lazyRequireTask('lint', {src: serverSources})); - const fse = require('fs-extra'); +// usage: gulp loaddb --db fixture/db +gulp.task('loaddb', lazyRequireTask('loadDb')); - fse.removeSync('www/fonts'); - fse.removeSync('www/img'); +gulp.task("supervisor", ['link-modules'], lazyRequireTask('supervisor', { cmd: "bin/www", watch: ["hmvc", "modules"] })); - fse.mkdirsSync('www/fonts'); - fse.mkdirsSync('www/img'); - gp.dirSync('app/fonts', 'www/fonts'); - gp.dirSync('app/img', 'www/img'); +gulp.task('link-modules', lazyRequireTask('linkModules', { src: ['modules/*', 'hmvc/*'] })); - gulp.watch("app/**/*.sprites/**", ['sprite']); - gulp.watch("app/**/*.styl", ['stylus']); -}); +gulp.task("app:sync-resources", lazyRequireTask('syncResources', { + 'app/fonts' : 'www/fonts', + 'app/img': 'www/img' +})); +gulp.task('app:sprite-once', lazyRequireTask('spriteOnce', { + spritesSearchFsRoot: 'app', + spritesWebRoot: '/sprites', + spritesFsDir: 'www/sprites', + styleFsDir: 'app/stylesheets/sprites' +})); +gulp.task('app:sprite', ['app:sprite-once'], lazyRequireTask('sprite', { watch: "app/**/*.sprite/**"})); -gulp.task("app:browserify:clean", function() { - const fse = require('fs-extra'); - fse.removeSync('www/js'); - fse.mkdirsSync('www/js'); +gulp.task('app:clean-compiled-css', function(callback) { + fs.unlink('./www/stylesheets/base.css', callback); }); +// Show errors if encountered +gulp.task('app:compile-css-once', + ['app:clean-compiled-css'], + lazyRequireTask('compileCssOnce', { + src: './app/stylesheets/base.styl', + dst: './www/stylesheets' + }) +); -gulp.task("watch:app:browserify", ['app:browserify:clean'], function(neverCalled) { +gulp.task('app:compile-css', ['app:compile-css-once'], lazyRequireTask('compileCss', { watch: "app/**/*.styl"})); - const browserify = require('./tasks/browserify'); - browserify(); -}); +gulp.task("app:browserify:clean", lazyRequireTask('browserifyClean', { dst: './www/js'} )); -gulp.task("watch:link-modules", function() { - gulp.watch(serverSources, ['link-modules']); -}); -gulp.task('dev', ['watch:server', 'watch:app:resources']); +gulp.task("app:browserify", ['app:browserify:clean'], lazyRequireTask('browserify')); -// Show errors if encountered -gulp.task('stylus', ['clean-compiled-css', 'sprite'], function() { - return gulp.src('./app/stylesheets/base.styl') - // without plumber if stylus emits PluginError, it will disappear at the next step - // plumber propagates it down the chain - .pipe(gp.plumber({errorHandler: gp.notify.onError("<%= error.message %>")})) - .pipe(gp.stylus({use: [require('nib')()]})) - .pipe(gp.autoprefixer("last 1 version")) - .pipe(gulp.dest('./www/stylesheets')) - .pipe(gp.livereload()); -}); -gulp.task('clean-compiled-css', function() { - return gulp.src('./www/stylesheets/base.css').pipe(gp.rimraf()); -}); +// compile-css and sprites are independant tasks +// run both or run *-once separately +gulp.task('run', ['supervisor', "app:sync-resources", 'app:compile-css', 'app:sprite', 'app:browserify']); +// TODO: refactor me out! gulp.task('import', function(callback) { const mongoose = require('config/mongoose'); const taskImport = require('tutorial/tasks/import'); @@ -126,21 +96,3 @@ gulp.task('import', function(callback) { callback.apply(null, arguments); }); }); - -gulp.task('link-modules', function() { - const linkModules = require('./tasks/linkModules'); - - return linkModules(['modules/*', 'hmvc/*']).apply(this, arguments); -}); - - -gulp.task('sprite', function() { - var options = { - spritesSearchFsRoot: 'app', - spritesWebRoot: '/img', - spritesFsDir: 'www/img', - styleFsDir: 'app/stylesheets/sprites' - }; - - return gp.stylusSprite(options).apply(this, arguments); -}); diff --git a/package.json b/package.json index 9ee03d6f5..2aaf7fc20 100755 --- a/package.json +++ b/package.json @@ -4,10 +4,9 @@ "private": true, "scripts": { "prod": "NODE_ENV=production node --harmony ./bin/www", - "dev": "./gulp link-modules && NODE_ENV=development supervisor --harmony --debug --ignore node_modules ./bin/www", + "dev": "NODE_ENV=development supervisor --harmony --debug --ignore node_modules ./bin/www", "debug": "NODE_ENV=development supervisor --harmony --debug-brk --ignore node_modules ./bin/www", "test": "NODE_ENV=test supervisor --harmony --debug --ignore node_modules ./bin/www", - "postinstall": "NODE_ENV=development node --harmony `which gulp` link-modules", "fixperms": "sudo chown -R `id -u` . ~/.npm* ~/.node-gyp" }, "precommit": "NODE_ENV=development node --harmony `which gulp` pre-commit", diff --git a/tasks/browserify.js b/tasks/browserify.js index cb80dc967..7269b1941 100644 --- a/tasks/browserify.js +++ b/tasks/browserify.js @@ -9,61 +9,77 @@ var assert = require('assert'); var _ = require('lodash'); var path = require('path'); +// TODO: add uglify if not development function makeBundler(options) { - // dst has same name as (single) src + // dst has same name as (single) src var opts = _.assign({}, options, { - debug: (process.env.NODE_ENV === 'development') + debug: (process.env.NODE_ENV === 'development'), + cache: {}, + packageCache: {}, + fullPaths: true }); var bundler = browserify(opts); bundler.rebundle = function() { - console.log(path.basename(this._options.dst)); - bundler.bundle() + this.bundle() + .on('error', function(e) { + gutil.log(e.message); + new Notification().notify({ + message: e + }); + }) .pipe(source(path.basename(this._options.dst))) .pipe(gulp.dest(path.dirname(this._options.dst))); }; + bundler.on('update', bundler.rebundle); + bundler.on('log', function(msg) { + gutil.log("browserify: " + msg); + }); - // bundler.on('update', bundler.rebundle); + if (options.externals) { + for (var i = 0; i < options.externals.length; i++) { + var external = options.externals[i]; + bundler.external(external); + } + } + + if (process.env.NODE_ENV == 'development') { + bundler = watchify(bundler); + } return bundler; } module.exports = function() { -/* - var externals = ['jquery']; - var bundler = makeBundler({ - src: './app/js/vendor.js', - dst: './www/js/vendor.js', - require: externals - }); + return function(callback) { - bundler = watchify(bundler); - bundler.rebundle(); + var vendor = ['jquery']; - var bundler = makeBundler({ - src: './app/js/head.js', - dst: './www/js/head.js' - }); + var bundler = makeBundler({ + entries: [], + dst: './www/js/vendor.js', + require: vendor + }); - bundler = watchify(bundler); - bundler.rebundle(); + bundler.rebundle(); + + var bundler = makeBundler({ + entries: './app/js/head.js', + dst: './www/js/head.js' + }); + + bundler.rebundle(); - */ - var bundler = makeBundler({ - src: './app/js/main.js', - dst: './www/js/main.js' - }); - bundler.rebundle(); - - console.log("TEST"); -/* - bundler.on('prebundle', function(bundle) { - for (var i = 0; i < externals.length; i++) { - var external = externals[i]; - this.external(external); - } - });*/ + var bundler = makeBundler({ + entries: './app/js/main.js', + dst: './www/js/main.js', + externals: vendor + }); + + + bundler.rebundle(); + }; }; diff --git a/tasks/browserifyClean.js b/tasks/browserifyClean.js new file mode 100644 index 000000000..f27e68a69 --- /dev/null +++ b/tasks/browserifyClean.js @@ -0,0 +1,11 @@ +const fse = require('fs-extra'); + +module.exports = function(options) { + + return function(callback) { + fse.removeSync(options.dst); + fse.mkdirsSync(options.dst); + callback(); + }; +}; + diff --git a/tasks/compileCss.js b/tasks/compileCss.js new file mode 100644 index 000000000..49fcfbdbb --- /dev/null +++ b/tasks/compileCss.js @@ -0,0 +1,12 @@ + +const gulp = require('gulp'); + +module.exports = function(options) { + return function(callback) { + if (process.env.NODE_ENV == 'development') { + gulp.watch(options.watch, ['app:compile-css-once']); + } else { + callback(); + } + }; +}; diff --git a/tasks/compileCssOnce.js b/tasks/compileCssOnce.js new file mode 100644 index 000000000..d09c5aeb9 --- /dev/null +++ b/tasks/compileCssOnce.js @@ -0,0 +1,19 @@ +const gulp = require('gulp'); +const gp = require('gulp-load-plugins')(); + +module.exports = function(options) { + + return function() { + + return gulp.src(options.src) + // without plumber if stylus emits PluginError, it will disappear at the next step + // plumber propagates it down the chain + .pipe(gp.plumber({errorHandler: gp.notify.onError("<%= error.message %>")})) + .pipe(gp.stylus({use: [require('nib')()]})) + .pipe(gp.autoprefixer("last 1 version")) + .pipe(gulp.dest(options.dst)); + }; + + +}; + diff --git a/tasks/linkModules.js b/tasks/linkModules.js index ebf2a9d06..2d7f3aacf 100644 --- a/tasks/linkModules.js +++ b/tasks/linkModules.js @@ -31,11 +31,11 @@ function ensureSymlinkSync(linkSrc, linkDst) { return true; } -module.exports = function(sources) { +module.exports = function(options) { - return function() { + return function(callback) { var modules = []; - sources.forEach(function(pattern) { + options.src.forEach(function(pattern) { modules = modules.concat(glob.sync(pattern)); }); @@ -48,6 +48,8 @@ module.exports = function(sources) { gutil.log(linkSrc + " -> " + linkDst); } } + callback(); }; }; + diff --git a/tasks/lint.js b/tasks/lint.js new file mode 100644 index 000000000..56f200236 --- /dev/null +++ b/tasks/lint.js @@ -0,0 +1,12 @@ + +const gulp = require('gulp'); + +module.exports = function(options) { + return function(callback) { + if (process.env.NODE_ENV == 'development') { + gulp.watch(options.src, ['lint']); + } else { + callback(); + } + }; +}; diff --git a/tasks/lintOnce.js b/tasks/lintOnce.js new file mode 100644 index 000000000..768fb5ac7 --- /dev/null +++ b/tasks/lintOnce.js @@ -0,0 +1,8 @@ +const gp = require('gulp-load-plugins')(); + +module.exports = function(options) { + + return function(callback) { + return gp.jshintCache(options).apply(this, arguments); + }; +}; diff --git a/tasks/loadDb.js b/tasks/loadDb.js index be90fb671..51a8361bd 100644 --- a/tasks/loadDb.js +++ b/tasks/loadDb.js @@ -5,10 +5,16 @@ var gutil = require('gulp-util'); var dataUtil = require('lib/dataUtil'); var mongoose = require('config/mongoose'); -module.exports = function(dbPath) { - +module.exports = function() { return function(callback) { + var args = require('yargs') + .usage("Path to DB is required.") + .demand(['db']) + .argv; + + var dbPath = path.join(process.cwd(), args.db); + gutil.log("loading db " + dbPath); co(function*() { @@ -23,5 +29,5 @@ module.exports = function(dbPath) { }); }; - }; + diff --git a/tasks/sprite.js b/tasks/sprite.js new file mode 100644 index 000000000..c3b41da60 --- /dev/null +++ b/tasks/sprite.js @@ -0,0 +1,12 @@ + +const gulp = require('gulp'); + +module.exports = function(options) { + return function(callback) { + if (process.env.NODE_ENV == 'development') { + gulp.watch(options.watch, ['app:sprite']); + } else { + callback(); + } + }; +}; diff --git a/tasks/spriteOnce.js b/tasks/spriteOnce.js new file mode 100644 index 000000000..549adf4af --- /dev/null +++ b/tasks/spriteOnce.js @@ -0,0 +1,9 @@ +const gp = require('gulp-load-plugins')(); + +module.exports = function(options) { + + return function(callback) { + return gp.stylusSprite(options).apply(this, arguments); + }; +}; + diff --git a/tasks/supervisor.js b/tasks/supervisor.js new file mode 100644 index 000000000..e1c4c50f4 --- /dev/null +++ b/tasks/supervisor.js @@ -0,0 +1,16 @@ + +const gp = require('gulp-load-plugins')(); + +module.exports = function(options) { + + return function(callback) { + gp.supervisor(options.cmd, { + args: [], + watch: options.watch, + pollInterval: 100, + extensions: [ "js" ], + debug: true, + harmony: true + }); + }; +}; diff --git a/tasks/syncResources.js b/tasks/syncResources.js new file mode 100644 index 000000000..d20046a26 --- /dev/null +++ b/tasks/syncResources.js @@ -0,0 +1,23 @@ +const fse = require('fs-extra'); +const gp = require('gulp-load-plugins')(); + +module.exports = function(resources) { + + return function(callback) { + + for (var src in resources) { + var dst = resources[src]; + + fse.removeSync(src); + + if (process.env.NODE_ENV == 'development') { + fse.mkdirsSync(dst); + gp.dirSync(src, dst); + } else { + fse.copySync(src, dst); + } + } + + if (process.env.NODE_ENV != 'development') callback(); + }; +}; diff --git a/templates/blocks/head.jade b/templates/blocks/head.jade index 0306258b7..52240e5db 100644 --- a/templates/blocks/head.jade +++ b/templates/blocks/head.jade @@ -4,7 +4,7 @@ html title= title link(href='http://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700|Open+Sans+Condensed:700&subset=latin,latin-ext,cyrillic,cyrillic-ext' rel='stylesheet' type='text/css') link(href='/stylesheets/base.css',rel='stylesheet') - script(src='/js/build.js') + script(src='/js/head.js') //- link(href='../app/assets/stylesheets/base.css' rel='stylesheet') //if lte IE 9 //- link(href='../app/assets/stylesheets/base.ie.css' rel='stylesheet') From e306ac224534f4922ebb130503a283ffbbebcfe5 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Mon, 28 Jul 2014 17:22:22 +0400 Subject: [PATCH 120/130] client-side clean up --- README.md | 4 +- app/js/head.js | 7 +- app/js/hi.js | 3 - app/js/jquery.fancybox.pack.js | 46 ---------- app/js/login.js | 76 ++++++++++++++++ app/js/main.js | 2 - app/js/polyfill/dom4.js | 79 +++++++++++++++++ app/js/polyfill/index.js | 2 +- app/js/polyfill/matches.js | 3 - app/js/polyfill/on.js | 8 +- app/js/prism-my.js | 111 ------------------------ app/js/prism.js | 1 + app/stylesheets/blocks/login/login.styl | 7 ++ gulpfile.js | 5 +- tasks/syncResources.js | 3 +- templates/blocks/scripts.jade | 13 +-- templates/blocks/top-parts.jade | 3 + 17 files changed, 185 insertions(+), 188 deletions(-) delete mode 100644 app/js/hi.js delete mode 100755 app/js/jquery.fancybox.pack.js create mode 100644 app/js/login.js create mode 100644 app/js/polyfill/dom4.js delete mode 100644 app/js/polyfill/matches.js delete mode 100644 app/js/prism-my.js create mode 100644 app/stylesheets/blocks/login/login.styl diff --git a/README.md b/README.md index a06d1b2da..c2d2bc254 100755 --- a/README.md +++ b/README.md @@ -24,10 +24,8 @@ Также в опен-сорсе - учебник JavaScript. Правда, это в другом репозитарии, здесь только код. -## А чё, здорово! +## ♡ -Вообще, всё не так просто, есть над чем покумекать, но мы стараемся :) - Пишите в issues, если есть о чём. diff --git a/app/js/head.js b/app/js/head.js index aa99e9e2f..51a88b205 100644 --- a/app/js/head.js +++ b/app/js/head.js @@ -1,6 +1,9 @@ require('./polyfill'); -document.on('click', 'a', function(e) { - alert('ok'); +var login = require('./login'); + +document.on('click', 'a.login', function(e) { e.preventDefault(); + + login(); }); diff --git a/app/js/hi.js b/app/js/hi.js deleted file mode 100644 index f8a59e599..000000000 --- a/app/js/hi.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = function() { - alert('TEST'); -}; diff --git a/app/js/jquery.fancybox.pack.js b/app/js/jquery.fancybox.pack.js deleted file mode 100755 index 73f757843..000000000 --- a/app/js/jquery.fancybox.pack.js +++ /dev/null @@ -1,46 +0,0 @@ -/*! fancyBox v2.1.5 fancyapps.com | fancyapps.com/fancybox/#license */ -(function(r,G,f,v){var J=f("html"),n=f(r),p=f(G),b=f.fancybox=function(){b.open.apply(this,arguments)},I=navigator.userAgent.match(/msie/i),B=null,s=G.createTouch!==v,t=function(a){return a&&a.hasOwnProperty&&a instanceof f},q=function(a){return a&&"string"===f.type(a)},E=function(a){return q(a)&&0
',image:'',iframe:'",error:'

The requested content cannot be loaded.
Please try again later.

',closeBtn:'',next:'',prev:''},openEffect:"fade",openSpeed:250,openEasing:"swing",openOpacity:!0, -openMethod:"zoomIn",closeEffect:"fade",closeSpeed:250,closeEasing:"swing",closeOpacity:!0,closeMethod:"zoomOut",nextEffect:"elastic",nextSpeed:250,nextEasing:"swing",nextMethod:"changeIn",prevEffect:"elastic",prevSpeed:250,prevEasing:"swing",prevMethod:"changeOut",helpers:{overlay:!0,title:!0},onCancel:f.noop,beforeLoad:f.noop,afterLoad:f.noop,beforeShow:f.noop,afterShow:f.noop,beforeChange:f.noop,beforeClose:f.noop,afterClose:f.noop},group:{},opts:{},previous:null,coming:null,current:null,isActive:!1, -isOpen:!1,isOpened:!1,wrap:null,skin:null,outer:null,inner:null,player:{timer:null,isActive:!1},ajaxLoad:null,imgPreload:null,transitions:{},helpers:{},open:function(a,d){if(a&&(f.isPlainObject(d)||(d={}),!1!==b.close(!0)))return f.isArray(a)||(a=t(a)?f(a).get():[a]),f.each(a,function(e,c){var k={},g,h,j,m,l;"object"===f.type(c)&&(c.nodeType&&(c=f(c)),t(c)?(k={href:c.data("fancybox-href")||c.attr("href"),title:c.data("fancybox-title")||c.attr("title"),isDom:!0,element:c},f.metadata&&f.extend(!0,k, -c.metadata())):k=c);g=d.href||k.href||(q(c)?c:null);h=d.title!==v?d.title:k.title||"";m=(j=d.content||k.content)?"html":d.type||k.type;!m&&k.isDom&&(m=c.data("fancybox-type"),m||(m=(m=c.prop("class").match(/fancybox\.(\w+)/))?m[1]:null));q(g)&&(m||(b.isImage(g)?m="image":b.isSWF(g)?m="swf":"#"===g.charAt(0)?m="inline":q(c)&&(m="html",j=c)),"ajax"===m&&(l=g.split(/\s+/,2),g=l.shift(),l=l.shift()));j||("inline"===m?g?j=f(q(g)?g.replace(/.*(?=#[^\s]+$)/,""):g):k.isDom&&(j=c):"html"===m?j=g:!m&&(!g&& -k.isDom)&&(m="inline",j=c));f.extend(k,{href:g,type:m,content:j,title:h,selector:l});a[e]=k}),b.opts=f.extend(!0,{},b.defaults,d),d.keys!==v&&(b.opts.keys=d.keys?f.extend({},b.defaults.keys,d.keys):!1),b.group=a,b._start(b.opts.index)},cancel:function(){var a=b.coming;a&&!1!==b.trigger("onCancel")&&(b.hideLoading(),b.ajaxLoad&&b.ajaxLoad.abort(),b.ajaxLoad=null,b.imgPreload&&(b.imgPreload.onload=b.imgPreload.onerror=null),a.wrap&&a.wrap.stop(!0,!0).trigger("onReset").remove(),b.coming=null,b.current|| -b._afterZoomOut(a))},close:function(a){b.cancel();!1!==b.trigger("beforeClose")&&(b.unbindEvents(),b.isActive&&(!b.isOpen||!0===a?(f(".fancybox-wrap").stop(!0).trigger("onReset").remove(),b._afterZoomOut()):(b.isOpen=b.isOpened=!1,b.isClosing=!0,f(".fancybox-item, .fancybox-nav").remove(),b.wrap.stop(!0,!0).removeClass("fancybox-opened"),b.transitions[b.current.closeMethod]())))},play:function(a){var d=function(){clearTimeout(b.player.timer)},e=function(){d();b.current&&b.player.isActive&&(b.player.timer= -setTimeout(b.next,b.current.playSpeed))},c=function(){d();p.unbind(".player");b.player.isActive=!1;b.trigger("onPlayEnd")};if(!0===a||!b.player.isActive&&!1!==a){if(b.current&&(b.current.loop||b.current.index=c.index?"next":"prev"],b.router=e||"jumpto",c.loop&&(0>a&&(a=c.group.length+a%c.group.length),a%=c.group.length),c.group[a]!==v&&(b.cancel(),b._start(a)))},reposition:function(a,d){var e=b.current,c=e?e.wrap:null,k;c&&(k=b._getPosition(d),a&&"scroll"===a.type?(delete k.position,c.stop(!0,!0).animate(k,200)):(c.css(k),e.pos=f.extend({},e.dim,k)))},update:function(a){var d= -a&&a.type,e=!d||"orientationchange"===d;e&&(clearTimeout(B),B=null);b.isOpen&&!B&&(B=setTimeout(function(){var c=b.current;c&&!b.isClosing&&(b.wrap.removeClass("fancybox-tmp"),(e||"load"===d||"resize"===d&&c.autoResize)&&b._setDimension(),"scroll"===d&&c.canShrink||b.reposition(a),b.trigger("onUpdate"),B=null)},e&&!s?0:300))},toggle:function(a){b.isOpen&&(b.current.fitToView="boolean"===f.type(a)?a:!b.current.fitToView,s&&(b.wrap.removeAttr("style").addClass("fancybox-tmp"),b.trigger("onUpdate")), -b.update())},hideLoading:function(){p.unbind(".loading");f("#fancybox-loading").remove()},showLoading:function(){var a,d;b.hideLoading();a=f('
').click(b.cancel).appendTo("body");p.bind("keydown.loading",function(a){if(27===(a.which||a.keyCode))a.preventDefault(),b.cancel()});b.defaults.fixed||(d=b.getViewport(),a.css({position:"absolute",top:0.5*d.h+d.y,left:0.5*d.w+d.x}))},getViewport:function(){var a=b.current&&b.current.locked||!1,d={x:n.scrollLeft(), -y:n.scrollTop()};a?(d.w=a[0].clientWidth,d.h=a[0].clientHeight):(d.w=s&&r.innerWidth?r.innerWidth:n.width(),d.h=s&&r.innerHeight?r.innerHeight:n.height());return d},unbindEvents:function(){b.wrap&&t(b.wrap)&&b.wrap.unbind(".fb");p.unbind(".fb");n.unbind(".fb")},bindEvents:function(){var a=b.current,d;a&&(n.bind("orientationchange.fb"+(s?"":" resize.fb")+(a.autoCenter&&!a.locked?" scroll.fb":""),b.update),(d=a.keys)&&p.bind("keydown.fb",function(e){var c=e.which||e.keyCode,k=e.target||e.srcElement; -if(27===c&&b.coming)return!1;!e.ctrlKey&&(!e.altKey&&!e.shiftKey&&!e.metaKey&&(!k||!k.type&&!f(k).is("[contenteditable]")))&&f.each(d,function(d,k){if(1h[0].clientWidth||h[0].clientHeight&&h[0].scrollHeight>h[0].clientHeight),h=f(h).parent();if(0!==c&&!j&&1g||0>k)b.next(0>g?"up":"right");d.preventDefault()}}))},trigger:function(a,d){var e,c=d||b.coming||b.current;if(c){f.isFunction(c[a])&&(e=c[a].apply(c,Array.prototype.slice.call(arguments,1)));if(!1===e)return!1;c.helpers&&f.each(c.helpers,function(d,e){if(e&&b.helpers[d]&&f.isFunction(b.helpers[d][a]))b.helpers[d][a](f.extend(!0, -{},b.helpers[d].defaults,e),c)});p.trigger(a)}},isImage:function(a){return q(a)&&a.match(/(^data:image\/.*,)|(\.(jp(e|g|eg)|gif|png|bmp|webp|svg)((\?|#).*)?$)/i)},isSWF:function(a){return q(a)&&a.match(/\.(swf)((\?|#).*)?$/i)},_start:function(a){var d={},e,c;a=l(a);e=b.group[a]||null;if(!e)return!1;d=f.extend(!0,{},b.opts,e);e=d.margin;c=d.padding;"number"===f.type(e)&&(d.margin=[e,e,e,e]);"number"===f.type(c)&&(d.padding=[c,c,c,c]);d.modal&&f.extend(!0,d,{closeBtn:!1,closeClick:!1,nextClick:!1,arrows:!1, -mouseWheel:!1,keys:null,helpers:{overlay:{closeClick:!1}}});d.autoSize&&(d.autoWidth=d.autoHeight=!0);"auto"===d.width&&(d.autoWidth=!0);"auto"===d.height&&(d.autoHeight=!0);d.group=b.group;d.index=a;b.coming=d;if(!1===b.trigger("beforeLoad"))b.coming=null;else{c=d.type;e=d.href;if(!c)return b.coming=null,b.current&&b.router&&"jumpto"!==b.router?(b.current.index=a,b[b.router](b.direction)):!1;b.isActive=!0;if("image"===c||"swf"===c)d.autoHeight=d.autoWidth=!1,d.scrolling="visible";"image"===c&&(d.aspectRatio= -!0);"iframe"===c&&s&&(d.scrolling="scroll");d.wrap=f(d.tpl.wrap).addClass("fancybox-"+(s?"mobile":"desktop")+" fancybox-type-"+c+" fancybox-tmp "+d.wrapCSS).appendTo(d.parent||"body");f.extend(d,{skin:f(".fancybox-skin",d.wrap),outer:f(".fancybox-outer",d.wrap),inner:f(".fancybox-inner",d.wrap)});f.each(["Top","Right","Bottom","Left"],function(a,b){d.skin.css("padding"+b,w(d.padding[a]))});b.trigger("onReady");if("inline"===c||"html"===c){if(!d.content||!d.content.length)return b._error("content")}else if(!e)return b._error("href"); -"image"===c?b._loadImage():"ajax"===c?b._loadAjax():"iframe"===c?b._loadIframe():b._afterLoad()}},_error:function(a){f.extend(b.coming,{type:"html",autoWidth:!0,autoHeight:!0,minWidth:0,minHeight:0,scrolling:"no",hasError:a,content:b.coming.tpl.error});b._afterLoad()},_loadImage:function(){var a=b.imgPreload=new Image;a.onload=function(){this.onload=this.onerror=null;b.coming.width=this.width/b.opts.pixelRatio;b.coming.height=this.height/b.opts.pixelRatio;b._afterLoad()};a.onerror=function(){this.onload= -this.onerror=null;b._error("image")};a.src=b.coming.href;!0!==a.complete&&b.showLoading()},_loadAjax:function(){var a=b.coming;b.showLoading();b.ajaxLoad=f.ajax(f.extend({},a.ajax,{url:a.href,error:function(a,e){b.coming&&"abort"!==e?b._error("ajax",a):b.hideLoading()},success:function(d,e){"success"===e&&(a.content=d,b._afterLoad())}}))},_loadIframe:function(){var a=b.coming,d=f(a.tpl.iframe.replace(/\{rnd\}/g,(new Date).getTime())).attr("scrolling",s?"auto":a.iframe.scrolling).attr("src",a.href); -f(a.wrap).bind("onReset",function(){try{f(this).find("iframe").hide().attr("src","//about:blank").end().empty()}catch(a){}});a.iframe.preload&&(b.showLoading(),d.one("load",function(){f(this).data("ready",1);s||f(this).bind("load.fb",b.update);f(this).parents(".fancybox-wrap").width("100%").removeClass("fancybox-tmp").show();b._afterLoad()}));a.content=d.appendTo(a.inner);a.iframe.preload||b._afterLoad()},_preloadImages:function(){var a=b.group,d=b.current,e=a.length,c=d.preload?Math.min(d.preload, -e-1):0,f,g;for(g=1;g<=c;g+=1)f=a[(d.index+g)%e],"image"===f.type&&f.href&&((new Image).src=f.href)},_afterLoad:function(){var a=b.coming,d=b.current,e,c,k,g,h;b.hideLoading();if(a&&!1!==b.isActive)if(!1===b.trigger("afterLoad",a,d))a.wrap.stop(!0).trigger("onReset").remove(),b.coming=null;else{d&&(b.trigger("beforeChange",d),d.wrap.stop(!0).removeClass("fancybox-opened").find(".fancybox-item, .fancybox-nav").remove());b.unbindEvents();e=a.content;c=a.type;k=a.scrolling;f.extend(b,{wrap:a.wrap,skin:a.skin, -outer:a.outer,inner:a.inner,current:a,previous:d});g=a.href;switch(c){case "inline":case "ajax":case "html":a.selector?e=f("
").html(e).find(a.selector):t(e)&&(e.data("fancybox-placeholder")||e.data("fancybox-placeholder",f('
').insertAfter(e).hide()),e=e.show().detach(),a.wrap.bind("onReset",function(){f(this).find(e).length&&e.hide().replaceAll(e.data("fancybox-placeholder")).data("fancybox-placeholder",!1)}));break;case "image":e=a.tpl.image.replace("{href}", -g);break;case "swf":e='',h="",f.each(a.swf,function(a,b){e+='';h+=" "+a+'="'+b+'"'}),e+='"}(!t(e)||!e.parent().is(a.inner))&&a.inner.append(e);b.trigger("beforeShow");a.inner.css("overflow","yes"===k?"scroll": -"no"===k?"hidden":k);b._setDimension();b.reposition();b.isOpen=!1;b.coming=null;b.bindEvents();if(b.isOpened){if(d.prevMethod)b.transitions[d.prevMethod]()}else f(".fancybox-wrap").not(a.wrap).stop(!0).trigger("onReset").remove();b.transitions[b.isOpened?a.nextMethod:a.openMethod]();b._preloadImages()}},_setDimension:function(){var a=b.getViewport(),d=0,e=!1,c=!1,e=b.wrap,k=b.skin,g=b.inner,h=b.current,c=h.width,j=h.height,m=h.minWidth,u=h.minHeight,n=h.maxWidth,p=h.maxHeight,s=h.scrolling,q=h.scrollOutside? -h.scrollbarWidth:0,x=h.margin,y=l(x[1]+x[3]),r=l(x[0]+x[2]),v,z,t,C,A,F,B,D,H;e.add(k).add(g).width("auto").height("auto").removeClass("fancybox-tmp");x=l(k.outerWidth(!0)-k.width());v=l(k.outerHeight(!0)-k.height());z=y+x;t=r+v;C=E(c)?(a.w-z)*l(c)/100:c;A=E(j)?(a.h-t)*l(j)/100:j;if("iframe"===h.type){if(H=h.content,h.autoHeight&&1===H.data("ready"))try{H[0].contentWindow.document.location&&(g.width(C).height(9999),F=H.contents().find("body"),q&&F.css("overflow-x","hidden"),A=F.outerHeight(!0))}catch(G){}}else if(h.autoWidth|| -h.autoHeight)g.addClass("fancybox-tmp"),h.autoWidth||g.width(C),h.autoHeight||g.height(A),h.autoWidth&&(C=g.width()),h.autoHeight&&(A=g.height()),g.removeClass("fancybox-tmp");c=l(C);j=l(A);D=C/A;m=l(E(m)?l(m,"w")-z:m);n=l(E(n)?l(n,"w")-z:n);u=l(E(u)?l(u,"h")-t:u);p=l(E(p)?l(p,"h")-t:p);F=n;B=p;h.fitToView&&(n=Math.min(a.w-z,n),p=Math.min(a.h-t,p));z=a.w-y;r=a.h-r;h.aspectRatio?(c>n&&(c=n,j=l(c/D)),j>p&&(j=p,c=l(j*D)),cz||y>r)&&(c>m&&j>u)&&!(19n&&(c=n,j=l(c/D)),g.width(c).height(j),e.width(c+x),a=e.width(),y=e.height();else c=Math.max(m,Math.min(c,c-(a-z))),j=Math.max(u,Math.min(j,j-(y-r)));q&&("auto"===s&&jz||y>r)&&c>m&&j>u;c=h.aspectRatio?cu&&j
').appendTo(b.coming?b.coming.parent:a.parent);this.fixed=!1;a.fixed&&b.defaults.fixed&&(this.overlay.addClass("fancybox-overlay-fixed"),this.fixed=!0)},open:function(a){var d=this;a=f.extend({},this.defaults,a);this.overlay?this.overlay.unbind(".overlay").width("auto").height("auto"):this.create(a);this.fixed||(n.bind("resize.overlay",f.proxy(this.update,this)),this.update());a.closeClick&&this.overlay.bind("click.overlay",function(a){if(f(a.target).hasClass("fancybox-overlay"))return b.isActive? -b.close():d.close(),!1});this.overlay.css(a.css).show()},close:function(){var a,b;n.unbind("resize.overlay");this.el.hasClass("fancybox-lock")&&(f(".fancybox-margin").removeClass("fancybox-margin"),a=n.scrollTop(),b=n.scrollLeft(),this.el.removeClass("fancybox-lock"),n.scrollTop(a).scrollLeft(b));f(".fancybox-overlay").remove().hide();f.extend(this,{overlay:null,fixed:!1})},update:function(){var a="100%",b;this.overlay.width(a).height("100%");I?(b=Math.max(G.documentElement.offsetWidth,G.body.offsetWidth), -p.width()>b&&(a=p.width())):p.width()>n.width()&&(a=p.width());this.overlay.width(a).height(p.height())},onReady:function(a,b){var e=this.overlay;f(".fancybox-overlay").stop(!0,!0);e||this.create(a);a.locked&&(this.fixed&&b.fixed)&&(e||(this.margin=p.height()>n.height()?f("html").css("margin-right").replace("px",""):!1),b.locked=this.overlay.append(b.wrap),b.fixed=!1);!0===a.showEarly&&this.beforeShow.apply(this,arguments)},beforeShow:function(a,b){var e,c;b.locked&&(!1!==this.margin&&(f("*").filter(function(){return"fixed"=== -f(this).css("position")&&!f(this).hasClass("fancybox-overlay")&&!f(this).hasClass("fancybox-wrap")}).addClass("fancybox-margin"),this.el.addClass("fancybox-margin")),e=n.scrollTop(),c=n.scrollLeft(),this.el.addClass("fancybox-lock"),n.scrollTop(e).scrollLeft(c));this.open(a)},onUpdate:function(){this.fixed||this.update()},afterClose:function(a){this.overlay&&!b.coming&&this.overlay.fadeOut(a.speedOut,f.proxy(this.close,this))}};b.helpers.title={defaults:{type:"float",position:"bottom"},beforeShow:function(a){var d= -b.current,e=d.title,c=a.type;f.isFunction(e)&&(e=e.call(d.element,d));if(q(e)&&""!==f.trim(e)){d=f('
'+e+"
");switch(c){case "inside":c=b.skin;break;case "outside":c=b.wrap;break;case "over":c=b.inner;break;default:c=b.skin,d.appendTo("body"),I&&d.width(d.width()),d.wrapInner(''),b.current.margin[2]+=Math.abs(l(d.css("margin-bottom")))}d["top"===a.position?"prependTo":"appendTo"](c)}}};f.fn.fancybox=function(a){var d, -e=f(this),c=this.selector||"",k=function(g){var h=f(this).blur(),j=d,k,l;!g.ctrlKey&&(!g.altKey&&!g.shiftKey&&!g.metaKey)&&!h.is(".fancybox-wrap")&&(k=a.groupAttr||"data-fancybox-group",l=h.attr(k),l||(k="rel",l=h.get(0)[k]),l&&(""!==l&&"nofollow"!==l)&&(h=c.length?f(c):e,h=h.filter("["+k+'="'+l+'"]'),j=h.index(this)),a.index=j,!1!==b.open(h,a)&&g.preventDefault())};a=a||{};d=a.index||0;!c||!1===a.live?e.unbind("click.fb-start").bind("click.fb-start",k):p.undelegate(c,"click.fb-start").delegate(c+ -":not('.fancybox-item, .fancybox-nav')","click.fb-start",k);this.filter("[data-fancybox-start=1]").trigger("click");return this};p.ready(function(){var a,d;f.scrollbarWidth===v&&(f.scrollbarWidth=function(){var a=f('
').appendTo("body"),b=a.children(),b=b.innerWidth()-b.height(99).innerWidth();a.remove();return b});if(f.support.fixedPosition===v){a=f.support;d=f('
').appendTo("body");var e=20=== -d[0].offsetTop||15===d[0].offsetTop;d.remove();a.fixedPosition=e}f.extend(b.defaults,{scrollbarWidth:f.scrollbarWidth(),fixed:f.support.fixedPosition,parent:f("body")});a=f(r).width();J.addClass("fancybox-lock-test");d=f(r).width();J.removeClass("fancybox-lock-test");f("").appendTo("head")})})(window,document,jQuery); \ No newline at end of file diff --git a/app/js/login.js b/app/js/login.js new file mode 100644 index 000000000..d3d78d0a8 --- /dev/null +++ b/app/js/login.js @@ -0,0 +1,76 @@ + +/* Показать полупрозрачный DIV, затеняющий всю страницу + (а форма будет не в нем, а рядом с ним, чтобы не полупрозрачная) */ +function showCover() { + var coverDiv = document.createElement('div'); + coverDiv.className = 'cover-div'; + document.body.appendChild(coverDiv); +} + +function hideCover() { + document.querySelector('.cover-div').remove(); +} + +// Run like this: +// login() +// login({whyMessage:.. followLinkMessage:..}) +// login({whyMessage:.. followLinkMessage:..}, callback) +module.exports = function(options, callback) { + options = options || {}; + callback = callback || function() { }; + + showCover(); + + // TODO + var container = document.getElementById('prompt-form-container'); + document.getElementById('prompt-message').innerHTML = text; + form.elements.text.value = ''; + + function complete(value) { + hideCover(); + container.style.display = 'none'; + document.onkeydown = null; + callback(value); + } + + form.onsubmit = function() { + var value = form.elements.text.value; + if (value == '') return false; // игнорировать пустой submit + + complete(value); + return false; + }; + + form.elements.cancel.onclick = function() { + complete(null); + }; + + document.onkeydown = function(e) { + e = e || event; + if (e.keyCode == 27) { // escape + complete(null); + } + }; + + var lastElem = form.elements[form.elements.length-1]; + var firstElem = form.elements[0]; + + lastElem.onkeydown = function(e) { + if (e.keyCode == 9 && !e.shiftKey) { + firstElem.focus(); + return false; + } + }; + + firstElem.onkeydown = function(e) { + if (e.keyCode == 9 && e.shiftKey) { + lastElem.focus(); + return false; + } + }; + + + container.style.display = 'block'; + form.elements.text.focus(); + +}; diff --git a/app/js/main.js b/app/js/main.js index 373d16cc1..fbd6d398d 100644 --- a/app/js/main.js +++ b/app/js/main.js @@ -1,7 +1,5 @@ -var hi = require('./hi'); require('jquery'); -hi(); //window.$ = require('jquery'); //require('./prism'); diff --git a/app/js/polyfill/dom4.js b/app/js/polyfill/dom4.js new file mode 100644 index 000000000..cddfcdbf8 --- /dev/null +++ b/app/js/polyfill/dom4.js @@ -0,0 +1,79 @@ + +function textNodeIfString(node) { + return typeof node === 'string' ? document.createTextNode(node) : node; + } + +function mutationMacro(nodes) { + if (nodes.length === 1) { + return textNodeIfString(nodes[0]); + } + var fragment = document.createDocumentFragment(); + var list = [].slice.call(nodes); + + for (var i = 0; i < list.length; i++) { + fragment.appendChild(textNodeIfString(list[i])); + } + return fragment; +} + +var methods = { + matches: Element.prototype.matchesSelector || Element.prototype.msMatchesSelector, + prepend: function() { + var node = mutationMacro(arguments); + this.insertBefore(node, this.firstChild); + }, + append: function() { + this.appendChild(mutationMacro(arguments)); + }, + before: function() { + var parentNode = this.parentNode; + if (parentNode) { + parentNode.insertBefore(mutationMacro(arguments), this); + } + }, + after: function() { + var parentNode = this.parentNode, + nextSibling = this.nextSibling, + node = mutationMacro(arguments); + if (parentNode) { + parentNode.insertBefore(node, nextSibling); + } + }, + replace: function() { + var parentNode = this.parentNode; + if (parentNode) { + parentNode.replaceChild(mutationMacro(arguments), this); + } + }, + remove: function() { + var parentNode = this.parentNode; + if (parentNode) { + parentNode.removeChild(this); + } + } +}; + +for (var methodName in methods) { + if (!Element.prototype[methodName]) { + Element.prototype[methodName] = methods[methodName]; + } +} + +try { + new CustomEvent("IE has CustomEvent, but doesn't support constructor"); +} catch(e) { + + window.CustomEvent = function(event, params) { + var evt; + params = params || { + bubbles: false, + cancelable: false, + detail: undefined + }; + evt = document.createEvent("CustomEvent"); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + return evt; + }; + + CustomEvent.prototype = Object.create(window.Event.prototype); +} diff --git a/app/js/polyfill/index.js b/app/js/polyfill/index.js index 18a239889..02bcba7e2 100644 --- a/app/js/polyfill/index.js +++ b/app/js/polyfill/index.js @@ -1,2 +1,2 @@ -require('./matches'); +require('./dom4'); require('./on'); diff --git a/app/js/polyfill/matches.js b/app/js/polyfill/matches.js deleted file mode 100644 index 7d2a3bcaf..000000000 --- a/app/js/polyfill/matches.js +++ /dev/null @@ -1,3 +0,0 @@ -if (!Element.prototype.matches) { - Element.prototype.matches = Element.prototype.matchesSelector || Element.prototype.msMatchesSelector; -} diff --git a/app/js/polyfill/on.js b/app/js/polyfill/on.js index a569accc1..ab3dd67a1 100644 --- a/app/js/polyfill/on.js +++ b/app/js/polyfill/on.js @@ -1,4 +1,4 @@ -require('./matches'); +require('./dom4'); function findDelegateTarget(event, selector) { var currentNode = event.target; @@ -18,7 +18,7 @@ function findDelegateTarget(event, selector) { // IE doesn't have EventTarget, corresponding methods are in Node var prototype = (window.EventTarget || Node).prototype; - +// currentTarget is top-level element! prototype.on = function(eventName, selector, handler) { this.addEventListener(eventName, function(event) { var found = findDelegateTarget(event, selector); @@ -28,7 +28,7 @@ prototype.on = function(eventName, selector, handler) { // so, keep in mind: // --> event.currentTarget is top-level element! - event.delegateTarget = event.currentTarget; // for copat. with jQuery + event.delegateTarget = event.currentTarget; // for compat. with jQuery if (found) { handler.call(found, event); } @@ -36,5 +36,5 @@ prototype.on = function(eventName, selector, handler) { }; prototype.off = function() { - throw new Error("Not implemented (you need it? write an issue)"); + throw new Error("Not implemented (you need it? file an issue)"); }; diff --git a/app/js/prism-my.js b/app/js/prism-my.js deleted file mode 100644 index 7ac9c1f37..000000000 --- a/app/js/prism-my.js +++ /dev/null @@ -1,111 +0,0 @@ -!function () { - document.removeEventListener('DOMContentLoaded', Prism.highlightAll); - - - function addLineNumbers(pre) { - - var linesNum = (1 + pre.innerHTML.split('\n').length); - var lineNumbersWrapper; - - lines = new Array(linesNum); - lines = lines.join(''); - - lineNumbersWrapper = document.createElement('span'); - lineNumbersWrapper.className = 'line-numbers-rows'; - lineNumbersWrapper.innerHTML = lines; - - if (pre.hasAttribute('data-start')) { - pre.style.counterReset = 'linenumber ' + (parseInt(pre.getAttribute('data-start'), 10) - 1); - } - - pre.appendChild(lineNumbersWrapper); - } - - - function addBlockHighlight(pre) { - - var lines = $(pre).data('highlightBlock'); - - if (!lines) { - return; - } - - var ranges = lines.replace(/\s+/g, '').split(','); - - for (var i = 0, range; range = ranges[i++];) { - range = range.split('-'); - - var start = +range[0], - end = +range[1] || start; - - - var mask = $('
' + - new Array(start + 1).join('\n') + - '
' + new Array(end - start + 2).join('\n') + '
'); - - $(pre).prepend(mask); - } - - } - - function esc(str) { - return str - .replace(/&/g, '&') - .replace(//g, '>'); - } - - function unesc(str) { - return str - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>'); - } - - function addInlineHighlight(pre) { - var ranges = $(pre).data('highlightInline'); - var codeElem = $('code', pre); - - ranges = ranges ? ranges.split(",") : []; - - for (var i = 0; i < ranges.length; i++) { - var piece = ranges[i].split(':'); - var lineNum = +piece[0], strRange = piece[1].split('-'); - var start = +strRange[0], end = +strRange[1]; - var mask = $('
' + - new Array(lineNum + 1).join('\n') + - new Array(start + 1).join(' ') + - '' + new Array(end - start + 1).join(' ') + '
'); - - codeElem.prepend(mask); - } - } - - - $(function() { - - // highlight inline - var codePre = $('pre[class*="language-"]'); - - codePre.each(function () { - this.code = unesc(this.innerHTML); - $(this).wrapInner(""); - - Prism.highlightElement(this.firstChild); - - addLineNumbers(this); - addBlockHighlight(this); - addInlineHighlight(this); - new CodeBox(this); - }); - - - }); - - $(function() { - $('iframe.result__iframe').each(function() { - new IframeBox(this); - }) - }); - -}(); diff --git a/app/js/prism.js b/app/js/prism.js index 40ee76f66..47398ef90 100644 --- a/app/js/prism.js +++ b/app/js/prism.js @@ -65,6 +65,7 @@ require('prismjs/components/prism-java.js'); } + // fixme: require lodash.escape function esc(str) { return str .replace(/&/g, '&') diff --git a/app/stylesheets/blocks/login/login.styl b/app/stylesheets/blocks/login/login.styl new file mode 100644 index 000000000..8920c1a39 --- /dev/null +++ b/app/stylesheets/blocks/login/login.styl @@ -0,0 +1,7 @@ + +form.auth-form + fixed top 50% left 50% + width 400px + height 400px + margin-left -(@width / 2) + margin-top -(@height / 2) diff --git a/gulpfile.js b/gulpfile.js index 0ef8cb25f..7df9a4dc6 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -55,7 +55,10 @@ gulp.task('app:sprite-once', lazyRequireTask('spriteOnce', { gulp.task('app:sprite', ['app:sprite-once'], lazyRequireTask('sprite', { watch: "app/**/*.sprite/**"})); gulp.task('app:clean-compiled-css', function(callback) { - fs.unlink('./www/stylesheets/base.css', callback); + fs.unlink('./www/stylesheets/base.css', function(err) { + if (err && err.code == 'ENOENT') err = null; + callback(err); + }); }); // Show errors if encountered diff --git a/tasks/syncResources.js b/tasks/syncResources.js index d20046a26..c6b161c7f 100644 --- a/tasks/syncResources.js +++ b/tasks/syncResources.js @@ -5,10 +5,11 @@ module.exports = function(resources) { return function(callback) { + for (var src in resources) { var dst = resources[src]; - fse.removeSync(src); + fse.removeSync(dst); if (process.env.NODE_ENV == 'development') { fse.mkdirsSync(dst); diff --git a/templates/blocks/scripts.jade b/templates/blocks/scripts.jade index 09335ea22..05b76cf1a 100644 --- a/templates/blocks/scripts.jade +++ b/templates/blocks/scripts.jade @@ -1,11 +1,2 @@ -//- TODO: Почему бы не использовать абсолютные пути? -script(src='http://code.jquery.com/jquery-1.10.1.min.js') -script(src='http://code.jquery.com/jquery-migrate-1.2.1.min.js') -script(src='../app/assets/javascripts/fancybox.pack.js') -script(src='../app/assets/javascripts/base.js') -script(src='../app/assets/javascripts/prism-core.js') -script(src='../app/assets/javascripts/prism-markup.js') -script(src='../app/assets/javascripts/prism-css.js') -script(src='../app/assets/javascripts/prism-clike.js') -script(src='../app/assets/javascripts/prism-javascript.js') -script(src='../app/assets/javascripts/prism-my.js') \ No newline at end of file +script(src='/js/vendor.js') +script(src='/js/main.js') diff --git a/templates/blocks/top-parts.jade b/templates/blocks/top-parts.jade index 299c88205..f7b471ba3 100644 --- a/templates/blocks/top-parts.jade +++ b/templates/blocks/top-parts.jade @@ -1,6 +1,9 @@ + .top-part .logo a(href='http://javascript.ru/') img(src='/img/logo.png', alt='Javascript.ru') .user.dropdown.down-left.inherit-min-width a.user__entrance(href='/user') Вход на сайт + + From 10337f51a715e0e85e91954e988b5755dfc90a27 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Mon, 28 Jul 2014 21:03:06 +0400 Subject: [PATCH 121/130] hacking auth --- app/js/head.js | 2 +- app/js/login.js | 88 +++++++----------- app/stylesheets/base.styl | 19 +++- .../facebook.sprite/facebook.png | Bin 128 -> 0 bytes .../facebook.sprite/facebook_hover.png | Bin 128 -> 0 bytes .../blocks/block_facebook/facebook.styl | 6 -- .../blocks/block_home/home.sprite/home.png | Bin 183 -> 0 bytes .../block_home/home.sprite/home_hover.png | Bin 183 -> 0 bytes app/stylesheets/blocks/login/login.styl | 10 +- app/stylesheets/blocks/progress/progress.styl | 69 ++++++++++++++ fixture/db.js | 7 ++ gulpfile.js | 8 +- hmvc/auth/controller/form.js | 6 ++ hmvc/auth/controller/login/local.js | 19 ++++ hmvc/auth/controller/logout.js | 6 ++ hmvc/auth/index.js | 8 +- hmvc/auth/lib/passport.js | 33 +++++++ hmvc/auth/router.js | 12 +++ hmvc/auth/templates/form.jade | 20 ++++ hmvc/auth/templates/login-register.jade | 15 --- modules/config/mongoose.js | 2 +- modules/setup/passport.js | 2 +- modules/setup/router.js | 4 +- package.json | 1 + tasks/livereload.js | 20 ++++ 25 files changed, 266 insertions(+), 91 deletions(-) delete mode 100644 app/stylesheets/blocks/block_facebook/facebook.sprite/facebook.png delete mode 100644 app/stylesheets/blocks/block_facebook/facebook.sprite/facebook_hover.png delete mode 100644 app/stylesheets/blocks/block_facebook/facebook.styl delete mode 100644 app/stylesheets/blocks/block_home/home.sprite/home.png delete mode 100644 app/stylesheets/blocks/block_home/home.sprite/home_hover.png create mode 100644 app/stylesheets/blocks/progress/progress.styl create mode 100644 hmvc/auth/controller/form.js create mode 100644 hmvc/auth/controller/login/local.js create mode 100644 hmvc/auth/controller/logout.js create mode 100644 hmvc/auth/lib/passport.js create mode 100644 hmvc/auth/router.js create mode 100644 hmvc/auth/templates/form.jade delete mode 100644 hmvc/auth/templates/login-register.jade create mode 100644 tasks/livereload.js diff --git a/app/js/head.js b/app/js/head.js index 51a88b205..5019813e9 100644 --- a/app/js/head.js +++ b/app/js/head.js @@ -2,7 +2,7 @@ require('./polyfill'); var login = require('./login'); -document.on('click', 'a.login', function(e) { +document.on('click', 'a.user__entrance', function(e) { e.preventDefault(); login(); diff --git a/app/js/login.js b/app/js/login.js index d3d78d0a8..a038cd50f 100644 --- a/app/js/login.js +++ b/app/js/login.js @@ -1,16 +1,4 @@ -/* Показать полупрозрачный DIV, затеняющий всю страницу - (а форма будет не в нем, а рядом с ним, чтобы не полупрозрачная) */ -function showCover() { - var coverDiv = document.createElement('div'); - coverDiv.className = 'cover-div'; - document.body.appendChild(coverDiv); -} - -function hideCover() { - document.querySelector('.cover-div').remove(); -} - // Run like this: // login() // login({whyMessage:.. followLinkMessage:..}) @@ -19,58 +7,46 @@ module.exports = function(options, callback) { options = options || {}; callback = callback || function() { }; - showCover(); - - // TODO - var container = document.getElementById('prompt-form-container'); - document.getElementById('prompt-message').innerHTML = text; - form.elements.text.value = ''; + var authWindow = document.createElement('div'); + authWindow.className = "auth-form"; - function complete(value) { - hideCover(); - container.style.display = 'none'; - document.onkeydown = null; - callback(value); - } + authWindow.innerHTML = '
'; + document.body.append(authWindow); - form.onsubmit = function() { - var value = form.elements.text.value; - if (value == '') return false; // игнорировать пустой submit - - complete(value); - return false; + var xhr = new XMLHttpRequest(); + xhr.open('GET', '/auth/form', true); + xhr.onloadend = function() { + if (this.status != 200 || !this.responseText) { + alert("Извините, ошибка на сервере"); + return; + } + authWindow.innerHTML = this.responseText; + addLoginFormEvents(authWindow.querySelector('.login-form'), callback); }; - form.elements.cancel.onclick = function() { - complete(null); - }; + xhr.send(); - document.onkeydown = function(e) { - e = e || event; - if (e.keyCode == 27) { // escape - complete(null); - } - }; +}; - var lastElem = form.elements[form.elements.length-1]; - var firstElem = form.elements[0]; +function addLoginFormEvents(form, callback) { + form.addEventListener('submit', function(event) { + event.preventDefault(); - lastElem.onkeydown = function(e) { - if (e.keyCode == 9 && !e.shiftKey) { - firstElem.focus(); - return false; - } - }; + var email = form.elements.email; + var password = form.elements.password; - firstElem.onkeydown = function(e) { - if (e.keyCode == 9 && e.shiftKey) { - lastElem.focus(); - return false; - } - }; + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/auth/login/local', true); + xhr.onloadend = function() { + if (!this.status || this.status >= 500 || !this.responseText) { + alert("Извините, ошибка на сервере"); + return; + } + alert(this.responseText); + }; + xhr.send(new FormData(form)); - container.style.display = 'block'; - form.elements.text.focus(); + }); -}; +} diff --git a/app/stylesheets/base.styl b/app/stylesheets/base.styl index 5f2ed5c10..06862ef22 100755 --- a/app/stylesheets/base.styl +++ b/app/stylesheets/base.styl @@ -1,11 +1,26 @@ -@require "sprites/*" -@require "blocks/block_facebook/facebook" +// import all from nib except +// -flex +// -vendor +// -iconic +// -reset +@import 'nib/border' +@import 'nib/clearfix' +@import 'nib/color-image' +@import 'nib/gradients' +@import 'nib/image' +@import 'nib/overflow' +@import 'nib/positions' +@import 'nib/text' +@import 'nib/size' +@require "sprites/*" @require "blocks/variables/variables" @require "blocks/reset/reset" @require "blocks/placeholders/*" @require "blocks/mixins/*" +@require "blocks/login/login" +@require "blocks/progress/progress" @require "blocks/body/body" @require "blocks/top-part/top-part" @require "blocks/logo/logo" diff --git a/app/stylesheets/blocks/block_facebook/facebook.sprite/facebook.png b/app/stylesheets/blocks/block_facebook/facebook.sprite/facebook.png deleted file mode 100644 index a5df67cd0af8c1bc8eb8e375261929b2d1d92377..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`?w&4=Ar_~TAIz-#WgzfhU06PC zfrGJxvh)E5rDgLO?>YRD>Xur>ThZpsdy(zIB=u7aUjmlzXB4SUERb;6KgFXgZIOz# bJR`%V_>a3nic=l{jb!k2^>bP0l+XkK@f0Z0 diff --git a/app/stylesheets/blocks/block_facebook/facebook.sprite/facebook_hover.png b/app/stylesheets/blocks/block_facebook/facebook.sprite/facebook_hover.png deleted file mode 100644 index 18fdbd6f1dd46313650b0ed68a77c9b906e3e871..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 128 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`?w&4=Ar_~TE1oZ}k?8n8xs%^G zV1Z=MMBW4i!OQ0v@)hcOU3o9E*&JTjmcd+bQaF{NYQ^RI3|)H-EO-{gd#P9(2Tq#X b&%m%-=h3cpYEI99MlyK1`njxgN@xNA6NxDy diff --git a/app/stylesheets/blocks/block_facebook/facebook.styl b/app/stylesheets/blocks/block_facebook/facebook.styl deleted file mode 100644 index b507621af..000000000 --- a/app/stylesheets/blocks/block_facebook/facebook.styl +++ /dev/null @@ -1,6 +0,0 @@ -.facebook - sprite($facebook) - -.facebook:hover - sprite($facebook_hover) - diff --git a/app/stylesheets/blocks/block_home/home.sprite/home.png b/app/stylesheets/blocks/block_home/home.sprite/home.png deleted file mode 100644 index 498cebe53cd71338aa5a4a9276570679b7f8f6e1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`b)GJcAr`0aPBP?cP~dU>&uGJB zyZw6OP0sL-I$WNfTHBfmiz}SH|9dkh%n~lkhpB+ zSuAZ)nCA7QL9}klg6&EEO#2>AENE>}SyXAo<7B%0Zk>M3O#R*ujsjblU&pT~wioRw g68<5c7+s7k>f-Pr8AkZZYp00i_>zopr0K=0&q5uE@ diff --git a/app/stylesheets/blocks/login/login.styl b/app/stylesheets/blocks/login/login.styl index 8920c1a39..f964e3c67 100644 --- a/app/stylesheets/blocks/login/login.styl +++ b/app/stylesheets/blocks/login/login.styl @@ -1,7 +1,15 @@ -form.auth-form +.auth-form fixed top 50% left 50% width 400px height 400px margin-left -(@width / 2) margin-top -(@height / 2) + background white + border 1px solid black + z-index 10000 + +.auth-form .progress + margin: 100px auto 0 auto + + diff --git a/app/stylesheets/blocks/progress/progress.styl b/app/stylesheets/blocks/progress/progress.styl new file mode 100644 index 000000000..1ce520f07 --- /dev/null +++ b/app/stylesheets/blocks/progress/progress.styl @@ -0,0 +1,69 @@ +/** + * (C)Leanest CSS spinner ever + * From http://lea.verou.me/2013/11/cleanest-css-spinner-ever/ + * + * Usage:
Loading…
+ +
Loading…
+ +
Loading…
+ */ + +@keyframes spin { + to { transform: rotate(1turn); } +} + +.progress { + position: relative; +// display: inline-block; + width: 5em; + height: 5em; + margin: 0 .5em; + font-size: 12px; + text-indent: 999em; + overflow: hidden; + animation: spin 1s infinite steps(8); +} + +.small.progress { + font-size: 6px; +} + +.large.progress { + font-size: 24px; +} + +.progress:before, +.progress:after, +.progress > div:before, +.progress > div:after { + content: ''; + position: absolute; + top: 0; + left: 2.25em; /* (container width - part width)/2 */ + width: .5em; + height: 1.5em; + border-radius: .2em; + background: #eee; + box-shadow: 0 3.5em #eee; /* container height - part height */ + transform-origin: 50% 2.5em; /* container height / 2 */ +} + +.progress:before { + background: #555; +} + +.progress:after { + transform: rotate(-45deg); + background: #777; +} + +.progress > div:before { + transform: rotate(-90deg); + background: #999; +} + +.progress > div:after { + transform: rotate(-135deg); + background: #bbb; +} diff --git a/fixture/db.js b/fixture/db.js index 78c0b9546..5d00fe295 100644 --- a/fixture/db.js +++ b/fixture/db.js @@ -1,6 +1,7 @@ const mongoose = require('mongoose'); var OrderTemplate = require('payments').OrderTemplate; +var User = require('auth/models/user'); exports.OrderTemplate = [ { @@ -22,3 +23,9 @@ exports.OrderTemplate = [ amount: 1 } ]; + +exports.User = [{ + email: "iliakan@gmail.com", + username: "Ilya Kantor", + password: "123456" +}]; diff --git a/gulpfile.js b/gulpfile.js index 7df9a4dc6..546683636 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -33,8 +33,9 @@ gulp.task('lint', ['lint-once'], lazyRequireTask('lint', {src: serverSources})); // usage: gulp loaddb --db fixture/db gulp.task('loaddb', lazyRequireTask('loadDb')); -gulp.task("supervisor", ['link-modules'], lazyRequireTask('supervisor', { cmd: "bin/www", watch: ["hmvc", "modules"] })); +gulp.task("supervisor", ['link-modules'], lazyRequireTask('supervisor', { cmd: "./bin/www", watch: ["hmvc", "modules"] })); +gulp.task("app:livereload", lazyRequireTask("livereload", { watch: "www/**/*.*" })); gulp.task('link-modules', lazyRequireTask('linkModules', { src: ['modules/*', 'hmvc/*'] })); @@ -44,7 +45,6 @@ gulp.task("app:sync-resources", lazyRequireTask('syncResources', { 'app/img': 'www/img' })); - gulp.task('app:sprite-once', lazyRequireTask('spriteOnce', { spritesSearchFsRoot: 'app', spritesWebRoot: '/sprites', @@ -70,6 +70,8 @@ gulp.task('app:compile-css-once', }) ); + + gulp.task('app:compile-css', ['app:compile-css-once'], lazyRequireTask('compileCss', { watch: "app/**/*.styl"})); @@ -81,7 +83,7 @@ gulp.task("app:browserify", ['app:browserify:clean'], lazyRequireTask('browserif // compile-css and sprites are independant tasks // run both or run *-once separately -gulp.task('run', ['supervisor', "app:sync-resources", 'app:compile-css', 'app:sprite', 'app:browserify']); +gulp.task('run', ['supervisor', 'app:livereload', "app:sync-resources", 'app:compile-css', 'app:sprite', 'app:browserify']); // TODO: refactor me out! diff --git a/hmvc/auth/controller/form.js b/hmvc/auth/controller/form.js new file mode 100644 index 000000000..1f93c3baf --- /dev/null +++ b/hmvc/auth/controller/form.js @@ -0,0 +1,6 @@ + +exports.get = function *get (next) { + this.render(__dirname, 'form'); +}; + + diff --git a/hmvc/auth/controller/login/local.js b/hmvc/auth/controller/login/local.js new file mode 100644 index 000000000..a522c22b0 --- /dev/null +++ b/hmvc/auth/controller/login/local.js @@ -0,0 +1,19 @@ +var passport = require('koa-passport'); + +exports.post = function*(next) { + console.log("HERE"); + var ctx = this; + yield passport.authenticate('local', function*(err, user, info) { + // missing credentials ?!? + console.log("HERE 2", err, user, info); + if (err) throw err; + if (user === false) { + ctx.status = 401; + ctx.body = { success: false }; + } else { + yield ctx.login(user); + ctx.body = { success: true }; + } + }).call(this, next); +}; + diff --git a/hmvc/auth/controller/logout.js b/hmvc/auth/controller/logout.js new file mode 100644 index 000000000..4032eb244 --- /dev/null +++ b/hmvc/auth/controller/logout.js @@ -0,0 +1,6 @@ + +exports.post = function*(next) { + this.logout(); + this.redirect('/'); +}; + diff --git a/hmvc/auth/index.js b/hmvc/auth/index.js index 3f7d3cd21..5a299106e 100644 --- a/hmvc/auth/index.js +++ b/hmvc/auth/index.js @@ -1,5 +1,5 @@ -/* -var requireTree = require('require-tree'); +const router = require('./router'); -requireTree('./model'); -*/ +require('./lib/passport'); + +exports.middleware = router.middleware(); diff --git a/hmvc/auth/lib/passport.js b/hmvc/auth/lib/passport.js new file mode 100644 index 000000000..c4383dd91 --- /dev/null +++ b/hmvc/auth/lib/passport.js @@ -0,0 +1,33 @@ +const passport = require('koa-passport'); +const LocalStrategy = require('passport-local').Strategy; +const User = require('../models/user'); + +// setup auth strategy +passport.serializeUser(function(user, done) { + done(null, user.id); +}); + +passport.deserializeUser(function(id, done) { + User.findById(id, function (err, user) { + done(err, user); + }); +}); + +passport.use(new LocalStrategy({ + usernameField: 'email', + passwordField: 'password' +}, function(email, password, done) { + console.log("!!!!", email, password); + + if (!email) return done(null, false, { message: 'Please provide email.' }); + if (!password) return done(null, false, { message: 'Please provide password.' }); + User.findOne({email: email}, function(err, user) { + console.log(email, password, err, user); + + if (err) return done(err); + if (!user) return done(null, false, { message: 'Non-registered email.' }); + return user.checkPassword(password) + ? done(null, user) + : done(null, false, { message: 'Incorrect password.' }); + }); +})); diff --git a/hmvc/auth/router.js b/hmvc/auth/router.js new file mode 100644 index 000000000..253eeb0b8 --- /dev/null +++ b/hmvc/auth/router.js @@ -0,0 +1,12 @@ +var Router = require('koa-router'); +var form = require('./controller/form'); +var local = require('./controller/login/local'); +var logout = require('./controller/logout'); + +var router = module.exports = new Router(); + +router.get('/form', form.get); + +router.post('/login/local', local.post); + +router.post('/logout', logout.post); diff --git a/hmvc/auth/templates/form.jade b/hmvc/auth/templates/form.jade new file mode 100644 index 000000000..a2d69fc73 --- /dev/null +++ b/hmvc/auth/templates/form.jade @@ -0,0 +1,20 @@ +form.login-form + h2 Вход в систему + + div + label Email + input(name="email" type="email") + + div + label Пароль + input(name="password" type="password") + + div + input(type="submit" value="Войти") + + +form.register-form(style="display:none") + h2 Регистрация + + p ... + diff --git a/hmvc/auth/templates/login-register.jade b/hmvc/auth/templates/login-register.jade deleted file mode 100644 index 8c33f0b80..000000000 --- a/hmvc/auth/templates/login-register.jade +++ /dev/null @@ -1,15 +0,0 @@ -form.login-form - h2 Вход в систему - - div - label Email - input(type="email") - - div - label Пароль - input(type="password") - - div - input(type="submit" value="Войти") - - diff --git a/modules/config/mongoose.js b/modules/config/mongoose.js index 567a3aa14..19cdf6be1 100644 --- a/modules/config/mongoose.js +++ b/modules/config/mongoose.js @@ -13,7 +13,7 @@ var fs = require('fs'); var log = require('js-log')(); var autoIncrement = require('mongoose-auto-increment'); -//mongoose.set('debug', true); +mongoose.set('debug', true); var config = require('config'); var _ = require('lodash'); diff --git a/modules/setup/passport.js b/modules/setup/passport.js index 994f75b71..bee347347 100644 --- a/modules/setup/passport.js +++ b/modules/setup/passport.js @@ -1,5 +1,5 @@ const mongoose = require('mongoose'); -const passport = require('koa-passport') +const passport = require('koa-passport'); const config = require('config'); diff --git a/modules/setup/router.js b/modules/setup/router.js index 5a4d87514..d21373625 100644 --- a/modules/setup/router.js +++ b/modules/setup/router.js @@ -12,7 +12,9 @@ module.exports = function(app) { app.use(mount('/markup', require('markup').middleware)); } - // need to compose, because mount takes only 1 middleware + app.use(mount('/auth', require('auth').middleware)); + app.csrf.addIgnorePath('/auth/login/:any*'); + app.use(mount('/getpdf', require('getpdf').middleware)); app.use(mount('/payments', require('payments').middleware)); diff --git a/package.json b/package.json index 2aaf7fc20..fe8a55e05 100755 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "nodemailer": "^1.0.4", "nodemailer-ses-transport": "^0.1.1", "passport": "*", + "passport-local": "^1.0.0", "path-to-regexp": "^0.2.3", "prism": "0.0.1", "prismjs": "git://github.com/LeaVerou/prism#gh-pages", diff --git a/tasks/livereload.js b/tasks/livereload.js new file mode 100644 index 000000000..5defc1b5f --- /dev/null +++ b/tasks/livereload.js @@ -0,0 +1,20 @@ +var livereload = require('gulp-livereload'); +var gulp = require('gulp'); +var gutil = require('gulp-util'); + +// options.watch must NOT be www/**, because that breaks (why?!?) supervisor reloading +// www/**/*.* is fine +module.exports = function(options) { + + // listen to changes after 7 secs, to let initial jobs finish + // no one is going to livereload right now anyway + return function(callback) { + livereload.listen(); + setTimeout(function() { + gutil.log("livereload: deferred listen on change " + options.watch); + gulp.watch(options.watch).on('change', livereload.changed); + }, 7000); + }; +}; + + From da4c875efbe22dc5b55b5fd1e5cd54852e32db63 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 29 Jul 2014 17:56:26 +0400 Subject: [PATCH 122/130] cleanup --- package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/package.json b/package.json index fe8a55e05..97c7fe5b0 100755 --- a/package.json +++ b/package.json @@ -63,7 +63,6 @@ "koa-views": "*", "lodash": "*", "map-stream": "*", - "markdown-js": "*", "moment": "^2.7.0", "mongoose": "3.8", "mongoose-auto-increment": "^3.0.8", @@ -74,9 +73,7 @@ "passport": "*", "passport-local": "^1.0.0", "path-to-regexp": "^0.2.3", - "prism": "0.0.1", "prismjs": "git://github.com/LeaVerou/prism#gh-pages", - "require-tree": "*", "stylus": "*", "svgutils": "^0.7.0", "through2": "^0.5.1", From b70207720b3f24b996419ece1189f3df18b65971 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 29 Jul 2014 22:32:35 +0400 Subject: [PATCH 123/130] replace bodyParser with a wrapper around formidable (supports multipart) --- bin/www | 12 ++--- hmvc/getpdf/controller/checkout.js | 12 ++--- hmvc/payments/lib/loadOrder.js | 2 +- hmvc/payments/lib/loadTransaction.js | 2 +- .../payments/payanyway/controller/callback.js | 8 +-- hmvc/payments/paypal/controller/callback.js | 26 ++++----- hmvc/payments/webmoney/controller/callback.js | 14 ++--- hmvc/payments/webmoney/router.js | 2 +- modules/app.js | 54 ++++++++++++------- modules/config/mongoose.js | 2 +- .../bodyParser.js => httpPostParser/index.js} | 36 +++++++++---- modules/httpPostParser/test/post.js | 31 +++++++++++ modules/lib/csrf.js | 9 +++- modules/lib/debug.js | 18 +++++++ modules/setup/bodyParser.js | 11 ---- modules/setup/formidable.js | 7 --- modules/setup/httpPostParser.js | 11 ++++ modules/setup/router.js | 6 ++- package.json | 4 +- 19 files changed, 174 insertions(+), 93 deletions(-) rename modules/{lib/bodyParser.js => httpPostParser/index.js} (52%) create mode 100644 modules/httpPostParser/test/post.js create mode 100644 modules/lib/debug.js delete mode 100644 modules/setup/bodyParser.js delete mode 100644 modules/setup/formidable.js create mode 100644 modules/setup/httpPostParser.js diff --git a/bin/www b/bin/www index 5561b9ff8..f9616793f 100755 --- a/bin/www +++ b/bin/www @@ -3,15 +3,13 @@ const log = require('js-log')(); const config = require('config'); -const mongoose = require('config/mongoose'); +const co = require('co'); const app = require('app'); -mongoose.waitConnect(function(err) { - if (err) throw err; +co(function*() { - app.listen(config.port, config.host, function() { - log.info('App listen %s:%d', config.host, config.port); - }); + yield* app.run(); +})(function(err) { + if (err) throw err; }); - diff --git a/hmvc/getpdf/controller/checkout.js b/hmvc/getpdf/controller/checkout.js index 5d91e5223..25143862b 100644 --- a/hmvc/getpdf/controller/checkout.js +++ b/hmvc/getpdf/controller/checkout.js @@ -10,22 +10,22 @@ log.debugOn(); exports.post = function*(next) { yield* this.loadOrder(); - var method = methods[this.request.body.paymentMethod]; + var method = methods[this.req.body.paymentMethod]; if (!method) { this.throw(403, "Unsupported payment method"); } if (this.order) { log.debug("order exists", this.order.number); - yield* updateOrderFromBody(this.request.body, this.order); + yield* updateOrderFromBody(this.req.body, this.order); } else { // if we don't have the order in our database, then make a new one // (use the incoming order post for that, but don't trust it) - console.log(this.request.body.orderTemplate); + console.log(this.req.body.orderTemplate); var orderTemplate = yield OrderTemplate.findOne({ - slug: this.request.body.orderTemplate + slug: this.req.body.orderTemplate }).exec(); if (!orderTemplate) { @@ -36,10 +36,10 @@ exports.post = function*(next) { // create order from template, don't trust the incoming post this.order = Order.createFromTemplate(orderTemplate, { module: 'getpdf', - email: this.request.body.email + email: this.req.body.email }); - yield* updateOrderFromBody(this.request.body, this.order); + yield* updateOrderFromBody(this.req.body, this.order); log.debug("order created", this.order.number); diff --git a/hmvc/payments/lib/loadOrder.js b/hmvc/payments/lib/loadOrder.js index f71e937ca..1b4672254 100644 --- a/hmvc/payments/lib/loadOrder.js +++ b/hmvc/payments/lib/loadOrder.js @@ -7,7 +7,7 @@ module.exports = function* (field) { if (!field) field = 'orderNumber'; - var orderNumber = this.request.body && this.request.body[field] || this.params[field] || this.query[field]; + var orderNumber = this.req.body && this.req.body[field] || this.params[field] || this.query[field]; if (!orderNumber) { return; diff --git a/hmvc/payments/lib/loadTransaction.js b/hmvc/payments/lib/loadTransaction.js index 2603a7c34..f46c1786e 100644 --- a/hmvc/payments/lib/loadTransaction.js +++ b/hmvc/payments/lib/loadTransaction.js @@ -9,7 +9,7 @@ module.exports = function* (field, options) { options = options || {}; if (!field) field = 'transactionNumber'; - var transactionNumber = this.request.body && this.request.body[field] || this.params[field] || this.query[field]; + var transactionNumber = this.req.body && this.req.body[field] || this.params[field] || this.query[field]; log.debug('tx number: ' + transactionNumber); if (!transactionNumber) { diff --git a/hmvc/payments/payanyway/controller/callback.js b/hmvc/payments/payanyway/controller/callback.js index c0d80df54..4124855cb 100644 --- a/hmvc/payments/payanyway/controller/callback.js +++ b/hmvc/payments/payanyway/controller/callback.js @@ -10,7 +10,7 @@ log.debugOn(); exports.post = function* (next) { - checkSignature(this.request.body); + checkSignature(this.req.body); this.body = 'SUCCESS'; return; @@ -19,7 +19,7 @@ exports.post = function* (next) { yield this.transaction.logRequest('callback unverified', this.request); - if (!checkSignature(this.request.body)) { + if (!checkSignature(this.req.body)) { log.debug("wrong signature"); this.throw(403, "wrong signature"); } @@ -27,8 +27,8 @@ exports.post = function* (next) { yield this.transaction.logRequest('callback', this.request); // signature is valid, so everything MUST be fine - if (this.transaction.amount != parseFloat(this.request.body.MNT_AMOUNT) || - this.request.body.MNT_ID != payanywayConfig.id) { + if (this.transaction.amount != parseFloat(this.req.body.MNT_AMOUNT) || + this.req.body.MNT_ID != payanywayConfig.id) { yield this.transaction.persist({ status: Transaction.STATUS_FAIL, statusMessage: "данные транзакции не совпадают с базой, свяжитесь с поддержкой" diff --git a/hmvc/payments/paypal/controller/callback.js b/hmvc/payments/paypal/controller/callback.js index 8cc387785..2771f0f9b 100644 --- a/hmvc/payments/paypal/controller/callback.js +++ b/hmvc/payments/paypal/controller/callback.js @@ -23,8 +23,8 @@ exports.post = function* (next) { 'cmd': '_notify-validate' }; - for (var field in this.request.body) { - qs[field] = this.request.body[field]; + for (var field in this.req.body) { + qs[field] = this.req.body[field]; } // request oauth token @@ -50,9 +50,9 @@ exports.post = function* (next) { } // ipn is verified now! But we check if it's data matches the transaction (as recommended in docs) - if (this.transaction.amount != parseFloat(this.request.body.mc_gross) || - this.request.body.receiver_email != paypalConfig.email || - this.request.body.mc_currency != config.payments.currency) { + if (this.transaction.amount != parseFloat(this.req.body.mc_gross) || + this.req.body.receiver_email != paypalConfig.email || + this.req.body.mc_currency != config.payments.currency) { yield this.transaction.persist({ status: Transaction.STATUS_FAIL, @@ -70,8 +70,8 @@ exports.post = function* (next) { transaction: this.transaction._id }).sort({created: -1}).exec(); - if (previousIpn && previousIpn.data.payment_status == this.request.body.payment_status) { - yield this.transaction.log("ipn duplicate", this.request.body); + if (previousIpn && previousIpn.data.payment_status == this.req.body.payment_status) { + yield this.transaction.log("ipn duplicate", this.req.body); // ignore duplicate this.body = ''; return; @@ -80,18 +80,18 @@ exports.post = function* (next) { // now we have a valid non-duplicate IPN, let's update the transaction // log it right now to evade conflicts with duplicates - yield this.transaction.log("ipn", this.request.body); + yield this.transaction.log("ipn", this.req.body); // Do not perform any processing on WPS transactions here that do not have // transaction IDs, indicating they are non-payment IPNs such as those used // for subscription signup requests. - if (!this.request.body.txn_id) { - yield this.transaction.log("ipn without txn_id", this.request.body); + if (!this.req.body.txn_id) { + yield this.transaction.log("ipn without txn_id", this.req.body); this.body = ''; return; } - switch(this.request.body.payment_status) { + switch(this.req.body.payment_status) { case 'Failed': case 'Voided': yield this.transaction.persist({ @@ -102,7 +102,7 @@ exports.post = function* (next) { case 'Pending': yield this.transaction.persist({ status: Transaction.STATUS_PENDING, - statusMessage: this.request.body.pending_reason + statusMessage: this.req.body.pending_reason }); this.body = ''; return; @@ -116,7 +116,7 @@ exports.post = function* (next) { return; default: // Refunded ... - yield this.transaction.log("ipn payment_status unknown", this.request.body); + yield this.transaction.log("ipn payment_status unknown", this.req.body); this.body = ''; return; diff --git a/hmvc/payments/webmoney/controller/callback.js b/hmvc/payments/webmoney/controller/callback.js index 84d4545fb..310828faa 100644 --- a/hmvc/payments/webmoney/controller/callback.js +++ b/hmvc/payments/webmoney/controller/callback.js @@ -16,10 +16,10 @@ exports.prerequest = function* (next) { yield this.transaction.logRequest('prerequest', this.request); if (this.transaction.status == Transaction.STATUS_SUCCESS || - this.transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) || - this.request.body.LMI_PAYEE_PURSE != webmoneyConfig.purse + this.transaction.amount != parseFloat(this.req.body.LMI_PAYMENT_AMOUNT) || + this.req.body.LMI_PAYEE_PURSE != webmoneyConfig.purse ) { - log.debug("no pending transaction " + this.request.body.LMI_PAYMENT_NO); + log.debug("no pending transaction " + this.req.body.LMI_PAYMENT_NO); this.throw(404, 'unfinished transaction with given params not found'); } @@ -31,15 +31,15 @@ exports.post = function* (next) { yield* this.loadTransaction('LMI_PAYMENT_NO', {skipOwnerCheck : true}); - if (!checkSignature(this.request.body)) { + if (!checkSignature(this.req.body)) { log.debug("wrong signature"); this.throw(403, "wrong signature"); } yield this.transaction.logRequest('callback', this.request); - if (this.transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) || - this.request.body.LMI_PAYEE_PURSE != webmoneyConfig.purse) { + if (this.transaction.amount != parseFloat(this.req.body.LMI_PAYMENT_AMOUNT) || + this.req.body.LMI_PAYEE_PURSE != webmoneyConfig.purse) { // STRANGE, signature is correct yield this.transaction.persist({ status: Transaction.STATUS_FAIL, @@ -48,7 +48,7 @@ exports.post = function* (next) { this.throw(404, "transaction data doesn't match the POST body"); } - if (!this.request.body.LMI_SIM_MODE || this.request.body.LMI_SIM_MODE == '0') { + if (!this.req.body.LMI_SIM_MODE || this.req.body.LMI_SIM_MODE == '0') { this.transaction.status = Transaction.STATUS_SUCCESS; yield this.transaction.persist(); } diff --git a/hmvc/payments/webmoney/router.js b/hmvc/payments/webmoney/router.js index adcb5361e..ddd8ba7bc 100644 --- a/hmvc/payments/webmoney/router.js +++ b/hmvc/payments/webmoney/router.js @@ -9,7 +9,7 @@ var fail = require('./controller/fail'); // webmoney server posts here (in background) router.post('/callback', function* (next) { - if (this.request.body.LMI_PREREQUEST == '1') { + if (this.req.body.LMI_PREREQUEST == '1') { yield* callback.prerequest.call(this, next); } else { yield* callback.post.call(this, next); diff --git a/modules/app.js b/modules/app.js index 4b305b4dc..74aead9c7 100644 --- a/modules/app.js +++ b/modules/app.js @@ -1,8 +1,10 @@ "use strict"; +require('lib/debug'); const koa = require('koa'); const log = require('js-log')(); const config = require('config'); +const mongoose = require('config/mongoose'); const app = koa(); // trust all headers from proxy @@ -39,7 +41,7 @@ requireSetup('setup/errorHandler'); requireSetup('setup/accessLogger'); // before anything that may deal with body -requireSetup('setup/bodyParser'); +requireSetup('setup/httpPostParser'); // right after parsing body, make sure we logged for development requireSetup('setup/verboseLogger'); @@ -50,8 +52,6 @@ if (process.env.NODE_ENV == 'development') { requireSetup('setup/session'); -requireSetup('setup/formidable'); - requireSetup('setup/passport'); requireSetup('setup/csrf'); @@ -60,23 +60,37 @@ requireSetup('setup/payments'); requireSetup('setup/router'); -if (process.env.NODE_ENV == 'test') { - app.listen(config.port, config.host, function() { - console.log("App listening..."); - }); -} - -module.exports = app; - -if (process.env.NODE_ENV == 'development') { - - global.p = function() { - var stack = new Error().stack.split("\n")[2].trim(); - console.log("----> " + global.p.counter++ + " at " + stack); +// wait for full app load and all associated warm-ups to finish +app.waitBoot = function* () { + yield function(callback) { + mongoose.waitConnect(callback); }; - global.p.counter = 1; -} else { - global.p = function() { +}; + + +// adding middlewares only possible before app.run +app.run = function*() { + yield* app.waitBoot(); + + // every test may use app.run() + // app will only start the 1st time + if (!app.isListening) { + yield function(callback) { + app.listen(config.port, config.host, function() { + log.info('App listen %s:%d', config.host, config.port); + callback(); + }); + }; + app.isListening = true; + } +}; +// for supertest(app), it wants app.address().port +app.address = function() { + return { + port: config.port }; -} +}; + +module.exports = app; + diff --git a/modules/config/mongoose.js b/modules/config/mongoose.js index 19cdf6be1..567a3aa14 100644 --- a/modules/config/mongoose.js +++ b/modules/config/mongoose.js @@ -13,7 +13,7 @@ var fs = require('fs'); var log = require('js-log')(); var autoIncrement = require('mongoose-auto-increment'); -mongoose.set('debug', true); +//mongoose.set('debug', true); var config = require('config'); var _ = require('lodash'); diff --git a/modules/lib/bodyParser.js b/modules/httpPostParser/index.js similarity index 52% rename from modules/lib/bodyParser.js rename to modules/httpPostParser/index.js index a97cc4454..dfb61dc3e 100644 --- a/modules/lib/bodyParser.js +++ b/modules/httpPostParser/index.js @@ -1,6 +1,6 @@ 'use strict'; -const koaBodyParser = require('koa-bodyparser'); +const koaFormidable = require('koa-formidable'); const _ = require('lodash'); const pathToRegexp = require('path-to-regexp'); const log = require('js-log')(); @@ -10,18 +10,18 @@ const log = require('js-log')(); * allows to set per-path options which are used in middleware * usage: * - * app.bodyParser = new BodyParser + * app.httpPostParser = new HttpPostParser * app.use(app.bodyParser.middleware()) * ... - * app.bodyParser.addPathOptions('/upload/path', {limit: 1e10}); + * app.httpPostParser.addPathOptions('/upload/path', {bytesExpected: 1e10}); * @constructor */ -function BodyParser() { +function HttpPostParser() { this.pathOptions = []; } // options should be an object { path: string|regexp, options } -BodyParser.prototype.addPathOptions = function(path, options) { +HttpPostParser.prototype.addPathOptions = function(path, options) { if (path instanceof RegExp) { this.pathOptions.push({path: path, options: options}); } else if (typeof path == 'string') { @@ -31,13 +31,14 @@ BodyParser.prototype.addPathOptions = function(path, options) { } }; -BodyParser.prototype.middleware = function() { +HttpPostParser.prototype.middleware = function() { var self = this; - var optionsDefault = { limit: 1e6 }; + var optionsDefault = { bytesExpected: 1e7 }; return function* (next) { var options = Object.create(optionsDefault); + for (var i = 0; i < self.pathOptions.length; i++) { var path = self.pathOptions[i].path; log.debug("test " + this.req.url + " against " + path); @@ -48,10 +49,27 @@ BodyParser.prototype.middleware = function() { } } - yield* koaBodyParser(options).call(this, next); + // if request file too big, don't start accepting it + // in normal situation, large uploads have the header and get stopped here + if (this.get('content-length')) { + var bytesExpected = parseInt(this.get('content-length'), 10); + + if (bytesExpected > options.bytesExpected) { + this.status = 413; + this.body = 'Request entity too large: ' + bytesExpected + ' > ' + options.bytesExpected; + return; + } + } + + // safety: + // even if a bad person did not supply content-length, + // formidable will not read more than options.bytesExpected + + + yield* koaFormidable(options).call(this, next); }; }; -exports.BodyParser = BodyParser; +module.exports = HttpPostParser; diff --git a/modules/httpPostParser/test/post.js b/modules/httpPostParser/test/post.js new file mode 100644 index 000000000..721aed2ba --- /dev/null +++ b/modules/httpPostParser/test/post.js @@ -0,0 +1,31 @@ +const app = require('app'); +const supertest = require('supertest'); +const should = require('should'); + +describe("HttpPostParser", function() { + + before(function* () { + + app.use(function*(next) { + if ('/test/http-post-parser' != this.path) return yield next; + this.body = this.req.body; + }); + + yield app.run(); + + }); + + it("parses body", function(done) { + + var message = { name: 'Manny', species: 'cat' }; + supertest(app) + .post('/test/http-post-parser') + .send(message) + .end(function(error, res) { + res.body.should.be.eql(message); + done(error); + }); + + }); + +}); diff --git a/modules/lib/csrf.js b/modules/lib/csrf.js index 22b93a31b..9f9e5ef72 100644 --- a/modules/lib/csrf.js +++ b/modules/lib/csrf.js @@ -37,8 +37,15 @@ Csrf.prototype.middleware = function() { } } + // If test check CSRF only when "X-Test-Csrf" header is set + if (process.env.NODE_ENV == 'test') { + if (!this.get('X-Test-Csrf')) { + checkCsrf = false; + } + } + if (checkCsrf) { - this.assertCSRF(this.request.body); + this.assertCSRF(this.req.body); } yield* next; diff --git a/modules/lib/debug.js b/modules/lib/debug.js new file mode 100644 index 000000000..9601c077c --- /dev/null +++ b/modules/lib/debug.js @@ -0,0 +1,18 @@ + + +/* + some crap to log & isolate steps for stackless errors + p() will print next number + */ +if (process.env.NODE_ENV == 'development') { + + global.p = function() { + var stack = new Error().stack.split("\n")[2].trim(); + console.log("----> " + global.p.counter++ + " at " + stack); + }; + global.p.counter = 1; +} else { + global.p = function() { + + }; +} diff --git a/modules/setup/bodyParser.js b/modules/setup/bodyParser.js deleted file mode 100644 index 521897aef..000000000 --- a/modules/setup/bodyParser.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -const BodyParser = require('lib/bodyparser').BodyParser; -const _ = require('lodash'); - -module.exports = function(app) { - - app.bodyParser = new BodyParser(); - app.use(app.bodyParser.middleware()); - -}; diff --git a/modules/setup/formidable.js b/modules/setup/formidable.js deleted file mode 100644 index fb11509f3..000000000 --- a/modules/setup/formidable.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const formidable = require('koa-formidable'); - -module.exports = function (app) { - app.use(formidable()); -}; \ No newline at end of file diff --git a/modules/setup/httpPostParser.js b/modules/setup/httpPostParser.js new file mode 100644 index 000000000..d83007ed6 --- /dev/null +++ b/modules/setup/httpPostParser.js @@ -0,0 +1,11 @@ +'use strict'; + +const HttpPostParser = require('httpPostParser'); +const _ = require('lodash'); + +module.exports = function(app) { + + app.httpPostParser = new HttpPostParser(); + app.use(app.httpPostParser.middleware()); + +}; diff --git a/modules/setup/router.js b/modules/setup/router.js index d21373625..33ec2f3af 100644 --- a/modules/setup/router.js +++ b/modules/setup/router.js @@ -45,7 +45,11 @@ module.exports = function(app) { // by default if the router didn't find anything => it yields to next middleware // so I throw error here manually app.use(function* (next) { - this.throw(404); + yield* next; + + if (this.status == 404) { + this.throw(404); // still not found? pass to default errorHandler + } }); }; diff --git a/package.json b/package.json index 9414f3bdc..0cd0a764c 100755 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "imagemin-svgo": "^0.1.0", "jade": "*", "javascript-gulp-task-lint": "*", - "javascript-log": "*", "javascript-parser": "*", "jquery": "^2.1.1", "js-log": "^0.2.2", @@ -96,13 +95,12 @@ "gulp-sourcemaps": "^1.1.0", "gulp-stylus-sprite": "*", "gulp-supervisor": "^0.1.2", - "gulp-tap": "^0.1.1", - "javascript-brunch": "*", "lazypipe": "^0.2.1", "mocha": "*", "node-notifier": "^3.1.1", "should": "*", "sinon": "*", + "superagent": "^0.18.2", "supertest": "^0.13.0", "supervisor": "*", "trace": "*", From c9fbbf01b5fe73ceab861878c0b1cd987a49e0ae Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 29 Jul 2014 22:43:33 +0400 Subject: [PATCH 124/130] minor fix for httpPostParser test --- modules/httpPostParser/test/post.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/httpPostParser/test/post.js b/modules/httpPostParser/test/post.js index 721aed2ba..5ff69b188 100644 --- a/modules/httpPostParser/test/post.js +++ b/modules/httpPostParser/test/post.js @@ -6,6 +6,9 @@ describe("HttpPostParser", function() { before(function* () { + // if app.isListening, then we can't add our middleware + should.not.exist(app.isListening); + app.use(function*(next) { if ('/test/http-post-parser' != this.path) return yield next; this.body = this.req.body; From 30c32ac928b88c2f1c80f5f84e83585c509d8c0a Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 29 Jul 2014 23:12:10 +0400 Subject: [PATCH 125/130] better test --- gulpfile.js | 5 +++++ modules/httpPostParser/test/post.js | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/gulpfile.js b/gulpfile.js index 546683636..4aaea444c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -45,6 +45,11 @@ gulp.task("app:sync-resources", lazyRequireTask('syncResources', { 'app/img': 'www/img' })); +gulp.task("app:sync-stylesheet-images", lazyRequireTask('syncStylesheetImages', { + 'app/fonts' : 'www/fonts', + 'app/img': 'www/img' +})); + gulp.task('app:sprite-once', lazyRequireTask('spriteOnce', { spritesSearchFsRoot: 'app', spritesWebRoot: '/sprites', diff --git a/modules/httpPostParser/test/post.js b/modules/httpPostParser/test/post.js index 5ff69b188..5ef4c876c 100644 --- a/modules/httpPostParser/test/post.js +++ b/modules/httpPostParser/test/post.js @@ -31,4 +31,15 @@ describe("HttpPostParser", function() { }); + it("dies when the file is too big", function(done) { + + // fixme: superagent console.warns: double callback! + // seems like a bug in superagent: https://github.com/visionmedia/superagent/issues/351 + supertest(app) + .post('/test/http-post-parser') + .send({big: new Array(1e7).join(' ')}) + .expect(413, done); + + }); + }); From 4f395112c92b95cee10e1c0b82aa088d0eb59e5a Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 29 Jul 2014 23:42:09 +0400 Subject: [PATCH 126/130] copy all images from stylesheets and sprites to i/ --- gulpfile.js | 35 ++++++++++++++++++++--------- modules/httpPostParser/test/post.js | 2 +- package.json | 1 + tasks/compileCss.js | 21 +++++++++++------ tasks/compileCssOnce.js | 19 ---------------- tasks/sprite.js | 11 ++++----- tasks/spriteOnce.js | 9 -------- tasks/syncCssImages.js | 17 ++++++++++++++ tasks/syncResources.js | 1 - 9 files changed, 62 insertions(+), 54 deletions(-) delete mode 100644 tasks/compileCssOnce.js delete mode 100644 tasks/spriteOnce.js create mode 100644 tasks/syncCssImages.js diff --git a/gulpfile.js b/gulpfile.js index 4aaea444c..e43b33a12 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -24,6 +24,16 @@ function lazyRequireTask(name) { }; } +function wrapWatch(watch, task) { + return function(callback) { + if (process.env.NODE_ENV == 'development') { + gulp.watch(watch, [task]); + } else { + callback(); + } + }; +} + gulp.task('lint-once', lazyRequireTask('lintOnce', { src: serverSources })); gulp.task('lint-or-die', lazyRequireTask('lintOnce', { src: serverSources, dieOnError: true })); @@ -45,19 +55,24 @@ gulp.task("app:sync-resources", lazyRequireTask('syncResources', { 'app/img': 'www/img' })); -gulp.task("app:sync-stylesheet-images", lazyRequireTask('syncStylesheetImages', { - 'app/fonts' : 'www/fonts', - 'app/img': 'www/img' +gulp.task("app:sync-css-images-once", lazyRequireTask('syncCssImages', { + src: 'app/stylesheets/**/*.{png,svg,gif,jpg}', + dst: 'www/i' })); -gulp.task('app:sprite-once', lazyRequireTask('spriteOnce', { +gulp.task('app:sync-css-images', ['app:sync-css-images-once'], + wrapWatch('app/stylesheets/**/*.{png,svg,gif,jpg}', 'app:sync-css-images-once') +); + + +gulp.task('app:sprite-once', lazyRequireTask('sprite', { spritesSearchFsRoot: 'app', - spritesWebRoot: '/sprites', - spritesFsDir: 'www/sprites', + spritesWebRoot: '/i', + spritesFsDir: 'www/i', styleFsDir: 'app/stylesheets/sprites' })); -gulp.task('app:sprite', ['app:sprite-once'], lazyRequireTask('sprite', { watch: "app/**/*.sprite/**"})); +gulp.task('app:sprite', ['app:sprite-once'], wrapWatch("app/**/*.sprite/**", 'sprite')); gulp.task('app:clean-compiled-css', function(callback) { fs.unlink('./www/stylesheets/base.css', function(err) { @@ -69,7 +84,7 @@ gulp.task('app:clean-compiled-css', function(callback) { // Show errors if encountered gulp.task('app:compile-css-once', ['app:clean-compiled-css'], - lazyRequireTask('compileCssOnce', { + lazyRequireTask('compileCss', { src: './app/stylesheets/base.styl', dst: './www/stylesheets' }) @@ -77,7 +92,7 @@ gulp.task('app:compile-css-once', -gulp.task('app:compile-css', ['app:compile-css-once'], lazyRequireTask('compileCss', { watch: "app/**/*.styl"})); +gulp.task('app:compile-css', ['app:compile-css-once'], wrapWatch("app/**/*.styl", "app:compile-css-once")); gulp.task("app:browserify:clean", lazyRequireTask('browserifyClean', { dst: './www/js'} )); @@ -88,7 +103,7 @@ gulp.task("app:browserify", ['app:browserify:clean'], lazyRequireTask('browserif // compile-css and sprites are independant tasks // run both or run *-once separately -gulp.task('run', ['supervisor', 'app:livereload', "app:sync-resources", 'app:compile-css', 'app:sprite', 'app:browserify']); +gulp.task('run', ['supervisor', 'app:livereload', "app:sync-resources", 'app:compile-css', 'app:sprite', 'app:browserify', 'app:sync-css-images']); // TODO: refactor me out! diff --git a/modules/httpPostParser/test/post.js b/modules/httpPostParser/test/post.js index 5ef4c876c..91257f6bf 100644 --- a/modules/httpPostParser/test/post.js +++ b/modules/httpPostParser/test/post.js @@ -9,7 +9,7 @@ describe("HttpPostParser", function() { // if app.isListening, then we can't add our middleware should.not.exist(app.isListening); - app.use(function*(next) { + app.use(function* echoBody(next) { if ('/test/http-post-parser' != this.path) return yield next; this.body = this.req.body; }); diff --git a/package.json b/package.json index 0cd0a764c..91539ce00 100755 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "co-mocha": "*", "gulp": "*", "gulp-autoprefixer": "0.0.8", + "gulp-flatten": "0.0.2", "gulp-jshint": "*", "gulp-livereload": "^2.1.0", "gulp-mocha": "^0.5.1", diff --git a/tasks/compileCss.js b/tasks/compileCss.js index 49fcfbdbb..d09c5aeb9 100644 --- a/tasks/compileCss.js +++ b/tasks/compileCss.js @@ -1,12 +1,19 @@ - const gulp = require('gulp'); +const gp = require('gulp-load-plugins')(); module.exports = function(options) { - return function(callback) { - if (process.env.NODE_ENV == 'development') { - gulp.watch(options.watch, ['app:compile-css-once']); - } else { - callback(); - } + + return function() { + + return gulp.src(options.src) + // without plumber if stylus emits PluginError, it will disappear at the next step + // plumber propagates it down the chain + .pipe(gp.plumber({errorHandler: gp.notify.onError("<%= error.message %>")})) + .pipe(gp.stylus({use: [require('nib')()]})) + .pipe(gp.autoprefixer("last 1 version")) + .pipe(gulp.dest(options.dst)); }; + + }; + diff --git a/tasks/compileCssOnce.js b/tasks/compileCssOnce.js deleted file mode 100644 index d09c5aeb9..000000000 --- a/tasks/compileCssOnce.js +++ /dev/null @@ -1,19 +0,0 @@ -const gulp = require('gulp'); -const gp = require('gulp-load-plugins')(); - -module.exports = function(options) { - - return function() { - - return gulp.src(options.src) - // without plumber if stylus emits PluginError, it will disappear at the next step - // plumber propagates it down the chain - .pipe(gp.plumber({errorHandler: gp.notify.onError("<%= error.message %>")})) - .pipe(gp.stylus({use: [require('nib')()]})) - .pipe(gp.autoprefixer("last 1 version")) - .pipe(gulp.dest(options.dst)); - }; - - -}; - diff --git a/tasks/sprite.js b/tasks/sprite.js index c3b41da60..549adf4af 100644 --- a/tasks/sprite.js +++ b/tasks/sprite.js @@ -1,12 +1,9 @@ - -const gulp = require('gulp'); +const gp = require('gulp-load-plugins')(); module.exports = function(options) { + return function(callback) { - if (process.env.NODE_ENV == 'development') { - gulp.watch(options.watch, ['app:sprite']); - } else { - callback(); - } + return gp.stylusSprite(options).apply(this, arguments); }; }; + diff --git a/tasks/spriteOnce.js b/tasks/spriteOnce.js deleted file mode 100644 index 549adf4af..000000000 --- a/tasks/spriteOnce.js +++ /dev/null @@ -1,9 +0,0 @@ -const gp = require('gulp-load-plugins')(); - -module.exports = function(options) { - - return function(callback) { - return gp.stylusSprite(options).apply(this, arguments); - }; -}; - diff --git a/tasks/syncCssImages.js b/tasks/syncCssImages.js new file mode 100644 index 000000000..fd8e06697 --- /dev/null +++ b/tasks/syncCssImages.js @@ -0,0 +1,17 @@ +const fse = require('fs-extra'); +const gp = require('gulp-load-plugins')(); +const gulp = require('gulp'); + +module.exports = function(options) { + + return function(callback) { + + fse.ensureDirSync(options.dst); + + return gulp.src(options.src) + .pipe(gp.flatten()) + .pipe(gp.newer(options.dst)) + .pipe(gulp.dest(options.dst)); + + }; +}; diff --git a/tasks/syncResources.js b/tasks/syncResources.js index c6b161c7f..7e21a24e7 100644 --- a/tasks/syncResources.js +++ b/tasks/syncResources.js @@ -5,7 +5,6 @@ module.exports = function(resources) { return function(callback) { - for (var src in resources) { var dst = resources[src]; From cd411faeb91caf0f9a2f976aae339ed43810730c Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 29 Jul 2014 23:48:24 +0400 Subject: [PATCH 127/130] readme --- db | 1 + 1 file changed, 1 insertion(+) diff --git a/db b/db index 837704482..b514d4487 100755 --- a/db +++ b/db @@ -1,2 +1,3 @@ #!/bin/bash +echo Loading the "default" site fixture. ./gulp loaddb --db fixture/db.js From f0e3a5f9d2cc209c996548a99f7628936901b820 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 30 Jul 2014 00:28:05 +0400 Subject: [PATCH 128/130] this.req.body -> this.request.body (koa-formidable) --- hmvc/getpdf/controller/checkout.js | 12 +- hmvc/payments/lib/loadOrder.js | 2 +- hmvc/payments/lib/loadTransaction.js | 2 +- .../payments/payanyway/controller/callback.js | 8 +- hmvc/payments/paypal/controller/callback.js | 26 ++-- hmvc/payments/webmoney/controller/callback.js | 14 +-- hmvc/payments/webmoney/router.js | 2 +- modules/httpPostParser/test/post.js | 2 +- modules/lib/csrf.js | 2 +- package.json | 115 +++++++++--------- 10 files changed, 92 insertions(+), 93 deletions(-) diff --git a/hmvc/getpdf/controller/checkout.js b/hmvc/getpdf/controller/checkout.js index 25143862b..5d91e5223 100644 --- a/hmvc/getpdf/controller/checkout.js +++ b/hmvc/getpdf/controller/checkout.js @@ -10,22 +10,22 @@ log.debugOn(); exports.post = function*(next) { yield* this.loadOrder(); - var method = methods[this.req.body.paymentMethod]; + var method = methods[this.request.body.paymentMethod]; if (!method) { this.throw(403, "Unsupported payment method"); } if (this.order) { log.debug("order exists", this.order.number); - yield* updateOrderFromBody(this.req.body, this.order); + yield* updateOrderFromBody(this.request.body, this.order); } else { // if we don't have the order in our database, then make a new one // (use the incoming order post for that, but don't trust it) - console.log(this.req.body.orderTemplate); + console.log(this.request.body.orderTemplate); var orderTemplate = yield OrderTemplate.findOne({ - slug: this.req.body.orderTemplate + slug: this.request.body.orderTemplate }).exec(); if (!orderTemplate) { @@ -36,10 +36,10 @@ exports.post = function*(next) { // create order from template, don't trust the incoming post this.order = Order.createFromTemplate(orderTemplate, { module: 'getpdf', - email: this.req.body.email + email: this.request.body.email }); - yield* updateOrderFromBody(this.req.body, this.order); + yield* updateOrderFromBody(this.request.body, this.order); log.debug("order created", this.order.number); diff --git a/hmvc/payments/lib/loadOrder.js b/hmvc/payments/lib/loadOrder.js index 1b4672254..f71e937ca 100644 --- a/hmvc/payments/lib/loadOrder.js +++ b/hmvc/payments/lib/loadOrder.js @@ -7,7 +7,7 @@ module.exports = function* (field) { if (!field) field = 'orderNumber'; - var orderNumber = this.req.body && this.req.body[field] || this.params[field] || this.query[field]; + var orderNumber = this.request.body && this.request.body[field] || this.params[field] || this.query[field]; if (!orderNumber) { return; diff --git a/hmvc/payments/lib/loadTransaction.js b/hmvc/payments/lib/loadTransaction.js index f46c1786e..2603a7c34 100644 --- a/hmvc/payments/lib/loadTransaction.js +++ b/hmvc/payments/lib/loadTransaction.js @@ -9,7 +9,7 @@ module.exports = function* (field, options) { options = options || {}; if (!field) field = 'transactionNumber'; - var transactionNumber = this.req.body && this.req.body[field] || this.params[field] || this.query[field]; + var transactionNumber = this.request.body && this.request.body[field] || this.params[field] || this.query[field]; log.debug('tx number: ' + transactionNumber); if (!transactionNumber) { diff --git a/hmvc/payments/payanyway/controller/callback.js b/hmvc/payments/payanyway/controller/callback.js index 4124855cb..c0d80df54 100644 --- a/hmvc/payments/payanyway/controller/callback.js +++ b/hmvc/payments/payanyway/controller/callback.js @@ -10,7 +10,7 @@ log.debugOn(); exports.post = function* (next) { - checkSignature(this.req.body); + checkSignature(this.request.body); this.body = 'SUCCESS'; return; @@ -19,7 +19,7 @@ exports.post = function* (next) { yield this.transaction.logRequest('callback unverified', this.request); - if (!checkSignature(this.req.body)) { + if (!checkSignature(this.request.body)) { log.debug("wrong signature"); this.throw(403, "wrong signature"); } @@ -27,8 +27,8 @@ exports.post = function* (next) { yield this.transaction.logRequest('callback', this.request); // signature is valid, so everything MUST be fine - if (this.transaction.amount != parseFloat(this.req.body.MNT_AMOUNT) || - this.req.body.MNT_ID != payanywayConfig.id) { + if (this.transaction.amount != parseFloat(this.request.body.MNT_AMOUNT) || + this.request.body.MNT_ID != payanywayConfig.id) { yield this.transaction.persist({ status: Transaction.STATUS_FAIL, statusMessage: "данные транзакции не совпадают с базой, свяжитесь с поддержкой" diff --git a/hmvc/payments/paypal/controller/callback.js b/hmvc/payments/paypal/controller/callback.js index 2771f0f9b..8cc387785 100644 --- a/hmvc/payments/paypal/controller/callback.js +++ b/hmvc/payments/paypal/controller/callback.js @@ -23,8 +23,8 @@ exports.post = function* (next) { 'cmd': '_notify-validate' }; - for (var field in this.req.body) { - qs[field] = this.req.body[field]; + for (var field in this.request.body) { + qs[field] = this.request.body[field]; } // request oauth token @@ -50,9 +50,9 @@ exports.post = function* (next) { } // ipn is verified now! But we check if it's data matches the transaction (as recommended in docs) - if (this.transaction.amount != parseFloat(this.req.body.mc_gross) || - this.req.body.receiver_email != paypalConfig.email || - this.req.body.mc_currency != config.payments.currency) { + if (this.transaction.amount != parseFloat(this.request.body.mc_gross) || + this.request.body.receiver_email != paypalConfig.email || + this.request.body.mc_currency != config.payments.currency) { yield this.transaction.persist({ status: Transaction.STATUS_FAIL, @@ -70,8 +70,8 @@ exports.post = function* (next) { transaction: this.transaction._id }).sort({created: -1}).exec(); - if (previousIpn && previousIpn.data.payment_status == this.req.body.payment_status) { - yield this.transaction.log("ipn duplicate", this.req.body); + if (previousIpn && previousIpn.data.payment_status == this.request.body.payment_status) { + yield this.transaction.log("ipn duplicate", this.request.body); // ignore duplicate this.body = ''; return; @@ -80,18 +80,18 @@ exports.post = function* (next) { // now we have a valid non-duplicate IPN, let's update the transaction // log it right now to evade conflicts with duplicates - yield this.transaction.log("ipn", this.req.body); + yield this.transaction.log("ipn", this.request.body); // Do not perform any processing on WPS transactions here that do not have // transaction IDs, indicating they are non-payment IPNs such as those used // for subscription signup requests. - if (!this.req.body.txn_id) { - yield this.transaction.log("ipn without txn_id", this.req.body); + if (!this.request.body.txn_id) { + yield this.transaction.log("ipn without txn_id", this.request.body); this.body = ''; return; } - switch(this.req.body.payment_status) { + switch(this.request.body.payment_status) { case 'Failed': case 'Voided': yield this.transaction.persist({ @@ -102,7 +102,7 @@ exports.post = function* (next) { case 'Pending': yield this.transaction.persist({ status: Transaction.STATUS_PENDING, - statusMessage: this.req.body.pending_reason + statusMessage: this.request.body.pending_reason }); this.body = ''; return; @@ -116,7 +116,7 @@ exports.post = function* (next) { return; default: // Refunded ... - yield this.transaction.log("ipn payment_status unknown", this.req.body); + yield this.transaction.log("ipn payment_status unknown", this.request.body); this.body = ''; return; diff --git a/hmvc/payments/webmoney/controller/callback.js b/hmvc/payments/webmoney/controller/callback.js index 310828faa..84d4545fb 100644 --- a/hmvc/payments/webmoney/controller/callback.js +++ b/hmvc/payments/webmoney/controller/callback.js @@ -16,10 +16,10 @@ exports.prerequest = function* (next) { yield this.transaction.logRequest('prerequest', this.request); if (this.transaction.status == Transaction.STATUS_SUCCESS || - this.transaction.amount != parseFloat(this.req.body.LMI_PAYMENT_AMOUNT) || - this.req.body.LMI_PAYEE_PURSE != webmoneyConfig.purse + this.transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) || + this.request.body.LMI_PAYEE_PURSE != webmoneyConfig.purse ) { - log.debug("no pending transaction " + this.req.body.LMI_PAYMENT_NO); + log.debug("no pending transaction " + this.request.body.LMI_PAYMENT_NO); this.throw(404, 'unfinished transaction with given params not found'); } @@ -31,15 +31,15 @@ exports.post = function* (next) { yield* this.loadTransaction('LMI_PAYMENT_NO', {skipOwnerCheck : true}); - if (!checkSignature(this.req.body)) { + if (!checkSignature(this.request.body)) { log.debug("wrong signature"); this.throw(403, "wrong signature"); } yield this.transaction.logRequest('callback', this.request); - if (this.transaction.amount != parseFloat(this.req.body.LMI_PAYMENT_AMOUNT) || - this.req.body.LMI_PAYEE_PURSE != webmoneyConfig.purse) { + if (this.transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) || + this.request.body.LMI_PAYEE_PURSE != webmoneyConfig.purse) { // STRANGE, signature is correct yield this.transaction.persist({ status: Transaction.STATUS_FAIL, @@ -48,7 +48,7 @@ exports.post = function* (next) { this.throw(404, "transaction data doesn't match the POST body"); } - if (!this.req.body.LMI_SIM_MODE || this.req.body.LMI_SIM_MODE == '0') { + if (!this.request.body.LMI_SIM_MODE || this.request.body.LMI_SIM_MODE == '0') { this.transaction.status = Transaction.STATUS_SUCCESS; yield this.transaction.persist(); } diff --git a/hmvc/payments/webmoney/router.js b/hmvc/payments/webmoney/router.js index ddd8ba7bc..adcb5361e 100644 --- a/hmvc/payments/webmoney/router.js +++ b/hmvc/payments/webmoney/router.js @@ -9,7 +9,7 @@ var fail = require('./controller/fail'); // webmoney server posts here (in background) router.post('/callback', function* (next) { - if (this.req.body.LMI_PREREQUEST == '1') { + if (this.request.body.LMI_PREREQUEST == '1') { yield* callback.prerequest.call(this, next); } else { yield* callback.post.call(this, next); diff --git a/modules/httpPostParser/test/post.js b/modules/httpPostParser/test/post.js index 91257f6bf..b58572fcb 100644 --- a/modules/httpPostParser/test/post.js +++ b/modules/httpPostParser/test/post.js @@ -11,7 +11,7 @@ describe("HttpPostParser", function() { app.use(function* echoBody(next) { if ('/test/http-post-parser' != this.path) return yield next; - this.body = this.req.body; + this.body = this.request.body; }); yield app.run(); diff --git a/modules/lib/csrf.js b/modules/lib/csrf.js index 9f9e5ef72..dfc7c0447 100644 --- a/modules/lib/csrf.js +++ b/modules/lib/csrf.js @@ -45,7 +45,7 @@ Csrf.prototype.middleware = function() { } if (checkCsrf) { - this.assertCSRF(this.req.body); + this.assertCSRF(this.request.body); } yield* next; diff --git a/package.json b/package.json index 91539ce00..f15eaf84a 100755 --- a/package.json +++ b/package.json @@ -11,101 +11,100 @@ }, "precommit": "NODE_ENV=development node --harmony `which gulp` pre-commit", "dependencies": { - "MD5": "^1.2.1", - "bluebird": "^2.2.2", + "MD5": "*", "body-parser": "*", - "brfs": "^1.1.2", + "brfs": "*", "co": "*", - "escape-html": "^1.0.1", - "event-stream": "^3.1.5", - "factor-bundle": "^1.0.0", - "fs-extra": "^0.10.0", - "glob": "^4.0.4", - "gm": "^1.16.0", - "gulp-cache": "^0.2.0", - "gulp-concat": "^2.3.3", - "gulp-debug": "^0.3.0", - "gulp-dir-sync": "^0.1.1", - "gulp-if": "^1.2.2", - "gulp-ignore": "^1.1.0", + "escape-html": "*", + "event-stream": "*", + "factor-bundle": "*", + "fs-extra": "*", + "glob": "*", + "gm": "*", + "gulp-cache": "*", + "gulp-concat": "*", + "gulp-debug": "*", + "gulp-dir-sync": "*", + "gulp-if": "*", + "gulp-ignore": "*", "gulp-jshint-cache": "*", - "gulp-load-plugins": "^0.5.3", - "gulp-newer": "^0.3.0", - "gulp-notify": "^1.4.0", - "gulp-plumber": "^0.6.3", - "gulp-rimraf": "^0.1.0", - "gulp-stylus": "^1.1.0", + "gulp-load-plugins": "*", + "gulp-newer": "*", + "gulp-notify": "*", + "gulp-plumber": "*", + "gulp-rimraf": "*", + "gulp-stylus": "*", "gulp-util": "*", - "gulp.spritesmith": "^1.1.1", - "imagemin": "^0.4.6", - "imagemin-pngcrush": "^0.1.0", - "imagemin-svgo": "^0.1.0", + "gulp.spritesmith": "*", + "imagemin": "*", + "imagemin-pngcrush": "*", + "imagemin-svgo": "*", "jade": "*", "javascript-gulp-task-lint": "*", "javascript-parser": "*", - "jquery": "^2.1.1", - "js-log": "^0.2.2", + "jquery": "*", + "js-log": "*", "koa": "*", "koa-bodyparser": "*", - "koa-compose": "^2.3.0", - "koa-csrf": "^2.1.2", + "koa-compose": "*", + "koa-csrf": "*", "koa-favicon": "*", - "koa-formidable": "^0.1.0", + "koa-formidable": "*", "koa-generic-session": "*", "koa-logger": "*", - "koa-mount": "^1.3.0", - "koa-passport": "^0.5.1", - "koa-request": "^1.0.0", + "koa-mount": "*", + "koa-passport": "*", + "koa-request": "*", "koa-router": "*", - "koa-send": "^1.2.4", + "koa-send": "*", "koa-session-mongoose": "*", "koa-static": "*", "koa-views": "*", "lodash": "*", "map-stream": "*", - "moment": "^2.7.0", + "moment": "*", "mongoose": "3.8", - "mongoose-auto-increment": "^3.0.8", + "mongoose-auto-increment": "*", "mongoose-troop": "git://github.com/iliakan/mongoose-troop", - "nib": "^1.0.3", - "nodemailer": "^1.0.4", - "nodemailer-ses-transport": "^0.1.1", + "nib": "*", + "nodemailer": "*", + "nodemailer-ses-transport": "*", "passport": "*", - "passport-local": "^1.0.0", - "path-to-regexp": "^0.2.3", + "passport-local": "*", + "path-to-regexp": "*", "prismjs": "git://github.com/LeaVerou/prism#gh-pages", "stylus": "*", - "svgutils": "^0.7.0", - "through2": "^0.5.1", + "svgutils": "*", + "through2": "*", "thunkify": "*", - "vinyl-fs": "^0.3.4", - "vinyl-source-stream": "^0.1.1", + "vinyl-fs": "*", + "vinyl-source-stream": "*", "winston": "*", - "yargs": "^1.2.6" + "yargs": "*" }, "devDependencies": { - "browserify": "^5.9.1", + "browserify": "*", "clarify": "*", "co-mocha": "*", "gulp": "*", - "gulp-autoprefixer": "0.0.8", - "gulp-flatten": "0.0.2", + "gulp-autoprefixer": "*", + "gulp-flatten": "*", "gulp-jshint": "*", - "gulp-livereload": "^2.1.0", - "gulp-mocha": "^0.5.1", - "gulp-sourcemaps": "^1.1.0", + "gulp-livereload": "*", + "gulp-mocha": "*", + "gulp-sourcemaps": "*", "gulp-stylus-sprite": "*", - "gulp-supervisor": "^0.1.2", - "lazypipe": "^0.2.1", + "gulp-supervisor": "*", + "lazypipe": "*", "mocha": "*", - "node-notifier": "^3.1.1", + "node-notifier": "*", "should": "*", "sinon": "*", - "superagent": "^0.18.2", - "supertest": "^0.13.0", + "superagent": "*", + "supertest": "*", "supervisor": "*", "trace": "*", - "watchify": "^1.0.1" + "watchify": "*" }, "engines": { "node": ">=0.11.13" From 7ca6e251984a50fbd4ece0a4184802fcf53ac371 Mon Sep 17 00:00:00 2001 From: Shuvalov Anton Date: Wed, 30 Jul 2014 00:28:38 +0400 Subject: [PATCH 129/130] add some broken tests for auth --- hmvc/auth/test/auth.js | 65 +++++++++++++++++++++++++++++++++++ hmvc/auth/test/fixtures/db.js | 26 ++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 hmvc/auth/test/auth.js create mode 100644 hmvc/auth/test/fixtures/db.js diff --git a/hmvc/auth/test/auth.js b/hmvc/auth/test/auth.js new file mode 100644 index 000000000..6941f3909 --- /dev/null +++ b/hmvc/auth/test/auth.js @@ -0,0 +1,65 @@ +/* globals describe, it, before */ + +const db = require('lib/dataUtil'); +const mongoose = require('mongoose'); +const path = require('path'); +const request = require('supertest'); +const fixtures = require(path.join(__dirname, './fixtures/db')); +const app = require('app'); +const assert = require('better-assert'); + +var sessionId = ''; + +describe('Authorization', function() { + before(function * () { + yield db.loadDb(path.join(__dirname, './fixtures/db')); + yield app.run(); + }); + describe('login', function () { + it('should log me in', function (done) { + request(app) + .post('/auth/login/local') + .send({ + email: fixtures.User[0].email, + password: fixtures.User[0].password + }) + .expect(200) + .end(function (err, res) { + console.log(res.headers); + // sessionId = res.headers['set-cookie'][0]; + done(err); + }); + }); + it('should return current user info', function (done) { + request(app) + .get('/api/auth/user') + .set('Cookie', sessionId) + .expect(200) + .end(function (err, res) { + assert('object' === typeof res.body.user); + assert(db[0].email === res.body.user.email); + done(err); + }); + }); + }); + describe('logout', function () { + it('should log me out', function (done) { + request(app) + .get('/api/logout') + .set('Cookie', sessionId) + .expect(200) + .end(function (err, res) { + done(err); + }); + }); + it('should return error because session is incorrected', function (done) { + request(app) + .get('/api/auth/user') + .set('Cookie', sessionId) + .expect(401) + .end(function (err, res) { + done(err); + }); + }); + }); +}); \ No newline at end of file diff --git a/hmvc/auth/test/fixtures/db.js b/hmvc/auth/test/fixtures/db.js new file mode 100644 index 000000000..bdfad8093 --- /dev/null +++ b/hmvc/auth/test/fixtures/db.js @@ -0,0 +1,26 @@ +exports.User = [ + { "_id": "000000000000000000000001", + "created": new Date(2014,0,1), + "username": "ilya kantor", + "email": "iliakan@gmail.com", + "password": "123", + "avatar": "1.jpg", + "following": [] + }, + { "_id": "000000000000000000000002", + "created": new Date(2014,0,1), + "username": "tester", + "email": "tester@mail.com", + "password": "123", + "avatar": "2.jpg", + "following": ["000000000000000000000001"] + }, + { "_id": "000000000000000000000003", + "created": new Date(2014,0,1), + "username": "vasya", + "email": "vasya@mail.com", + "password": "123", + "avatar": "3.jpg", + "following": ["000000000000000000000001", "000000000000000000000002"] + } +]; \ No newline at end of file From 70e64ab6b22952c4d3924f33a801884299181e9e Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 30 Jul 2014 01:34:11 +0400 Subject: [PATCH 130/130] 2 auth tests, more pending --- hmvc/auth/controller/login/local.js | 3 +- hmvc/auth/controller/user.js | 13 ++++++ hmvc/auth/lib/passport.js | 1 - hmvc/auth/router.js | 2 + hmvc/auth/test/auth.js | 62 ++++++++++++++++------------- hmvc/auth/test/fixtures/db.js | 12 ++---- package.json | 1 + 7 files changed, 54 insertions(+), 40 deletions(-) create mode 100644 hmvc/auth/controller/user.js diff --git a/hmvc/auth/controller/login/local.js b/hmvc/auth/controller/login/local.js index a522c22b0..afb018313 100644 --- a/hmvc/auth/controller/login/local.js +++ b/hmvc/auth/controller/login/local.js @@ -1,11 +1,10 @@ var passport = require('koa-passport'); exports.post = function*(next) { - console.log("HERE"); var ctx = this; yield passport.authenticate('local', function*(err, user, info) { // missing credentials ?!? - console.log("HERE 2", err, user, info); +// console.log("HERE 2", err, user, info); if (err) throw err; if (user === false) { ctx.status = 401; diff --git a/hmvc/auth/controller/user.js b/hmvc/auth/controller/user.js new file mode 100644 index 000000000..8b8b2acdb --- /dev/null +++ b/hmvc/auth/controller/user.js @@ -0,0 +1,13 @@ + +exports.get = function *get (next) { + + this.body = { + username: this.req.user.username, + email: this.req.user.email, + created: this.req.user.created + }; + + +}; + + diff --git a/hmvc/auth/lib/passport.js b/hmvc/auth/lib/passport.js index c4383dd91..02bf0b384 100644 --- a/hmvc/auth/lib/passport.js +++ b/hmvc/auth/lib/passport.js @@ -17,7 +17,6 @@ passport.use(new LocalStrategy({ usernameField: 'email', passwordField: 'password' }, function(email, password, done) { - console.log("!!!!", email, password); if (!email) return done(null, false, { message: 'Please provide email.' }); if (!password) return done(null, false, { message: 'Please provide password.' }); diff --git a/hmvc/auth/router.js b/hmvc/auth/router.js index 253eeb0b8..dc44f0a68 100644 --- a/hmvc/auth/router.js +++ b/hmvc/auth/router.js @@ -1,11 +1,13 @@ var Router = require('koa-router'); var form = require('./controller/form'); +var user = require('./controller/user'); var local = require('./controller/login/local'); var logout = require('./controller/logout'); var router = module.exports = new Router(); router.get('/form', form.get); +router.get('/user', user.get); router.post('/login/local', local.post); diff --git a/hmvc/auth/test/auth.js b/hmvc/auth/test/auth.js index 6941f3909..66610fe41 100644 --- a/hmvc/auth/test/auth.js +++ b/hmvc/auth/test/auth.js @@ -1,65 +1,71 @@ /* globals describe, it, before */ -const db = require('lib/dataUtil'); -const mongoose = require('mongoose'); -const path = require('path'); -const request = require('supertest'); -const fixtures = require(path.join(__dirname, './fixtures/db')); -const app = require('app'); -const assert = require('better-assert'); - -var sessionId = ''; +const db = require('lib/dataUtil'); +const mongoose = require('mongoose'); +const path = require('path'); +const request = require('supertest'); +const fixtures = require(path.join(__dirname, './fixtures/db')); +const app = require('app'); +const assert = require('better-assert'); describe('Authorization', function() { + + var agent; before(function * () { yield db.loadDb(path.join(__dirname, './fixtures/db')); yield app.run(); + + agent = request.agent(app); + }); - describe('login', function () { - it('should log me in', function (done) { - request(app) + + describe('login', function() { + + it('should log me in', function(done) { + agent .post('/auth/login/local') .send({ - email: fixtures.User[0].email, + email: fixtures.User[0].email, password: fixtures.User[0].password }) .expect(200) - .end(function (err, res) { - console.log(res.headers); + .end(function(err, res) { +// console.log(res.headers); // sessionId = res.headers['set-cookie'][0]; done(err); }); }); - it('should return current user info', function (done) { - request(app) - .get('/api/auth/user') - .set('Cookie', sessionId) + + it('should return current user info', function(done) { + agent + .get('/auth/user') .expect(200) - .end(function (err, res) { - assert('object' === typeof res.body.user); - assert(db[0].email === res.body.user.email); + .end(function(err, res) { + res.body.email.should.be.eql(fixtures.User[0].email); done(err); }); }); + }); - describe('logout', function () { - it('should log me out', function (done) { + + 0 && describe('logout', function() { + it('should log me out', function(done) { request(app) .get('/api/logout') .set('Cookie', sessionId) .expect(200) - .end(function (err, res) { + .end(function(err, res) { done(err); }); }); - it('should return error because session is incorrected', function (done) { + it('should return error because session is incorrected', function(done) { request(app) .get('/api/auth/user') .set('Cookie', sessionId) .expect(401) - .end(function (err, res) { + .end(function(err, res) { done(err); }); }); }); -}); \ No newline at end of file +}); diff --git a/hmvc/auth/test/fixtures/db.js b/hmvc/auth/test/fixtures/db.js index bdfad8093..fe8fba922 100644 --- a/hmvc/auth/test/fixtures/db.js +++ b/hmvc/auth/test/fixtures/db.js @@ -3,24 +3,18 @@ exports.User = [ "created": new Date(2014,0,1), "username": "ilya kantor", "email": "iliakan@gmail.com", - "password": "123", - "avatar": "1.jpg", - "following": [] + "password": "123" }, { "_id": "000000000000000000000002", "created": new Date(2014,0,1), "username": "tester", "email": "tester@mail.com", - "password": "123", - "avatar": "2.jpg", - "following": ["000000000000000000000001"] + "password": "123" }, { "_id": "000000000000000000000003", "created": new Date(2014,0,1), "username": "vasya", "email": "vasya@mail.com", - "password": "123", - "avatar": "3.jpg", - "following": ["000000000000000000000001", "000000000000000000000002"] + "password": "123" } ]; \ No newline at end of file diff --git a/package.json b/package.json index f15eaf84a..098f65036 100755 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "yargs": "*" }, "devDependencies": { + "better-assert": "^1.0.1", "browserify": "*", "clarify": "*", "co-mocha": "*",
`, это стандартная проверка при делегировании. +
  • Лишними для нас будут `mouseover/mouseout`, связанные с переходом на дочерние элементы внутри `
  • `. Проверить их можно по `event.target/event.relatedTarget`. + + +Детали кода вы можете посмотреть в примере ниже, который демонстрирует этот подход: + +[example src="tutorial/browser/events/mouseenter-mouseleave-delegation-2"] + +Попробуйте по-разному, быстро или медленно заходить и выходить в ячейки таблицы. Обработчики `mouseover/mouseout` стоят на `table`, но при помощи делегирования корректно обрабатывают вход-выход. + +## Особенности IE8- + +В IE8- нет свойства `relatedTarget`. Вместо него используется `fromElement` для `mouseover` и `toElement` для `mouseout`. + +Можно "исправить" несовместимость с `relatedTarget` так: +[js] +function fixRelatedTarget(e) { + if (e.relatedTarget === undefined) { + if (e.type == 'mouseover') e.relatedTarget = e.fromElement; + if (e.type == 'mouseout') e.relatedTarget = e.toElement; + } +} +[/js] + + +## Итого + +**У `mouseover, mousemove, mouseout` есть следующие особенности:** +
      +
    1. События `mouseover` и `mouseout` -- единственные, у которых есть вторая цель: `relatedTarget` (`toElement/fromElement` в IE).
    2. +
    3. Событие `mouseout` срабатывает, когда мышь уходит с родительского элемента на дочерний. Используйте `mouseenter/mouseleave` или фильтруйте их, чтобы избежать излишнего реагирования.
    4. +
    5. При быстром движении мыши события `mouseover, mousemove, mouseout` могут пропускать промежуточные элементы. Мышь может моментально возникнуть над потомком, миновав при этом его родителя.
    6. +
    + + +[task id=830] +[task id=172] + +[task id=608] + +[task id=609] + +[head] + + + + +[/head] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/01-track-cursor-movements.task/question.md b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/01-track-cursor-movements.task/question.md new file mode 100644 index 000000000..30ca50aba --- /dev/null +++ b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/01-track-cursor-movements.task/question.md @@ -0,0 +1,9 @@ +# Как отследить прекращение движения курсора? + +[importance 5] + +Представим ситуацию -- посетитель быстро проводит мышью над элементами и останавливается на интересном ему. + +Нам надо запустить код (например открыть пункт меню) для того, на котором он остановился, а те элементы, над которыми он быстро провёл мышь, но на которых не остановился -- игнорировать, даже если события мыши на них произошли. + +Как бы вы решали такую задачу? diff --git a/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/01-track-cursor-movements.task/solution.md b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/01-track-cursor-movements.task/solution.md new file mode 100644 index 000000000..46e823fd4 --- /dev/null +++ b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/01-track-cursor-movements.task/solution.md @@ -0,0 +1,7 @@ +Самый простой способ решения этой задачи -- замерять скорость движения курсора. То есть, при `mousemove` вычислять расстояние между текущими координатами и предыдущими, а затем делить на разницу во времени. + +Когда скорость будет очень маленькой, например 5 пикселей за 100 миллисекунд -- можно считать, что курсор остановился (некоторое дрожание может присутствовать) и обработать этот факт. + +Обработчик `mousemove` может стоять на всём документе, либо на контейнере, который включает в себя интересующие нас элементы. + + diff --git a/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/question.code/index.html b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/question.code/index.html new file mode 100755 index 000000000..48f938755 --- /dev/null +++ b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/question.code/index.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/question.md b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/question.md new file mode 100644 index 000000000..aae0e0ba8 --- /dev/null +++ b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/question.md @@ -0,0 +1,19 @@ +# Выделение кнопки при заходе и клике + +[importance 5] + +Создайте кнопку, которая выделяется при проходе курсора мыши над ней и при клике. + +Кликните на демке ниже, чтобы увидеть, о чем речь: + +[iframe src="solution"] + +**Сделайте два варианта решения:** +
      +
    1. первый пусть использует CSS-селекторы `:hover` и `:active`
    2. +
    3. второй -- JavaScript-события вместо них.
    4. +
    + +Есть ли разница? + +[edit src="question" task/] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/solution.code/index.html b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/solution.code/index.html new file mode 100755 index 000000000..983507899 --- /dev/null +++ b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/solution.code/index.html @@ -0,0 +1,50 @@ + + + + + + + +
    + + + + + + diff --git a/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/solution.code/rollover-css/index.html b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/solution.code/rollover-css/index.html new file mode 100755 index 000000000..c8ad9c46d --- /dev/null +++ b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/solution.code/rollover-css/index.html @@ -0,0 +1,32 @@ + + + + + + + + +

    DIV:

    + +
    + +

    A:

    + + + + + diff --git a/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/solution.code/rollover/index.html b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/solution.code/rollover/index.html new file mode 100755 index 000000000..6462ae859 --- /dev/null +++ b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/solution.code/rollover/index.html @@ -0,0 +1,50 @@ + + + + + + + +
    + + + + + diff --git a/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/solution.md b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/solution.md new file mode 100644 index 000000000..d8e999440 --- /dev/null +++ b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/02-rollover.task/solution.md @@ -0,0 +1,22 @@ +# CSS-решение + +Для HTML можно использовать элементы `INPUT`, `BUTTON`, либо просто `DIV`. На последнем и остановимся: + +[html] +
    +[/html] +Решение при помощи CSS использует псевдоклассы `:hover` и `active`. + +Состояния объединены в CSS-спрайт, нужная часть которого подставляется при помощи `background-position`. Это важно, так как иначе при проведении курсора над мышкой соответствующая картинка может долго подгружаться, и реакция задержится. + +По возможности, стоит пойти ещё дальше и сделать вообще без картинок, через стили. + +[edit src="solution"/] + +# JavaScript-решение + +Для отслеживания действий посетителя в случае с JavaScript -- нужны события `mouseover`, `mouseout`, которые будут отслеживать состояние "курсор над кнопкой" (hover), а также `mousedown` и `mouseup` для состояния "кнопка нажата". + +Это сложнее, такое решение стоит использовать в тех случаях, когда CSS почему-то не подходит, например нужна сложная анимация или другие JavaScript-действия. + +[edit src="solution"]Открыть в песочнице[/edit] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/question.code/index.html b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/question.code/index.html new file mode 100755 index 000000000..6a3b81149 --- /dev/null +++ b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/question.code/index.html @@ -0,0 +1,72 @@ + + + + + + + + +

    ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя

    +

    ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя

    + +Короткая ссылка +Еще ссылка + +

    Прокрутите страницу и проверьте, правильно ли показывается подсказка

    + + + + + + diff --git a/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/question.md b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/question.md new file mode 100644 index 000000000..0245c18e5 --- /dev/null +++ b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/question.md @@ -0,0 +1,35 @@ +# Поведение "подсказка" + +[importance 5] + +Напишите JS-код, который будет показывать всплывающую подсказку над элементом, если у него есть атрибут `data-tooltip`. + +Например, две ссылки: +[html] + + Короткая ссылка + + + + Еще ссылка + +[/html] + +Результат в ифрейме с документом: + +[iframe src="solution" height=200 border=1] + +
      +
    • Подсказка должна появляться при наведении на элемент, по центру и на небольшом расстоянии сверху. При уходе курсора с элемента -- исчезать.
    • +
    • Текст подсказки брать из значения атрибута `data-tooltip`. Это может быть произвольный HTML.
    • +
    • Оформление подсказки должно задаваться CSS.
    • +
    • Подсказка не должна вылезать за границы экрана, в том числе если страница частично прокручена. Если нельзя показать сверху -- показывать снизу элемента.
    • +
    + +**Если хотите -- в этой задаче для простоты можно считать, что у элемента, на котором "висит" подсказка, нет детей.** + +В исходном документе есть вспомогательные функции: [](#getPageScroll) для определения текущей прокрутки страницы и [](#getCoords) -- для определения координат элемента относительно страницы. + +[edit task src="question"/]. + +P.S. Эта задача -- на практическое применение ["Шаблона проектирования \"поведение\""](article:607). \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/solution.code/behavior-tooltip/index.html b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/solution.code/behavior-tooltip/index.html new file mode 100755 index 000000000..81ad0166e --- /dev/null +++ b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/solution.code/behavior-tooltip/index.html @@ -0,0 +1,124 @@ + + + + + + + + +

    ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя

    +

    ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя

    + +Короткая ссылка +Еще ссылка + +

    Прокрутите страницу и проверьте, правильно ли показывается подсказка

    + + + + + + diff --git a/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/solution.code/index.html b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/solution.code/index.html new file mode 100755 index 000000000..81ad0166e --- /dev/null +++ b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/solution.code/index.html @@ -0,0 +1,124 @@ + + + + + + + + +

    ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя

    +

    ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя ЛяЛяЛя

    + +Короткая ссылка +Еще ссылка + +

    Прокрутите страницу и проверьте, правильно ли показывается подсказка

    + + + + + + diff --git a/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/solution.md b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/solution.md new file mode 100644 index 000000000..330005220 --- /dev/null +++ b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/03-behavior-tooltip.task/solution.md @@ -0,0 +1 @@ +[edit src="solution"/] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/question.code/index.html b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/question.code/index.html new file mode 100755 index 000000000..b573bc2a3 --- /dev/null +++ b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/question.code/index.html @@ -0,0 +1,95 @@ + + + + + + + + + +
    +
    + +

    Жили-были на свете три поросёнка. Три брата.

    + +

    Все одинакового роста, кругленькие, розовые, с одинаковыми весёлыми хвостиками.

    + +

    Даже имена у них были похожи. Звали поросят Ниф-Ниф, Нуф-Нуф и Наф-Наф. Всё лето они кувыркались в зелёной траве, грелись на солнышке, нежились в лужах.

    + +

    Но вот наступила осень. Наведи на меня

    + +
    + + + + + diff --git a/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/question.md b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/question.md new file mode 100644 index 000000000..dd17a7615 --- /dev/null +++ b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/question.md @@ -0,0 +1,25 @@ +# Поведение "вложенная подсказка" + +[importance 5] + +Напишите JS-код, который будет показывать всплывающую подсказку над элементом, если у него есть атрибут `data-tooltip`. + +Условие аналогично задаче [](task:608), но здесь необходима поддержка вложенных элементов. При наведении показывается самая вложенная подсказка. + +Например: +[html] + +[/html] + +Результат в ифрейме с документом: + +[iframe src="solution" height=300 border=1] + +Исходный документ содержит вспомогательные функции [](#getPageScroll) и [](#getCoords). +Вы также можете использовать как заготовку решение задачи [](task:608). + +[edit src="question" task/] diff --git a/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/solution.code/behavior-tooltip-nested/index.html b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/solution.code/behavior-tooltip-nested/index.html new file mode 100755 index 000000000..cf4e585dd --- /dev/null +++ b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/solution.code/behavior-tooltip-nested/index.html @@ -0,0 +1,170 @@ + + + + + + + + + +
    +
    + +

    Жили-были на свете три поросёнка. Три брата.

    + +

    Все одинакового роста, кругленькие, розовые, с одинаковыми весёлыми хвостиками.

    + +

    Даже имена у них были похожи. Звали поросят Ниф-Ниф, Нуф-Нуф и Наф-Наф. Всё лето они кувыркались в зелёной траве, грелись на солнышке, нежились в лужах.

    + +

    Но вот наступила осень. Наведи на меня

    + +
    + + + + + diff --git a/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/solution.code/index.html b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/solution.code/index.html new file mode 100755 index 000000000..cf4e585dd --- /dev/null +++ b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/solution.code/index.html @@ -0,0 +1,170 @@ + + + + + + + + + +
    +
    + +

    Жили-были на свете три поросёнка. Три брата.

    + +

    Все одинакового роста, кругленькие, розовые, с одинаковыми весёлыми хвостиками.

    + +

    Даже имена у них были похожи. Звали поросят Ниф-Ниф, Нуф-Нуф и Наф-Наф. Всё лето они кувыркались в зелёной траве, грелись на солнышке, нежились в лужах.

    + +

    Но вот наступила осень. Наведи на меня

    + +
    + + + + + diff --git a/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/solution.md b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/solution.md new file mode 100644 index 000000000..330005220 --- /dev/null +++ b/tutorial/02-ui/03-event-details/03-mousemove-mouseover-mouseout-mouseenter-mouseleave/04-behavior-nested-tooltip.task/solution.md @@ -0,0 +1 @@ +[edit src="solution"/] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/04-mousewheel.md b/tutorial/02-ui/03-event-details/04-mousewheel.md new file mode 100644 index 000000000..7b3630773 --- /dev/null +++ b/tutorial/02-ui/03-event-details/04-mousewheel.md @@ -0,0 +1,96 @@ +# Мышь: колёсико, событие wheel + +Колёсико мыши используется редко. Оно есть даже не у всех мышей. Поэтому существуют пользователи, которые в принципе не могут сгенерировать такое событие. + +...Но, тем не менее, его использование может быть оправдано. Например, можно добавить дополнительные удобства для тех, у кого колёсико есть. +[cut] +## Отличия колёсика от прокрутки + +Несмотря на то, что колёсико мыши обычно ассоциируется с прокруткой, это совсем разные вещи. + +
      +
    • При прокрутке срабатывает событие [onscroll](article:380) -- рассмотрим его в дальнейшем. Оно произойдёт *при любой прокрутке*, в том числе через клавиатурy, но *только на прокручиваемых элементах*. Например, элемент с `overflow: hidden` в принципе не может сгенерировать `onscroll`.
    • +
    • А событие `wheel` является чисто "мышиным". Оно генерируется *над любым элементом* при передвижении колеса мыши. При этом не важно, прокручиваемый он или нет. В частности, `overflow:hidden` никак не препятствует обработке колеса мыши.
    • +
    + +Кроме того, событие `onscroll` происходит после прокрутки, а `onwheel` -- до прокрутки, поэтому в нём можно отменить саму прокрутку (действие браузера). + +## Зоопарк `wheel` в разных браузерах + +Событие `wheel` появилось в [стандарте](http://www.w3.org/TR/DOM-Level-3-Events/#event-type-wheel) не так давно. Оно поддерживается IE9+, Firefox 17+. Возможно, другими браузерами на момент чтения этой статьи. + +До него браузеры обрабатывали прокрутку при помощи событий [mousewheel](http://msdn.microsoft.com/en-us/library/ie/ms536951.aspx) (все кроме Firefox) и [DOMMouseScroll](https://developer.mozilla.org/en-US/docs/DOM/DOM_event_reference/DOMMouseScroll), [MozMousePixelScroll](https://developer.mozilla.org/en-US/docs/DOM/DOM_event_reference/MozMousePixelScroll) (только Firefox). + +Самые важные свойства современного события и его нестандартных аналогов: +
    +
    `wheel`
    +
    Свойство `deltaY` -- количество прокрученных пикселей по горизонтали и вертикали. Существуют также свойства `deltaX` и `deltaZ` для других направлений прокрутки.
    +
    `MozMousePixelScroll`
    +
    Срабатывает, начиная с Firefox 3.5, только в Firefox. Даёт возможность отменить прокрутку и получить размер в пикселях через свойство `detail`, ось прокрутки в свойстве `axis`.
    +
    `DOMMouseScroll`
    +
    Существует в Firefox очень давно, отличается от предыдущего тем, что даёт в `detail` количество строк. Если не нужна поддержка Firefox < 3.5, то не нужно и это событие.
    +
    `mousewheel` +
    Срабатывает в браузерах, которые ещё не реализовали `wheel`. В свойстве `wheelDelta` -- условный "размер прокрутки", обычно равен `120` для прокрутки вверх и `-120` -- вниз. Он не соответствует какому-либо конкретному количеству пикселей.
    +
    + +Чтобы кросс-браузерно отловить прокрутку и, при необходимости, отменить её, можно использовать все эти события. + +Пример: +[js] +if (elem.addEventListener) { + if ('onwheel' in document) { + // IE9+, FF17+ + elem.addEventListener ("wheel", onWheel, false); + } else if ('onmousewheel' in document) { + // устаревший вариант события + elem.addEventListener ("mousewheel", onWheel, false); + } else { + // 3.5 <= Firefox < 17, более старое событие DOMMouseScroll пропустим + elem.addEventListener ("MozMousePixelScroll", onWheel, false); + } +} else { // IE<9 + elem.attachEvent ("onmousewheel", onWheel); +} + +function onWheel(e) { + e = e || window.event; + + // wheelDelta не дает возможность узнать количество пикселей + var delta = e.deltaY || e.detail || e.wheelDelta; + + var info = document.getElementById('delta'); + + info.innerHTML = +info.innerHTML + delta; + + e.preventDefault ? e.preventDefault() : (e.returnValue = false); +} +[/js] + +В действии: +[iframe src="wheel" link play] + +[warn header="Ошибка в IE8"] + +В браузере IE8 (только версия 8) есть ошибка. При наличии обработчика `mousewheel` -- элемент не скроллится. Иначе говоря, действие браузера отменяется по умолчанию. + +Это, конечно, не имеет значения, если элемент в принципе не прокручиваемый. +[/warn] + +[task id=933] +[task id=997] +[head] + + + + +[/head] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/04-mousewheel/01-scale-with-mouse-wheel.task/question.md b/tutorial/02-ui/03-event-details/04-mousewheel/01-scale-with-mouse-wheel.task/question.md new file mode 100644 index 000000000..e714eba18 --- /dev/null +++ b/tutorial/02-ui/03-event-details/04-mousewheel/01-scale-with-mouse-wheel.task/question.md @@ -0,0 +1,14 @@ +# Масштабирование колёсиком мыши + +[importance 5] + +Сделайте так, чтобы при прокрутке колёсиком мыши над элементом, он масштабировался. + +Масштабирование обеспечивайте при помощи свойства CSS transform: +[js] +// увеличение в 1.5 раза +elem.style.transform = elem.style.WebkitTransform = elem.style.MsTransform = 'scale(1.5)'; +[/js] + +Результат в iframe: +[iframe link border="1" src="solution" height="160"] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/04-mousewheel/01-scale-with-mouse-wheel.task/solution.code/index.html b/tutorial/02-ui/03-event-details/04-mousewheel/01-scale-with-mouse-wheel.task/solution.code/index.html new file mode 100755 index 000000000..b176c8db3 --- /dev/null +++ b/tutorial/02-ui/03-event-details/04-mousewheel/01-scale-with-mouse-wheel.task/solution.code/index.html @@ -0,0 +1,50 @@ + + + + + + + +

    +При прокрутке колёсика мыши над этим элементом, он будет масштабироваться. +

    + + + + + + diff --git a/tutorial/02-ui/03-event-details/04-mousewheel/01-scale-with-mouse-wheel.task/solution.code/scale-wheel/index.html b/tutorial/02-ui/03-event-details/04-mousewheel/01-scale-with-mouse-wheel.task/solution.code/scale-wheel/index.html new file mode 100755 index 000000000..b176c8db3 --- /dev/null +++ b/tutorial/02-ui/03-event-details/04-mousewheel/01-scale-with-mouse-wheel.task/solution.code/scale-wheel/index.html @@ -0,0 +1,50 @@ + + + + + + + +

    +При прокрутке колёсика мыши над этим элементом, он будет масштабироваться. +

    + + + + + + diff --git a/tutorial/02-ui/03-event-details/04-mousewheel/01-scale-with-mouse-wheel.task/solution.md b/tutorial/02-ui/03-event-details/04-mousewheel/01-scale-with-mouse-wheel.task/solution.md new file mode 100644 index 000000000..59e98690a --- /dev/null +++ b/tutorial/02-ui/03-event-details/04-mousewheel/01-scale-with-mouse-wheel.task/solution.md @@ -0,0 +1,3 @@ +Решение использует кросс-браузерный код назначения обработчика `onwheel` на элемент и `style.transform`. + +[edit src="solution"/] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/question.code/index.html b/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/question.code/index.html new file mode 100755 index 000000000..34c5bfa30 --- /dev/null +++ b/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/question.code/index.html @@ -0,0 +1,37 @@ + + + + + + + +
    +

    Начало документа

    +
    + + + + +
    +Конец документа. +
    + + diff --git a/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/question.code/no-doc-scroll-src/index.html b/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/question.code/no-doc-scroll-src/index.html new file mode 100755 index 000000000..34c5bfa30 --- /dev/null +++ b/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/question.code/no-doc-scroll-src/index.html @@ -0,0 +1,37 @@ + + + + + + + +
    +

    Начало документа

    +
    + + + + +
    +Конец документа. +
    + + diff --git a/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/question.md b/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/question.md new file mode 100644 index 000000000..2f70527cf --- /dev/null +++ b/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/question.md @@ -0,0 +1,28 @@ +# Прокрутка без влияния на страницу + +[importance 5] + +В большинстве браузеров (кроме Firefox) если в процессе прокрутки `textarea` мышкой (или жестами) мы достигаем границы, то прокрутка продолжается уже на уровне страницы. + +Иными словами, если в примере ниже вы попробуете прокрутить `textarea` вниз, то когда прокрутка дойдёт до конца -- начнёт прокручиваться документ: + +[iframe src="question" border="1" height=300] + +То же самое происходит при прокрутке вверх. + +В интерфейсах редактирования, когда большая `textarea` является основным элементом страницы, такое поведение может быть неудобно. + +Для редакторования более оптимально, чтобы при прокрутке до конца `textarea` страница не "улетала" вверх и вниз. + +Вот тот же документ, но с желаемым поведением `textarea`: + +[iframe src="solution" border="1" height=300] + +Задача: +
      +
    • Создать скрипт, который при подключении к документу исправлял бы поведение всех `textarea`, чтобы при прокрутке они не трогали документ.
    • +
    • Направление прокрутки -- только вверх или вниз.
    • +
    • Редактор прокручивает только мышкой или жестами (на мобильных устройствах), прокрутку клавиатурой здесь рассматривать не нужно (хотя это и возможно).
    • +
    + +[edit src="question" task/] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.code/fix-textarea-scroll.js b/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.code/fix-textarea-scroll.js new file mode 100755 index 000000000..e4bcc7753 --- /dev/null +++ b/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.code/fix-textarea-scroll.js @@ -0,0 +1,14 @@ +document.onwheel = function(e) { + if (e.target.tagName != 'TEXTAREA') return; + var area = e.target; + + var delta = e.deltaY || e.detail || e.wheelDelta; + + if (delta < 0 && area.scrollTop == 0) { + e.preventDefault(); + } + + if (delta > 0 && area.scrollHeight - area.clientHeight - area.scrollTop <= 1) { + e.preventDefault(); + } +}; \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.code/index.html b/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.code/index.html new file mode 100755 index 000000000..d9fcd846f --- /dev/null +++ b/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.code/index.html @@ -0,0 +1,38 @@ + + + + + + + + +
    +

    Начало документа

    +
    + + + + +
    +Конец документа. +
    + + diff --git a/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.code/no-doc-scroll/fix-textarea-scroll.js b/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.code/no-doc-scroll/fix-textarea-scroll.js new file mode 100755 index 000000000..e4bcc7753 --- /dev/null +++ b/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.code/no-doc-scroll/fix-textarea-scroll.js @@ -0,0 +1,14 @@ +document.onwheel = function(e) { + if (e.target.tagName != 'TEXTAREA') return; + var area = e.target; + + var delta = e.deltaY || e.detail || e.wheelDelta; + + if (delta < 0 && area.scrollTop == 0) { + e.preventDefault(); + } + + if (delta > 0 && area.scrollHeight - area.clientHeight - area.scrollTop <= 1) { + e.preventDefault(); + } +}; \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.code/no-doc-scroll/index.html b/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.code/no-doc-scroll/index.html new file mode 100755 index 000000000..d9fcd846f --- /dev/null +++ b/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.code/no-doc-scroll/index.html @@ -0,0 +1,38 @@ + + + + + + + + +
    +

    Начало документа

    +
    + + + + +
    +Конец документа. +
    + + diff --git a/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.md b/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.md new file mode 100644 index 000000000..330005220 --- /dev/null +++ b/tutorial/02-ui/03-event-details/04-mousewheel/02-no-doc-scroll.task/solution.md @@ -0,0 +1 @@ +[edit src="solution"/] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/04-mousewheel/wheel.code/index.html b/tutorial/02-ui/03-event-details/04-mousewheel/wheel.code/index.html new file mode 100755 index 000000000..46587004f --- /dev/null +++ b/tutorial/02-ui/03-event-details/04-mousewheel/wheel.code/index.html @@ -0,0 +1,59 @@ + + + + + + + + +Прокрутка: 0 +
    +Прокрути надо мной. +
    + + + + diff --git a/tutorial/02-ui/03-event-details/05-fixevent.md b/tutorial/02-ui/03-event-details/05-fixevent.md new file mode 100644 index 000000000..a5364decc --- /dev/null +++ b/tutorial/02-ui/03-event-details/05-fixevent.md @@ -0,0 +1,83 @@ +# Мышь: исправление события для IE8- + +В предыдущих главах мы говорили о различных несовместимостях при работе с событиями для IE8-. + +Самая главная -- это, конечно, назначение событий при помощи `attachEvent/detachEvent` вместо `addEventListener/removeEventListener` и отсутствие фазы перехвата. + +Что же касается событий мыши, то различия в свойствах можно легко исправить при помощи функции `fixEvent`, которая описана в этой главе. +[cut] +[ref id="fixEvent"] + +[warn header="Только IE8"] +Эта функция нужна только для IE8. +[/warn] + + +Функция `fixEvent` предназначена для запуска в начале обработчика, вот так: + +[js] +elem.onclick = function(event) { +*!* + // если IE8-, то получить объект события window.event и исправить его + event = event || fixEvent.call(this, window.event); +*/!* + ... +} +[/js] + +Она добавлит объекту события в IE8- следующие стандартные свойства: +
      +
    • `target`
    • +
    • `currentTarget` -- если обработчик назначен не через `attachEvent`.
    • +
    • `relatedTarget` -- для `mouseover/mouseout` и `mouseenter/mouseleave`.
    • +
    • `pageX/pageY`
    • +
    • `which`
    • +
    + +Код функции: + +[js] +function fixEvent(e) { + + e.currentTarget = this; + e.target = e.srcElement; + + if (e.type == 'mouseover' || e.type == 'mouseenter') e.relatedTarget = e.fromElement; + if (e.type == 'mouseout' || e.type == 'mouseleave') e.relatedTarget = e.toElement; + + if (e.pageX == null && e.clientX != null ) { + var html = document.documentElement; + var body = document.body; + + e.pageX = e.clientX + (html.scrollLeft || body && body.scrollLeft || 0); + e.pageX -= html.clientLeft || 0; + + e.pageY = e.clientY + (html.scrollTop || body && body.scrollTop || 0); + e.pageY -= html.clientTop || 0; + } + + if (!e.which && e.button) { + e.which = e.button & 1 ? 1 : ( e.button & 2 ? 3 : (e.button & 4 ? 2 : 0) ); + } + + return e; +} +[/js] + +Эта функция не нужна, если используются JavaScript-фреймворки, но может быть полезной, если вы по какой-то причине пишите без них. +[head] + + + + +[/head] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop.md b/tutorial/02-ui/03-event-details/06-drag-and-drop.md new file mode 100644 index 000000000..229347d12 --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop.md @@ -0,0 +1,255 @@ +# Мышь: Drag'n'Drop + +Drag'n'Drop -- это возможность захватить мышью элемент и перенести его. В свое время это было замечательным открытием в области интерфейсов, которое позволило упростить большое количество операций. + +Перенос мышкой может заменить целую последовательность кликов. И, самое главное, он упрощает внешний вид интерфейса: функции, реализуемые через Drag'n'Drop, в ином случае потребовали бы дополнительных полей, виджетов и т.п. + +[cut] +## Отличия от HTML5 Drag'n'Drop + +В современном стандарте HTML5 есть поддержка Drag'n'Drop при помощи [специальных событий](http://www.html5rocks.com/en/tutorials/dnd/basics/). Эти события поддерживаются всеми браузерами, в мелочах отстаёт от них IE. + +У них есть своя область применения, например можно перетащить файл в браузер, но здесь сосредоточимся на реализации техник Drag'n'Drop в более широком смысле, для более обширного класса задач. + +**Далее речь пойдет о реализации Drag'n'Drop при помощи событий мыши.** + +**Изложенные методы применяются в элементах управления для обработки любых действий вида "захватить - потянуть - отпустить".** + +## Основная логика Drag'n'Drop + +Для организации Drag'n'Drop нужно: +
      +
    1. При помощи события `mousedown` отследить нажатие кнопки на переносимом элементе.
    2. +
    3. При нажатии -- подготовить элемент к перемещению: обычно ему назначается `position:absolute` и ставятся координаты `left/top` по координатам курсора.
    4. +
    5. Далее отслеживаем движение мыши через mousemove и передвигаем переносимый элемент на новые координаты путём смены `left/top`.
    6. +
    7. При отпускании кнопки мыши, то есть наступлении события mouseup -- остановить перенос элемента и произвести все действия, связанные с окончанием Drag'n'Drop.
    8. +
    + +В следующем примере эти шаги реализованы для переноса мяча: + +[js autorun] +var ball = document.getElementById('ball'); + +ball.onmousedown = function(e) { // 1. отследить нажатие*!* + var self = this; + + // подготовить к перемещению + // 2. разместить на том же месте, но в абсолютных координатах*!* + this.style.position = 'absolute'; + moveAt(e); + // переместим в body, чтобы мяч был точно не внутри position:relative + document.body.appendChild(this); + + this.style.zIndex = 1000; // показывать мяч над другими элементами + + // передвинуть мяч под координаты курсора + function moveAt(e) { + self.style.left = e.pageX-20+'px'; // 20 - половина ширины/высоты мяча + self.style.top = e.pageY-20+'px'; + } + + // 3, перемещать по экрану*!* + document.onmousemove = function(e) { + moveAt(e); + } + + // 4. отследить окончание переноса *!* + this.onmouseup = function() { + document.onmousemove = self.onmouseup = null; + } +} +[/js] + +В действии: +
    +Кликните по мячу и тяните, чтобы двигать его. + +
    + +**Попробуйте этот пример. Он не совсем работает, мячик "раздваивается".** + +Сейчас мы это исправим. + +## Отмена переноса браузера + +При нажатии мышью на `` браузер начинает выполнять свой собственный, встроенный Drag'n'Drop, который и портит наш перенос. + +Чтобы браузер не вмешивался, нужно отменить действие по умолчанию для события `dragstart`: + +[js] +ball.ondragstart = function() { + return false; +}; +[/js] + +Исправленный пример: + +[js autorun] +var ball = document.getElementById('ball2'); + +ball.onmousedown = function(e) { + var self = this; + + this.style.position = 'absolute'; + moveAt(e); + document.body.appendChild(this); + + this.style.zIndex = 1000; + + function moveAt(e) { + self.style.left = e.pageX-20+'px'; + self.style.top = e.pageY-20+'px'; + } + + document.onmousemove = function(e) { + moveAt(e); + }; + + this.onmouseup = function() { + document.onmousemove = self.onmouseup = null; + }; +} + +*!* +ball.ondragstart = function() { + return false; +}; +*/!* +[/js] + +В действии: + +
    +Кликните по мячу и тяните, чтобы двигать его. + +
    + +[smart header="Обработчик `mousemove` ставим на `document`"] + +**Почему событие `mousemove` в примере отслеживается на `document`, а не на `ball`?** + +С первого взгляда кажется, что мышь всегда над мячом и обработчик `mousemove` можно повесить на сам мяч, а не на документ. + +Однако, на самом деле **мышь во время переноса не всегда над мячом**. Вспомните, браузер регистрирует `mousemove` часто, но не для каждого пикселя. + +Быстрое движение курсора вызовет `mousemove` уже не над мячом, а, например, в дальнем конце страницы. +Вот почему мы должны отслеживать `mousemove` на всём `document`. +[/smart] + +## Правильное позиционирование + +В примерах выше мяч позиционируется в центре под курсором мыши: + +[js] +self.style.left = e.pageX - 20 + 'px'; +self.style.top = e.pageY - 20 + 'px'; +[/js] + +Число `20` здесь -- половина длины мячика. Оно использовано здесь потому, что если поставить `left/top` ровно в `pageX/pageY`, то мячик прилипнет верхним-левым углом к курсору мыши. Будет некрасиво. + +**Для правильного переноса необходимо, чтобы изначальный сдвиг курсора относительно элемента сохранялся: где захватили, за ту "часть элемента" и переносим.** + +[img src="ball_shift.png"] + +
      +
    • Когда человек нажимает на мячик `mousedown` -- курсор сдвинут относительно левого-верхнего угла мяча на расстояние `shiftX/shiftY`. И мы хотели бы сохранить этот сдвиг. + +Получить значения `shiftX/shiftY` легко: достаточно вычесть из координат курсора `pageX/pageY` левую-верхнюю границу мячика, полученную при помощи функции [getCoords](#getCoords). + +**При Drag'n'Drop мы везде используем координаты относительно документа, так как они подходят в большем количестве ситуаций.** + +Конечно же, не проблема перейти к координатам относительно окна, если это понадобится. Достаточно использовать `position:fixed`, `elem.getBoundingClientRect()` для определения координат и `e.clientX/Y`. + +[js] +// onmousedown +shiftX = e.pageX - getCoords(ball).left; +shiftY = e.pageY - getCoords(ball).top; +[/js] +
    • +
    • Далее при переносе мяча мы располагаем его `left/top` с учетом сдвига, то есть вот так: +[js] +// onmousemove +self.style.left = e.pageX - *!*shiftX*/!* + 'px'; +self.style.top = e.pageY - *!*shiftY*/!* + 'px'; +[/js] +
    • +
    + + +**Пример с правильным позиционированием:** + +В этом примере позиционирование осуществляется не на `20px`, а с учётом изначального сдвига. + +[js autorun] +var ball = document.getElementById('ball3'); + +ball.onmousedown = function(e) { + var self = this; + + var coords = getCoords(this); +*!* + var shiftX = e.pageX - coords.left; + var shiftY = e.pageY - coords.top; +*/!* + + this.style.position = 'absolute'; + document.body.appendChild(this); + moveAt(e); + + this.style.zIndex = 1000; // над другими элементами + + function moveAt(e) { + self.style.left = e.pageX - *!*shiftX*/!* + 'px'; + self.style.top = e.pageY - *!*shiftY*/!* + 'px'; + } + + document.onmousemove = function(e) { + moveAt(e); + }; + + this.onmouseup = function() { + document.onmousemove = self.onmouseup = null; + }; + +} + +ball.ondragstart = function() { + return false; +}; +[/js] + +В действии: + +
    +Кликните по мячу и тяните, чтобы двигать его. + +
    + +Различие особенно заметно, если захватить мяч за правый-нижний угол. В предыдущем примере мячик "прыгнет" серединой под курсор, в этом -- будет плавно переноситься с текущей позиции. + +## Итого + +Мы рассмотрели "минимальный каркас" `Drag'n'Drop`. + +Его компоненты: + +
      +
    1. События `mousedown` -> `document.mousemove` -> `mouseup`.
    2. +
    3. Передвижение с учётом изначального сдвига `shiftX/shiftY`.
    4. +
    5. Отмена действия браузера по событию `dragstart`.
    6. +
    + +На этой основе можно сделать очень многое. + +
      +
    • При `mouseup` можно обработать окончание переноса, произвести изменения в данных, если они нужны.
    • +
    • Во время самого переноса можно подсвечивать элементы, над которыми проходит элемент.
    • +
    + +Это и многое другое мы рассмотрим в статье про [Drag'n'Drop объектов](article:370). + +[task id=437] +[task id=779] +[libs] +getCoords.js +[/libs] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/question.code/index.html b/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/question.code/index.html new file mode 100755 index 000000000..7536c19be --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/question.code/index.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/question.code/lib.js b/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/question.code/lib.js new file mode 100755 index 000000000..4b84d52ac --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/question.code/lib.js @@ -0,0 +1,44 @@ + +function fixEvent(e) { + e = e || window.event; + + if (!e.target) e.target = e.srcElement; + + if (e.pageX == null && e.clientX != null ) { // если нет pageX.. + var html = document.documentElement; + var body = document.body; + + e.pageX = e.clientX + (html.scrollLeft || body && body.scrollLeft || 0); + e.pageX -= html.clientLeft || 0; + + e.pageY = e.clientY + (html.scrollTop || body && body.scrollTop || 0); + e.pageY -= html.clientTop || 0; + } + + if (!e.which && e.button) { + e.which = e.button & 1 ? 1 : ( e.button & 2 ? 3 : ( e.button & 4 ? 2 : 0 ) ) + } + + return e; +} + + +function getCoords(elem) { + var box = elem.getBoundingClientRect(); + + var body = document.body; + var docElem = document.documentElement; + + var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop; + var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft; + + var clientTop = docElem.clientTop || body.clientTop || 0; + var clientLeft = docElem.clientLeft || body.clientLeft || 0; + + var top = box.top + scrollTop - clientTop; + var left = box.left + scrollLeft - clientLeft; + + return { top: Math.round(top), left: Math.round(left) }; +} + + diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/question.md b/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/question.md new file mode 100644 index 000000000..cdd19cb4f --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/question.md @@ -0,0 +1,19 @@ +# Слайдер + +[importance 5] + +Создайте слайдер: +[iframe src="solution" height=60 border=1] + +Захватите мышкой синий бегунок и двигайте его, чтобы увидеть в работе. + +Позже к этому слайдеру можно будет добавить дополнительные функции по чтению/установке значения. + +[edit src="question" task/] ([getCoords](#getCoords) -- в lib.js). + +Важно: +
      +
    • Слайдер должен нормально работать при резком движении мыши влево или вправо, за пределы полосы. При этом бегунок должен останавливаться четко в нужном конце полосы.
    • +
    • Курсор при передвижении слайдера должен быть рукой(`hand`) или крестиком(`move`).
    • +
    • При нажатом бегунке мышь может выходить за пределы полосы слайдера, но слайдер пусть все равно работает (удобство для пользователя).
    • +
    \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.code/index.html b/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.code/index.html new file mode 100755 index 000000000..a8e293f18 --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.code/index.html @@ -0,0 +1,79 @@ + + + + + + + + + +
    +
    +
    + + + + + + diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.code/lib.js b/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.code/lib.js new file mode 100755 index 000000000..4b84d52ac --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.code/lib.js @@ -0,0 +1,44 @@ + +function fixEvent(e) { + e = e || window.event; + + if (!e.target) e.target = e.srcElement; + + if (e.pageX == null && e.clientX != null ) { // если нет pageX.. + var html = document.documentElement; + var body = document.body; + + e.pageX = e.clientX + (html.scrollLeft || body && body.scrollLeft || 0); + e.pageX -= html.clientLeft || 0; + + e.pageY = e.clientY + (html.scrollTop || body && body.scrollTop || 0); + e.pageY -= html.clientTop || 0; + } + + if (!e.which && e.button) { + e.which = e.button & 1 ? 1 : ( e.button & 2 ? 3 : ( e.button & 4 ? 2 : 0 ) ) + } + + return e; +} + + +function getCoords(elem) { + var box = elem.getBoundingClientRect(); + + var body = document.body; + var docElem = document.documentElement; + + var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop; + var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft; + + var clientTop = docElem.clientTop || body.clientTop || 0; + var clientLeft = docElem.clientLeft || body.clientLeft || 0; + + var top = box.top + scrollTop - clientTop; + var left = box.left + scrollLeft - clientLeft; + + return { top: Math.round(top), left: Math.round(left) }; +} + + diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.code/slider-simple/index.html b/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.code/slider-simple/index.html new file mode 100755 index 000000000..a8e293f18 --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.code/slider-simple/index.html @@ -0,0 +1,79 @@ + + + + + + + + + +
    +
    +
    + + + + + + diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.code/slider-simple/lib.js b/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.code/slider-simple/lib.js new file mode 100755 index 000000000..4b84d52ac --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.code/slider-simple/lib.js @@ -0,0 +1,44 @@ + +function fixEvent(e) { + e = e || window.event; + + if (!e.target) e.target = e.srcElement; + + if (e.pageX == null && e.clientX != null ) { // если нет pageX.. + var html = document.documentElement; + var body = document.body; + + e.pageX = e.clientX + (html.scrollLeft || body && body.scrollLeft || 0); + e.pageX -= html.clientLeft || 0; + + e.pageY = e.clientY + (html.scrollTop || body && body.scrollTop || 0); + e.pageY -= html.clientTop || 0; + } + + if (!e.which && e.button) { + e.which = e.button & 1 ? 1 : ( e.button & 2 ? 3 : ( e.button & 4 ? 2 : 0 ) ) + } + + return e; +} + + +function getCoords(elem) { + var box = elem.getBoundingClientRect(); + + var body = document.body; + var docElem = document.documentElement; + + var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop; + var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft; + + var clientTop = docElem.clientTop || body.clientTop || 0; + var clientLeft = docElem.clientLeft || body.clientLeft || 0; + + var top = box.top + scrollTop - clientTop; + var left = box.left + scrollLeft - clientLeft; + + return { top: Math.round(top), left: Math.round(left) }; +} + + diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.md b/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.md new file mode 100644 index 000000000..63aac5e97 --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop/01-slider.task/solution.md @@ -0,0 +1,45 @@ +# HTML/CSS, подсказка + +Слайдер -- это `DIV`, подкрашенный фоном/градиентом, внутри которого находится другой `DIV`, оформленный как бегунок, с `position:relative`. + +Бегунок немного поднят, и вылезает по высоте из родителя. + +# HTML/CSS для слайдера + +Например, вот так: +[html run] + + +
    +
    +
    +[/html] + +Теперь на этом реализуйте перенос бегунка. + +# Полное решение + +[edit src="solution"]Полное решение[/edit] + +Это горизонтальный Drag'n'Drop, ограниченный по ширине. Его особенность -- в `position:relative` у переносимого элемента, т.е. координата ставится не абсолютная, а относительно родителя. \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/question.code/index.html b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/question.code/index.html new file mode 100755 index 000000000..66de69718 --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/question.code/index.html @@ -0,0 +1,38 @@ + + + + + + + + +

    Расставьте супергероев по полю.

    + +

    Супергерои -- это элементы с классом "draggable". Сделайте так, чтобы их можно было переносить.

    + +

    Важно: если супергероя подносят к низу или верху страницы, она должна автоматически прокручиваться. Конечно, можно прокрутить и клавиатурой, но так -- удобнее. Если страница помещается на вашем экране целиком и не имеет вертикальной прокрутки -- сделайте окно браузера меньше, чтобы протестировать эту возможность.

    + +

    Да, и ещё: супергерои ни при каких условиях не должны попасть за край экрана.

    + + +
    + +
    + +
    +
    +
    +
    +
    +
    + + + +
    + + + + + + + diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/question.code/soccer.css b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/question.code/soccer.css new file mode 100755 index 000000000..c79a9e7a9 --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/question.code/soccer.css @@ -0,0 +1,58 @@ +html, body { + margin: 0; + padding: 0; +} + +#field { + background: url(http://js.cx/drag-heroes/field.png); + width: 800px; + height: 600px; + float: left; +} + +.hero { + background: url(http://js.cx/drag-heroes/heroes.png); + width: 105px; + height: 128px; + float: left; +} + +#hero1 { + background-position: 0 0; +} + +#hero2 { + background-position: 0 -128px; +} + +#hero3 { + background-position: -131px 0; +} + +#hero4 { + background-position: -131px -128px; +} + +#hero5 { + background-position: -236px 0; + width: 130px; +} + +#winnie { + background: url(http://js.cx/drag-heroes/winnie.png); + width: 115px; + height: 128px; + float: left; +} + +.draggable { + cursor: pointer; +} + +.dragging { + z-index: 1000; + position: absolute; + cursor: move; +} + + diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/question.code/soccer.js b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/question.code/soccer.js new file mode 100755 index 000000000..eb778e9fd --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/question.code/soccer.js @@ -0,0 +1,2 @@ + +/* ваш код */ \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/question.md b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/question.md new file mode 100644 index 000000000..80ec1c26a --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/question.md @@ -0,0 +1,13 @@ +# Расставить супергероев по полю + +[importance 5] + +Сделайте так, чтобы элементы с классом `draggable` можно было переносить мышкой. + +Футбольное поле в этой задач слишком большое, чтобы показывать его здесь, поэтому откройте его, кликнув по ссылке ниже. Там же и подробное описание задачи (осторожно, винни-пух и супергерои!). + +[demo src="solution"] + +[edit src="question" task/] + +P.S. Для вашего удобства добавлены функции `getCoords` -- для координат и `getDocumentScroll` -- для получения границ видимой области и прокрутки в документе. \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/drag-heroes/index.html b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/drag-heroes/index.html new file mode 100755 index 000000000..3cb1f8733 --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/drag-heroes/index.html @@ -0,0 +1,37 @@ + + + + + + + + +

    Расставьте супергероев по полю.

    + +

    Супергерои -- это элементы с классом "draggable". Сделайте так, чтобы их можно было переносить.

    + +

    Важно: если супергероя подносят к низу или верху страницы, она должна автоматически прокручиваться. Конечно, можно прокрутить и клавиатурой, но так -- удобнее. Если страница помещается на вашем экране целиком и не имеет вертикальной прокрутки -- сделайте окно браузера меньше, чтобы протестировать эту возможность.

    + +

    Да, и ещё: супергерои ни при каких условиях не должны попасть за край экрана.

    + +
    + +
    + +
    +
    +
    +
    +
    +
    + + + +
    + + + + + + + diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/drag-heroes/soccer.css b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/drag-heroes/soccer.css new file mode 100755 index 000000000..c79a9e7a9 --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/drag-heroes/soccer.css @@ -0,0 +1,58 @@ +html, body { + margin: 0; + padding: 0; +} + +#field { + background: url(http://js.cx/drag-heroes/field.png); + width: 800px; + height: 600px; + float: left; +} + +.hero { + background: url(http://js.cx/drag-heroes/heroes.png); + width: 105px; + height: 128px; + float: left; +} + +#hero1 { + background-position: 0 0; +} + +#hero2 { + background-position: 0 -128px; +} + +#hero3 { + background-position: -131px 0; +} + +#hero4 { + background-position: -131px -128px; +} + +#hero5 { + background-position: -236px 0; + width: 130px; +} + +#winnie { + background: url(http://js.cx/drag-heroes/winnie.png); + width: 115px; + height: 128px; + float: left; +} + +.draggable { + cursor: pointer; +} + +.dragging { + z-index: 1000; + position: absolute; + cursor: move; +} + + diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/drag-heroes/soccer.js b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/drag-heroes/soccer.js new file mode 100755 index 000000000..405e0d93b --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/drag-heroes/soccer.js @@ -0,0 +1,80 @@ + + +document.body.onmousedown = function(e) { + + var dragElement = e.target; + + if (!dragElement.classList.contains('draggable')) return; + + var coords, shiftX, shiftY; + + startDrag(e.pageX, e.pageY); + + document.onmousemove = function(e) { + moveAt(e.pageX, e.pageY); + }; + + dragElement.onmouseup = function() { + finishDrag(); + }; + + + // ------------------------- + + function startDrag(pageX, pageY) { + + coords = getCoords(dragElement); + shiftX = pageX - coords.left; + shiftY = pageY - coords.top; + + dragElement.classList.add("dragging"); + dragElement.style.position = 'absolute'; + document.body.appendChild(dragElement); + + moveAt(pageX, pageY); + }; + + function finishDrag() { + dragElement.classList.remove('dragging'); + document.onmousemove = dragElement.onmouseup = null; + } + + function moveAt(pageX, pageY) { + var newX = pageX - shiftX; + var newY = pageY - shiftY; + + var newBottom = newY + dragElement.offsetHeight; + + var docScroll = getDocumentScroll(); + + // прокрутить вниз, если нужно + if (newBottom > docScroll.bottom) { + // ...но не за конец документа + var scrollSizeToEnd = docScroll.height - docScroll.bottom; + + // scrollBy, если его не ограничить, + // может заскроллить за текущую границу страницы + var toScrollY = Math.min(scrollSizeToEnd, 10); + window.scrollBy(0, toScrollY ); + + // при необходимости двигаем элемент вверх, чтобы поместился + // метод scrollBy асинхронный, поэтому учитываем будущую прокрутку (+toScrollY) + newY = Math.min(newY, docScroll.bottom - dragElement.offsetHeight + toScrollY); + } + + if (newY < docScroll.top) { + var toScrollY = Math.min(docScroll.top, 10); + window.scrollBy(0, -toScrollY ); + newY = Math.max(newY, docScroll.top - toScrollY ); + } + + // зажать в границах экрана по горизонтали + newX = Math.max(newX, 0); + newX = Math.min(newX, document.documentElement.clientWidth - dragElement.offsetHeight); + + dragElement.style.left = newX + 'px'; + dragElement.style.top = newY + 'px'; + } + + return false; +} \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/index.html b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/index.html new file mode 100755 index 000000000..3cb1f8733 --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/index.html @@ -0,0 +1,37 @@ + + + + + + + + +

    Расставьте супергероев по полю.

    + +

    Супергерои -- это элементы с классом "draggable". Сделайте так, чтобы их можно было переносить.

    + +

    Важно: если супергероя подносят к низу или верху страницы, она должна автоматически прокручиваться. Конечно, можно прокрутить и клавиатурой, но так -- удобнее. Если страница помещается на вашем экране целиком и не имеет вертикальной прокрутки -- сделайте окно браузера меньше, чтобы протестировать эту возможность.

    + +

    Да, и ещё: супергерои ни при каких условиях не должны попасть за край экрана.

    + +
    + +
    + +
    +
    +
    +
    +
    +
    + + + +
    + + + + + + + diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/soccer.css b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/soccer.css new file mode 100755 index 000000000..c79a9e7a9 --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/soccer.css @@ -0,0 +1,58 @@ +html, body { + margin: 0; + padding: 0; +} + +#field { + background: url(http://js.cx/drag-heroes/field.png); + width: 800px; + height: 600px; + float: left; +} + +.hero { + background: url(http://js.cx/drag-heroes/heroes.png); + width: 105px; + height: 128px; + float: left; +} + +#hero1 { + background-position: 0 0; +} + +#hero2 { + background-position: 0 -128px; +} + +#hero3 { + background-position: -131px 0; +} + +#hero4 { + background-position: -131px -128px; +} + +#hero5 { + background-position: -236px 0; + width: 130px; +} + +#winnie { + background: url(http://js.cx/drag-heroes/winnie.png); + width: 115px; + height: 128px; + float: left; +} + +.draggable { + cursor: pointer; +} + +.dragging { + z-index: 1000; + position: absolute; + cursor: move; +} + + diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/soccer.js b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/soccer.js new file mode 100755 index 000000000..405e0d93b --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.code/soccer.js @@ -0,0 +1,80 @@ + + +document.body.onmousedown = function(e) { + + var dragElement = e.target; + + if (!dragElement.classList.contains('draggable')) return; + + var coords, shiftX, shiftY; + + startDrag(e.pageX, e.pageY); + + document.onmousemove = function(e) { + moveAt(e.pageX, e.pageY); + }; + + dragElement.onmouseup = function() { + finishDrag(); + }; + + + // ------------------------- + + function startDrag(pageX, pageY) { + + coords = getCoords(dragElement); + shiftX = pageX - coords.left; + shiftY = pageY - coords.top; + + dragElement.classList.add("dragging"); + dragElement.style.position = 'absolute'; + document.body.appendChild(dragElement); + + moveAt(pageX, pageY); + }; + + function finishDrag() { + dragElement.classList.remove('dragging'); + document.onmousemove = dragElement.onmouseup = null; + } + + function moveAt(pageX, pageY) { + var newX = pageX - shiftX; + var newY = pageY - shiftY; + + var newBottom = newY + dragElement.offsetHeight; + + var docScroll = getDocumentScroll(); + + // прокрутить вниз, если нужно + if (newBottom > docScroll.bottom) { + // ...но не за конец документа + var scrollSizeToEnd = docScroll.height - docScroll.bottom; + + // scrollBy, если его не ограничить, + // может заскроллить за текущую границу страницы + var toScrollY = Math.min(scrollSizeToEnd, 10); + window.scrollBy(0, toScrollY ); + + // при необходимости двигаем элемент вверх, чтобы поместился + // метод scrollBy асинхронный, поэтому учитываем будущую прокрутку (+toScrollY) + newY = Math.min(newY, docScroll.bottom - dragElement.offsetHeight + toScrollY); + } + + if (newY < docScroll.top) { + var toScrollY = Math.min(docScroll.top, 10); + window.scrollBy(0, -toScrollY ); + newY = Math.max(newY, docScroll.top - toScrollY ); + } + + // зажать в границах экрана по горизонтали + newX = Math.max(newX, 0); + newX = Math.min(newX, document.documentElement.clientWidth - dragElement.offsetHeight); + + dragElement.style.left = newX + 'px'; + dragElement.style.top = newY + 'px'; + } + + return false; +} \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.md b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.md new file mode 100644 index 000000000..099f5e815 --- /dev/null +++ b/tutorial/02-ui/03-event-details/06-drag-and-drop/02-drag-heroes.task/solution.md @@ -0,0 +1 @@ +[edit src="solution"]Открыть в песочнице[/edit] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/06-drag-and-drop/ball_shift.png b/tutorial/02-ui/03-event-details/06-drag-and-drop/ball_shift.png new file mode 100755 index 0000000000000000000000000000000000000000..e1ac464180ccb4e6bed03bef545c6472b1ac6706 GIT binary patch literal 17913 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 Nl;t(#N@bqE{y*x6%`5-_ literal 0 HcmV?d00001 diff --git a/tutorial/02-ui/03-event-details/07-drag-and-drop-objects.md b/tutorial/02-ui/03-event-details/07-drag-and-drop-objects.md new file mode 100644 index 000000000..63de8ab36 --- /dev/null +++ b/tutorial/02-ui/03-event-details/07-drag-and-drop-objects.md @@ -0,0 +1,672 @@ +# Мышь: Drag'n'Drop объектов + +В [предыдущей статье](article:369) мы рассмотрели основы Drag'n'Drop. Здесь мы построим на этой основе фреймворк, предназначенный для переноса *объектов* -- элементов списка, узлов дерева и т.п. + +Почти все javascript-библиотеки реализуют Drag'n'Drop так, как написано (хотя бывает что и менее эффективно ;)). Зная, что и как, вы сможете легко написать свой код переноса или поправить, адаптировать существующую библиотеку под себя. +[cut] +## Документ + +Как пример задачи -- возьмём документ с иконками браузера ("объекты переноса"), которые можно переносить в компьютер ("цель переноса"): + +
      +
    • Элементы, которые можно переносить (иконки браузеров), помечены атрибутом `draggable`.
    • +
    • Элементы, на которые можно положить (компьютер), имеют атрибут `droppable`.
    • +
    + +[html] + + + + + + +

    Браузер переносить сюда:

    + + +[/html] + +Работающий пример с переносом. + + +[iframe border=1 src="dragDemo" height=250 link play] + +Далее мы рассмотрим, как делается фреймворк для таких переносов, а в перспективе -- и для более сложных. + +Требования: +
      +
    • Поддержка большого количества элементов без "тормозов".
    • +
    • Продвинутые возможности по анимации переноса.
    • +
    • Удобная обработка успешного и неудачного переноса.
    • +
    + +## Начало переноса + +Чтобы начать перенос элемента, мы должны отловить нажатие левой кнопки мыши на нём. Для этого используем событие `mousedown`. + +..И здесь нас ждёт первая особенность переноса объектов. На что ставить обработчик? + +**Переносимых элементов может быть много.** В нашем документе-примере это всего лишь несколько иконок, но если мы хотим переносить элементы списка или дерева, то их может быть 100 штук и более. + +Назначать обработчик на каждый DOM-элемент -- нецелесообразно. Проще всего решить эту задачу делегированием. + +**Повесим обработчик `mousedown` на контейнер, который содержит переносимые элементы,** и будем определять нужный элемент поиском ближайшего `draggable` вверх по иерархии от `event.target`. В качестве контейнера здесь и далее будем брать `document`. + +Найденный переносимый объект сохраним в переменной `dragObject` и начнём двигать. + +Код обработчика `mousedown`: + +[js] +var dragObject = {}; + +document.onmousedown = function(e){ + e = fixEvent(e); + + if (e.which != 1) { + return; // нажатие правой кнопкой не запускает перенос + } + + // найти ближайший draggable, пройдя по цепочке родителей target + var elem = findDraggable(e.target, document); + + if (!elem) return; // не нашли, клик вне draggable объекта + + // запомнить переносимый объект + dragObject.elem = elem; + + // запомнить координаты, с которых начат перенос объекта + dragObject.downX = e.pageX; + dragObject.downY = e.pageY; +} +[/js] + +В обработчике используется функция `findDraggable` для поиска переносимого элемента по `event.target`: +[js] +function findDraggable(event) { + var elem = event.target; + + // найти ближайший draggable, пройдя по цепочке родителей target + while(elem != document && elem.getAttribute('draggable') == null) { + elem = elem.parentNode; + } + return elem == document ? null : elem; +} +[/js] + +**Здесь мы пока не начинаем перенос, потому что нажатие на элемент вовсе не означает, что его собираются куда-то двигать.** Возможно, на нём просто кликают. + +Это важное различие. Нужно отличать перенос от клика, в том числе -- от клика, который сопровождается нечаянным перемещением на пару пикселей (рука дрогнула). + +Иначе при клике элемент будет сниматься со своего места, и потом тут же возвращаться обратно (никуда не положили). Это лишняя работа и, вообще, выглядит некрасиво. + +**Поэтому в `mousedown` мы запоминаем, какой элемент и где был зажат, а начало переноса будем инициировать из `mousemove`,** если он передвинут хотя бы на несколько пикселей. + +## Перенос элемента + +Первой задачей обработчика `mousemove` является инициировать начало переноса, если элемент передвинули в зажатом состоянии. + +Ну а второй задачей -- отображать его перенос при каждом передвижении мыши. Схематично, обработчик имеет такой вид: +[js] +document.onmousemove = function(e) { + if (!dragObject.elem) return; // элемент не зажат + + e = fixEvent(e); + + if ( !dragObject.avatar ) { // элемент нажат, но пока не начали его двигать + ... начать перенос, присвоить dragObject.avatar = переносимый элемент + } + + ...отобразить перенос элемента... +} +[/js] + +Здесь мы видим новое свойство `dragObject.avatar`. При начале переноса *аватар* делается из элемента и сохраняется в свойство `dragObject.avatar`. + +***"Аватар"* -- это DOM-элемент, который перемещается по экрану.** + +Почему бы не перемещать по экрану сам `draggable`-элемент? Зачем, вообще, нужен аватар? + +Дело в том, что иногда сам элемент передвигать неудобно, например он слишком большой. А удобно создать некоторое визуальное представление элемента, и его уже переносить. Аватар дает такую возможность. + +А в простейшем случае аватаром можно будет сделать сам элемент, и это не повлечёт дополнительных расходов. + +### Визуальное перемещение аватара + +Для того, чтобы отобразить перенос аватара, достаточно поставить ему `position: absolute` и менять координаты `left/top`. + +Для использования абсолютных координат относительно документа, аватар должен быть прямым потомком `BODY`. + +Следующий код готовит аватар к переносу: + +[js] +// в начале переноса: +if (avatar.parentNode != document.body) { + document.body.appendChild(avatar); // переместить в BODY, если надо +} +avatar.style.zIndex = 9999; // сделать, чтобы элемент был над другими +avatar.style.position = 'absolute'; +[/js] + +... А затем его можно двигать: +[js] +// при каждом движении мыши + +avatar.style.left = новая координата + 'px'; +avatar.style.top = новая координата + 'px'; +[/js] + +**Как вычислять новые координаты `left/top` при переносе?** + +Чтобы элемент сохранял свою позицию под курсором, необходимо при нажатии запомнить его изначальный сдвиг относительно курсора, и сохранять его при переносе. + +[img src="shiftx.png"] + +Этот сдвиг по горизонтали обозначен `shiftX` на рисунке выше. Аналогично, есть `shiftY`. Они вычисляются как расстояние между курсором и левой/верхней границей элемента при `mousedown`. Детали вычислений описаны в главе [](article:369). + +Таким образом, при `mousemove` мы будем назначать элементу координаты курсора с учетом сдвига `shiftX/shiftY`: +[js] +avatar.style.left = e.pageX - shiftX + 'px'; +avatar.style.top = e.pageY - shiftY + 'px'; +[/js] + + + +### Полный код `mousemove` + +Код `mousemove`, решающий задачу начала переноса и позиционирования аватара: + +[js] +document.onmousemove = function(e) { + if (!dragObject.elem) return; // элемент не зажат + + e = fixEvent(e); + + if ( !dragObject.avatar ) { // если перенос не начат... + + // посчитать дистанцию, на которую переместился курсор мыши + var moveX = e.pageX - dragObject.downX; + var moveY = e.pageY - dragObject.downY; + if ( Math.abs(moveX) < 3 && Math.abs(moveY) < 3 ) { + return; // ничего не делать, мышь не передвинулась достаточно далеко + } + + *!*dragObject.avatar = createAvatar(e)*/!*; // захватить элемент + if (!dragObject.avatar) { + dragObject = {}; // аватар создать не удалось, отмена переноса + return; // возможно, нельзя захватить за эту часть элемента + } + + // аватар создан успешно + // создать вспомогательные свойства shiftX/shiftY + var coords = getCoords(avatar); + dragObject.shiftX = dragObject.downX - coords.left; + dragObject.shiftY = dragObject.downY - coords.top; + + *!*startDrag(e)*/!*; // отобразить начало переноса + } + + // отобразить перенос объекта при каждом движении мыши + dragObject.avatar.style.left = e.pageX - dragObject.shiftX + 'px'; + dragObject.avatar.style.top = e.pageY - dragObject.shiftY + 'px'; + + return false; +} +[/js] + +Здесь используются две функции для начала переноса: `createAvatar(e)` и `startDrag(e)`. + +Функция `createAvatar(e)` создает аватар. В нашем случае в качестве аватара берется сам `draggable` элемент. После создания аватара в него записывается функция `avatar.rollback`, которая задает поведение при отмене переноса. + +Как правило, отмена переноса влечет за собой разрушение аватара, если это был клон, или возвращение его на прежнее место, если это сам элемент. + +В нашем случае для отмены переноса нужно запомнить старую позицию элемента и его родителя. + +[js hide="Раскрыть код createAvatar"] +function createAvatar(e) { + + // запомнить старые свойства, чтобы вернуться к ним при отмене переноса + var avatar = dragObject.elem; + var old = { + parent: avatar.parentNode, + nextSibling: avatar.nextSibling, + position: avatar.position || '', + left: avatar.left || '', + top: avatar.top || '', + zIndex: avatar.zIndex || '' + }; + + // функция для отмены переноса + avatar.rollback = function() { + old.parent.insertBefore(avatar, old.nextSibling); + avatar.style.position = old.position; + avatar.style.left = old.left; + avatar.style.top = old.top; + avatar.style.zIndex = old.zIndex + }; + + return avatar; +} +[/js] + +Функция `startDrag(e)` инициирует начало переноса и позиционирует аватар на странице. + +[js] +function startDrag(e) { + var avatar = dragObject.avatar; + + document.body.appendChild(avatar); + avatar.style.zIndex = 9999; + avatar.style.position = 'absolute'; +} +[/js] + +### Окончание переноса + +Окончание переноса происходит по событию `mouseup`. + +Его обработчик можно поставить на аватаре, т.к. аватар всегда под курсором и `mouseup` происходит на нем. Но для универсальности и большей гибкости (вдруг мы захотим перемещать аватар *рядом* с курсором?) поставим его, как и остальные, на `document`. + +Задача обработчика `mouseup`: +
      +
    1. Обработать успешный перенос, если он идет (существует аватар)
    2. +
    3. Очистить данные `dragObject`.
    4. +
    + +Это дает нам следующий код: + +[js] +document.onmouseup = function(e) { + // (1) обработать перенос, если он идет + if (dragObject.avatar) { + e = fixEvent(e); + *!*finishDrag(e)*/!*; + } + + // в конце mouseup перенос либо завершился, либо даже не начинался + // (2) в любом случае очистим "состояние переноса" dragObject + dragObject = {}; +} +[/js] + +Для завершения переноса в функции `finishDrag(e)` нам нужно понять, на каком элементе мы находимся, и если над `droppable` -- обработать перенос, а нет -- откатиться: + +[js] +function finishDrag(e) { + var dropElem = *!*findDroppable(e)*/!*; + + if (dropElem) { + ... успешный перенос ... + } else { + ... отмена переноса ... + } +} +[/js] + +### Определяем элемент под курсором + +Чтобы понять, над каким элементом мы остановились -- используем метод [document.elementFromPoint(clientX, clientY)](https://developer.mozilla.org/en/DOM/document.elementFromPoint), который мы обсуждали в разделе [координаты](#elementFromPoint). Этот метод получает координаты *относительно окна* и возвращает самый глубокий элемент, который там находится. + +Функция `findDroppable(event)`, описанная ниже, использует его и находит самый глубокий элемент с атрибутом `droppable` под курсором мыши: + +[js] +// возвратит ближайший droppable или null +// *!*предварительный вариант findDroppable, исправлен ниже!*/!* +function findDroppable(event) { + + // взять элемент на данных координатах + var elem = document.elementFromPoint(event.clientX, event.clientY); + + // пройти вверх по цепочке родителей в поисках droppable + while(elem != document && elem.getAttribute('droppable') == null) { + elem = elem.parentNode; + } + + return elem == document ? null : elem; +} +[/js] + +Обратите внимание -- для `elementFromPoint` нужны координаты относительно окна `clientX/clientY`, а не `pageX/pageY`. + +Вариант выше -- предварительный. Он не будет работать. Если попробовать применить эту функцию, будет все время возвращать один и тот же элемент! А именно -- *текущий переносимый*. Почему так? + +..Дело в том, что в процессе переноса под мышкой находится именно аватар. При начале переноса ему даже `z-index` ставится большой, чтобы он был поверх всех остальных. + +**Аватар перекрывает остальные элементы. Поэтому функция `document.elementFromPoint()` увидит на текущих координатах именно его.** + +Чтобы это изменить, нужно либо поправить код переноса, чтобы аватар двигался *рядом* с курсором мыши, либо поступить проще: + +
    1. Спрятать аватар.
    2. +
    3. Вызывать `elementFromPoint`.
    4. +
    5. Показать аватар.
    + +Напишем вспомогательную функцию `getElementUnderClientXY(elem, clientX, clientY)`, которая это делает: +[js] +/* получить элемент на координатах clientX/clientY, под elem */ +function getElementUnderClientXY(elem, clientX, clientY) { + // сохранить старый display и спрятать переносимый элемент + var display = elem.style.display || ''; + elem.style.display = 'none'; + + // получить самый вложенный элемент под курсором мыши + var target = document.elementFromPoint(clientX, clientY); + + // показать переносимый элемент обратно + elem.style.display = display; + + if (!target || target == document) { // такое бывает при выносе за границы окна + target = document.body; // поправить значение, чтобы был именно элемент + } + + return target; +} +[/js] + +Правильный код `findDroppable`: +[js] +function findDroppable(event) { + var elem = getElementUnderClientXY(dragObject.avatar, event.clientX, event.clientY); + + while(elem != document && elem.getAttribute('droppable') == null) { + elem = elem.parentNode; + } + + return elem == document ? null : elem; +} +[/js] + + +## Сводим части фреймворка вместе + +Сейчас в нашем распоряжении находятся основные фрагменты кода и решения всех технических подзадач. + +Приведем их в нормальный вид. + +### `dragManager` + +Перенос будет координироваться единым объектом. Назовем его `dragManager`. + +Для его создания используем не обычный синтаксис `{...}`, а вызов `new function`. Это позволит прямо при создании объявить дополнительные переменные и функции в замыкании, которыми могут пользоваться методы объекта, а также назначить обработчики: + +[js] +var dragManager = new function() { + + var dragObject = {}; + + var self = this; // для доступа к себе из обработчиков + + function onMouseDown(e) { ... } + function onMouseMove(e) { ... } + function onMouseUp(e) { ... } + + document.onmousedown = onMouseDown; + document.onmousemove = onMouseMove; + document.onmouseup = onMouseUp; + + this.onDragEnd = function(dragObject, dropElem) { }; + this.onDragCancel = function(dragObject) { }; +} +[/js] + +Всю работу будут выполнять обработчики `onMouse*`, которые оформлены как локальные функции. В данном случае они ставятся на `document` через `on...`, но вы легко можете поменять это на `addEventListener/attachEvent`. + +Внутренний объект `dragObject` будет содержать информацию об объекте переноса. + +У него будут следующие свойства: +
    +
    `elem`
    +
    Текущий зажатый мышью объект, если есть (ставится в `mousedown`).
    +
    `avatar`
    +
    Элемент-аватар, который передвигается по странице.
    +
    `downX/downY`
    +
    Координаты, на которых был клик `mousedown`
    +
    `shiftX/shiftY`
    +
    Относительный сдвиг курсора от угла элемента, вспомогательное свойство вычисляется в начале переноса.
    +
    + +**Задачей `dragManager` является общее управление переносом.** Для обработки его окончания вызываются методы `onDrag*`, которые назначаются внешним кодом. + +Разработчик, подключив `dragManager`, описывает в этих методах, что делать при начале и завершении переноса. + +[smart] +Если в вашем распоряжении есть современный JavaScript-фреймворк, то можно заменить вызовы методов `onDrag*` на генерацию соответствующих событий. +[/smart] + +### Реализация переноса иконок + +С использованием этого фреймворка перенос иконок браузеров в компьютер реализуется просто: + +[js] +dragManager.onDragEnd = function(dragObject, dropElem) { + + // успешный перенос, показать улыбку классом computer-smile + dropElem.className = 'computer computer-smile'; + + // скрыть переносимый объект + dragObject.elem.style.display = 'none'; + + // убрать улыбку через 0.2 сек + setTimeout(function() { dropElem.className = 'computer'; }, 200); +}; + +dragManager.onDragCancel = function(dragObject) { + // откат переноса + dragObject.avatar.rollback(); +}; +[/js] + + +Результат: + + +[iframe border=1 src="dragDemo" height=250 link play] + + + +## Варианты расширения этого кода + +Существует масса возможных применений Drag'n'Drop. Реализациях всех из них здесь превратила бы довольно простой фреймворк в страшнейшего монстра. + +Поэтому мы разберем варианты расширений и их реализации, чтобы вы, при необходимости, легко могли написать то, что нужно. + +### Захватывать элемент можно только за "ручку" + +**Функция `createAvatar(e)` может быть модифицирована, чтобы захватывать элемент можно было, только ухватившись за определенную зону.** + +Например, окно чата можно "взять", только захватив его за заголовок. + +Для этого достаточно проверять по `e.target`, куда, всё же, нажал посетитель. Если взять элемент на данных координатах нельзя, то `createAvatar` может вернуть `false`, и перенос не будет начат. + +### Не всегда можно положить на `droppable` + +Бывает так, что не на любое место в `droppable` можно положить элемент. + +Само решение "можно или нет" может зависеть как от переносимого элемента (или "аватара" как его полномочного представителя), так и от конкретного места в `droppable`, над которым посетитель отпустил кнопку мыши. + +Например: в админке есть дерево всех объектов сайта: статей, разделов, посетителей и т.п. + +
      +
    • В этом дереве есть узлы различных типов: "статьи", "разделы" и "пользователи".
    • +
    • Все узлы являются переносимыми объектами.
    • +
    • Узел "статья" (draggable) можно переносить в "раздел" (droppable), а узел "пользователи" -- нельзя. Но и то и другое можно поместить в "корзину".
    • +
    + +**Эта задача решается добавлением проверки в `findDroppable(e)`. Эта функция знает и об аватаре и о событии.** При попытке положить в "неправильное" место функция `findDroppable(e)` должна возвращать `null`. + + +#### Проверка прав + +Рассмотрим эту задачу поглубже, так как она встречается часто. + +Есть две наиболее частые причины, по которым аватар нельзя положить на потенциальный `droppable`. + +Первую мы уже рассмотрели: "несовпадение типов". Это как раз тот случай, когда "узлы-разделы" дерева админки могут принимать только "статьи", и не могут -- "посетителей". + +В этом случае мы можем проверить это в `findDroppable`, так как вся необходимая информация о типах у нас есть. + +Вторая причина -- посложнее. + +**Может не хватать прав на такое действие с `draggable/droppable`, в рамках наложенных на посетителя ограничений.** Например, человек не может положить статью именно в данный раздел. + +А возможно, объект переноса удален из базы администратором, и браузер об этом (пока) не знает -- мы должны корректно обрабатывать все случаи, включая этот. + +Здесь сложность в том, что **окончательное решение знает только сервер**. Значит, нужно сделать запрос. А элемент после отпускания мыши не может "зависнуть" над элементом в ожидании ответа, нужно его куда-то положить. + +**Как правило, применяют "оптимистичный" сценарий, по которому мы считаем, что перенос обычно успешен.** + +При нём посетитель кладет объект туда, куда он хочет. + +Затем, в коде `onDragEnd`: +
      +
    1. Визуально обрабатывается завершение переноса, как будто все ок.
    2. +
    3. Производится асинхронный запрос к серверу, содержащий информацию о переносе.
    4. +
    5. Сервер обрабатывает перенос и возвращает ответ, все ли в порядке.
    6. +
    7. Если нет -- выводится ошибка и возвращается `avatar.rollback()`. Аватар в этом случае должен предусматривать возможность отката после успешного завершения.
    8. +
    + +**Процесс общения с сервером сопровождается индикацией загрузки и, при необходимости, блокировкой новых операций переноса до получения подтверждения.** + + + + +### Индикация переноса + +Можно добавить методы `onDragEnter/onDragMove/onDragLeave` для интеграции с внешним кодом, которые вызываются при заходе (`onDragEnter`), при каждом передвижении (`onDragMove`), и при выходе из `droppable` (`onDragLeave`). + +При этом бывает, что нужно поддерживать перенос *в элемент*, но и перенос *между элементами*. Разберем два варианта такой ситуации: + +
    +
    Поддержка трех видов переноса: "между", "под", "над" `droppable`
    +
    Этот сценарий встречается, когда можно вставить статью как *в* существующий "узел-раздел" дерева, так и *между* разделами. + +В этом случае `droppable` делится на 3 части по высоте: 25% - 50% - 25%, берутся координаты элемента и определяется попадание координаты события на нужную часть. + +В параметры `dragObject` добавляется флаг, обозначающий, куда по отношению к найденному `droppable` происходит перенос. +
    +
    Поддержка переноса только "между"
    +
    Здесь есть два варианта. +
      +
    1. Во-первых, можно разделить `droppable` в отношении 50%/50% и по координатам смотреть, куда мы попали.
    2. +
    3. В некоторых случаях, например при вставке между `droppable`-элементами списка, можно считать, что *элемент вставляется перед тем `LI`, над которым проходит*. + +А чтобы вставить после последнего -- нужно перетащить аватар на сам DOM-элемент списка, но не на существующий `droppable`, а в пустое место, которое оставляется внизу для этой цели.
    4. +
    +
    +
    + +Индикацию "переноса между" удобнее всего делать либо раздвижением элементов, либо показом полосы-индикатора border-top/border-bottom, как показано на рисунке ниже: + +[img src="between.png"] + +### Анимация отмены переноса +Отмену переноса и возврат аватара на место можно красиво анимировать. + +Один из частых вариантов -- скольжение объекта обратно к исходному месту, откуда его взяли. Для этого достаточно поправить `avatar.revert()` соотвествующим образом. + +## Итого + +Алгоритм Drag'n'Drop: +
      +
    1. При `mousedown` запомнить координаты нажатия.
    2. +
    3. При `mousemove` инициировать перенос, как только зажатый элемент передвинули на 3 пикселя или больше. +
        +
      1. Создать аватар, если можно начать перенос с этой точки `draggable`.
      2. +
      3. Перемещать его по экрану. Новые координаты ставить по `e.pageX/pageY` с учетом изначального сдвига элемента относительно курсора.
      4. +
      +
    4. +
    5. При `mouseup` обработать завершение переноса. Элемент под аватаром получить по координатам, предварительно спрятав аватар.
    6. +
    + +Получившийся попутно с обсуждением технических моментов Drag'n'Drop фреймворк обладает рядом особенностей: + +[compare] ++Он прост. Он действительно очень прост. ++Он полностью кросс-браузерный, не содержит хаков. ++Он позволяет работать с большим количеством потенциальных `draggable`/`droppable`. ++Его легко расширить и поменять. +[/compare] + +Вы можете получить файлы и посмотреть итоговое демо [edit src="dragDemo"]в песочнице[/edit]. + + +В зависимости от ваших задач, вы можете либо использовать его как отправную точку, либо реализовать свой. + +ООП-вариант фреймворка находится в статье [](article:372). +[head] + +[/head] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/07-drag-and-drop-objects/between.png b/tutorial/02-ui/03-event-details/07-drag-and-drop-objects/between.png new file mode 100755 index 0000000000000000000000000000000000000000..f570158fdf6fa064151221901f8fcc88902e64be GIT binary patch literal 7518 zcmZvgbx_n%_wc_+HxklFcX!t!jR*@asUWe0bayv`^ioU7E}@`wN`vGIh$tzrbV*6W zkLP**dS~95d+wPt_s*R;=gv9jb8o_n=NiNWbOZnZ5Nm0w89wO#2dTq*^f0G)o&R}I z`0kpfUI0Ky@gKne@(O4GfI#071bXqp!Oh#v%fZc^SqlVWcK39%cYJFH0RD4@V5kvz z`-#H&(y6LWe0Zjgn;{h*v!QBIIAt0qFAF}Ic05bTZz_YH$IqT&(SIwB$4p5HPo^^9 zCrTvvg^Ok>O^GUtPaL^k2`Y2`*>S!xe%CmwxcTvhMt@w#Qh04_r1<@s2{?mkbK(}6gw=hH8J?V~Wt_4PT zG)A{~nzRmHI3=JQn4*dRl+`i93t^mKpco6V8nt<|2D}gethmB9rh)Lni`*a#z&w+M z3Zpm;V5YE(R|6d6fRCt`iO&ENVSv(Jr(Xv6B>)I(LF_ew&&@#h1Q}r+fJX!f8^p)( z09ZkQ)i*Xae;_&+pj1DG$RF|5kZtllL@KvVzLj0(S%f*BfCnxFBFMx&p+(I{DQW%4 zI!{@!Hz1c*G(w#8&&e176sJ)=czbf|KS5G6F(H}KMDki-rwiwv_05~LyLD8xyD|W* zc!y5j@e9?{h0EcDyWEv>9AG)TA}sbfPqMEgRc-`|x91=y?*EC+vy%6%zkjcz(Z96& zRn4tOAR%|q-q+ob+qajY@;B#af4;7=M~GTRXkcCZ=^Z=OEoVTb<43<)*htm5X(qhC zVVq{|*Lu@x!b`bkK<<&EmX~}YTF#oFn$9!8F?Dady3Bu#skkfwe0EaA2uc&Eu(ZMa zr^7{N;@!P^4gkmPZaqJ^@i4<3qE=9T_lHWi8pRwyxV=`o2LQZMV;3|UsaG1p0|2$+ z2;R>s^rt;E{M|SVJ=hC9gf~`_(W>nIy{crY1h(M}o>n{`BUQO$$3HXjS_%D7W)$x> zaEQ%tClTy5ZXr={C%v-9gZFTEMBrnoe8Z!#W|@n}c#~+tmV!r}8*{;qEOOT(SPQ!h(Z+s$Zn}@iS~jzp#EJ zsve~K{PH7ad%~Fw>Wd#1dBn4Bd`T=nRc31@6-^UeljpxQE0`JB#UIb%OW|Wi8}xFq zW>n}jaJD_Z?5BAnDiUX+$prDO**!W;W=C9I$W7derCRJG3Y{cA6g7ek7hYGCn$WQ?D63Oyvl7j;-pQxPPQ(- zj$;QCRWUBhGhD1FF?uYKYNqAg+Q!+$T?FJnD&-PWh+OtZ6z2YKL>j+AZ(4 z$BS7^&vU**rbf3lwsN*GPyL99W2vHs;-TEM`82pRP+C8lT9__^b~=f6$Vga9WEZxX zOPcFuY{J{b8%&ipLXf4M#hFFVD`Zqzg{WGpA~ZfR9yLm>d<71vQ8yL?uT|pz$gVc7 zDtxgErUyeS+&ts-_7&Q;kt*(TMR)dtlXyPc9X`^Z8>=d*Ux>j)B+A#E-TE)8hv1a{#j_&0bF zNq~H!ryzS)BUt5JPG8cl=GWzlJb=N!z6yuyx*?YWIybKvLWFLOZe6aBXBtE%IQ2yA zIP*jkH1Y!y9&YlCLPPUM1|Z8mWl*_wmUYfbbXx1AeA$fpJ9W}z$3crV_BEG5m%%bl z98MNa9lagBgFHjMRs;`1PFGHMXnbtEbo^(oM81N^tmtO`X8vscan-hMOvzX+N+3 z!kMpWa%l>&hQ6VOHV@SF9b|N7gtUdcyH&s?j{8kg&1l9L>eb*$Df?1(DQ}|SKq+M4 zG<#bJYJws|{SsSiq8R1rcPs?wPisJ_p{-s~C-F%MI+3G-If{b$^n;7vI?>wRXC9U- zfm3IN`Stm|`xW!G2a*Sl2XPBUW8y_Ta8og_?|t7ue+B|pPS45s?MzssTKbzdKRrIuE|R+i7uJG9+T>B{?VEI5Bp0`;P^qLweu`Z0`NZPzZSVaj zmh(;6MB0QSNz9ATt)Jgk1{p!#8cEl;Uzcz8PvTY!R|(gG zs9{3iEZqZ2CVEd}kWVTF$@yiom38BvOK1L7A>*xhlFV^T;m46e5YWrm3uP6Wl$?zJ z1+w5}dVZ|Qs~4EQm)0Q>z&;>W&cEIo%~konLzh^0F<%z`B+r-GMN~nyQzR-q?Y)O; z6^mX*6Lbfi$++l>LTm$j) z%ZB~4f6wNHa>lvm+oPI5tz}*EEgg2q;fZlMC8G~Ur$$#sk_S_Zjn=alu9FA!K^_0Z z_h#nJJI-$vhriAnV;Jkr*w5HqYhBl$E7c@aZC_+xyI6jD?&7gOggJ$Ii_1pRG~4=P zhH*;4$T0OM->wG9(9^GXg@=9u#FE6u?+ON2+1%9v(A($u%jZnQvU9%4K_6Fer=R*V zLK%geRP&DW)bqi4*XD1`+w{j2Cx7lN1>R`PKP4>?mp2X2J~0}0oZF6^&VU}Hk_)p6 zLqY{F7p&$>n}RlNCYGH@-45HxyjP~4-6w6zb;wOv9JhEM*Uhg}Kw2PqwX&~WcD#4w z?=q(KhuRC;^J-cxoLdh9I~#-S&$sE}QWJ}(odHKr?tjj}WfT@WPXgV}2hPuypDc!Y za@|ghw*U6yI{4I~-|_lXd0*u>XfbX^$^35N*Wp5#Q@3C7$>f5@IZjLz>Mrdpl@x@X znwvTrOB}lkmsXUKSyl49-M&GBP_`&eIQ3oW-DnN<6QR(HncdlmN!Vmb&T&pgmrHQ@ z?N0YDZSDME?;z9HVSh1^SEnzp_j~*n{1^|*mPz%|!OUUY_YRj|&cNT1*BP`K8TVM2 zu#+1bF@gC9-o%2c=&1m}mz3aZgNz58*+$b)4*&u=03bXH050zz^fmza2m`>56#&S> z0D#&p{q=x40FVc0si_$G&+QeOzh#&W8SKg}*YtRnNNuocxnZ0$)i%|{uKnDWN81M3 zScv6=iR8AT+Qqm!UdK&9bchnL(*$ z!Ux?&%`}UHiFJ#s&p!fXnhQ+@A#;oO=gI{NUK?Y31o9fy+;%j(%D3vv?xx#Kns?Y_ftG}UcVeC~QeozWa za3l{*9pUcgBq^Y0lx3|4UOejgvKDoloo)^>?r7-D78LJ6^NvS5Du&~S$LcZaNHq$E z)b*Ji&lH_e=wfB$5T(Fy04F?EE6%DSX@%gOJ>l>J;_=1Z{LxWuBW&ZDpNv$`Fy8w! zSzl`MO2e==MJOIq@G=us5CrPKV~~;jy9f74j3gy35W{YA*$(f2gBj`7Hgf9FZJg{8 zLMOs#z?c_Nk>a(falW_o{fC7)q$?OzLkyiD9`bg;IpwCQ?`1Df_R~x(Wk1&%!8BFm z6cxx%_u_XnJUG=Q6MJM}^AFRAn}8_AQ4bJ~F6DMI!ZaGAAtMXj(Z%6|r-U^Cn=laj zy0C6RQbmMjH0BEC_%#T+ccMuPmN3Nq;$B0X}lLMY_%|>FkN}fR5Ss( zu$N0S{U$z-fdH_O4YIRE`+{ut6P5^HOt!Z9JX=eojRL7ogCa$WX0CE$=Xt#QlQPAG zwFJF6%i2mRcDe^5zD&vyk2mgH?mrZR#42r;=&C^?&Y17x&(f2MxBO46R}wTwj^J9J z<76UHpyP+#5{(jPE_`Kq`1suNDfE=es?*W$7og3VG)Tka6>05sPjXKC9Yha3uMW_- zGQfD0n_>GRkr|tri5$~6}0l& zJl$1YkEI1|i04ky zQ~oF2%bimugOiOOjL~;G=iFtpd^1R2)ZW)Gv#HDNk?gcv9b22~g)_lUot`@(%+WQE z)Udd`ap;Ee9V%c$BgKZy8duSG{62&N~bJdS&IbCWx&TR{F3&aGm z`{st|lafG-`U+17VpfxLRTkP@nRE`{q{wk@nveL;7S-{3`ryFA30Zh~dDELc^qHlh zk@ZazZ8c}_jtVxicOHgUSLfyB<^00ppsfe4|3wCW{@!H@$9`;Nf5gI$7J43^4FZ)F z9(ePm8dA&YmNf7NF6=RAx*Q`Ei!<1D`N^j~m1yaZo~Z+cO+&%dSD*n7XwF_!YX0!{ z?$m)90=?=hKsbtdtt(84*n1ArXh?V9bze?PVHgRS_hQjx6eKmEv^a-7z{mG&CW+;5 z9fvv@T91Akq%j0?TFyE|P5W7WMn$ z&#%Cr!z99D@@Z{DyQinHFb%ua}Q~&0Z@OF)%Q|6tgysaGdwgfy)Q`CNz|B3FaE4p0_Yv zo29^R)KLdse!)J4uhuV#z;#Mu@W20{gYD(9*?QSGvtbvtfbZTGzPGoMqTG<^znABx z_q_pfi@akjsNP^}?F%tES=m*5w=je`ijtD8xizzhK}q4g9oV;{a7iilCXVL-m5f~b z-v)6MVWHg%{5T;({3iEF$bIcsF;w+lSMq%CD$}BgWM?7MlWB_;o*<1@!>eP_BQnElIrH}&NLdzYNv~)V}7j59+{9iGy=ySxYM36M?+X(7H0v= zGj6v%MP{P+`_x^(T6eL7R;rH4o#@;0y=HtSdU+N(G6LnadqQQtuo*HXsm}m4}7~9iKTc#a|c)yyl9Gu-z`Dy)2 z)63a==3>}djOdu@LKf48G)w=Il5^Z6o{QP9R}Imk^AbGo$qn(GQdQ%IS7nYOwGy9p z{^J9bZmw%lBwj~Sa6?tzDX+}>4y{HZ#clhhQ{SqV^^CG(mTpQ14qYC9)}!AmuSSKN z>%Or3?Q|g1SY@j-vuBd#r6xcq^H~$Hg#Nt}3uleK&_-noaV?R*V75)Po`8-WUBU#0 z4K9VPwSCQGQv$t$IFF*1nlWxHQBOhgEx8%*37Drka!yQIM-}rve2inrBBl7xd5O_Q zufO#TJvWPOy_(At|318tn$M6$hzEXwEYDokF#k%ee=6VjTZwy#_>b4QhHc}Ryt|Pg zp04A^=-hv`KLxlt?F4RTpH`SDRoh)vcDO$!zA&=$SsW0<1!oTRQpkT;_@Tu;rTy} z(m6jr$^WeMh>OMqfk_W){{N6cmu2OdW#23Qv>Qvg50O%yZ2@lXLjHab>J^1x7L+&V+5V0F5e7W+FLsB=`eDzGviy9SnggpdHaabRdC>vlDw;_)G-IfQrermGl7j^t-ZL(8e z14`{!g7_grIsfj;;*bT@P_E>bA~F012%W_BB_cH#)en0D`hoUf+bFMxsH?k5iT{Le zbplj{)#ecK8kmSQySM^7%?TK39v+aoTw3(iDPjYpi0Vhp5;b5|8ksH z3}zydFm1~G=<=NB+vn!n%&3gBRPl`8(drVEH<*r#yw{th=M(|#heM(2>BS?s7O1c!Cl zL+HP5i-4twmtL4Ysvs6Rh({H$M;T)A07uLiuw*gt`PQb`R-AXo0#9vYQ5kdb{`7tB zg-FtsU&99b|AR2u6PkHcO13O5g2Rqlys)_JhZ6HZ#F{5$G6GuIm=rPs_z2y^f^01C zpi%g9ok+(L@u$dgjyR?&DskNtZ!_7iJFWf6PQ0=F<;aGuz$3GIrj=_%MGmoG@G zIj{oyXk)*@n|;^@+gK+jd#Z7Zi;m0UNxGMqK>(%i37&u4IXlc=LLX^|$ZV3Ce*Cz3 zQQ;@~zsnPMIqn`T{^V&<;!6=|?{Ldjld{{~(3$f0>jgYSjE@;&_L*HGjvE6e<81GA zUz;NbvRq;))p>GKay#QwGJo$jGK>}W*3Zk-&kWINVmG(@3P;v;q&_5`&o+Hp6cfxH z_(b-)tdvk?Bo6MVwOOP@kYlwmqC|m9`}iSc#mCYdV1!lI)M4a|zoElKemB+uYf_-3 z-HKa01dy1r=X|k9n(lS6@8?<=I`o|r;@zpeuOW_m)F~w+GEap(+0^6X>mO>#J^ypcYtnW%7@(Vuf{xdtL#5Xe>|uE~J)cn@>M7 zMMqm5^G(>DARx?q_EGi|cZKB~t?iM9@*RhEP6m;~jGV(g&wD%Yx$F5cenw}$pyaRG zY@+T=XtvLWw7RDP&qvH*>}$d{-w|D7h8QKPQ~>i2(|ZoAOzq(g`9J?!4izC)?Cbct z2r^+dVdzGRvzE#b#cc+iNKEk)4w%IN3`k<3i|9nX04}Xq%YhOa8!U<*v>*#0#%q%| zFvg$3iLQUDQ}#pTvF-zf;UH|(8&P()Q?CJ&D#8hV3eI+y@frqVBUNGSY-?dW#?c(! zqe|ake=tY%Bh3n!9$?jGE!>RXQiR2WvNr1oKEErvZ%6fx=ikMfow6jl#{REz=F)En z!WGmZ5o!t)37mgaDB%m{2s-81jXsUv^0<$d`W-nHY*>lr`Xfo z?9Jc)^l$jism51o4XfBrMv=oHjDqYXVTG?AyD?%Kl{19Yh=#&!#q|u~MMzGEH3`?(jpEgnijB<{FjrLY3G)8CI+Fbt zJ+mG4v>I2*mlLq`t@ zGR67Ix#;Mar>tH~q9FBtJ-u$O!66JWi zgBN}hhgQ7R`>GSZ6h|vSa{kc5$gBxx)ScM)$^{hQmcsi+x5-`>5W2B$@G!9{Bsmr za4S95lE^rt8{OU_T2;s!J` + + + + + + + + + + + + + + + + +

    Браузер переносить сюда:

    + +
    +
    + + + + diff --git a/tutorial/02-ui/03-event-details/07-drag-and-drop-objects/dragDemo.code/lib.js b/tutorial/02-ui/03-event-details/07-drag-and-drop-objects/dragDemo.code/lib.js new file mode 100755 index 000000000..6d2a78154 --- /dev/null +++ b/tutorial/02-ui/03-event-details/07-drag-and-drop-objects/dragDemo.code/lib.js @@ -0,0 +1,54 @@ + +function fixEvent(e) { + e = e || window.event; + + if (!e.target) e.target = e.srcElement; + + if (e.pageX == null && e.clientX != null ) { // если нет pageX.. + var html = document.documentElement; + var body = document.body; + + e.pageX = e.clientX + (html.scrollLeft || body && body.scrollLeft || 0); + e.pageX -= html.clientLeft || 0; + + e.pageY = e.clientY + (html.scrollTop || body && body.scrollTop || 0); + e.pageY -= html.clientTop || 0; + } + + if (!e.which && e.button) { + e.which = e.button & 1 ? 1 : ( e.button & 2 ? 3 : ( e.button & 4 ? 2 : 0 ) ) + } + + return e; +} + + +function getCoords(elem) { + var box = elem.getBoundingClientRect(); + + var body = document.body; + var docElem = document.documentElement; + + var scrollTop = window.pageYOffset || docElem.scrollTop || body.scrollTop; + var scrollLeft = window.pageXOffset || docElem.scrollLeft || body.scrollLeft; + + var clientTop = docElem.clientTop || body.clientTop || 0; + var clientLeft = docElem.clientLeft || body.clientLeft || 0; + + var top = box.top + scrollTop - clientTop; + var left = box.left + scrollLeft - clientLeft; + + return { top: Math.round(top), left: Math.round(left) }; +} + +function getElementUnderClientXY(elem, clientX, clientY) { + var display = elem.style.display || ''; + elem.style.display = 'none'; + + var target = document.elementFromPoint(clientX, clientY); + + elem.style.display = display; + + return target; +} + diff --git a/tutorial/02-ui/03-event-details/07-drag-and-drop-objects/shiftx.png b/tutorial/02-ui/03-event-details/07-drag-and-drop-objects/shiftx.png new file mode 100755 index 0000000000000000000000000000000000000000..1d4a3a05c5486bf9a622675428cbdec572dfe027 GIT binary patch literal 4847 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 +[/html] + + +### Какими бывают скан-коды? + +**Для буквенно-цифровых клавиш, скан-код будет равен коду соответствующей заглавной английской буквы/цифры.** + +Например, при нажатии клавиши [key S] (не важно, каков регистр и раскладка) её скан-код будет равен `"S".charCodeAt(0)`. + +С другими символами, например, знаками пунктуации, ситуация тоже понятна. Есть таблица кодов, которую можно взять, например, из статьи Джона Уолтера: JavaScript Madness: Keyboard Events, или же можно нажать на нужную клавишу на [тестовом стенде](#keyboard-test-stand) и получить код. + +Когда-то в этих кодах была масса кросс-браузерных несовместимостей. Сейчас всё проще -- таблицы кодов в различных браузерах почти полностью совпадают. + +Но некоторые несовместимости, всё же, остались. Вы можете увидеть их в таблице ниже. Слева -- клавиша с символом, а справа -- скан-коды в различных браузерах. + + + + + + + + + + + + + + + + + + +
    КлавишаFirefoxОстальные браузеры
    [key ;]59186
    [key =]107187
    [key -]109189
    + + +## Событие `keypress` + +**Событие `keypress` возникает сразу после `keydown`, если нажата *символьная* клавиша, т.е. нажатие приводит к появлению символа.** + +Любые буквы, цифры генерируют `keypress`. Управляющие клавиши, такие как [key Ctrl], [key Shift], [key F1], [key F2].. -- `keypress` не генерируют. + +Событие `keypress` позволяет получить *код символа*. В отличие от скан-кода, он специфичен именно для символа и различен для `"z"` и `"я"`. + +Код символа хранится в свойствах: `charCode` и `which`. Здесь скрывается целое "гнездо" кросс-браузерных несовместимостей, разбираться с которыми нет никакого смысла -- запомнить сложно, а на практике нужна лишь одна "правильная" функция, позволяющая получить код везде. + +[ref id="getChar"] +### Получение символа в `keypress` + +Кросс-браузерная функция для получения символа из события `keypress`: + +[js autorun] +// event.type должен быть keypress +function getChar(event) { + if (event.which == null) { // IE + if (event.keyCode < 32) return null; // спец. символ + return String.fromCharCode(event.keyCode) + } + + if (event.which!=0 && event.charCode!=0) { // все кроме IE + if (event.which < 32) return null; // спец. символ + return String.fromCharCode(event.which); // остальные + } + + return null; // спец. символ +} +[/js] + + +Для общей информации -- вот основные браузерные особенности, учтённые в `getChar(event)`: + +
      +
    1. Во всех браузерах, кроме IE, у события `keypress` есть свойство `charCode`, которое содержит код символа.
    2. +
    3. Браузер IE для `keypress` не устанавливает `charCode`, а вместо этого он записывает код символа в `keyCode` (в `keydown/keyup` там хранится скан-код).
    4. +
    + +Также, посмотрите -- в функции выше используется проверка `if(event.which!=0)`, а не более короткая `if(event.which)`. Это не случайно! При `event.which=null` первое сравнение даст `true`, а второе -- `false`. + +В действии: +[html] + +[/html] + + + +[warn header="Неправильный `getChar`"] +В сети вы можете найти другую функцию того же назначения: +[js] +function getChar(event) { + return String.fromCharCode(event.keyCode || event.charCode); +} +[/js] + +Она работает неверно для многих специальных клавиш, потому что не фильтрует их. Например, она возвращает символ амперсанда `"&"`, когда нажата клавиша 'Стрелка Вверх'. +[/warn] + +[smart header="Свойства-модификаторы"] + +Как и у других событий, связанных с пользовательским вводом, поддерживаются свойства `shiftKey`, `ctrlKey`, `altKey` и `metaKey`. + +Они установлены в `true`, если нажаты клавиши-модификаторы -- соответственно, [key Shift], [key Ctrl], [key Alt] и [key Cmd] для Mac. +[/smart] + +## Отмена пользовательского ввода + +Появление символа можно предотвратить, если отменить действие браузера на `keydown/keypress`: + +[html] +Попробуйте что-нибудь ввести в этих полях: + + +[/html] + +Попробуйте что-нибудь ввести в этих полях (не получится): + + + + + +При тестировании на стенде вы можете заметить, что отмена действия браузера при `keydown` также предотвращает само событие `keypress`. + +[warn header="При `keydown/keypress` значение ещё старое"] +На момент срабатывания `keydown/keypress` *клавиша ещё не обработана браузером*. + +Поэтому в обработчике значение `input.value` -- старое, т.е. до ввода. Это можно увидеть в примере ниже. Вводите символы `abcd..`, а справа будет текущее `input.value`: `abc..` + + + +А что, если мы хотим обработать `input.value` именно после ввода? Самое простое решение -- использовать событие `keyup`, либо запланировать обрабочик через `setTimeout(..,0)`. +[/warn] + + +[warn header="Не для работы с формами"] +Распространённая ошибка -- использовать события клавиатуры для работы с полями ввода в формах. + +Это неправильно. События клавиатуры предназначены именно для работы с клавиатурой. Да, их можно использовать и для ``, но будут побочные эффекты. + +Например, текст может быть вставлен мышкой, при помощи правого клика и меню, без единого нажатия клавиши. И как нам помогут события клавиатуры? + +Некоторые мобильные устройства не генерируют `keypress/keydown`, а сразу вставляют текст в поле. Отменить ввод для них таким образом нельзя, как и вообще отловить его. + +Далее мы разберём [события для элементов форм](article:430), которые позволяют работать с вводом в формы правильно. +[/warn] + +### Отмена любых действий + +**Отменять можно не только символ, а любое действие клавиш.** + +Например: +
      +
    • При отмене [key Backspace] -- символ не удалится.
    • +
    • При отмене [key PageDown] -- страница не прокрутится.
    • +
    • При отмене [key Tab] -- курсор не перейдёт на следующее поле.
    • +
    + +Конечно же, есть действия, которые в принципе нельзя отменить, в первую очередь -- те, которые происходят на уровне операционной системы. Комбинация Alt+F4 инициирует закрытие браузера в Windows, что бы мы ни делали в JavaScript. + +### Демо: перевод символа в верхний регистр + +В примере ниже действие браузера отменяется с помощью `return false`, а вместо него в `input` добавляется значение в верхнем регистре: + +[html] + + +[/html] + +В действии: + + + +[ref id="keyboard-events-order"] +## Особенности клавиш и ОС + +События `keydown/keyup` возникают всегда. А `keypress` -- по-разному. + +Есть три основных категории клавиш, работа с которыми отличается. + +Перечислим их в таблице, обращая основное внимание на особенности работы с ними. + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    КатегорияПримерыСобытияОписание
    Печатные клавиши[key S] [key 1] [key ,]`keydown` +`keypress` +`keyup`Нажатие вызывает `keydown` и `keypress`. +Когда клавишу отпускают, срабатывает `keyup`. + +Исключение -- CapsLock под MacOS, он глючит неимоверно: +
      +
    • В Safari/Chrome/Opera: при включении только `keydown`, при отключении только `keyup`.
    • +
    • В Firefox: при включении и отключении только `keydown`.
    • +
    +Попробуйте на тестовом стенде. +
    Специальные клавиши[key Alt] [key Esc] [key ⇧]`keydown` +`keyup`Нажатие вызывает `keydown`. +Когда клавишу отпускают, срабатывает `keyup`. + +Некоторые браузеры могут дополнительно генерировать и `keypress`, например IE для [key Esc]. Попробуйте нажать [key Esc] на тестовом стенде в разных браузерах -- и увидите: в IE `keypress` есть, а в остальных -- нет. + +На практике это не доставляет проблем, так как для специальных клавиш мы всегда используем `keydown/keyup`. +
    Сочетания с печатной клавишей + + [key Alt+E] + [key Ctrl+У] + [key Cmd+1] + +`keydown` +`keypress?` +`keyup` +Браузеры под Windows -- не генерируют `keypress`, браузеры под MacOS -- генерируют. + +Кроме того, если сочетание вызвало браузерное действие или диалог ("Сохранить файл", "Открыть" и т.п., ряд диалогов можно отменить при `keydown`), то `keypress/keyup` может не быть. +
    + +## Автоповтор + +При долгом нажатии клавиши возникает *автоповтор*. По стандарту, должны генерироваться многократные события `keydown (+keypress)`, и вдобавок стоять свойство [repeat=true](http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent-repeat) у объекта события. + +То есть поток событий должен быть такой: + +[txt] +keydown +keypress +keydown +keypress +..повторяется, пока клавиша не отжата... +keyup +[/txt] + +Однако в реальности на это полагаться нельзя. На момент написания статьи, под Firefox(Linux) генерируется и `keyup`: + +[txt] +keydown +keypress +keyup +keydown +keypress +keyup +..повторяется, пока клавиша не отжата... +keyup +[/txt] +...А Chrome под MacOS не генерирует `keypress`. В общем, "зоопарк". Полагаться можно только на `keydown` при каждом автонажатии и `keyup` по отпусканию клавиши. + +## Итого, рецепты + +Если обобщить данные из этой таблицы и статьи, то можно сделать ряд выводов: + +
      +
    1. Для реализации горячих клавиш, включая сочетания -- используем `keydown`.
    2. +
    3. Если нужен именно символ -- используем `keypress`. При этом функция `getChar` позволит получить символ и отфильтровать лишние срабатывания.
    4. +
    5. Ловля CapsLock глючит под MacOS. Её можно организовать при помощи проверки `navigator.userAgent` и `navigator.platform`, а лучше вообще не трогать эту клавишу.
    6. +
    + +Для работы с вводом в формы, существуют [события для форм](article:430), которые мы разберём позже. Их можно использовать как отдельно от событий клавиатуры, так и вместе с ними. + +[task id=128] +[task id=509] +[head] + + +[/head] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/question.code/index.html b/tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/question.code/index.html new file mode 100755 index 000000000..1050e2e48 --- /dev/null +++ b/tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/question.code/index.html @@ -0,0 +1,33 @@ + + + + + +Введите ваш возраст: + + + + + + diff --git a/tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/question.md b/tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/question.md new file mode 100644 index 000000000..601933af8 --- /dev/null +++ b/tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/question.md @@ -0,0 +1,13 @@ +# Поле только для цифр + +[importance 5] + +При помощи событий клавиатуры сделайте так, чтобы в поле можно было вводить только цифры. Пример ниже. + +[iframe border=1 src="solution"] + +В поле должны нормально работать специальные клавиши [key Delete]/[key Backspace] и сочетания c [key Ctrl]/[key Alt]/[key Cmd]. + +[edit src="question" task/] + +P.S. Конечно, при помощи альтернативных способов ввода (например, вставки мышью), посетитель всё же может ввести что угодно. \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/solution.code/index.html b/tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/solution.code/index.html new file mode 100755 index 000000000..defe83192 --- /dev/null +++ b/tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/solution.code/index.html @@ -0,0 +1,46 @@ + + + + + +Введите ваш возраст: + + + + + + diff --git a/tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/solution.code/numeric-input/index.html b/tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/solution.code/numeric-input/index.html new file mode 100755 index 000000000..defe83192 --- /dev/null +++ b/tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/solution.code/numeric-input/index.html @@ -0,0 +1,46 @@ + + + + + +Введите ваш возраст: + + + + + + diff --git a/tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/solution.md b/tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/solution.md new file mode 100644 index 000000000..c60b16c19 --- /dev/null +++ b/tutorial/02-ui/03-event-details/08-keyboard-events/01-numeric-input.task/solution.md @@ -0,0 +1,35 @@ +# Подсказка: выбор события + +Нам нужно событие `keypress`, так как по скан-коду мы не отличим, например, клавишу `'2'` обычную и в верхнем регистре (символ `'@'`). + +Нужно отменять действие по умолчанию (т.е. ввод), если введена не цифра. + +# Решение + +Нам нужно проверять *символы* при вводе, поэтому, будем использовать событие `keypress`. + +Алгоритм такой: получаем символ и проверяем, является ли он цифрой. Если не является, то отменяем действие по умолчанию. + +Кроме того, игнорируем специальные символы и нажатия со включенным [key Ctrl]/[key Alt]/[key Cmd]. + +Итак, вот решение: +[js] +input.onkeypress = function(e) { + e = e || event; + + if (e.ctrlKey || e.altKey || e.metaKey) return; + + var chr = getChar(e); + + // с null надо осторожно в неравенствах, + // т.к. например null >= '0' => true + // на всякий случай лучше вынести проверку chr == null отдельно + if (chr == null) return; + + if (chr < '0' || chr > '9') { + return false; + } +} +[/js] + +[edit src="solution"]Открыть полное решение в песочнице[/edit] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/08-keyboard-events/02-check-sync-keydown.task/question.md b/tutorial/02-ui/03-event-details/08-keyboard-events/02-check-sync-keydown.task/question.md new file mode 100644 index 000000000..5c93b4c51 --- /dev/null +++ b/tutorial/02-ui/03-event-details/08-keyboard-events/02-check-sync-keydown.task/question.md @@ -0,0 +1,16 @@ +# Отследить одновременное нажатие + +[importance 5] + +Создайте функцию `runOnKeys(func, code1, code2, ... code_n)`, которая запускает `func` при одновременном нажатии клавиш со скан-кодами `code1`, `code2`, ..., `code_n`. + +Например, код ниже выведет `alert` при одновременном нажатии клавиш `"Q"` и `"W"` (в любом регистре, в любой раскладке) +[js] +runOnKeys( + function() { alert("Привет!") }, + "Q".charCodeAt(0), + "W".charCodeAt(0) +); +[/js] + +[demo src="solution"] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/08-keyboard-events/02-check-sync-keydown.task/solution.code/index.html b/tutorial/02-ui/03-event-details/08-keyboard-events/02-check-sync-keydown.task/solution.code/index.html new file mode 100755 index 000000000..1867633ef --- /dev/null +++ b/tutorial/02-ui/03-event-details/08-keyboard-events/02-check-sync-keydown.task/solution.code/index.html @@ -0,0 +1,52 @@ + + + + + +

    Нажмите одновременно "Q" и "W" (в любой раскладке).

    + + + + + diff --git a/tutorial/02-ui/03-event-details/08-keyboard-events/02-check-sync-keydown.task/solution.code/multikeys/index.html b/tutorial/02-ui/03-event-details/08-keyboard-events/02-check-sync-keydown.task/solution.code/multikeys/index.html new file mode 100755 index 000000000..1867633ef --- /dev/null +++ b/tutorial/02-ui/03-event-details/08-keyboard-events/02-check-sync-keydown.task/solution.code/multikeys/index.html @@ -0,0 +1,52 @@ + + + + + +

    Нажмите одновременно "Q" и "W" (в любой раскладке).

    + + + + + diff --git a/tutorial/02-ui/03-event-details/08-keyboard-events/02-check-sync-keydown.task/solution.md b/tutorial/02-ui/03-event-details/08-keyboard-events/02-check-sync-keydown.task/solution.md new file mode 100644 index 000000000..b7d3275e6 --- /dev/null +++ b/tutorial/02-ui/03-event-details/08-keyboard-events/02-check-sync-keydown.task/solution.md @@ -0,0 +1,11 @@ +# Ход решения + +
      +
    • Функция `runOnKeys` -- с переменным числом аргументов. Для их получения используйте `arguments`.
    • +
    • Используйте два обработчика: `document.onkeydown` и `document.onkeyup`. Первый отмечает нажатие клавиши в объекте `pressed = {}`, устанавливая `pressed[keyCode] = true`, а второй -- удаляет это свойство. Если все клавиши с кодами из `arguments` нажаты -- запускайте `func`.
    • +
    • Возникнет проблема с повторным нажатием сочетания клавиш после `alert`, решите её.
    • +
    + +# Решение + +[edit src="solution"/] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/09-event-onscroll.md b/tutorial/02-ui/03-event-details/09-event-onscroll.md new file mode 100644 index 000000000..8bf671917 --- /dev/null +++ b/tutorial/02-ui/03-event-details/09-event-onscroll.md @@ -0,0 +1,34 @@ +# Прокрутка: событие scroll + +Событие `onscroll` происходит, когда элемент прокручивается. + +В отличие от события `onwheel` (колесико мыши), его могут генерировать только прокручиваемые элементы или окно `window`. Но зато оно генерируется всегда, при любой прокрутке, не обязательно "мышиной". + +[cut] +Например, следующая функция при прокрутке окна выдает количество прокрученных пикселей: + +[js autorun] +window.onscroll = function() { + var scrolled = window.pageYOffset || document.documentElement.scrollTop; + document.getElementById('showScroll').innerHTML = scrolled + 'px'; +} +[/js] + +В действии: +Текущая прокрутка = прокрутите окно + +Каких-либо особенностей события здесь нет, разве что для его использования нужно отлично представлять, как получить текущее значение прокрутки или прокрутить документ. Об этом мы говорили ранее, в главе [](article:80). + +Событие `onscroll` используется достаточно часто для: +
      +
    • Подгрузки содержимого элементов, ставших видимыми после прокрутки.
    • +
    • Показа дополнительных элементов навигации, важных в текущем положении прокрутки.
    • +
    + +Вашему вниманию предлагаются несколько задач, которые вы можете решить сами или посмотреть использование `onscroll` на их примере. + +[task id=508] +[task id=400] + +[task id=378] +[task id=381] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/question.code/index.html b/tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/question.code/index.html new file mode 100755 index 000000000..3c793861b --- /dev/null +++ b/tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/question.code/index.html @@ -0,0 +1,91 @@ + + + + + + + + +
    Шапка
    + +
    +
    +

    Персонажи:

    +
      +
    • Винни-Пух
    • +
    • Ослик Иа
    • +
    • Сова
    • +
    • Кролик
    • +
    +
    +
    +
    +
    + +

    Винни-Пух

    + +
    + +
    Кадр из мультфильма
    +
    + +

    Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге). Один из самых известных героев детской литературы XX века.

    + +

    В 1960-е—1970-е годы, благодаря пересказу Бориса Заходера «Винни-Пух и все-все-все», а затем и фильмам студии «Союзмультфильм», где мишку озвучивал Евгений Леонов, Винни-Пух стал очень популярен и в Советском Союзе.

    + +

    Как и многие другие персонажи книги Милна, медвежонок Винни получил имя от одной из реальных игрушек Кристофера Робина (1920—1996), сына писателя. В свою очередь, плюшевый мишка Винни-Пух был назван по имени медведицы по кличке Виннипег (Винни), содержавшейся в 1920-х в Лондонском зоопарке.

    + +

    Медведица Виннипег (американский чёрный медведь) попала в Великобританию как живой талисман (маскот) Канадского армейского ветеринарного корпуса из Канады, а именно из окрестностей города Виннипега. Она оказалась в кавалерийском полку «Форт Гарри Хорс» 24 августа 1914 года ещё будучи медвежонком (её купил у канадского охотника-траппера за двадцать долларов 27-летний полковой ветеринар лейтенант Гарри Колборн, заботившийся о ней и в дальнейшем). Уже в октябре того же года медвежонок был привезён вместе с войсками в Британию, а так как полк должен был быть в ходе Первой мировой войны переправлен во Францию, то в декабре было принято решение оставить зверя до конца войны в Лондонском зоопарке. Медведица полюбилась лондонцам, и военные не стали возражать против того, чтобы не забирать её из зоопарка и после войны[1]. До конца дней (она умерла 12 мая 1934 года) медведица находилась на довольствии ветеринарного корпуса, о чём в 1919 году на её клетке сделали соответствующую надпись.

    + +

    «Винни-Пух» представляет собой дилогию, но каждая из двух книг Милна распадается на 10 рассказов (stories) с собственным сюжетом, которые могут читаться, экранизироваться и т. д. независимо друг от друга. В некоторых переводах деление на две части не сохранено, в других не переведена вторая («Дом на Пуховой опушке»). Иногда первая и вторая книги выполнены разными переводчиками. Такова необычная судьба немецкого Винни-Пуха: первая книга вышла в немецком переводе в 1928 году, а вторая лишь в 1954; между этими датами — ряд трагических событий германской истории.

    + + +

    Действие книг о Пухе происходит в 500-акровом лесу Эшдаун близ купленной Милнами в 1925 году фермы Кочфорд в графстве Восточный Сассекс, Англия, представленном в книге как Стоакровый лес (англ. The Hundred Acre Wood, в пересказе Заходера — Чудесный лес). Реальными являются также Шесть сосен и ручеёк, у которого был найден Северный Полюс, а также упоминаемая в тексте растительность, в том числе колючий утёсник (gorse-bush, чертополох у Заходера), в который падает Пух[2]. Маленький Кристофер Робин любил забираться в дупла деревьев и играть там с Пухом, поэтому многие персонажи книг живут в дуплах, и значительная часть действия происходит в таких жилищах или на ветвях деревьев[2]. +Алан Милн, Кристофер Робин и Винни-Пух. Фотография из Британской национальной портретной галереи

    + +

    Действие «Винни-Пуха» разворачивается одновременно в трёх планах — это мир игрушек в детской, мир зверей «на своей территории» в Стоакровом лесу и мир персонажей в рассказах отца сыну (это наиболее чётко показано в самом начале)[4]. В дальнейшем рассказчик исчезает из повествования, и сказочный мир начинает собственное существование, разрастаясь от главы к главе[6]. Отмечалось сходство пространства и мира персонажей «Винни-Пуха» с классическим античным и средневековым эпосом[6]. Многообещающие эпические начинания персонажей (путешествия, подвиги, охоты, игры) оказываются комически малозначительными, в то время как настоящие события происходят во внутреннем мире героев (помощь в беде, гостеприимство, дружба)[6].

    + +

    Книги Милна выросли из устных рассказов и игр с Кристофером Робином; устное происхождение характерно и для многих других знаменитых литературных сказок[6]. «Я, собственно, ничего не придумывал, мне оставалось только описывать», как говорил впоследствии Милн[5]. Реальными игрушками Кристофера Робина были также Пятачок (подарок соседей), Иа-Иа без хвоста (ранний подарок родителей), Кенга с Крошкой Ру в сумке и Тигра (куплены родителями впоследствии специально для развития сюжета вечерних рассказов сыну). В рассказах они появляются именно в таком порядке[2]. Сову и Кролика Милн придумал сам; на иллюстрациях Шепарда они выглядят не как игрушки, а как настоящие животные, Кролик говорит Сове: «Только у меня и тебя есть мозги. У остальных — опилки». В процессе игры все эти персонажи получили индивидуальные повадки, привычки и манеру разговора[6]. На созданный Милном мир животных повлияла повесть Кеннета Грэма «Ветер в ивах», которой он восхищался и которую ранее иллюстрировал Шепард[5], возможна также скрытая полемика с «Книгой джунглей» Киплинга[5]. Текст взят из Википедии.

    +
    +
    + + + + diff --git a/tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/question.md b/tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/question.md new file mode 100644 index 000000000..5f2e869cc --- /dev/null +++ b/tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/question.md @@ -0,0 +1,12 @@ +# Аватар наверху при прокрутке + +[importance 5] + +Сделайте так, чтобы при прокрутке ниже элемента `#avatar` (картинка с Винни-Пухом) -- он продолжал показываться в левом-верхнем углу. + +При прокрутке вверх -- должен возвращаться на обычное место. + +Прокрутите вниз, чтобы увидеть: +[iframe src="solution" height=300 link border="1"] + +[edit task src="question"/] diff --git a/tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/solution.code/index.html b/tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/solution.code/index.html new file mode 100755 index 000000000..21216dc7b --- /dev/null +++ b/tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/solution.code/index.html @@ -0,0 +1,109 @@ + + + + + + + + +
    Шапка
    + +
    +
    +

    Персонажи:

    +
      +
    • Винни-Пух
    • +
    • Ослик Иа
    • +
    • Сова
    • +
    • Кролик
    • +
    +
    +
    +
    +
    + +

    Винни-Пух

    + +
    + +
    Кадр из мультфильма
    +
    + +

    Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге). Один из самых известных героев детской литературы XX века.

    + +

    В 1960-е—1970-е годы, благодаря пересказу Бориса Заходера «Винни-Пух и все-все-все», а затем и фильмам студии «Союзмультфильм», где мишку озвучивал Евгений Леонов, Винни-Пух стал очень популярен и в Советском Союзе.

    + +

    Как и многие другие персонажи книги Милна, медвежонок Винни получил имя от одной из реальных игрушек Кристофера Робина (1920—1996), сына писателя. В свою очередь, плюшевый мишка Винни-Пух был назван по имени медведицы по кличке Виннипег (Винни), содержавшейся в 1920-х в Лондонском зоопарке.

    + +

    Медведица Виннипег (американский чёрный медведь) попала в Великобританию как живой талисман (маскот) Канадского армейского ветеринарного корпуса из Канады, а именно из окрестностей города Виннипега. Она оказалась в кавалерийском полку «Форт Гарри Хорс» 24 августа 1914 года ещё будучи медвежонком (её купил у канадского охотника-траппера за двадцать долларов 27-летний полковой ветеринар лейтенант Гарри Колборн, заботившийся о ней и в дальнейшем). Уже в октябре того же года медвежонок был привезён вместе с войсками в Британию, а так как полк должен был быть в ходе Первой мировой войны переправлен во Францию, то в декабре было принято решение оставить зверя до конца войны в Лондонском зоопарке. Медведица полюбилась лондонцам, и военные не стали возражать против того, чтобы не забирать её из зоопарка и после войны[1]. До конца дней (она умерла 12 мая 1934 года) медведица находилась на довольствии ветеринарного корпуса, о чём в 1919 году на её клетке сделали соответствующую надпись.

    + +

    «Винни-Пух» представляет собой дилогию, но каждая из двух книг Милна распадается на 10 рассказов (stories) с собственным сюжетом, которые могут читаться, экранизироваться и т. д. независимо друг от друга. В некоторых переводах деление на две части не сохранено, в других не переведена вторая («Дом на Пуховой опушке»). Иногда первая и вторая книги выполнены разными переводчиками. Такова необычная судьба немецкого Винни-Пуха: первая книга вышла в немецком переводе в 1928 году, а вторая лишь в 1954; между этими датами — ряд трагических событий германской истории.

    + + +

    Действие книг о Пухе происходит в 500-акровом лесу Эшдаун близ купленной Милнами в 1925 году фермы Кочфорд в графстве Восточный Сассекс, Англия, представленном в книге как Стоакровый лес (англ. The Hundred Acre Wood, в пересказе Заходера — Чудесный лес). Реальными являются также Шесть сосен и ручеёк, у которого был найден Северный Полюс, а также упоминаемая в тексте растительность, в том числе колючий утёсник (gorse-bush, чертополох у Заходера), в который падает Пух[2]. Маленький Кристофер Робин любил забираться в дупла деревьев и играть там с Пухом, поэтому многие персонажи книг живут в дуплах, и значительная часть действия происходит в таких жилищах или на ветвях деревьев[2]. +Алан Милн, Кристофер Робин и Винни-Пух. Фотография из Британской национальной портретной галереи

    + +

    Действие «Винни-Пуха» разворачивается одновременно в трёх планах — это мир игрушек в детской, мир зверей «на своей территории» в Стоакровом лесу и мир персонажей в рассказах отца сыну (это наиболее чётко показано в самом начале)[4]. В дальнейшем рассказчик исчезает из повествования, и сказочный мир начинает собственное существование, разрастаясь от главы к главе[6]. Отмечалось сходство пространства и мира персонажей «Винни-Пуха» с классическим античным и средневековым эпосом[6]. Многообещающие эпические начинания персонажей (путешествия, подвиги, охоты, игры) оказываются комически малозначительными, в то время как настоящие события происходят во внутреннем мире героев (помощь в беде, гостеприимство, дружба)[6].

    + +

    Книги Милна выросли из устных рассказов и игр с Кристофером Робином; устное происхождение характерно и для многих других знаменитых литературных сказок[6]. «Я, собственно, ничего не придумывал, мне оставалось только описывать», как говорил впоследствии Милн[5]. Реальными игрушками Кристофера Робина были также Пятачок (подарок соседей), Иа-Иа без хвоста (ранний подарок родителей), Кенга с Крошкой Ру в сумке и Тигра (куплены родителями впоследствии специально для развития сюжета вечерних рассказов сыну). В рассказах они появляются именно в таком порядке[2]. Сову и Кролика Милн придумал сам; на иллюстрациях Шепарда они выглядят не как игрушки, а как настоящие животные, Кролик говорит Сове: «Только у меня и тебя есть мозги. У остальных — опилки». В процессе игры все эти персонажи получили индивидуальные повадки, привычки и манеру разговора[6]. На созданный Милном мир животных повлияла повесть Кеннета Грэма «Ветер в ивах», которой он восхищался и которую ранее иллюстрировал Шепард[5], возможна также скрытая полемика с «Книгой джунглей» Киплинга[5]. Текст взят из Википедии.

    +
    +
    + + + + + diff --git a/tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/solution.code/scroll-position/index.html b/tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/solution.code/scroll-position/index.html new file mode 100755 index 000000000..21216dc7b --- /dev/null +++ b/tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/solution.code/scroll-position/index.html @@ -0,0 +1,109 @@ + + + + + + + + +
    Шапка
    + +
    +
    +

    Персонажи:

    +
      +
    • Винни-Пух
    • +
    • Ослик Иа
    • +
    • Сова
    • +
    • Кролик
    • +
    +
    +
    +
    +
    + +

    Винни-Пух

    + +
    + +
    Кадр из мультфильма
    +
    + +

    Ви́нни-Пу́х (англ. Winnie-the-Pooh) — плюшевый мишка, персонаж повестей и стихов Алана Александра Милна (цикл не имеет общего названия и обычно тоже называется «Винни-Пух», по первой книге). Один из самых известных героев детской литературы XX века.

    + +

    В 1960-е—1970-е годы, благодаря пересказу Бориса Заходера «Винни-Пух и все-все-все», а затем и фильмам студии «Союзмультфильм», где мишку озвучивал Евгений Леонов, Винни-Пух стал очень популярен и в Советском Союзе.

    + +

    Как и многие другие персонажи книги Милна, медвежонок Винни получил имя от одной из реальных игрушек Кристофера Робина (1920—1996), сына писателя. В свою очередь, плюшевый мишка Винни-Пух был назван по имени медведицы по кличке Виннипег (Винни), содержавшейся в 1920-х в Лондонском зоопарке.

    + +

    Медведица Виннипег (американский чёрный медведь) попала в Великобританию как живой талисман (маскот) Канадского армейского ветеринарного корпуса из Канады, а именно из окрестностей города Виннипега. Она оказалась в кавалерийском полку «Форт Гарри Хорс» 24 августа 1914 года ещё будучи медвежонком (её купил у канадского охотника-траппера за двадцать долларов 27-летний полковой ветеринар лейтенант Гарри Колборн, заботившийся о ней и в дальнейшем). Уже в октябре того же года медвежонок был привезён вместе с войсками в Британию, а так как полк должен был быть в ходе Первой мировой войны переправлен во Францию, то в декабре было принято решение оставить зверя до конца войны в Лондонском зоопарке. Медведица полюбилась лондонцам, и военные не стали возражать против того, чтобы не забирать её из зоопарка и после войны[1]. До конца дней (она умерла 12 мая 1934 года) медведица находилась на довольствии ветеринарного корпуса, о чём в 1919 году на её клетке сделали соответствующую надпись.

    + +

    «Винни-Пух» представляет собой дилогию, но каждая из двух книг Милна распадается на 10 рассказов (stories) с собственным сюжетом, которые могут читаться, экранизироваться и т. д. независимо друг от друга. В некоторых переводах деление на две части не сохранено, в других не переведена вторая («Дом на Пуховой опушке»). Иногда первая и вторая книги выполнены разными переводчиками. Такова необычная судьба немецкого Винни-Пуха: первая книга вышла в немецком переводе в 1928 году, а вторая лишь в 1954; между этими датами — ряд трагических событий германской истории.

    + + +

    Действие книг о Пухе происходит в 500-акровом лесу Эшдаун близ купленной Милнами в 1925 году фермы Кочфорд в графстве Восточный Сассекс, Англия, представленном в книге как Стоакровый лес (англ. The Hundred Acre Wood, в пересказе Заходера — Чудесный лес). Реальными являются также Шесть сосен и ручеёк, у которого был найден Северный Полюс, а также упоминаемая в тексте растительность, в том числе колючий утёсник (gorse-bush, чертополох у Заходера), в который падает Пух[2]. Маленький Кристофер Робин любил забираться в дупла деревьев и играть там с Пухом, поэтому многие персонажи книг живут в дуплах, и значительная часть действия происходит в таких жилищах или на ветвях деревьев[2]. +Алан Милн, Кристофер Робин и Винни-Пух. Фотография из Британской национальной портретной галереи

    + +

    Действие «Винни-Пуха» разворачивается одновременно в трёх планах — это мир игрушек в детской, мир зверей «на своей территории» в Стоакровом лесу и мир персонажей в рассказах отца сыну (это наиболее чётко показано в самом начале)[4]. В дальнейшем рассказчик исчезает из повествования, и сказочный мир начинает собственное существование, разрастаясь от главы к главе[6]. Отмечалось сходство пространства и мира персонажей «Винни-Пуха» с классическим античным и средневековым эпосом[6]. Многообещающие эпические начинания персонажей (путешествия, подвиги, охоты, игры) оказываются комически малозначительными, в то время как настоящие события происходят во внутреннем мире героев (помощь в беде, гостеприимство, дружба)[6].

    + +

    Книги Милна выросли из устных рассказов и игр с Кристофером Робином; устное происхождение характерно и для многих других знаменитых литературных сказок[6]. «Я, собственно, ничего не придумывал, мне оставалось только описывать», как говорил впоследствии Милн[5]. Реальными игрушками Кристофера Робина были также Пятачок (подарок соседей), Иа-Иа без хвоста (ранний подарок родителей), Кенга с Крошкой Ру в сумке и Тигра (куплены родителями впоследствии специально для развития сюжета вечерних рассказов сыну). В рассказах они появляются именно в таком порядке[2]. Сову и Кролика Милн придумал сам; на иллюстрациях Шепарда они выглядят не как игрушки, а как настоящие животные, Кролик говорит Сове: «Только у меня и тебя есть мозги. У остальных — опилки». В процессе игры все эти персонажи получили индивидуальные повадки, привычки и манеру разговора[6]. На созданный Милном мир животных повлияла повесть Кеннета Грэма «Ветер в ивах», которой он восхищался и которую ранее иллюстрировал Шепард[5], возможна также скрытая полемика с «Книгой джунглей» Киплинга[5]. Текст взят из Википедии.

    +
    +
    + + + + + diff --git a/tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/solution.md b/tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/solution.md new file mode 100644 index 000000000..330005220 --- /dev/null +++ b/tutorial/02-ui/03-event-details/09-event-onscroll/01-avatar-above-scroll.task/solution.md @@ -0,0 +1 @@ +[edit src="solution"/] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/question.code/index.html b/tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/question.code/index.html new file mode 100755 index 000000000..814e090c6 --- /dev/null +++ b/tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/question.code/index.html @@ -0,0 +1,35 @@ + + + + + + + + + + + +
    + +
    + + + + diff --git a/tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/question.md b/tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/question.md new file mode 100644 index 000000000..4cfb9d4e7 --- /dev/null +++ b/tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/question.md @@ -0,0 +1,21 @@ +# Кнопка вверх-вниз + +[importance 5] + +Создайте кнопку навигации, которая помогает при прокрутке страницы. + +Работать должна следующим образом: +
      +
    • Пока страница промотана меньше чем на высоту экрана вниз -- кнопка не видна.
    • +
    • При промотке страницы вниз больше, чем на высоту экрана, появляется кнопка "стрелка вверх". При нажатии на нее страница запоминает текущую прокрутку и прыгает вверх, а кнопка меняется на "стрелка вниз" и остается такой, пока посетитель не прокрутит вниз больше, чем один экран, после чего вновь изменится на "стрелка вверх".
    • +
    • Нажатие на "стрелка вниз" отправляет обратно на запомненную позицию.
    • +
    +Должен получиться удобный навигационный помощник. + +Посмотрите, как оно должно работать, в ифрейме ниже. Прокрутите ифрейм, навигационная стрелка появится слева. + +[iframe border="1" height="200" link src="solution"] + +В исходный документ включена кнопка. + +[edit src="question" task/] diff --git a/tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/solution.code/index.html b/tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/solution.code/index.html new file mode 100755 index 000000000..d94be96f0 --- /dev/null +++ b/tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/solution.code/index.html @@ -0,0 +1,97 @@ + + + + + + + + +
    + +
    + +
    + + + + diff --git a/tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/solution.code/updown/index.html b/tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/solution.code/updown/index.html new file mode 100755 index 000000000..d94be96f0 --- /dev/null +++ b/tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/solution.code/updown/index.html @@ -0,0 +1,97 @@ + + + + + + + + +
    + +
    + +
    + + + + diff --git a/tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/solution.md b/tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/solution.md new file mode 100644 index 000000000..4706e9093 --- /dev/null +++ b/tutorial/02-ui/03-event-details/09-event-onscroll/02-updown-button.task/solution.md @@ -0,0 +1,92 @@ +Добавим в документ `DIV` с кнопкой: +[html] +
    +[/html] + +Сама кнопка должна иметь `position:fixed`. + +[css] +#updown { + position: fixed; + top: 30px; + left: 10px; + cursor: pointer; +} +[/css] + +Кнопка является CSS-спрайтом, поэтому мы дополнительно добавляем ей размер и два состояния: + +[css] +#updown { + height: 9px; + width: 14px; + position: fixed; + top: 30px; + left: 10px; + cursor: pointer; +} + +#updown.up { + background: url(...updown.gif) left top; +} + +#updown.down { + background: url(...updown.gif) left -9px; +} +[/css] + +Для решения необходимо аккуратно разобрать все возможные состояния кнопки и указать, что делать при каждом. + +Состояние -- это просто класс элемента: `up/down` или пустая строка, если кнопка не видна. + +При прокрутке состояния меняются следующим образом: +[js] +window.onscroll = function() { + var pageY = window.pageYOffset || document.documentElement.scrollTop; + var innerHeight = document.documentElement.clientHeight; + + switch(updownElem.className) { + case '': + if (pageY > innerHeight) { + updownElem.className = 'up'; + } + break; + + case 'up': + if (pageY < innerHeight) { + updownElem.className = ''; + } + break; + + case 'down': + if (pageY > innerHeight) { + updownElem.className = 'up'; + } + break; + } +} +[/js] + +При клике: +[js] +var pageYLabel = 0; + +updownElem.onclick = function() { + var pageY = window.pageYOffset || document.documentElement.scrollTop; + + switch(this.className) { + case 'up': + pageYLabel = pageY; + window.scrollTo(0, 0); + this.className = 'down'; + break; + + case 'down': + window.scrollTo(0, pageYLabel); + this.className = 'up'; + } + +} +[/js] + +[edit src="solution"/] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/09-event-onscroll/03-load-visible-img.task/question.code/index.html b/tutorial/02-ui/03-event-details/09-event-onscroll/03-load-visible-img.task/question.code/index.html new file mode 100755 index 000000000..37843ce16 --- /dev/null +++ b/tutorial/02-ui/03-event-details/09-event-onscroll/03-load-visible-img.task/question.code/index.html @@ -0,0 +1,101 @@ + + + + + + + + +

    Тексты и картинки взяты с сайта http://etoday.ru.

    + +

    Все изображения с realsrc загружаются, когда становятся видимыми.

    + + +
    +
    Космопорт Америка \ Architecture
    + +Будущее уже сейчас! Скоро фраза из фантастического фильма "флипнуть до космопорта" станет реальностью. По крайней мере вторую ее часть человечество обеспечило. В октябре состоялась официальная церемония открытия космопорта «Америка», первой в мире коммерческой площадки для частных космических полетов. Космопорт открылся в пустыне штата Нью-Мексико. Проект был реализован английским бюро Foster and Partners. Космопорт включает в себя зал ожидания и подготовки к полетам, диспетчерский пункт и ангар. Также обеспечена взлетно-посадочная полоса длиной в три километра. + +
    + +
    +
    + +
    +
    Рокер и супермодель в Vogue Russia \ Celebrities
    + +Супермодель Анна Вялицына (Anne Vyalitsyna) и музыкант Адам Ливайн (Adam Levine) снялись в ноябрьском номере Vogue Russia. Снимал их Аликс Малка (Alix Malka). Анна и Адам примерили на себя рок-н-ролльные наряды от Alexander Wang, Louis Vuitton, Alexander McQueen, Balmain, Yves Saint Laurent, подобранные для них Катериной Мухиной. +
    + + +
    +
    + +
    +
    Старость - не радость в Vogue Italia \ Fashion Photo
    + +Стивен Мейзел (Steven Meisel) снял фотосессию для октябрьского Vogue Italia. В съемках приняли участие: Карен Элсон (Karen Elson), Джиневер ван Синус (Guinevere van Seenus), Эмма Балфур (Emma Balfour), Эн Уст (An Oost), Коринна Ингенлеф (Corinna Ingenleuf), Танга Моро (Tanga Moreau), Кордула Рейер (Cordula Reyer), Гейл о`Нил (Gail O'Neil), Эвелин Кун (Evelyn Kuhn), Каролин де Мэгрэ (Caroline de Maigret), Дэльфин Бафор (Delfine Bafort), Кирстен Оуэн (Kirsten Owen), Гунилла Линдблад (Gunilla Lindblad). +
    + + +
    +
    + +
    +
    "Вышитый рентген" Matthew Cox \ Art
    + +Художник из Филадельфии Мэтью Кокс (Matthew Cox) создал серию работ, в которых объединены медицинский рентген и вышивка. Художник взял рентгенограммы и вышил их предполагаемое содержание частично со скелетными элементами. Получилось зловеще и интригующе. Выставка "Вышитый рентген" будет демонстрироваться в галерее Packer/Schopf в Майами, в рамках Базельской Художественной Недели. + +Эта серия - только треть творческой продукции Кокса. Он также создает традиционные картины и иллюстрации. +
    +
    + +
    +
    Подарочный каталог Apple 1983 \ Creative
    + +Etoday предлагает полистать страницы подарочного каталога продукции Apple образца 1983 года. Кажется, это было так давно! +Эта серия - только треть творческой продукции Кокса. Он также создает традиционные картины и иллюстрации. +
    +
    + +
    +
    Винтажные открытки к празднику Halloween \ Illustrations
    + +Занимательная коллекция старых почтовых открыток праздника Halloween. Открытки взяты из ньюйоркской публичной библиотеки и датируются примерно 1910 г. + +
    +
    + +
    +
    Фотограф Emily Lee \ Photography
    + +Молодой фотограф Эмили Ли (Emily Lee) использует фотографию, чтобы выразить свои чувства. "Когда я смотрю на жизнь через камеру, вижу все более ясно, - пишет она на своем профиле Flickr. - Фотосъемка - это искусство наблюдения." Эмили Ли - обладательница большого таланта и умения глубоко понимать искусство, хотя учится еще только в средней школе. + +
    +
    + +
    +
    Иконы моды в Fashimals \ Creative
    + +Fashimals - tumblr-блог, посвященный иконам моды, превращенным в животных. Здесь есть Анна Винтур, Карл Лагерфельд, Терри Ричардсон, а также много других их коллег. + +
    +
    + + + + + + diff --git a/tutorial/02-ui/03-event-details/09-event-onscroll/03-load-visible-img.task/question.md b/tutorial/02-ui/03-event-details/09-event-onscroll/03-load-visible-img.task/question.md new file mode 100644 index 000000000..5d96aa711 --- /dev/null +++ b/tutorial/02-ui/03-event-details/09-event-onscroll/03-load-visible-img.task/question.md @@ -0,0 +1,45 @@ +# Загрузка видимых изображений + +[importance 5] + +Задача, которая описана ниже, демонстрирует результативный метод оптимизации страницы. + +С целью экономии трафика и более быстрой загрузки страницы изображения на ней заменяются на "макеты". + +Вместо такого изображения: +[html] + +[/html] + + + +Стоит вот такое: +[html] + +[/html] + + +То есть настоящий URL находится в атрибуте `realsrc` (название атрибута можно выбрать любое). А в `src` поставлен серый GIF размера 1x1, и так как `width/height` правильные, то он растягивается, так что вместо изображения виден серый прямоугольник. + +При этом, чтобы браузер загрузил изображение, нужно заменить значение `src` на то, которое находится в `realsrc`. + +**Если страница большая, то замена больших изображений на такие макеты существенно убыстряет полную загрузку страницы.** Это особенно заметно в случае, когда на странице много анонсов новостей с картинками или изображений товаров, из которых многие находятся за пределами прокрутки. + + Кроме того, для мобильных устройств JavaScript может подставлять URL уменьшенного варианта картинки. + +**Напишите код, который при прокрутке окна загружает ставшие видимыми изображения.** То есть, как только изображение попало в видимую часть документа -- в `src` нужно прописать правильный URL из `realsrc`. + +Работать должно так: открыть работающую страницу в новом окне. + +
      +
    • При начальной загрузке некоторые изображения должны быть видны сразу, до прокрутки. Код должен это учитывать.
    • +
    • Некоторые изображения могут быть обычными, без `realsrc`. Их код не должен трогать вообще.
    • +
    • Также код не должен перегружать уже показанное изображение.
    • +
    + +**Дополнительно: расширьте код, чтобы загружались изображения не только видимые сейчас, но и на страницу вперед и назад от текущего места.** + +[edit src="question" task/] + + +P.S. Страница прокручивается только вверх или вниз, горизонтальной прокрутки нет. \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/09-event-onscroll/03-load-visible-img.task/solution.code/index.html b/tutorial/02-ui/03-event-details/09-event-onscroll/03-load-visible-img.task/solution.code/index.html new file mode 100755 index 000000000..7b5514f9a --- /dev/null +++ b/tutorial/02-ui/03-event-details/09-event-onscroll/03-load-visible-img.task/solution.code/index.html @@ -0,0 +1,159 @@ + + + + + + + + +

    Тексты и картинки взяты с сайта http://etoday.ru.

    + +

    Все изображения с realsrc загружаются, когда становятся видимыми.

    + + +
    +
    Космопорт Америка \ Architecture
    + +Будущее уже сейчас! Скоро фраза из фантастического фильма "флипнуть до космопорта" станет реальностью. По крайней мере вторую ее часть человечество обеспечило. В октябре состоялась официальная церемония открытия космопорта «Америка», первой в мире коммерческой площадки для частных космических полетов. Космопорт открылся в пустыне штата Нью-Мексико. Проект был реализован английским бюро Foster and Partners. Космопорт включает в себя зал ожидания и подготовки к полетам, диспетчерский пункт и ангар. Также обеспечена взлетно-посадочная полоса длиной в три километра. + +
    + +
    +
    + +
    +
    Рокер и супермодель в Vogue Russia \ Celebrities
    + +Супермодель Анна Вялицына (Anne Vyalitsyna) и музыкант Адам Ливайн (Adam Levine) снялись в ноябрьском номере Vogue Russia. Снимал их Аликс Малка (Alix Malka). Анна и Адам примерили на себя рок-н-ролльные наряды от Alexander Wang, Louis Vuitton, Alexander McQueen, Balmain, Yves Saint Laurent, подобранные для них Катериной Мухиной. +
    + + +
    +
    + +
    +
    Старость - не радость в Vogue Italia \ Fashion Photo
    + +Стивен Мейзел (Steven Meisel) снял фотосессию для октябрьского Vogue Italia. В съемках приняли участие: Карен Элсон (Karen Elson), Джиневер ван Синус (Guinevere van Seenus), Эмма Балфур (Emma Balfour), Эн Уст (An Oost), Коринна Ингенлеф (Corinna Ingenleuf), Танга Моро (Tanga Moreau), Кордула Рейер (Cordula Reyer), Гейл о`Нил (Gail O'Neil), Эвелин Кун (Evelyn Kuhn), Каролин де Мэгрэ (Caroline de Maigret), Дэльфин Бафор (Delfine Bafort), Кирстен Оуэн (Kirsten Owen), Гунилла Линдблад (Gunilla Lindblad). +
    + + +
    +
    + +
    +
    "Вышитый рентген" Matthew Cox \ Art
    + +Художник из Филадельфии Мэтью Кокс (Matthew Cox) создал серию работ, в которых объединены медицинский рентген и вышивка. Художник взял рентгенограммы и вышил их предполагаемое содержание частично со скелетными элементами. Получилось зловеще и интригующе. Выставка "Вышитый рентген" будет демонстрироваться в галерее Packer/Schopf в Майами, в рамках Базельской Художественной Недели. + +Эта серия - только треть творческой продукции Кокса. Он также создает традиционные картины и иллюстрации. +
    +
    + +
    +
    Подарочный каталог Apple 1983 \ Creative
    + +Etoday предлагает полистать страницы подарочного каталога продукции Apple образца 1983 года. Кажется, это было так давно! +Эта серия - только треть творческой продукции Кокса. Он также создает традиционные картины и иллюстрации. +
    +
    + +
    +
    Винтажные открытки к празднику Halloween \ Illustrations
    + +Занимательная коллекция старых почтовых открыток праздника Halloween. Открытки взяты из ньюйоркской публичной библиотеки и датируются примерно 1910 г. + +
    +
    + +
    +
    Фотограф Emily Lee \ Photography
    + +Молодой фотограф Эмили Ли (Emily Lee) использует фотографию, чтобы выразить свои чувства. "Когда я смотрю на жизнь через камеру, вижу все более ясно, - пишет она на своем профиле Flickr. - Фотосъемка - это искусство наблюдения." Эмили Ли - обладательница большого таланта и умения глубоко понимать искусство, хотя учится еще только в средней школе. + +
    +
    + +
    +
    Иконы моды в Fashimals \ Creative
    + +Fashimals - tumblr-блог, посвященный иконам моды, превращенным в животных. Здесь есть Анна Винтур, Карл Лагерфельд, Терри Ричардсон, а также много других их коллег. + +
    +
    + + + + + + + diff --git a/tutorial/02-ui/03-event-details/09-event-onscroll/03-load-visible-img.task/solution.md b/tutorial/02-ui/03-event-details/09-event-onscroll/03-load-visible-img.task/solution.md new file mode 100644 index 000000000..152129860 --- /dev/null +++ b/tutorial/02-ui/03-event-details/09-event-onscroll/03-load-visible-img.task/solution.md @@ -0,0 +1,36 @@ +Функция должна по текущей прокрутке определять, какие изображения видимы, и загружать их. + +Она должна срабатывать не только при прокрутке, но и при загрузке. Вполне достаточно для этого -- указать ее вызов в скрипте под страницей, вот так: + +[js] +... страница ... + +function isVisible(elem) { + + var coords = elem.getBoundingClientRect(); + + var windowHeight = document.documentElement.clientHeight; + + // верхняя граница elem в пределах видимости ИЛИ нижняя граница видима + var topVisible = coords.top > 0 && coords.top < windowHeight; + var bottomVisible = coords.bottom < windowHeight && coords.bottom > 0; + + return topVisible || bottomVisible; +} + + +*!* +showVisible(); +window.onscroll = showVisible; +*/!* +[/js] + +При запуске функция ищет все видимые картинки с `realsrc` и перемещает значение `realsrc` в `src`. Обратите внимание, т.к. атрибут `realsrc` нестандартный, то для доступа к нему мы используем `get/setAttribute`. А `src` -- стандартный, поэтому можно обратиться по DOM-свойству. + +**Функция проверки видимости `isVisible(elem)` получает координаты текущей видимой области и сравнивает их с элементом.** + +Для видимости достаточно, чтобы координаты верхней(или нижней) границы элемента находились между границами видимой области. + +В решении также указан вариант с `isVisible`, который расширяет область видимости на +-1 страницу (высота страницы -- `document.documentElement.clientHeight`). + +[edit src="solution"]Открыть полное решение в песочнице[/edit] \ No newline at end of file diff --git a/tutorial/02-ui/03-event-details/10-onload-ondomcontentloaded.md b/tutorial/02-ui/03-event-details/10-onload-ondomcontentloaded.md new file mode 100644 index 000000000..f290018fc --- /dev/null +++ b/tutorial/02-ui/03-event-details/10-onload-ondomcontentloaded.md @@ -0,0 +1,208 @@ +# Загрузка документа: DOMContentLoaded, load, beforeunload, unload + +Процесс загрузки HTML-документа, условно, состоит из трёх стадий: +
      +
    • `DOMContentLoaded` -- браузер полностью загрузил HTML, и построил DOM-дерево.
    • +
    • `load` -- браузер загрузил все ресурсы.
    • +
    • `beforeunload/unload` -- уход со страницы.
    • +
    + +Все эти стадии очень важны. На каждую можно повесить обработчик, чтобы совершить полезные действия: +
      +
    • `DOMContentLoaded` -- означает, что все DOM-элементы разметки уже созданы, можно их искать, вешать обработчики, создавать интерфейс, но при этом, возможно, ещё не догрузились какие-то картинки или стили.
    • +
    • `load` -- страница и все ресурсы загружены, используется редко, обычно нет нужды ждать этого момента.
    • +
    • `beforeunload/unload` -- можно проверить, сохранил ли посетитель изменения, уточнить, действительно ли он хочет покинуть страницу.
    • +
    + +Далее мы рассмотрим важные детали этих событий. + +[cut] + +## `DOMContentLoaded` +Событие `DOMContentLoaded` поддерживается во всех браузерах, кроме IE8-. Про поддержку аналогичного функционала в старых IE мы поговорим в конце главы. + +Обработчик на него вешается только через `addEventListener`: +[js] +document.addEventListener( "DOMContentLoaded", ready, false ); +[/js] + +Пример: + +[html run height=150] + + + +[/html] + +В примере выше размеры обработчик `DOMContentLoaded` сработает сразу после загрузки документа, не дожидаясь получения картинки. + +Поэтому на момент вывода `alert` и сама картинка будет невидна и её размеры -- неизвестны (кроме случая, когда картинка взята из кеша браузера). + +В своей сути, событие `onDOMContentLoaded` -- простое, как пробка. Полностью создано DOM-дерево -- и вот событие. Но с ним связан ряд существенных тонкостей. + +### `DOMContentLoaded` и скрипты + +Если в документе есть теги ` +[/html] + +Такое поведение прописано в стандарте. Его причина -- скрипт может захотеть получить информацию со страницы, зависящую от стилей, например, ширину элемента, и поэтому обязан дождаться загрузки `style.css`. + +**Побочный эффект -- так как событие `DOMContentLoaded` будет ждать выполнения скрипта, то оно подождёт и загрузки стилей, которые идут перед ` + + +[/html] + +## `window.onunload` + +Когда человек уходит со страницы или закрывает окно, срабатывает `window.unload`. В нём можно сделать что-то, не требующее ожидания, например, закрыть вспомогательные popup-окна, но отменить сам переход нельзя. + +Это позволяет другое событие -- `window.onbeforeunload`, которое поэтому используется гораздо чаще. + +[ref id="window.onbeforeunload"] +## `window.onbeforeunload` + +**Если посетитель инициировал переход на другую страницу или нажал "закрыть окно", то обработчик `onbeforeunload` может приостановить процесс и спросить подтверждение.** + +Для этого ему нужно вернуть строку, которую браузеры покажут посетителю, спрашивая -- нужно ли переходить. + +Например: + +[js] +window.onbeforeunload = function() { + return "Данные не сохранены. Точно перейти?"; +}; +[/js] + +[warn header="Firefox игнорирует текст, он показывает своё сообщение"] +Firefox игнорирует текст, а всегда показывает своё сообщение. + +Это сделано в целях безопасности. +[/warn] + +Кликните на кнопку в `IFRAME'е` ниже, чтобы поставить обработчик, а затем по ссылке, чтобы увидеть его в действии: + +[iframe src="window-onbeforeunload" border="1" height="80" link] + + +## Эмуляция `DOMContentLoaded` для IE8- + +Прежде чем что-то эмулировать, заметим, что альтернативой событию `onDOMContentLoaded` является вызов функции `init` из скрипта в самом конце `BODY`, когда основная часть DOM уже готова: +[html] + + ... + + +[/html] + +Причина, по которой обычно предпочитают именно событие -- одна: удобство. Вешается обработчик и не надо ничего писать в конец `BODY`. + +### Мини-скрипт `documentReady` +Если вы всё же хотите использовать `onDOMContentLoaded` кросс-браузерно, то нужно либо подключить какой-нибудь фреймворк -- почти все предоставляют такой функционал, либо использовать функцию из мини-библиотеки [jquery.documentReady.js](https://github.com/addyosmani/jquery.parts/blob/master/jquery.documentReady.js). + +Несмотря на то, что в названии содержится слово "jquery", эта библиотечка не требует [jQuery](http://jquery.com). Наоборот, она представляет собой единственную функцию с названием `$`, вызов которой `$(callback)` добавляет обработчик `callback` на `DOMContentLoaded` (можно вызывать много раз), либо, если документ уже загружен -- выполняет его тут же. + +Пример использования: +[html run] + + + + + +
    Текст страницы
    +[/html] + +Здесь `alert` сработает до загрузки картинки, но после создания DOM, в частности, после появления текста. И так будет для всех браузеров, включая даже очень старые IE. + +[smart header="Как именно эмулируется `DOMContentLoaded`?"] +Технически, эмуляция `DOMContentLoaded` для старых IE осуществляется очень забавно. + +Основной приём -- это попытка прокрутить документ вызовом: +[js] +document.documentElement.doScroll("left"); +[/js] +Метод `doScroll` работает только в IE и "методом тыка" было обнаружено, что он бросает исключение, если DOM не полностью создан. + +Поэтому библиотека пытается вызвать прокрутку, если не получается -- через `setTimeout(.., 1)` пытается прокрутить его ещё раз, и так до тех пор, пока действие не перестанет вызывать ошибку. На этом этапе документ считается загрузившимся. + +Внутри фреймов и в очень старых браузерах такой подход может ошибаться, поэтому дополнительно ставится резервный обработчик на `onload`, чтобы уж точно сработал. +[/smart] + + +## Итого + +
      +
    • Самое востребованное событие из описанных -- без сомнения, `DOMContentLoaded`. Многие страницы сделаны так, что инициализуют интерфейсы именно по этому событию. + +Это удобно, ведь можно в `` написать скрипт, который будет запущен в момент, когда все DOM-элементы доступны. + +С другой стороны, следует иметь в виду, что событие `DOMContentLoaded` будет ждать не только, собственно, HTML-страницу, но и внешние скрипты, подключенные тегом ` + + + +Уйти на EXAMPLE.COM + + diff --git a/tutorial/02-ui/03-event-details/11-onload-onerror.md b/tutorial/02-ui/03-event-details/11-onload-onerror.md new file mode 100644 index 000000000..0b966639d --- /dev/null +++ b/tutorial/02-ui/03-event-details/11-onload-onerror.md @@ -0,0 +1,237 @@ +# Загрузка скриптов, картинок, фреймов: onload и onerror + +Браузер позволяет отслеживать загрузку внешних ресурсов -- скриптов, ифреймов, картинок и других. + +Для этого есть два события: +
        +
      • `onload` -- если загрузка успешна.
      • +
      • `onerror` -- если при загрузке произошла ошибка.
      • +
      + +## Загрузка `SCRIPT` + +Рассмотрим следующую задачу. + +В браузере работает сложный интерфейс и, чтобы создать очередной компонент, нужно загрузить скрипт с сервера. + +Подгрузить внешний скрипт -- достаточно просто: +[js] +var script = document.createElement('script'); +script.src = "my.js"; + +document.documentElement.appendChild(script); +[/js] + +...Но, как подгрузки выполнить функцию из этого скрипта? Конечно, можно вызвать её в самом скрипте, но если скрипт -- это универсальная библиотека, то это было бы неправильно. + +[ref id="onload"] +### `script.onload` + +Первым нашим помощником станет событие `onload`. + +**Событие `onload` сработает, когда скрипт загрузился *и* выполнился.** + +Например: +[js run] +var script = document.createElement('script'); +script.src = "http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.js" +document.documentElement.appendChild(script); + +*!* +script.onload = function() { + alert(jQuery); +} +*/!* +[/js] + +Это даёт возможность, как в примере выше, получить переменные из скрипта и выполнять объявленные в нём функции. + +...А что, если загрузка скрипта не удалась? Например, такого скрипта на сервере нет (ошибка 404) или сервер "упал" (ошибка 500). + +По-хорошему, такое тоже нужно как-то обрабатывать, хотя бы сообщить посетителю о возникшей проблеме. + +[ref id="onerror"] +### `script.onerror` + +Любые ошибки загрузки (но не выполнения) скрипта отслеживаются обработчиком `onerror`. + +Например, для заведомо отсутствующего скрипта: + +[js run] +var script = document.createElement('script'); +script.src = "http://example.com/404.js" +document.documentElement.appendChild(script); + +*!* +script.onerror = function() { + alert("Ошибка: " + this.src); +}; +*/!* +[/js] + +[ref id="onreadystatechange"] +### IE8-: `script.onreadystatechange` + +Примеры выше работают во всех браузерах, кроме IE8-. + +В IE для отслеживания загрузки есть другое событие: `onreadystatechange`. Оно срабатывает многократно, при каждом обновлении состояния загрузки. + +Текущая стадия процесса находится в `script.readyState`: +
      +
      `loading`
      +
      В процессе загрузки.
      +
      `loaded`
      +
      Получен ответ с сервера -- скрипт или ошибка. Скрипт на фазе `loaded` может быть ещё не выполнен.
      +
      `complete`
      +
      Скрипт выполнен.
      +
      + +Например, рабочий скрипт: + +[js run] +var script = document.createElement('script'); +script.src = "http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.js"; +document.documentElement.appendChild(script); + +*!* +script.onreadystatechange = function() { + alert(this.readyState); // loading -> loaded -> complete +} +*/!* +[/js] + +Скрипт с ошибкой: + +[js run] +var script = document.createElement('script'); +script.src = "http://ajax.googleapis.com/404.js"; +document.documentElement.appendChild(script); + +*!* +script.onreadystatechange = function() { + alert(this.readyState); // loading -> loaded +} +*/!* +[/js] + +Обратим внимание на две особенности: +
        +
      • **Стадии могут пропускаться.** + +Если скрипт в кэше браузера -- он сразу даст `complete`. Вы можете увидеть это, если несколько раз запустите первый пример.
      • +
      • **Нет особой стадии для ошибки.** + +В примере выше это видно, обработка останавливается на `loaded`. +
      • +
      + +Итак, самое надёжное средство для IE8- поймать загрузку (или ошибку загрузки) -- это повесить обработчик на событие `onreadystatechange`, который будет срабатывать и на стадии `complete` и на стадии `loaded`. Так как скрипт может быть ещё не выполнен к этому моменту, то вызов функции лучше сделать через `setTimeout(.., 0)`. + +Пример ниже вызывает `afterLoad` после загрузки скрипта и работает только в IE: + +[js run] +var script = document.createElement('script'); +script.src = "http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.js"; +document.documentElement.appendChild(script); + +function afterLoad() { + alert("Загрузка завершена: " + typeof(jQuery)); +} + +*!* +script.onreadystatechange = function() { + if (this.readyState == "complete") { // на случай пропуска loaded + afterLoad(); // (2) + } + + if (this.readyState == "loaded") { + setTimeout(afterLoad, 0); // (1) + + // убираем обработчик, чтобы не сработал на complete + this.onreadystatechange = null; + } +} +*/!* +[/js] + +Вызов `(1)` выполнится при первой загрузке скрипта, а `(2)` -- при второй, когда он уже будет в кеше, и стадия станет сразу `complete`. + +Функция `afterLoad` может и не обнаружить `jQuery`, если при загрузке была ошибка, причём не важно какая -- файл не найден или синтаксис скрипта ошибочен. + +### Кросс-браузерное решение + +Для кросс-браузерной обработки загрузки скрипта или её ошибки поставим обработчик на все три события: `onload`, `onerror`, `onreadystatechange`. + +Пример ниже выполняет функцию `afterLoad` после загрузки скрипта *или* при ошибке. + +Работает во всех браузерах: + +[js run] +var script = document.createElement('script'); +script.src = "http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.js"; +document.documentElement.appendChild(script); + +function afterLoad() { + alert("Загрузка завершена: " + typeof(jQuery)); +} + +script.onload = script.onerror = function() { + if (!this.executed) { // выполнится только один раз + this.executed = true; + afterLoad(); + } +}; + +script.onreadystatechange = function() { + var self = this; + if (this.readyState == "complete" || this.readyState == "loaded") { + setTimeout(function() { self.onload() }, 0);// сохранить "this" для onload + } +}; +[/js] + + +## Загрузка других ресурсов + +Поддержка этих событий для других типов ресурсов различна: + +
      +
      ``, `` (стили)
      +
      Поддерживает `onload/onerror` во всех браузерах.
      +
      ` - -[/html] - -## `window.onunload` - -Когда человек уходит со страницы или закрывает окно, срабатывает `window.unload`. В нём можно сделать что-то, не требующее ожидания, например, закрыть вспомогательные popup-окна, но отменить сам переход нельзя. - -Это позволяет другое событие -- `window.onbeforeunload`, которое поэтому используется гораздо чаще. - -[ref id="window.onbeforeunload"] -## `window.onbeforeunload` - -**Если посетитель инициировал переход на другую страницу или нажал "закрыть окно", то обработчик `onbeforeunload` может приостановить процесс и спросить подтверждение.** - -Для этого ему нужно вернуть строку, которую браузеры покажут посетителю, спрашивая -- нужно ли переходить. - -Например: - -[js] -window.onbeforeunload = function() { - return "Данные не сохранены. Точно перейти?"; -}; -[/js] - -[warn header="Firefox игнорирует текст, он показывает своё сообщение"] -Firefox игнорирует текст, а всегда показывает своё сообщение. - -Это сделано в целях безопасности. -[/warn] - -Кликните на кнопку в `IFRAME'е` ниже, чтобы поставить обработчик, а затем по ссылке, чтобы увидеть его в действии: - -[iframe src="window-onbeforeunload" border="1" height="80" link] - - -## Эмуляция `DOMContentLoaded` для IE8- - -Прежде чем что-то эмулировать, заметим, что альтернативой событию `onDOMContentLoaded` является вызов функции `init` из скрипта в самом конце `BODY`, когда основная часть DOM уже готова: -[html] - - ... - - -[/html] - -Причина, по которой обычно предпочитают именно событие -- одна: удобство. Вешается обработчик и не надо ничего писать в конец `BODY`. - -### Мини-скрипт `documentReady` -Если вы всё же хотите использовать `onDOMContentLoaded` кросс-браузерно, то нужно либо подключить какой-нибудь фреймворк -- почти все предоставляют такой функционал, либо использовать функцию из мини-библиотеки [jquery.documentReady.js](https://github.com/addyosmani/jquery.parts/blob/master/jquery.documentReady.js). - -Несмотря на то, что в названии содержится слово "jquery", эта библиотечка не требует [jQuery](http://jquery.com). Наоборот, она представляет собой единственную функцию с названием `$`, вызов которой `$(callback)` добавляет обработчик `callback` на `DOMContentLoaded` (можно вызывать много раз), либо, если документ уже загружен -- выполняет его тут же. - -Пример использования: -[html run] - - - - - -
      Текст страницы
      -[/html] - -Здесь `alert` сработает до загрузки картинки, но после создания DOM, в частности, после появления текста. И так будет для всех браузеров, включая даже очень старые IE. - -[smart header="Как именно эмулируется `DOMContentLoaded`?"] -Технически, эмуляция `DOMContentLoaded` для старых IE осуществляется очень забавно. - -Основной приём -- это попытка прокрутить документ вызовом: -[js] -document.documentElement.doScroll("left"); -[/js] -Метод `doScroll` работает только в IE и "методом тыка" было обнаружено, что он бросает исключение, если DOM не полностью создан. - -Поэтому библиотека пытается вызвать прокрутку, если не получается -- через `setTimeout(.., 1)` пытается прокрутить его ещё раз, и так до тех пор, пока действие не перестанет вызывать ошибку. На этом этапе документ считается загрузившимся. - -Внутри фреймов и в очень старых браузерах такой подход может ошибаться, поэтому дополнительно ставится резервный обработчик на `onload`, чтобы уж точно сработал. -[/smart] - - -## Итого - -
        -
      • Самое востребованное событие из описанных -- без сомнения, `DOMContentLoaded`. Многие страницы сделаны так, что инициализуют интерфейсы именно по этому событию. - -Это удобно, ведь можно в `` написать скрипт, который будет запущен в момент, когда все DOM-элементы доступны. - -С другой стороны, следует иметь в виду, что событие `DOMContentLoaded` будет ждать не только, собственно, HTML-страницу, но и внешние скрипты, подключенные тегом ` - - - -Уйти на EXAMPLE.COM - - diff --git a/tutorial/02-ui/03-event-details/11-onload-onerror.md b/tutorial/02-ui/03-event-details/11-onload-onerror.md deleted file mode 100644 index 0b966639d..000000000 --- a/tutorial/02-ui/03-event-details/11-onload-onerror.md +++ /dev/null @@ -1,237 +0,0 @@ -# Загрузка скриптов, картинок, фреймов: onload и onerror - -Браузер позволяет отслеживать загрузку внешних ресурсов -- скриптов, ифреймов, картинок и других. - -Для этого есть два события: -
          -
        • `onload` -- если загрузка успешна.
        • -
        • `onerror` -- если при загрузке произошла ошибка.
        • -
        - -## Загрузка `SCRIPT` - -Рассмотрим следующую задачу. - -В браузере работает сложный интерфейс и, чтобы создать очередной компонент, нужно загрузить скрипт с сервера. - -Подгрузить внешний скрипт -- достаточно просто: -[js] -var script = document.createElement('script'); -script.src = "my.js"; - -document.documentElement.appendChild(script); -[/js] - -...Но, как подгрузки выполнить функцию из этого скрипта? Конечно, можно вызвать её в самом скрипте, но если скрипт -- это универсальная библиотека, то это было бы неправильно. - -[ref id="onload"] -### `script.onload` - -Первым нашим помощником станет событие `onload`. - -**Событие `onload` сработает, когда скрипт загрузился *и* выполнился.** - -Например: -[js run] -var script = document.createElement('script'); -script.src = "http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.js" -document.documentElement.appendChild(script); - -*!* -script.onload = function() { - alert(jQuery); -} -*/!* -[/js] - -Это даёт возможность, как в примере выше, получить переменные из скрипта и выполнять объявленные в нём функции. - -...А что, если загрузка скрипта не удалась? Например, такого скрипта на сервере нет (ошибка 404) или сервер "упал" (ошибка 500). - -По-хорошему, такое тоже нужно как-то обрабатывать, хотя бы сообщить посетителю о возникшей проблеме. - -[ref id="onerror"] -### `script.onerror` - -Любые ошибки загрузки (но не выполнения) скрипта отслеживаются обработчиком `onerror`. - -Например, для заведомо отсутствующего скрипта: - -[js run] -var script = document.createElement('script'); -script.src = "http://example.com/404.js" -document.documentElement.appendChild(script); - -*!* -script.onerror = function() { - alert("Ошибка: " + this.src); -}; -*/!* -[/js] - -[ref id="onreadystatechange"] -### IE8-: `script.onreadystatechange` - -Примеры выше работают во всех браузерах, кроме IE8-. - -В IE для отслеживания загрузки есть другое событие: `onreadystatechange`. Оно срабатывает многократно, при каждом обновлении состояния загрузки. - -Текущая стадия процесса находится в `script.readyState`: -
        -
        `loading`
        -
        В процессе загрузки.
        -
        `loaded`
        -
        Получен ответ с сервера -- скрипт или ошибка. Скрипт на фазе `loaded` может быть ещё не выполнен.
        -
        `complete`
        -
        Скрипт выполнен.
        -
        - -Например, рабочий скрипт: - -[js run] -var script = document.createElement('script'); -script.src = "http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.js"; -document.documentElement.appendChild(script); - -*!* -script.onreadystatechange = function() { - alert(this.readyState); // loading -> loaded -> complete -} -*/!* -[/js] - -Скрипт с ошибкой: - -[js run] -var script = document.createElement('script'); -script.src = "http://ajax.googleapis.com/404.js"; -document.documentElement.appendChild(script); - -*!* -script.onreadystatechange = function() { - alert(this.readyState); // loading -> loaded -} -*/!* -[/js] - -Обратим внимание на две особенности: -
          -
        • **Стадии могут пропускаться.** - -Если скрипт в кэше браузера -- он сразу даст `complete`. Вы можете увидеть это, если несколько раз запустите первый пример.
        • -
        • **Нет особой стадии для ошибки.** - -В примере выше это видно, обработка останавливается на `loaded`. -
        • -
        - -Итак, самое надёжное средство для IE8- поймать загрузку (или ошибку загрузки) -- это повесить обработчик на событие `onreadystatechange`, который будет срабатывать и на стадии `complete` и на стадии `loaded`. Так как скрипт может быть ещё не выполнен к этому моменту, то вызов функции лучше сделать через `setTimeout(.., 0)`. - -Пример ниже вызывает `afterLoad` после загрузки скрипта и работает только в IE: - -[js run] -var script = document.createElement('script'); -script.src = "http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.js"; -document.documentElement.appendChild(script); - -function afterLoad() { - alert("Загрузка завершена: " + typeof(jQuery)); -} - -*!* -script.onreadystatechange = function() { - if (this.readyState == "complete") { // на случай пропуска loaded - afterLoad(); // (2) - } - - if (this.readyState == "loaded") { - setTimeout(afterLoad, 0); // (1) - - // убираем обработчик, чтобы не сработал на complete - this.onreadystatechange = null; - } -} -*/!* -[/js] - -Вызов `(1)` выполнится при первой загрузке скрипта, а `(2)` -- при второй, когда он уже будет в кеше, и стадия станет сразу `complete`. - -Функция `afterLoad` может и не обнаружить `jQuery`, если при загрузке была ошибка, причём не важно какая -- файл не найден или синтаксис скрипта ошибочен. - -### Кросс-браузерное решение - -Для кросс-браузерной обработки загрузки скрипта или её ошибки поставим обработчик на все три события: `onload`, `onerror`, `onreadystatechange`. - -Пример ниже выполняет функцию `afterLoad` после загрузки скрипта *или* при ошибке. - -Работает во всех браузерах: - -[js run] -var script = document.createElement('script'); -script.src = "http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.js"; -document.documentElement.appendChild(script); - -function afterLoad() { - alert("Загрузка завершена: " + typeof(jQuery)); -} - -script.onload = script.onerror = function() { - if (!this.executed) { // выполнится только один раз - this.executed = true; - afterLoad(); - } -}; - -script.onreadystatechange = function() { - var self = this; - if (this.readyState == "complete" || this.readyState == "loaded") { - setTimeout(function() { self.onload() }, 0);// сохранить "this" для onload - } -}; -[/js] - - -## Загрузка других ресурсов - -Поддержка этих событий для других типов ресурсов различна: - -
        -
        ``, `` (стили)
        -
        Поддерживает `onload/onerror` во всех браузерах.
        -
        `",error:'

        The requested content cannot be loaded.
        Please try again later.

        ',closeBtn:'',next:'',prev:''},openEffect:"fade",openSpeed:250,openEasing:"swing",openOpacity:!0, +openMethod:"zoomIn",closeEffect:"fade",closeSpeed:250,closeEasing:"swing",closeOpacity:!0,closeMethod:"zoomOut",nextEffect:"elastic",nextSpeed:250,nextEasing:"swing",nextMethod:"changeIn",prevEffect:"elastic",prevSpeed:250,prevEasing:"swing",prevMethod:"changeOut",helpers:{overlay:!0,title:!0},onCancel:f.noop,beforeLoad:f.noop,afterLoad:f.noop,beforeShow:f.noop,afterShow:f.noop,beforeChange:f.noop,beforeClose:f.noop,afterClose:f.noop},group:{},opts:{},previous:null,coming:null,current:null,isActive:!1, +isOpen:!1,isOpened:!1,wrap:null,skin:null,outer:null,inner:null,player:{timer:null,isActive:!1},ajaxLoad:null,imgPreload:null,transitions:{},helpers:{},open:function(a,d){if(a&&(f.isPlainObject(d)||(d={}),!1!==b.close(!0)))return f.isArray(a)||(a=t(a)?f(a).get():[a]),f.each(a,function(e,c){var k={},g,h,j,m,l;"object"===f.type(c)&&(c.nodeType&&(c=f(c)),t(c)?(k={href:c.data("fancybox-href")||c.attr("href"),title:c.data("fancybox-title")||c.attr("title"),isDom:!0,element:c},f.metadata&&f.extend(!0,k, +c.metadata())):k=c);g=d.href||k.href||(q(c)?c:null);h=d.title!==v?d.title:k.title||"";m=(j=d.content||k.content)?"html":d.type||k.type;!m&&k.isDom&&(m=c.data("fancybox-type"),m||(m=(m=c.prop("class").match(/fancybox\.(\w+)/))?m[1]:null));q(g)&&(m||(b.isImage(g)?m="image":b.isSWF(g)?m="swf":"#"===g.charAt(0)?m="inline":q(c)&&(m="html",j=c)),"ajax"===m&&(l=g.split(/\s+/,2),g=l.shift(),l=l.shift()));j||("inline"===m?g?j=f(q(g)?g.replace(/.*(?=#[^\s]+$)/,""):g):k.isDom&&(j=c):"html"===m?j=g:!m&&(!g&& +k.isDom)&&(m="inline",j=c));f.extend(k,{href:g,type:m,content:j,title:h,selector:l});a[e]=k}),b.opts=f.extend(!0,{},b.defaults,d),d.keys!==v&&(b.opts.keys=d.keys?f.extend({},b.defaults.keys,d.keys):!1),b.group=a,b._start(b.opts.index)},cancel:function(){var a=b.coming;a&&!1!==b.trigger("onCancel")&&(b.hideLoading(),b.ajaxLoad&&b.ajaxLoad.abort(),b.ajaxLoad=null,b.imgPreload&&(b.imgPreload.onload=b.imgPreload.onerror=null),a.wrap&&a.wrap.stop(!0,!0).trigger("onReset").remove(),b.coming=null,b.current|| +b._afterZoomOut(a))},close:function(a){b.cancel();!1!==b.trigger("beforeClose")&&(b.unbindEvents(),b.isActive&&(!b.isOpen||!0===a?(f(".fancybox-wrap").stop(!0).trigger("onReset").remove(),b._afterZoomOut()):(b.isOpen=b.isOpened=!1,b.isClosing=!0,f(".fancybox-item, .fancybox-nav").remove(),b.wrap.stop(!0,!0).removeClass("fancybox-opened"),b.transitions[b.current.closeMethod]())))},play:function(a){var d=function(){clearTimeout(b.player.timer)},e=function(){d();b.current&&b.player.isActive&&(b.player.timer= +setTimeout(b.next,b.current.playSpeed))},c=function(){d();p.unbind(".player");b.player.isActive=!1;b.trigger("onPlayEnd")};if(!0===a||!b.player.isActive&&!1!==a){if(b.current&&(b.current.loop||b.current.index=c.index?"next":"prev"],b.router=e||"jumpto",c.loop&&(0>a&&(a=c.group.length+a%c.group.length),a%=c.group.length),c.group[a]!==v&&(b.cancel(),b._start(a)))},reposition:function(a,d){var e=b.current,c=e?e.wrap:null,k;c&&(k=b._getPosition(d),a&&"scroll"===a.type?(delete k.position,c.stop(!0,!0).animate(k,200)):(c.css(k),e.pos=f.extend({},e.dim,k)))},update:function(a){var d= +a&&a.type,e=!d||"orientationchange"===d;e&&(clearTimeout(B),B=null);b.isOpen&&!B&&(B=setTimeout(function(){var c=b.current;c&&!b.isClosing&&(b.wrap.removeClass("fancybox-tmp"),(e||"load"===d||"resize"===d&&c.autoResize)&&b._setDimension(),"scroll"===d&&c.canShrink||b.reposition(a),b.trigger("onUpdate"),B=null)},e&&!s?0:300))},toggle:function(a){b.isOpen&&(b.current.fitToView="boolean"===f.type(a)?a:!b.current.fitToView,s&&(b.wrap.removeAttr("style").addClass("fancybox-tmp"),b.trigger("onUpdate")), +b.update())},hideLoading:function(){p.unbind(".loading");f("#fancybox-loading").remove()},showLoading:function(){var a,d;b.hideLoading();a=f('
        ').click(b.cancel).appendTo("body");p.bind("keydown.loading",function(a){if(27===(a.which||a.keyCode))a.preventDefault(),b.cancel()});b.defaults.fixed||(d=b.getViewport(),a.css({position:"absolute",top:0.5*d.h+d.y,left:0.5*d.w+d.x}))},getViewport:function(){var a=b.current&&b.current.locked||!1,d={x:n.scrollLeft(), +y:n.scrollTop()};a?(d.w=a[0].clientWidth,d.h=a[0].clientHeight):(d.w=s&&r.innerWidth?r.innerWidth:n.width(),d.h=s&&r.innerHeight?r.innerHeight:n.height());return d},unbindEvents:function(){b.wrap&&t(b.wrap)&&b.wrap.unbind(".fb");p.unbind(".fb");n.unbind(".fb")},bindEvents:function(){var a=b.current,d;a&&(n.bind("orientationchange.fb"+(s?"":" resize.fb")+(a.autoCenter&&!a.locked?" scroll.fb":""),b.update),(d=a.keys)&&p.bind("keydown.fb",function(e){var c=e.which||e.keyCode,k=e.target||e.srcElement; +if(27===c&&b.coming)return!1;!e.ctrlKey&&(!e.altKey&&!e.shiftKey&&!e.metaKey&&(!k||!k.type&&!f(k).is("[contenteditable]")))&&f.each(d,function(d,k){if(1h[0].clientWidth||h[0].clientHeight&&h[0].scrollHeight>h[0].clientHeight),h=f(h).parent();if(0!==c&&!j&&1g||0>k)b.next(0>g?"up":"right");d.preventDefault()}}))},trigger:function(a,d){var e,c=d||b.coming||b.current;if(c){f.isFunction(c[a])&&(e=c[a].apply(c,Array.prototype.slice.call(arguments,1)));if(!1===e)return!1;c.helpers&&f.each(c.helpers,function(d,e){if(e&&b.helpers[d]&&f.isFunction(b.helpers[d][a]))b.helpers[d][a](f.extend(!0, +{},b.helpers[d].defaults,e),c)});p.trigger(a)}},isImage:function(a){return q(a)&&a.match(/(^data:image\/.*,)|(\.(jp(e|g|eg)|gif|png|bmp|webp|svg)((\?|#).*)?$)/i)},isSWF:function(a){return q(a)&&a.match(/\.(swf)((\?|#).*)?$/i)},_start:function(a){var d={},e,c;a=l(a);e=b.group[a]||null;if(!e)return!1;d=f.extend(!0,{},b.opts,e);e=d.margin;c=d.padding;"number"===f.type(e)&&(d.margin=[e,e,e,e]);"number"===f.type(c)&&(d.padding=[c,c,c,c]);d.modal&&f.extend(!0,d,{closeBtn:!1,closeClick:!1,nextClick:!1,arrows:!1, +mouseWheel:!1,keys:null,helpers:{overlay:{closeClick:!1}}});d.autoSize&&(d.autoWidth=d.autoHeight=!0);"auto"===d.width&&(d.autoWidth=!0);"auto"===d.height&&(d.autoHeight=!0);d.group=b.group;d.index=a;b.coming=d;if(!1===b.trigger("beforeLoad"))b.coming=null;else{c=d.type;e=d.href;if(!c)return b.coming=null,b.current&&b.router&&"jumpto"!==b.router?(b.current.index=a,b[b.router](b.direction)):!1;b.isActive=!0;if("image"===c||"swf"===c)d.autoHeight=d.autoWidth=!1,d.scrolling="visible";"image"===c&&(d.aspectRatio= +!0);"iframe"===c&&s&&(d.scrolling="scroll");d.wrap=f(d.tpl.wrap).addClass("fancybox-"+(s?"mobile":"desktop")+" fancybox-type-"+c+" fancybox-tmp "+d.wrapCSS).appendTo(d.parent||"body");f.extend(d,{skin:f(".fancybox-skin",d.wrap),outer:f(".fancybox-outer",d.wrap),inner:f(".fancybox-inner",d.wrap)});f.each(["Top","Right","Bottom","Left"],function(a,b){d.skin.css("padding"+b,w(d.padding[a]))});b.trigger("onReady");if("inline"===c||"html"===c){if(!d.content||!d.content.length)return b._error("content")}else if(!e)return b._error("href"); +"image"===c?b._loadImage():"ajax"===c?b._loadAjax():"iframe"===c?b._loadIframe():b._afterLoad()}},_error:function(a){f.extend(b.coming,{type:"html",autoWidth:!0,autoHeight:!0,minWidth:0,minHeight:0,scrolling:"no",hasError:a,content:b.coming.tpl.error});b._afterLoad()},_loadImage:function(){var a=b.imgPreload=new Image;a.onload=function(){this.onload=this.onerror=null;b.coming.width=this.width/b.opts.pixelRatio;b.coming.height=this.height/b.opts.pixelRatio;b._afterLoad()};a.onerror=function(){this.onload= +this.onerror=null;b._error("image")};a.src=b.coming.href;!0!==a.complete&&b.showLoading()},_loadAjax:function(){var a=b.coming;b.showLoading();b.ajaxLoad=f.ajax(f.extend({},a.ajax,{url:a.href,error:function(a,e){b.coming&&"abort"!==e?b._error("ajax",a):b.hideLoading()},success:function(d,e){"success"===e&&(a.content=d,b._afterLoad())}}))},_loadIframe:function(){var a=b.coming,d=f(a.tpl.iframe.replace(/\{rnd\}/g,(new Date).getTime())).attr("scrolling",s?"auto":a.iframe.scrolling).attr("src",a.href); +f(a.wrap).bind("onReset",function(){try{f(this).find("iframe").hide().attr("src","//about:blank").end().empty()}catch(a){}});a.iframe.preload&&(b.showLoading(),d.one("load",function(){f(this).data("ready",1);s||f(this).bind("load.fb",b.update);f(this).parents(".fancybox-wrap").width("100%").removeClass("fancybox-tmp").show();b._afterLoad()}));a.content=d.appendTo(a.inner);a.iframe.preload||b._afterLoad()},_preloadImages:function(){var a=b.group,d=b.current,e=a.length,c=d.preload?Math.min(d.preload, +e-1):0,f,g;for(g=1;g<=c;g+=1)f=a[(d.index+g)%e],"image"===f.type&&f.href&&((new Image).src=f.href)},_afterLoad:function(){var a=b.coming,d=b.current,e,c,k,g,h;b.hideLoading();if(a&&!1!==b.isActive)if(!1===b.trigger("afterLoad",a,d))a.wrap.stop(!0).trigger("onReset").remove(),b.coming=null;else{d&&(b.trigger("beforeChange",d),d.wrap.stop(!0).removeClass("fancybox-opened").find(".fancybox-item, .fancybox-nav").remove());b.unbindEvents();e=a.content;c=a.type;k=a.scrolling;f.extend(b,{wrap:a.wrap,skin:a.skin, +outer:a.outer,inner:a.inner,current:a,previous:d});g=a.href;switch(c){case "inline":case "ajax":case "html":a.selector?e=f("
        ").html(e).find(a.selector):t(e)&&(e.data("fancybox-placeholder")||e.data("fancybox-placeholder",f('
        ').insertAfter(e).hide()),e=e.show().detach(),a.wrap.bind("onReset",function(){f(this).find(e).length&&e.hide().replaceAll(e.data("fancybox-placeholder")).data("fancybox-placeholder",!1)}));break;case "image":e=a.tpl.image.replace("{href}", +g);break;case "swf":e='',h="",f.each(a.swf,function(a,b){e+='';h+=" "+a+'="'+b+'"'}),e+='"}(!t(e)||!e.parent().is(a.inner))&&a.inner.append(e);b.trigger("beforeShow");a.inner.css("overflow","yes"===k?"scroll": +"no"===k?"hidden":k);b._setDimension();b.reposition();b.isOpen=!1;b.coming=null;b.bindEvents();if(b.isOpened){if(d.prevMethod)b.transitions[d.prevMethod]()}else f(".fancybox-wrap").not(a.wrap).stop(!0).trigger("onReset").remove();b.transitions[b.isOpened?a.nextMethod:a.openMethod]();b._preloadImages()}},_setDimension:function(){var a=b.getViewport(),d=0,e=!1,c=!1,e=b.wrap,k=b.skin,g=b.inner,h=b.current,c=h.width,j=h.height,m=h.minWidth,u=h.minHeight,n=h.maxWidth,p=h.maxHeight,s=h.scrolling,q=h.scrollOutside? +h.scrollbarWidth:0,x=h.margin,y=l(x[1]+x[3]),r=l(x[0]+x[2]),v,z,t,C,A,F,B,D,H;e.add(k).add(g).width("auto").height("auto").removeClass("fancybox-tmp");x=l(k.outerWidth(!0)-k.width());v=l(k.outerHeight(!0)-k.height());z=y+x;t=r+v;C=E(c)?(a.w-z)*l(c)/100:c;A=E(j)?(a.h-t)*l(j)/100:j;if("iframe"===h.type){if(H=h.content,h.autoHeight&&1===H.data("ready"))try{H[0].contentWindow.document.location&&(g.width(C).height(9999),F=H.contents().find("body"),q&&F.css("overflow-x","hidden"),A=F.outerHeight(!0))}catch(G){}}else if(h.autoWidth|| +h.autoHeight)g.addClass("fancybox-tmp"),h.autoWidth||g.width(C),h.autoHeight||g.height(A),h.autoWidth&&(C=g.width()),h.autoHeight&&(A=g.height()),g.removeClass("fancybox-tmp");c=l(C);j=l(A);D=C/A;m=l(E(m)?l(m,"w")-z:m);n=l(E(n)?l(n,"w")-z:n);u=l(E(u)?l(u,"h")-t:u);p=l(E(p)?l(p,"h")-t:p);F=n;B=p;h.fitToView&&(n=Math.min(a.w-z,n),p=Math.min(a.h-t,p));z=a.w-y;r=a.h-r;h.aspectRatio?(c>n&&(c=n,j=l(c/D)),j>p&&(j=p,c=l(j*D)),cz||y>r)&&(c>m&&j>u)&&!(19n&&(c=n,j=l(c/D)),g.width(c).height(j),e.width(c+x),a=e.width(),y=e.height();else c=Math.max(m,Math.min(c,c-(a-z))),j=Math.max(u,Math.min(j,j-(y-r)));q&&("auto"===s&&jz||y>r)&&c>m&&j>u;c=h.aspectRatio?cu&&j
        ').appendTo(b.coming?b.coming.parent:a.parent);this.fixed=!1;a.fixed&&b.defaults.fixed&&(this.overlay.addClass("fancybox-overlay-fixed"),this.fixed=!0)},open:function(a){var d=this;a=f.extend({},this.defaults,a);this.overlay?this.overlay.unbind(".overlay").width("auto").height("auto"):this.create(a);this.fixed||(n.bind("resize.overlay",f.proxy(this.update,this)),this.update());a.closeClick&&this.overlay.bind("click.overlay",function(a){if(f(a.target).hasClass("fancybox-overlay"))return b.isActive? +b.close():d.close(),!1});this.overlay.css(a.css).show()},close:function(){var a,b;n.unbind("resize.overlay");this.el.hasClass("fancybox-lock")&&(f(".fancybox-margin").removeClass("fancybox-margin"),a=n.scrollTop(),b=n.scrollLeft(),this.el.removeClass("fancybox-lock"),n.scrollTop(a).scrollLeft(b));f(".fancybox-overlay").remove().hide();f.extend(this,{overlay:null,fixed:!1})},update:function(){var a="100%",b;this.overlay.width(a).height("100%");I?(b=Math.max(G.documentElement.offsetWidth,G.body.offsetWidth), +p.width()>b&&(a=p.width())):p.width()>n.width()&&(a=p.width());this.overlay.width(a).height(p.height())},onReady:function(a,b){var e=this.overlay;f(".fancybox-overlay").stop(!0,!0);e||this.create(a);a.locked&&(this.fixed&&b.fixed)&&(e||(this.margin=p.height()>n.height()?f("html").css("margin-right").replace("px",""):!1),b.locked=this.overlay.append(b.wrap),b.fixed=!1);!0===a.showEarly&&this.beforeShow.apply(this,arguments)},beforeShow:function(a,b){var e,c;b.locked&&(!1!==this.margin&&(f("*").filter(function(){return"fixed"=== +f(this).css("position")&&!f(this).hasClass("fancybox-overlay")&&!f(this).hasClass("fancybox-wrap")}).addClass("fancybox-margin"),this.el.addClass("fancybox-margin")),e=n.scrollTop(),c=n.scrollLeft(),this.el.addClass("fancybox-lock"),n.scrollTop(e).scrollLeft(c));this.open(a)},onUpdate:function(){this.fixed||this.update()},afterClose:function(a){this.overlay&&!b.coming&&this.overlay.fadeOut(a.speedOut,f.proxy(this.close,this))}};b.helpers.title={defaults:{type:"float",position:"bottom"},beforeShow:function(a){var d= +b.current,e=d.title,c=a.type;f.isFunction(e)&&(e=e.call(d.element,d));if(q(e)&&""!==f.trim(e)){d=f('
        '+e+"
        ");switch(c){case "inside":c=b.skin;break;case "outside":c=b.wrap;break;case "over":c=b.inner;break;default:c=b.skin,d.appendTo("body"),I&&d.width(d.width()),d.wrapInner(''),b.current.margin[2]+=Math.abs(l(d.css("margin-bottom")))}d["top"===a.position?"prependTo":"appendTo"](c)}}};f.fn.fancybox=function(a){var d, +e=f(this),c=this.selector||"",k=function(g){var h=f(this).blur(),j=d,k,l;!g.ctrlKey&&(!g.altKey&&!g.shiftKey&&!g.metaKey)&&!h.is(".fancybox-wrap")&&(k=a.groupAttr||"data-fancybox-group",l=h.attr(k),l||(k="rel",l=h.get(0)[k]),l&&(""!==l&&"nofollow"!==l)&&(h=c.length?f(c):e,h=h.filter("["+k+'="'+l+'"]'),j=h.index(this)),a.index=j,!1!==b.open(h,a)&&g.preventDefault())};a=a||{};d=a.index||0;!c||!1===a.live?e.unbind("click.fb-start").bind("click.fb-start",k):p.undelegate(c,"click.fb-start").delegate(c+ +":not('.fancybox-item, .fancybox-nav')","click.fb-start",k);this.filter("[data-fancybox-start=1]").trigger("click");return this};p.ready(function(){var a,d;f.scrollbarWidth===v&&(f.scrollbarWidth=function(){var a=f('
        ').appendTo("body"),b=a.children(),b=b.innerWidth()-b.height(99).innerWidth();a.remove();return b});if(f.support.fixedPosition===v){a=f.support;d=f('
        ').appendTo("body");var e=20=== +d[0].offsetTop||15===d[0].offsetTop;d.remove();a.fixedPosition=e}f.extend(b.defaults,{scrollbarWidth:f.scrollbarWidth(),fixed:f.support.fixedPosition,parent:f("body")});a=f(r).width();J.addClass("fancybox-lock-test");d=f(r).width();J.removeClass("fancybox-lock-test");f("").appendTo("head")})})(window,document,jQuery); \ No newline at end of file diff --git a/app/js/prism-clike.js b/app/js/prism-clike.js new file mode 100644 index 000000000..b1302b307 --- /dev/null +++ b/app/js/prism-clike.js @@ -0,0 +1,26 @@ +Prism.languages.clike = { + 'comment': { + pattern: /(^|[^\\])(\/\*[\w\W]*?\*\/|(^|[^:])\/\/.*?(\r?\n|$))/g, + lookbehind: true + }, + 'string': /("|')(\\?.)*?\1/g, + 'class-name': { + pattern: /((?:(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/ig, + lookbehind: true, + inside: { + punctuation: /(\.|\\)/ + } + }, + 'keyword': /\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/g, + 'boolean': /\b(true|false)\b/g, + 'function': { + pattern: /[a-z0-9_]+\(/ig, + inside: { + punctuation: /\(/ + } + }, + 'number': /\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/g, + 'operator': /[-+]{1,2}|!|<=?|>=?|={1,3}|&{1,2}|\|?\||\?|\*|\/|\~|\^|\%/g, + 'ignore': /&(lt|gt|amp);/gi, + 'punctuation': /[{}[\];(),.:]/g +}; diff --git a/app/js/prism-core.js b/app/js/prism-core.js new file mode 100644 index 000000000..5eeb4cc5b --- /dev/null +++ b/app/js/prism-core.js @@ -0,0 +1,373 @@ +var self = (typeof window !== 'undefined') ? window : {}; + +/** + * Prism: Lightweight, robust, elegant syntax highlighting + * MIT license http://www.opensource.org/licenses/mit-license.php/ + * @author Lea Verou http://lea.verou.me + */ + +var Prism = (function(){ + +// Private helper vars +var lang = /\blang(?:uage)?-(?!\*)(\w+)\b/i; + +var _ = self.Prism = { + util: { + encode: function (tokens) { + if (tokens instanceof Token) { + return new Token(tokens.type, _.util.encode(tokens.content)); + } else if (_.util.type(tokens) === 'Array') { + return tokens.map(_.util.encode); + } else { + return tokens.replace(/&/g, '&').replace(/ text.length) { + // Something went terribly wrong, ABORT, ABORT! + break tokenloop; + } + + if (str instanceof Token) { + continue; + } + + pattern.lastIndex = 0; + + var match = pattern.exec(str); + + if (match) { + if(lookbehind) { + lookbehindLength = match[1].length; + } + + var from = match.index - 1 + lookbehindLength, + match = match[0].slice(lookbehindLength), + len = match.length, + to = from + len, + before = str.slice(0, from + 1), + after = str.slice(to + 1); + + var args = [i, 1]; + + if (before) { + args.push(before); + } + + var wrapped = new Token(token, inside? _.tokenize(match, inside) : match); + + args.push(wrapped); + + if (after) { + args.push(after); + } + + Array.prototype.splice.apply(strarr, args); + } + } + } + + return strarr; + }, + + hooks: { + all: {}, + + add: function (name, callback) { + var hooks = _.hooks.all; + + hooks[name] = hooks[name] || []; + + hooks[name].push(callback); + }, + + run: function (name, env) { + var callbacks = _.hooks.all[name]; + + if (!callbacks || !callbacks.length) { + return; + } + + for (var i=0, callback; callback = callbacks[i++];) { + callback(env); + } + } + } +}; + +var Token = _.Token = function(type, content) { + this.type = type; + this.content = content; +}; + +Token.stringify = function(o, language, parent) { + if (typeof o == 'string') { + return o; + } + + if (Object.prototype.toString.call(o) == '[object Array]') { + return o.map(function(element) { + return Token.stringify(element, language, o); + }).join(''); + } + + var env = { + type: o.type, + content: Token.stringify(o.content, language, parent), + tag: 'span', + classes: ['token', o.type], + attributes: {}, + language: language, + parent: parent + }; + + if (env.type == 'comment') { + env.attributes['spellcheck'] = 'true'; + } + + _.hooks.run('wrap', env); + + var attributes = ''; + + for (var name in env.attributes) { + attributes += name + '="' + (env.attributes[name] || '') + '"'; + } + + return '<' + env.tag + ' class="' + env.classes.join(' ') + '" ' + attributes + '>' + env.content + ''; + +}; + +if (!self.document) { + if (!self.addEventListener) { + // in Node.js + return self.Prism; + } + // In worker + self.addEventListener('message', function(evt) { + var message = JSON.parse(evt.data), + lang = message.language, + code = message.code; + + self.postMessage(JSON.stringify(_.tokenize(code, _.languages[lang]))); + self.close(); + }, false); + + return self.Prism; +} + +// Get current script and highlight +var script = document.getElementsByTagName('script'); + +script = script[script.length - 1]; + +if (script) { + _.filename = script.src; + + if (document.addEventListener && !script.hasAttribute('data-manual')) { + document.addEventListener('DOMContentLoaded', _.highlightAll); + } +} + +return self.Prism; + +})(); + +if (typeof module !== 'undefined' && module.exports) { + module.exports = Prism; +} diff --git a/app/js/prism-css.js b/app/js/prism-css.js new file mode 100644 index 000000000..4766c0edb --- /dev/null +++ b/app/js/prism-css.js @@ -0,0 +1,31 @@ +Prism.languages.css = { + 'comment': /\/\*[\w\W]*?\*\//g, + 'atrule': { + pattern: /@[\w-]+?.*?(;|(?=\s*{))/gi, + inside: { + 'punctuation': /[;:]/g + } + }, + 'url': /url\((["']?).*?\1\)/gi, + 'selector': /[^\{\}\s][^\{\};]*(?=\s*\{)/g, + 'property': /(\b|\B)[\w-]+(?=\s*:)/ig, + 'string': /("|')(\\?.)*?\1/g, + 'important': /\B!important\b/gi, + 'punctuation': /[\{\};:]/g, + 'function': /[-a-z0-9]+(?=\()/ig +}; + +if (Prism.languages.markup) { + Prism.languages.insertBefore('markup', 'tag', { + 'style': { + pattern: /[\w\W]*?<\/style>/ig, + inside: { + 'tag': { + pattern: /|<\/style>/ig, + inside: Prism.languages.markup.tag.inside + }, + rest: Prism.languages.css + } + } + }); +} \ No newline at end of file diff --git a/app/js/prism-javascript.js b/app/js/prism-javascript.js new file mode 100644 index 000000000..7d62b958f --- /dev/null +++ b/app/js/prism-javascript.js @@ -0,0 +1,26 @@ +Prism.languages.javascript = Prism.languages.extend('clike', { + 'keyword': /\b(break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|function|get|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|set|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)\b/g, + 'number': /\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?|NaN|-?Infinity)\b/g +}); + +Prism.languages.insertBefore('javascript', 'keyword', { + 'regex': { + pattern: /(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/g, + lookbehind: true + } +}); + +if (Prism.languages.markup) { + Prism.languages.insertBefore('markup', 'tag', { + 'script': { + pattern: /[\w\W]*?<\/script>/ig, + inside: { + 'tag': { + pattern: /|<\/script>/ig, + inside: Prism.languages.markup.tag.inside + }, + rest: Prism.languages.javascript + } + } + }); +} diff --git a/app/js/prism-markup.js b/app/js/prism-markup.js new file mode 100644 index 000000000..e6c49953a --- /dev/null +++ b/app/js/prism-markup.js @@ -0,0 +1,41 @@ +Prism.languages.markup = { + 'comment': //g, + 'prolog': /<\?.+?\?>/, + 'doctype': //, + 'cdata': //i, + 'tag': { + pattern: /<\/?[\w:-]+\s*(?:\s+[\w:-]+(?:=(?:("|')(\\?[\w\W])*?\1|[^\s'">=]+))?\s*)*\/?>/gi, + inside: { + 'tag': { + pattern: /^<\/?[\w:-]+/i, + inside: { + 'punctuation': /^<\/?/, + 'namespace': /^[\w-]+?:/ + } + }, + 'attr-value': { + pattern: /=(?:('|")[\w\W]*?(\1)|[^\s>]+)/gi, + inside: { + 'punctuation': /=|>|"/g + } + }, + 'punctuation': /\/?>/g, + 'attr-name': { + pattern: /[\w:-]+/g, + inside: { + 'namespace': /^[\w-]+?:/ + } + } + + } + }, + 'entity': /\&#?[\da-z]{1,8};/gi +}; + +// Plugin to make entity title show the real entity, idea by Roman Komarov +Prism.hooks.add('wrap', function(env) { + + if (env.type === 'entity') { + env.attributes['title'] = env.content.replace(/&/, '&'); + } +}); diff --git a/app/js/prism-my.js b/app/js/prism-my.js new file mode 100644 index 000000000..7ac9c1f37 --- /dev/null +++ b/app/js/prism-my.js @@ -0,0 +1,111 @@ +!function () { + document.removeEventListener('DOMContentLoaded', Prism.highlightAll); + + + function addLineNumbers(pre) { + + var linesNum = (1 + pre.innerHTML.split('\n').length); + var lineNumbersWrapper; + + lines = new Array(linesNum); + lines = lines.join(''); + + lineNumbersWrapper = document.createElement('span'); + lineNumbersWrapper.className = 'line-numbers-rows'; + lineNumbersWrapper.innerHTML = lines; + + if (pre.hasAttribute('data-start')) { + pre.style.counterReset = 'linenumber ' + (parseInt(pre.getAttribute('data-start'), 10) - 1); + } + + pre.appendChild(lineNumbersWrapper); + } + + + function addBlockHighlight(pre) { + + var lines = $(pre).data('highlightBlock'); + + if (!lines) { + return; + } + + var ranges = lines.replace(/\s+/g, '').split(','); + + for (var i = 0, range; range = ranges[i++];) { + range = range.split('-'); + + var start = +range[0], + end = +range[1] || start; + + + var mask = $('
        ' + + new Array(start + 1).join('\n') + + '
        ' + new Array(end - start + 2).join('\n') + '
        '); + + $(pre).prepend(mask); + } + + } + + function esc(str) { + return str + .replace(/&/g, '&') + .replace(//g, '>'); + } + + function unesc(str) { + return str + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>'); + } + + function addInlineHighlight(pre) { + var ranges = $(pre).data('highlightInline'); + var codeElem = $('code', pre); + + ranges = ranges ? ranges.split(",") : []; + + for (var i = 0; i < ranges.length; i++) { + var piece = ranges[i].split(':'); + var lineNum = +piece[0], strRange = piece[1].split('-'); + var start = +strRange[0], end = +strRange[1]; + var mask = $('
        ' + + new Array(lineNum + 1).join('\n') + + new Array(start + 1).join(' ') + + '' + new Array(end - start + 1).join(' ') + '
        '); + + codeElem.prepend(mask); + } + } + + + $(function() { + + // highlight inline + var codePre = $('pre[class*="language-"]'); + + codePre.each(function () { + this.code = unesc(this.innerHTML); + $(this).wrapInner(""); + + Prism.highlightElement(this.firstChild); + + addLineNumbers(this); + addBlockHighlight(this); + addInlineHighlight(this); + new CodeBox(this); + }); + + + }); + + $(function() { + $('iframe.result__iframe').each(function() { + new IframeBox(this); + }) + }); + +}(); diff --git a/app/stylesheets/base.styl b/app/stylesheets/base.styl index f7be37959..6179c3e44 100755 --- a/app/stylesheets/base.styl +++ b/app/stylesheets/base.styl @@ -27,10 +27,10 @@ @require "blocks/dropdown/dropdown" -// @require "blocks/toolbar/toolbar" -// @require "blocks/codebox/codebox" -// @require "blocks/result/result" -// @require "blocks/code-example/code-example" +@require "blocks/toolbar/toolbar" +@require "blocks/codebox/codebox" +@require "blocks/result/result" +@require "blocks/code-example/code-example" // @require "blocks/complex-code/complex-code" // @require "blocks/balance/balance" // @require "blocks/rating/rating" @@ -45,7 +45,7 @@ // @require "blocks/primary-tabs/primary-tabs" // @require "blocks/pager/pager" // @require "blocks/disqus/disqus" -// @require "blocks/standard-table/standard-table" +@require "blocks/standard-table/standard-table" // @require "blocks/book-navigation/book-navigation" // @require "blocks/corrector/corrector" // @require "blocks/course-type/course-type" @@ -121,11 +121,12 @@ // @require "blocks/task/task" // @require "blocks/mibbit-irc/mibbit-irc" -// @require "blocks/prism/prism" -// @require "blocks/prism/prism-line-highlight" -// @require "blocks/prism/prism-line-numbers" -// @require "blocks/prism/my-prism" +@require "blocks/prism/prism" +@require "blocks/prism/prism-line-highlight" +@require "blocks/prism/prism-line-numbers" +@require "blocks/prism/my-prism" // @require "blocks/fancybox/jquery.fancybox" // @require "jquery-modal/jquery.modal" + diff --git a/app/stylesheets/blocks/code-example/code-example.styl b/app/stylesheets/blocks/code-example/code-example.styl new file mode 100644 index 000000000..6e38bf570 --- /dev/null +++ b/app/stylesheets/blocks/code-example/code-example.styl @@ -0,0 +1,10 @@ +.code-example + margin 1.5em 0 + + &__codebox, + &__codebox .codebox, + &__codebox pre[class*="language-"] + margin-bottom 0 + + &__result + margin-top 0 diff --git a/app/stylesheets/blocks/codebox/codebox.styl b/app/stylesheets/blocks/codebox/codebox.styl new file mode 100644 index 000000000..b77dc19c8 --- /dev/null +++ b/app/stylesheets/blocks/codebox/codebox.styl @@ -0,0 +1,14 @@ +.codebox + position relative + margin 1.5em 0 + overflow auto + +.codebox__toolbar + position absolute + top 0 + right 0 + z-index 9 + opacity 0.8 + +.codebox__code + width 100% diff --git a/app/stylesheets/blocks/important/important.sprite/info.png b/app/stylesheets/blocks/important/important.sprite/info.png new file mode 100644 index 0000000000000000000000000000000000000000..17236c791d77e3e2b442d5d9ec59e70e9850a48d GIT binary patch literal 480 zcmeAS@N?(olHy`uVBq!ia0vp^!ayv-!3HGrL)l7z6icy_X9x!n)NrJ90QsB+9+AZi z4BSE>%y{W;-5;Q!Scz*yiE~kEVo7Fxox0hna`DhV%u_2M7CJ{ZJRVqU^fq++NFXd-vH% zX0Q0IJHh%)%e45FUvv}~y!kHi^Ern}#?5uzS8Mr74(vVLcdqJvr`DD^P8)ZHA6UKp zX2-PF$)DdIJ?~)1&Gp>HH{ini9n5UErOc+~1gMU`s zNXXM|Q{VDN%;$!&xEF(~(u_IFS-1`s@rXHA8-_ctITH8dF)#O-HFsE;B=VM+a-R9z z`H;g$&YWp}=bt>W?x|6WQgL~Q5Vm-oRLXqzsbIl`(6W=uD_mSSFB6TS9FfEXK U=IaaLz_4cUboFyt=akR{0Qlj#T>t<8 literal 0 HcmV?d00001 diff --git a/app/stylesheets/blocks/important/important.sprite/ok.png b/app/stylesheets/blocks/important/important.sprite/ok.png new file mode 100644 index 0000000000000000000000000000000000000000..073ddb7546ad9da0c439e782eaf8bb7e449ce2dc GIT binary patch literal 486 zcmeAS@N?(olHy`uVBq!ia0vp^!ayv-!3HGrL)l7z6icy_X9x!n)NrJ90QsB+9+AZi z4BSE>%y{W;-5;Q!Scz*yiE~kEVo7Fxoz)olj*F}LC{y?}qhEvvN|7cEj`-Dl2pZ$oUI(8k@`IoG$nxLLj_!-1igb;;r< z$JQ#GSSkEFq%gDjREE*wlMPxdMR)&C;misN_|<#y36ll;tQ#JQ(XY3b3oA7CXFJGU z>v}JHp|9caPx)i-M9ONGs2vbhXt;Iw*TsXZ1*|WQXGGciNfylTv`VT!?9ISZkZ&?q z=1Y<5#W`(${MQU-g+EAUUXq?NbLVlxgTLM?GOTJSUu=2u0gKuK!?G($f_98wFJ1o2 Y-RzTTP`Qn#3>e}Jp00i_>zopr0K@FJ0RR91 literal 0 HcmV?d00001 diff --git a/app/stylesheets/blocks/important/important.sprite/question.png b/app/stylesheets/blocks/important/important.sprite/question.png new file mode 100644 index 0000000000000000000000000000000000000000..c53409bd6b86e8251600603191d44f1319e68446 GIT binary patch literal 538 zcmV+#0_FXQP)oEzoac)3j9i&72~T(scq>}pMK|^bwPw3a1u5}0^~EW zEzklzm-HKPXCwA`Lc9&ueht8jHtGzsrQ2~`V<2da1(^eV%w0^p=Nm6xP6E^bPcg3v zV=bN|=NntPeXx&$fHqpj^)Fg0xH zD&S3A8t`9*F->_6@H&knIfC}U`#+-@fL%D(=7wYDqgi^nFUE<|g~}UP08JUcS{kT3 cppK31KN$gqK?uH;{Qv*}07*qoM6N<$g4d1Eod5s; literal 0 HcmV?d00001 diff --git a/app/stylesheets/blocks/important/important.sprite/warning.png b/app/stylesheets/blocks/important/important.sprite/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..ce30b1f20518e3f0237559eaa88435c296d6f640 GIT binary patch literal 478 zcmV<40U`d0P)2dJ|_M{qXC2I?V{ z;L5Tr8~uO)isb*ICDMunm^#mZ57}OnZn`PXIzn@AAoPqeN?(> zW4zUrFwbgJeCfhc?LH9NOKh1iP%ey(-?6Fl0{E0|1$1I(z`NKdrJLXZL19zpFTfA5 z6_f zE7f$7JU8mL;xdvv7bSr5Mt2N1pcVT-{ObTt%G7yB+LX<~J>`GA2y5U?)_>yb7hzLV Uus>Tl{r~^~07*qoM6N<$g32($!T to the outer
        ,
        +// because we need to handle many ... inside single 
        +// (this we need for highlighting *!*...* /!* inline
        +.line-numbers .line-numbers-rows
        +	left 0
        +	top 0
        +	padding 1em 0
        +	border 0
        +	background #e7e5e3
        +	width auto
        +
        +.line-numbers-rows > span:before
        +	padding 0 .7em 0 .8em
        +	text-shadow none
        +
        +@media (min-width: largescreen)
        +	pre[class*="language-"],
        +	code[class*="language-"]
        +		font-size 16px
        +		line-height 19px
        diff --git a/app/stylesheets/blocks/prism/prism-line-highlight.styl b/app/stylesheets/blocks/prism/prism-line-highlight.styl
        new file mode 100644
        index 000000000..5e053e0d9
        --- /dev/null
        +++ b/app/stylesheets/blocks/prism/prism-line-highlight.styl
        @@ -0,0 +1,31 @@
        +.inline-highlight
        +	position absolute
        +	pointer-events none
        +	line-height inherit
        +	white-space pre
        +	left 0
        +	top -1px
        +	z-index -1
        +
        +	.mask 
        +		background #F5E7C6
        +		outline 2px solid #F5E7C6
        +
        +.block-highlight
        +	position absolute
        +	left 0
        +	right 0
        +	top -1px
        +	padding inherit 0
        +	margin-top 1em /* Same as .prism’s padding-top */
        +
        +	pointer-events none
        +
        +	line-height inherit
        +	white-space pre
        +	.mask
        +		background #F5E7C6
        +		outline 2px solid #F5E7C6
        +		left 0
        +		right 0
        +		position absolute
        \ No newline at end of file
        diff --git a/app/stylesheets/blocks/prism/prism-line-numbers.styl b/app/stylesheets/blocks/prism/prism-line-numbers.styl
        new file mode 100644
        index 000000000..7f4a87511
        --- /dev/null
        +++ b/app/stylesheets/blocks/prism/prism-line-numbers.styl
        @@ -0,0 +1,31 @@
        +pre.line-numbers
        +	position relative
        +	padding-left 3.8em
        +	counter-reset linenumber
        +
        +.line-numbers .line-numbers-rows
        +	position absolute
        +	pointer-events none
        +	top 0
        +	font-size 100%
        +	left -3.8em
        +	width 3em /* works for line-numbers below 1000 lines */
        +	letter-spacing -1px
        +	border-right 1px solid #999
        +
        +	-webkit-user-select none
        +	-moz-user-select none
        +	-ms-user-select none
        +	user-select none
        +
        +.line-numbers-rows > span
        +	pointer-events none
        +	display block
        +	counter-increment linenumber
        +
        +.line-numbers-rows > span:before
        +	content counter(linenumber)
        +	color #999
        +	display block
        +	padding-right 0.8em
        +	text-align right
        \ No newline at end of file
        diff --git a/app/stylesheets/blocks/prism/prism.styl b/app/stylesheets/blocks/prism/prism.styl
        new file mode 100644
        index 000000000..3a42ad81d
        --- /dev/null
        +++ b/app/stylesheets/blocks/prism/prism.styl
        @@ -0,0 +1,101 @@
        +/**
        + * prism.js default theme for JavaScript, CSS and HTML
        + * Based on dabblet (http://dabblet.com)
        + * @author Lea Verou
        + */
        +
        +code[class*="language-"],
        +pre[class*="language-"]
        +	color black
        +	text-shadow 0 1px white
        +	font-family Consolas, Monaco, 'Andale Mono', monospace
        +	direction ltr
        +	text-align left
        +	white-space pre
        +	word-spacing normal
        +
        +	-moz-tab-size 4
        +	-o-tab-size 4
        +	tab-size 4
        +
        +	-webkit-hyphens none
        +	-moz-hyphens none
        +	-ms-hyphens none
        +	hyphens none
        +
        +pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
        +code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection
        +	text-shadow none
        +	background #b3d4fc
        +
        +pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
        +code[class*="language-"]::selection, code[class*="language-"] ::selection
        +	text-shadow none
        +	background #b3d4fc
        +
        +@media print
        +	code[class*="language-"],
        +	pre[class*="language-"]
        +		text-shadow: none
        +
        +/* Code blocks */
        +pre[class*="language-"]
        +	padding 1em
        +	margin 1.5em 0
        +	overflow auto
        +
        +:not(pre) > code[class*="language-"],
        +pre[class*="language-"]
        +	background: background_blocks
        +
        +/* Inline code */
        +:not(pre) > code[class*="language-"]
        +	padding .1em
        +	border-radius .3em
        +
        +.token.comment,
        +.token.prolog,
        +.token.doctype,
        +.token.cdata
        +	color slategray
        +
        +.token.punctuation
        +	color #999
        +
        +.namespace
        +	opacity .7
        +
        +.token.property,
        +.token.tag,
        +.token.boolean,
        +.token.number
        +	color #905
        +
        +.token.selector,
        +.token.attr-name,
        +.token.string
        +	color #690
        +
        +.token.operator,
        +.token.entity,
        +.token.url,
        +.language-css .token.string,
        +.style .token.string
        +	color #a67f59
        +	/* background: hsla(0,0%,100%,.5); */
        +
        +.token.atrule,
        +.token.attr-value,
        +.token.keyword
        +	color #07a
        +
        +
        +.token.regex,
        +.token.important
        +	color #e90
        +
        +.token.important
        +	font-weight bold
        +
        +.token.entity 
        +	cursor help
        diff --git a/app/stylesheets/blocks/result/result.styl b/app/stylesheets/blocks/result/result.styl
        new file mode 100644
        index 000000000..55cf73c5f
        --- /dev/null
        +++ b/app/stylesheets/blocks/result/result.styl
        @@ -0,0 +1,15 @@
        +.result
        +	position relative
        +	border 1px solid #e7e5e3
        +	margin 1.5em 0
        +
        +	&__toolbar
        +		position absolute
        +		top 0
        +		right 0
        +
        +	&__iframe
        +		display block
        +		border 0
        +		width 100%
        +		height 100px
        diff --git a/app/stylesheets/blocks/standard-table/standard-table.styl b/app/stylesheets/blocks/standard-table/standard-table.styl
        new file mode 100644
        index 000000000..a15827e78
        --- /dev/null
        +++ b/app/stylesheets/blocks/standard-table/standard-table.styl
        @@ -0,0 +1,41 @@
        +.main table
        +	width 100%
        +	border-collapse collapse
        +	font-size 13px
        +	margin 30px 0
        +	
        +	tbody
        +		border 0
        +	
        +	tr
        +		border-bottom 1px solid #ccc
        +
        +	tr:first-child th
        +		border-bottom 3px solid #CCC
        +		vertical-align bottom
        +	
        +	th
        +		text-align left
        +
        +	th,
        +	td
        +		padding 2px 1em 2px 5px
        +	
        +	tr:nth-child(even)
        +		background #f9f9f9
        +
        +	code
        +		font-weight inherit
        +
        +.main table.bordered
        +	td,
        +	th
        +		border solid #ccc
        +		border-width 0 1px
        +
        +	th
        +		border-width 0 0 1px
        +
        +@media (min-width: largescreen)
        +	.main table
        +		font-size: 15px;
        diff --git a/app/stylesheets/blocks/toolbar/toolbar.styl b/app/stylesheets/blocks/toolbar/toolbar.styl
        new file mode 100644
        index 000000000..deea71802
        --- /dev/null
        +++ b/app/stylesheets/blocks/toolbar/toolbar.styl
        @@ -0,0 +1,40 @@
        +.toolbar
        +	display table-row
        +
        +.toolbar__tool
        +	display table-cell
        +	padding-left 1px
        +
        +.toolbar__button
        +	display inline-block
        +	vertical-align bottom
        +	width 30px
        +	height 30px
        +	background #c4c2c0
        +	text-align center
        +	line-height 30px
        +	font-size 16px
        +	@extend $plain-link
        +	&:link,
        +	&:visited,
        +	&:hover,
        +	&:active
        +		color #fff
        +		text-decoration none
        +	&:hover
        +		background darken(@background, 5%)
        +
        +.toolbar__button_run::before
        +	@extend $font-run
        +
        +.toolbar__button_external::before
        +	@extend $font-external
        +
        +.toolbar__button_download::before
        +	@extend $font-download
        +
        +.toolbar__button_edit::before
        +	@extend $font-edit
        +
        +.toolbar__button-text
        +	display none
        diff --git a/app/stylesheets/sprite/facebook.styl b/app/stylesheets/sprite/facebook.styl
        index ea5a79424..b5cc9f91f 100644
        --- a/app/stylesheets/sprite/facebook.styl
        +++ b/app/stylesheets/sprite/facebook.styl
        @@ -1,2 +1,2 @@
        -$facebook-facebook = 0px 0px 0px 0px 16px 16px 16px 32px '/img/facebook.png?t=1405631444240';
        -$facebook-facebook_hover = 0px 16px 0px -16px 16px 16px 16px 32px '/img/facebook.png?t=1405631444240';
        +$facebook-facebook = 0px 0px 0px 0px 16px 16px 16px 32px '/img/facebook.png?t=1405758499261';
        +$facebook-facebook_hover = 0px 16px 0px -16px 16px 16px 16px 32px '/img/facebook.png?t=1405758499261';
        diff --git a/app/stylesheets/sprite/home.styl b/app/stylesheets/sprite/home.styl
        index 912788194..64f2fe5ad 100644
        --- a/app/stylesheets/sprite/home.styl
        +++ b/app/stylesheets/sprite/home.styl
        @@ -1,2 +1,2 @@
        -$home-home = 0px 0px 0px 0px 16px 16px 16px 32px '/img/home.png?t=1405631444255';
        -$home-home_hover = 0px 16px 0px -16px 16px 16px 16px 32px '/img/home.png?t=1405631444255';
        +$home-home = 0px 0px 0px 0px 16px 16px 16px 32px '/img/home.png?t=1405758499280';
        +$home-home_hover = 0px 16px 0px -16px 16px 16px 16px 32px '/img/home.png?t=1405758499280';
        diff --git a/gulpfile.js b/gulpfile.js
        index d94f82325..b6aa3c3fd 100644
        --- a/gulpfile.js
        +++ b/gulpfile.js
        @@ -38,6 +38,9 @@ gulp.task('watch', ['sprite', 'stylus'], function(neverCalled) {
           fse.ensureDirSync('www/img');
           gp.dirSync('app/img', 'www/img');
         
        +  fse.ensureDirSync('www/js');
        +  gp.dirSync('app/js', 'www/js');
        +
           gulp.watch("app/**/*.sprite/**", ['sprite']);
           gulp.watch("app/**/*.styl", ['stylus']);
         });
        diff --git a/hmvc/markup/template/blocks/scripts.jade b/hmvc/markup/template/blocks/scripts.jade
        index 09335ea22..88c28b05f 100644
        --- a/hmvc/markup/template/blocks/scripts.jade
        +++ b/hmvc/markup/template/blocks/scripts.jade
        @@ -1,11 +1,26 @@
         //- TODO: Почему бы не использовать абсолютные пути?
         script(src='http://code.jquery.com/jquery-1.10.1.min.js')
         script(src='http://code.jquery.com/jquery-migrate-1.2.1.min.js')
        -script(src='../app/assets/javascripts/fancybox.pack.js')
        -script(src='../app/assets/javascripts/base.js')
        -script(src='../app/assets/javascripts/prism-core.js')
        -script(src='../app/assets/javascripts/prism-markup.js')
        -script(src='../app/assets/javascripts/prism-css.js')
        -script(src='../app/assets/javascripts/prism-clike.js')
        -script(src='../app/assets/javascripts/prism-javascript.js')
        -script(src='../app/assets/javascripts/prism-my.js')
        \ No newline at end of file
        +script(src='/js/prism-core.js')
        +script(src='/js/prism-markup.js')
        +script(src='/js/prism-css.js')
        +script(src='/js/prism-css-extras.js')
        +script(src='/js/prism-clike.js')
        +script(src='/js/prism-javascript.js')
        +script(src='/js/prism-coffeescript.js')
        +script(src='/js/prism-http.js')
        +script(src='/js/prism-scss.js')
        +script(src='/js/prism-sql.js')
        +script(src='/js/prism-php.js')
        +script(src='/js/prism-php-extras.js')
        +script(src='/js/prism-python.js')
        +script(src='/js/prism-ruby.js')
        +script(src='/js/prism-java.js')
        +script(src='/js/codebox.js')
        +script(src='/js/iframebox.js')
        +script(src='/js/prism-my.js')
        +script(src='/js/select2.js')
        +script(src='/js/select2_locale_ru.js')
        +script(src='/js/base.js')
        +script(src='/js/iframe-resize.js')
        +script(src='/js/jquery.modal.js')
        diff --git a/hmvc/markup/template/pages/article.html b/hmvc/markup/template/pages/article.html
        index b0b873a94..5c7535e69 100644
        --- a/hmvc/markup/template/pages/article.html
        +++ b/hmvc/markup/template/pages/article.html
        @@ -47,3 +47,154 @@ 

        Ч <iframe class="result__iframe" src="http://sass-lang.com/documentation/Sass/Script/Functions"></iframe> </div> </div>

        + +

        Пример кода

        + +
        function sayHi(name) {
        +  var phrase = "Привет, " + name;
        +  alert(phrase);
        +}
        +
        +function HelloWorld(world) {
        +    alert('Hello, ' + world);
        +}
        +
        +HelloWorld('World');
        +
        +sayHi('Вася');
        + +

        Пример результата без кода, такой код должен создаваться динамически в js

        +
        <div class="code-example">
        +    <div class="result code-example__result">
        +        <div class="toolbar result__toolbar">
        +            <div class="toolbar__tool">
        +                <a href="/files/file.doc" class="toolbar__button toolbar__button_edit" title="редактировать">
        +                    <span class="toolbar__button-text">редактировать</span>
        +                </a>
        +            </div>
        +        </div>
        +        <iframe class="result__iframe" src="http://sass-lang.com/documentation/Sass/Script/Functions"></iframe>
        +    </div>
        +</div>
        + +

        В этом плане JavaScript сильно отличается от другого языка, который называется Java.

        + +

        Выделенный блок с информацией.

        + +
        +
        + +

        Почему JavaScript?

        +
        +
        +
        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        XMLHttpRequestIFRAMESCRIPTEventSourceWebSocket
        Кросс-доменностьда, кроме IE<10x1да, сложности в IE<8i1дадада
        МетодыЛюбыеGET / POSTGETGETСвой протокол
        COMETДлинные опросыx2Непрерывное соединениеДлинные опросыНепрерывное соединениеНепрерывное соединение в обе стороны
        ПоддержкаВсе браузеры, ограничения в IE<10x3Все браузерыВсе браузерыКроме IEIE 10, FF11, Chrome 16, Safari 6, Opera 12.5w1
        + +

        Выделенный блок c предупреждением

        + +
        +
        + Важно: +

        Почему JavaScript?

        +
        +
        +

        Когда создавался язык JavaScript, у него изначально было другое название: «LiveScript». Но тогда был очень популярен язык Java, и маркетологи решили, что схожее название сделает новый язык более популярным.

        +

        Планировалось, что JavaScript будет эдаким «младшим братом» Java. Однако, история распорядилась по-своему, JavaScript сильно вырос, и сейчас это совершенно независимый язык, со своей спецификацией, которая называется ECMAScript, и к Java не имеет никакого отношения.

        +

        У него много особенностей, которые усложняют освоение, но по ходу учебника мы с ними разберемся.

        +
        +
        + +

        Выделенный блок c вопросом

        + +
        +
        + Вопрос: +

        Почему JavaScript?

        +
        +
        +

        Когда создавался язык JavaScript, у него изначально было другое название: «LiveScript». Но тогда был очень популярен язык Java, и маркетологи решили, что схожее название сделает новый язык более популярным.

        +

        Планировалось, что JavaScript будет эдаким «младшим братом» Java. Однако, история распорядилась по-своему, JavaScript сильно вырос, и сейчас это совершенно независимый язык, со своей спецификацией, которая называется ECMAScript, и к Java не имеет никакого отношения.

        +

        У него много особенностей, которые усложняют освоение, но по ходу учебника мы с ними разберемся.

        +
        +
        + +

        Выделенный блок c ответом

        + +
        +
        + Задание + + Важность: 4 +
        +
        +

        Когда создавался язык JavaScript, у него изначально было другое название: «LiveScript». Но тогда был очень популярен язык Java, и маркетологи решили, что схожее название сделает новый язык более популярным.

        +

        Планировалось, что JavaScript будет эдаким «младшим братом» Java. Однако, история распорядилась по-своему, JavaScript сильно вырос, и сейчас это совершенно независимый язык, со своей спецификацией, которая называется ECMAScript, и к Java не имеет никакого отношения.

        +

        У него много особенностей, которые усложняют освоение, но по ходу учебника мы с ними разберемся в песочнице.

        +
        +
        + +

        Чтобы читать и выполнять текст на JavaScript, нужна специальная программа — интерпретатор. Процесс выполнения скрипта называют «интерпретацией».

        + +
        +
        + На заметку: +

        Компиляция и интерпретация, для программистов

        +
        +
        +

        Строго говоря, для выполнения программ существуют «компиляторы» и «интерпретаторы».

        +

        Когда-то между ними была большая разница. Компиляторы преобразовывали программу в машинный код, который потом можно выполнять. А интерпретаторы — просто выполняли.

        +

        Сейчас разница гораздо меньше. Современные интерпретаторы перед выполнением преобразуют JavaScript в машинный код (или близко к нему), чтобы выполнялся он быстрее. То есть, по сути, компилируют, а затем запускают.

        +
        +
        + +

        Во все основные браузеры встроен интерпретатор JavaScript, именно поэтому они могут выполнять скрипты на странице.

        + +

        Но, разумеется, этим возможности JavaScript не ограничены. Это полноценный язык, программы на котором можно запускать и на сервере, и даже в стиральной машинке, если в ней установлен соответствующий интерпретатор.

        \ No newline at end of file diff --git a/hmvc/markup/template/pages/article.jade b/hmvc/markup/template/pages/article.jade index c9122ee41..8f349e959 100644 --- a/hmvc/markup/template/pages/article.jade +++ b/hmvc/markup/template/pages/article.jade @@ -7,10 +7,9 @@ block variables - self.title = 'Учебник — Javascript.ru'; - self.comments = {} // хм? - self.comments.lenght = 5; - - var content = readFile('pages/my.html'); + - var content = readFile('pages/article.html'); block content - | BLA - div!= content + != content From dc14db64af7075f7ca7f0ed4199e4227b01c6820 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 19 Jul 2014 12:47:48 +0400 Subject: [PATCH 075/130] hacking on --- .gitignore | 14 +++++-------- app.js | 10 ++++++++++ app/stylesheets/sprite/mixin.styl | 18 ----------------- brunch-config.coffee | 32 ------------------------------ flo.js | 25 ----------------------- lib/mongoose.js | 2 +- package.json | 1 + setup/logger.js | 2 +- setup/router.js | 6 +++--- summary.js | 33 ------------------------------- 10 files changed, 21 insertions(+), 122 deletions(-) delete mode 100644 app/stylesheets/sprite/mixin.styl delete mode 100755 brunch-config.coffee delete mode 100644 flo.js delete mode 100644 summary.js diff --git a/.gitignore b/.gitignore index 729500ea3..8d51483ce 100755 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -.* *.diff *.err *.orig @@ -12,27 +11,24 @@ # OS or Editor folders .DS_Store +.idea .cache .project .settings .tmproj +.nvmrc nbproject Thumbs.db # NPM packages folder. node_modules/ -# Brunch folder for temporary files. +# TMP folder (anything) tmp/ -# Brunch output folder. -# Картинки для CSS -# Стили CSS -# JS -# www/ +# Generated content www/* - -app/stylesheets/sprite +app/stylesheets/sprite/* # Bower stuff. bower_components/ diff --git a/app.js b/app.js index 93f72f5a3..272b99ac2 100755 --- a/app.js +++ b/app.js @@ -3,6 +3,7 @@ const koa = require('koa'); const log = require('javascript-log')(module); + const app = koa(); function requireMiddleware(path) { @@ -24,8 +25,17 @@ requireMiddleware('setup/static'); requireMiddleware('setup/errors'); requireMiddleware('setup/logger'); + requireMiddleware('setup/bodyParser'); + +if (process.env.NODE_ENV == 'development') { + requireMiddleware('setup/bodyLogger'); +} + requireMiddleware('setup/session'); +requireMiddleware('setup/csrf'); + + requireMiddleware('setup/hmvc'); requireMiddleware('setup/render'); requireMiddleware('setup/router'); diff --git a/app/stylesheets/sprite/mixin.styl b/app/stylesheets/sprite/mixin.styl deleted file mode 100644 index 291aa3fb6..000000000 --- a/app/stylesheets/sprite/mixin.styl +++ /dev/null @@ -1,18 +0,0 @@ -spriteWidth($sprite) - width $sprite[4] - -spriteHeight($sprite) - height $sprite[5] - -spritePosition($sprite) - background-position $sprite[2] $sprite[3] - -spriteImage($sprite) - background-image url($sprite[8]) - -sprite($sprite) - if !match('hover', selector()) && !match('active', selector()) - spriteImage($sprite) - spritePosition($sprite) - spriteWidth($sprite) - spriteHeight($sprite) diff --git a/brunch-config.coffee b/brunch-config.coffee deleted file mode 100755 index 34f086d1f..000000000 --- a/brunch-config.coffee +++ /dev/null @@ -1,32 +0,0 @@ -exports.config = -# See https://github.com/brunch/brunch/blob/master/docs/config.md for documentation. - paths: - public: 'www' - files: - javascripts: - joinTo: - 'javascripts/app.js': /^app/ - 'javascripts/vendor.js': /^(vendor|bower_components)/ - order: - before: [] - - stylesheets: - joinTo: - 'stylesheets/app.css': /^app/ - order: - before: [] - after: [] - - templates: - joinTo: 'javascripts/app.js' - - - plugins: - jade: - pretty: yes # Adds pretty-indentation whitespaces to output (false by default) - stylus: - linenos: true - includeCss: true - - conventions: - assets: /^assets/ \ No newline at end of file diff --git a/flo.js b/flo.js deleted file mode 100644 index 8cd613d5e..000000000 --- a/flo.js +++ /dev/null @@ -1,25 +0,0 @@ -/* DEPRECATED NOT USED (livereload instead) */ - -var flo = require('fb-flo'); -var fs = require('fs'); - -var server = flo( - './www/', - { - port: 8888, - host: 'localhost', - verbose: true, - glob: [ - '**/*.{js,css,html,png}' - ] - }, - function resolver(filepath, callback) { - console.log('Reloading \'' + filepath + '\' with flo...'); - var file = './www/' + filepath; - callback({ - resourceURL : filepath, - contents : fs.readFileSync(file), - reload : filepath.match(/\.(js|html)$/) - }); - } -); diff --git a/lib/mongoose.js b/lib/mongoose.js index c991f6781..ac25f55d2 100755 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -84,7 +84,7 @@ mongoose.waitConnect = function(callback) { module.exports = mongoose; -//requireTree('../model'); +requireTree('../model'); /* // models may want this mongoose that's why we require them AFTER module.exports = mongoose requireModels(); diff --git a/package.json b/package.json index a76f41c1e..507e4cd60 100755 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "jquery": "^2.1.1", "koa": "*", "koa-bodyparser": "*", + "koa-csrf": "^2.1.2", "koa-favicon": "*", "koa-generic-session": "*", "koa-logger": "*", diff --git a/setup/logger.js b/setup/logger.js index 1674e7e1c..09d0c8c9d 100755 --- a/setup/logger.js +++ b/setup/logger.js @@ -3,4 +3,4 @@ const logger = require('koa-logger'); module.exports = function(app) { app.use(logger()); -}; \ No newline at end of file +}; diff --git a/setup/router.js b/setup/router.js index 7ddb26b8b..f0ad84a70 100755 --- a/setup/router.js +++ b/setup/router.js @@ -1,18 +1,18 @@ 'use strict'; var mount = require('koa-mount'); -var Router = require('koa-router'); module.exports = function(app) { - //app.use(router(app)); - app.use(mount('/', app.hmvc.frontpage.middleware)); if (process.env.NODE_ENV == 'development') { app.use(mount('/markup', app.hmvc.markup.middleware)); } + app.use(mount('/getpdf', app.hmvc.getpdf.middleware)); + + // stick to bottom app.use(mount('/', app.hmvc.tutorial.middleware)); // by default if the router didn't find anything => it yields to next middleware diff --git a/summary.js b/summary.js deleted file mode 100644 index 1f3a735a2..000000000 --- a/summary.js +++ /dev/null @@ -1,33 +0,0 @@ -const mongoose = require('lib/mongoose'); -const co = require('co'); -const treeUtil = require('lib/treeUtil'); - -var Article = mongoose.models.Article; - -co(function*() { - var tree = yield Article.findTree(); - - treeUtil.walkArray(tree, function(node) { - if (!node.parent) { - node.level = 1; - node.path = (node.weight > 9 ? node.weight : '0' + node.weight) + '-' + node.slug; - node.url = node.path + (node.isFolder ? '/index.md' : '/article.md'); - } else { - node.level = tree.byId[node.parent].level + 1; - node.path = tree.byId[node.parent].path + '/' + (node.weight > 9 ? node.weight : '0' + node.weight) + '-' + node.slug; - node.url = node.path + (node.isFolder ? '/index.md' : '/article.md'); - } - - }); - - var arr = treeUtil.flattenArray(tree); - - for (var i = 0; i < arr.length; i++) { - var node = arr[i]; - console.log(new Array(node.level).join(' ') + '- [' + node.title + '](' + node.url + ')'); - } - - -})(function(err) { - if (err) throw err; -}); From b7a06dbea09987652cfcd73a39d68c8691fbbf5d Mon Sep 17 00:00:00 2001 From: Anton Vernigor Date: Sun, 20 Jul 2014 10:23:58 +0400 Subject: [PATCH 076/130] moving markup to jade and stylus --- hmvc/markup/template/pages/article.html | 104 +++++++++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/hmvc/markup/template/pages/article.html b/hmvc/markup/template/pages/article.html index 5c7535e69..843ac9021 100644 --- a/hmvc/markup/template/pages/article.html +++ b/hmvc/markup/template/pages/article.html @@ -197,4 +197,106 @@

        Компиляция и интерпретация,

        Во все основные браузеры встроен интерпретатор JavaScript, именно поэтому они могут выполнять скрипты на странице.

        -

        Но, разумеется, этим возможности JavaScript не ограничены. Это полноценный язык, программы на котором можно запускать и на сервере, и даже в стиральной машинке, если в ней установлен соответствующий интерпретатор.

        \ No newline at end of file +

        Но, разумеется, этим возможности JavaScript не ограничены. Это полноценный язык, программы на котором можно запускать и на сервере, и даже в стиральной машинке, если в ней установлен соответствующий интерпретатор.

        + +

        Что умеет JavaScript?

        + +

        Современный JavaScript — это «безопасный» язык программирования общего назначения. Он не предоставляет низкоуровневых средств работы с памятью, процессором, так как изначально был ориентирован на браузеры, в которых это не требуется.

        +

        В браузере JavaScript умеет делать все, что относится к манипуляции со страницей, взаимодействию с посетителем и, в какой-то мере, с сервером:

        +
          +
        • Создавать новые HTML-теги, удалять существующие, менять стили элементов, прятать, показывать элементы и т.п.
        • +
        • Реагировать на действия посетителя, обрабатывать клики мыши, перемещение курсора, нажатие на клавиатуру и т.п.
        • +
        • Посылать запросы на сервер и загружать данные без перезагрузки страницы(эта технология называется "AJAX").
        • +
        • Получать и устанавливать cookie, запрашивать данные, выводить сообщения…
        • +
        • …и многое, многое другое!
        • +
        + +

        Что НЕ умеет JavaScript?

        +

        JavaScript — быстрый и мощный язык, но браузер накладывает на его исполнение некоторые ограничения.

        +

        Это сделано для безопасности пользователей, чтобы злоумышленник не мог с помощью JavaScript получить личные данные или как-то навредить компьютеру пользователя. В браузере Firefox существует способ «подписи» скриптов с целью обхода части ограничений, но он нестандартный и не кросс-браузерный.

        + +
        +
        + Задание: +

        Вызов «на месте»

        + Важность: 4 +
        +
        +

        Каков будет результат выполнения кода? Почему?

        +
        +
        var a = 5
        +
        +(function() {
        +alert(a)
        +})()
        +
        + +

        P.S. Подумайте хорошо! Здесь все ошибаются!
        + P.P.S. Внимание, здесь подводный камень! Ок, вы предупреждены.

        + +
        + + +
        +

        Результат - ошибка. Попробуйте:

        +
        +
        var a = 5
        +
        +(function() {
        +alert(a)
        +})()
        +
        +

        Дело в том, что после var a = 5 нет точки с запятой.

        +

        JavaScript воспринимает этот код как если бы перевода строки не было:

        + +
        +
        var a = 5(function() {
        +alert(a)
        +})()
        +
        +

        То есть, он пытается вызвать функцию 5, что и приводит к ошибке.

        +

        Если точку с запятой поставить, все будет хорошо:

        +
        +
        var a = 5;
        +
        +(function() {
        +alert(a)
        +})()
        +
        +

        Это один из наиболее частых и опасных подводных камней, приводящих к ошибкам тех, кто не ставит точки с запятой.

        +
        +
        +
        + + +
        +

        К одной задаче могут быть добавлены одно или несколько решений. Решения, идущие подряд, «стыкуются» без промежутков

        +
        +
        +
        +
        + +

        Этих ограничений нет там, где JavaScript используется вне браузера, например на сервере.

        +

        Большинство возможностей JavaScript в браузере ограничено текущим окном и страницей.

        +

        +
          +
        • +

          JavaScript не может читать/записывать произвольные файлы на жесткий диск, копировать их или вызывать программы. Он не имеет прямого доступа к операционной системе.

          +

          Современные браузеры могут работать с файлами, но эта возможность ограничена специально выделенной директорией — песочницей. Возможности по доступу к устройствам также прорабатываются в современных стандартах и, частично, доступны в некоторых браузерах.

        • +
        • +

          JavaScript, работающий в одной вкладке, почти не может общаться с другими вкладками и окнами. За исключением случая, когда он сам открыл это окно или несколько вкладок из одного источника (одинаковый домен, порт, протокол).

          +

          Есть способы это обойти, и они раскрыты в учебнике, но для этого требуется как минимум явное согласие обеих сторон. Просто так взять и залезть в произвольную вкладку с другого домена нельзя.

          +
        • +
        • +

          Из JavaScript можно легко посылать запросы на сервер, с которого пришла страничка. Запрос на другой домен тоже возможен, но менее удобен, т.к. и здесь есть ограничения безопасности.

          +
        • +
        From 4f75c0cf5502bd73d5c363f1b03eb559c9766363 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 20 Jul 2014 10:45:24 +0400 Subject: [PATCH 077/130] hacking on --- app/.jshintrc | 14 +++ app/stylesheets/sprite/facebook.styl | 6 +- app/stylesheets/sprite/home.styl | 6 +- config/base.js | 7 +- .../{secret.default.js => secret.template.js} | 0 hmvc/auth/index.js | 2 + hmvc/auth/test/.jshintrc | 23 +++++ hmvc/getpdf/controller/finish.js | 6 ++ hmvc/getpdf/controller/order.js | 42 +++++++++ hmvc/getpdf/controller/root.js | 6 ++ hmvc/getpdf/index.js | 4 + hmvc/getpdf/paymentMethods.js | 7 ++ hmvc/getpdf/router.js | 10 +++ hmvc/getpdf/template/root.jade | 31 +++++++ hmvc/markup/template/pages/form.jade | 4 + hmvc/payment/index.js | 11 +++ hmvc/payment/model/order.js | 32 +++++++ hmvc/payment/model/transaction.js | 42 +++++++++ hmvc/payment/model/transactionLog.js | 21 +++++ hmvc/tutorial/index.js | 6 +- hmvc/tutorial/test/.jshintrc | 23 +++++ hmvc/webmoney/controller/back.js | 36 ++++++++ hmvc/webmoney/controller/result.js | 86 +++++++++++++++++++ hmvc/webmoney/controller/wait.js | 49 +++++++++++ hmvc/webmoney/index.js | 19 ++++ hmvc/webmoney/router.js | 14 +++ hmvc/webmoney/template/back.jade | 20 +++++ hmvc/webmoney/template/form.jade | 7 ++ lib/mongoose.js | 10 ++- model/.gitkeep | 0 package.json | 2 + setup/bodyLogger.js | 10 +++ setup/csrf.js | 10 +++ setup/render.js | 55 ++++++++---- setup/router.js | 1 + test/.jshintrc | 23 +++++ 36 files changed, 616 insertions(+), 29 deletions(-) create mode 100644 app/.jshintrc rename config/{secret.default.js => secret.template.js} (100%) create mode 100644 hmvc/auth/test/.jshintrc create mode 100644 hmvc/getpdf/controller/finish.js create mode 100644 hmvc/getpdf/controller/order.js create mode 100644 hmvc/getpdf/controller/root.js create mode 100644 hmvc/getpdf/index.js create mode 100644 hmvc/getpdf/paymentMethods.js create mode 100644 hmvc/getpdf/router.js create mode 100644 hmvc/getpdf/template/root.jade create mode 100644 hmvc/markup/template/pages/form.jade create mode 100644 hmvc/payment/index.js create mode 100644 hmvc/payment/model/order.js create mode 100644 hmvc/payment/model/transaction.js create mode 100644 hmvc/payment/model/transactionLog.js create mode 100644 hmvc/tutorial/test/.jshintrc create mode 100644 hmvc/webmoney/controller/back.js create mode 100644 hmvc/webmoney/controller/result.js create mode 100644 hmvc/webmoney/controller/wait.js create mode 100644 hmvc/webmoney/index.js create mode 100644 hmvc/webmoney/router.js create mode 100644 hmvc/webmoney/template/back.jade create mode 100644 hmvc/webmoney/template/form.jade create mode 100644 model/.gitkeep create mode 100644 setup/bodyLogger.js create mode 100644 setup/csrf.js create mode 100644 test/.jshintrc diff --git a/app/.jshintrc b/app/.jshintrc new file mode 100644 index 000000000..b3a2dbc1f --- /dev/null +++ b/app/.jshintrc @@ -0,0 +1,14 @@ +{ + "maxerr": 25, + "latedef": "nofunc", + "browser": true, + "node": true, // for browserify require etc + "indent": 2, + "camelcase": true, + "newcap": true, + "undef": true, + "multistr": true, + "noyield": true, + "devel": true, + "-W004": true +} diff --git a/app/stylesheets/sprite/facebook.styl b/app/stylesheets/sprite/facebook.styl index b5cc9f91f..7512aa58c 100644 --- a/app/stylesheets/sprite/facebook.styl +++ b/app/stylesheets/sprite/facebook.styl @@ -1,2 +1,4 @@ -$facebook-facebook = 0px 0px 0px 0px 16px 16px 16px 32px '/img/facebook.png?t=1405758499261'; -$facebook-facebook_hover = 0px 16px 0px -16px 16px 16px 16px 32px '/img/facebook.png?t=1405758499261'; +$facebook-facebook = 0px 0px 0px 0px 16px 16px 16px 32px '/img/facebook.png?time=1405761478508'; +$facebook-facebook_hover = 0px 16px 0px -16px 16px 16px 16px 32px '/img/facebook.png?time=1405761478508'; + +$facebook = 16px 32px '/img/facebook.png?time=1405761478508' \ No newline at end of file diff --git a/app/stylesheets/sprite/home.styl b/app/stylesheets/sprite/home.styl index 64f2fe5ad..287607fba 100644 --- a/app/stylesheets/sprite/home.styl +++ b/app/stylesheets/sprite/home.styl @@ -1,2 +1,4 @@ -$home-home = 0px 0px 0px 0px 16px 16px 16px 32px '/img/home.png?t=1405758499280'; -$home-home_hover = 0px 16px 0px -16px 16px 16px 16px 32px '/img/home.png?t=1405758499280'; +$home-home = 0px 0px 0px 0px 16px 16px 16px 32px '/img/home.png?time=1405761478581'; +$home-home_hover = 0px 16px 0px -16px 16px 16px 16px 32px '/img/home.png?time=1405761478581'; + +$home = 16px 32px '/img/home.png?time=1405761478581' \ No newline at end of file diff --git a/config/base.js b/config/base.js index e1d309648..6731bc5d2 100755 --- a/config/base.js +++ b/config/base.js @@ -1,7 +1,7 @@ var path = require('path'); var fs = require('fs'); -var secretPath = fs.existsSync(path.join(__dirname, 'secret')) ? './secret' : './secret.default'; +var secretPath = fs.existsSync(path.join(__dirname, 'secret.js')) ? './secret' : './secret.template'; var secret = require(secretPath); module.exports = function() { @@ -20,9 +20,10 @@ module.exports = function() { } } }, - "session": { - "keys": [secret.SESSION_KEY] + session: { + keys: [secret.sessionKey] }, + webmoney: secret.webmoney, template: { options: { 'cache': process.env.NODE_ENV != 'development' diff --git a/config/secret.default.js b/config/secret.template.js similarity index 100% rename from config/secret.default.js rename to config/secret.template.js diff --git a/hmvc/auth/index.js b/hmvc/auth/index.js index c044c33d6..3f7d3cd21 100644 --- a/hmvc/auth/index.js +++ b/hmvc/auth/index.js @@ -1,3 +1,5 @@ +/* var requireTree = require('require-tree'); requireTree('./model'); +*/ diff --git a/hmvc/auth/test/.jshintrc b/hmvc/auth/test/.jshintrc new file mode 100644 index 000000000..077663629 --- /dev/null +++ b/hmvc/auth/test/.jshintrc @@ -0,0 +1,23 @@ +{ + "maxerr": 25, + "latedef": "nofunc", + "browser": false, + "node": true, + "indent": 2, + "camelcase": true, + "newcap": true, + "undef": true, + "esnext": true, + "multistr": true, + "noyield": true, + "expr": true, + "-W004": true, + "globals" : { + "describe" : false, + "it" : false, + "before" : false, + "beforeEach" : false, + "after" : false, + "afterEach" : false + } +} diff --git a/hmvc/getpdf/controller/finish.js b/hmvc/getpdf/controller/finish.js new file mode 100644 index 000000000..d6e48667f --- /dev/null +++ b/hmvc/getpdf/controller/finish.js @@ -0,0 +1,6 @@ +exports.get = function*(next) { + + this.locals.paymentMethods = require('../paymentMethods').methods; + + this.render(__dirname, 'root'); +}; diff --git a/hmvc/getpdf/controller/order.js b/hmvc/getpdf/controller/order.js new file mode 100644 index 000000000..f6b6bd932 --- /dev/null +++ b/hmvc/getpdf/controller/order.js @@ -0,0 +1,42 @@ +var mongoose = require('mongoose'); + +var Order = mongoose.models.Order; +var Transaction = mongoose.models.Transaction; + +var methods = require('../paymentMethods').methods; + +exports.post = function*(next) { + + var method = methods[this.request.body.paymentMethod]; + if (!method) { + this.throw(403, "Unsupported payment method"); + } + + var methodApi = this.app.hmvc[method.name]; // /hmvc/webmoney + + var order = new Order({ + amount: 1, + module: 'getpdf', + data: { } + }); + yield order.persist(); + + + var transaction = new Transaction({ + order: order._id, + amount: order.amount, + paymentType: 'webmoney' + }); + + yield transaction.persist(); + + if (!this.session.transactions) { + this.session.transactions = []; + } + this.session.transactions.push(transaction.number); + + var form = yield methodApi.createTransactionForm(transaction); + + this.body = form; + +}; diff --git a/hmvc/getpdf/controller/root.js b/hmvc/getpdf/controller/root.js new file mode 100644 index 000000000..d6e48667f --- /dev/null +++ b/hmvc/getpdf/controller/root.js @@ -0,0 +1,6 @@ +exports.get = function*(next) { + + this.locals.paymentMethods = require('../paymentMethods').methods; + + this.render(__dirname, 'root'); +}; diff --git a/hmvc/getpdf/index.js b/hmvc/getpdf/index.js new file mode 100644 index 000000000..cdfa18f19 --- /dev/null +++ b/hmvc/getpdf/index.js @@ -0,0 +1,4 @@ + +var router = require('./router'); + +exports.middleware = router.middleware(); diff --git a/hmvc/getpdf/paymentMethods.js b/hmvc/getpdf/paymentMethods.js new file mode 100644 index 000000000..521238af7 --- /dev/null +++ b/hmvc/getpdf/paymentMethods.js @@ -0,0 +1,7 @@ +exports.methods = { + 'webmoney': {name: "webmoney", title: "Webmoney"}, + 'yandex_money': {name: "yandex_money", title: "Яндекс.Деньги"}, + 'payanyway': {name: "payanyway", title: "PayAnyWay"}, + 'interkassa': {name: "interkassa", title: "Интеркасса"}, + 'paypal': {name: "paypal", title: "Paypal"} +}; diff --git a/hmvc/getpdf/router.js b/hmvc/getpdf/router.js new file mode 100644 index 000000000..6d1fbcd77 --- /dev/null +++ b/hmvc/getpdf/router.js @@ -0,0 +1,10 @@ +var Router = require('koa-router'); + + +var router = module.exports = new Router(); + +var root = require('./controller/root'); +var order = require('./controller/order'); + +router.get('', root.get); +router.post('/order', order.post); diff --git a/hmvc/getpdf/template/root.jade b/hmvc/getpdf/template/root.jade new file mode 100644 index 000000000..607382883 --- /dev/null +++ b/hmvc/getpdf/template/root.jade @@ -0,0 +1,31 @@ +div + p Выберите способ оплаты: + form.pay-form.notready(onsubmit="alert('Минуточку, идёт загрузка...'); return false") + select(name="paymentMethod") + each paymentMethod in paymentMethods + option(value=paymentMethod.name) #{paymentMethod.title} + input(type="submit" value="Оплатить") + +script(src="http://code.jquery.com/jquery-2.1.1.js") + +script var csrf = "#{csrf}"; + +script. + $('.notready').removeClass('notready').prop('onsubmit', null); + + $('.pay-form') + .on('submit', function(e) { + e.preventDefault(); + $.ajax({ + method: 'POST', + url: '/getpdf/order', + data: { + _csrf: csrf, + paymentMethod: this.elements.paymentMethod.value + } + }) + .done(function(html) { + $(html).submit(); + }); + + }); diff --git a/hmvc/markup/template/pages/form.jade b/hmvc/markup/template/pages/form.jade new file mode 100644 index 000000000..588d99efd --- /dev/null +++ b/hmvc/markup/template/pages/form.jade @@ -0,0 +1,4 @@ +form(method="POST",action="/test") + input(type="hidden",name="name",value="value") + input(type="submit",value="submit") + diff --git a/hmvc/payment/index.js b/hmvc/payment/index.js new file mode 100644 index 000000000..a0ef48b09 --- /dev/null +++ b/hmvc/payment/index.js @@ -0,0 +1,11 @@ +var mongoose = require('mongoose'); +var Transaction = mongoose.model.Transaction; + +exports.createTransaction = function* (amount, orderNum, type) { + return yield new Transaction({ + amount: amount, + orderNum: orderNum, + paymentType: type + }).persist(); +}; + diff --git a/hmvc/payment/model/order.js b/hmvc/payment/model/order.js new file mode 100644 index 000000000..9d2cc5c31 --- /dev/null +++ b/hmvc/payment/model/order.js @@ -0,0 +1,32 @@ +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; +var autoIncrement = require('mongoose-auto-increment'); + +var schema = new Schema({ + amount: { + type: Number, + required: true + }, + module: { // module so that transaction handler knows where to go back e.g. 'getpdf' + type: String, + required: true + }, + data: String, + created: { + type: Date, + default: Date.now + } +}); + +schema.methods.getSuccessUrl = function() { + return '/' + this.module + '/success'; +}; + +schema.methods.getFailUrl = function() { + return '/' + this.module + '/fail'; +}; + +schema.plugin(autoIncrement.plugin, {model: 'Order', field: 'number'}); + +mongoose.model('Order', schema); + diff --git a/hmvc/payment/model/transaction.js b/hmvc/payment/model/transaction.js new file mode 100644 index 000000000..8419ef1eb --- /dev/null +++ b/hmvc/payment/model/transaction.js @@ -0,0 +1,42 @@ +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; +var autoIncrement = require('mongoose-auto-increment'); + +/** + * Transaction is an actual payment for something + * Order may exist without any transactions (pay later) + * Transaction has it's own separate number (payment attempt) + * @type {Schema} + */ +var schema = new Schema({ + order: { + type: Schema.Types.ObjectId, + ref: 'Order' + }, + amount: { + type: Number, + required: true + }, + paymentType: { + type: String, + required: true + }, + created: { + type: Date, + default: Date.now + }, + status: { + type: String + }, + data: String +}); + +schema.plugin(autoIncrement.plugin, {model: 'Transaction', field: 'number'}); + + +const Transaction = mongoose.model('Transaction', schema); + +Transaction.STATUS_SUCCESS = 'success'; +Transaction.STATUS_FAIL = 'fail'; + + diff --git a/hmvc/payment/model/transactionLog.js b/hmvc/payment/model/transactionLog.js new file mode 100644 index 000000000..a6066af87 --- /dev/null +++ b/hmvc/payment/model/transactionLog.js @@ -0,0 +1,21 @@ +var mongoose = require('mongoose'); + +var Schema = mongoose.Schema; + +var schema = new Schema({ + + transaction: { + type: Schema.Types.ObjectId, + ref: 'Transaction' + }, + event: String, + data: String, + + created: { + type: Date, + default: Date.now + } +}); + +mongoose.model('TransactionLog', schema); + diff --git a/hmvc/tutorial/index.js b/hmvc/tutorial/index.js index 1d86d2c51..05c3ee659 100644 --- a/hmvc/tutorial/index.js +++ b/hmvc/tutorial/index.js @@ -1,6 +1,6 @@ -var requireTree = require('require-tree'); - -requireTree('./model'); +//var requireTree = require('require-tree'); +// +//requireTree('./model'); var router = require('./router'); diff --git a/hmvc/tutorial/test/.jshintrc b/hmvc/tutorial/test/.jshintrc new file mode 100644 index 000000000..077663629 --- /dev/null +++ b/hmvc/tutorial/test/.jshintrc @@ -0,0 +1,23 @@ +{ + "maxerr": 25, + "latedef": "nofunc", + "browser": false, + "node": true, + "indent": 2, + "camelcase": true, + "newcap": true, + "undef": true, + "esnext": true, + "multistr": true, + "noyield": true, + "expr": true, + "-W004": true, + "globals" : { + "describe" : false, + "it" : false, + "before" : false, + "beforeEach" : false, + "after" : false, + "afterEach" : false + } +} diff --git a/hmvc/webmoney/controller/back.js b/hmvc/webmoney/controller/back.js new file mode 100644 index 000000000..87db3572e --- /dev/null +++ b/hmvc/webmoney/controller/back.js @@ -0,0 +1,36 @@ +const payment = require('../../payment'); +const config = require('config'); +const mongoose = require('mongoose'); +const Order = mongoose.models.Order; +const Transaction = mongoose.models.Transaction; +const TransactionLog = mongoose.models.TransactionLog; +const log = require('javascript-log')(module); +const md5 = require('MD5'); + +log.debugOn(); + + +exports.get = function* (next) { + + var transactionNumber = this.query.LMI_PAYMENT_NO; + var transaction = yield Transaction.findOne({number: transactionNumber}).populate('order').exec(); + + if (!transaction) { + this.throw(404, 'transaction not found'); + } + + var order = transaction.order; + if (!this.session.orders || this.session.orders.indexOf(order.number) == -1) { + this.throw(403, 'order not in your session'); + } + + if (transaction.status) { + this.redirect(transaction.status == Transaction.STATUS_SUCCESS ? order.getSuccessUrl() : order.getFailUrl()); + } else { + this.render(__dirname, 'back', { + number: this.query.LMI_PAYMENT_NO, + module: '/' + transaction.order.module + '/finish' + }); + } + +}; diff --git a/hmvc/webmoney/controller/result.js b/hmvc/webmoney/controller/result.js new file mode 100644 index 000000000..5e2f6b881 --- /dev/null +++ b/hmvc/webmoney/controller/result.js @@ -0,0 +1,86 @@ +const payment = require('../../payment'); +const config = require('config'); +const mongoose = require('mongoose'); +const Order = mongoose.models.Order; +const Transaction = mongoose.models.Transaction; +const TransactionLog = mongoose.models.TransactionLog; +const log = require('javascript-log')(module); +const md5 = require('MD5'); + +log.debugOn(); + +exports.prerequest = function* (next) { + + log.debug("prerequest"); + + var transaction = yield Transaction.findOne({number: this.request.body.LMI_PAYMENT_NO}).exec(); + + if (!transaction) { + log.debug("no transaction " + this.request.body.LMI_PAYMENT_NO); + this.throw(404, 'transaction not found'); + } + + yield new TransactionLog().persist({ + transaction: transaction._id, + event: 'prerequest', + data: JSON.stringify(this.request.body) + }); + + if (transaction.status == Transaction.STATUS_SUCCESS || + transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) || + this.request.body.LMI_PAYEE_PURSE != config.webmoney.purse + ) { + log.debug("no pending transaction " + this.request.body.LMI_PAYMENT_NO); + this.throw(404, 'unfinished transaction with given params not found'); + } + + this.body = 'YES'; + +}; + +exports.post = function* (next) { + + if (this.request.body.LMI_PREREQUEST == '1') { + yield exports.prerequest.call(this, next); + return; + } + + if (!checkSign(this.request.body)) { + log.debug("wrong signature"); + this.throw(403, "wrong signature"); + } + + var transaction = yield Transaction.findOne({number: this.request.body.LMI_PAYMENT_NO}).exec(); + + if (!transaction) { + this.throw(404, 'transaction not found'); + } + + yield new TransactionLog().persist({ + transaction: transaction._id, + event: 'result', + data: JSON.stringify(this.request.body) + }); + + if (transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) || + this.request.body.LMI_PAYEE_PURSE != config.webmoney.purse) { + this.throw(404, 'transaction with given params not found'); + } + + if (!this.request.body.LMI_SIM_MODE || this.request.body.LMI_SIM_MODE == '0') { + transaction.status = Transaction.STATUS_SUCCESS; + yield transaction.persist(); + } + + this.body = 'OK'; + +}; + +function checkSign(body) { + + var signature = md5(body.LMI_PAYEE_PURSE + body.LMI_PAYMENT_AMOUNT + body.LMI_PAYMENT_NO + + body.LMI_MODE + body.LMI_SYS_INVS_NO + body.LMI_SYS_TRANS_NO + body.LMI_SYS_TRANS_DATE + + config.webmoney.secretKey + body.LMI_PAYER_PURSE + body.LMI_PAYER_WM).toUpperCase(); + + return signature == body.LMI_HASH; +} diff --git a/hmvc/webmoney/controller/wait.js b/hmvc/webmoney/controller/wait.js new file mode 100644 index 000000000..eb522bc2d --- /dev/null +++ b/hmvc/webmoney/controller/wait.js @@ -0,0 +1,49 @@ +const payment = require('../../payment'); +const config = require('config'); +const mongoose = require('mongoose'); +const Order = mongoose.models.Order; +const Transaction = mongoose.models.Transaction; +const TransactionLog = mongoose.models.TransactionLog; +const log = require('javascript-log')(module); +const md5 = require('MD5'); + +log.debugOn(); + + + +exports.post = function* (next) { + + var transaction = yield Transaction.findOne({number: this.query.number}).populate('order').exec(); + + if (!transaction) { + this.throw(404, 'transaction not found'); + } + + if (!this.session.orders || this.session.orders.indexOf(transaction.order.number) == -1) { + this.throw(403, 'order not in your session'); + } + + var attempt = 0; + while (!transaction.status) { + attempt++; + if (attempt == 10) { + log.debug("timeout"); + this.body = 'TIMEOUT'; + return; + } + + yield delay(1000); + + transaction = yield Transaction.findOne({number: this.query.number }).exec(); + } + + log.debug('received'); + + this.body = 'RECEIVED'; +}; + +function delay(ms) { + return function(callback) { + setTimeout(callback, ms); + }; +} diff --git a/hmvc/webmoney/index.js b/hmvc/webmoney/index.js new file mode 100644 index 000000000..bd510abd5 --- /dev/null +++ b/hmvc/webmoney/index.js @@ -0,0 +1,19 @@ +const config = require('config'); +const jade = require('jade'); +const path = require('path'); +var mongoose = require('mongoose'); +var Transaction = mongoose.models.Transaction; + +var router = require('./router'); + +exports.middleware = router.middleware(); + +exports.createTransactionForm = function* (transaction) { + + return jade.renderFile(path.join(__dirname, 'template/form.jade'), { + amount: transaction.amount, + number: transaction.number, + purse: config.webmoney.purse + }); + +}; diff --git a/hmvc/webmoney/router.js b/hmvc/webmoney/router.js new file mode 100644 index 000000000..c8b0110fc --- /dev/null +++ b/hmvc/webmoney/router.js @@ -0,0 +1,14 @@ +var Router = require('koa-router'); + + +var router = module.exports = new Router(); + +var result = require('./controller/result'); +var back = require('./controller/back'); +var wait = require('./controller/wait'); + +router.post('/result', result.post); +router.get('/back', back.get); +router.post('/wait', wait.post); + + diff --git a/hmvc/webmoney/template/back.jade b/hmvc/webmoney/template/back.jade new file mode 100644 index 000000000..79b10dc27 --- /dev/null +++ b/hmvc/webmoney/template/back.jade @@ -0,0 +1,20 @@ +div + p Минуточку, ожидаем информацию от сервера.. + +script var number = #{number}, finishPage = '#{finishPage}'; + +script. + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/webmoney/wait?number=' + number); + xhr.timeout = 20000; + function finish() { + location.href = finishPage; + }; + + xhr.onreadystatechange = function() { + if (xhr.readyState != 4) return; + finish(); + }; + + xhr.ontimeout = xhr.onabort = finish; + xhr.send(''); diff --git a/hmvc/webmoney/template/form.jade b/hmvc/webmoney/template/form.jade new file mode 100644 index 000000000..cd6c2e071 --- /dev/null +++ b/hmvc/webmoney/template/form.jade @@ -0,0 +1,7 @@ +form(method="POST",action="https://merchant.webmoney.ru/lmi/payment.asp", class="webmoney-form") + input(type="hidden",name="LMI_PAYMENT_AMOUNT",value=amount) + input(type="hidden",name="LMI_PAYMENT_DESC_BASE64",value=new Buffer('оплата по счету ' + number).toString('base64')) + input(type="hidden",name="LMI_PAYMENT_NO",value=number) + input(type="hidden",name="LMI_PAYEE_PURSE",value=purse) + input(type="hidden",name="LMI_SIM_MODE",value=(isTest ? 1 : 0)) + input(type="submit",value="Оплатить") diff --git a/lib/mongoose.js b/lib/mongoose.js index ac25f55d2..4eeebd0d5 100755 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -12,6 +12,7 @@ var requireTree = require('require-tree'); var path = require('path'); var fs = require('fs'); var log = require('javascript-log')(module); +var autoIncrement = require('mongoose-auto-increment'); //mongoose.set('debug', true); @@ -20,6 +21,8 @@ var _ = require('lodash'); mongoose.connect(config.mongoose.uri, config.mongoose.options); +autoIncrement.initialize(mongoose.connection); + // bind context now for thunkify without bind _.bindAll(mongoose.connection); _.bindAll(mongoose.connection.db); @@ -82,11 +85,10 @@ mongoose.waitConnect = function(callback) { +// models may want lib/mongoose that's why we require them AFTER module.exports = mongoose module.exports = mongoose; -requireTree('../model'); -/* -// models may want this mongoose that's why we require them AFTER module.exports = mongoose +// read ALL models NOW, so that any hmvc app may require a model of another app requireModels(); function requireModels() { @@ -100,4 +102,4 @@ function requireModels() { if (fs.existsSync(modelPath)) requireTree(modelPath); }); } -*/ + diff --git a/model/.gitkeep b/model/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/package.json b/package.json index 507e4cd60..eef6ffad0 100755 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "precommit": "NODE_PATH=. NODE_ENV=development node --harmony `which gulp` pre-commit", "dependencies": { + "MD5": "^1.2.1", "body-parser": "*", "brfs": "^1.1.2", "co": "*", @@ -57,6 +58,7 @@ "markdown-js": "*", "moment": "^2.7.0", "mongoose": "3.8", + "mongoose-auto-increment": "^3.0.8", "mongoose-troop": "git://github.com/iliakan/mongoose-troop", "nib": "^1.0.3", "passport": "*", diff --git a/setup/bodyLogger.js b/setup/bodyLogger.js new file mode 100644 index 000000000..bd60acf5f --- /dev/null +++ b/setup/bodyLogger.js @@ -0,0 +1,10 @@ + +module.exports = function(app) { + + app.use(function*(next) { + if (this.request.body) { + console.log(this.request.body); + } + yield next; + }); +}; diff --git a/setup/csrf.js b/setup/csrf.js new file mode 100644 index 000000000..1d7382ff4 --- /dev/null +++ b/setup/csrf.js @@ -0,0 +1,10 @@ +const csrf = require('koa-csrf'); + +// every request gets different this._csrf to use in POST +// but ALL tokens are valid +module.exports = function(app) { + csrf(app); + +// manual check to skip api calls +// app.use(csrf.middleware); +}; diff --git a/setup/render.js b/setup/render.js index 47c07a58c..5452046db 100644 --- a/setup/render.js +++ b/setup/render.js @@ -3,22 +3,38 @@ const moment = require('moment'); const Parser = require('jade').Parser; const util = require('util'); -const path= require('path'); -const config= require('config'); +const path = require('path'); +const config = require('config'); const fs = require('fs'); const log = require('javascript-log')(module); const jade = require('jade'); const _ = require('lodash'); +const assert = require('assert'); //log.debugOn(); module.exports = function render(app) { app.use(function *(next) { + var ctx = this; - this.locals = { }; - + this.locals = _.assign({}, config.template.options); this.locals.moment = moment; + // warning! + // _.assign does NOT copy defineProperty + Object.defineProperty(this.locals, "csrf", { + get: function() { + var csrf = ctx.csrf; + assert(csrf); + return csrf; + } + }); + + // this.locals.debug causes jade to dump function + this.locals.deb = function() { + debugger; + }; + // render(__dirname, 'article', {}) -- 3 args // render(__dirname, 'article') -- 2 args // render('article', {}) -- 2 args @@ -43,6 +59,7 @@ module.exports = function render(app) { function JadeParser(str, filename, options) { Parser.apply(this, arguments); } + util.inherits(JadeParser, Parser); JadeParser.prototype.resolvePath = function(templatePath, purpose) { @@ -59,20 +76,16 @@ module.exports = function render(app) { return resolvePathUp(templateDir, templatePath + '.jade'); }; - var serviceLocals = { - parser: JadeParser, + var loc = Object.create(this.locals); + + var parseLocals = { + parser: JadeParser, readFile: function(file) { - if (file[0] == '.') { - throw new Error("readFile file must not start with . : bad file " + file); - } - var path = resolvePathUp(templateDir, file); - if (!path) { - throw new Error("Not found " + file + " (from dir " + templateDir + ")"); - } - return fs.readFileSync(path); + return readFile(templateDir, file); } }; - var loc = _.assign(serviceLocals, config.template.options, this.locals, locals); + + _.assign(loc, parseLocals, locals); // console.log(loc); var file = resolvePathUp(templateDir, templatePath + '.jade'); @@ -88,6 +101,18 @@ module.exports = function render(app) { }; + +function readFile(temlpateDir, file) { + if (file[0] == '.') { + throw new Error("readFile file must not start with . : bad file " + file); + } + var path = resolvePathUp(templateDir, file); + if (!path) { + throw new Error("Not found " + file + " (from dir " + templateDir + ")"); + } + return fs.readFileSync(path); +} + function resolvePathUp(templateDir, templateName) { templateDir = path.resolve(templateDir); diff --git a/setup/router.js b/setup/router.js index f0ad84a70..13d0f8d06 100755 --- a/setup/router.js +++ b/setup/router.js @@ -11,6 +11,7 @@ module.exports = function(app) { } app.use(mount('/getpdf', app.hmvc.getpdf.middleware)); + app.use(mount('/webmoney', app.hmvc.webmoney.middleware)); // stick to bottom app.use(mount('/', app.hmvc.tutorial.middleware)); diff --git a/test/.jshintrc b/test/.jshintrc new file mode 100644 index 000000000..077663629 --- /dev/null +++ b/test/.jshintrc @@ -0,0 +1,23 @@ +{ + "maxerr": 25, + "latedef": "nofunc", + "browser": false, + "node": true, + "indent": 2, + "camelcase": true, + "newcap": true, + "undef": true, + "esnext": true, + "multistr": true, + "noyield": true, + "expr": true, + "-W004": true, + "globals" : { + "describe" : false, + "it" : false, + "before" : false, + "beforeEach" : false, + "after" : false, + "afterEach" : false + } +} From 1caefe4597f99adcc20846cca42d471f03e5bcf7 Mon Sep 17 00:00:00 2001 From: Anton Vernigor Date: Sun, 20 Jul 2014 10:51:15 +0400 Subject: [PATCH 078/130] moving markup to jade and stylus, broken rendering --- app/stylesheets/base.styl | 2 +- app/stylesheets/blocks/balance/balance.styl | 149 ++++++++++++++++++++ hmvc/markup/template/pages/article.html | 23 +++ 3 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 app/stylesheets/blocks/balance/balance.styl diff --git a/app/stylesheets/base.styl b/app/stylesheets/base.styl index 6179c3e44..efc12f6e3 100755 --- a/app/stylesheets/base.styl +++ b/app/stylesheets/base.styl @@ -32,7 +32,7 @@ @require "blocks/result/result" @require "blocks/code-example/code-example" // @require "blocks/complex-code/complex-code" -// @require "blocks/balance/balance" +@require "blocks/balance/balance" // @require "blocks/rating/rating" // @require "blocks/comments/comments" // @require "blocks/submit-button/submit-button" diff --git a/app/stylesheets/blocks/balance/balance.styl b/app/stylesheets/blocks/balance/balance.styl new file mode 100644 index 000000000..3829a68bf --- /dev/null +++ b/app/stylesheets/blocks/balance/balance.styl @@ -0,0 +1,149 @@ + +.balance + position relative + margin 38px -content_horizontal_padding + + &__content + display: table; + width: 100%; + pre + margin: lineheight 0 + code + color #400000 + + &__title + margin 0 0 20px + text-align center + + &__list + padding-left 25px + + // convert to class .balance__list-item + &__list > li + margin 1em 0 1em + + &__list > li::before + position absolute + margin-left -25px + + &__pluses, + &__minuses + display table-cell + width 50% + + &__pluses + padding 20px 35px 20px 40px + background: #D7F1B9; + ul.balance__list > li::before + @extend $font-pros + color #238C00 + + &__pluses &__title + color #004010 + + &__minuses + padding 20px 40px 20px 35px + background #FFCFBF + + ul.balance__list > li::before + @extend $font-cons; + color #B20000 + + & &__title + color #401000 + + // .balance_single { + // margin: 20px -#{$content-horizontal-padding} 30px; + // .balance__content { + // width: 100%; + // } + // .balance__title { + // text-align: left; + // } + + // .balance__pluses, + // .balance__minuses { + // padding: 20px $content-horizontal-padding; + // width: auto; + // } + // } + + // .balance::before { + // @include size-of('scales.png'); + // background: image-url('scales.png'); + // margin-left: image-width('scales.png') / -2; + // content: ""; + // position: absolute; + // top: -3px; + // left: 50%; + // } + + // .balance_single::before { + // display: none; + // } + + // @media (min-width: $media-step-1) { + // .balance { + // margin: 38px -#{$content-horizontal-padding + 10px}; + + // .balance__pluses { + // padding: 20px 35px 20px ($content-horizontal-padding + 10px); + // } + + // .balance__minuses { + // padding: 20px ($content-horizontal-padding + 10px) 20px 35px; + // } + // } + // .balance_single { + // margin: 20px -#{$content-horizontal-padding + 10px} 30px; + + // .balance__pluses, + // .balance__minuses { + // padding: 20px ($content-horizontal-padding + 10px); + // } + // } + // } + + // @media (min-width: $media-step-2) { + // .balance { + // margin: 38px -#{$content-horizontal-padding + 20px}; + + // .balance__pluses { + // padding: 20px 35px 20px ($content-horizontal-padding + 20px); + // } + + // .balance__minuses { + // padding: 20px ($content-horizontal-padding + 20px) 20px 35px; + // } + // } + // .balance_single { + // margin: 20px -#{$content-horizontal-padding + 20px} 30px; + + // .balance__pluses, + // .balance__minuses { + // padding: 20px ($content-horizontal-padding + 20px); + // } + // } + // } + + // @media (min-width: $media-step-3) { + // .balance { + // margin: 38px -#{$content-horizontal-padding + 30px}; + + // .balance__pluses { + // padding: 20px 35px 20px ($content-horizontal-padding + 30px); + // } + + // .balance__minuses { + // padding: 20px ($content-horizontal-padding + 30px) 20px 35px; + // } + // } + // .balance_single { + // margin: 20px -#{$content-horizontal-padding + 30px} 30px; + + // .balance__pluses, + // .balance__minuses { + // padding: 20px ($content-horizontal-padding + 30px); + // } + // } + // } \ No newline at end of file diff --git a/hmvc/markup/template/pages/article.html b/hmvc/markup/template/pages/article.html index 843ac9021..4998b03a5 100644 --- a/hmvc/markup/template/pages/article.html +++ b/hmvc/markup/template/pages/article.html @@ -300,3 +300,26 @@

        В чем уникальность JavaScript?

        +

        Есть как минимум три замечательных особенности JavaScript:

        +
        +
        +
        + +
          +
        • Полная интеграция с HTML/CSS. function f (x, y, z) { return x + y + z; }

        • +
        • + Простые вещи делаются просто. +

          function sayHi(name) {
          +var phrase = "Привет, " + name;
          +alert(phrase);
          +}
          +
          +sayHi('Вася');
          +

        • +
        • Поддерживается всеми распространенными браузерами и включен по умолчанию.

        • +
        +
        +
        +
        From 8d2c1309593f3286eb72f6ff37d89960b70ba7e2 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 20 Jul 2014 10:53:39 +0400 Subject: [PATCH 079/130] merge & typo fix --- hmvc/frontpage/index.js | 2 ++ hmvc/getpdf/controller/order.js | 19 +++++-------------- hmvc/webmoney/index.js | 2 +- setup/render.js | 2 +- setup/router.js | 3 +++ 5 files changed, 12 insertions(+), 16 deletions(-) diff --git a/hmvc/frontpage/index.js b/hmvc/frontpage/index.js index cdfa18f19..b0be3b151 100644 --- a/hmvc/frontpage/index.js +++ b/hmvc/frontpage/index.js @@ -2,3 +2,5 @@ var router = require('./router'); exports.middleware = router.middleware(); + +exports.root = '/'; diff --git a/hmvc/getpdf/controller/order.js b/hmvc/getpdf/controller/order.js index f6b6bd932..bb15f904d 100644 --- a/hmvc/getpdf/controller/order.js +++ b/hmvc/getpdf/controller/order.js @@ -1,11 +1,11 @@ var mongoose = require('mongoose'); var Order = mongoose.models.Order; -var Transaction = mongoose.models.Transaction; var methods = require('../paymentMethods').methods; exports.post = function*(next) { + this.assertCSRF(this.request.body); var method = methods[this.request.body.paymentMethod]; if (!method) { @@ -21,21 +21,12 @@ exports.post = function*(next) { }); yield order.persist(); - - var transaction = new Transaction({ - order: order._id, - amount: order.amount, - paymentType: 'webmoney' - }); - - yield transaction.persist(); - - if (!this.session.transactions) { - this.session.transactions = []; + if (!this.session.orders) { + this.session.orders = []; } - this.session.transactions.push(transaction.number); + this.session.orders.push(order.number); - var form = yield methodApi.createTransactionForm(transaction); + var form = yield methodApi.createTransactionForm(order); this.body = form; diff --git a/hmvc/webmoney/index.js b/hmvc/webmoney/index.js index bd510abd5..92aeaff0a 100644 --- a/hmvc/webmoney/index.js +++ b/hmvc/webmoney/index.js @@ -8,7 +8,7 @@ var router = require('./router'); exports.middleware = router.middleware(); -exports.createTransactionForm = function* (transaction) { +exports.createTransactionForm = function(transaction) { return jade.renderFile(path.join(__dirname, 'template/form.jade'), { amount: transaction.amount, diff --git a/setup/render.js b/setup/render.js index 5452046db..8041a23f4 100644 --- a/setup/render.js +++ b/setup/render.js @@ -102,7 +102,7 @@ module.exports = function render(app) { }; -function readFile(temlpateDir, file) { +function readFile(templateDir, file) { if (file[0] == '.') { throw new Error("readFile file must not start with . : bad file " + file); } diff --git a/setup/router.js b/setup/router.js index 13d0f8d06..2a8ac1e78 100755 --- a/setup/router.js +++ b/setup/router.js @@ -4,6 +4,9 @@ var mount = require('koa-mount'); module.exports = function(app) { + for (var hmvc in app.hmvc) { + + } app.use(mount('/', app.hmvc.frontpage.middleware)); if (process.env.NODE_ENV == 'development') { From 75ca73511579eff5f4350af7b68e33cc8f96ece2 Mon Sep 17 00:00:00 2001 From: Anton Vernigor Date: Sun, 20 Jul 2014 14:13:34 +0400 Subject: [PATCH 080/130] moving markup to jade and stylus --- app/stylesheets/base.styl | 6 +- app/stylesheets/blocks/balance/balance.styl | 190 ++++++++---------- app/stylesheets/blocks/balance/scales.png | Bin 0 -> 833 bytes app/stylesheets/blocks/hide/hide.styl | 29 +++ .../blocks/important/important.styl | 12 ++ app/stylesheets/blocks/mixins/imagesize.styl | 6 +- .../blocks/nav-dropdown/nav-dropdown.styl | 5 +- app/stylesheets/blocks/navbar/navbar.styl | 6 +- app/stylesheets/blocks/spoiler/spoiler.styl | 38 ++++ app/stylesheets/blocks/summary/summary.styl | 15 ++ app/stylesheets/sprite/facebook.styl | 6 +- app/stylesheets/sprite/home.styl | 6 +- hmvc/markup/template/pages/article.html | 187 +++++++++++++++-- 13 files changed, 363 insertions(+), 143 deletions(-) create mode 100644 app/stylesheets/blocks/balance/scales.png create mode 100644 app/stylesheets/blocks/hide/hide.styl create mode 100644 app/stylesheets/blocks/spoiler/spoiler.styl create mode 100644 app/stylesheets/blocks/summary/summary.styl diff --git a/app/stylesheets/base.styl b/app/stylesheets/base.styl index efc12f6e3..8f7321db8 100755 --- a/app/stylesheets/base.styl +++ b/app/stylesheets/base.styl @@ -21,9 +21,9 @@ @require "blocks/breadcrumbs/breadcrumbs" @require "blocks/important/important" -// @require "blocks/summary/summary" +@require "blocks/summary/summary" @require "blocks/shortcut/shortcut" -// @require "blocks/spoiler/spoiler" +@require "blocks/spoiler/spoiler" @require "blocks/dropdown/dropdown" @@ -110,7 +110,7 @@ // @require "blocks/ul/ul" // @require "blocks/course-start-notify/course-start-notify" // @require "blocks/course-start-subscribe/course-start-subscribe" -// @require "blocks/hide/hide" +@require "blocks/hide/hide" // @require "blocks/error/error" // @require "blocks/redirect/redirect" // @require "blocks/search-query/search-query" diff --git a/app/stylesheets/blocks/balance/balance.styl b/app/stylesheets/blocks/balance/balance.styl index 3829a68bf..ea9b0cd7a 100644 --- a/app/stylesheets/blocks/balance/balance.styl +++ b/app/stylesheets/blocks/balance/balance.styl @@ -1,28 +1,24 @@ .balance position relative - margin 38px -content_horizontal_padding + margin 38px -1*content_horizontal_padding &__content display: table; width: 100%; - pre - margin: lineheight 0 - code - color #400000 &__title margin 0 0 20px text-align center - &__list + & &__list padding-left 25px // convert to class .balance__list-item - &__list > li + & &__list > li margin 1em 0 1em - &__list > li::before + & &__list > li::before position absolute margin-left -25px @@ -34,116 +30,96 @@ &__pluses padding 20px 35px 20px 40px background: #D7F1B9; - ul.balance__list > li::before - @extend $font-pros - color #238C00 &__pluses &__title color #004010 + & &__pluses ul&__list > li::before + @extend $font-pros + color #238C00 + &__minuses padding 20px 40px 20px 35px background #FFCFBF - ul.balance__list > li::before + &__minuses &__title + color #401000 + + & &__minuses ul&__list > li::before @extend $font-cons; color #B20000 - & &__title - color #401000 + &_single + margin 20px -1*content_horizontal_padding 30px - // .balance_single { - // margin: 20px -#{$content-horizontal-padding} 30px; - // .balance__content { - // width: 100%; - // } - // .balance__title { - // text-align: left; - // } - - // .balance__pluses, - // .balance__minuses { - // padding: 20px $content-horizontal-padding; - // width: auto; - // } - // } - - // .balance::before { - // @include size-of('scales.png'); - // background: image-url('scales.png'); - // margin-left: image-width('scales.png') / -2; - // content: ""; - // position: absolute; - // top: -3px; - // left: 50%; - // } - - // .balance_single::before { - // display: none; - // } - - // @media (min-width: $media-step-1) { - // .balance { - // margin: 38px -#{$content-horizontal-padding + 10px}; - - // .balance__pluses { - // padding: 20px 35px 20px ($content-horizontal-padding + 10px); - // } - - // .balance__minuses { - // padding: 20px ($content-horizontal-padding + 10px) 20px 35px; - // } - // } - // .balance_single { - // margin: 20px -#{$content-horizontal-padding + 10px} 30px; - - // .balance__pluses, - // .balance__minuses { - // padding: 20px ($content-horizontal-padding + 10px); - // } - // } - // } - - // @media (min-width: $media-step-2) { - // .balance { - // margin: 38px -#{$content-horizontal-padding + 20px}; - - // .balance__pluses { - // padding: 20px 35px 20px ($content-horizontal-padding + 20px); - // } - - // .balance__minuses { - // padding: 20px ($content-horizontal-padding + 20px) 20px 35px; - // } - // } - // .balance_single { - // margin: 20px -#{$content-horizontal-padding + 20px} 30px; + &_single &__content + width 100% + + &_single &__title + text-align left + + &_single &__pluses, + &_single &__minuses + padding 20px content_horizontal_padding + width auto + + &::before + size-of ('scales.png') + background url('/img/scales.png') + margin-left (image-width('scales.png') / -2) + content "" + position absolute + top -3px + left 50% + + &_single::before + display none + + @media (min-width: media_step_1) + margin: 38px -1*(content_horizontal_padding + 10px) + + &__pluses + padding 20px 35px 20px (content_horizontal_padding + 10px) + + &__minuses + padding 20px (content_horizontal_padding + 10px) 20px 35px + + &_single + margin 20px -1*(content_horizontal_padding + 10px) 30px + + &_single &__pluses, + &_single &__minuses + padding 20px (content_horizontal_padding + 10px) + + @media (min-width: media_step_2) + margin 38px -1*(content_horizontal_padding + 20px) + + &__pluses + padding 20px 35px 20px (content_horizontal_padding + 20px) + + &__minuses + padding 20px (content_horizontal_padding + 20px) 20px 35px + + &_single + margin 20px -1*(content_horizontal_padding + 20px) 30px - // .balance__pluses, - // .balance__minuses { - // padding: 20px ($content-horizontal-padding + 20px); - // } - // } - // } - - // @media (min-width: $media-step-3) { - // .balance { - // margin: 38px -#{$content-horizontal-padding + 30px}; - - // .balance__pluses { - // padding: 20px 35px 20px ($content-horizontal-padding + 30px); - // } - - // .balance__minuses { - // padding: 20px ($content-horizontal-padding + 30px) 20px 35px; - // } - // } - // .balance_single { - // margin: 20px -#{$content-horizontal-padding + 30px} 30px; + &_single &__pluses, + &_single &__minuses + padding: 20px (content_horizontal_padding + 20px) + + + @media (min-width: media_step_3) + margin 38px -1*(content_horizontal_padding + 30px) + + &__pluses + padding 20px 35px 20px (content_horizontal_padding + 30px) + + &__minuses + padding 20px (content_horizontal_padding + 30px) 20px 35px + + &_single + margin 20px -1*(content_horizontal_padding + 30px) 30px - // .balance__pluses, - // .balance__minuses { - // padding: 20px ($content-horizontal-padding + 30px); - // } - // } - // } \ No newline at end of file + &_single &__pluses, + &_single &__minuses + padding: 20px (content_horizontal_padding + 30px) diff --git a/app/stylesheets/blocks/balance/scales.png b/app/stylesheets/blocks/balance/scales.png new file mode 100644 index 0000000000000000000000000000000000000000..9e774bf4b84e63c6a0aa3d584f1b981ea44a9c71 GIT binary patch literal 833 zcmV-H1HSx;P)dbVG7wVRUJ4ZXi@?ZDjy7FEKeU zF*7Y%0GI#(0338hSaefwW^{L9a%BKPWN%_+AVz6&Wp{6KYjYq&Q#Rmrq9U%sbSFzYU<$5zJIaFjF1D zOjR1#ZIW=CBxTw0u`DCYw#%{t{)Z^b)`@PfyiJnQV_KytUmWM)9PJvL1p$?I8-V%a zBP+!Mzqq+s&mWJa*Dc54{Q5fc)JY3A^}GyhG8i!RyjOEBJDm({>UqjJjr;p-j!xP& zR&ibFc>kfYv8hjSuyz|uvNje`BpnM`j+0fr6tIfxV%avUG-Z{hO4FCo$YK)r2p`L~ zwQyRnWgIgPL*`+~GLCco+cJE#U{AiEoj>`$GES95jIVr(f{7SqF_~0N%-#3V#d#1+ zlwYVwoJD;Og1s!ikV%}f7~p9|f3nue5BNdbdlx+uiy7^Yd;aVmS`0BvFC^xqM`jR_w>!o%Y7ba46k= zgcec6WH{9NsGcRQ*!O;)UqL{<*TZf!IIUIzSjI7F81mEib9D&cdOhZ0NX2!fK9j*f z*#arso>>s&>YH>@wLv=lyREfKmz@q5%_e8HTGk{A&BkLEQN%0=3RSBR*mP06Go*G)YwM8(>n>U2Q~}11?7DcH_!+iwvg@LZN63+`O28T00000 LNkvXXu0mjfWN2}o literal 0 HcmV?d00001 diff --git a/app/stylesheets/blocks/hide/hide.styl b/app/stylesheets/blocks/hide/hide.styl new file mode 100644 index 000000000..40734c21d --- /dev/null +++ b/app/stylesheets/blocks/hide/hide.styl @@ -0,0 +1,29 @@ +.hide-open, +.hide-closed + margin round(lineheight*.65) 0 + +.hide-closed .hide-content + display none + +.hide-open .hide-link, +.hide-closed .hide-link, +.hide-open .hide-link u, +.hide-closed .hide-link u + text-decoration none + color $link-color + +.hide-link:hover u + border-bottom 1px dashed + +.hide-link code + font inherit + color inherit + +.hide-link::after + margin-left 8px + +.hide-closed .hide-link::after + @extend $font-open + +.hide-open .hide-link::after + @extend $font-close \ No newline at end of file diff --git a/app/stylesheets/blocks/important/important.styl b/app/stylesheets/blocks/important/important.styl index 1e02b6168..4553e12da 100644 --- a/app/stylesheets/blocks/important/important.styl +++ b/app/stylesheets/blocks/important/important.styl @@ -57,6 +57,18 @@ &__content margin 12px 25px 22px + & > .spoiler + border 0 + background none + margin 15px 0 + padding 10px 20px 10px 0 + + & > .spoiler + .spoiler + margin-top -15px + + & .spoiler__button + color #747361 + &_warn &__type::before spriteElem $important-warning diff --git a/app/stylesheets/blocks/mixins/imagesize.styl b/app/stylesheets/blocks/mixins/imagesize.styl index ea1e28d70..5a9b77d90 100644 --- a/app/stylesheets/blocks/mixins/imagesize.styl +++ b/app/stylesheets/blocks/mixins/imagesize.styl @@ -2,4 +2,8 @@ image-width(img) return image-size(img)[0] image-height(img) - return image-size(img)[1] \ No newline at end of file + return image-size(img)[1] + +size-of(img) + width image-width(img) + height image-height(img) \ No newline at end of file diff --git a/app/stylesheets/blocks/nav-dropdown/nav-dropdown.styl b/app/stylesheets/blocks/nav-dropdown/nav-dropdown.styl index 903f485a9..6a97ae5bf 100644 --- a/app/stylesheets/blocks/nav-dropdown/nav-dropdown.styl +++ b/app/stylesheets/blocks/nav-dropdown/nav-dropdown.styl @@ -1,16 +1,13 @@ .dropdown_nav position relative -.dropdown_nav.open - background #373E3F - .dropdown__toggle_nav::after @extend $font-open margin-left 5px color #b0b2b3 line-height 0 -.navbar__sections-item_open .dropdown__toggle_nav::after, +.navbar__sections-item.open .dropdown__toggle_nav::after, .navbar__sections-item_active .dropdown__toggle_nav::after color secondary_color diff --git a/app/stylesheets/blocks/navbar/navbar.styl b/app/stylesheets/blocks/navbar/navbar.styl index c7c821530..e28a1f7b1 100644 --- a/app/stylesheets/blocks/navbar/navbar.styl +++ b/app/stylesheets/blocks/navbar/navbar.styl @@ -28,7 +28,8 @@ .navbar__sections-item_active .navbar__sections-link:link, .navbar__sections-item_active .navbar__sections-link:visited, .navbar__sections-item_active .navbar__sections-link:hover, -.navbar__sections-item_active .navbar__sections-link:active +.navbar__sections-item_active .navbar__sections-link:active, +.navbar__sections-item.open .navbar__sections-link background none color secondary_color @@ -52,7 +53,8 @@ line-height 0 color #caf3ff -.navbar__sections-item_active .navbar__icon +.navbar__sections-item_active .navbar__icon, +.navbar__sections-item.open .navbar__icon color secondary_color .navbar__sections-link::before diff --git a/app/stylesheets/blocks/spoiler/spoiler.styl b/app/stylesheets/blocks/spoiler/spoiler.styl new file mode 100644 index 000000000..9564b427e --- /dev/null +++ b/app/stylesheets/blocks/spoiler/spoiler.styl @@ -0,0 +1,38 @@ +.spoiler + background background_blocks + border-top 1px solid #D9D7D6 + margin 20px 0 + padding 15px 20px 20px + + &__button + @extend $button-reset + color link_color + position relative + font size/lineheight font + padding 1px 1px 1px 0 + text-decoration none + outline none + u + text-decoration none + + + &__button:hover + color link_hover_color + u + @extend $pseudo + + &__button::after + margin-left 1ex + @extend $font-close + + &.closed &__button::after + @extend $font-open + + &__content + margin-top 20px + + & + & + margin-top -20px + + &.closed &__content + display none diff --git a/app/stylesheets/blocks/summary/summary.styl b/app/stylesheets/blocks/summary/summary.styl new file mode 100644 index 000000000..35ea3c092 --- /dev/null +++ b/app/stylesheets/blocks/summary/summary.styl @@ -0,0 +1,15 @@ +.summary + background background_blocks + margin round(lineheight*.65) 0 + border 1px solid separator_color + + &__content + margin 22px + + &_noborder + border 0 + padding 1px 0 + border-radius 4px + + @media print + page-break-inside: avoid; diff --git a/app/stylesheets/sprite/facebook.styl b/app/stylesheets/sprite/facebook.styl index 7512aa58c..8de080996 100644 --- a/app/stylesheets/sprite/facebook.styl +++ b/app/stylesheets/sprite/facebook.styl @@ -1,4 +1,2 @@ -$facebook-facebook = 0px 0px 0px 0px 16px 16px 16px 32px '/img/facebook.png?time=1405761478508'; -$facebook-facebook_hover = 0px 16px 0px -16px 16px 16px 16px 32px '/img/facebook.png?time=1405761478508'; - -$facebook = 16px 32px '/img/facebook.png?time=1405761478508' \ No newline at end of file +$facebook-facebook = 0px 0px 0px 0px 16px 16px 16px 32px '/img/facebook.png?t=1405847881116'; +$facebook-facebook_hover = 0px 16px 0px -16px 16px 16px 16px 32px '/img/facebook.png?t=1405847881116'; diff --git a/app/stylesheets/sprite/home.styl b/app/stylesheets/sprite/home.styl index 287607fba..535fea885 100644 --- a/app/stylesheets/sprite/home.styl +++ b/app/stylesheets/sprite/home.styl @@ -1,4 +1,2 @@ -$home-home = 0px 0px 0px 0px 16px 16px 16px 32px '/img/home.png?time=1405761478581'; -$home-home_hover = 0px 16px 0px -16px 16px 16px 16px 32px '/img/home.png?time=1405761478581'; - -$home = 16px 32px '/img/home.png?time=1405761478581' \ No newline at end of file +$home-home = 0px 0px 0px 0px 16px 16px 16px 32px '/img/home.png?t=1405847881133'; +$home-home_hover = 0px 16px 0px -16px 16px 16px 16px 32px '/img/home.png?t=1405847881133'; diff --git a/hmvc/markup/template/pages/article.html b/hmvc/markup/template/pages/article.html index 4998b03a5..95dc6107e 100644 --- a/hmvc/markup/template/pages/article.html +++ b/hmvc/markup/template/pages/article.html @@ -236,10 +236,7 @@

        - -
        @@ -272,11 +269,8 @@

        - - +
        + +
        +

        К одной задаче могут быть добавлены одно или несколько решений. Решения, идущие подряд, «стыкуются» без промежутков

        +
        +
        +

        Этих ограничений нет там, где JavaScript используется вне браузера, например на сервере.

        Большинство возможностей JavaScript в браузере ограничено текущим окном и страницей.

        @@ -309,17 +347,130 @@

        Плюсы

        -->
        • Полная интеграция с HTML/CSS. function f (x, y, z) { return x + y + z; }

        • -
        • - Простые вещи делаются просто. -

          function sayHi(name) {
          -var phrase = "Привет, " + name;
          -alert(phrase);
          -}
          -
          -sayHi('Вася');
          -

        • +
        • Простые вещи делаются просто. +

          function sayHi(name) {
          +  var phrase = "Привет, " + name;
          +  alert(phrase);
          +}
          +
          +sayHi('Вася');

        • +
        • Поддерживается всеми распространенными браузерами и включен по умолчанию.

        • +
        +

        +

        + + +

        Вариант блока с достоинствами и недостатками

        + +
        +
        +
        +

        Достоинства

        +
          +
        • Полная интеграция с HTML/CSS.

        • +
        • Простые вещи делаются просто.

        • +
        • Поддерживается всеми распространенными браузерами и включен по умолчанию.

        • +
        +
        +
        +

        Недостатки

        +
          +
        • Полная интеграция с HTML/CSS.

        • +
        • Простые вещи делаются просто.

        • Поддерживается всеми распространенными браузерами и включен по умолчанию.

        + +

        Этих трех вещей одновременно нет больше ни в одной браузерной технологии. Поэтому JavaScript и является самым распространенным средством создания браузерных интерфейсов.

        +

        Пример блока "balance" с узким контентом, который может вызвать ошибку

        + +
        +
        +
        +

        Достоинства

        +
          +
        • +

          Простота реализации.

          +
        • +
        +
        +
        +

        Недостатки

        +
          +
        • +

          Задержки между событием и уведомлением.

          +
        • +
        • +

          Лишний трафик и запросы на сервер.

          +
        • +
        +
        +
        +
        + +
        + Показать простой вариант compareNumeric +
        +

        Функция должна возвращать положительное число, если a > b, отрицательное, если наоборот, и, например, 0, если числа равны.

        +

        Всем этим требованиям удовлетворяет функция.

        +

        А примера кода не будет.

        +
        +
        + +

        Тенденции развития.

        +

        Перед тем, как вы планируете изучить новую технологию, полезно ознакомиться с ее развитием и перспективами. Здесь в JavaScript все более чем хорошо.

        + +

        HTML 5

        +

        HTML 5 — эволюция стандарта HTML, добавляющая новые теги и, что более важно, ряд новых возможностей браузерам.

        +
        + + +
        +

        Здесь может быть что угодно

        +
        +
        +

        Вот несколько примеров:

        +
          +
        • Чтение/запись файлов на диск (в специальной «песочнице», то есть не любые).
        • +
        • Встроенная в браузер база данных, которая позволяет хранить данные на компьютере пользователя.
        • +
        • Многозадачность с одновременным использованием нескольких ядер процессора.
        • +
        • Проигрывание видео/аудио, без Flash.
        • +
        • 2d и 3d-рисование с аппаратной поддержкой, как в современных играх.
        • +
        +

        Многие возможности HTML5 все еще в разработке, но браузеры постепенно начинают их поддерживать.

        +
        +

        Тенденция: JavaScript становится все более и более мощным и возможности браузера растут в сторону десктопных приложений.

        +
        + +

        EcmaScript

        +

        Сам язык JavaScript улучшается. Современный стандарт EcmaScript 5 включает в себя новые возможности для разработки.

        +

        Современные браузеры улучшают свои движки, чтобы увеличить скорость исполнения JavaScript, исправляют баги и стараются следовать стандартам.

        +
        +
        +

        Тенденция: JavaScript становится все быстрее и стабильнее.

        +
        +
        +

        Очень важно то, что новые стандарты HTML5 и ECMAScript сохраняют максимальную совместимость с предыдущими версиями. Это позволяет избежать неприятностей с уже существующими приложениями.

        +

        Впрочем, небольшая проблема с HTML5 все же есть. Иногда браузеры стараются включить новые возможности, которые еще не полностью описаны в стандарте, но настолько интересны, что разработчики просто не могут ждать.

        +

        …Однако, со временем стандарт меняется и браузерам приходится подстраиваться к нему, что может привести к ошибкам в уже написанном (старом) коде. Поэтому следует дважды подумать перед тем, как применять на практике такие «супер-новые» решения.

        +

        При этом все браузеры сходятся к стандарту, и различий между ними уже гораздо меньше, чем всего лишь несколько лет назад.

        +
        +
        +

        Тенденция: все идет к полной совместимости со стандартом.

        +
        +
        + +

        Недостатки JavaScript

        +

        Зачастую, недостатки подходов и технологий — это обратная сторона их полезности. Стоит ли упрекать молоток в том, что он — тяжелый? Да, неудобно, зато гвозди забиваются лучше.

        + +

        Заголовок третьего уровня

        +

        В JavaScript, однако, есть вполне объективные недоработки, связанные с тем, что язык, по выражению его автора (Brendan Eich) делался «за 10 бессонных дней и ночей». Поэтому некоторые моменты продуманы плохо, есть и откровенные ошибки (которые признает тот же Brendan).

        +

        Конкретные примеры мы увидим в дальнейшем, т.к. их удобнее обсуждать в процессе освоения языка.

        + +

        Заголовок четвертого уровня

        +

        Пока что нам важно знать, что некоторые «странности» языка не являются чем-то очень умным, а просто не были достаточно хорошо продуманы в своё время. В этом учебнике мы будем обращать особое внимание на основные недоработки и «грабли». Ничего критичного в них нет, если знаешь — не наступишь.

        +

        В новых версиях JavaScript (ECMAScript) эти недостатки постепенно убирают. Процесс внедрения небыстрый, в первую очередь из-за старых версий IE, но они постепенно вымирают. Современный IE в этом отношении несравнимо лучше.

        \ No newline at end of file From 48d548c275f478ce7107aa6e291ec6c620c66712 Mon Sep 17 00:00:00 2001 From: Anton Vernigor Date: Mon, 21 Jul 2014 00:40:43 +0400 Subject: [PATCH 081/130] moving markup to jade and stylus --- app/img/facebook.png | Bin 241 -> 0 bytes app/img/home.png | 0 app/stylesheets/base.styl | 4 +- app/stylesheets/blocks/main/main.styl | 12 +-- app/stylesheets/blocks/mixins/sprite.styl | 5 +- app/stylesheets/blocks/soc-icon/soc-icon.styl | 14 ++++ .../soc-icon/soc_icon.sprite/facebook.png | Bin 0 -> 1263 bytes .../soc-icon/soc_icon.sprite/google.png | Bin 0 -> 1624 bytes .../soc-icon/soc_icon.sprite/twitter.png | Bin 0 -> 1036 bytes .../blocks/soc-icon/soc_icon.sprite/vk.png | Bin 0 -> 1255 bytes app/stylesheets/blocks/social/social.styl | 74 ++++++++++++++++++ app/stylesheets/sprite/facebook.styl | 6 +- app/stylesheets/sprite/home.styl | 6 +- hmvc/markup/template/blocks/article-foot.jade | 7 ++ hmvc/markup/template/blocks/social-aside.jade | 8 +- .../markup/template/blocks/social-inline.jade | 2 + hmvc/markup/template/blocks/social.jade | 8 ++ hmvc/markup/template/layouts/base.jade | 1 + hmvc/markup/template/pages/article.html | 2 +- hmvc/markup/template/pages/article.jade | 1 - 20 files changed, 131 insertions(+), 19 deletions(-) delete mode 100644 app/img/facebook.png delete mode 100644 app/img/home.png create mode 100644 app/stylesheets/blocks/soc-icon/soc-icon.styl create mode 100644 app/stylesheets/blocks/soc-icon/soc_icon.sprite/facebook.png create mode 100644 app/stylesheets/blocks/soc-icon/soc_icon.sprite/google.png create mode 100644 app/stylesheets/blocks/soc-icon/soc_icon.sprite/twitter.png create mode 100644 app/stylesheets/blocks/soc-icon/soc_icon.sprite/vk.png create mode 100644 app/stylesheets/blocks/social/social.styl create mode 100644 hmvc/markup/template/blocks/article-foot.jade create mode 100644 hmvc/markup/template/blocks/social-inline.jade create mode 100644 hmvc/markup/template/blocks/social.jade diff --git a/app/img/facebook.png b/app/img/facebook.png deleted file mode 100644 index 7905c329ada7c1462d1a0b38f233acc553a3db08..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 241 zcmeAS@N?(olHy`uVBq!ia0vp^0zj<5!3HFyJAa%3QcRvMjv*C{Z=~uj0eP!Hyrl;> z<{mZ>U^wub{q&TqJ>6y>m~QH{O6N~xR0_YT)NQ~PBVdpssP_KRJ!edzgWcfv=fSJssr)|WP}T3NQl^JPTFp*|B$ npy`JiO;%>CV(dJ-s~uNb@B*Td{US2K9J`njxgN@xNAX4YaM diff --git a/app/img/home.png b/app/img/home.png deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/stylesheets/base.styl b/app/stylesheets/base.styl index 8f7321db8..6abe8fb86 100755 --- a/app/stylesheets/base.styl +++ b/app/stylesheets/base.styl @@ -38,8 +38,8 @@ // @require "blocks/submit-button/submit-button" // @require "blocks/secondary-button/secondary-button" // @require "blocks/page-footer/page-footer" -// @require "blocks/soc-icon/soc-icon" -// @require "blocks/social/social" +@require "blocks/soc-icon/soc-icon" +@require "blocks/social/social" // @require "blocks/user/user" // @require "blocks/user-menu/user-menu" // @require "blocks/primary-tabs/primary-tabs" diff --git a/app/stylesheets/blocks/main/main.styl b/app/stylesheets/blocks/main/main.styl index 588820ece..64581d62d 100644 --- a/app/stylesheets/blocks/main/main.styl +++ b/app/stylesheets/blocks/main/main.styl @@ -257,27 +257,27 @@ $main-loud @extend $font-star @media (min-width: media_step_1) - padding 0 content_horizontal_padding + 10px 35px + padding 0 (content_horizontal_padding + 10px) 35px line-height 19px &__footer - margin 30px -content_horizontal_padding + 10px 27px + margin 30px -1*(content_horizontal_padding + 10px) 27px padding 16px (content_horizontal_padding + 10px) 9px @media (min-width: media_step_2) - padding 0 content_horizontal_padding + 20px 35px + padding 0 (content_horizontal_padding + 20px) 35px line-height 20px &__footer - margin 30px -content_horizontal_padding + 20px 27px + margin 30px -1*(content_horizontal_padding + 20px) 27px padding 16px (content_horizontal_padding + 20px) 9px @media (min-width: media_step_3) - padding 0 content_horizontal_padding + 30px 35px + padding 0 (content_horizontal_padding + 30px) 35px line-height 22px &__footer - margin 30px -content_horizontal_padding + 30px 27px + margin 30px -1*(content_horizontal_padding + 30px) 27px padding 16px (content_horizontal_padding + 30px) 9px @media (min-width: largescreen) diff --git a/app/stylesheets/blocks/mixins/sprite.styl b/app/stylesheets/blocks/mixins/sprite.styl index dbe7b4666..13ead9f1a 100644 --- a/app/stylesheets/blocks/mixins/sprite.styl +++ b/app/stylesheets/blocks/mixins/sprite.styl @@ -8,7 +8,10 @@ spritePosition($sprite) background-position $sprite[2] $sprite[3] spriteImage($sprite) - background-image url($sprite[8]) + if length($sprite) == 3 + background-image url($sprite[2]) + else + background-image url($sprite[8]) sprite($sprite) if !match('hover', selector()) && !match('active', selector()) diff --git a/app/stylesheets/blocks/soc-icon/soc-icon.styl b/app/stylesheets/blocks/soc-icon/soc-icon.styl new file mode 100644 index 000000000..b8bb3fc51 --- /dev/null +++ b/app/stylesheets/blocks/soc-icon/soc-icon.styl @@ -0,0 +1,14 @@ +.soc-icon + spriteImage $soc_icon + + &.google + spriteElem $soc_icon-google + + &.facebook + spriteElem $soc_icon-facebook + + &.twitter + spriteElem $soc_icon-twitter + + &.vk + spriteElem $soc_icon-vk diff --git a/app/stylesheets/blocks/soc-icon/soc_icon.sprite/facebook.png b/app/stylesheets/blocks/soc-icon/soc_icon.sprite/facebook.png new file mode 100644 index 0000000000000000000000000000000000000000..dfb94bc837ea15065cd32f6093d70b827a88cf98 GIT binary patch literal 1263 zcmVdbVG7wVRUJ4ZXi@?ZDjy7FETSP zF*5~6nk)bS0338hSaefwW^{L9a%BKPWN%_+AVz6&Wp{6KYjYq&Q#R#zf8V3`N#V8yY6&zTsa{Y6VodNAFfk+=6Js#Ol_bU`32cmyy)iCH zSZdt3A#p=u)D5I;lxP%!2$1-Kl7jY z48XImf3kOI{n{f#j|_~6sPGVGt+5kVr^Y6(PaXQ@+fPT$Gp~QNXV(+kMqhqzCm6>= z;$w{kjhy`PENA|>xYw`k>pQw-(=exg`L@c&7;G^T%Ndk)E5fn z`uLRwQ_U96TOJUeP7;})&(Q8!6)L43%4N^h$tGDEDHO_ep9D>2+llwqfau~6E13wi zlZ=tgeLTLUAKPV8DmgBUO>uF2hH}X<+fGP=CSC-BL{#oydl99RM)o~F%-gTk**Msb zwF{gJ9Q*DJKb`%TVzG)yo{SgO#lyW!f%!z(zO}}C`*+Y^8vs>Aa?aL*h$BQLioqBV zRlGI^1 zI~hm5KF#<)Q&cJiCZ}7}R+R`ru~vzq3nfxS?;RkDQYm>V<#M-RmjABLa_on5^f-rg zhGM}}>~T3(7t?E0R9TUMh|p|j#2`#Iz**M~U}(*!T$^k&w62#RdDhK&`XJB@4HttVW2Uh&n_gNfd%8iee2IENRR!1;uBbHfiT3?YU`+ z&Mv#`3?M3q8bTIPRbq%_Sw=cHO;Soh7sLxm6w->4b=vq1^*X~NPhoE~D7clEwRC4i zB@jgk^GdN&W!s*8tXtE=?Q@z2Ve9%D#j)S8u z$Nn7MS>Mj){y{oXSZ1hMSXI5Ndr^h5w+!|Xc4V=AnfmUPspaIzN z>W6!)wc3&1>Y5QiFn8BUHGdp_r;eZ$z2oxrHeVher?*<@id{jmv#pzBbB)HKbKieH Z`acoWlncyRgq8pR002ovPDHLkV1j%+JFx%& literal 0 HcmV?d00001 diff --git a/app/stylesheets/blocks/soc-icon/soc_icon.sprite/google.png b/app/stylesheets/blocks/soc-icon/soc_icon.sprite/google.png new file mode 100644 index 0000000000000000000000000000000000000000..d4d94951d7b6851e204606136cd9bccb64158be1 GIT binary patch literal 1624 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Заголовок третьего уровня

        Заголовок четвертого уровня

        Пока что нам важно знать, что некоторые «странности» языка не являются чем-то очень умным, а просто не были достаточно хорошо продуманы в своё время. В этом учебнике мы будем обращать особое внимание на основные недоработки и «грабли». Ничего критичного в них нет, если знаешь — не наступишь.

        -

        В новых версиях JavaScript (ECMAScript) эти недостатки постепенно убирают. Процесс внедрения небыстрый, в первую очередь из-за старых версий IE, но они постепенно вымирают. Современный IE в этом отношении несравнимо лучше.

        \ No newline at end of file +

        В новых версиях JavaScript (ECMAScript) эти недостатки постепенно убирают. Процесс внедрения небыстрый, в первую очередь из-за старых версий IE, но они постепенно вымирают. Современный IE в этом отношении несравнимо лучше.

        diff --git a/hmvc/markup/template/pages/article.jade b/hmvc/markup/template/pages/article.jade index 8f349e959..85e529c25 100644 --- a/hmvc/markup/template/pages/article.jade +++ b/hmvc/markup/template/pages/article.jade @@ -12,4 +12,3 @@ block variables block content != content - From 82b72e4eaa636ec6c869c081c431b789aa750c85 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Mon, 21 Jul 2014 12:43:36 +0400 Subject: [PATCH 082/130] hacking on --- app.js | 3 +- bin/www | 2 +- config/base.js | 1 + gulpfile.js | 9 +- hmvc/frontpage/index.js | 2 - hmvc/getpdf/controller/finish.js | 6 - hmvc/getpdf/controller/main.js | 33 +++++ hmvc/getpdf/controller/order.js | 33 ----- hmvc/getpdf/controller/pay.js | 49 ++++++++ hmvc/getpdf/controller/root.js | 6 - hmvc/getpdf/controller/success.js | 3 + hmvc/getpdf/index.js | 1 + hmvc/getpdf/paymentMethods.js | 2 +- hmvc/getpdf/router.js | 14 ++- hmvc/getpdf/template/{root.jade => main.jade} | 16 ++- hmvc/payment/index.js | 17 ++- hmvc/payment/lib/loadOrderMiddleware.js | 30 +++++ hmvc/payment/lib/loadTransactionMiddleware.js | 35 ++++++ hmvc/payment/model/order.js | 13 +- hmvc/payment/model/transaction.js | 47 ++++++-- hmvc/payment/model/transactionLog.js | 5 +- hmvc/webmoney/controller/back.js | 36 ------ hmvc/webmoney/controller/fail.js | 25 ++++ hmvc/webmoney/controller/result.js | 9 +- hmvc/webmoney/controller/success.js | 32 +++++ hmvc/webmoney/controller/wait.js | 18 +-- hmvc/webmoney/index.js | 10 +- hmvc/webmoney/router.js | 16 ++- hmvc/webmoney/template/back.jade | 20 --- hmvc/webmoney/template/form.jade | 2 +- hmvc/webmoney/template/wait.jade | 25 ++++ hmvc/yandexmoney/controller/back.js | 114 ++++++++++++++++++ hmvc/yandexmoney/index.js | 29 +++++ hmvc/yandexmoney/router.js | 10 ++ hmvc/yandexmoney/template/form.jade | 7 ++ hmvc/yandexmoney/template/wait.jade | 25 ++++ lib/dataUtil.js | 2 +- lib/mongoose.js | 2 +- package.json | 5 + routes/index.js | 2 +- setup/csrf.js | 36 +++++- setup/errors.js | 10 +- setup/headersLogger.js | 11 ++ setup/render.js | 2 +- setup/router.js | 7 +- tasks/import.js | 4 +- tasks/lint-full-die.js | 38 ------ tasks/lint.js | 57 --------- 48 files changed, 601 insertions(+), 280 deletions(-) delete mode 100644 hmvc/getpdf/controller/finish.js create mode 100644 hmvc/getpdf/controller/main.js delete mode 100644 hmvc/getpdf/controller/order.js create mode 100644 hmvc/getpdf/controller/pay.js delete mode 100644 hmvc/getpdf/controller/root.js create mode 100644 hmvc/getpdf/controller/success.js rename hmvc/getpdf/template/{root.jade => main.jade} (61%) create mode 100644 hmvc/payment/lib/loadOrderMiddleware.js create mode 100644 hmvc/payment/lib/loadTransactionMiddleware.js delete mode 100644 hmvc/webmoney/controller/back.js create mode 100644 hmvc/webmoney/controller/fail.js create mode 100644 hmvc/webmoney/controller/success.js delete mode 100644 hmvc/webmoney/template/back.jade create mode 100644 hmvc/webmoney/template/wait.jade create mode 100644 hmvc/yandexmoney/controller/back.js create mode 100644 hmvc/yandexmoney/index.js create mode 100644 hmvc/yandexmoney/router.js create mode 100644 hmvc/yandexmoney/template/form.jade create mode 100644 hmvc/yandexmoney/template/wait.jade create mode 100644 setup/headersLogger.js delete mode 100644 tasks/lint-full-die.js delete mode 100644 tasks/lint.js diff --git a/app.js b/app.js index 272b99ac2..5659a4d72 100755 --- a/app.js +++ b/app.js @@ -1,7 +1,7 @@ "use strict"; const koa = require('koa'); -const log = require('javascript-log')(module); +const log = require('js-log')();; const app = koa(); @@ -29,6 +29,7 @@ requireMiddleware('setup/logger'); requireMiddleware('setup/bodyParser'); if (process.env.NODE_ENV == 'development') { + requireMiddleware('setup/headersLogger'); requireMiddleware('setup/bodyLogger'); } diff --git a/bin/www b/bin/www index 5b655e00d..0882a245a 100755 --- a/bin/www +++ b/bin/www @@ -1,7 +1,7 @@ #!/usr/bin/env node 'use strict'; -const log = require('javascript-log')(module); +const log = require('js-log')();; const config = require('config'); const mongoose = require('lib/mongoose'); const app = require('app'); diff --git a/config/base.js b/config/base.js index 6731bc5d2..2e8270c27 100755 --- a/config/base.js +++ b/config/base.js @@ -24,6 +24,7 @@ module.exports = function() { keys: [secret.sessionKey] }, webmoney: secret.webmoney, + yandexmoney: secret.yandexmoney, template: { options: { 'cache': process.env.NODE_ENV != 'development' diff --git a/gulpfile.js b/gulpfile.js index b6aa3c3fd..26fd4410a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -6,18 +6,19 @@ const debug = require('gulp-debug'); const path = require('path'); const source = require('vinyl-source-stream'); const watchify = require('watchify'); -const browserifyTask = require('tasks/browserify'); +//const browserifyTask = require('tasks/browserify'); + const serverSources = [ 'config/**/*.js', 'controllers/**/*.js', 'lib/**/*.js', 'renderer/**/*.js', 'routes/**/*.js', 'setup/**/*.js', 'tasks/**/*.js', '*.js' ]; -gulp.task('lint', require('./tasks/lint')(serverSources)); - +gulp.task('lint', gp.jshintCache({ src: serverSources })); +gulp.task('lint-or-die', gp.jshintCache({ src: serverSources, dieOnError: true })); -gulp.task('lint-watch', ['lint'], function() { +gulp.task('lint-watch', ['lint'], function(neverCalled) { gulp.watch(serverSources, ['lint']); }); diff --git a/hmvc/frontpage/index.js b/hmvc/frontpage/index.js index b0be3b151..cdfa18f19 100644 --- a/hmvc/frontpage/index.js +++ b/hmvc/frontpage/index.js @@ -2,5 +2,3 @@ var router = require('./router'); exports.middleware = router.middleware(); - -exports.root = '/'; diff --git a/hmvc/getpdf/controller/finish.js b/hmvc/getpdf/controller/finish.js deleted file mode 100644 index d6e48667f..000000000 --- a/hmvc/getpdf/controller/finish.js +++ /dev/null @@ -1,6 +0,0 @@ -exports.get = function*(next) { - - this.locals.paymentMethods = require('../paymentMethods').methods; - - this.render(__dirname, 'root'); -}; diff --git a/hmvc/getpdf/controller/main.js b/hmvc/getpdf/controller/main.js new file mode 100644 index 000000000..434486499 --- /dev/null +++ b/hmvc/getpdf/controller/main.js @@ -0,0 +1,33 @@ +const mongoose = require('mongoose'); +var Order = mongoose.models.Order; +var Transaction = mongoose.models.Transaction; + +exports.get = function*(next) { + + if (!this.order) { + + // this order is not saved anywhere, + // it's only used to initially fill the form + this.order = new Order({ + amount: 1, + module: 'getpdf', + data: { + email: Math.round(Math.random()*1e6).toString(36) + '@gmail.com' + } + }); + + } + + this.locals.order = this.order; + + var lastTransaction = yield Transaction.findOne({ order: this.order._id }).sort({created: -1}).exec(); + + if (lastTransaction) { + console.log(lastTransaction.getStatusDescription); + this.locals.message = 'Статус последней оплаты: ' + lastTransaction.getStatusDescription(); + } + + this.locals.paymentMethods = require('../paymentMethods').methods; + + this.render(__dirname, 'main'); +}; diff --git a/hmvc/getpdf/controller/order.js b/hmvc/getpdf/controller/order.js deleted file mode 100644 index bb15f904d..000000000 --- a/hmvc/getpdf/controller/order.js +++ /dev/null @@ -1,33 +0,0 @@ -var mongoose = require('mongoose'); - -var Order = mongoose.models.Order; - -var methods = require('../paymentMethods').methods; - -exports.post = function*(next) { - this.assertCSRF(this.request.body); - - var method = methods[this.request.body.paymentMethod]; - if (!method) { - this.throw(403, "Unsupported payment method"); - } - - var methodApi = this.app.hmvc[method.name]; // /hmvc/webmoney - - var order = new Order({ - amount: 1, - module: 'getpdf', - data: { } - }); - yield order.persist(); - - if (!this.session.orders) { - this.session.orders = []; - } - this.session.orders.push(order.number); - - var form = yield methodApi.createTransactionForm(order); - - this.body = form; - -}; diff --git a/hmvc/getpdf/controller/pay.js b/hmvc/getpdf/controller/pay.js new file mode 100644 index 000000000..d53ccb277 --- /dev/null +++ b/hmvc/getpdf/controller/pay.js @@ -0,0 +1,49 @@ +var mongoose = require('mongoose'); +var log = require('js-log')(); +var Order = mongoose.models.Order; +var methods = require('../paymentMethods').methods; + +log.debugOn(); + +exports.post = function*(next) { + + var method = methods[this.request.body.paymentMethod]; + if (!method) { + this.throw(403, "Unsupported payment method"); + } + var methodApi = this.app.hmvc[method.name]; // /hmvc/webmoney + + if (this.order) { + log.debug("order exists", this.order.number); + yield* updateOrderFromBody(this.request.body, this.order); + } else { + // this order is not saved anywhere, + // it's only used to initially fill the form + this.order = new Order({ + amount: 1, + module: 'getpdf', + data: { } + }); + + yield* updateOrderFromBody(this.request.body, this.order); + + log.debug("order created", this.order.number); + + if (!this.session.orders) { + this.session.orders = []; + } + this.session.orders.push(this.order.number); + } + + var form = yield* methodApi.createTransactionForm(this.order); + + this.body = form; + +}; + +function* updateOrderFromBody(body, order) { + order.data.email = body.email; + order.markModified('data'); + + yield order.persist(); +} diff --git a/hmvc/getpdf/controller/root.js b/hmvc/getpdf/controller/root.js deleted file mode 100644 index d6e48667f..000000000 --- a/hmvc/getpdf/controller/root.js +++ /dev/null @@ -1,6 +0,0 @@ -exports.get = function*(next) { - - this.locals.paymentMethods = require('../paymentMethods').methods; - - this.render(__dirname, 'root'); -}; diff --git a/hmvc/getpdf/controller/success.js b/hmvc/getpdf/controller/success.js new file mode 100644 index 000000000..bbc36cc18 --- /dev/null +++ b/hmvc/getpdf/controller/success.js @@ -0,0 +1,3 @@ +exports.get = function*(next) { + this.body = 'THANK YOU'; +}; diff --git a/hmvc/getpdf/index.js b/hmvc/getpdf/index.js index cdfa18f19..f6e5943fb 100644 --- a/hmvc/getpdf/index.js +++ b/hmvc/getpdf/index.js @@ -2,3 +2,4 @@ var router = require('./router'); exports.middleware = router.middleware(); + diff --git a/hmvc/getpdf/paymentMethods.js b/hmvc/getpdf/paymentMethods.js index 521238af7..31811b581 100644 --- a/hmvc/getpdf/paymentMethods.js +++ b/hmvc/getpdf/paymentMethods.js @@ -1,6 +1,6 @@ exports.methods = { + 'yandexmoney': {name: "yandexmoney", title: "Яндекс.Деньги"}, 'webmoney': {name: "webmoney", title: "Webmoney"}, - 'yandex_money': {name: "yandex_money", title: "Яндекс.Деньги"}, 'payanyway': {name: "payanyway", title: "PayAnyWay"}, 'interkassa': {name: "interkassa", title: "Интеркасса"}, 'paypal': {name: "paypal", title: "Paypal"} diff --git a/hmvc/getpdf/router.js b/hmvc/getpdf/router.js index 6d1fbcd77..5443e41d9 100644 --- a/hmvc/getpdf/router.js +++ b/hmvc/getpdf/router.js @@ -1,10 +1,14 @@ +var payment = require('../payment'); var Router = require('koa-router'); - var router = module.exports = new Router(); -var root = require('./controller/root'); -var order = require('./controller/order'); +var main = require('./controller/main'); +var pay = require('./controller/pay'); +var success = require('./controller/success'); + +router.get('', main.get); +router.get('/order/:orderNumber', payment.loadOrderMiddleware('orderNumber'), main.get); -router.get('', root.get); -router.post('/order', order.post); +router.post('/pay', payment.loadOrderMiddleware('orderNumber'), pay.post); +router.get('/success/:orderNumber', payment.loadOrderMiddleware('orderNumber'), success.get); diff --git a/hmvc/getpdf/template/root.jade b/hmvc/getpdf/template/main.jade similarity index 61% rename from hmvc/getpdf/template/root.jade rename to hmvc/getpdf/template/main.jade index 607382883..ce74f6b6b 100644 --- a/hmvc/getpdf/template/root.jade +++ b/hmvc/getpdf/template/main.jade @@ -1,6 +1,11 @@ +if message + div= message + div p Выберите способ оплаты: form.pay-form.notready(onsubmit="alert('Минуточку, идёт загрузка...'); return false") + input(type="text" name="orderNumber" value=(!order.isNew && order.number) placeholder="order number") + input(name="email" value=order.data.email placeholder="E-mail") select(name="paymentMethod") each paymentMethod in paymentMethods option(value=paymentMethod.name) #{paymentMethod.title} @@ -18,14 +23,19 @@ script. e.preventDefault(); $.ajax({ method: 'POST', - url: '/getpdf/order', + url: '/getpdf/pay', data: { _csrf: csrf, + orderNumber: this.elements.orderNumber.value, + email: this.elements.email.value, paymentMethod: this.elements.paymentMethod.value } }) - .done(function(html) { - $(html).submit(); + .fail(function(err) { + alert("Ошибка на сервере"); + }) + .done(function(htmlForm) { + $(htmlForm).submit(); }); }); diff --git a/hmvc/payment/index.js b/hmvc/payment/index.js index a0ef48b09..25d597a4c 100644 --- a/hmvc/payment/index.js +++ b/hmvc/payment/index.js @@ -1,11 +1,10 @@ -var mongoose = require('mongoose'); -var Transaction = mongoose.model.Transaction; +exports.loadOrderMiddleware = require('./lib/loadOrderMiddleware'); +exports.loadTransactionMiddleware = require('./lib/loadTransactionMiddleware'); -exports.createTransaction = function* (amount, orderNum, type) { - return yield new Transaction({ - amount: amount, - orderNum: orderNum, - paymentType: type - }).persist(); +exports.loadMiddleware = function(field) { + return function* (next) { + yield* exports.loadTransactionMiddleware.call(this); + yield* exports.loadOrderMiddleware.call(this); + yield* next; + }; }; - diff --git a/hmvc/payment/lib/loadOrderMiddleware.js b/hmvc/payment/lib/loadOrderMiddleware.js new file mode 100644 index 000000000..79e78935f --- /dev/null +++ b/hmvc/payment/lib/loadOrderMiddleware.js @@ -0,0 +1,30 @@ +var mongoose = require('mongoose'); +var Order = mongoose.models.Order; + +module.exports = function(field) { + + if (!field) field = 'orderNumber'; + + return function* (next) { + + var orderNumber = this.request.body && this.request.body[field] || this.params[field] || this.query[field]; + + if (!orderNumber) { + return yield* next; + } + + var order = yield Order.findOne({number: orderNumber}).populate('order').exec(); + + if (!order) { + this.throw(404, 'Нет такого заказа'); + } + + // todo: add belongs to check (with auth) + if (!this.session.orders || this.session.orders.indexOf(order.number) == -1) { + this.throw(403, 'Заказ в сессии не найден'); + } + + this.order = order; + yield* next; + }; +}; diff --git a/hmvc/payment/lib/loadTransactionMiddleware.js b/hmvc/payment/lib/loadTransactionMiddleware.js new file mode 100644 index 000000000..286b54f7b --- /dev/null +++ b/hmvc/payment/lib/loadTransactionMiddleware.js @@ -0,0 +1,35 @@ +var mongoose = require('mongoose'); +var Transaction = mongoose.models.Transaction; +var log = require('js-log')(); + +log.debugOn(); + +module.exports = function(field) { + + if (!field) field = 'transactionNumber'; + + return function* (next) { + + var transactionNumber = this.request.body && this.request.body[field] || this.params[field] || this.query[field]; + + log.debug('tx number: ' + transactionNumber); + if (!transactionNumber) { + return yield* next; + } + + var transaction = yield Transaction.findOne({number: transactionNumber}).populate('order').exec(); + + if (!transaction) { + this.throw(404, 'Нет такой транзакции'); + } + + // todo: add belongs to check (with auth) + console.log("NUM", transaction.order.number, this.session); + if (!this.session.orders || this.session.orders.indexOf(transaction.order.number) == -1) { + this.throw(403, 'Не найден заказ в сессии для этой транзакции'); + } + + this.transaction = transaction; + yield* next; + }; +}; diff --git a/hmvc/payment/model/order.js b/hmvc/payment/model/order.js index 9d2cc5c31..2bc21c3ca 100644 --- a/hmvc/payment/model/order.js +++ b/hmvc/payment/model/order.js @@ -11,21 +11,16 @@ var schema = new Schema({ type: String, required: true }, - data: String, + status: { + type: String + }, + data: Schema.Types.Mixed, created: { type: Date, default: Date.now } }); -schema.methods.getSuccessUrl = function() { - return '/' + this.module + '/success'; -}; - -schema.methods.getFailUrl = function() { - return '/' + this.module + '/fail'; -}; - schema.plugin(autoIncrement.plugin, {model: 'Order', field: 'number'}); mongoose.model('Order', schema); diff --git a/hmvc/payment/model/transaction.js b/hmvc/payment/model/transaction.js index 8419ef1eb..9d25ab48b 100644 --- a/hmvc/payment/model/transaction.js +++ b/hmvc/payment/model/transaction.js @@ -9,34 +9,63 @@ var autoIncrement = require('mongoose-auto-increment'); * @type {Schema} */ var schema = new Schema({ - order: { + order: { type: Schema.Types.ObjectId, ref: 'Order' }, - amount: { + amount: { type: Number, required: true }, - paymentType: { + paymentType: { type: String, required: true }, - created: { + created: { type: Date, default: Date.now }, - status: { + status: { type: String }, - data: String + statusMessage: { + type: String + }, + data: String }); schema.plugin(autoIncrement.plugin, {model: 'Transaction', field: 'number'}); +schema.statics.STATUS_SUCCESS = 'success'; +schema.statics.STATUS_FAIL = 'fail'; + +schema.pre('save', function (next) { + if (this.status == Transaction.STATUS_SUCCESS) { + var orderId = this.order._id || this.order; + Order.findByIdAndUpdate(orderId, {status: Transaction.STATUS_SUCCESS}, function(err) { + if (err) throw(err); + }); + } +}); + +schema.methods.getStatusDescription = function() { + if (this.status == Transaction.STATUS_SUCCESS) { + return 'оплата прошла успешно'; + } -const Transaction = mongoose.model('Transaction', schema); + if (!this.status) { + return 'нет информации об оплате'; + } -Transaction.STATUS_SUCCESS = 'success'; -Transaction.STATUS_FAIL = 'fail'; + return 'оплата не прошла'; +}; +schema.methods.log = function*(options) { + options.transaction = this._id; + var log = new mongoose.models.TransactionLog(options); + yield log.persist(); +}; +/* jshint -W003 */ +var Transaction = module.exports = mongoose.model('Transaction', schema); +var Order = mongoose.models.Order; diff --git a/hmvc/payment/model/transactionLog.js b/hmvc/payment/model/transactionLog.js index a6066af87..08cd284c1 100644 --- a/hmvc/payment/model/transactionLog.js +++ b/hmvc/payment/model/transactionLog.js @@ -9,7 +9,7 @@ var schema = new Schema({ ref: 'Transaction' }, event: String, - data: String, + data: Schema.Types.Mixed, created: { type: Date, @@ -17,5 +17,6 @@ var schema = new Schema({ } }); -mongoose.model('TransactionLog', schema); +/* jshint -W003 */ +var TransactionLog = mongoose.model('TransactionLog', schema); diff --git a/hmvc/webmoney/controller/back.js b/hmvc/webmoney/controller/back.js deleted file mode 100644 index 87db3572e..000000000 --- a/hmvc/webmoney/controller/back.js +++ /dev/null @@ -1,36 +0,0 @@ -const payment = require('../../payment'); -const config = require('config'); -const mongoose = require('mongoose'); -const Order = mongoose.models.Order; -const Transaction = mongoose.models.Transaction; -const TransactionLog = mongoose.models.TransactionLog; -const log = require('javascript-log')(module); -const md5 = require('MD5'); - -log.debugOn(); - - -exports.get = function* (next) { - - var transactionNumber = this.query.LMI_PAYMENT_NO; - var transaction = yield Transaction.findOne({number: transactionNumber}).populate('order').exec(); - - if (!transaction) { - this.throw(404, 'transaction not found'); - } - - var order = transaction.order; - if (!this.session.orders || this.session.orders.indexOf(order.number) == -1) { - this.throw(403, 'order not in your session'); - } - - if (transaction.status) { - this.redirect(transaction.status == Transaction.STATUS_SUCCESS ? order.getSuccessUrl() : order.getFailUrl()); - } else { - this.render(__dirname, 'back', { - number: this.query.LMI_PAYMENT_NO, - module: '/' + transaction.order.module + '/finish' - }); - } - -}; diff --git a/hmvc/webmoney/controller/fail.js b/hmvc/webmoney/controller/fail.js new file mode 100644 index 000000000..4c2c6311c --- /dev/null +++ b/hmvc/webmoney/controller/fail.js @@ -0,0 +1,25 @@ +const config = require('config'); +const mongoose = require('mongoose'); +const Order = mongoose.models.Order; +const Transaction = mongoose.models.Transaction; +const TransactionLog = mongoose.models.TransactionLog; +const log = require('js-log')(); +const md5 = require('MD5'); + +log.debugOn(); + + +exports.get = function* (next) { + + this.transaction.status = Transaction.STATUS_FAIL; + yield this.transaction.persist(); + + yield new TransactionLog({ + event: 'fail', + transaction: this.transaction._id + }).persist(); + + var order = this.transaction.order; + this.redirect('/' + order.module + '/order/' + order.number); + +}; diff --git a/hmvc/webmoney/controller/result.js b/hmvc/webmoney/controller/result.js index 5e2f6b881..059195a31 100644 --- a/hmvc/webmoney/controller/result.js +++ b/hmvc/webmoney/controller/result.js @@ -4,11 +4,14 @@ const mongoose = require('mongoose'); const Order = mongoose.models.Order; const Transaction = mongoose.models.Transaction; const TransactionLog = mongoose.models.TransactionLog; -const log = require('javascript-log')(module); +const log = require('js-log')(); const md5 = require('MD5'); + + log.debugOn(); +// ONLY ACCESSED from WEBMONEY SERVER exports.prerequest = function* (next) { log.debug("prerequest"); @@ -23,7 +26,7 @@ exports.prerequest = function* (next) { yield new TransactionLog().persist({ transaction: transaction._id, event: 'prerequest', - data: JSON.stringify(this.request.body) + data: {url: this.request.originalUrl, body: this.request.body} }); if (transaction.status == Transaction.STATUS_SUCCESS || @@ -59,7 +62,7 @@ exports.post = function* (next) { yield new TransactionLog().persist({ transaction: transaction._id, event: 'result', - data: JSON.stringify(this.request.body) + data: {url: this.request.originalUrl, body: this.request.body} }); if (transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) || diff --git a/hmvc/webmoney/controller/success.js b/hmvc/webmoney/controller/success.js new file mode 100644 index 000000000..9a80bdde0 --- /dev/null +++ b/hmvc/webmoney/controller/success.js @@ -0,0 +1,32 @@ +const payment = require('../../payment'); +const config = require('config'); +const mongoose = require('mongoose'); +const Order = mongoose.models.Order; +const Transaction = mongoose.models.Transaction; +const TransactionLog = mongoose.models.TransactionLog; +const log = require('js-log')(); +const md5 = require('MD5'); + +log.debugOn(); + + +exports.get = function* (next) { + + var transaction = this.transaction; + var order = this.transaction.order; + var successUrl = '/' + order.module + '/success/' + order.number; + var failUrl = '/' + order.module + '/order/' + order.number; + + log.debug("transaction status: " + transaction.status); + + if (transaction.status) { + this.redirect(transaction.status == Transaction.STATUS_SUCCESS ? successUrl : failUrl); + } else { + this.render(__dirname, 'wait', { + transactionNumber: transaction.number, + successUrl: successUrl, + failUrl: failUrl + }); + } + +}; diff --git a/hmvc/webmoney/controller/wait.js b/hmvc/webmoney/controller/wait.js index eb522bc2d..4228e2aa1 100644 --- a/hmvc/webmoney/controller/wait.js +++ b/hmvc/webmoney/controller/wait.js @@ -4,7 +4,7 @@ const mongoose = require('mongoose'); const Order = mongoose.models.Order; const Transaction = mongoose.models.Transaction; const TransactionLog = mongoose.models.TransactionLog; -const log = require('javascript-log')(module); +const log = require('js-log')(); const md5 = require('MD5'); log.debugOn(); @@ -13,18 +13,8 @@ log.debugOn(); exports.post = function* (next) { - var transaction = yield Transaction.findOne({number: this.query.number}).populate('order').exec(); - - if (!transaction) { - this.throw(404, 'transaction not found'); - } - - if (!this.session.orders || this.session.orders.indexOf(transaction.order.number) == -1) { - this.throw(403, 'order not in your session'); - } - var attempt = 0; - while (!transaction.status) { + while (!this.transaction.status) { attempt++; if (attempt == 10) { log.debug("timeout"); @@ -34,12 +24,12 @@ exports.post = function* (next) { yield delay(1000); - transaction = yield Transaction.findOne({number: this.query.number }).exec(); + this.transaction = yield Transaction.findOne({number: this.transaction.number }).exec(); } log.debug('received'); - this.body = 'RECEIVED'; + this.body = this.transaction.status; }; function delay(ms) { diff --git a/hmvc/webmoney/index.js b/hmvc/webmoney/index.js index 92aeaff0a..5a71f2689 100644 --- a/hmvc/webmoney/index.js +++ b/hmvc/webmoney/index.js @@ -8,7 +8,15 @@ var router = require('./router'); exports.middleware = router.middleware(); -exports.createTransactionForm = function(transaction) { +exports.createTransactionForm = function* (order) { + + var transaction = new Transaction({ + order: order._id, + amount: order.amount, + paymentType: 'webmoney' + }); + + yield transaction.persist(); return jade.renderFile(path.join(__dirname, 'template/form.jade'), { amount: transaction.amount, diff --git a/hmvc/webmoney/router.js b/hmvc/webmoney/router.js index c8b0110fc..721de7a5b 100644 --- a/hmvc/webmoney/router.js +++ b/hmvc/webmoney/router.js @@ -1,14 +1,22 @@ var Router = require('koa-router'); - +var payment = require('../payment'); var router = module.exports = new Router(); var result = require('./controller/result'); -var back = require('./controller/back'); +var success = require('./controller/success'); +var fail = require('./controller/fail'); var wait = require('./controller/wait'); +// webmoney server posts here (in background) router.post('/result', result.post); -router.get('/back', back.get); -router.post('/wait', wait.post); + +// webmoney server redirects here if payment successful +router.get('/success', payment.loadTransactionMiddleware('LMI_PAYMENT_NO'), success.get); +// but if transaction status is not yet received, we wait... +router.post('/wait', payment.loadTransactionMiddleware(), wait.post); + +// webmoney server redirects here if payment failed +router.get('/fail', payment.loadTransactionMiddleware('LMI_PAYMENT_NO'), fail.get); diff --git a/hmvc/webmoney/template/back.jade b/hmvc/webmoney/template/back.jade deleted file mode 100644 index 79b10dc27..000000000 --- a/hmvc/webmoney/template/back.jade +++ /dev/null @@ -1,20 +0,0 @@ -div - p Минуточку, ожидаем информацию от сервера.. - -script var number = #{number}, finishPage = '#{finishPage}'; - -script. - var xhr = new XMLHttpRequest(); - xhr.open('POST', '/webmoney/wait?number=' + number); - xhr.timeout = 20000; - function finish() { - location.href = finishPage; - }; - - xhr.onreadystatechange = function() { - if (xhr.readyState != 4) return; - finish(); - }; - - xhr.ontimeout = xhr.onabort = finish; - xhr.send(''); diff --git a/hmvc/webmoney/template/form.jade b/hmvc/webmoney/template/form.jade index cd6c2e071..566ade235 100644 --- a/hmvc/webmoney/template/form.jade +++ b/hmvc/webmoney/template/form.jade @@ -1,4 +1,4 @@ -form(method="POST",action="https://merchant.webmoney.ru/lmi/payment.asp", class="webmoney-form") +form(method="POST",action="https://merchant.webmoney.ru/lmi/payment.asp") input(type="hidden",name="LMI_PAYMENT_AMOUNT",value=amount) input(type="hidden",name="LMI_PAYMENT_DESC_BASE64",value=new Buffer('оплата по счету ' + number).toString('base64')) input(type="hidden",name="LMI_PAYMENT_NO",value=number) diff --git a/hmvc/webmoney/template/wait.jade b/hmvc/webmoney/template/wait.jade new file mode 100644 index 000000000..28e2830d9 --- /dev/null +++ b/hmvc/webmoney/template/wait.jade @@ -0,0 +1,25 @@ +div + p Минуточку, ожидаем информацию от сервера.. + p Если эта страница долго не отвечает - + | + a(href='#' onclick='window.location.reload(true)') перезагрузите её + | . + +script var transactionNumber = #{transactionNumber}, successUrl = '#{successUrl}', failUrl = '#{failUrl}'; + +script. + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/webmoney/wait'); + xhr.timeout = 20000; + + + xhr.onreadystatechange = function() { + if (xhr.readyState != 4) return; + + location.href = (xhr.responseText == 'success') ? successUrl : failUrl; + }; + + xhr.ontimeout = xhr.onabort = function() { + location.href = failUrl; + }; + xhr.send('transactionNumber=' + transactionNumber); diff --git a/hmvc/yandexmoney/controller/back.js b/hmvc/yandexmoney/controller/back.js new file mode 100644 index 000000000..c381d4f5c --- /dev/null +++ b/hmvc/yandexmoney/controller/back.js @@ -0,0 +1,114 @@ +const config = require('config'); +const mongoose = require('mongoose'); +const Order = mongoose.models.Order; +const Transaction = mongoose.models.Transaction; +const TransactionLog = mongoose.models.TransactionLog; +const log = require('js-log')(); +const md5 = require('MD5'); +const request = require('koa-request'); +log.debugOn(); + +/* jshint -W106 */ +function* fail(ctx) { + ctx.transaction.status = Transaction.STATUS_FAIL; + yield ctx.transaction.persist(); + + yield ctx.transaction.log({ event: 'fail' }); + + var order = ctx.transaction.order; + ctx.redirect('/' + order.module + '/order/' + order.number); +} + +exports.get = function* (next) { + + yield this.transaction.log({ + event: 'back', + data: {url: this.request.originalUrl, body: this.request.body} + }); + + if (this.query.error) { + fail(this); + return; + } + + if (this.query.code) { + + // request oauth token + var options = { + method: 'POST', + form: { + code: this.query.code, + client_id: config.yandexmoney.clientId, + grant_type: 'authorization_code', + redirect_uri: config.yandexmoney.redirectUri + '?transactionNumber=' + this.transaction.number, + client_secret: config.yandexmoney.clientSecret + }, + url: 'https://sp-money.yandex.ru/oauth/token' + }; + + yield this.transaction.log({ event: 'request oauth/token', data: options }); + + var response; + try { + var response = request(options); + yield this.transaction.log({ event: 'response oauth/token', data: response }); + + response = JSON.parse(response); + if (!response.access_token) { + throw new Error(response.error); + } + } catch(e) { + fail(this); + return; + } + + var accessToken = response.access_token; + + // request payment + var options = { + method: 'POST', + form: { + pattern_id: 'p2p', + to: config.yandexmoney.purse, + amount: this.transaction.amount, + comment: 'оплата по счету ' + this.transaction.number, + message: 'оплата по счету ' + this.transaction.number, + identifier_type: 'account' + }, + headers: { + 'Authorization': 'Bearer ' + accessToken + }, + url: 'https://money.yandex.ru/api/request-payment' + }; + + // TODO! + + yield this.transaction.log({ event: 'request api/request-payment', data: options }); + + var response; + try { + var response = request(options); + yield this.transaction.log({ event: 'response api/request-payment', data: response }); + + response = JSON.parse(response); + if (!response.access_token) { + throw new Error(response.error); + } + } catch(e) { + fail(this); + return; + } + + + + + this.body = 'OK'; + } + + /* + this.transaction.status = Transaction.STATUS_FAIL; + yield this.transaction.persist(); + var order = this.transaction.order; + this.redirect('/' + order.module + '/order/' + order.number); +*/ +}; diff --git a/hmvc/yandexmoney/index.js b/hmvc/yandexmoney/index.js new file mode 100644 index 000000000..3e6510e54 --- /dev/null +++ b/hmvc/yandexmoney/index.js @@ -0,0 +1,29 @@ +const config = require('config'); +const jade = require('jade'); +const path = require('path'); +var mongoose = require('mongoose'); +var Transaction = mongoose.models.Transaction; + +var router = require('./router'); + +exports.middleware = router.middleware(); + +exports.createTransactionForm = function* (order) { + + var transaction = new Transaction({ + order: order._id, + amount: order.amount, + paymentType: 'yandexmoney' + }); + + yield transaction.persist(); + + return jade.renderFile(path.join(__dirname, 'template/form.jade'), { + clientId: config.yandexmoney.clientId, + redirectUri: config.yandexmoney.redirectUri, + purse: config.yandexmoney.purse, + transactionNumber: transaction.number, + amount: transaction.amount + }); + +}; diff --git a/hmvc/yandexmoney/router.js b/hmvc/yandexmoney/router.js new file mode 100644 index 000000000..682a584dd --- /dev/null +++ b/hmvc/yandexmoney/router.js @@ -0,0 +1,10 @@ +var Router = require('koa-router'); +var payment = require('../payment'); + +var router = module.exports = new Router(); + +var back = require('./controller/back'); + +router.get('/back', payment.loadTransactionMiddleware(), back.get); + + diff --git a/hmvc/yandexmoney/template/form.jade b/hmvc/yandexmoney/template/form.jade new file mode 100644 index 000000000..5612a281c --- /dev/null +++ b/hmvc/yandexmoney/template/form.jade @@ -0,0 +1,7 @@ + +form(method="POST",action="https://sp-money.yandex.ru/oauth/authorize") + input(type="hidden",name="client_id",value=clientId) + input(type="hidden",name="response_type",value="code") + input(type="hidden",name="redirect_uri",value=(redirectUri + '?transactionNumber=' + transactionNumber)) + input(type="hidden",name="scope",value=('payment.to-account("' + purse + '").limit(,' + amount + ')')) + input(type="submit",value="Оплатить") diff --git a/hmvc/yandexmoney/template/wait.jade b/hmvc/yandexmoney/template/wait.jade new file mode 100644 index 000000000..28e2830d9 --- /dev/null +++ b/hmvc/yandexmoney/template/wait.jade @@ -0,0 +1,25 @@ +div + p Минуточку, ожидаем информацию от сервера.. + p Если эта страница долго не отвечает - + | + a(href='#' onclick='window.location.reload(true)') перезагрузите её + | . + +script var transactionNumber = #{transactionNumber}, successUrl = '#{successUrl}', failUrl = '#{failUrl}'; + +script. + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/webmoney/wait'); + xhr.timeout = 20000; + + + xhr.onreadystatechange = function() { + if (xhr.readyState != 4) return; + + location.href = (xhr.responseText == 'success') ? successUrl : failUrl; + }; + + xhr.ontimeout = xhr.onabort = function() { + location.href = failUrl; + }; + xhr.send('transactionNumber=' + transactionNumber); diff --git a/lib/dataUtil.js b/lib/dataUtil.js index 84770caf3..2fea8c548 100644 --- a/lib/dataUtil.js +++ b/lib/dataUtil.js @@ -1,7 +1,7 @@ "use strict"; var mongoose = require('mongoose'); -var log = require('javascript-log')(module); +var log = require('js-log')(); var co = require('co'); var thunk = require('thunkify'); var db = mongoose.connection.db; diff --git a/lib/mongoose.js b/lib/mongoose.js index 4eeebd0d5..71f0eadee 100755 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -11,7 +11,7 @@ var mongoose = require('mongoose'); var requireTree = require('require-tree'); var path = require('path'); var fs = require('fs'); -var log = require('javascript-log')(module); +var log = require('js-log')(); var autoIncrement = require('mongoose-auto-increment'); //mongoose.set('debug', true); diff --git a/package.json b/package.json index eef6ffad0..424369fb6 100755 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "precommit": "NODE_PATH=. NODE_ENV=development node --harmony `which gulp` pre-commit", "dependencies": { "MD5": "^1.2.1", + "bluebird": "^2.2.2", "body-parser": "*", "brfs": "^1.1.2", "co": "*", @@ -26,6 +27,8 @@ "gulp-debug": "^0.3.0", "gulp-dir-sync": "^0.1.1", "gulp-if": "^1.2.2", + "gulp-ignore": "^1.1.0", + "gulp-jshint-cache": "*", "gulp-load-plugins": "^0.5.3", "gulp-newer": "^0.3.0", "gulp-notify": "^1.4.0", @@ -42,6 +45,7 @@ "javascript-log": "*", "javascript-parser": "*", "jquery": "^2.1.1", + "js-log": "^0.2.2", "koa": "*", "koa-bodyparser": "*", "koa-csrf": "^2.1.2", @@ -49,6 +53,7 @@ "koa-generic-session": "*", "koa-logger": "*", "koa-mount": "^1.3.0", + "koa-request": "^1.0.0", "koa-router": "*", "koa-session-mongoose": "*", "koa-static": "*", diff --git a/routes/index.js b/routes/index.js index ce6a6ac2d..05bf30721 100755 --- a/routes/index.js +++ b/routes/index.js @@ -1,6 +1,6 @@ 'use strict'; -var log = require('javascript-log')(module); +var log = require('js-log')(); var config = require('config'); var requireTree = require('require-tree'); var controllers = requireTree('../controllers'); diff --git a/setup/csrf.js b/setup/csrf.js index 1d7382ff4..0a33d1b0f 100644 --- a/setup/csrf.js +++ b/setup/csrf.js @@ -5,6 +5,38 @@ const csrf = require('koa-csrf'); module.exports = function(app) { csrf(app); -// manual check to skip api calls -// app.use(csrf.middleware); + if (!app.noCsrf) app.noCsrf = []; + + app.use(function* (next) { + // skip these methods + if (this.method === 'GET' || this.method === 'HEAD' || this.method === 'OPTIONS') { + return yield* next; + } + + // don't check filtered urls + // e.g for access from outside non-browser apis + var checkCsrf = true; + + for (var i=0; i Date: Mon, 21 Jul 2014 12:43:36 +0400 Subject: [PATCH 083/130] hacking on, before refactor of hvmc into modules --- app.js | 3 +- bin/www | 2 +- config/base.js | 1 + gulpfile.js | 9 +- hmvc/frontpage/index.js | 2 - hmvc/getpdf/controller/finish.js | 6 - hmvc/getpdf/controller/main.js | 33 +++++ hmvc/getpdf/controller/order.js | 33 ----- hmvc/getpdf/controller/pay.js | 49 ++++++++ hmvc/getpdf/controller/root.js | 6 - hmvc/getpdf/controller/success.js | 3 + hmvc/getpdf/index.js | 1 + hmvc/getpdf/paymentMethods.js | 2 +- hmvc/getpdf/router.js | 14 ++- hmvc/getpdf/template/{root.jade => main.jade} | 16 ++- hmvc/payment/index.js | 17 ++- hmvc/payment/lib/loadOrderMiddleware.js | 30 +++++ hmvc/payment/lib/loadTransactionMiddleware.js | 35 ++++++ hmvc/payment/model/order.js | 13 +- hmvc/payment/model/transaction.js | 47 ++++++-- hmvc/payment/model/transactionLog.js | 5 +- hmvc/webmoney/controller/back.js | 36 ------ hmvc/webmoney/controller/fail.js | 25 ++++ hmvc/webmoney/controller/result.js | 9 +- hmvc/webmoney/controller/success.js | 32 +++++ hmvc/webmoney/controller/wait.js | 18 +-- hmvc/webmoney/index.js | 10 +- hmvc/webmoney/router.js | 16 ++- hmvc/webmoney/template/back.jade | 20 --- hmvc/webmoney/template/form.jade | 2 +- hmvc/webmoney/template/wait.jade | 25 ++++ hmvc/yandexmoney/controller/back.js | 114 ++++++++++++++++++ hmvc/yandexmoney/index.js | 29 +++++ hmvc/yandexmoney/router.js | 10 ++ hmvc/yandexmoney/template/form.jade | 7 ++ hmvc/yandexmoney/template/wait.jade | 25 ++++ lib/dataUtil.js | 2 +- lib/mongoose.js | 2 +- package.json | 5 + routes/index.js | 2 +- setup/csrf.js | 36 +++++- setup/errors.js | 10 +- setup/headersLogger.js | 11 ++ setup/render.js | 2 +- setup/router.js | 7 +- tasks/import.js | 4 +- tasks/lint-full-die.js | 38 ------ tasks/lint.js | 57 --------- 48 files changed, 601 insertions(+), 280 deletions(-) delete mode 100644 hmvc/getpdf/controller/finish.js create mode 100644 hmvc/getpdf/controller/main.js delete mode 100644 hmvc/getpdf/controller/order.js create mode 100644 hmvc/getpdf/controller/pay.js delete mode 100644 hmvc/getpdf/controller/root.js create mode 100644 hmvc/getpdf/controller/success.js rename hmvc/getpdf/template/{root.jade => main.jade} (61%) create mode 100644 hmvc/payment/lib/loadOrderMiddleware.js create mode 100644 hmvc/payment/lib/loadTransactionMiddleware.js delete mode 100644 hmvc/webmoney/controller/back.js create mode 100644 hmvc/webmoney/controller/fail.js create mode 100644 hmvc/webmoney/controller/success.js delete mode 100644 hmvc/webmoney/template/back.jade create mode 100644 hmvc/webmoney/template/wait.jade create mode 100644 hmvc/yandexmoney/controller/back.js create mode 100644 hmvc/yandexmoney/index.js create mode 100644 hmvc/yandexmoney/router.js create mode 100644 hmvc/yandexmoney/template/form.jade create mode 100644 hmvc/yandexmoney/template/wait.jade create mode 100644 setup/headersLogger.js delete mode 100644 tasks/lint-full-die.js delete mode 100644 tasks/lint.js diff --git a/app.js b/app.js index 272b99ac2..5659a4d72 100755 --- a/app.js +++ b/app.js @@ -1,7 +1,7 @@ "use strict"; const koa = require('koa'); -const log = require('javascript-log')(module); +const log = require('js-log')();; const app = koa(); @@ -29,6 +29,7 @@ requireMiddleware('setup/logger'); requireMiddleware('setup/bodyParser'); if (process.env.NODE_ENV == 'development') { + requireMiddleware('setup/headersLogger'); requireMiddleware('setup/bodyLogger'); } diff --git a/bin/www b/bin/www index 5b655e00d..0882a245a 100755 --- a/bin/www +++ b/bin/www @@ -1,7 +1,7 @@ #!/usr/bin/env node 'use strict'; -const log = require('javascript-log')(module); +const log = require('js-log')();; const config = require('config'); const mongoose = require('lib/mongoose'); const app = require('app'); diff --git a/config/base.js b/config/base.js index 6731bc5d2..2e8270c27 100755 --- a/config/base.js +++ b/config/base.js @@ -24,6 +24,7 @@ module.exports = function() { keys: [secret.sessionKey] }, webmoney: secret.webmoney, + yandexmoney: secret.yandexmoney, template: { options: { 'cache': process.env.NODE_ENV != 'development' diff --git a/gulpfile.js b/gulpfile.js index b6aa3c3fd..26fd4410a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -6,18 +6,19 @@ const debug = require('gulp-debug'); const path = require('path'); const source = require('vinyl-source-stream'); const watchify = require('watchify'); -const browserifyTask = require('tasks/browserify'); +//const browserifyTask = require('tasks/browserify'); + const serverSources = [ 'config/**/*.js', 'controllers/**/*.js', 'lib/**/*.js', 'renderer/**/*.js', 'routes/**/*.js', 'setup/**/*.js', 'tasks/**/*.js', '*.js' ]; -gulp.task('lint', require('./tasks/lint')(serverSources)); - +gulp.task('lint', gp.jshintCache({ src: serverSources })); +gulp.task('lint-or-die', gp.jshintCache({ src: serverSources, dieOnError: true })); -gulp.task('lint-watch', ['lint'], function() { +gulp.task('lint-watch', ['lint'], function(neverCalled) { gulp.watch(serverSources, ['lint']); }); diff --git a/hmvc/frontpage/index.js b/hmvc/frontpage/index.js index b0be3b151..cdfa18f19 100644 --- a/hmvc/frontpage/index.js +++ b/hmvc/frontpage/index.js @@ -2,5 +2,3 @@ var router = require('./router'); exports.middleware = router.middleware(); - -exports.root = '/'; diff --git a/hmvc/getpdf/controller/finish.js b/hmvc/getpdf/controller/finish.js deleted file mode 100644 index d6e48667f..000000000 --- a/hmvc/getpdf/controller/finish.js +++ /dev/null @@ -1,6 +0,0 @@ -exports.get = function*(next) { - - this.locals.paymentMethods = require('../paymentMethods').methods; - - this.render(__dirname, 'root'); -}; diff --git a/hmvc/getpdf/controller/main.js b/hmvc/getpdf/controller/main.js new file mode 100644 index 000000000..434486499 --- /dev/null +++ b/hmvc/getpdf/controller/main.js @@ -0,0 +1,33 @@ +const mongoose = require('mongoose'); +var Order = mongoose.models.Order; +var Transaction = mongoose.models.Transaction; + +exports.get = function*(next) { + + if (!this.order) { + + // this order is not saved anywhere, + // it's only used to initially fill the form + this.order = new Order({ + amount: 1, + module: 'getpdf', + data: { + email: Math.round(Math.random()*1e6).toString(36) + '@gmail.com' + } + }); + + } + + this.locals.order = this.order; + + var lastTransaction = yield Transaction.findOne({ order: this.order._id }).sort({created: -1}).exec(); + + if (lastTransaction) { + console.log(lastTransaction.getStatusDescription); + this.locals.message = 'Статус последней оплаты: ' + lastTransaction.getStatusDescription(); + } + + this.locals.paymentMethods = require('../paymentMethods').methods; + + this.render(__dirname, 'main'); +}; diff --git a/hmvc/getpdf/controller/order.js b/hmvc/getpdf/controller/order.js deleted file mode 100644 index bb15f904d..000000000 --- a/hmvc/getpdf/controller/order.js +++ /dev/null @@ -1,33 +0,0 @@ -var mongoose = require('mongoose'); - -var Order = mongoose.models.Order; - -var methods = require('../paymentMethods').methods; - -exports.post = function*(next) { - this.assertCSRF(this.request.body); - - var method = methods[this.request.body.paymentMethod]; - if (!method) { - this.throw(403, "Unsupported payment method"); - } - - var methodApi = this.app.hmvc[method.name]; // /hmvc/webmoney - - var order = new Order({ - amount: 1, - module: 'getpdf', - data: { } - }); - yield order.persist(); - - if (!this.session.orders) { - this.session.orders = []; - } - this.session.orders.push(order.number); - - var form = yield methodApi.createTransactionForm(order); - - this.body = form; - -}; diff --git a/hmvc/getpdf/controller/pay.js b/hmvc/getpdf/controller/pay.js new file mode 100644 index 000000000..d53ccb277 --- /dev/null +++ b/hmvc/getpdf/controller/pay.js @@ -0,0 +1,49 @@ +var mongoose = require('mongoose'); +var log = require('js-log')(); +var Order = mongoose.models.Order; +var methods = require('../paymentMethods').methods; + +log.debugOn(); + +exports.post = function*(next) { + + var method = methods[this.request.body.paymentMethod]; + if (!method) { + this.throw(403, "Unsupported payment method"); + } + var methodApi = this.app.hmvc[method.name]; // /hmvc/webmoney + + if (this.order) { + log.debug("order exists", this.order.number); + yield* updateOrderFromBody(this.request.body, this.order); + } else { + // this order is not saved anywhere, + // it's only used to initially fill the form + this.order = new Order({ + amount: 1, + module: 'getpdf', + data: { } + }); + + yield* updateOrderFromBody(this.request.body, this.order); + + log.debug("order created", this.order.number); + + if (!this.session.orders) { + this.session.orders = []; + } + this.session.orders.push(this.order.number); + } + + var form = yield* methodApi.createTransactionForm(this.order); + + this.body = form; + +}; + +function* updateOrderFromBody(body, order) { + order.data.email = body.email; + order.markModified('data'); + + yield order.persist(); +} diff --git a/hmvc/getpdf/controller/root.js b/hmvc/getpdf/controller/root.js deleted file mode 100644 index d6e48667f..000000000 --- a/hmvc/getpdf/controller/root.js +++ /dev/null @@ -1,6 +0,0 @@ -exports.get = function*(next) { - - this.locals.paymentMethods = require('../paymentMethods').methods; - - this.render(__dirname, 'root'); -}; diff --git a/hmvc/getpdf/controller/success.js b/hmvc/getpdf/controller/success.js new file mode 100644 index 000000000..bbc36cc18 --- /dev/null +++ b/hmvc/getpdf/controller/success.js @@ -0,0 +1,3 @@ +exports.get = function*(next) { + this.body = 'THANK YOU'; +}; diff --git a/hmvc/getpdf/index.js b/hmvc/getpdf/index.js index cdfa18f19..f6e5943fb 100644 --- a/hmvc/getpdf/index.js +++ b/hmvc/getpdf/index.js @@ -2,3 +2,4 @@ var router = require('./router'); exports.middleware = router.middleware(); + diff --git a/hmvc/getpdf/paymentMethods.js b/hmvc/getpdf/paymentMethods.js index 521238af7..31811b581 100644 --- a/hmvc/getpdf/paymentMethods.js +++ b/hmvc/getpdf/paymentMethods.js @@ -1,6 +1,6 @@ exports.methods = { + 'yandexmoney': {name: "yandexmoney", title: "Яндекс.Деньги"}, 'webmoney': {name: "webmoney", title: "Webmoney"}, - 'yandex_money': {name: "yandex_money", title: "Яндекс.Деньги"}, 'payanyway': {name: "payanyway", title: "PayAnyWay"}, 'interkassa': {name: "interkassa", title: "Интеркасса"}, 'paypal': {name: "paypal", title: "Paypal"} diff --git a/hmvc/getpdf/router.js b/hmvc/getpdf/router.js index 6d1fbcd77..5443e41d9 100644 --- a/hmvc/getpdf/router.js +++ b/hmvc/getpdf/router.js @@ -1,10 +1,14 @@ +var payment = require('../payment'); var Router = require('koa-router'); - var router = module.exports = new Router(); -var root = require('./controller/root'); -var order = require('./controller/order'); +var main = require('./controller/main'); +var pay = require('./controller/pay'); +var success = require('./controller/success'); + +router.get('', main.get); +router.get('/order/:orderNumber', payment.loadOrderMiddleware('orderNumber'), main.get); -router.get('', root.get); -router.post('/order', order.post); +router.post('/pay', payment.loadOrderMiddleware('orderNumber'), pay.post); +router.get('/success/:orderNumber', payment.loadOrderMiddleware('orderNumber'), success.get); diff --git a/hmvc/getpdf/template/root.jade b/hmvc/getpdf/template/main.jade similarity index 61% rename from hmvc/getpdf/template/root.jade rename to hmvc/getpdf/template/main.jade index 607382883..ce74f6b6b 100644 --- a/hmvc/getpdf/template/root.jade +++ b/hmvc/getpdf/template/main.jade @@ -1,6 +1,11 @@ +if message + div= message + div p Выберите способ оплаты: form.pay-form.notready(onsubmit="alert('Минуточку, идёт загрузка...'); return false") + input(type="text" name="orderNumber" value=(!order.isNew && order.number) placeholder="order number") + input(name="email" value=order.data.email placeholder="E-mail") select(name="paymentMethod") each paymentMethod in paymentMethods option(value=paymentMethod.name) #{paymentMethod.title} @@ -18,14 +23,19 @@ script. e.preventDefault(); $.ajax({ method: 'POST', - url: '/getpdf/order', + url: '/getpdf/pay', data: { _csrf: csrf, + orderNumber: this.elements.orderNumber.value, + email: this.elements.email.value, paymentMethod: this.elements.paymentMethod.value } }) - .done(function(html) { - $(html).submit(); + .fail(function(err) { + alert("Ошибка на сервере"); + }) + .done(function(htmlForm) { + $(htmlForm).submit(); }); }); diff --git a/hmvc/payment/index.js b/hmvc/payment/index.js index a0ef48b09..25d597a4c 100644 --- a/hmvc/payment/index.js +++ b/hmvc/payment/index.js @@ -1,11 +1,10 @@ -var mongoose = require('mongoose'); -var Transaction = mongoose.model.Transaction; +exports.loadOrderMiddleware = require('./lib/loadOrderMiddleware'); +exports.loadTransactionMiddleware = require('./lib/loadTransactionMiddleware'); -exports.createTransaction = function* (amount, orderNum, type) { - return yield new Transaction({ - amount: amount, - orderNum: orderNum, - paymentType: type - }).persist(); +exports.loadMiddleware = function(field) { + return function* (next) { + yield* exports.loadTransactionMiddleware.call(this); + yield* exports.loadOrderMiddleware.call(this); + yield* next; + }; }; - diff --git a/hmvc/payment/lib/loadOrderMiddleware.js b/hmvc/payment/lib/loadOrderMiddleware.js new file mode 100644 index 000000000..79e78935f --- /dev/null +++ b/hmvc/payment/lib/loadOrderMiddleware.js @@ -0,0 +1,30 @@ +var mongoose = require('mongoose'); +var Order = mongoose.models.Order; + +module.exports = function(field) { + + if (!field) field = 'orderNumber'; + + return function* (next) { + + var orderNumber = this.request.body && this.request.body[field] || this.params[field] || this.query[field]; + + if (!orderNumber) { + return yield* next; + } + + var order = yield Order.findOne({number: orderNumber}).populate('order').exec(); + + if (!order) { + this.throw(404, 'Нет такого заказа'); + } + + // todo: add belongs to check (with auth) + if (!this.session.orders || this.session.orders.indexOf(order.number) == -1) { + this.throw(403, 'Заказ в сессии не найден'); + } + + this.order = order; + yield* next; + }; +}; diff --git a/hmvc/payment/lib/loadTransactionMiddleware.js b/hmvc/payment/lib/loadTransactionMiddleware.js new file mode 100644 index 000000000..286b54f7b --- /dev/null +++ b/hmvc/payment/lib/loadTransactionMiddleware.js @@ -0,0 +1,35 @@ +var mongoose = require('mongoose'); +var Transaction = mongoose.models.Transaction; +var log = require('js-log')(); + +log.debugOn(); + +module.exports = function(field) { + + if (!field) field = 'transactionNumber'; + + return function* (next) { + + var transactionNumber = this.request.body && this.request.body[field] || this.params[field] || this.query[field]; + + log.debug('tx number: ' + transactionNumber); + if (!transactionNumber) { + return yield* next; + } + + var transaction = yield Transaction.findOne({number: transactionNumber}).populate('order').exec(); + + if (!transaction) { + this.throw(404, 'Нет такой транзакции'); + } + + // todo: add belongs to check (with auth) + console.log("NUM", transaction.order.number, this.session); + if (!this.session.orders || this.session.orders.indexOf(transaction.order.number) == -1) { + this.throw(403, 'Не найден заказ в сессии для этой транзакции'); + } + + this.transaction = transaction; + yield* next; + }; +}; diff --git a/hmvc/payment/model/order.js b/hmvc/payment/model/order.js index 9d2cc5c31..2bc21c3ca 100644 --- a/hmvc/payment/model/order.js +++ b/hmvc/payment/model/order.js @@ -11,21 +11,16 @@ var schema = new Schema({ type: String, required: true }, - data: String, + status: { + type: String + }, + data: Schema.Types.Mixed, created: { type: Date, default: Date.now } }); -schema.methods.getSuccessUrl = function() { - return '/' + this.module + '/success'; -}; - -schema.methods.getFailUrl = function() { - return '/' + this.module + '/fail'; -}; - schema.plugin(autoIncrement.plugin, {model: 'Order', field: 'number'}); mongoose.model('Order', schema); diff --git a/hmvc/payment/model/transaction.js b/hmvc/payment/model/transaction.js index 8419ef1eb..9d25ab48b 100644 --- a/hmvc/payment/model/transaction.js +++ b/hmvc/payment/model/transaction.js @@ -9,34 +9,63 @@ var autoIncrement = require('mongoose-auto-increment'); * @type {Schema} */ var schema = new Schema({ - order: { + order: { type: Schema.Types.ObjectId, ref: 'Order' }, - amount: { + amount: { type: Number, required: true }, - paymentType: { + paymentType: { type: String, required: true }, - created: { + created: { type: Date, default: Date.now }, - status: { + status: { type: String }, - data: String + statusMessage: { + type: String + }, + data: String }); schema.plugin(autoIncrement.plugin, {model: 'Transaction', field: 'number'}); +schema.statics.STATUS_SUCCESS = 'success'; +schema.statics.STATUS_FAIL = 'fail'; + +schema.pre('save', function (next) { + if (this.status == Transaction.STATUS_SUCCESS) { + var orderId = this.order._id || this.order; + Order.findByIdAndUpdate(orderId, {status: Transaction.STATUS_SUCCESS}, function(err) { + if (err) throw(err); + }); + } +}); + +schema.methods.getStatusDescription = function() { + if (this.status == Transaction.STATUS_SUCCESS) { + return 'оплата прошла успешно'; + } -const Transaction = mongoose.model('Transaction', schema); + if (!this.status) { + return 'нет информации об оплате'; + } -Transaction.STATUS_SUCCESS = 'success'; -Transaction.STATUS_FAIL = 'fail'; + return 'оплата не прошла'; +}; +schema.methods.log = function*(options) { + options.transaction = this._id; + var log = new mongoose.models.TransactionLog(options); + yield log.persist(); +}; +/* jshint -W003 */ +var Transaction = module.exports = mongoose.model('Transaction', schema); +var Order = mongoose.models.Order; diff --git a/hmvc/payment/model/transactionLog.js b/hmvc/payment/model/transactionLog.js index a6066af87..08cd284c1 100644 --- a/hmvc/payment/model/transactionLog.js +++ b/hmvc/payment/model/transactionLog.js @@ -9,7 +9,7 @@ var schema = new Schema({ ref: 'Transaction' }, event: String, - data: String, + data: Schema.Types.Mixed, created: { type: Date, @@ -17,5 +17,6 @@ var schema = new Schema({ } }); -mongoose.model('TransactionLog', schema); +/* jshint -W003 */ +var TransactionLog = mongoose.model('TransactionLog', schema); diff --git a/hmvc/webmoney/controller/back.js b/hmvc/webmoney/controller/back.js deleted file mode 100644 index 87db3572e..000000000 --- a/hmvc/webmoney/controller/back.js +++ /dev/null @@ -1,36 +0,0 @@ -const payment = require('../../payment'); -const config = require('config'); -const mongoose = require('mongoose'); -const Order = mongoose.models.Order; -const Transaction = mongoose.models.Transaction; -const TransactionLog = mongoose.models.TransactionLog; -const log = require('javascript-log')(module); -const md5 = require('MD5'); - -log.debugOn(); - - -exports.get = function* (next) { - - var transactionNumber = this.query.LMI_PAYMENT_NO; - var transaction = yield Transaction.findOne({number: transactionNumber}).populate('order').exec(); - - if (!transaction) { - this.throw(404, 'transaction not found'); - } - - var order = transaction.order; - if (!this.session.orders || this.session.orders.indexOf(order.number) == -1) { - this.throw(403, 'order not in your session'); - } - - if (transaction.status) { - this.redirect(transaction.status == Transaction.STATUS_SUCCESS ? order.getSuccessUrl() : order.getFailUrl()); - } else { - this.render(__dirname, 'back', { - number: this.query.LMI_PAYMENT_NO, - module: '/' + transaction.order.module + '/finish' - }); - } - -}; diff --git a/hmvc/webmoney/controller/fail.js b/hmvc/webmoney/controller/fail.js new file mode 100644 index 000000000..4c2c6311c --- /dev/null +++ b/hmvc/webmoney/controller/fail.js @@ -0,0 +1,25 @@ +const config = require('config'); +const mongoose = require('mongoose'); +const Order = mongoose.models.Order; +const Transaction = mongoose.models.Transaction; +const TransactionLog = mongoose.models.TransactionLog; +const log = require('js-log')(); +const md5 = require('MD5'); + +log.debugOn(); + + +exports.get = function* (next) { + + this.transaction.status = Transaction.STATUS_FAIL; + yield this.transaction.persist(); + + yield new TransactionLog({ + event: 'fail', + transaction: this.transaction._id + }).persist(); + + var order = this.transaction.order; + this.redirect('/' + order.module + '/order/' + order.number); + +}; diff --git a/hmvc/webmoney/controller/result.js b/hmvc/webmoney/controller/result.js index 5e2f6b881..059195a31 100644 --- a/hmvc/webmoney/controller/result.js +++ b/hmvc/webmoney/controller/result.js @@ -4,11 +4,14 @@ const mongoose = require('mongoose'); const Order = mongoose.models.Order; const Transaction = mongoose.models.Transaction; const TransactionLog = mongoose.models.TransactionLog; -const log = require('javascript-log')(module); +const log = require('js-log')(); const md5 = require('MD5'); + + log.debugOn(); +// ONLY ACCESSED from WEBMONEY SERVER exports.prerequest = function* (next) { log.debug("prerequest"); @@ -23,7 +26,7 @@ exports.prerequest = function* (next) { yield new TransactionLog().persist({ transaction: transaction._id, event: 'prerequest', - data: JSON.stringify(this.request.body) + data: {url: this.request.originalUrl, body: this.request.body} }); if (transaction.status == Transaction.STATUS_SUCCESS || @@ -59,7 +62,7 @@ exports.post = function* (next) { yield new TransactionLog().persist({ transaction: transaction._id, event: 'result', - data: JSON.stringify(this.request.body) + data: {url: this.request.originalUrl, body: this.request.body} }); if (transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) || diff --git a/hmvc/webmoney/controller/success.js b/hmvc/webmoney/controller/success.js new file mode 100644 index 000000000..9a80bdde0 --- /dev/null +++ b/hmvc/webmoney/controller/success.js @@ -0,0 +1,32 @@ +const payment = require('../../payment'); +const config = require('config'); +const mongoose = require('mongoose'); +const Order = mongoose.models.Order; +const Transaction = mongoose.models.Transaction; +const TransactionLog = mongoose.models.TransactionLog; +const log = require('js-log')(); +const md5 = require('MD5'); + +log.debugOn(); + + +exports.get = function* (next) { + + var transaction = this.transaction; + var order = this.transaction.order; + var successUrl = '/' + order.module + '/success/' + order.number; + var failUrl = '/' + order.module + '/order/' + order.number; + + log.debug("transaction status: " + transaction.status); + + if (transaction.status) { + this.redirect(transaction.status == Transaction.STATUS_SUCCESS ? successUrl : failUrl); + } else { + this.render(__dirname, 'wait', { + transactionNumber: transaction.number, + successUrl: successUrl, + failUrl: failUrl + }); + } + +}; diff --git a/hmvc/webmoney/controller/wait.js b/hmvc/webmoney/controller/wait.js index eb522bc2d..4228e2aa1 100644 --- a/hmvc/webmoney/controller/wait.js +++ b/hmvc/webmoney/controller/wait.js @@ -4,7 +4,7 @@ const mongoose = require('mongoose'); const Order = mongoose.models.Order; const Transaction = mongoose.models.Transaction; const TransactionLog = mongoose.models.TransactionLog; -const log = require('javascript-log')(module); +const log = require('js-log')(); const md5 = require('MD5'); log.debugOn(); @@ -13,18 +13,8 @@ log.debugOn(); exports.post = function* (next) { - var transaction = yield Transaction.findOne({number: this.query.number}).populate('order').exec(); - - if (!transaction) { - this.throw(404, 'transaction not found'); - } - - if (!this.session.orders || this.session.orders.indexOf(transaction.order.number) == -1) { - this.throw(403, 'order not in your session'); - } - var attempt = 0; - while (!transaction.status) { + while (!this.transaction.status) { attempt++; if (attempt == 10) { log.debug("timeout"); @@ -34,12 +24,12 @@ exports.post = function* (next) { yield delay(1000); - transaction = yield Transaction.findOne({number: this.query.number }).exec(); + this.transaction = yield Transaction.findOne({number: this.transaction.number }).exec(); } log.debug('received'); - this.body = 'RECEIVED'; + this.body = this.transaction.status; }; function delay(ms) { diff --git a/hmvc/webmoney/index.js b/hmvc/webmoney/index.js index 92aeaff0a..5a71f2689 100644 --- a/hmvc/webmoney/index.js +++ b/hmvc/webmoney/index.js @@ -8,7 +8,15 @@ var router = require('./router'); exports.middleware = router.middleware(); -exports.createTransactionForm = function(transaction) { +exports.createTransactionForm = function* (order) { + + var transaction = new Transaction({ + order: order._id, + amount: order.amount, + paymentType: 'webmoney' + }); + + yield transaction.persist(); return jade.renderFile(path.join(__dirname, 'template/form.jade'), { amount: transaction.amount, diff --git a/hmvc/webmoney/router.js b/hmvc/webmoney/router.js index c8b0110fc..721de7a5b 100644 --- a/hmvc/webmoney/router.js +++ b/hmvc/webmoney/router.js @@ -1,14 +1,22 @@ var Router = require('koa-router'); - +var payment = require('../payment'); var router = module.exports = new Router(); var result = require('./controller/result'); -var back = require('./controller/back'); +var success = require('./controller/success'); +var fail = require('./controller/fail'); var wait = require('./controller/wait'); +// webmoney server posts here (in background) router.post('/result', result.post); -router.get('/back', back.get); -router.post('/wait', wait.post); + +// webmoney server redirects here if payment successful +router.get('/success', payment.loadTransactionMiddleware('LMI_PAYMENT_NO'), success.get); +// but if transaction status is not yet received, we wait... +router.post('/wait', payment.loadTransactionMiddleware(), wait.post); + +// webmoney server redirects here if payment failed +router.get('/fail', payment.loadTransactionMiddleware('LMI_PAYMENT_NO'), fail.get); diff --git a/hmvc/webmoney/template/back.jade b/hmvc/webmoney/template/back.jade deleted file mode 100644 index 79b10dc27..000000000 --- a/hmvc/webmoney/template/back.jade +++ /dev/null @@ -1,20 +0,0 @@ -div - p Минуточку, ожидаем информацию от сервера.. - -script var number = #{number}, finishPage = '#{finishPage}'; - -script. - var xhr = new XMLHttpRequest(); - xhr.open('POST', '/webmoney/wait?number=' + number); - xhr.timeout = 20000; - function finish() { - location.href = finishPage; - }; - - xhr.onreadystatechange = function() { - if (xhr.readyState != 4) return; - finish(); - }; - - xhr.ontimeout = xhr.onabort = finish; - xhr.send(''); diff --git a/hmvc/webmoney/template/form.jade b/hmvc/webmoney/template/form.jade index cd6c2e071..566ade235 100644 --- a/hmvc/webmoney/template/form.jade +++ b/hmvc/webmoney/template/form.jade @@ -1,4 +1,4 @@ -form(method="POST",action="https://merchant.webmoney.ru/lmi/payment.asp", class="webmoney-form") +form(method="POST",action="https://merchant.webmoney.ru/lmi/payment.asp") input(type="hidden",name="LMI_PAYMENT_AMOUNT",value=amount) input(type="hidden",name="LMI_PAYMENT_DESC_BASE64",value=new Buffer('оплата по счету ' + number).toString('base64')) input(type="hidden",name="LMI_PAYMENT_NO",value=number) diff --git a/hmvc/webmoney/template/wait.jade b/hmvc/webmoney/template/wait.jade new file mode 100644 index 000000000..28e2830d9 --- /dev/null +++ b/hmvc/webmoney/template/wait.jade @@ -0,0 +1,25 @@ +div + p Минуточку, ожидаем информацию от сервера.. + p Если эта страница долго не отвечает - + | + a(href='#' onclick='window.location.reload(true)') перезагрузите её + | . + +script var transactionNumber = #{transactionNumber}, successUrl = '#{successUrl}', failUrl = '#{failUrl}'; + +script. + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/webmoney/wait'); + xhr.timeout = 20000; + + + xhr.onreadystatechange = function() { + if (xhr.readyState != 4) return; + + location.href = (xhr.responseText == 'success') ? successUrl : failUrl; + }; + + xhr.ontimeout = xhr.onabort = function() { + location.href = failUrl; + }; + xhr.send('transactionNumber=' + transactionNumber); diff --git a/hmvc/yandexmoney/controller/back.js b/hmvc/yandexmoney/controller/back.js new file mode 100644 index 000000000..c381d4f5c --- /dev/null +++ b/hmvc/yandexmoney/controller/back.js @@ -0,0 +1,114 @@ +const config = require('config'); +const mongoose = require('mongoose'); +const Order = mongoose.models.Order; +const Transaction = mongoose.models.Transaction; +const TransactionLog = mongoose.models.TransactionLog; +const log = require('js-log')(); +const md5 = require('MD5'); +const request = require('koa-request'); +log.debugOn(); + +/* jshint -W106 */ +function* fail(ctx) { + ctx.transaction.status = Transaction.STATUS_FAIL; + yield ctx.transaction.persist(); + + yield ctx.transaction.log({ event: 'fail' }); + + var order = ctx.transaction.order; + ctx.redirect('/' + order.module + '/order/' + order.number); +} + +exports.get = function* (next) { + + yield this.transaction.log({ + event: 'back', + data: {url: this.request.originalUrl, body: this.request.body} + }); + + if (this.query.error) { + fail(this); + return; + } + + if (this.query.code) { + + // request oauth token + var options = { + method: 'POST', + form: { + code: this.query.code, + client_id: config.yandexmoney.clientId, + grant_type: 'authorization_code', + redirect_uri: config.yandexmoney.redirectUri + '?transactionNumber=' + this.transaction.number, + client_secret: config.yandexmoney.clientSecret + }, + url: 'https://sp-money.yandex.ru/oauth/token' + }; + + yield this.transaction.log({ event: 'request oauth/token', data: options }); + + var response; + try { + var response = request(options); + yield this.transaction.log({ event: 'response oauth/token', data: response }); + + response = JSON.parse(response); + if (!response.access_token) { + throw new Error(response.error); + } + } catch(e) { + fail(this); + return; + } + + var accessToken = response.access_token; + + // request payment + var options = { + method: 'POST', + form: { + pattern_id: 'p2p', + to: config.yandexmoney.purse, + amount: this.transaction.amount, + comment: 'оплата по счету ' + this.transaction.number, + message: 'оплата по счету ' + this.transaction.number, + identifier_type: 'account' + }, + headers: { + 'Authorization': 'Bearer ' + accessToken + }, + url: 'https://money.yandex.ru/api/request-payment' + }; + + // TODO! + + yield this.transaction.log({ event: 'request api/request-payment', data: options }); + + var response; + try { + var response = request(options); + yield this.transaction.log({ event: 'response api/request-payment', data: response }); + + response = JSON.parse(response); + if (!response.access_token) { + throw new Error(response.error); + } + } catch(e) { + fail(this); + return; + } + + + + + this.body = 'OK'; + } + + /* + this.transaction.status = Transaction.STATUS_FAIL; + yield this.transaction.persist(); + var order = this.transaction.order; + this.redirect('/' + order.module + '/order/' + order.number); +*/ +}; diff --git a/hmvc/yandexmoney/index.js b/hmvc/yandexmoney/index.js new file mode 100644 index 000000000..3e6510e54 --- /dev/null +++ b/hmvc/yandexmoney/index.js @@ -0,0 +1,29 @@ +const config = require('config'); +const jade = require('jade'); +const path = require('path'); +var mongoose = require('mongoose'); +var Transaction = mongoose.models.Transaction; + +var router = require('./router'); + +exports.middleware = router.middleware(); + +exports.createTransactionForm = function* (order) { + + var transaction = new Transaction({ + order: order._id, + amount: order.amount, + paymentType: 'yandexmoney' + }); + + yield transaction.persist(); + + return jade.renderFile(path.join(__dirname, 'template/form.jade'), { + clientId: config.yandexmoney.clientId, + redirectUri: config.yandexmoney.redirectUri, + purse: config.yandexmoney.purse, + transactionNumber: transaction.number, + amount: transaction.amount + }); + +}; diff --git a/hmvc/yandexmoney/router.js b/hmvc/yandexmoney/router.js new file mode 100644 index 000000000..682a584dd --- /dev/null +++ b/hmvc/yandexmoney/router.js @@ -0,0 +1,10 @@ +var Router = require('koa-router'); +var payment = require('../payment'); + +var router = module.exports = new Router(); + +var back = require('./controller/back'); + +router.get('/back', payment.loadTransactionMiddleware(), back.get); + + diff --git a/hmvc/yandexmoney/template/form.jade b/hmvc/yandexmoney/template/form.jade new file mode 100644 index 000000000..5612a281c --- /dev/null +++ b/hmvc/yandexmoney/template/form.jade @@ -0,0 +1,7 @@ + +form(method="POST",action="https://sp-money.yandex.ru/oauth/authorize") + input(type="hidden",name="client_id",value=clientId) + input(type="hidden",name="response_type",value="code") + input(type="hidden",name="redirect_uri",value=(redirectUri + '?transactionNumber=' + transactionNumber)) + input(type="hidden",name="scope",value=('payment.to-account("' + purse + '").limit(,' + amount + ')')) + input(type="submit",value="Оплатить") diff --git a/hmvc/yandexmoney/template/wait.jade b/hmvc/yandexmoney/template/wait.jade new file mode 100644 index 000000000..28e2830d9 --- /dev/null +++ b/hmvc/yandexmoney/template/wait.jade @@ -0,0 +1,25 @@ +div + p Минуточку, ожидаем информацию от сервера.. + p Если эта страница долго не отвечает - + | + a(href='#' onclick='window.location.reload(true)') перезагрузите её + | . + +script var transactionNumber = #{transactionNumber}, successUrl = '#{successUrl}', failUrl = '#{failUrl}'; + +script. + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/webmoney/wait'); + xhr.timeout = 20000; + + + xhr.onreadystatechange = function() { + if (xhr.readyState != 4) return; + + location.href = (xhr.responseText == 'success') ? successUrl : failUrl; + }; + + xhr.ontimeout = xhr.onabort = function() { + location.href = failUrl; + }; + xhr.send('transactionNumber=' + transactionNumber); diff --git a/lib/dataUtil.js b/lib/dataUtil.js index 84770caf3..2fea8c548 100644 --- a/lib/dataUtil.js +++ b/lib/dataUtil.js @@ -1,7 +1,7 @@ "use strict"; var mongoose = require('mongoose'); -var log = require('javascript-log')(module); +var log = require('js-log')(); var co = require('co'); var thunk = require('thunkify'); var db = mongoose.connection.db; diff --git a/lib/mongoose.js b/lib/mongoose.js index 4eeebd0d5..71f0eadee 100755 --- a/lib/mongoose.js +++ b/lib/mongoose.js @@ -11,7 +11,7 @@ var mongoose = require('mongoose'); var requireTree = require('require-tree'); var path = require('path'); var fs = require('fs'); -var log = require('javascript-log')(module); +var log = require('js-log')(); var autoIncrement = require('mongoose-auto-increment'); //mongoose.set('debug', true); diff --git a/package.json b/package.json index eef6ffad0..424369fb6 100755 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "precommit": "NODE_PATH=. NODE_ENV=development node --harmony `which gulp` pre-commit", "dependencies": { "MD5": "^1.2.1", + "bluebird": "^2.2.2", "body-parser": "*", "brfs": "^1.1.2", "co": "*", @@ -26,6 +27,8 @@ "gulp-debug": "^0.3.0", "gulp-dir-sync": "^0.1.1", "gulp-if": "^1.2.2", + "gulp-ignore": "^1.1.0", + "gulp-jshint-cache": "*", "gulp-load-plugins": "^0.5.3", "gulp-newer": "^0.3.0", "gulp-notify": "^1.4.0", @@ -42,6 +45,7 @@ "javascript-log": "*", "javascript-parser": "*", "jquery": "^2.1.1", + "js-log": "^0.2.2", "koa": "*", "koa-bodyparser": "*", "koa-csrf": "^2.1.2", @@ -49,6 +53,7 @@ "koa-generic-session": "*", "koa-logger": "*", "koa-mount": "^1.3.0", + "koa-request": "^1.0.0", "koa-router": "*", "koa-session-mongoose": "*", "koa-static": "*", diff --git a/routes/index.js b/routes/index.js index ce6a6ac2d..05bf30721 100755 --- a/routes/index.js +++ b/routes/index.js @@ -1,6 +1,6 @@ 'use strict'; -var log = require('javascript-log')(module); +var log = require('js-log')(); var config = require('config'); var requireTree = require('require-tree'); var controllers = requireTree('../controllers'); diff --git a/setup/csrf.js b/setup/csrf.js index 1d7382ff4..0a33d1b0f 100644 --- a/setup/csrf.js +++ b/setup/csrf.js @@ -5,6 +5,38 @@ const csrf = require('koa-csrf'); module.exports = function(app) { csrf(app); -// manual check to skip api calls -// app.use(csrf.middleware); + if (!app.noCsrf) app.noCsrf = []; + + app.use(function* (next) { + // skip these methods + if (this.method === 'GET' || this.method === 'HEAD' || this.method === 'OPTIONS') { + return yield* next; + } + + // don't check filtered urls + // e.g for access from outside non-browser apis + var checkCsrf = true; + + for (var i=0; i Date: Tue, 22 Jul 2014 00:16:42 +0400 Subject: [PATCH 084/130] Moving markup to jade and stylus --- app/js/base.js | 4 +- app/stylesheets/base.styl | 4 +- .../blocks/corrector/corrector.styl | 4 ++ app/stylesheets/blocks/main/main.styl | 5 +- .../blocks/prev-next/prev-next.styl | 53 +++++++++++++++++++ app/stylesheets/sprite/facebook.styl | 6 +-- app/stylesheets/sprite/home.styl | 6 +-- hmvc/markup/template/blocks/corrector.jade | 2 + .../template/blocks/lesson-next-prev.jade | 9 ---- .../template/blocks/prev-next-bottom.jade | 17 ++++++ .../markup/template/blocks/prev-next-top.jade | 13 +++++ hmvc/markup/template/layouts/base.jade | 4 +- 12 files changed, 106 insertions(+), 21 deletions(-) create mode 100644 app/stylesheets/blocks/corrector/corrector.styl create mode 100644 app/stylesheets/blocks/prev-next/prev-next.styl create mode 100644 hmvc/markup/template/blocks/corrector.jade delete mode 100644 hmvc/markup/template/blocks/lesson-next-prev.jade create mode 100644 hmvc/markup/template/blocks/prev-next-bottom.jade create mode 100644 hmvc/markup/template/blocks/prev-next-top.jade diff --git a/app/js/base.js b/app/js/base.js index 484b99d2a..9c7e23159 100644 --- a/app/js/base.js +++ b/app/js/base.js @@ -232,8 +232,8 @@ function getRandomIdentifier(prefix) { /////////////////////// $(document).keydown(function (e) { - var back = $('.book-navigation .page-previous').eq(0).attr('href'); - var forward = $('.book-navigation .page-next').eq(0).attr('href'); + var back = $('.prev-next .prev-next__prev .prev-next__link').eq(0).attr('href'); + var forward = $('.prev-next .prev-next__next .prev-next__link').eq(0).attr('href'); if (e.ctrlKey || e.metaKey) { switch (e.keyCode) { case 37: diff --git a/app/stylesheets/base.styl b/app/stylesheets/base.styl index 6abe8fb86..92ae8bf61 100755 --- a/app/stylesheets/base.styl +++ b/app/stylesheets/base.styl @@ -46,8 +46,8 @@ // @require "blocks/pager/pager" // @require "blocks/disqus/disqus" @require "blocks/standard-table/standard-table" -// @require "blocks/book-navigation/book-navigation" -// @require "blocks/corrector/corrector" +@require "blocks/prev-next/prev-next" +@require "blocks/corrector/corrector" // @require "blocks/course-type/course-type" // @require "blocks/currency/currency" // @require "blocks/lessons/lessons" diff --git a/app/stylesheets/blocks/corrector/corrector.styl b/app/stylesheets/blocks/corrector/corrector.styl new file mode 100644 index 000000000..38bc3b5a3 --- /dev/null +++ b/app/stylesheets/blocks/corrector/corrector.styl @@ -0,0 +1,4 @@ +.corrector + padding 18px 0 + color #999 + font-style italic \ No newline at end of file diff --git a/app/stylesheets/blocks/main/main.styl b/app/stylesheets/blocks/main/main.styl index 64581d62d..6b9ccab8b 100644 --- a/app/stylesheets/blocks/main/main.styl +++ b/app/stylesheets/blocks/main/main.styl @@ -30,7 +30,7 @@ $main-loud border-bottom 0 margin-top 33px - &__header-title + & &__header-title border-bottom 2px solid #f5f2f0 font 700 40px/40px secondary_font margin 18px 0 12px @@ -72,6 +72,9 @@ $main-loud &__header-comments::before @extend $font-comment margin-right 3px + + &__header-nav + float right &__lesson-nav display table diff --git a/app/stylesheets/blocks/prev-next/prev-next.styl b/app/stylesheets/blocks/prev-next/prev-next.styl new file mode 100644 index 000000000..434938e1e --- /dev/null +++ b/app/stylesheets/blocks/prev-next/prev-next.styl @@ -0,0 +1,53 @@ +.prev-next + display table + table-layout fixed + + &__prev, + &__next + display table-cell + + &__prev + text-align left + padding-right 10px + + &__next + text-align right + padding-left 10px + + &__shortcut + color #999 + font inherit + + &__arr + position relative + top -.1em + + &_top &__link + color #656565 + text-decoration underline + margin 0 .5ex + + &_top &__prev + border-right 2px solid #f5f2f0 + + &_top &__prev:last-child + text-align right + border 0 + padding-right 0 + + &_top &__next:first-child + padding-left 0 + + &_top &__shortcut + font-size 11px + + &_bottom + width 100% + padding 12px 0 + border-bottom 1px solid #ddd + + &_bottom &__shortcut + margin-left .5ex + + &_bottom &__shortcut-wrap + margin-bottom 5px diff --git a/app/stylesheets/sprite/facebook.styl b/app/stylesheets/sprite/facebook.styl index 2a5da7522..786ab70c3 100644 --- a/app/stylesheets/sprite/facebook.styl +++ b/app/stylesheets/sprite/facebook.styl @@ -1,4 +1,4 @@ -$facebook-facebook = 0px 0px 0px 0px 16px 16px 16px 32px '/img/facebook.png?time=1405867629459'; -$facebook-facebook_hover = 0px 16px 0px -16px 16px 16px 16px 32px '/img/facebook.png?time=1405867629459'; +$facebook-facebook = 0px 0px 0px 0px 16px 16px 16px 32px '/img/facebook.png?time=1405969624946'; +$facebook-facebook_hover = 0px 16px 0px -16px 16px 16px 16px 32px '/img/facebook.png?time=1405969624946'; -$facebook = 16px 32px '/img/facebook.png?time=1405867629459' \ No newline at end of file +$facebook = 16px 32px '/img/facebook.png?time=1405969624946' \ No newline at end of file diff --git a/app/stylesheets/sprite/home.styl b/app/stylesheets/sprite/home.styl index b81521851..db2ccc67c 100644 --- a/app/stylesheets/sprite/home.styl +++ b/app/stylesheets/sprite/home.styl @@ -1,4 +1,4 @@ -$home-home = 0px 0px 0px 0px 16px 16px 16px 32px '/img/home.png?time=1405867629595'; -$home-home_hover = 0px 16px 0px -16px 16px 16px 16px 32px '/img/home.png?time=1405867629595'; +$home-home = 0px 0px 0px 0px 16px 16px 16px 32px '/img/home.png?time=1405969625197'; +$home-home_hover = 0px 16px 0px -16px 16px 16px 16px 32px '/img/home.png?time=1405969625197'; -$home = 16px 32px '/img/home.png?time=1405867629595' \ No newline at end of file +$home = 16px 32px '/img/home.png?time=1405969625197' \ No newline at end of file diff --git a/hmvc/markup/template/blocks/corrector.jade b/hmvc/markup/template/blocks/corrector.jade new file mode 100644 index 000000000..d97edf608 --- /dev/null +++ b/hmvc/markup/template/blocks/corrector.jade @@ -0,0 +1,2 @@ +.corrector + Нашли опечатку на сайте? Что-то кажется странным? Выделите соответствующий текст и нажмите Ctrl+Enter \ No newline at end of file diff --git a/hmvc/markup/template/blocks/lesson-next-prev.jade b/hmvc/markup/template/blocks/lesson-next-prev.jade deleted file mode 100644 index 28fbc20e7..000000000 --- a/hmvc/markup/template/blocks/lesson-next-prev.jade +++ /dev/null @@ -1,9 +0,0 @@ -.main__lesson-nav - .main__lesson-nav-prev - kbd - != '(Ctrl+)' - a.main__lesson-nav-link(href='#prev') Предыдущий урок - .main__lesson-nav-next - a.main__lesson-nav-link(href='#next') Следующий урок - kbd - != '(Ctrl+)' \ No newline at end of file diff --git a/hmvc/markup/template/blocks/prev-next-bottom.jade b/hmvc/markup/template/blocks/prev-next-bottom.jade new file mode 100644 index 000000000..bcdcf07f3 --- /dev/null +++ b/hmvc/markup/template/blocks/prev-next-bottom.jade @@ -0,0 +1,17 @@ +.prev-next.prev-next_bottom + .prev-next__prev + .prev-next__shortcut-wrap + | Предыдущий урок + kbd.prev-next__shortcut + | (Ctrl +  + span.prev-next__arr ← + | ) + a.prev-next__link(href="#prev") Введение в JavaScript + .prev-next__next + .prev-next__shortcut-wrap + | Следующий урок + kbd.prev-next__shortcut + | (Ctrl +  + span.prev-next__arr → + | ) + a.prev-next__link(href="#next") Книги по JS, HTML/CSS и не только diff --git a/hmvc/markup/template/blocks/prev-next-top.jade b/hmvc/markup/template/blocks/prev-next-top.jade new file mode 100644 index 000000000..034e8147c --- /dev/null +++ b/hmvc/markup/template/blocks/prev-next-top.jade @@ -0,0 +1,13 @@ +.prev-next.prev-next_top.main__header-nav + .prev-next__prev + kbd.prev-next__shortcut + | (Ctrl+ + span.prev-next__arr ← + | ) + a.prev-next__link(href="#prev") Предыдущий урок + .prev-next__next + a.prev-next__link(href="#next") Следующий урок + kbd.prev-next__shortcut + | (Ctrl+ + span.prev-next__arr → + | ) diff --git a/hmvc/markup/template/layouts/base.jade b/hmvc/markup/template/layouts/base.jade index cb4cd13ab..70c638838 100644 --- a/hmvc/markup/template/layouts/base.jade +++ b/hmvc/markup/template/layouts/base.jade @@ -13,7 +13,9 @@ body(id='#{self.bodyId}') h1.main__header-title= self.title span.main__header-comments a(href='http://learn.javascript.ru/intro#disqus_thread') Комментариев #{self.comments.length} - include ../blocks/lesson-next-prev + include ../blocks/prev-next-top block content include ../blocks/article-foot + include ../blocks/prev-next-bottom + include ../blocks/corrector include ../blocks/scripts \ No newline at end of file From c17f3eb4fabbf4cfae2cb1a1f7c453e590100de2 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Tue, 22 Jul 2014 14:15:07 +0400 Subject: [PATCH 085/130] remote NODE_PATH=. in favor of links under node_modules --- .gitignore | 6 +- app.js | 45 --------------- app/stylesheets/base.styl | 2 +- app/stylesheets/sprite/.gitkeep | 0 app/stylesheets/sprite/facebook.styl | 4 -- app/stylesheets/sprite/home.styl | 4 -- bin/www | 4 +- config/base.js | 42 -------------- gulpfile.js | 19 ++----- hmvc/auth/{model => models}/user.js | 2 +- hmvc/auth/test/unit/model/user.js | 4 +- hmvc/frontpage/templates/index.jade | 4 ++ hmvc/getpdf/controller/main.js | 6 +- hmvc/getpdf/controller/pay.js | 5 +- hmvc/getpdf/index.js | 3 + hmvc/getpdf/paymentMethods.js | 10 ++-- hmvc/getpdf/router.js | 8 +-- hmvc/getpdf/{template => templates}/main.jade | 2 +- .../blocks/breadcrumbs.jade | 0 .../{template => templates}/blocks/head.jade | 0 .../blocks/lesson-next-prev.jade | 0 .../blocks/lesson.jade | 0 .../blocks/lessons.jade | 0 .../blocks/navbar.jade | 0 .../blocks/scripts.jade | 0 .../blocks/social-aside.jade | 0 .../blocks/task-block.jade | 0 .../blocks/top-parts.jade | 0 .../{template => templates}/example/user.jade | 0 .../{template => templates}/layouts/base.jade | 0 .../pages/article.html | 0 .../pages/article.jade | 0 .../{template => templates}/pages/form.jade | 0 .../{template => templates}/pages/my.html | 0 hmvc/payment/index.js | 10 ---- hmvc/tutorial/controller/article.js | 4 +- hmvc/tutorial/controller/task.js | 4 +- hmvc/tutorial/{model => models}/article.js | 6 +- hmvc/tutorial/{model => models}/reference.js | 2 +- hmvc/tutorial/{model => models}/task.js | 2 +- hmvc/tutorial/renderer/referenceResolver.js | 10 ++-- hmvc/tutorial/renderer/taskResolver.js | 7 ++- hmvc/tutorial/router.js | 4 +- {tasks => hmvc/tutorial/tasks}/import.js | 8 +-- .../{template => templates}/article.jade | 0 hmvc/tutorial/test/model/article.js | 3 +- .../tutorial/test/renderer/articleRenderer.js | 4 +- hmvc/tutorial/test/renderer/taskRenderer.js | 4 +- hmvc/webmoney/controller/fail.js | 19 +++---- hmvc/webmoney/controller/result.js | 50 ++++++---------- hmvc/webmoney/controller/success.js | 11 ++-- hmvc/webmoney/controller/wait.js | 8 +-- hmvc/webmoney/index.js | 18 +++--- hmvc/webmoney/router.js | 13 +++-- .../{template => templates}/form.jade | 0 .../{template => templates}/wait.jade | 0 hmvc/webmoney/test/.jshintrc | 23 ++++++++ hmvc/webmoney/test/web/test.js | 9 +++ .../{template => templates}/form.jade | 0 .../{template => templates}/wait.jade | 0 lib/middleware/auth.js | 27 --------- lib/passport.js | 42 -------------- model/.gitkeep | 0 modules/app.js | 52 +++++++++++++++++ modules/config/base.js | 44 ++++++++++++++ {config => modules/config}/index.js | 3 +- {lib => modules/config}/mongoose.js | 7 ++- modules/config/secret.js | 17 ++++++ {config => modules/config}/secret.template.js | 0 modules/lib/bodyParser.js | 57 +++++++++++++++++++ modules/lib/csrf.js | 48 ++++++++++++++++ {lib => modules/lib}/dataUtil.js | 0 {lib => modules/lib}/treeUtil.js | 0 modules/lib/verboseLogger.js | 56 ++++++++++++++++++ modules/payment/index.js | 20 +++++++ .../payment/middleware/loadOrder.js | 3 +- .../payment/middleware/loadTransaction.js | 19 ++++--- .../model => modules/payment/models}/order.js | 2 +- .../payment/models}/transaction.js | 14 +++-- .../payment/models}/transactionLog.js | 4 +- .../setup/accessLogger.js | 0 modules/setup/bodyParser.js | 11 ++++ modules/setup/csrf.js | 12 ++++ .../setup/errorHandler.js | 51 +++-------------- {setup => modules/setup}/render.js | 4 +- modules/setup/router.js | 34 +++++++++++ {setup => modules/setup}/session.js | 4 +- {setup => modules/setup}/static.js | 2 +- modules/setup/verboseLogger.js | 9 +++ package.json | 15 +++-- routes/index.js | 36 ------------ setup/bodyLogger.js | 10 ---- setup/bodyParser.js | 7 --- setup/csrf.js | 42 -------------- setup/headersLogger.js | 11 ---- setup/hmvc.js | 15 ----- setup/mongoose.js | 3 - setup/router.js | 31 ---------- tasks/linkModules.js | 44 ++++++++++++++ .../blocks/breadcrumbs.jade | 0 {template => templates}/blocks/footer.jade | 0 {template => templates}/blocks/head.jade | 0 {template => templates}/blocks/lesson.jade | 0 {template => templates}/blocks/lessons.jade | 0 {template => templates}/blocks/navbar.jade | 0 {template => templates}/blocks/scripts.jade | 0 .../blocks/social-aside.jade | 0 .../blocks/task-block.jade | 0 {template => templates}/blocks/top-parts.jade | 0 .../blocks/tutorial/bottom-navigation.jade | 0 .../blocks/tutorial/footer.jade | 0 .../blocks/tutorial/top-navigation.jade | 0 {template => templates}/error.jade | 0 {template => templates}/index.jade | 0 {template => templates}/layout.jade | 0 {template => templates}/layouts/base.jade | 0 test/mocha.opts | 2 +- 117 files changed, 607 insertions(+), 540 deletions(-) delete mode 100755 app.js delete mode 100644 app/stylesheets/sprite/.gitkeep delete mode 100644 app/stylesheets/sprite/facebook.styl delete mode 100644 app/stylesheets/sprite/home.styl delete mode 100755 config/base.js rename hmvc/auth/{model => models}/user.js (96%) create mode 100644 hmvc/frontpage/templates/index.jade rename hmvc/getpdf/{template => templates}/main.jade (94%) rename hmvc/markup/{template => templates}/blocks/breadcrumbs.jade (100%) rename hmvc/markup/{template => templates}/blocks/head.jade (100%) rename hmvc/markup/{template => templates}/blocks/lesson-next-prev.jade (100%) rename hmvc/markup/{template => templates}/blocks/lesson.jade (100%) rename hmvc/markup/{template => templates}/blocks/lessons.jade (100%) rename hmvc/markup/{template => templates}/blocks/navbar.jade (100%) rename hmvc/markup/{template => templates}/blocks/scripts.jade (100%) rename hmvc/markup/{template => templates}/blocks/social-aside.jade (100%) rename hmvc/markup/{template => templates}/blocks/task-block.jade (100%) rename hmvc/markup/{template => templates}/blocks/top-parts.jade (100%) rename hmvc/markup/{template => templates}/example/user.jade (100%) rename hmvc/markup/{template => templates}/layouts/base.jade (100%) rename hmvc/markup/{template => templates}/pages/article.html (100%) rename hmvc/markup/{template => templates}/pages/article.jade (100%) rename hmvc/markup/{template => templates}/pages/form.jade (100%) rename hmvc/markup/{template => templates}/pages/my.html (100%) delete mode 100644 hmvc/payment/index.js rename hmvc/tutorial/{model => models}/article.js (96%) rename hmvc/tutorial/{model => models}/reference.js (91%) rename hmvc/tutorial/{model => models}/task.js (96%) rename {tasks => hmvc/tutorial/tasks}/import.js (98%) rename hmvc/tutorial/{template => templates}/article.jade (100%) rename hmvc/webmoney/{template => templates}/form.jade (100%) rename hmvc/webmoney/{template => templates}/wait.jade (100%) create mode 100644 hmvc/webmoney/test/.jshintrc create mode 100644 hmvc/webmoney/test/web/test.js rename hmvc/yandexmoney/{template => templates}/form.jade (100%) rename hmvc/yandexmoney/{template => templates}/wait.jade (100%) delete mode 100755 lib/middleware/auth.js delete mode 100755 lib/passport.js delete mode 100644 model/.gitkeep create mode 100644 modules/app.js create mode 100644 modules/config/base.js rename {config => modules/config}/index.js (79%) mode change 100755 => 100644 rename {lib => modules/config}/mongoose.js (98%) mode change 100755 => 100644 create mode 100644 modules/config/secret.js rename {config => modules/config}/secret.template.js (100%) create mode 100644 modules/lib/bodyParser.js create mode 100644 modules/lib/csrf.js rename {lib => modules/lib}/dataUtil.js (100%) rename {lib => modules/lib}/treeUtil.js (100%) create mode 100644 modules/lib/verboseLogger.js create mode 100644 modules/payment/index.js rename hmvc/payment/lib/loadOrderMiddleware.js => modules/payment/middleware/loadOrder.js (87%) rename hmvc/payment/lib/loadTransactionMiddleware.js => modules/payment/middleware/loadTransaction.js (51%) rename {hmvc/payment/model => modules/payment/models}/order.js (91%) rename {hmvc/payment/model => modules/payment/models}/transaction.js (89%) rename {hmvc/payment/model => modules/payment/models}/transactionLog.js (77%) rename setup/logger.js => modules/setup/accessLogger.js (100%) mode change 100755 => 100644 create mode 100644 modules/setup/bodyParser.js create mode 100644 modules/setup/csrf.js rename setup/errors.js => modules/setup/errorHandler.js (60%) mode change 100755 => 100644 rename {setup => modules/setup}/render.js (96%) create mode 100644 modules/setup/router.js rename {setup => modules/setup}/session.js (88%) mode change 100755 => 100644 rename {setup => modules/setup}/static.js (98%) mode change 100755 => 100644 create mode 100644 modules/setup/verboseLogger.js delete mode 100755 routes/index.js delete mode 100644 setup/bodyLogger.js delete mode 100755 setup/bodyParser.js delete mode 100644 setup/csrf.js delete mode 100644 setup/headersLogger.js delete mode 100644 setup/hmvc.js delete mode 100644 setup/mongoose.js delete mode 100755 setup/router.js create mode 100644 tasks/linkModules.js rename {template => templates}/blocks/breadcrumbs.jade (100%) rename {template => templates}/blocks/footer.jade (100%) rename {template => templates}/blocks/head.jade (100%) rename {template => templates}/blocks/lesson.jade (100%) rename {template => templates}/blocks/lessons.jade (100%) rename {template => templates}/blocks/navbar.jade (100%) rename {template => templates}/blocks/scripts.jade (100%) rename {template => templates}/blocks/social-aside.jade (100%) rename {template => templates}/blocks/task-block.jade (100%) rename {template => templates}/blocks/top-parts.jade (100%) rename {template => templates}/blocks/tutorial/bottom-navigation.jade (100%) rename {template => templates}/blocks/tutorial/footer.jade (100%) rename {template => templates}/blocks/tutorial/top-navigation.jade (100%) rename {template => templates}/error.jade (100%) rename {template => templates}/index.jade (100%) rename {template => templates}/layout.jade (100%) rename {template => templates}/layouts/base.jade (100%) diff --git a/.gitignore b/.gitignore index 8d51483ce..64cb1cb7d 100755 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,6 @@ .settings .tmproj .nvmrc -nbproject Thumbs.db # NPM packages folder. @@ -26,9 +25,12 @@ node_modules/ # TMP folder (anything) tmp/ +# contains v8 executable for linux-tick-processor (run from project root) +out/* + # Generated content www/* -app/stylesheets/sprite/* +app/stylesheets/sprites/* # Bower stuff. bower_components/ diff --git a/app.js b/app.js deleted file mode 100755 index 5659a4d72..000000000 --- a/app.js +++ /dev/null @@ -1,45 +0,0 @@ -"use strict"; - -const koa = require('koa'); -const log = require('js-log')();; - - -const app = koa(); - -function requireMiddleware(path) { - // if debug is on => will log the middleware travel chain - if (process.env.NODE_ENV == 'development') { - app.use(function *(next) { - log.debug("middleware " + path); - yield next; - }); - } - require(path)(app); -} - - -requireMiddleware('setup/mongoose'); - -requireMiddleware('setup/static'); - -requireMiddleware('setup/errors'); - -requireMiddleware('setup/logger'); - -requireMiddleware('setup/bodyParser'); - -if (process.env.NODE_ENV == 'development') { - requireMiddleware('setup/headersLogger'); - requireMiddleware('setup/bodyLogger'); -} - -requireMiddleware('setup/session'); -requireMiddleware('setup/csrf'); - - -requireMiddleware('setup/hmvc'); -requireMiddleware('setup/render'); -requireMiddleware('setup/router'); - -module.exports = app; - diff --git a/app/stylesheets/base.styl b/app/stylesheets/base.styl index efc12f6e3..b04482a00 100755 --- a/app/stylesheets/base.styl +++ b/app/stylesheets/base.styl @@ -1,4 +1,4 @@ -@require "sprite/*" +@require "sprites/*" @require "blocks/block_facebook/facebook" @require "blocks/variables/variables" diff --git a/app/stylesheets/sprite/.gitkeep b/app/stylesheets/sprite/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/stylesheets/sprite/facebook.styl b/app/stylesheets/sprite/facebook.styl deleted file mode 100644 index 7512aa58c..000000000 --- a/app/stylesheets/sprite/facebook.styl +++ /dev/null @@ -1,4 +0,0 @@ -$facebook-facebook = 0px 0px 0px 0px 16px 16px 16px 32px '/img/facebook.png?time=1405761478508'; -$facebook-facebook_hover = 0px 16px 0px -16px 16px 16px 16px 32px '/img/facebook.png?time=1405761478508'; - -$facebook = 16px 32px '/img/facebook.png?time=1405761478508' \ No newline at end of file diff --git a/app/stylesheets/sprite/home.styl b/app/stylesheets/sprite/home.styl deleted file mode 100644 index 287607fba..000000000 --- a/app/stylesheets/sprite/home.styl +++ /dev/null @@ -1,4 +0,0 @@ -$home-home = 0px 0px 0px 0px 16px 16px 16px 32px '/img/home.png?time=1405761478581'; -$home-home_hover = 0px 16px 0px -16px 16px 16px 16px 32px '/img/home.png?time=1405761478581'; - -$home = 16px 32px '/img/home.png?time=1405761478581' \ No newline at end of file diff --git a/bin/www b/bin/www index 0882a245a..5561b9ff8 100755 --- a/bin/www +++ b/bin/www @@ -1,9 +1,9 @@ #!/usr/bin/env node 'use strict'; -const log = require('js-log')();; +const log = require('js-log')(); const config = require('config'); -const mongoose = require('lib/mongoose'); +const mongoose = require('config/mongoose'); const app = require('app'); mongoose.waitConnect(function(err) { diff --git a/config/base.js b/config/base.js deleted file mode 100755 index 2e8270c27..000000000 --- a/config/base.js +++ /dev/null @@ -1,42 +0,0 @@ -var path = require('path'); -var fs = require('fs'); - -var secretPath = fs.existsSync(path.join(__dirname, 'secret.js')) ? './secret' : './secret.template'; -var secret = require(secretPath); - -module.exports = function() { - - return { - "port": process.env.PORT || 3000, - "host": process.env.HOST || '0.0.0.0', - "mongoose": { - "uri": "mongodb://localhost/" + (process.env.NODE_ENV == 'test' ? "js_test" : "js"), - "options": { - "server": { - "socketOptions": { - "keepAlive": 1 - }, - "poolSize": 5 - } - } - }, - session: { - keys: [secret.sessionKey] - }, - webmoney: secret.webmoney, - yandexmoney: secret.yandexmoney, - template: { - options: { - 'cache': process.env.NODE_ENV != 'development' - } - }, - crypto: { - hash: { - length: 128, - // may be slow(!): iterations = 12000 take ~60ms to generate strong password - iterations: process.env.NODE_ENV == 'prod' ? 12000 : 1 - } - }, - publicPath: path.join(process.cwd(), 'www') - }; -}; diff --git a/gulpfile.js b/gulpfile.js index 26fd4410a..05a9febec 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,10 +1,12 @@ const gulp = require('gulp'); +const fs = require('fs'); const fse = require('fs-extra'); const spawn = require('child_process').spawn; const gp = require('gulp-load-plugins')(); const debug = require('gulp-debug'); const path = require('path'); const source = require('vinyl-source-stream'); +const linkModules = require('./tasks/linkModules'); const watchify = require('watchify'); //const browserifyTask = require('tasks/browserify'); @@ -64,8 +66,8 @@ gulp.task('clean-compiled-css', function() { gulp.task('import', function(callback) { - const mongoose = require('lib/mongoose'); - const taskImport = require('./tasks/import'); + const mongoose = require('config/mongoose'); + const taskImport = require('tutorial/tasks/import'); taskImport({ root: path.join(path.dirname(__dirname), 'javascript-tutorial'), @@ -77,20 +79,11 @@ gulp.task('import', function(callback) { }); }); -/* - gulp.task('flo', function() { - var node = spawn('node', ['flo.js'], { stdio: 'inherit' }); - node.on('close', function(code) { - if (code === 8) { - gulp.log('Error detected, turning off fb-flo...'); - } - }); - }); - */ +gulp.task('link-modules', linkModules(['modules/*', 'hmvc/*'])); gulp.task('sprite', gp.stylusSprite({ spritesSearchFsRoot: 'app', spritesWebRoot: '/img', spritesFsDir: 'www/img', - styleFsDir: 'app/stylesheets/sprite' + styleFsDir: 'app/stylesheets/sprites' })); diff --git a/hmvc/auth/model/user.js b/hmvc/auth/models/user.js similarity index 96% rename from hmvc/auth/model/user.js rename to hmvc/auth/models/user.js index 326685319..65c1d9a71 100644 --- a/hmvc/auth/model/user.js +++ b/hmvc/auth/models/user.js @@ -53,5 +53,5 @@ schema.path('email').validate(function(value) { // all references using mongoose.model for safe recreation // when I recreate model (for tests) => I can reload it from mongoose.model (single source of truth) // exports are less convenient to update -mongoose.model('User', schema); +module.exports = mongoose.model('User', schema); diff --git a/hmvc/auth/test/unit/model/user.js b/hmvc/auth/test/unit/model/user.js index 3e5b31500..4de1a3391 100644 --- a/hmvc/auth/test/unit/model/user.js +++ b/hmvc/auth/test/unit/model/user.js @@ -1,11 +1,11 @@ var app = require('app'); -var mongoose = require('lib/mongoose'); +var mongoose = require('config/mongoose'); var dataUtil = require('lib/dataUtil'); describe('User', function() { - var User = mongoose.models.User; + var User = require('../../../models/user'); before(function* () { yield dataUtil.createEmptyDb; diff --git a/hmvc/frontpage/templates/index.jade b/hmvc/frontpage/templates/index.jade new file mode 100644 index 000000000..d1b0de973 --- /dev/null +++ b/hmvc/frontpage/templates/index.jade @@ -0,0 +1,4 @@ + +block content + h1= title + p Welcome to #{title} diff --git a/hmvc/getpdf/controller/main.js b/hmvc/getpdf/controller/main.js index 434486499..837b92153 100644 --- a/hmvc/getpdf/controller/main.js +++ b/hmvc/getpdf/controller/main.js @@ -1,6 +1,6 @@ -const mongoose = require('mongoose'); -var Order = mongoose.models.Order; -var Transaction = mongoose.models.Transaction; +const payment = require('payment'); +var Order = payment.Order; +var Transaction = payment.Transaction; exports.get = function*(next) { diff --git a/hmvc/getpdf/controller/pay.js b/hmvc/getpdf/controller/pay.js index d53ccb277..254b584dd 100644 --- a/hmvc/getpdf/controller/pay.js +++ b/hmvc/getpdf/controller/pay.js @@ -1,6 +1,7 @@ var mongoose = require('mongoose'); var log = require('js-log')(); -var Order = mongoose.models.Order; +var payment = require('payment'); +var Order = payment.Order; var methods = require('../paymentMethods').methods; log.debugOn(); @@ -11,7 +12,7 @@ exports.post = function*(next) { if (!method) { this.throw(403, "Unsupported payment method"); } - var methodApi = this.app.hmvc[method.name]; // /hmvc/webmoney + var methodApi = require(method.module); // webmoney if (this.order) { log.debug("order exists", this.order.number); diff --git a/hmvc/getpdf/index.js b/hmvc/getpdf/index.js index f6e5943fb..0e4e17290 100644 --- a/hmvc/getpdf/index.js +++ b/hmvc/getpdf/index.js @@ -3,3 +3,6 @@ var router = require('./router'); exports.middleware = router.middleware(); +exports.onSuccess = function(order) { + console.log("Order success: " + order.number); +}; diff --git a/hmvc/getpdf/paymentMethods.js b/hmvc/getpdf/paymentMethods.js index 31811b581..37296f6f6 100644 --- a/hmvc/getpdf/paymentMethods.js +++ b/hmvc/getpdf/paymentMethods.js @@ -1,7 +1,7 @@ exports.methods = { - 'yandexmoney': {name: "yandexmoney", title: "Яндекс.Деньги"}, - 'webmoney': {name: "webmoney", title: "Webmoney"}, - 'payanyway': {name: "payanyway", title: "PayAnyWay"}, - 'interkassa': {name: "interkassa", title: "Интеркасса"}, - 'paypal': {name: "paypal", title: "Paypal"} + 'yandexmoney': {module: "yandexmoney", title: "Яндекс.Деньги"}, + 'webmoney': {module: "webmoney", title: "Webmoney"}, + 'payanyway': {module: "payanyway", title: "PayAnyWay"}, + 'interkassa': {module: "interkassa", title: "Интеркасса"}, + 'paypal': {module: "paypal", title: "Paypal"} }; diff --git a/hmvc/getpdf/router.js b/hmvc/getpdf/router.js index 5443e41d9..17eab3c45 100644 --- a/hmvc/getpdf/router.js +++ b/hmvc/getpdf/router.js @@ -1,4 +1,4 @@ -var payment = require('../payment'); +var payment = require('payment'); var Router = require('koa-router'); var router = module.exports = new Router(); @@ -8,7 +8,7 @@ var pay = require('./controller/pay'); var success = require('./controller/success'); router.get('', main.get); -router.get('/order/:orderNumber', payment.loadOrderMiddleware('orderNumber'), main.get); +router.get('/order/:orderNumber', payment.middleware.loadOrder(), main.get); -router.post('/pay', payment.loadOrderMiddleware('orderNumber'), pay.post); -router.get('/success/:orderNumber', payment.loadOrderMiddleware('orderNumber'), success.get); +router.post('/pay', payment.middleware.loadOrder(), pay.post); +router.get('/success/:orderNumber', payment.middleware.loadOrder(), success.get); diff --git a/hmvc/getpdf/template/main.jade b/hmvc/getpdf/templates/main.jade similarity index 94% rename from hmvc/getpdf/template/main.jade rename to hmvc/getpdf/templates/main.jade index ce74f6b6b..050b403d8 100644 --- a/hmvc/getpdf/template/main.jade +++ b/hmvc/getpdf/templates/main.jade @@ -8,7 +8,7 @@ div input(name="email" value=order.data.email placeholder="E-mail") select(name="paymentMethod") each paymentMethod in paymentMethods - option(value=paymentMethod.name) #{paymentMethod.title} + option(value=paymentMethod.module) #{paymentMethod.title} input(type="submit" value="Оплатить") script(src="http://code.jquery.com/jquery-2.1.1.js") diff --git a/hmvc/markup/template/blocks/breadcrumbs.jade b/hmvc/markup/templates/blocks/breadcrumbs.jade similarity index 100% rename from hmvc/markup/template/blocks/breadcrumbs.jade rename to hmvc/markup/templates/blocks/breadcrumbs.jade diff --git a/hmvc/markup/template/blocks/head.jade b/hmvc/markup/templates/blocks/head.jade similarity index 100% rename from hmvc/markup/template/blocks/head.jade rename to hmvc/markup/templates/blocks/head.jade diff --git a/hmvc/markup/template/blocks/lesson-next-prev.jade b/hmvc/markup/templates/blocks/lesson-next-prev.jade similarity index 100% rename from hmvc/markup/template/blocks/lesson-next-prev.jade rename to hmvc/markup/templates/blocks/lesson-next-prev.jade diff --git a/hmvc/markup/template/blocks/lesson.jade b/hmvc/markup/templates/blocks/lesson.jade similarity index 100% rename from hmvc/markup/template/blocks/lesson.jade rename to hmvc/markup/templates/blocks/lesson.jade diff --git a/hmvc/markup/template/blocks/lessons.jade b/hmvc/markup/templates/blocks/lessons.jade similarity index 100% rename from hmvc/markup/template/blocks/lessons.jade rename to hmvc/markup/templates/blocks/lessons.jade diff --git a/hmvc/markup/template/blocks/navbar.jade b/hmvc/markup/templates/blocks/navbar.jade similarity index 100% rename from hmvc/markup/template/blocks/navbar.jade rename to hmvc/markup/templates/blocks/navbar.jade diff --git a/hmvc/markup/template/blocks/scripts.jade b/hmvc/markup/templates/blocks/scripts.jade similarity index 100% rename from hmvc/markup/template/blocks/scripts.jade rename to hmvc/markup/templates/blocks/scripts.jade diff --git a/hmvc/markup/template/blocks/social-aside.jade b/hmvc/markup/templates/blocks/social-aside.jade similarity index 100% rename from hmvc/markup/template/blocks/social-aside.jade rename to hmvc/markup/templates/blocks/social-aside.jade diff --git a/hmvc/markup/template/blocks/task-block.jade b/hmvc/markup/templates/blocks/task-block.jade similarity index 100% rename from hmvc/markup/template/blocks/task-block.jade rename to hmvc/markup/templates/blocks/task-block.jade diff --git a/hmvc/markup/template/blocks/top-parts.jade b/hmvc/markup/templates/blocks/top-parts.jade similarity index 100% rename from hmvc/markup/template/blocks/top-parts.jade rename to hmvc/markup/templates/blocks/top-parts.jade diff --git a/hmvc/markup/template/example/user.jade b/hmvc/markup/templates/example/user.jade similarity index 100% rename from hmvc/markup/template/example/user.jade rename to hmvc/markup/templates/example/user.jade diff --git a/hmvc/markup/template/layouts/base.jade b/hmvc/markup/templates/layouts/base.jade similarity index 100% rename from hmvc/markup/template/layouts/base.jade rename to hmvc/markup/templates/layouts/base.jade diff --git a/hmvc/markup/template/pages/article.html b/hmvc/markup/templates/pages/article.html similarity index 100% rename from hmvc/markup/template/pages/article.html rename to hmvc/markup/templates/pages/article.html diff --git a/hmvc/markup/template/pages/article.jade b/hmvc/markup/templates/pages/article.jade similarity index 100% rename from hmvc/markup/template/pages/article.jade rename to hmvc/markup/templates/pages/article.jade diff --git a/hmvc/markup/template/pages/form.jade b/hmvc/markup/templates/pages/form.jade similarity index 100% rename from hmvc/markup/template/pages/form.jade rename to hmvc/markup/templates/pages/form.jade diff --git a/hmvc/markup/template/pages/my.html b/hmvc/markup/templates/pages/my.html similarity index 100% rename from hmvc/markup/template/pages/my.html rename to hmvc/markup/templates/pages/my.html diff --git a/hmvc/payment/index.js b/hmvc/payment/index.js deleted file mode 100644 index 25d597a4c..000000000 --- a/hmvc/payment/index.js +++ /dev/null @@ -1,10 +0,0 @@ -exports.loadOrderMiddleware = require('./lib/loadOrderMiddleware'); -exports.loadTransactionMiddleware = require('./lib/loadTransactionMiddleware'); - -exports.loadMiddleware = function(field) { - return function* (next) { - yield* exports.loadTransactionMiddleware.call(this); - yield* exports.loadOrderMiddleware.call(this); - yield* next; - }; -}; diff --git a/hmvc/tutorial/controller/article.js b/hmvc/tutorial/controller/article.js index 5c92d951a..1578b6a99 100644 --- a/hmvc/tutorial/controller/article.js +++ b/hmvc/tutorial/controller/article.js @@ -1,5 +1,5 @@ const mongoose = require('mongoose'); -const Article = mongoose.models.Article; +const Article = require('../models/article'); const ArticleRenderer = require('../renderer/articleRenderer').ArticleRenderer; const treeUtil = require('lib/treeUtil'); const jade = require('jade'); @@ -7,7 +7,7 @@ const _ = require('lodash'); exports.get = function *get(next) { - const article = yield Article.findOne({ slug: this.params[0] }).exec(); + const article = yield Article.findOne({ slug: this.params.slug }).exec(); if (!article) { yield next; return; diff --git a/hmvc/tutorial/controller/task.js b/hmvc/tutorial/controller/task.js index 9cc4d68dd..182a261da 100644 --- a/hmvc/tutorial/controller/task.js +++ b/hmvc/tutorial/controller/task.js @@ -1,10 +1,10 @@ const mongoose = require('mongoose'); -const Task = mongoose.models.Task; +const Task = require('../models/task'); const TaskRenderer = require('../renderer/taskRenderer').TaskRenderer; exports.get = function *get(next) { const task = yield Task.findOne({ - slug: this.params[0] + slug: this.params.slug }).exec(); if (!task) { diff --git a/hmvc/tutorial/model/article.js b/hmvc/tutorial/models/article.js similarity index 96% rename from hmvc/tutorial/model/article.js rename to hmvc/tutorial/models/article.js index e6001464d..b8b9cdced 100644 --- a/hmvc/tutorial/model/article.js +++ b/hmvc/tutorial/models/article.js @@ -4,6 +4,7 @@ const ObjectId = mongoose.Schema.Types.ObjectId; const Schema = mongoose.Schema; const config = require('config'); const path = require('path'); +const Reference = require('./reference'); const schema = new Schema({ title: { @@ -128,12 +129,11 @@ schema.statics.findTree = function* () { schema.pre('remove', function(next) { // require it here to be sure that Reference model actually exists - const Reference = mongoose.models.Reference; - + // ? Reference.remove({article: this._id}, next); }); schema.plugin(troop.timestamp); -mongoose.model('Article', schema); +module.exports = mongoose.model('Article', schema); diff --git a/hmvc/tutorial/model/reference.js b/hmvc/tutorial/models/reference.js similarity index 91% rename from hmvc/tutorial/model/reference.js rename to hmvc/tutorial/models/reference.js index 8dd706a01..b82103ee0 100644 --- a/hmvc/tutorial/model/reference.js +++ b/hmvc/tutorial/models/reference.js @@ -25,4 +25,4 @@ schema.methods.getUrl = function() { return this.article.getUrl() + '#' + this.anchor; }; -mongoose.model('Reference', schema); +module.exports = mongoose.model('Reference', schema); diff --git a/hmvc/tutorial/model/task.js b/hmvc/tutorial/models/task.js similarity index 96% rename from hmvc/tutorial/model/task.js rename to hmvc/tutorial/models/task.js index fe00bec51..3e9471e3b 100644 --- a/hmvc/tutorial/model/task.js +++ b/hmvc/tutorial/models/task.js @@ -70,6 +70,6 @@ schema.methods.getUrl = function() { schema.plugin(troop.timestamp); -mongoose.model('Task', schema); +module.exports = mongoose.model('Task', schema); diff --git a/hmvc/tutorial/renderer/referenceResolver.js b/hmvc/tutorial/renderer/referenceResolver.js index ec38e894d..17481966f 100644 --- a/hmvc/tutorial/renderer/referenceResolver.js +++ b/hmvc/tutorial/renderer/referenceResolver.js @@ -4,10 +4,10 @@ const ReferenceNode = require('javascript-parser').ReferenceNode; const CompositeTag = require('javascript-parser').CompositeTag; const TextNode = require('javascript-parser').TextNode; const ErrorTag = require('javascript-parser').ErrorTag; -const mongoose = require('lib/mongoose'); -const Reference = mongoose.models.Reference; -const Article = mongoose.models.Article; -const Task = mongoose.models.Task; +const mongoose = require('mongoose'); +const Reference = require('../models/reference'); +const Article = require('../models/article'); +const Task = require('../models/task'); function ReferenceResolver(root) { this.root = root; @@ -59,4 +59,4 @@ ReferenceResolver.prototype.resolve = function* (value) { return article && {title: article.title, url: article.getUrl()}; }; -exports.ReferenceResolver = ReferenceResolver; \ No newline at end of file +exports.ReferenceResolver = ReferenceResolver; diff --git a/hmvc/tutorial/renderer/taskResolver.js b/hmvc/tutorial/renderer/taskResolver.js index b8fb35a46..35fb78d94 100644 --- a/hmvc/tutorial/renderer/taskResolver.js +++ b/hmvc/tutorial/renderer/taskResolver.js @@ -5,9 +5,10 @@ const CompositeTag = require('javascript-parser').CompositeTag; const TextNode = require('javascript-parser').TextNode; const ErrorTag = require('javascript-parser').ErrorTag; const mongoose = require('mongoose'); -const Reference = mongoose.models.Reference; -const Article = mongoose.models.Article; -const Task = mongoose.models.Task; +const Reference = require('../models/reference'); +const Article = require('../models/article'); +const Task = require('../models/task'); + function TaskResolver(root) { this.root = root; diff --git a/hmvc/tutorial/router.js b/hmvc/tutorial/router.js index bdaa70c3f..d3f854a22 100644 --- a/hmvc/tutorial/router.js +++ b/hmvc/tutorial/router.js @@ -5,6 +5,6 @@ var article = require('./controller/article'); var router = module.exports = new Router(); -router.get(/^\/task\/(.*)$/, task.get); -router.get(/^\/(.*)$/, article.get); +router.get('/task/:slug', task.get); +router.get('/:slug', article.get); diff --git a/tasks/import.js b/hmvc/tutorial/tasks/import.js similarity index 98% rename from tasks/import.js rename to hmvc/tutorial/tasks/import.js index 963571cef..7a4d441a7 100644 --- a/tasks/import.js +++ b/hmvc/tutorial/tasks/import.js @@ -4,10 +4,10 @@ const fse = require('fs-extra'); const path = require('path'); const config = require('config'); const log = require('js-log')(); -const mongoose = require('lib/mongoose'); -const Article = mongoose.models.Article; -const Reference = mongoose.models.Reference; -const Task = mongoose.models.Task; +const mongoose = require('mongoose'); +const Article = require('../models/article'); +const Reference = require('../models/reference'); +const Task = require('../models/task'); const BodyParser = require('javascript-parser').BodyParser; const HtmlTransformer = require('javascript-parser').HtmlTransformer; const TreeWalker = require('javascript-parser').TreeWalker; diff --git a/hmvc/tutorial/template/article.jade b/hmvc/tutorial/templates/article.jade similarity index 100% rename from hmvc/tutorial/template/article.jade rename to hmvc/tutorial/templates/article.jade diff --git a/hmvc/tutorial/test/model/article.js b/hmvc/tutorial/test/model/article.js index 401a7048f..0f296ebe4 100644 --- a/hmvc/tutorial/test/model/article.js +++ b/hmvc/tutorial/test/model/article.js @@ -5,8 +5,7 @@ var mongoose = require('mongoose'); var assert = require('assert'); var path = require('path'); var treeUtil = require('lib/treeUtil'); -var Article = mongoose.models.Article; - +var Article = require('../../models/article'); describe('Article', function() { diff --git a/hmvc/tutorial/test/renderer/articleRenderer.js b/hmvc/tutorial/test/renderer/articleRenderer.js index 8abdb4afd..d0cd9b835 100644 --- a/hmvc/tutorial/test/renderer/articleRenderer.js +++ b/hmvc/tutorial/test/renderer/articleRenderer.js @@ -1,8 +1,8 @@ const app = require('app'); const ArticleRenderer = require('../../renderer/articleRenderer').ArticleRenderer; -const mongoose = require('lib/mongoose'); -const Article = mongoose.models.Article; +const mongoose = require('config/mongoose'); +const Article = require('../../models/article'); describe("ArticleRenderer", function() { diff --git a/hmvc/tutorial/test/renderer/taskRenderer.js b/hmvc/tutorial/test/renderer/taskRenderer.js index 05c139823..867110cb3 100644 --- a/hmvc/tutorial/test/renderer/taskRenderer.js +++ b/hmvc/tutorial/test/renderer/taskRenderer.js @@ -1,8 +1,8 @@ const app = require('app'); const TaskRenderer = require('../../renderer/taskRenderer').TaskRenderer; -const mongoose = require('lib/mongoose'); -const Task = mongoose.models.Task; +const mongoose = require('config/mongoose'); +const Task = require('../../models/task'); describe("TaskRenderer", function() { diff --git a/hmvc/webmoney/controller/fail.js b/hmvc/webmoney/controller/fail.js index 4c2c6311c..a041f3366 100644 --- a/hmvc/webmoney/controller/fail.js +++ b/hmvc/webmoney/controller/fail.js @@ -1,8 +1,7 @@ -const config = require('config'); const mongoose = require('mongoose'); -const Order = mongoose.models.Order; -const Transaction = mongoose.models.Transaction; -const TransactionLog = mongoose.models.TransactionLog; +const payment = require('payment'); +const Order = payment.Order; +const Transaction = payment.Transaction; const log = require('js-log')(); const md5 = require('MD5'); @@ -11,15 +10,13 @@ log.debugOn(); exports.get = function* (next) { - this.transaction.status = Transaction.STATUS_FAIL; - yield this.transaction.persist(); + yield this.transaction.persist({ + status: Transaction.STATUS_FAIL + }); - yield new TransactionLog({ - event: 'fail', - transaction: this.transaction._id - }).persist(); + yield this.transaction.log({ event: 'fail' }); var order = this.transaction.order; - this.redirect('/' + order.module + '/order/' + order.number); + this.redirect(payment.getOrderUrl(order)); }; diff --git a/hmvc/webmoney/controller/result.js b/hmvc/webmoney/controller/result.js index 059195a31..6a5f24a0f 100644 --- a/hmvc/webmoney/controller/result.js +++ b/hmvc/webmoney/controller/result.js @@ -1,14 +1,11 @@ -const payment = require('../../payment'); +const payment = require('payment'); const config = require('config'); const mongoose = require('mongoose'); -const Order = mongoose.models.Order; -const Transaction = mongoose.models.Transaction; -const TransactionLog = mongoose.models.TransactionLog; +const Order = payment.Order; +const Transaction = payment.Transaction; const log = require('js-log')(); const md5 = require('MD5'); - - log.debugOn(); // ONLY ACCESSED from WEBMONEY SERVER @@ -16,21 +13,13 @@ exports.prerequest = function* (next) { log.debug("prerequest"); - var transaction = yield Transaction.findOne({number: this.request.body.LMI_PAYMENT_NO}).exec(); - - if (!transaction) { - log.debug("no transaction " + this.request.body.LMI_PAYMENT_NO); - this.throw(404, 'transaction not found'); - } - - yield new TransactionLog().persist({ - transaction: transaction._id, - event: 'prerequest', - data: {url: this.request.originalUrl, body: this.request.body} + yield this.transaction.log({ + event: 'prerequest', + data: {url: this.request.originalUrl, body: this.request.body} }); - if (transaction.status == Transaction.STATUS_SUCCESS || - transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) || + if (this.transaction.status == Transaction.STATUS_SUCCESS || + this.transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) || this.request.body.LMI_PAYEE_PURSE != config.webmoney.purse ) { log.debug("no pending transaction " + this.request.body.LMI_PAYMENT_NO); @@ -53,28 +42,25 @@ exports.post = function* (next) { this.throw(403, "wrong signature"); } - var transaction = yield Transaction.findOne({number: this.request.body.LMI_PAYMENT_NO}).exec(); - - if (!transaction) { - this.throw(404, 'transaction not found'); - } - - yield new TransactionLog().persist({ - transaction: transaction._id, - event: 'result', - data: {url: this.request.originalUrl, body: this.request.body} + yield this.transaction.log({ + event: 'result', + data: {url: this.request.originalUrl, body: this.request.body} }); - if (transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) || + if (this.transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) || this.request.body.LMI_PAYEE_PURSE != config.webmoney.purse) { this.throw(404, 'transaction with given params not found'); } if (!this.request.body.LMI_SIM_MODE || this.request.body.LMI_SIM_MODE == '0') { - transaction.status = Transaction.STATUS_SUCCESS; - yield transaction.persist(); + this.transaction.status = Transaction.STATUS_SUCCESS; + yield this.transaction.persist(); } + var order = this.transaction.order; + log.debug("will call order onSuccess module=" + order.module); + require(order.module).onSuccess(order); + this.body = 'OK'; }; diff --git a/hmvc/webmoney/controller/success.js b/hmvc/webmoney/controller/success.js index 9a80bdde0..564c1ad7b 100644 --- a/hmvc/webmoney/controller/success.js +++ b/hmvc/webmoney/controller/success.js @@ -1,9 +1,7 @@ -const payment = require('../../payment'); const config = require('config'); const mongoose = require('mongoose'); -const Order = mongoose.models.Order; -const Transaction = mongoose.models.Transaction; -const TransactionLog = mongoose.models.TransactionLog; +const payment = require('payment'); +const Transaction = payment.Transaction; const log = require('js-log')(); const md5 = require('MD5'); @@ -14,8 +12,9 @@ exports.get = function* (next) { var transaction = this.transaction; var order = this.transaction.order; - var successUrl = '/' + order.module + '/success/' + order.number; - var failUrl = '/' + order.module + '/order/' + order.number; + + var successUrl = payment.getOrderSuccessUrl(order); + var failUrl = payment.getOrderUrl(order); log.debug("transaction status: " + transaction.status); diff --git a/hmvc/webmoney/controller/wait.js b/hmvc/webmoney/controller/wait.js index 4228e2aa1..ef5b1e95b 100644 --- a/hmvc/webmoney/controller/wait.js +++ b/hmvc/webmoney/controller/wait.js @@ -1,16 +1,12 @@ -const payment = require('../../payment'); +const payment = require('payment'); const config = require('config'); const mongoose = require('mongoose'); -const Order = mongoose.models.Order; -const Transaction = mongoose.models.Transaction; -const TransactionLog = mongoose.models.TransactionLog; +const Transaction = payment.Transaction; const log = require('js-log')(); const md5 = require('MD5'); log.debugOn(); - - exports.post = function* (next) { var attempt = 0; diff --git a/hmvc/webmoney/index.js b/hmvc/webmoney/index.js index 5a71f2689..ce6f0c53d 100644 --- a/hmvc/webmoney/index.js +++ b/hmvc/webmoney/index.js @@ -1,8 +1,9 @@ -const config = require('config'); const jade = require('jade'); const path = require('path'); -var mongoose = require('mongoose'); -var Transaction = mongoose.models.Transaction; +var config = require('config'); +var payment = require('payment'); + +var Transaction = payment.Transaction; var router = require('./router'); @@ -11,17 +12,20 @@ exports.middleware = router.middleware(); exports.createTransactionForm = function* (order) { var transaction = new Transaction({ - order: order._id, - amount: order.amount, - paymentType: 'webmoney' + order: order._id, + amount: order.amount, + module: 'webmoney' }); yield transaction.persist(); - return jade.renderFile(path.join(__dirname, 'template/form.jade'), { + return jade.renderFile(path.join(__dirname, 'templates/form.jade'), { amount: transaction.amount, number: transaction.number, purse: config.webmoney.purse }); }; + + + diff --git a/hmvc/webmoney/router.js b/hmvc/webmoney/router.js index 721de7a5b..4ab89e218 100644 --- a/hmvc/webmoney/router.js +++ b/hmvc/webmoney/router.js @@ -1,5 +1,5 @@ var Router = require('koa-router'); -var payment = require('../payment'); +var payment = require('payment'); var router = module.exports = new Router(); @@ -9,14 +9,17 @@ var fail = require('./controller/fail'); var wait = require('./controller/wait'); // webmoney server posts here (in background) -router.post('/result', result.post); +router.post('/result', + payment.middleware.loadTransaction('LMI_PAYMENT_NO', {skipOwnerCheck : true}), + result.post +); // webmoney server redirects here if payment successful -router.get('/success', payment.loadTransactionMiddleware('LMI_PAYMENT_NO'), success.get); +router.get('/success', payment.middleware.loadTransaction('LMI_PAYMENT_NO'), success.get); // but if transaction status is not yet received, we wait... -router.post('/wait', payment.loadTransactionMiddleware(), wait.post); +router.post('/wait', payment.middleware.loadTransaction(), wait.post); // webmoney server redirects here if payment failed -router.get('/fail', payment.loadTransactionMiddleware('LMI_PAYMENT_NO'), fail.get); +router.get('/fail', payment.middleware.loadTransaction('LMI_PAYMENT_NO'), fail.get); diff --git a/hmvc/webmoney/template/form.jade b/hmvc/webmoney/templates/form.jade similarity index 100% rename from hmvc/webmoney/template/form.jade rename to hmvc/webmoney/templates/form.jade diff --git a/hmvc/webmoney/template/wait.jade b/hmvc/webmoney/templates/wait.jade similarity index 100% rename from hmvc/webmoney/template/wait.jade rename to hmvc/webmoney/templates/wait.jade diff --git a/hmvc/webmoney/test/.jshintrc b/hmvc/webmoney/test/.jshintrc new file mode 100644 index 000000000..077663629 --- /dev/null +++ b/hmvc/webmoney/test/.jshintrc @@ -0,0 +1,23 @@ +{ + "maxerr": 25, + "latedef": "nofunc", + "browser": false, + "node": true, + "indent": 2, + "camelcase": true, + "newcap": true, + "undef": true, + "esnext": true, + "multistr": true, + "noyield": true, + "expr": true, + "-W004": true, + "globals" : { + "describe" : false, + "it" : false, + "before" : false, + "beforeEach" : false, + "after" : false, + "afterEach" : false + } +} diff --git a/hmvc/webmoney/test/web/test.js b/hmvc/webmoney/test/web/test.js new file mode 100644 index 000000000..c573e07a7 --- /dev/null +++ b/hmvc/webmoney/test/web/test.js @@ -0,0 +1,9 @@ +var app = require('app'); + +describe("Test", function() { + + it("works", function() { + //console.log("WORKS"); + }); + +}); diff --git a/hmvc/yandexmoney/template/form.jade b/hmvc/yandexmoney/templates/form.jade similarity index 100% rename from hmvc/yandexmoney/template/form.jade rename to hmvc/yandexmoney/templates/form.jade diff --git a/hmvc/yandexmoney/template/wait.jade b/hmvc/yandexmoney/templates/wait.jade similarity index 100% rename from hmvc/yandexmoney/template/wait.jade rename to hmvc/yandexmoney/templates/wait.jade diff --git a/lib/middleware/auth.js b/lib/middleware/auth.js deleted file mode 100755 index d5c17a2ca..000000000 --- a/lib/middleware/auth.js +++ /dev/null @@ -1,27 +0,0 @@ -/* FROM old site, todo: adapt for koa or JUNK! */ -var HttpError = require('lib/error').HttpError; - -exports.mustBeAuthenticated = function(req, res, next) { - if (req.isAuthenticated()) { - next(); - } else { - return next(403); - } -}; - -exports.userIdMustBeCurrentUser = function(req, res, next) { - if (req.params.userId != req.user.id) { - next(403); - } else { - next(); - } -}; - -exports.mustBeAnonymous = function(req, res, next) { - if (req.isAuthenticated()) { - res.redirect('/'); - } else { - next(); - } - -}; \ No newline at end of file diff --git a/lib/passport.js b/lib/passport.js deleted file mode 100755 index 912f974cf..000000000 --- a/lib/passport.js +++ /dev/null @@ -1,42 +0,0 @@ -var passport = require("passport"); -var mongoose = require('mongoose'); -var LocalStrategy = require('passport-local').Strategy; - -var User = mongoose.models.User; - -passport.serializeUser(function(user, done) { - done(null, user.id); -}); - -passport.deserializeUser(function(id, done) { - User.findOne({ _id: id }, function(err, user) { - done(err, user); - }); -}); - -passport.use(new LocalStrategy({ - usernameField: 'email', - passwordField: 'password' - }, - function(email, password, done) { - if (!email) return done(null, false, { message: 'Please provide email.' }); - if (!password) return done(null, false, { message: 'Please provide password.' }); - - User.findOne({email: email}, function(err, user) { - if (err) return done(err); - if (!user) return done(null, false, { message: 'Non-registered email.' }); - - if (user.checkPassword(password)) { - return done(null, user); - } else { - done(null, false, { - message: 'Incorrect password.' - }); - } - }); - } -)); - - - -module.exports = passport; diff --git a/model/.gitkeep b/model/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/modules/app.js b/modules/app.js new file mode 100644 index 000000000..6b74aba4e --- /dev/null +++ b/modules/app.js @@ -0,0 +1,52 @@ +"use strict"; + +const koa = require('koa'); +const log = require('js-log')(); +const config = require('config'); +const app = koa(); + +function requireSetup(path) { + // if debug is on => will log the middleware travel chain + if (process.env.NODE_ENV == 'development') { + app.use(function *(next) { + log.debug("-> setup " + path); + yield next; + log.debug("<- setup " + path); + }); + } + require(path)(app); +} + +// usually nginx will handle this +requireSetup('setup/static'); + +// errors wrap everything +requireSetup('setup/errorHandler'); + +// this logger only logs HTTP status and URL +// before everything to make sure it log all +requireSetup('setup/accessLogger'); + +// before anything that may deal with body +requireSetup('setup/bodyParser'); + +// right after parsing body, make sure we logged for development +requireSetup('setup/verboseLogger'); + +if (process.env.NODE_ENV == 'development') { +// app.verboseLogger.addPath('/:any*'); +} + +requireSetup('setup/session'); +requireSetup('setup/csrf'); + +requireSetup('setup/render'); +requireSetup('setup/router'); + +if (process.env.NODE_ENV == 'test') { + app.listen(config.port, config.host, function() { + console.log("App listening..."); + }); +} + +module.exports = app; diff --git a/modules/config/base.js b/modules/config/base.js new file mode 100644 index 000000000..6aae53fad --- /dev/null +++ b/modules/config/base.js @@ -0,0 +1,44 @@ +var path = require('path'); +var fs = require('fs'); + +var secretPath = fs.existsSync(path.join(__dirname, 'secret.js')) ? './secret' : './secret.template'; +var secret = require(secretPath); + +module.exports = { + "port": process.env.PORT || 3000, + "host": process.env.HOST || '0.0.0.0', + "mongoose": { + "uri": "mongodb://localhost/" + (process.env.NODE_ENV == 'test' ? "js_test" : "js"), + "options": { + "server": { + "socketOptions": { + "keepAlive": 1 + }, + "poolSize": 5 + } + } + }, + session: { + keys: [secret.sessionKey] + }, + /* + verboseLogger: { + paths: ['/webmoney/any*'] + }, + */ + webmoney: secret.webmoney, + yandexmoney: secret.yandexmoney, + template: { + options: { + 'cache': process.env.NODE_ENV != 'development' + } + }, + crypto: { + hash: { + length: 128, + // may be slow(!): iterations = 12000 take ~60ms to generate strong password + iterations: process.env.NODE_ENV == 'prod' ? 12000 : 1 + } + }, + publicPath: path.join(process.cwd(), 'www') +}; diff --git a/config/index.js b/modules/config/index.js old mode 100755 new mode 100644 similarity index 79% rename from config/index.js rename to modules/config/index.js index 7fddc968e..877de2cdd --- a/config/index.js +++ b/modules/config/index.js @@ -9,7 +9,6 @@ if (process.env.NODE_ENV == 'development' && process.env.DEV_TRACE) { require('clarify'); // Exclude node internal calls from the stack } -var base = require('./base')(); -//var env = require('./env/' + process.env.NODE_ENV)(base); +var base = require('./base'); module.exports = base; diff --git a/lib/mongoose.js b/modules/config/mongoose.js old mode 100755 new mode 100644 similarity index 98% rename from lib/mongoose.js rename to modules/config/mongoose.js index 71f0eadee..567a3aa14 --- a/lib/mongoose.js +++ b/modules/config/mongoose.js @@ -8,7 +8,6 @@ */ var mongoose = require('mongoose'); -var requireTree = require('require-tree'); var path = require('path'); var fs = require('fs'); var log = require('js-log')(); @@ -83,10 +82,11 @@ mongoose.waitConnect = function(callback) { } }; +module.exports = mongoose; - +/* // models may want lib/mongoose that's why we require them AFTER module.exports = mongoose -module.exports = mongoose; + // read ALL models NOW, so that any hmvc app may require a model of another app requireModels(); @@ -103,3 +103,4 @@ function requireModels() { }); } +*/ diff --git a/modules/config/secret.js b/modules/config/secret.js new file mode 100644 index 000000000..174511fff --- /dev/null +++ b/modules/config/secret.js @@ -0,0 +1,17 @@ +// this file contains all passwords etc, +// should not be in repo + +exports.sessionKey = "KillerIsJim"; + +exports.webmoney = { + secretKey: 'hjvRVxstw42VDdpk9', + purse: 'R146240663944' +}; + +exports.yandexmoney = { + redirectUri: 'http://stage.javascript.ru/yandexmoney/back', + clientId: '6527BEA7C6189BF55A46FB379E642E2A20098792D24E98939540B0870F5B2228', + clientSecret: '581E6A7542381F4F206789140E39A73CFB4C39A9C625127D9A731A5A95C2D1B8FE0E26E267E829C95A76F3313E3DD9F02CC223E890972E75952A70C93DCBEDBF', + purse: '4100155697197' +}; + diff --git a/config/secret.template.js b/modules/config/secret.template.js similarity index 100% rename from config/secret.template.js rename to modules/config/secret.template.js diff --git a/modules/lib/bodyParser.js b/modules/lib/bodyParser.js new file mode 100644 index 000000000..a97cc4454 --- /dev/null +++ b/modules/lib/bodyParser.js @@ -0,0 +1,57 @@ +'use strict'; + +const koaBodyParser = require('koa-bodyparser'); +const _ = require('lodash'); +const pathToRegexp = require('path-to-regexp'); +const log = require('js-log')(); + +/** + * Wrapper around koa-bodyparser + * allows to set per-path options which are used in middleware + * usage: + * + * app.bodyParser = new BodyParser + * app.use(app.bodyParser.middleware()) + * ... + * app.bodyParser.addPathOptions('/upload/path', {limit: 1e10}); + * @constructor + */ +function BodyParser() { + this.pathOptions = []; +} + +// options should be an object { path: string|regexp, options } +BodyParser.prototype.addPathOptions = function(path, options) { + if (path instanceof RegExp) { + this.pathOptions.push({path: path, options: options}); + } else if (typeof path == 'string') { + this.pathOptions.push({path: pathToRegexp(path), options: options}); + } else { + throw new Error("unsupported path type: " + path); + } +}; + +BodyParser.prototype.middleware = function() { + + var self = this; + var optionsDefault = { limit: 1e6 }; + + return function* (next) { + var options = Object.create(optionsDefault); + for (var i = 0; i < self.pathOptions.length; i++) { + var path = self.pathOptions[i].path; + log.debug("test " + this.req.url + " against " + path); + if (path.test(this.req.url)) { + log.debug("found options", self.pathOptions[i].options); + _.assign(options, self.pathOptions[i].options); + break; + } + } + + yield* koaBodyParser(options).call(this, next); + + }; +}; + + +exports.BodyParser = BodyParser; diff --git a/modules/lib/csrf.js b/modules/lib/csrf.js new file mode 100644 index 000000000..22b93a31b --- /dev/null +++ b/modules/lib/csrf.js @@ -0,0 +1,48 @@ +const pathToRegexp = require('path-to-regexp'); +const log = require('js-log')(); + +function Csrf() { + this.ignorePaths = []; +} + + +// csrf.addIgnore adds a path into "disabled csrf" list +Csrf.prototype.addIgnorePath = function(path) { + if (path instanceof RegExp) { + this.ignorePaths.push(path); + } else if (typeof path == 'string') { + this.ignorePaths.push(pathToRegexp(path)); + } else { + throw new Error("unsupported path type: " + path); + } +}; + +Csrf.prototype.middleware = function() { + var self = this; + + return function*(next) { + // skip these methods + if (this.method === 'GET' || this.method === 'HEAD' || this.method === 'OPTIONS') { + return yield* next; + } + + var checkCsrf = true; + for (var i = 0; i < self.ignorePaths.length; i++) { + var path = self.ignorePaths[i]; + log.debug("test " + this.req.url + " against " + path); + if (path.test(this.req.url)) { + log.debug("match found, disable csrf check"); + checkCsrf = false; + break; + } + } + + if (checkCsrf) { + this.assertCSRF(this.request.body); + } + + yield* next; + }; +}; + +exports.Csrf = Csrf; diff --git a/lib/dataUtil.js b/modules/lib/dataUtil.js similarity index 100% rename from lib/dataUtil.js rename to modules/lib/dataUtil.js diff --git a/lib/treeUtil.js b/modules/lib/treeUtil.js similarity index 100% rename from lib/treeUtil.js rename to modules/lib/treeUtil.js diff --git a/modules/lib/verboseLogger.js b/modules/lib/verboseLogger.js new file mode 100644 index 000000000..5272e9c11 --- /dev/null +++ b/modules/lib/verboseLogger.js @@ -0,0 +1,56 @@ +const pathToRegexp = require('path-to-regexp'); +const log = require('js-log')(); + +function VerboseLogger() { + this.logPaths = []; +} + + +// csrf.addIgnore adds a path into "disabled csrf" list +VerboseLogger.prototype.addPath = function(path) { + if (path instanceof RegExp) { + this.logPaths.push(path); + } else if (typeof path == 'string') { + this.logPaths.push(pathToRegexp(path)); + } else { + throw new Error("unsupported path type: " + path); + } +}; + +VerboseLogger.prototype.middleware = function() { + var self = this; + + return function*(next) { + + var shouldLog = false; + for (var i = 0; i < self.logPaths.length; i++) { + var path = self.logPaths[i]; + log.debug("test " + this.req.url + " against " + path); + if (path.test(this.req.url)) { + log.debug("match found, will log all"); + shouldLog = true; + break; + } + } + + if (shouldLog) { + self.log(this); + } + + yield* next; + }; +}; + +VerboseLogger.prototype.log = function(context) { + + for (var name in context.req.headers) { + console.log(name + ": " + context.req.headers[name]); + } + + if (context.request.body) { + console.log(context.request.body); + } + +}; + +exports.VerboseLogger = VerboseLogger; diff --git a/modules/payment/index.js b/modules/payment/index.js new file mode 100644 index 000000000..9d883f862 --- /dev/null +++ b/modules/payment/index.js @@ -0,0 +1,20 @@ +exports.middleware = { + loadOrder: require('./middleware/loadOrder'), + loadTransaction: require('./middleware/loadTransaction') +}; + +exports.Order = require('./models/order'); +exports.Transaction = require('./models/transaction'); +exports.TransactionLog = require('./models/transactionLog'); + +exports.getOrderSuccessUrl = function(order) { + return '/' + order.module + '/success/' + order.number; +}; +exports.getOrderUrl = function(order) { + return '/' + order.module + '/order/' + order.number; +}; + +exports.getOrderPendingUrl = function(order) { + return '/' + order.module + '/pending/' + order.number; +}; + diff --git a/hmvc/payment/lib/loadOrderMiddleware.js b/modules/payment/middleware/loadOrder.js similarity index 87% rename from hmvc/payment/lib/loadOrderMiddleware.js rename to modules/payment/middleware/loadOrder.js index 79e78935f..6d6f532a9 100644 --- a/hmvc/payment/lib/loadOrderMiddleware.js +++ b/modules/payment/middleware/loadOrder.js @@ -1,6 +1,7 @@ var mongoose = require('mongoose'); -var Order = mongoose.models.Order; +var Order = require('../models/order'); +// Populates this.order with the order by "orderNumber" parameter module.exports = function(field) { if (!field) field = 'orderNumber'; diff --git a/hmvc/payment/lib/loadTransactionMiddleware.js b/modules/payment/middleware/loadTransaction.js similarity index 51% rename from hmvc/payment/lib/loadTransactionMiddleware.js rename to modules/payment/middleware/loadTransaction.js index 286b54f7b..c3e042143 100644 --- a/hmvc/payment/lib/loadTransactionMiddleware.js +++ b/modules/payment/middleware/loadTransaction.js @@ -1,11 +1,11 @@ var mongoose = require('mongoose'); -var Transaction = mongoose.models.Transaction; +var Transaction = require('../models/transaction'); var log = require('js-log')(); -log.debugOn(); - -module.exports = function(field) { - +// Populates this.transaction with the transaction by "transactionNumber" parameter +// options.skipOwnerCheck is for signed submissions, set to true allows anyone to load transaction +module.exports = function(field, options) { + options = options || {}; if (!field) field = 'transactionNumber'; return function* (next) { @@ -23,10 +23,11 @@ module.exports = function(field) { this.throw(404, 'Нет такой транзакции'); } - // todo: add belongs to check (with auth) - console.log("NUM", transaction.order.number, this.session); - if (!this.session.orders || this.session.orders.indexOf(transaction.order.number) == -1) { - this.throw(403, 'Не найден заказ в сессии для этой транзакции'); + if (!options.skipOwnerCheck) { + // todo: add belongs to check (with auth) + if (!this.session.orders || this.session.orders.indexOf(transaction.order.number) == -1) { + this.throw(403, 'Не найден заказ в сессии для этой транзакции'); + } } this.transaction = transaction; diff --git a/hmvc/payment/model/order.js b/modules/payment/models/order.js similarity index 91% rename from hmvc/payment/model/order.js rename to modules/payment/models/order.js index 2bc21c3ca..bf407ca13 100644 --- a/hmvc/payment/model/order.js +++ b/modules/payment/models/order.js @@ -23,5 +23,5 @@ var schema = new Schema({ schema.plugin(autoIncrement.plugin, {model: 'Order', field: 'number'}); -mongoose.model('Order', schema); +module.exports = mongoose.model('Order', schema); diff --git a/hmvc/payment/model/transaction.js b/modules/payment/models/transaction.js similarity index 89% rename from hmvc/payment/model/transaction.js rename to modules/payment/models/transaction.js index 9d25ab48b..d25346e62 100644 --- a/hmvc/payment/model/transaction.js +++ b/modules/payment/models/transaction.js @@ -1,6 +1,8 @@ var mongoose = require('mongoose'); var Schema = mongoose.Schema; var autoIncrement = require('mongoose-auto-increment'); +var Order = require('./order'); +var TransactionLog = require('./transactionLog'); /** * Transaction is an actual payment for something @@ -17,7 +19,7 @@ var schema = new Schema({ type: Number, required: true }, - paymentType: { + module: { type: String, required: true }, @@ -42,9 +44,9 @@ schema.statics.STATUS_FAIL = 'fail'; schema.pre('save', function (next) { if (this.status == Transaction.STATUS_SUCCESS) { var orderId = this.order._id || this.order; - Order.findByIdAndUpdate(orderId, {status: Transaction.STATUS_SUCCESS}, function(err) { - if (err) throw(err); - }); + Order.findByIdAndUpdate(orderId, {status: Transaction.STATUS_SUCCESS}, next); + } else { + next(); } }); @@ -62,10 +64,10 @@ schema.methods.getStatusDescription = function() { schema.methods.log = function*(options) { options.transaction = this._id; - var log = new mongoose.models.TransactionLog(options); + var log = new TransactionLog(options); yield log.persist(); }; /* jshint -W003 */ var Transaction = module.exports = mongoose.model('Transaction', schema); -var Order = mongoose.models.Order; + diff --git a/hmvc/payment/model/transactionLog.js b/modules/payment/models/transactionLog.js similarity index 77% rename from hmvc/payment/model/transactionLog.js rename to modules/payment/models/transactionLog.js index 08cd284c1..39dbfe8ec 100644 --- a/hmvc/payment/model/transactionLog.js +++ b/modules/payment/models/transactionLog.js @@ -1,5 +1,4 @@ var mongoose = require('mongoose'); - var Schema = mongoose.Schema; var schema = new Schema({ @@ -17,6 +16,5 @@ var schema = new Schema({ } }); -/* jshint -W003 */ -var TransactionLog = mongoose.model('TransactionLog', schema); +module.exports = mongoose.model('TransactionLog', schema); diff --git a/setup/logger.js b/modules/setup/accessLogger.js old mode 100755 new mode 100644 similarity index 100% rename from setup/logger.js rename to modules/setup/accessLogger.js diff --git a/modules/setup/bodyParser.js b/modules/setup/bodyParser.js new file mode 100644 index 000000000..521897aef --- /dev/null +++ b/modules/setup/bodyParser.js @@ -0,0 +1,11 @@ +'use strict'; + +const BodyParser = require('lib/bodyparser').BodyParser; +const _ = require('lodash'); + +module.exports = function(app) { + + app.bodyParser = new BodyParser(); + app.use(app.bodyParser.middleware()); + +}; diff --git a/modules/setup/csrf.js b/modules/setup/csrf.js new file mode 100644 index 000000000..01c817737 --- /dev/null +++ b/modules/setup/csrf.js @@ -0,0 +1,12 @@ +const csrf = require('koa-csrf'); +const Csrf = require('lib/csrf').Csrf; + +// every request gets different this._csrf to use in POST +// but ALL tokens are valid +module.exports = function(app) { + csrf(app); + + app.csrf = new Csrf(); + + app.use(app.csrf.middleware()); +}; diff --git a/setup/errors.js b/modules/setup/errorHandler.js old mode 100755 new mode 100644 similarity index 60% rename from setup/errors.js rename to modules/setup/errorHandler.js index 1f2e631cc..e6eb6e6eb --- a/setup/errors.js +++ b/modules/setup/errorHandler.js @@ -4,7 +4,7 @@ const config = require('config'); const log = require('js-log')(); const escapeHtml = require('escape-html'); -function* renderError(error) { +function renderError(error) { /*jshint -W040 */ this.status = error.status; @@ -17,7 +17,7 @@ function* renderError(error) { } } -function* renderDevError(error) { +function renderDevError(error) { /*jshint -W040 */ this.status = 500; @@ -39,13 +39,16 @@ function* renderDevError(error) { module.exports = function(app) { app.use(function*(next) { + this.renderError = renderError; + this.renderDevError = renderDevError; + try { - yield next; + yield* next; } catch (err) { if (err.status) { // user-level error - yield renderError.call(this, err); + this.renderError(err); } else { // if error is "call stack too long", then log.error(err) is not verbose @@ -56,9 +59,9 @@ module.exports = function(app) { this.set('X-Content-Type-Options', 'nosniff'); if (process.env.NODE_ENV == 'development') { - yield* renderDevError.call(this, err); + this.renderDevError(err); } else { - yield* renderError.call(this, {status: 500, message: "Internal Error"}); + this.renderError({status: 500, message: "Internal Error"}); } } } @@ -85,40 +88,4 @@ module.exports = function(app) { }); - /* TODO: rewrite this express-style error handling in koa - @see https://github.com/koajs/koa/wiki/Error-Handling - - this.use(function(req, res, next) { - next(404); - }); - - this.use(function(err, req, res, next) { - if (typeof err == 'number') { - err = new HttpError(err); - } - - if (err.name == 'CastError') { - // malformed or absent mongoose params - if (process.env.NODE_ENV == 'development') { - log.error(err); - } - res.sendHttpError(new HttpError(400)); - return; - } - - if (err instanceof HttpError) { - res.sendHttpError(err); - } else { - // if error is "call stack too long", then log.error(err) is not verbose - // so I cast it to string - log.error(err.toString()); - я - if (process.env.NODE_ENV == 'development') { - errorhandler()(err, req, res, next); - } else { - res.sendHttpError(new HttpError(500)); - } - } - }); - */ }; diff --git a/setup/render.js b/modules/setup/render.js similarity index 96% rename from setup/render.js rename to modules/setup/render.js index 9dca99255..eab12e538 100644 --- a/setup/render.js +++ b/modules/setup/render.js @@ -119,7 +119,7 @@ function resolvePathUp(templateDir, templateName) { var top = path.resolve(process.cwd()); while (templateDir != path.dirname(top)) { - var template = path.join(templateDir, 'template', templateName); + var template = path.join(templateDir, 'templates', templateName); log.debug("-- try path", template); if (fs.existsSync(template)) { log.debug("-- found"); @@ -129,6 +129,6 @@ function resolvePathUp(templateDir, templateName) { templateDir = path.dirname(templateDir); } - log.debug("-- failed", templateRoot, top); + log.debug("-- failed", templateDir, top); return null; } diff --git a/modules/setup/router.js b/modules/setup/router.js new file mode 100644 index 000000000..201bf0c58 --- /dev/null +++ b/modules/setup/router.js @@ -0,0 +1,34 @@ +'use strict'; + +var mount = require('koa-mount'); + +module.exports = function(app) { + + + app.use(mount('/', require('frontpage').middleware)); + + if (process.env.NODE_ENV == 'development') { + app.use(mount('/markup', require('markup').middleware)); + } + + app.use(mount('/getpdf', require('getpdf').middleware)); + + app.use(mount('/webmoney', require('webmoney').middleware)); + app.csrf.addIgnorePath('/webmoney/:any*'); + app.verboseLogger.addPath('/webmoney/:any*'); + + /* + app.use(mount('/yandexmoney', app.hmvc.yandexmoney.middleware)); + app.noCsrf.push(/^\/yandexmoney\//); +*/ + + // stick to bottom + app.use(mount('/', require('tutorial').middleware)); + + // by default if the router didn't find anything => it yields to next middleware + // so I throw error here manually + app.use(function* (next) { + this.throw(404); + }); + +}; diff --git a/setup/session.js b/modules/setup/session.js old mode 100755 new mode 100644 similarity index 88% rename from setup/session.js rename to modules/setup/session.js index 22cddbd3e..ec8b7e6b7 --- a/setup/session.js +++ b/modules/setup/session.js @@ -1,4 +1,4 @@ -const mongoose = require('lib/mongoose'); +const mongoose = require('mongoose'); const session = require('koa-generic-session'); const mongooseStore = require('koa-session-mongoose'); const config = require('config'); @@ -14,4 +14,4 @@ module.exports = function(app) { app.keys = config.session.keys; // needed for cookie-signing -}; \ No newline at end of file +}; diff --git a/setup/static.js b/modules/setup/static.js old mode 100755 new mode 100644 similarity index 98% rename from setup/static.js rename to modules/setup/static.js index 2b6101cfb..8f6548f06 --- a/setup/static.js +++ b/modules/setup/static.js @@ -6,4 +6,4 @@ const favicon = require('koa-favicon'); module.exports = function(app) { app.use(serve('www')); app.use(favicon()); -}; \ No newline at end of file +}; diff --git a/modules/setup/verboseLogger.js b/modules/setup/verboseLogger.js new file mode 100644 index 000000000..fe35f10ec --- /dev/null +++ b/modules/setup/verboseLogger.js @@ -0,0 +1,9 @@ +const VerboseLogger = require('lib/verboseLogger').VerboseLogger; + + +module.exports = function(app) { + + app.verboseLogger = new VerboseLogger(); + app.use(app.verboseLogger.middleware()); + +}; diff --git a/package.json b/package.json index 424369fb6..089ff7e04 100755 --- a/package.json +++ b/package.json @@ -3,12 +3,13 @@ "version": "0.0.1", "private": true, "scripts": { - "prod": "NODE_ENV=production NODE_PATH=. node --harmony ./bin/www", - "dev": "NODE_ENV=development NODE_PATH=. supervisor --harmony --debug --ignore node_modules ./bin/www", - "debug": "NODE_ENV=development NODE_PATH=. supervisor --harmony --debug-brk --ignore node_modules ./bin/www", - "test": "NODE_ENV=test NODE_PATH=. supervisor --harmony --debug --ignore node_modules ./bin/www" + "prod": "NODE_ENV=production node --harmony ./bin/www", + "dev": "NODE_ENV=development supervisor --harmony --debug --ignore node_modules ./bin/www", + "debug": "NODE_ENV=development supervisor --harmony --debug-brk --ignore node_modules ./bin/www", + "test": "NODE_ENV=test supervisor --harmony --debug --ignore node_modules ./bin/www" }, - "precommit": "NODE_PATH=. NODE_ENV=development node --harmony `which gulp` pre-commit", + "postinstall": "NODE_ENV=development node --harmony `which gulp` link-modules", + "precommit": "NODE_ENV=development node --harmony `which gulp` pre-commit", "dependencies": { "MD5": "^1.2.1", "bluebird": "^2.2.2", @@ -20,6 +21,7 @@ "event-stream": "^3.1.5", "fb-flo": "^0.2.1", "fs-extra": "^0.10.0", + "glob": "^4.0.4", "gm": "^1.16.0", "gulp-add-src": "^0.1.1", "gulp-cache": "^0.2.0", @@ -34,7 +36,7 @@ "gulp-notify": "^1.4.0", "gulp-plumber": "^0.6.3", "gulp-rimraf": "^0.1.0", - "gulp-stylus": "^1.0.5", + "gulp-stylus": "^1.1.0", "gulp-util": "*", "gulp.spritesmith": "^1.1.1", "imagemin": "^0.4.6", @@ -95,6 +97,7 @@ "should": "*", "sinon": "*", "stylus-brunch": "*", + "supertest": "^0.13.0", "supervisor": "*", "trace": "*", "uglify-js-brunch": "*", diff --git a/routes/index.js b/routes/index.js deleted file mode 100755 index 05bf30721..000000000 --- a/routes/index.js +++ /dev/null @@ -1,36 +0,0 @@ -'use strict'; - -var log = require('js-log')(); -var config = require('config'); -var requireTree = require('require-tree'); -var controllers = requireTree('../controllers'); -var mount = require('koa-mount'); -var Router = require('koa-router'); - -// Первым делом идут роуты для схем с жёстким началом /markup/* /task/* и т.п. -// В конце идут роуты для статей и справочников, которые в корне сайта, например http://javascript.ru/String -// TODO: добавить обработку 404 Not Found, сделать возможность любому роуту легко вызывать ошибку 404, как и любую другую ошибку -module.exports = function(app) { - -// app.get('/', controllers.frontpage.get); -// -// if (process.env.NODE_ENV == 'development') { -// app.get(/^\/markup\/(.*)/, controllers.markup.get); -// } -// -// - - var router = new Router(); - - router.get(/./, function*() { - console.log("OH"); - }); - - - app.use(mount('/', router.middleware())); - -//router.get(/^\/task\/(.*)$/, task.get); -//router.get(/^\/(.*)$/, article.get); - - -}; diff --git a/setup/bodyLogger.js b/setup/bodyLogger.js deleted file mode 100644 index bd60acf5f..000000000 --- a/setup/bodyLogger.js +++ /dev/null @@ -1,10 +0,0 @@ - -module.exports = function(app) { - - app.use(function*(next) { - if (this.request.body) { - console.log(this.request.body); - } - yield next; - }); -}; diff --git a/setup/bodyParser.js b/setup/bodyParser.js deleted file mode 100755 index 61f12fbe6..000000000 --- a/setup/bodyParser.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const bodyParser = require('koa-bodyparser'); - -module.exports = function(app) { - app.use(bodyParser()); -}; \ No newline at end of file diff --git a/setup/csrf.js b/setup/csrf.js deleted file mode 100644 index 0a33d1b0f..000000000 --- a/setup/csrf.js +++ /dev/null @@ -1,42 +0,0 @@ -const csrf = require('koa-csrf'); - -// every request gets different this._csrf to use in POST -// but ALL tokens are valid -module.exports = function(app) { - csrf(app); - - if (!app.noCsrf) app.noCsrf = []; - - app.use(function* (next) { - // skip these methods - if (this.method === 'GET' || this.method === 'HEAD' || this.method === 'OPTIONS') { - return yield* next; - } - - // don't check filtered urls - // e.g for access from outside non-browser apis - var checkCsrf = true; - - for (var i=0; i it yields to next middleware - // so I throw error here - app.use(function* (next) { - this.throw(404); - }); - -}; diff --git a/tasks/linkModules.js b/tasks/linkModules.js new file mode 100644 index 000000000..ee5777b3d --- /dev/null +++ b/tasks/linkModules.js @@ -0,0 +1,44 @@ +var fs = require('fs'); +var gulp = require('gulp'); +var glob = require('glob'); +var path = require('path'); +var gutil = require('gulp-util'); + +function ensureSymlinkSync(linkSrc, linkDst) { + var lstat; + try { + lstat = fs.lstatSync(linkDst); + } catch (e) { + } + + if (lstat) { + if (lstat.isSymbolicLink()) { + fs.unlinkSync(linkDst); + } else { + throw new Error("Conflict: path exist and is not a link: " + linkDst); + } + } + + fs.symlinkSync(linkSrc, linkDst); + +} + +module.exports = function(sources) { + + return function() { + var modules = []; + sources.forEach(function(pattern) { + modules = modules.concat(glob.sync(pattern)); + }); + + for (var i = 0; i < modules.length; i++) { + var moduleToLinkRelPath = modules[i]; // hmvc/auth + var moduleToLinkName = path.basename(moduleToLinkRelPath); // auth + var linkSrc = path.join('..', moduleToLinkRelPath); + var linkDst = path.join('node_modules', moduleToLinkName); + gutil.log(linkSrc + " -> " + linkDst); + ensureSymlinkSync(linkSrc, linkDst); + } + }; + +}; diff --git a/template/blocks/breadcrumbs.jade b/templates/blocks/breadcrumbs.jade similarity index 100% rename from template/blocks/breadcrumbs.jade rename to templates/blocks/breadcrumbs.jade diff --git a/template/blocks/footer.jade b/templates/blocks/footer.jade similarity index 100% rename from template/blocks/footer.jade rename to templates/blocks/footer.jade diff --git a/template/blocks/head.jade b/templates/blocks/head.jade similarity index 100% rename from template/blocks/head.jade rename to templates/blocks/head.jade diff --git a/template/blocks/lesson.jade b/templates/blocks/lesson.jade similarity index 100% rename from template/blocks/lesson.jade rename to templates/blocks/lesson.jade diff --git a/template/blocks/lessons.jade b/templates/blocks/lessons.jade similarity index 100% rename from template/blocks/lessons.jade rename to templates/blocks/lessons.jade diff --git a/template/blocks/navbar.jade b/templates/blocks/navbar.jade similarity index 100% rename from template/blocks/navbar.jade rename to templates/blocks/navbar.jade diff --git a/template/blocks/scripts.jade b/templates/blocks/scripts.jade similarity index 100% rename from template/blocks/scripts.jade rename to templates/blocks/scripts.jade diff --git a/template/blocks/social-aside.jade b/templates/blocks/social-aside.jade similarity index 100% rename from template/blocks/social-aside.jade rename to templates/blocks/social-aside.jade diff --git a/template/blocks/task-block.jade b/templates/blocks/task-block.jade similarity index 100% rename from template/blocks/task-block.jade rename to templates/blocks/task-block.jade diff --git a/template/blocks/top-parts.jade b/templates/blocks/top-parts.jade similarity index 100% rename from template/blocks/top-parts.jade rename to templates/blocks/top-parts.jade diff --git a/template/blocks/tutorial/bottom-navigation.jade b/templates/blocks/tutorial/bottom-navigation.jade similarity index 100% rename from template/blocks/tutorial/bottom-navigation.jade rename to templates/blocks/tutorial/bottom-navigation.jade diff --git a/template/blocks/tutorial/footer.jade b/templates/blocks/tutorial/footer.jade similarity index 100% rename from template/blocks/tutorial/footer.jade rename to templates/blocks/tutorial/footer.jade diff --git a/template/blocks/tutorial/top-navigation.jade b/templates/blocks/tutorial/top-navigation.jade similarity index 100% rename from template/blocks/tutorial/top-navigation.jade rename to templates/blocks/tutorial/top-navigation.jade diff --git a/template/error.jade b/templates/error.jade similarity index 100% rename from template/error.jade rename to templates/error.jade diff --git a/template/index.jade b/templates/index.jade similarity index 100% rename from template/index.jade rename to templates/index.jade diff --git a/template/layout.jade b/templates/layout.jade similarity index 100% rename from template/layout.jade rename to templates/layout.jade diff --git a/template/layouts/base.jade b/templates/layouts/base.jade similarity index 100% rename from template/layouts/base.jade rename to templates/layouts/base.jade diff --git a/test/mocha.opts b/test/mocha.opts index b3733f2a6..790728212 100755 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,8 +1,8 @@ --reporter spec --colors --timeout 10000 +--require config/mongoose --require should ---require lib/mongoose --require co --require co-mocha --recursive From 5ea8f778ebd9e758e11225d946a505cbd559e0f1 Mon Sep 17 00:00:00 2001 From: Anton Vernigor Date: Tue, 22 Jul 2014 19:47:29 +0400 Subject: [PATCH 086/130] Moving markup to jade and stylus --- app/stylesheets/base.styl | 2 +- app/stylesheets/blocks/comments/comments.styl | 215 ++++++++++++++++++ hmvc/markup/template/blocks/comments.jade | 30 +++ hmvc/markup/template/layouts/base.jade | 1 + 4 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 app/stylesheets/blocks/comments/comments.styl create mode 100644 hmvc/markup/template/blocks/comments.jade diff --git a/app/stylesheets/base.styl b/app/stylesheets/base.styl index 92ae8bf61..be94581f6 100755 --- a/app/stylesheets/base.styl +++ b/app/stylesheets/base.styl @@ -34,7 +34,7 @@ // @require "blocks/complex-code/complex-code" @require "blocks/balance/balance" // @require "blocks/rating/rating" -// @require "blocks/comments/comments" +@require "blocks/comments/comments" // @require "blocks/submit-button/submit-button" // @require "blocks/secondary-button/secondary-button" // @require "blocks/page-footer/page-footer" diff --git a/app/stylesheets/blocks/comments/comments.styl b/app/stylesheets/blocks/comments/comments.styl new file mode 100644 index 000000000..b047c5546 --- /dev/null +++ b/app/stylesheets/blocks/comments/comments.styl @@ -0,0 +1,215 @@ +comment-reply-color = #00A3D9 +.comments + margin-top 15px + +// .comments .comments__header-title { +// border-bottom: 0; +// display: inline; +// margin-right: 16px; +// color: $color; +// font: 30px/36px $secondary-font; +// } + +// .comments__header-title::before { +// @extend %font-comment; + +// font-size: 80%; +// color: #eee; +// margin-right: 9px; +// } + +// .comments__header-number { +// color: #aaa; +// } +// .comments__header-number::before { +// content: "("; +// } +// .comments__header-number::after { +// content: ")"; +// } + +// .comments__header-write:link { +// @extend %pseudo; +// color: #666; +// font-size: 12px; +// vertical-align: .4em; +// position: relative; +// display: inline-block; +// line-height: 1; +// } +// .comments__header-write::after { +// content: "↓"; +// position: absolute; +// margin-left: 3px; +// vertical-align: middle; +// } + +// .comments__items { +// overflow-x: hidden; +// margin: 25px -#{$content-horizontal-padding} 0; +// padding: 0 $content-horizontal-padding; +// border-bottom: 1px solid $separator-color; +// } + +// .comments__comment { +// font-size: 92%; +// line-height: 130%; +// position: relative; +// padding-top: 24px; +// } + +// .comments__comment::before { +// content: ""; +// position: absolute; +// top: 0; +// left: -200px; +// width: 1500px; +// height: 0; +// border-bottom: 1px solid $separator-color; +// } + +// .comments__comment .comments__comment { +// padding-left: 20px; +// } + +// // Don't increase indentation after 4th level +// .comments__comment +// .comments__comment +// .comments__comment +// .comments__comment +// .comments__comment { +// padding-left: 0; +// } + +// .comments__comment-header { +// @extend %clearfix; +// border-bottom: 0; +// padding-bottom: 0; +// margin-bottom: $lineheight*.65; +// } + +// .comments .comments__username { +// color: #444; +// } + +// .comments .comments__username:hover, +// .comments .comments__anchor:hover { +// color: $link-hover-color; +// } + +// .comments__userpic { +// float: left; +// } + +// .comments .comments__userpic { +// margin: 0; +// } + +// .comments__rating { +// float: right; +// } + +// .comments__info { +// padding: 0 12px; +// overflow: auto; +// } + +// .comments__date::before { +// display: none; +// } + +// .comments__date, +// .comments .comments__anchor { +// color: #888; +// font: 11px Arial, Helvetica, sans-serif; +// margin-right: 5px; +// } + +// .comments__star::before { +// @extend %font-star; +// color: #bbb; +// } + +// .comments__comment pre { +// font: 12px $fixed-width-font; +// color: #400000; +// } + +// .comments__footer { +// margin: 12px 0 22px; +// } + +// .comments .comments__reply { +// @extend %button-reset; + +// color: $comment-reply-color; +// cursor: pointer; +// text-decoration: none; // Тут может быть и ссылка, если будет якорь, или кнопка — если инлайн-форма +// border-bottom: 1px dashed transparent; +// font-size: 13px; +// } + +// .comments__reply:hover { +// color: $link-hover-color; +// border-bottom: 1px dashed transparentize($link-hover-color, .7); +// } + +// .comments__form { +// margin: 33px 0; +// } + +// .comments__form-header { +// @extend %clearfix; +// margin-bottom: 25px; +// } + +// .comments .comments__form-title { +// color: #888; +// margin: 0; +// float: left; +// } +// .comments__form-title::before { +// @extend %font-comment; +// color: #ccc; +// margin-right: 15px; +// } +// .comments .comments__form-formatting { +// @extend %pseudo; +// float: right; +// font-size: 12px; +// line-height: 1; +// margin-top: 8px; +// color: #656565; +// } +// .comments .comments__form-formatting:hover { +// color: $link-hover-color; +// } +// .comment-form__text { + +// width: 100%; +// height: 144px; +// margin: 16px 0; +// display: block; +// clear: both; +// } + +// @media (min-width: $media-step-1) { +// .comments__items { +// margin: 23px -#{$content-horizontal-padding + 10px}; +// padding: 0 ($content-horizontal-padding + 10px); +// } +// } + +// @media (min-width: $media-step-2) { +// .comments__items { +// margin: 23px -#{$content-horizontal-padding + 20px}; +// padding: 0 ($content-horizontal-padding + 20px); +// } +// } + +// @media (min-width: $media-step-3) { +// .comments__items { +// margin: 23px -#{$content-horizontal-padding + 30px}; +// padding: 0 ($content-horizontal-padding + 30px); +// } +// } \ No newline at end of file diff --git a/hmvc/markup/template/blocks/comments.jade b/hmvc/markup/template/blocks/comments.jade new file mode 100644 index 000000000..613799d41 --- /dev/null +++ b/hmvc/markup/template/blocks/comments.jade @@ -0,0 +1,30 @@ +.comments#comments + .comments__header + h2.comments__header-title + | Комментарии + span.comments__header-number 5 + a.comments__header-write(href="#write-comment") Написать + ul + li Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них. + li Если ваш комментарий касается задачи — откройте её в отдельном окне и напишите там. + li + | Для кода внутри строки используйте тег + <code> + | , для блока кода — тег + <pre> + | , если больше 10 строк — ссылку на + песочницу + | . + li Если что-то непонятно — пишите, что именно и с какого места. +//-
        +//-
        +//-

        Комментарии 5

        +//- Написать +//-
          +//-
        • Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
        • +//-
        • Если ваш комментарий касается задачи — откройте её в отдельном окне и напишите там.
        • +//-
        • Для кода внутри строки используйте тег <code>, для блока кода — тег <pre>, если больше 10 строк — ссылку на песочницу.
        • +//-
        • Если что-то непонятно — пишите, что именно и с какого места.
        • +//-
        +//-
        +//-
        \ No newline at end of file diff --git a/hmvc/markup/template/layouts/base.jade b/hmvc/markup/template/layouts/base.jade index 70c638838..21dc129e4 100644 --- a/hmvc/markup/template/layouts/base.jade +++ b/hmvc/markup/template/layouts/base.jade @@ -18,4 +18,5 @@ body(id='#{self.bodyId}') include ../blocks/article-foot include ../blocks/prev-next-bottom include ../blocks/corrector + include ../blocks/comments include ../blocks/scripts \ No newline at end of file From 6fbef2f33e95847de9d1c8198b5cd939e0110583 Mon Sep 17 00:00:00 2001 From: Anton Vernigor Date: Wed, 23 Jul 2014 00:11:06 +0400 Subject: [PATCH 087/130] moving markup to jade and stylus --- app/stylesheets/base.styl | 2 +- app/stylesheets/blocks/comments/comments.styl | 77 ++++++++---------- .../blocks/page-footer/page-footer.png | Bin 0 -> 2186 bytes .../blocks/page-footer/page-footer.styl | 69 ++++++++++++++++ hmvc/markup/template/blocks/page-footer.jade | 35 ++++++++ hmvc/markup/template/layouts/base.jade | 1 + 6 files changed, 142 insertions(+), 42 deletions(-) create mode 100644 app/stylesheets/blocks/page-footer/page-footer.png create mode 100644 app/stylesheets/blocks/page-footer/page-footer.styl create mode 100644 hmvc/markup/template/blocks/page-footer.jade diff --git a/app/stylesheets/base.styl b/app/stylesheets/base.styl index be94581f6..d7e4c9d49 100755 --- a/app/stylesheets/base.styl +++ b/app/stylesheets/base.styl @@ -37,7 +37,7 @@ @require "blocks/comments/comments" // @require "blocks/submit-button/submit-button" // @require "blocks/secondary-button/secondary-button" -// @require "blocks/page-footer/page-footer" +@require "blocks/page-footer/page-footer" @require "blocks/soc-icon/soc-icon" @require "blocks/social/social" // @require "blocks/user/user" diff --git a/app/stylesheets/blocks/comments/comments.styl b/app/stylesheets/blocks/comments/comments.styl index b047c5546..5f8177299 100644 --- a/app/stylesheets/blocks/comments/comments.styl +++ b/app/stylesheets/blocks/comments/comments.styl @@ -2,47 +2,42 @@ comment-reply-color = #00A3D9 .comments margin-top 15px -// .comments .comments__header-title { -// border-bottom: 0; -// display: inline; -// margin-right: 16px; -// color: $color; -// font: 30px/36px $secondary-font; -// } - -// .comments__header-title::before { -// @extend %font-comment; - -// font-size: 80%; -// color: #eee; -// margin-right: 9px; -// } - -// .comments__header-number { -// color: #aaa; -// } -// .comments__header-number::before { -// content: "("; -// } -// .comments__header-number::after { -// content: ")"; -// } - -// .comments__header-write:link { -// @extend %pseudo; -// color: #666; -// font-size: 12px; -// vertical-align: .4em; -// position: relative; -// display: inline-block; -// line-height: 1; -// } -// .comments__header-write::after { -// content: "↓"; -// position: absolute; -// margin-left: 3px; -// vertical-align: middle; -// } + & &__header-title + border-bottom 0 + display inline + margin-right 16px + color color + font 30px/36px secondary_font + + &__header-title::before + @extend $font-comment + font-size 80% + color #eee + margin-right 9px + + &__header-number + color #aaa + + &__header-number::before + content "(" + + &__header-number::after + content ")" + + &__header-write:link + @extend $pseudo + color #666 + font-size 12px + vertical-align .4em + position relative + display inline-block + line-height 1 + + &__header-write::after + content "↓" + position absolute + margin-left 3px + vertical-align middle // .comments__items { // overflow-x: hidden; diff --git a/app/stylesheets/blocks/page-footer/page-footer.png b/app/stylesheets/blocks/page-footer/page-footer.png new file mode 100644 index 0000000000000000000000000000000000000000..4c165cc2d12754a93f56edc56c8872646d036474 GIT binary patch literal 2186 zcmV;52zB>~P)dbVG7wVRUJ4ZXi@?ZDjy7FETSP zF*5~6nk)bS0338hSaefwW^{L9a%BKPWN%_+AVz6&Wp{6KYjYq&Q#Rg)&#ll!VltXN*bl%;uaCf2hLNI^ z()#GpkLgR+wxL#x5s@YFKFS2EZiz8wg_W|t*eI=pc1HiR_B|TW##hDh(4sRwJ{n^V zBJvFU+~IkEUyU(K^{oHw@caq;lFq6(l<|G0`?UP0h-|Qo-FJ9?4;++67p8#i`*UpN zD2eIumSXXOU2slmHtc=z(XFe7keF5P?$l#xj5!)(a)z-*by&qzdLKz_7FlmkF)-&? zWzWbU$?$6Q-X#L9L?q)j9&jz) z#+a{&9O$13<>OZkMgjQrzhV=CE+Tj%^x@4Ilk|=G4jqFH-;^8h7wk?z8u3hDG>$2T zFc%WAYC?bi7JgcGo1214b zpa=XGA)P?H#K168Q(}znhtKs2w?C5Ldrtq<*i~%B=p})2I$Zp|yWfa<|lAED^!M^3TsFJo9B3JOnm|R3UGz-A0 zZGv>C;Zi!o+iRWM4PuZZlZ-!I-{r{XL&A-N4y z73bL5>l%~x#+a)rTQ~MrsuUo_&J-bYY7alCxBEz8L(-Zya{W922j44J`b%J`k~F8B znZ9qJU5j3s;X2)B*!|=e0$tYf8O0f;*o=fa?9uZ}*j4#i8kOlQ{o0s1KlZ%jsa0hr|h(wqefAj^$v*_*#O{nAp;iDNPvVgqx@9V;|GmV(nVq z<>9-XiBtwH8UA3`D|Y6~`w+S7VNoa<wDj8!e<^PJv#xvYiVcAsa zf7bgI8f$W@^on(s!c%DeY?0#aBdfF|UWdcFP%r_dzoi_BJLzw^SNBAbxLB9<^u}d? z-2c9%Ix}-mG0r6D;~DP(j4?9+5y>gPjcIW24=SNpB!-Nctk>⩔IVFyZ6GHJNLV` z?P#s2mSP{7etuEHSQ6otw@FT%P-ItzWg%lAcv~Wp_B{)IOS~7685;2M z3shB{D4+D+NzD`9zRzN@*sa&=@C~GtQ#igU-B<3E4Achiuo+BGT>p#lb&JT9>Y|#pcxf z#FafG5t&xlwOl_Zb@csUcqqP!e!gMf$R}(x{zzeSpqNFV_+KgaCZ~Gx_1d)iBqHCv z=TQ2uSl?pKbxF&82QGyF-xDJ8gIBbn@Rw#3(pFO8cT^wu-BD~wtSG;cgo5j*#bS|r zWh+%1-=2hGo>P&vNujR3ckJAOs#E?=>f>JE$-9Tb?t#^cj7#kQ2DYX>PAY(_c>n+a M07*qoM6N<$f;|8-82|tP literal 0 HcmV?d00001 diff --git a/app/stylesheets/blocks/page-footer/page-footer.styl b/app/stylesheets/blocks/page-footer/page-footer.styl new file mode 100644 index 000000000..59c893dba --- /dev/null +++ b/app/stylesheets/blocks/page-footer/page-footer.styl @@ -0,0 +1,69 @@ +.page-footer + display table + width 100% + background #666 + + &__contents, + &__copy + display table-cell + vertical-align top + + &__title, + a + color #fff + font-size 15px + + a + text-decoration underline + + &__contents + padding 35px 40px 60px + background url('/img/page-footer.png') 97.5% 35px no-repeat + + &__list + font-weight 700 + + &__list-item + margin-bottom 6px + + &__title + margin-bottom 8px + + &__list.small &__list-item + margin-bottom 3px + a + font-weight 400 + font-size 13px + + &__col + float left + width 20.6% + margin 0 3% 0 0 + + &__copy + width sidebar_width + padding 35px sidebar_padding_right 60px sidebar_padding_left + color #fff + font-style normal + a + font-size 13px + + &__line + display block + margin-bottom 3px + + &__line.author + margin-bottom 6px + + @media (min-width: media_step_1) + &__contents + padding-left content_horizontal_padding + 10px + + + @media (min-width: media_step_2) + &__contents + padding-left content_horizontal_padding + 20px + + @media (min-width: media_step_3) + &__contents + padding-left content_horizontal_padding + 30px diff --git a/hmvc/markup/template/blocks/page-footer.jade b/hmvc/markup/template/blocks/page-footer.jade new file mode 100644 index 000000000..ca09bbfa1 --- /dev/null +++ b/hmvc/markup/template/blocks/page-footer.jade @@ -0,0 +1,35 @@ +.page-footer + .page-footer__contents + nav.page-footer__col + h3.page-footer__title Обучение + ul.page-footer__list.small + li.page-footer__list-item: a(href="#a1") Учебник + li.page-footer__list-item: a(href="#a2") Тесты знаний + li.page-footer__list-item: a(href="#a3") Курсы Javascript + nav.page-footer__col + h3.page-footer__title Справочник + ul.page-footer__list.small + li.page-footer__list-item: a(href="#es5") Стандарт ES5 + li.page-footer__list-item: a(href="#ref") Справочник + li.page-footer__list-item: a(href="#php") PHP-функции + li.page-footer__list-item: a(href="#books") Книги + nav.page-footer__col + ul.page-footer__list + li.page-footer__list-item: a(href="#blogs") Блоги + li.page-footer__list-item: a(href="#qa") Вопрос/Ответ + li.page-footer__list-item: a(href="#events") События + li.page-footer__list-item: a(href="#job") Работа + nav.page-footer__col + ul.page-footer__list + li.page-footer__list-item: a(href="#chat") Чат + li.page-footer__list-item: a(href="#sandbox") Песочница + address.page-footer__copy + //- spaces between span tags are inserted explicitly + span.page-footer__line.author + ©2007—2012 Илья Кантор + | + span.page-footer__line: a(href="/feedback/") Обратная связь + | + span.page-footer__line + a(href="/about/") О проекте + | diff --git a/hmvc/markup/template/layouts/base.jade b/hmvc/markup/template/layouts/base.jade index 21dc129e4..96b98cb61 100644 --- a/hmvc/markup/template/layouts/base.jade +++ b/hmvc/markup/template/layouts/base.jade @@ -19,4 +19,5 @@ body(id='#{self.bodyId}') include ../blocks/prev-next-bottom include ../blocks/corrector include ../blocks/comments + include ../blocks/page-footer include ../blocks/scripts \ No newline at end of file From 7304027b6d54bc8018ec38744023ca55a276eea2 Mon Sep 17 00:00:00 2001 From: Anton Vernigor Date: Wed, 23 Jul 2014 00:24:40 +0400 Subject: [PATCH 088/130] moving markup to jade and stylus --- app/stylesheets/base.styl | 2 +- app/stylesheets/sprite/important.styl | 6 ++++++ app/stylesheets/sprite/mixin.styl | 18 ++++++++++++++++++ app/stylesheets/sprite/soc_icon.styl | 6 ++++++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100755 app/stylesheets/sprite/important.styl create mode 100644 app/stylesheets/sprite/mixin.styl create mode 100755 app/stylesheets/sprite/soc_icon.styl diff --git a/app/stylesheets/base.styl b/app/stylesheets/base.styl index 44fa78e18..d7e4c9d49 100755 --- a/app/stylesheets/base.styl +++ b/app/stylesheets/base.styl @@ -1,4 +1,4 @@ -@require "sprites/*" +@require "sprite/*" @require "blocks/block_facebook/facebook" @require "blocks/variables/variables" diff --git a/app/stylesheets/sprite/important.styl b/app/stylesheets/sprite/important.styl new file mode 100755 index 000000000..882340a02 --- /dev/null +++ b/app/stylesheets/sprite/important.styl @@ -0,0 +1,6 @@ +$important-info = 0px 0px 0px 0px 19px 20px 22px 81px '/img/important.png?time=1405867629610'; +$important-ok = 0px 20px 0px -20px 19px 20px 22px 81px '/img/important.png?time=1405867629610'; +$important-question = 0px 40px 0px -40px 19px 20px 22px 81px '/img/important.png?time=1405867629610'; +$important-warning = 0px 60px 0px -60px 22px 21px 22px 81px '/img/important.png?time=1405867629610'; + +$important = 22px 81px '/img/important.png?time=1405867629610' \ No newline at end of file diff --git a/app/stylesheets/sprite/mixin.styl b/app/stylesheets/sprite/mixin.styl new file mode 100644 index 000000000..291aa3fb6 --- /dev/null +++ b/app/stylesheets/sprite/mixin.styl @@ -0,0 +1,18 @@ +spriteWidth($sprite) + width $sprite[4] + +spriteHeight($sprite) + height $sprite[5] + +spritePosition($sprite) + background-position $sprite[2] $sprite[3] + +spriteImage($sprite) + background-image url($sprite[8]) + +sprite($sprite) + if !match('hover', selector()) && !match('active', selector()) + spriteImage($sprite) + spritePosition($sprite) + spriteWidth($sprite) + spriteHeight($sprite) diff --git a/app/stylesheets/sprite/soc_icon.styl b/app/stylesheets/sprite/soc_icon.styl new file mode 100755 index 000000000..f0de7c6e2 --- /dev/null +++ b/app/stylesheets/sprite/soc_icon.styl @@ -0,0 +1,6 @@ +$soc_icon-facebook = 0px 0px 0px 0px 24px 24px 24px 96px '/img/soc_icon.png?time=1405867631497'; +$soc_icon-google = 0px 24px 0px -24px 24px 24px 24px 96px '/img/soc_icon.png?time=1405867631497'; +$soc_icon-twitter = 0px 48px 0px -48px 24px 24px 24px 96px '/img/soc_icon.png?time=1405867631497'; +$soc_icon-vk = 0px 72px 0px -72px 24px 24px 24px 96px '/img/soc_icon.png?time=1405867631497'; + +$soc_icon = 24px 96px '/img/soc_icon.png?time=1405867631497' \ No newline at end of file From 42b1451e4578725f4743f0f29045a888a536dbef Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 23 Jul 2014 01:36:09 +0400 Subject: [PATCH 089/130] merge --- .jshintrc | 3 +- hmvc/getpdf/controller/main.js | 5 +- hmvc/getpdf/controller/pay.js | 1 + hmvc/getpdf/controller/success.js | 2 + hmvc/getpdf/router.js | 6 +- hmvc/webmoney/controller/fail.js | 9 +- hmvc/webmoney/controller/result.js | 8 +- hmvc/webmoney/controller/success.js | 7 +- hmvc/webmoney/controller/wait.js | 2 + hmvc/webmoney/router.js | 17 +- hmvc/yandexmoney/controller/back.js | 221 ++++++++++++------ hmvc/yandexmoney/index.js | 20 +- hmvc/yandexmoney/router.js | 4 +- modules/app.js | 13 ++ modules/payment/index.js | 35 ++- modules/payment/middleware/loadOrder.js | 31 --- modules/payment/middleware/loadTransaction.js | 36 --- modules/payment/models/transaction.js | 15 +- modules/setup/router.js | 14 +- package.json | 1 + 20 files changed, 260 insertions(+), 190 deletions(-) delete mode 100644 modules/payment/middleware/loadOrder.js delete mode 100644 modules/payment/middleware/loadTransaction.js diff --git a/.jshintrc b/.jshintrc index 2527b4e49..5f3a922bb 100644 --- a/.jshintrc +++ b/.jshintrc @@ -10,5 +10,6 @@ "esnext": true, "multistr": true, "noyield": true, - "-W004": true + "-W004": true, + "-W030": true // for yield* ... } diff --git a/hmvc/getpdf/controller/main.js b/hmvc/getpdf/controller/main.js index 837b92153..3120e970f 100644 --- a/hmvc/getpdf/controller/main.js +++ b/hmvc/getpdf/controller/main.js @@ -4,8 +4,11 @@ var Transaction = payment.Transaction; exports.get = function*(next) { - if (!this.order) { + console.log("HERE"); + if (this.params.orderNumber) { + yield* this.loadOrder(); + } else { // this order is not saved anywhere, // it's only used to initially fill the form this.order = new Order({ diff --git a/hmvc/getpdf/controller/pay.js b/hmvc/getpdf/controller/pay.js index 254b584dd..ce18edca7 100644 --- a/hmvc/getpdf/controller/pay.js +++ b/hmvc/getpdf/controller/pay.js @@ -7,6 +7,7 @@ var methods = require('../paymentMethods').methods; log.debugOn(); exports.post = function*(next) { + yield* this.loadOrder(); var method = methods[this.request.body.paymentMethod]; if (!method) { diff --git a/hmvc/getpdf/controller/success.js b/hmvc/getpdf/controller/success.js index bbc36cc18..79259a18a 100644 --- a/hmvc/getpdf/controller/success.js +++ b/hmvc/getpdf/controller/success.js @@ -1,3 +1,5 @@ exports.get = function*(next) { + yield* this.loadOrder(); + this.body = 'THANK YOU'; }; diff --git a/hmvc/getpdf/router.js b/hmvc/getpdf/router.js index 17eab3c45..d28c7f06f 100644 --- a/hmvc/getpdf/router.js +++ b/hmvc/getpdf/router.js @@ -8,7 +8,7 @@ var pay = require('./controller/pay'); var success = require('./controller/success'); router.get('', main.get); -router.get('/order/:orderNumber', payment.middleware.loadOrder(), main.get); +router.get('/order/:orderNumber', main.get); -router.post('/pay', payment.middleware.loadOrder(), pay.post); -router.get('/success/:orderNumber', payment.middleware.loadOrder(), success.get); +router.post('/pay', pay.post); +router.get('/success/:orderNumber', success.get); diff --git a/hmvc/webmoney/controller/fail.js b/hmvc/webmoney/controller/fail.js index a041f3366..b4e360d2e 100644 --- a/hmvc/webmoney/controller/fail.js +++ b/hmvc/webmoney/controller/fail.js @@ -3,20 +3,21 @@ const payment = require('payment'); const Order = payment.Order; const Transaction = payment.Transaction; const log = require('js-log')(); -const md5 = require('MD5'); log.debugOn(); + exports.get = function* (next) { - yield this.transaction.persist({ + yield* this.loadTransaction(); + + this.transaction.persist({ status: Transaction.STATUS_FAIL }); yield this.transaction.log({ event: 'fail' }); - var order = this.transaction.order; - this.redirect(payment.getOrderUrl(order)); + this.redirect(this.getOrderUrl()); }; diff --git a/hmvc/webmoney/controller/result.js b/hmvc/webmoney/controller/result.js index 6a5f24a0f..89bc1f104 100644 --- a/hmvc/webmoney/controller/result.js +++ b/hmvc/webmoney/controller/result.js @@ -10,6 +10,7 @@ log.debugOn(); // ONLY ACCESSED from WEBMONEY SERVER exports.prerequest = function* (next) { + yield* this.loadTransaction('LMI_PAYMENT_NO', {skipOwnerCheck : true}); log.debug("prerequest"); @@ -32,10 +33,7 @@ exports.prerequest = function* (next) { exports.post = function* (next) { - if (this.request.body.LMI_PREREQUEST == '1') { - yield exports.prerequest.call(this, next); - return; - } + yield* this.loadTransaction('LMI_PAYMENT_NO', {skipOwnerCheck : true}); if (!checkSign(this.request.body)) { log.debug("wrong signature"); @@ -59,7 +57,7 @@ exports.post = function* (next) { var order = this.transaction.order; log.debug("will call order onSuccess module=" + order.module); - require(order.module).onSuccess(order); + yield* require(order.module).onSuccess(order); this.body = 'OK'; diff --git a/hmvc/webmoney/controller/success.js b/hmvc/webmoney/controller/success.js index 564c1ad7b..c4e0cb62e 100644 --- a/hmvc/webmoney/controller/success.js +++ b/hmvc/webmoney/controller/success.js @@ -9,12 +9,13 @@ log.debugOn(); exports.get = function* (next) { + yield* this.loadTransaction('LMI_PAYMENT_NO'); var transaction = this.transaction; - var order = this.transaction.order; + var order = this.order; - var successUrl = payment.getOrderSuccessUrl(order); - var failUrl = payment.getOrderUrl(order); + var successUrl = this.getOrderSuccessUrl(); + var failUrl = this.getOrderUrl(); log.debug("transaction status: " + transaction.status); diff --git a/hmvc/webmoney/controller/wait.js b/hmvc/webmoney/controller/wait.js index ef5b1e95b..ca00b6ebb 100644 --- a/hmvc/webmoney/controller/wait.js +++ b/hmvc/webmoney/controller/wait.js @@ -9,6 +9,8 @@ log.debugOn(); exports.post = function* (next) { + yield* this.loadTransaction(); + var attempt = 0; while (!this.transaction.status) { attempt++; diff --git a/hmvc/webmoney/router.js b/hmvc/webmoney/router.js index 4ab89e218..6ec5e1de5 100644 --- a/hmvc/webmoney/router.js +++ b/hmvc/webmoney/router.js @@ -9,17 +9,20 @@ var fail = require('./controller/fail'); var wait = require('./controller/wait'); // webmoney server posts here (in background) -router.post('/result', - payment.middleware.loadTransaction('LMI_PAYMENT_NO', {skipOwnerCheck : true}), - result.post -); +router.post('/result', function* (next) { + if (this.request.body.LMI_PREREQUEST == '1') { + yield* result.prerequest.call(this, next); + } else { + yield* result.post.call(this, next); + } +}); // webmoney server redirects here if payment successful -router.get('/success', payment.middleware.loadTransaction('LMI_PAYMENT_NO'), success.get); +router.get('/success', success.get); // but if transaction status is not yet received, we wait... -router.post('/wait', payment.middleware.loadTransaction(), wait.post); +router.post('/wait', wait.post); // webmoney server redirects here if payment failed -router.get('/fail', payment.middleware.loadTransaction('LMI_PAYMENT_NO'), fail.get); +router.get('/fail', fail.get); diff --git a/hmvc/yandexmoney/controller/back.js b/hmvc/yandexmoney/controller/back.js index c381d4f5c..42253e689 100644 --- a/hmvc/yandexmoney/controller/back.js +++ b/hmvc/yandexmoney/controller/back.js @@ -1,114 +1,199 @@ const config = require('config'); -const mongoose = require('mongoose'); -const Order = mongoose.models.Order; -const Transaction = mongoose.models.Transaction; -const TransactionLog = mongoose.models.TransactionLog; +const payment = require('payment'); +const Order = payment.Order; +const Transaction = payment.Transaction; const log = require('js-log')(); -const md5 = require('MD5'); const request = require('koa-request'); + log.debugOn(); /* jshint -W106 */ -function* fail(ctx) { - ctx.transaction.status = Transaction.STATUS_FAIL; - yield ctx.transaction.persist(); - - yield ctx.transaction.log({ event: 'fail' }); +exports.get = function* (next) { - var order = ctx.transaction.order; - ctx.redirect('/' + order.module + '/order/' + order.number); -} + var self = this; -exports.get = function* (next) { + yield* this.loadTransaction(); yield this.transaction.log({ - event: 'back', - data: {url: this.request.originalUrl, body: this.request.body} + event: 'back', + data: {url: this.request.originalUrl, body: this.request.body} }); - if (this.query.error) { - fail(this); + + if (!this.query.code) { + yield* fail(this.query.error_description || this.query.error); return; } - if (this.query.code) { + + try { + var oauthTokenResponse = yield* requestOauthToken(this.query.code); + + var oauthToken = oauthTokenResponse.access_token; + if (!oauthToken) { + throwResponseError(oauthTokenResponse); + } + + var requestPaymentResponse = yield* requestPayment(oauthToken); + + if (requestPaymentResponse.status != "success") { + + if (requestPaymentResponse.error == 'ext_action_required') { + self.redirect(requestPaymentResponse.ext_action_uri); + return; + } + + throwResponseError(requestPaymentResponse); + } + + var requestId = requestPaymentResponse.request_id; + + var startTime = new Date(); + + while_in_progress: + while (true) { + if (new Date() - startTime > 5 * 60 * 1e3) { // 5 minutes wait max + yield* fail("timeout"); + return; + } + var processPaymentResponse = yield* processPayment(oauthToken, requestId); + + switch (processPaymentResponse.status) { + case 'success': + break while_in_progress; + case 'refused': + yield* fail(processPaymentResponse.error); + return; + case 'ext_auth_required': + yield* fail("необходимо подтвердить авторизацию по технологии 3D-Secure"); + return; + case 'in_progress': + yield delay(processPaymentResponse.next_retry); + break; + default: + yield delay(1000); + } + + } + + + // success! + yield* require(this.order.module).onSuccess(this.order); + + + self.redirect(self.getOrderSuccessUrl()); + + } catch (e) { + + yield* fail(e.message); + return; + } + + /* jshint -W106 */ + function* fail(reason) { + self.transaction.status = Transaction.STATUS_FAIL; + self.transaction.statusMessage = reason; + + yield self.transaction.persist(); + + yield self.transaction.log({ event: 'fail', data: reason }); + + + self.redirect(self.getOrderUrl()); + } + + + function* requestOauthToken(code) { // request oauth token var options = { method: 'POST', form: { - code: this.query.code, + code: code, client_id: config.yandexmoney.clientId, grant_type: 'authorization_code', - redirect_uri: config.yandexmoney.redirectUri + '?transactionNumber=' + this.transaction.number, + redirect_uri: config.yandexmoney.redirectUri + '?transactionNumber=' + self.transaction.number, client_secret: config.yandexmoney.clientSecret }, url: 'https://sp-money.yandex.ru/oauth/token' }; - yield this.transaction.log({ event: 'request oauth/token', data: options }); - var response; - try { - var response = request(options); - yield this.transaction.log({ event: 'response oauth/token', data: response }); + yield self.transaction.log({ event: 'request oauth/token', data: options }); - response = JSON.parse(response); - if (!response.access_token) { - throw new Error(response.error); - } - } catch(e) { - fail(this); - return; - } + var response = yield request(options); - var accessToken = response.access_token; + yield self.transaction.log({ event: 'response oauth/token', data: response.body }); - // request payment + return JSON.parse(response.body); + } + + // request payment + // return return request_id + function* requestPayment(oauthToken) { var options = { - method: 'POST', - form: { - pattern_id: 'p2p', - to: config.yandexmoney.purse, - amount: this.transaction.amount, - comment: 'оплата по счету ' + this.transaction.number, - message: 'оплата по счету ' + this.transaction.number, - identifier_type: 'account' + method: 'POST', + form: { + pattern_id: 'p2p', + to: config.yandexmoney.purse, + amount: self.transaction.amount, + comment: 'оплата по счету ' + self.transaction.number, + message: 'оплата по счету ' + self.transaction.number, + identifier_type: 'account' }, headers: { - 'Authorization': 'Bearer ' + accessToken + 'Authorization': 'Bearer ' + oauthToken }, - url: 'https://money.yandex.ru/api/request-payment' + url: 'https://money.yandex.ru/api/request-payment' }; - // TODO! + yield self.transaction.log({ event: 'request api/request-payment', data: options }); - yield this.transaction.log({ event: 'request api/request-payment', data: options }); + var response = yield request(options); + yield self.transaction.log({ event: 'response api/request-payment', data: response.body }); - var response; - try { - var response = request(options); - yield this.transaction.log({ event: 'response api/request-payment', data: response }); - - response = JSON.parse(response); - if (!response.access_token) { - throw new Error(response.error); - } - } catch(e) { - fail(this); - return; - } + return JSON.parse(response.body); + } + function* processPayment(oauthToken, requestId) { + var options = { + method: 'POST', + form: { + request_id: requestId + }, + headers: { + 'Authorization': 'Bearer ' + oauthToken + }, + url: 'https://money.yandex.ru/api/process-payment' + }; + yield self.transaction.log({ event: 'request api/process-payment', data: options }); + var response = yield request(options); + yield self.transaction.log({ event: 'response api/process-payment', data: response.body }); - this.body = 'OK'; + return JSON.parse(response.body); } - /* - this.transaction.status = Transaction.STATUS_FAIL; - yield this.transaction.persist(); - var order = this.transaction.order; - this.redirect('/' + order.module + '/order/' + order.number); -*/ + }; + +function throwResponseError(response) { + var message; + + if (response.error && response.error_description) { + message = '[' + response.error + '] ' + response.error_description; + } else if (response.error) { + message = response.error; + } else { + message = "детали ошибки не указаны"; + } + + throw new Error(message); +} + +function delay(ms) { + return function(callback) { + setTimeout(callback, ms); + }; +} diff --git a/hmvc/yandexmoney/index.js b/hmvc/yandexmoney/index.js index 3e6510e54..610e7d9c1 100644 --- a/hmvc/yandexmoney/index.js +++ b/hmvc/yandexmoney/index.js @@ -1,8 +1,8 @@ const config = require('config'); const jade = require('jade'); const path = require('path'); -var mongoose = require('mongoose'); -var Transaction = mongoose.models.Transaction; +var payment = require('payment'); +var Transaction = payment.Transaction; var router = require('./router'); @@ -11,19 +11,19 @@ exports.middleware = router.middleware(); exports.createTransactionForm = function* (order) { var transaction = new Transaction({ - order: order._id, - amount: order.amount, - paymentType: 'yandexmoney' + order: order._id, + amount: order.amount, + module: 'yandexmoney' }); yield transaction.persist(); - return jade.renderFile(path.join(__dirname, 'template/form.jade'), { - clientId: config.yandexmoney.clientId, - redirectUri: config.yandexmoney.redirectUri, - purse: config.yandexmoney.purse, + return jade.renderFile(path.join(__dirname, 'templates/form.jade'), { + clientId: config.yandexmoney.clientId, + redirectUri: config.yandexmoney.redirectUri, + purse: config.yandexmoney.purse, transactionNumber: transaction.number, - amount: transaction.amount + amount: transaction.amount }); }; diff --git a/hmvc/yandexmoney/router.js b/hmvc/yandexmoney/router.js index 682a584dd..d1506426f 100644 --- a/hmvc/yandexmoney/router.js +++ b/hmvc/yandexmoney/router.js @@ -1,10 +1,10 @@ var Router = require('koa-router'); -var payment = require('../payment'); +var payment = require('payment'); var router = module.exports = new Router(); var back = require('./controller/back'); -router.get('/back', payment.loadTransactionMiddleware(), back.get); +router.get('/back', back.get); diff --git a/modules/app.js b/modules/app.js index 6b74aba4e..6c4cfc06f 100644 --- a/modules/app.js +++ b/modules/app.js @@ -50,3 +50,16 @@ if (process.env.NODE_ENV == 'test') { } module.exports = app; + +if (process.env.NODE_ENV == 'development') { + + global.p = function() { + var stack = new Error().stack.split("\n")[2].trim(); + console.log("----> " + global.p.counter++ + " at " + stack); + }; + global.p.counter = 1; +} else { + global.p = function() { + + }; +} diff --git a/modules/payment/index.js b/modules/payment/index.js index 9d883f862..cf9dddf98 100644 --- a/modules/payment/index.js +++ b/modules/payment/index.js @@ -1,20 +1,31 @@ -exports.middleware = { - loadOrder: require('./middleware/loadOrder'), - loadTransaction: require('./middleware/loadTransaction') -}; exports.Order = require('./models/order'); exports.Transaction = require('./models/transaction'); exports.TransactionLog = require('./models/transactionLog'); -exports.getOrderSuccessUrl = function(order) { - return '/' + order.module + '/success/' + order.number; -}; -exports.getOrderUrl = function(order) { - return '/' + order.module + '/order/' + order.number; -}; +var orderUtils = require('./lib/orderUtils'); +exports.orderUtils = orderUtils; + +var loadOrder = require('./lib/context/loadOrder'); +var loadTransaction = require('./lib/context/loadTransaction'); + + +exports.middleware = function*(next) { + this.loadOrder = loadOrder; + this.loadTransaction = loadTransaction; + + this.getOrderSuccessUrl = function() { + return orderUtils.getSuccessUrl(this.order); + }; + + this.getOrderUrl = function() { + return orderUtils.getUrl(this.order); + }; + + this.getOrderPendingUrl = function() { + return orderUtils.getPendingUrl(this.order); + }; -exports.getOrderPendingUrl = function(order) { - return '/' + order.module + '/pending/' + order.number; + yield* next; }; diff --git a/modules/payment/middleware/loadOrder.js b/modules/payment/middleware/loadOrder.js deleted file mode 100644 index 6d6f532a9..000000000 --- a/modules/payment/middleware/loadOrder.js +++ /dev/null @@ -1,31 +0,0 @@ -var mongoose = require('mongoose'); -var Order = require('../models/order'); - -// Populates this.order with the order by "orderNumber" parameter -module.exports = function(field) { - - if (!field) field = 'orderNumber'; - - return function* (next) { - - var orderNumber = this.request.body && this.request.body[field] || this.params[field] || this.query[field]; - - if (!orderNumber) { - return yield* next; - } - - var order = yield Order.findOne({number: orderNumber}).populate('order').exec(); - - if (!order) { - this.throw(404, 'Нет такого заказа'); - } - - // todo: add belongs to check (with auth) - if (!this.session.orders || this.session.orders.indexOf(order.number) == -1) { - this.throw(403, 'Заказ в сессии не найден'); - } - - this.order = order; - yield* next; - }; -}; diff --git a/modules/payment/middleware/loadTransaction.js b/modules/payment/middleware/loadTransaction.js deleted file mode 100644 index c3e042143..000000000 --- a/modules/payment/middleware/loadTransaction.js +++ /dev/null @@ -1,36 +0,0 @@ -var mongoose = require('mongoose'); -var Transaction = require('../models/transaction'); -var log = require('js-log')(); - -// Populates this.transaction with the transaction by "transactionNumber" parameter -// options.skipOwnerCheck is for signed submissions, set to true allows anyone to load transaction -module.exports = function(field, options) { - options = options || {}; - if (!field) field = 'transactionNumber'; - - return function* (next) { - - var transactionNumber = this.request.body && this.request.body[field] || this.params[field] || this.query[field]; - - log.debug('tx number: ' + transactionNumber); - if (!transactionNumber) { - return yield* next; - } - - var transaction = yield Transaction.findOne({number: transactionNumber}).populate('order').exec(); - - if (!transaction) { - this.throw(404, 'Нет такой транзакции'); - } - - if (!options.skipOwnerCheck) { - // todo: add belongs to check (with auth) - if (!this.session.orders || this.session.orders.indexOf(transaction.order.number) == -1) { - this.throw(403, 'Не найден заказ в сессии для этой транзакции'); - } - } - - this.transaction = transaction; - yield* next; - }; -}; diff --git a/modules/payment/models/transaction.js b/modules/payment/models/transaction.js index d25346e62..c3511c533 100644 --- a/modules/payment/models/transaction.js +++ b/modules/payment/models/transaction.js @@ -59,11 +59,24 @@ schema.methods.getStatusDescription = function() { return 'нет информации об оплате'; } - return 'оплата не прошла'; + var result = 'оплата не прошла'; + if (this.statusMessage) result += ': ' + this.statusMessage; + return result; }; schema.methods.log = function*(options) { options.transaction = this._id; + + // for complex objects -> prior to logging make them simple (must be jsonable) + // e.g for HTTP response (HTTP.IncomingMessage) + if (options.data && typeof options.data == 'object') { + // object keys may not contain "." in mongodb, so I may not store arbitrary objects + // only json can help + options.data = JSON.stringify(options.data); + } + + console.log(options); + var log = new TransactionLog(options); yield log.persist(); }; diff --git a/modules/setup/router.js b/modules/setup/router.js index 201bf0c58..6a920dc10 100644 --- a/modules/setup/router.js +++ b/modules/setup/router.js @@ -1,6 +1,8 @@ 'use strict'; var mount = require('koa-mount'); +var payment = require('payment'); +var compose = require('koa-compose'); module.exports = function(app) { @@ -11,16 +13,16 @@ module.exports = function(app) { app.use(mount('/markup', require('markup').middleware)); } - app.use(mount('/getpdf', require('getpdf').middleware)); + // need to compose, because mount takes only 1 middleware + app.use(mount('/getpdf', compose([payment.middleware, require('getpdf').middleware]))); - app.use(mount('/webmoney', require('webmoney').middleware)); + app.use(mount('/webmoney', compose([payment.middleware, require('webmoney').middleware]))); app.csrf.addIgnorePath('/webmoney/:any*'); app.verboseLogger.addPath('/webmoney/:any*'); - /* - app.use(mount('/yandexmoney', app.hmvc.yandexmoney.middleware)); - app.noCsrf.push(/^\/yandexmoney\//); -*/ + app.use(mount('/yandexmoney', compose([payment.middleware, require('yandexmoney').middleware]))); + app.csrf.addIgnorePath('/yandexmoney/:any*'); + app.verboseLogger.addPath('/yandexmoney/:any*'); // stick to bottom app.use(mount('/', require('tutorial').middleware)); diff --git a/package.json b/package.json index 089ff7e04..5238172d0 100755 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "js-log": "^0.2.2", "koa": "*", "koa-bodyparser": "*", + "koa-compose": "^2.3.0", "koa-csrf": "^2.1.2", "koa-favicon": "*", "koa-generic-session": "*", From 3e960aaca6d632e98695a44b2f63dc52cb57ae5b Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 23 Jul 2014 01:36:42 +0400 Subject: [PATCH 090/130] remove sprite --- app/stylesheets/sprite/important.styl | 6 ------ app/stylesheets/sprite/mixin.styl | 18 ------------------ app/stylesheets/sprite/soc_icon.styl | 6 ------ 3 files changed, 30 deletions(-) delete mode 100755 app/stylesheets/sprite/important.styl delete mode 100644 app/stylesheets/sprite/mixin.styl delete mode 100755 app/stylesheets/sprite/soc_icon.styl diff --git a/app/stylesheets/sprite/important.styl b/app/stylesheets/sprite/important.styl deleted file mode 100755 index 882340a02..000000000 --- a/app/stylesheets/sprite/important.styl +++ /dev/null @@ -1,6 +0,0 @@ -$important-info = 0px 0px 0px 0px 19px 20px 22px 81px '/img/important.png?time=1405867629610'; -$important-ok = 0px 20px 0px -20px 19px 20px 22px 81px '/img/important.png?time=1405867629610'; -$important-question = 0px 40px 0px -40px 19px 20px 22px 81px '/img/important.png?time=1405867629610'; -$important-warning = 0px 60px 0px -60px 22px 21px 22px 81px '/img/important.png?time=1405867629610'; - -$important = 22px 81px '/img/important.png?time=1405867629610' \ No newline at end of file diff --git a/app/stylesheets/sprite/mixin.styl b/app/stylesheets/sprite/mixin.styl deleted file mode 100644 index 291aa3fb6..000000000 --- a/app/stylesheets/sprite/mixin.styl +++ /dev/null @@ -1,18 +0,0 @@ -spriteWidth($sprite) - width $sprite[4] - -spriteHeight($sprite) - height $sprite[5] - -spritePosition($sprite) - background-position $sprite[2] $sprite[3] - -spriteImage($sprite) - background-image url($sprite[8]) - -sprite($sprite) - if !match('hover', selector()) && !match('active', selector()) - spriteImage($sprite) - spritePosition($sprite) - spriteWidth($sprite) - spriteHeight($sprite) diff --git a/app/stylesheets/sprite/soc_icon.styl b/app/stylesheets/sprite/soc_icon.styl deleted file mode 100755 index f0de7c6e2..000000000 --- a/app/stylesheets/sprite/soc_icon.styl +++ /dev/null @@ -1,6 +0,0 @@ -$soc_icon-facebook = 0px 0px 0px 0px 24px 24px 24px 96px '/img/soc_icon.png?time=1405867631497'; -$soc_icon-google = 0px 24px 0px -24px 24px 24px 24px 96px '/img/soc_icon.png?time=1405867631497'; -$soc_icon-twitter = 0px 48px 0px -48px 24px 24px 24px 96px '/img/soc_icon.png?time=1405867631497'; -$soc_icon-vk = 0px 72px 0px -72px 24px 24px 24px 96px '/img/soc_icon.png?time=1405867631497'; - -$soc_icon = 24px 96px '/img/soc_icon.png?time=1405867631497' \ No newline at end of file From 1a4350ad77e97f916dd92cadba6711bb9da22f7c Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 23 Jul 2014 01:36:54 +0400 Subject: [PATCH 091/130] merge on --- modules/payment/lib/context/loadOrder.js | 32 ++++++++++++++++ .../payment/lib/context/loadTransaction.js | 38 +++++++++++++++++++ modules/payment/lib/orderUtils.js | 11 ++++++ 3 files changed, 81 insertions(+) create mode 100644 modules/payment/lib/context/loadOrder.js create mode 100644 modules/payment/lib/context/loadTransaction.js create mode 100644 modules/payment/lib/orderUtils.js diff --git a/modules/payment/lib/context/loadOrder.js b/modules/payment/lib/context/loadOrder.js new file mode 100644 index 000000000..12db151d3 --- /dev/null +++ b/modules/payment/lib/context/loadOrder.js @@ -0,0 +1,32 @@ +var mongoose = require('mongoose'); +var Order = require('../../models/order'); +var assert = require('assert'); + +// Populates this.order with the order by "orderNumber" parameter +module.exports = function* (field) { + + if (!field) field = 'orderNumber'; + + var orderNumber = this.request.body && this.request.body[field] || this.params[field] || this.query[field]; + + if (!orderNumber) { + return; + } + + var order = yield Order.findOne({number: orderNumber}).populate('order').exec(); + + if (!order) { + this.throw(404, 'Нет такого заказа'); + } + + // todo: add belongs to check (with auth) + if (!this.session.orders || this.session.orders.indexOf(order.number) == -1) { + this.throw(403, 'Заказ в сессии не найден'); + } + + + assert(!this.order, "this.order is already set (by loadTransaction?)"); + + this.order = order; + +}; diff --git a/modules/payment/lib/context/loadTransaction.js b/modules/payment/lib/context/loadTransaction.js new file mode 100644 index 000000000..e1f14b732 --- /dev/null +++ b/modules/payment/lib/context/loadTransaction.js @@ -0,0 +1,38 @@ +var mongoose = require('mongoose'); +var Transaction = require('../../models/transaction'); +var log = require('js-log')(); +var assert = require('assert'); + +// Populates this.transaction with the transaction by "transactionNumber" parameter +// options.skipOwnerCheck is for signed submissions, set to true allows anyone to load transaction +module.exports = function* (field, options) { + options = options || {}; + if (!field) field = 'transactionNumber'; + + var transactionNumber = this.request.body && this.request.body[field] || this.params[field] || this.query[field]; + + log.debug('tx number: ' + transactionNumber); + if (!transactionNumber) { + return; + } + + var transaction = yield Transaction.findOne({number: transactionNumber}).populate('order').exec(); + + if (!transaction) { + this.throw(404, 'Нет такой транзакции'); + } + + if (!options.skipOwnerCheck) { + // todo: add belongs to check (with auth) + if (!this.session.orders || this.session.orders.indexOf(transaction.order.number) == -1) { + this.throw(403, 'Не найден заказ в сессии для этой транзакции'); + } + } + + assert(!this.transaction, "this.transaction is already set"); + assert(!this.order, "this.order is already set"); + + this.transaction = transaction; + this.order = transaction.order; + +}; diff --git a/modules/payment/lib/orderUtils.js b/modules/payment/lib/orderUtils.js new file mode 100644 index 000000000..565f289b9 --- /dev/null +++ b/modules/payment/lib/orderUtils.js @@ -0,0 +1,11 @@ +module.exports = { + getSuccessUrl: function(order) { + return '/' + order.module + '/success/' + order.number; + }, + getUrl: function(order) { + return '/' + order.module + '/order/' + order.number; + }, + getPendingUrl: function(order) { + return '/' + order.module + '/pending/' + order.number; + } +}; From 9ee54643011c0d39f92439809f5cac7f01d36ac0 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 23 Jul 2014 09:31:47 +0400 Subject: [PATCH 092/130] fix postinstall --- package.json | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 5238172d0..1001fce04 100755 --- a/package.json +++ b/package.json @@ -6,9 +6,10 @@ "prod": "NODE_ENV=production node --harmony ./bin/www", "dev": "NODE_ENV=development supervisor --harmony --debug --ignore node_modules ./bin/www", "debug": "NODE_ENV=development supervisor --harmony --debug-brk --ignore node_modules ./bin/www", - "test": "NODE_ENV=test supervisor --harmony --debug --ignore node_modules ./bin/www" + "test": "NODE_ENV=test supervisor --harmony --debug --ignore node_modules ./bin/www", + "postinstall": "NODE_ENV=development node --harmony `which gulp` link-modules", + "fixperms": "sudo chown -R `id -u` . ~/.npm* ~/.node-gyp" }, - "postinstall": "NODE_ENV=development node --harmony `which gulp` link-modules", "precommit": "NODE_ENV=development node --harmony `which gulp` pre-commit", "dependencies": { "MD5": "^1.2.1", @@ -16,14 +17,11 @@ "body-parser": "*", "brfs": "^1.1.2", "co": "*", - "errorhandler": "^1.1.1", "escape-html": "^1.0.1", "event-stream": "^3.1.5", - "fb-flo": "^0.2.1", "fs-extra": "^0.10.0", "glob": "^4.0.4", "gm": "^1.16.0", - "gulp-add-src": "^0.1.1", "gulp-cache": "^0.2.0", "gulp-concat": "^2.3.3", "gulp-debug": "^0.3.0", @@ -77,15 +75,11 @@ "thunkify": "*", "vinyl-fs": "^0.3.4", "vinyl-source-stream": "^0.1.1", - "watchify": "^0.10.2", "winston": "*" }, "devDependencies": { - "autoprefixer-brunch": "*", "clarify": "*", - "clean-css-brunch": "*", "co-mocha": "*", - "css-brunch": "*", "gulp": "*", "gulp-autoprefixer": "0.0.8", "gulp-jshint": "*", @@ -97,11 +91,9 @@ "mocha": "*", "should": "*", "sinon": "*", - "stylus-brunch": "*", "supertest": "^0.13.0", "supervisor": "*", "trace": "*", - "uglify-js-brunch": "*", "watchify": "^0.10.2" }, "engines": { From 96446db54d1ae71e6523973ac072c528c4f02441 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 23 Jul 2014 19:00:59 +0400 Subject: [PATCH 093/130] removed sprite/ from styles (should be sprites/) --- app/stylesheets/base.styl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/stylesheets/base.styl b/app/stylesheets/base.styl index d7e4c9d49..44fa78e18 100755 --- a/app/stylesheets/base.styl +++ b/app/stylesheets/base.styl @@ -1,4 +1,4 @@ -@require "sprite/*" +@require "sprites/*" @require "blocks/block_facebook/facebook" @require "blocks/variables/variables" From c4df7e5bae9129867926605d6bffd4a2a6390ff9 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 23 Jul 2014 19:01:09 +0400 Subject: [PATCH 094/130] added link-modules on watch --- gulpfile.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gulpfile.js b/gulpfile.js index 05a9febec..6415ac831 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -46,6 +46,8 @@ gulp.task('watch', ['sprite', 'stylus'], function(neverCalled) { gulp.watch("app/**/*.sprite/**", ['sprite']); gulp.watch("app/**/*.styl", ['stylus']); + + gulp.watch(serverSources, ['link-modules']); }); // Show errors if encountered From 7589671ac5ff431fa088a23b63b18488baaa0ec2 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Wed, 23 Jul 2014 19:01:16 +0400 Subject: [PATCH 095/130] hacking on --- hmvc/getpdf/index.js | 2 +- hmvc/payanyway/controller/cancel.js | 20 ++++++++++ hmvc/payanyway/controller/fail.js | 19 +++++++++ hmvc/payanyway/controller/inprogress.js | 19 +++++++++ hmvc/payanyway/controller/result.js | 48 +++++++++++++++++++++++ hmvc/payanyway/controller/success.js | 32 ++++++++++++++++ hmvc/payanyway/controller/wait.js | 37 ++++++++++++++++++ hmvc/payanyway/index.js | 31 +++++++++++++++ hmvc/payanyway/router.js | 21 ++++++++++ hmvc/payanyway/templates/form.jade | 9 +++++ hmvc/payanyway/templates/wait.jade | 25 ++++++++++++ hmvc/paypal/controller/cancel.js | 19 +++++++++ hmvc/paypal/controller/result.js | 48 +++++++++++++++++++++++ hmvc/paypal/controller/success.js | 32 ++++++++++++++++ hmvc/paypal/controller/wait.js | 37 ++++++++++++++++++ hmvc/paypal/index.js | 35 +++++++++++++++++ hmvc/paypal/router.js | 21 ++++++++++ hmvc/paypal/templates/form.jade | 19 +++++++++ hmvc/paypal/templates/wait.jade | 25 ++++++++++++ hmvc/yandexmoney/controller/back.js | 17 ++++++--- modules/config/base.js | 44 --------------------- modules/config/index.js | 45 ++++++++++++++++++++-- modules/config/secret.js | 8 ++++ modules/payment/models/transaction.js | 51 +++++++++++++++++++------ modules/setup/router.js | 10 ++++- tasks/linkModules.js | 18 ++++++--- 26 files changed, 622 insertions(+), 70 deletions(-) create mode 100644 hmvc/payanyway/controller/cancel.js create mode 100644 hmvc/payanyway/controller/fail.js create mode 100644 hmvc/payanyway/controller/inprogress.js create mode 100644 hmvc/payanyway/controller/result.js create mode 100644 hmvc/payanyway/controller/success.js create mode 100644 hmvc/payanyway/controller/wait.js create mode 100644 hmvc/payanyway/index.js create mode 100644 hmvc/payanyway/router.js create mode 100644 hmvc/payanyway/templates/form.jade create mode 100644 hmvc/payanyway/templates/wait.jade create mode 100644 hmvc/paypal/controller/cancel.js create mode 100644 hmvc/paypal/controller/result.js create mode 100644 hmvc/paypal/controller/success.js create mode 100644 hmvc/paypal/controller/wait.js create mode 100644 hmvc/paypal/index.js create mode 100644 hmvc/paypal/router.js create mode 100644 hmvc/paypal/templates/form.jade create mode 100644 hmvc/paypal/templates/wait.jade delete mode 100644 modules/config/base.js diff --git a/hmvc/getpdf/index.js b/hmvc/getpdf/index.js index 0e4e17290..e8f94377c 100644 --- a/hmvc/getpdf/index.js +++ b/hmvc/getpdf/index.js @@ -3,6 +3,6 @@ var router = require('./router'); exports.middleware = router.middleware(); -exports.onSuccess = function(order) { +exports.onSuccess = function* (order) { console.log("Order success: " + order.number); }; diff --git a/hmvc/payanyway/controller/cancel.js b/hmvc/payanyway/controller/cancel.js new file mode 100644 index 000000000..6ec3e3c94 --- /dev/null +++ b/hmvc/payanyway/controller/cancel.js @@ -0,0 +1,20 @@ +const mongoose = require('mongoose'); +const payment = require('payment'); +const Order = payment.Order; +const Transaction = payment.Transaction; +const log = require('js-log')(); + + +exports.get = function* (next) { + + yield* this.loadTransaction('MNT_TRANSACTION_ID'); + + this.transaction.persist({ + status: Transaction.STATUS_FAIL, + statusMessage: 'отказ от оплаты' + }); + + this.redirect(this.getOrderUrl()); +}; + + diff --git a/hmvc/payanyway/controller/fail.js b/hmvc/payanyway/controller/fail.js new file mode 100644 index 000000000..d1e987d06 --- /dev/null +++ b/hmvc/payanyway/controller/fail.js @@ -0,0 +1,19 @@ +const mongoose = require('mongoose'); +const payment = require('payment'); +const Order = payment.Order; +const Transaction = payment.Transaction; +const log = require('js-log')(); + + +exports.get = function* (next) { + + yield* this.loadTransaction('MNT_TRANSACTION_ID'); + + this.transaction.persist({ + status: Transaction.STATUS_FAIL + }); + + this.redirect(this.getOrderUrl()); +}; + + diff --git a/hmvc/payanyway/controller/inprogress.js b/hmvc/payanyway/controller/inprogress.js new file mode 100644 index 000000000..4f0d172be --- /dev/null +++ b/hmvc/payanyway/controller/inprogress.js @@ -0,0 +1,19 @@ +const mongoose = require('mongoose'); +const payment = require('payment'); +const Order = payment.Order; +const Transaction = payment.Transaction; +const log = require('js-log')(); + + +exports.get = function* (next) { + + yield* this.loadTransaction('MNT_TRANSACTION_ID'); + + this.transaction.persist({ + status: Transaction.STATUS_PENDING + }); + + this.redirect(this.getOrderPendingUrl()); +}; + + diff --git a/hmvc/payanyway/controller/result.js b/hmvc/payanyway/controller/result.js new file mode 100644 index 000000000..b46605dbd --- /dev/null +++ b/hmvc/payanyway/controller/result.js @@ -0,0 +1,48 @@ +const payment = require('payment'); +const config = require('config'); +const mongoose = require('mongoose'); +const Order = payment.Order; +const Transaction = payment.Transaction; +const log = require('js-log')(); +const md5 = require('MD5'); + +log.debugOn(); + +exports.post = function* (next) { + + yield* this.loadTransaction('MNT_TRANSACTION_ID', {skipOwnerCheck : true}); + + if (!checkSign(this.request.body)) { + log.debug("wrong signature"); + this.throw(403, "wrong signature"); + } + + yield this.transaction.log({ + event: 'result', + data: {url: this.request.originalUrl, body: this.request.body} + }); + + if (this.transaction.amount != parseFloat(this.request.body.MNT_AMOUNT) || + this.request.body.MNT_ID != config.payanyway.id) { + this.throw(404, 'transaction with given params not found'); + } + + yield this.transaction.persist({ + status: Transaction.STATUS_SUCCESS + }); + + var order = this.order; + log.debug("will call order onSuccess module=" + order.module); + yield* require(order.module).onSuccess(order); + + this.body = 'SUCCESS'; +}; + +function checkSign(body) { + + var signature = md5(body.MNT_ID + body.MNT_TRANSACTION_ID + body.MNT_OPERATION_ID + + body.MNT_AMOUNT + body.MNT_CURRENCY_CODE + body.MNT_SUBSCRIBER_ID + body.MNT_TEST_MODE + + config.payanyway.secret).toUpperCase(); + + return signature == body.MNT_SIGNATURE; +} diff --git a/hmvc/payanyway/controller/success.js b/hmvc/payanyway/controller/success.js new file mode 100644 index 000000000..f14902359 --- /dev/null +++ b/hmvc/payanyway/controller/success.js @@ -0,0 +1,32 @@ +const config = require('config'); +const mongoose = require('mongoose'); +const payment = require('payment'); +const Transaction = payment.Transaction; +const log = require('js-log')(); +const md5 = require('MD5'); + +log.debugOn(); + + +exports.get = function* (next) { + yield* this.loadTransaction('MNT_TRANSACTION_ID'); + + var transaction = this.transaction; + var order = this.order; + + var successUrl = this.getOrderSuccessUrl(); + var failUrl = this.getOrderUrl(); + + log.debug("transaction status: " + transaction.status); + + if (transaction.status) { + this.redirect(transaction.status == Transaction.STATUS_SUCCESS ? successUrl : failUrl); + } else { + this.render(__dirname, 'wait', { + transactionNumber: transaction.number, + successUrl: successUrl, + failUrl: failUrl + }); + } + +}; diff --git a/hmvc/payanyway/controller/wait.js b/hmvc/payanyway/controller/wait.js new file mode 100644 index 000000000..ca00b6ebb --- /dev/null +++ b/hmvc/payanyway/controller/wait.js @@ -0,0 +1,37 @@ +const payment = require('payment'); +const config = require('config'); +const mongoose = require('mongoose'); +const Transaction = payment.Transaction; +const log = require('js-log')(); +const md5 = require('MD5'); + +log.debugOn(); + +exports.post = function* (next) { + + yield* this.loadTransaction(); + + var attempt = 0; + while (!this.transaction.status) { + attempt++; + if (attempt == 10) { + log.debug("timeout"); + this.body = 'TIMEOUT'; + return; + } + + yield delay(1000); + + this.transaction = yield Transaction.findOne({number: this.transaction.number }).exec(); + } + + log.debug('received'); + + this.body = this.transaction.status; +}; + +function delay(ms) { + return function(callback) { + setTimeout(callback, ms); + }; +} diff --git a/hmvc/payanyway/index.js b/hmvc/payanyway/index.js new file mode 100644 index 000000000..f86f39465 --- /dev/null +++ b/hmvc/payanyway/index.js @@ -0,0 +1,31 @@ +const jade = require('jade'); +const path = require('path'); +var config = require('config'); +var payment = require('payment'); + +var Transaction = payment.Transaction; + +var router = require('./router'); + +exports.middleware = router.middleware(); + +exports.createTransactionForm = function* (order) { + + var transaction = new Transaction({ + order: order._id, + amount: order.amount, + module: 'payanyway' + }); + + yield transaction.persist(); + + return jade.renderFile(path.join(__dirname, 'templates/form.jade'), { + amount: transaction.amount, + number: transaction.number, + id: config.payanyway.id + }); + +}; + + + diff --git a/hmvc/payanyway/router.js b/hmvc/payanyway/router.js new file mode 100644 index 000000000..ab80cc16c --- /dev/null +++ b/hmvc/payanyway/router.js @@ -0,0 +1,21 @@ +var Router = require('koa-router'); +var payment = require('payment'); + +var router = module.exports = new Router(); + +var result = require('./controller/result'); +var success = require('./controller/success'); +var cancel = require('./controller/cancel'); +var wait = require('./controller/wait'); + +// webmoney server posts here (in background) +router.post('/result', result.post); + +// webmoney server redirects here if payment successful +router.get('/success', success.get); +// but if transaction status is not yet received, we wait... +router.post('/wait', wait.post); + +router.get('/cancel', cancel.get); + + diff --git a/hmvc/payanyway/templates/form.jade b/hmvc/payanyway/templates/form.jade new file mode 100644 index 000000000..7f63860c4 --- /dev/null +++ b/hmvc/payanyway/templates/form.jade @@ -0,0 +1,9 @@ + +form(method="POST" action="https://www.moneta.ru/assistant.htm" accept-charset="UTF-8") + input(type="hidden",name="MNT_ID",value=id) + input(type="hidden",name="MNT_TRANSACTION_ID",value=number) + input(type="hidden",name="MNT_CURRENCY_CODE",value="RUB") + input(type="hidden",name="MNT_AMOUNT",value=amount) + input(type="hidden",name="paymentSystem.limitIds",value="843858,248362,822360,545234,1028,499669") + input(type="submit",value="Оплатить") + diff --git a/hmvc/payanyway/templates/wait.jade b/hmvc/payanyway/templates/wait.jade new file mode 100644 index 000000000..28e2830d9 --- /dev/null +++ b/hmvc/payanyway/templates/wait.jade @@ -0,0 +1,25 @@ +div + p Минуточку, ожидаем информацию от сервера.. + p Если эта страница долго не отвечает - + | + a(href='#' onclick='window.location.reload(true)') перезагрузите её + | . + +script var transactionNumber = #{transactionNumber}, successUrl = '#{successUrl}', failUrl = '#{failUrl}'; + +script. + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/webmoney/wait'); + xhr.timeout = 20000; + + + xhr.onreadystatechange = function() { + if (xhr.readyState != 4) return; + + location.href = (xhr.responseText == 'success') ? successUrl : failUrl; + }; + + xhr.ontimeout = xhr.onabort = function() { + location.href = failUrl; + }; + xhr.send('transactionNumber=' + transactionNumber); diff --git a/hmvc/paypal/controller/cancel.js b/hmvc/paypal/controller/cancel.js new file mode 100644 index 000000000..cea2ca84d --- /dev/null +++ b/hmvc/paypal/controller/cancel.js @@ -0,0 +1,19 @@ +const mongoose = require('mongoose'); +const payment = require('payment'); +const Order = payment.Order; +const Transaction = payment.Transaction; +const log = require('js-log')(); + + +exports.get = function* (next) { + + yield* this.loadTransaction(); + + yield this.transaction.persist({ + status: Transaction.STATUS_FAIL, + statusMessage: 'отказ от оплаты' + }); + + this.redirect(this.getOrderUrl()); +}; + diff --git a/hmvc/paypal/controller/result.js b/hmvc/paypal/controller/result.js new file mode 100644 index 000000000..b46605dbd --- /dev/null +++ b/hmvc/paypal/controller/result.js @@ -0,0 +1,48 @@ +const payment = require('payment'); +const config = require('config'); +const mongoose = require('mongoose'); +const Order = payment.Order; +const Transaction = payment.Transaction; +const log = require('js-log')(); +const md5 = require('MD5'); + +log.debugOn(); + +exports.post = function* (next) { + + yield* this.loadTransaction('MNT_TRANSACTION_ID', {skipOwnerCheck : true}); + + if (!checkSign(this.request.body)) { + log.debug("wrong signature"); + this.throw(403, "wrong signature"); + } + + yield this.transaction.log({ + event: 'result', + data: {url: this.request.originalUrl, body: this.request.body} + }); + + if (this.transaction.amount != parseFloat(this.request.body.MNT_AMOUNT) || + this.request.body.MNT_ID != config.payanyway.id) { + this.throw(404, 'transaction with given params not found'); + } + + yield this.transaction.persist({ + status: Transaction.STATUS_SUCCESS + }); + + var order = this.order; + log.debug("will call order onSuccess module=" + order.module); + yield* require(order.module).onSuccess(order); + + this.body = 'SUCCESS'; +}; + +function checkSign(body) { + + var signature = md5(body.MNT_ID + body.MNT_TRANSACTION_ID + body.MNT_OPERATION_ID + + body.MNT_AMOUNT + body.MNT_CURRENCY_CODE + body.MNT_SUBSCRIBER_ID + body.MNT_TEST_MODE + + config.payanyway.secret).toUpperCase(); + + return signature == body.MNT_SIGNATURE; +} diff --git a/hmvc/paypal/controller/success.js b/hmvc/paypal/controller/success.js new file mode 100644 index 000000000..9c08f2e56 --- /dev/null +++ b/hmvc/paypal/controller/success.js @@ -0,0 +1,32 @@ +const config = require('config'); +const mongoose = require('mongoose'); +const payment = require('payment'); +const Transaction = payment.Transaction; +const log = require('js-log')(); +const md5 = require('MD5'); + +log.debugOn(); + + +exports.get = function* (next) { + yield* this.loadTransaction(); + + var transaction = this.transaction; + var order = this.order; + + var successUrl = this.getOrderSuccessUrl(); + var failUrl = this.getOrderUrl(); + + log.debug("transaction status: " + transaction.status); + + if (transaction.status) { + this.redirect(transaction.status == Transaction.STATUS_SUCCESS ? successUrl : failUrl); + } else { + this.render(__dirname, 'wait', { + transactionNumber: transaction.number, + successUrl: successUrl, + failUrl: failUrl + }); + } + +}; diff --git a/hmvc/paypal/controller/wait.js b/hmvc/paypal/controller/wait.js new file mode 100644 index 000000000..ca00b6ebb --- /dev/null +++ b/hmvc/paypal/controller/wait.js @@ -0,0 +1,37 @@ +const payment = require('payment'); +const config = require('config'); +const mongoose = require('mongoose'); +const Transaction = payment.Transaction; +const log = require('js-log')(); +const md5 = require('MD5'); + +log.debugOn(); + +exports.post = function* (next) { + + yield* this.loadTransaction(); + + var attempt = 0; + while (!this.transaction.status) { + attempt++; + if (attempt == 10) { + log.debug("timeout"); + this.body = 'TIMEOUT'; + return; + } + + yield delay(1000); + + this.transaction = yield Transaction.findOne({number: this.transaction.number }).exec(); + } + + log.debug('received'); + + this.body = this.transaction.status; +}; + +function delay(ms) { + return function(callback) { + setTimeout(callback, ms); + }; +} diff --git a/hmvc/paypal/index.js b/hmvc/paypal/index.js new file mode 100644 index 000000000..3b447451f --- /dev/null +++ b/hmvc/paypal/index.js @@ -0,0 +1,35 @@ +const jade = require('jade'); +const path = require('path'); +var config = require('config'); +var payment = require('payment'); + +var Transaction = payment.Transaction; + +var router = require('./router'); + +exports.middleware = router.middleware(); + +exports.createTransactionForm = function* (order) { + + var transaction = new Transaction({ + order: order._id, + amount: order.amount, + module: 'paypal' + }); + + yield transaction.persist(); + + return jade.renderFile(path.join(__dirname, 'templates/form.jade'), { + amount: transaction.amount, + number: transaction.number, + email: config.paypal.email, + resultUrl: 'http://' + config.domain + '/paypal/result?transactionNumber=' + transaction.number, + cancelUrl: 'http://' + config.domain + '/paypal/cancel?transactionNumber=' + transaction.number, + successUrl: 'http://' + config.domain + '/paypal/success?transactionNumber=' + transaction.number + + }); + +}; + + + diff --git a/hmvc/paypal/router.js b/hmvc/paypal/router.js new file mode 100644 index 000000000..ab80cc16c --- /dev/null +++ b/hmvc/paypal/router.js @@ -0,0 +1,21 @@ +var Router = require('koa-router'); +var payment = require('payment'); + +var router = module.exports = new Router(); + +var result = require('./controller/result'); +var success = require('./controller/success'); +var cancel = require('./controller/cancel'); +var wait = require('./controller/wait'); + +// webmoney server posts here (in background) +router.post('/result', result.post); + +// webmoney server redirects here if payment successful +router.get('/success', success.get); +// but if transaction status is not yet received, we wait... +router.post('/wait', wait.post); + +router.get('/cancel', cancel.get); + + diff --git a/hmvc/paypal/templates/form.jade b/hmvc/paypal/templates/form.jade new file mode 100644 index 000000000..4bd1eeebc --- /dev/null +++ b/hmvc/paypal/templates/form.jade @@ -0,0 +1,19 @@ + +form(method="POST" action="https://www.paypal.com/cgi-bin/webscr" accept-charset="UTF-8") + input(type="hidden",name="business",value=email) + input(type="hidden",name="notify_url",value=resultUrl) + input(type="hidden",name="cancel_return",value=cancelUrl) + input(type="hidden",name="return",value=successUrl) + input(type="hidden",name="invoice",value=number) + input(type="hidden",name="amount_1",value=amount) + input(type="hidden",name="item_name_1",value=("Оплата по счету " + number)) + input(type="hidden",name="cmd",value="_cart") + input(type="hidden",name="upload",value="1") + input(type="hidden",name="charset",value="utf-8") + input(type="hidden",name="no_note",value="1") + input(type="hidden",name="no_shipping",value="1") + input(type="hidden",name="rm",value="2") + input(type="hidden",name="currency_code",value="RUB") + input(type="hidden",name="lc",value="RU") + input(type="submit",value="Оплатить") + diff --git a/hmvc/paypal/templates/wait.jade b/hmvc/paypal/templates/wait.jade new file mode 100644 index 000000000..28e2830d9 --- /dev/null +++ b/hmvc/paypal/templates/wait.jade @@ -0,0 +1,25 @@ +div + p Минуточку, ожидаем информацию от сервера.. + p Если эта страница долго не отвечает - + | + a(href='#' onclick='window.location.reload(true)') перезагрузите её + | . + +script var transactionNumber = #{transactionNumber}, successUrl = '#{successUrl}', failUrl = '#{failUrl}'; + +script. + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/webmoney/wait'); + xhr.timeout = 20000; + + + xhr.onreadystatechange = function() { + if (xhr.readyState != 4) return; + + location.href = (xhr.responseText == 'success') ? successUrl : failUrl; + }; + + xhr.ontimeout = xhr.onabort = function() { + location.href = failUrl; + }; + xhr.send('transactionNumber=' + transactionNumber); diff --git a/hmvc/yandexmoney/controller/back.js b/hmvc/yandexmoney/controller/back.js index 42253e689..b57caa339 100644 --- a/hmvc/yandexmoney/controller/back.js +++ b/hmvc/yandexmoney/controller/back.js @@ -78,15 +78,22 @@ exports.get = function* (next) { // success! - yield* require(this.order.module).onSuccess(this.order); + var orderModule = require(this.order.module); + yield* orderModule.onSuccess(this.order); self.redirect(self.getOrderSuccessUrl()); } catch (e) { - - yield* fail(e.message); - return; + if (e instanceof URIError) { + yield* fail(e.message); + return; + } else if (e instanceof SyntaxError) { + yield* fail("некорректный ответ платёжной системы"); + return; + } else { + throw e; + } } /* jshint -W106 */ @@ -189,7 +196,7 @@ function throwResponseError(response) { message = "детали ошибки не указаны"; } - throw new Error(message); + throw new URIError(message); } function delay(ms) { diff --git a/modules/config/base.js b/modules/config/base.js deleted file mode 100644 index 6aae53fad..000000000 --- a/modules/config/base.js +++ /dev/null @@ -1,44 +0,0 @@ -var path = require('path'); -var fs = require('fs'); - -var secretPath = fs.existsSync(path.join(__dirname, 'secret.js')) ? './secret' : './secret.template'; -var secret = require(secretPath); - -module.exports = { - "port": process.env.PORT || 3000, - "host": process.env.HOST || '0.0.0.0', - "mongoose": { - "uri": "mongodb://localhost/" + (process.env.NODE_ENV == 'test' ? "js_test" : "js"), - "options": { - "server": { - "socketOptions": { - "keepAlive": 1 - }, - "poolSize": 5 - } - } - }, - session: { - keys: [secret.sessionKey] - }, - /* - verboseLogger: { - paths: ['/webmoney/any*'] - }, - */ - webmoney: secret.webmoney, - yandexmoney: secret.yandexmoney, - template: { - options: { - 'cache': process.env.NODE_ENV != 'development' - } - }, - crypto: { - hash: { - length: 128, - // may be slow(!): iterations = 12000 take ~60ms to generate strong password - iterations: process.env.NODE_ENV == 'prod' ? 12000 : 1 - } - }, - publicPath: path.join(process.cwd(), 'www') -}; diff --git a/modules/config/index.js b/modules/config/index.js index 877de2cdd..544e77f5e 100644 --- a/modules/config/index.js +++ b/modules/config/index.js @@ -1,4 +1,3 @@ - if (!process.env.NODE_ENV) { throw new Error("NODE_ENV environment variable is required"); } @@ -9,6 +8,46 @@ if (process.env.NODE_ENV == 'development' && process.env.DEV_TRACE) { require('clarify'); // Exclude node internal calls from the stack } -var base = require('./base'); -module.exports = base; +var path = require('path'); +var fs = require('fs'); + +var secretPath = fs.existsSync(path.join(__dirname, 'secret.js')) ? './secret' : './secret.template'; +var secret = require(secretPath); + +module.exports = { + "port": process.env.PORT || 3000, + "host": process.env.HOST || '0.0.0.0', + "domain": "stage.javascript.ru", + "mongoose": { + "uri": "mongodb://localhost/" + (process.env.NODE_ENV == 'test' ? "js_test" : "js"), + "options": { + "server": { + "socketOptions": { + "keepAlive": 1 + }, + "poolSize": 5 + } + } + }, + session: { + keys: [secret.sessionKey] + }, + webmoney: secret.webmoney, + yandexmoney: secret.yandexmoney, + payanyway: secret.payanyway, + paypal: secret.paypal, + template: { + options: { + 'cache': process.env.NODE_ENV != 'development' + } + }, + crypto: { + hash: { + length: 128, + // may be slow(!): iterations = 12000 take ~60ms to generate strong password + iterations: process.env.NODE_ENV == 'prod' ? 12000 : 1 + } + }, + publicPath: path.join(process.cwd(), 'www') +}; diff --git a/modules/config/secret.js b/modules/config/secret.js index 174511fff..46421c86c 100644 --- a/modules/config/secret.js +++ b/modules/config/secret.js @@ -15,3 +15,11 @@ exports.yandexmoney = { purse: '4100155697197' }; +exports.payanyway = { + id: "31873866", + secret: "cERfervdf43lkjl3cCDweqr2SSDFVbro" +}; + +exports.paypal = { + email: "iliakan@gmail.com" +}; diff --git a/modules/payment/models/transaction.js b/modules/payment/models/transaction.js index c3511c533..e781e1b56 100644 --- a/modules/payment/models/transaction.js +++ b/modules/payment/models/transaction.js @@ -5,9 +5,11 @@ var Order = require('./order'); var TransactionLog = require('./transactionLog'); /** - * Transaction is an actual payment for something - * Order may exist without any transactions (pay later) - * Transaction has it's own separate number (payment attempt) + * Transaction is an actual payment attempt (successful or not) for something + * - Order may exist without any transactions (pay later) + * - Transaction has it's own separate number (payment attempt) + * - Transaction amount can be different from order amount (partial payment) + * - Every transaction save generates a log record * @type {Schema} */ var schema = new Schema({ @@ -19,7 +21,7 @@ var schema = new Schema({ type: Number, required: true }, - module: { + module: { type: String, required: true }, @@ -32,16 +34,17 @@ var schema = new Schema({ }, statusMessage: { type: String - }, - data: String + } }); schema.plugin(autoIncrement.plugin, {model: 'Transaction', field: 'number'}); schema.statics.STATUS_SUCCESS = 'success'; +schema.statics.STATUS_PENDING = 'pending'; schema.statics.STATUS_FAIL = 'fail'; -schema.pre('save', function (next) { +// autoupdate order to SUCCESS when succeeded +schema.pre('save', function(next) { if (this.status == Transaction.STATUS_SUCCESS) { var orderId = this.order._id || this.order; Order.findByIdAndUpdate(orderId, {status: Transaction.STATUS_SUCCESS}, next); @@ -50,20 +53,46 @@ schema.pre('save', function (next) { } }); +// autolog all changes +schema.pre('save', function(next) { + + var log = new TransactionLog({ + transaction: this._id, + event: 'save', + data: { + status: this.status, + statusMessage: this.statusMessage, + amount: this.amount + } + }); + + log.save(function(err, doc) { + next(err); + }); +}); + schema.methods.getStatusDescription = function() { if (this.status == Transaction.STATUS_SUCCESS) { return 'оплата прошла успешно'; } + if (this.status == Transaction.STATUS_PENDING) { + return 'оплата ожидается'; + } + + if (this.status == Transaction.STATUS_FAIL) { + var result = 'оплата не прошла'; + if (this.statusMessage) result += ': ' + this.statusMessage; + return result; + } if (!this.status) { return 'нет информации об оплате'; } - var result = 'оплата не прошла'; - if (this.statusMessage) result += ': ' + this.statusMessage; - return result; + throw new Error("неподдерживаемый статус транзакции"); }; +// log anything related to the transaction schema.methods.log = function*(options) { options.transaction = this._id; @@ -75,7 +104,7 @@ schema.methods.log = function*(options) { options.data = JSON.stringify(options.data); } - console.log(options); +// console.log(options); var log = new TransactionLog(options); yield log.persist(); diff --git a/modules/setup/router.js b/modules/setup/router.js index 6a920dc10..5d138c392 100644 --- a/modules/setup/router.js +++ b/modules/setup/router.js @@ -24,7 +24,15 @@ module.exports = function(app) { app.csrf.addIgnorePath('/yandexmoney/:any*'); app.verboseLogger.addPath('/yandexmoney/:any*'); - // stick to bottom + app.use(mount('/payanyway', compose([payment.middleware, require('payanyway').middleware]))); + app.csrf.addIgnorePath('/payanyway/:any*'); + app.verboseLogger.addPath('/payanyway/:any*'); + + app.use(mount('/paypal', compose([payment.middleware, require('paypal').middleware]))); + app.csrf.addIgnorePath('/paypal/:any*'); + app.verboseLogger.addPath('/paypal/:any*'); + + // stick to bottom to detect any not-yet-processed /:slug app.use(mount('/', require('tutorial').middleware)); // by default if the router didn't find anything => it yields to next middleware diff --git a/tasks/linkModules.js b/tasks/linkModules.js index ee5777b3d..70b9dbee8 100644 --- a/tasks/linkModules.js +++ b/tasks/linkModules.js @@ -1,9 +1,12 @@ var fs = require('fs'); -var gulp = require('gulp'); var glob = require('glob'); var path = require('path'); var gutil = require('gulp-util'); +// Ensures the existance of a symlink linkDst -> linkSrc +// returns true if link was created +// returns false if link exists alread (and is correct) +// throws error if conflict (another file or link by that name) function ensureSymlinkSync(linkSrc, linkDst) { var lstat; try { @@ -13,14 +16,18 @@ function ensureSymlinkSync(linkSrc, linkDst) { if (lstat) { if (lstat.isSymbolicLink()) { - fs.unlinkSync(linkDst); + var oldDst = fs.readlinkSync(linkDst); + if (oldDst != linkSrc) { + throw new Error("Conflict: link already exists and has another value: " + oldDst); + } + return false; } else { throw new Error("Conflict: path exist and is not a link: " + linkDst); } } fs.symlinkSync(linkSrc, linkDst); - + return true; } module.exports = function(sources) { @@ -36,8 +43,9 @@ module.exports = function(sources) { var moduleToLinkName = path.basename(moduleToLinkRelPath); // auth var linkSrc = path.join('..', moduleToLinkRelPath); var linkDst = path.join('node_modules', moduleToLinkName); - gutil.log(linkSrc + " -> " + linkDst); - ensureSymlinkSync(linkSrc, linkDst); + if (ensureSymlinkSync(linkSrc, linkDst)) { + gutil.log(linkSrc + " -> " + linkDst); + } } }; From 6ef035d41cecde36640cfc6ad9c266618d28a38b Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 24 Jul 2014 00:20:09 +0400 Subject: [PATCH 096/130] typo --- gulpfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gulpfile.js b/gulpfile.js index 05a9febec..eb8506acd 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -44,7 +44,7 @@ gulp.task('watch', ['sprite', 'stylus'], function(neverCalled) { fse.ensureDirSync('www/js'); gp.dirSync('app/js', 'www/js'); - gulp.watch("app/**/*.sprite/**", ['sprite']); + gulp.watch("app/**/*.sprites/**", ['sprite']); gulp.watch("app/**/*.styl", ['stylus']); }); From 67e82e2f4662aac039daf27442eef842582535ed Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 24 Jul 2014 00:20:36 +0400 Subject: [PATCH 097/130] sprites gitkeep --- app/sprites/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/sprites/.gitkeep diff --git a/app/sprites/.gitkeep b/app/sprites/.gitkeep new file mode 100644 index 000000000..e69de29bb From 7500d695a4a3654d010bb98446fed5c85cc36169 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 24 Jul 2014 00:26:57 +0400 Subject: [PATCH 098/130] fix --- app/sprites/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/sprites/.gitkeep diff --git a/app/sprites/.gitkeep b/app/sprites/.gitkeep deleted file mode 100644 index e69de29bb..000000000 From f37b2675a63ca95798d588361b57f25f55d3601a Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 24 Jul 2014 00:28:04 +0400 Subject: [PATCH 099/130] fix --- app/stylesheets/sprites/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/stylesheets/sprites/.gitkeep diff --git a/app/stylesheets/sprites/.gitkeep b/app/stylesheets/sprites/.gitkeep new file mode 100644 index 000000000..e69de29bb From 06ee721718d2f16f89ffc203c0a50117331e51db Mon Sep 17 00:00:00 2001 From: Shuvalov Anton Date: Thu, 24 Jul 2014 00:45:30 +0400 Subject: [PATCH 100/130] add `path-to-regexp` module to `package.json` --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 1001fce04..e88cd0e6a 100755 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "mongoose-troop": "git://github.com/iliakan/mongoose-troop", "nib": "^1.0.3", "passport": "*", + "path-to-regexp": "^0.2.3", "require-tree": "*", "stylus": "*", "svgutils": "^0.7.0", From 48c6d0f31cada584d55b98b5acc73e18b52efabf Mon Sep 17 00:00:00 2001 From: Anton Vernigor Date: Thu, 24 Jul 2014 00:48:57 +0400 Subject: [PATCH 101/130] moving markup to jade and stylus --- app/stylesheets/base.styl | 4 +- .../blocks/important/important.styl | 2 +- app/stylesheets/blocks/sidebar/sidebar-bg.png | Bin 0 -> 1898 bytes app/stylesheets/blocks/sidebar/sidebar.styl | 87 ++++++++++++++++++ .../blocks/comments.jade | 0 .../blocks/page-footer.jade | 0 hmvc/markup/templates/blocks/sidebar.jade | 2 + .../markup/templates/blocks/social-aside.jade | 8 +- hmvc/markup/templates/layouts/base.jade | 1 + 9 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 app/stylesheets/blocks/sidebar/sidebar-bg.png create mode 100644 app/stylesheets/blocks/sidebar/sidebar.styl rename hmvc/markup/{template => templates}/blocks/comments.jade (100%) rename hmvc/markup/{template => templates}/blocks/page-footer.jade (100%) create mode 100644 hmvc/markup/templates/blocks/sidebar.jade diff --git a/app/stylesheets/base.styl b/app/stylesheets/base.styl index d7e4c9d49..5f2ed5c10 100755 --- a/app/stylesheets/base.styl +++ b/app/stylesheets/base.styl @@ -1,4 +1,4 @@ -@require "sprite/*" +@require "sprites/*" @require "blocks/block_facebook/facebook" @require "blocks/variables/variables" @@ -15,7 +15,7 @@ @require "blocks/nav-dropdown/nav-dropdown" @require "blocks/main-content/main-content" @require "blocks/main/main" -// @require "blocks/sidebar/sidebar" +@require "blocks/sidebar/sidebar" // @require "blocks/sidehelper/sidehelper" // @require "blocks/page-contents/page-contents" @require "blocks/breadcrumbs/breadcrumbs" diff --git a/app/stylesheets/blocks/important/important.styl b/app/stylesheets/blocks/important/important.styl index 4553e12da..fd12982c8 100644 --- a/app/stylesheets/blocks/important/important.styl +++ b/app/stylesheets/blocks/important/important.styl @@ -31,7 +31,7 @@ a:hover u text-decoration underline - &__task-link + & &__header &__task-link // we really need such a strong selector @extend $plain-link margin-right 40px diff --git a/app/stylesheets/blocks/sidebar/sidebar-bg.png b/app/stylesheets/blocks/sidebar/sidebar-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..121144413c0f978d472f0023dc5e38e24e28a618 GIT binary patch literal 1898 zcmV-w2bK7VP)+ART_x1Jl`T6dbVG7wVRUJ4ZXi@?ZDjy7FEKbTF*B9sXZ-*G z0338hSaefwW^{L9a%BKPWN%_+AVz6&Wp{6KYjYq&Q#R8HCL@R4`SsQ?GyaYybxoLkWIi*y0zfYCXxtp_8)E@xd%}lh zT*Uvz`!JLM!ueJp%#*(y4VGnRlHELZ-Y4&`0pvUR=<&qfB>D3Wn{bA$Z+hN%?7IXI zmWlRP=EFG4d6(RDEeX$+{Li3QpMMT}pCm2_Q51EN&bU@VEYGlDUx(kZxI_1c=5odFRF(vzh>e z0PMl`DqiN>3P4DwZ+#}$_W}xCi3P9Uw%yB;OD5WtE z_Kf;XifG}o5k8NUEm zTD3#GZLk*1K|_~vYnu{4m9u@_`UWuq>_hU44K3e^^g#ivdcmfD+qx1!Fq1#P`N1bm z-3+%}t)UPNYUyD~B>*HNuk9BVPyi^K6!e=6vM@UDcj~}FscevgH`y5>{Z z6@b`IAZgBY_-1gZiX{YXWDp8$y9Cez6O@uT+F@RQh?=tv9}Wi3vpw&PDFMW*^Z84* z>{AIKt_$L-Vq7W!Y8Mc~D>8oceXVnt61Ho*HHSsKq;3O!2_SP6=YYIbB3Z3M!n9sF zi6?X=fSRLJs8xAX<)r&80i=j-nVTRXNasQ>PC`S|BS`!!0VFQ!%x18s5pFt#x2h0* z!f5R{|4;#>S$t8MXnX~Lgqysm6bPmp7f>Wlb>6Xt6 zz65CaotEC+({zA3u@B(kEzZVze9&Gg!`E~%Y{)V4)PzT$eWR}c^w}8D*IEOslR(Ff z3^O_dmVl7m@}&?iM;#*>ZR)RE#vHpS)eM`PKa0mjHU+N?vNQwgTj_$G1!7v98aaRR!)dfXwQQJNAFe+3NJ5t&Sf>k^Bcg z>>-^JKrk%m=g+ud$zceOYop~PY>n(Za|xih}R9Er7M_QwzqD*)ZT^vVkL<-xUk z;hmQBML?^A!7Z*uFP?~RRy3yoPWfi*KD+>n#-)O`!~{(O8}iD1%^+0bGb7>(fc}!lPAxtS zxu~EDK)#vhx%2lU91GWKhXz^K060v3U#=#C%gDeLKw|+D)0n4K`3~g2^Fb2l+Z$g2 zDCXRW7zJVtEan)wySrFRr~owQE!@jnRxcKU0J`?F7lV~60NMTJC1`q&I;@t&)LR1R z3-UU|oX`TGy%JV0rR4ODR+B7mszxW8Q~@Zw(Q=r=I~kqPj**X_@%oI_bwi;7{F_*+ kpN!1^zbX0u{x_w60P|nG9;Pr%0ssI207*qoM6N<$f(EgIlmGw# literal 0 HcmV?d00001 diff --git a/app/stylesheets/blocks/sidebar/sidebar.styl b/app/stylesheets/blocks/sidebar/sidebar.styl new file mode 100644 index 000000000..634a1cb60 --- /dev/null +++ b/app/stylesheets/blocks/sidebar/sidebar.styl @@ -0,0 +1,87 @@ +.sidebar + box-shadow inset 10px 0 10px -10px #ddd + display table-cell + vertical-align top + width sidebar_width + min-width sidebar_width + padding: 0 sidebar_padding_right 35px sidebar_padding_left; + background url('/img/sidebar-bg.png') #F1F1F1 + +// .sidebar__section { +// margin: 25px 0 35px; +// } + +// .sidebar__list ul { +// margin: 8px 0 0 10px; +// } + +// .sidebar__list-item { +// margin: 0 0 6px; +// list-style: none; +// text-overflow: ellipsis; +// overflow: hidden; +// line-height: normal; + +// > .menu { +// margin: 10px 0 0 10px; +// } +// } + +// .sidebar__title { +// display: block; +// text-decoration: none; +// margin: 25px 0 12px; +// padding: 0 0 12px; +// font-size: 16px; +// font-weight: 700; +// color: #666; +// border-bottom: 1px solid #ccc; +// } + +// .sidebar__title_small { +// font-size: 12px; +// padding: 0 0 8px; +// } + +// .sidebar__complex-title { +// margin: 35px 0 10px; +// } + +// .sidebar__subtitle { +// font-size: 12px; +// font-weight: 400; +// } + +// .sidebar__subtitle + .sidebar__title { +// margin: -3px 0 10px; +// } + +// a { +// font-size: 13px; +// color: $link-color; +// } + +// a:hover { +// color: $link-hover-color; +// } + +// .sidebar__list-item.active > a { +// color: #B20000; +// font-weight: 700; +// } + +// .keep-visible.cloned { +// width: $sidebar-width; +// } + +// .fixed { +// position: fixed; +// top: 10px; +// margin: 0; +// width: $sidebar-width; +// .sidebar__title, +// .sidebar__complex-title { +// margin-top: 0; +// } +// } +// } \ No newline at end of file diff --git a/hmvc/markup/template/blocks/comments.jade b/hmvc/markup/templates/blocks/comments.jade similarity index 100% rename from hmvc/markup/template/blocks/comments.jade rename to hmvc/markup/templates/blocks/comments.jade diff --git a/hmvc/markup/template/blocks/page-footer.jade b/hmvc/markup/templates/blocks/page-footer.jade similarity index 100% rename from hmvc/markup/template/blocks/page-footer.jade rename to hmvc/markup/templates/blocks/page-footer.jade diff --git a/hmvc/markup/templates/blocks/sidebar.jade b/hmvc/markup/templates/blocks/sidebar.jade new file mode 100644 index 000000000..bf512d4a7 --- /dev/null +++ b/hmvc/markup/templates/blocks/sidebar.jade @@ -0,0 +1,2 @@ +.sidebar + | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Molestiae at consequuntur consectetur ad ipsam itaque enim hic, labore eveniet quaerat praesentium fuga amet sequi explicabo excepturi ut distinctio dolores officiis. \ No newline at end of file diff --git a/hmvc/markup/templates/blocks/social-aside.jade b/hmvc/markup/templates/blocks/social-aside.jade index fe2cdeec9..e6bd981b9 100644 --- a/hmvc/markup/templates/blocks/social-aside.jade +++ b/hmvc/markup/templates/blocks/social-aside.jade @@ -1,10 +1,10 @@ .social.aside a.social__anchor-up(href='#page') Наверх a.social__soc(href='https://plus.google.com/share?url=http://design.javascript.ru/intro', target='_blank') - img.soc-icon.google(src='/assets/x.gif', width='24', height='24', alt='g+', title='Поделиться в гугл+') + img.soc-icon.google(src='/img/x.gif', width='24', height='24', alt='g+', title='Поделиться в гугл+') a.social__soc(href='http://www.facebook.com/sharer/sharer.php?s=100&p[url]=http://design.javascript.ru/intro', target='_blank') - img.soc-icon.facebook(src='/assets/x.gif', width='24', height='24', alt='f', title='Поделиться в фейсбуке') + img.soc-icon.facebook(src='/img/x.gif', width='24', height='24', alt='f', title='Поделиться в фейсбуке') a.social__soc(href='https://twitter.com/share?url=http://design.javascript.ru/intro', target='_blank') - img.soc-icon.twitter(src='/assets/x.gif', width='24', height='24', alt='t', title='Поделиться в твитере') + img.soc-icon.twitter(src='/img/x.gif', width='24', height='24', alt='t', title='Поделиться в твитере') a.social__soc(href='http://vkontakte.ru/share.php?url=http://design.javascript.ru/intro', target='_blank') - img.soc-icon.vk(src='/assets/x.gif', width='24', height='24', alt='v', title='Поделиться во вконтакте') \ No newline at end of file + img.soc-icon.vk(src='/img/x.gif', width='24', height='24', alt='v', title='Поделиться во вконтакте') \ No newline at end of file diff --git a/hmvc/markup/templates/layouts/base.jade b/hmvc/markup/templates/layouts/base.jade index 96b98cb61..846013168 100644 --- a/hmvc/markup/templates/layouts/base.jade +++ b/hmvc/markup/templates/layouts/base.jade @@ -19,5 +19,6 @@ body(id='#{self.bodyId}') include ../blocks/prev-next-bottom include ../blocks/corrector include ../blocks/comments + include ../blocks/sidebar include ../blocks/page-footer include ../blocks/scripts \ No newline at end of file From 5522f1a071e8cd7fef4f8fc2cb2cf936934799a1 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 24 Jul 2014 11:05:20 +0400 Subject: [PATCH 102/130] hacking payments --- app/sprites/.gitkeep | 0 gulpfile.js | 4 +- hmvc/getpdf/controller/{main.js => order.js} | 9 ++- hmvc/getpdf/controller/pay.js | 10 ++- hmvc/getpdf/controller/success.js | 2 +- hmvc/getpdf/paymentMethods.js | 9 ++- hmvc/getpdf/router.js | 7 +-- hmvc/getpdf/templates/main.jade | 2 +- hmvc/payments/index.js | 61 +++++++++++++++++++ .../payments/lib}/loadOrder.js | 2 +- .../payments/lib}/loadTransaction.js | 2 +- .../payment => hmvc/payments}/models/order.js | 0 .../payments}/models/transaction.js | 3 + .../payments}/models/transactionLog.js | 0 .../webmoney/controller/callback.js} | 9 ++- hmvc/payments/webmoney/controller/fail.js | 19 ++++++ hmvc/payments/webmoney/controller/success.js | 9 +++ hmvc/payments/webmoney/index.js | 5 ++ hmvc/payments/webmoney/renderForm.js | 15 +++++ hmvc/payments/webmoney/router.js | 25 ++++++++ .../webmoney/templates/form.jade | 2 +- .../webmoney/templates/wait.jade | 0 hmvc/{ => payments}/webmoney/test/.jshintrc | 0 hmvc/webmoney/controller/fail.js | 23 ------- hmvc/webmoney/controller/success.js | 32 ---------- hmvc/webmoney/controller/wait.js | 37 ----------- hmvc/webmoney/index.js | 31 ---------- hmvc/webmoney/router.js | 28 --------- hmvc/webmoney/test/web/test.js | 9 --- modules/app.js | 2 + modules/config/index.js | 5 +- modules/config/secret.js | 38 +++++++----- modules/payment/index.js | 31 ---------- modules/payment/lib/orderUtils.js | 11 ---- modules/setup/payments.js | 7 +++ modules/setup/router.js | 9 ++- package.json | 1 + 37 files changed, 203 insertions(+), 256 deletions(-) delete mode 100644 app/sprites/.gitkeep rename hmvc/getpdf/controller/{main.js => order.js} (86%) create mode 100644 hmvc/payments/index.js rename {modules/payment/lib/context => hmvc/payments/lib}/loadOrder.js (95%) rename {modules/payment/lib/context => hmvc/payments/lib}/loadTransaction.js (95%) rename {modules/payment => hmvc/payments}/models/order.js (100%) rename {modules/payment => hmvc/payments}/models/transaction.js (98%) rename {modules/payment => hmvc/payments}/models/transactionLog.js (100%) rename hmvc/{webmoney/controller/result.js => payments/webmoney/controller/callback.js} (92%) create mode 100644 hmvc/payments/webmoney/controller/fail.js create mode 100644 hmvc/payments/webmoney/controller/success.js create mode 100644 hmvc/payments/webmoney/index.js create mode 100644 hmvc/payments/webmoney/renderForm.js create mode 100644 hmvc/payments/webmoney/router.js rename hmvc/{ => payments}/webmoney/templates/form.jade (86%) rename hmvc/{ => payments}/webmoney/templates/wait.jade (100%) rename hmvc/{ => payments}/webmoney/test/.jshintrc (100%) delete mode 100644 hmvc/webmoney/controller/fail.js delete mode 100644 hmvc/webmoney/controller/success.js delete mode 100644 hmvc/webmoney/controller/wait.js delete mode 100644 hmvc/webmoney/index.js delete mode 100644 hmvc/webmoney/router.js delete mode 100644 hmvc/webmoney/test/web/test.js delete mode 100644 modules/payment/index.js delete mode 100644 modules/payment/lib/orderUtils.js create mode 100644 modules/setup/payments.js diff --git a/app/sprites/.gitkeep b/app/sprites/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/gulpfile.js b/gulpfile.js index 5996071a7..20812c54e 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -27,7 +27,7 @@ gulp.task('lint-watch', ['lint'], function(neverCalled) { //gulp.task('lint', require('./tasks/lint-full-die')(serverSources)); -gulp.task('watch', ['sprite', 'stylus'], function(neverCalled) { +gulp.task('watch', ['stylus'], function(neverCalled) { /* browserifyTask({ src: 'app/js/index.js', @@ -51,7 +51,7 @@ gulp.task('watch', ['sprite', 'stylus'], function(neverCalled) { }); // Show errors if encountered -gulp.task('stylus', ['clean-compiled-css'], function() { +gulp.task('stylus', ['clean-compiled-css', 'sprite'], function() { return gulp.src('./app/stylesheets/base.styl') // without plumber if stylus emits PluginError, it will disappear at the next step // plumber propagates it down the chain diff --git a/hmvc/getpdf/controller/main.js b/hmvc/getpdf/controller/order.js similarity index 86% rename from hmvc/getpdf/controller/main.js rename to hmvc/getpdf/controller/order.js index 3120e970f..845ea2f36 100644 --- a/hmvc/getpdf/controller/main.js +++ b/hmvc/getpdf/controller/order.js @@ -1,14 +1,13 @@ -const payment = require('payment'); -var Order = payment.Order; -var Transaction = payment.Transaction; +const payments = require('payments'); +var Order = payments.Order; +var Transaction = payments.Transaction; exports.get = function*(next) { - console.log("HERE"); - if (this.params.orderNumber) { yield* this.loadOrder(); } else { + // this order is not saved anywhere, // it's only used to initially fill the form this.order = new Order({ diff --git a/hmvc/getpdf/controller/pay.js b/hmvc/getpdf/controller/pay.js index ce18edca7..dd34e8962 100644 --- a/hmvc/getpdf/controller/pay.js +++ b/hmvc/getpdf/controller/pay.js @@ -1,7 +1,7 @@ var mongoose = require('mongoose'); var log = require('js-log')(); -var payment = require('payment'); -var Order = payment.Order; +var payments = require('payments'); +var Order = payments.Order; var methods = require('../paymentMethods').methods; log.debugOn(); @@ -13,14 +13,12 @@ exports.post = function*(next) { if (!method) { this.throw(403, "Unsupported payment method"); } - var methodApi = require(method.module); // webmoney if (this.order) { log.debug("order exists", this.order.number); yield* updateOrderFromBody(this.request.body, this.order); } else { - // this order is not saved anywhere, - // it's only used to initially fill the form + // new order template this.order = new Order({ amount: 1, module: 'getpdf', @@ -37,7 +35,7 @@ exports.post = function*(next) { this.session.orders.push(this.order.number); } - var form = yield* methodApi.createTransactionForm(this.order); + var form = yield* payments.createTransactionForm(this.order, method.name); this.body = form; diff --git a/hmvc/getpdf/controller/success.js b/hmvc/getpdf/controller/success.js index 79259a18a..3b001db83 100644 --- a/hmvc/getpdf/controller/success.js +++ b/hmvc/getpdf/controller/success.js @@ -1,5 +1,5 @@ exports.get = function*(next) { - yield* this.loadOrder(); +// yield* payment.loadOrder(this); this.body = 'THANK YOU'; }; diff --git a/hmvc/getpdf/paymentMethods.js b/hmvc/getpdf/paymentMethods.js index 37296f6f6..27067fc7b 100644 --- a/hmvc/getpdf/paymentMethods.js +++ b/hmvc/getpdf/paymentMethods.js @@ -1,7 +1,6 @@ exports.methods = { - 'yandexmoney': {module: "yandexmoney", title: "Яндекс.Деньги"}, - 'webmoney': {module: "webmoney", title: "Webmoney"}, - 'payanyway': {module: "payanyway", title: "PayAnyWay"}, - 'interkassa': {module: "interkassa", title: "Интеркасса"}, - 'paypal': {module: "paypal", title: "Paypal"} + 'yandexmoney': {name: "yandexmoney", title: "Яндекс.Деньги"}, + 'webmoney': {name: "webmoney", title: "Webmoney"}, + 'payanyway': {name: "payanyway", title: "PayAnyWay"}, + 'paypal': {name: "paypal", title: "Paypal"} }; diff --git a/hmvc/getpdf/router.js b/hmvc/getpdf/router.js index d28c7f06f..87985f28a 100644 --- a/hmvc/getpdf/router.js +++ b/hmvc/getpdf/router.js @@ -1,14 +1,13 @@ -var payment = require('payment'); var Router = require('koa-router'); var router = module.exports = new Router(); -var main = require('./controller/main'); +var order = require('./controller/order'); var pay = require('./controller/pay'); var success = require('./controller/success'); -router.get('', main.get); -router.get('/order/:orderNumber', main.get); +router.get('', order.get); +router.get('/order/:orderNumber', order.get); router.post('/pay', pay.post); router.get('/success/:orderNumber', success.get); diff --git a/hmvc/getpdf/templates/main.jade b/hmvc/getpdf/templates/main.jade index 050b403d8..ce74f6b6b 100644 --- a/hmvc/getpdf/templates/main.jade +++ b/hmvc/getpdf/templates/main.jade @@ -8,7 +8,7 @@ div input(name="email" value=order.data.email placeholder="E-mail") select(name="paymentMethod") each paymentMethod in paymentMethods - option(value=paymentMethod.module) #{paymentMethod.title} + option(value=paymentMethod.name) #{paymentMethod.title} input(type="submit" value="Оплатить") script(src="http://code.jquery.com/jquery-2.1.1.js") diff --git a/hmvc/payments/index.js b/hmvc/payments/index.js new file mode 100644 index 000000000..375ff7c22 --- /dev/null +++ b/hmvc/payments/index.js @@ -0,0 +1,61 @@ +var mount = require('koa-mount'); +var config = require('config'); + +// Interaction with payment systems only. + +exports.loadOrder = require('./lib/loadOrder'); +exports.loadTransaction = require('./lib/loadTransaction'); + +var Order = exports.Order = require('./models/order'); +var Transaction = exports.Transaction = require('./models/transaction'); +var TransactionLog = exports.TransactionLog = require('./models/transactionLog'); + +// all submodules +var paymentModules = {}; +for(var name in config.payments.modules) { + paymentModules[name] = require('./' + name); +} + +// mount('/webmoney', webmoney.middleware()) +var paymentMounts = []; +for(var name in paymentModules) { + paymentMounts.push(mount('/' + name, paymentModules[name].middleware)); +} + +// delegate all HTTP calls to payment modules +exports.middleware = function*(next) { + + for (var i = 0; i < paymentMounts.length; i++) { + yield* paymentMounts[i].call(this, next); + } + +}; + +exports.populateContextMiddleware = function*(next) { + this.redirectToOrder = function(order) { + order = order || this.order; + this.redirect('/' + order.module + '/order/' + order.number); + }; + this.loadOrder = exports.loadOrder; + this.loadTransaction = exports.loadTransaction; + yield* next; +}; + +// creates transaction and returns the form to submit for its payment +// delegates form to the method +exports.createTransactionForm = function* (order, method) { + + var transaction = new Transaction({ + order: order._id, + amount: order.amount, + module: method + }); + + yield transaction.persist(); + + return paymentModules[method].renderForm(transaction); + +}; + + + diff --git a/modules/payment/lib/context/loadOrder.js b/hmvc/payments/lib/loadOrder.js similarity index 95% rename from modules/payment/lib/context/loadOrder.js rename to hmvc/payments/lib/loadOrder.js index 12db151d3..f71e937ca 100644 --- a/modules/payment/lib/context/loadOrder.js +++ b/hmvc/payments/lib/loadOrder.js @@ -1,5 +1,5 @@ var mongoose = require('mongoose'); -var Order = require('../../models/order'); +var Order = require('../models/order'); var assert = require('assert'); // Populates this.order with the order by "orderNumber" parameter diff --git a/modules/payment/lib/context/loadTransaction.js b/hmvc/payments/lib/loadTransaction.js similarity index 95% rename from modules/payment/lib/context/loadTransaction.js rename to hmvc/payments/lib/loadTransaction.js index e1f14b732..2603a7c34 100644 --- a/modules/payment/lib/context/loadTransaction.js +++ b/hmvc/payments/lib/loadTransaction.js @@ -1,5 +1,5 @@ var mongoose = require('mongoose'); -var Transaction = require('../../models/transaction'); +var Transaction = require('../models/transaction'); var log = require('js-log')(); var assert = require('assert'); diff --git a/modules/payment/models/order.js b/hmvc/payments/models/order.js similarity index 100% rename from modules/payment/models/order.js rename to hmvc/payments/models/order.js diff --git a/modules/payment/models/transaction.js b/hmvc/payments/models/transaction.js similarity index 98% rename from modules/payment/models/transaction.js rename to hmvc/payments/models/transaction.js index e781e1b56..f6c64d2f7 100644 --- a/modules/payment/models/transaction.js +++ b/hmvc/payments/models/transaction.js @@ -43,6 +43,8 @@ schema.statics.STATUS_SUCCESS = 'success'; schema.statics.STATUS_PENDING = 'pending'; schema.statics.STATUS_FAIL = 'fail'; +/* +// DEPRECATED: orderModule.onSuccess updates its status // autoupdate order to SUCCESS when succeeded schema.pre('save', function(next) { if (this.status == Transaction.STATUS_SUCCESS) { @@ -52,6 +54,7 @@ schema.pre('save', function(next) { next(); } }); +*/ // autolog all changes schema.pre('save', function(next) { diff --git a/modules/payment/models/transactionLog.js b/hmvc/payments/models/transactionLog.js similarity index 100% rename from modules/payment/models/transactionLog.js rename to hmvc/payments/models/transactionLog.js diff --git a/hmvc/webmoney/controller/result.js b/hmvc/payments/webmoney/controller/callback.js similarity index 92% rename from hmvc/webmoney/controller/result.js rename to hmvc/payments/webmoney/controller/callback.js index 89bc1f104..9819e22f0 100644 --- a/hmvc/webmoney/controller/result.js +++ b/hmvc/payments/webmoney/controller/callback.js @@ -1,8 +1,7 @@ -const payment = require('payment'); const config = require('config'); const mongoose = require('mongoose'); -const Order = payment.Order; -const Transaction = payment.Transaction; +const Order = require('../../models/order'); +const Transaction = require('../../models/transaction'); const log = require('js-log')(); const md5 = require('MD5'); @@ -35,7 +34,7 @@ exports.post = function* (next) { yield* this.loadTransaction('LMI_PAYMENT_NO', {skipOwnerCheck : true}); - if (!checkSign(this.request.body)) { + if (!checkSignature(this.request.body)) { log.debug("wrong signature"); this.throw(403, "wrong signature"); } @@ -63,7 +62,7 @@ exports.post = function* (next) { }; -function checkSign(body) { +function checkSignature(body) { var signature = md5(body.LMI_PAYEE_PURSE + body.LMI_PAYMENT_AMOUNT + body.LMI_PAYMENT_NO + body.LMI_MODE + body.LMI_SYS_INVS_NO + body.LMI_SYS_TRANS_NO + body.LMI_SYS_TRANS_DATE + diff --git a/hmvc/payments/webmoney/controller/fail.js b/hmvc/payments/webmoney/controller/fail.js new file mode 100644 index 000000000..8cfa96a06 --- /dev/null +++ b/hmvc/payments/webmoney/controller/fail.js @@ -0,0 +1,19 @@ +const mongoose = require('mongoose'); +const Transaction = require('../../models/transaction'); +const log = require('js-log')(); + +log.debugOn(); + + + +exports.post = function* (next) { + + yield* this.loadTransaction('LMI_PAYMENT_NO'); + + yield this.transaction.persist({ + status: Transaction.STATUS_FAIL + }); + + this.redirectToOrder(); + +}; diff --git a/hmvc/payments/webmoney/controller/success.js b/hmvc/payments/webmoney/controller/success.js new file mode 100644 index 000000000..6b4d94e5f --- /dev/null +++ b/hmvc/payments/webmoney/controller/success.js @@ -0,0 +1,9 @@ +const config = require('config'); +const mongoose = require('mongoose'); +const log = require('js-log')(); + +exports.post = function* (next) { + yield* this.loadTransaction('LMI_PAYMENT_NO'); + + this.redirectToOrder(); +}; diff --git a/hmvc/payments/webmoney/index.js b/hmvc/payments/webmoney/index.js new file mode 100644 index 000000000..1a9c21a42 --- /dev/null +++ b/hmvc/payments/webmoney/index.js @@ -0,0 +1,5 @@ +var router = require('./router'); + +exports.middleware = router.middleware(); + +exports.renderForm = require('./renderForm'); diff --git a/hmvc/payments/webmoney/renderForm.js b/hmvc/payments/webmoney/renderForm.js new file mode 100644 index 000000000..f32e4e22c --- /dev/null +++ b/hmvc/payments/webmoney/renderForm.js @@ -0,0 +1,15 @@ +const jade = require('jade'); +const config = require('config'); +const path = require('path'); + +module.exports = function (transaction) { + + return jade.renderFile(path.join(__dirname, 'templates/form.jade'), { + amount: transaction.amount, + number: transaction.number, + webmoney: config.payments.modules.webmoney + }); + +}; + + diff --git a/hmvc/payments/webmoney/router.js b/hmvc/payments/webmoney/router.js new file mode 100644 index 000000000..adcb5361e --- /dev/null +++ b/hmvc/payments/webmoney/router.js @@ -0,0 +1,25 @@ +var Router = require('koa-router'); + +var router = module.exports = new Router(); + +var callback = require('./controller/callback'); +var success = require('./controller/success'); +var fail = require('./controller/fail'); + + +// webmoney server posts here (in background) +router.post('/callback', function* (next) { + if (this.request.body.LMI_PREREQUEST == '1') { + yield* callback.prerequest.call(this, next); + } else { + yield* callback.post.call(this, next); + } +}); + +// webmoney server redirects here if payment successful +router.post('/success', success.post); + +// webmoney server redirects here if payment failed +router.post('/fail', fail.post); + + diff --git a/hmvc/webmoney/templates/form.jade b/hmvc/payments/webmoney/templates/form.jade similarity index 86% rename from hmvc/webmoney/templates/form.jade rename to hmvc/payments/webmoney/templates/form.jade index 566ade235..8ad6d67ce 100644 --- a/hmvc/webmoney/templates/form.jade +++ b/hmvc/payments/webmoney/templates/form.jade @@ -2,6 +2,6 @@ form(method="POST",action="https://merchant.webmoney.ru/lmi/payment.asp") input(type="hidden",name="LMI_PAYMENT_AMOUNT",value=amount) input(type="hidden",name="LMI_PAYMENT_DESC_BASE64",value=new Buffer('оплата по счету ' + number).toString('base64')) input(type="hidden",name="LMI_PAYMENT_NO",value=number) - input(type="hidden",name="LMI_PAYEE_PURSE",value=purse) + input(type="hidden",name="LMI_PAYEE_PURSE",value=webmoney.purse) input(type="hidden",name="LMI_SIM_MODE",value=(isTest ? 1 : 0)) input(type="submit",value="Оплатить") diff --git a/hmvc/webmoney/templates/wait.jade b/hmvc/payments/webmoney/templates/wait.jade similarity index 100% rename from hmvc/webmoney/templates/wait.jade rename to hmvc/payments/webmoney/templates/wait.jade diff --git a/hmvc/webmoney/test/.jshintrc b/hmvc/payments/webmoney/test/.jshintrc similarity index 100% rename from hmvc/webmoney/test/.jshintrc rename to hmvc/payments/webmoney/test/.jshintrc diff --git a/hmvc/webmoney/controller/fail.js b/hmvc/webmoney/controller/fail.js deleted file mode 100644 index b4e360d2e..000000000 --- a/hmvc/webmoney/controller/fail.js +++ /dev/null @@ -1,23 +0,0 @@ -const mongoose = require('mongoose'); -const payment = require('payment'); -const Order = payment.Order; -const Transaction = payment.Transaction; -const log = require('js-log')(); - -log.debugOn(); - - - -exports.get = function* (next) { - - yield* this.loadTransaction(); - - this.transaction.persist({ - status: Transaction.STATUS_FAIL - }); - - yield this.transaction.log({ event: 'fail' }); - - this.redirect(this.getOrderUrl()); - -}; diff --git a/hmvc/webmoney/controller/success.js b/hmvc/webmoney/controller/success.js deleted file mode 100644 index c4e0cb62e..000000000 --- a/hmvc/webmoney/controller/success.js +++ /dev/null @@ -1,32 +0,0 @@ -const config = require('config'); -const mongoose = require('mongoose'); -const payment = require('payment'); -const Transaction = payment.Transaction; -const log = require('js-log')(); -const md5 = require('MD5'); - -log.debugOn(); - - -exports.get = function* (next) { - yield* this.loadTransaction('LMI_PAYMENT_NO'); - - var transaction = this.transaction; - var order = this.order; - - var successUrl = this.getOrderSuccessUrl(); - var failUrl = this.getOrderUrl(); - - log.debug("transaction status: " + transaction.status); - - if (transaction.status) { - this.redirect(transaction.status == Transaction.STATUS_SUCCESS ? successUrl : failUrl); - } else { - this.render(__dirname, 'wait', { - transactionNumber: transaction.number, - successUrl: successUrl, - failUrl: failUrl - }); - } - -}; diff --git a/hmvc/webmoney/controller/wait.js b/hmvc/webmoney/controller/wait.js deleted file mode 100644 index ca00b6ebb..000000000 --- a/hmvc/webmoney/controller/wait.js +++ /dev/null @@ -1,37 +0,0 @@ -const payment = require('payment'); -const config = require('config'); -const mongoose = require('mongoose'); -const Transaction = payment.Transaction; -const log = require('js-log')(); -const md5 = require('MD5'); - -log.debugOn(); - -exports.post = function* (next) { - - yield* this.loadTransaction(); - - var attempt = 0; - while (!this.transaction.status) { - attempt++; - if (attempt == 10) { - log.debug("timeout"); - this.body = 'TIMEOUT'; - return; - } - - yield delay(1000); - - this.transaction = yield Transaction.findOne({number: this.transaction.number }).exec(); - } - - log.debug('received'); - - this.body = this.transaction.status; -}; - -function delay(ms) { - return function(callback) { - setTimeout(callback, ms); - }; -} diff --git a/hmvc/webmoney/index.js b/hmvc/webmoney/index.js deleted file mode 100644 index ce6f0c53d..000000000 --- a/hmvc/webmoney/index.js +++ /dev/null @@ -1,31 +0,0 @@ -const jade = require('jade'); -const path = require('path'); -var config = require('config'); -var payment = require('payment'); - -var Transaction = payment.Transaction; - -var router = require('./router'); - -exports.middleware = router.middleware(); - -exports.createTransactionForm = function* (order) { - - var transaction = new Transaction({ - order: order._id, - amount: order.amount, - module: 'webmoney' - }); - - yield transaction.persist(); - - return jade.renderFile(path.join(__dirname, 'templates/form.jade'), { - amount: transaction.amount, - number: transaction.number, - purse: config.webmoney.purse - }); - -}; - - - diff --git a/hmvc/webmoney/router.js b/hmvc/webmoney/router.js deleted file mode 100644 index 6ec5e1de5..000000000 --- a/hmvc/webmoney/router.js +++ /dev/null @@ -1,28 +0,0 @@ -var Router = require('koa-router'); -var payment = require('payment'); - -var router = module.exports = new Router(); - -var result = require('./controller/result'); -var success = require('./controller/success'); -var fail = require('./controller/fail'); -var wait = require('./controller/wait'); - -// webmoney server posts here (in background) -router.post('/result', function* (next) { - if (this.request.body.LMI_PREREQUEST == '1') { - yield* result.prerequest.call(this, next); - } else { - yield* result.post.call(this, next); - } -}); - -// webmoney server redirects here if payment successful -router.get('/success', success.get); -// but if transaction status is not yet received, we wait... -router.post('/wait', wait.post); - -// webmoney server redirects here if payment failed -router.get('/fail', fail.get); - - diff --git a/hmvc/webmoney/test/web/test.js b/hmvc/webmoney/test/web/test.js deleted file mode 100644 index c573e07a7..000000000 --- a/hmvc/webmoney/test/web/test.js +++ /dev/null @@ -1,9 +0,0 @@ -var app = require('app'); - -describe("Test", function() { - - it("works", function() { - //console.log("WORKS"); - }); - -}); diff --git a/modules/app.js b/modules/app.js index 6c4cfc06f..2f6c4af07 100644 --- a/modules/app.js +++ b/modules/app.js @@ -40,6 +40,8 @@ if (process.env.NODE_ENV == 'development') { requireSetup('setup/session'); requireSetup('setup/csrf'); +requireSetup('setup/payments'); + requireSetup('setup/render'); requireSetup('setup/router'); diff --git a/modules/config/index.js b/modules/config/index.js index 544e77f5e..ba226e7a1 100644 --- a/modules/config/index.js +++ b/modules/config/index.js @@ -33,10 +33,7 @@ module.exports = { session: { keys: [secret.sessionKey] }, - webmoney: secret.webmoney, - yandexmoney: secret.yandexmoney, - payanyway: secret.payanyway, - paypal: secret.paypal, + payments: secret.payments, template: { options: { 'cache': process.env.NODE_ENV != 'development' diff --git a/modules/config/secret.js b/modules/config/secret.js index 46421c86c..7e974f3ab 100644 --- a/modules/config/secret.js +++ b/modules/config/secret.js @@ -3,23 +3,29 @@ exports.sessionKey = "KillerIsJim"; -exports.webmoney = { - secretKey: 'hjvRVxstw42VDdpk9', - purse: 'R146240663944' -}; +exports.payments = { + modules: { + webmoney: { + secretKey: 'hjvRVxstw42VDdpk9', + purse: 'R146240663944' + } + /*, -exports.yandexmoney = { - redirectUri: 'http://stage.javascript.ru/yandexmoney/back', - clientId: '6527BEA7C6189BF55A46FB379E642E2A20098792D24E98939540B0870F5B2228', - clientSecret: '581E6A7542381F4F206789140E39A73CFB4C39A9C625127D9A731A5A95C2D1B8FE0E26E267E829C95A76F3313E3DD9F02CC223E890972E75952A70C93DCBEDBF', - purse: '4100155697197' -}; + yandexmoney: { + redirectUri: 'http://stage.javascript.ru/yandexmoney/back', + clientId: '6527BEA7C6189BF55A46FB379E642E2A20098792D24E98939540B0870F5B2228', + clientSecret: '581E6A7542381F4F206789140E39A73CFB4C39A9C625127D9A731A5A95C2D1B8FE0E26E267E829C95A76F3313E3DD9F02CC223E890972E75952A70C93DCBEDBF', + purse: '4100155697197' + }, -exports.payanyway = { - id: "31873866", - secret: "cERfervdf43lkjl3cCDweqr2SSDFVbro" -}; + payanyway: { + id: "31873866", + secret: "cERfervdf43lkjl3cCDweqr2SSDFVbro" + }, -exports.paypal = { - email: "iliakan@gmail.com" + paypal: { + email: "iliakan@gmail.com" + }*/ + } }; + diff --git a/modules/payment/index.js b/modules/payment/index.js deleted file mode 100644 index cf9dddf98..000000000 --- a/modules/payment/index.js +++ /dev/null @@ -1,31 +0,0 @@ - -exports.Order = require('./models/order'); -exports.Transaction = require('./models/transaction'); -exports.TransactionLog = require('./models/transactionLog'); - -var orderUtils = require('./lib/orderUtils'); -exports.orderUtils = orderUtils; - -var loadOrder = require('./lib/context/loadOrder'); -var loadTransaction = require('./lib/context/loadTransaction'); - - -exports.middleware = function*(next) { - this.loadOrder = loadOrder; - this.loadTransaction = loadTransaction; - - this.getOrderSuccessUrl = function() { - return orderUtils.getSuccessUrl(this.order); - }; - - this.getOrderUrl = function() { - return orderUtils.getUrl(this.order); - }; - - this.getOrderPendingUrl = function() { - return orderUtils.getPendingUrl(this.order); - }; - - yield* next; -}; - diff --git a/modules/payment/lib/orderUtils.js b/modules/payment/lib/orderUtils.js deleted file mode 100644 index 565f289b9..000000000 --- a/modules/payment/lib/orderUtils.js +++ /dev/null @@ -1,11 +0,0 @@ -module.exports = { - getSuccessUrl: function(order) { - return '/' + order.module + '/success/' + order.number; - }, - getUrl: function(order) { - return '/' + order.module + '/order/' + order.number; - }, - getPendingUrl: function(order) { - return '/' + order.module + '/pending/' + order.number; - } -}; diff --git a/modules/setup/payments.js b/modules/setup/payments.js new file mode 100644 index 000000000..16d4d9fbe --- /dev/null +++ b/modules/setup/payments.js @@ -0,0 +1,7 @@ +const mongoose = require('mongoose'); +const payments = require('payments'); +const config = require('config'); + +module.exports = function(app) { + app.use(payments.populateContextMiddleware); +}; diff --git a/modules/setup/router.js b/modules/setup/router.js index 5d138c392..5a4d87514 100644 --- a/modules/setup/router.js +++ b/modules/setup/router.js @@ -1,7 +1,6 @@ 'use strict'; var mount = require('koa-mount'); -var payment = require('payment'); var compose = require('koa-compose'); module.exports = function(app) { @@ -14,8 +13,13 @@ module.exports = function(app) { } // need to compose, because mount takes only 1 middleware - app.use(mount('/getpdf', compose([payment.middleware, require('getpdf').middleware]))); + app.use(mount('/getpdf', require('getpdf').middleware)); + app.use(mount('/payments', require('payments').middleware)); + app.csrf.addIgnorePath('/payments/:any*'); + app.verboseLogger.addPath('/payments/:any*'); + + /* app.use(mount('/webmoney', compose([payment.middleware, require('webmoney').middleware]))); app.csrf.addIgnorePath('/webmoney/:any*'); app.verboseLogger.addPath('/webmoney/:any*'); @@ -31,6 +35,7 @@ module.exports = function(app) { app.use(mount('/paypal', compose([payment.middleware, require('paypal').middleware]))); app.csrf.addIgnorePath('/paypal/:any*'); app.verboseLogger.addPath('/paypal/:any*'); +*/ // stick to bottom to detect any not-yet-processed /:slug app.use(mount('/', require('tutorial').middleware)); diff --git a/package.json b/package.json index 1001fce04..e88cd0e6a 100755 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "mongoose-troop": "git://github.com/iliakan/mongoose-troop", "nib": "^1.0.3", "passport": "*", + "path-to-regexp": "^0.2.3", "require-tree": "*", "stylus": "*", "svgutils": "^0.7.0", From 09c6cc353be5af5d5431340fd4ca967a0806a80a Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 24 Jul 2014 11:08:45 +0400 Subject: [PATCH 103/130] kill old links on link-modules --- tasks/linkModules.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tasks/linkModules.js b/tasks/linkModules.js index 70b9dbee8..ebf2a9d06 100644 --- a/tasks/linkModules.js +++ b/tasks/linkModules.js @@ -15,15 +15,16 @@ function ensureSymlinkSync(linkSrc, linkDst) { } if (lstat) { - if (lstat.isSymbolicLink()) { - var oldDst = fs.readlinkSync(linkDst); - if (oldDst != linkSrc) { - throw new Error("Conflict: link already exists and has another value: " + oldDst); - } - return false; - } else { + if (!lstat.isSymbolicLink()) { throw new Error("Conflict: path exist and is not a link: " + linkDst); } + + var oldDst = fs.readlinkSync(linkDst); + if (oldDst == linkSrc) { + return false; // already exists + } + // kill old link! + fs.unlinkSync(linkDst); } fs.symlinkSync(linkSrc, linkDst); From 876775db61b39d6ef78175408bd52022a8823ec8 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Thu, 24 Jul 2014 16:12:31 +0400 Subject: [PATCH 104/130] gulp.sh/mocha.sh -> gulp/mocha, added link-modules autorun, hacking payments --- gulp.sh => gulp | 0 gulpfile.js | 46 +++++++++++-------- hmvc/getpdf/controller/pay.js | 1 + hmvc/payanyway/controller/fail.js | 19 -------- hmvc/payanyway/controller/inprogress.js | 19 -------- hmvc/payanyway/controller/success.js | 32 ------------- hmvc/payanyway/index.js | 31 ------------- hmvc/payments/index.js | 8 +--- hmvc/payments/models/transaction.js | 3 ++ .../payanyway/controller/callback.js} | 16 +++---- .../payanyway/controller/cancel.js | 7 +-- hmvc/payments/payanyway/controller/fail.js | 14 ++++++ .../payanyway/controller/inprogress.js | 11 +++++ hmvc/payments/payanyway/controller/success.js | 7 +++ hmvc/payments/payanyway/index.js | 11 +++++ hmvc/payments/payanyway/renderForm.js | 15 ++++++ hmvc/{paypal => payments/payanyway}/router.js | 9 ++-- .../payanyway/templates/form.jade | 0 .../paypal/controller/cancel.js | 0 .../paypal/controller/result.js | 2 +- .../paypal/controller/success.js | 0 .../paypal}/controller/wait.js | 0 hmvc/{ => payments}/paypal/index.js | 0 hmvc/{payanyway => payments/paypal}/router.js | 0 .../{ => payments}/paypal/templates/form.jade | 0 .../paypal}/templates/wait.jade | 0 hmvc/payments/webmoney/controller/callback.js | 2 +- hmvc/payments/webmoney/controller/fail.js | 4 -- hmvc/payments/webmoney/templates/wait.jade | 25 ---------- .../yandexmoney/controller/back.js | 18 +++----- hmvc/payments/yandexmoney/index.js | 9 ++++ hmvc/payments/yandexmoney/renderForm.js | 18 ++++++++ hmvc/{ => payments}/yandexmoney/router.js | 1 - .../yandexmoney/templates/form.jade | 0 hmvc/paypal/controller/wait.js | 37 --------------- hmvc/paypal/templates/wait.jade | 25 ---------- hmvc/yandexmoney/index.js | 29 ------------ hmvc/yandexmoney/templates/wait.jade | 25 ---------- mocha.sh => mocha | 3 +- modules/app.js | 6 +++ modules/config/secret.js | 9 ++-- modules/setup/render.js | 1 + package.json | 1 + 43 files changed, 155 insertions(+), 309 deletions(-) rename gulp.sh => gulp (100%) mode change 100755 => 100644 delete mode 100644 hmvc/payanyway/controller/fail.js delete mode 100644 hmvc/payanyway/controller/inprogress.js delete mode 100644 hmvc/payanyway/controller/success.js delete mode 100644 hmvc/payanyway/index.js rename hmvc/{payanyway/controller/result.js => payments/payanyway/controller/callback.js} (75%) rename hmvc/{ => payments}/payanyway/controller/cancel.js (58%) create mode 100644 hmvc/payments/payanyway/controller/fail.js create mode 100644 hmvc/payments/payanyway/controller/inprogress.js create mode 100644 hmvc/payments/payanyway/controller/success.js create mode 100644 hmvc/payments/payanyway/index.js create mode 100644 hmvc/payments/payanyway/renderForm.js rename hmvc/{paypal => payments/payanyway}/router.js (64%) rename hmvc/{ => payments}/payanyway/templates/form.jade (100%) rename hmvc/{ => payments}/paypal/controller/cancel.js (100%) rename hmvc/{ => payments}/paypal/controller/result.js (98%) rename hmvc/{ => payments}/paypal/controller/success.js (100%) rename hmvc/{payanyway => payments/paypal}/controller/wait.js (100%) rename hmvc/{ => payments}/paypal/index.js (100%) rename hmvc/{payanyway => payments/paypal}/router.js (100%) rename hmvc/{ => payments}/paypal/templates/form.jade (100%) rename hmvc/{payanyway => payments/paypal}/templates/wait.jade (100%) delete mode 100644 hmvc/payments/webmoney/templates/wait.jade rename hmvc/{ => payments}/yandexmoney/controller/back.js (91%) create mode 100644 hmvc/payments/yandexmoney/index.js create mode 100644 hmvc/payments/yandexmoney/renderForm.js rename hmvc/{ => payments}/yandexmoney/router.js (82%) rename hmvc/{ => payments}/yandexmoney/templates/form.jade (100%) delete mode 100644 hmvc/paypal/controller/wait.js delete mode 100644 hmvc/paypal/templates/wait.jade delete mode 100644 hmvc/yandexmoney/index.js delete mode 100644 hmvc/yandexmoney/templates/wait.jade rename mocha.sh => mocha (79%) mode change 100755 => 100644 diff --git a/gulp.sh b/gulp old mode 100755 new mode 100644 similarity index 100% rename from gulp.sh rename to gulp diff --git a/gulpfile.js b/gulpfile.js index 20812c54e..42a322081 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,24 +1,25 @@ +/** + * NB: All tasks are initialized lazily, even plugins are required lazily, + * running 1 task does not require all tasks' files +*/ + const gulp = require('gulp'); -const fs = require('fs'); -const fse = require('fs-extra'); -const spawn = require('child_process').spawn; const gp = require('gulp-load-plugins')(); -const debug = require('gulp-debug'); const path = require('path'); -const source = require('vinyl-source-stream'); -const linkModules = require('./tasks/linkModules'); -const watchify = require('watchify'); //const browserifyTask = require('tasks/browserify'); - const serverSources = [ - 'config/**/*.js', 'controllers/**/*.js', 'lib/**/*.js', 'renderer/**/*.js', 'routes/**/*.js', + 'config/**/*.js', 'hmvc/**/*.js', 'modules/**/*.js', 'renderer/**/*.js', 'routes/**/*.js', 'setup/**/*.js', 'tasks/**/*.js', '*.js' ]; -gulp.task('lint', gp.jshintCache({ src: serverSources })); +gulp.task('lint', function() { + return gp.jshintCache({ src: serverSources }).apply(this, arguments); +}); -gulp.task('lint-or-die', gp.jshintCache({ src: serverSources, dieOnError: true })); +gulp.task('lint-or-die', function() { + return gp.jshintCache({ src: serverSources, dieOnError: true }).apply(this, arguments); +}); gulp.task('lint-watch', ['lint'], function(neverCalled) { gulp.watch(serverSources, ['lint']); @@ -34,6 +35,7 @@ gulp.task('watch', ['stylus'], function(neverCalled) { dst: 'www/js' })(); */ + const fse = require('fs-extra'); fse.ensureDirSync('www/fonts'); gp.dirSync('app/fonts', 'www/fonts'); @@ -81,11 +83,19 @@ gulp.task('import', function(callback) { }); }); -gulp.task('link-modules', linkModules(['modules/*', 'hmvc/*'])); +gulp.task('link-modules', function() { + const linkModules = require('./tasks/linkModules'); -gulp.task('sprite', gp.stylusSprite({ - spritesSearchFsRoot: 'app', - spritesWebRoot: '/img', - spritesFsDir: 'www/img', - styleFsDir: 'app/stylesheets/sprites' -})); + return linkModules(['modules/*', 'hmvc/*']).apply(this, arguments); +}); + +gulp.task('sprite', function() { + var options = { + spritesSearchFsRoot: 'app', + spritesWebRoot: '/img', + spritesFsDir: 'www/img', + styleFsDir: 'app/stylesheets/sprites' + }; + + return gp.stylusSprite(options).apply(this, arguments); +}); diff --git a/hmvc/getpdf/controller/pay.js b/hmvc/getpdf/controller/pay.js index dd34e8962..b24885ba5 100644 --- a/hmvc/getpdf/controller/pay.js +++ b/hmvc/getpdf/controller/pay.js @@ -7,6 +7,7 @@ var methods = require('../paymentMethods').methods; log.debugOn(); exports.post = function*(next) { + yield* this.loadOrder(); var method = methods[this.request.body.paymentMethod]; diff --git a/hmvc/payanyway/controller/fail.js b/hmvc/payanyway/controller/fail.js deleted file mode 100644 index d1e987d06..000000000 --- a/hmvc/payanyway/controller/fail.js +++ /dev/null @@ -1,19 +0,0 @@ -const mongoose = require('mongoose'); -const payment = require('payment'); -const Order = payment.Order; -const Transaction = payment.Transaction; -const log = require('js-log')(); - - -exports.get = function* (next) { - - yield* this.loadTransaction('MNT_TRANSACTION_ID'); - - this.transaction.persist({ - status: Transaction.STATUS_FAIL - }); - - this.redirect(this.getOrderUrl()); -}; - - diff --git a/hmvc/payanyway/controller/inprogress.js b/hmvc/payanyway/controller/inprogress.js deleted file mode 100644 index 4f0d172be..000000000 --- a/hmvc/payanyway/controller/inprogress.js +++ /dev/null @@ -1,19 +0,0 @@ -const mongoose = require('mongoose'); -const payment = require('payment'); -const Order = payment.Order; -const Transaction = payment.Transaction; -const log = require('js-log')(); - - -exports.get = function* (next) { - - yield* this.loadTransaction('MNT_TRANSACTION_ID'); - - this.transaction.persist({ - status: Transaction.STATUS_PENDING - }); - - this.redirect(this.getOrderPendingUrl()); -}; - - diff --git a/hmvc/payanyway/controller/success.js b/hmvc/payanyway/controller/success.js deleted file mode 100644 index f14902359..000000000 --- a/hmvc/payanyway/controller/success.js +++ /dev/null @@ -1,32 +0,0 @@ -const config = require('config'); -const mongoose = require('mongoose'); -const payment = require('payment'); -const Transaction = payment.Transaction; -const log = require('js-log')(); -const md5 = require('MD5'); - -log.debugOn(); - - -exports.get = function* (next) { - yield* this.loadTransaction('MNT_TRANSACTION_ID'); - - var transaction = this.transaction; - var order = this.order; - - var successUrl = this.getOrderSuccessUrl(); - var failUrl = this.getOrderUrl(); - - log.debug("transaction status: " + transaction.status); - - if (transaction.status) { - this.redirect(transaction.status == Transaction.STATUS_SUCCESS ? successUrl : failUrl); - } else { - this.render(__dirname, 'wait', { - transactionNumber: transaction.number, - successUrl: successUrl, - failUrl: failUrl - }); - } - -}; diff --git a/hmvc/payanyway/index.js b/hmvc/payanyway/index.js deleted file mode 100644 index f86f39465..000000000 --- a/hmvc/payanyway/index.js +++ /dev/null @@ -1,31 +0,0 @@ -const jade = require('jade'); -const path = require('path'); -var config = require('config'); -var payment = require('payment'); - -var Transaction = payment.Transaction; - -var router = require('./router'); - -exports.middleware = router.middleware(); - -exports.createTransactionForm = function* (order) { - - var transaction = new Transaction({ - order: order._id, - amount: order.amount, - module: 'payanyway' - }); - - yield transaction.persist(); - - return jade.renderFile(path.join(__dirname, 'templates/form.jade'), { - amount: transaction.amount, - number: transaction.number, - id: config.payanyway.id - }); - -}; - - - diff --git a/hmvc/payments/index.js b/hmvc/payments/index.js index 375ff7c22..0543eec01 100644 --- a/hmvc/payments/index.js +++ b/hmvc/payments/index.js @@ -1,5 +1,6 @@ var mount = require('koa-mount'); var config = require('config'); +var compose = require('koa-compose'); // Interaction with payment systems only. @@ -23,13 +24,8 @@ for(var name in paymentModules) { } // delegate all HTTP calls to payment modules -exports.middleware = function*(next) { +exports.middleware = compose(paymentMounts); - for (var i = 0; i < paymentMounts.length; i++) { - yield* paymentMounts[i].call(this, next); - } - -}; exports.populateContextMiddleware = function*(next) { this.redirectToOrder = function(order) { diff --git a/hmvc/payments/models/transaction.js b/hmvc/payments/models/transaction.js index f6c64d2f7..36d097fcb 100644 --- a/hmvc/payments/models/transaction.js +++ b/hmvc/payments/models/transaction.js @@ -97,6 +97,9 @@ schema.methods.getStatusDescription = function() { // log anything related to the transaction schema.methods.log = function*(options) { + + console.log(options); + options.transaction = this._id; // for complex objects -> prior to logging make them simple (must be jsonable) diff --git a/hmvc/payanyway/controller/result.js b/hmvc/payments/payanyway/controller/callback.js similarity index 75% rename from hmvc/payanyway/controller/result.js rename to hmvc/payments/payanyway/controller/callback.js index b46605dbd..97561c3f0 100644 --- a/hmvc/payanyway/controller/result.js +++ b/hmvc/payments/payanyway/controller/callback.js @@ -1,8 +1,6 @@ -const payment = require('payment'); const config = require('config'); -const mongoose = require('mongoose'); -const Order = payment.Order; -const Transaction = payment.Transaction; +const Order = require('../../models/order'); +const Transaction = require('../../models/transaction'); const log = require('js-log')(); const md5 = require('MD5'); @@ -12,18 +10,18 @@ exports.post = function* (next) { yield* this.loadTransaction('MNT_TRANSACTION_ID', {skipOwnerCheck : true}); - if (!checkSign(this.request.body)) { + if (!checkSignature(this.request.body)) { log.debug("wrong signature"); this.throw(403, "wrong signature"); } yield this.transaction.log({ - event: 'result', + event: 'callback', data: {url: this.request.originalUrl, body: this.request.body} }); if (this.transaction.amount != parseFloat(this.request.body.MNT_AMOUNT) || - this.request.body.MNT_ID != config.payanyway.id) { + this.request.body.MNT_ID != config.payments.modules.payanyway.id) { this.throw(404, 'transaction with given params not found'); } @@ -38,11 +36,11 @@ exports.post = function* (next) { this.body = 'SUCCESS'; }; -function checkSign(body) { +function checkSignature(body) { var signature = md5(body.MNT_ID + body.MNT_TRANSACTION_ID + body.MNT_OPERATION_ID + body.MNT_AMOUNT + body.MNT_CURRENCY_CODE + body.MNT_SUBSCRIBER_ID + body.MNT_TEST_MODE + - config.payanyway.secret).toUpperCase(); + config.payments.modules.payanyway.secret).toUpperCase(); return signature == body.MNT_SIGNATURE; } diff --git a/hmvc/payanyway/controller/cancel.js b/hmvc/payments/payanyway/controller/cancel.js similarity index 58% rename from hmvc/payanyway/controller/cancel.js rename to hmvc/payments/payanyway/controller/cancel.js index 6ec3e3c94..9c366a4fa 100644 --- a/hmvc/payanyway/controller/cancel.js +++ b/hmvc/payments/payanyway/controller/cancel.js @@ -1,7 +1,4 @@ -const mongoose = require('mongoose'); -const payment = require('payment'); -const Order = payment.Order; -const Transaction = payment.Transaction; +const Transaction = require('../../models/transaction'); const log = require('js-log')(); @@ -14,7 +11,7 @@ exports.get = function* (next) { statusMessage: 'отказ от оплаты' }); - this.redirect(this.getOrderUrl()); + this.redirectToOrder(); }; diff --git a/hmvc/payments/payanyway/controller/fail.js b/hmvc/payments/payanyway/controller/fail.js new file mode 100644 index 000000000..af3f6b83f --- /dev/null +++ b/hmvc/payments/payanyway/controller/fail.js @@ -0,0 +1,14 @@ +const Transaction = require('../../models/transaction'); + +exports.get = function* (next) { + + yield* this.loadTransaction('MNT_TRANSACTION_ID'); + + this.transaction.persist({ + status: Transaction.STATUS_FAIL + }); + + this.redirectToOrder(); +}; + + diff --git a/hmvc/payments/payanyway/controller/inprogress.js b/hmvc/payments/payanyway/controller/inprogress.js new file mode 100644 index 000000000..f5d43d90f --- /dev/null +++ b/hmvc/payments/payanyway/controller/inprogress.js @@ -0,0 +1,11 @@ +const Transaction = require('../../models/transaction'); + + +exports.get = function* (next) { + + yield* this.loadTransaction('MNT_TRANSACTION_ID'); + + this.redirectToOrder(); +}; + + diff --git a/hmvc/payments/payanyway/controller/success.js b/hmvc/payments/payanyway/controller/success.js new file mode 100644 index 000000000..b80caa6f7 --- /dev/null +++ b/hmvc/payments/payanyway/controller/success.js @@ -0,0 +1,7 @@ +const Transaction = require('../../models/transaction'); + +exports.get = function* (next) { + yield* this.loadTransaction('MNT_TRANSACTION_ID'); + + this.redirectToOrder(); +}; diff --git a/hmvc/payments/payanyway/index.js b/hmvc/payments/payanyway/index.js new file mode 100644 index 000000000..347b3a8e0 --- /dev/null +++ b/hmvc/payments/payanyway/index.js @@ -0,0 +1,11 @@ +const jade = require('jade'); +const path = require('path'); +var config = require('config'); +var payment = require('payment'); + +var router = require('./router'); + +exports.middleware = router.middleware(); + +exports.renderForm = require('./renderForm'); + diff --git a/hmvc/payments/payanyway/renderForm.js b/hmvc/payments/payanyway/renderForm.js new file mode 100644 index 000000000..692626f38 --- /dev/null +++ b/hmvc/payments/payanyway/renderForm.js @@ -0,0 +1,15 @@ +const jade = require('jade'); +const config = require('config'); +const path = require('path'); + +module.exports = function (transaction) { + + return jade.renderFile(path.join(__dirname, 'templates/form.jade'), { + amount: transaction.amount, + number: transaction.number, + id: config.payments.modules.payanyway.id + }); + +}; + + diff --git a/hmvc/paypal/router.js b/hmvc/payments/payanyway/router.js similarity index 64% rename from hmvc/paypal/router.js rename to hmvc/payments/payanyway/router.js index ab80cc16c..4a29d8aa7 100644 --- a/hmvc/paypal/router.js +++ b/hmvc/payments/payanyway/router.js @@ -3,18 +3,17 @@ var payment = require('payment'); var router = module.exports = new Router(); -var result = require('./controller/result'); +var callback = require('./controller/callback'); + var success = require('./controller/success'); +var inprogress = require('./controller/inprogress'); var cancel = require('./controller/cancel'); -var wait = require('./controller/wait'); // webmoney server posts here (in background) -router.post('/result', result.post); +router.post('/callback', callback.post); // webmoney server redirects here if payment successful router.get('/success', success.get); -// but if transaction status is not yet received, we wait... -router.post('/wait', wait.post); router.get('/cancel', cancel.get); diff --git a/hmvc/payanyway/templates/form.jade b/hmvc/payments/payanyway/templates/form.jade similarity index 100% rename from hmvc/payanyway/templates/form.jade rename to hmvc/payments/payanyway/templates/form.jade diff --git a/hmvc/paypal/controller/cancel.js b/hmvc/payments/paypal/controller/cancel.js similarity index 100% rename from hmvc/paypal/controller/cancel.js rename to hmvc/payments/paypal/controller/cancel.js diff --git a/hmvc/paypal/controller/result.js b/hmvc/payments/paypal/controller/result.js similarity index 98% rename from hmvc/paypal/controller/result.js rename to hmvc/payments/paypal/controller/result.js index b46605dbd..cf53812d2 100644 --- a/hmvc/paypal/controller/result.js +++ b/hmvc/payments/paypal/controller/result.js @@ -18,7 +18,7 @@ exports.post = function* (next) { } yield this.transaction.log({ - event: 'result', + event: 'callback', data: {url: this.request.originalUrl, body: this.request.body} }); diff --git a/hmvc/paypal/controller/success.js b/hmvc/payments/paypal/controller/success.js similarity index 100% rename from hmvc/paypal/controller/success.js rename to hmvc/payments/paypal/controller/success.js diff --git a/hmvc/payanyway/controller/wait.js b/hmvc/payments/paypal/controller/wait.js similarity index 100% rename from hmvc/payanyway/controller/wait.js rename to hmvc/payments/paypal/controller/wait.js diff --git a/hmvc/paypal/index.js b/hmvc/payments/paypal/index.js similarity index 100% rename from hmvc/paypal/index.js rename to hmvc/payments/paypal/index.js diff --git a/hmvc/payanyway/router.js b/hmvc/payments/paypal/router.js similarity index 100% rename from hmvc/payanyway/router.js rename to hmvc/payments/paypal/router.js diff --git a/hmvc/paypal/templates/form.jade b/hmvc/payments/paypal/templates/form.jade similarity index 100% rename from hmvc/paypal/templates/form.jade rename to hmvc/payments/paypal/templates/form.jade diff --git a/hmvc/payanyway/templates/wait.jade b/hmvc/payments/paypal/templates/wait.jade similarity index 100% rename from hmvc/payanyway/templates/wait.jade rename to hmvc/payments/paypal/templates/wait.jade diff --git a/hmvc/payments/webmoney/controller/callback.js b/hmvc/payments/webmoney/controller/callback.js index 9819e22f0..81f7eb8bb 100644 --- a/hmvc/payments/webmoney/controller/callback.js +++ b/hmvc/payments/webmoney/controller/callback.js @@ -40,7 +40,7 @@ exports.post = function* (next) { } yield this.transaction.log({ - event: 'result', + event: 'callback', data: {url: this.request.originalUrl, body: this.request.body} }); diff --git a/hmvc/payments/webmoney/controller/fail.js b/hmvc/payments/webmoney/controller/fail.js index 8cfa96a06..0a50f9bb6 100644 --- a/hmvc/payments/webmoney/controller/fail.js +++ b/hmvc/payments/webmoney/controller/fail.js @@ -2,10 +2,6 @@ const mongoose = require('mongoose'); const Transaction = require('../../models/transaction'); const log = require('js-log')(); -log.debugOn(); - - - exports.post = function* (next) { yield* this.loadTransaction('LMI_PAYMENT_NO'); diff --git a/hmvc/payments/webmoney/templates/wait.jade b/hmvc/payments/webmoney/templates/wait.jade deleted file mode 100644 index 28e2830d9..000000000 --- a/hmvc/payments/webmoney/templates/wait.jade +++ /dev/null @@ -1,25 +0,0 @@ -div - p Минуточку, ожидаем информацию от сервера.. - p Если эта страница долго не отвечает - - | - a(href='#' onclick='window.location.reload(true)') перезагрузите её - | . - -script var transactionNumber = #{transactionNumber}, successUrl = '#{successUrl}', failUrl = '#{failUrl}'; - -script. - var xhr = new XMLHttpRequest(); - xhr.open('POST', '/webmoney/wait'); - xhr.timeout = 20000; - - - xhr.onreadystatechange = function() { - if (xhr.readyState != 4) return; - - location.href = (xhr.responseText == 'success') ? successUrl : failUrl; - }; - - xhr.ontimeout = xhr.onabort = function() { - location.href = failUrl; - }; - xhr.send('transactionNumber=' + transactionNumber); diff --git a/hmvc/yandexmoney/controller/back.js b/hmvc/payments/yandexmoney/controller/back.js similarity index 91% rename from hmvc/yandexmoney/controller/back.js rename to hmvc/payments/yandexmoney/controller/back.js index b57caa339..9008d9876 100644 --- a/hmvc/yandexmoney/controller/back.js +++ b/hmvc/payments/yandexmoney/controller/back.js @@ -1,7 +1,6 @@ const config = require('config'); -const payment = require('payment'); -const Order = payment.Order; -const Transaction = payment.Transaction; +const Order = require('../../models/order'); +const Transaction = require('../../models/transaction'); const log = require('js-log')(); const request = require('koa-request'); @@ -103,10 +102,7 @@ exports.get = function* (next) { yield self.transaction.persist(); - yield self.transaction.log({ event: 'fail', data: reason }); - - - self.redirect(self.getOrderUrl()); + self.redirectToOrder(); } @@ -117,10 +113,10 @@ exports.get = function* (next) { method: 'POST', form: { code: code, - client_id: config.yandexmoney.clientId, + client_id: config.payments.modules.yandexmoney.clientId, grant_type: 'authorization_code', - redirect_uri: config.yandexmoney.redirectUri + '?transactionNumber=' + self.transaction.number, - client_secret: config.yandexmoney.clientSecret + redirect_uri: config.payments.modules.yandexmoney.redirectUri + '?transactionNumber=' + self.transaction.number, + client_secret: config.payments.modules.yandexmoney.clientSecret }, url: 'https://sp-money.yandex.ru/oauth/token' }; @@ -142,7 +138,7 @@ exports.get = function* (next) { method: 'POST', form: { pattern_id: 'p2p', - to: config.yandexmoney.purse, + to: config.payments.modules.yandexmoney.purse, amount: self.transaction.amount, comment: 'оплата по счету ' + self.transaction.number, message: 'оплата по счету ' + self.transaction.number, diff --git a/hmvc/payments/yandexmoney/index.js b/hmvc/payments/yandexmoney/index.js new file mode 100644 index 000000000..741de07fe --- /dev/null +++ b/hmvc/payments/yandexmoney/index.js @@ -0,0 +1,9 @@ +const config = require('config'); +const jade = require('jade'); +const path = require('path'); + +var router = require('./router'); + +exports.middleware = router.middleware(); + +exports.renderForm = require('./renderForm'); diff --git a/hmvc/payments/yandexmoney/renderForm.js b/hmvc/payments/yandexmoney/renderForm.js new file mode 100644 index 000000000..562b8530a --- /dev/null +++ b/hmvc/payments/yandexmoney/renderForm.js @@ -0,0 +1,18 @@ +const jade = require('jade'); +const config = require('config'); +const path = require('path'); + +module.exports = function (transaction) { + + return jade.renderFile(path.join(__dirname, 'templates/form.jade'), { + clientId: config.payments.modules.yandexmoney.clientId, + redirectUri: config.payments.modules.yandexmoney.redirectUri, + purse: config.payments.modules.yandexmoney.purse, + transactionNumber: transaction.number, + amount: transaction.amount + }); + +}; + + + diff --git a/hmvc/yandexmoney/router.js b/hmvc/payments/yandexmoney/router.js similarity index 82% rename from hmvc/yandexmoney/router.js rename to hmvc/payments/yandexmoney/router.js index d1506426f..bb51a3220 100644 --- a/hmvc/yandexmoney/router.js +++ b/hmvc/payments/yandexmoney/router.js @@ -1,5 +1,4 @@ var Router = require('koa-router'); -var payment = require('payment'); var router = module.exports = new Router(); diff --git a/hmvc/yandexmoney/templates/form.jade b/hmvc/payments/yandexmoney/templates/form.jade similarity index 100% rename from hmvc/yandexmoney/templates/form.jade rename to hmvc/payments/yandexmoney/templates/form.jade diff --git a/hmvc/paypal/controller/wait.js b/hmvc/paypal/controller/wait.js deleted file mode 100644 index ca00b6ebb..000000000 --- a/hmvc/paypal/controller/wait.js +++ /dev/null @@ -1,37 +0,0 @@ -const payment = require('payment'); -const config = require('config'); -const mongoose = require('mongoose'); -const Transaction = payment.Transaction; -const log = require('js-log')(); -const md5 = require('MD5'); - -log.debugOn(); - -exports.post = function* (next) { - - yield* this.loadTransaction(); - - var attempt = 0; - while (!this.transaction.status) { - attempt++; - if (attempt == 10) { - log.debug("timeout"); - this.body = 'TIMEOUT'; - return; - } - - yield delay(1000); - - this.transaction = yield Transaction.findOne({number: this.transaction.number }).exec(); - } - - log.debug('received'); - - this.body = this.transaction.status; -}; - -function delay(ms) { - return function(callback) { - setTimeout(callback, ms); - }; -} diff --git a/hmvc/paypal/templates/wait.jade b/hmvc/paypal/templates/wait.jade deleted file mode 100644 index 28e2830d9..000000000 --- a/hmvc/paypal/templates/wait.jade +++ /dev/null @@ -1,25 +0,0 @@ -div - p Минуточку, ожидаем информацию от сервера.. - p Если эта страница долго не отвечает - - | - a(href='#' onclick='window.location.reload(true)') перезагрузите её - | . - -script var transactionNumber = #{transactionNumber}, successUrl = '#{successUrl}', failUrl = '#{failUrl}'; - -script. - var xhr = new XMLHttpRequest(); - xhr.open('POST', '/webmoney/wait'); - xhr.timeout = 20000; - - - xhr.onreadystatechange = function() { - if (xhr.readyState != 4) return; - - location.href = (xhr.responseText == 'success') ? successUrl : failUrl; - }; - - xhr.ontimeout = xhr.onabort = function() { - location.href = failUrl; - }; - xhr.send('transactionNumber=' + transactionNumber); diff --git a/hmvc/yandexmoney/index.js b/hmvc/yandexmoney/index.js deleted file mode 100644 index 610e7d9c1..000000000 --- a/hmvc/yandexmoney/index.js +++ /dev/null @@ -1,29 +0,0 @@ -const config = require('config'); -const jade = require('jade'); -const path = require('path'); -var payment = require('payment'); -var Transaction = payment.Transaction; - -var router = require('./router'); - -exports.middleware = router.middleware(); - -exports.createTransactionForm = function* (order) { - - var transaction = new Transaction({ - order: order._id, - amount: order.amount, - module: 'yandexmoney' - }); - - yield transaction.persist(); - - return jade.renderFile(path.join(__dirname, 'templates/form.jade'), { - clientId: config.yandexmoney.clientId, - redirectUri: config.yandexmoney.redirectUri, - purse: config.yandexmoney.purse, - transactionNumber: transaction.number, - amount: transaction.amount - }); - -}; diff --git a/hmvc/yandexmoney/templates/wait.jade b/hmvc/yandexmoney/templates/wait.jade deleted file mode 100644 index 28e2830d9..000000000 --- a/hmvc/yandexmoney/templates/wait.jade +++ /dev/null @@ -1,25 +0,0 @@ -div - p Минуточку, ожидаем информацию от сервера.. - p Если эта страница долго не отвечает - - | - a(href='#' onclick='window.location.reload(true)') перезагрузите её - | . - -script var transactionNumber = #{transactionNumber}, successUrl = '#{successUrl}', failUrl = '#{failUrl}'; - -script. - var xhr = new XMLHttpRequest(); - xhr.open('POST', '/webmoney/wait'); - xhr.timeout = 20000; - - - xhr.onreadystatechange = function() { - if (xhr.readyState != 4) return; - - location.href = (xhr.responseText == 'success') ? successUrl : failUrl; - }; - - xhr.ontimeout = xhr.onabort = function() { - location.href = failUrl; - }; - xhr.send('transactionNumber=' + transactionNumber); diff --git a/mocha.sh b/mocha old mode 100755 new mode 100644 similarity index 79% rename from mocha.sh rename to mocha index eab061324..7b01318e1 --- a/mocha.sh +++ b/mocha @@ -8,5 +8,6 @@ # tried also gulp-mocha and node --harmony `which gulp` test, # but it hangs after tests, not sure why, mocha.sh works fine so leave it as is -NODE_ENV=test NODE_PATH=. mocha --harmony $* +NODE_ENV=development ./gulp link-modules +NODE_ENV=test mocha --harmony $* diff --git a/modules/app.js b/modules/app.js index 2f6c4af07..dbd32cfaa 100644 --- a/modules/app.js +++ b/modules/app.js @@ -5,6 +5,12 @@ const log = require('js-log')(); const config = require('config'); const app = koa(); +// trust all headers from proxy +// X-Forwarded-Host +// X-Forwarded-Proto +// X-Forwarded-For -> ip +app.proxy = true; + function requireSetup(path) { // if debug is on => will log the middleware travel chain if (process.env.NODE_ENV == 'development') { diff --git a/modules/config/secret.js b/modules/config/secret.js index 7e974f3ab..96614141e 100644 --- a/modules/config/secret.js +++ b/modules/config/secret.js @@ -8,15 +8,14 @@ exports.payments = { webmoney: { secretKey: 'hjvRVxstw42VDdpk9', purse: 'R146240663944' - } - /*, - + }, yandexmoney: { - redirectUri: 'http://stage.javascript.ru/yandexmoney/back', + // full redirectUri, with host, because form-creating function isn't middleware, doesn't know context + redirectUri: 'http://stage.javascript.ru/payments/yandexmoney/back', clientId: '6527BEA7C6189BF55A46FB379E642E2A20098792D24E98939540B0870F5B2228', clientSecret: '581E6A7542381F4F206789140E39A73CFB4C39A9C625127D9A731A5A95C2D1B8FE0E26E267E829C95A76F3313E3DD9F02CC223E890972E75952A70C93DCBEDBF', purse: '4100155697197' - }, + }/*, payanyway: { id: "31873866", diff --git a/modules/setup/render.js b/modules/setup/render.js index eab12e538..a5c5817e9 100644 --- a/modules/setup/render.js +++ b/modules/setup/render.js @@ -31,6 +31,7 @@ module.exports = function render(app) { }); // this.locals.debug causes jade to dump function + /* jshint -W087 */ this.locals.deb = function() { debugger; }; diff --git a/package.json b/package.json index e88cd0e6a..600fb921d 100755 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "gulp-autoprefixer": "0.0.8", "gulp-jshint": "*", "gulp-livereload": "^2.1.0", + "gulp-mocha": "^0.5.1", "gulp-stylus-sprite": "*", "gulp-tap": "^0.1.1", "javascript-brunch": "*", From 93adb008fcf5f19fcc2deefd4c105352557c9fe5 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 26 Jul 2014 01:06:33 +0400 Subject: [PATCH 105/130] hacking payments --- .gitignore | 4 +- hmvc/payments/index.js | 6 +- hmvc/payments/models/transaction.js | 16 +++- hmvc/payments/models/transactionLog.js | 8 +- .../payments/payanyway/controller/callback.js | 12 +-- hmvc/payments/payanyway/index.js | 4 - hmvc/payments/payanyway/renderForm.js | 3 +- hmvc/payments/payanyway/router.js | 4 +- hmvc/payments/payanyway/templates/form.jade | 2 +- hmvc/payments/paypal/controller/callback.js | 84 +++++++++++++++++++ hmvc/payments/paypal/controller/cancel.js | 7 +- hmvc/payments/paypal/controller/result.js | 48 ----------- hmvc/payments/paypal/controller/success.js | 30 +------ hmvc/payments/paypal/controller/wait.js | 37 -------- hmvc/payments/paypal/index.js | 30 +------ hmvc/payments/paypal/renderForm.js | 46 ++++++++++ hmvc/payments/paypal/router.js | 10 +-- hmvc/payments/paypal/signCart.js | 76 +++++++++++++++++ hmvc/payments/paypal/templates/form.jade | 23 ++--- hmvc/payments/paypal/templates/wait.jade | 25 ------ hmvc/payments/paypal/test/.jshintrc | 23 +++++ hmvc/payments/paypal/test/unit/signCart.js | 17 ++++ hmvc/payments/webmoney/controller/callback.js | 17 ++-- hmvc/payments/webmoney/renderForm.js | 2 +- hmvc/payments/yandexmoney/controller/back.js | 18 ++-- hmvc/payments/yandexmoney/renderForm.js | 2 +- modules/config/secret.js | 30 ------- 27 files changed, 315 insertions(+), 269 deletions(-) create mode 100644 hmvc/payments/paypal/controller/callback.js delete mode 100644 hmvc/payments/paypal/controller/result.js delete mode 100644 hmvc/payments/paypal/controller/wait.js create mode 100644 hmvc/payments/paypal/renderForm.js create mode 100644 hmvc/payments/paypal/signCart.js delete mode 100644 hmvc/payments/paypal/templates/wait.jade create mode 100644 hmvc/payments/paypal/test/.jshintrc create mode 100644 hmvc/payments/paypal/test/unit/signCart.js delete mode 100644 modules/config/secret.js diff --git a/.gitignore b/.gitignore index 64cb1cb7d..bc2d203d1 100755 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,6 @@ app/stylesheets/sprites/* bower_components/ # Passwords and other secret stuff -config/secret.js +modules/config/secret.js +modules/config/certs/* + diff --git a/hmvc/payments/index.js b/hmvc/payments/index.js index 0543eec01..cfd3261f6 100644 --- a/hmvc/payments/index.js +++ b/hmvc/payments/index.js @@ -49,7 +49,11 @@ exports.createTransactionForm = function* (order, method) { yield transaction.persist(); - return paymentModules[method].renderForm(transaction); + var form = yield* paymentModules[method].renderForm(transaction); + + yield transaction.log('form', form); + + return form; }; diff --git a/hmvc/payments/models/transaction.js b/hmvc/payments/models/transaction.js index 36d097fcb..a150f2745 100644 --- a/hmvc/payments/models/transaction.js +++ b/hmvc/payments/models/transaction.js @@ -95,12 +95,22 @@ schema.methods.getStatusDescription = function() { throw new Error("неподдерживаемый статус транзакции"); }; +schema.methods.logRequest = function*(event, request) { + yield this.log(event, {url: request.originalUrl, body: request.body}); +}; + // log anything related to the transaction -schema.methods.log = function*(options) { +schema.methods.log = function*(event, data) { - console.log(options); + if (typeof event != "string") { + throw new Error("event name must be a string"); + } - options.transaction = this._id; + var options = { + transaction: this._id, + event: event, + data: data + }; // for complex objects -> prior to logging make them simple (must be jsonable) // e.g for HTTP response (HTTP.IncomingMessage) diff --git a/hmvc/payments/models/transactionLog.js b/hmvc/payments/models/transactionLog.js index 39dbfe8ec..0ef24a5a8 100644 --- a/hmvc/payments/models/transactionLog.js +++ b/hmvc/payments/models/transactionLog.js @@ -5,9 +5,13 @@ var schema = new Schema({ transaction: { type: Schema.Types.ObjectId, - ref: 'Transaction' + ref: 'Transaction', + index: true + }, + event: { + type: String, + index: true }, - event: String, data: Schema.Types.Mixed, created: { diff --git a/hmvc/payments/payanyway/controller/callback.js b/hmvc/payments/payanyway/controller/callback.js index 97561c3f0..10eb0bc65 100644 --- a/hmvc/payments/payanyway/controller/callback.js +++ b/hmvc/payments/payanyway/controller/callback.js @@ -15,14 +15,16 @@ exports.post = function* (next) { this.throw(403, "wrong signature"); } - yield this.transaction.log({ - event: 'callback', - data: {url: this.request.originalUrl, body: this.request.body} - }); + yield this.transaction.logRequest('callback', this.request); + // signature is valid, so everything MUST be fine if (this.transaction.amount != parseFloat(this.request.body.MNT_AMOUNT) || this.request.body.MNT_ID != config.payments.modules.payanyway.id) { - this.throw(404, 'transaction with given params not found'); + yield this.transaction.persist({ + status: Transaction.STATUS_FAIL, + statusMessage: "данные транзакции не совпадают с базой, свяжитесь с поддержкой" + }); + this.throw(404, "transaction data doesn't match the POST body"); } yield this.transaction.persist({ diff --git a/hmvc/payments/payanyway/index.js b/hmvc/payments/payanyway/index.js index 347b3a8e0..6a6df2fb3 100644 --- a/hmvc/payments/payanyway/index.js +++ b/hmvc/payments/payanyway/index.js @@ -1,7 +1,3 @@ -const jade = require('jade'); -const path = require('path'); -var config = require('config'); -var payment = require('payment'); var router = require('./router'); diff --git a/hmvc/payments/payanyway/renderForm.js b/hmvc/payments/payanyway/renderForm.js index 692626f38..0f1ffa6d9 100644 --- a/hmvc/payments/payanyway/renderForm.js +++ b/hmvc/payments/payanyway/renderForm.js @@ -2,11 +2,12 @@ const jade = require('jade'); const config = require('config'); const path = require('path'); -module.exports = function (transaction) { +module.exports = function* (transaction) { return jade.renderFile(path.join(__dirname, 'templates/form.jade'), { amount: transaction.amount, number: transaction.number, + currency: config.payments.currency, id: config.payments.modules.payanyway.id }); diff --git a/hmvc/payments/payanyway/router.js b/hmvc/payments/payanyway/router.js index 4a29d8aa7..aaaf0ed6b 100644 --- a/hmvc/payments/payanyway/router.js +++ b/hmvc/payments/payanyway/router.js @@ -1,5 +1,4 @@ var Router = require('koa-router'); -var payment = require('payment'); var router = module.exports = new Router(); @@ -9,11 +8,10 @@ var success = require('./controller/success'); var inprogress = require('./controller/inprogress'); var cancel = require('./controller/cancel'); -// webmoney server posts here (in background) router.post('/callback', callback.post); -// webmoney server redirects here if payment successful router.get('/success', success.get); +router.get('/inprogress', inprogress.get); router.get('/cancel', cancel.get); diff --git a/hmvc/payments/payanyway/templates/form.jade b/hmvc/payments/payanyway/templates/form.jade index 7f63860c4..8bcd1df10 100644 --- a/hmvc/payments/payanyway/templates/form.jade +++ b/hmvc/payments/payanyway/templates/form.jade @@ -2,7 +2,7 @@ form(method="POST" action="https://www.moneta.ru/assistant.htm" accept-charset="UTF-8") input(type="hidden",name="MNT_ID",value=id) input(type="hidden",name="MNT_TRANSACTION_ID",value=number) - input(type="hidden",name="MNT_CURRENCY_CODE",value="RUB") + input(type="hidden",name="MNT_CURRENCY_CODE",value=currency) input(type="hidden",name="MNT_AMOUNT",value=amount) input(type="hidden",name="paymentSystem.limitIds",value="843858,248362,822360,545234,1028,499669") input(type="submit",value="Оплатить") diff --git a/hmvc/payments/paypal/controller/callback.js b/hmvc/payments/paypal/controller/callback.js new file mode 100644 index 000000000..c59fc1428 --- /dev/null +++ b/hmvc/payments/paypal/controller/callback.js @@ -0,0 +1,84 @@ +const config = require('config') +const paypalConfig = config.payments.modules.paypal; +const Order = require('../../models/order'); +const Transaction = require('../../models/transaction'); +const TransactionLog = require('../../models/transactionLog'); +const log = require('js-log')(); +const request = require('koa-request'); + +// docs: +// +// https://developer.paypal.com/webapps/developer/docs/classic/ipn/integration-guide/IPNIntro/ + +log.debugOn(); + +/* jshint -W106 */ +exports.post = function* (next) { + + yield* this.loadTransaction('invoice', {skipOwnerCheck: true}); + + yield this.transaction.logRequest('ipn-unverified', this.request); + + var qs = { + 'cmd': '_notify-validate' + }; + + for (var field in this.request.body) { + qs[field] = this.request.body[field]; + } + + // request oauth token + var options = { + method: 'GET', + qs: qs, + url: 'https://www.paypal.com/cgi-bin/webscr' + }; + + + yield this.transaction.log('request ipn verify', options); + + var response; + try { + response = yield request(options); + } catch(e) { + yield this.transaction.log('request ipn verify failed', e.message); + this.throw(403, "Couldn't verify ipn"); + } + + if (response.body != "VERIFIED") { + this.throw(403, "Invalid IPN"); + } + + // ipn is verified now! But we check if it's data matches the transaction (as recommended in docs) + if (this.transaction.amount != parseFloat(this.request.body.mc_gross) || + this.request.body.receiver_email != paypalConfig.email || + this.request.body.mc_currency != config.payments.currency) { + + yield this.transaction.persist({ + status: Transaction.STATUS_FAIL, + statusMessage: "данные транзакции не совпадают с базой, свяжитесь с поддержкой" + }); + this.throw(404, "transaction data doesn't match the POST body"); + } + + // match agains latest ipn in logs as recommended: + // if there was an IPN about the same transaction, and it's state is the same + // => then the current one is a duplicate + + var previousIpn = yield TransactionLog.findOne({ + event: "ipn", + transaction: this.transaction._id + }).sort({created: -1}).exec(); + + if (previousIpn && previousIpn.data.payment_status == this.request.body.payment_status) { + yield this.transaction.logRequest("ipn duplicate", this.request); + // ignore duplicate + this.body = ''; + return; + } + + // now we have a valid non-duplicate IPN, let's update the transaction + + + this.body = ''; +}; diff --git a/hmvc/payments/paypal/controller/cancel.js b/hmvc/payments/paypal/controller/cancel.js index cea2ca84d..fcef4fd87 100644 --- a/hmvc/payments/paypal/controller/cancel.js +++ b/hmvc/payments/paypal/controller/cancel.js @@ -1,7 +1,4 @@ -const mongoose = require('mongoose'); -const payment = require('payment'); -const Order = payment.Order; -const Transaction = payment.Transaction; +const Transaction = require('../../models/transaction'); const log = require('js-log')(); @@ -14,6 +11,6 @@ exports.get = function* (next) { statusMessage: 'отказ от оплаты' }); - this.redirect(this.getOrderUrl()); + this.redirectToOrder(); }; diff --git a/hmvc/payments/paypal/controller/result.js b/hmvc/payments/paypal/controller/result.js deleted file mode 100644 index cf53812d2..000000000 --- a/hmvc/payments/paypal/controller/result.js +++ /dev/null @@ -1,48 +0,0 @@ -const payment = require('payment'); -const config = require('config'); -const mongoose = require('mongoose'); -const Order = payment.Order; -const Transaction = payment.Transaction; -const log = require('js-log')(); -const md5 = require('MD5'); - -log.debugOn(); - -exports.post = function* (next) { - - yield* this.loadTransaction('MNT_TRANSACTION_ID', {skipOwnerCheck : true}); - - if (!checkSign(this.request.body)) { - log.debug("wrong signature"); - this.throw(403, "wrong signature"); - } - - yield this.transaction.log({ - event: 'callback', - data: {url: this.request.originalUrl, body: this.request.body} - }); - - if (this.transaction.amount != parseFloat(this.request.body.MNT_AMOUNT) || - this.request.body.MNT_ID != config.payanyway.id) { - this.throw(404, 'transaction with given params not found'); - } - - yield this.transaction.persist({ - status: Transaction.STATUS_SUCCESS - }); - - var order = this.order; - log.debug("will call order onSuccess module=" + order.module); - yield* require(order.module).onSuccess(order); - - this.body = 'SUCCESS'; -}; - -function checkSign(body) { - - var signature = md5(body.MNT_ID + body.MNT_TRANSACTION_ID + body.MNT_OPERATION_ID + - body.MNT_AMOUNT + body.MNT_CURRENCY_CODE + body.MNT_SUBSCRIBER_ID + body.MNT_TEST_MODE + - config.payanyway.secret).toUpperCase(); - - return signature == body.MNT_SIGNATURE; -} diff --git a/hmvc/payments/paypal/controller/success.js b/hmvc/payments/paypal/controller/success.js index 9c08f2e56..fecd136ef 100644 --- a/hmvc/payments/paypal/controller/success.js +++ b/hmvc/payments/paypal/controller/success.js @@ -1,32 +1,6 @@ -const config = require('config'); -const mongoose = require('mongoose'); -const payment = require('payment'); -const Transaction = payment.Transaction; -const log = require('js-log')(); -const md5 = require('MD5'); -log.debugOn(); - - -exports.get = function* (next) { +exports.post = function* (next) { yield* this.loadTransaction(); - var transaction = this.transaction; - var order = this.order; - - var successUrl = this.getOrderSuccessUrl(); - var failUrl = this.getOrderUrl(); - - log.debug("transaction status: " + transaction.status); - - if (transaction.status) { - this.redirect(transaction.status == Transaction.STATUS_SUCCESS ? successUrl : failUrl); - } else { - this.render(__dirname, 'wait', { - transactionNumber: transaction.number, - successUrl: successUrl, - failUrl: failUrl - }); - } - + this.redirectToOrder(); }; diff --git a/hmvc/payments/paypal/controller/wait.js b/hmvc/payments/paypal/controller/wait.js deleted file mode 100644 index ca00b6ebb..000000000 --- a/hmvc/payments/paypal/controller/wait.js +++ /dev/null @@ -1,37 +0,0 @@ -const payment = require('payment'); -const config = require('config'); -const mongoose = require('mongoose'); -const Transaction = payment.Transaction; -const log = require('js-log')(); -const md5 = require('MD5'); - -log.debugOn(); - -exports.post = function* (next) { - - yield* this.loadTransaction(); - - var attempt = 0; - while (!this.transaction.status) { - attempt++; - if (attempt == 10) { - log.debug("timeout"); - this.body = 'TIMEOUT'; - return; - } - - yield delay(1000); - - this.transaction = yield Transaction.findOne({number: this.transaction.number }).exec(); - } - - log.debug('received'); - - this.body = this.transaction.status; -}; - -function delay(ms) { - return function(callback) { - setTimeout(callback, ms); - }; -} diff --git a/hmvc/payments/paypal/index.js b/hmvc/payments/paypal/index.js index 3b447451f..fb208bb72 100644 --- a/hmvc/payments/paypal/index.js +++ b/hmvc/payments/paypal/index.js @@ -1,35 +1,7 @@ -const jade = require('jade'); -const path = require('path'); -var config = require('config'); -var payment = require('payment'); - -var Transaction = payment.Transaction; - var router = require('./router'); exports.middleware = router.middleware(); -exports.createTransactionForm = function* (order) { - - var transaction = new Transaction({ - order: order._id, - amount: order.amount, - module: 'paypal' - }); - - yield transaction.persist(); - - return jade.renderFile(path.join(__dirname, 'templates/form.jade'), { - amount: transaction.amount, - number: transaction.number, - email: config.paypal.email, - resultUrl: 'http://' + config.domain + '/paypal/result?transactionNumber=' + transaction.number, - cancelUrl: 'http://' + config.domain + '/paypal/cancel?transactionNumber=' + transaction.number, - successUrl: 'http://' + config.domain + '/paypal/success?transactionNumber=' + transaction.number - - }); - -}; - +exports.renderForm = require('./renderForm'); diff --git a/hmvc/payments/paypal/renderForm.js b/hmvc/payments/paypal/renderForm.js new file mode 100644 index 000000000..692fb5395 --- /dev/null +++ b/hmvc/payments/paypal/renderForm.js @@ -0,0 +1,46 @@ +const jade = require('jade'); +const config = require('config'); +const paypalConfig = config.payments.modules.paypal; +const path = require('path'); +const signCart = require('./signCart')(paypalConfig.myCertPath, paypalConfig.myKeyPath, paypalConfig.paypalCertPath); + +module.exports = function* (transaction) { + + /* jshint -W106 */ + var cart = { + cert_id: paypalConfig.certId, + business: paypalConfig.email, + invoice: transaction.number, + amount: transaction.amount, + item_name: "Оплата по счёту " + transaction.number, + cmd: "_xclick", // Buy now button (buying a single item, works with Encrypted Website Payments, not sure if _cart works too) + charset: "utf-8", + no_note: 1, + no_shipping: 1, + rm: 2, + currency_code: config.payments.currency, + lc: "RU" + }; + + var cartFormatted = []; + for(var key in cart) { + cartFormatted.push(key + '=' + cart[key]); + } + cartFormatted = cartFormatted.join("\n"); + + yield transaction.log("cartFormatted", cartFormatted); + + var cartEncrypted = yield signCart(cartFormatted); + + var form = jade.renderFile(path.join(__dirname, 'templates/form.jade'), { + encrypted: cartEncrypted, + notifyUrl: paypalConfig.callbackUrl + '?transactionNumber=' + transaction.number, + cancelUrl: paypalConfig.cancelUrl + '?transactionNumber=' + transaction.number, + returnUrl: paypalConfig.successUrl + '?transactionNumber=' + transaction.number + }); + + return form; + +}; + + diff --git a/hmvc/payments/paypal/router.js b/hmvc/payments/paypal/router.js index ab80cc16c..ec7df27a6 100644 --- a/hmvc/payments/paypal/router.js +++ b/hmvc/payments/paypal/router.js @@ -1,20 +1,16 @@ var Router = require('koa-router'); -var payment = require('payment'); var router = module.exports = new Router(); -var result = require('./controller/result'); +var callback = require('./controller/callback'); var success = require('./controller/success'); var cancel = require('./controller/cancel'); -var wait = require('./controller/wait'); // webmoney server posts here (in background) -router.post('/result', result.post); +router.post('/callback', callback.post); // webmoney server redirects here if payment successful -router.get('/success', success.get); -// but if transaction status is not yet received, we wait... -router.post('/wait', wait.post); +router.post('/success', success.post); router.get('/cancel', cancel.get); diff --git a/hmvc/payments/paypal/signCart.js b/hmvc/payments/paypal/signCart.js new file mode 100644 index 000000000..a5b8e0542 --- /dev/null +++ b/hmvc/payments/paypal/signCart.js @@ -0,0 +1,76 @@ +/* + # Signing the cart for paypal WPS + + ## Why OpenSSL Cli ? + Node crypto can't sign pkcs7 + node-forge (not sure) couldn't find the way + so I'm using openssl CLI + + ## OpenSSL params origins + Thanks to from http://www.stellarwebsolutions.com/en/articles/paypal_button_encryption_php.php + + ## Preparation + First, make certs: + openssl genrsa -out app_key.pem 1024 + openssl req -new -key app_key.pem -x509 -days 365 -out app_cert.pem + + (for java optional) openssl pkcs12 -export -inkey app_key.pem -in app_cert.pem -out app_cert.p12 + (for java optional) p12 is a format which groups key and cert and signs, so password is required + + ## Alternative way to sign: + EWP Soft: + https://www.paypal.com/us/cgi-bin/webscr?cmd=p/xcl/rec/ewp-code + to compile java, get http://www.bouncycastle.org/archive/124/crypto-124.tar.gz + put to parent dir and run the compiler like this (modified build_app.sh): + === + export CRYPTO_HOME="/js/javascript-nodejs/tmp/crypto" + + CLASSPATH="." + CLASSPATH="$CLASSPATH:$CRYPTO_HOME/jars/bcmail-jdk15-124.jar" + CLASSPATH="$CLASSPATH:$CRYPTO_HOME/jars/bcpg-jdk15-124.jar" + CLASSPATH="$CLASSPATH:$CRYPTO_HOME/jars/bcprov-jdk15-124.jar" + CLASSPATH="$CLASSPATH:$CRYPTO_HOME/jars/bctest-jdk15-124.jar" + CLASSPATH="$CLASSPATH:$CRYPTO_HOME/jars/jce-jdk13-124.jar" + export CLASSPATH + + javac -g -classpath "$CLASSPATH" ButtonEncryption.java com/paypal/crypto/sample/*.java + + Then: + (where Kah1voo8 is p12 password, 7BXVJJ3YFS3HQ is cert id from paypal (upload app_cert.pem to paypal to see it) + java ButtonEncryption app_cert.pem app_cert.p12 paypal_cert.pem Kah1voo8 "cert_id=7BXVJJ3YFS3HQ,business=iliakan@gmail.com,notify_url=http://stage.javascript.ru/payments/paypal/callback?transactionNumber=123,cancel_return=http://stage.javascript.ru/payments/paypal/cancel?transactionNumber=123,notify_url=http://stage.javascript.ru/payments/paypal/callback?transactionNumber=123,return=http://stage.javascript.ru/payments/paypal/success?transactionNumber=123,invoice=123,amount=2,item_name=Оплата по счету 123,cmd=_xclick,charset=utf-8,no_note=1,no_shipping=1,rm=2,currency_code=RUB,lc=RU,secret=blabla" out.html + ===== +*/ + +/* jshint -W021 */ + +var exec = require('child_process').exec; +var fs = require('fs'); +var assert = require('assert'); +var thunkify = require('thunkify'); + +function signCart(myCertPath, myKeyPath, paypalCertPath, message, callback) { + + var cmd = 'openssl smime -sign -signer ' + myCertPath + ' -inkey ' + myKeyPath + ' -outform der -nodetach -binary | openssl smime -encrypt -des3 -binary -outform pem ' + paypalCertPath; + + var child = exec( + cmd, + function(err, stdout, stderr) { + if (err) return callback(err); + return callback(null, stdout); + } + ); + + child.stdin.end(message); +} + +module.exports = function(myCertPath, myKeyPath, paypalCertPath) { + assert(fs.existsSync(myCertPath)); + assert(fs.existsSync(myKeyPath)); + assert(fs.existsSync(paypalCertPath)); + + return thunkify(signCart.bind(null, myCertPath, myKeyPath, paypalCertPath)); +}; + + +//openssl smime -sign -signer $MY_CERT_FILE -inkey $MY_KEY_FILE -in content -outform der -nodetach -binary +// | openssl smime -encrypt -des3 -binary -outform pem $PAYPAL_CERT_FILE diff --git a/hmvc/payments/paypal/templates/form.jade b/hmvc/payments/paypal/templates/form.jade index 4bd1eeebc..a195ad254 100644 --- a/hmvc/payments/paypal/templates/form.jade +++ b/hmvc/payments/paypal/templates/form.jade @@ -1,19 +1,8 @@ -form(method="POST" action="https://www.paypal.com/cgi-bin/webscr" accept-charset="UTF-8") - input(type="hidden",name="business",value=email) - input(type="hidden",name="notify_url",value=resultUrl) +form(method="post" action="https://www.paypal.com/cgi-bin/webscr") + input(type="hidden",name="cmd",value="_s-xclick") + input(type="submit" value="submit") + input(type="hidden",name="encrypted",value=encrypted) + input(type="hidden",name="notify_url",value=notifyUrl) input(type="hidden",name="cancel_return",value=cancelUrl) - input(type="hidden",name="return",value=successUrl) - input(type="hidden",name="invoice",value=number) - input(type="hidden",name="amount_1",value=amount) - input(type="hidden",name="item_name_1",value=("Оплата по счету " + number)) - input(type="hidden",name="cmd",value="_cart") - input(type="hidden",name="upload",value="1") - input(type="hidden",name="charset",value="utf-8") - input(type="hidden",name="no_note",value="1") - input(type="hidden",name="no_shipping",value="1") - input(type="hidden",name="rm",value="2") - input(type="hidden",name="currency_code",value="RUB") - input(type="hidden",name="lc",value="RU") - input(type="submit",value="Оплатить") - + input(type="hidden",name="return",value=returnUrl) diff --git a/hmvc/payments/paypal/templates/wait.jade b/hmvc/payments/paypal/templates/wait.jade deleted file mode 100644 index 28e2830d9..000000000 --- a/hmvc/payments/paypal/templates/wait.jade +++ /dev/null @@ -1,25 +0,0 @@ -div - p Минуточку, ожидаем информацию от сервера.. - p Если эта страница долго не отвечает - - | - a(href='#' onclick='window.location.reload(true)') перезагрузите её - | . - -script var transactionNumber = #{transactionNumber}, successUrl = '#{successUrl}', failUrl = '#{failUrl}'; - -script. - var xhr = new XMLHttpRequest(); - xhr.open('POST', '/webmoney/wait'); - xhr.timeout = 20000; - - - xhr.onreadystatechange = function() { - if (xhr.readyState != 4) return; - - location.href = (xhr.responseText == 'success') ? successUrl : failUrl; - }; - - xhr.ontimeout = xhr.onabort = function() { - location.href = failUrl; - }; - xhr.send('transactionNumber=' + transactionNumber); diff --git a/hmvc/payments/paypal/test/.jshintrc b/hmvc/payments/paypal/test/.jshintrc new file mode 100644 index 000000000..077663629 --- /dev/null +++ b/hmvc/payments/paypal/test/.jshintrc @@ -0,0 +1,23 @@ +{ + "maxerr": 25, + "latedef": "nofunc", + "browser": false, + "node": true, + "indent": 2, + "camelcase": true, + "newcap": true, + "undef": true, + "esnext": true, + "multistr": true, + "noyield": true, + "expr": true, + "-W004": true, + "globals" : { + "describe" : false, + "it" : false, + "before" : false, + "beforeEach" : false, + "after" : false, + "afterEach" : false + } +} diff --git a/hmvc/payments/paypal/test/unit/signCart.js b/hmvc/payments/paypal/test/unit/signCart.js new file mode 100644 index 000000000..784f51f66 --- /dev/null +++ b/hmvc/payments/paypal/test/unit/signCart.js @@ -0,0 +1,17 @@ +var signCart = require('../../signCart'); +var paypalConfig = require('config').payments.modules.paypal; + +describe("signCart", function() { + + it("signs a message", function*() { + + var signed = yield signCart("cart content", + paypalConfig.myCertPath, paypalConfig.myKeyPath, paypalConfig.paypalCertPath); + + var header = '-----BEGIN PKCS7-----'; + + signed.slice(0, header.length).should.eql(header); + + }); + +}); diff --git a/hmvc/payments/webmoney/controller/callback.js b/hmvc/payments/webmoney/controller/callback.js index 81f7eb8bb..b01b822d7 100644 --- a/hmvc/payments/webmoney/controller/callback.js +++ b/hmvc/payments/webmoney/controller/callback.js @@ -13,10 +13,7 @@ exports.prerequest = function* (next) { log.debug("prerequest"); - yield this.transaction.log({ - event: 'prerequest', - data: {url: this.request.originalUrl, body: this.request.body} - }); + yield this.transaction.logRequest('prerequest', this.request); if (this.transaction.status == Transaction.STATUS_SUCCESS || this.transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) || @@ -39,14 +36,16 @@ exports.post = function* (next) { this.throw(403, "wrong signature"); } - yield this.transaction.log({ - event: 'callback', - data: {url: this.request.originalUrl, body: this.request.body} - }); + yield this.transaction.logRequest('callback', this.request); if (this.transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) || this.request.body.LMI_PAYEE_PURSE != config.webmoney.purse) { - this.throw(404, 'transaction with given params not found'); + // STRANGE, signature is correct + yield this.transaction.persist({ + status: Transaction.STATUS_FAIL, + statusMessage: "данные транзакции не совпадают с базой, свяжитесь с поддержкой" + }); + this.throw(404, "transaction data doesn't match the POST body"); } if (!this.request.body.LMI_SIM_MODE || this.request.body.LMI_SIM_MODE == '0') { diff --git a/hmvc/payments/webmoney/renderForm.js b/hmvc/payments/webmoney/renderForm.js index f32e4e22c..0a6f6d526 100644 --- a/hmvc/payments/webmoney/renderForm.js +++ b/hmvc/payments/webmoney/renderForm.js @@ -2,7 +2,7 @@ const jade = require('jade'); const config = require('config'); const path = require('path'); -module.exports = function (transaction) { +module.exports = function* (transaction) { return jade.renderFile(path.join(__dirname, 'templates/form.jade'), { amount: transaction.amount, diff --git a/hmvc/payments/yandexmoney/controller/back.js b/hmvc/payments/yandexmoney/controller/back.js index 9008d9876..794f8b121 100644 --- a/hmvc/payments/yandexmoney/controller/back.js +++ b/hmvc/payments/yandexmoney/controller/back.js @@ -13,11 +13,7 @@ exports.get = function* (next) { yield* this.loadTransaction(); - yield this.transaction.log({ - event: 'back', - data: {url: this.request.originalUrl, body: this.request.body} - }); - + yield this.transaction.logRequest('back', this.request); if (!this.query.code) { yield* fail(this.query.error_description || this.query.error); @@ -122,11 +118,11 @@ exports.get = function* (next) { }; - yield self.transaction.log({ event: 'request oauth/token', data: options }); + yield self.transaction.log('request oauth/token', options); var response = yield request(options); - yield self.transaction.log({ event: 'response oauth/token', data: response.body }); + yield self.transaction.log('response oauth/token', response.body); return JSON.parse(response.body); } @@ -150,10 +146,10 @@ exports.get = function* (next) { url: 'https://money.yandex.ru/api/request-payment' }; - yield self.transaction.log({ event: 'request api/request-payment', data: options }); + yield self.transaction.log('request api/request-payment', options); var response = yield request(options); - yield self.transaction.log({ event: 'response api/request-payment', data: response.body }); + yield self.transaction.log('response api/request-payment', response.body); return JSON.parse(response.body); } @@ -170,10 +166,10 @@ exports.get = function* (next) { url: 'https://money.yandex.ru/api/process-payment' }; - yield self.transaction.log({ event: 'request api/process-payment', data: options }); + yield self.transaction.log('request api/process-payment', options); var response = yield request(options); - yield self.transaction.log({ event: 'response api/process-payment', data: response.body }); + yield self.transaction.log('response api/process-payment', response.body); return JSON.parse(response.body); } diff --git a/hmvc/payments/yandexmoney/renderForm.js b/hmvc/payments/yandexmoney/renderForm.js index 562b8530a..868baa511 100644 --- a/hmvc/payments/yandexmoney/renderForm.js +++ b/hmvc/payments/yandexmoney/renderForm.js @@ -2,7 +2,7 @@ const jade = require('jade'); const config = require('config'); const path = require('path'); -module.exports = function (transaction) { +module.exports = function* (transaction) { return jade.renderFile(path.join(__dirname, 'templates/form.jade'), { clientId: config.payments.modules.yandexmoney.clientId, diff --git a/modules/config/secret.js b/modules/config/secret.js deleted file mode 100644 index 96614141e..000000000 --- a/modules/config/secret.js +++ /dev/null @@ -1,30 +0,0 @@ -// this file contains all passwords etc, -// should not be in repo - -exports.sessionKey = "KillerIsJim"; - -exports.payments = { - modules: { - webmoney: { - secretKey: 'hjvRVxstw42VDdpk9', - purse: 'R146240663944' - }, - yandexmoney: { - // full redirectUri, with host, because form-creating function isn't middleware, doesn't know context - redirectUri: 'http://stage.javascript.ru/payments/yandexmoney/back', - clientId: '6527BEA7C6189BF55A46FB379E642E2A20098792D24E98939540B0870F5B2228', - clientSecret: '581E6A7542381F4F206789140E39A73CFB4C39A9C625127D9A731A5A95C2D1B8FE0E26E267E829C95A76F3313E3DD9F02CC223E890972E75952A70C93DCBEDBF', - purse: '4100155697197' - }/*, - - payanyway: { - id: "31873866", - secret: "cERfervdf43lkjl3cCDweqr2SSDFVbro" - }, - - paypal: { - email: "iliakan@gmail.com" - }*/ - } -}; - From f8287309fc64ea06bce8b4b5aaac9e52dc24eca6 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 26 Jul 2014 01:20:22 +0400 Subject: [PATCH 106/130] paypal now works: EWP (encrypted cart) and IPN listener are functional --- hmvc/payments/paypal/controller/callback.js | 45 ++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/hmvc/payments/paypal/controller/callback.js b/hmvc/payments/paypal/controller/callback.js index c59fc1428..55296c474 100644 --- a/hmvc/payments/paypal/controller/callback.js +++ b/hmvc/payments/paypal/controller/callback.js @@ -79,6 +79,49 @@ exports.post = function* (next) { // now we have a valid non-duplicate IPN, let's update the transaction + // log it right now to evade conflicts with duplicates + yield this.transaction.log("ipn", this.request.body); + + // Do not perform any processing on WPS transactions here that do not have + // transaction IDs, indicating they are non-payment IPNs such as those used + // for subscription signup requests. + if (!this.request.body.txn_id) { + this.body = ''; + return; + } + + // Exit when we don't get a payment status we recognize + + switch(this.request.body.payment_status) { + case 'Failed': + case 'Voided': + yield this.transaction.persist({ + status: Transaction.STATUS_FAIL + }); + this.body = ''; + return; + case 'Pending': + yield this.transaction.persist({ + status: Transaction.STATUS_PENDING, + statusMessage: this.request.body.pending_reason + }); + this.body = ''; + return; + case 'Completed': + yield this.transaction.persist({ + status: Transaction.STATUS_SUCCESS + }); + + yield* require(this.order.module).onSuccess(this.order); + this.body = ''; + return; + default: + yield this.transaction.logRequest("ipn status ignored", this.request); + + this.body = ''; + return; + } + + - this.body = ''; }; From 6dfc915ad7113a577d3bc3c365f6b7b2e823c9c2 Mon Sep 17 00:00:00 2001 From: Anton Vernigor Date: Sat, 26 Jul 2014 10:59:14 +0400 Subject: [PATCH 107/130] Moving markup to jade and stylus. Links. General selectors less specific. --- .../blocks/important/important.styl | 4 +- app/stylesheets/blocks/links/links.styl | 58 +++++++++---------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/app/stylesheets/blocks/important/important.styl b/app/stylesheets/blocks/important/important.styl index fd12982c8..62bdd8048 100644 --- a/app/stylesheets/blocks/important/important.styl +++ b/app/stylesheets/blocks/important/important.styl @@ -31,8 +31,10 @@ a:hover u text-decoration underline - & &__header &__task-link // we really need such a strong selector + & &__task-link @extend $plain-link + + &__task-link margin-right 40px &__task-link_empty diff --git a/app/stylesheets/blocks/links/links.styl b/app/stylesheets/blocks/links/links.styl index 3b99c1c2b..d191a1e73 100644 --- a/app/stylesheets/blocks/links/links.styl +++ b/app/stylesheets/blocks/links/links.styl @@ -22,18 +22,18 @@ $link-type .pseudo:hover color link_hover_color -a:link +:link color link_color -a:visited +:visited color link_visited_color @media (print) a:visited color link_color -a:link, -a:visited +:link, +:visited text-decoration none a:hover, @@ -41,8 +41,8 @@ a:active color link_hover_color text-decoration underline -a.anchor, -a.pseudo +.anchor, +.pseudo @extend $pseudo .link-ref @@ -50,83 +50,83 @@ a.pseudo font-size 90% // -- -.main a[href^="mailto:"], +.main [href^="mailto:"], a.mailto @extend $link-type link-type 'mailto' // -- -.main a[target="_blank"], +.main [target="_blank"], a.external @extend $link-type link-type 'newwindow' // -- -.main a[href^='/play/'], -.main a[href^='http://plnkr.co/'], +.main [href^='/play/'], +.main [href^='http://plnkr.co/'], a.sandbox @extend $link-type link-type 'sandbox' // -- -.main a[href$=".doc"], -.main a[href$=".docx"], +.main [href$=".doc"], +.main [href$=".docx"], a.doc @extend $link-type link-type 'doc' // -- -.main a[href$=".zip"], +.main [href$=".zip"], a.zip @extend $link-type link-type 'zip' // -- -.main a[href$=".xls"], -.main a[href$=".xlsx"], +.main [href$=".xls"], +.main [href$=".xlsx"], a.xls @extend $link-type link-type 'xls' // -- -.main a[href$=".pdf"], +.main [href$=".pdf"], a.pdf @extend $link-type link-type 'pdf' // -- -.main a[href^='http://developer.mozilla.org'], -.main a[href^='https://developer.mozilla.org'], +.main [href^='http://developer.mozilla.org'], +.main [href^='https://developer.mozilla.org'], a.mdn @extend $link-type link-type 'mdn' // -- -.main a[href^='http://msdn.microsoft.com'], -.main a[href^='https://msdn.microsoft.com'], +.main [href^='http://msdn.microsoft.com'], +.main [href^='https://msdn.microsoft.com'], a.msdn @extend $link-type link-type 'msdn' // -- -.main a[href^='http://wikipedia.org'], -.main a[href*='wikipedia.org'], +.main [href^='http://wikipedia.org'], +.main [href*='wikipedia.org'], a.wiki @extend $link-type link-type 'wiki' // -- -.main a[href^='http://w3.org'], -.main a[href^='http://dev.w3.org'], -.main a[href^='http://www.w3.org'], -.main a[href^='https://www.w3.org'], -.main a[href^='https://w3.org'], +.main [href^='http://w3.org'], +.main [href^='http://dev.w3.org'], +.main [href^='http://www.w3.org'], +.main [href^='https://www.w3.org'], +.main [href^='https://w3.org'], a.w3c @extend $link-type link-type 'w3c' // -- -.main a[href^='http://es5.github.com'], +.main [href^='http://es5.github.com'], a.ecma @extend $link-type link-type 'ecma' @@ -139,5 +139,5 @@ $plain-link padding 0 background-image none -.main a.plain +.main .plain @extend $plain-link From acff8bf4f2c4a852cccd6a3238c748e6f46ea9eb Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 26 Jul 2014 11:01:27 +0400 Subject: [PATCH 108/130] pending work to getpdf.onSuccess --- files/Readme.md | 3 ++ hmvc/getpdf/index.js | 4 +-- hmvc/getpdf/onSuccess.js | 28 +++++++++++++++++++ hmvc/payments/paypal/controller/callback.js | 10 +++---- hmvc/payments/paypal/renderForm.js | 4 +-- modules/expiring-download/index.js | 1 + .../models/expiringDownloadLink.js | 21 ++++++++++++++ package.json | 2 ++ 8 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 files/Readme.md create mode 100644 hmvc/getpdf/onSuccess.js create mode 100644 modules/expiring-download/index.js create mode 100644 modules/expiring-download/models/expiringDownloadLink.js diff --git a/files/Readme.md b/files/Readme.md new file mode 100644 index 000000000..484c968f8 --- /dev/null +++ b/files/Readme.md @@ -0,0 +1,3 @@ +Restricted files, not directly accessible from outside. + +A user can download these using ExpiringDownloadLink or by other non-direct means. diff --git a/hmvc/getpdf/index.js b/hmvc/getpdf/index.js index e8f94377c..362d503c3 100644 --- a/hmvc/getpdf/index.js +++ b/hmvc/getpdf/index.js @@ -3,6 +3,4 @@ var router = require('./router'); exports.middleware = router.middleware(); -exports.onSuccess = function* (order) { - console.log("Order success: " + order.number); -}; +exports.onSuccess = require('./onSuccess'); diff --git a/hmvc/getpdf/onSuccess.js b/hmvc/getpdf/onSuccess.js new file mode 100644 index 000000000..6c3070a0e --- /dev/null +++ b/hmvc/getpdf/onSuccess.js @@ -0,0 +1,28 @@ +const expiringDownload = require('expiring-download'); + +const ExpiringDownloadLink = expiringDownload.ExpiringDownloadLink; +const nodemailer = require('nodemailer'); +const ses = require('nodemailer-ses-transport'); + +module.exports = function* (order) { + + // CREATE DOWNLOAD LINK + + // EMAIL IT TO USER (move nodemailer to a separate site-wide "mail" module) + /* + var transporter = nodemailer.createTransport(ses({ + accessKeyId: 'AWSACCESSKEY', + secretAccessKey: 'AWS/Secret/key' + })); + transporter.sendMail({ + from: 'sender@address', + to: 'receiver@address', + subject: 'hello', + text: 'hello world!' + }); +*/ + + // ... + + console.log("Order success: " + order.number); +}; diff --git a/hmvc/payments/paypal/controller/callback.js b/hmvc/payments/paypal/controller/callback.js index 55296c474..8cc387785 100644 --- a/hmvc/payments/paypal/controller/callback.js +++ b/hmvc/payments/paypal/controller/callback.js @@ -17,7 +17,7 @@ exports.post = function* (next) { yield* this.loadTransaction('invoice', {skipOwnerCheck: true}); - yield this.transaction.logRequest('ipn-unverified', this.request); + yield this.transaction.logRequest('ipn-request unverified', this.request); var qs = { 'cmd': '_notify-validate' @@ -71,7 +71,7 @@ exports.post = function* (next) { }).sort({created: -1}).exec(); if (previousIpn && previousIpn.data.payment_status == this.request.body.payment_status) { - yield this.transaction.logRequest("ipn duplicate", this.request); + yield this.transaction.log("ipn duplicate", this.request.body); // ignore duplicate this.body = ''; return; @@ -86,12 +86,11 @@ exports.post = function* (next) { // transaction IDs, indicating they are non-payment IPNs such as those used // for subscription signup requests. if (!this.request.body.txn_id) { + yield this.transaction.log("ipn without txn_id", this.request.body); this.body = ''; return; } - // Exit when we don't get a payment status we recognize - switch(this.request.body.payment_status) { case 'Failed': case 'Voided': @@ -116,7 +115,8 @@ exports.post = function* (next) { this.body = ''; return; default: - yield this.transaction.logRequest("ipn status ignored", this.request); + // Refunded ... + yield this.transaction.log("ipn payment_status unknown", this.request.body); this.body = ''; return; diff --git a/hmvc/payments/paypal/renderForm.js b/hmvc/payments/paypal/renderForm.js index 692fb5395..43c3c29ab 100644 --- a/hmvc/payments/paypal/renderForm.js +++ b/hmvc/payments/paypal/renderForm.js @@ -22,14 +22,14 @@ module.exports = function* (transaction) { lc: "RU" }; + yield transaction.log("cart", cart); + var cartFormatted = []; for(var key in cart) { cartFormatted.push(key + '=' + cart[key]); } cartFormatted = cartFormatted.join("\n"); - yield transaction.log("cartFormatted", cartFormatted); - var cartEncrypted = yield signCart(cartFormatted); var form = jade.renderFile(path.join(__dirname, 'templates/form.jade'), { diff --git a/modules/expiring-download/index.js b/modules/expiring-download/index.js new file mode 100644 index 000000000..418e8b804 --- /dev/null +++ b/modules/expiring-download/index.js @@ -0,0 +1 @@ +exports.ExpiringDownloadLink = require('./models/expiringDownloadLink'); diff --git a/modules/expiring-download/models/expiringDownloadLink.js b/modules/expiring-download/models/expiringDownloadLink.js new file mode 100644 index 000000000..8047f0c61 --- /dev/null +++ b/modules/expiring-download/models/expiringDownloadLink.js @@ -0,0 +1,21 @@ +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; + +// files use /files/ dir +var schema = new Schema({ + // use _id as unique token + + // path from /files/ + relativePath: { + type: String, + required: true + }, + created: { + type: Date, + default: Date.now, + expires: '3d' // link must die in 3 days + } +}); + +module.exports = mongoose.model('ExpiringDownloadLink', schema); + diff --git a/package.json b/package.json index 600fb921d..7bf4c1c05 100755 --- a/package.json +++ b/package.json @@ -67,6 +67,8 @@ "mongoose-auto-increment": "^3.0.8", "mongoose-troop": "git://github.com/iliakan/mongoose-troop", "nib": "^1.0.3", + "nodemailer": "^1.0.4", + "nodemailer-ses-transport": "^0.1.1", "passport": "*", "path-to-regexp": "^0.2.3", "require-tree": "*", From c672a77beb042a91062f1a8acdb373eb7e833b43 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 26 Jul 2014 18:33:27 +0400 Subject: [PATCH 109/130] loaddb task & hacking getpdf --- README.md | 63 +++++++------- db | 2 + docs/payment.sketch/Data | Bin 0 -> 240489 bytes docs/payment.sketch/metadata | 21 +++++ docs/payment.sketch/version | 1 + fixture/db.js | 24 ++++++ gulpfile.js | 34 ++++++-- .../getpdf/controller/{pay.js => checkout.js} | 25 ++++-- .../getpdf/controller/{order.js => orders.js} | 25 ++++-- hmvc/getpdf/controller/payResult.js | 24 ++++++ hmvc/getpdf/controller/success.js | 5 -- hmvc/getpdf/router.js | 15 ++-- hmvc/getpdf/templates/main.jade | 80 ++++++++++++++---- hmvc/getpdf/templates/payment.jade | 6 ++ hmvc/payments/index.js | 5 +- hmvc/payments/models/order.js | 45 ++++++++-- hmvc/payments/models/orderTemplate.js | 37 ++++++++ hmvc/payments/models/transaction.js | 2 +- modules/app.js | 5 +- modules/lib/dataUtil.js | 18 ++-- modules/setup/errorHandler.js | 3 + package.json | 5 +- tasks/loadDb.js | 27 ++++++ 23 files changed, 375 insertions(+), 97 deletions(-) create mode 100644 db create mode 100644 docs/payment.sketch/Data create mode 100644 docs/payment.sketch/metadata create mode 100644 docs/payment.sketch/version create mode 100644 fixture/db.js rename hmvc/getpdf/controller/{pay.js => checkout.js} (61%) rename hmvc/getpdf/controller/{order.js => orders.js} (52%) create mode 100644 hmvc/getpdf/controller/payResult.js delete mode 100644 hmvc/getpdf/controller/success.js create mode 100644 hmvc/getpdf/templates/payment.jade create mode 100644 hmvc/payments/models/orderTemplate.js create mode 100644 tasks/loadDb.js diff --git a/README.md b/README.md index f2221da90..bd274e7d4 100755 --- a/README.md +++ b/README.md @@ -1,40 +1,45 @@ -**Всем желающим предлагается поучаствовать в разработке новой версии сайта http://javascript.ru на Node.JS, Open Source on GitHub.** - -О проекте: - -* Это сайт по JavaScript и смежным технологиям (AJAX, COMET, Browser APIs...) -* Сайт достаточно большой и сложный. В новом проекте предусмотрены разделы: - * учебник (разделы, статьи) - * buy-PDF (элементы eshopping) - * вопрос-ответ - * тесты знаний - * онлайн-курсы - * справочник - * события - * работа -* Вход в сайт - через соц. сети в том числе, личные сообщения и профиль. + +# Движок javascript.ru + +## powered by Node.js + +Всем привет! + +А это исходный код для нового движка сайта http://javascript.ru. + +## Что делаем? + +* Сайт по JavaScript и смежным технологиям (AJAX, COMET, Browser APIs...) * Сайт достаточно посещаемый: порядка 1-1.5 млн просмотров в месяц, и их станет больше при успешной реализации. -* Сайт должен работать очень быстро, при помощи Node.JS и правильного кода можно сделать так, что он будет "летать" -* Планируется перевод на английский, после реализации на русском. -* Основная аудитория - разработчики, так что поддержка старых IE не нужна. Совсем. +* Сайт быстрый, при помощи Node.JS и правильного фронтенда он будет "летать". +* Сайт пока на русском, на английском сделаем потом. +* Сайт для разработчиков, да, кстати, они не пользуются старыми и страшными IE. + +С элементами SPA, но не SPA, так как должен хорошо индексироваться поисковиками, и вообще нафига козе баян. + +## Что в опен-сорсе? + +В опен-сорсе весь код, который будет заставлять двигаться эту штуку. +Многие модули из него можно взять и выделить в отдельные проекты, мы об этом подумаем, потом. + +Также в опен-сорсе - учебник JavaScript. Правда, это в другом репозитарии, здесь только код. + +## А чё, здорово! + +Вообще, всё не так просто, есть над чем покумекать, но мы стараемся :) + + + + + + -Так как сайт должен хорошо индексироваться поисковиками, он будет состоять из страниц с переходом между ними, не SPA. Хотя в различных интерфейсах элементы SPA приветствуются. -Сейчас есть существенная часть дизайна и его вёрстка в HTML/SASS. -Общий стиль вы можете посмотреть здесь: https://www.dropbox.com/s/mo6yx0ct9rrzic4/Learn_Home.png. -RoadMap: -* Реализовать профиль посетителя, логин через соц. сети, с заглушкой на title-page. -* Реализовать показ учебника и навигацию по нему, древовидные комментарии с оценками, подгрузкой. -* Сделать покупку PDF учебника (оформление, приём оплаты, почтовое уведомление, скачивание), при этом учебник можно будет поднять у себя и локально из репозитария, но кто-то будет покупать PDF. -Этот набор фич примерно соответствует текущему http://learn.javascript.ru. Когда закончим -- можно будет выложить его вместо старого learn.javascript.ru. -Далее или, если будет возможность, параллельно, реализуем вопрос-ответ, справочник, тесты знаний. -Обсуждения происходят в чате http://bit.ly/js-skype, собрания анонсируются в issues. -Если не можете войти в чат - напишите мне в Skype, ник: "ilya.a.kantor". diff --git a/db b/db new file mode 100644 index 000000000..837704482 --- /dev/null +++ b/db @@ -0,0 +1,2 @@ +#!/bin/bash +./gulp loaddb --db fixture/db.js diff --git a/docs/payment.sketch/Data b/docs/payment.sketch/Data new file mode 100644 index 0000000000000000000000000000000000000000..3ec90aed0930d4d1acac72b9d2767cb04e53f0b8 GIT binary patch literal 240489 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

        O$1E5hR=qSB{yZ}-ZyzB+w31IDI`SAqg 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ß))AZU~Wm2jaSgy-}_oids=45L}pwOZpcn+9RlV&NeKB*!dqf}Z&k zl;uB#3y<(ISf>Y_LJ^7JE0CPH&-P;u80gp+MF2!V5a%1^D0U+GGGHw-b%H;64nW>T z&*PnEADMwYU^P1)h=S?DDNLpAOzBv-29?ZL2@6_Pyc$(}iJ1j3|H1H;%Hd%MM{tk_ zCi?Xu$O&5p9M=uUW7A{0=#;PfOs2Pf?k#ECIor}5ue`(J{L<^>99Y)A%<|{jHP@ya z@4Hd?H);=S)%d^{l+S485#i zvP?`=+?}wMxYre{rcui78or@D8=qJR*Jyet*f8|+`3lFb`Emb{@wzL68cIT|^Z?w8 zwWJaQ=LGarydkRw=X*$bEC&(=&tn-vjp@FBM+%5b zJC8LmOiH>jw#;N=n1L4<0S6M$lXUqDm{WGU!z`4{z2+8$P=g59N~Z=P9w)SG@%V`W zdq$^5bl>}+S5z^)smLoVVMf<;=bk&$j{Y6#o^QSeEjNDd(=cd`U%3W$ODEecKo8pem4g}c&IZq6}{IF2rA25h0%#Mc5p}}RqLm<#g z!2E&>0D_DNFlg}8M>94A@UBU?&yFhIB-O^$tqEmhL@&|?+%l$M+*j;rrTH*9!mNDx_B)weF&YJ&1D>N)Kh-s8>`dF1l~<_Y!04(N~;k5!K%fZoKOgxJu1W?Iv^Hg)Q=^|jhos{vMLwA-&H0w6-b;J9+8c0UnNV7R{;W#SHgG-Tmo z;o!zk?$!f{bQZx_fF7*R18}N1aOf8>`ovD)FSq~*!bHYXhl%vLoe2ph|FF{qAwrqr zqlmE;fb^FU1_eOT7W|rH(=f-2bodPuJgb zecEwsN3AUC;2J3E7M9!`_<{{i9#4Zq$J3s{Cv=_L7YwhjeQDab&$!9o2YGGpK9WOdPlKatrL>r(`yva{dh#o-CZ49Owg`*jSvP^LBX=9DTsT$A|P1Y z6*?#}3is`s`@h<#qTG;lF>`Tv$?n}qS^hYO-=t5F$-Svz< zStnvuw}oK2d)1uT@7{NJ+WztF>9(!6rEO<#!{sGa$pMB+_gsHZy7H4(F5D9czf_DU zvoMDaPM8b*_{2ckIq^W++5dpKb!aK>tnN+eY~8?oj-GW(*!dRqD6el0&E2S^hJhf! zT*QN*;t720CB7mI?vn#t?k5-g8G`iaWyA0>`!Ay@I2d@~qBBw7plVD2L}o%X0z%TD zkWE}c=6jThI07xig(ZYxz~PaDg#d7bAk*4J6pBnU;$e2;j%Qg?!3%dV!o5Z*OpFcp z4nL!B)9d^6>c$@#Ka9s(1tD$mm_x-4Sf2hchd!74eDKVT-oTAtxiS6m(|??9x$u^B z-RkRbeMx0GfDpLrHFu@!w`)#7!?LnGDEcUe+#G%zg;nsGzlA=5K1X-Q!0IPhh2E7RraqfhOg z=m$8z5dmL<4*`LFQF;K8$pnCTryc_3I3f|_W+2Whc_eEB09-xn1K4}~>2zk_*{MYg zZwXe)LG01^%d>{`MP{J;&rHxU$?EPk{P5!t#_R`l6789xr)<{vSusB0n+hFEm>1IP z;VuNr-5WIvZ~NixY5V84r|VW-mu|U8fz9L<`;_|em@BDQ6$}4BpRqCn7E3cbYi_01v zwYaR~5)N>~BS#)gN5{0Yhd7};eK`4r``BF+n6qB7JQ((x-x_3IQ)4}Vh!CV;e1-vh z;ix8gNP$bA5CYis5POyZxgX|{BRkV2t1ow9C}+sJ$0C4`n8xf5tG#OiAyT=2Tn{x*>*)!+3ow%Up(&uvvKlN~O*wm6y^S=(@VNht>q0t}aK z{_4%O{GftF8PzlAu<#YI&;y?9;Z{dyuIL}0+inm06fRx4HJ#UYVcNR-<=V`|#yw~L zsHcz!h1s5hF7p7KiXGK*@m;$=Xg=)=!K4YCR(*Y=)tXpu+kJk`1AuurCWMN405;`C z@7x^be>Elmpb?pTj-vI1?9Ti|(RU^hU5_*{qRAFNb^Z1A5Nqw5B;5|D$BsUn&RedJ zgqT7+6d7WvEz$*035rxgkmpH#4d;o0$25WSxJ}{+REt-{?}gImqn*3u<_J57Rn1j< z?k96IK_Fpp>#keV4Qp>m@810Gv}wsEFkaN^rt@w}_a3@8F@G{kQ9&Xvb8{dCEcAe* z2kd$1qxz)Xmk#`+J%7IIj&#MUm+84Gy=?U>?AmslhoUCaGaw*Zd+|f9Za1Z4W5?2m z9{pcw^u&n4CnhzBkF>OEvv0FT^-QwERm=hKEe@az_A|5h0-ei0;n& z+M)Xs%P-42SENUeJ#1^AOxQg$_>?jJvqQ!B9s1^Z9=#Eqg=iGy%XC(0*7Ls*EO(#J zTy>knzGHAl+Ocy-x_Rf#)*HCtj2qImE3Zx0t-S64udqyx=?|}LT9XN)*HFiw&08kMx=HHeSuz;gCs3e4(-hB8?e8qtXyif0o^&Az; zk4g-5>S@=UWSgEQ*|-OAN>hW=I;DIty-m{TyYct?-}(D#N&6BHqDWXIlJMXDskfV8 zFa+FD=(#yyq0i?QIA4!GpWk|JIqdnEtA6U)=Yk1Nj{rfiscTcZ<-A+K{1>mT|Lpa4 z^;ntEsLG+^5a!V9!NTo@)aRkKR{IUET78v8+xSno#t-P?hsX9mmUi#o?Qvh3ZWfR2 z6^|VdKkyMSKFc$zKEb$PC$tidO&7VPxU`#y2&=hE6#tj;|bWRuHqKd2m?Jg<2$$K=BnEZWsf-2 z3#*=6j+eX7XO6fA&O-Ki!q_vqC*Aq%o$0;1-wTx&g<`eD>cw37LM}w!h90nm^uVv` z3;7Fo{Drxq_i8^3JW%pMaRl1zOoWH>u;bg?T&n2lL3hq|WZuEaF&P*%7SO}n0%&s1 z;$>z}pr2Cqt_d{gtS|#mi-z!E^m-4>58MPmo?*Ees<}NkSKX?1a8Ko_`VV_fBVeKD z@u`PbbO#bD#VbAdbHkUpJF7sYd z_v`V+h$aJzddoz*BQw)RV~d)_T?X-2%mYNcGpD(XjSGOBLik!~4DZq>0!56m7pYfk z1w{rtd{|?A{VdU$Zi$zDWv6{XEE4S@1-)F)&0ytr%+;rgvyi>&Kc^plbDV`>x%*t^ zJdB)nZqEG%ydErQ;r1d?bM^vAf8MSy;_p57vlV;5=VF;#DvFgKLH00T_Zf?)bRkx}g-1(j=e$}4)4Y!40 zxqH=|)5y)>Rqdw=D@ZJcw&S=?G=BD+^p+iONp~Nttj8}#fD6!k_qp$OjD_+DthsQ2 zFZ5LX&t=cUsN%?tFGV_W5ABU(AcCin!c;=x!4N2!lKK-MN?{ z&P)4-bvK+`=J=Q^zp8mz#huF@{&Iaj+&XSOy09b|-SlYv5d!)Rjt$o0-J(9We+Fc- z@Z`LOM+5=gf0#dJ32;RPVJim?IbMycryj;)C3b2e5@cgN|JzslHGt5CN?C1dtJ%`O_&S_Nf>e=Ul z<#3P9;=Z%+_KfXGH$HS@y7CKGCiW_vZp9KP+k{PJ@miGnKp@pHs<5!Axt@r}dnxhw(p>h6oWkbfPq=hrR&o1Fh*$M24lnlUjfCU`an&2t*=sJvWoG+@71O zZZEW*u;K~YVbA07e9Top_3U%O0#2Mxha$Y}*)g=kguty&+jd| zvd|E!kdV<|_5E6aBm=z_^8#fIfXepok2^fRiMj zLA(dBp<4nVk9e3vCm~_Z_1s*=!7ZMu+o{@vPQCEzxy=QOaC16=o4YmQUJoZ=&{|9t zaSolZ!YqAz|Mt@=1UM&QDvLuNpVAEjIH>2NH{z!rP63MvKxY=O`#E2XpJ19_szrTm zI4$FN`^0 z9jzUS>+yp5OP$`ZvCK>wp3vSO=(i|zWohQj zf|rQL0;-|;!mn=Htj{t_s1Pw^3#S3uqtvN~4hm3Dm#+kPhlB_=OF(GwpU&EQe6Mx+ zp+WpnbbU;hech6EwMqmq&*a=3ZuMY|xSa}4;G(0lFlljayW}>9=kLJDVRM=1!G(El z-gUG4tcO<*2KHR=x#IAA;e_MGcXF7(i+Iozxc!;i(@kgI^n62V*x%NrTN9VATG&|} z8YYk325 z)7ImrnPq)osqmSTlfO%6zM2knFg|ZgH6#G8-rSSAdwbJAFyn|~)s6rOhouN2f&k!4 z=5$kBMtM)5O}m*U(=~kX;K=Baw7hG%JdhHlSP?FTfObKh+jnkyTxb0)13)4pcu={X zn_nomDxC%F++lAMPv9+2f$2# z^O~Qzh54C-Lm?t$+WFOqG&o+(1jDHNYG?@X$bEm89{$Y7od8{a=(8k2;QnC(U!tIs z;#tAh%vrh^S1;jV!YwUQcOSkxd@i1zg*k6MAqL>08qtI9e7N=8DzM(R z+99B*^EnTUjBDNxKh0`hn{DrD;-9oG4Vb34@*2Ag7b3eJciidqMRUX%f2jPV|^toV*8MnE@ zj5vj!;XO6>)MCe|MxXFe5syanz+XtuY3I1PImao?9r_4JTT7eO^l0=<2&fl;KCPud z+XMui*0c*v@t?GZ#r2pBJRxLSe)w*a%txeuK)B9zY4Xt>spskE%zGoRh6KQ+FB?m* zx>nMOKq7H0K^)Wr)3X_f#ylqfS`@`*6AKeEacjVC{FwkS#9FJLOTe{D_(%g23sQs= zVdZ*mK2^NH4?V)j^>C}|IZng@oJCSwd$!I_e}qqXVGg|tTckLZWFy?rtKkE$!V1u$ zY7w7Yk9aJk=YGOGmml1#ZuMXV8s#->{XgsZQvZ2I#`uJs7z{dW0j5bs0I=1EVKqPa z3tnR{5i4(BHSAhFQSbcW;q%iIBTNc3rWz6ePyhAKwDwy*1uhifiZ{Rc=f8Vq{pBs` zjP*U?vY_6O0C;%AW$C`&SGWN92?hM5o@e!J)Wf_6y#cx7n8EDQUL_)_ z=kcSDEB3`N7{9QS5u188d87+`vtSMt=6ddP!SK6~Ti~3E4(=kVZELrk9Pes6!XEk} z#-}dn2sd<)!d&P~&fT!;hPvKgP$1!<>gkm@&52j_UyoMc)N`8y*4?HFGj;dl$S-w0 zm>+#;tk?k5Y?FVZVszU06UOlL2ABYZ`QbB8*zkD^w&O>5n^QWvV`nD&Q5%^{$#|3%Py=HARZ!t=(!m;R3or@eyjchr$PO6>~eeehIjj2e{;nt(qDDA zj9KJYHB9WNDqNsj&c0>QR{-jUoyR5O7JBX$=5VX(xL09eujf{Q(fV^Q=Byro>blvb zQ~%2)z!q^86Mk&;g@1gyk4bf)Gq&c>M|olXCx!L6^mnA0%ij3yGyTiDQvdSqG}PuR zZA5V))sO)A)aSpDzVOjcrZ4~UhiemnP(E9MGg{%3=8R2P8KA}Y1km{>A(Q&GNho}U z%JD@16J^550AqeKfuAyh$&w3~7D6D;$lNTV(6T4Yp*M<`5hoca7$-70?j+WM@{ zQ^#rKO2)@79w?Xppwp(2Js;g;6~N~JSORpOqLF1E01Ygh#g%+CwK8A5WIA2iH=WLF zp3suLjuSVi8WI3~=f5V6U-5>t?-koT!1W52^?xGFtw3xHVtImSA_Q7x;s66rEMpY& z0DLNZ=b@d(?2#d%N9J6x^nzOSBXfa-89g_LTcfb~@WZVE{d8Q4ant&n(zY|Ud3y8o zMIO|H5%EjsO?4{Z`lW zI{?KXW$O>ER!yz9YW&aUdcFt1ggWDYI4LFp;WaQe=qbWFwD`(5INJC| zpoRp%747$>YkPO6?_QzZiL~qArRTK@{EO0w$YDLK2C!KHDaB-k%P3#Q@a4e!EwYO6 z&ci!};6KX{Nx}GGKVP3*3S5P7ve|$Id@gq~rcw~I?DM(hG=okZ{dDvKhzd+pt_tMw z&f}EF8S`SR2%B)i9J)Z)ExRt=bLl;&r|YlqtSln@M)ipMTsm~BZl?%i?VoRASgRlP z99>WJe#`a$*7P+o0-Jj|0f_F0fSjLV{O`MP83x40Jn8$UE=?zf&P^w#Hl(2wToTZj zYDfV5{b$(mId)ZHKIRXoO=GMup$l!fME3K>4whQ zKi7Yqm||H?sx3WO@Xok+1|Tf%-F`TajdzUw;jF!K*{-Jucj>wl>iQ3fcScihYeO4+ z<=Jl$GsAqZ&6mAAUGVDb(iPwHR*w>$5uT^@Fx+6r@`xhEW(6iIh&1BBhXKeh5_3Wc z{4x>lAL~y~_CM(vgCx`sK@d7Z;4*!MH4bU8u=f{V&|3Bi;qWv<>X|QN8IRgMw@c1Mk6YgyM z3I01D~%sLnkI$@()h#+JOt?4wLSF?{8d`>-0!4!|NGV6^&rxZ z>FHM_xZ;elKRp130DdKo1P`FhxvFLm(zEvbLwvUG4|cj{l-o<^r%;32?$ z?PsS?9@>)r>d{Nn9S>fXdcNgNGl~5G8}(eM2ll3G!Z(5Vu@3;L#nc5Y|CAtn&4i_m z78e4X3jozJ!%Xfq7X+6syHsAJ$lN@GbF(2V;9m8QpSfV+7JA_2dbmmFUcA$-mbEN1 zhI?|W1i|WU9(*-^iy5gRtn2!&^LSj5#bYtUorAWO0dYdjr&sf+9&SCDQ=Uz`9$pTU znf(uC9WNNZWPVKwvi3hctqwE(82`f`oS*TfKkn3EGJQ%7s!syKF3h=YF4KurNtd)A zP3ya+Q{PmpzB{OqM0Z4ZVZEU_fX$b5rwd=+o!)rO()8BXpOtQW{p)-P5a*D&AApp> z_Dlr&5TLevfe>Ku07fwk;G64TJD@oaGKSV)zYqrKz%E&ONiEY!OJs0v1{Uzp7jm!X zC;Wz6=v7)3`>*f)bs*mmwW-5f-Y3O?@7?fTYl+LeSi~4@U9vSjdHIv+o(u0u+g5E` z#E=>z{p-&bWssL4Ugxu)s#`t$Dl7|=&xW)2zpT^6Du6e0daK?Kj}>SC@z<&Wb#u}b zz74AV2rvtL!5H)aSZ!OY=a8OVdVUCx>P_$6m-g)%N>lv@Qp=#O5gg>Keq*X30dVk3 zUrO!UKbwX={^>OEzdw?uKk|Oh+;=IEhZT{h^&kwG2xQ#ik@h_RbY*-w5Xr{jpf5_N zH;^7U_<%eV%%2MZWez_kP)Z??XLfFe5Gse|W_O+KJ{JssRlf_^_l)nc+i!y8H$q1c z+_L_b^yFnvru#3xf6=2r4}>uPIVi^ddl-gA*)33Bv&wfq+SKag=x4?b-Zg0qAGJ%UVHUbOTgc`sCCFxD`m&t(q0-2GHx zfggH=m+7}Ybt|s_EEKKti%H%3wDLa6f`o^;s)t)u2d@eXd(|!Aq0iyo+SF=y|ChsA z>a>2e`u2!DjQJyuEt(wn4ZdutqxEOBKPg_J>mPGzehasAtX~R>e+hx!rl(S0_aUtU zbfhI+y=mDJmgO2#4GDm=yN1*Gy3O>mo|*KDi-!~#2a*4e^}jH ze~vNu#D)ul&h#m{oBHzpFVD)DYDR77+K@IZ*)TtoLn#U)H%A6PUs(7HIuS-i-#)PY z8{P{bsxLNG=6j_)%DRxlF~>tjRo$vE?5O$h=5V*Azk(60G0Rs0ipFhlMEC!@e=hzw zsD^YvVfj2+43~WZ-6z;riQ_nYw@(ipXE+Qc9vs})kq!>9p=dmf4-KaAajh&gs2UOg zk3HU$9(w4h^p%|trhC4;Gkx}t?@MpJ;$?LEQ~D`IK^yyYL8y;U*cr&N=Qa}nz8AoU z0Z2AqG;Gp`jGJ}mnKJjG!w(7hWC@uIf|KriCvkHXOb(As$YHrT=+uKX;ubj4ttG<# zk~(cUU_Gz-%8r=SbAO>=RXPferN3Dhe@5${)$!5s(KIzJ4#t0L+AfY^l6*vuZvbN1 z+&`~Jk9tZ_mf5}gPj8z^{{J9dy!N)A{GOSa?v-h(YfTy%S(Zk|m!{G2Xf~P6#4ZO+}_-S;≶Dnz0tLGkg3%LcI0&n~A?dk3#r+-C& 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 VPjv9wf5$#@?Z2=8|ERWe|3A~7CiVaT literal 0 HcmV?d00001 diff --git a/docs/payment.sketch/metadata b/docs/payment.sketch/metadata new file mode 100644 index 000000000..e0754f9f3 --- /dev/null +++ b/docs/payment.sketch/metadata @@ -0,0 +1,21 @@ + + + + + app + com.bohemiancoding.sketch3 + build + 7891 + commit + debc570766a4cc5a2e31258967910f7e5776f485 + fonts + + Helvetica + Helvetica-Bold + + length + 240489 + version + 37 + + diff --git a/docs/payment.sketch/version b/docs/payment.sketch/version new file mode 100644 index 000000000..7c091989d --- /dev/null +++ b/docs/payment.sketch/version @@ -0,0 +1 @@ +37 \ No newline at end of file diff --git a/fixture/db.js b/fixture/db.js new file mode 100644 index 000000000..78c0b9546 --- /dev/null +++ b/fixture/db.js @@ -0,0 +1,24 @@ +const mongoose = require('mongoose'); + +var OrderTemplate = require('payments').OrderTemplate; + +exports.OrderTemplate = [ + { + title: "Основы JavaScript", + description: "500 стр, 10мб", + slug: "jsbasics", + amount: 1 + }, + { + title: "JS-DOM", + description: "400 стр, 8мб", + slug: "dom", + amount: 1 + }, + { + title: "Две книги сразу", + description: "500 стр, 8мб", + slug: "api", + amount: 1 + } +]; diff --git a/gulpfile.js b/gulpfile.js index 42a322081..d0f6c3f87 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,11 +1,13 @@ /** * NB: All tasks are initialized lazily, even plugins are required lazily, * running 1 task does not require all tasks' files -*/ + */ const gulp = require('gulp'); const gp = require('gulp-load-plugins')(); const path = require('path'); +const fs = require('fs'); +const assert = require('assert'); //const browserifyTask = require('tasks/browserify'); const serverSources = [ @@ -25,7 +27,20 @@ gulp.task('lint-watch', ['lint'], function(neverCalled) { gulp.watch(serverSources, ['lint']); }); -//gulp.task('lint', require('./tasks/lint-full-die')(serverSources)); +// usage: gulp loaddb --db fixture/db +gulp.task('loaddb', function(callback) { + var task = require('tasks/loadDb'); + + var args = require('yargs') + .usage("Path to DB is required.") + .demand(['db']) + .argv; + + var dbPath = path.join(__dirname, args.db); + assert(fs.existsSync(dbPath)); + + task(dbPath)(callback); +}); gulp.task('watch', ['stylus'], function(neverCalled) { @@ -37,13 +52,16 @@ gulp.task('watch', ['stylus'], function(neverCalled) { */ const fse = require('fs-extra'); - fse.ensureDirSync('www/fonts'); - gp.dirSync('app/fonts', 'www/fonts'); + fse.removeSync(['www/fonts']); + fse.removeSync(['www/img']); + fse.removeSync(['www/js']); - fse.ensureDirSync('www/img'); - gp.dirSync('app/img', 'www/img'); + fse.mkdirsSync('www/fonts'); + fse.mkdirsSync('www/img'); + fse.mkdirsSync('www/js'); - fse.ensureDirSync('www/js'); + gp.dirSync('app/fonts', 'www/fonts'); + gp.dirSync('app/img', 'www/img'); gp.dirSync('app/js', 'www/js'); gulp.watch("app/**/*.sprites/**", ['sprite']); @@ -89,6 +107,8 @@ gulp.task('link-modules', function() { return linkModules(['modules/*', 'hmvc/*']).apply(this, arguments); }); + + gulp.task('sprite', function() { var options = { spritesSearchFsRoot: 'app', diff --git a/hmvc/getpdf/controller/pay.js b/hmvc/getpdf/controller/checkout.js similarity index 61% rename from hmvc/getpdf/controller/pay.js rename to hmvc/getpdf/controller/checkout.js index b24885ba5..5d91e5223 100644 --- a/hmvc/getpdf/controller/pay.js +++ b/hmvc/getpdf/controller/checkout.js @@ -2,6 +2,7 @@ var mongoose = require('mongoose'); var log = require('js-log')(); var payments = require('payments'); var Order = payments.Order; +var OrderTemplate = payments.OrderTemplate; var methods = require('../paymentMethods').methods; log.debugOn(); @@ -9,7 +10,6 @@ log.debugOn(); exports.post = function*(next) { yield* this.loadOrder(); - var method = methods[this.request.body.paymentMethod]; if (!method) { this.throw(403, "Unsupported payment method"); @@ -19,11 +19,24 @@ exports.post = function*(next) { log.debug("order exists", this.order.number); yield* updateOrderFromBody(this.request.body, this.order); } else { - // new order template - this.order = new Order({ - amount: 1, + // if we don't have the order in our database, then make a new one + // (use the incoming order post for that, but don't trust it) + + console.log(this.request.body.orderTemplate); + + var orderTemplate = yield OrderTemplate.findOne({ + slug: this.request.body.orderTemplate + }).exec(); + + if (!orderTemplate) { + this.throw(404); + } + + console.log("GOT TEMPLATE"); + // create order from template, don't trust the incoming post + this.order = Order.createFromTemplate(orderTemplate, { module: 'getpdf', - data: { } + email: this.request.body.email }); yield* updateOrderFromBody(this.request.body, this.order); @@ -43,7 +56,7 @@ exports.post = function*(next) { }; function* updateOrderFromBody(body, order) { - order.data.email = body.email; + order.email = body.email; order.markModified('data'); yield order.persist(); diff --git a/hmvc/getpdf/controller/order.js b/hmvc/getpdf/controller/orders.js similarity index 52% rename from hmvc/getpdf/controller/order.js rename to hmvc/getpdf/controller/orders.js index 845ea2f36..1f8738538 100644 --- a/hmvc/getpdf/controller/order.js +++ b/hmvc/getpdf/controller/orders.js @@ -1,29 +1,40 @@ const payments = require('payments'); var Order = payments.Order; +var OrderTemplate = payments.OrderTemplate; var Transaction = payments.Transaction; exports.get = function*(next) { + this.set('Cache-Control', 'no-cache, no-store, must-revalidate'); + var lastTransaction; if (this.params.orderNumber) { yield* this.loadOrder(); + + var lastTransaction = yield Transaction.findOne({ order: this.order._id }).sort({created: -1}).exec(); } else { + var orderTemplate = yield OrderTemplate.findOne({ + slug: this.params.orderTemplate + }).exec(); + + if (!orderTemplate) { + this.throw(404); + } + + this.locals.orderTemplate = this.params.orderTemplate; + // this order is not saved anywhere, // it's only used to initially fill the form - this.order = new Order({ - amount: 1, + // order.isNew = true! + this.order = Order.createFromTemplate(orderTemplate, { module: 'getpdf', - data: { - email: Math.round(Math.random()*1e6).toString(36) + '@gmail.com' - } + email: Math.round(Math.random()*1e6).toString(36) + '@gmail.com' }); } this.locals.order = this.order; - var lastTransaction = yield Transaction.findOne({ order: this.order._id }).sort({created: -1}).exec(); - if (lastTransaction) { console.log(lastTransaction.getStatusDescription); this.locals.message = 'Статус последней оплаты: ' + lastTransaction.getStatusDescription(); diff --git a/hmvc/getpdf/controller/payResult.js b/hmvc/getpdf/controller/payResult.js new file mode 100644 index 000000000..65cee6888 --- /dev/null +++ b/hmvc/getpdf/controller/payResult.js @@ -0,0 +1,24 @@ +const payments = require('payments'); +var Order = payments.Order; +var OrderTemplate = payments.OrderTemplate; +var Transaction = payments.Transaction; + +exports.get = function*(next) { + this.set('Cache-Control', 'no-cache, no-store, must-revalidate'); + + yield* this.loadOrder(); + + var lastTransaction = yield Transaction.findOne({ order: this.order._id }).sort({created: -1}).exec(); + + if (!lastTransaction.status) { + this.body = ''; + this.status = 204; // no content + return; + } + + if (lastTransaction) { + console.log(lastTransaction.getStatusDescription); + this.body = lastTransaction.getStatusDescription(); + } + +}; diff --git a/hmvc/getpdf/controller/success.js b/hmvc/getpdf/controller/success.js deleted file mode 100644 index 3b001db83..000000000 --- a/hmvc/getpdf/controller/success.js +++ /dev/null @@ -1,5 +0,0 @@ -exports.get = function*(next) { -// yield* payment.loadOrder(this); - - this.body = 'THANK YOU'; -}; diff --git a/hmvc/getpdf/router.js b/hmvc/getpdf/router.js index 87985f28a..c956568c7 100644 --- a/hmvc/getpdf/router.js +++ b/hmvc/getpdf/router.js @@ -2,12 +2,13 @@ var Router = require('koa-router'); var router = module.exports = new Router(); -var order = require('./controller/order'); -var pay = require('./controller/pay'); -var success = require('./controller/success'); +var orders = require('./controller/orders'); +var payResult = require('./controller/payResult'); +var checkout = require('./controller/checkout'); -router.get('', order.get); -router.get('/order/:orderNumber', order.get); +router.get('/:orderTemplate', orders.get); +router.get('/orders/:orderNumber(\\d+)', orders.get); -router.post('/pay', pay.post); -router.get('/success/:orderNumber', success.get); +router.get('/pay-result/:orderNumber(\\d+)', payResult.get); + +router.post('/checkout', checkout.post); diff --git a/hmvc/getpdf/templates/main.jade b/hmvc/getpdf/templates/main.jade index ce74f6b6b..2bd69de1e 100644 --- a/hmvc/getpdf/templates/main.jade +++ b/hmvc/getpdf/templates/main.jade @@ -1,32 +1,79 @@ -if message - div= message +script var csrf = "#{csrf}", orderNumber; div - p Выберите способ оплаты: - form.pay-form.notready(onsubmit="alert('Минуточку, идёт загрузка...'); return false") + p Спасибо, что решили приобрести PDF-учебник! + + form.pay-form input(type="text" name="orderNumber" value=(!order.isNew && order.number) placeholder="order number") - input(name="email" value=order.data.email placeholder="E-mail") - select(name="paymentMethod") - each paymentMethod in paymentMethods - option(value=paymentMethod.name) #{paymentMethod.title} - input(type="submit" value="Оплатить") + input(type="text" name="orderTemplate" value=orderTemplate) -script(src="http://code.jquery.com/jquery-2.1.1.js") + fieldset + legend Описание книги и стоимость + h2= order.title + div= order.description + div #{order.amount} р. + + fieldset + legend Укажите свой email (если не авторизован) -script var csrf = "#{csrf}"; + input(name="email" value=order.email placeholder="E-mail") + fieldset + legend Оплата или результат оплаты + + if order.isNew + include payment + else + div.pay-result Загружаем информацию... + +script(src="http://code.jquery.com/jquery-2.1.1.js") script. - $('.notready').removeClass('notready').prop('onsubmit', null); + var payForm = $('.pay-form'); - $('.pay-form') - .on('submit', function(e) { + var payResult = $('.pay-result'); + + if (payResult.length) { + var requestPayResultStart = new Date(); + requestPayResult(); + } + + payForm.on('submit', onPayFormSubmit); + + function requestPayResult() { + $.ajax({ + method: 'GET', + url: '/getpdf/pay-result/' + payForm[0].elements.orderNumber.value, + data: { + orderNumber: payForm[0].elements.orderNumber.value + } + }) + .done(function(result) { + if (!result) { + if (new Date() - requestPayResultStart > 120e3) { // 2 mins + payResult.html("Таймаут ответа от платёжной системы. Попробуйте обновить эту страницу позже или обратиться в поддержку."); + } else { + setTimeout(requestPayResult, 1000); + } + return; + } + payResult.html(result); + }) + .fail(function(err) { + setTimeout(requestPayResult, 1000); + }); + + } + + + function onPayFormSubmit(e) { e.preventDefault(); $.ajax({ method: 'POST', - url: '/getpdf/pay', + url: '/getpdf/checkout', data: { _csrf: csrf, orderNumber: this.elements.orderNumber.value, + orderTemplate: this.elements.orderTemplate.value, email: this.elements.email.value, paymentMethod: this.elements.paymentMethod.value } @@ -37,5 +84,4 @@ script. .done(function(htmlForm) { $(htmlForm).submit(); }); - - }); + } diff --git a/hmvc/getpdf/templates/payment.jade b/hmvc/getpdf/templates/payment.jade new file mode 100644 index 000000000..8cd61d50a --- /dev/null +++ b/hmvc/getpdf/templates/payment.jade @@ -0,0 +1,6 @@ +p Выберите способ оплаты: + select(name="paymentMethod") + each paymentMethod in paymentMethods + option(value=paymentMethod.name) #{paymentMethod.title} + input(type="submit" value="Оплатить") + diff --git a/hmvc/payments/index.js b/hmvc/payments/index.js index cfd3261f6..8907bb8c4 100644 --- a/hmvc/payments/index.js +++ b/hmvc/payments/index.js @@ -8,6 +8,7 @@ exports.loadOrder = require('./lib/loadOrder'); exports.loadTransaction = require('./lib/loadTransaction'); var Order = exports.Order = require('./models/order'); +var OrderTemplate = exports.OrderTemplate = require('./models/orderTemplate'); var Transaction = exports.Transaction = require('./models/transaction'); var TransactionLog = exports.TransactionLog = require('./models/transactionLog'); @@ -30,7 +31,7 @@ exports.middleware = compose(paymentMounts); exports.populateContextMiddleware = function*(next) { this.redirectToOrder = function(order) { order = order || this.order; - this.redirect('/' + order.module + '/order/' + order.number); + this.redirect('/' + order.module + '/orders/' + order.number); }; this.loadOrder = exports.loadOrder; this.loadTransaction = exports.loadTransaction; @@ -49,6 +50,8 @@ exports.createTransactionForm = function* (order, method) { yield transaction.persist(); + console.log(transaction); + var form = yield* paymentModules[method].renderForm(transaction); yield transaction.log('form', form); diff --git a/hmvc/payments/models/order.js b/hmvc/payments/models/order.js index bf407ca13..0618939c2 100644 --- a/hmvc/payments/models/order.js +++ b/hmvc/payments/models/order.js @@ -1,27 +1,60 @@ var mongoose = require('mongoose'); var Schema = mongoose.Schema; var autoIncrement = require('mongoose-auto-increment'); +var OrderTemplate = require('./orderTemplate'); +var _ = require('lodash'); var schema = new Schema({ - amount: { + amount: { type: Number, required: true }, - module: { // module so that transaction handler knows where to go back e.g. 'getpdf' + module: { // module so that transaction handler knows where to go back e.g. 'getpdf' type: String, required: true }, - status: { + title: { + type: String, + required: true + }, + description: { + type: String + }, + status: { + type: String + }, + + // order can be bound to either an email or a user + email: { type: String }, - data: Schema.Types.Mixed, - created: { + user: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + + data: Schema.Types.Mixed, + created: { type: Date, default: Date.now } }); -schema.plugin(autoIncrement.plugin, {model: 'Order', field: 'number'}); +schema.statics.createFromTemplate = function(orderTemplate, body) { + var Order = this; + + var data = _.assign({ + title: orderTemplate.title, + description: orderTemplate.description, + amount: orderTemplate.amount, + data: {} + }, body || {}); + + return new Order(data); + +}; + +schema.plugin(autoIncrement.plugin, {model: 'Order', field: 'number', startAt: 1}); module.exports = mongoose.model('Order', schema); diff --git a/hmvc/payments/models/orderTemplate.js b/hmvc/payments/models/orderTemplate.js new file mode 100644 index 000000000..04626e66a --- /dev/null +++ b/hmvc/payments/models/orderTemplate.js @@ -0,0 +1,37 @@ +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; + +/** + * In other words, "Store items" + * New orders do *not* reference the items, because store items may change + * Instead new orders contain full information about themselves. + * + * OrderTemplate can be deleted, but the order is self-contained. + * @type {Schema} + */ +var schema = new Schema({ + title: { + type: String, + required: true + }, + description: { + type: String + }, + // when a user visits /order/slug, the new order is created from this template + slug: { + type: String, + required: true, + index: true + }, + amount: { + type: Number, + required: true + }, + created: { + type: Date, + default: Date.now + } +}); + +module.exports = mongoose.model('OrderTemplate', schema); + diff --git a/hmvc/payments/models/transaction.js b/hmvc/payments/models/transaction.js index a150f2745..2a1a8f9c0 100644 --- a/hmvc/payments/models/transaction.js +++ b/hmvc/payments/models/transaction.js @@ -37,7 +37,7 @@ var schema = new Schema({ } }); -schema.plugin(autoIncrement.plugin, {model: 'Transaction', field: 'number'}); +schema.plugin(autoIncrement.plugin, {model: 'Transaction', field: 'number', startAt: 1}); schema.statics.STATUS_SUCCESS = 'success'; schema.statics.STATUS_PENDING = 'pending'; diff --git a/modules/app.js b/modules/app.js index dbd32cfaa..23c25ef61 100644 --- a/modules/app.js +++ b/modules/app.js @@ -26,6 +26,10 @@ function requireSetup(path) { // usually nginx will handle this requireSetup('setup/static'); +// this middleware adds this.render method +// it is *before error*, because errors need this.render +requireSetup('setup/render'); + // errors wrap everything requireSetup('setup/errorHandler'); @@ -48,7 +52,6 @@ requireSetup('setup/csrf'); requireSetup('setup/payments'); -requireSetup('setup/render'); requireSetup('setup/router'); if (process.env.NODE_ENV == 'test') { diff --git a/modules/lib/dataUtil.js b/modules/lib/dataUtil.js index 2fea8c548..3af7ba54f 100644 --- a/modules/lib/dataUtil.js +++ b/modules/lib/dataUtil.js @@ -4,8 +4,9 @@ var mongoose = require('mongoose'); var log = require('js-log')(); var co = require('co'); var thunk = require('thunkify'); -var db = mongoose.connection.db; +var glob = require('glob'); +var db; //log.debugOn(); function *createEmptyDb() { @@ -13,11 +14,12 @@ function *createEmptyDb() { function *open() { if (mongoose.connection.readyState == 1) { // connected - return; + return mongoose.connection.db; } yield thunk(mongoose.connection.on)('open'); + return mongoose.connection.db; } function *clearDatabase() { @@ -68,16 +70,16 @@ function *createEmptyDb() { log.debug("co"); - yield open; + db = yield open(); log.debug("open"); - yield clearDatabase; + yield clearDatabase(); log.debug("clear"); - yield ensureIndexes; + yield ensureIndexes(); log.debug('indexes'); - yield ensureCapped; + yield ensureCapped(); log.debug('capped'); } @@ -85,8 +87,7 @@ function *createEmptyDb() { // not using pow-mongoose-fixtures, becuae it fails with capped collections // it calls remove() on them => everything dies function *loadDb(dataFile) { - yield createEmptyDb; - + yield* createEmptyDb(); var modelsData = require(dataFile); yield Object.keys(modelsData).map(function(modelName) { @@ -94,6 +95,7 @@ function *loadDb(dataFile) { }); } +// fixture file must make sure that the model is loaded! function *loadModel(name, data) { var Model = mongoose.models[name]; diff --git a/modules/setup/errorHandler.js b/modules/setup/errorHandler.js index e6eb6e6eb..7e41690b7 100644 --- a/modules/setup/errorHandler.js +++ b/modules/setup/errorHandler.js @@ -48,6 +48,9 @@ module.exports = function(app) { if (err.status) { // user-level error + if (process.env.NODE_ENV == 'development') { + console.log(err); + } this.renderError(err); } else { diff --git a/package.json b/package.json index 7bf4c1c05..360faa494 100755 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "prod": "NODE_ENV=production node --harmony ./bin/www", - "dev": "NODE_ENV=development supervisor --harmony --debug --ignore node_modules ./bin/www", + "dev": "./gulp link-modules && NODE_ENV=development supervisor --harmony --debug --ignore node_modules ./bin/www", "debug": "NODE_ENV=development supervisor --harmony --debug-brk --ignore node_modules ./bin/www", "test": "NODE_ENV=test supervisor --harmony --debug --ignore node_modules ./bin/www", "postinstall": "NODE_ENV=development node --harmony `which gulp` link-modules", @@ -78,7 +78,8 @@ "thunkify": "*", "vinyl-fs": "^0.3.4", "vinyl-source-stream": "^0.1.1", - "winston": "*" + "winston": "*", + "yargs": "^1.2.6" }, "devDependencies": { "clarify": "*", diff --git a/tasks/loadDb.js b/tasks/loadDb.js new file mode 100644 index 000000000..be90fb671 --- /dev/null +++ b/tasks/loadDb.js @@ -0,0 +1,27 @@ +var fs = require('fs'); +var co = require('co'); +var path = require('path'); +var gutil = require('gulp-util'); +var dataUtil = require('lib/dataUtil'); +var mongoose = require('config/mongoose'); + +module.exports = function(dbPath) { + + return function(callback) { + + gutil.log("loading db " + dbPath); + + co(function*() { + + yield* dataUtil.loadDb(dbPath); + + gutil.log("loaded db " + dbPath); + })(function(err) { + if (err) throw err; + mongoose.disconnect(); + callback(); + }); + + }; + +}; From 813f9ce6525907a80008a65fa2e6486313135179 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 26 Jul 2014 18:35:07 +0400 Subject: [PATCH 110/130] issues --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bd274e7d4..a06d1b2da 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# Движок javascript.ru +# Новый javascript.ru ## powered by Node.js @@ -28,6 +28,7 @@ Вообще, всё не так просто, есть над чем покумекать, но мы стараемся :) +Пишите в issues, если есть о чём. From ca7015750cc05f4a7e0733173313d5fc5ab4c5d2 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 27 Jul 2014 01:16:42 +0400 Subject: [PATCH 111/130] hacking payments --- hmvc/getpdf/controller/payResult.js | 36 ++++++++++-- hmvc/getpdf/onSuccess.js | 5 ++ hmvc/getpdf/templates/main.jade | 56 +++++++++++++------ hmvc/getpdf/templates/payment.jade | 6 -- hmvc/payments/bank-simple/index.js | 2 + hmvc/payments/bank-simple/renderForm.js | 15 +++++ hmvc/payments/bank-simple/templates/form.jade | 7 +++ hmvc/payments/bank-simple/test/.jshintrc | 23 ++++++++ hmvc/payments/models/transaction.js | 8 +-- .../payments/payanyway/controller/callback.js | 31 ++++++++-- hmvc/payments/payanyway/renderForm.js | 3 +- hmvc/payments/payanyway/templates/form.jade | 3 +- hmvc/payments/webmoney/controller/callback.js | 8 +-- modules/config/index.js | 2 +- 14 files changed, 163 insertions(+), 42 deletions(-) delete mode 100644 hmvc/getpdf/templates/payment.jade create mode 100644 hmvc/payments/bank-simple/index.js create mode 100644 hmvc/payments/bank-simple/renderForm.js create mode 100644 hmvc/payments/bank-simple/templates/form.jade create mode 100644 hmvc/payments/bank-simple/test/.jshintrc diff --git a/hmvc/getpdf/controller/payResult.js b/hmvc/getpdf/controller/payResult.js index 65cee6888..6de493bdc 100644 --- a/hmvc/getpdf/controller/payResult.js +++ b/hmvc/getpdf/controller/payResult.js @@ -8,17 +8,45 @@ exports.get = function*(next) { yield* this.loadOrder(); + + if (this.order.status == Transaction.STATUS_SUCCESS) { + this.body = { + status: Transaction.STATUS_SUCCESS, + html: 'Спасибо за покупку! Вот ваши ништяки.' + }; + return; + } + + var lastTransaction = yield Transaction.findOne({ order: this.order._id }).sort({created: -1}).exec(); - if (!lastTransaction.status) { + // no payment at all? strange + if (!lastTransaction) { + this.body = { + status: Transaction.STATUS_FAIL, + html: 'Оплаты не было.' + }; + return; + } + + // last transaction was successful, but the order is not + // let's wait a little bit + if (lastTransaction.status == Transaction.STATUS_SUCCESS) { this.body = ''; this.status = 204; // no content return; } - if (lastTransaction) { - console.log(lastTransaction.getStatusDescription); - this.body = lastTransaction.getStatusDescription(); + // transaction status unknown -> waiting + if (!lastTransaction.status) { + this.body = ''; + this.status = 204; // no content + return; } + this.body = { + status: lastTransaction.status, + html: lastTransaction.getStatusDescription() + }; + }; diff --git a/hmvc/getpdf/onSuccess.js b/hmvc/getpdf/onSuccess.js index 6c3070a0e..3eb9a7db3 100644 --- a/hmvc/getpdf/onSuccess.js +++ b/hmvc/getpdf/onSuccess.js @@ -1,3 +1,4 @@ +const Transaction = require('payments').Transaction; const expiringDownload = require('expiring-download'); const ExpiringDownloadLink = expiringDownload.ExpiringDownloadLink; @@ -6,6 +7,10 @@ const ses = require('nodemailer-ses-transport'); module.exports = function* (order) { + yield order.persist({ + status: Transaction.STATUS_SUCCESS + }); + // CREATE DOWNLOAD LINK // EMAIL IT TO USER (move nodemailer to a separate site-wide "mail" module) diff --git a/hmvc/getpdf/templates/main.jade b/hmvc/getpdf/templates/main.jade index 2bd69de1e..809b41807 100644 --- a/hmvc/getpdf/templates/main.jade +++ b/hmvc/getpdf/templates/main.jade @@ -1,9 +1,23 @@ +style. + form.pay-form[is_new="0"] .pay-info { + display: none; + } + + form.pay-form[is_new="1"] .pay-result { + display: none; + } + + + form.pay-form[is_new="0"].fail .pay-info { + display: block; + } + script var csrf = "#{csrf}", orderNumber; div p Спасибо, что решили приобрести PDF-учебник! - form.pay-form + form.pay-form(is_new=(order.isNew ? "1" : "0")) input(type="text" name="orderNumber" value=(!order.isNew && order.number) placeholder="order number") input(type="text" name="orderTemplate" value=orderTemplate) @@ -14,32 +28,41 @@ div div #{order.amount} р. fieldset + legend Укажите свой email (если не авторизован) - input(name="email" value=order.email placeholder="E-mail") + input(name="email" value=order.email placeholder="E-mail" disabled=!order.isNew) fieldset legend Оплата или результат оплаты - if order.isNew - include payment - else - div.pay-result Загружаем информацию... + div.pay-result Загружаем информацию... + + //- показываем выбор оплаты только если заказ новый, иначе пока прячем и ждём, нужно ли + div.pay-info + p Выберите способ оплаты: + select(name="paymentMethod") + each paymentMethod in paymentMethods + option(value=paymentMethod.name) #{paymentMethod.title} + input(type="submit" value="Оплатить") + + script(src="http://code.jquery.com/jquery-2.1.1.js") script. var payForm = $('.pay-form'); - var payResult = $('.pay-result'); - if (payResult.length) { var requestPayResultStart = new Date(); requestPayResult(); } - payForm.on('submit', onPayFormSubmit); - function requestPayResult() { + if (new Date() - requestPayResultStart > 120e3) { // 2 mins + payResult.html("Таймаут ответа от платёжной системы. Попробуйте обновить эту страницу позже или обратиться в поддержку."); + return; + } + $.ajax({ method: 'GET', url: '/getpdf/pay-result/' + payForm[0].elements.orderNumber.value, @@ -49,21 +72,20 @@ script. }) .done(function(result) { if (!result) { - if (new Date() - requestPayResultStart > 120e3) { // 2 mins - payResult.html("Таймаут ответа от платёжной системы. Попробуйте обновить эту страницу позже или обратиться в поддержку."); - } else { - setTimeout(requestPayResult, 1000); - } + setTimeout(requestPayResult, 1000); return; } - payResult.html(result); + showPayResult(result); }) .fail(function(err) { setTimeout(requestPayResult, 1000); }); - } + function showPayResult(result) { + payForm.addClass(result.status); + payResult.html(result.html); + } function onPayFormSubmit(e) { e.preventDefault(); diff --git a/hmvc/getpdf/templates/payment.jade b/hmvc/getpdf/templates/payment.jade deleted file mode 100644 index 8cd61d50a..000000000 --- a/hmvc/getpdf/templates/payment.jade +++ /dev/null @@ -1,6 +0,0 @@ -p Выберите способ оплаты: - select(name="paymentMethod") - each paymentMethod in paymentMethods - option(value=paymentMethod.name) #{paymentMethod.title} - input(type="submit" value="Оплатить") - diff --git a/hmvc/payments/bank-simple/index.js b/hmvc/payments/bank-simple/index.js new file mode 100644 index 000000000..ca311839c --- /dev/null +++ b/hmvc/payments/bank-simple/index.js @@ -0,0 +1,2 @@ + +exports.renderForm = require('./renderForm'); diff --git a/hmvc/payments/bank-simple/renderForm.js b/hmvc/payments/bank-simple/renderForm.js new file mode 100644 index 000000000..0a6f6d526 --- /dev/null +++ b/hmvc/payments/bank-simple/renderForm.js @@ -0,0 +1,15 @@ +const jade = require('jade'); +const config = require('config'); +const path = require('path'); + +module.exports = function* (transaction) { + + return jade.renderFile(path.join(__dirname, 'templates/form.jade'), { + amount: transaction.amount, + number: transaction.number, + webmoney: config.payments.modules.webmoney + }); + +}; + + diff --git a/hmvc/payments/bank-simple/templates/form.jade b/hmvc/payments/bank-simple/templates/form.jade new file mode 100644 index 000000000..8ad6d67ce --- /dev/null +++ b/hmvc/payments/bank-simple/templates/form.jade @@ -0,0 +1,7 @@ +form(method="POST",action="https://merchant.webmoney.ru/lmi/payment.asp") + input(type="hidden",name="LMI_PAYMENT_AMOUNT",value=amount) + input(type="hidden",name="LMI_PAYMENT_DESC_BASE64",value=new Buffer('оплата по счету ' + number).toString('base64')) + input(type="hidden",name="LMI_PAYMENT_NO",value=number) + input(type="hidden",name="LMI_PAYEE_PURSE",value=webmoney.purse) + input(type="hidden",name="LMI_SIM_MODE",value=(isTest ? 1 : 0)) + input(type="submit",value="Оплатить") diff --git a/hmvc/payments/bank-simple/test/.jshintrc b/hmvc/payments/bank-simple/test/.jshintrc new file mode 100644 index 000000000..077663629 --- /dev/null +++ b/hmvc/payments/bank-simple/test/.jshintrc @@ -0,0 +1,23 @@ +{ + "maxerr": 25, + "latedef": "nofunc", + "browser": false, + "node": true, + "indent": 2, + "camelcase": true, + "newcap": true, + "undef": true, + "esnext": true, + "multistr": true, + "noyield": true, + "expr": true, + "-W004": true, + "globals" : { + "describe" : false, + "it" : false, + "before" : false, + "beforeEach" : false, + "after" : false, + "afterEach" : false + } +} diff --git a/hmvc/payments/models/transaction.js b/hmvc/payments/models/transaction.js index 2a1a8f9c0..308cde10d 100644 --- a/hmvc/payments/models/transaction.js +++ b/hmvc/payments/models/transaction.js @@ -76,16 +76,16 @@ schema.pre('save', function(next) { schema.methods.getStatusDescription = function() { if (this.status == Transaction.STATUS_SUCCESS) { - return 'оплата прошла успешно'; + return 'Оплата прошла успешно.'; } if (this.status == Transaction.STATUS_PENDING) { - return 'оплата ожидается'; + return 'Оплата ожидается, о её успешномо окончании вы будете извещены по e-mail.'; } if (this.status == Transaction.STATUS_FAIL) { - var result = 'оплата не прошла'; + var result = 'Оплата не прошла'; if (this.statusMessage) result += ': ' + this.statusMessage; - return result; + return result + '.'; } if (!this.status) { diff --git a/hmvc/payments/payanyway/controller/callback.js b/hmvc/payments/payanyway/controller/callback.js index 10eb0bc65..3fb76351a 100644 --- a/hmvc/payments/payanyway/controller/callback.js +++ b/hmvc/payments/payanyway/controller/callback.js @@ -1,4 +1,6 @@ const config = require('config'); +//require('config/mongoose'); +const payanywayConfig = config.payments.modules.payanyway; const Order = require('../../models/order'); const Transaction = require('../../models/transaction'); const log = require('js-log')(); @@ -10,6 +12,9 @@ exports.post = function* (next) { yield* this.loadTransaction('MNT_TRANSACTION_ID', {skipOwnerCheck : true}); + + yield this.transaction.logRequest('callback unverified', this.request); + if (!checkSignature(this.request.body)) { log.debug("wrong signature"); this.throw(403, "wrong signature"); @@ -19,7 +24,7 @@ exports.post = function* (next) { // signature is valid, so everything MUST be fine if (this.transaction.amount != parseFloat(this.request.body.MNT_AMOUNT) || - this.request.body.MNT_ID != config.payments.modules.payanyway.id) { + this.request.body.MNT_ID != payanywayConfig.id) { yield this.transaction.persist({ status: Transaction.STATUS_FAIL, statusMessage: "данные транзакции не совпадают с базой, свяжитесь с поддержкой" @@ -40,9 +45,27 @@ exports.post = function* (next) { function checkSignature(body) { - var signature = md5(body.MNT_ID + body.MNT_TRANSACTION_ID + body.MNT_OPERATION_ID + - body.MNT_AMOUNT + body.MNT_CURRENCY_CODE + body.MNT_SUBSCRIBER_ID + body.MNT_TEST_MODE + - config.payments.modules.payanyway.secret).toUpperCase(); + var signature = body.MNT_ID + body.MNT_TRANSACTION_ID + body.MNT_AMOUNT + + body.MNT_CURRENCY_CODE + (body.MNT_SUBSCRIBER_ID || '') + (+body.MNT_TEST_MODE ? '1' : '0') + payanywayConfig.secret; + +// console.log(signature); + signature = md5(signature); +// console.log(signature); return signature == body.MNT_SIGNATURE; } + +/* +var body ={ MNT_ID: '31873866', + MNT_TRANSACTION_ID: '12', + MNT_OPERATION_ID: '55923826', + MNT_AMOUNT: '1.00', + MNT_CURRENCY_CODE: 'RUB', + MNT_TEST_MODE: '0', + MNT_SIGNATURE: 'ebf8d4b9fa10301b858cc314b356cc41', + 'paymentSystem.unitId': '822360', + MNT_CORRACCOUNT: '15598507', + qiwiphone: '9035419441' } + +console.log(+checkSignature(body)); +*/ diff --git a/hmvc/payments/payanyway/renderForm.js b/hmvc/payments/payanyway/renderForm.js index 0f1ffa6d9..922e3a11c 100644 --- a/hmvc/payments/payanyway/renderForm.js +++ b/hmvc/payments/payanyway/renderForm.js @@ -8,7 +8,8 @@ module.exports = function* (transaction) { amount: transaction.amount, number: transaction.number, currency: config.payments.currency, - id: config.payments.modules.payanyway.id + id: config.payments.modules.payanyway.id, + limitIds: process.env.NODE_ENV == 'development' ? '' : '843858,248362,822360,545234,1028,499669' }); }; diff --git a/hmvc/payments/payanyway/templates/form.jade b/hmvc/payments/payanyway/templates/form.jade index 8bcd1df10..afe7fdb6c 100644 --- a/hmvc/payments/payanyway/templates/form.jade +++ b/hmvc/payments/payanyway/templates/form.jade @@ -4,6 +4,7 @@ form(method="POST" action="https://www.moneta.ru/assistant.htm" accept-charset=" input(type="hidden",name="MNT_TRANSACTION_ID",value=number) input(type="hidden",name="MNT_CURRENCY_CODE",value=currency) input(type="hidden",name="MNT_AMOUNT",value=amount) - input(type="hidden",name="paymentSystem.limitIds",value="843858,248362,822360,545234,1028,499669") + if limitIds + input(type="hidden",name="paymentSystem.limitIds",value=limitIds) input(type="submit",value="Оплатить") diff --git a/hmvc/payments/webmoney/controller/callback.js b/hmvc/payments/webmoney/controller/callback.js index b01b822d7..84d4545fb 100644 --- a/hmvc/payments/webmoney/controller/callback.js +++ b/hmvc/payments/webmoney/controller/callback.js @@ -1,4 +1,4 @@ -const config = require('config'); +const webmoneyConfig = require('config').payments.modules.webmoney; const mongoose = require('mongoose'); const Order = require('../../models/order'); const Transaction = require('../../models/transaction'); @@ -17,7 +17,7 @@ exports.prerequest = function* (next) { if (this.transaction.status == Transaction.STATUS_SUCCESS || this.transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) || - this.request.body.LMI_PAYEE_PURSE != config.webmoney.purse + this.request.body.LMI_PAYEE_PURSE != webmoneyConfig.purse ) { log.debug("no pending transaction " + this.request.body.LMI_PAYMENT_NO); this.throw(404, 'unfinished transaction with given params not found'); @@ -39,7 +39,7 @@ exports.post = function* (next) { yield this.transaction.logRequest('callback', this.request); if (this.transaction.amount != parseFloat(this.request.body.LMI_PAYMENT_AMOUNT) || - this.request.body.LMI_PAYEE_PURSE != config.webmoney.purse) { + this.request.body.LMI_PAYEE_PURSE != webmoneyConfig.purse) { // STRANGE, signature is correct yield this.transaction.persist({ status: Transaction.STATUS_FAIL, @@ -65,7 +65,7 @@ function checkSignature(body) { var signature = md5(body.LMI_PAYEE_PURSE + body.LMI_PAYMENT_AMOUNT + body.LMI_PAYMENT_NO + body.LMI_MODE + body.LMI_SYS_INVS_NO + body.LMI_SYS_TRANS_NO + body.LMI_SYS_TRANS_DATE + - config.webmoney.secretKey + body.LMI_PAYER_PURSE + body.LMI_PAYER_WM).toUpperCase(); + webmoneyConfig.secretKey + body.LMI_PAYER_PURSE + body.LMI_PAYER_WM).toUpperCase(); return signature == body.LMI_HASH; } diff --git a/modules/config/index.js b/modules/config/index.js index ba226e7a1..b13637cab 100644 --- a/modules/config/index.js +++ b/modules/config/index.js @@ -1,5 +1,5 @@ if (!process.env.NODE_ENV) { - throw new Error("NODE_ENV environment variable is required"); + process.env.NODE_ENV = 'development'; } if (process.env.NODE_ENV == 'development' && process.env.DEV_TRACE) { From 99260aa810457f8e76454d2f823970de386d5f54 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 27 Jul 2014 12:40:48 +0400 Subject: [PATCH 112/130] hacking payments & remove readFile jade help in favour of include ./article.html --- hmvc/getpdf/controller/orders.js | 8 ------ hmvc/getpdf/controller/payResult.js | 2 +- hmvc/getpdf/templates/main.jade | 28 +++++++++++++++---- hmvc/markup/templates/pages/article.jade | 4 +-- hmvc/payments/models/transaction.js | 3 +- .../payments/payanyway/controller/callback.js | 8 ++++-- modules/setup/render.js | 21 +++----------- 7 files changed, 37 insertions(+), 37 deletions(-) diff --git a/hmvc/getpdf/controller/orders.js b/hmvc/getpdf/controller/orders.js index 1f8738538..e545bd855 100644 --- a/hmvc/getpdf/controller/orders.js +++ b/hmvc/getpdf/controller/orders.js @@ -6,11 +6,8 @@ var Transaction = payments.Transaction; exports.get = function*(next) { this.set('Cache-Control', 'no-cache, no-store, must-revalidate'); - var lastTransaction; if (this.params.orderNumber) { yield* this.loadOrder(); - - var lastTransaction = yield Transaction.findOne({ order: this.order._id }).sort({created: -1}).exec(); } else { var orderTemplate = yield OrderTemplate.findOne({ @@ -35,11 +32,6 @@ exports.get = function*(next) { this.locals.order = this.order; - if (lastTransaction) { - console.log(lastTransaction.getStatusDescription); - this.locals.message = 'Статус последней оплаты: ' + lastTransaction.getStatusDescription(); - } - this.locals.paymentMethods = require('../paymentMethods').methods; this.render(__dirname, 'main'); diff --git a/hmvc/getpdf/controller/payResult.js b/hmvc/getpdf/controller/payResult.js index 6de493bdc..a1659de4b 100644 --- a/hmvc/getpdf/controller/payResult.js +++ b/hmvc/getpdf/controller/payResult.js @@ -46,7 +46,7 @@ exports.get = function*(next) { this.body = { status: lastTransaction.status, - html: lastTransaction.getStatusDescription() + statusMessage: lastTransaction.statusMessage }; }; diff --git a/hmvc/getpdf/templates/main.jade b/hmvc/getpdf/templates/main.jade index 809b41807..dea391868 100644 --- a/hmvc/getpdf/templates/main.jade +++ b/hmvc/getpdf/templates/main.jade @@ -1,23 +1,41 @@ style. - form.pay-form[is_new="0"] .pay-info { + + /* выбор оплаты - только для новых заказов или для неудавшейся оплаты */ + .pay-chooser { display: none; } + form.pay-form[data-new="1"] .pay-chooser { + display: block; + } + form.pay-form[data-payment-state="fail"] .pay-chooser { + display: block; + } - form.pay-form[is_new="1"] .pay-result { + /* индикатор загрузки - только для существующих заказов без статуса оплаты */ + .pay-loading { display: none; } + form.pay-form[data-new="0"]:not([data-payment-state]) .pay-loading { + display: block; + } + + /* результат оплаты (должен быть над .pay-chooser) - только когда он есть */ + .pay-result { + display: none; + } - form.pay-form[is_new="0"].fail .pay-info { + form.pay-form[data-payment-state] .pay-result { display: block; } + script var csrf = "#{csrf}", orderNumber; + div - p Спасибо, что решили приобрести PDF-учебник! - form.pay-form(is_new=(order.isNew ? "1" : "0")) + form.pay-form(data-new=(order.isNew ? "1" : "0")) input(type="text" name="orderNumber" value=(!order.isNew && order.number) placeholder="order number") input(type="text" name="orderTemplate" value=orderTemplate) diff --git a/hmvc/markup/templates/pages/article.jade b/hmvc/markup/templates/pages/article.jade index 85e529c25..2ce0bd653 100644 --- a/hmvc/markup/templates/pages/article.jade +++ b/hmvc/markup/templates/pages/article.jade @@ -7,8 +7,6 @@ block variables - self.title = 'Учебник — Javascript.ru'; - self.comments = {} // хм? - self.comments.lenght = 5; - - var content = readFile('pages/article.html'); - block content - != content + include ./article.html diff --git a/hmvc/payments/models/transaction.js b/hmvc/payments/models/transaction.js index 308cde10d..98ae34f11 100644 --- a/hmvc/payments/models/transaction.js +++ b/hmvc/payments/models/transaction.js @@ -73,7 +73,7 @@ schema.pre('save', function(next) { next(err); }); }); - +/* schema.methods.getStatusDescription = function() { if (this.status == Transaction.STATUS_SUCCESS) { return 'Оплата прошла успешно.'; @@ -94,6 +94,7 @@ schema.methods.getStatusDescription = function() { throw new Error("неподдерживаемый статус транзакции"); }; +*/ schema.methods.logRequest = function*(event, request) { yield this.log(event, {url: request.originalUrl, body: request.body}); diff --git a/hmvc/payments/payanyway/controller/callback.js b/hmvc/payments/payanyway/controller/callback.js index 3fb76351a..c0d80df54 100644 --- a/hmvc/payments/payanyway/controller/callback.js +++ b/hmvc/payments/payanyway/controller/callback.js @@ -10,6 +10,10 @@ log.debugOn(); exports.post = function* (next) { + checkSignature(this.request.body); + this.body = 'SUCCESS'; + return; + yield* this.loadTransaction('MNT_TRANSACTION_ID', {skipOwnerCheck : true}); @@ -48,10 +52,10 @@ function checkSignature(body) { var signature = body.MNT_ID + body.MNT_TRANSACTION_ID + body.MNT_AMOUNT + body.MNT_CURRENCY_CODE + (body.MNT_SUBSCRIBER_ID || '') + (+body.MNT_TEST_MODE ? '1' : '0') + payanywayConfig.secret; -// console.log(signature); + console.log(signature); signature = md5(signature); -// console.log(signature); + console.log(signature); return signature == body.MNT_SIGNATURE; } diff --git a/modules/setup/render.js b/modules/setup/render.js index a5c5817e9..c6373789a 100644 --- a/modules/setup/render.js +++ b/modules/setup/render.js @@ -69,7 +69,9 @@ module.exports = function render(app) { if (!this.filename) { throw new Error('the "filename" option is required to use "' + purpose + '" with "relative" paths'); } - templatePath = path.join(path.dirname(this.filename), templatePath) + '.jade'; + + // files like article.html are included in a special way (Filter) + templatePath = path.join(path.dirname(this.filename), templatePath) + (path.extname(templatePath) ? '' : '.jade'); //console.log("Resolve to ", path); return templatePath; } @@ -80,10 +82,7 @@ module.exports = function render(app) { var loc = Object.create(this.locals); var parseLocals = { - parser: JadeParser, - readFile: function(file) { - return readFile(templateDir, file); - } + parser: JadeParser }; _.assign(loc, parseLocals, locals); @@ -102,18 +101,6 @@ module.exports = function render(app) { }; - -function readFile(templateDir, file) { - if (file[0] == '.') { - throw new Error("readFile file must not start with . : bad file " + file); - } - var path = resolvePathUp(templateDir, file); - if (!path) { - throw new Error("Not found " + file + " (from dir " + templateDir + ")"); - } - return fs.readFileSync(path); -} - function resolvePathUp(templateDir, templateName) { templateDir = path.resolve(templateDir); From 2d6da51c68b4d2e1f7294898937da3453fcb708f Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 27 Jul 2014 16:05:07 +0400 Subject: [PATCH 113/130] pause tutorial pdf, it's obvous now, move to more complex stuff --- hmvc/getpdf/controller/payResult.js | 47 +++++++-- hmvc/getpdf/templates/main.jade | 142 +++++++++++++++++++++++----- 2 files changed, 156 insertions(+), 33 deletions(-) diff --git a/hmvc/getpdf/controller/payResult.js b/hmvc/getpdf/controller/payResult.js index a1659de4b..dc0b8d50a 100644 --- a/hmvc/getpdf/controller/payResult.js +++ b/hmvc/getpdf/controller/payResult.js @@ -2,7 +2,15 @@ const payments = require('payments'); var Order = payments.Order; var OrderTemplate = payments.OrderTemplate; var Transaction = payments.Transaction; +const escapeHtml = require('escape-html'); +/** + * 3 kinds of response + * 1) { status, successHtml } - if order success + * 2) { status, statusMessage(optional) } - if another status + * 3) "" - empty string if no information + * @param next + */ exports.get = function*(next) { this.set('Cache-Control', 'no-cache, no-store, must-revalidate'); @@ -20,33 +28,52 @@ exports.get = function*(next) { var lastTransaction = yield Transaction.findOne({ order: this.order._id }).sort({created: -1}).exec(); - // no payment at all? strange + // no payment at all?!? strange if (!lastTransaction) { this.body = { status: Transaction.STATUS_FAIL, - html: 'Оплаты не было.' + html: 'Оплаты не было.' }; return; } - // last transaction was successful, but the order is not + // the order is not yet successful, but the last transaction is not, + // that's possible if order.onSuccess hook has not yet finished // let's wait a little bit if (lastTransaction.status == Transaction.STATUS_SUCCESS) { this.body = ''; - this.status = 204; // no content return; } - // transaction status unknown -> waiting + // transaction status unknown + // -> it means we're awaiting a response from the payment system if (!lastTransaction.status) { this.body = ''; - this.status = 204; // no content return; } - this.body = { - status: lastTransaction.status, - statusMessage: lastTransaction.statusMessage - }; + if (lastTransaction.status == Transaction.STATUS_FAIL) { + this.body = { + status: lastTransaction.status, + html: 'Оплата не прошла.' + }; + + if (lastTransaction.statusMessage) { + this.body.html += '

        ' + escapeHtml(lastTransaction.statusMessage) + '
        '; + } + return; + } + + + if (lastTransaction.status == Transaction.STATUS_PENDING) { + this.body = { + status: lastTransaction.status, + html: 'Оплата ожидается.' + }; + + if (lastTransaction.statusMessage) { + this.body.html += '
        ' + escapeHtml(lastTransaction.statusMessage) + '
        '; + } + } }; diff --git a/hmvc/getpdf/templates/main.jade b/hmvc/getpdf/templates/main.jade index dea391868..f82316cc2 100644 --- a/hmvc/getpdf/templates/main.jade +++ b/hmvc/getpdf/templates/main.jade @@ -1,13 +1,16 @@ +p. + TODO: доделать форму оплаты, как частный случай более сложных курсов + style. /* выбор оплаты - только для новых заказов или для неудавшейся оплаты */ .pay-chooser { display: none; } - form.pay-form[data-new="1"] .pay-chooser { + form.order-form[data-new="1"] .pay-chooser { display: block; } - form.pay-form[data-payment-state="fail"] .pay-chooser { + form.order-form[data-payment-state="fail"] .pay-chooser { display: block; } @@ -16,47 +19,57 @@ style. display: none; } - form.pay-form[data-new="0"]:not([data-payment-state]) .pay-loading { + form.order-form[data-new="0"]:not([data-payment-state]) .pay-loading { display: block; } - /* результат оплаты (должен быть над .pay-chooser) - только когда он есть */ - .pay-result { + /* информация о проблеме с оплатой оплаты (над .pay-chooser) - только когда она есть */ + .pay-fail { display: none; } - form.pay-form[data-payment-state] .pay-result { + form.order-form[data-payment-state="fail"] .pay-fail { display: block; } + /* результат заказа оплаченного */ + .order-result { + display: none; + } + form.order-form[data-payment-state="success"] .order-result, + form.order-form[data-payment-state="pending"] .order-result { + display: block; + } -script var csrf = "#{csrf}", orderNumber; +script var csrf = "#{csrf}"; +script var orderNumber = #{order.isNew ? order.number : 'null'}; +script var orderTemplate = "#{orderTemplate}"; + // TODO div - form.pay-form(data-new=(order.isNew ? "1" : "0")) - input(type="text" name="orderNumber" value=(!order.isNew && order.number) placeholder="order number") - input(type="text" name="orderTemplate" value=orderTemplate) + form.order-form(data-new=(order.isNew ? "1" : "0")) fieldset legend Описание книги и стоимость h2= order.title div= order.description - div #{order.amount} р. + div #{order.amount}р. fieldset - legend Укажите свой email (если не авторизован) input(name="email" value=order.email placeholder="E-mail" disabled=!order.isNew) + fieldset legend Оплата или результат оплаты - div.pay-result Загружаем информацию... + .pay-loading Загружаем информацию об оплате... + + .pay-fail - //- показываем выбор оплаты только если заказ новый, иначе пока прячем и ждём, нужно ли - div.pay-info + .pay-chooser p Выберите способ оплаты: select(name="paymentMethod") each paymentMethod in paymentMethods @@ -64,20 +77,89 @@ div input(type="submit" value="Оплатить") + .pay-result + legend Результат заказа при успешной оплате + + .content + script(src="http://code.jquery.com/jquery-2.1.1.js") script. - var payForm = $('.pay-form'); + var orderForm = $('.order-form'); var payResult = $('.pay-result'); - if (payResult.length) { + + if (orderForm.data('new') == 0) { var requestPayResultStart = new Date(); requestPayResult(); } - payForm.on('submit', onPayFormSubmit); - function requestPayResult() { - if (new Date() - requestPayResultStart > 120e3) { // 2 mins - payResult.html("Таймаут ответа от платёжной системы. Попробуйте обновить эту страницу позже или обратиться в поддержку."); + + orderForm.on('submit', onPayFormSubmit); + + function requestPayResult() { + if (new Date() - requestPayResultStart > 120e3) { // 2 mins + orderForm.attr('data-payment-state', 'timeout'); + payResult.html("Таймаут ответа от платёжной системы. Попробуйте обновить эту страницу позже или обратиться в поддержку."); + return; + } + + $.ajax({ + method: 'GET', + url: '/getpdf/pay-result/' + orderForm[0].elements.orderNumber.value, + data: { + orderNumber: orderForm[0].elements.orderNumber.value + } + }) + .done(function(result) { + if (!result) { + setTimeout(requestPayResult, 1000); + return; + } + showPayResult(result); + }) + .fail(function(err) { + setTimeout(requestPayResult, 1000); + }); + } + + function showPayResult(result) { + orderForm.attr('data-payment-state', result.status); + switch(result.status) { + case 'success': + orderForm.find('.order-success').html(result.successHtml); + break; + case 'pending': + payResult.html('Оплата ожидается, о результате мы напишем по email'); + break; + case 'fail': + $('.pay-fail').html(result.html); + break; + default: + throw new Error("Unknown payment status: " + result.status); + } + } + + function onPayFormSubmit(e) { + e.preventDefault(); + $.ajax({ + method: 'POST', + url: '/getpdf/checkout', + data: { + _csrf: csrf, + orderNumber: this.elements.orderNumber.value, + orderTemplate: this.elements.orderTemplate.value, + email: this.elements.email.value, + paymentMethod: this.elements.paymentMethod.value + } + }) + .fail(function(err) { + alert("Ошибка на сервере"); + }) + .done(function(htmlForm) { + $(htmlForm).submit(); + }); + } + "mailto:help@javascript.ru\">поддержку."); return; } @@ -101,8 +183,22 @@ script. } function showPayResult(result) { - payForm.addClass(result.status); - payResult.html(result.html); + payForm.attr('data-payment-state', result.status); + switch(result.status) { + case 'success': + payForm.find('.order-success').html(result.successHtml); + break; + case 'pending': + payResult.html('Оплата ожидается, о результате мы напишем по email'); + break; + case 'fail': + var message = 'Оплата не прошла'; + if (result.statusMessage) message += '
        Ошибка: ' + result.statusMessage + '
        '; + payResult.html(message); + break; + default: + throw new Error("Unknown payment status: " + result.status); + } } function onPayFormSubmit(e) { From d362f3a0b56ef0927c24002f6ed7e862a0b8d443 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 27 Jul 2014 19:02:21 +0400 Subject: [PATCH 114/130] browserify && no big errors on static 404 && start passport --- app/js/hi.js | 3 ++ app/js/index.js | 5 --- app/js/main.js | 6 ++++ gulpfile.js | 19 ++++++---- hmvc/activities/Readme.md | 1 + hmvc/auth/templates/login-register.jade | 15 ++++++++ hmvc/frontpage/controller/frontpage.js | 2 +- hmvc/frontpage/templates/index.jade | 1 + hmvc/getpdf/templates/main.jade | 2 ++ hmvc/payments/bank-simple/index.js | 2 ++ modules/app.js | 3 ++ modules/setup/passport.js | 11 ++++++ modules/setup/render.js | 3 ++ modules/setup/static.js | 30 ++++++++++++++-- package.json | 7 +++- tasks/browserify.js | 47 +++++++++++++++---------- templates/blocks/head.jade | 3 +- templates/error.jade | 2 +- templates/index.jade | 9 ----- templates/layout.jade | 7 ---- 20 files changed, 125 insertions(+), 53 deletions(-) create mode 100644 app/js/hi.js delete mode 100644 app/js/index.js create mode 100644 app/js/main.js create mode 100644 hmvc/activities/Readme.md create mode 100644 hmvc/auth/templates/login-register.jade create mode 100644 modules/setup/passport.js delete mode 100644 templates/index.jade delete mode 100644 templates/layout.jade diff --git a/app/js/hi.js b/app/js/hi.js new file mode 100644 index 000000000..55d6f8d2c --- /dev/null +++ b/app/js/hi.js @@ -0,0 +1,3 @@ +module.exports = function() { + alert('hi'); +}; diff --git a/app/js/index.js b/app/js/index.js deleted file mode 100644 index 8c44e7322..000000000 --- a/app/js/index.js +++ /dev/null @@ -1,5 +0,0 @@ - -alert(2); - - -]]]]]]] diff --git a/app/js/main.js b/app/js/main.js new file mode 100644 index 000000000..3e4355ab4 --- /dev/null +++ b/app/js/main.js @@ -0,0 +1,6 @@ + +var hi = require('./hi'); + +exports.sayHi = hi; + + diff --git a/gulpfile.js b/gulpfile.js index d0f6c3f87..f42417b5b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -8,7 +8,6 @@ const gp = require('gulp-load-plugins')(); const path = require('path'); const fs = require('fs'); const assert = require('assert'); -//const browserifyTask = require('tasks/browserify'); const serverSources = [ 'config/**/*.js', 'hmvc/**/*.js', 'modules/**/*.js', 'renderer/**/*.js', 'routes/**/*.js', @@ -43,13 +42,19 @@ gulp.task('loaddb', function(callback) { }); +gulp.task('watchify', function(neverCalled) { + + const browserify = require('./tasks/browserify'); + + browserify({ + src: './app/js/main.js', + dst: './www/js', + watch: true + }); + +}); + gulp.task('watch', ['stylus'], function(neverCalled) { - /* - browserifyTask({ - src: 'app/js/index.js', - dst: 'www/js' - })(); - */ const fse = require('fs-extra'); fse.removeSync(['www/fonts']); diff --git a/hmvc/activities/Readme.md b/hmvc/activities/Readme.md new file mode 100644 index 000000000..d1330f8ce --- /dev/null +++ b/hmvc/activities/Readme.md @@ -0,0 +1 @@ +Курсы. TODO. diff --git a/hmvc/auth/templates/login-register.jade b/hmvc/auth/templates/login-register.jade new file mode 100644 index 000000000..8c33f0b80 --- /dev/null +++ b/hmvc/auth/templates/login-register.jade @@ -0,0 +1,15 @@ +form.login-form + h2 Вход в систему + + div + label Email + input(type="email") + + div + label Пароль + input(type="password") + + div + input(type="submit" value="Войти") + + diff --git a/hmvc/frontpage/controller/frontpage.js b/hmvc/frontpage/controller/frontpage.js index da93770c8..f6f0296b6 100644 --- a/hmvc/frontpage/controller/frontpage.js +++ b/hmvc/frontpage/controller/frontpage.js @@ -1,6 +1,6 @@ exports.get = function *get (next) { - this.render('index', { + this.render(__dirname, 'index', { title: 'Hello, world' }); }; diff --git a/hmvc/frontpage/templates/index.jade b/hmvc/frontpage/templates/index.jade index d1b0de973..382d24d71 100644 --- a/hmvc/frontpage/templates/index.jade +++ b/hmvc/frontpage/templates/index.jade @@ -1,3 +1,4 @@ +extends layouts/base block content h1= title diff --git a/hmvc/getpdf/templates/main.jade b/hmvc/getpdf/templates/main.jade index f82316cc2..bc9e33ea0 100644 --- a/hmvc/getpdf/templates/main.jade +++ b/hmvc/getpdf/templates/main.jade @@ -1,5 +1,7 @@ p. TODO: доделать форму оплаты, как частный случай более сложных курсов + TODO: при выборе способа оплаты "позже" не надо перезагружать страницу, нужно менять состояние формы на ней + style. diff --git a/hmvc/payments/bank-simple/index.js b/hmvc/payments/bank-simple/index.js index ca311839c..ad9f729eb 100644 --- a/hmvc/payments/bank-simple/index.js +++ b/hmvc/payments/bank-simple/index.js @@ -1,2 +1,4 @@ +// TODO + exports.renderForm = require('./renderForm'); diff --git a/modules/app.js b/modules/app.js index 23c25ef61..f2df15a8d 100644 --- a/modules/app.js +++ b/modules/app.js @@ -48,6 +48,9 @@ if (process.env.NODE_ENV == 'development') { } requireSetup('setup/session'); + +requireSetup('setup/passport'); + requireSetup('setup/csrf'); requireSetup('setup/payments'); diff --git a/modules/setup/passport.js b/modules/setup/passport.js new file mode 100644 index 000000000..994f75b71 --- /dev/null +++ b/modules/setup/passport.js @@ -0,0 +1,11 @@ +const mongoose = require('mongoose'); +const passport = require('koa-passport') +const config = require('config'); + + +module.exports = function(app) { + + app.use(passport.initialize()); + app.use(passport.session()); + +}; diff --git a/modules/setup/render.js b/modules/setup/render.js index c6373789a..076e22829 100644 --- a/modules/setup/render.js +++ b/modules/setup/render.js @@ -76,6 +76,8 @@ module.exports = function render(app) { return templatePath; } + log.debug("resolvePathUp " + templateDir + " " + templatePath); + return resolvePathUp(templateDir, templatePath + '.jade'); }; @@ -92,6 +94,7 @@ module.exports = function render(app) { if (!file) { throw new Error("Template file not found: " + templatePath + " (in dir " + templateDir + ") "); } + log.debug("render file " + file); this.body = jade.renderFile(file, loc); }; diff --git a/modules/setup/static.js b/modules/setup/static.js index 8f6548f06..35260a183 100644 --- a/modules/setup/static.js +++ b/modules/setup/static.js @@ -1,9 +1,35 @@ 'use strict'; -const serve = require('koa-static'); + const favicon = require('koa-favicon'); +const send = require('koa-send'); +const path = require('path'); + +/** + * koa-static is a thin wrapper around koa-send + * Here we statically send all paths with extension. + * + * ...And if we fail, there is no big-nice-error-screen which is slow to render + * just a simple default error message + * @param app + */ module.exports = function(app) { - app.use(serve('www')); + app.use(favicon()); + app.use(function*(next) { + var opts = { + root: 'www', + index: 'index.html' + }; + + if (this.idempotent && path.extname(this.path) !== '') { + yield send(this, this.path, opts); + return; + } + + yield* next; + + }); + }; diff --git a/package.json b/package.json index 360faa494..16c392d47 100755 --- a/package.json +++ b/package.json @@ -54,8 +54,10 @@ "koa-generic-session": "*", "koa-logger": "*", "koa-mount": "^1.3.0", + "koa-passport": "^0.5.1", "koa-request": "^1.0.0", "koa-router": "*", + "koa-send": "^1.2.4", "koa-session-mongoose": "*", "koa-static": "*", "koa-views": "*", @@ -82,6 +84,7 @@ "yargs": "^1.2.6" }, "devDependencies": { + "browserify": "^5.9.1", "clarify": "*", "co-mocha": "*", "gulp": "*", @@ -89,17 +92,19 @@ "gulp-jshint": "*", "gulp-livereload": "^2.1.0", "gulp-mocha": "^0.5.1", + "gulp-sourcemaps": "^1.1.0", "gulp-stylus-sprite": "*", "gulp-tap": "^0.1.1", "javascript-brunch": "*", "lazypipe": "^0.2.1", "mocha": "*", + "node-notifier": "^3.1.1", "should": "*", "sinon": "*", "supertest": "^0.13.0", "supervisor": "*", "trace": "*", - "watchify": "^0.10.2" + "watchify": "^1.0.1" }, "engines": { "node": ">=0.11.13" diff --git a/tasks/browserify.js b/tasks/browserify.js index f2e8b224a..a989725b8 100644 --- a/tasks/browserify.js +++ b/tasks/browserify.js @@ -1,8 +1,10 @@ const gp = require('gulp-load-plugins')(); const gulp = require('gulp'); const source = require('vinyl-source-stream'); -const es = require('event-stream'); +const gutil = require('gulp-util'); const watchify = require('watchify'); +const browserify = require('browserify'); +var Notification = require('node-notifier'); /* var w; @@ -64,26 +66,33 @@ module.exports = function(options) { module.exports = function(options) { - return function() { + var bundler = browserify({ + debug: options.watch, + entries: options.src, + cache: {}, + packageCache: {}, + fullPaths: true + }); - return gulp.src(options.src, {read: false}) - .pipe(gp.plumber({errorHandler: gp.notify.onError("<%= error.message %>")})) - .pipe(gp.tap(function(file) { - - const bundler = watchify({entries: [file.path]}); - - bundler - .on("log", gp.util.log) - .on("update", rebundle); + if (options.watch) { + bundler = watchify(bundler); + } - function rebundle() { - return bundler.bundle({debug: true}) - .pipe(source('base.js')) - .pipe(gulp.dest(options.dst)); - } + bundler.on('update', rebundle); + + function rebundle () { + return bundler.bundle() + // log errors if they happen + .on('error', function(e) { + gutil.log(e.message); + new Notification().notify({ + message: e + }); + }) + .pipe(source('build.js')) + .pipe(gulp.dest(options.dst)); + } - return rebundle(); - })); - }; + return rebundle(); }; diff --git a/templates/blocks/head.jade b/templates/blocks/head.jade index b55677513..0306258b7 100644 --- a/templates/blocks/head.jade +++ b/templates/blocks/head.jade @@ -4,6 +4,7 @@ html title= title link(href='http://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700|Open+Sans+Condensed:700&subset=latin,latin-ext,cyrillic,cyrillic-ext' rel='stylesheet' type='text/css') link(href='/stylesheets/base.css',rel='stylesheet') + script(src='/js/build.js') //- link(href='../app/assets/stylesheets/base.css' rel='stylesheet') //if lte IE 9 - //- link(href='../app/assets/stylesheets/base.ie.css' rel='stylesheet') \ No newline at end of file + //- link(href='../app/assets/stylesheets/base.ie.css' rel='stylesheet') diff --git a/templates/error.jade b/templates/error.jade index 15a2a6b1b..8ef0aa284 100644 --- a/templates/error.jade +++ b/templates/error.jade @@ -1,4 +1,4 @@ -extends layout +extends layouts/base block content h1 HTTP Error (user-land report) diff --git a/templates/index.jade b/templates/index.jade deleted file mode 100644 index 128334f63..000000000 --- a/templates/index.jade +++ /dev/null @@ -1,9 +0,0 @@ -extends layout - -block content - h1= title - p Welcome to #{title} - - - - diff --git a/templates/layout.jade b/templates/layout.jade deleted file mode 100644 index 2f4b875e5..000000000 --- a/templates/layout.jade +++ /dev/null @@ -1,7 +0,0 @@ -doctype html -html - head - title= title - link(rel='stylesheet', href='/stylesheets/base.css') - body - block content From 57277381e874367e1d8955670a3bc28f9ea2ce5a Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sun, 27 Jul 2014 19:38:30 +0400 Subject: [PATCH 115/130] ./gulp dev -> runs server and watch-autobuilding tasks --- app/js/main.js | 2 +- gulpfile.js | 49 ++++++++++++++++++++++++++++------------- modules/app.js | 5 +++-- modules/setup/static.js | 1 + package.json | 1 + 5 files changed, 40 insertions(+), 18 deletions(-) diff --git a/app/js/main.js b/app/js/main.js index 3e4355ab4..ebd863d32 100644 --- a/app/js/main.js +++ b/app/js/main.js @@ -3,4 +3,4 @@ var hi = require('./hi'); exports.sayHi = hi; - +hi(); diff --git a/gulpfile.js b/gulpfile.js index f42417b5b..e8c542389 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -9,9 +9,9 @@ const path = require('path'); const fs = require('fs'); const assert = require('assert'); + const serverSources = [ - 'config/**/*.js', 'hmvc/**/*.js', 'modules/**/*.js', 'renderer/**/*.js', 'routes/**/*.js', - 'setup/**/*.js', 'tasks/**/*.js', '*.js' + 'hmvc/**/*.js', 'modules/**/*.js', 'tasks/**/*.js', '*.js' ]; gulp.task('lint', function() { @@ -41,25 +41,27 @@ gulp.task('loaddb', function(callback) { task(dbPath)(callback); }); +gulp.task("watch:server", function() { -gulp.task('watchify', function(neverCalled) { + gp.supervisor("bin/www", { + args: [], + watch: ['hmvc', 'modules'], + pollInterval: 100, + extensions: [ "js" ], + debug: true, + harmony: true + }); +}); - const browserify = require('./tasks/browserify'); - browserify({ - src: './app/js/main.js', - dst: './www/js', - watch: true - }); +gulp.task("watch:app:resources", ['stylus'], function() { -}); -gulp.task('watch', ['stylus'], function(neverCalled) { const fse = require('fs-extra'); - fse.removeSync(['www/fonts']); - fse.removeSync(['www/img']); - fse.removeSync(['www/js']); + fse.removeSync('www/fonts'); + fse.removeSync('www/img'); + fse.removeSync('www/js'); fse.mkdirsSync('www/fonts'); fse.mkdirsSync('www/img'); @@ -72,9 +74,27 @@ gulp.task('watch', ['stylus'], function(neverCalled) { gulp.watch("app/**/*.sprites/**", ['sprite']); gulp.watch("app/**/*.styl", ['stylus']); +}); + + +gulp.task("watch:app:browserify", function() { + + const browserify = require('./tasks/browserify'); + + browserify({ + src: './app/js/main.js', + dst: './www/js', + watch: true + }); + +}); + +gulp.task("watch:link-modules", function() { gulp.watch(serverSources, ['link-modules']); }); +gulp.task('dev', ['watch:server', 'watch:app:resources', 'watch:app:browserify']); + // Show errors if encountered gulp.task('stylus', ['clean-compiled-css', 'sprite'], function() { return gulp.src('./app/stylesheets/base.styl') @@ -113,7 +133,6 @@ gulp.task('link-modules', function() { }); - gulp.task('sprite', function() { var options = { spritesSearchFsRoot: 'app', diff --git a/modules/app.js b/modules/app.js index f2df15a8d..1d7106eab 100644 --- a/modules/app.js +++ b/modules/app.js @@ -23,11 +23,12 @@ function requireSetup(path) { require(path)(app); } -// usually nginx will handle this +// usually nginx will handle this before node +// that's why we put it at the top requireSetup('setup/static'); // this middleware adds this.render method -// it is *before error*, because errors need this.render +// it is *before errorHandler*, because errors need this.render requireSetup('setup/render'); // errors wrap everything diff --git a/modules/setup/static.js b/modules/setup/static.js index 35260a183..d69adc7fe 100644 --- a/modules/setup/static.js +++ b/modules/setup/static.js @@ -17,6 +17,7 @@ const path = require('path'); module.exports = function(app) { app.use(favicon()); + app.use(function*(next) { var opts = { root: 'www', diff --git a/package.json b/package.json index 16c392d47..b53f9cfa1 100755 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "gulp-mocha": "^0.5.1", "gulp-sourcemaps": "^1.1.0", "gulp-stylus-sprite": "*", + "gulp-supervisor": "^0.1.2", "gulp-tap": "^0.1.1", "javascript-brunch": "*", "lazypipe": "^0.2.1", From 6d342c00c4f6a470e7a6c9791b631a28b2a661d1 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Mon, 28 Jul 2014 09:55:44 +0400 Subject: [PATCH 116/130] hacking browserify 3 builds --- app/.jshintrc | 1 + app/js/head.js | 6 + app/js/hi.js | 2 +- app/js/main.js | 4 +- app/js/prism-clike.js | 26 --- app/js/prism-core.js | 373 ------------------------------------- app/js/prism-css.js | 31 --- app/js/prism-javascript.js | 26 --- app/js/prism-markup.js | 41 ---- app/js/prism.js | 128 +++++++++++++ app/js/vendor.js | 1 + gulpfile.js | 23 +-- package.json | 3 + tasks/browserify.js | 123 +++++------- 14 files changed, 201 insertions(+), 587 deletions(-) create mode 100644 app/js/head.js delete mode 100644 app/js/prism-clike.js delete mode 100644 app/js/prism-core.js delete mode 100644 app/js/prism-css.js delete mode 100644 app/js/prism-javascript.js delete mode 100644 app/js/prism-markup.js create mode 100644 app/js/prism.js create mode 100644 app/js/vendor.js diff --git a/app/.jshintrc b/app/.jshintrc index b3a2dbc1f..4b274c1ee 100644 --- a/app/.jshintrc +++ b/app/.jshintrc @@ -3,6 +3,7 @@ "latedef": "nofunc", "browser": true, "node": true, // for browserify require etc + "globals": ["$", "Prism"], "indent": 2, "camelcase": true, "newcap": true, diff --git a/app/js/head.js b/app/js/head.js new file mode 100644 index 000000000..efdc3a8d4 --- /dev/null +++ b/app/js/head.js @@ -0,0 +1,6 @@ + +/* File to be loaded at the top of the page */ +/* No jQuery here */ + +require('./hi'); + diff --git a/app/js/hi.js b/app/js/hi.js index 55d6f8d2c..f8a59e599 100644 --- a/app/js/hi.js +++ b/app/js/hi.js @@ -1,3 +1,3 @@ module.exports = function() { - alert('hi'); + alert('TEST'); }; diff --git a/app/js/main.js b/app/js/main.js index ebd863d32..6461764ac 100644 --- a/app/js/main.js +++ b/app/js/main.js @@ -1,6 +1,6 @@ var hi = require('./hi'); -exports.sayHi = hi; - hi(); +//window.$ = require('jquery'); +//require('./prism'); diff --git a/app/js/prism-clike.js b/app/js/prism-clike.js deleted file mode 100644 index b1302b307..000000000 --- a/app/js/prism-clike.js +++ /dev/null @@ -1,26 +0,0 @@ -Prism.languages.clike = { - 'comment': { - pattern: /(^|[^\\])(\/\*[\w\W]*?\*\/|(^|[^:])\/\/.*?(\r?\n|$))/g, - lookbehind: true - }, - 'string': /("|')(\\?.)*?\1/g, - 'class-name': { - pattern: /((?:(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/ig, - lookbehind: true, - inside: { - punctuation: /(\.|\\)/ - } - }, - 'keyword': /\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/g, - 'boolean': /\b(true|false)\b/g, - 'function': { - pattern: /[a-z0-9_]+\(/ig, - inside: { - punctuation: /\(/ - } - }, - 'number': /\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/g, - 'operator': /[-+]{1,2}|!|<=?|>=?|={1,3}|&{1,2}|\|?\||\?|\*|\/|\~|\^|\%/g, - 'ignore': /&(lt|gt|amp);/gi, - 'punctuation': /[{}[\];(),.:]/g -}; diff --git a/app/js/prism-core.js b/app/js/prism-core.js deleted file mode 100644 index 5eeb4cc5b..000000000 --- a/app/js/prism-core.js +++ /dev/null @@ -1,373 +0,0 @@ -var self = (typeof window !== 'undefined') ? window : {}; - -/** - * Prism: Lightweight, robust, elegant syntax highlighting - * MIT license http://www.opensource.org/licenses/mit-license.php/ - * @author Lea Verou http://lea.verou.me - */ - -var Prism = (function(){ - -// Private helper vars -var lang = /\blang(?:uage)?-(?!\*)(\w+)\b/i; - -var _ = self.Prism = { - util: { - encode: function (tokens) { - if (tokens instanceof Token) { - return new Token(tokens.type, _.util.encode(tokens.content)); - } else if (_.util.type(tokens) === 'Array') { - return tokens.map(_.util.encode); - } else { - return tokens.replace(/&/g, '&').replace(/ text.length) { - // Something went terribly wrong, ABORT, ABORT! - break tokenloop; - } - - if (str instanceof Token) { - continue; - } - - pattern.lastIndex = 0; - - var match = pattern.exec(str); - - if (match) { - if(lookbehind) { - lookbehindLength = match[1].length; - } - - var from = match.index - 1 + lookbehindLength, - match = match[0].slice(lookbehindLength), - len = match.length, - to = from + len, - before = str.slice(0, from + 1), - after = str.slice(to + 1); - - var args = [i, 1]; - - if (before) { - args.push(before); - } - - var wrapped = new Token(token, inside? _.tokenize(match, inside) : match); - - args.push(wrapped); - - if (after) { - args.push(after); - } - - Array.prototype.splice.apply(strarr, args); - } - } - } - - return strarr; - }, - - hooks: { - all: {}, - - add: function (name, callback) { - var hooks = _.hooks.all; - - hooks[name] = hooks[name] || []; - - hooks[name].push(callback); - }, - - run: function (name, env) { - var callbacks = _.hooks.all[name]; - - if (!callbacks || !callbacks.length) { - return; - } - - for (var i=0, callback; callback = callbacks[i++];) { - callback(env); - } - } - } -}; - -var Token = _.Token = function(type, content) { - this.type = type; - this.content = content; -}; - -Token.stringify = function(o, language, parent) { - if (typeof o == 'string') { - return o; - } - - if (Object.prototype.toString.call(o) == '[object Array]') { - return o.map(function(element) { - return Token.stringify(element, language, o); - }).join(''); - } - - var env = { - type: o.type, - content: Token.stringify(o.content, language, parent), - tag: 'span', - classes: ['token', o.type], - attributes: {}, - language: language, - parent: parent - }; - - if (env.type == 'comment') { - env.attributes['spellcheck'] = 'true'; - } - - _.hooks.run('wrap', env); - - var attributes = ''; - - for (var name in env.attributes) { - attributes += name + '="' + (env.attributes[name] || '') + '"'; - } - - return '<' + env.tag + ' class="' + env.classes.join(' ') + '" ' + attributes + '>' + env.content + ''; - -}; - -if (!self.document) { - if (!self.addEventListener) { - // in Node.js - return self.Prism; - } - // In worker - self.addEventListener('message', function(evt) { - var message = JSON.parse(evt.data), - lang = message.language, - code = message.code; - - self.postMessage(JSON.stringify(_.tokenize(code, _.languages[lang]))); - self.close(); - }, false); - - return self.Prism; -} - -// Get current script and highlight -var script = document.getElementsByTagName('script'); - -script = script[script.length - 1]; - -if (script) { - _.filename = script.src; - - if (document.addEventListener && !script.hasAttribute('data-manual')) { - document.addEventListener('DOMContentLoaded', _.highlightAll); - } -} - -return self.Prism; - -})(); - -if (typeof module !== 'undefined' && module.exports) { - module.exports = Prism; -} diff --git a/app/js/prism-css.js b/app/js/prism-css.js deleted file mode 100644 index 4766c0edb..000000000 --- a/app/js/prism-css.js +++ /dev/null @@ -1,31 +0,0 @@ -Prism.languages.css = { - 'comment': /\/\*[\w\W]*?\*\//g, - 'atrule': { - pattern: /@[\w-]+?.*?(;|(?=\s*{))/gi, - inside: { - 'punctuation': /[;:]/g - } - }, - 'url': /url\((["']?).*?\1\)/gi, - 'selector': /[^\{\}\s][^\{\};]*(?=\s*\{)/g, - 'property': /(\b|\B)[\w-]+(?=\s*:)/ig, - 'string': /("|')(\\?.)*?\1/g, - 'important': /\B!important\b/gi, - 'punctuation': /[\{\};:]/g, - 'function': /[-a-z0-9]+(?=\()/ig -}; - -if (Prism.languages.markup) { - Prism.languages.insertBefore('markup', 'tag', { - 'style': { - pattern: /[\w\W]*?<\/style>/ig, - inside: { - 'tag': { - pattern: /|<\/style>/ig, - inside: Prism.languages.markup.tag.inside - }, - rest: Prism.languages.css - } - } - }); -} \ No newline at end of file diff --git a/app/js/prism-javascript.js b/app/js/prism-javascript.js deleted file mode 100644 index 7d62b958f..000000000 --- a/app/js/prism-javascript.js +++ /dev/null @@ -1,26 +0,0 @@ -Prism.languages.javascript = Prism.languages.extend('clike', { - 'keyword': /\b(break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|function|get|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|set|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)\b/g, - 'number': /\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?|NaN|-?Infinity)\b/g -}); - -Prism.languages.insertBefore('javascript', 'keyword', { - 'regex': { - pattern: /(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/g, - lookbehind: true - } -}); - -if (Prism.languages.markup) { - Prism.languages.insertBefore('markup', 'tag', { - 'script': { - pattern: /[\w\W]*?<\/script>/ig, - inside: { - 'tag': { - pattern: /|<\/script>/ig, - inside: Prism.languages.markup.tag.inside - }, - rest: Prism.languages.javascript - } - } - }); -} diff --git a/app/js/prism-markup.js b/app/js/prism-markup.js deleted file mode 100644 index e6c49953a..000000000 --- a/app/js/prism-markup.js +++ /dev/null @@ -1,41 +0,0 @@ -Prism.languages.markup = { - 'comment': //g, - 'prolog': /<\?.+?\?>/, - 'doctype': //, - 'cdata': //i, - 'tag': { - pattern: /<\/?[\w:-]+\s*(?:\s+[\w:-]+(?:=(?:("|')(\\?[\w\W])*?\1|[^\s'">=]+))?\s*)*\/?>/gi, - inside: { - 'tag': { - pattern: /^<\/?[\w:-]+/i, - inside: { - 'punctuation': /^<\/?/, - 'namespace': /^[\w-]+?:/ - } - }, - 'attr-value': { - pattern: /=(?:('|")[\w\W]*?(\1)|[^\s>]+)/gi, - inside: { - 'punctuation': /=|>|"/g - } - }, - 'punctuation': /\/?>/g, - 'attr-name': { - pattern: /[\w:-]+/g, - inside: { - 'namespace': /^[\w-]+?:/ - } - } - - } - }, - 'entity': /\&#?[\da-z]{1,8};/gi -}; - -// Plugin to make entity title show the real entity, idea by Roman Komarov -Prism.hooks.add('wrap', function(env) { - - if (env.type === 'entity') { - env.attributes['title'] = env.content.replace(/&/, '&'); - } -}); diff --git a/app/js/prism.js b/app/js/prism.js new file mode 100644 index 000000000..40ee76f66 --- /dev/null +++ b/app/js/prism.js @@ -0,0 +1,128 @@ +require('prismjs/components/prism-core.js'); +require('prismjs/components/prism-markup.js'); +require('prismjs/components/prism-css.js'); +require('prismjs/components/prism-css-extras.js'); +require('prismjs/components/prism-clike.js'); +require('prismjs/components/prism-javascript.js'); +require('prismjs/components/prism-coffeescript.js'); +require('prismjs/components/prism-http.js'); +require('prismjs/components/prism-scss.js'); +require('prismjs/components/prism-sql.js'); +require('prismjs/components/prism-php.js'); +require('prismjs/components/prism-php-extras.js'); +require('prismjs/components/prism-python.js'); +require('prismjs/components/prism-ruby.js'); +require('prismjs/components/prism-java.js'); + +!function () { + document.removeEventListener('DOMContentLoaded', Prism.highlightAll); + + + function addLineNumbers(pre) { + + var linesNum = (1 + pre.innerHTML.split('\n').length); + var lineNumbersWrapper; + + var lines = new Array(linesNum); + lines = lines.join(''); + + lineNumbersWrapper = document.createElement('span'); + lineNumbersWrapper.className = 'line-numbers-rows'; + lineNumbersWrapper.innerHTML = lines; + + if (pre.hasAttribute('data-start')) { + pre.style.counterReset = 'linenumber ' + (parseInt(pre.getAttribute('data-start'), 10) - 1); + } + + pre.appendChild(lineNumbersWrapper); + } + + + function addBlockHighlight(pre) { + + var lines = $(pre).data('highlightBlock'); + + if (!lines) { + return; + } + + var ranges = lines.replace(/\s+/g, '').split(','); + + /*jshint -W084 */ + for (var i = 0, range; range = ranges[i++];) { + range = range.split('-'); + + var start = +range[0], + end = +range[1] || start; + + + var mask = $('
        ' + + new Array(start + 1).join('\n') + + '
        ' + new Array(end - start + 2).join('\n') + '
        '); + + $(pre).prepend(mask); + } + + } + + function esc(str) { + return str + .replace(/&/g, '&') + .replace(//g, '>'); + } + + function unesc(str) { + return str + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>'); + } + + function addInlineHighlight(pre) { + var ranges = $(pre).data('highlightInline'); + var codeElem = $('code', pre); + + ranges = ranges ? ranges.split(",") : []; + + for (var i = 0; i < ranges.length; i++) { + var piece = ranges[i].split(':'); + var lineNum = +piece[0], strRange = piece[1].split('-'); + var start = +strRange[0], end = +strRange[1]; + var mask = $('
        ' + + new Array(lineNum + 1).join('\n') + + new Array(start + 1).join(' ') + + '' + new Array(end - start + 1).join(' ') + '
        '); + + codeElem.prepend(mask); + } + } + + + $(function() { + + // highlight inline + var codePre = $('pre[class*="language-"]'); + + codePre.each(function () { + this.code = unesc(this.innerHTML); + $(this).wrapInner(""); + + Prism.highlightElement(this.firstChild); + + addLineNumbers(this); + addBlockHighlight(this); + addInlineHighlight(this); + new CodeBox(this); + }); + + + }); + + $(function() { + $('iframe.result__iframe').each(function() { + new IframeBox(this); + }) + }); + +}(); diff --git a/app/js/vendor.js b/app/js/vendor.js new file mode 100644 index 000000000..ab48c02b2 --- /dev/null +++ b/app/js/vendor.js @@ -0,0 +1 @@ +/* @see task for the list of vendor js */ diff --git a/gulpfile.js b/gulpfile.js index e8c542389..abe4bfb90 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -61,15 +61,12 @@ gulp.task("watch:app:resources", ['stylus'], function() { fse.removeSync('www/fonts'); fse.removeSync('www/img'); - fse.removeSync('www/js'); fse.mkdirsSync('www/fonts'); fse.mkdirsSync('www/img'); - fse.mkdirsSync('www/js'); gp.dirSync('app/fonts', 'www/fonts'); gp.dirSync('app/img', 'www/img'); - gp.dirSync('app/js', 'www/js'); gulp.watch("app/**/*.sprites/**", ['sprite']); gulp.watch("app/**/*.styl", ['stylus']); @@ -77,15 +74,19 @@ gulp.task("watch:app:resources", ['stylus'], function() { }); -gulp.task("watch:app:browserify", function() { - const browserify = require('./tasks/browserify'); - browserify({ - src: './app/js/main.js', - dst: './www/js', - watch: true - }); +gulp.task("app:browserify:clean", function() { + const fse = require('fs-extra'); + fse.removeSync('www/js'); + fse.mkdirsSync('www/js'); +}); + + +gulp.task("watch:app:browserify", ['app:browserify:clean'], function(neverCalled) { + + const browserify = require('./tasks/browserify'); + browserify(); }); @@ -93,7 +94,7 @@ gulp.task("watch:link-modules", function() { gulp.watch(serverSources, ['link-modules']); }); -gulp.task('dev', ['watch:server', 'watch:app:resources', 'watch:app:browserify']); +gulp.task('dev', ['watch:server', 'watch:app:resources']); // Show errors if encountered gulp.task('stylus', ['clean-compiled-css', 'sprite'], function() { diff --git a/package.json b/package.json index b53f9cfa1..9ee03d6f5 100755 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "co": "*", "escape-html": "^1.0.1", "event-stream": "^3.1.5", + "factor-bundle": "^1.0.0", "fs-extra": "^0.10.0", "glob": "^4.0.4", "gm": "^1.16.0", @@ -73,6 +74,8 @@ "nodemailer-ses-transport": "^0.1.1", "passport": "*", "path-to-regexp": "^0.2.3", + "prism": "0.0.1", + "prismjs": "git://github.com/LeaVerou/prism#gh-pages", "require-tree": "*", "stylus": "*", "svgutils": "^0.7.0", diff --git a/tasks/browserify.js b/tasks/browserify.js index a989725b8..cb80dc967 100644 --- a/tasks/browserify.js +++ b/tasks/browserify.js @@ -5,94 +5,65 @@ const gutil = require('gulp-util'); const watchify = require('watchify'); const browserify = require('browserify'); var Notification = require('node-notifier'); +var assert = require('assert'); +var _ = require('lodash'); +var path = require('path'); -/* -var w; - -function bundler(file) { - if (!w) { - w = watchify({ - entries: [file.path], //file.contents may be used if {buffer: false} is set - extensions:['.jsx'] - }); - w.on('log', $.util.log) - .on('update', function() { - gulp.start('scripts'); - }); - } - - var stream = w.bundle(); - file.contents = stream; - -} - - -module.exports = function(options) { - - const bundlerFactory = options.watch ? require("watchify") : require("browserify"); +function makeBundler(options) { + // dst has same name as (single) src - var bundler; - - gulp.src(options.src, {read: false}) - .pipe(gp.plumber({errorHandler: gp.notify.onError("<%= error.message %>")})) - .pipe(es.map(function(file, callback) { - if (!bundler) { - gp.util.log("Starting browserify"); - bundler = bundlerFactory({entries: [file.path]}) - bundler - .on("log", gp.util.log) - .on("update", function() { - gulp.start("scripts"); - }); + var opts = _.assign({}, options, { + debug: (process.env.NODE_ENV === 'development') + }); - bundler.transform(require("reactify")) - // bundler.add(es6ify.runtime) - var es6ify = require("es6ify") - es6ify.traceurOverrides = {experimental: true} - bundler.transform(es6ify) + var bundler = browserify(opts); + bundler.rebundle = function() { + console.log(path.basename(this._options.dst)); + bundler.bundle() + .pipe(source(path.basename(this._options.dst))) + .pipe(gulp.dest(path.dirname(this._options.dst))); + }; - if (opts.minify) { - bundler.transform(require("uglifyify")) - } - } + // bundler.on('update', bundler.rebundle); - var stream = bundler.bundle() - file.contents = stream - })) - .pipe(gulp.dest("./dist/")) + return bundler; } -*/ +module.exports = function() { +/* + var externals = ['jquery']; -module.exports = function(options) { - - var bundler = browserify({ - debug: options.watch, - entries: options.src, - cache: {}, - packageCache: {}, - fullPaths: true + var bundler = makeBundler({ + src: './app/js/vendor.js', + dst: './www/js/vendor.js', + require: externals }); - if (options.watch) { - bundler = watchify(bundler); - } + bundler = watchify(bundler); + bundler.rebundle(); - bundler.on('update', rebundle); + var bundler = makeBundler({ + src: './app/js/head.js', + dst: './www/js/head.js' + }); + + bundler = watchify(bundler); + bundler.rebundle(); - function rebundle () { - return bundler.bundle() - // log errors if they happen - .on('error', function(e) { - gutil.log(e.message); - new Notification().notify({ - message: e - }); - }) - .pipe(source('build.js')) - .pipe(gulp.dest(options.dst)); - } + */ + var bundler = makeBundler({ + src: './app/js/main.js', + dst: './www/js/main.js' + }); + bundler.rebundle(); - return rebundle(); + console.log("TEST"); +/* + bundler.on('prebundle', function(bundle) { + for (var i = 0; i < externals.length; i++) { + var external = externals[i]; + this.external(external); + } + });*/ }; From bd77f471d19492590353292edae3fbd71cb67f5f Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Mon, 28 Jul 2014 09:57:32 +0400 Subject: [PATCH 117/130] fix config --- modules/config/secret.template.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/config/secret.template.js b/modules/config/secret.template.js index d798cc01d..9674cb1db 100644 --- a/modules/config/secret.template.js +++ b/modules/config/secret.template.js @@ -2,3 +2,7 @@ // should not be in repo exports.SESSION_KEY = "KillerIsJim"; + +exports.payments = { + modules: {} +}; \ No newline at end of file From 47ab6102f2d7251573da6f7a618ebd4174818ce2 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Mon, 28 Jul 2014 12:09:28 +0400 Subject: [PATCH 118/130] fix --- gulpfile.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index d0f6c3f87..4e382dab9 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -52,9 +52,9 @@ gulp.task('watch', ['stylus'], function(neverCalled) { */ const fse = require('fs-extra'); - fse.removeSync(['www/fonts']); - fse.removeSync(['www/img']); - fse.removeSync(['www/js']); + fse.removeSync('www/fonts'); + fse.removeSync('www/img'); + fse.removeSync('www/js'); fse.mkdirsSync('www/fonts'); fse.mkdirsSync('www/img'); From 642883edbc6a9fda49034e0cdd8e33a4963c1e66 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Mon, 28 Jul 2014 14:00:29 +0400 Subject: [PATCH 119/130] refactor gulp & browserify --- app/js/base.js | 262 +++++++++++++++++++++---------------- app/js/head.js | 10 +- app/js/main.js | 1 + app/js/polyfill/index.js | 2 + app/js/polyfill/matches.js | 3 + app/js/polyfill/on.js | 40 ++++++ app/js/vendor.js | 1 - gulp | 2 +- gulp-debug | 2 + gulpfile.js | 134 ++++++------------- package.json | 3 +- tasks/browserify.js | 86 +++++++----- tasks/browserifyClean.js | 11 ++ tasks/compileCss.js | 12 ++ tasks/compileCssOnce.js | 19 +++ tasks/linkModules.js | 8 +- tasks/lint.js | 12 ++ tasks/lintOnce.js | 8 ++ tasks/loadDb.js | 12 +- tasks/sprite.js | 12 ++ tasks/spriteOnce.js | 9 ++ tasks/supervisor.js | 16 +++ tasks/syncResources.js | 23 ++++ templates/blocks/head.jade | 2 +- 24 files changed, 438 insertions(+), 252 deletions(-) create mode 100644 app/js/polyfill/index.js create mode 100644 app/js/polyfill/matches.js create mode 100644 app/js/polyfill/on.js delete mode 100644 app/js/vendor.js create mode 100644 gulp-debug create mode 100644 tasks/browserifyClean.js create mode 100644 tasks/compileCss.js create mode 100644 tasks/compileCssOnce.js create mode 100644 tasks/lint.js create mode 100644 tasks/lintOnce.js create mode 100644 tasks/sprite.js create mode 100644 tasks/spriteOnce.js create mode 100644 tasks/supervisor.js create mode 100644 tasks/syncResources.js diff --git a/app/js/base.js b/app/js/base.js index 9c7e23159..c9dcfc3dc 100644 --- a/app/js/base.js +++ b/app/js/base.js @@ -26,11 +26,14 @@ function getNumEnding(iNumber, aEndings) return sEnding; } +// ====================== + function getRandomIdentifier(prefix) { - if (typeof prefix == 'undefined') prefix = ''; - return prefix + Math.random().toString(36).substr(2); + return (prefix || '') + Math.random().toString(36).substr(2); } +// ====================== + (function ($) { $(document).ready(function () { //////////////////////// @@ -85,7 +88,7 @@ function getRandomIdentifier(prefix) { closeAllDropdowns(); } } - + function initDropdowns() { $('.dropdown:not(.dropdown_open_hover):not(.dropdown_inited) .dropdown__toggle').click(function () { var root = $(this).parents('.dropdown'); @@ -118,7 +121,7 @@ function getRandomIdentifier(prefix) { return false; }); } - + initDropdowns(); //////////////////////// @@ -134,6 +137,8 @@ function getRandomIdentifier(prefix) { return false; }); + + // открытие окна с соц сетью при клике $('.social__soc').click(function() { var winHeight = 400, winWidth = 500; var params = 'scrollbars=no,status=no,location=no,toolbar=no,' @@ -142,8 +147,9 @@ function getRandomIdentifier(prefix) { + ',top=' + (screen.availHeight / 2 - winHeight / 2); window.open($(this).attr('href'), 'share', params) return false; - }) + }); + // sticky-соц плашка /////////////////////// function updateSharing() { if ($('.social.aside.unfixed').offset().top - $(window).scrollTop() < 20) { @@ -172,6 +178,8 @@ function getRandomIdentifier(prefix) { asideSocial.data('handler', true); // prevent from setting handler multiple times } + + // навигация по текущему уроку, sticky /////////////////////// function fixNavigation() { var sidebar = $('.sidebar'); @@ -221,15 +229,15 @@ function getRandomIdentifier(prefix) { $(window).off('.sidebar'); // FIXME: why two times? } + // количество комментариев текущее /////////////////////// (function () { var s = document.createElement('script'); - s.async = true; - s.type = 'text/javascript'; s.src = 'http://learnjavascriptru.disqus.com/count.js'; (document.getElementsByTagName('HEAD')[0] || document.getElementsByTagName('BODY')[0]).appendChild(s); }()); + // навигация ctrl <- -> /////////////////////// $(document).keydown(function (e) { var back = $('.prev-next .prev-next__prev .prev-next__link').eq(0).attr('href'); @@ -245,7 +253,9 @@ function getRandomIdentifier(prefix) { } } }) - + + // Для профиля карандашик справа от поля + ////////////////////////////////////////////////////// function startInlineEdit(jQInlineEditable) { jQInlineEditable.addClass('profile__inline-editable_editing'); window.getSelection().removeAllRanges(); @@ -253,7 +263,7 @@ function getRandomIdentifier(prefix) { // if not set at all. So text input can have no type. jQInlineEditable.find('input:not(input[type]), input[type="text"], input[type="email"], select, textarea').first().focus(); } - + function finishInlineEdit(jqInlineEditable) { jqInlineEditable.removeClass('profile__inline-editable_editing'); } @@ -288,7 +298,9 @@ function getRandomIdentifier(prefix) { $('.profile__upic-upload').change(function() { $(this).parents('.profile__upic-change').submit(); }) - + + + // отзывы о курсах, слайдер с бендером //////////////////// will it leak with turbolinks or not? $('.slider').each(function(){ var slider = $(this) @@ -314,22 +326,22 @@ function getRandomIdentifier(prefix) { frame1 = frames.find('.slider__frame:nth-child(1)'); frame2 = frames.find('.slider__frame:nth-child(2)'); frame3 = frames.find('.slider__frame:nth-child(3)'); - + frames.css({ left: '-100%' }); - + currentItem = getCurrentItem(); frame2.append(itemsList.eq(currentItem).clone()); itemsList.eq(currentItem).addClass('slider__item_current'); fixButtons(); - + slider.find('.slider__next').click(function(){ slideTo(currentItem + 1); }) - + slider.find('.slider__prev').click(function(){ slideTo(currentItem - 1); }) - + function slideTo(itemIndex) { var newHeight , animateToPosition @@ -347,10 +359,10 @@ function getRandomIdentifier(prefix) { } else { // itemIndex == currentItem return; } - + tmpFrame.append(itemsList.eq(itemIndex).clone()); itemsList.eq(itemIndex).addClass('slider__item_current'); - + // Two next animations run in parallel but not in sync, // it seems it shouldn't cause any serious problems frames.animate({left: animateToPosition}, animationDuration, function() { @@ -366,7 +378,7 @@ function getRandomIdentifier(prefix) { currentItem = getCurrentItem(); fixButtons(); } - + function getCurrentItem() { var index = itemsList.index(items.find('.slider__item_current')); if (index == -1) { @@ -375,7 +387,7 @@ function getRandomIdentifier(prefix) { return index; } } - + function fixButtons() { slider.find('.slider__prev, .slider__next').show(); if (currentItem == 0) { @@ -386,7 +398,8 @@ function getRandomIdentifier(prefix) { } } }); - + + // универсальное решение для любых табов (навигационных или в блоке кода) // .tabs is a container for all content and controls // There are .tabs__tab elements inside .tabs, // each of them should have .tabs__switch element inside. @@ -409,7 +422,7 @@ function getRandomIdentifier(prefix) { tabs.find('.tabs__switch').appendTo(switchesContainer) switchesList = switchesContainer.find('.tabs__switch-control'); tabsList = tabs.find('.tabs__tab'); - + if (tabs.find('.tabs__tab_current').length) { index = tabsList.index(tabs.find('.tabs__tab_current')); setCurrentTabSwitch(index); @@ -417,14 +430,14 @@ function getRandomIdentifier(prefix) { tabsList.eq(0).addClass('tabs__tab_current'); setCurrentTabSwitch(0); } - + switchesContainer.on('click', '.tabs__switch-control', function() { var clickedIndex = switchesList.index($(this)); tabsList.removeClass('tabs__tab_current'); tabsList.eq(clickedIndex).addClass('tabs__tab_current'); setCurrentTabSwitch(clickedIndex); }) - + function setCurrentTabSwitch(index) { var clickedItem = switchesList.eq(index); tabs.find('.tabs__switch_current').removeClass('tabs__switch_current'); @@ -434,10 +447,11 @@ function getRandomIdentifier(prefix) { clickedItem.parents('.tabs__switch').eq(0).addClass('tabs__switch_current'); } } - + tabs.addClass('tabs_inited'); }); - + + // тоже со страницы с бендером и отзывами // Required elements: // .accordion>.accordion__item>(.accordion__switch+.accordion__content) // .accordion__item.accordion__item_open is open by default @@ -451,7 +465,7 @@ function getRandomIdentifier(prefix) { accordion.find('.accordion__item:gt(0)').find('.accordion__content').css({height: 0}); accordion.find('.accordion__item').eq(0).addClass('accordion__item_open'); } - + accordion.on('click', '.accordion__switch', function() { var currentItem = $(this) , currentContent @@ -470,20 +484,24 @@ function getRandomIdentifier(prefix) { }); } }) - + accordion.addClass('accordion_inited'); }); - + + + + // placeholder? + // если будет нужен - стилизуемый placeholder function initCompactLabels() { $('.text-compact-label').not('.text-compact-label_inited').each(function() { var textCompactLabel = $(this) , label = textCompactLabel.find('.text-compact-label__label') , input = textCompactLabel.find('.text-compact-label__input .text-input__control'); - + input.focus(function() { label.hide(); }) - + input.blur(function() { if (input.val() == '') { label.show(); @@ -491,21 +509,24 @@ function getRandomIdentifier(prefix) { label.hide(); // if it is triggered to update dynamically added input } }) - + textCompactLabel.addClass('text-compact-label_inited'); input.triggerHandler('blur'); }); }; - + initCompactLabels(); - + + // блок [hide] + ///////////////////////////////////////////////// $('.hide-link').click(function(e) { $(this).parent().toggleClass('hide-closed hide-open'); return false; }); - + + // для записи на курсы контрол для выбора количества участников с +- // $('.number-input').on('valuechanged', function(e, newVal, oldVal) {console.log(newVal + ', ' + oldVal)}); - + // move state into an object and pass it to handlers $('.number-input').each(function() { var numberInput = $(this); @@ -514,14 +535,14 @@ function getRandomIdentifier(prefix) { var min = numberInput.attr('data-min') != "" ? +numberInput.attr('data-min') : undefined; var max = numberInput.attr('data-max') != "" ? +numberInput.attr('data-max') : undefined; var step = +numberInput.attr('data-step') || 1; - + fixValue(); - + numberInput.on('click', '.number-input__dec', decValue) .on('click', '.number-input__inc', incValue) .on('keydown', '.number-input__input', processKey) .on('blur', '.number-input__input', fixValue); - + function incValue() { var newValue = (value || 0) + step; if (isNaN(max) || newValue <= max) { @@ -556,7 +577,7 @@ function getRandomIdentifier(prefix) { updateValue(max); return; } - + updateValue(currentValue); } @@ -571,7 +592,9 @@ function getRandomIdentifier(prefix) { } } }); - + + // выбор метода оплаты + /////////////////////////////////////////////////// // pay-method block behaviour (just demo, block not finished yet) $('.pay-method__radio').removeAttr('checked'); $('.pay-method__insert').hide(); @@ -580,12 +603,14 @@ function getRandomIdentifier(prefix) { // root.find('.pay-method__insert').hide(); root.find('.pay-method__insert_bank-bill').show(); }); - + $('.pay-method__insert .form-insert__close').click(function() { $(this).parents('.pay-method__insert').first().hide(); $(this).parents('.pay-method').first().find('.pay-method__radio').removeAttr('checked'); - }) - + }); + + // код для формы курса + // количество мест, email'ы участников... // There can be only one form, but just don't run the code if there is no form at all $('.request-form').each(function() { var participantsAmount = +$('.order-form__control_amount .number-input__input').prop('value'); @@ -600,7 +625,7 @@ function getRandomIdentifier(prefix) { var listTrigger = form.find('.order-form__participants-trigger'); var particicpantsListWrap = form.find('.order-form__participants'); var listVisible = false; // by default is hidden - + $('.order-form__control_amount').on('valuechanged', function(e, newVal, oldVal) { participantsAmount = newVal; if (newVal > oldVal) { @@ -613,10 +638,10 @@ function getRandomIdentifier(prefix) { fixParticipantSwitch(); fixParticipants(); }); - + form.on('input change', '.order-form__email', updateEmails) .on('input change', '.order-form__email', fixParticipantSwitch); - + selfUser.click(function(){ if($(this).prop('checked')) { addOwner(); @@ -627,13 +652,13 @@ function getRandomIdentifier(prefix) { } fixParticipants(); }); - + updatePrice(); - + // sometimes it's cached after page refresh selfUser.prop('checked', true).removeAttr('disabled'); selfUserChecked = selfUser.prop('checked'); - + listTrigger.on('click', function() { if (listVisible) { removeList(); @@ -641,15 +666,15 @@ function getRandomIdentifier(prefix) { addList(); } }); - + $('.order-form__close').on('click', removeList); - + form.attr('novalidate', 'nodalidate'); - + // + form parts // - + form.find('.request-form__step-contact, .request-form__step-payment, .request-form__step-confirm').hide(); - + form.find('#pay-form-contract').prop('checked', false).click(function() { if($(this).prop('checked')) { form.find('.pay-form__contract-info').show(); @@ -658,7 +683,7 @@ function getRandomIdentifier(prefix) { } }); form.find('.pay-form__contract-info').hide(); - + form.find('.order-form__submit').click(function() { if (validateEmails() == false) { return false @@ -683,9 +708,9 @@ function getRandomIdentifier(prefix) { $('.request-form__payment').addClass('receipts__receipt_last').removeClass('receipts__receipt_pending'); $('.request-form__next-confirm').addClass('complex-form__next-item_finished'); }); - + // - form parts // - + function fixParticipants() { if (!selfUserChecked || participantsAmount > 1) { particicpantsListWrap.removeClass('order-form__participants_hidden'); @@ -693,12 +718,12 @@ function getRandomIdentifier(prefix) { particicpantsListWrap.addClass('order-form__participants_hidden'); } } - + function addList() { $('.order-form__participants-addresses') .removeClass('order-form__participants-addresses_hidden') .append('
          '); - + addEmail(participantsAmount); if (selfUserChecked && !userIncluded) { addOwner(); @@ -709,25 +734,25 @@ function getRandomIdentifier(prefix) { } listVisible = true; } - + function removeList() { $('.order-form__participants-list').remove(); $('.order-form__participants-addresses').addClass('order-form__participants-addresses_hidden'); listVisible = false; } - + function addEmail(amount) { amount = amount || 1; // let the function be called without argument var emailsHtml = ''; var startIndex = form.find('.order-form__participant').length + 1; - + for (var i = startIndex; i < startIndex + amount; i++) { emailsHtml += getEmailItem(i); } - + form.find('.order-form__participants-list').append(emailsHtml); initCompactLabels(); - + function getEmailItem(itemNumber) { var emaiString = '
      ' + i + '