/
tabs.opa
619 lines (548 loc) · 19.4 KB
/
tabs.opa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
/*
Copyright © 2011 MLstate
This file is part of OPA.
OPA is free software: you can redistribute it and/or modify it under the
terms of the GNU Affero General Public License, version 3, as published by
the Free Software Foundation.
OPA is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
more details.
You should have received a copy of the GNU Affero General Public License
along with OPA. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* A configurable tabs widget.
*
* @author Frederic Ye, 2010
* @category WIDGET
* @destination PUBLIC
* @stability EXPERIMENTAL
* @version 0.7
*/
/*
* {1 About this module}
*
* WTabs widget
* Using WHList widget
* WTabs are closable and duplicatable compared to basic hlists
*
* {1 Where should I start?}
*
* If you simply want a tabs widgets, use [WTabs.html]
*
* {1 Recommandations}
*
* Once a tab is added, you can add a custom_data to recognize the added tab
* This custom data is a string for the moment
*
* {1 What if I need more?}
*
* {1 TODO (ideas) }
*
* - Handle recursive tabs list (list(list(...(tab)))) for hierarchical tabs initilialization
* @see Opera 11 / Firefox Tab Kit
* - Choose vertical / horizontal styler
* - Drag&Drop
* - Content handling (components/tabs_ext.opa ?)
*
* {1 Developers Notes}
*
* v0.5 was richer and not using hlist, so if you want to take a look, you can find it in GIT
* stop_propagation is usefull because the tab will be selected otherwise...
*/
import stdlib.web.client
import stdlib.widgets.deprecated.hlist
import stdlib.widgets.core
DEBUG = false // sould be replaced by something usable at runtime for debugging...
type WTabs.pos = WHList.pos
/**
* A tab content
*
* [title] the title to use for the link (mouseover usually)
* [value] the content value
*/
type WTabs.content = WHList.item_content
/**
* A tab state
*/
type WTabs.state = WHList.item_state
/**
* A tab properties
*/
type WTabs.properties = {
base_properties: WHList.item_properties
closable: bool
duplicatable: bool
}
/**
* A tab
*/
type WTabs.tab = {
content: int -> WTabs.content
state: WTabs.state
properties: WTabs.properties
custom_data: option(string)
remove_text: option((string, xhtml))
duplicate_text: option((string, xhtml))
styler: WStyler.styler
}
type WTabs.stylers = {
tabs: WStyler.styler
tab: WStyler.styler
tab_content: WStyler.styler
tab_sons: WStyler.styler
tab_selectable: WStyler.styler
tab_checkable: WStyler.styler
tab_no_sons: WStyler.styler
tab_closable: WStyler.styler
tab_duplicatable: WStyler.styler
tab_close: WStyler.styler
tab_duplicate: WStyler.styler
tab_add: WStyler.styler
}
/**
* The widget configuration
*/
type WTabs.config = {
prefix_class: option(string);
on_select: int, WTabs.tab -> bool // tab clicked
on_add: int, WTabs.tab -> option(WTabs.tab) // tab added
on_remove: int, WTabs.tab -> bool // tab removed
on_duplicate: int, WTabs.tab, int, WTabs.tab -> option(WTabs.tab) // tab duplicated
add_text: option((string, xhtml)) // if none, does not display the button
remove_text: option((string, xhtml)) // if none, does not display the button
duplicate_text: option((string, xhtml)) // if none, does not display the button
new_tab_content: int -> WTabs.content // content to use for a new tab
select_new_tab: bool // select the new tab at creation
insert_pos: WTabs.pos // tab insert position
delete_pos: WTabs.pos // tab to select on delete
duplicate_pos: WTabs.pos // tab duplicate position
new_tab_closable: bool // is new tab closable by default
new_tab_duplicatable: bool // is new tab duplicatable by default
max_content_chars: option(int) // max chars in a content (dynamic)
max_tabs_number: option(int) // max number of tabs (in one level)
stylers: WTabs.stylers
}
WTabs = {{
/*
* {1 ID shortcuts}
*/
gen_id(prefix_id:string, suffix:string) =
"{prefix_id}_tabs{suffix}"
tabs_id(prefix_id:string) =
gen_id(prefix_id, "")
tabs_list_id(prefix_id:string) =
gen_id(prefix_id, "_list")
counter_id(prefix_id:string) =
gen_id(prefix_id, "_counter")
number_id(prefix_id:string) =
gen_id(prefix_id, "_number")
selected_id(prefix_id:string) =
gen_id(prefix_id, "_selected")
tab_id(prefix_id, num) =
gen_id(prefix_id, "_tab_{num}")
edit_id(prefix_id, num) =
gen_id(prefix_id, "_edit_{num}")
/*
* {1 Class shortcuts}
*/
gen_class(prefix_class:string, suffix:string) =
"{prefix_class}_tabs{suffix}"
// tab_closable_class(prefix_id:string) =
// gen_class(prefix_id, "_tab_closable")
// tab_duplicatable_class(prefix_id:string) =
// gen_class(prefix_id, "_tab_duplicatable")
// tab_custom_class_data(prefix_id:string) =
// gen_class(prefix_id, "_tab_custom_data")
tab_close_class(prefix_class:string) =
gen_class(prefix_class, "_tab_close")
tab_duplicate_class(prefix_class:string) =
gen_class(prefix_class, "_tab_duplicate")
/*
* {1 Config}
*/
to_hlist_config(config:WTabs.config) = {
WHList.default_config with
prefix_class = config.prefix_class
helper = {
stringify = (num, _tab) -> "{num}"
father = _ -> none
}
on_add = key, item ->
match key with
| ~{some=(num, tab)} ->
match config.on_add(num, tab) with
| ~{some} -> Option.some(((num, some), item))
| {none} -> Option.none
end
| {none} -> Option.none
on_select = (num, tab) -> config.on_select(num, tab)
on_remove = (num, tab) -> config.on_remove(num, tab)
insert_pos = config.insert_pos
delete_pos = config.delete_pos
stylers = {
hlist = config.stylers.tabs
ul = WStyler.make_style(css {list-style:{{style=none style_position=none}};})
item = config.stylers.tab
item_content = config.stylers.tab_content
item_sons = config.stylers.tab_sons
item_selectable = config.stylers.tab_selectable
item_checkable = config.stylers.tab_checkable
item_no_sons = config.stylers.tab_no_sons
}
}
print_pos(pos:option(int)) =
if DEBUG then
match pos with
| ~{some} -> jlog("{some}")
| {none} -> void
else void
/*
* {1 Constructors}
*/
// Content //
make_constant_content(s:string) = { title=s value=Xhtml.of_string(s) }
// Tab //
make_tab(content:(int->WTabs.content),
selected:bool, closable:bool, duplicatable:bool,
custom_data:option(string),
remove_text:option((string, xhtml)), duplicate_text:option((string, xhtml))) = {
content = content
state = {
selected = selected
checked = false
}
properties = {
base_properties = {
selectable = true
checkable = false
no_sons = true
} : WHList.item_properties
closable = closable
duplicatable = duplicatable
}
custom_data = custom_data
remove_text = remove_text
duplicate_text = duplicate_text
styler = WStyler.empty
} : WTabs.tab
no_tab() = make_tab((_->make_constant_content("")), false, false, false, none, none, none)
// Config
/* Lambda-lifted default actions */
default_action = _num, _tab -> true
default_action_option = _num, tab -> some(tab)
default_action_duplicate = _num, _tab, _new_num, new_tab -> some(new_tab)
default_action_new_tab = _ -> {title="Tab" value=Xhtml.of_string("Tab")}
default_config = {
prefix_class = none
on_select = default_action
on_add = default_action_option
on_remove = default_action
on_duplicate = default_action_duplicate
add_text = some(("Add", Xhtml.of_string("Add")))
remove_text = some(("Remove", Xhtml.of_string("X")))
duplicate_text = some(("Duplicate", Xhtml.of_string("D")))
new_tab_content = default_action_new_tab
select_new_tab = true
insert_pos = {at_end}
delete_pos = {after_select}
duplicate_pos = {after_select}
new_tab_closable = true
new_tab_duplicatable = true
max_content_chars = none
max_tabs_number = none
stylers = {
tabs = WStyler.empty
tab = WStyler.empty
tab_content = WStyler.empty
tab_sons = WStyler.empty
tab_selectable = WStyler.empty
tab_checkable = WStyler.empty
tab_no_sons = WStyler.empty
tab_closable = WStyler.empty
tab_duplicatable = WStyler.empty
tab_close = WStyler.empty
tab_duplicate = WStyler.empty
tab_add = WStyler.empty
}
} : WTabs.config
default_config_with_css(css_prefix:string) = { default_config with
prefix_class = some(css_prefix)
}
/*
* {1 XHTML reading / writing}
*/
// Global tabs infos // @private //
extract_counter(prefix_id:string) =
match Parser.int(Dom.get_text(#{counter_id(prefix_id)})) with
| ~{some} -> some
| {none} -> 0
set_counter(prefix_id:string, nb:int) =
Dom.transform([#{counter_id(prefix_id)} <- nb])
extract_number(prefix_id:string) =
match Parser.int(Dom.get_text(#{number_id(prefix_id)})) with
| ~{some} -> some
| {none} -> 0
set_number(prefix_id:string, nb:int) =
Dom.transform([#{number_id(prefix_id)} <- nb])
// Tab // @private //
extract_tab_num(s:string) =
list = String.explode("_", s)
if List.length(list) > 0 then
Parser.int(List.max(list))
else none
// tab_closable_class_option(prefix_id, tab) =
// if tab.properties.closable then some(tab_closable_class(prefix_id))
// else none
// is_closable(prefix_id, id) =
// Dom.has_class(#{id}, tab_class_closable(prefix_id))
// tab_class_duplicatable_option(prefix_id, tab) =
// if tab.properties.duplicatable then some(tab_class_duplicatable(prefix_id))
// else none
// is_duplicatable(prefix_id, id) =
// Dom.has_class(#{id}, tab_class_duplicatable(prefix_id))
// extract_custom_data(prefix_id, num) =
// id = tab_id(prefix_id, num)
// jq = #{"{id} > ul > li.{tab_class_custom_data(prefix_id)}:first"}
// if Dom.length(jq) > 0 then
// some(Dom.get_text(jq))
// else none
/*
* {1 Actions}
*/
/**
* Create the "content" of a tab, that means the content and close button
*
* @param config the widget configuration
* @param prefix_id the prefix id to use
* @param tab the tab to use for content creation
* @return The XHTML corresponding to a tab content
*/
@private
create_tab_content(config:WTabs.config, prefix_id:string, num:int, tab:WTabs.tab) =
prefix_class = WCore.compute_prefix_class(config, prefix_id)
ocr = _event -> if tab.properties.closable then remove_tab(config, prefix_id, num, tab) else void
ocd = _event -> if tab.properties.duplicatable then duplicate_tab(config, prefix_id, num, tab) else void
<>
<span>{tab.content(num).value}</span>
{if tab.properties.duplicatable then
match tab.duplicate_text with
| ~{some=(title, duplicate)} ->
<a class={[tab_duplicate_class(prefix_class)]} title={title} onclick={ocd} options:onclick="stop_propagation">{duplicate}</a>
|> WStyler.add(config.stylers.tab_duplicate, _)
| {none} -> <></>
else <></>}
{if tab.properties.closable then
match tab.remove_text with
| ~{some=(title, remove)} ->
<a class={[tab_close_class(prefix_class)]} title={title} onclick={ocr} options:onclick="stop_propagation">{remove}</a>
|> WStyler.add(config.stylers.tab_close, _)
| {none} -> <></>
else <></>}
</>
// {if Option.is_some(tab.custom_data) then
// <li style="display:none" class={[tab_class_custom_data(prefix_id)]}>{Option.get(tab.custom_data)}</li>
// else <></>}
/**
* Select a tab
* if tab.num = 0 then unselect only
*
* @param config the widget configuration
* @param prefix_id the prefix id to use
* @param tab the tab to select
*/
select_tab(config:WTabs.config, prefix_id:string, num:int, tab:WTabs.tab, notify:bool) =
hlist_config = to_hlist_config(config)
_ = WHList.select_item(hlist_config, prefix_id, (num, tab), notify)
void
/**
* Insert a tab at a meaningful position
* It will use default values if content or position are given
*
* @param config the widget configuration
* @param prefix_id the prefix id to use
* @param num the tab numerical identifier
* @param tab the tab to insert
* @param sel_key consider this optional (num, tab) as selected
* @param notify
*/
@private
internal_insert_tab(config:WTabs.config, prefix_id:string, num:int, tab:WTabs.tab, pos:WTabs.pos, _sel_key:option((int, WTabs.tab)), notify:bool) =
tab_content = create_tab_content(config, prefix_id, num, tab)
hlist_config = { to_hlist_config(config) with insert_pos = pos }
item = WHList.make_item({tab.content(num) with value=tab_content},
{selected=tab.state.selected; checked=tab.state.checked},
{selectable=tab.properties.base_properties.selectable;
no_sons=tab.properties.base_properties.no_sons;
checkable=tab.properties.base_properties.checkable})
_ = WHList.insert_item(hlist_config, prefix_id, (num, tab), item, none, notify)
// do set_number(prefix_id, extract_number(prefix_id) + 1)
do set_counter(prefix_id, extract_counter(prefix_id) + 1)
void
/**
* Insert a tab with the behaviour defined in the configuration
* Will compute the id
*
* @param config the widget configuration
* @param prefix_id the prefix id to use
* @param tab the tab to insert
*/
insert_tab(config:WTabs.config, prefix_id:string, tab:WTabs.tab) =
id = extract_counter(prefix_id) + 1
internal_insert_tab(config, prefix_id, id, tab, config.insert_pos, none, true)
/**
* Add a tab with the behaviour defined in the configuration
*
* @param config the widget configuration
* @param prefix_id the prefix id to use
*/
add_tab(config:WTabs.config, prefix_id:string) =
tab = make_tab(config.new_tab_content, config.select_new_tab, config.new_tab_closable, config.new_tab_duplicatable, none, config.remove_text, config.duplicate_text)
insert_tab(config, prefix_id, tab)
/**
* Duplicate a tab with the behaviour defined in the configuration
*
* @param config the widget configuration
* @param prefix_id the prefix id to use
* @param num the tab numerical identifier
* @param tab the tab to duplicate
*/
duplicate_tab(config:WTabs.config, prefix_id:string, num:int, tab:WTabs.tab) =
id = extract_counter(prefix_id) + 1
new_tab = make_tab(tab.content, config.select_new_tab, tab.properties.closable, tab.properties.duplicatable, none, tab.remove_text, tab.duplicate_text)
match config.on_duplicate(num, tab, id, new_tab) with
| ~{some} -> internal_insert_tab(config, prefix_id, id, some, config.duplicate_pos, none, true)
| {none} -> void
/**
* Replace a tab content with a new one with possible new properties (stays at the same position)
*
* @param config the widget configuration
* @param prefix_id the prefix id to use
* @param num the tab numerical identifier
* @param tab the tab to use for replacement
*/
replace_tab_content(config:WTabs.config, prefix_id:string, num:int, tab:WTabs.tab) =
tab_content = create_tab_content(config, prefix_id, num, tab)
hlist_config = to_hlist_config(config)
item = WHList.make_item({tab.content(num) with value=tab_content},
{selected=tab.state.selected; checked=tab.state.checked},
{selectable=tab.properties.base_properties.selectable;
no_sons=tab.properties.base_properties.no_sons;
checkable=tab.properties.base_properties.checkable})
_ = WHList.replace_item_content(hlist_config, prefix_id, (num, tab), item)
void
/**
* Remove a tab
* {at_select} or {after_select} do not change the selected tab
* otherwise, we simulate a selection change
*
* @param config the widget configuration
* @param prefix_id the prefix id to use
* @param num the tab numerical identifier
* @param tab the tab to remove
*/
remove_tab(config:WTabs.config, prefix_id:string, num:int, tab:WTabs.tab) =
hlist_config = to_hlist_config(config)
_ = WHList.remove_item(hlist_config, prefix_id, (num, tab), true)
// do set_number(prefix_id, extract_number(prefix_id) - 1)
void
/**
* The main function to be called for creating a tabs widget, with some default tabs
*
* @param config the widget configuration
* @param prefix_id the prefix id to use
* @param init_tabs the list of tabs to show at initilialization
* @return The XHTML corresponding to the widget
*/
html(config:WTabs.config, prefix_id:string, init_tabs:list((int, WTabs.tab))) =
prefix_class = WCore.compute_prefix_class(config, prefix_id)
hlist_config = to_hlist_config(config)
(init_items, max_id) = List.fold_backwards((num, tab), (acc, max_id) ->
(((num, tab), WHList.make_item(tab.content(num),
{selected=tab.state.selected; checked=tab.state.checked},
{selectable=tab.properties.base_properties.selectable;
no_sons=tab.properties.base_properties.no_sons;
checkable=tab.properties.base_properties.checkable})) +> acc,
max(num, max_id))
, init_tabs, ([], List.length(init_tabs)))
core_html = WHList.html(hlist_config, prefix_id, init_items)
style = if DEBUG then "" else "display:none"
<>
{core_html}
{<div class={[gen_class(prefix_class, "_add")]} id=#{gen_id(prefix_id, "_add")}>
{match config.add_text with
| ~{some=(title, add)} ->
<a title={title} onclick={_event -> add_tab(config, prefix_id)}>{add}</a>
| {none} -> <></>}
</div> |> WStyler.add(config.stylers.tab_add, _)}
<div style="{style}" id=#{gen_id(prefix_id, "_status")}>
{if DEBUG then <span>WTabs counter: </span> else <></>}
<span id=#{counter_id(prefix_id)}>{max_id}</span>
</div>
<div style="clear: both"></div>
</>
// <!--<span>Number of tabs: </span>-->
// <!--<span id=#{number_id(prefix_id)}>{List.length(init_tabs)}</span>-->
// <!--<span>Selected tab: </span>-->
// <!--<span id=#{selected_id(prefix_id)}>0</span>-->
}}
// private_default_css =
// div._hlist {
// padding: 5px;
// padding-bottom: 0;
// display: block;
// }
// div._hlist a {
// text-decoration: none;
// color: black;
// }
// div._hlist a:link {
// text-decoration: none;
// color: black;
// }
// div._hlist a:visited {
// text-decoration: none;
// color: black;
// }
// div._hlist a:hover {
// text-decoration: none;
// color: black;
// }
// div._hlist a:active {
// text-decoration: none;
// color: black;
// }
// div._hlist ul {
// list-style: none;
// padding: 0;
// margin: 0;
// }
// div._hlist li._hlist_item {
// float: left;
// margin: 0 1px;
// }
// div._hlist li._hlist_item > ul > li._hlist_item_content {
// border: 1px solid #aaaaaa;
// border-bottom-width: 0;
// padding: 0 10px;
// background: #eeeeee;
// }
// div._hlist li._hlist_item > ul > li._hlist_item_content > a:hover > span { text-decoration: underline; }
// div._hlist a._tabs_tab_duplicate { color: green; }
// div._hlist a._tabs_tab_close { color: red; }
// div._hlist ul > li._hlist_item_selected {
// font-weight: bold;
// border: 1px solid #000000;
// border-bottom: 0;
// }
// div._hlist ul > li._hlist_item_selected > ul > li._hlist_item_content {
// background: #ffffff;
// position: relative;
// top: 1px;
// border: 0;
// }
// div._tabs_add {
// float: right;
// margin-right: 5px;
// }