├── .babelrc
├── .eslintrc
├── .gitignore
├── .svnignore
├── CODE-OF-CONDUCT.md
├── LICENSE.md
├── README.md
├── circle.yml
├── css
├── cdm-admin.css
└── customize-direct-manipulation.css
├── customize-direct-manipulation.php
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
├── admin.js
├── helpers
│ ├── animate.js
│ ├── api.js
│ ├── click-handler.js
│ ├── customizer.js
│ ├── icon-buttons.js
│ ├── messenger.js
│ ├── options.js
│ ├── record-event.js
│ ├── small-screen-preview.js
│ ├── user-agent.js
│ └── window.js
├── modules
│ ├── edit-post-links.js
│ ├── focus-callout.js
│ ├── focus-listener.js
│ ├── focusable.js
│ ├── footer-focus.js
│ ├── guide-steps.js
│ ├── guide.js
│ ├── header-focus.js
│ ├── menu-focus.js
│ ├── post-focus.js
│ ├── site-logo-focus.js
│ └── widget-focus.js
└── preview.js
└── test
├── icon-buttons-test.js
├── mock-html.js
└── mock-window.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "env", {
5 | "modules": false
6 | }
7 | ]
8 | ],
9 | "env": {
10 | "test": {
11 | "presets": [
12 | [ "env", { "modules": "commonjs" } ]
13 | ]
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "wpcalypso",
4 | "env": {
5 | "browser": true,
6 | "es6": true,
7 | "mocha": true,
8 | "node": true
9 | },
10 | rules: {
11 | "max-len": 0,
12 | "wpcalypso/import-docblock": 0
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | js
3 | .env
4 | npm-debug.log
5 |
--------------------------------------------------------------------------------
/.svnignore:
--------------------------------------------------------------------------------
1 | .*
2 | *.md
3 | *.log
4 | src
5 | test
6 | rollup.config.js
7 | package.json
8 | package-lock.json
9 | circle.yml
10 | node_modules
11 |
--------------------------------------------------------------------------------
/CODE-OF-CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4 |
5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
6 |
7 | Examples of unacceptable behavior by participants include:
8 |
9 | * The use of sexualized language or imagery
10 | * Personal attacks
11 | * Trolling or insulting/derogatory comments
12 | * Public or private harassment
13 | * Publishing other's private information, such as physical or electronic addresses, without explicit permission
14 | * Other unethical or unprofessional conduct
15 |
16 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
17 |
18 | By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
19 |
20 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
21 |
22 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing a project maintainer via [this contact form](https://developer.wordpress.com/contact/?g21-subject=Code%20of%20Conduct), with a subject that includes `Code of Conduct`. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident.
23 |
24 |
25 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.3.0, available at [http://contributor-covenant.org/version/1/3/0/][version]
26 |
27 | [homepage]: http://contributor-covenant.org
28 | [version]: http://contributor-covenant.org/version/1/3/0/
29 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The GNU General Public License, Version 2, June 1991 (GPLv2)
2 | ============================================================
3 |
4 | > Copyright (C) 1989, 1991 Free Software Foundation, Inc.
5 | > 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
6 |
7 | Everyone is permitted to copy and distribute verbatim copies of this license
8 | document, but changing it is not allowed.
9 |
10 |
11 | Preamble
12 | --------
13 |
14 | The licenses for most software are designed to take away your freedom to share
15 | and change it. By contrast, the GNU General Public License is intended to
16 | guarantee your freedom to share and change free software--to make sure the
17 | software is free for all its users. This General Public License applies to most
18 | of the Free Software Foundation's software and to any other program whose
19 | authors commit to using it. (Some other Free Software Foundation software is
20 | covered by the GNU Library General Public License instead.) You can apply it to
21 | your programs, too.
22 |
23 | When we speak of free software, we are referring to freedom, not price. Our
24 | General Public Licenses are designed to make sure that you have the freedom to
25 | distribute copies of free software (and charge for this service if you wish),
26 | that you receive source code or can get it if you want it, that you can change
27 | the software or use pieces of it in new free programs; and that you know you can
28 | do these things.
29 |
30 | To protect your rights, we need to make restrictions that forbid anyone to deny
31 | you these rights or to ask you to surrender the rights. These restrictions
32 | translate to certain responsibilities for you if you distribute copies of the
33 | software, or if you modify it.
34 |
35 | For example, if you distribute copies of such a program, whether gratis or for a
36 | fee, you must give the recipients all the rights that you have. You must make
37 | sure that they, too, receive or can get the source code. And you must show them
38 | these terms so they know their rights.
39 |
40 | We protect your rights with two steps: (1) copyright the software, and (2) offer
41 | you this license which gives you legal permission to copy, distribute and/or
42 | modify the software.
43 |
44 | Also, for each author's protection and ours, we want to make certain that
45 | everyone understands that there is no warranty for this free software. If the
46 | software is modified by someone else and passed on, we want its recipients to
47 | know that what they have is not the original, so that any problems introduced by
48 | others will not reflect on the original authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software patents. We wish
51 | to avoid the danger that redistributors of a free program will individually
52 | obtain patent licenses, in effect making the program proprietary. To prevent
53 | this, we have made it clear that any patent must be licensed for everyone's free
54 | use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and modification
57 | follow.
58 |
59 |
60 | Terms And Conditions For Copying, Distribution And Modification
61 | ---------------------------------------------------------------
62 |
63 | **0.** This License applies to any program or other work which contains a notice
64 | placed by the copyright holder saying it may be distributed under the terms of
65 | this General Public License. The "Program", below, refers to any such program or
66 | work, and a "work based on the Program" means either the Program or any
67 | derivative work under copyright law: that is to say, a work containing the
68 | Program or a portion of it, either verbatim or with modifications and/or
69 | translated into another language. (Hereinafter, translation is included without
70 | limitation in the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not covered by
73 | this License; they are outside its scope. The act of running the Program is not
74 | restricted, and the output from the Program is covered only if its contents
75 | constitute a work based on the Program (independent of having been made by
76 | running the Program). Whether that is true depends on what the Program does.
77 |
78 | **1.** You may copy and distribute verbatim copies of the Program's source code
79 | as you receive it, in any medium, provided that you conspicuously and
80 | appropriately publish on each copy an appropriate copyright notice and
81 | disclaimer of warranty; keep intact all the notices that refer to this License
82 | and to the absence of any warranty; and give any other recipients of the Program
83 | a copy of this License along with the Program.
84 |
85 | You may charge a fee for the physical act of transferring a copy, and you may at
86 | your option offer warranty protection in exchange for a fee.
87 |
88 | **2.** You may modify your copy or copies of the Program or any portion of it,
89 | thus forming a work based on the Program, and copy and distribute such
90 | modifications or work under the terms of Section 1 above, provided that you also
91 | meet all of these conditions:
92 |
93 | * **a)** You must cause the modified files to carry prominent notices stating
94 | that you changed the files and the date of any change.
95 |
96 | * **b)** You must cause any work that you distribute or publish, that in whole
97 | or in part contains or is derived from the Program or any part thereof, to
98 | be licensed as a whole at no charge to all third parties under the terms of
99 | this License.
100 |
101 | * **c)** If the modified program normally reads commands interactively when
102 | run, you must cause it, when started running for such interactive use in the
103 | most ordinary way, to print or display an announcement including an
104 | appropriate copyright notice and a notice that there is no warranty (or
105 | else, saying that you provide a warranty) and that users may redistribute
106 | the program under these conditions, and telling the user how to view a copy
107 | of this License. (Exception: if the Program itself is interactive but does
108 | not normally print such an announcement, your work based on the Program is
109 | not required to print an announcement.)
110 |
111 | These requirements apply to the modified work as a whole. If identifiable
112 | sections of that work are not derived from the Program, and can be reasonably
113 | considered independent and separate works in themselves, then this License, and
114 | its terms, do not apply to those sections when you distribute them as separate
115 | works. But when you distribute the same sections as part of a whole which is a
116 | work based on the Program, the distribution of the whole must be on the terms of
117 | this License, whose permissions for other licensees extend to the entire whole,
118 | and thus to each and every part regardless of who wrote it.
119 |
120 | Thus, it is not the intent of this section to claim rights or contest your
121 | rights to work written entirely by you; rather, the intent is to exercise the
122 | right to control the distribution of derivative or collective works based on the
123 | Program.
124 |
125 | In addition, mere aggregation of another work not based on the Program with the
126 | Program (or with a work based on the Program) on a volume of a storage or
127 | distribution medium does not bring the other work under the scope of this
128 | License.
129 |
130 | **3.** You may copy and distribute the Program (or a work based on it, under
131 | Section 2) in object code or executable form under the terms of Sections 1 and 2
132 | above provided that you also do one of the following:
133 |
134 | * **a)** Accompany it with the complete corresponding machine-readable source
135 | code, which must be distributed under the terms of Sections 1 and 2 above on
136 | a medium customarily used for software interchange; or,
137 |
138 | * **b)** Accompany it with a written offer, valid for at least three years, to
139 | give any third party, for a charge no more than your cost of physically
140 | performing source distribution, a complete machine-readable copy of the
141 | corresponding source code, to be distributed under the terms of Sections 1
142 | and 2 above on a medium customarily used for software interchange; or,
143 |
144 | * **c)** Accompany it with the information you received as to the offer to
145 | distribute corresponding source code. (This alternative is allowed only for
146 | noncommercial distribution and only if you received the program in object
147 | code or executable form with such an offer, in accord with Subsection b
148 | above.)
149 |
150 | The source code for a work means the preferred form of the work for making
151 | modifications to it. For an executable work, complete source code means all the
152 | source code for all modules it contains, plus any associated interface
153 | definition files, plus the scripts used to control compilation and installation
154 | of the executable. However, as a special exception, the source code distributed
155 | need not include anything that is normally distributed (in either source or
156 | binary form) with the major components (compiler, kernel, and so on) of the
157 | operating system on which the executable runs, unless that component itself
158 | accompanies the executable.
159 |
160 | If distribution of executable or object code is made by offering access to copy
161 | from a designated place, then offering equivalent access to copy the source code
162 | from the same place counts as distribution of the source code, even though third
163 | parties are not compelled to copy the source along with the object code.
164 |
165 | **4.** You may not copy, modify, sublicense, or distribute the Program except as
166 | expressly provided under this License. Any attempt otherwise to copy, modify,
167 | sublicense or distribute the Program is void, and will automatically terminate
168 | your rights under this License. However, parties who have received copies, or
169 | rights, from you under this License will not have their licenses terminated so
170 | long as such parties remain in full compliance.
171 |
172 | **5.** You are not required to accept this License, since you have not signed
173 | it. However, nothing else grants you permission to modify or distribute the
174 | Program or its derivative works. These actions are prohibited by law if you do
175 | not accept this License. Therefore, by modifying or distributing the Program (or
176 | any work based on the Program), you indicate your acceptance of this License to
177 | do so, and all its terms and conditions for copying, distributing or modifying
178 | the Program or works based on it.
179 |
180 | **6.** Each time you redistribute the Program (or any work based on the
181 | Program), the recipient automatically receives a license from the original
182 | licensor to copy, distribute or modify the Program subject to these terms and
183 | conditions. You may not impose any further restrictions on the recipients'
184 | exercise of the rights granted herein. You are not responsible for enforcing
185 | compliance by third parties to this License.
186 |
187 | **7.** If, as a consequence of a court judgment or allegation of patent
188 | infringement or for any other reason (not limited to patent issues), conditions
189 | are imposed on you (whether by court order, agreement or otherwise) that
190 | contradict the conditions of this License, they do not excuse you from the
191 | conditions of this License. If you cannot distribute so as to satisfy
192 | simultaneously your obligations under this License and any other pertinent
193 | obligations, then as a consequence you may not distribute the Program at all.
194 | For example, if a patent license would not permit royalty-free redistribution of
195 | the Program by all those who receive copies directly or indirectly through you,
196 | then the only way you could satisfy both it and this License would be to refrain
197 | entirely from distribution of the Program.
198 |
199 | If any portion of this section is held invalid or unenforceable under any
200 | particular circumstance, the balance of the section is intended to apply and the
201 | section as a whole is intended to apply in other circumstances.
202 |
203 | It is not the purpose of this section to induce you to infringe any patents or
204 | other property right claims or to contest validity of any such claims; this
205 | section has the sole purpose of protecting the integrity of the free software
206 | distribution system, which is implemented by public license practices. Many
207 | people have made generous contributions to the wide range of software
208 | distributed through that system in reliance on consistent application of that
209 | system; it is up to the author/donor to decide if he or she is willing to
210 | distribute software through any other system and a licensee cannot impose that
211 | choice.
212 |
213 | This section is intended to make thoroughly clear what is believed to be a
214 | consequence of the rest of this License.
215 |
216 | **8.** If the distribution and/or use of the Program is restricted in certain
217 | countries either by patents or by copyrighted interfaces, the original copyright
218 | holder who places the Program under this License may add an explicit
219 | geographical distribution limitation excluding those countries, so that
220 | distribution is permitted only in or among countries not thus excluded. In such
221 | case, this License incorporates the limitation as if written in the body of this
222 | License.
223 |
224 | **9.** The Free Software Foundation may publish revised and/or new versions of
225 | the General Public License from time to time. Such new versions will be similar
226 | in spirit to the present version, but may differ in detail to address new
227 | problems or concerns.
228 |
229 | Each version is given a distinguishing version number. If the Program specifies
230 | a version number of this License which applies to it and "any later version",
231 | you have the option of following the terms and conditions either of that version
232 | or of any later version published by the Free Software Foundation. If the
233 | Program does not specify a version number of this License, you may choose any
234 | version ever published by the Free Software Foundation.
235 |
236 | **10.** If you wish to incorporate parts of the Program into other free programs
237 | whose distribution conditions are different, write to the author to ask for
238 | permission. For software which is copyrighted by the Free Software Foundation,
239 | write to the Free Software Foundation; we sometimes make exceptions for this.
240 | Our decision will be guided by the two goals of preserving the free status of
241 | all derivatives of our free software and of promoting the sharing and reuse of
242 | software generally.
243 |
244 |
245 | No Warranty
246 | -----------
247 |
248 | **11.** BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
249 | THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
250 | STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM
251 | "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING,
252 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
253 | PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
254 | PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
255 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
256 |
257 | **12.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
258 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE
259 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
260 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR
261 | INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA
262 | BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
263 | FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER
264 | OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | A WordPress plugin that adds small icons to the customizer preview that open the respective section in the sidebar.
2 |
3 | 
4 |
5 | # Installation
6 |
7 | 1. Visit the [Releases page](https://github.com/Automattic/customize-direct-manipulation/releases) and download the latest release (not the source code).
8 | 2. Navigate to Plugins → Add New in your WordPress site's admin area and click the "Upload Plugin" button at the top in order to upload the ZIP file. If this button doesn't exist due to file permissions, you can [install it manually](https://codex.wordpress.org/Managing_Plugins#Manual_Plugin_Installation).
9 | 3. Activate the plugin, either from the page that shows up after uploading the ZIP file or from your plugins list if you uploaded the plugin manually.
10 | 4. Navigate to Appearance → Customize from your WordPress menu.
11 | 5. Click the small icons to activate the appropriate controls in the sidebar.
12 |
13 | 
14 |
15 | # Contributing
16 |
17 | 1. Clone this repo and install it on a WordPress site. You can clone the repo directly into your `/wp-content/plugins` directory.
18 | 2. After cloning, run `npm install` and then `npm run dist` to compile the JavaScript.
19 | 3. To start a watcher process for development, run `npm start`.
20 | 4. If you want to see detailed info in your browser console, enable debugging by running the following command in the console and then reloading the page: `localStorage.setItem('debug', 'cdm:*');`.
21 |
22 | # Testing
23 |
24 | Run `npm install` and then `npm test` to run the test suite.
25 |
26 | [](https://circleci.com/gh/Automattic/customize-direct-manipulation)
27 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | node:
3 | version: 6.12.2
4 |
--------------------------------------------------------------------------------
/css/cdm-admin.css:
--------------------------------------------------------------------------------
1 | .cdm-subtle-focus {
2 | -webkit-animation: bounce .7s 1;
3 | animation: bounce .7s 1;
4 | -webkit-transform-origin: center bottom;
5 | transform-origin: center bottom;
6 | }
7 |
8 | @-webkit-keyframes bounce {
9 | from, 20%, 53%, 80%, to {
10 | -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
11 | -webkit-transform: translate3d(0,0,0);
12 | }
13 |
14 | 40%, 43% {
15 | -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
16 | -webkit-transform: translate3d(0, -12px, 0);
17 | }
18 |
19 | 70% {
20 | -webkit-animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
21 | -webkit-transform: translate3d(0, -6px, 0);
22 | }
23 |
24 | 90% {
25 | -webkit-transform: translate3d(0,-1px,0);
26 | }
27 | }
28 |
29 | @keyframes bounce {
30 | from, 20%, 53%, 80%, to {
31 | animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
32 | transform: translate3d(0,0,0);
33 | }
34 |
35 | 40%, 43% {
36 | animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
37 | transform: translate3d(0, -12px, 0);
38 | }
39 |
40 | 70% {
41 | animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
42 | transform: translate3d(0, -6px, 0);
43 | }
44 |
45 | 90% {
46 | transform: translate3d(0,-1px,0);
47 | }
48 | }
49 |
50 | #dmguide-overlay {
51 | background-color: rgba(255,255,255,.25);
52 | display: block;
53 | position: fixed;
54 | top: 0;
55 | left: 0;
56 | z-index: 9999998;
57 | width: 100vw;
58 | height: 100vh;
59 | }
60 |
61 | #dmguide {
62 | background-color: #151e25;
63 | border-radius: 5px;
64 | box-shadow: 3px 1px 10px -2px rgba(0,0,0,.5);
65 | font-size: 14px;
66 | margin: 0 12px 0 0;
67 | position: fixed;
68 | top: 97px;
69 | left: 305px;
70 | z-index: 999999998;
71 | text-align: left;
72 | width: 410px;
73 | }
74 | #dmguide::after {
75 | content: '';
76 | width: 0;
77 | height: 0;
78 | border-style: solid;
79 | border-width: 16px 16px 16px 0;
80 | border-color: transparent #151e25 transparent transparent;
81 | position: absolute;
82 | top: 16px;
83 | left: -16px;
84 | }
85 | #dmguide.dmguide-moving {
86 | transition-duration: 250ms;
87 | transition-property: transform;
88 | transition-timing-function: cubic-bezier(.84,.45,.68,1.44);
89 | }
90 |
91 | .entering #dmguide {
92 | -webkit-animation: bounceInLeft .5s ease-in-out 1 .25s;
93 | animation: bounceInLeft .5s ease-in-out 1 .25s;
94 | -webkit-transform: translate3d(100vw, 0, 0);
95 | transform: translate3d(100vw, 0, 0);
96 | }
97 | .exiting #dmguide {
98 | -webkit-animation: bounceOutRight .66s ease-in-out 1;
99 | animation: bounceOutRight .66s ease-in-out 1;
100 | }
101 |
102 | #dmguide-text {
103 | display: inline-block;
104 | line-height: 1.6;
105 | padding: 1.5em 2em;
106 | vertical-align: bottom;
107 | color: #fff;
108 | -webkit-font-smoothing: antialiased;
109 | -moz-osx-font-smoothing: grayscale;
110 | }
111 | #dmguide-text b::before {
112 | content: ' ';
113 | display: inline-block;
114 | background: url( 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHJlY3QgeD0iMCIgZmlsbD0ibm9uZSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ii8+PGc+PHBhdGggZmlsbD0iIzY2OGVhYSIgZD0iTTEyIDJDNi40NzcgMiAyIDYuNDc3IDIgMTJzNC40NzcgMTAgMTAgMTAgMTAtNC40NzcgMTAtMTBTMTcuNTIzIDIgMTIgMnpNMy41IDEyYzAtMS4yMzIuMjY0LTIuNDAyLjczNi0zLjQ2TDguMjkgMTkuNjVDNS40NTYgMTguMjcyIDMuNSAxNS4zNjUgMy41IDEyem04LjUgOC41Yy0uODM0IDAtMS42NC0uMTItMi40LS4zNDVsMi41NS03LjQxIDIuNjEzIDcuMTU3Yy4wMTcuMDQyLjAzOC4wOC4wNi4xMTctLjg4NC4zMS0xLjgzMy40OC0yLjgyMy40OHptMS4xNzItMTIuNDg1Yy41MTItLjAyNy45NzMtLjA4Ljk3My0uMDguNDU4LS4wNTUuNDA0LS43MjgtLjA1NC0uNzAyIDAgMC0xLjM3Ni4xMDgtMi4yNjUuMTA4LS44MzUgMC0yLjI0LS4xMDctMi4yNC0uMTA3LS40NTgtLjAyNi0uNTEuNjc0LS4wNTMuNyAwIDAgLjQzNC4wNTUuODkyLjA4MmwxLjMyNCAzLjYzLTEuODYgNS41NzgtMy4wOTYtOS4yMDhjLjUxMi0uMDI3Ljk3My0uMDguOTczLS4wOC40NTgtLjA1NS40MDMtLjcyOC0uMDU1LS43MDIgMCAwLTEuMzc2LjEwOC0yLjI2NS4xMDgtLjE2IDAtLjM0Ny0uMDAzLS41NDctLjAxQzYuNDE4IDUuMDI1IDkuMDMgMy41IDEyIDMuNWMyLjIxMyAwIDQuMjI4Ljg0NiA1Ljc0IDIuMjMyLS4wMzctLjAwMi0uMDcyLS4wMDctLjExLS4wMDctLjgzNSAwLTEuNDI3LjcyNy0xLjQyNyAxLjUxIDAgLjcuNDA0IDEuMjkyLjgzNSAxLjk5My4zMjMuNTY2LjcgMS4yOTMuNyAyLjM0NCAwIC43MjctLjI4IDEuNTcyLS42NDYgMi43NDhsLS44NDggMi44MzMtMy4wNzItOS4xMzh6bTMuMSAxMS4zMzJsMi41OTctNy41MDZjLjQ4NC0xLjIxMi42NDUtMi4xOC42NDUtMy4wNDQgMC0uMzEzLS4wMi0uNjAzLS4wNTctLjg3NC42NjQgMS4yMSAxLjA0MiAyLjYgMS4wNDIgNC4wNzggMCAzLjEzNi0xLjcgNS44NzQtNC4yMjcgNy4zNDd6Ii8+PC9nPjwvc3ZnPg==' );
115 | width: 24px;
116 | height: 24px;
117 | vertical-align: baseline;
118 | position: relative;
119 | bottom: -6px;
120 | margin-right: 2px;
121 | }
122 |
123 | #dmguide h2 {
124 | color: #2e4453;
125 | font-size: 20px;
126 | line-height: 1.6;
127 | letter-spacing: 0;
128 | margin: 0;
129 | padding: 0;
130 | text-transform: none;
131 | }
132 | #dmguide-text,
133 | #dmguide-header,
134 | #dmguide .dmguide-button {
135 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen-Sans", "Ubuntu", "Cantarell", "Helvetica Neue", sans-serif;
136 | }
137 |
138 | @media screen and ( max-width: 730px ) {
139 | #dmguide {
140 | width: auto;
141 | right: 1em;
142 | }
143 | }
144 | @media screen and ( max-width: 640px ) {
145 | .entering #dmguide {
146 | -webkit-animation: bounceInDown .66s ease-in-out 1 .25s;
147 | animation: bounceInDown .66s ease-in-out 1 .25s;
148 | -webkit-transform: translate3d(0, -3000px, 0);
149 | transform: translate3d(0, -3000px, 0);
150 | }
151 | .exiting #dmguide {
152 | -webkit-animation: bounceOutUp .66s ease-in-out 1;
153 | animation: bounceOutUp .66s ease-in-out 1;
154 | }
155 | #dmguide {
156 | top: 60px;
157 | left: 10px;
158 | right: 0;
159 | }
160 | #dmguide::after {
161 | display: none;
162 | }
163 | #dmguide-text {
164 | line-height: 22px;
165 | display: block;
166 | padding: 20px;
167 | }
168 | .dmguide-button-wrap {
169 | margin-top: 20px;
170 | }
171 | .dmguide-button-primary {
172 | text-align: center;
173 | width: 100%;
174 | }
175 | }
176 |
177 | /* from https://daneden.github.io/animate.css/ */
178 | @-webkit-keyframes bounceInLeft {
179 | from, 60%, 75%, 90%, to {
180 | -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
181 | }
182 | from {
183 | opacity: 0;
184 | -webkit-transform: translate3d(100vw, 0, 0);
185 | }
186 | 60% {
187 | opacity: 1;
188 | -webkit-transform: translate3d(-20px, 0, 0);
189 | }
190 | 75% {
191 | -webkit-transform: translate3d(10px, 0, 0);
192 | }
193 | 90% {
194 | -webkit-transform: translate3d(-5px, 0, 0);
195 | }
196 | to {
197 | -webkit-transform: translate3d(0, 0, 0);
198 | transform: translate3d(0, 0, 0);
199 | }
200 | }
201 |
202 | @keyframes bounceInLeft {
203 | from, 60%, 75%, 90%, to {
204 | animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
205 | }
206 | from {
207 | opacity: 0;
208 | transform: translate3d(100vw, 0, 0);
209 | }
210 | 60% {
211 | opacity: 1;
212 | transform: translate3d(-20px, 0, 0);
213 | }
214 | 75% {
215 | transform: translate3d(10px, 0, 0);
216 | }
217 | 90% {
218 | transform: translate3d(-5px, 0, 0);
219 | }
220 | to {
221 | transform: translate3d(0, 0, 0);
222 | }
223 | }
224 |
225 | @-webkit-keyframes bounceOutRight {
226 | 20% {
227 | -webkit-transform: translate3d(10px, 0, 0);
228 | }
229 | 40%, 45% {
230 | opacity: 1;
231 | -webkit-transform: translate3d(-20px, 0, 0);
232 | }
233 | to {
234 | opacity: 0;
235 | -webkit-transform: translate3d(2000px, 0, 0);
236 | }
237 | }
238 |
239 | @keyframes bounceOutRight {
240 | 20% {
241 | transform: translate3d(10px, 0, 0);
242 | }
243 | 40%, 45% {
244 | opacity: 1;
245 | transform: translate3d(-20px, 0, 0);
246 | }
247 | to {
248 | opacity: 0;
249 | transform: translate3d(2000px, 0, 0);
250 | }
251 | }
252 |
253 | @-webkit-keyframes bounceInDown {
254 | from, 60%, 75%, 90%, to {
255 | -webkit-animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
256 | }
257 |
258 | 0% {
259 | opacity: 0;
260 | -webkit-transform: translate3d(0, -3000px, 0);
261 | }
262 |
263 | 60% {
264 | opacity: 1;
265 | -webkit-transform: translate3d(0, 25px, 0);
266 | }
267 |
268 | 75% {
269 | -webkit-transform: translate3d(0, -10px, 0);
270 | }
271 |
272 | 90% {
273 | -webkit-transform: translate3d(0, 5px, 0);
274 | }
275 |
276 | to {
277 | -webkit-transform: none;
278 | }
279 | }
280 |
281 | @keyframes bounceInDown {
282 | from, 60%, 75%, 90%, to {
283 | animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
284 | }
285 |
286 | 0% {
287 | opacity: 0;
288 | transform: translate3d(0, -3000px, 0);
289 | }
290 |
291 | 60% {
292 | opacity: 1;
293 | transform: translate3d(0, 25px, 0);
294 | }
295 |
296 | 75% {
297 | transform: translate3d(0, -10px, 0);
298 | }
299 |
300 | 90% {
301 | transform: translate3d(0, 5px, 0);
302 | }
303 |
304 | to {
305 | transform: none;
306 | }
307 | }
308 |
309 | @-webkit-keyframes bounceOutUp {
310 | 20% {
311 | -webkit-transform: translate3d(0, -10px, 0);
312 | transform: translate3d(0, -10px, 0);
313 | }
314 |
315 | 40%, 45% {
316 | opacity: 1;
317 | -webkit-transform: translate3d(0, 20px, 0);
318 | transform: translate3d(0, 20px, 0);
319 | }
320 |
321 | to {
322 | opacity: 0;
323 | -webkit-transform: translate3d(0, -2000px, 0);
324 | transform: translate3d(0, -2000px, 0);
325 | }
326 | }
327 |
328 | @keyframes bounceOutUp {
329 | 20% {
330 | -webkit-transform: translate3d(0, -10px, 0);
331 | transform: translate3d(0, -10px, 0);
332 | }
333 |
334 | 40%, 45% {
335 | opacity: 1;
336 | -webkit-transform: translate3d(0, 20px, 0);
337 | transform: translate3d(0, 20px, 0);
338 | }
339 |
340 | to {
341 | opacity: 0;
342 | -webkit-transform: translate3d(0, -2000px, 0);
343 | transform: translate3d(0, -2000px, 0);
344 | }
345 | }
346 |
347 | /** Buttons **/
348 | .dmguide-button-wrap {
349 | clear: both;
350 | margin-top: 1em;
351 | }
352 | .dmguide-button {
353 | background: white;
354 | border-color: #c8d7e1;
355 | border-style: solid;
356 | border-width: 1px 1px 2px;
357 | color: #2e4453;
358 | cursor: pointer;
359 | display: inline-block;
360 | margin: 0;
361 | outline: 0;
362 | overflow: hidden;
363 | font-weight: 500;
364 | text-overflow: ellipsis;
365 | text-decoration: none;
366 | text-transform: none;
367 | box-sizing: border-box;
368 | font-size: 14px;
369 | line-height: 21px;
370 | border-radius: 4px;
371 | padding: 7px 14px 9px;
372 | -webkit-appearance: none;
373 | appearance: none;
374 | vertical-align: baseline;
375 | }
376 |
377 | .dmguide-button:hover {
378 | border-color: #a8bece;
379 | color: #2e4453;
380 | }
381 |
382 | .dmguide-button:active {
383 | border-width: 2px 1px 1px;
384 | }
385 |
386 | .dmguide-button:visited {
387 | color: #2e4453;
388 | }
389 |
390 | .dmguide-button[disabled], .dmguide-button:disabled {
391 | color: #e9eff3;
392 | background: white;
393 | border-color: #e9eff3;
394 | cursor: default;
395 | }
396 |
397 | .dmguide-button[disabled]:active, .dmguide-button:disabled:active {
398 | border-width: 1px 1px 2px;
399 | }
400 |
401 | .dmguide-button:focus {
402 | border-color: #00aadc;
403 | box-shadow: 0 0 0 2px #78dcfa;
404 | }
405 |
406 | .dmguide-button.dmguide-button-primary {
407 | background: #1cabda;
408 | border-color: #0087be;
409 | color: white;
410 | }
411 |
412 | .dmguide-button.dmguide-button-primary:hover, .dmguide-button.dmguide-button-primary:focus {
413 | border-color: #005082;
414 | color: white;
415 | }
416 |
417 | .dmguide-button.dmguide-button-primary[disabled], .dmguide-button.dmguide-button-primary:disabled {
418 | background: tint(#78dcfa, 50%);
419 | border-color: tint(#0087be, 55%);
420 | color: white;
421 | }
422 |
423 | .dmguide-button.dmguide-button-primary.is-compact {
424 | color: white;
425 | }
426 |
427 |
428 | .dmguide-bounce {
429 | -webkit-animation: bounce .7s 1;
430 | animation: bounce .7s 1;
431 | -webkit-transform-origin: center bottom;
432 | transform-origin: center bottom;
433 | }
434 |
435 | #customize-info .site-title {
436 | cursor: pointer;
437 | }
--------------------------------------------------------------------------------
/css/customize-direct-manipulation.css:
--------------------------------------------------------------------------------
1 | .cdm-icon {
2 | fill: #fff;
3 | position: absolute;
4 | top: 0;
5 | left: 0;
6 | width: 30px;
7 | height: 30px;
8 | font-size: 18px;
9 | z-index: 5;
10 | background: #0087BE;
11 | border-radius: 50%;
12 | border: 2px solid #fff;
13 | box-shadow: 0 2px 1px rgba(46,68,83,0.15);
14 | text-align: center;
15 | display: flex;
16 | flex-direction: row;
17 | justify-content: center;
18 | align-items: center;
19 | cursor: pointer;
20 | animation-name: bounce-appear;
21 | animation-delay: 0.8s;
22 | animation-duration: 1s;
23 | animation-fill-mode: both;
24 | animation-duration: .75s;
25 | }
26 |
27 | .cdm-icon:hover {
28 | background: #00aadc;
29 | transition: background .15s ease-in-out;
30 | }
31 |
32 | .cdm-icon--hidden {
33 | display: none;
34 | }
35 |
36 | .cdm-icon svg {
37 | width: 18px;
38 | height: 18px;
39 | }
40 |
41 | .cdm-icon--text {
42 | margin-left: -2em;
43 | }
44 |
45 | .cdm-icon--header-image {
46 | margin: 10px;
47 | }
48 |
49 | .cdm-icon--header-image.cdm-icon__site_logo {
50 | margin: -15px 0 0 -15px;
51 | }
52 |
53 | @keyframes bounce-appear {
54 | from, 20%, 40%, 60%, 80%, to {
55 | animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
56 | }
57 | 0% {
58 | opacity: 0;
59 | transform: scale3d(.3, .3, .3);
60 | }
61 | 20% {
62 | transform: scale3d(1.1, 1.1, 1.1);
63 | }
64 | 40% {
65 | transform: scale3d(.9, .9, .9);
66 | }
67 | 60% {
68 | opacity: 1;
69 | transform: scale3d(1.03, 1.03, 1.03);
70 | }
71 | 80% {
72 | transform: scale3d(.97, .97, .97);
73 | }
74 | to {
75 | opacity: 1;
76 | transform: scale3d(1, 1, 1);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/customize-direct-manipulation.php:
--------------------------------------------------------------------------------
1 | make_unique_id( 'cdm-menu-for-' . $location );
68 | $this->store_menu( $class_name, $location );
69 | if ( ! empty( $args['container_class'] ) ) {
70 | $class_name = $args['container_class'] . ' ' . $class_name;
71 | }
72 | $args['container_class'] = $class_name;
73 | }
74 |
75 | return $args;
76 | }
77 |
78 | private function make_unique_id( $id ) {
79 | $this->menu_counter++;
80 | return $id . '-' . $this->menu_counter;
81 | }
82 |
83 | private function store_menu( $id, $location ) {
84 | // dedupe
85 | if ( isset( $this->nav_menus[ $id ] ) ) {
86 | return;
87 | }
88 | $locations = get_nav_menu_locations();
89 | if ( ! isset( $locations[ $location ] ) ) {
90 | return;
91 | }
92 | $menu = wp_get_nav_menu_object( $locations[ $location ] );
93 | if ( ! $menu ) {
94 | return;
95 | }
96 | $this->nav_menus[ $id ] = "nav_menu[{$menu->term_id}]";
97 | }
98 |
99 | private function get_menu_data() {
100 | $menus = array();
101 | foreach( $this->nav_menus as $id => $location ) {
102 | $menus[] = compact( 'id', 'location' );
103 | }
104 |
105 | return $menus;
106 | }
107 |
108 | private function should_show_guide() {
109 | if ( isset( $_GET['guide'] ) ) {
110 | return true;
111 | }
112 |
113 | // Only to newer users
114 | $this_user_id = (int) get_current_user_id();
115 | $minimum_user_id = 99855465;
116 | if ( $this_user_id < $minimum_user_id ) {
117 | return false;
118 | }
119 |
120 | // check the attribute set when shown
121 | if ( get_user_attribute( $this_user_id, 'customizer-guide-shown' ) ) {
122 | return false;
123 | }
124 |
125 | // we can show it, but just this once
126 | update_user_attribute( $this_user_id, 'customizer-guide-shown', 1 );
127 | return true;
128 | }
129 |
130 | public function admin_enqueue() {
131 | wp_enqueue_script( 'customize-dm-admin', plugins_url( 'js/customize-dm-admin.js', __FILE__ ), array( 'customize-controls' ), '20161129', true );
132 | wp_enqueue_style( 'customize-dm-admin', plugins_url( 'css/cdm-admin.css', __FILE__ ) );
133 |
134 | $steps = array(
135 | array(
136 | 'content' => __( 'Here you can control the design of your site. Change your site title, update the colors and fonts, and even add a header image. Explore widgets to find new features and content to add to your website.' ),
137 | 'smallContent' => __( 'Click the Preview icon to preview your site appearance before saving.' ),
138 | 'button' => __( 'Thanks, got it!' )
139 | ),
140 | );
141 |
142 | $steps = apply_filters( 'customizer_direct_manipulation_steps', $steps );
143 |
144 | $showGuide = $this->should_show_guide();
145 | wp_localize_script( 'customize-dm-admin', '_Customizer_DM', compact( 'steps', 'showGuide' ) );
146 | }
147 |
148 | public function preview_enqueue() {
149 | wp_enqueue_style( 'customize-dm-preview', plugins_url( 'css/customize-direct-manipulation.css', __FILE__ ), array(), '20160411' );
150 | wp_enqueue_script( 'customize-dm-preview', plugins_url( 'js/customize-dm-preview.js', __FILE__ ), array( 'jquery', 'customize-selective-refresh' ), '20161205', true );
151 | add_action( 'wp_footer', array( $this, 'add_script_data_in_footer' ) );
152 | add_filter( 'widget_links_args', array( $this, 'fix_widget_links' ) );
153 | }
154 |
155 | public function add_script_data_in_footer() {
156 |
157 | /**
158 | * Filters the modules to disable in the Customize Direct Manipulation plugin.
159 | *
160 | * Plugins can push modules onto this list to disable aspects of the plugin.
161 | * For example, the Customize Posts plugin can disable the 'edit-post-links'
162 | * module because it has its own integration with the edit post links
163 | * whereby the posts can be edited in the customizer directly.
164 | *
165 | * Not all modules can currently be disabled.
166 | *
167 | * @param array $disabled_modules Disabled modules, defaulting to empty array.
168 | * @returns array Disabled modules.
169 | */
170 | $disabled_modules = apply_filters( 'customize_direct_manipulation_disabled_modules', array() );
171 |
172 | $translations = array(
173 | 'siteTitle' => __( 'Click to edit the site title' ),
174 | 'footerCredit' => __( 'Click to edit the footer credit' ),
175 | 'menu' => __( 'Click to edit this menu' ),
176 | 'post' => __( 'Click to edit this post' ),
177 | 'widget' => __( 'Click to edit this widget' ),
178 | );
179 |
180 | wp_localize_script( 'customize-dm-preview', '_Customizer_DM', array(
181 | 'menus' => $this->get_menu_data(),
182 | 'headerImageSupport' => current_theme_supports( 'custom-header' ),
183 | 'disabledModules' => $disabled_modules,
184 | 'translations' => $translations,
185 | ) );
186 | }
187 |
188 | public function maybe_add_page_menu_class( $args ) {
189 | if ( ! is_customize_preview() ) {
190 | return $args;
191 | }
192 | if ( ! ( 'wp_page_menu' === $args['fallback_cb'] && isset( $args['theme_location'] ) ) ) {
193 | return $args;
194 | }
195 | $args['menu_class'] .= " cdm-fallback-menu cdm-menu-location-{$args['theme_location']}";
196 | return $args;
197 | }
198 |
199 | /**
200 | * The links widget is awesome because it doesn't spit out the widget instance ID, instead punting
201 | * to `wp_list_bookmarks()` which assigns IDs based on link categories (and even spits out multiple widgets if
202 | * you have more than one link category) which is all completely awesome.
203 | *
204 | * AWESOME!!!!!!
205 | */
206 | public function fix_widget_links( $args ) {
207 | foreach( debug_backtrace() as $traced ) {
208 | if ( isset( $traced['class'] ) && 'WP_Widget_Links' === $traced['class'] && isset( $traced['object']) ) {
209 | $args['category_before'] = str_replace( 'id="%id"', 'data-id="%id" id="' . $traced['object']->id . '"', $args['category_before'] );
210 | break;
211 | }
212 | }
213 | return $args;
214 | }
215 | }
216 |
217 | add_action( 'init', array( Jetpack_Customizer_DM::get_instance(), 'init' ) );
218 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "customize-direct-manipulation",
3 | "version": "1.1.0",
4 | "description": "Click to edit in the WordPress Customizer",
5 | "engines": {
6 | "node": ">6.0.0"
7 | },
8 | "scripts": {
9 | "start": "rollup -c -w",
10 | "dist": "NODE_ENV=production rollup -c",
11 | "test": "NODE_ENV=test mocha --require babel-register",
12 | "lint": "eslint src",
13 | "lint:fix": "eslint src --fix",
14 | "postinstall": "(svn info . 1>/dev/null 2>&1 && svn propset svn:ignore -F .svnignore .) || echo 'Not a working copy.'"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git://github.com/Automattic/customize-direct-manipulation"
19 | },
20 | "author": "Automattic",
21 | "license": "GPL-2.0",
22 | "bugs": {
23 | "url": "https://github.com/Automattic/customize-direct-manipulation/issues"
24 | },
25 | "homepage": "https://github.com/Automattic/customize-direct-manipulation",
26 | "devDependencies": {
27 | "babel-eslint": "^8.0.3",
28 | "babel-preset-env": "^1.6.1",
29 | "babel-register": "^6.26.0",
30 | "chai": "^4.1.2",
31 | "eslint": "^4.13.1",
32 | "eslint-config-wpcalypso": "^1.2.0",
33 | "eslint-plugin-wpcalypso": "^4.0.1",
34 | "jquery": "^3.2.1",
35 | "jsdom": "^11.5.1",
36 | "mocha": "^4.0.1",
37 | "rollup": "^0.52.2",
38 | "rollup-plugin-babel": "^3.0.2",
39 | "rollup-plugin-commonjs": "^8.2.6",
40 | "rollup-plugin-eslint": "^4.0.0",
41 | "rollup-plugin-node-resolve": "^3.0.0",
42 | "rollup-plugin-uglify": "^2.0.1",
43 | "sinon": "^4.1.3",
44 | "sinon-chai": "^2.14.0",
45 | "underscore": "^1.8.3"
46 | },
47 | "dependencies": {
48 | "debug": "^3.1.0"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import babel from 'rollup-plugin-babel';
2 | import commonjs from 'rollup-plugin-commonjs';
3 | import eslint from 'rollup-plugin-eslint';
4 | import resolve from 'rollup-plugin-node-resolve';
5 | import uglify from 'rollup-plugin-uglify';
6 |
7 | const prod = ( process.env.NODE_ENV === 'production' );
8 |
9 | const commonConfig = {
10 | plugins: [
11 | resolve({
12 | browser: true,
13 | }),
14 | commonjs(),
15 | ...( ! prod && [ eslint() ] ),
16 | babel({
17 | exclude: 'node_modules/**'
18 | }),
19 | ...( prod && [ uglify() ] ),
20 | ],
21 | external: [
22 | 'jquery',
23 | 'underscore',
24 | ...( prod && [ 'debug' ] ),
25 | ],
26 | globals: {
27 | jquery: 'window.jQuery',
28 | underscore: 'window._',
29 | ...( prod && { debug: 'function(){ return window._.noop; }' } ),
30 | },
31 | };
32 |
33 | export default [
34 | Object.assign({
35 | input: 'src/admin.js',
36 | output: {
37 | file: 'js/customize-dm-admin.js',
38 | format: 'iife',
39 | },
40 | }, commonConfig),
41 | Object.assign({
42 | input: 'src/preview.js',
43 | output: {
44 | file: 'js/customize-dm-preview.js',
45 | format: 'iife',
46 | },
47 | }, commonConfig),
48 | ];
--------------------------------------------------------------------------------
/src/admin.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import getAPI from './helpers/api';
3 | import { send } from './helpers/messenger';
4 | import addFocusListener from './modules/focus-listener';
5 | import { bindPreviewEventsListener } from './helpers/record-event';
6 | import addGuide from './modules/guide';
7 | import getOpts from './helpers/options';
8 | import debugFactory from 'debug';
9 |
10 | const debug = debugFactory( 'cdm:admin' );
11 | const api = getAPI();
12 |
13 | // do some focusing
14 | api.bind( 'ready', () => {
15 | debug( 'admin is ready' );
16 |
17 | addFocusListener( 'control-focus', id => api.control( id ) );
18 | addFocusListener( 'focus-menu', id => api.section( id ) );
19 | addFocusListener( 'focus-menu-location', id => api.control( `nav_menu_locations[${ id }]` ) );
20 |
21 | // Toggle icons when customizer toggles preview mode
22 | $( '.collapse-sidebar' ).on( 'click', () => send( 'cdm-toggle-visible' ) );
23 |
24 | // Make the site title clickable
25 | $( '.customize-info .site-title' ).on( 'click', () => {
26 | if ( api.previewer ) {
27 | api.previewer.trigger( 'control-focus', 'blogname' );
28 | }
29 | } );
30 |
31 | bindPreviewEventsListener();
32 |
33 | // Show 'em around the place the first time
34 | if ( getOpts().showGuide ) {
35 | addGuide();
36 | }
37 | } );
38 |
--------------------------------------------------------------------------------
/src/helpers/animate.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 |
3 | let animationIsSupported;
4 |
5 | export function supportsAnimation() {
6 | if ( typeof animationIsSupported !== 'undefined' ) {
7 | return animationIsSupported;
8 | }
9 | let animation = false;
10 | const elm = document.createElement( 'div' );
11 |
12 | if ( elm.style.animationName !== undefined || elm.style.WebkitAnimationName !== 'undefined' ) {
13 | animation = true;
14 | }
15 | animationIsSupported = animation;
16 | return animation;
17 | }
18 |
19 | export function animateWithClass( selector, className, cb, removeAtEnd ) {
20 | const $el = $( selector );
21 |
22 | $el.addClass( className ).on( 'animationend webkitAnimationEnd', function() {
23 | $el.removeClass( className ).off( 'animationend webkitAnimationEnd' );
24 |
25 | if ( $.isFunction( cb ) ) {
26 | cb.apply( $el );
27 | }
28 |
29 | if ( removeAtEnd ) {
30 | $el.remove();
31 | }
32 | } );
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/src/helpers/api.js:
--------------------------------------------------------------------------------
1 | import getWindow from './window';
2 |
3 | export default function getAPI() {
4 | if ( ! getWindow().wp || ! getWindow().wp.customize ) {
5 | throw new Error( 'No WordPress customizer API found' );
6 | }
7 | return getWindow().wp.customize;
8 | }
9 |
--------------------------------------------------------------------------------
/src/helpers/click-handler.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import debugFactory from 'debug';
3 |
4 | const debug = debugFactory( 'cdm:click-handler' );
5 |
6 | export default function addClickHandler( clickTarget, handler ) {
7 | debug( 'adding click handler to target', clickTarget );
8 | return $( 'body' ).on( 'click', clickTarget, handler );
9 | }
10 |
--------------------------------------------------------------------------------
/src/helpers/customizer.js:
--------------------------------------------------------------------------------
1 | import getAPI from '../helpers/api';
2 | import getWindow from '../helpers/window';
3 |
4 | const api = getAPI();
5 | const thisWindow = getWindow();
6 |
7 | /**
8 | * Run a function whenever a specific setting changes.
9 | *
10 | * The callback will receive two arguments:
11 | *
12 | * 1. The setting Id
13 | * 2. The new setting value
14 | *
15 | * @param {string} settingId - The setting name.
16 | * @param {function} callback - The function to call when a setting changes.
17 | */
18 | export function onSettingChange( settingId, callback ) {
19 | api( settingId, setting => setting.bind( () => callback( settingId, getSettingValue( settingId ) ) ) );
20 | }
21 |
22 | /**
23 | * Run a function whenever any setting changes.
24 | *
25 | * The callback will receive two arguments:
26 | *
27 | * 1. The setting Id
28 | * 2. The new setting value
29 | *
30 | * @param {function} callback - The function to call when a setting changes.
31 | */
32 | export function onChange( callback ) {
33 | api.bind( 'change', setting => callback( setting.id, getSettingValue( setting.id ) ) );
34 | }
35 |
36 | /**
37 | * Return true if any setting has changed.
38 | *
39 | * @return {boolean} True if any setting has changed.
40 | */
41 | export function areSettingsChanged() {
42 | return getAllSettingIds().some( isSettingChanged );
43 | }
44 |
45 | /**
46 | * Return an object of changed settings.
47 | *
48 | * Each key is a setting Id, and each value is the setting value.
49 | *
50 | * @return {Object} All changed settings.
51 | */
52 | export function getChangedSettings() {
53 | return getAllSettingIds()
54 | .filter( isSettingChanged )
55 | .reduce( ( settings, settingId ) => {
56 | settings[ settingId ] = getSettingValue( settingId );
57 | return settings;
58 | }, {} );
59 | }
60 |
61 | /**
62 | * Return true if this script is running in the preview.
63 | *
64 | * Returns false if the script is running in the control frame.
65 | *
66 | * @return {boolean} True if this is the preview frame.
67 | */
68 | export function isPreviewFrame() {
69 | return typeof api.preview !== 'undefined';
70 | }
71 |
72 | /**
73 | * Changes a setting value.
74 | *
75 | * @param {string} settingId - The setting name.
76 | * @param {*} value - The new setting value.
77 | */
78 | export function changeSettingValue( settingId, value ) {
79 | if ( isPreviewFrame() ) {
80 | const parentApi = getParentApi();
81 | if ( ! parentApi ) {
82 | return;
83 | }
84 | changeSettingValueForApi( parentApi, settingId, value );
85 | }
86 | changeSettingValueForApi( api, settingId, value );
87 | }
88 |
89 | /**
90 | * Return the current value of a setting.
91 | *
92 | * @param {string} settingId - The setting name.
93 | * @return {*} The setting value.
94 | */
95 | export function getSettingValue( settingId ) {
96 | return api.get()[ settingId ];
97 | }
98 |
99 | /**
100 | * Return all setting Ids.
101 | *
102 | * @return {Array} All the setting names.
103 | */
104 | export function getAllSettingIds() {
105 | return Object.keys( api.get() );
106 | }
107 |
108 | /**
109 | * Return true if the setting has changed.
110 | *
111 | * @param {string} settingId - The setting name.
112 | * @return {boolean} True if the setting has changed.
113 | */
114 | export function isSettingChanged( settingId ) {
115 | return api.instance( settingId )._dirty;
116 | }
117 |
118 | /**
119 | * Force the preview frame to reload.
120 | */
121 | export function reloadPreview() {
122 | if ( api.previewer ) {
123 | api.previewer.refresh();
124 | }
125 | const parentApi = getParentApi();
126 | if ( parentApi && parentApi.previewer ) {
127 | parentApi.previewer.refresh();
128 | }
129 | }
130 |
131 | /*******************
132 | * Private functions
133 | *******************/
134 |
135 | function getParentApi() {
136 | if ( thisWindow.parent.wp ) {
137 | return thisWindow.parent.wp.customize;
138 | }
139 | return null;
140 | }
141 |
142 | function changeSettingValueForApi( thisApi, settingId, value ) {
143 | const instance = thisApi.instance( settingId );
144 | if ( ! instance ) {
145 | return null;
146 | }
147 | instance.set( value );
148 | return value;
149 | }
150 |
--------------------------------------------------------------------------------
/src/helpers/icon-buttons.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import _ from 'underscore';
3 | import getWindow from '../helpers/window';
4 | import { on } from '../helpers/messenger';
5 | import addClickHandler from '../helpers/click-handler';
6 | import getOptions from '../helpers/options';
7 | import debugFactory from 'debug';
8 |
9 | const debug = debugFactory( 'cdm:icon-buttons' );
10 |
11 | // Elements will default to using `editIcon` but if an element has the `icon`
12 | // property set, it will use that as the key for one of these icons instead:
13 | const icons = {
14 | headerIcon: '',
15 | editIcon: '',
16 | };
17 |
18 | /**
19 | * Create (if necessary) and position an icon button relative to its target.
20 | *
21 | * See `makeFocusable` for the format of the `element` param.
22 | *
23 | * If positioning the icon was successful, this function returns a copy of the
24 | * element it was passed with the additional parameters `$target` and `$icon`
25 | * that are cached references to the DOM elements. If the positioning failed, it
26 | * just returns the element unchanged.
27 | *
28 | * @param {Object} element - The data to use when constructing the icon.
29 | * @return {Object} The element that was passed, with additional data included.
30 | */
31 | export function positionIcon( element ) {
32 | const $target = getElementTarget( element );
33 | if ( ! $target.length ) {
34 | debug( `Could not find target element for icon ${ element.id } with selector ${ element.selector }` );
35 | return element;
36 | }
37 |
38 | const $partialContainer = ( $target.data( 'customize-partial-id' ) ) ? $target : null;
39 | const $icon = findOrCreateIcon( element );
40 | const css = getCalculatedCssForIcon( element, $target, $icon );
41 | debug( `positioning icon for ${ element.id } with CSS ${ JSON.stringify( css ) }` );
42 | $icon.css( css );
43 | return _.extend( {}, element, { $target, $icon, $partialContainer } );
44 | }
45 |
46 | export function addClickHandlerToIcon( element ) {
47 | if ( ! element.$icon ) {
48 | return element;
49 | }
50 |
51 | addClickHandler( `.${ getIconClassName( element.id ) }`, element.handler );
52 | return element;
53 | }
54 |
55 | const iconRepositioner = _.debounce( elements => {
56 | debug( `repositioning ${ elements.length } icons` );
57 | elements.map( positionIcon );
58 | }, 350 );
59 |
60 | export function repositionIcons( elements ) {
61 | iconRepositioner( elements );
62 | }
63 |
64 | export function repositionAfterFontsLoad( elements ) {
65 | iconRepositioner( elements );
66 |
67 | if ( getWindow().document.fonts ) {
68 | getWindow().document.fonts.ready.then( iconRepositioner.bind( null, elements ) );
69 | }
70 | }
71 |
72 | /**
73 | * Toggle icons when customizer toggles preview mode.
74 | */
75 | export function enableIconToggle() {
76 | on( 'cdm-toggle-visible', () => $( '.cdm-icon' ).toggleClass( 'cdm-icon--hidden' ) );
77 | }
78 |
79 | function findOrCreateIcon( element ) {
80 | if ( element.$icon ) {
81 | return element.$icon;
82 | }
83 | const $icon = $( `.${ getIconClassName( element.id ) }` );
84 | if ( $icon.length ) {
85 | return $icon;
86 | }
87 |
88 | const title = getOptions().translations[ element.type ] || `Click to edit the ${ element.title }`;
89 |
90 | return createAndAppendIcon( element.id, element.icon, title );
91 | }
92 |
93 | function getIconClassName( id ) {
94 | return `cdm-icon__${ id }`;
95 | }
96 |
97 | function isTargetHidden( $target ) {
98 | return ( ! $target.is( ':visible' ) || $target.css( 'visibility' ) === 'hidden' || $target.css( 'clip' ) === 'rect(1px 1px 1px 1px)' );
99 | }
100 |
101 | function getCalculatedCssForIcon( element, $target, $icon ) {
102 | const isRTL = ( 'rtl' === getWindow().document.dir );
103 | const position = element.position;
104 | const hiddenIconPos = isRTL ? { right: -1000, left: 'auto' } : { left: -1000, right: 'auto' };
105 |
106 | if ( isTargetHidden( $target ) ) {
107 | debug( `target is not visible when positioning ${ element.id }. I will hide the icon. target:`, $target );
108 | return hiddenIconPos;
109 | }
110 |
111 | const offset = $target.offset();
112 | let top = offset.top;
113 | const left = offset.left;
114 | const right = getWindow().innerWidth - offset.left - $target.outerWidth() - $icon.outerWidth();
115 | let middle = $target.innerHeight() / 2;
116 | let iconMiddle = $icon.innerHeight() / 2;
117 |
118 | if ( top < 0 ) {
119 | debug( `target top offset ${ top } is unusually low when positioning ${ element.id }. I will hide the icon. target:`, $target );
120 | return hiddenIconPos;
121 | }
122 | if ( middle < 0 ) {
123 | debug( `target middle offset ${ middle } is unusually low when positioning ${ element.id }. I will hide the icon. target:`, $target );
124 | return hiddenIconPos;
125 | }
126 | if ( top < 1 ) {
127 | debug( `target top offset ${ top } is unusually low when positioning ${ element.id }. I will adjust the icon downwards. target:`, $target );
128 | top = 0;
129 | }
130 | if ( middle < 1 ) {
131 | debug( `target middle offset ${ middle } is unusually low when positioning ${ element.id }. I will adjust the icon downwards. target:`, $target );
132 | middle = 0;
133 | iconMiddle = 0;
134 | }
135 |
136 | const coords = { top };
137 | if ( isRTL ) {
138 | _.extend( coords, { left: 'auto', right } );
139 | } else {
140 | _.extend( coords, { left, right: 'auto' } );
141 | }
142 |
143 | if ( position === 'middle' ) {
144 | coords.top += middle - iconMiddle;
145 | } else if ( position === 'top-right' ) {
146 | if ( isRTL ) {
147 | // Actually, the icon will be shown at the top left of the target in RTL mode.
148 | coords.right += $target.width() + 70;
149 | } else {
150 | coords.left += $target.width() + 70;
151 | }
152 | }
153 |
154 | return adjustCoordinates( coords );
155 | }
156 |
157 | function adjustCoordinates( coords ) {
158 | const minLeft = 35;
159 | const minRight = 110;
160 |
161 | if ( 'auto' !== coords.left ) {
162 | // Try to avoid overlapping hamburger menus
163 | const maxLeft = getWindow().innerWidth - minRight;
164 | coords.left = clamp( minLeft, coords.left, maxLeft );
165 | } else if ( 'auto' !== coords.right ) {
166 | const maxRight = getWindow().innerWidth - minLeft;
167 | coords.right = clamp( minRight, coords.right, maxRight );
168 | }
169 | return coords;
170 | }
171 |
172 | function clamp( min, value, max ) {
173 | if ( min > value ) {
174 | value = min;
175 | }
176 | if ( max < value ) {
177 | value = max;
178 | }
179 | return value;
180 | }
181 |
182 | function createIcon( id, iconType, title ) {
183 | const iconClassName = getIconClassName( id );
184 | switch ( iconType ) {
185 | case 'headerIcon':
186 | return $( `
${ icons.headerIcon }
` );
187 | default:
188 | return $( `
${ icons.editIcon }
` );
189 | }
190 | }
191 |
192 | function createAndAppendIcon( id, iconType, title ) {
193 | const $icon = createIcon( id, iconType, title );
194 | $( getWindow().document.body ).append( $icon );
195 | return $icon;
196 | }
197 |
198 | function getElementTarget( element ) {
199 | if ( element.$target && ! element.$target.parent().length ) {
200 | // target was removed from DOM, likely by partial refresh
201 | element.$target = null;
202 | }
203 |
204 | return element.$target || $( element.selector );
205 | }
206 |
--------------------------------------------------------------------------------
/src/helpers/messenger.js:
--------------------------------------------------------------------------------
1 | import getAPI from './api';
2 | import debugFactory from 'debug';
3 |
4 | const debug = debugFactory( 'cdm:messenger' );
5 | const api = getAPI();
6 |
7 | function getPreview() {
8 | // wp-admin is previewer, frontend is preview. why? no idea.
9 | return typeof api.preview !== 'undefined' ? api.preview : api.previewer;
10 | }
11 |
12 | export function send( id, data ) {
13 | debug( 'send', id, data );
14 | return getPreview().send( id, data );
15 | }
16 |
17 | export function on( id, callback ) {
18 | debug( 'on', id, callback );
19 | return getPreview().bind( id, callback );
20 | }
21 |
22 | export function off( id, callback = false ) {
23 | debug( 'off', id, callback );
24 | if ( callback ) {
25 | return getPreview().unbind( id, callback );
26 | }
27 | // no callback? Get rid of all of 'em
28 | const topic = getPreview().topics[ id ];
29 | if ( topic ) {
30 | return topic.empty();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/helpers/options.js:
--------------------------------------------------------------------------------
1 | import getWindow from './window';
2 |
3 | export default function getOptions() {
4 | return getWindow()._Customizer_DM;
5 | }
6 |
7 | export function isDisabled( moduleName ) {
8 | return ( -1 !== getOptions().disabledModules.indexOf( moduleName ) );
9 | }
10 |
--------------------------------------------------------------------------------
/src/helpers/record-event.js:
--------------------------------------------------------------------------------
1 | import getWindow from './window';
2 | import { on } from './messenger';
3 | import debugFactory from 'debug';
4 |
5 | const debug = debugFactory( 'cdm:event' );
6 |
7 | export function recordEvent( eventName, props = {} ) {
8 | debug( `recording Tracks event ${ eventName } with props:`, props );
9 | getWindow()._tkq = getWindow()._tkq || [];
10 | getWindow()._tkq.push( [ 'recordEvent', eventName, props ] );
11 | }
12 |
13 | export function bindPreviewEventsListener() {
14 | on( 'recordEvent', data => {
15 | if ( ! data.name || ! data.props ) {
16 | return;
17 | }
18 | recordEvent( data.name, data.props );
19 | } );
20 | }
21 |
--------------------------------------------------------------------------------
/src/helpers/small-screen-preview.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 |
3 | export function isPreviewing() {
4 | // Get truth from DOM. Gross.
5 | return $( '.wp-full-overlay' ).hasClass( 'preview-only' );
6 | }
7 |
8 | export function disablePreview() {
9 | $( '.customize-controls-preview-toggle' ).click();
10 | }
11 |
--------------------------------------------------------------------------------
/src/helpers/user-agent.js:
--------------------------------------------------------------------------------
1 | import getWindow from '../helpers/window';
2 |
3 | export function getUserAgent() {
4 | return getWindow().navigator.userAgent;
5 | }
6 |
7 | export function isSafari() {
8 | return ( !! getUserAgent().match( /Version\/[\d\.]+.*Safari/ ) );
9 | }
10 |
11 | export function isMobileSafari() {
12 | return ( !! getUserAgent().match( /(iPod|iPhone|iPad)/ ) );
13 | }
14 |
--------------------------------------------------------------------------------
/src/helpers/window.js:
--------------------------------------------------------------------------------
1 | let windowObj = null;
2 |
3 | export function setWindow( obj ) {
4 | windowObj = obj;
5 | }
6 |
7 | export default function getWindow() {
8 | if ( ! windowObj && ! window ) {
9 | throw new Error( 'No window object found.' );
10 | }
11 | return windowObj || window;
12 | }
13 |
--------------------------------------------------------------------------------
/src/modules/edit-post-links.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import debugFactory from 'debug';
3 | import getWindow from '../helpers/window';
4 | import { send } from '../helpers/messenger';
5 |
6 | const debug = debugFactory( 'cdm:edit-post-links' );
7 |
8 | export function modifyEditPostLinks( selector ) {
9 | debug( 'listening for clicks on post edit links with selector', selector );
10 | // We use mousedown because click has been blocked by some other JS
11 | $( 'body' ).on( 'mousedown', selector, event => {
12 | getWindow().open( event.target.href );
13 | send( 'recordEvent', {
14 | name: 'wpcom_customize_direct_manipulation_click',
15 | props: { type: 'post-edit' },
16 | } );
17 | } );
18 | }
19 |
20 | export function disableEditPostLinks( selector ) {
21 | debug( 'hiding post edit links with selector', selector );
22 | $( selector ).hide();
23 | }
24 |
--------------------------------------------------------------------------------
/src/modules/focus-callout.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import debugFactory from 'debug';
3 |
4 | const debug = debugFactory( 'cdm:focus-callout' );
5 |
6 | let timeout;
7 |
8 | function addCallout( section, type ) {
9 | // Highlight menu item controls
10 | if ( section && section.container && type === 'menu' ) {
11 | const menuItems = section.container.find( '.customize-control-nav_menu_item' );
12 | if ( menuItems.length ) {
13 | debug( 'highlighting menu item', menuItems );
14 | return callout( menuItems );
15 | }
16 | }
17 |
18 | // Highlight header image "new" button
19 | if ( section && section.btnNew && type === 'header_image' ) {
20 | const button = $( section.btnNew );
21 | if ( button.length ) {
22 | debug( 'highlighting "new" button', button );
23 | return callout( button );
24 | }
25 | }
26 |
27 | // Highlight widget
28 | if ( section && section.container && type === 'widget' ) {
29 | debug( 'highlighting widget container' );
30 | callout( section.container );
31 | // focus the first input, not the stupid toggle
32 | return section.container.find( ':input' ).not( 'button' ).first().focus();
33 | }
34 |
35 | // Highlight whatever is focused
36 | const focused = $( ':focus' );
37 | if ( focused.length ) {
38 | debug( 'highlighting the focused element', focused );
39 | return callout( focused );
40 | }
41 |
42 | debug( 'could not find any focused element to highlight' );
43 | }
44 |
45 | function callout( $el ) {
46 | $el.focus();
47 | $el.addClass( 'cdm-subtle-focus' ).on( 'animationend webkitAnimationEnd', () => {
48 | $el.off( 'animationend webkitAnimationEnd' ).removeClass( 'cdm-subtle-focus' );
49 | } );
50 | }
51 |
52 | export default function focusCallout( section, type ) {
53 | clearTimeout( timeout );
54 | section.focus();
55 | setTimeout( () => addCallout( section, type ), 410 );
56 | }
57 |
--------------------------------------------------------------------------------
/src/modules/focus-listener.js:
--------------------------------------------------------------------------------
1 | import { on } from '../helpers/messenger';
2 | import { isPreviewing, disablePreview } from '../helpers/small-screen-preview';
3 | import focusCallout from './focus-callout';
4 | import { recordEvent } from '../helpers/record-event';
5 | import debugFactory from 'debug';
6 |
7 | const debug = debugFactory( 'cdm:focus-listener' );
8 | const eventMap = {
9 | 'focus-widget-control': 'widget',
10 | 'focus-menu': 'menu',
11 | 'focus-menu-location': 'menu',
12 | };
13 |
14 | export default function addFocusListener( eventName, getControlCallback ) {
15 | on( eventName, makeHandler( eventName, getControlCallback ) );
16 | }
17 |
18 | function makeHandler( eventName, getControlCallback ) {
19 | return function( ...args ) {
20 | const eventTargetId = args[ 0 ];
21 | debug( `received ${ eventName } event for target id ${ eventTargetId }` );
22 | const focusableControl = getControlCallback.apply( getControlCallback, args );
23 |
24 | if ( ! focusableControl ) {
25 | debug( `no control found for event ${ eventName } and args:`, args );
26 | return;
27 | }
28 |
29 | const type = getEventType( eventName, eventTargetId );
30 | recordEvent( 'wpcom_customize_direct_manipulation_click', { type } );
31 |
32 | // If we are in the small screen preview mode, bring back the controls pane
33 | if ( isPreviewing() ) {
34 | debug( 'focusing controls pane' );
35 | disablePreview();
36 | }
37 |
38 | focusCallout( focusableControl, type );
39 | };
40 | }
41 |
42 | function getEventType( eventName, eventTargetId ) {
43 | return eventMap[ eventName ] ? eventMap[ eventName ] : eventTargetId;
44 | }
45 |
--------------------------------------------------------------------------------
/src/modules/focusable.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import getWindow from '../helpers/window';
3 | import getAPI from '../helpers/api';
4 | import { send } from '../helpers/messenger';
5 | import { positionIcon, addClickHandlerToIcon, repositionAfterFontsLoad, enableIconToggle } from '../helpers/icon-buttons';
6 | import debugFactory from 'debug';
7 |
8 | const debug = debugFactory( 'cdm:focusable' );
9 | const api = getAPI();
10 |
11 | /**
12 | * Give DOM elements an icon button bound to click handlers
13 | *
14 | * Accepts an array of element objects of the form:
15 | *
16 | * {
17 | * id: A string to identify this element
18 | * selector: A CSS selector string to uniquely target the DOM element
19 | * type: A string to group the element, eg: 'widget'
20 | * position: (optional) A string for positioning the icon, one of 'top-left' (default), 'top-right', or 'middle' (vertically center)
21 | * icon (optional): A string specifying which icon to use. See options in icon-buttons.js
22 | * handler (optional): A callback function which will be called when the icon is clicked
23 | * }
24 | *
25 | * If no handler is specified, the default will be used, which will send
26 | * `control-focus` to the API with the element ID.
27 | *
28 | * @param {Array} elements - An array of element objects of the form above.
29 | */
30 | export default function makeFocusable( elements ) {
31 | const elementsWithIcons = elements
32 | .reduce( removeDuplicateReducer, [] )
33 | .map( positionIcon )
34 | .map( createHandler )
35 | .map( addClickHandlerToIcon );
36 |
37 | if ( elementsWithIcons.length ) {
38 | startIconMonitor( elementsWithIcons );
39 | enableIconToggle();
40 | }
41 | }
42 |
43 | function makeRepositioner( elements, changeType ) {
44 | return function() {
45 | debug( 'detected change:', changeType );
46 | repositionAfterFontsLoad( elements );
47 | };
48 | }
49 |
50 | /**
51 | * Register a group of listeners to reposition icon buttons if the DOM changes.
52 | *
53 | * See `makeFocusable` for the format of the `elements` param.
54 | *
55 | * @param {Array} elements - The element objects.
56 | */
57 | function startIconMonitor( elements ) {
58 | // Reposition icons after any theme fonts load
59 | repositionAfterFontsLoad( elements );
60 |
61 | // Reposition icons after a few seconds just in case (eg: infinite scroll or other scripts complete)
62 | setTimeout( makeRepositioner( elements, 'follow-up' ), 2000 );
63 |
64 | // Reposition icons after the window is resized
65 | $( getWindow() ).resize( makeRepositioner( elements, 'resize' ) );
66 |
67 | // Reposition icons after the text of any element changes
68 | elements.filter( el => [ 'siteTitle', 'headerIcon' ].indexOf( el.type ) !== -1 )
69 | .map( el => api( el.id, value => value.bind( makeRepositioner( elements, 'title or header' ) ) ) );
70 |
71 | // Reposition icons after custom-fonts change the elements
72 | api( 'jetpack_fonts[selected_fonts]', value => value.bind( makeRepositioner( elements, 'custom-fonts' ) ) );
73 |
74 | // When the widget partial refresh runs, reposition icons
75 | api.bind( 'widget-updated', makeRepositioner( elements, 'widgets' ) );
76 |
77 | // Reposition icons after any customizer setting is changed
78 | api.bind( 'change', makeRepositioner( elements, 'any setting' ) );
79 |
80 | const $document = $( getWindow().document );
81 |
82 | // Reposition after menus updated
83 | $document.on( 'customize-preview-menu-refreshed', makeRepositioner( elements, 'menus' ) );
84 |
85 | // Reposition after scrolling in case there are fixed position elements
86 | $document.on( 'scroll', makeRepositioner( elements, 'scroll' ) );
87 |
88 | // Reposition after page click (eg: hamburger menus)
89 | $document.on( 'click', makeRepositioner( elements, 'click' ) );
90 |
91 | // Reposition after any page changes (if the browser supports it)
92 | const page = getWindow().document.querySelector( '#page' );
93 | if ( page && MutationObserver ) {
94 | const observer = new MutationObserver( makeRepositioner( elements, 'DOM mutation' ) );
95 | observer.observe( page, { attributes: true, childList: true, characterData: true } );
96 | }
97 |
98 | // Support partial update
99 | const partialUpdateHandler = createPartialUpdateHandler( elements );
100 | api.selectiveRefresh.partial.bind( 'add', partialUpdateHandler );
101 | api.selectiveRefresh.partial.each( ( partial ) => {
102 | partial.deferred.ready.done( () => partialUpdateHandler( partial ) );
103 | } );
104 | }
105 |
106 | function createHandler( element ) {
107 | element.handler = element.handler || makeDefaultHandler( element.id );
108 | return element;
109 | }
110 |
111 | function removeDuplicateReducer( prev, el ) {
112 | if ( prev.map( x => x.id ).indexOf( el.id ) !== -1 ) {
113 | debug( `tried to add duplicate element for ${ el.id }` );
114 | return prev;
115 | }
116 | return prev.concat( el );
117 | }
118 |
119 | function makeDefaultHandler( id ) {
120 | return function( event ) {
121 | event.preventDefault();
122 | event.stopPropagation();
123 | debug( 'click detected on', id );
124 | send( 'control-focus', id );
125 | };
126 | }
127 |
128 | function createPartialUpdateHandler( elements ) {
129 | return ( partial ) => {
130 | // Get the elements that refer partial containers.
131 | const elementsToUpdate = elements.filter( element => {
132 | const $container = element.$partialContainer;
133 |
134 | return ( $container && $container.data( 'customize-partial-id' ) === partial.id );
135 | } );
136 |
137 | // Trigger onPartialUpdate on the elements if possible
138 | elementsToUpdate.map( element => {
139 | if ( 'function' === typeof element.onPartialUpdate ) {
140 | element.onPartialUpdate.call( element, partial, element, elements );
141 | }
142 | } );
143 |
144 | // Reposition the icons that are associated with the partial containers.
145 | if ( elementsToUpdate.length > 0 ) {
146 | repositionAfterFontsLoad( elementsToUpdate );
147 | }
148 | };
149 | }
150 |
--------------------------------------------------------------------------------
/src/modules/footer-focus.js:
--------------------------------------------------------------------------------
1 | export function getFooterElements() {
2 | return [
3 | {
4 | id: 'footercredit',
5 | selector: 'a[data-type="footer-credit"]',
6 | type: 'footerCredit',
7 | position: 'middle',
8 | title: 'footer credit',
9 | },
10 | ];
11 | }
12 |
--------------------------------------------------------------------------------
/src/modules/guide-steps.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery';
2 | import getOptions from '../helpers/options';
3 |
4 | let currentStep = 1;
5 |
6 | const smallScreenWidth = 640;
7 |
8 | export function nextStep() {
9 | currentStep++;
10 | $( '#dmguide .dmguide-step' ).html( getStepHtml() );
11 | $( '#dmguide' ).offset( getStepPosition() );
12 | }
13 |
14 | export function isLastStep() {
15 | return currentStep >= getOptions().steps.length;
16 | }
17 |
18 | export function getTotalSteps() {
19 | return getOptions().steps.length;
20 | }
21 |
22 | function getStepData( step ) {
23 | return getOptions().steps[ step - 1 ];
24 | }
25 |
26 | function getCurrentStepData() {
27 | return getStepData( currentStep );
28 | }
29 |
30 | export function getCurrentStep() {
31 | return currentStep;
32 | }
33 |
34 | export function getHtml() {
35 | return `
36 |
${ getStepHtml() }
37 |
`;
38 | }
39 |
40 | export function getStepHtml() {
41 | const stepData = getCurrentStepData();
42 | let html = '
';
43 |
44 | if ( stepData.title ) {
45 | html += `
${ stepData.title }
`;
46 | }
47 |
48 | if ( stepData.content ) {
49 | html += stepData.content;
50 | }
51 |
52 | if ( stepData.smallContent && matchMedia && matchMedia( `screen and (max-width:${ smallScreenWidth }px)` ).matches ) {
53 | html += ' ' + stepData.smallContent;
54 | }
55 |
56 | html += getButtons();
57 |
58 | html += '
The content of the front page template is
110 | displayed here.
111 | This is a great place to add your “call to action” with a brief
112 | message.
113 | The image behind this text is added as a featured image.
114 | If you wish to add a button like below, use a CSS class:
115 | button-minimal.
In the theme options, you can choose whether you want your
175 | sidebar on the right or on the left. Depending on which option you
176 | select, the sidebar will appear on the opposite side.