PluginProbe
codoc / trunk
codoc vtrunk
0.9.8.8 0.9.8.9 0.9.9 0.9.9.1 trunk 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.8.1 0.8.2 0.8.3 0.8.4 0.8.6 0.8.7 0.8.8 0.8.9 0.9 0.9.1 0.9.10 0.9.11 All 114 releases
codoc / src / block / block.js

block.js in codoc trunk, at src/block/block.js

612 lines 20.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * BLOCK: codoc-block
3 *
4 * Registering a basic block with Gutenberg.
5 * Simple block, renders and saves the same content without any interactivity.
6 */
7
8 // Import CSS.
9 import './style.scss';
10 import './editor.scss';
11 import icon from './icon';
12 //import { ENTER } from '@wordpress/keycodes';
13 const { ENTER } = wp.keycodes;
14 const { __ } = wp.i18n; // Import __() from wp.i18n
15 const { Component } = wp.element;
16
17 const { registerBlockType } = wp.blocks; // Import registerBlockType() from wp.blocks
18 const {
19 InspectorControls,
20 RichText
21 } = wp.editor;
22
23 const {
24 SelectControl,
25 PanelBody,
26 ToggleControl,
27 CheckboxControl,
28 RangeControl,
29 TextControl,
30 Notice,
31 } = wp.components;
32
33 const { withSelect } = wp.data;
34
35 const {
36 getDefaultBlockName,
37 createBlock,
38 } = wp.blocks;
39
40 const { withState } = wp.compose;
41
42 const CODOC_URL = OPTIONS.codoc_url;
43 const CODOC_USER_CODE = OPTIONS.codoc_usercode;
44 const CODOC_PLUGIN_VERSION = OPTIONS.codoc_plugin_version;
45 const CODOC_ENTRIES_CLASS = 'codoc-entries';
46 const CODOC_ACCOUNT_IS_PRO = OPTIONS.codoc_account_is_pro;
47 const CODOC_CURRENCY_CODE = OPTIONS.codoc_currency_code;
48 const CODOC_CURRENCY_DECIMAL_PLACES = OPTIONS.codoc_currency_decimal_places;
49
50 //import Cookies from 'universal-cookie';
51 //const cookies = new Cookies();
52
53 const CODOC_MAX_LENGTHS = OPTIONS.codoc_max_lengths || {};
54
55 // codoc ブロックの直前/直後で本文を分割
56 function splitCodocContent(content) {
57 const blockRegex = /<!--\s*wp:codoc\/codoc-block[\s\S]*?<!--\s*\/wp:codoc\/codoc-block\s*-->/;
58 const splited = (content || '').split(blockRegex);
59 return {
60 bodyFree: splited[0] || '',
61 bodyPaywalled: splited.slice(1).join('') || '',
62 };
63 }
64
65 function buildLengthErrors(title, bodyFree, bodyPaywalled) {
66 const errors = [];
67 const total = bodyFree.length + bodyPaywalled.length;
68 if (CODOC_MAX_LENGTHS.title && title.length > CODOC_MAX_LENGTHS.title) {
69 errors.push(__('Title exceeds the maximum length of', 'codoc') + ' ' + CODOC_MAX_LENGTHS.title + ' (' + title.length + ')');
70 }
71 if (CODOC_MAX_LENGTHS.body_free && bodyFree.length > CODOC_MAX_LENGTHS.body_free) {
72 errors.push(__('Free area exceeds the maximum length of', 'codoc') + ' ' + CODOC_MAX_LENGTHS.body_free + ' (' + bodyFree.length + ')');
73 }
74 if (CODOC_MAX_LENGTHS.body_paywalled && bodyPaywalled.length > CODOC_MAX_LENGTHS.body_paywalled) {
75 errors.push(__('Paid area exceeds the maximum length of', 'codoc') + ' ' + CODOC_MAX_LENGTHS.body_paywalled + ' (' + bodyPaywalled.length + ')');
76 }
77 if (CODOC_MAX_LENGTHS.body && total > CODOC_MAX_LENGTHS.body) {
78 errors.push(__('Total body exceeds the maximum length of', 'codoc') + ' ' + CODOC_MAX_LENGTHS.body + ' (' + total + ')');
79 }
80 return errors;
81 }
82
83 // codoc ブロック選択時の Inspector サイドバーに警告を表示する
84 const CodocLengthNotices = withSelect((select) => {
85 const editor = select('core/editor');
86 return {
87 postTitle: editor ? editor.getEditedPostAttribute('title') : '',
88 postContent: editor ? editor.getEditedPostAttribute('content') : '',
89 };
90 })(({ postTitle, postContent }) => {
91 const { bodyFree, bodyPaywalled } = splitCodocContent(postContent);
92 const errors = buildLengthErrors(postTitle || '', bodyFree, bodyPaywalled);
93 if (errors.length === 0) {
94 return null;
95 }
96 return (
97 <Notice status="error" isDismissible={ false }>
98 <strong>{ __('codoc length limit exceeded:', 'codoc') }</strong>
99 <ul style={{ marginTop: '0.5em', marginBottom: 0 }}>
100 { errors.map((e, i) => <li key={i}>{ e }</li>) }
101 </ul>
102 </Notice>
103 );
104 });
105
106 // codoc ブロックを選択していなくても、本文中に存在すればエディタ上部に通知を表示する
107 (function() {
108 if (!wp.data || !wp.data.subscribe) {
109 return;
110 }
111 const NOTICE_ID = 'codoc-length-limit';
112 let lastState = '';
113 wp.data.subscribe(function() {
114 const editor = wp.data.select('core/editor');
115 const blockEditor = wp.data.select('core/block-editor') || wp.data.select('core/editor');
116 const noticesDispatch = wp.data.dispatch('core/notices');
117 if (!editor || !blockEditor || !noticesDispatch) {
118 return;
119 }
120 const blocks = blockEditor.getBlocks ? blockEditor.getBlocks() : [];
121 const hasCodocBlock = blocks.some(function(b) { return b && b.name === 'codoc/codoc-block'; });
122 if (!hasCodocBlock) {
123 if (lastState !== '') {
124 noticesDispatch.removeNotice(NOTICE_ID);
125 lastState = '';
126 }
127 return;
128 }
129 const title = editor.getEditedPostAttribute('title') || '';
130 const content = editor.getEditedPostAttribute('content') || '';
131 const { bodyFree, bodyPaywalled } = splitCodocContent(content);
132 const errors = buildLengthErrors(title, bodyFree, bodyPaywalled);
133 const state = errors.join('||');
134 if (state === lastState) {
135 return;
136 }
137 lastState = state;
138 noticesDispatch.removeNotice(NOTICE_ID);
139 if (errors.length > 0) {
140 noticesDispatch.createNotice(
141 'error',
142 __('codoc length limit exceeded:', 'codoc') + ' ' + errors.join(' / '),
143 { id: NOTICE_ID, isDismissible: false }
144 );
145 }
146 });
147 })();
148
149 class CodocControls extends Component {
150 constructor( props ) {
151 super( ...arguments );
152 this.subscriptionsFetched = [];
153 this.subscriptionSearchTerm = '';
154 this.state = {
155 searchTerm: '',
156 filteredSubscriptions: []
157 };
158 this.fetchSubscriptions();
159 this.handleSearchChange = this.handleSearchChange.bind(this);
160 }
161
162 fetchSubscriptions(searchTitle = '') {
163 const {
164 setAttributes,
165 attributes: {
166 fetching,
167 }
168 } = this.props;
169
170 if (fetching) {
171 return
172 }
173
174 setAttributes( { fetching: true } )
175 let url = CODOC_URL + '/api/v1/cms/' + CODOC_USER_CODE + '/subscriptions?without_token=1';
176
177 // Add title parameter if search term exists
178 if (searchTitle) {
179 url += '&title=' + encodeURIComponent(searchTitle);
180 }
181
182 fetch(url)
183 .then( res => res.json() )
184 .then( res => {
185 let list = [];
186 if (res.status && res.subscriptions) {
187 for ( var i = 0; i < res.subscriptions.length; i++ ) {
188 let info = {
189 value: res.subscriptions[i].code,
190 label: res.subscriptions[i].title,
191 term: res.subscriptions[i].term,
192 price: res.subscriptions[i].price,
193 currency: res.subscriptions[i].currency
194 }
195 list[i] = info
196 }
197 }
198 if (list.length || searchTitle) {
199 this.subscriptionsFetched = list;
200 this.setState({ filteredSubscriptions: list });
201 }
202 setAttributes( { fetching: false } )
203 })
204 }
205
206 handleSearchChange(value) {
207 this.setState({ searchTerm: value });
208
209 // Debounce the API call
210 clearTimeout(this.searchTimeout);
211 this.searchTimeout = setTimeout(() => {
212 this.fetchSubscriptions(value);
213 }, 500);
214 }
215
216 getShowPriceHelp( checked ) {
217 return checked ?
218 __( 'Enable','codoc' ) :
219 __( 'Disable','codoc' );
220 }
221 getShowSupportHelp( checked ) {
222 return checked ?
223 __( 'Accept' ,'codoc') :
224 __( 'Do not accept' ,'codoc');
225 }
226 getShowPaywalledSupportHelp( checked ) {
227 return checked ?
228 __( 'The paid part will be hidden upon access, but will be displayed after tapping "Pay to Read". Viewers can freely specify the amount to purchase based on the content. Please note that there may be cases where it is not purchased.' ,'codoc') :
229 __( ' ' );
230 }
231 getStatusLimitedHelp( checked ) {
232 return checked ?
233 __( 'Unlisted' ,'codoc') :
234 __( 'Published' ,'codoc');
235 }
236
237 render() {
238 const {
239 setAttributes,
240 attributes: {
241 showPrice,
242 price,
243 limited,
244 limitedCount,
245
246 affiliateMode,
247 affiliateRate,
248
249 showSupport,
250 showPaywalledSupport,
251 statusLimited,
252 subscriptions,
253 }
254 } = this.props;
255
256 const toggleShowPrice = () => setAttributes( { showPrice: ! showPrice } );
257 const toggleLimited = () => setAttributes( { limited: ! limited } );
258 const toggleShowPaywalledSupport = () => setAttributes( { showPaywalledSupport: ! showPaywalledSupport } );
259 const toggleStatusLimited = () => setAttributes( { statusLimited: ! statusLimited } );
260 const toggleAffiliateMode = () => setAttributes( { affiliateMode: ! affiliateMode } );
261 const toggleShowSupport = () => setAttributes( { showSupport: ! showSupport } );
262
263 const SubscriptionCheckBoxes = withState({
264 checked_obj: Object.assign({}, subscriptions)
265 })( ({ checked_obj, setState }) => (
266 <div>
267 {this.state.filteredSubscriptions.length === 0 ? (
268 <p style={{ fontStyle: 'italic', color: '#666' }}>
269 { this.state.searchTerm
270 ? __('No plans found matching your search.','codoc')
271 : __('No reader plans available.','codoc')
272 }
273 </p>
274 ) : (
275 <ul>
276 {
277 this.state.filteredSubscriptions.map((v) => {
278 const isChecked = !!checked_obj[v.value];
279 const checkedCount = Object.keys(checked_obj).length;
280
281 return (
282 <li key={v.value}>
283 <CheckboxControl
284 className="check_items"
285 label={v.label}
286 checked={isChecked}
287 onChange={(check) => {
288 // 追加する場合、5個以上は無効
289 if (check && checkedCount >= 5) {
290 return; // これ以上チェックできない
291 }
292
293 const newChecked = { ...checked_obj };
294
295 if (check) {
296 newChecked[v.value] = true;
297 } else {
298 delete newChecked[v.value];
299 }
300
301 setAttributes({ subscriptions: newChecked });
302 setState({ checked_obj: newChecked });
303 }}
304 />
305 </li>
306 );
307 })
308 }
309 </ul>
310 )}
311 </div>
312 ) )
313 const affiliateRateOptions = [
314 { value: '0.0500', label:'5%' },
315 { value: '0.1000', label:'10%' },
316 { value: '0.1500', label:'15%' },
317 { value: '0.2000', label:'20%' },
318 { value: '0.2500', label:'25%' },
319 { value: '0.3000', label:'30%' },
320 { value: '0.3500', label:'35%' },
321 { value: '0.4000', label:'40%' },
322 { value: '0.4500', label:'45%' },
323 { value: '0.5000', label:'50%' },
324 ];
325
326 return(
327 <InspectorControls key="CodocControls">
328
329 <CodocLengthNotices />
330
331 <PanelBody>
332
333 <ToggleControl
334 label={ __( 'individual sale','codoc' ) }
335 checked={ !! showPrice }
336 help={ this.getShowPriceHelp }
337 ref="showPrice"
338 onChange={ toggleShowPrice }
339 />
340
341 <div style={{ display: showPrice ? 'initial' : 'none' }}>
342 <RangeControl
343 label={ __('price','codoc') + ' ' + (CODOC_CURRENCY_DECIMAL_PLACES > 0 ? (price / 100).toFixed(CODOC_CURRENCY_DECIMAL_PLACES) : price) + ' ' + __(CODOC_CURRENCY_CODE) + __(showPaywalledSupport ? '' + __('Displayed as the your suggested price','codoc') + '' : '' ) }
344 value={ price }
345 initialPosition={ price }
346 onChange={ ( value ) => {
347 // do validate this
348 setAttributes({ price: value });
349 }}
350 min="100"
351 max={ CODOC_ACCOUNT_IS_PRO == 1 ? 100000 : 50000 }
352 /><br />
353
354 <ToggleControl
355 label={ __( 'Pay-what-you-want','codoc' ) }
356 checked={ !! showPaywalledSupport }
357 help={ this.getShowPaywalledSupportHelp }
358 onChange={ toggleShowPaywalledSupport }
359 />
360
361 <div style={{ display: showPaywalledSupport ? 'none' : 'initial' }}>
362 <ToggleControl
363 label={ __( 'Limited quantity sale','codoc' ) }
364 checked={ !! limited }
365 onChange={ toggleLimited }
366 />
367
368 <div style={{ display: limited ? 'initial' : 'none' }}>
369 <RangeControl
370 label={ __( 'Limited quantity','codoc' ) }
371 value={ limitedCount }
372 onChange={ ( value ) => {
373 // do validate this
374 setAttributes({ limitedCount: value });
375 }}
376 initialPosition={ limitedCount }
377 min="0"
378 max="100"
379 /><br />
380 </div>
381
382 <ToggleControl
383 label={ __( 'Affiliate','codoc' ) }
384 checked={ !! affiliateMode }
385 onChange={ toggleAffiliateMode }
386 />
387
388 <div style={{ display: affiliateMode ? 'initial' : 'none' }}>
389 <SelectControl
390 label={ __( 'Rate' , 'codoc') }
391 description={ __( 'You can offer affiliate marketing at a specified rate for purchasers.', 'codoc') }
392 options={ affiliateRateOptions }
393 value={ affiliateRate }
394 onChange={ ( value ) => {
395 setAttributes( { affiliateRate: value } );
396 }}/><br />
397 </div>
398
399 </div>
400 </div>
401
402 <div style={{ display: showPaywalledSupport ? 'none' : 'initial' }}>
403 <ToggleControl
404 label={ __( 'Tipping', 'codoc') }
405 checked={ !! showSupport }
406 value="1"
407 onChange={ toggleShowSupport }
408 help={ this.getShowSupportHelp }
409 />
410 </div>
411
412 <div style={{ display: 'initial' }}>
413 <ToggleControl
414 label={ __( 'Unlisted','codoc' ) }
415 checked={ !! statusLimited }
416 value="1"
417 onChange={ toggleStatusLimited }
418 help={ this.getStatusLimitedHelp }
419 />
420 </div>
421
422 <div class="codoc-subscription-title">
423 <label>
424 { __('Reader Plans','codoc') }
425 </label>
426 <a
427 href={ CODOC_URL + '/me/subscriptions'}
428 target="_blank" class="codoc-subscription-add"
429 >
430 { __('Add','codoc') }
431 </a>
432 </div>
433
434 <TextControl
435 label={ __('Search Plans','codoc') }
436 value={ this.state.searchTerm }
437 onChange={ this.handleSearchChange }
438 placeholder={ __('Enter title to search...','codoc') }
439 />
440
441 <SubscriptionCheckBoxes />
442
443
444 </PanelBody>
445 </InspectorControls>
446 );
447 }
448 };
449
450 class EditBlockContent extends Component {
451 constructor() {
452 super( ...arguments );
453 // デフォルト値
454 const bd = OPTIONS.codoc_block_defaults || {};
455 const d = (key, fallback) => bd.hasOwnProperty(key) ? bd[key] : fallback;
456 this.props.setAttributes({ version: CODOC_PLUGIN_VERSION });
457 this.props.setAttributes({ showPrice: this.props.attributes.showPrice == null ? d('showPrice', true) : this.props.attributes.showPrice });
458 this.props.setAttributes({ price: this.props.attributes.price == null ? d('price', 500) : this.props.attributes.price });
459 this.props.setAttributes({ limited: this.props.attributes.limited == null ? d('limited', false) : this.props.attributes.limited });
460 this.props.setAttributes({ limitedCount: this.props.attributes.limitedCount == null ? d('limitedCount', 10) : this.props.attributes.limitedCount });
461 this.props.setAttributes({ affiliateMode: this.props.attributes.affiliateMode == null ? d('affiliateMode', false) : this.props.attributes.affiliateMode });
462 this.props.setAttributes({ affiliateRate: this.props.attributes.affiliateRate == null ? d('affiliateRate', '0.0500') : this.props.attributes.affiliateRate });
463 this.props.setAttributes({ showSupport: this.props.attributes.showSupport == null ? d('showSupport', false) : this.props.attributes.showSupport });
464 this.props.setAttributes({ showPaywalledSupport: this.props.attributes.showPaywalledSupport == null ? d('showPaywalledSupport', false) : this.props.attributes.showPaywalledSupport });
465 this.props.setAttributes({ statusLimited: this.props.attributes.statusLimited == null ? d('statusLimited', false) : this.props.attributes.statusLimited });
466 this.props.setAttributes({ subscriptions: this.props.attributes.subscriptions == null ? d('subscriptions', {}) : this.props.attributes.subscriptions });
467
468 this.onChangeInput = this.onChangeInput.bind( this );
469 this.onKeyDown = this.onKeyDown.bind( this );
470 this.state = {
471 defaultText: __( 'To continue reading, ...','codoc' ),
472 };
473 }
474 onChangeInput( event ) {
475 this.setState( {
476 defaultText: '',
477 } );
478
479 const value = event.target.value.length === 0 ? undefined : event.target.value;
480 this.props.setAttributes( { customText: value } );
481 }
482 onKeyDown( event ) {
483 const { keyCode } = event;
484 const { insertBlocksAfter } = this.props;
485 if ( keyCode === ENTER ) {
486 insertBlocksAfter( [ createBlock( getDefaultBlockName() ) ] );
487 }
488 }
489
490 render() {
491 const {
492 attributes: {
493 customText,
494 },
495 setAttributes
496 } = this.props;
497
498 const { defaultText } = this.state;
499 //const value = customText !== undefined ? customText : defaultText;
500 const value = customText ? customText : defaultText;
501 //const value = defaultText;
502 const inputLength = value.length + 10;
503
504 return[
505 <CodocControls { ...{setAttributes, ...this.props } } />,
506
507 <div className="wp-block-more">
508 <input
509 type="text"
510 value={ value }
511 size={ inputLength }
512 onChange={ this.onChangeInput }
513 onKeyDown={ this.onKeyDown }
514 />
515 </div>
516
517 ];
518 }
519 };
520
521
522 registerBlockType( 'codoc/codoc-block', {
523 title: __( 'codoc' ,'codoc'),
524 description: __( 'The area from this block down is a paid area that only authenticated codoc users can view.','codoc' ),
525 icon,
526 category: 'layout',
527 supports: {
528 customClassName: false,
529 className: false,
530 html: false,
531 multiple: false,
532 },
533 keywords: [
534 __( 'codoc Block' ,'codoc'),
535 __( 'plugin for codoc' ,'codoc'),
536 __( 'codoc-block' ,'codoc'),
537 ],
538 attributes: {
539 showPrice: {
540 type: 'boolean',
541 default: null,
542 },
543 price: {
544 type: 'number',
545 default: null,
546 },
547 limited: {
548 type: 'boolean',
549 default: null,
550 },
551 limitedCount: {
552 type: 'number',
553 default: null,
554 },
555 affiliateMode: {
556 type: 'boolean',
557 default: null,
558 },
559 affiliateRate: {
560 type: 'string',
561 default: null,
562 },
563 showSupport: {
564 type: 'boolean',
565 default: null,
566 },
567 showPaywalledSupport: {
568 type: 'boolean',
569 default: null,
570 },
571 statusLimited: {
572 type: 'boolean',
573 default: null,
574 },
575 subscriptions: {
576 type: 'object',
577 default: null,
578 },
579 customText: {
580 type: 'string',
581 default: '',
582 },
583 version: {
584 type: 'string',
585 default: null,
586 },
587 },
588
589 edit: EditBlockContent,
590
591 save: function( props ) {
592 const {
593 setAttributes,
594 attributes: {
595 customText,
596 }
597 } = props;
598 return ( // return if link behavior normal
599 <div
600 data-id='codoc-tag'
601 className={ CODOC_ENTRIES_CLASS }
602 >
603 { customText && !! customText.length && (
604 <RichText.Content
605 value={ customText }
606 />
607 )}
608 </div>
609 )
610 },
611 } );
612