PluginProbe
codoc / 0.9.61
codoc v0.9.61
0.9.61 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 All 115 releases
codoc / src / block / block.js

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

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